Skip to content

Rapthor source-finding contract

This inventory records the behaviour Rapthor consumes from its source-finding step, audited at the pinned revisions:

Component Revision Role
Rapthor main at aff26023 (17 October 2026) Schedules the step; master is the CWL/Toil release line and not a target
LSMTool master at 156ae6a (v1.9.0 plus 27 commits) Runs the finder and filters the sky model
PyBDSF master at c70103b The binding scientific and performance reference

The pins move forward only at the checkpoints the plan names. The page fixes what must be tested without requiring Hebog to copy PyBDSF internals, and a compatibility observation here is not a scientific endorsement.

Invocation boundary

Rapthor's main schedules one Prefect filter_skymodel task per image sector after WSClean has written its image and source-list products. The task runs on Rapthor's Dask task runner (local_dask, or external_dask on a cluster whose workers run one task each with --nthreads 1), but does no science itself: it starts a fresh Python interpreter (python -m rapthor.execution.image.skymodel_filter_cli) with OMP_NUM_THREADS, OPENBLAS_NUM_THREADS, MKL_NUM_THREADS and BLIS_NUM_THREADS set to 1, so that native thread pools do not multiply the finder's own parallelism. That interpreter calls LSMTool's filter_skymodel(..., source_finder=..., ncores=...), which dispatches through its KNOWN_SOURCE_FINDERS registry: bdsf, plus sofia when its optional extra is installed. ncores is the parset's filter_skymodel_ncores (default 15; 0 means max_threads), and PyBDSF uses it as its count of fitting processes. No Dask client is in reach of the finder. The task returns serializable file records, and image diagnostics run as a separate dependent task.

A registry backend, the route this step offers today, takes the bdsf backend's arguments: the two images, the two input sky models, the two output sky-model paths, vertices_file, beam_ms, input_bright_skymodel, the thresholds and RMS options below, keep_mask, output_catalog, output_flat_noise_rms, output_true_rms and ncores, returning the source count. (The registry calls every backend with those arguments; sofia's function takes fewer and could not be called that way at 156ae6a.) Rapthor's Hebog branch instead calls Hebog from the native task; see How Hebog runs inside Rapthor.

Input Meaning
Flat-noise image The primary-beam-uncorrected FITS image, with comparatively flat noise
True-sky image The primary-beam-corrected FITS image used for intrinsic fluxes; not literal truth
True-sky sky model WSClean component list with true-sky fluxes; optional when absent
Apparent-sky sky model WSClean component list with beam-attenuated fluxes; optional when absent
Bright true-sky sky model Optional peeled bright components added back before filtering
Sector vertices NumPy polygon file defining the valid imaging sector
Beam Measurement Sets Observation paths used to select a representative beam attenuation
Configuration Thresholds, RMS boxes, adaptive threshold, mask-filter flag, source finder and core count

Rapthor passes strategy thresholds explicitly, so the helper fallback is not the production profile:

Context Detection threshold Island threshold
Normal imaging, later self-calibration and the retained Prefect demo 5.0 sigma 3.0 sigma
Initial/normalization and early self-calibration cycles 5.0 sigma 4.0 sigma
filter_image_skymodel helper fallback 7.5 sigma 5.0 sigma

What LSMTool asks of PyBDSF, and what Hebog does instead

LSMTool's bdsf backend calls bdsf.process_image on the true-sky image (the flat-noise image when no beam Measurement Set is given) with the options below, and a second time on the flat-noise image with the same options and stop_at="isl". Rapthor passes the thresholds, ncores, keep_mask=True and the RMS options at its own defaults, which equal LSMTool's; the rest are LSMTool's.

