Python scripting
Python interface
All equipment calls block until their operation returns a completion result. There are no client-imposed exposure, handover, or session-duration limits. Existing OBS/device requirements and failures remain authoritative.
Observatory and sessions
obs = Observatory(port=8221, progress=print)
obs.site
obs.status() # snapshot, including per-source measurement times
obs.capabilities() # cameras, devices, filters, protocol version
with obs.session(name="Experiment", userid="student01") as session:
...
# session.cancel() requests cancellation of this session
progress=None suppresses printed connection progress. userid is optional
run/product attribution, not an authenticated identity or permission grant.
All operations use the existing local operator access model.
Equipment
obs.mount.slew(ra_hours=12.5, dec_deg=-30, center=False)
obs.mount.slew_altaz(alt_deg=45, az_deg=180)
obs.mount.set_tracking(True)
obs.mount.home()
obs.mount.park()
obs.camera.cool_to(-30, tolerance=1) # default tolerance: camera configuration
obs.camera.warm_ready() # existing OBS warming ramp
images = obs.camera.expose(seconds=10, filter="V", count=3,
frame_type="light", target_name="Student target")
obs.camera.expose(seconds=60, count=30, frame_type="dark")
obs.camera.bias(count=30)
obs.filterwheel.filters # mapping of installed wheels to names
obs.filterwheel.select("Ha")
obs.rotator.move_mechanical(15) # degrees in mechanical frame
obs.rotator.set_field_angle(90) # sky position angle, existing OBS transform
obs.rotator.start_derotation()
obs.rotator.stop()
obs.focuser.move_steps(12000)
obs.focuser.move_relative_steps(100)
obs.focuser.autofocus()
Camera/filter selection uses the configured default unless a camera is named:
camera = obs.camera("camera:main") # actual ID from capabilities/configuration
camera.filterwheel.select("V")
images = camera.expose(seconds=5)
obs.capabilities()['student_client']['cameras'] contains the existing OBS
camera inventory once it has been published in this runtime. The inventory and
default_camera_id are reused from local status rather than recomputed by the
client. Full status retains the source measurement timestamps.
The initial acquisition API covers single FITS light, dark, flat and bias frames.
It executes the requested count as individual frames so completed images remain
available if a later frame fails. A count of zero returns an empty batch without
device work. Exposures do not implicitly require a cooling step;
cool_to is an explicit operation and its target is retained across captures.
Mechanical rotation uses PWI4's goto_mech, the native Optec mechanical frame,
or a driver's ASCOM MoveMechanical operation.
A driver without a mechanical operation reports that actual capability gap;
the client does not substitute a sky-angle movement.
Results and exceptions
ExposureBatch is list-like even for a single image. It has command_id and
paths. Each Exposure has exposure_id, a local path, camera_id,
frame_type, seconds, filter, started_at, and metadata. A returned
image has completed its requested primary raw write. Reduction/preview/upload
work can continue through normal OBS processing.
CommandFailed carries command_id and the recorded result. Completed images
before a failure appear in error.result['detail']['images'].
CommandCancelled identifies a stopped command. ConnectionLost means delivery
or execution outcome may be uncertain; retain its command_id and retrieve
obs.command_result(command_id) rather than repeating the exposure or movement.
ProtocolError identifies an incompatible/older OBS build.
Additional controls and typed telemetry
mount = obs.mount.status() # MountStatus; no controlling session required
print(mount.ra_hours, mount.dec_deg, mount.alt_deg, mount.source_time)
camera = obs.camera.status() # CameraStatus
print(camera.temperature_c, camera.target_c, camera.cooler_power_percent)
focus = obs.focuser.status() # FocuserStatus
print(focus.position_steps, focus.position_microns, focus.temperature_c)
print(obs.rotator.status()) # Target angle, never claimed as measured mechanical PA
with obs.session(name="Additional controls"):
obs.mount.unpark()
obs.mount.offset(east_arcsec=15, north_arcsec=-10)
obs.mount.moon()
obs.mount.planet("Jupiter")
obs.mount.minor_planet("Ceres", orbital_elements=elements)
obs.mount.comet("comet name", orbital_elements=elements)
obs.camera.dark(seconds=30, count=3)
obs.camera.flat(seconds=2, filter="V", count=3)
obs.camera.darkslide(opened=False)
summary = obs.camera.sky_flats(morning=False)
obs.power_switch("configured device name").set(channel=1, enabled=True)
Typed status preserves missing telemetry as None, includes raw and the source
publication time, and does not claim a fresh driver measurement just because the
client made a GET. Camera-specific status resolves the configured camera ID.
An east offset is a sky tangent displacement, not raw RA arcseconds. Offsets use
a spherical rotation, including RA wrap and polar pointings, and preserve target
tracking through the existing dither path. Moving-target operations use existing
OBS ephemerides; absent minor-planet/comet elements use OBS's existing Hub lookup.
Sky flats call the existing observatory routine, including its configured filters, conditions, thermal handling and termination rules. Named outcomes such as a deferred run are returned to the student. Darkslides and power switches refer to installed equipment; switch channels retain the configured driver's numbering. Existing protection/stop behaviour still applies. Screen flats are not exposed: the current OBS routine is unimplemented. Lamp control is available where the lamp is wired to an installed power switch.
Native captures
video = obs.camera.video(frame_seconds=.02, duration=10, filter="V", count=2,
roi={"mode": "max_pixels", "max_pixels": 512})
planet = obs.camera.planet(count=100, filter="V", target_name="Jupiter")
speckle = obs.camera.speckle(settings=speckle_settings, count=2, filter="V")
print(video.paths, speckle.metadata)
These methods require a controlling session and return CaptureResult, retaining
native metadata rather than pretending that a SER/SRZ capture is a FITS exposure.
Video count counts clips; planet count requests retained still frames; speckle
count counts target/reference pairs. Zero count requests no capture or camera
selection. Native video ROI is passed to the existing camera implementation;
actual ROI, frame counts, drops and calibration acknowledgements remain in the
returned metadata. Native planet capture keeps OBS's exposure selection and
candidate selection; its selection_report is returned. Paired speckle requires
the existing OBS settings schema and reference/target information; it does not
fabricate a reference. CaptureResult.paths collects native SER/SRZ paths and
planet selection reports, without searching image directories. Failed commands
retain already reported native results under error.result['detail']['captures'].
Normal FITS acquisitions currently use OBS's configured binning, gain and readout
profile. Per-exposure overrides are not implemented by these camera paths, so the
client does not accept and silently ignore such arguments. Actual recorded FITS
detector fields are available as image.metadata['detector'], including dimensions,
binning, ADU conversion gain, and driver gain/readout fields when recorded. GAIN
in electrons/ADU differs from a CMOS driver's gain setting. Analysis region=
selects pixels for measurement and does not alter the detector acquisition ROI.
Explicit image measurements and processing results
image = images[0]
stats = image.statistics(region=(100, 100, 256, 256), saturation=50000)
aperture = image.aperture_photometry(x=230.5, y=180.2, radius=5,
sky_inner=8, sky_outer=12)
source = image.measure_source(x=230.5, y=180.2, radius=8,
sky_inner=12, sky_outer=18)
noise = image.difference_statistics(other_image)
print(image.processing()) # Small worker acknowledgement and product paths
print(image.measurements()) # Pipeline header measurements, or None if pending
print(image.plate_solution()) # pending / solved / failed / skipped / unavailable
print(image.sources()) # Pipeline source catalogue, or None if not published
Image pixels are opened only for explicit statistics/photometry/difference calls,
in the student's interpreter. These are teaching measurements: pixel-centre
apertures with median annular sky, no fractional pixel weighting, and moment-based
Gaussian-equivalent FWHM rather than a fitted PSF. Coordinates are zero-based
pixel centres; image regions are (x, y, width, height). Nonfinite pixels are
counted/excluded. Missing sky or source signal returns None; negative flux,
edge clipping, saturation counts and poor widths are diagnostics, never capture
rejection gates. Errors use annulus sample noise and optional source Poisson noise
when gain_e_per_adu is supplied; they omit correlated noise and flat-field errors.
processing() is a snapshot and never waits or reads pixels. queued, running,
products_queued and failed describe the post-exposure handoff. products_queued
does not claim downstream solver, photometry, reduction or upload completion.
Read the corresponding result method again to see a later product. A solved
result reads the actual header-only .wcs product; acquisition pointing headers
are never used as evidence of a plate solve. .sources/.sources.zst values retain
the pipeline's coordinate conventions (for example X_IMAGE/Y_IMAGE are FITS/
catalogue coordinates); convert conventions before using local zero-based apertures.