Skip to content

Commit

Permalink
Improved thermistor monitoring logic and finer control of thermistor …
Browse files Browse the repository at this point in the history
…config
  • Loading branch information
albireox committed Sep 17, 2024
1 parent c804f6e commit 43377dd
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 85 deletions.
8 changes: 6 additions & 2 deletions src/lvmcryo/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ async def ln2(
config.cameras,
interactive=config.interactive == "yes",
log=log,
valve_info=config.valves,
valve_info=config.valve_info,
dry_run=config.dry_run,
alerts_route=config.internal_config["api_routes"]["alerts"],
)
Expand Down Expand Up @@ -542,6 +542,10 @@ async def ln2(
with json_path.open("r") as ff:
log_data = [json.loads(line) for line in ff.readlines()]

configuration_json = config.model_dump() | {
valve: valve_model.model_dump()
for valve, valve_model in config.valve_info.items()
}
record_pk = await post_fill_tasks(
handler,
write_data=config.write_data,
Expand All @@ -556,7 +560,7 @@ async def ln2(
"log_file": str(config.log_path) if config.log_path else None,
"json_file": str(json_path) if json_path else None,
"log_data": log_data,
"configuration": config.model_dump(),
"configuration": configuration_json,
},
)

Expand Down
31 changes: 25 additions & 6 deletions src/lvmcryo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,31 @@ class NotificationLevel(str, Enum):
error = "error"


class ThermistorConfig(BaseModel):
"""Thermistor configuration model."""

channel: str | None = None
monitoring_interval: float = 1.0
close_valve: bool = True
required_active_time: float = 10.0
disabled: bool = False


class ValveConfig(BaseModel):
"""Valve configuration model."""

actor: str
outlet: str
thermistor: str | None = None
thermistor: ThermistorConfig | None = Field(default_factory=ThermistorConfig)

@model_validator(mode="after")
def validate_after(self) -> Self:
"""Validates the valve configuration after the fields have been set."""

if self.thermistor and self.thermistor.channel is None:
self.thermistor.channel = self.outlet

return self


ExcludedField = Field(repr=False, exclude=True)
Expand Down Expand Up @@ -112,7 +131,7 @@ class Config(BaseModel):
data_path: pathlib.Path | None = None
data_extra_time: float = 0.0

valves: Annotated[dict[str, ValveConfig], ExcludedField] = {}
valve_info: Annotated[dict[str, ValveConfig], ExcludedField] = {}

config_file: Annotated[pathlib.Path | None, ExcludedField] = None
_internal_config: Annotated[Configuration, PrivateAttr] = Configuration({})
Expand Down Expand Up @@ -181,10 +200,10 @@ def check_ommitted_fields(cls, data: Any) -> Any:
raise ValueError("No cameras defined in the configuration file.")
data["cameras"] = defaults["cameras"]

if "valves" not in data or data["valves"] is None:
if "valves" not in config:
raise ValueError("No valves defined in the configuration file.")
data["valves"] = config["valves"]
if "valve_info" not in data or data["valve_info"] is None:
if "valve_info" not in config:
raise ValueError("No valve_info defined in the configuration file.")
data["valve_info"] = config["valve_info"]

# Fill out the interactive value for now with None. Will be validated later.
if "interactive" not in data:
Expand Down
5 changes: 3 additions & 2 deletions src/lvmcryo/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ api_routes:
fill_data: http://lvm-hub.lco.cl:8080/api/spectrographs/fills/measurements
register_fill: http://lvm-hub.lco.cl:8080/api/spectrographs/fills/register

valves:
valve_info:
r1:
actor: lvmnps.sp1
outlet: r1
Expand Down Expand Up @@ -66,4 +66,5 @@ valves:
purge:
actor: lvmnps.sp1
outlet: purge
thermistor: supply
thermistor:
channel: supply
33 changes: 22 additions & 11 deletions src/lvmcryo/handlers/ln2.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def get_valve_info():
internal_config = get_internal_config()

return {
valve: ValveConfig(**data) for valve, data in internal_config["valves"].items()
valve: ValveConfig(**data)
for valve, data in internal_config["valve_info"].items()
}


Expand Down Expand Up @@ -104,7 +105,7 @@ def __post_init__(self):
self._progress_bar = None
self.console = Console()

self._valve_handlers: dict[str, ValveHandler] = {}
self.valve_handlers: dict[str, ValveHandler] = {}
for camera in self.cameras + [self.purge_valve]:
if camera not in self.valve_info:
raise ValueError(f"Cannot find valve infor for {camera!r}.")
Expand All @@ -113,11 +114,11 @@ def __post_init__(self):
outlet = self.valve_info[camera].outlet
thermistor = self.valve_info[camera].thermistor

self._valve_handlers[camera] = ValveHandler(
self.valve_handlers[camera] = ValveHandler(
camera,
actor,
outlet,
thermistor_channel=thermistor,
thermistor_info=thermistor.model_dump() if thermistor else None,
progress_bar=self._progress_bar,
log=self.log,
dry_run=self.dry_run,
Expand Down Expand Up @@ -224,11 +225,21 @@ async def check(
self.fail()
raise RuntimeError(f"Failed reading thermistors: {err}")

for valve in self._valve_handlers:
channel = self._valve_handlers[valve].thermistor_channel or valve
assert channel is not None, f"invalid thermistor channel {channel!r}."
for valve in self.valve_handlers:
thermistor_info = self.valve_info[valve].thermistor

thermistor_value = thermistors[channel]
if thermistor_info is None:
self.log.warning(f"Cannot check thermistor for {valve}.")
continue

if thermistor_info.disabled:
self.log.warning(f"Thermistor for {valve} is disabled.")
continue

if thermistor_info.channel is None:
raise RuntimeError(f"Thermistor channel for {valve} not defined.")

thermistor_value = thermistors[thermistor_info.channel]
if thermistor_value is True:
self.fail()
raise RuntimeError(f"Thermistor for valve {valve} is active.")
Expand Down Expand Up @@ -275,7 +286,7 @@ async def purge(
if purge_valve is None:
purge_valve = self.purge_valve

valve_handler = self._valve_handlers[purge_valve]
valve_handler = self.valve_handlers[purge_valve]

self.event_times.purge_start = self._get_now()

Expand Down Expand Up @@ -348,7 +359,7 @@ async def fill(

for camera in cameras:
try:
valve_handler = self._valve_handlers[camera]
valve_handler = self.valve_handlers[camera]
except KeyError:
raise RuntimeError(f"Unable to find valve for camera {camera!r}.")

Expand Down Expand Up @@ -465,7 +476,7 @@ async def close_valves(self, only_active: bool = True):

tasks: list[Coroutine] = []

for valve_handler in self._valve_handlers.values():
for valve_handler in self.valve_handlers.values():
if valve_handler.active or not only_active:
tasks.append(valve_handler.finish())

Expand Down
137 changes: 84 additions & 53 deletions src/lvmcryo/handlers/thermistor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from __future__ import annotations

import asyncio
import logging
import warnings
from dataclasses import dataclass
from time import time
Expand Down Expand Up @@ -82,8 +81,6 @@ async def _monitor(self):

self.data.append({"timestamp": time(), "data": data})

await asyncio.sleep(0.01)

await asyncio.sleep(self.interval)

@classmethod
Expand All @@ -103,101 +100,135 @@ class ThermistorHandler:
----------
valve_handler
The `.ValveHandler` instance associated with this thermistor.
interval
channel
The name of the thermistor channel to monitor.
monitoring_interval
The interval in seconds between thermistor checks.
min_open_time
The minimum valve open time. The thermistor will not be read until
the minimum time has been reached.
close_valve
If ``True``, closes the valve once the thermistor is active.
min_active_time
required_active_time
Number of seconds the thermistor must be active before the valve is
closed.
log
A logger instance.
disabled
If ``True``, the thermistor is disabled and we will not monitor it.
"""

valve_handler: ValveHandler
interval: float = 1.0
channel: str
monitoring_interval: float = 1.0
min_open_time: float = 0.0
close_valve: bool = True
min_active_time: float = 10.0
log: logging.Logger | None = None
required_active_time: float = 10.0
disabled: bool = False

def __post_init__(self):
self.channel = self.valve_handler.thermistor_channel or self.valve_handler.valve

# Unix time at which we start monitoring.
self._start_time: float = -1
self.log = self.valve_handler.log

# Unix time at which we saw the last data point.
self._last_seen: float = -1

# Unix time at which the thermistor became active.
self._active_time: float = -1

self.thermistor_monitor = ThermistorMonitor(interval=self.interval)
self.thermistor_monitor = ThermistorMonitor(interval=self.monitoring_interval)

async def start_monitoring(self):
"""Monitors the thermistor and potentially closes the valve."""

self._start_time = time()
# If the thermistor is not working return immediately and
# the valve will close on a timeout.
if self.disabled:
self.log.warning(
f"The thermistor for valve {self.valve_handler.valve} is disabled. "
"Will not monitor it."
)
return

# Unix time at which we start monitoring.
start_time = time()
self.thermistor_monitor.start()

self.valve_handler.log.debug(f"Starting to monitor thermistor {self.channel}.")

elapsed: float = -1
# Unix time at which we saw the last data point.
last_seen: float = 0

# Unix time at which the thermistor became active.
active_time: float = 0

# Elapsed time we have been monitoring.
elapsed_running: float = 0

# Elapsed time the thermistor has been active
elapsed_active: float = 0

# How long to wait to issue a warning if ThermistorMonitor
# is not providing new data.
alert_seconds = int(10 * self.monitoring_interval)

while True:
await asyncio.sleep(self.interval)
await asyncio.sleep(self.monitoring_interval)

elapsed_running = time() - start_time

# Check that there are measurements in the data list.
if len(self.thermistor_monitor.data) > 0:
# Get the last measurement.
data_point = self.thermistor_monitor.data[-1]

# Check that the last measurement includes the valve we are monitoring.
if self.channel in data_point["data"]:
th_data = data_point["data"][self.channel]
timestamp = data_point["timestamp"]

if abs(timestamp - self._last_seen) > 0.1:
self._last_seen = timestamp
if th_data:
if self._active_time < 0:
self._active_time = time()

elapsed = time() - self._active_time

if (
self._active_time > 0
and elapsed > self.min_active_time
and elapsed > self.min_open_time
):
# The thermistor has been active for long enough.
# Exit the loop and close the valve.
break
th_data = data_point["data"][self.channel] # Boolean
timestamp = data_point["timestamp"] # Unix time

# Check that the data is fresh.
if abs(timestamp - last_seen) > 0.1:
last_seen = timestamp

# If the thermistor is active, track for how long.
if th_data:
if active_time <= 0:
active_time = time()

# Time the thermistor has been active.
elapsed_active = time() - active_time

# We require the time we have been running to
# be > min_open_time and the the time the thermistor
# has been active to be > required_active_time.
if (
active_time > 0
and elapsed_active > self.required_active_time
and elapsed_running > self.min_open_time
):
# The thermistor has been active for long enough.
# Exit the loop and close the valve.
break

else:
# The thermistor is not active. Reset the active time and
# elapsed active time in case we had a case in which
# the thermistor was active for a short period and then
# became inactive.
active_time = 0
elapsed_active = 0

# Run some checks to be sure we are getting fresh data, but we won't
# fail the fill if that's the case, so we only do it if there's a logger.
# fail the fill if that's the case, so we only do it if there's a logger
# to which we can report.
if self.log is not None:
last_seen_elapsed = time() - self._last_seen
monitoring_elapsed = time() - self._start_time

alert_seconds = int(10 * self.interval)
last_seen_elapsed = time() - last_seen

is_late = self._last_seen > 0 and last_seen_elapsed > alert_seconds
never_seen = self._last_seen < 0 and monitoring_elapsed > alert_seconds
is_late = last_seen > 0 and last_seen_elapsed > alert_seconds
never_seen = last_seen <= 0 and elapsed_running > alert_seconds

if is_late or never_seen:
self.log.warning(
f"No data from the thermistor {self.channel} "
f"in the last {alert_seconds} seconds."
)

await asyncio.sleep(self.interval)

self.valve_handler.log.debug(
f"Thermistor {self.channel} has been active for more than "
f"{elapsed:.1f} seconds."
f"{elapsed_active:.1f} seconds. Stopping thermistor monitoring."
)

if self.close_valve:
Expand Down
Loading

0 comments on commit 43377dd

Please sign in to comment.