PyBDSF behaviour Value Hebog Status
mean_map "zero": no background is subtracted SourceFinderConfig.background: estimated (default) subtracts Hebog's spatial background; zero subtracts nothing and keeps the RMS estimate, as PyBDSF does Implemented as a setting. Against injected truth zero is worse; against pinned master it gives closer fluxes and lower SDC1 completeness. Rapthor runs zero with the continuum profile, which passes the truth-based gate under either background and covers more of the true emission with this one
rms_map, rms_box On, (150, 50) The coarse RMS grid is 150-pixel windows on a 50-pixel step Implemented
adaptive_rms_box, rms_box_bright, adaptive_thresh On, (35, 7), 75.0 Bright-region refinement on 35-pixel windows on a 7-pixel step around candidates at 75σ or more, plus local-noise refinement on the same windows everywhere Implemented, with two bounded differences: the continuum RMS tail and a clipped RMS 1.6 to 3.9% low on noise alone
thresh, thresh_pix, thresh_isl "hard"; Rapthor's 5/3, 5/4 or the helper's 7.5/5 SourceFinderConfig's detection and island thresholds, executed exactly Implemented
minpix_isl Unset, so max(6, int(beam area in pixels / 3)) SourceFinderConfig.minimum_island_pixels, which the adapter sets by PyBDSF's rule from the true-sky image's beam Implemented. Hebog applies one configuration to both images, so a flat-noise image whose beam gives another minimum is refused; WSClean writes both from one restoring beam
atrous_do, atrous_jmax On, 3 scales Three residual multiscale scales in continuum; compact omits them Implemented as Hebog's own multiscale association, not PyBDSF's wavelet decomposition
Catalogue write_catalog(format="fits", catalog_type="srl", force_output=True) catalogue.fits; the adapter writes the eight-column view Rapthor reads (Rapthor catalogue view) as <root>.source_catalog.fits Implemented
True-sky RMS export_image(img_type="rms") rms.fits, copied to <root>.true_sky_rms.fits; a two-axis float64 image, which Rapthor's FITSImage reads Implemented
Island mask export_image(img_type="island_mask") as <image>.mask.fits source-mask.fits, the publication support, which the adapter writes as <true-sky image name>.mask.fits in PyBDSF's layout: float32 ones and zeros on two celestial axes, then a Stokes and a frequency axis of one pixel each. LSMTool's polygon clipping indexes data[0, 0] and its grouping reads the mask through casacore, so a two-axis mask fails there Implemented, with a bounded difference: about 92% of PyBDSF's island pixels. LSMTool groups components into patches by the mask's islands, so the mask's connectivity, not only its coverage, reaches the sky model: under compact/zero, 96.9% of the components both masks keep are grouped alike, against 99.9% for released PyBDSF
Flat-noise RMS A second pass with stop_at="isl", RMS only The request's flat_noise_image_path: the run estimates that image's background and RMS with the same configuration and publishes flat-noise-rms.fits, which the adapter copies to <root>.flat_noise_rms.fits Implemented; the two branches run one after the other until the executor can run them together
Source count img.nsrc SourceFinderResult.source_count Implemented
All pixels blanked RuntimeError("All pixels in the image are blanked.") from collapse.py find_sources publishes an empty catalogue, an all-NaN RMS and a zero mask; the adapter then raises RapthorBlankedImageError, a RuntimeError carrying PyBDSF's message, for an image of either branch with no pixel to measure. Hebog also counts a pixel inside a 3×3 square of one value as blank, as imagers fill blanked regions, so an image of one value is blanked too Implemented; Rapthor's existing handler writes its empty products
Reference frequency The frequency axis at the image plane, then RESTFREQ, then a FREQ keyword The frequency axis at the image plane, then RESTFRQ, then RESTFREQ; a supplied value when the header has none Implemented. Hebog also reads the standard RESTFRQ, which PyBDSF ignores, and refuses rather than reads a non-standard FREQ keyword
ncores Fitting processes The cores the Dask worker declares, used by the thread executor or by each Dask task's threads (see below) Planned with the distributed path

RapthorCompatibilityConfig holds what the finder honours, under Rapthor's names: threshpix and threshisl, and Hebog's profile and background, which default to Rapthor's continuum and zero. The minimum island follows PyBDSF's rule, LSMTool's filter_by_mask stays with Rapthor's call to filter_sources, and the other options are fixed by Hebog's reviewed science.

Sector image sizes

Rapthor sizes each sector from its polygon at cellsize_arcsec (default 1.5″). With no grid width set, one sector covers 1.7 times the primary-beam FWHM, which Rapthor computes as 1.1 λ/D over the sine of the mean elevation from the first antenna's dish diameter.

