================== Algorithm overview ================== Sphot is a **joint Sersic + multi-source PSF + sky** fitter that alternates three sub-problems until they stop fighting each other: 1. fit a smooth Sersic galaxy model to the image **after** point-source light has been removed, 2. fit a forest of point-source PSFs to the image **after** the Sersic galaxy has been removed, 3. estimate and subtract a residual sky from what's left. Each step is straightforward in isolation. The trick is that they share the same pixels, so getting any one of them wrong biases the other two. Sphot solves the deadlock by **iterating** them — each pass uses the cleanest estimate of the *other* components currently available — and by **calibrating the PSF kernel itself** as it goes (because the input library PSF is rarely an exact match for the data). The rest of this page walks through the structure of that iteration, from the top-level pipeline down to a single PSF photometry pass. ------------------------------------------------------------------ Top-level pipeline ================== The command-line entry point ``run_sphot`` (or its programmatic equivalent in ``sphot.run_sphot.run_sphot``) drives a galaxy through four stages: .. mermaid:: flowchart TB H[("input HDF5
(galaxy cutouts + library PSF)")]:::io H --> L["load_and_crop
per-filter cutouts, auto-crop, blur"]:::step L --> G["Galaxy
cd[filter] = CutoutData"]:::data G --> B["run_basefit
base filter only"]:::heavy B --> P["base_params + psf_table
(Sersic shape + source list)"]:::data P --> S["run_scalefit
parallel, all other filters"]:::heavy S --> A["run_aperphot
(optional)"]:::step A --> O[("output sphot.h5
+ results CSV")]:::io click L "../api/utils.html#sphot.utils.load_and_crop" "load_and_crop in sphot.utils" click B "#run-basefit" "Iterative Sersic + PSF fit on the base filter" click S "#run-scalefit" "Pin Sersic shape, fit per-filter scale + PSF photometry" click A "#run-aperphot" "Aperture photometry on psf_sub_data" classDef io fill:#1f2a44,stroke:#1f2a44,color:#fff,rx:8,ry:8; classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef step fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef heavy fill:#ffe8c2,stroke:#c97a13,color:#5a3604,rx:8,ry:8,stroke-width:2px; The two heavy boxes — ``run_basefit`` and ``run_scalefit`` — share most of their structure. The base fit is run once and produces both a *Sersic shape* (``sersic_params``) and a *source catalogue* (``psf_table``). The scale fit then runs in parallel across the remaining filters with the Sersic shape **pinned** to the base solution, fitting only an overall brightness scale (and optionally a small re-fit) plus per-filter PSF photometry. .. note:: For very blurry IR filters where source detection is unreliable, the variant :func:`sphot.core.run_scalefit_forced` skips detection entirely and force-extracts flux at the base filter's source positions. See :ref:`forced-scalefit` below. ------------------------------------------------------------------ .. _run-basefit: The base fit (``run_basefit``) ============================== This is where almost all of the work happens. It loads the base filter, builds an initial Sersic model and PSF photometry fitter, runs a primer fit on the raw data, then enters the **main loop** — alternating Sersic, PSF, sky, and (optionally) PSF-kernel calibration until the Sersic parameters stop moving. .. container:: sphot-grid sphot-grid-3 .. container:: sphot-col 1. Init + primer fit .. mermaid:: flowchart TB I["init
perform_bkg_stats · blur_psf
seed dao_fwhm_factor
build fitter_1, fitter_2, fitter_psf"]:::init I --> S0["fitter_1.fit
Sersic on RAW data
fit_to = data"]:::sersic S0 --> R0["remove_sky · sersic"]:::sky R0 --> P0["fitter_psf.fit
fit_to = sersic_residual"]:::psf P0 --> Q0["remove_sky · psf"]:::sky Q0 --> C0["recalibrate_psf
(if psf-calib.in_mainloop)"]:::cal C0 --> CONT(["→ continues in column 2:
Main loop"]):::cont click P0 "#fitter-psf-fit" "PSF photometry sub-step" click C0 "#recalibrate-psf" "Kernel recalibration sub-step" classDef init fill:#f4ecff,stroke:#6f4cc2,color:#2c1a55,rx:8,ry:8; classDef sersic fill:#fff1d6,stroke:#c97a13,color:#5a3604,rx:8,ry:8; classDef psf fill:#e1efff,stroke:#2b69b3,color:#0d2748,rx:8,ry:8; classDef sky fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef cal fill:#ffe1ec,stroke:#b53274,color:#451026,rx:8,ry:8; classDef cont fill:#444,stroke:#222,color:#fff,rx:8,ry:8; .. container:: sphot-col 2. Main loop .. mermaid:: flowchart TB PREV(["← from column 1:
Init + primer fit"]):::cont PREV --> LOOP{{"Main loop
i = 0 to N_mainloop_iter"}} LOOP --> BR{"iter 0 and
use_dual_annealing?"} BR -->|yes| DA["fitter_2.fit
method = dual_annealing
fit_to = psf_sub_data"]:::sersic BR -->|no| NM["fitter_2.fit
method = iterative_NM
fit_to = psf_sub_data"]:::sersic DA --> R1["remove_sky · sersic"]:::sky NM --> R1 R1 --> P1["fitter_psf.fit
threshold ladder + final refit"]:::psf P1 --> Q1["remove_sky · psf"]:::sky Q1 --> C1["recalibrate_psf
(if psf-calib.in_mainloop)"]:::cal C1 --> CONV{"sersic params change
under atol for
patience iters?"} CONV -->|"keep going"| LOOP CONV -->|"converged or i = N"| NEXT(["→ continues in column 3:
Polish + done"]):::cont click P1 "#fitter-psf-fit" "PSF photometry sub-step" click C1 "#recalibrate-psf" "Kernel recalibration sub-step" click DA "../api/fitting.html#sphot.fitting.ModelFitter.fit" "ModelFitter.fit (method='dual_annealing')" click NM "../api/fitting.html#sphot.fitting.ModelFitter.fit" "ModelFitter.fit (method='iterative_NM')" classDef sersic fill:#fff1d6,stroke:#c97a13,color:#5a3604,rx:8,ry:8; classDef psf fill:#e1efff,stroke:#2b69b3,color:#0d2748,rx:8,ry:8; classDef sky fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef cal fill:#ffe1ec,stroke:#b53274,color:#451026,rx:8,ry:8; classDef cont fill:#444,stroke:#222,color:#fff,rx:8,ry:8; .. container:: sphot-col 3. Polish + done .. mermaid:: flowchart TB PREV(["← from column 2:
Main loop"]):::cont PREV --> EXIT["exit loop"] EXIT --> POL{"use_final_polish?"} POL -->|yes| LB["fitter_2.fit
method = lbfgsb_polish"]:::sersic LB --> RF["remove_sky · sersic"]:::sky RF --> PF["fitter_psf.fit
final pass"]:::psf PF --> QF["remove_sky · psf"]:::sky POL -->|no| QF QF --> DONE(["cd.sersic_params
cd.psf_table
cd.psf_sub_data
cd.psf_modelimg
cd.residual_masked"]):::data click PF "#fitter-psf-fit" "PSF photometry sub-step" click LB "../api/fitting.html#sphot.fitting.ModelFitter.fit" "ModelFitter.fit (method='lbfgsb_polish')" classDef sersic fill:#fff1d6,stroke:#c97a13,color:#5a3604,rx:8,ry:8; classDef psf fill:#e1efff,stroke:#2b69b3,color:#0d2748,rx:8,ry:8; classDef sky fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef cont fill:#444,stroke:#222,color:#fff,rx:8,ry:8; Why the structure looks like this --------------------------------- * **The first Sersic fit uses raw data.** Before any PSF photometry has run, there is no ``psf_sub_data`` to fit against — and in any case, the dominant signal at this stage is the galaxy itself. ``fitter_1`` is the *simple* Sersic model and is enough to seed everything that follows. * **Iter 0 of the main loop uses dual annealing.** By then, ``psf_sub_data`` is clean enough that the chi² landscape has only a handful of basins, and global escape is cheap. Subsequent iterations use ``iterative_NM`` because Sersic params are already in the right basin and we just need them to converge. (Toggle: ``[core].use_dual_annealing``.) * **Sky is removed twice per iteration.** The first call subtracts a sky that best fits ``residual_masked`` and applies it to ``sersic_residual``; the second applies a sky to ``psf_sub_data``. Decoupling the two prevents the PSF-fit residual from biasing the Sersic-fit sky and vice versa. * **Kernel recalibration runs at the end of each iteration**, never in the middle. After every recal, ``cd.sersic_modelimg`` is *re-rendered* (not re-fit) against the new effective PSF so downstream code sees a self-consistent state. Toggle: ``[psf-calib].in_mainloop`` — defaults to ``false`` in the library default config and ``true`` in the test config. * **The L-BFGS-B polish is opt-in** and runs *outside* the main loop. It buys the last few percent of chi² that ``iterative_NM`` and ``dual_annealing`` leave on the table. The polish is followed by one more PSF photometry + sky pass so the saved ``psf_modelimg`` matches the polished Sersic. Convergence and early-exit -------------------------- Three knobs in ``[core]`` control when the loop stops: * ``mainloop_convergence_atol`` — the L∞ tolerance on standardized Sersic parameter changes between iterations. * ``mainloop_convergence_patience`` — how many *consecutive* within-tolerance iterations are required before bailing out. * ``mainloop_min_iter`` — a hard floor on the number of iterations regardless of convergence. Together these make early-exit conservative: a single quiet iteration is never enough. References ---------- * :func:`sphot.core.run_basefit` — the function this section describes. * :class:`sphot.fitting.ModelFitter` — Sersic fitter; see :doc:`/api/fitting` for the available ``method`` values and what they do. * :doc:`/rst/config` — every knob mentioned above is documented under ``[core]`` and ``[psf-calib]``. ------------------------------------------------------------------ .. _run-scalefit: The scale fit (``run_scalefit``) ================================ Once the base fit has produced a Sersic shape, the *shape* is held fixed and only an overall brightness scale (plus a small free offset) is fit per filter. This is much cheaper than a full Sersic re-fit and avoids the band ambiguities that plague free joint fits. .. mermaid:: flowchart TB IN[("base_params
(from run_basefit)")]:::data IN --> SI["init
perform_bkg_stats · blur_psf · seed dao_fwhm
build fitter_scale (+ optional fitter_2), fitter_psf"]:::init SI --> S0["fitter_scale.fit
fit_to = data"]:::sersic S0 --> X0["remove_sky · sersic →
fitter_psf.fit · remove_sky · psf →
recalibrate_psf"]:::cycle X0 --> AR{"allow_refit?"} AR -->|yes| RE["fitter_2.fit (free Sersic refit)
+ remove_sky + fitter_psf.fit + remove_sky"]:::sersic AR -->|no| LOOP{{"Main loop
i = 0 to N_mainloop_iter"}} RE --> LOOP LOOP --> ST["fitter_scale.fit (or fitter_2 if allow_refit)
fit_to = psf_sub_data"]:::sersic ST --> CYC["remove_sky · sersic →
fitter_psf.fit · remove_sky · psf →
recalibrate_psf"]:::cycle CYC --> CONV{"converged?"} CONV -->|no| LOOP CONV -->|yes| EXIT["exit loop"] EXIT --> CLN["cleanup: remove_sky · sersic →
fitter_psf.fit · remove_sky · psf
(if calibrated in-loop)"]:::cycle CLN --> DONE(["cd.sersic_params (scaled)
cd.psf_table
cd.psf_sub_data"]):::data click ST "../api/fitting.html#sphot.fitting.ModelScaleFitter" "ModelScaleFitter — pins Sersic shape, fits scale + small offset" click RE "../api/fitting.html#sphot.fitting.ModelFitter" "ModelFitter — free Sersic re-fit, used only if core.allow_refit=true" classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef init fill:#f4ecff,stroke:#6f4cc2,color:#2c1a55,rx:8,ry:8; classDef sersic fill:#fff1d6,stroke:#c97a13,color:#5a3604,rx:8,ry:8; classDef cycle fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; There is no ``dual_annealing`` or L-BFGS-B polish in scalefit: the fixed-shape problem is convex enough in ``ModelScaleFitter``'s reduced parameter space (typically 1–3 free params) that ``iterative_NM`` finds the optimum directly. In ``run_sphot`` (the CLI orchestrator), all non-base filters are scalefit **in parallel** via :func:`sphot.parallel.parallel_scalefit`, with each worker writing its progress to a per-filter log and merging the results back into the shared HDF5 at the end. References ---------- * :func:`sphot.core.run_scalefit` — the loop above. * :class:`sphot.fitting.ModelScaleFitter` — the pinned-shape fitter. * :func:`sphot.parallel.parallel_scalefit` — the parallel driver. ------------------------------------------------------------------ .. _forced-scalefit: Forced-position scalefit (``run_scalefit_forced``) ================================================== For filters where DAO source detection is unreliable (typically wide IR bands where the PSF is too blurry to resolve crowded fields), this variant **skips detection entirely** and force-extracts the flux at every quality-passing position from the base filter's ``psf_table``: .. mermaid:: flowchart LR BPHOT[("base_filter.psf_table")]:::data BPHOT --> FLT["filter rows by photutils flag bits
(2|4|32|64|128|256) · finite + flux>0"]:::step FLT --> POS["base_x, base_y, base_f"]:::data POS --> LOOP["main loop:
fitter_scale.fit →
remove_sky · sersic →
run_forced_photometry_on_cutout(base_x, base_y, flux_init=base_f) →
remove_sky · psf →
(optional) recalibrate_psf"]:::cycle LOOP --> DONE(["cd.psf_table (forced positions, refit fluxes)"]):::data click LOOP "../api/psf.html#sphot.psf.run_forced_photometry_on_cutout" "Forced PSF photometry: pinned positions, NNLS-style flux solve" classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef step fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef cycle fill:#e1efff,stroke:#2b69b3,color:#0d2748,rx:8,ry:8; This bypasses the threshold ladder and the leftover-detection loop; PSF positions are completely pinned. Use it when the goal is **consistency across filters**, not maximum sensitivity within a single filter. References ---------- * :func:`sphot.core.run_scalefit_forced` * :func:`sphot.psf.run_forced_photometry_on_cutout` ------------------------------------------------------------------ .. _fitter-psf-fit: PSF photometry sub-step (``fitter_psf.fit``) ============================================ This is the inner workhorse called from every Sersic ↔ PSF iteration in both basefit and scalefit. It implements **threshold-ladder PSF photometry** followed by an optional **simultaneous refit** that re-solves all source fluxes at once (and optionally hunts for leftover sources missed by the ladder). .. container:: sphot-grid sphot-grid-2 .. container:: sphot-col 1. Threshold ladder .. mermaid:: flowchart TB IN[("inputs:
cd.sersic_residual · cd.psf · cd.bkg_std")]:::data IN --> SETUP["build psf_model from cd.psf
build threshold list (geomspace
th_max → th_min, th_iter steps)
build galaxy-centre mask"]:::step SETUP --> LADDER{{"Threshold ladder
for th in threshold_list:"}} LADDER --> DAO["DAOStarFinder detect at
threshold·bkg_std (matched filter
via dao_fwhm_factor·psf_sigma)"]:::detect DAO --> EMPTY{"any detections?"} EMPTY -->|"no, N consec"| LSTOP["early-stop ladder
(th_max_consec_empty)"]:::detect EMPTY -->|yes| FIT["photutils PSFPhotometry (LM)
local-bg, fit_shape,
finder_kwargs"]:::detect FIT --> DEDUP["dedup vs accumulated sources
(ladder_dedup_psfsigma)"]:::detect DEDUP --> ACC["append to phot_result
subtract psf_modelimg from resid"]:::detect ACC --> BKR["re-estimate bkg_std
(bkg_floor_factor floor)"]:::detect BKR --> LADDER LSTOP --> NEXT(["→ continues in column 2:
Final refit + write"]):::cont LADDER -.->|done| NEXT click LADDER "../api/psf.html#sphot.psf.iterative_psf_fitting" "iterative_psf_fitting wraps the ladder" click DAO "../api/psf.html#sphot.psf.do_psf_photometry" "do_psf_photometry: one ladder pass" classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef step fill:#f4ecff,stroke:#6f4cc2,color:#2c1a55,rx:8,ry:8; classDef detect fill:#e1efff,stroke:#2b69b3,color:#0d2748,rx:8,ry:8; classDef cont fill:#444,stroke:#222,color:#fff,rx:8,ry:8; .. container:: sphot-col 2. Final refit + write .. mermaid:: flowchart TB PREV(["← from column 1:
Threshold ladder"]):::cont PREV --> REFIT{"final_refit_method?"} REFIT -->|"'iterative'"| JOINT["_final_joint_refit
simultaneous LM with
leftover detection
(repeat until residual MAD
stops improving)"]:::refit REFIT -->|"'nnls'"| NNLS["_final_nnls_refit
Cholesky-Gram NNLS on
quality-passing rows
+ find_peaks leftover loop"]:::refit REFIT -->|"'none'"| SKIP["no refit"]:::refit JOINT --> OUT[("phot_table, residual")]:::data NNLS --> OUT SKIP --> OUT OUT --> WRITE["PSFFitter.fit writes onto cd:
· psf_table
· psf_modelimg = data − resid
· psf_sub_data
· residual
· residual_masked"]:::write click NNLS "#nnls-refit" "How NNLS refit results flow back to cutoutdata" click JOINT "#nnls-refit" "How the iterative joint refit flows back to cutoutdata" classDef data fill:#eef3fb,stroke:#5b7fb8,color:#1f2a44,rx:8,ry:8; classDef refit fill:#fff1d6,stroke:#c97a13,color:#5a3604,rx:8,ry:8; classDef write fill:#e8f3ec,stroke:#3b8a5a,color:#143824,rx:8,ry:8; classDef cont fill:#444,stroke:#222,color:#fff,rx:8,ry:8; Why the ladder -------------- A single ``DAOStarFinder.find_peaks`` + ``PSFPhotometry.fit`` call would, in a crowded field, hand the LM optimizer hundreds of overlapping sources at once. LM is a local optimizer: with that many highly correlated parameters, it routinely settles into compensating-pair pathologies (one source goes hugely positive, a neighbour absorbs it as a large negative flux), leaves residuals that look fine but a ``psf_modelimg`` that biases the next Sersic fit. The ladder sidesteps the failure mode by extracting sources **brightest first**. At threshold ``th_max``, only the brightest dozen sources survive; LM fits them cleanly because they don't overlap much. Their model is subtracted from the residual, and the next pass operates at a slightly lower threshold on a cleaner image, and so on. Each pass deduplicates against everything found by earlier passes (``ladder_dedup_psfsigma``). ``[psf].bkg_refit_per_iteration`` re-estimates ``bkg_std`` after each successful pass — important because subtracting bright sources changes the robust noise level the lower thresholds key off. .. _nnls-refit: The final refit --------------- After the ladder finishes, two things may have gone wrong: 1. *Compensating pairs may have crept in anyway*, because earlier passes committed to a subtraction that later passes can't change. 2. *The ladder may have missed sources* that DAO's matched-filter smoothing washed below threshold (typically very compact sources whose width doesn't match ``dao_fwhm_factor·psf_sigma``). The final refit tackles both: * ``final_refit_method = 'iterative'`` (default in the library config) runs ``_final_joint_refit``: a simultaneous LM fit on every accumulated source, followed by ``find_peaks`` on the residual to pick up leftovers, repeated until residual MAD stops improving. * ``final_refit_method = 'nnls'`` (default in the test config) runs ``_final_nnls_refit``: a Cholesky-Gram non-negative least-squares solve on the design matrix whose columns are unit-flux PSF renderings at each source's pinned position. NNLS hard-bans negative fluxes, eliminating the compensating-pair pathology by construction. The same ``find_peaks`` leftover loop appends new columns and re-solves. * ``final_refit_method = 'none'`` skips this stage entirely. Either way, the new ``(phot_table, residual)`` is what ``iterative_psf_fitting`` returns. ``PSFFitter.fit`` then derives ``psf_modelimg`` by subtraction and writes ``psf_table``, ``psf_modelimg``, ``psf_sub_data``, ``residual``, ``residual_masked`` onto the cutoutdata. The next Sersic iteration reads ``psf_sub_data``; the kernel recalibrator reads ``psf_table``. Quality cuts ------------ Both refit branches filter rows through :func:`sphot.psf.filter_psfphot_results` before exposing them as the final ``psf_table``. The cuts (all in ``[psf]``) are documented in :doc:`/rst/config`: * ``cuts_pos_err_max`` — drop fits whose centroid uncertainty exceeds N px. * ``cuts_res_cen_sigma_clip`` — drop sources whose central residual is more than σ above the typical residual. * ``cuts_cfit_sigma_clip`` — drop fits whose χ²/dof is an outlier. * ``cuts_chi2dof_median_factor`` / ``cuts_pos_diff_median_factor`` — population-level outlier cuts. * ``cuts_flux_SNR_min`` — drop everything below a minimum flux/error. References ---------- * :func:`sphot.psf.iterative_psf_fitting` — wraps the ladder + final refit. * :func:`sphot.psf.do_psf_photometry` — one ladder pass. * :func:`sphot.psf._final_joint_refit`, :func:`sphot.psf._final_nnls_refit` — the two final-refit branches. * :class:`sphot.psf.PSFFitter` — the cutoutdata-level wrapper. ------------------------------------------------------------------ .. _recalibrate-psf: Kernel recalibration (``_maybe_recalibrate_psf``) ================================================= The library PSF supplied by the user is rarely a pixel-perfect match for the data: telescope focus drifts, dither sub-pixel sampling, and detector charge-diffusion all leave a residual *effective-PSF blur* that survives PSFPhotometry's per-source LM fit. Sphot's solution is to **fit a kernel** ``K`` such that ``cd.psf = library_PSF ⊛ K`` reproduces the bright-source residuals best, then **convolve** that kernel into the working PSF for the next iteration. When this runs (only if ``[psf-calib].in_mainloop = true``): 1. After each PSF photometry pass, the calibrator picks the ``K`` brightest anchors from ``cd.psf_table`` (``[psf-calib].kernel_anchor_K``). 2. It builds a multi-source NNLS design (``kernel_fit_objective='multi_source'``) or single-source LM (``='single_source'``) over small stamps around each anchor and fits the kernel parameters of the chosen ``kernel_family`` (``'gaussian'`` / ``'moffat'`` / ``'drizzle'``). 3. The optional FWHM scan (``fwhm_method``, ``scan_method``) updates ``cd.dao_fwhm_factor`` so the next ladder's matched filter matches the newly-blurred PSF. 4. ``cd.psf`` is replaced; ``cd.sersic_modelimg`` is **re-rendered** (not re-fit) so it stays consistent with the new effective PSF; the next Sersic / PSF iteration picks up the updated state automatically. The default library config has ``in_mainloop = false`` (kernel calibration is opt-in). The test config has it on — recommended for any data where the library PSF is known to be approximate. References ---------- * :func:`sphot.calibrate_psf.calibrate_psf_step` — the recalibrator. * :doc:`/api/calibrate_psf` * :doc:`/rst/config` — every ``[psf-calib]`` knob. ------------------------------------------------------------------ .. _run-aperphot: Aperture photometry (``run_aperphot``) ====================================== The optional fourth stage runs Petrosian aperture photometry on ``cd.psf_sub_data`` (the PSF-cleaned image) to produce final integrated fluxes per filter. * Aperture geometry is determined from a base-filter isophote fit (``[aperture].isophot_base_filter`` / ``fit_isophot_to``) and then applied unchanged across all filters. * Sky is measured from ``cd.residual_masked`` (the post-PSF, post-Sersic image) — see ``[aperture].measure_sky_on``. * Inner aperture mask radius is set by ``[aperture].center_mask``. The output is a single CSV alongside the sphot HDF5 with one row per filter and per source. References ---------- * :func:`sphot.core.run_aperphot` * :func:`sphot.aperture.aperture_routine` * :doc:`/api/aperture` ------------------------------------------------------------------ .. sidebar:: Why the alternation works Each sub-step would be unbiased if the *other* two were perfect. Sersic needs PSF light gone; PSF photometry needs the galaxy core gone; sky needs both gone. Iterating works because each pass operates on the cleanest version of the *other* components currently available, and the non-modelled component each step injects (Sersic-fit residual into PSF data, PSF-fit residual into Sersic data) shrinks geometrically. Empirically, five to ten iterations with a strict convergence atol is enough on the targets sphot was built for. Putting it all together ======================= A typical end-to-end run on a multi-band cutout looks like: .. code-block:: bash run_sphot mygalaxy.h5 --out_folder=results/ That single command performs: 1. Load + per-filter cutouts + auto-crop. 2. ``run_basefit`` on the base filter — the bulk of the runtime. 3. ``run_scalefit`` in parallel over every other filter — typically much faster because the Sersic shape is fixed. 4. ``run_aperphot`` (only if ``--photometry`` is passed) — Petrosian aperture photometry on the cleaned images. To customize behaviour, drop a ``sphot_config.toml`` next to the data file (see :doc:`/rst/config` for the full reference) — every threshold, ladder step, fitter method, and convergence tolerance is exposed there.