argos.core.hardware¶
Telescope hardware profiles — what instrument Argos is driving.
Before this package, the physical description of the telescope was a set of
module-level constants scattered across core/alpaca/camera.py,
core/imaging/fits_writer.py, core/imaging/metrics.py and four more
files — with the focal length defined twice and the aperture nowhere, which is
how the FITS writer came to report f/3.2 for an f/5.3 telescope.
Public surface:
from argos.core.hardware import active, catalog
active.profile().arcsec_per_full_px # the value, at the point of use
active.load_from_config(config) # called once, at startup
catalog.PROFILES # the built-in registry
- class argos.core.hardware.TelescopeProfile(key: str, name: str, validated: bool, caveats: tuple[str, ...] = (), aperture_mm: float = 0.0, focal_length_mm: float = 0.0, sensor: str = '', pixel_size_um: float = 0.0, sensor_width_px: int = 0, sensor_height_px: int = 0, bayer_pattern: str = 'GRBG', adc_bits: int = 12, full_well_adu: int = 60000, linearity_max_adu: int = 50000, filter_names: tuple[str, ...] = (), ap_host: str = '10.0.0.1')[source]¶
Bases:
objectOne telescope model: what it is, and what its camera actually does.
- Parameters:
key – Stable identifier used in config and on the command line.
name – Human name, written to the FITS
TELESCOPheader.validated – True once every field has been checked against the real instrument. Unvalidated profiles are usable but must be surfaced as such — see
caveats.caveats – Short notes naming the fields that are not confirmed on hardware. Empty for a validated profile.
aperture_mm – Clear aperture. Together with the focal length this is the only source of the focal ratio.
focal_length_mm – Written to FITS
FOCALLEN, and the denominator of the plate scale.sensor – Sensor model name, written to FITS
INSTRUMEand used to select the matching fallback EGAIN/read-noise reference curve.pixel_size_um – Unbinned physical pixel pitch, FITS
XPIXSZ/YPIXSZ.sensor_width_px – Full-frame width. A fallback only — the Alpaca driver’s
CameraXSizewins at connect.sensor_height_px – Full-frame height, same rule.
bayer_pattern – CFA layout, FITS
BAYERPAT. Getting this wrong silently corrupts photometry, so it is called out incaveatson every unvalidated profile.adc_bits – Real ADC depth before the driver scales to 16-bit.
full_well_adu – Saturation threshold in ADU, for the clipping indicator.
linearity_max_adu – Upper bound of the trusted linear range — the saturation flag in aperture photometry uses this, not full well.
filter_names – Internal filter-wheel slots in position order. Empty when the model has no internal wheel.
ap_host – Fixed address the telescope serves on when running its own access point in the field. The connection port is not a profile field: every model uses 32323 and the user’s actual port lives in the per-network connection profiles under
alpaca.
- property arcsec_per_green_px: float¶
Plate scale per green-plane pixel — one green sample per 2×2 tile.
TelescopeProfile — the physical description of one instrument.
Every hardware number Argos needs to do science lives in one frozen dataclass:
optics, sensor geometry, ADC behaviour, filter slots, network defaults. Code
asks the active profile (see argos.core.hardware.active) instead of
importing a module-level constant, which is how the same focal length used to
exist in two files and the aperture in none.
Derived quantities are never stored. Focal ratio and plate scale are
computed from the primitives, so they cannot drift apart from them — the bug
this package exists to kill was a hard-coded FOCALLEN / 50.0 that reported
f/3.2 for an f/5.3 telescope, because the aperture was never a field anywhere.
Qt-free by construction: this module is imported by core, workers and
ui alike.
- argos.core.hardware.profile.ARCSEC_CONSTANT = 206.265¶
Arcseconds per radian ÷ 1000 — the small-angle constant for plate scale,
206.265 = 180 × 3600 / π / 1000, used with µm pixels and mm focal length.
- class argos.core.hardware.profile.TelescopeProfile(key: str, name: str, validated: bool, caveats: tuple[str, ...] = (), aperture_mm: float = 0.0, focal_length_mm: float = 0.0, sensor: str = '', pixel_size_um: float = 0.0, sensor_width_px: int = 0, sensor_height_px: int = 0, bayer_pattern: str = 'GRBG', adc_bits: int = 12, full_well_adu: int = 60000, linearity_max_adu: int = 50000, filter_names: tuple[str, ...] = (), ap_host: str = '10.0.0.1')[source]¶
Bases:
objectOne telescope model: what it is, and what its camera actually does.
- Parameters:
key – Stable identifier used in config and on the command line.
name – Human name, written to the FITS
TELESCOPheader.validated – True once every field has been checked against the real instrument. Unvalidated profiles are usable but must be surfaced as such — see
caveats.caveats – Short notes naming the fields that are not confirmed on hardware. Empty for a validated profile.
aperture_mm – Clear aperture. Together with the focal length this is the only source of the focal ratio.
focal_length_mm – Written to FITS
FOCALLEN, and the denominator of the plate scale.sensor – Sensor model name, written to FITS
INSTRUMEand used to select the matching fallback EGAIN/read-noise reference curve.pixel_size_um – Unbinned physical pixel pitch, FITS
XPIXSZ/YPIXSZ.sensor_width_px – Full-frame width. A fallback only — the Alpaca driver’s
CameraXSizewins at connect.sensor_height_px – Full-frame height, same rule.
bayer_pattern – CFA layout, FITS
BAYERPAT. Getting this wrong silently corrupts photometry, so it is called out incaveatson every unvalidated profile.adc_bits – Real ADC depth before the driver scales to 16-bit.
full_well_adu – Saturation threshold in ADU, for the clipping indicator.
linearity_max_adu – Upper bound of the trusted linear range — the saturation flag in aperture photometry uses this, not full well.
filter_names – Internal filter-wheel slots in position order. Empty when the model has no internal wheel.
ap_host – Fixed address the telescope serves on when running its own access point in the field. The connection port is not a profile field: every model uses 32323 and the user’s actual port lives in the per-network connection profiles under
alpaca.
- property arcsec_per_green_px: float¶
Plate scale per green-plane pixel — one green sample per 2×2 tile.
The built-in telescope profiles — data, not logic.
Adding a model means adding an entry here and nothing else. Every number is
either measured on the instrument or taken from the manufacturer’s published
specification; anything neither measured nor published belongs in caveats,
not in a plausible-looking field.
Only the S30 Pro is validated: it is the model Argos was built against and every one of its values is confirmed by the code it replaces. The S30 and S50 entries carry published optics and sensor geometry — the plate scales they imply match the manufacturer’s own field-of-view figures, which is a real cross-check. Their sensor reference curves are available as fallbacks, but their CFA layout and Seestar-specific gain scaling still need hardware verification before precision photometry.
The S50 Pro is deliberately absent from 0.4.1 even though its published telephoto specification is now sufficient to derive a physical plate scale: 50 mm, 260 mm, 2.9 µm pixels and 3840 × 2160 (about 2.30 arcsec/pixel). What remains unverified is the raw Seestar CFA layout and the camera’s gain, linearity and saturation behaviour. Exposing it as an ordinary selectable profile before that work would encourage scientifically unsafe quick-look photometry; it should be added with explicit unvalidated caveats once a raw frame validation is available.
- argos.core.hardware.catalog.DEFAULT_PROFILE = TelescopeProfile(key='s30pro', name='ZWO Seestar S30 Pro', validated=True, caveats=(), aperture_mm=30.0, focal_length_mm=160.0, sensor='IMX585', pixel_size_um=2.9, sensor_width_px=3840, sensor_height_px=2160, bayer_pattern='GRBG', adc_bits=12, full_well_adu=60000, linearity_max_adu=50000, filter_names=('Dark', 'IR', 'LP'), ap_host='10.0.0.1')¶
Used whenever config names a profile that does not exist.
- argos.core.hardware.catalog.PROFILES: dict[str, TelescopeProfile] = {'s30': TelescopeProfile(key='s30', name='ZWO Seestar S30', validated=False, caveats=('Bayer pattern assumed GRBG — confirm before trusting photometry', 'Seestar gain scale and EGAIN curve require a photon-transfer calibration', 'ADC depth and saturation thresholds inherited from the S30 Pro'), aperture_mm=30.0, focal_length_mm=150.0, sensor='IMX662', pixel_size_um=2.9, sensor_width_px=1920, sensor_height_px=1080, bayer_pattern='GRBG', adc_bits=12, full_well_adu=60000, linearity_max_adu=50000, filter_names=('Dark', 'IR', 'LP'), ap_host='10.0.0.1'), 's30pro': TelescopeProfile(key='s30pro', name='ZWO Seestar S30 Pro', validated=True, caveats=(), aperture_mm=30.0, focal_length_mm=160.0, sensor='IMX585', pixel_size_um=2.9, sensor_width_px=3840, sensor_height_px=2160, bayer_pattern='GRBG', adc_bits=12, full_well_adu=60000, linearity_max_adu=50000, filter_names=('Dark', 'IR', 'LP'), ap_host='10.0.0.1'), 's50': TelescopeProfile(key='s50', name='ZWO Seestar S50', validated=False, caveats=('Bayer pattern assumed GRBG — confirm before trusting photometry', 'Seestar gain scale and EGAIN curve require a photon-transfer calibration', 'ADC depth and saturation thresholds inherited from the S30 Pro'), aperture_mm=50.0, focal_length_mm=250.0, sensor='IMX462', pixel_size_um=2.9, sensor_width_px=1920, sensor_height_px=1080, bayer_pattern='GRBG', adc_bits=12, full_well_adu=60000, linearity_max_adu=50000, filter_names=('Dark', 'IR', 'LP'), ap_host='10.0.0.1')}¶
Ordered registry, keyed by
TelescopeProfile.key. The reference model comes first so pickers default to the one that is actually validated.
- argos.core.hardware.catalog.get(key: str) TelescopeProfile | None[source]¶
Return the profile registered under key, or
None.
- argos.core.hardware.catalog.keys() tuple[str, ...][source]¶
Registered profile keys, in display order.
Cross-check the active profile against what the driver actually reports.
Picking the wrong telescope profile is a silent failure: the frames keep coming, the plate solve keeps working, and every magnitude is computed against the wrong plate scale. The driver knows its own sensor size and name, so there is no reason to let that go unnoticed.
Deliberately advisory. mismatches() reports, it does not correct — the
caller warns and the observer decides. Switching profile mid-session would
change the plate scale under a running photometry series, which is worse than
the mismatch it would be fixing.
suggest() only answers when the evidence is unambiguous. The S30 and S50
share a 1920×1080 sensor, so resolution alone cannot separate them; guessing
between two wrong answers is not better than saying nothing.
- argos.core.hardware.detect.mismatches(profile: TelescopeProfile, *, camera_name: str = '', width: int | None = None, height: int | None = None) tuple[str, ...][source]¶
Human-readable disagreements between profile and the connected camera.
Empty when everything the driver reported is consistent with the profile. Unknown values (empty name,
Nonedimensions) are skipped rather than treated as conflicts — a driver that does not report a field is not evidence of anything.
- argos.core.hardware.detect.suggest(*, camera_name: str = '', width: int | None = None, height: int | None = None) TelescopeProfile | None[source]¶
The one registered profile matching this camera, or
None.Returns
Nonewhen nothing matches and when several do — an ambiguous suggestion would be worse than none, because the observer would trust it.
The active telescope profile for this process.
One module-level profile, resolved from config at startup and readable from anywhere:
from argos.core.hardware import active
scale = active.profile().arcsec_per_full_px
Call it, don’t bind it. Every consumer reads through profile() at the
point of use rather than copying the value into a module constant at import
time. The theme layer does bind at import, which is why main.py carries a
comment about applying the palette before any widget is constructed — a real
ordering constraint that is easy to break. Nothing here has that hazard: a
profile change is visible to the next call, wherever it comes from.
User overrides ride on top of the built-in profile, so a corrected saturation threshold survives an Argos upgrade that revises the shipped defaults.
- argos.core.hardware.active.CFG_PROFILE = 'hardware.profile'¶
Config keys.
- argos.core.hardware.active.OVERRIDABLE = frozenset({'adc_bits', 'ap_host', 'full_well_adu', 'linearity_max_adu'})¶
these are the values a careful observer can measure themselves (saturation, linearity, ADC depth) or must correct for a variant. Optics and CFA layout are not in the list — if those are wrong, the profile is wrong and needs fixing at the source.
- Type:
Fields a user may override. Deliberately narrow
- argos.core.hardware.active.apply_overrides(base: TelescopeProfile, overrides: dict[str, Any]) TelescopeProfile[source]¶
Return base with the whitelisted entries of overrides applied.
Unknown or non-overridable keys are logged and skipped rather than raising: a hand-edited config should never stop the application from starting.
- argos.core.hardware.active.load_from_config(config) TelescopeProfile[source]¶
Resolve the profile named in config, apply overrides, make it active.
An unknown key falls back to the reference profile with a warning: landing on a working telescope beats refusing to start over a typo.
- argos.core.hardware.active.profile() TelescopeProfile[source]¶
The profile in force right now.
- argos.core.hardware.active.set_profile(new: TelescopeProfile) None[source]¶
Make new the active profile.