Configuration Sector size Source
Default, LOFAR HBA near 144 MHz about 17,000 to 20,000 pixels a side, depending on elevation Rapthor main's sizing rule
Rapthor's Prefect demonstration strategy (1.25°, 1.5″) 3,000 × 3,000 examples/prefect_demo_strategy.py and its parset
The ical benchmark runs on the development cluster (10° at 2″) 18,000 × 18,000, and a 17,060 × 20,428 full-field image Rapthor logs of September 2026

The demonstration sector's restoring beam is about 45″ by 43″, 30 pixels at 1.5″, in every imaging cycle of the local demonstration runs of 15 and 18 September 2026 and in the representative sector (44.8″): the demonstration's data generator places synthetic antennas within 2,500 wavelengths of the centre, so its longest baselines, about 5,000 wavelengths, resolve about 41″. The public finder refuses a beam wider than 10 pixels, so the adapter refuses that sector with UnsupportedSourceFinderConfigurationError and the branch's task falls back to PyBDSF; scaling the meshes with the beam is deferred. Every real Rapthor image in the archived cluster runs of September and October 2026 (P113+24, the ical benchmark, NGC6338 and AA2) is at most about 7 pixels: its sector images have 1.5″ cells and 4 to 10″ beams, and its normalization images 5.9″ cells and 20 to 36″ beams.

PyBDSF's filter_skymodel command took between 298 and 2,955 s per 18,000² sector in one three-node benchmark run (16 and 17 September 2026). Hebog's 22,500-pixel envelope covers the default and benchmarked sector sizes.

Materialised products

Rapthor requires the catalogue, RMS images, filtered models and diagnostics for every successful sector; the mask is returned only when its file exists.

Product Current suffix Downstream use
Filtered true-sky model .true_sky.txt Calibration and true-flux sky-model state
Filtered apparent-sky model .apparent_sky.txt Beam-attenuated prediction/model state
True-sky RMS image .true_sky_rms.fits RMS statistics and true-sky dynamic range
Flat-noise RMS image .flat_noise_rms.fits RMS statistics, local dynamic range and facet diagnostics
Source catalogue .source_catalog.fits Source count, photometry, astrometry and preview selection
Island mask <basename of the true-sky image>.mask.fits, in the working directory Sky-model membership and patch grouping; supplementary output
Diagnostics .image_diagnostics.json Starts with nsources, then receives image-quality metrics

The filtered model image is a later Rapthor product rebuilt from the filtered apparent-sky model, not a source-finder output.

Catalogue compatibility fields

Rapthor's diagnostic code reads these source-list columns directly, through Astropy; it converts Source_id, RA, DEC and a flux column to a minimal makesourcedb text model before LSMTool loads it, so LSMTool never reads the FITS table. Per-channel columns used by later flux normalization are outside the MFS-only contract.

Column Meaning and use
Source_id Stable identifier used when converting rows to a comparison sky model
RA, DEC Position used for matching, beam-radius cuts and astrometry
Isl_Total_flux Island-integrated flux used by the default astrometry comparison
Total_flux Source flux used for photometry and flux-normalization consistency. In continuum it is the sum of the source's fitted Gaussian components, PyBDSF's definition, falling back to the signed aperture when no fit was admitted; see public products
DC_Maj Deconvolved major axis in degrees; sources at or above 10 arcsec are excluded from compact-source checks
E_RA, E_DEC Position uncertainties in degrees, both great-circle angles; sources at or above 2 arcsec are excluded from astrometry checks. PyBDSF publishes E_RA the same way, so it is not divided by cos(dec), which would tighten the cut by 1/cos(dec) and drop sources PyBDSF keeps

The Rapthor catalogue view freezes these as an exact eight-column table: zero-based 32-bit source numbering, 64-bit values, degrees for position, size and error, Jy for flux, FITS NaN for an unavailable error, zero DC_Maj only for a reviewed unresolved source, a major-axis-only source's positive major FWHM in DC_Maj, and a structurally complete zero-row table for an empty result.

