Add channel material and downtime analytics
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""Configuration for reusable channel-level extruder consumption."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from production_analytics.calculations.config import CalculationConfigError, finite_number
|
||||
from production_analytics.service.channel_material import ChannelDefinition
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ChannelMaterialCalculationConfig:
|
||||
id: str
|
||||
machine_ref: str
|
||||
max_sample_gap_seconds: float
|
||||
percentage_tolerance: float
|
||||
channels: tuple[ChannelDefinition, ...]
|
||||
material_names: dict[str, str]
|
||||
|
||||
|
||||
def load_channel_material_calculation(path: str | Path) -> ChannelMaterialCalculationConfig:
|
||||
try:
|
||||
document = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, yaml.YAMLError) as exc:
|
||||
raise CalculationConfigError("Cannot read channel material configuration") from exc
|
||||
if (
|
||||
not isinstance(document, dict)
|
||||
or set(document) != {"calculations"}
|
||||
or not isinstance(document["calculations"], list)
|
||||
):
|
||||
raise CalculationConfigError("Configuration must contain only calculations")
|
||||
if len(document["calculations"]) != 1:
|
||||
raise CalculationConfigError("Channel configuration requires one calculation")
|
||||
entry = document["calculations"][0]
|
||||
required = {
|
||||
"id",
|
||||
"type",
|
||||
"version",
|
||||
"machine_ref",
|
||||
"max_sample_gap_seconds",
|
||||
"percentage_tolerance",
|
||||
"extruders",
|
||||
}
|
||||
if (
|
||||
not isinstance(entry, dict)
|
||||
or set(entry) - (required | {"material_names"})
|
||||
or required - set(entry)
|
||||
):
|
||||
raise CalculationConfigError("Invalid channel calculation fields")
|
||||
if entry["type"] != "channel_material_consumption" or entry["version"] != "1":
|
||||
raise CalculationConfigError("Unsupported channel calculation type or version")
|
||||
if not all(isinstance(entry[x], str) and entry[x].strip() for x in ("id", "machine_ref")):
|
||||
raise CalculationConfigError("id and machine_ref must be non-empty strings")
|
||||
channels = []
|
||||
extruders = entry["extruders"]
|
||||
if not isinstance(extruders, list) or not extruders:
|
||||
raise CalculationConfigError("extruders must be a non-empty list")
|
||||
for extruder in extruders:
|
||||
if not isinstance(extruder, dict) or set(extruder) != {
|
||||
"id",
|
||||
"total_rate_signal_ref",
|
||||
"channels",
|
||||
}:
|
||||
raise CalculationConfigError("Invalid extruder fields")
|
||||
if not all(
|
||||
isinstance(extruder[x], str) and extruder[x].strip()
|
||||
for x in ("id", "total_rate_signal_ref")
|
||||
):
|
||||
raise CalculationConfigError("Extruder id and total signal must be strings")
|
||||
rows = extruder["channels"]
|
||||
if not isinstance(rows, list) or len(rows) != 7:
|
||||
raise CalculationConfigError("Each extruder requires exactly seven channels")
|
||||
for row in rows:
|
||||
allowed = {
|
||||
"id",
|
||||
"status_signal_ref",
|
||||
"percentage_signal_ref",
|
||||
"screw_speed_signal_ref",
|
||||
"material_number_signal_ref",
|
||||
}
|
||||
if (
|
||||
not isinstance(row, dict)
|
||||
or set(row) - allowed
|
||||
or {"id", "status_signal_ref", "percentage_signal_ref", "screw_speed_signal_ref"}
|
||||
- set(row)
|
||||
):
|
||||
raise CalculationConfigError("Invalid dosing channel fields")
|
||||
if not all(
|
||||
isinstance(row[x], str) and row[x].strip()
|
||||
for x in (
|
||||
"id",
|
||||
"status_signal_ref",
|
||||
"percentage_signal_ref",
|
||||
"screw_speed_signal_ref",
|
||||
)
|
||||
):
|
||||
raise CalculationConfigError("Dosing signal references must be non-empty strings")
|
||||
material = row.get("material_number_signal_ref")
|
||||
if material is not None and (not isinstance(material, str) or not material.strip()):
|
||||
raise CalculationConfigError("material signal must be a string or null")
|
||||
channels.append(
|
||||
ChannelDefinition(
|
||||
extruder["id"],
|
||||
row["id"],
|
||||
extruder["total_rate_signal_ref"],
|
||||
row["status_signal_ref"],
|
||||
row["percentage_signal_ref"],
|
||||
row["screw_speed_signal_ref"],
|
||||
material,
|
||||
)
|
||||
)
|
||||
if len({(c.extruder, c.channel) for c in channels}) != len(channels):
|
||||
raise CalculationConfigError("Extruder/channel identities must be unique")
|
||||
names = entry.get("material_names", {})
|
||||
if not isinstance(names, dict) or any(
|
||||
not isinstance(k, str) or not isinstance(v, str) for k, v in names.items()
|
||||
):
|
||||
raise CalculationConfigError("material_names must map strings to strings")
|
||||
return ChannelMaterialCalculationConfig(
|
||||
entry["id"],
|
||||
entry["machine_ref"],
|
||||
finite_number(entry["max_sample_gap_seconds"], "max_sample_gap_seconds", positive=True),
|
||||
finite_number(entry["percentage_tolerance"], "percentage_tolerance"),
|
||||
tuple(channels),
|
||||
names,
|
||||
)
|
||||
Reference in New Issue
Block a user