Skip to content

Commit cfa5be1

Browse files
authored
Merge pull request #178 from LSSTDESC/test_ccdgains_robustfit
Test ccdgains robustfit
2 parents cc1c95c + 83a7460 commit cfa5be1

14 files changed

+4166
-328
lines changed

pyproject.toml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,25 @@ Homepage = "https://github.com/LSSTDESC/Spectractor"
3939
Repository = "https://github.com/LSSTDESC/Spectractor"
4040

4141
[tool.setuptools]
42-
packages = [
43-
"spectractor",
44-
"spectractor.extractor",
45-
"spectractor.fit",
46-
"spectractor.simulation",
47-
]
4842
include-package-data = true
4943

44+
[tool.setuptools.packages.find]
45+
where = ["."]
46+
include = ["spectractor*"]
47+
5048
[tool.setuptools.dynamic]
5149
version = {attr = "spectractor._version.__version__"}
5250

5351
[tool.setuptools.package-data]
52+
"*" = ["*.ini", "*.txt"]
5453
spectractor = [
5554
"config/*.ini",
56-
"extractor/dispersers/**/*",
57-
"simulation/CTIOThroughput/*.txt",
58-
"simulation/AuxTelThroughput/*.txt",
59-
"simulation/StarDiceThroughput/*.txt"
55+
"extractor/dispersers/**/*"
56+
]
57+
"spectractor.simulation" = [
58+
"CTIOThroughput/*.txt",
59+
"AuxTelThroughput/*.txt",
60+
"StarDiceThroughput/*.txt"
6061
]
6162

6263
[project.optional-dependencies]

spectractor/extractor/extractor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ def simulate(self, *params):
495495
if self.flat is not None:
496496
# multiply each M matrix columns by the flat array (see the docstring)
497497
# TODO: if flat array is a cube flat, needs to multiply directly in build_sparse_M
498-
dia = sparse.dia_matrix(([self.flat], [0]), shape=(self.flat.size, self.flat.size))
498+
dia = sparse.dia_matrix(([self.flat.astype("float32")], [0]), shape=(self.flat.size, self.flat.size))
499499
M = (dia @ M).tocsc()
500500

501501

