Source code for panoptes.pocs.camera.simulator.ccd

"""Simulated cooled CCD camera using the SDK camera interfaces.

Provides a minimal SDKDriver shim (loading libc) and a Camera implementation
that simulates cooling behavior (temperature, cooling power) and connects like
an SDK-backed device for use in tests and simulations.
"""

import math
import random
import time
from abc import ABC
from contextlib import suppress

import astropy.units as u

from panoptes.utils.config.client import get_config

from panoptes.pocs.camera.sdk import AbstractSDKCamera, AbstractSDKDriver
from panoptes.pocs.camera.simulator.dslr import Camera as SimCamera


[docs] class SDKDriver(AbstractSDKDriver): """Lightweight simulated SDK driver used for the simulator camera.""" def __init__(self, library_path=None, **kwargs): # Get library loader to load libc, which should usually be present... super().__init__(name="c", library_path=library_path, **kwargs)
[docs] def get_SDK_version(self): """Return a human-readable version string for the simulated SDK.""" return "Simulated SDK Driver v0.001"
[docs] def get_devices(self): """Return simulated device mapping from the main configuration. Returns: dict: Mapping of simulated camera names to their ports/IDs as configured under 'cameras.devices'. """ self.logger.debug(f"Getting camera device connection config for {self}") camera_devices = dict() for cam_info in get_config("cameras.devices"): name = cam_info.get("name") or cam_info.get("model") port = cam_info.get("port") or cam_info.get("serial_number") camera_devices[name] = port self.logger.trace(f"camera_devices={camera_devices!r}") return camera_devices
[docs] class Camera(AbstractSDKCamera, SimCamera, ABC): """Simulated cooled camera that follows the AbstractSDKCamera contract. Combines the DSLR simulator behavior with SDK-style cooling controls to emulate a cooled scientific camera for testing pipeline behavior. """ def __init__( self, name="Simulated SDK camera", driver=SDKDriver, target_temperature=0 * u.Celsius, *args, **kwargs, ): kwargs.update({"target_temperature": target_temperature}) super().__init__(name, driver, *args, **kwargs) @AbstractSDKCamera.cooling_enabled.getter def cooling_enabled(self): """Whether simulated cooling is currently enabled. Returns: bool: True if cooling is enabled. """ return self._cooling_enabled @AbstractSDKCamera.target_temperature.getter def target_temperature(self): """Simulated target temperature for the sensor. Returns: astropy.units.Quantity: Target temperature in degrees Celsius. """ return self._target_temperature @property def temperature(self): """Current simulated sensor temperature. The temperature drifts exponentially toward a limit set by cooling state with a small random jitter added. Returns: astropy.units.Quantity: Simulated temperature in degrees Celsius. """ now = time.monotonic() delta_time = (now - self._last_time) / self._time_constant if self.cooling_enabled: limit_temp = max(self.target_temperature, self._min_temp) else: limit_temp = self._max_temp delta_temp = limit_temp - self._last_temp temperature = limit_temp - delta_temp * math.exp(-delta_time) add_temp = random.uniform(-self._temp_var / 2, self._temp_var / 2) temperature += random.uniform(-self._temp_var / 2, self._temp_var / 2) self.logger.trace(f"Temp adding {add_temp:.02f} \t Total: {temperature:.02f} for {self}") return temperature @property def cooling_power(self): """Simulated cooling power level. Returns: astropy.units.Quantity: Cooling duty cycle as a percentage. """ if self.cooling_enabled: return ( 100.0 * float((self._max_temp - self.temperature) / (self._max_temp - self._min_temp)) * u.percent ) else: return 0.0 * u.percent
[docs] def connect(self): """Initialize the simulated camera and cooling parameters.""" self._is_cooled_camera = True self._cooling_enabled = False self._temperature = 5 * u.Celsius self._max_temp = 25 * u.Celsius self._min_temp = -15 * u.Celsius self._temp_var = 0.05 * u.Celsius self._time_constant = 0.25 self._last_temp = 25 * u.Celsius self._last_time = time.monotonic() self._connected = True
def _set_target_temperature(self, target): """Set the simulated target temperature. Args: target (astropy.units.Quantity | float): Desired sensor setpoint in C. """ # Upon init the camera won't have an existing temperature. with suppress(AttributeError): self._last_temp = self.temperature self._last_time = time.monotonic() if not isinstance(target, u.Quantity): target = target * u.Celsius self._target_temperature = target.to(u.Celsius) def _set_cooling_enabled(self, enable): """Enable or disable simulated cooling. Args: enable (bool): True to enable, False to disable. """ self._last_temp = self.temperature self._last_time = time.monotonic() self._cooling_enabled = bool(enable)