Symptom
write_amicaout, the EEGLAB export shared by the torch, NumPy and MLX backends (write_amica_output), writes a square sphere S in C order, while the Fortran reference writes it column-major and every reader (EEGLAB's loadmodout15.m, pamica's loadmodout) reads it column-major. For the default symmetric (ZCA) sphere this is invisible (2e-17). With do_approx_sphere=False the sphere is not symmetric, and the exported sphere comes back exactly transposed. Measured on the bundled sample (torch, 3 iterations): max|S_loaded - S| = 0.51, max|S_loaded - S.T| = 0.0. EEGLAB would then build icasphere and icaweights from the wrong matrix.
Root cause
pamica/numpy_impl/load.py write_amicaout: the rank-reduced branch pads and writes order="F", but the square branch keeps order="C", on the reasoning that the ZCA sphere is its own transpose and that C order keeps output "byte-identical to the Fortran reference". That reasoning does not hold. The test that backs it (test_square_sphere_bytes_are_unchanged in pamica/tests/test_amicaout_reduced.py) pins C order for a sphere pamica itself computed, not against the reference's bytes. Fortran's layout is column-major, so C order is wrong by the sphere's asymmetry: 2e-17 for ZCA, O(1) otherwise. pamica/numpy_impl/data.py load_results mirrors the writer, reading a square sphere in C order.
Fix
Write S column-major in both branches, read it column-major in load_results, and replace the C-order pin with round-trip tests. For every backend, with do_approx_sphere True and False and with and without pcakeep, loadmodout and load_results must return the fitted sphere bit-exactly, and the raw bytes must equal sphere.ravel(order="F") (after padding). Update the comments and docstrings that claim C order is the byte-identical choice, and add a changelog entry.
Found during epic #324 Phase 6 (#315, PR #332). Part of epic #324.
Symptom
write_amicaout, the EEGLAB export shared by the torch, NumPy and MLX backends (write_amica_output), writes a square sphereSin C order, while the Fortran reference writes it column-major and every reader (EEGLAB'sloadmodout15.m, pamica'sloadmodout) reads it column-major. For the default symmetric (ZCA) sphere this is invisible (2e-17). Withdo_approx_sphere=Falsethe sphere is not symmetric, and the exported sphere comes back exactly transposed. Measured on the bundled sample (torch, 3 iterations):max|S_loaded - S| = 0.51,max|S_loaded - S.T| = 0.0. EEGLAB would then buildicasphereandicaweightsfrom the wrong matrix.Root cause
pamica/numpy_impl/load.pywrite_amicaout: the rank-reduced branch pads and writesorder="F", but the square branch keepsorder="C", on the reasoning that the ZCA sphere is its own transpose and that C order keeps output "byte-identical to the Fortran reference". That reasoning does not hold. The test that backs it (test_square_sphere_bytes_are_unchangedinpamica/tests/test_amicaout_reduced.py) pins C order for a sphere pamica itself computed, not against the reference's bytes. Fortran's layout is column-major, so C order is wrong by the sphere's asymmetry: 2e-17 for ZCA, O(1) otherwise.pamica/numpy_impl/data.pyload_resultsmirrors the writer, reading a square sphere in C order.Fix
Write
Scolumn-major in both branches, read it column-major inload_results, and replace the C-order pin with round-trip tests. For every backend, withdo_approx_sphereTrue and False and with and withoutpcakeep,loadmodoutandload_resultsmust return the fitted sphere bit-exactly, and the raw bytes must equalsphere.ravel(order="F")(after padding). Update the comments and docstrings that claim C order is the byte-identical choice, and add a changelog entry.Found during epic #324 Phase 6 (#315, PR #332). Part of epic #324.