Skip to content

Implement Design A: monitor subsystem compute engine - #25

Open
mpound wants to merge 19 commits into
mainfrom
feature/monitor-compute-engine
Open

Implement Design A: monitor subsystem compute engine#25
mpound wants to merge 19 commits into
mainfrom
feature/monitor-compute-engine

Conversation

@mpound

@mpound mpound commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New slama.monitor.compute package: a declarative recipe engine (Design A, chosen over the plugin-class Design B per docs/monitorsystem_writer_design.md) that reads SMAX monitor points, applies registered Python functions to them, and writes derived values + computed validity back to SMAX under a new monitorsystem: table — mirroring the SmaxRedisClient/Validity metadata pattern already used by slama.fault.
  • computeconfig.py: parses conf/computations.json, resolves input patterns (exact names, {i} ranges via the existing _parse_index_set, and glob wildcards) against the real MonitorSystem tree, and topologically sorts entries so a downstream aggregate-of-an-aggregate sees this tick's upstream result, not a stale one.
  • functions.py: built-in registry (worst_validity, count_true, count_valid, min/max/mean/median_value, sequence_validity), including an explicit severity-ranking table since Validity's IntEnum ordinal is declaration order, not severity (INVALID_NO_DATA has a lower ordinal than VALID_GOOD).
  • engine.py: ComputeEngine.tick()/run_forever(), with engine-enforced staleness/missing-input policy (skip vs propagate) and per-node persistent state (ComputeContext.state) so a stateless function design can still express stuck-timeout/trajectory checks like sequence_validity.
  • One real, end-to-end wired example: monitorsystem:array:antennas_online (count_true over antenna:1-8:is_online), declared under a new monitorsystem subtree in conf/smax.json.
  • Includes a follow-up commit fixing three production-path bugs found in review (all stemming from tests using plain Python values vs. production's smax.smax_data_types.Smax* instances): unhashable SmaxStr in state-machine validity lookup, _parse_index_set raising on real hyphenated segment names like 4K-plate, and _wrap() passing a bare float where SmaxVarBase.timestamp needs a datetime.

Test plan

  • uv run pytest src/slama/monitor/ -q — 145 passed
  • uv run ruff check src/slama/monitor/ — clean (aside from pre-existing, unrelated warnings verified via git stash against main)
  • Manual smoke test against a live Valkey instance (localhost:6380): seeded antenna:1-8:is_online, ran python -m slama.monitor.compute conf/computations.json --once, confirmed monitorsystem:array:antennas_online = 6 and validity metadata = VALID_WARNING_LOW (correct: warn_low=6, value=6)
  • Live deployment: seed monitorsystem:* output points with an initial value before first run — MonitorSystem.read_all() (shared with FaultSystem) eagerly pulls every declared point including outputs, so an unwritten output point crashes the first tick

🤖 Generated with Claude Code

mpound and others added 18 commits August 5, 2026 16:30
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds slama.monitor.compute, the declarative recipe engine chosen in
docs/monitorsystem_writer_design.md: computations.json wires named,
registered Python functions to resolved SMAX input points, the engine
ticks in topological order (so aggregate-of-aggregate outputs see
this-tick values, not stale ones), and writes each result back to SMAX
as an ordinary point plus a "validity" metadata entry (validity is
computed, not natively stored by SMAX/Redis).

Structured like slama.fault (FaultSystem/FaultConfig/FaultNode): same
tick()/run_forever()/stop()/reload_config() shape, __each__ template
expansion, and load-time validation that fails fast on unknown
functions, undeclared outputs, or dependency cycles.

Built-in functions: worst_validity (severity-ranked, since Validity's
enum int order does not match severity order), count_true, count_valid,
min/max/mean/median_value, and sequence_validity (a stuck-timeout
trajectory check for state-machine string points, using a per-node
persistent ctx.state dict and ctx.params, per design doc §3.5/§6.1).

One real end-to-end example is wired: monitorsystem:array:antennas_online,
counting antenna:{1..8}:is_online. sequence_validity is unit-tested but
not wired into computations.json (no real tuning-state point exists in
smax.json yet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three shared one blind spot: every test fed plain Python values
while production feeds smax.smax_data_types.Smax* instances.

- MonitorPoint._state_machine_validity(): a real smax_pull() result is
  a SmaxStr, which (being an @DataClass with default eq=True) has
  __hash__ set to None, so a dict lookup on self.value directly raised
  TypeError. Coerce to str first.
- ComputeConfig._resolve_pattern(): a colon segment containing '-' that
  isn't a numeric range (e.g. smax.json's literal "4K-plate", "a1-a2"
  segments) made _parse_index_set raise. Catch that locally and fall
  back to treating the segment as a literal, rather than loosening
  _parse_index_set's own contract (shared with slama.fault's __each__
  expansion, where a malformed range is more likely a config typo).
- engine._wrap(): passed the float wall-clock straight through as
  `timestamp`, but SmaxVarBase.timestamp is datetime | None and
  MonitorPoint.time does Time(self._smax_result.timestamp) -- astropy
  rejects a bare float. Convert to an aware datetime. Also narrowed the
  bare `except Exception` around the resulting mp.time read in
  _resolve_one(), which would otherwise convert this exact class of bug
  into a silent, permanent INVALID_NO_DATA.

Also: warn (rather than silently ignore) when a computation sets the
reserved-but-unconsulted per-entry interval_s; and worst_validity now
writes a severity-ranked score instead of int(Validity), since the
whole reason _SEVERITY_ORDER exists is that the enum's own ordinal
isn't severity-ordered -- writing it as a value would have scrambled
any later numeric/threshold comparison on that point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
output now accepts a dict of role-name -> canonical-name for entries
whose function naturally computes several related points in one pass
(e.g. min/max), avoiding a double call and keeping the function
reusable under __each__ expansion. Also hardens tick() with per-node
error isolation: any exception, including a multi-output key
mismatch, now only invalidates that node instead of aborting the
whole tick. Folds the design into docs/monitorsystem_writer_design.md
§8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers every function and method in engine.py and computeconfig.py
that lacked one or had only a one-line summary, including private
helpers -- Parameters/Returns/Raises added throughout. No behavior
change. computenode.py already had full docstrings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- coordinates/core.py: sun_distance() referenced angular_separation
  without importing it (NameError on call); switched hasattr(x,
  "unit") checks to isinstance(x, u.Quantity) since a real SMAX value
  (SmaxFloat etc.) carries its own unrelated .unit metadata attribute,
  always making hasattr() true and skipping the degree conversion.
  Same fix applied to sun_distance_from_coord. Length check now uses
  np.size() so scalar (single-antenna) calls work, not just arrays.
- functions.py: replaced the 32-input/8-output sun_distance_degrees
  (which had an output-shape mismatch against the engine's contract,
  an off-by-one slice, and passed ResolvedInput objects instead of
  raw values) with a single-antenna, dict-input version paired with
  a __each__-expanded entry per antenna instead.
- computations.json / smax.json: sun_distance entries/points declared
  per-antenna via __each__, with warn_low=45, err_low=40 degrees per
  Marc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mpound mpound mentioned this pull request Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant