NextAstro logo NextAstroDocumentation Open Hub

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.