Update fit_auto for CameraModel and warn on underdetermined geometry fits

fit_auto now threads camera/camera_tolerance through to fit and _bic
(whose parameter count must include any free camera/z unknowns). Emits a
UserWarning, not an error, when free camera+z geometry parameters exceed
the number of measurement planes -- a new documented degeneracy pitfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-07-03 12:14:07 +02:00
parent ef57ec81e4
commit b81964fbad
2 changed files with 102 additions and 13 deletions
+53 -6
View File
@@ -1,3 +1,5 @@
import warnings
import numpy as np
import pytest
@@ -244,24 +246,69 @@ def test_fit_recovers_offset_z_within_tolerance():
def test_fit_auto_does_not_add_modes_for_pure_fundamental():
basis = make_basis()
gen = make_generator(basis)
camera = make_camera()
gen = make_generator(basis, camera)
planes = gen.generate(
coefficients={(0, 0): 1.0 + 0j}, z_list=Z_LIST, noise_std=1e-4, seed=4
coefficients={(0, 0): 1.0 + 0j}, z_list=Z_LIST, image_shape=IMAGE_SHAPE, noise_std=1e-4, seed=4
)
fitter = ModalFitter(basis)
result = fitter.fit_auto(planes, max_order=2)
result = fitter.fit_auto(planes, camera=camera, camera_tolerance=zero_tolerance(), max_order=2)
assert set(result.purity.keys()) == {(0, 0)}
def test_fit_auto_grows_to_include_second_mode():
basis = make_basis()
gen = make_generator(basis)
camera = make_camera()
gen = make_generator(basis, camera)
true_coeffs = {(0, 0): 0.9 + 0j, (0, 1): 0.4 + 0j}
planes = gen.generate(coefficients=true_coeffs, z_list=Z_LIST, noise_std=1e-4, seed=5)
planes = gen.generate(
coefficients=true_coeffs, z_list=Z_LIST, image_shape=IMAGE_SHAPE, noise_std=1e-4, seed=5
)
fitter = ModalFitter(basis)
result = fitter.fit_auto(planes, max_order=2)
result = fitter.fit_auto(planes, camera=camera, camera_tolerance=zero_tolerance(), max_order=2)
assert (0, 1) in result.purity or (0, -1) in result.purity
def test_fit_auto_warns_when_free_geometry_params_exceed_plane_count():
basis = make_basis()
camera = make_camera()
gen = make_generator(basis, camera)
planes = gen.generate(
coefficients={(0, 0): 1.0 + 0j},
z_list=Z_LIST, # 4 planes
image_shape=IMAGE_SHAPE,
z_tolerance=0.05, # +4 free z params
noise_std=1e-4,
seed=11,
)
# +7 free camera params (all but the 2 principal_point components) +
# 4 free z params = 11 free geometry params > 4 planes.
generous_tolerance = CameraModelTolerance(
focal_length_px=camera.focal_length_px * 0.05,
position=(0.01, 0.01, 0.01),
orientation_deg=(2.0, 2.0, 2.0),
principal_point=(0.0, 0.0),
)
fitter = ModalFitter(basis)
with pytest.warns(UserWarning, match="free camera/z geometry parameters"):
fitter.fit_auto(planes, camera=camera, camera_tolerance=generous_tolerance, max_order=1)
def test_fit_auto_does_not_warn_when_geometry_fully_fixed():
basis = make_basis()
camera = make_camera()
gen = make_generator(basis, camera)
planes = gen.generate(
coefficients={(0, 0): 1.0 + 0j}, z_list=Z_LIST, image_shape=IMAGE_SHAPE, noise_std=1e-4, seed=12
)
fitter = ModalFitter(basis)
with warnings.catch_warnings():
warnings.simplefilter("error", UserWarning)
fitter.fit_auto(planes, camera=camera, camera_tolerance=zero_tolerance(), max_order=1)