Scientific flow

With beam Measurement Sets, a full PyBDSF pass on the true-sky image detects islands, measures sources, writes the source-list catalogue and true-sky RMS and exports the island mask; a second pass on the flat-noise image stops after island construction and writes only its RMS. Without beam data the flat-noise image supplies the first pass and its RMS is reused. The mask is clipped to the sector polygon; with mask filtering enabled only sky-model components inside detected emission remain, surviving true-sky components are grouped into patches by island, and apparent-sky membership is matched by component Name with the patch grouping transferred. Rapthor consumes the resulting source rows and island mask, never PyBDSF's live objects.

Empty and failure behaviour

Two empty paths must be represented explicitly in tests:

  • Processing succeeds but finds no islands: LSMTool writes a dummy central sky-model component with negligible flux and reports zero sources. The row is a serialization workaround, not a source.
  • PyBDSF raises All pixels in the image are blanked: Rapthor writes header-only sky models and an empty catalogue with all required columns, copies the two input images to the expected RMS paths and records {"nsources": 0}. The copied pixels are placeholders, not RMS estimates.

The reviewed policy keeps any required dummy component at the adapter boundary and represents unavailable RMS explicitly in the scientific core. With Hebog, the first path stays Rapthor's: when the adapter reports no island (island_count 0), Rapthor's task writes LSMTool's dummy component rather than calling filter_sources. The branch also preserves this dummy path when neither a true-sky nor a bright sky model is present. For the second, the adapter raises RapthorBlankedImageError, which Rapthor's step handles as it handles PyBDSF's error.

The adapter fails in ways Rapthor can act on:

  • Hebog's refusals are typed SourceFinderErrors: an unreadable or unsupported image, an image above Hebog's size envelope, images whose beams give different minimum islands, or another run's bundle at <root>.hebog. Before the analysis starts the adapter removes the four files Rapthor reads, and a failure while writing them removes those it wrote, so a fallback to PyBDSF never finds Hebog's files beside its own.
  • A task that declares more memory than the executor admits raises ExecutorAdmissionError before it is submitted, and the analysis stops as on any other failure; Rapthor's Hebog branch falls back on it too.
  • A retried task with the same request and run ID reuses Hebog's finished bundle without analysing the images again, and writes Rapthor's files from it once more, so a mask LSMTool has already clipped is replaced.
  • An executor retries a lost task, and the products equal those of a run that lost none.
  • When the caller's deadline raises in the thread running the analysis, as a Prefect task timeout does, every task the analysis started finishes before the error propagates, none starts afterwards, and its staging directory is removed.

Missing inputs and unexpected errors fail the task rather than producing a silent empty result.

How Hebog runs inside Rapthor

Rapthor owns the Prefect/Dask graph, retries, resource admission and the restartable file lifecycle, and Hebog never starts a private cluster. For source_finder = hebog, Rapthor replaces the subprocess call with a native Prefect task that runs Hebog in-process on its Dask worker, then LSMTool's filter_sources; the PyBDSF subprocess stays for bdsf and as the fallback (ADR-004).

Rapthor's Hebog branch implements today's one-single-threaded-worker-per-node path. Its real Prefect task, native Hebog and LSMTool filtering, fallback and dual subprocess runs pass in the pinned Linux integration container. Upstream publication and merge remain pending. The distributed layout and resource annotations described below are later work.

On the current layout the task caps its ThreadExecutor by the filter core request, sector max_threads and configured cpus_per_task. It limits native thread pools to one and subtracts hebog.analysis_memory_bytes and resident process RSS from the smaller configured/worker memory budget before admitting tile tasks. It reserves for the sources hebog.expected_source_count estimates from the detection image at threshpix; peeled sources are not in the image and reserve nothing. A component list is far longer than the source list: on the matched benchmark's 3,000² sector WSClean's 101,144 components reserved 5.9 GB for 890 sources, where the image's 1,155 peaks reserve 0.27 GB. When no memory remains for the tile tasks, or one needs more than remains, Hebog's ExecutorAdmissionError is a typed refusal and the task falls back to PyBDSF, recording the reason in the image diagnostics; an unsupported worker layout fails explicitly. The whole task occupies the worker's only task slot, so its analysis reservation does not require Dask resource annotations on this interim path. Synchronous runs also respect host available memory and container/process memory limits.

