Add persistent material integration state
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from production_analytics.calculations.material_consumption import (
|
||||
MaterialConsumptionIntegrator,
|
||||
MaterialIntegrationState,
|
||||
MaterialIntegratorConfig,
|
||||
MaterialSample,
|
||||
)
|
||||
from production_analytics.calculations.material_state import (
|
||||
load_material_state,
|
||||
material_state_from_dict,
|
||||
material_state_to_dict,
|
||||
save_material_state,
|
||||
)
|
||||
|
||||
|
||||
def test_material_state_dict_roundtrip() -> None:
|
||||
original = MaterialIntegrationState(
|
||||
cumulative_consumption_kg=51.42714233398436,
|
||||
integrated_running_seconds=180.0,
|
||||
last_processed_timestamp=datetime(2026, 9, 3, 5, 10, 50, tzinfo=UTC),
|
||||
last_material_rate_kg_per_hour=1028.5428466796875,
|
||||
last_gate_value=2.1673481464385986,
|
||||
integration_active=True,
|
||||
)
|
||||
|
||||
restored = material_state_from_dict(material_state_to_dict(original))
|
||||
|
||||
assert restored == original
|
||||
|
||||
|
||||
def test_material_state_json_file_roundtrip(tmp_path) -> None:
|
||||
original = MaterialIntegrationState(
|
||||
cumulative_consumption_kg=12.5,
|
||||
integrated_running_seconds=90.0,
|
||||
last_processed_timestamp=datetime(2026, 9, 3, 5, 10, 50, tzinfo=UTC),
|
||||
last_material_rate_kg_per_hour=500.0,
|
||||
last_gate_value=2.0,
|
||||
integration_active=True,
|
||||
)
|
||||
|
||||
path = tmp_path / "material-state.json"
|
||||
|
||||
save_material_state(str(path), original)
|
||||
restored = load_material_state(str(path))
|
||||
|
||||
assert restored == original
|
||||
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert raw["last_processed_timestamp"] == "2026-09-03T05:10:50+00:00"
|
||||
|
||||
|
||||
def test_integrator_restore_continues_without_double_counting(tmp_path) -> None:
|
||||
config = MaterialIntegratorConfig(
|
||||
gate_threshold=0.5,
|
||||
max_sample_gap_seconds=20.0,
|
||||
)
|
||||
|
||||
samples = [
|
||||
MaterialSample(
|
||||
timestamp=datetime(2026, 9, 3, 5, 9, 40, tzinfo=UTC),
|
||||
material_rate_kg_per_hour=3600.0,
|
||||
gate_value=1.0,
|
||||
),
|
||||
MaterialSample(
|
||||
timestamp=datetime(2026, 9, 3, 5, 9, 50, tzinfo=UTC),
|
||||
material_rate_kg_per_hour=3600.0,
|
||||
gate_value=1.0,
|
||||
),
|
||||
MaterialSample(
|
||||
timestamp=datetime(2026, 9, 3, 5, 10, 0, tzinfo=UTC),
|
||||
material_rate_kg_per_hour=3600.0,
|
||||
gate_value=1.0,
|
||||
),
|
||||
]
|
||||
|
||||
continuous = MaterialConsumptionIntegrator(config)
|
||||
continuous.process_many(samples)
|
||||
|
||||
before_restart = MaterialConsumptionIntegrator(config)
|
||||
before_restart.process_many(samples[:2])
|
||||
|
||||
path = tmp_path / "material-state.json"
|
||||
save_material_state(str(path), before_restart.state)
|
||||
|
||||
restored = MaterialConsumptionIntegrator(
|
||||
config,
|
||||
initial_state=load_material_state(str(path)),
|
||||
)
|
||||
|
||||
# Polling deliberately starts inclusively at the last processed timestamp.
|
||||
restored.process_many(samples[1:])
|
||||
|
||||
assert restored.state == continuous.state
|
||||
assert restored.state.cumulative_consumption_kg == 20.0
|
||||
assert restored.state.integrated_running_seconds == 20.0
|
||||
Reference in New Issue
Block a user