"""Subband processing configuration for OVRO-LWA pipeline.
Contains node-to-subband mapping, imaging parameters, reference file paths,
and all configuration needed by the subband processing Celery tasks.
All settings live within the orca package for use with the Celery-based
subband pipeline.
"""
from astropy.coordinates import EarthLocation
import astropy.units as u
# --- Observatory Location (single source of truth) ---
[docs]
OVRO_LOC = EarthLocation(lat=37.23977727*u.deg, lon=-118.2816667*u.deg, height=1222*u.m)
# --- Conda env required for running the pipeline worker ---
[docs]
REQUIRED_CONDA_ENV = 'py38_orca_nkosogor'
# ---------------------------------------------------------------------------
# Node ↔ Subband mapping
# Each calim server has local NVMe at /fast/pipeline/ that is NOT shared.
# Subbands are pinned to specific nodes so data stays local.
# ---------------------------------------------------------------------------
[docs]
NODE_SUBBAND_MAP = {
'18MHz': 'lwacalim00', '23MHz': 'lwacalim00',
'27MHz': 'lwacalim01', '32MHz': 'lwacalim01',
'36MHz': 'lwacalim02', '41MHz': 'lwacalim02',
'46MHz': 'lwacalim03', '50MHz': 'lwacalim03',
'55MHz': 'lwacalim04',
'59MHz': 'lwacalim05',
'64MHz': 'lwacalim06',
'69MHz': 'lwacalim07',
'73MHz': 'lwacalim08',
'78MHz': 'lwacalim09',
'82MHz': 'lwacalim00',
}
# Reverse map: node → list of subbands
for _sb, _node in NODE_SUBBAND_MAP.items():
SUBBAND_NODE_MAP.setdefault(_node, []).append(_sb)
# All unique calim nodes
[docs]
CALIM_NODES = sorted(set(NODE_SUBBAND_MAP.values()))
# ---------------------------------------------------------------------------
# Dynamic dispatch — node pool
# Used with ``--dynamic`` mode. Any node in this list can process any
# subband. Edit this list to match currently active nodes.
# ---------------------------------------------------------------------------
[docs]
DYNAMIC_NODE_POOL = [
'calim00', 'calim01', 'calim03', 'calim04',
'calim05', 'calim06', 'calim07', 'calim08', 'calim09',
]
[docs]
def get_queue_for_subband(subband: str) -> str:
"""Return the Celery queue name for a given subband.
Args:
subband: e.g. '73MHz'
Returns:
Queue name, e.g. 'calim08'
"""
node = NODE_SUBBAND_MAP.get(subband)
if node is None:
raise ValueError(f"Unknown subband: {subband}")
# Queue name = last part of hostname: lwacalim08 → calim08
return node.replace('lwa', '')
# ---------------------------------------------------------------------------
# Directory layout
# ---------------------------------------------------------------------------
[docs]
NVME_BASE_DIR = '/fast/pipeline' # Local NVMe on each calim node
[docs]
LUSTRE_ARCHIVE_DIR = '/lustre/pipeline/images' # Shared Lustre storage
# Where archive MS files live (input)
[docs]
LUSTRE_NIGHTTIME_DIR = '/lustre/pipeline/night-time/averaged'
# Where final products go
[docs]
LUSTRE_PRODUCTS_DIR = '/lustre/pipeline/products'
# ---------------------------------------------------------------------------
# Reference files (on shared Lustre, accessible from all nodes)
# ---------------------------------------------------------------------------
[docs]
PEELING_PARAMS = {
'sky_env': 'julia060',
'rfi_env': 'ttcal_dev',
'sky_model': '/lustre/gh/calibration/pipeline/reference/sources/sources.json',
'rfi_model': '/lustre/gh/calibration/pipeline/reference/sources/rfi_43.2_ver20251101.json',
'beam': 'constant',
'minuvw': 5,
'maxiter': 5,
'tolerance': '1e-4',
'args': '--beam constant --minuvw 5 --maxiter 5 --tolerance 1e-4',
}
[docs]
AOFLAGGER_STRATEGY = '/lustre/ghellbourg/AOFlagger_strat_opt/LWA_opt_GH1.lua'
[docs]
VLSSR_CATALOG = '/lustre/gh/calibration/pipeline/reference/surveys/FullVLSSCatalog.text'
[docs]
BEAM_MODEL_H5 = '/lustre/gh/calibration/pipeline/reference/beams/OVRO-LWA_MROsoil_updatedheight.h5'
# ---------------------------------------------------------------------------
# Calibrator flux scale (Scaife & Heald 2012 + Perley-Butler 2017)
# ---------------------------------------------------------------------------
from astropy.coordinates import SkyCoord
[docs]
CALIB_DATA = {
'3C48': {'coords': SkyCoord('01h37m41.3s', '+33d09m35s'), 'scale': 'SH12',
'coeffs': [64.768, -0.387, -0.420, 0.181]},
'3C147': {'coords': SkyCoord('05h42m36.1s', '+49d51m07s'), 'scale': 'SH12',
'coeffs': [66.738, -0.022, -1.012, 0.549]},
'3C196': {'coords': SkyCoord('08h13m36.0s', '+48d13m03s'), 'scale': 'SH12',
'coeffs': [83.084, -0.699, -0.110]},
'3C286': {'coords': SkyCoord('13h31m08.3s', '+30d30m33s'), 'scale': 'SH12',
'coeffs': [27.477, -0.158, 0.032, -0.180]},
'3C295': {'coords': SkyCoord('14h11m20.5s', '+52d12m10s'), 'scale': 'SH12',
'coeffs': [97.763, -0.582, -0.298, 0.583, -0.363]},
'3C380': {'coords': SkyCoord('18h29m31.8s', '+48d44m46s'), 'scale': 'SH12',
'coeffs': [77.352, -0.767]},
'3C123': {'coords': SkyCoord('04h37m04.4s', '+29d40m14s'), 'scale': 'PB17',
'coeffs': [1.8017, -0.7884, -0.1035, -0.0248, 0.0090]},
}
# ---------------------------------------------------------------------------
# Hot baseline analysis parameters
# ---------------------------------------------------------------------------
[docs]
HOT_BASELINE_PARAMS = {
'run_uv_analysis': True,
'run_heatmap_analysis': True,
'uv_sigma': 7.0,
'heatmap_sigma': 5.0,
'bad_antenna_threshold': 0.25,
'uv_cut_lambda': 4.0,
'uv_window_size': 100,
'apply_flags': True,
}
# ---------------------------------------------------------------------------
# Imaging configurations
# Imaging configurations
# ---------------------------------------------------------------------------
[docs]
SNAPSHOT_PARAMS = {
'suffix': 'Pilot-Snapshot',
'args': [
'-log-time',
'-pol', 'IV',
'-niter', '0',
'-mem', '20',
'-size', '4096', '4096',
'-scale', '0.03125',
'-taper-inner-tukey', '30',
'-weight', 'briggs', '0',
'-no-dirty',
'-make-psf',
'-no-update-model-required',
],
}
# Stokes-I-only CLEANed snapshots (produced IN ADDITION to dirty pilots).
# Optimised per Marin Torchiarolo's wsclean benchmarks:
# auto-mask=5 (sweet spot), mgain=0.9999 (~2 major cycles),
# auto-threshold=1 (safe floor, negligible time impact).
# Output goes to snapshots_clean/ and is always fpack-compressed.
[docs]
SNAPSHOT_CLEAN_I_PARAMS = {
'suffix': 'Clean-Snapshot',
'args': [
'-log-time',
'-pol', 'I',
'-niter', '50000',
'-mgain', '0.9999',
'-auto-mask', '5',
'-auto-threshold', '1',
'-local-rms',
'-horizon-mask', '10deg',
'-mem', '50',
'-no-dirty',
'-size', '4096', '4096',
'-scale', '0.03125',
'-taper-inner-tukey', '30',
'-weight', 'briggs', '0',
'-no-update-model-required',
],
}
[docs]
IMAGING_STEPS = [
# --- STOKES I ---
{
'pol': 'I', 'category': 'deep', 'suffix': 'I-Deep-Taper-Robust-0.75',
'args': [
'-log-time', '-pol', 'I', '-multiscale', '-multiscale-scale-bias', '0.8',
'-niter', '500000', '-mgain', '0.95', '-horizon-mask', '10deg',
'-mem', '50', '-auto-threshold', '0.5', '-auto-mask', '3', '-local-rms',
'-size', '4096', '4096', '-scale', '0.03125',
'-taper-inner-tukey', '30', '-weight', 'briggs', '-0.75',
'-no-update-model-required',
],
},
{
'pol': 'I', 'category': 'deep', 'suffix': 'I-Deep-Taper-Robust-0',
'args': [
'-log-time', '-pol', 'I', '-multiscale', '-multiscale-scale-bias', '0.8',
'-niter', '500000', '-mgain', '0.95', '-horizon-mask', '10deg',
'-mem', '50', '-auto-threshold', '0.5', '-auto-mask', '3', '-local-rms',
'-size', '4096', '4096', '-scale', '0.03125',
'-taper-inner-tukey', '30', '-weight', 'briggs', '0',
'-no-update-model-required',
],
},
{
'pol': 'I', 'category': 'deep', 'suffix': 'I-Deep-NoTaper-Robust-0.75',
'args': [
'-log-time', '-pol', 'I', '-multiscale', '-multiscale-scale-bias', '0.8',
'-niter', '150000', '-mgain', '0.95', '-horizon-mask', '10deg',
'-mem', '50', '-auto-threshold', '0.5', '-auto-mask', '3', '-local-rms',
'-size', '4096', '4096', '-scale', '0.03125',
'-weight', 'briggs', '-0.75',
'-no-update-model-required',
],
},
{
'pol': 'I', 'category': 'deep', 'suffix': 'I-Deep-NoTaper-Robust-0',
'args': [
'-log-time', '-pol', 'I', '-multiscale', '-multiscale-scale-bias', '0.8',
'-niter', '150000', '-mgain', '0.95', '-horizon-mask', '10deg',
'-mem', '50', '-auto-threshold', '0.5', '-auto-mask', '3', '-local-rms',
'-size', '4096', '4096', '-scale', '0.03125',
'-weight', 'briggs', '0',
'-no-update-model-required',
],
},
{
'pol': 'I', 'category': '10min', 'suffix': 'I-Taper-10min',
'args': [
'-log-time', '-pol', 'I', '-multiscale', '-multiscale-scale-bias', '0.8',
'-niter', '50000', '-mgain', '0.95', '-horizon-mask', '10deg',
'-mem', '50', '-auto-threshold', '0.5', '-auto-mask', '3', '-local-rms',
'-size', '4096', '4096', '-scale', '0.03125',
'-taper-inner-tukey', '30', '-weight', 'briggs', '0',
'-intervals-out', '6',
'-no-update-model-required',
],
},
# --- STOKES V ---
{
'pol': 'V', 'category': 'deep', 'suffix': 'V-Taper-Deep',
'args': [
'-log-time', '-pol', 'V', '-niter', '0',
'-horizon-mask', '10deg', '-mem', '50',
'-size', '4096', '4096', '-scale', '0.03125',
'-taper-inner-tukey', '30', '-weight', 'briggs', '0',
'-no-dirty', '-no-update-model-required',
],
},
{
'pol': 'V', 'category': '10min', 'suffix': 'V-Taper-10min',
'args': [
'-log-time', '-pol', 'V', '-niter', '0',
'-horizon-mask', '10deg', '-mem', '50',
'-size', '4096', '4096', '-scale', '0.03125',
'-taper-inner-tukey', '30', '-weight', 'briggs', '0',
'-no-dirty', '-intervals-out', '6',
'-no-update-model-required',
],
},
]
# ---------------------------------------------------------------------------
# Resource management for shared calim nodes
# ---------------------------------------------------------------------------
# Nodes with 2 subbands (e.g. calim00: 18+23 MHz) share 32 cores / 128 GB.
# Nodes with 1 subband get all resources.
_DUAL_SUBBAND_NODES = {
n for n, subs in SUBBAND_NODE_MAP.items() if len(subs) > 1
}
# ---------------------------------------------------------------------------
# Per-subband pixel scaling
# Lower-frequency subbands have wider beams → fewer pixels needed.
# This speeds up imaging significantly for the lowest bands.
# ---------------------------------------------------------------------------
_SUBBAND_PIXEL_SIZE = {
'18MHz': 1024, '23MHz': 1024, '27MHz': 1024, '32MHz': 1024, '36MHz': 1024,
'41MHz': 2048, '46MHz': 2048, '50MHz': 2048, '55MHz': 2048, '59MHz': 2048,
'64MHz': 4096, '69MHz': 4096, '73MHz': 4096, '78MHz': 4096, '82MHz': 4096,
}
# Pixel scale (deg/pixel) paired with _SUBBAND_PIXEL_SIZE so that
# npix * scale = const (≈128°), preserving full FoV at every tier.
_SUBBAND_PIXEL_SCALE = {
'18MHz': 0.125, '23MHz': 0.125, '27MHz': 0.125, '32MHz': 0.125, '36MHz': 0.125,
'41MHz': 0.0625, '46MHz': 0.0625, '50MHz': 0.0625, '55MHz': 0.0625, '59MHz': 0.0625,
'64MHz': 0.03125,'69MHz': 0.03125,'73MHz': 0.03125,'78MHz': 0.03125,'82MHz': 0.03125,
}
[docs]
def get_pixel_size(subband: str) -> int:
"""Return the image pixel dimension for a given subband.
Lower subbands use fewer pixels (wider beam → coarser resolution):
18-36 MHz → 1024 (4096/4)
41-59 MHz → 2048 (4096/2)
64-82 MHz → 4096
Args:
subband: e.g. '55MHz'
Returns:
Pixel dimension (square images: NxN).
"""
return _SUBBAND_PIXEL_SIZE.get(subband, 4096)
[docs]
def get_pixel_scale(subband: str) -> float:
"""Return the pixel scale (deg/pixel) paired with :func:`get_pixel_size`.
The product ``get_pixel_size(sb) * get_pixel_scale(sb)`` is constant
(~128°) so that the field-of-view is preserved across frequency tiers.
18-36 MHz → 0.125 (0.03125 * 4)
41-59 MHz → 0.0625 (0.03125 * 2)
64-82 MHz → 0.03125
Args:
subband: e.g. '55MHz'
Returns:
Pixel scale in degrees.
"""
return _SUBBAND_PIXEL_SCALE.get(subband, 0.03125)
[docs]
def get_image_resources(subband: str):
"""Return (cpus, mem_gb, wsclean_j) for a given subband.
In dynamic dispatch mode any subband can land on any node, so we
always allocate full node resources (44 cores). The old dual-node
halving (22 cores) is no longer used.
Args:
subband: e.g. '73MHz'
Returns:
Tuple of (cpus: int, mem_gb: int, wsclean_j: int).
"""
return 44, 120, 44