The no-beam branch supplies the flat image as both adapter inputs, as pinned LSMTool does. A configured missing peeled model is an error. Native selection and memory/configuration details live in <root>.image_diagnostics.json and survive Rapthor's normal diagnostic update and restart reuse. The branch's parset defaults are hebog_profile = continuum and hebog_background = zero, which the adapter also defaults to: the profile that passes the truth-based gate, with PyBDSF's mean map. Either setting can be changed in the parset, and invalid settings fail validation.

Rapthor usually images one sector across a cluster, the first performance focus; one sector on one node is the other main run, and several sectors stay supported. The executor follows Rapthor's workers:

Rapthor's workers Executor Rapthor passes Why
Give each node several task slots, in a layout Rapthor chooses DaskExecutor on Rapthor's client Dask schedules every free core, on one node or many, for as long as one sector keeps scaling (its serial share is measured and cut first)
One single-threaded worker a node, today's layout ThreadExecutor inside the worker's task, sized to the node's cores Through Dask that worker would run one Hebog task at a time; one sector uses one node. Retired inside Rapthor once each node has several task slots

Rapthor's nodes reach 192 cores, and its workers run one task each with --nthreads 1, one a node today. On Rapthor's client, Hebog's tile tasks stay single-threaded and numerous, with each stage's tile core and task count chosen within the core budget Rapthor states, so Dask schedules every core; several worker processes of a few threads each a node, rather than one process of 192 threads, keep Python's global lock from throttling a node. Hebog's analysis steps out of its worker slot while it waits on its tile tasks, so concurrent sectors cannot deadlock (planned), and its tasks carry their declared memory and cores as Dask resource annotations under the names Rapthor gives DaskExecutor. Rapthor's workers must limit their native thread pools to their declared cores, because Hebog runs in-process without the subprocess's single-thread settings. How Hebog runs on Rapthor's Dask layouts draws each layout and one way to provide several task slots a node; ADR-010 states what Hebog needs from any cluster. The registry route, a hebog entry in LSMTool's KNOWN_SOURCE_FINDERS, is deferred.

Rapthor's task builds a RapthorSourceFindingRequest from the sector's two images, its <working directory>/<image name> root and a run ID that a retry repeats, and a RapthorCompatibilityConfig from its strategy's threshpix and threshisl. It calls hebog.adapters.find_sector_sources(request, config, executor) and then, when the result reports an island, LSMTool's filter_sources(result.mask_path, vertices_file, ...) with keep_mask=True. The adapter imports no Rapthor, Prefect or LSMTool, and writes Hebog's own bundle, with its diagnostics and provenance, in <root>.hebog.

In dual-run mode Rapthor runs both finders on the sector, each under its own root under <root>.source-finders/, and calls hebog.adapters.rapthor_comparison.compare_sector_products(result, pybdsf, report_path) after LSMTool filtering, then selects the Hebog products for Rapthor. The full comparison is embedded in ordinary image diagnostics. Blanked images keep both empty sets and record why comparison is unavailable. Private input symlinks isolate PyBDSF masks, which it writes beside its input filename irrespective of the subprocess working directory. The report reads both sets and changes neither. It binds the path, size and SHA-256 of all eight files and Hebog's provenance, and records each finder's source count, the sources that are each other's nearest within half the restoring beam's major axis, the median ratio of their Total_flux, the median ratio of each pair of RMS maps on every sixteenth pixel of each axis, and the island pixels each mask holds and both hold. On the frozen 256² input against pinned master, all three sources match, the flux ratio is 1.000, the RMS ratio is 0.981 and Hebog's mask holds 165 of PyBDSF's 178 island pixels and one more of its own.

Reference comparisons use the explicit 5.0/3.0 profile with clean Rapthor and LSMTool checkouts at their recorded commits. Released and pinned-master PyBDSF do not produce identical catalogues on the same image, so neither is treated as truth.