Coverage for python/lsst/meas/extensions/multiprofit/pipetasks_fit.py: 63%
265 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 02:26 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 02:26 -0700
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 "component_names_default",
24 "model_names_default",
25 "MultiProFitCoaddPsfFitConfig",
26 "MultiProFitCoaddPsfFitTask",
27 "MultiProFitCoaddObjectFitConfig",
28 "MultiProFitCoaddPointFitConfig",
29 "MultiProFitCoaddSersicFitConfig",
30 "MultiProFitCoaddSersicFitTask",
31 "MultiProFitCoaddGaussFitConfig",
32 "MultiProFitCoaddGaussFitTask",
33 "MultiProFitCoaddExpFitConfig",
34 "MultiProFitCoaddExpFitTask",
35 "MultiProFitCoaddDeVFitConfig",
36 "MultiProFitCoaddDeVFitTask",
37 "MultiProFitCoaddExpDeVFitConfig",
38 "MultiProFitCoaddExpDeVFitTask",
39)
41from abc import abstractmethod
42import itertools
43import math
44from types import SimpleNamespace
45from typing import Any, Mapping, Sequence
47from lsst.daf.butler import DeferredDatasetHandle
48import lsst.gauss2d.fit as g2f
49from lsst.multiprofit.componentconfig import (
50 GaussianComponentConfig,
51 ParameterConfig,
52 SersicComponentConfig,
53 SersicIndexParameterConfig,
54)
55from lsst.multiprofit.fitting.fit_source import CatalogExposureSourcesABC, CatalogSourceFitterConfigData
56from lsst.multiprofit.modelconfig import ModelConfig
57from lsst.multiprofit.sourceconfig import ComponentGroupConfig, SourceConfig
58from lsst.pex.config import ConfigDictField, Field
59from lsst.pipe.tasks.fit_coadd_multiband import (
60 CatalogExposureInputs,
61 CoaddMultibandFitConfig,
62 CoaddMultibandFitConnections,
63 CoaddMultibandFitTask,
64)
65from lsst.pipe.tasks.fit_coadd_psf import CoaddPsfFitConfig, CoaddPsfFitConnections, CoaddPsfFitTask
67from .fit_coadd_multiband import (
68 CachedBasicModelInitializer,
69 MagnitudeDependentSizePriorConfig,
70 MakeBasicInitializerAction,
71 ModelInitializer,
72 MultiProFitSourceTask,
73 PsfComponentsActionBase,
74 SourceTablePsfComponentsAction,
75)
76from .fit_coadd_psf import MultiProFitPsfTask
77from .input_config import InputConfig
79component_names_default = SimpleNamespace(
80 point="point",
81 gauss="gauss",
82 exp="exp",
83 deV="deV",
84 sersic="sersic",
85)
87model_names_default = SimpleNamespace(
88 point="Point",
89 gauss="Gauss",
90 exp="Exp",
91 deV="DeV",
92 sersic="Sersic",
93 fixed_cen="FixedCen",
94 shapelet_psf="ShapeletPsf",
95)
98class MultiProFitCoaddPsfFitConfig(
99 CoaddPsfFitConfig,
100 pipelineConnections=CoaddPsfFitConnections,
101):
102 """MultiProFit PSF fit task config."""
104 def setDefaults(self):
105 super().setDefaults()
106 self.fit_coadd_psf.retarget(MultiProFitPsfTask)
107 self.fit_coadd_psf.config_fit.eval_residual = False
110class MultiProFitCoaddPsfFitTask(CoaddPsfFitTask):
111 """MultiProFit PSF fit task."""
113 ConfigClass = MultiProFitCoaddPsfFitConfig
114 _DefaultName = "multiProFitCoaddPsfFit"
117class MultiProFitCoaddObjectFitConnections(CoaddMultibandFitConnections):
118 def __init__(self, *, config=None):
119 super().__init__(config=config)
120 for name, config_input in config.inputs_init.items():
121 if hasattr(self, name):
122 raise ValueError(
123 f"{config_input=} {name=} is invalid, due to being an existing attribute" f" of {self=}"
124 )
125 if config_input.is_multipatch or not config_input.is_multiband:
126 raise ValueError(
127 f"Single-band and/or multipatch initialization config_input entries ({name})"
128 f" are not supported yet."
129 )
130 connection = config_input.get_connection(name)
131 setattr(self, name, connection)
134class MultiProFitCoaddObjectFitConfig(
135 CoaddMultibandFitConfig,
136 pipelineConnections=MultiProFitCoaddObjectFitConnections,
137):
138 """Generic MultiProFit source fit task config."""
140 inputs_init = ConfigDictField(
141 doc="Mapping of optional input dataset configs by name, for initialization",
142 keytype=str,
143 itemtype=InputConfig,
144 default={},
145 )
147 # This needs to be set, ideally in setDefaults of subclasses
148 name_model = Field[str](doc="The name of the model", default=None)
150 def _get_source(self):
151 return next(iter(self.fit_coadd_multiband.config_model.sources.values()))
153 def _get_component_group(self, source: SourceConfig | None = None):
154 if source is None:
155 source = self._get_source()
156 return next(iter(source.component_groups.values()))
158 def add_point_source(self, name: str | None = None):
159 """Add a point source component.
161 Parameters
162 ----------
163 name
164 The name of the component.
165 """
166 if name is None: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true
167 name = component_names_default.point
168 source = self._get_source()
169 group = self._get_component_group(source=source)
170 if name in group.components_gauss: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 raise RuntimeError(f"{name=} component already exists in {source=}")
172 group.components_gauss[name] = self.make_point_source_component()
173 self.connections.name_table += model_names_default.point
175 def finalize(
176 self,
177 add_point_source: bool = False,
178 fix_centroid: bool = False,
179 use_shapelet_psf: bool = False,
180 prior_axrat_stddev: float | str | None = None,
181 ):
182 """Apply runtime configuration changes to this config.
184 Parameters
185 ----------
186 add_point_source
187 Whether to add a point source component.
188 fix_centroid
189 Whether to fix the centroid.
190 use_shapelet_psf
191 Whether to initialize PSF parameters from prior shapelet fits.
192 prior_axrat_stddev
193 The standard deviation for the axis ratio prior. Ignored if None,
194 otherwise it must be convertible to a float.
195 """
196 if add_point_source:
197 self.add_point_source()
198 if fix_centroid:
199 self.fix_centroid()
200 if use_shapelet_psf:
201 self.use_shapelet_psf()
202 if prior_axrat_stddev is not None:
203 self.set_prior_axrat_stddev(float(prior_axrat_stddev))
205 def fix_centroid(self):
206 """Fix (freeze) the source centroid parameters."""
207 group = self._get_component_group()
208 centroids = group.centroids["default"]
209 centroids.x.fixed = True
210 centroids.y.fixed = True
211 self.connections.name_table += model_names_default.fixed_cen
213 @classmethod
214 @abstractmethod
215 def get_model_name_default(cls) -> str:
216 """Return the default name for this model in table columns."""
217 raise NotImplementedError("Subclasses must implement get_model_name_default")
219 @classmethod
220 def get_model_name_full(cls) -> str:
221 """Return a longer, more descriptive name for the model."""
222 return cls.get_model_name_default()
224 @abstractmethod
225 def make_default_model_config(self) -> ModelConfig:
226 """Make a default configuration object for this model."""
227 raise NotImplementedError("Subclasses must implement make_default_model_config")
229 @staticmethod
230 def make_point_source_component() -> GaussianComponentConfig:
231 """Make a point source component config (zero-size Gaussian)."""
232 return GaussianComponentConfig(
233 size_x=ParameterConfig(value_initial=0.0, fixed=True),
234 size_y=ParameterConfig(value_initial=0.0, fixed=True),
235 rho=ParameterConfig(value_initial=0.0, fixed=True),
236 )
238 @staticmethod
239 def make_sersic_component(**kwargs) -> SersicComponentConfig:
240 """Make a default Sersic component config.
242 Parameters
243 ----------
244 **kwargs
245 Keyword arguments to pass to the SersicIndexParameterConfig.
247 Returns
248 -------
249 config
250 The default-initialized config.
251 """
252 return SersicComponentConfig(
253 prior_axrat_stddev=1.0,
254 prior_size_stddev=0.2,
255 sersic_index=SersicIndexParameterConfig(**kwargs),
256 )
258 @staticmethod
259 def make_single_model_config(group: ComponentGroupConfig) -> ModelConfig:
260 """Make a default single-source, single component group config.
262 Parameters
263 ----------
264 group
265 The component group config for the single source.
267 Returns
268 -------
269 config
270 A model config with a single nameless source and component group.
271 """
272 return ModelConfig(
273 sources={
274 "": SourceConfig(
275 component_groups={
276 "": group,
277 }
278 )
279 }
280 )
282 def set_prior_axrat_stddev(self, stddev: float) -> None:
283 """Set the standard deviation for all axis ratio priors.
285 Parameters
286 ----------
287 stddev
288 The standard deviation.
289 """
290 for source in self.fit_coadd_multiband.config_model.sources.values():
291 for group in source.component_groups.values():
292 for comp in itertools.chain(
293 group.components_gauss.values(),
294 group.components_sersic.values(),
295 ):
296 comp.prior_axrat_stddev = stddev
298 group = self._get_component_group()
299 centroids = group.centroids["default"]
300 centroids.x.fixed = True
301 centroids.y.fixed = True
302 self.connections.name_table += model_names_default.fixed_cen
304 def setDefaults(self):
305 super().setDefaults()
306 self.fit_coadd_multiband.retarget(MultiProFitSourceTask)
307 self.fit_coadd_multiband.action_psf = PsfComponentsActionBase()
308 self.fit_coadd_multiband.bands_fit = ("u", "g", "r", "i", "z", "y")
310 self.fit_coadd_multiband.config_model = self.make_default_model_config()
311 self.name_model = self.get_model_name_default()
312 self.connections.name_table = self.name_model
314 def use_shapelet_psf(self):
315 """Reconfigure self to use prior shapelet PSF fit parameters."""
316 self.fit_coadd_multiband.action_psf = SourceTablePsfComponentsAction()
317 self.drop_psf_connection = True
318 self.connections.name_table += model_names_default.shapelet_psf
321class MultiProFitCoaddObjectFitTask(CoaddMultibandFitTask):
322 """MultiProFit coadd object model fitting task."""
324 ConfigClass = MultiProFitCoaddObjectFitConfig
325 _DefaultName = "multiProFitCoaddObjectFit"
327 def make_kwargs(self, butlerQC, inputRefs, inputs):
328 inputs_init = {}
329 for name, config in self.config.inputs_init.items():
330 input_ = inputs[name][0]
331 if isinstance(input_, DeferredDatasetHandle):
332 parameters = None
333 if config.needs_metadata:
334 parameters = {"strip_astropy_meta_yaml": False}
335 input_ = input_.get(parameters=parameters)
337 inputs_init[name] = (config, input_)
339 kwargs = {}
340 if inputs_init:
341 kwargs["inputs_init"] = inputs_init
343 return kwargs
346class MultiProFitCoaddPointFitConfig(
347 MultiProFitCoaddObjectFitConfig,
348 pipelineConnections=MultiProFitCoaddObjectFitConnections,
349):
350 """MultiProFit single Sersic model fit task config."""
352 @classmethod
353 def get_model_name_default(cls) -> str:
354 return model_names_default.point
356 @classmethod
357 def get_model_name_full(cls) -> str:
358 return "Point Source"
360 def make_default_model_config(self) -> ModelConfig:
361 config_group = ComponentGroupConfig()
362 # This is a bit silly but add_point_source will look for the first
363 # source so it must be added now. Perhaps add_point_source should
364 # add to a config instance or only self by default
365 self.fit_coadd_multiband.config_model = self.make_single_model_config(group=config_group)
366 self.add_point_source()
367 return self.fit_coadd_multiband.config_model
370class MultiProFitCoaddSersicFitConfig(
371 MultiProFitCoaddObjectFitConfig,
372 pipelineConnections=MultiProFitCoaddObjectFitConnections,
373):
374 """MultiProFit single Sersic model fit task config."""
376 def _rename_defaults(
377 self,
378 name_new: str,
379 name_model: str | None = None,
380 name_old: str | None = None,
381 index_new: float | None = None,
382 fix_index: bool = False,
383 ):
384 """Rename the default Sersic component to something more specific.
386 This is intended for fixed index models such as exponential and
387 deVaucouleurs.
389 Parameters
390 ----------
391 name_new
392 The new name for the component.
393 name_model
394 The new name of the model. Default is to capitalize name_new.
395 name_old
396 The old name of the component. Default is to set to
397 component_names_default.sersic.
398 index_new
399 The initial value for the Sersic index.
400 fix_index
401 Whether the fix the index to the new value.
402 """
403 if name_old is None: 403 ↛ 405line 403 didn't jump to line 405 because the condition on line 403 was always true
404 name_old = component_names_default.sersic
405 if name_model is None: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true
406 name_model = name_new.capitalize()
407 group = self._get_component_group()
408 comps_sersic = group.components_sersic
410 if name_new in comps_sersic: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 raise RuntimeError(f"{name_new=} is already in {comps_sersic=}")
413 comp_sersic = comps_sersic[name_old]
414 del comps_sersic[name_old]
415 if index_new is not None: 415 ↛ 417line 415 didn't jump to line 417 because the condition on line 415 was always true
416 comp_sersic.sersic_index.value_initial = index_new
417 if fix_index: 417 ↛ 419line 417 didn't jump to line 419 because the condition on line 417 was always true
418 comp_sersic.sersic_index.fixed = True
419 comps_sersic[name_new] = comp_sersic
421 if prior_old := self.fit_coadd_multiband.size_priors.get(name_old): 421 ↛ 425line 421 didn't jump to line 425 because the condition on line 421 was always true
422 self.fit_coadd_multiband.size_priors[name_new] = prior_old
423 del self.fit_coadd_multiband.size_priors[name_old]
425 self.name_model = name_model
426 self.connections.name_table = name_model
428 @classmethod
429 def get_model_name_default(cls) -> str:
430 return model_names_default.sersic
432 @classmethod
433 def get_model_name_full(cls) -> str:
434 return "Sersic"
436 def make_default_model_config(self) -> ModelConfig:
437 config_group = ComponentGroupConfig(
438 components_sersic={
439 component_names_default.sersic: self.make_sersic_component(),
440 },
441 )
442 return self.make_single_model_config(group=config_group)
444 def setDefaults(self):
445 super().setDefaults()
446 # This is in pixels and based on DC2. See DM-46498 for details.
447 self.fit_coadd_multiband.size_priors[component_names_default.sersic] = (
448 MagnitudeDependentSizePriorConfig(
449 intercept_mag=22.6,
450 slope_median_per_mag=-0.15,
451 slope_stddev_per_mag=0,
452 )
453 )
456class MultiProFitCoaddSersicFitTask(MultiProFitCoaddObjectFitTask):
457 """MultiProFit single Sersic model fit task."""
459 ConfigClass = MultiProFitCoaddSersicFitConfig
460 _DefaultName = "multiProFitCoaddSersicFit"
463class MultiProFitCoaddGaussFitConfig(
464 MultiProFitCoaddSersicFitConfig,
465 pipelineConnections=MultiProFitCoaddObjectFitConnections,
466):
467 """MultiProFit single Gaussian model fit task config."""
469 @classmethod
470 def get_model_name_default(cls) -> str:
471 return model_names_default.gauss
473 @classmethod
474 def get_model_name_full(cls) -> str:
475 return "Gaussian"
477 def setDefaults(self):
478 super().setDefaults()
479 self._rename_defaults(
480 name_new=component_names_default.gauss,
481 name_model=model_names_default.gauss,
482 index_new=0.5,
483 fix_index=True,
484 )
487class MultiProFitCoaddGaussFitTask(MultiProFitCoaddObjectFitTask):
488 """MultiProFit single Gaussian model fit task."""
490 ConfigClass = MultiProFitCoaddGaussFitConfig
491 _DefaultName = "multiProFitCoaddGaussFit"
494class MultiProFitCoaddExpFitConfig(
495 MultiProFitCoaddSersicFitConfig,
496 pipelineConnections=MultiProFitCoaddObjectFitConnections,
497):
498 """MultiProFit single exponential model fit task config."""
500 @classmethod
501 def get_model_name_default(cls) -> str:
502 return model_names_default.exp
504 @classmethod
505 def get_model_name_full(cls) -> str:
506 return "Exponential"
508 def setDefaults(self):
509 super().setDefaults()
510 self._rename_defaults(
511 name_new=component_names_default.exp,
512 name_model=model_names_default.exp,
513 index_new=1.0,
514 fix_index=True,
515 )
516 # These are typical values from DC2 and could/should be switched to a
517 # more data-driven prior (from HSC?)
518 prior_size = self.fit_coadd_multiband.size_priors[component_names_default.exp]
519 prior_size.intercept_mag = 23.4
520 prior_size.slope_median_per_mag = -0.14
523class MultiProFitCoaddExpFitTask(MultiProFitCoaddObjectFitTask):
524 """MultiProFit single exponential model fit task."""
526 ConfigClass = MultiProFitCoaddExpFitConfig
527 _DefaultName = "multiProFitCoaddExpFit"
530class MultiProFitCoaddDeVFitConfig(
531 MultiProFitCoaddSersicFitConfig,
532 pipelineConnections=MultiProFitCoaddObjectFitConnections,
533):
534 """MultiProFit single DeVaucouleurs model fit task config."""
536 @classmethod
537 def get_model_name_default(cls) -> str:
538 return model_names_default.deV
540 @classmethod
541 def get_model_name_full(cls) -> str:
542 return "de Vaucouleurs"
544 def setDefaults(self):
545 super().setDefaults()
546 self._rename_defaults(
547 name_new=component_names_default.deV,
548 name_model=model_names_default.deV,
549 index_new=4.0,
550 fix_index=True,
551 )
552 # These are typical values from DC2 and could/should be switched to a
553 # more data-driven prior (from HSC?). See DM-46498 for details.
554 prior_size = self.fit_coadd_multiband.size_priors[component_names_default.deV]
555 prior_size.intercept_mag = 21.2
556 prior_size.slope_median_per_mag = -0.14
559class MultiProFitCoaddDeVFitTask(MultiProFitCoaddObjectFitTask):
560 """MultiProFit single DeVaucouleurs model fit task."""
562 ConfigClass = MultiProFitCoaddDeVFitConfig
563 _DefaultName = "multiProFitCoaddDeVFit"
566class CachedChainedModelInitializer(CachedBasicModelInitializer):
567 def get_centroid_and_shape(
568 self,
569 source: Mapping[str, Any],
570 catexps: list[CatalogExposureSourcesABC],
571 config_data: CatalogSourceFitterConfigData,
572 values_init: Mapping[g2f.ParameterD, float] | None = None,
573 ) -> tuple[tuple[float, float], tuple[float, float, float]]:
574 row_best = None
575 chisq_red_min = math.inf
576 for name, input_data in self.inputs.items():
577 data = input_data.data
578 index_row = input_data.id_index.get(source["id"])
579 if index_row is not None:
580 row = data[index_row]
581 chisq_red = input_data.get_column("chisq_reduced", data=row)
582 if chisq_red < chisq_red_min:
583 row_best = (row, input_data)
584 chisq_red_min = chisq_red
585 if row_best is None:
586 return super().get_centroid_and_shape(
587 source=source,
588 catexps=catexps,
589 config_data=config_data,
590 values_init=values_init,
591 )
592 row_best, input_data = row_best
593 cen_x, cen_y, reff_x, reff_y, rho = (
594 input_data.get_column(column, data=row_best)
595 for column in (
596 input_data.get_column("cen_x").name,
597 input_data.get_column("cen_y").name,
598 input_data.get_column(f"{input_data.size_column}_x").name,
599 input_data.get_column(f"{input_data.size_column}_y").name,
600 input_data.get_column("rho").name,
601 )
602 )
603 return (cen_x, cen_y), (reff_x, reff_y, rho)
606class MakeCachedChainedInitializerAction(MakeBasicInitializerAction):
607 def _make_initializer(
608 self,
609 catalog_multi: Sequence,
610 catexps: list[CatalogExposureInputs],
611 config_data: CatalogSourceFitterConfigData,
612 ) -> ModelInitializer:
613 sources, priors = config_data.sources_priors
614 return CachedChainedModelInitializer(config=self.config, priors=priors, sources=sources)
617class MultiProFitCoaddExpDeVFitConfig(
618 MultiProFitCoaddObjectFitConfig,
619 pipelineConnections=MultiProFitCoaddObjectFitConnections,
620):
621 """MultiProFit single Exponential+DeVaucouleurs model fit task config."""
623 @classmethod
624 def get_model_name_default(cls) -> str:
625 return f"{model_names_default.exp}{model_names_default.deV}"
627 @classmethod
628 def get_model_name_full(cls) -> str:
629 return "Exponential + de Vaucouleurs"
631 def make_default_model_config(self) -> ModelConfig:
632 config_group = ComponentGroupConfig(
633 components_sersic={
634 component_names_default.exp: self.make_sersic_component(value_initial=1.0, fixed=True),
635 component_names_default.deV: self.make_sersic_component(value_initial=4.0, fixed=True),
636 },
637 )
638 return self.make_single_model_config(group=config_group)
640 def setDefaults(self):
641 super().setDefaults()
642 self.fit_coadd_multiband.action_initializer = MakeCachedChainedInitializerAction()
643 self.fit_coadd_multiband.config_model = self.make_default_model_config()
644 self.name_model = self.get_model_name_default()
645 self.connections.name_table = self.name_model
647 size_priors = self.fit_coadd_multiband.size_priors
648 size_priors[component_names_default.exp] = MagnitudeDependentSizePriorConfig(
649 intercept_mag=23.3,
650 slope_median_per_mag=-0.14,
651 )
652 size_priors[component_names_default.deV] = MagnitudeDependentSizePriorConfig(
653 intercept_mag=21.2,
654 slope_median_per_mag=-0.14,
655 )
658class MultiProFitCoaddExpDeVFitTask(MultiProFitCoaddObjectFitTask):
659 """MultiProFit single ExpDeV model fit task."""
661 ConfigClass = MultiProFitCoaddExpDeVFitConfig
662 _DefaultName = "multiProFitCoaddExpDeVFit"