spectractor/fit/fit_spectrogram.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
104104
for order in self.diffraction_orders:
105105
axis_names += [label+rf"$\!_{order}$" for label in psf_poly_params_names]
106106
bounds = [[0, 2], [0, 2], [0, 2], [0, 10], [0, 4], [100, 700], [0, 20], [0.8, 1.2], [0, np.inf],
107-
[D2CCD - 5 * parameters.DISTANCE2CCD_ERR, D2CCD + 5 * parameters.DISTANCE2CCD_ERR], [-2, 2],
107+
[D2CCD - 20 * parameters.DISTANCE2CCD_ERR, D2CCD + 20 * parameters.DISTANCE2CCD_ERR], [-10, 10],
108108
[-10, 10], [-90, 90], [0, np.inf]]
109109
bounds += list(psf_poly_params_bounds) * len(self.diffraction_orders)
110110
fixed = [False] * p.size
@@ -132,9 +132,9 @@ def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
132132
params.fixed[params.get_index(f"A{self.diffraction_orders[0]}")] = False # A1
133133
self.atm_params_indices = np.array([params.get_index(label) for label in ["VAOD", "angstrom_exp", "ozone [db]", "PWV [mm]"]])
134134
# A2 is free only if spectrogram is a simulation or if the order 2/1 ratio is not known and flat
135-
params.fixed[params.get_index(f"A{self.diffraction_orders[0]}")] = False # A1
135+
params.fixed[params.get_index(f"A{self.diffraction_orders[0]}")] = True # A1
136136
if "A2" in params.labels:
137-
params.fixed[params.get_index(f"A{self.diffraction_orders[1]}")] = False #not getCalspec.is_calspec(spectrum.target.label) #"A2_T" not in self.spectrum.header
137+
params.fixed[params.get_index(f"A{self.diffraction_orders[1]}")] = True #not getCalspec.is_calspec(spectrum.target.label) #"A2_T" not in self.spectrum.header
138138
if "A3" in params.labels:
139139
params.fixed[params.get_index(f"A{self.diffraction_orders[2]}")] = "A3_T" not in self.spectrum.header
140140
params.fixed[params.get_index(r"shift_x [pix]")] = False # Delta x
@@ -181,6 +181,11 @@ def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
181181
if not fit_angstrom_exponent:
182182
self.params.fixed[self.params.get_index("angstrom_exp")] = True # angstrom exponent
183183
self.params.values[self.params.get_index("angstrom_exp")] = self.atmosphere.angstrom_exponent_default
184+
if np.min(self.spectrum.lambdas) > 500:
185+
self.fit_angstrom_exponent = False
186+
self.params.fixed[self.params.get_index("angstrom_exp")] = True
187+
self.params.values[self.params.get_index("angstrom_exp")] = 0
188+
self.my_logger.warning("\n\tWavelengths below 500nm detected: angstrom exponent fitting disabled and fixed to 0.")
184189
self.spectrogram_simulation = SpectrogramModel(self.spectrum, atmosphere=self.atmosphere,
185190
diffraction_orders=self.diffraction_orders,
186191
fast_sim=False, with_adr=True)
@@ -577,7 +582,7 @@ def run_spectrogram_minimisation(fit_workspace, method="newton", verbose=False):
577582
my_logger = set_logger(__name__)
578583
guess = np.asarray(fit_workspace.params.values)
579584
fit_workspace.simulate(*guess)
580-
fit_workspace.plot_fit()
585+
# fit_workspace.plot_fit()
581586
if method != "newton":
582587
run_minimisation(fit_workspace, method=method)
583588
else:
@@ -610,7 +615,7 @@ def run_spectrogram_minimisation(fit_workspace, method="newton", verbose=False):
610615
fit_workspace.spectrogram_simulation.fast_sim = False
611616
fit_workspace.spectrogram_simulation.fix_psf_cube = False
612617
fit_workspace.params.fixed = [True] * len(fit_workspace.params.values)
613-
fit_workspace.params.fixed[fit_workspace.params.get_index(r"A1")] = False # A1
618+
fit_workspace.params.fixed[fit_workspace.params.get_index(r"VAOD")] = False # VAOD
614619
fit_workspace.params.fixed[fit_workspace.params.get_index(r"shift_y [pix]")] = False # shift y
615620
fit_workspace.params.fixed[fit_workspace.params.get_index(r"angle [deg]")] = False # angle
616621
run_minimisation(fit_workspace, "newton", xtol=1e-2, ftol=0.01, with_line_search=False)
@@ -624,8 +629,8 @@ def run_spectrogram_minimisation(fit_workspace, method="newton", verbose=False):
624629
# params_table, costs = run_gradient_descent(fit_workspace, guess, epsilon, params_table, costs,
625630
# fix=fit_workspace.fixed, xtol=1e-6, ftol=1 / fit_workspace.data.size,
626631
# niter=40)
627-
run_minimisation_sigma_clipping(fit_workspace, method="newton", xtol=1e-6,
628-
ftol=1 / fit_workspace.data.size, sigma_clip=100, niter_clip=3, verbose=verbose,
632+
run_minimisation_sigma_clipping(fit_workspace, method="newton", xtol=1e-10,
633+
ftol=1e-3 / fit_workspace.data.size, sigma_clip=100, niter_clip=3, verbose=verbose,
629634
with_line_search=True)
630635
extra = {"chi2": fit_workspace.costs[-1] / fit_workspace.data.size,
631636
"date-obs": fit_workspace.spectrum.date_obs,

spectractor/fit/fit_spectrum.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@ def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
7575
self.spectrum = spectrum
7676
p = np.array([1, 0, 0.05, 1.2, 400, 5, 1, self.spectrum.header['D2CCD'], self.spectrum.header['PIXSHIFT'], 0])
7777
fixed = [False] * p.size
78-
fixed[0] = False
79-
fixed[1] = False #"A2_T" not in self.spectrum.header # fit A2 only on sims to evaluate extraction biases
78+
fixed[0] = True # A1
79+
fixed[1] = True #"A2_T" not in self.spectrum.header # fit A2 only on sims to evaluate extraction biases
8080
fixed[5] = False
8181
# fixed[6:8] = [True, True]
82-
fixed[8] = True
82+
fixed[8] = False # alpha_pix
8383
fixed[9] = True
8484
# fixed[-1] = True
8585
if not fit_angstrom_exponent:
8686
fixed[3] = True # angstrom_exponent
8787
bounds = [(0, 2), (0, 2/parameters.GRATING_ORDER_2OVER1), (0, 10), (0, 4), (100, 700), (0, 20),
88-
(0.5, 20),(p[7] - 5 * parameters.DISTANCE2CCD_ERR, p[7] + 5 * parameters.DISTANCE2CCD_ERR),
89-
(-2, 2), (-np.inf, np.inf)]
88+
(0.5, 20),(p[7] - 20 * parameters.DISTANCE2CCD_ERR, p[7] + 20 * parameters.DISTANCE2CCD_ERR),
89+
(-10, 10), (-np.inf, np.inf)]
9090
params = FitParameters(p, labels=["A1", "A2", "VAOD", "angstrom_exp", "ozone [db]", "PWV [mm]",
9191
"reso [nm]", r"D_CCD [mm]", r"alpha_pix [pix]", "B"],
9292
axis_names=["$A_1$", "$A_2$", "VAOD", r'$\"a$', "ozone [db]", "PWV [mm]",
@@ -112,10 +112,15 @@ def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
112112
self.params.fixed[self.params.get_index("angstrom_exp")] = True # angstrom exponent
113113
if parameters.VERBOSE:
114114
self.my_logger.info(f'\n\tUse atmospheric grid models from file {atmgrid_file_name}. ')
115-
self.params.values[self.params.get_index("angstrom_exp")] = self.atmosphere.angstrom_exponent_default
115+
116116
self.lambdas = self.spectrum.lambdas
117117
self.fit_angstrom_exponent = fit_angstrom_exponent
118118
self.params.values[self.params.get_index("angstrom_exp")] = self.atmosphere.angstrom_exponent_default
119+
if np.min(self.spectrum.lambdas) > 500:
120+
self.fit_angstrom_exponent = False
121+
self.params.fixed[self.params.get_index("angstrom_exp")] = True
122+
self.params.values[self.params.get_index("angstrom_exp")] = 0
123+
self.my_logger.warning("\n\tWavelengths below 500nm detected: angstrom exponent fitting disabled and fixed to 0.")
119124
self.simulation = SpectrumSimulation(self.spectrum, atmosphere=self.atmosphere, fast_sim=True, with_adr=True)
120125
self.amplitude_truth = None
121126
self.lambdas_truth = None
@@ -397,12 +402,27 @@ def run_spectrum_minimisation(fit_workspace, method="newton", sigma_clip=20):
397402
fit_workspace.simulation.fast_sim = False
398403
fixed = copy.copy(fit_workspace.params.fixed)
399404
fit_workspace.params.fixed = [True] * len(fit_workspace.params)
400-
fit_workspace.params.fixed[0] = False
405+
fit_workspace.params.fixed[fit_workspace.params.get_index(r"VAOD")] = False
401406
run_minimisation(fit_workspace, method="newton", xtol=1e-3, ftol=100 / fit_workspace.data.size,
402-
verbose=False)
407+
verbose=False, with_line_search=False)
403408
fit_workspace.params.fixed = fixed
404409
run_minimisation_sigma_clipping(fit_workspace, method="newton", xtol=1e-6,
405-
ftol=1 / fit_workspace.data.size, sigma_clip=sigma_clip, niter_clip=3, verbose=False)
410+
ftol=1e-3 / fit_workspace.data.size, sigma_clip=sigma_clip, niter_clip=3, verbose=False)
411+
412+
# alternate fixing dccd and pixshift with fitting all parameters
413+
for i in range(3):
414+
fixed = copy.copy(fit_workspace.params.fixed)
415+
fit_workspace.params.fixed = [True] * len(fit_workspace.params)
416+
fit_workspace.params.fixed[6] = False # reso
417+
fit_workspace.params.fixed[7] = False # dccd
418+
fit_workspace.params.fixed[8] = False # pixshift
419+
run_minimisation_sigma_clipping(fit_workspace, method="newton", xtol=1e-6,
420+
ftol=1e-3 / fit_workspace.data.size, sigma_clip=sigma_clip, niter_clip=3, verbose=False, with_line_search=True)
421+
fit_workspace.params.fixed = fixed
422+
run_minimisation_sigma_clipping(fit_workspace, method="newton", xtol=1e-6,
423+
ftol=1e-3 / fit_workspace.data.size, sigma_clip=sigma_clip, niter_clip=3, verbose=False, with_line_search=True)
424+
425+
406426

407427
fit_workspace.params.plot_correlation_matrix()
408428
fit_workspace.plot_fit()

spectractor/fit/fitter.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,91 @@ def save_gradient_descent(self):
12771277
np.savetxt(output_filename, t.T, header=h, delimiter=",")
12781278
self.my_logger.info(f"\n\tSave gradient descent log {output_filename}.")
12791279

1280+
def plot_triangle_fisher(self, nsamples=10000, max_params=8):
1281+
"""Plot a triangle (corner) plot of probability contours and distributions using Fisher matrix method.
1282+
1283+
This method generates samples from a multivariate normal distribution based on the parameter
1284+
covariance matrix and creates a corner plot showing 1D and 2D marginal distributions.
1285+
1286+
Parameters
1287+
----------
1288+
nsamples: int, optional
1289+
Number of samples to generate from the multivariate normal distribution (default: 10000).
1290+
max_params: int, optional
1291+
Maximum number of parameters to display. Only the first max_params free parameters
1292+
will be shown (default: 8).
1293+
1294+
Examples
1295+
--------
1296+
>>> from spectractor.fit.fitter import FitParameters, FitWorkspace
1297+
>>> params = FitParameters(values=[1, 2, 3], axis_names=["x", "y", "z"])
1298+
>>> params.cov = np.array([[1, -0.5, 0], [-0.5, 1, -1], [0, -1, 1]])
1299+
>>> w = FitWorkspace(params=params, filename="test.fits")
1300+
>>> w.plot_triangle_fisher(nsamples=1000, max_params=3)
1301+
1302+
Notes
1303+
-----
1304+
This method requires the `corner` package to be installed.
1305+
The plot is saved if parameters.SAVE is True with suffix '_triangle.pdf'.
1306+
"""
1307+
try:
1308+
import corner
1309+
except ImportError:
1310+
self.my_logger.warning("Package 'corner' not installed. Cannot create triangle plot. "
1311+
"Install it with: pip install corner")
1312+
return
1313+
1314+
# Get free parameters indices
1315+
ipar = self.params.get_free_parameters()
1316+
1317+
# Limit to max_params parameters
1318+
n_params = min(len(ipar), max_params)
1319+
ipar = ipar[:n_params]
1320+
1321+
if len(ipar) == 0:
1322+
self.my_logger.warning("No free parameters to plot.")
1323+
return
1324+
1325+
# Check that covariance matrix is available
1326+
if self.params.cov.size == 0:
1327+
self.my_logger.warning("Covariance matrix not available. Run fit first.")
1328+
return
1329+
1330+
# Extract mean values and covariance matrix for free parameters
1331+
mean_values = self.params.values[ipar]
1332+
cov_matrix = self.params.cov[:n_params, :n_params]
1333+
1334+
# Check if covariance matrix is positive definite
1335+
eigenvalues = np.linalg.eigvals(cov_matrix)
1336+
if np.any(eigenvalues <= 0):
1337+
self.my_logger.warning("Covariance matrix is not positive definite. "
1338+
"Cannot generate samples.")
1339+
return
1340+
1341+
# Generate samples from multivariate normal distribution
1342+
samples = np.random.multivariate_normal(mean_values, cov_matrix, size=nsamples)
1343+
1344+
# Get axis names for the selected parameters
1345+
axis_names = [self.params.axis_names[i] for i in ipar]
1346+
1347+
# Create corner plot
1348+
fig = corner.corner(samples, labels=axis_names,
1349+
quantiles=[0.16, 0.5, 0.84],
1350+
show_titles=True,
1351+
title_kwargs={"fontsize": 12},
1352+
truths=mean_values)
1353+
1354+
# Save figure if requested
1355+
if (parameters.SAVE or parameters.LSST_SAVEFIGPATH) and self.filename != "": # pragma: no cover
1356+
figname = os.path.splitext(self.filename)[0] + "_triangle.pdf"
1357+
self.my_logger.info(f"\n\tSave triangle plot {figname}.")
1358+
fig.savefig(figname, dpi=100, bbox_inches='tight')
1359+
1360+
if parameters.DISPLAY: # pragma: no cover
1361+
plt.show()
1362+
1363+
return fig
1364+
12801365

12811366
def gradient_descent(fit_workspace, niter=10, xtol=1e-3, ftol=1e-3, with_line_search=True):
12821367
"""
@@ -1333,7 +1418,7 @@ def gradient_descent(fit_workspace, niter=10, xtol=1e-3, ftol=1e-3, with_line_se
13331418
fit_workspace.params.fixed[ip] = True
13341419
my_logger.warning(
13351420
f"\n\tStep {i}: {fit_workspace.params.labels[ip]} has a null Jacobian; parameter is fixed "
1336-
f"at its last known current value ({tmp_params[ip]}) because |J[par]|={J_norms[ip]:.3e}<{np.sqrt(J.shape[1])*np.finfo(np.float64).eps} with more than half values being zeros.")
1421+
f"at its last known current value ({tmp_params[ip]}) because |J[par]|={J_norms[ip]:.3e}<{np.sqrt(len(J_vectors[ip]))*np.finfo(np.float64).eps} with more than half values being zeros.")
13371422
continue
13381423
# check for degeneracies using Cauchy-Schwartz inequality; fix the second parameter
13391424
for jp in range(ip, J.shape[0]):

0 commit comments

Comments
 (0)