Coverage for python/lsst/meas/extensions/multiprofit/pipetasks_fit.py: 63%
266 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 10:24 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-02 10:24 +0000
1# This file is part of meas_extensions_multiprofit.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = (
23 "MultiProFitCoaddDeVFitConfig",
24 "MultiProFitCoaddDeVFitTask",
25 "MultiProFitCoaddExpDeVFitConfig",
26 "MultiProFitCoaddExpDeVFitTask",
27 "MultiProFitCoaddExpFitConfig",
28 "MultiProFitCoaddExpFitTask",
29 "MultiProFitCoaddGaussFitConfig",
30 "MultiProFitCoaddGaussFitTask",
31 "MultiProFitCoaddObjectFitConfig",
32 "MultiProFitCoaddPointFitConfig",
33 "MultiProFitCoaddPsfFitConfig",
34 "MultiProFitCoaddPsfFitTask",
35 "MultiProFitCoaddSersicFitConfig",
36 "MultiProFitCoaddSersicFitTask",
37 "component_names_default",
38 "model_names_default",
39)
41import itertools
42import math
43from abc import abstractmethod
44from collections.abc import Mapping, Sequence
45from types import SimpleNamespace
46from typing import Any
48import lsst.gauss2d.fit as g2f
49from lsst.daf.butler import DeferredDatasetHandle
50from lsst.multiprofit.componentconfig import (
51 GaussianComponentConfig,
52 ParameterConfig,
53 SersicComponentConfig,
54 SersicIndexParameterConfig,
55)
56from lsst.multiprofit.fitting.fit_source import CatalogExposureSourcesABC, CatalogSourceFitterConfigData
57from lsst.multiprofit.modelconfig import ModelConfig
58from lsst.multiprofit.sourceconfig import ComponentGroupConfig, SourceConfig
59from lsst.pex.config import ConfigDictField, Field
60from lsst.pipe.tasks.fit_coadd_multiband import (
61 CatalogExposureInputs,
62 CoaddMultibandFitConfig,
63 CoaddMultibandFitConnections,
64 CoaddMultibandFitTask,
65)
66from lsst.pipe.tasks.fit_coadd_psf import CoaddPsfFitConfig, CoaddPsfFitConnections, CoaddPsfFitTask
68from .fit_coadd_multiband import (
69 CachedBasicModelInitializer,
70 MagnitudeDependentSizePriorConfig,
71 MakeBasicInitializerAction,
72 ModelInitializer,
73 MultiProFitSourceTask,
74 PsfComponentsActionBase,
75 SourceTablePsfComponentsAction,
76)
77from .fit_coadd_psf import MultiProFitPsfTask
78from .input_config import InputConfig
80component_names_default = SimpleNamespace(
81 point="point",
82 gauss="gauss",
83 exp="exp",
84 deV="deV",
85 sersic="sersic",
86)
88model_names_default = SimpleNamespace(
89 point="Point",
90 gauss="Gauss",
91 exp="Exp",
92 deV="DeV",
93 sersic="Sersic",
94 fixed_cen="FixedCen",
95 shapelet_psf="ShapeletPsf",
96)
99class MultiProFitCoaddPsfFitConfig(
100 CoaddPsfFitConfig,
101 pipelineConnections=CoaddPsfFitConnections,
102):
103 """MultiProFit PSF fit task config."""
105 def setDefaults(self):
106 super().setDefaults()
107 self.fit_coadd_psf.retarget(MultiProFitPsfTask)
108 self.fit_coadd_psf.config_fit.eval_residual = False
111class MultiProFitCoaddPsfFitTask(CoaddPsfFitTask):
112 """MultiProFit PSF fit task."""
114 ConfigClass = MultiProFitCoaddPsfFitConfig
115 _DefaultName = "multiProFitCoaddPsfFit"
118class MultiProFitCoaddObjectFitConnections(CoaddMultibandFitConnections):
119 def __init__(self, *, config=None):
120 super().__init__(config=config)
121 for name, config_input in config.inputs_init.items():
122 if hasattr(self, name):
123 raise ValueError(
124 f"{config_input=} {name=} is invalid, due to being an existing attribute of {self=}"
125 )
126 if config_input.is_multipatch or not config_input.is_multiband:
127 raise ValueError(
128 f"Single-band and/or multipatch initialization config_input entries ({name})"
129 f" are not supported yet."
130 )
131 connection = config_input.get_connection(name)
132 setattr(self, name, connection)
135class MultiProFitCoaddObjectFitConfig(
136 CoaddMultibandFitConfig,
137 pipelineConnections=MultiProFitCoaddObjectFitConnections,
138):
139 """Generic MultiProFit source fit task config."""
141 inputs_init = ConfigDictField(
142 doc="Mapping of optional input dataset configs by name, for initialization",
143 keytype=str,
144 itemtype=InputConfig,
145 default={},
146 )
148 # This needs to be set, ideally in setDefaults of subclasses
149 name_model = Field[str](doc="The name of the model", default=None)
151 def _get_source(self):
152 return next(iter(self.fit_coadd_multiband.config_model.sources.values()))
154 def _get_component_group(self, source: SourceConfig | None = None):
155 if source is None:
156 source = self._get_source()
157 return next(iter(source.component_groups.values()))
159 def add_point_source(self, name: str | None = None):
160 """Add a point source component.
162 Parameters
163 ----------
164 name
165 The name of the component.
166 """
167 if name is None: 167 ↛ 169line 167 didn't jump to line 169 because the condition on line 167 was always true
168 name = component_names_default.point
169 source = self._get_source()
170 group = self._get_component_group(source=source)
171 if name in group.components_gauss: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 raise RuntimeError(f"{name=} component already exists in {source=}")
173 group.components_gauss[name] = self.make_point_source_component()
174 self.connections.name_table += model_names_default.point
176 def finalize(
177 self,
178 add_point_source: bool = False,
179 fix_centroid: bool = False,
180 use_shapelet_psf: bool = False,
181 prior_axrat_stddev: float | str | None = None,
182 ):
183 """Apply runtime configuration changes to this config.
185 Parameters
186 ----------
187 add_point_source
188 Whether to add a point source component.
189 fix_centroid
190 Whether to fix the centroid.
191 use_shapelet_psf
192 Whether to initialize PSF parameters from prior shapelet fits.
193 prior_axrat_stddev
194 The standard deviation for the axis ratio prior. Ignored if None,
195 otherwise it must be convertible to a float.
196 """
197 if add_point_source:
198 self.add_point_source()
199 if fix_centroid:
200 self.fix_centroid()
201 if use_shapelet_psf:
202 self.use_shapelet_psf()
203 if prior_axrat_stddev is not None:
204 self.set_prior_axrat_stddev(float(prior_axrat_stddev))
206 def fix_centroid(self):
207 """Fix (freeze) the source centroid parameters."""
208 group = self._get_component_group()
209 centroids = group.centroids["default"]
210 centroids.x.fixed = True
211 centroids.y.fixed = True
212 self.connections.name_table += model_names_default.fixed_cen
214 @classmethod
215 @abstractmethod
216 def get_model_name_default(cls) -> str:
217 """Return the default name for this model in table columns."""
218 raise NotImplementedError("Subclasses must implement get_model_name_default")
220 @classmethod
221 def get_model_name_full(cls) -> str:
222 """Return a longer, more descriptive name for the model."""
223 return cls.get_model_name_default()
225 @abstractmethod
226 def make_default_model_config(self) -> ModelConfig:
227 """Make a default configuration object for this model."""
228 raise NotImplementedError("Subclasses must implement make_default_model_config")
230 @staticmethod
231 def make_point_source_component() -> GaussianComponentConfig:
232 """Make a point source component config (zero-size Gaussian)."""
233 return GaussianComponentConfig(
234 size_x=ParameterConfig(value_initial=0.0, fixed=True),
235 size_y=ParameterConfig(value_initial=0.0, fixed=True),
236 rho=ParameterConfig(value_initial=0.0, fixed=True),
237 )
239 @staticmethod
240 def make_sersic_component(**kwargs) -> SersicComponentConfig:
241 """Make a default Sersic component config.
243 Parameters
244 ----------
245 **kwargs
246 Keyword arguments to pass to the SersicIndexParameterConfig.
248 Returns
249 -------
250 config
251 The default-initialized config.
252 """
253 return SersicComponentConfig(
254 prior_axrat_stddev=1.0,
255 prior_size_stddev=0.2,
256 sersic_index=SersicIndexParameterConfig(**kwargs),
257 )
259 @staticmethod
260 def make_single_model_config(group: ComponentGroupConfig) -> ModelConfig:
261 """Make a default single-source, single component group config.
263 Parameters
264 ----------
265 group
266 The component group config for the single source.
268 Returns
269 -------
270 config
271 A model config with a single nameless source and component group.
272 """
273 return ModelConfig(
274 sources={
275 "": SourceConfig(
276 component_groups={
277 "": group,
278 }
279 )
280 }
281 )
283 def set_prior_axrat_stddev(self, stddev: float) -> None:
284 """Set the standard deviation for all axis ratio priors.
286 Parameters
287 ----------
288 stddev
289 The standard deviation.
290 """
291 for source in self.fit_coadd_multiband.config_model.sources.values():
292 for group in source.component_groups.values():
293 for comp in itertools.chain(
294 group.components_gauss.values(),
295 group.components_sersic.values(),
296 ):
297 comp.prior_axrat_stddev = stddev
299 group = self._get_component_group()
300 centroids = group.centroids["default"]
301 centroids.x.fixed = True
302 centroids.y.fixed = True
303 self.connections.name_table += model_names_default.fixed_cen
305 def setDefaults(self):
306 super().setDefaults()
307 self.fit_coadd_multiband.retarget(MultiProFitSourceTask)
308 self.fit_coadd_multiband.action_psf = PsfComponentsActionBase()
309 self.fit_coadd_multiband.bands_fit = ("u", "g", "r", "i", "z", "y")
311 self.fit_coadd_multiband.config_model = self.make_default_model_config()
312 self.name_model = self.get_model_name_default()
313 self.connections.name_table = self.name_model
315 def use_shapelet_psf(self):
316 """Reconfigure self to use prior shapelet PSF fit parameters."""
317 self.fit_coadd_multiband.action_psf = SourceTablePsfComponentsAction()
318 self.drop_psf_connection = True
319 self.connections.name_table += model_names_default.shapelet_psf
322class MultiProFitCoaddObjectFitTask(CoaddMultibandFitTask):
323 """MultiProFit coadd object model fitting task."""
325 ConfigClass = MultiProFitCoaddObjectFitConfig
326 _DefaultName = "multiProFitCoaddObjectFit"
328 def make_kwargs(self, butlerQC, inputRefs, inputs):
329 inputs_init = {}
330 for name, config in self.config.inputs_init.items():
331 input_ = inputs[name][0]
332 if isinstance(input_, DeferredDatasetHandle):
333 parameters = None
334 if config.needs_metadata:
335 parameters = {"strip_astropy_meta_yaml": False}
336 input_ = input_.get(parameters=parameters)
338 inputs_init[name] = (config, input_)
340 kwargs = {}
341 if inputs_init:
342 kwargs["inputs_init"] = inputs_init
344 return kwargs
347class MultiProFitCoaddPointFitConfig(
348 MultiProFitCoaddObjectFitConfig,
349 pipelineConnections=MultiProFitCoaddObjectFitConnections,
350):
351 """MultiProFit single Sersic model fit task config."""
353 @classmethod
354 def get_model_name_default(cls) -> str:
355 return model_names_default.point
357 @classmethod
358 def get_model_name_full(cls) -> str:
359 return "Point Source"
361 def make_default_model_config(self) -> ModelConfig:
362 config_group = ComponentGroupConfig()
363 # This is a bit silly but add_point_source will look for the first
364 # source so it must be added now. Perhaps add_point_source should
365 # add to a config instance or only self by default
366 self.fit_coadd_multiband.config_model = self.make_single_model_config(group=config_group)
367 self.add_point_source()
368 return self.fit_coadd_multiband.config_model
371class MultiProFitCoaddSersicFitConfig(
372 MultiProFitCoaddObjectFitConfig,
373 pipelineConnections=MultiProFitCoaddObjectFitConnections,
374):
375 """MultiProFit single Sersic model fit task config."""
377 def _rename_defaults(
378 self,
379 name_new: str,
380 name_model: str | None = None,
381 name_old: str | None = None,
382 index_new: float | None = None,
383 fix_index: bool = False,
384 ):
385 """Rename the default Sersic component to something more specific.
387 This is intended for fixed index models such as exponential and
388 deVaucouleurs.
390 Parameters
391 ----------
392 name_new
393 The new name for the component.
394 name_model
395 The new name of the model. Default is to capitalize name_new.
396 name_old
397 The old name of the component. Default is to set to
398 component_names_default.sersic.
399 index_new
400 The initial value for the Sersic index.
401 fix_index
402 Whether the fix the index to the new value.
403 """
404 if name_old is None: 404 ↛ 406line 404 didn't jump to line 406 because the condition on line 404 was always true
405 name_old = component_names_default.sersic
406 if name_model is None: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true
407 name_model = name_new.capitalize()
408 group = self._get_component_group()
409 comps_sersic = group.components_sersic
411 if name_new in comps_sersic: 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 raise RuntimeError(f"{name_new=} is already in {comps_sersic=}")
414 comp_sersic = comps_sersic[name_old]
415 del comps_sersic[name_old]
416 if index_new is not None: 416 ↛ 418line 416 didn't jump to line 418 because the condition on line 416 was always true
417 comp_sersic.sersic_index.value_initial = index_new
418 if fix_index: 418 ↛ 420line 418 didn't jump to line 420 because the condition on line 418 was always true
419 comp_sersic.sersic_index.fixed = True
420 comps_sersic[name_new] = comp_sersic
422 if prior_old := self.fit_coadd_multiband.size_priors.get(name_old): 422 ↛ 426line 422 didn't jump to line 426 because the condition on line 422 was always true
423 self.fit_coadd_multiband.size_priors[name_new] = prior_old
424 del self.fit_coadd_multiband.size_priors[name_old]
426 self.name_model = name_model
427 self.connections.name_table = name_model
429 @classmethod
430 def get_model_name_default(cls) -> str:
431 return model_names_default.sersic
433 @classmethod
434 def get_model_name_full(cls) -> str:
435 return "Sersic"
437 def make_default_model_config(self) -> ModelConfig:
438 config_group = ComponentGroupConfig(
439 components_sersic={
440 component_names_default.sersic: self.make_sersic_component(),
441 },
442 )
443 return self.make_single_model_config(group=config_group)
445 def setDefaults(self):
446 super().setDefaults()
447 # This is in pixels and based on DC2. See DM-46498 for details.
448 self.fit_coadd_multiband.size_priors[component_names_default.sersic] = (
449 MagnitudeDependentSizePriorConfig(
450 intercept_mag=22.6,
451 slope_median_per_mag=-0.15,
452 slope_stddev_per_mag=0,
453 )
454 )
457class MultiProFitCoaddSersicFitTask(MultiProFitCoaddObjectFitTask):
458 """MultiProFit single Sersic model fit task."""
460 ConfigClass = MultiProFitCoaddSersicFitConfig
461 _DefaultName = "multiProFitCoaddSersicFit"
464class MultiProFitCoaddGaussFitConfig(
465 MultiProFitCoaddSersicFitConfig,
466 pipelineConnections=MultiProFitCoaddObjectFitConnections,
467):
468 """MultiProFit single Gaussian model fit task config."""
470 @classmethod
471 def get_model_name_default(cls) -> str:
472 return model_names_default.gauss
474 @classmethod
475 def get_model_name_full(cls) -> str:
476 return "Gaussian"
478 def setDefaults(self):
479 super().setDefaults()
480 self._rename_defaults(
481 name_new=component_names_default.gauss,
482 name_model=model_names_default.gauss,
483 index_new=0.5,
484 fix_index=True,
485 )
488class MultiProFitCoaddGaussFitTask(MultiProFitCoaddObjectFitTask):
489 """MultiProFit single Gaussian model fit task."""
491 ConfigClass = MultiProFitCoaddGaussFitConfig
492 _DefaultName = "multiProFitCoaddGaussFit"
495class MultiProFitCoaddExpFitConfig(
496 MultiProFitCoaddSersicFitConfig,
497 pipelineConnections=MultiProFitCoaddObjectFitConnections,
498):
499 """MultiProFit single exponential model fit task config."""
501 @classmethod
502 def get_model_name_default(cls) -> str:
503 return model_names_default.exp
505 @classmethod
506 def get_model_name_full(cls) -> str:
507 return "Exponential"
509 def setDefaults(self):
510 super().setDefaults()
511 self._rename_defaults(
512 name_new=component_names_default.exp,
513 name_model=model_names_default.exp,
514 index_new=1.0,
515 fix_index=True,
516 )
517 # These are typical values from DC2 and could/should be switched to a
518 # more data-driven prior (from HSC?)
519 prior_size = self.fit_coadd_multiband.size_priors[component_names_default.exp]
520 prior_size.intercept_mag = 23.4
521 prior_size.slope_median_per_mag = -0.14
524class MultiProFitCoaddExpFitTask(MultiProFitCoaddObjectFitTask):
525 """MultiProFit single exponential model fit task."""
527 ConfigClass = MultiProFitCoaddExpFitConfig
528 _DefaultName = "multiProFitCoaddExpFit"
531class MultiProFitCoaddDeVFitConfig(
532 MultiProFitCoaddSersicFitConfig,
533 pipelineConnections=MultiProFitCoaddObjectFitConnections,
534):
535 """MultiProFit single DeVaucouleurs model fit task config."""
537 @classmethod
538 def get_model_name_default(cls) -> str:
539 return model_names_default.deV
541 @classmethod
542 def get_model_name_full(cls) -> str:
543 return "de Vaucouleurs"
545 def setDefaults(self):
546 super().setDefaults()
547 self._rename_defaults(
548 name_new=component_names_default.deV,
549 name_model=model_names_default.deV,
550 index_new=4.0,
551 fix_index=True,
552 )
553 # These are typical values from DC2 and could/should be switched to a
554 # more data-driven prior (from HSC?). See DM-46498 for details.
555 prior_size = self.fit_coadd_multiband.size_priors[component_names_default.deV]
556 prior_size.intercept_mag = 21.2
557 prior_size.slope_median_per_mag = -0.14
560class MultiProFitCoaddDeVFitTask(MultiProFitCoaddObjectFitTask):
561 """MultiProFit single DeVaucouleurs model fit task."""
563 ConfigClass = MultiProFitCoaddDeVFitConfig
564 _DefaultName = "multiProFitCoaddDeVFit"
567class CachedChainedModelInitializer(CachedBasicModelInitializer):
568 def get_centroid_and_shape(
569 self,
570 source: Mapping[str, Any],
571 catexps: list[CatalogExposureSourcesABC],
572 config_data: CatalogSourceFitterConfigData,
573 values_init: Mapping[g2f.ParameterD, float] | None = None,
574 ) -> tuple[tuple[float, float], tuple[float, float, float]]:
575 row_best = None
576 chisq_red_min = math.inf
577 for name, input_data in self.inputs.items():
578 data = input_data.data
579 index_row = input_data.id_index.get(source["id"])
580 if index_row is not None:
581 row = data[index_row]
582 chisq_red = input_data.get_column("chisq_reduced", data=row)
583 if chisq_red < chisq_red_min:
584 row_best = (row, input_data)
585 chisq_red_min = chisq_red
586 if row_best is None:
587 return super().get_centroid_and_shape(
588 source=source,
589 catexps=catexps,
590 config_data=config_data,
591 values_init=values_init,
592 )
593 row_best, input_data = row_best
594 cen_x, cen_y, reff_x, reff_y, rho = (
595 input_data.get_column(column, data=row_best)
596 for column in (
597 input_data.get_column("cen_x").name,
598 input_data.get_column("cen_y").name,
599 input_data.get_column(f"{input_data.size_column}_x").name,
600 input_data.get_column(f"{input_data.size_column}_y").name,
601 input_data.get_column("rho").name,
602 )
603 )
604 return (cen_x, cen_y), (reff_x, reff_y, rho)
607class MakeCachedChainedInitializerAction(MakeBasicInitializerAction):
608 def _make_initializer(
609 self,
610 catalog_multi: Sequence,
611 catexps: list[CatalogExposureInputs],
612 config_data: CatalogSourceFitterConfigData,
613 ) -> ModelInitializer:
614 sources, priors = config_data.sources_priors
615 return CachedChainedModelInitializer(config=self.config, priors=priors, sources=sources)
618class MultiProFitCoaddExpDeVFitConfig(
619 MultiProFitCoaddObjectFitConfig,
620 pipelineConnections=MultiProFitCoaddObjectFitConnections,
621):
622 """MultiProFit single Exponential+DeVaucouleurs model fit task config."""
624 @classmethod
625 def get_model_name_default(cls) -> str:
626 return f"{model_names_default.exp}{model_names_default.deV}"
628 @classmethod
629 def get_model_name_full(cls) -> str:
630 return "Exponential + de Vaucouleurs"
632 def make_default_model_config(self) -> ModelConfig:
633 config_group = ComponentGroupConfig(
634 components_sersic={
635 component_names_default.exp: self.make_sersic_component(value_initial=1.0, fixed=True),
636 component_names_default.deV: self.make_sersic_component(value_initial=4.0, fixed=True),
637 },
638 )
639 return self.make_single_model_config(group=config_group)
641 def setDefaults(self):
642 super().setDefaults()
643 self.fit_coadd_multiband.action_initializer = MakeCachedChainedInitializerAction()
644 self.fit_coadd_multiband.config_model = self.make_default_model_config()
645 self.name_model = self.get_model_name_default()
646 self.connections.name_table = self.name_model
648 size_priors = self.fit_coadd_multiband.size_priors
649 size_priors[component_names_default.exp] = MagnitudeDependentSizePriorConfig(
650 intercept_mag=23.3,
651 slope_median_per_mag=-0.14,
652 )
653 size_priors[component_names_default.deV] = MagnitudeDependentSizePriorConfig(
654 intercept_mag=21.2,
655 slope_median_per_mag=-0.14,
656 )
659class MultiProFitCoaddExpDeVFitTask(MultiProFitCoaddObjectFitTask):
660 """MultiProFit single ExpDeV model fit task."""
662 ConfigClass = MultiProFitCoaddExpDeVFitConfig
663 _DefaultName = "multiProFitCoaddExpDeVFit"