Coverage for python/lsst/multiprofit/fitting/fit_psf.py: 15%
298 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:36 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:36 -0700
1# This file is part of 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 "CatalogExposurePsfABC",
24 "CatalogPsfFitterConfig",
25 "CatalogPsfFitterConfigData",
26 "CatalogPsfFitter",
27]
29from abc import abstractmethod
30from functools import cached_property
31import logging
32import time
33from typing import Any, ClassVar, Mapping, Type
35import astropy
36import lsst.gauss2d as g2
37import lsst.gauss2d.fit as g2f
38import lsst.pex.config as pexConfig
39from lsst.utils.logging import PeriodicLogger
40import numpy as np
41import pydantic
43from ..componentconfig import (
44 Fluxes,
45 FluxFractionParameterConfig,
46 FluxParameterConfig,
47 GaussianComponentConfig,
48 ParameterConfig,
49)
50from ..errors import PsfRebuildFitFlagError
51from ..modeller import FitInputsDummy, LinearGaussians, Modeller, make_psf_model_null
52from ..sourceconfig import ComponentConfigs, ComponentGroupConfig, SourceConfig
53from ..utils import frozen_arbitrary_allowed_config, get_params_uniq
54from .fit_catalog import CatalogExposureABC, CatalogFitterConfig, ColumnInfo
56SourceRow = astropy.table.Row | Mapping[str, Any]
59class CatalogPsfFitterConfig(CatalogFitterConfig):
60 """Configuration for MultiProFit PSF image fitter."""
62 model = pexConfig.ConfigField[SourceConfig](
63 default=SourceConfig(
64 component_groups={
65 "": ComponentGroupConfig(
66 components_gauss={
67 "gauss1": GaussianComponentConfig(
68 size_x=ParameterConfig(value_initial=1.5),
69 size_y=ParameterConfig(value_initial=1.5),
70 fluxfrac=FluxFractionParameterConfig(value_initial=0.5),
71 flux=FluxParameterConfig(value_initial=1.0, fixed=True),
72 ),
73 "gauss2": GaussianComponentConfig(
74 size_x=ParameterConfig(value_initial=3.0),
75 size_y=ParameterConfig(value_initial=3.0),
76 fluxfrac=FluxFractionParameterConfig(value_initial=1.0, fixed=True),
77 ),
78 },
79 is_fractional=True,
80 )
81 }
82 ),
83 doc="PSF model configuration",
84 )
85 prior_axrat_mean = pexConfig.Field[float](default=0.95, doc="Mean for axis ratio prior")
87 def make_psf_model(
88 self,
89 component_group_fluxes: list[list[Fluxes]] | None = None,
90 ) -> [g2f.PsfModel, list[g2f.Prior]]:
91 """Make a PsfModel object for a given source.
93 Parameters
94 ----------
95 component_group_fluxes
96 Initial fluxes for each constituent ComponentGroup.
98 Returns
99 -------
100 psf_model
101 The rebuilt PSF model.
103 Notes
104 -----
105 This function does not initialize the PSF model.
106 """
107 if component_group_fluxes is None:
108 channels = (g2f.Channel.NONE,)
109 component_group_fluxes = [
110 component_group.get_fluxes_default(
111 channels=channels,
112 component_configs=component_group.get_component_configs(),
113 is_fractional=component_group.is_fractional,
114 )
115 for component_group in self.model.component_groups.values()
116 ]
118 psf_model, _ = self.model.make_psf_model(component_group_fluxes=component_group_fluxes)
119 return psf_model
121 def schema_configurable(self) -> list[ColumnInfo]:
122 columns = []
123 if self.config_fit.eval_residual:
124 columns.append(ColumnInfo(key="n_eval_jac", dtype="i4"))
125 return columns
127 def schema(
128 self,
129 bands: list[str] | None = None,
130 ) -> list[ColumnInfo]:
131 """Return the schema as an ordered list of columns.
133 Parameters
134 ----------
135 bands
136 The bands to add band-dependent columns for.
137 """
138 if bands is not None:
139 if len(bands) != 1:
140 raise ValueError("CatalogPsfFitter must have exactly one band")
141 schema = super().schema(bands)
142 parameters = CatalogPsfFitterConfigData(config=self).parameters
143 unit_size = "pix"
144 units = {
145 g2f.ReffXParameterD: unit_size,
146 g2f.ReffYParameterD: unit_size,
147 g2f.SizeXParameterD: unit_size,
148 g2f.SizeYParameterD: unit_size,
149 }
150 schema.extend(
151 [
152 ColumnInfo(key=key, dtype="f8", unit=units.get(type(param)))
153 for key, param in parameters.items()
154 ]
155 )
156 schema.extend(self.schema_configurable())
158 return schema
160 def setDefaults(self) -> None:
161 self.prefix_column = "mpf_psf_"
162 self.compute_errors = "NONE"
165class CatalogPsfFitterConfigData(pydantic.BaseModel):
166 """A PSF fit configuration that can initialize models and images thereof.
168 This class relies on cached properties being computed once, mostly shortly
169 after initialization. Therefore, it and the config field must be frozen to
170 ensure that the model remains unchanged.
171 """
173 model_config: ClassVar[pydantic.ConfigDict] = frozen_arbitrary_allowed_config
175 config: CatalogPsfFitterConfig = pydantic.Field(title="A CatalogPsfFitterConfig to be frozen")
177 @pydantic.field_validator("config")
178 @classmethod
179 def validate_config(cls, v: CatalogPsfFitterConfig) -> CatalogPsfFitterConfig:
180 v.validate()
181 return v
183 @cached_property
184 def components(self) -> dict[str, g2f.Component]:
185 """Return the components of the PSF model by name."""
186 components = self.psf_model.components
187 names = self.component_configs.keys()
188 if len(components) != len(names):
189 raise RuntimeError(f"{len(components)=} != {len(names)=}")
190 components_names = {name: component for name, component in zip(names, components)}
191 return components_names
193 @cached_property
194 def component_configs(self) -> ComponentConfigs:
195 """Return the config for each component in the PSF model."""
196 return self.config.model.get_component_configs()
198 @cached_property
199 def componentgroup_configs(self) -> dict[str, ComponentGroupConfig]:
200 """Return the config for each component group in the PSF model
201 by name.
202 """
203 return {k: v for k, v in self.config.model.component_groups.items()}
205 def init_psf_model(
206 self,
207 params: SourceRow,
208 ) -> None:
209 """Initialize the PSF model for a single source.
211 Parameters
212 ----------
213 params : astropy.table.Row | typing.Mapping[str, typing.Any]
214 A mapping with parameter values for the best-fit PSF model at the
215 centroid of a single source.
216 """
217 # TODO: Improve _flag checking (add a total _flag column)
218 for flag in (col for col in params.keys() if col.endswith("_flag")):
219 if params[flag]:
220 raise PsfRebuildFitFlagError(f"Failed to rebuild PSF; {flag} set")
222 for name, param in self.parameters.items():
223 param.value = params[f"{self.config.prefix_column}{name}"]
225 def model_post_init(self, __context: Any) -> None:
226 self.config.freeze()
227 n_component_configs = len(self.component_configs)
228 n_components = len(self.psf_model.components)
229 if n_components != n_component_configs:
230 raise AssertionError(f"{n_components=} != {n_component_configs=}")
232 @cached_property
233 def parameters(self) -> dict[str, g2f.ParameterD]:
234 """Return the free parameters for the PSF model by name."""
235 parameters = {}
236 config = self.config
237 has_prefix_group = config.model.has_prefix_group()
238 components = self.psf_model.components
239 idx_comp_first = 0
241 label_cen = config.get_key_cen()
242 label_rho = config.get_key_rho()
243 label_flux = config.get_key_flux("")
244 label_fluxfrac = f"{label_flux}frac"
245 suffix_x, suffix_y = config.get_suffix_x(), config.get_suffix_y()
247 # Iterate over each component group
248 for name_group, config_group in self.componentgroup_configs.items():
249 prefix_group = f"{name_group}_" if has_prefix_group else ""
250 is_fractional = config_group.is_fractional
251 # Does this group have independent centroids?
252 # Should generally be False if not is_fractional, since there is
253 # little reason to make a group otherwise.
254 multicen = len(config_group.centroids) > 1
255 configs_comp = config_group.get_component_configs()
256 idx_last = len(configs_comp) - 1
257 n_params_flux_frac = 0
259 # Iterate over each component in the group
260 for idx_comp_group, (name_comp, config_comp) in enumerate(configs_comp.items()):
261 # The last component needs special handling if is_fractional
262 is_last = idx_comp_group == idx_last
263 component = components[idx_comp_first + idx_comp_group]
264 prefix_comp = f"{prefix_group}{name_comp}"
265 key_size = config.get_prefixed_label(
266 config.get_key_size(config_comp.get_size_label()),
267 prefix_comp,
268 )
269 key_rho = config.get_prefixed_label(label_rho, prefix_comp)
271 # Give the centroid parameters an appropriate prefix
272 if multicen or (idx_comp_group == 0):
273 prefix_cen = prefix_comp if multicen else prefix_group
274 # Avoid redundant -underscores if there's nothing to prefix
275 # or an existing prefix starting with an underscore
276 key_cen = config.get_prefixed_label(label_cen, prefix_cen)
277 parameters[f"{key_cen}{suffix_x}"] = component.centroid.x_param
278 parameters[f"{key_cen}{suffix_y}"] = component.centroid.y_param
279 # Add each free shape parameter
280 if not config_comp.size_x.fixed:
281 parameters[f"{key_size}{suffix_x}"] = component.ellipse.size_x_param
282 if not config_comp.size_y.fixed:
283 parameters[f"{key_size}{suffix_y}"] = component.ellipse.size_y_param
284 if not config_comp.rho.fixed:
285 parameters[key_rho] = component.ellipse.rho_param
287 # TODO: return this to component.integralmodel
288 # when binding for g2f.FractionalIntegralModel is fixed
289 params_flux = get_params_uniq(component, fixed=False, nonlinear=False)
290 # Fractional models should have one fixed flux parameter
291 # (with value=1), and hence an empty params_flux
292 has_params_flux = not config_comp.flux.fixed and (
293 (not is_fractional) or (idx_comp_group == 0)
294 )
295 n_params_flux_expect = int(has_params_flux)
296 if len(params_flux) != n_params_flux_expect:
297 raise RuntimeError(
298 f"{params_flux=} has len={len(params_flux)} but expected {n_params_flux_expect}"
299 )
300 if has_params_flux:
301 parameters[f"{prefix_comp}{label_flux}"] = params_flux[0]
302 # TODO: return this to component.integralmodel
303 # when binding for g2f.FractionalIntegralModel is fixed
304 params_fluxfrac = [
305 param
306 for param in get_params_uniq(component, fixed=False, linear=False)
307 if isinstance(param, g2f.ProperFractionParameterD)
308 ]
309 if is_fractional:
310 # The last flux fraction must be fixed at 1.0, since it
311 # by definition contains all of the remaining flux
312 if is_last:
313 if (config_comp.fluxfrac.value_initial != 1.0) or (not config_comp.fluxfrac.fixed):
314 raise ValueError(
315 f"{config_comp=} {is_last=} and must be fixed with value_initial==1.0"
316 )
317 else:
318 if not config_comp.fluxfrac.fixed:
319 parameters[f"{prefix_comp}{label_fluxfrac}"] = params_fluxfrac[-1]
320 n_params_flux_frac += 1
321 if len(params_fluxfrac) != n_params_flux_frac:
322 raise RuntimeError(
323 f"{config_comp=} has {params_fluxfrac=} but expected {n_params_flux_frac=}"
324 )
325 else:
326 if len(params_fluxfrac) > 0:
327 raise RuntimeError(f"{config_group=} has {params_fluxfrac=} but {is_fractional=}")
329 return parameters
331 @cached_property
332 def psf_model(self) -> g2f.PsfModel:
333 """Return the PSF model."""
334 psf_model = self.config.make_psf_model()
335 return psf_model
337 @cached_property
338 def psf_model_gaussians(self) -> g2.Gaussians:
339 """Return the Gaussians comprising the PSF model."""
340 gaussians = self.psf_model.gaussians()
341 return gaussians
344class CatalogExposurePsfABC(CatalogExposureABC):
345 """A CatalogExposure for PSF fitting."""
347 @abstractmethod
348 def get_psf_image(
349 self,
350 source: SourceRow,
351 ) -> np.ndarray:
352 """Get a PSF image for a specific source.
354 Parameters
355 ----------
356 source
357 The source row/dict.
359 Returns
360 -------
361 psf
362 The image of the PSF.
364 Notes
365 -----
366 The PSF image should be normalized, and centered in a 2D array of odd
367 dimensions on both sides.
368 """
371class CatalogPsfFitter:
372 """Fit a Gaussian mixture model to a pixelated PSF image.
374 Parameters
375 ----------
376 modeller : `multiprofit.Modeller`
377 A Modeller instance to use for fitting.
378 errors_expected : dict[Type[Exception], str]
379 A dictionary keyed by an Exception type, with a string value of the
380 flag column key to assign if this Exception is raised.
382 Notes
383 -----
384 Any exceptions raised and not in errors_expected will be logged in a
385 generic unknown_flag failure column.
386 """
388 def __init__(
389 self,
390 modeller: Modeller | None = None,
391 errors_expected: dict[Type[Exception], str] | None = None,
392 ):
393 if modeller is None:
394 modeller = Modeller()
395 if errors_expected is None:
396 errors_expected = {}
397 self.errors_expected = errors_expected
398 self.modeller = modeller
400 @staticmethod
401 def _get_data_default(img_psf: np.ndarray, gain: float = 1e5) -> g2f.DataD:
402 """Return a default-initialized data for a given PSF image.
404 Parameters
405 ----------
406 img_psf
407 A normalized image of the PSF, ideally with noise.
408 gain
409 The multiplicative factor needed to change the image to counts.
411 Returns
412 -------
413 data
414 A DataD instance than can be used to fit a PSF model.
415 """
416 # TODO: Try to improve these ad-hoc settings after DM-49008 merges
417 # e.g. estimate PSF model noise from S/N of PSF stars?
418 # If more than 1% of pixels are negative, try to estimate noise
419 if np.sum(img_psf < 0) / img_psf.size > 0.01:
420 background = np.std(img_psf[img_psf < 2 * np.abs(np.min(img_psf))])
421 # otherwise, try to estimate noise from the lowest-values pixels
422 # There might not actually be much noise; in that case, the
423 # background value is not going to matter much anyway
424 else:
425 background = np.max((1e-10, np.std(img_psf[img_psf <= np.percentile(img_psf, 5.0)])))
426 min_psf = np.min(img_psf)
427 # ensure there are no negative values before sqrt
428 if not (background > -min_psf):
429 background = -1.1 * min_psf
430 img_sig_inv = np.sqrt(gain / (img_psf + background))
431 return g2f.DataD(
432 [
433 g2f.ObservationD(
434 channel=g2f.Channel.NONE,
435 image=g2.ImageD(img_psf),
436 sigma_inv=g2.ImageD(img_sig_inv),
437 mask_inv=g2.ImageB(np.ones_like(img_psf)),
438 )
439 ]
440 )
442 def _get_data(self, img_psf: np.ndarray, gain: float = 1e5) -> g2f.DataD:
443 """Build a Model-able gauss2d.fit.Data from a normalized PSF image.
445 Parameters
446 ----------
447 img_psf
448 A normalized PSF image array.
449 gain
450 The number of counts in the image, used as a multiplicative
451 factor for the inverse variance.
453 Returns
454 -------
455 data
456 A Data object that can be passed to a Model(ler).
457 """
458 return self._get_data_default(img_psf=img_psf, gain=gain)
460 @staticmethod
461 def _get_logger() -> logging.Logger:
462 """Return a suitably-named and configured logger."""
463 logger = logging.getLogger(__name__)
464 return logger
466 @abstractmethod
467 def check_source(self, source: SourceRow, config: CatalogPsfFitterConfig) -> None:
468 """Check whether a source can have its PSF model fit.
470 Parameters
471 ----------
472 source
473 The source row to check.
474 config
475 The fitter config.
477 Notes
478 -----
479 Derived classes may use the source row as they deem fit. For example,
480 if the source has poor quality flags, a fitter may choose not to fit
481 the PSF model if it will not end up being used anyway.
482 """
484 def fit(
485 self,
486 catexp: CatalogExposurePsfABC,
487 config_data: CatalogPsfFitterConfigData | None = None,
488 logger: logging.Logger | None = None,
489 **kwargs: Any,
490 ) -> astropy.table.Table:
491 """Fit PSF models for a catalog with MultiProFit.
493 Each source has its PSF fit with a configureable Gaussian mixture PSF
494 model, given a pixellated PSF image from the CatalogExposure.
496 Parameters
497 ----------
498 catexp
499 An exposure to fit a model PSF at the position of all
500 sources in the corresponding catalog.
501 config_data
502 Configuration settings for fitting and output.
503 logger
504 The logger. Defaults to calling `_getlogger`.
505 **kwargs
506 Additional keyword arguments to pass to self.modeller.
508 Returns
509 -------
510 catalog
511 A table with fit parameters for the PSF model at the location
512 of each source.
513 """
514 if config_data is None:
515 config_data = CatalogPsfFitterConfigData(config=CatalogPsfFitterConfig())
516 if logger is None:
517 logger = CatalogPsfFitter._get_logger()
518 config = config_data.config
519 if config.compute_errors != "NONE":
520 raise ValueError("CatalogPsfFitter doesn't support computing errors")
522 errors_expected = set(self.errors_expected.values())
523 n_errors_expected = len(errors_expected)
524 if n_errors_expected != len(self.errors_expected):
525 raise ValueError(f"{self.errors_expected=} has duplicate values; they must be unique")
526 if n_errors_expected != len(config.flag_errors):
527 raise ValueError(f"len({self.errors_expected=}) != len({config.flag_errors=})")
529 priors = []
530 sigmas = [
531 np.linalg.norm((comp.size_x.value_initial, comp.size_y.value_initial))
532 for comp in config_data.component_configs.values()
533 ]
535 psf_model = config_data.psf_model
536 model_source = g2f.Source(psf_model.components)
538 for idx, (comp, config_comp) in enumerate(
539 zip(psf_model.components, config_data.component_configs.values())
540 ):
541 prior = config_comp.make_shape_prior(comp.ellipse)
542 if prior:
543 if prior_size := prior.prior_size:
544 prior_size.mean = sigmas[idx]
545 if prior_axrat := prior.prior_axrat:
546 prior_axrat.mean = config.prior_axrat_mean
547 priors.append(prior)
549 params = config_data.parameters
550 flux_total = tuple(get_params_uniq(psf_model, nonlinear=False, channel=g2f.Channel.NONE))
551 if len(flux_total) != 1:
552 raise RuntimeError(f"len({flux_total=}) != 1; PSF model is badly-formed")
553 flux_total = flux_total[0]
554 gaussians_linear = None
555 if config.fit_linear_init:
556 # The total flux must be freed first or else LinearGaussians.make
557 # will fail to find the required number of free linear params
558 flux_total.fixed = False
559 gaussians_linear = LinearGaussians.make(model_source, is_psf=True)
560 flux_total.fixed = True
562 # TODO: Remove isinstance when channel filtering is fixed: DM-45665
563 fluxfracs = tuple(
564 param
565 for param in get_params_uniq(model_source, linear=False, channel=g2f.Channel.NONE, fixed=False)
566 if isinstance(param, g2f.ProperFractionParameterD)
567 )
568 # We're fitting the PSF, so there's nothing to convolve with
569 model_psf = make_psf_model_null()
571 catalog = catexp.get_catalog()
572 n_rows = len(catalog)
573 range_idx = range(n_rows)
575 results, columns = config.make_catalog(n_rows)
576 prefix = config.prefix_column
577 columns_param = {f"{prefix}{key}": param for key, param in params.items()}
579 # dummy size for first iteration
580 size, size_new = 0, 0
581 fitInputs = FitInputsDummy()
582 time_init_all = time.process_time()
583 logger_periodic = PeriodicLogger(logger)
584 n_skipfail = 0
586 for idx in range_idx:
587 time_init = time.process_time()
588 row = results[idx]
589 source = catalog[idx]
590 id_source = source[config.column_id]
591 row[config.column_id] = id_source
592 time_final = time_init
594 try:
595 self.check_source(source, config=config)
596 img_psf = catexp.get_psf_image(source)
597 data = self._get_data(img_psf)
598 model = g2f.ModelD(data=data, psfmodels=[model_psf], sources=[model_source], priors=priors)
599 self.initialize_model(model=model, config_data=config_data)
601 # Caches the jacobian residual if the kernel size is unchanged
602 if img_psf.size != size:
603 fitInputs = None
604 size = int(img_psf.size)
605 # Some algorithms might not even use fitInputs
606 elif fitInputs is not None:
607 fitInputs = fitInputs if not fitInputs.validate_for_model(model) else None
609 if config.fit_linear_init:
610 result = self.modeller.fit_gaussians_linear(gaussians_linear, data[0])
611 result = list(result.values())[0]
612 # Re-normalize fluxes (hopefully close already)
613 result = np.clip(
614 result * np.array([x[1].value for x in gaussians_linear.gaussians_free]),
615 1e-2,
616 0.99,
617 )
618 result /= np.sum(result)
619 for idx_param, param in enumerate(fluxfracs):
620 param.value = result[idx_param]
621 # fluxfracs are the fraction of the remaining flux
622 # this renormalizes to the remaining parameters
623 result /= np.sum(result[idx_param + 1 :])
625 result_full = self.modeller.fit_model(model, fitinputs=fitInputs, **kwargs)
626 fitInputs = result_full.inputs
627 results[f"{prefix}n_iter"][idx] = result_full.n_eval_func
628 results[f"{prefix}time_eval"][idx] = result_full.time_eval
629 results[f"{prefix}time_fit"][idx] = result_full.time_run
630 results[f"{prefix}chisq_reduced"][idx] = result_full.chisq_best / size
631 if config.config_fit.eval_residual:
632 results[f"{prefix}n_eval_jac"][idx] = result_full.n_eval_jac
634 for (key, param), value in zip(columns_param.items(), result_full.params_best):
635 param.value_transformed = value
636 results[key][idx] = param.value
638 time_final = time.process_time()
639 results[f"{prefix}time_full"][idx] = time_final - time_init
640 except Exception as e:
641 n_skipfail += 1
642 size = 0 if fitInputs is None else size_new
643 column = self.errors_expected.get(e.__class__, "")
644 if column:
645 row[f"{prefix}{column}"] = True
646 logger.debug(
647 "id_source=%i (idx=%i/%i) PSF fit failed with known exception: %s",
648 id_source,
649 idx,
650 n_rows,
651 e,
652 )
653 else:
654 row[f"{prefix}unknown_flag"] = True
655 logger.info(
656 "id_source=%i (idx=%i/%i) PSF fit failed with unexpected exception: %s",
657 id_source,
658 idx,
659 n_rows,
660 e,
661 exc_info=1,
662 )
663 logger_periodic.log(
664 "Fit idx=%i/%i PSFs (%i skipped/failed) in %.2f",
665 idx,
666 n_rows,
667 n_skipfail,
668 time_final - time_init_all,
669 )
671 n_unknown = np.sum(row[f"{prefix}unknown_flag"])
672 if n_unknown > 0:
673 logger.warning("%i/%i PSF fits failed with unexpected exceptions", n_unknown, n_rows)
675 return results
677 def initialize_model(
678 self,
679 model: g2f.ModelD,
680 config_data: CatalogPsfFitterConfigData,
681 limits_x: g2f.LimitsD | None = None,
682 limits_y: g2f.LimitsD | None = None,
683 ) -> None:
684 """Initialize a Model for a single source row.
686 Parameters
687 ----------
688 model
689 The model object to initialize.
690 config_data
691 The fitter config with cached data.
692 limits_x
693 Hard limits for the source's x centroid. Default (0, n_columns).
694 limits_y
695 Hard limits for the source's y centroid. Default (0, n_rows).
696 """
697 n_rows, n_cols = model.data[0].image.data.shape
698 cen_x, cen_y = n_cols / 2.0, n_rows / 2.0
699 centroids = set()
700 if limits_x is None:
701 limits_x = g2f.LimitsD(0, n_cols)
702 if limits_y is None:
703 limits_y = g2f.LimitsD(0, n_rows)
705 for component, config_comp in zip(
706 config_data.components.values(), config_data.component_configs.values()
707 ):
708 centroid = component.centroid
709 if centroid not in centroids:
710 centroid.x_param.value = cen_x
711 centroid.x_param.limits = limits_x
712 centroid.y_param.value = cen_y
713 centroid.y_param.limits = limits_y
714 centroids.add(centroid)
715 ellipse = component.ellipse
716 ellipse.size_x_param.limits = limits_x
717 ellipse.size_x = config_comp.size_x.value_initial
718 ellipse.size_y_param.limits = limits_y
719 ellipse.size_y = config_comp.size_y.value_initial
720 ellipse.rho = config_comp.rho.value_initial