From 0663b94f32cfc8caac64e9e1620a80f679d2b379 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 10:26:25 -0700 Subject: [PATCH 01/12] Restore machine_maintenance template on real MANUFACTURING.PUBLIC data Restores v1/machine_maintenance (removed in #67), now backed by the real 50-machine MANUFACTURING.PUBLIC dataset (50 machines, 20 technicians, 8 products, 12 periods, 3 plants). Runbook reframed around the 13 reasoner- workflow eval questions; querying (Q1-5, Q7) and rules (Q9) verified against the real data and reproduce the eval's expected answers exactly. Graph, predictive, and prescriptive stages plus the script rebuild are in progress. --- v1/machine_maintenance/README.md | 588 ++++ v1/machine_maintenance/data/availability.csv | 241 ++ v1/machine_maintenance/data/degradation.csv | 6 + .../data/downtime_events.csv | 354 +++ .../data/failure_predictions.csv | 601 +++++ v1/machine_maintenance/data/fault_types.csv | 16 + .../data/machine_product_capabilities.csv | 121 + v1/machine_maintenance/data/machines.csv | 51 + .../data/production_runs.csv | 845 ++++++ v1/machine_maintenance/data/products.csv | 9 + .../data/qualifications.csv | 33 + .../data/sensor_readings.csv | 2401 +++++++++++++++++ v1/machine_maintenance/data/sensors.csv | 201 ++ v1/machine_maintenance/data/technicians.csv | 21 + .../data/training_options.csv | 42 + v1/machine_maintenance/data/travel.csv | 10 + v1/machine_maintenance/machine_maintenance.py | 1717 ++++++++++++ v1/machine_maintenance/pyproject.toml | 17 + v1/machine_maintenance/runbook.md | 166 ++ 19 files changed, 7440 insertions(+) create mode 100644 v1/machine_maintenance/README.md create mode 100644 v1/machine_maintenance/data/availability.csv create mode 100644 v1/machine_maintenance/data/degradation.csv create mode 100644 v1/machine_maintenance/data/downtime_events.csv create mode 100644 v1/machine_maintenance/data/failure_predictions.csv create mode 100644 v1/machine_maintenance/data/fault_types.csv create mode 100644 v1/machine_maintenance/data/machine_product_capabilities.csv create mode 100644 v1/machine_maintenance/data/machines.csv create mode 100644 v1/machine_maintenance/data/production_runs.csv create mode 100644 v1/machine_maintenance/data/products.csv create mode 100644 v1/machine_maintenance/data/qualifications.csv create mode 100644 v1/machine_maintenance/data/sensor_readings.csv create mode 100644 v1/machine_maintenance/data/sensors.csv create mode 100644 v1/machine_maintenance/data/technicians.csv create mode 100644 v1/machine_maintenance/data/training_options.csv create mode 100644 v1/machine_maintenance/data/travel.csv create mode 100644 v1/machine_maintenance/machine_maintenance.py create mode 100644 v1/machine_maintenance/pyproject.toml create mode 100644 v1/machine_maintenance/runbook.md diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md new file mode 100644 index 00000000..b117961f --- /dev/null +++ b/v1/machine_maintenance/README.md @@ -0,0 +1,588 @@ +--- +title: "Machine Maintenance" +description: "A multi-reasoner template that chains querying, graph analysis, rules-based classification, and prescriptive optimization to schedule preventive maintenance, surface hidden operational risk, and recommend cross-training to eliminate concentration vulnerabilities." +featured: false +experience_level: intermediate +industry: "Manufacturing" +reasoning_types: + - Graph + - Rules-based + - Prescriptive +tags: + - Multi-Reasoner + - Chained Reasoning + - Scheduling + - Maintenance + - Manufacturing + - Assignment + - OEE + - Sensor Anomalies + - Risk Classification +--- + +# Machine Maintenance + +## What this template is for + +Manufacturing facilities must schedule preventive maintenance for machines with ML-predicted failure probabilities. The challenge is that surface-level metrics (like OEE) can mask structural vulnerabilities -- a plant that looks mid-tier on performance may actually carry the highest concentration risk, discoverable only by chaining multiple analytical layers. + +This template uses RelationalAI's **querying**, **graph analysis**, **rules-based classification**, and **prescriptive reasoning (optimization)** capabilities in a five-stage multi-reasoner workflow: + +1. **Querying** computes OEE by facility, surfaces sensor anomalies, and identifies machines with the steepest failure degradation trajectories. Plant_B looks worst at 61.4% OEE -- but Plant_A, at 68.2%, has 7 of 9 sensor anomalies and the 3 steepest degradation curves. +2. **Graph analysis** builds a machine dependency graph from shared-technician qualifications. All 30 machines form a single connected cluster, and Pump-type machines score highest on betweenness centrality (24.0) as the most constrained scheduling bottlenecks. +3. **Rules** derive seven compliance flags and chain three of them (chronic downtime, high-risk, overdue) into a composite risk tier. M013 (Pump, Plant_A) is the only Critical-tier machine -- it triggers all three flags. +4. **Prescriptive optimization** schedules 20 maintenance jobs across 4 periods at $605K total cost, assigning qualified technicians. The optimizer consumes per-period failure predictions from Stage 0, betweenness centrality from Stage 1, and overdue-maintenance flags from Stage 2. +5. **Resilience analysis** reveals that all 3 Turbine-qualified technicians are in Houston_TX, forcing 67% of scheduled Turbine jobs to require travel. Cross-training T006 (Senior, Chicago_IL) for $3,200 over 5 weeks eliminates this geographic concentration risk. + +Each stage enriches the shared ontology, and downstream stages consume those enrichments -- this is the **accretive ontology enrichment** pattern. No Python dicts carry state between stages; the ontology is the single source of truth: + +- **Stage 0 writes** `Machine.performance_ratio`, `Machine.quality_ratio`, `Machine.anomaly_count`, `MachinePeriod.predicted_fp` -- consumed by Stage 2's rules AND Stage 3's objective. Both downstream reasoners see the same derived signals. +- **Stage 1 writes** `Machine.betweenness` (normalized centrality) -- consumed by Stage 3's failure cost term. Bottleneck machines are more expensive to leave vulnerable. +- **Stage 2 writes** `Machine.is_overdue_maintenance`, `Machine.is_high_risk`, `Machine.is_chronic_downtime`, `Machine.risk_tier` -- the overdue flag feeds a hard scheduling constraint in Stage 3 (overdue machines must be maintained by period 2). +- **Stage 3 writes** `x_maintain`, `x_vulnerable`, `x_assigned` (decision variables) -- parsed in Stage 4 to analyze technician utilization and concentration risk. + +### Reasoner overview + +| Stage | Reasoner | Reads from ontology | Writes to ontology | Role | +|-------|----------|---------------------|--------------------|------| +| 0 | Querying | ProductionRun, SensorReading, FailurePrediction | Machine.performance_ratio, Machine.quality_ratio, Machine.anomaly_count, MachinePeriod.predicted_fp | Plant_C leads at 79.8% OEE; Plant_A mid at 68.2% but has 7 of 9 sensor anomalies and the 3 steepest failure trajectories (M001 +0.230, M013 +0.228, M016 +0.219). | +| 1 | Graph | Qualification, Machine (as `node_concept`) | Machine.betweenness (normalized centrality) | All 30 machines form 1 connected cluster. Pump-type machines are the top bottlenecks (betweenness=24.0). Centrality scores feed the failure cost multiplier in Stage 3. | +| 2 | Rules | Machine (all derived properties from Stages 0-1) | Machine.is_overdue_maintenance, Machine.is_high_risk, Machine.is_chronic_downtime, Machine.risk_tier | 6 overdue, 1 high-risk, 3 chronic downtime. Composite tier: M013 is Critical (all 3 flags), M016 is Elevated (2 of 3). Overdue flag becomes a hard constraint in Stage 3. | +| 3 | Prescriptive | MachinePeriod.predicted_fp, Machine.betweenness, Machine.is_overdue_maintenance | x_maintain, x_vulnerable, x_assigned (decision variables) | 20 jobs across 4 periods at $605K total cost. Per-period failure predictions (not static probability) weight the objective. Overdue machines scheduled by period 2. | +| 4 | Analysis | Solution variables, Qualification, TrainingOption | (terminal -- prints recommendations) | All 3 Turbine techs in Houston_TX -- 67% of Turbine jobs require travel. Best cross-training: T006 (Chicago_IL, Senior) at $3,200 / 5 weeks. | + +## Why this problem matters + +OEE dashboards and failure-probability rankings are how most plants prioritize maintenance today. But these metrics evaluate machines in isolation -- they miss structural dependencies between machines, technicians, and locations that create cascading risk. A plant where all Turbine-qualified technicians happen to work from the same office looks fine on every individual metric. The concentration risk is invisible until someone leaves, a certification expires, or a weather event disrupts the location -- at which point multiple machines lose coverage simultaneously. + +The multi-reasoner approach is necessary because no single analytical technique surfaces this risk. Querying reveals sensor anomalies that OEE masks. Graph analysis exposes which machines share technician pools. Rules chain individual flags into composite risk tiers. Optimization produces a schedule, and resilience analysis stress-tests that schedule against the qualification structure. Each layer reveals something the previous one missed. + +### Key design patterns demonstrated + +- **Accretive ontology enrichment** -- each stage writes derived properties (betweenness, risk_tier, predicted_fp) that downstream stages consume, building a progressively richer model +- **Rules chaining** -- three boolean flags (is_chronic_downtime, is_high_risk, is_overdue_maintenance) are composed into a single risk_tier property using exhaustive enumeration with `model.not_()` +- **Graph directly on domain concept** -- the Graph reasoner uses `Machine` directly as `node_concept`, so centrality scores are stored as Machine properties without a mirror concept +- **Per-period failure predictions** -- the optimization objective uses `MachinePeriod.predicted_fp` (period-specific) rather than static `Machine.failure_probability`, giving the solver time-varying cost information +- **Post-solve resilience analysis** -- Stage 4 inspects the solution and qualification structure to identify concentration risk, producing actionable cross-training recommendations without re-solving + +## Who this is for + +- Manufacturing and plant managers scheduling preventive maintenance +- Operations researchers exploring multi-reasoner pipelines in RelationalAI +- Developers learning how to chain querying, graph, rules, and optimization in a single model + +## What you'll build + +- Machine-level production aggregates, OEE components, and anomaly counts as derived properties +- A machine dependency graph with cluster detection and centrality scoring +- Seven compliance rules as derived Relationships and Properties, including a composite risk tier that chains three boolean flags +- Binary decision variables for maintenance timing, vulnerability tracking, and technician assignment +- Cumulative coverage, capacity, and overdue-deadline constraints +- A cost minimization objective that incorporates per-period failure predictions and graph centrality +- Geographic concentration risk analysis with cross-training recommendations + +## What's included + +- `machine_maintenance.py` -- Main script with five chained reasoning stages +- `data/machines.csv` -- 30 machines with failure probability, criticality (1-5), duration, and parts cost +- `data/technicians.csv` -- 10 technicians with skill levels, certifications, hourly rates, and locations +- `data/availability.csv` -- Technician availability across the 4-period planning horizon +- `data/qualifications.csv` -- Mapping of which technicians can service which machine types +- `data/parts_inventory.csv` -- Spare parts stock levels at each facility +- `data/certification_expiry.csv` -- Days remaining on technician certifications per machine type +- `data/sensors.csv` -- 60 sensors (2 per machine) with warning and critical thresholds +- `data/sensor_readings.csv` -- 240 periodic sensor measurements with anomaly flags +- `data/failure_predictions.csv` -- 120 per-period failure probability trajectories with predicted failure modes +- `data/downtime_events.csv` -- 129 downtime events with fault categories and durations +- `data/production_runs.csv` -- 120 production runs with planned, actual, and good quantities +- `data/training_options.csv` -- 13 cross-training options with costs and durations +- `pyproject.toml` -- Python project configuration with dependencies + +## Prerequisites + +### Access +- A Snowflake account that has the RAI Native App installed. +- A Snowflake user with permissions to access the RAI Native App. + +### Tools +- Python >= 3.10 +- RelationalAI Python SDK (`relationalai`) >= 1.0.14 + +## Quickstart + +1. Download the ZIP file for this template and extract it: + + ```bash + curl -O https://docs.relational.ai/templates/zips/v1/machine_maintenance.zip + unzip machine_maintenance.zip + cd machine_maintenance + ``` + + > [!TIP] + > You can also download the template ZIP using the "Download ZIP" button at the top of this page. + +2. Create a virtual environment and activate it: + + ```bash + python -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + ``` + +3. Install dependencies: + + ```bash + python -m pip install . + ``` + +4. Configure your RAI connection: + + ```bash + rai init + ``` + +5. Run the template: + + ```bash + python machine_maintenance.py + ``` + +6. Expected output: + ```text + ====================================================================== + STAGE 0: Querying -- Operational Intelligence + ====================================================================== + + OEE proxy by facility (Performance x Quality): + Plant_C: Perf=81.3%, Qual=98.1%, OEE=79.8% + Plant_A: Perf=69.8%, Qual=97.8%, OEE=68.2% + Plant_B: Perf=62.6%, Qual=98.1%, OEE=61.4% + + Sensor anomalies (9 readings across 5 machines): + M013 (Pump, Plant_A): 3 anomalies + M001 (Turbine, Plant_A): 2 anomalies + M016 (Turbine, Plant_A): 2 anomalies + M002 (Compressor, Plant_B): 1 anomalies + M006 (Turbine, Plant_C): 1 anomalies + By facility: {'Plant_A': 7, 'Plant_B': 1, 'Plant_C': 1} + + Steepest failure trajectories (period 1 -> 4): + M001 (Turbine, Plant_A): 0.102 -> 0.332 (+0.230) [bearing_wear] + M013 (Pump, Plant_A): 0.435 -> 0.663 (+0.228) [impeller_erosion] + M016 (Turbine, Plant_A): 0.263 -> 0.482 (+0.219) [bearing_wear] + ... + + ====================================================================== + STAGE 1: Graph Analysis -- Dependency Clusters & Centrality + ====================================================================== + + Dependency clusters found: 1 + + Top bottleneck machines (betweenness centrality): + M003 (Pump, Plant_C): betweenness=24.0000, failure_prob=0.089 + M008 (Pump, Plant_B): betweenness=24.0000, failure_prob=0.076 + M013 (Pump, Plant_A): betweenness=24.0000, failure_prob=0.435 + ... + + ====================================================================== + STAGE 2: Rules -- Compliance Flags & Composite Risk Tier + ====================================================================== + + Overdue maintenance (6 machines): + M002 (Compressor_Beta_1): RUL=3.7h < duration=6h + M006 (Turbine_Alpha_2): RUL=3.4h < duration=8h + M013 (Pump_Gamma_3): RUL=2.3h < duration=4h + ... + + High-risk machines (1): + M013 (Pump_Gamma_3): prob=0.435, crit=4 + + Anomalous machines (5): + M013 (Pump_Gamma_3, Plant_A): 3 anomalies + M001 (Turbine_Alpha_1, Plant_A): 2 anomalies + M016 (Turbine_Alpha_4, Plant_A): 2 anomalies + ... + + Chronic downtime machines (>8 events, 3 machines): + M001 (Turbine_Alpha_1, Plant_A): 12 events, 1635 min total downtime + M016 (Turbine_Alpha_4, Plant_A): 11 events, 1314 min total downtime + M013 (Pump_Gamma_3, Plant_A): 10 events, 1272 min total downtime + + Composite risk tier: + Critical (1): M013 + Elevated (1): M016 + Standard (28): M001, M002, ... + + Parts needing reorder (4): + P001 (Spindle Bearings, Plant_A): stock=25 <= min_order=50 + ... + + Expiring certifications (5): + T001 (Alice_Johnson): Compressor -- 22 days remaining + T004 (Diana_Chen): Pump -- 8 days remaining + ... + + ====================================================================== + STAGE 3: Prescriptive -- Maintenance Scheduling + ====================================================================== + + Status: OPTIMAL + Objective value: 605240.61 + + Maintenance schedule (20 jobs): + Period 1: + M002 (Compressor, Plant_B, crit=5) + M006 (Turbine, Plant_C, crit=5) + M013 (Pump, Plant_A, crit=4) + M016 (Turbine, Plant_A, crit=3) + ... + Period 2: ... + Period 3: ... + Period 4: ... + + Technician assignments (20): + Period 1: + M002: T003 (6h x $65/h = $390) [TRAVEL] + M013: T006 (4h x $88/h = $352) [TRAVEL] + ... + + ====================================================================== + STAGE 4: Resilience -- Concentration Risk Analysis + ====================================================================== + + Technician utilization in optimal schedule: + T003 (Charlie_Brown, Junior, Houston_TX): 5 assignments (25%) + T004 (Diana_Chen, Junior, Chicago_IL): 5 assignments (25%) + ... + + Qualification coverage by machine type: + Compressor: 3 techs in Chicago_IL, Houston_TX -- OK + Generator: 3 techs in Chicago_IL, Phoenix_AZ -- OK + Motor: 4 techs in Chicago_IL, Phoenix_AZ -- OK + Pump: 3 techs in Chicago_IL, Phoenix_AZ -- OK + Turbine: 3 techs in Houston_TX -- CONCENTRATED -- all 3 techs in Houston_TX + + Concentration risk detail: + + Turbine: all 3 qualified techs in Houston_TX + Scheduled Turbine jobs: 3, of which 2 require travel (67%) + + ====================================================================== + RECOMMENDATION: Cross-Training to Eliminate Concentration Risk + ====================================================================== + + Turbine -- add coverage outside Houston_TX: + Best candidate: T006 (Fiona_Garcia): $3,200, 5 weeks + ``` + +## Template structure + +```text +. +├── README.md +├── pyproject.toml +├── machine_maintenance.py +└── data/ + ├── machines.csv + ├── technicians.csv + ├── availability.csv + ├── qualifications.csv + ├── parts_inventory.csv + ├── certification_expiry.csv + ├── sensors.csv + ├── sensor_readings.csv + ├── failure_predictions.csv + ├── downtime_events.csv + ├── production_runs.csv + └── training_options.csv +``` + +## How it works + +This section walks through the highlights in `machine_maintenance.py`. + +### Define concepts and load CSV data + +The model defines concepts for machines (with ML-predicted failure probability and numeric criticality), technicians (with skills and hourly rates), qualifications linking technicians to machine types, parts inventory, certification expiry, sensors, sensor readings, failure predictions, downtime events, and production runs. All data is loaded from CSV files: + +```python +Machine = model.Concept("Machine", identify_by={"machine_id": String}) +Machine.failure_probability = model.Property( + f"{Machine} has failure probability {Float:failure_probability}") +Machine.criticality = model.Property(f"{Machine} has criticality {Integer:criticality}") + +Technician = model.Concept("Technician", identify_by={"technician_id": String}) +Qualification = model.Concept( + "Qualification", identify_by={"technician_id": String, "machine_type": String}) + +Sensor = model.Concept("Sensor", identify_by={"sensor_id": String}) +SensorReading = model.Concept( + "SensorReading", + identify_by={"sensor_id": String, "machine_id": String, "pid": Integer}) +FailurePrediction = model.Concept( + "FailurePrediction", identify_by={"prediction_id": String}) +DowntimeEvent = model.Concept("DowntimeEvent", identify_by={"event_id": String}) +ProductionRun = model.Concept("ProductionRun", identify_by={"run_id": String}) +``` + +Machine-level derived aggregates are computed from the loaded data using `aggs.sum` and `aggs.count`, providing production ratios, downtime counts, and anomaly counts as derived properties: + +```python +Machine.total_planned_qty = model.Property( + f"{Machine} has total planned qty {Float:total_planned_qty}") +model.define(Machine.total_planned_qty( + aggs.sum(ProductionRun.planned_quantity).per(Machine) + .where(ProductionRun.machine(Machine)) | 0 +)) + +model.where(Machine.total_planned_qty > 0).define( + Machine.performance_ratio( + floats.float(Machine.total_actual_qty) + / floats.float(Machine.total_planned_qty) + ) +) +``` + +Cross-product concepts define the scheduling decision space. `MachinePeriod` pairs each machine with each planning period and stores per-period failure predictions. `TechnicianMachinePeriod` is restricted to qualified pairs -- technicians can only be assigned to machine types they are certified for: + +```python +MachinePeriod.predicted_fp = model.Property( + f"{MachinePeriod} has predicted failure probability {Float:predicted_fp}") +FPJoin = FailurePrediction.ref() +model.where( + MachinePeriod.machine_id == FPJoin.machine_id_str, + MachinePeriod.pid == FPJoin.period_int, +).define(MachinePeriod.predicted_fp(FPJoin.failure_probability)) +``` + +### Stage 0: Querying -- operational intelligence + +The querying stage computes OEE proxy (Performance x Quality) by facility, surfaces machines with above-threshold sensor readings, and identifies the steepest failure degradation trajectories. All queries use `model.select` with derived properties: + +```python +oee_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.facility.alias("facility"), + Machine.performance_ratio.alias("performance"), + Machine.quality_ratio.alias("quality"), + ) + .to_df() +) +``` + +### Stage 1: Graph -- dependency clusters and centrality + +The Graph reasoner uses `Machine` directly as `node_concept` -- no mirror concept needed. Edges connect machines when at least one technician is qualified for both machine types: + +```python +dep_graph = Graph( + model, directed=False, weighted=False, node_concept=Machine, aggregator="sum" +) +``` + +Weakly connected components identify dependency clusters (groups of machines that compete for the same technicians). Betweenness centrality scores bottleneck machines -- those whose maintenance blocks the most scheduling options. The scores are normalized and stored directly on `Machine`: + +```python +Machine.betweenness_raw = model.Property( + f"{Machine} has raw betweenness centrality {Float:betweenness_raw}") +m_btwn = Machine.ref("m_btwn") +model.define(m_btwn.betweenness_raw(btwn_score)).where(betweenness(m_btwn, btwn_score)) +max_betweenness = max(Machine.betweenness_raw) +Machine.betweenness = model.Property( + f"{Machine} has betweenness centrality {Float:betweenness}") +m_norm = Machine.ref("m_norm") +model.where(max_betweenness == 0).define(m_norm.betweenness(0.0)) +model.where(max_betweenness > 0).define( + m_norm.betweenness(m_norm.betweenness_raw / max_betweenness) +) +``` + +### Stage 2: Rules -- compliance flags and composite risk tier + +Seven derived Relationships and Properties flag compliance issues. Each rule is a pure logic derivation using `model.where(...).define(...)`: + +```python +Machine.is_overdue_maintenance = model.Relationship( + f"{Machine} is overdue maintenance") +model.where( + Machine.remaining_useful_life < floats.float(Machine.maintenance_duration_hours) +).define(Machine.is_overdue_maintenance()) + +Machine.is_chronic_downtime = model.Relationship(f"{Machine} has chronic downtime") +model.where( + Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD +).define(Machine.is_chronic_downtime()) +``` + +Individual flags are chained into a composite risk tier using `model.not_()` for negation. This exhaustively enumerates all eight combinations of three boolean flags: + +```python +Machine.risk_tier = model.Property(f"{Machine} has risk tier {String:risk_tier}") + +# Critical: all 3 flags. +model.where( + Machine.is_chronic_downtime(), + Machine.is_high_risk(), + Machine.is_overdue_maintenance(), +).define(Machine.risk_tier("Critical")) + +# Elevated: exactly 2 of 3 flags. +model.where( + Machine.is_chronic_downtime(), + Machine.is_high_risk(), + model.not_(Machine.is_overdue_maintenance()), +).define(Machine.risk_tier("Elevated")) +``` + +### Stage 3: Define decision variables, constraints, and objective + +Three binary decision variables control the schedule: whether to maintain a machine in a period, whether it remains vulnerable, and whether a technician is assigned. The formulation includes four standard constraints (cumulative coverage, assignment linkage, technician capacity, parts/bay capacity) plus a hard constraint from Stage 2's overdue flag: + +```python +maintained_by_deadline = ( + sum(MachinePeriod_overdue.x_maintain) + .where( + MachinePeriod_overdue.machine(Machine_overdue), + MachinePeriod_overdue.period(Period_overdue), + Period_overdue.pid <= OVERDUE_DEADLINE, + ) + .per(Machine_overdue) +) +problem.satisfy( + model.require(maintained_by_deadline >= 1).where( + Machine_overdue.is_overdue_maintenance() + ) +) +``` + +The objective minimizes expected total cost with three components. The failure cost term incorporates per-period failure predictions from Stage 0 and betweenness centrality from Stage 1, making it more expensive to leave bottleneck machines vulnerable in periods where their predicted failure probability is highest: + +```python +failure_cost = sum( + MachinePeriod_outer.x_vulnerable + * MachinePeriod_outer.predicted_fp + * Machine_obj.estimated_parts_cost + * Machine_obj.criticality + * (1 + CENTRALITY_WEIGHT * Machine_obj.betweenness) +).where( + MachinePeriod_outer.machine(Machine_obj), MachinePeriod_outer.period(Period_outer) +) +``` + +### Solve and extract results + +The model is solved using the HiGHS solver with a two-minute time limit. Assignment decisions are parsed from the solution to build the maintenance schedule: + +```python +problem.solve("highs", time_limit_sec=120) +si = problem.solve_info() +assert si.termination_status == "OPTIMAL" +``` + +### Stage 4: Resilience analysis and cross-training + +After solving, the script analyzes qualification coverage by machine type and location. For each machine type, it checks whether all qualified technicians are concentrated in a single location -- a geographic single-point-of-failure invisible to the optimizer: + +```python +for mtype in machine_types: + qual_techs = qualifications_df[ + qualifications_df["machine_type"] == mtype + ]["technician_id"].tolist() + tech_info = technicians_df[technicians_df["technician_id"].isin(qual_techs)] + locations = tech_info["base_location"].unique().tolist() + if len(locations) == 1: + concentrated_types.append((mtype, locations[0], len(qual_techs))) +``` + +For concentrated types, the script queries `training_options.csv` to recommend the cheapest candidate at a different location, producing a specific, costed action item (e.g., "Cross-train T006 for Turbine at $3,200 / 5 weeks"). + +### Stage 5: Inspect the model schema (post-pipeline) + +Templates that chain multiple reasoners over a rich ontology benefit from a quick schema dump once the pipeline has run. `relationalai.semantics.inspect` (available in `relationalai>=1.0.14`) returns a typed view of every registered concept, property, and relationship -- handy for confirming that decision variables, derived aggregates, and cross-product concepts all registered correctly across the five-stage pipeline. + +Once `Problem(...)` plus `solve_for` / `satisfy` / `minimize` / `maximize` have run in Stage 3, the prescriptive reasoner registers root concepts named `Variable`, `Expression`, `Constraint`, and `Objective` (plus per-solve `Variable_` / `Constraint_` / `Objective_` subconcepts) on the shared model. Graph(model, node_concept=Machine) from Stage 1 also registers an edge concept (e.g. `graph_Edge`). These are noise for user-facing introspection, so filter them out by exact name and reasoner-name prefix -- an underscore check won't catch them since the names don't start with `_`: + +```python +from relationalai.semantics import inspect + +schema = inspect.schema(model) +reasoner_names = {"Variable", "Expression", "Constraint", "Objective"} +user_concepts = [ + c for c in schema.concepts + if c.name not in reasoner_names + and not any(c.name.startswith(p + "_") for p in reasoner_names) + and not c.name.startswith("graph") # Graph reasoner registers e.g. graph_Edge +] +print(f"User concepts: {len(user_concepts)}") +for c in user_concepts: + print(f" {c.name}: {len(c.properties)} properties, {len(c.relationships)} relationships") +``` + +Print `inspect.schema(model)` first to see the actual names registered in your model, then extend the filter list if other reasoner-owned concepts appear. + +## Customize this template + +- **Adjust centrality weight** via `CENTRALITY_WEIGHT` to control how strongly graph bottleneck scores influence scheduling priority. +- **Change the overdue deadline** via `OVERDUE_DEADLINE` to give more or fewer periods for overdue machines. +- **Extend the planning horizon** by adding more periods to the availability and failure prediction data and increasing `PERIOD_HORIZON`. +- **Adjust capacity limits** via `PARTS_CAPACITY_PER_PERIOD` to see how tighter constraints shift scheduling priorities. +- **Tune travel cost** via `TRAVEL_COST_PER_HOUR` to control preference for local vs. cross-facility assignments. +- **Add rule thresholds** -- adjust `failure_probability > 0.3` or `criticality >= 4` in the high-risk rule to match your risk tolerance. +- **Change chronic downtime threshold** via `CHRONIC_DOWNTIME_THRESHOLD` to control which machines are flagged. +- **Add sensor types** -- extend `sensors.csv` with new sensor types and adjust `sensor_readings.csv` with corresponding measurements. +- **Add training options** -- extend `training_options.csv` to explore different cross-training strategies. + +## Troubleshooting + +
+Status: INFEASIBLE + +- The overdue-maintenance constraint requires certain machines to be scheduled in early periods. If technician capacity is too tight, this can cause infeasibility. +- Try increasing `OVERDUE_DEADLINE` from 2 to 3, or increase `PARTS_CAPACITY_PER_PERIOD`. +- Check that technician hours capacity across all periods can accommodate all machines. +
+ +
+All machines maintained in period 1 + +- The solver minimizes total cost. If capacity allows, it may schedule all maintenance early to avoid vulnerability costs. +- Tighten `PARTS_CAPACITY_PER_PERIOD` to spread maintenance across periods. +
+ +
+Graph shows 0 edges + +- This means no two machines share a qualified technician. Check that `qualifications.csv` has overlapping machine types across technicians. +- The graph edge construction uses type-based joins: two machines connect if any technician is qualified for both their `machine_type` values. +
+ +
+input definition is too large + +- This occurs with large cross-products. The qualification-filtered assignment space avoids this issue for the default 30-machine dataset. +- If you scale up significantly, consider reducing data size or querying solver + results via `Variable.values(...)` instead of broad `model.select(...)` + patterns. +
+ +
+ModuleNotFoundError + +- Make sure you activated the virtual environment and ran `python -m pip install .` from the template directory. +- The `pyproject.toml` declares the required dependencies. +
+ +
+Connection or authentication errors + +- Run `rai init` to configure your Snowflake connection. +- Verify that the RAI Native App is installed and your user has the required permissions. +
+ +
+No concentration risk detected in Stage 4 + +- This means all machine types have qualified technicians in multiple locations. The resilience analysis examines geographic diversity of the qualification pool, not individual assignment redundancy. +- Try modifying `qualifications.csv` to concentrate a machine type's technicians in one location to see how the analysis surfaces this risk. +
diff --git a/v1/machine_maintenance/data/availability.csv b/v1/machine_maintenance/data/availability.csv new file mode 100644 index 00000000..bfab277e --- /dev/null +++ b/v1/machine_maintenance/data/availability.csv @@ -0,0 +1,241 @@ +technician_id,period,available,availability_reason +T001,1,0.0,Vacation +T001,2,1.0,Available +T001,3,0.5,Training +T001,4,0.0,Training +T001,5,0.5,Training +T001,6,1.0,Available +T001,7,0.5,Onboarding +T001,8,0.8,Vacation +T001,9,0.8,Onboarding +T001,10,1.0,Available +T001,11,0.8,Onboarding +T001,12,0.5,Onboarding +T002,1,1.0,Available +T002,2,0.8,Onboarding +T002,3,1.0,Available +T002,4,0.8,Training +T002,5,1.0,Available +T002,6,1.0,Available +T002,7,1.0,Available +T002,8,1.0,Available +T002,9,1.0,Available +T002,10,0.8,Onboarding +T002,11,1.0,Available +T002,12,1.0,Available +T003,1,0.0,Vacation +T003,2,1.0,Available +T003,3,1.0,Available +T003,4,0.5,Onboarding +T003,5,1.0,Available +T003,6,0.0,Onboarding +T003,7,1.0,Available +T003,8,1.0,Available +T003,9,1.0,Available +T003,10,0.8,Onboarding +T003,11,1.0,Available +T003,12,1.0,Available +T004,1,1.0,Available +T004,2,1.0,Available +T004,3,1.0,Available +T004,4,0.8,Training +T004,5,1.0,Available +T004,6,0.8,Vacation +T004,7,0.8,Vacation +T004,8,0.8,Training +T004,9,1.0,Available +T004,10,1.0,Available +T004,11,1.0,Available +T004,12,1.0,Available +T005,7,1.0,Available +T005,2,0.8,Vacation +T005,3,0.5,Vacation +T005,4,1.0,Available +T005,5,1.0,Available +T005,6,1.0,Available +T005,1,1.0,Available +T005,8,1.0,Available +T005,9,1.0,Available +T005,10,0.5,Vacation +T005,11,0.8,Training +T005,12,1.0,Available +T006,1,1.0,Available +T006,2,1.0,Available +T006,3,1.0,Available +T006,4,1.0,Available +T006,5,1.0,Available +T006,6,1.0,Available +T006,7,1.0,Available +T006,8,1.0,Available +T006,9,1.0,Available +T006,10,1.0,Available +T006,11,1.0,Available +T006,12,0.5,Vacation +T007,7,1.0,Available +T007,2,1.0,Available +T007,3,1.0,Available +T007,4,1.0,Available +T007,5,1.0,Available +T007,6,0.8,Training +T007,1,1.0,Available +T007,8,1.0,Available +T007,9,1.0,Available +T007,10,1.0,Available +T007,11,1.0,Available +T007,12,1.0,Available +T008,7,1.0,Available +T008,2,1.0,Available +T008,3,0.5,Vacation +T008,4,1.0,Available +T008,5,1.0,Available +T008,6,1.0,Available +T008,1,0.8,Training +T008,8,1.0,Available +T008,9,1.0,Available +T008,10,0.8,Training +T008,11,0.8,Training +T008,12,1.0,Available +T009,1,0.0,Vacation +T009,2,1.0,Available +T009,3,1.0,Available +T009,4,0.5,Training +T009,5,1.0,Available +T009,6,0.8,Vacation +T009,7,1.0,Available +T009,8,1.0,Available +T009,9,1.0,Available +T009,10,0.8,Onboarding +T009,11,1.0,Available +T009,12,0.8,Vacation +T010,1,1.0,Available +T010,2,0.5,Onboarding +T010,3,0.5,Vacation +T010,4,0.5,Onboarding +T010,5,1.0,Available +T010,6,1.0,Available +T010,7,1.0,Available +T010,8,0.8,Vacation +T010,9,1.0,Available +T010,10,1.0,Available +T010,11,1.0,Available +T010,12,0.8,Training +T011,1,1.0,Available +T011,2,1.0,Available +T011,3,1.0,Available +T011,4,1.0,Available +T011,5,1.0,Available +T011,6,1.0,Available +T011,7,0.8,Training +T011,8,1.0,Available +T011,9,1.0,Available +T011,10,1.0,Available +T011,11,1.0,Available +T011,12,0.8,Onboarding +T012,1,0.5,Vacation +T012,2,0.5,Training +T012,3,0.5,Vacation +T012,4,1.0,Available +T012,5,1.0,Available +T012,6,1.0,Available +T012,7,0.8,Onboarding +T012,8,1.0,Available +T012,9,0.8,Vacation +T012,10,0.5,Onboarding +T012,11,1.0,Available +T012,12,1.0,Available +T013,1,1.0,Available +T013,2,1.0,Available +T013,3,1.0,Available +T013,4,1.0,Available +T013,5,1.0,Available +T013,6,1.0,Available +T013,7,1.0,Available +T013,8,1.0,Available +T013,9,1.0,Available +T013,10,1.0,Available +T013,11,1.0,Available +T013,12,1.0,Available +T014,1,1.0,Available +T014,2,1.0,Available +T014,3,1.0,Available +T014,4,1.0,Available +T014,5,1.0,Available +T014,6,1.0,Available +T014,7,1.0,Available +T014,8,0.8,Onboarding +T014,9,1.0,Available +T014,10,0.0,Onboarding +T014,11,1.0,Available +T014,12,1.0,Available +T015,1,1.0,Available +T015,2,1.0,Available +T015,3,0.0,Onboarding +T015,4,1.0,Available +T015,5,1.0,Available +T015,6,0.0,Training +T015,7,0.8,Training +T015,8,1.0,Available +T015,9,1.0,Available +T015,10,1.0,Available +T015,11,1.0,Available +T015,12,1.0,Available +T016,1,1.0,Available +T016,2,1.0,Available +T016,3,1.0,Available +T016,4,0.8,Training +T016,5,1.0,Available +T016,6,0.0,Vacation +T016,7,1.0,Available +T016,8,0.8,Onboarding +T016,9,1.0,Available +T016,10,1.0,Available +T016,11,1.0,Available +T016,12,1.0,Available +T017,1,0.0,Onboarding +T017,2,0.5,Training +T017,3,1.0,Available +T017,4,1.0,Available +T017,5,1.0,Available +T017,6,1.0,Available +T017,7,0.8,Vacation +T017,8,1.0,Available +T017,9,1.0,Available +T017,10,0.0,Training +T017,11,1.0,Available +T017,12,1.0,Available +T018,1,1.0,Available +T018,2,1.0,Available +T018,3,1.0,Available +T018,4,0.8,Onboarding +T018,5,1.0,Available +T018,6,0.0,Vacation +T018,7,0.5,Onboarding +T018,8,0.8,Onboarding +T018,9,1.0,Available +T018,10,1.0,Available +T018,11,1.0,Available +T018,12,1.0,Available +T019,1,0.5,Onboarding +T019,2,1.0,Available +T019,3,0.8,Vacation +T019,4,1.0,Available +T019,5,0.5,Training +T019,6,1.0,Available +T019,7,1.0,Available +T019,8,0.8,Vacation +T019,9,1.0,Available +T019,10,1.0,Available +T019,11,1.0,Available +T019,12,0.5,Training +T020,1,1.0,Available +T020,2,1.0,Available +T020,3,1.0,Available +T020,4,1.0,Available +T020,5,1.0,Available +T020,6,0.5,Vacation +T020,7,1.0,Available +T020,8,0.5,Training +T020,9,1.0,Available +T020,10,1.0,Available +T020,11,1.0,Available +T020,12,1.0,Available diff --git a/v1/machine_maintenance/data/degradation.csv b/v1/machine_maintenance/data/degradation.csv new file mode 100644 index 00000000..6494e541 --- /dev/null +++ b/v1/machine_maintenance/data/degradation.csv @@ -0,0 +1,6 @@ +machine_type,degradation_rate_per_period,maintenance_reset_factor +Compressor,0.038,0.8 +Generator,0.042,0.9 +Motor,0.025,0.7 +Pump,0.03,0.75 +Turbine,0.045,0.85 diff --git a/v1/machine_maintenance/data/downtime_events.csv b/v1/machine_maintenance/data/downtime_events.csv new file mode 100644 index 00000000..7db4872c --- /dev/null +++ b/v1/machine_maintenance/data/downtime_events.csv @@ -0,0 +1,354 @@ +event_id,machine_id,shift_id,period,fault_type_id,fault_name,reason_l1,reason_l2,reason_l3,duration_minutes,is_planned +DT0001,M001,SHIFT_SWING,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,64,0 +DT0002,M001,SHIFT_DAY,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,50,0 +DT0003,M001,SHIFT_SWING,2,FT13,Corrosion,Material,Surface,Corrosion,61,0 +DT0004,M001,SHIFT_NIGHT,3,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,61,0 +DT0005,M001,SHIFT_SWING,3,FT10,Pressure Loss,Process,Fluid,Pressure Loss,44,0 +DT0006,M001,SHIFT_DAY,4,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,76,0 +DT0007,M001,SHIFT_DAY,4,FT13,Corrosion,Material,Surface,Corrosion,68,1 +DT0008,M001,SHIFT_NIGHT,4,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,87,0 +DT0009,M001,SHIFT_NIGHT,5,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,88,0 +DT0010,M001,SHIFT_DAY,5,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,34,1 +DT0011,M001,SHIFT_SWING,6,FT09,Overheating,Process,Thermal,Overheating,58,0 +DT0012,M001,SHIFT_SWING,6,FT09,Overheating,Process,Thermal,Overheating,28,0 +DT0013,M001,SHIFT_DAY,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,73,0 +DT0014,M001,SHIFT_DAY,7,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,117,0 +DT0015,M001,SHIFT_NIGHT,8,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,121,0 +DT0016,M001,SHIFT_SWING,8,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,71,0 +DT0017,M001,SHIFT_NIGHT,8,FT13,Corrosion,Material,Surface,Corrosion,67,0 +DT0018,M001,SHIFT_SWING,9,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,85,0 +DT0019,M001,SHIFT_DAY,9,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,84,0 +DT0020,M001,SHIFT_DAY,10,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,89,0 +DT0021,M001,SHIFT_NIGHT,11,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,91,0 +DT0022,M001,SHIFT_DAY,12,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,40,0 +DT0023,M001,SHIFT_DAY,12,FT09,Overheating,Process,Thermal,Overheating,75,0 +DT0024,M002,SHIFT_NIGHT,5,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,59,0 +DT0025,M002,SHIFT_NIGHT,8,FT09,Overheating,Process,Thermal,Overheating,115,1 +DT0026,M003,SHIFT_SWING,1,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,61,0 +DT0027,M003,SHIFT_DAY,2,FT10,Pressure Loss,Process,Fluid,Pressure Loss,33,0 +DT0028,M003,SHIFT_SWING,2,FT09,Overheating,Process,Thermal,Overheating,56,1 +DT0029,M003,SHIFT_SWING,3,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,20,0 +DT0030,M003,SHIFT_SWING,3,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,22,0 +DT0031,M003,SHIFT_DAY,3,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,23,0 +DT0032,M003,SHIFT_SWING,4,FT09,Overheating,Process,Thermal,Overheating,54,0 +DT0033,M003,SHIFT_DAY,5,FT09,Overheating,Process,Thermal,Overheating,38,0 +DT0034,M003,SHIFT_NIGHT,6,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,42,0 +DT0035,M003,SHIFT_SWING,7,FT10,Pressure Loss,Process,Fluid,Pressure Loss,27,0 +DT0036,M003,SHIFT_SWING,7,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,16,0 +DT0037,M003,SHIFT_SWING,8,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,30,0 +DT0038,M003,SHIFT_SWING,8,FT10,Pressure Loss,Process,Fluid,Pressure Loss,34,0 +DT0039,M003,SHIFT_NIGHT,8,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,34,0 +DT0040,M003,SHIFT_SWING,9,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,24,0 +DT0041,M003,SHIFT_DAY,9,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,30,0 +DT0042,M003,SHIFT_SWING,10,FT10,Pressure Loss,Process,Fluid,Pressure Loss,24,0 +DT0043,M003,SHIFT_DAY,10,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,18,0 +DT0044,M003,SHIFT_NIGHT,11,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,64,0 +DT0045,M003,SHIFT_SWING,12,FT10,Pressure Loss,Process,Fluid,Pressure Loss,40,0 +DT0046,M004,SHIFT_DAY,2,FT09,Overheating,Process,Thermal,Overheating,86,1 +DT0047,M004,SHIFT_DAY,7,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,57,0 +DT0048,M005,SHIFT_SWING,5,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,22,0 +DT0049,M006,SHIFT_NIGHT,1,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,42,1 +DT0050,M006,SHIFT_DAY,2,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,44,0 +DT0051,M006,SHIFT_SWING,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,34,0 +DT0052,M006,SHIFT_NIGHT,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,42,0 +DT0053,M006,SHIFT_DAY,4,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,55,0 +DT0054,M006,SHIFT_NIGHT,5,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,44,0 +DT0055,M006,SHIFT_DAY,6,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,47,1 +DT0056,M006,SHIFT_NIGHT,6,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,42,0 +DT0057,M006,SHIFT_NIGHT,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,78,0 +DT0058,M006,SHIFT_DAY,7,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,107,0 +DT0059,M006,SHIFT_DAY,7,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,88,0 +DT0060,M006,SHIFT_DAY,8,FT13,Corrosion,Material,Surface,Corrosion,27,0 +DT0061,M006,SHIFT_NIGHT,8,FT10,Pressure Loss,Process,Fluid,Pressure Loss,45,0 +DT0062,M006,SHIFT_SWING,9,FT13,Corrosion,Material,Surface,Corrosion,32,0 +DT0063,M006,SHIFT_SWING,9,FT09,Overheating,Process,Thermal,Overheating,63,1 +DT0064,M006,SHIFT_DAY,10,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,43,0 +DT0065,M006,SHIFT_NIGHT,10,FT09,Overheating,Process,Thermal,Overheating,47,1 +DT0066,M006,SHIFT_SWING,10,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,31,0 +DT0067,M006,SHIFT_DAY,11,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,80,0 +DT0068,M006,SHIFT_DAY,11,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,81,0 +DT0069,M006,SHIFT_SWING,12,FT03,Gear Wear,Mechanical,Drive Train,Gear Wear,94,0 +DT0070,M006,SHIFT_DAY,12,FT13,Corrosion,Material,Surface,Corrosion,51,0 +DT0071,M006,SHIFT_DAY,12,FT09,Overheating,Process,Thermal,Overheating,75,1 +DT0072,M007,SHIFT_DAY,3,FT13,Corrosion,Material,Surface,Corrosion,95,0 +DT0073,M007,SHIFT_SWING,4,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,94,1 +DT0074,M007,SHIFT_SWING,7,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,74,0 +DT0075,M008,SHIFT_DAY,5,FT09,Overheating,Process,Thermal,Overheating,27,1 +DT0076,M008,SHIFT_SWING,9,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,41,0 +DT0077,M008,SHIFT_SWING,11,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,64,0 +DT0078,M009,SHIFT_SWING,8,FT09,Overheating,Process,Thermal,Overheating,92,0 +DT0079,M009,SHIFT_SWING,11,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,68,0 +DT0080,M009,SHIFT_SWING,12,FT09,Overheating,Process,Thermal,Overheating,49,0 +DT0081,M010,SHIFT_NIGHT,1,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,43,0 +DT0082,M010,SHIFT_SWING,5,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,40,0 +DT0083,M010,SHIFT_SWING,9,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,69,0 +DT0084,M011,SHIFT_SWING,1,FT09,Overheating,Process,Thermal,Overheating,116,0 +DT0085,M011,SHIFT_NIGHT,1,FT10,Pressure Loss,Process,Fluid,Pressure Loss,39,0 +DT0086,M011,SHIFT_DAY,2,FT10,Pressure Loss,Process,Fluid,Pressure Loss,79,0 +DT0087,M011,SHIFT_SWING,3,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,58,0 +DT0088,M011,SHIFT_NIGHT,4,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,75,1 +DT0089,M011,SHIFT_SWING,4,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,66,0 +DT0090,M011,SHIFT_NIGHT,4,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,36,0 +DT0091,M011,SHIFT_DAY,5,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,81,0 +DT0092,M011,SHIFT_SWING,5,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,57,0 +DT0093,M011,SHIFT_DAY,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,130,0 +DT0094,M011,SHIFT_SWING,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,145,0 +DT0095,M011,SHIFT_NIGHT,6,FT10,Pressure Loss,Process,Fluid,Pressure Loss,85,0 +DT0096,M011,SHIFT_NIGHT,7,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,30,0 +DT0097,M011,SHIFT_SWING,8,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,79,0 +DT0098,M011,SHIFT_SWING,9,FT10,Pressure Loss,Process,Fluid,Pressure Loss,43,0 +DT0099,M011,SHIFT_DAY,10,FT09,Overheating,Process,Thermal,Overheating,96,0 +DT0100,M011,SHIFT_DAY,10,FT10,Pressure Loss,Process,Fluid,Pressure Loss,56,0 +DT0101,M011,SHIFT_SWING,11,FT08,Control Board,Electrical,Instrumentation,Control Board,79,0 +DT0102,M011,SHIFT_SWING,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,95,0 +DT0103,M011,SHIFT_DAY,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,33,0 +DT0104,M012,SHIFT_SWING,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,112,0 +DT0105,M012,SHIFT_NIGHT,7,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,37,0 +DT0106,M012,SHIFT_SWING,9,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,43,0 +DT0107,M013,SHIFT_SWING,1,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,30,0 +DT0108,M013,SHIFT_DAY,5,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,76,0 +DT0109,M013,SHIFT_SWING,6,FT10,Pressure Loss,Process,Fluid,Pressure Loss,67,0 +DT0110,M014,SHIFT_NIGHT,1,FT09,Overheating,Process,Thermal,Overheating,22,0 +DT0111,M014,SHIFT_SWING,1,FT08,Control Board,Electrical,Instrumentation,Control Board,49,0 +DT0112,M014,SHIFT_SWING,2,FT10,Pressure Loss,Process,Fluid,Pressure Loss,39,0 +DT0113,M014,SHIFT_DAY,3,FT09,Overheating,Process,Thermal,Overheating,51,0 +DT0114,M014,SHIFT_DAY,3,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,18,0 +DT0115,M014,SHIFT_SWING,4,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,36,0 +DT0116,M014,SHIFT_NIGHT,4,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,20,0 +DT0117,M014,SHIFT_DAY,5,FT08,Control Board,Electrical,Instrumentation,Control Board,47,0 +DT0118,M014,SHIFT_DAY,5,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,50,0 +DT0119,M014,SHIFT_DAY,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,63,0 +DT0120,M014,SHIFT_NIGHT,6,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,25,0 +DT0121,M014,SHIFT_NIGHT,6,FT10,Pressure Loss,Process,Fluid,Pressure Loss,36,1 +DT0122,M014,SHIFT_SWING,7,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,31,0 +DT0123,M014,SHIFT_DAY,7,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,28,0 +DT0124,M014,SHIFT_SWING,7,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,22,0 +DT0125,M014,SHIFT_DAY,8,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,32,0 +DT0126,M014,SHIFT_SWING,8,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,66,0 +DT0127,M014,SHIFT_NIGHT,8,FT09,Overheating,Process,Thermal,Overheating,55,0 +DT0128,M014,SHIFT_SWING,9,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,30,0 +DT0129,M014,SHIFT_NIGHT,9,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,37,0 +DT0130,M014,SHIFT_DAY,9,FT09,Overheating,Process,Thermal,Overheating,18,0 +DT0131,M014,SHIFT_DAY,10,FT09,Overheating,Process,Thermal,Overheating,46,0 +DT0132,M014,SHIFT_DAY,11,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,17,0 +DT0133,M014,SHIFT_NIGHT,11,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,47,0 +DT0134,M014,SHIFT_SWING,12,FT10,Pressure Loss,Process,Fluid,Pressure Loss,40,0 +DT0135,M014,SHIFT_DAY,12,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,21,0 +DT0136,M015,SHIFT_SWING,3,FT09,Overheating,Process,Thermal,Overheating,39,0 +DT0137,M015,SHIFT_SWING,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,35,0 +DT0138,M016,SHIFT_DAY,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,141,0 +DT0139,M016,SHIFT_SWING,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,116,0 +DT0140,M016,SHIFT_DAY,2,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,67,0 +DT0141,M016,SHIFT_SWING,2,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,87,0 +DT0142,M016,SHIFT_DAY,2,FT09,Overheating,Process,Thermal,Overheating,42,0 +DT0143,M016,SHIFT_SWING,3,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,74,0 +DT0144,M016,SHIFT_SWING,3,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,30,0 +DT0145,M016,SHIFT_NIGHT,3,FT09,Overheating,Process,Thermal,Overheating,52,0 +DT0146,M016,SHIFT_SWING,4,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,53,0 +DT0147,M016,SHIFT_SWING,4,FT09,Overheating,Process,Thermal,Overheating,49,0 +DT0148,M016,SHIFT_DAY,5,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,32,1 +DT0149,M016,SHIFT_SWING,5,FT08,Control Board,Electrical,Instrumentation,Control Board,59,0 +DT0150,M016,SHIFT_NIGHT,6,FT08,Control Board,Electrical,Instrumentation,Control Board,68,0 +DT0151,M016,SHIFT_SWING,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,93,0 +DT0152,M016,SHIFT_NIGHT,6,FT09,Overheating,Process,Thermal,Overheating,63,1 +DT0153,M016,SHIFT_SWING,7,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,61,0 +DT0154,M016,SHIFT_DAY,8,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,63,0 +DT0155,M016,SHIFT_SWING,9,FT08,Control Board,Electrical,Instrumentation,Control Board,61,0 +DT0156,M016,SHIFT_NIGHT,10,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,88,0 +DT0157,M016,SHIFT_SWING,10,FT08,Control Board,Electrical,Instrumentation,Control Board,62,0 +DT0158,M016,SHIFT_DAY,10,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,97,0 +DT0159,M016,SHIFT_DAY,11,FT10,Pressure Loss,Process,Fluid,Pressure Loss,51,1 +DT0160,M016,SHIFT_SWING,11,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,28,0 +DT0161,M016,SHIFT_SWING,11,FT09,Overheating,Process,Thermal,Overheating,41,1 +DT0162,M016,SHIFT_SWING,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,66,1 +DT0163,M016,SHIFT_SWING,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,40,1 +DT0164,M017,SHIFT_SWING,2,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,62,0 +DT0165,M017,SHIFT_NIGHT,4,FT08,Control Board,Electrical,Instrumentation,Control Board,26,0 +DT0166,M017,SHIFT_SWING,6,FT09,Overheating,Process,Thermal,Overheating,39,1 +DT0167,M018,SHIFT_NIGHT,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,108,0 +DT0168,M018,SHIFT_NIGHT,6,FT10,Pressure Loss,Process,Fluid,Pressure Loss,44,0 +DT0169,M018,SHIFT_SWING,9,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,77,0 +DT0170,M018,SHIFT_SWING,10,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,119,1 +DT0171,M019,SHIFT_SWING,3,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,28,0 +DT0172,M019,SHIFT_SWING,6,FT09,Overheating,Process,Thermal,Overheating,36,0 +DT0173,M019,SHIFT_SWING,9,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,21,0 +DT0174,M020,SHIFT_DAY,6,FT15,Valve Fouling,Material,Wear Parts,Valve Fouling,50,0 +DT0175,M020,SHIFT_SWING,11,FT09,Overheating,Process,Thermal,Overheating,32,0 +DT0176,M022,SHIFT_NIGHT,1,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,74,1 +DT0177,M022,SHIFT_DAY,1,FT10,Pressure Loss,Process,Fluid,Pressure Loss,53,0 +DT0178,M022,SHIFT_SWING,2,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,83,0 +DT0179,M022,SHIFT_SWING,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,105,0 +DT0180,M022,SHIFT_DAY,3,FT13,Corrosion,Material,Surface,Corrosion,83,0 +DT0181,M022,SHIFT_DAY,4,FT11,Flow Blockage,Process,Fluid,Flow Blockage,70,0 +DT0182,M022,SHIFT_DAY,4,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,70,0 +DT0183,M022,SHIFT_NIGHT,5,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,132,0 +DT0184,M022,SHIFT_NIGHT,5,FT11,Flow Blockage,Process,Fluid,Flow Blockage,56,0 +DT0185,M022,SHIFT_SWING,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,81,1 +DT0186,M022,SHIFT_DAY,6,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,51,0 +DT0187,M022,SHIFT_DAY,7,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,78,0 +DT0188,M022,SHIFT_DAY,8,FT11,Flow Blockage,Process,Fluid,Flow Blockage,65,1 +DT0189,M022,SHIFT_SWING,8,FT10,Pressure Loss,Process,Fluid,Pressure Loss,32,1 +DT0190,M022,SHIFT_SWING,9,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,99,0 +DT0191,M022,SHIFT_NIGHT,10,FT11,Flow Blockage,Process,Fluid,Flow Blockage,32,0 +DT0192,M022,SHIFT_DAY,10,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,54,0 +DT0193,M022,SHIFT_SWING,10,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,144,0 +DT0194,M022,SHIFT_SWING,11,FT11,Flow Blockage,Process,Fluid,Flow Blockage,37,0 +DT0195,M022,SHIFT_NIGHT,11,FT11,Flow Blockage,Process,Fluid,Flow Blockage,62,0 +DT0196,M022,SHIFT_DAY,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,33,0 +DT0197,M022,SHIFT_NIGHT,12,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,33,0 +DT0198,M022,SHIFT_NIGHT,12,FT11,Flow Blockage,Process,Fluid,Flow Blockage,49,0 +DT0199,M023,SHIFT_SWING,2,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,23,0 +DT0200,M024,SHIFT_DAY,7,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,70,0 +DT0201,M024,SHIFT_DAY,8,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,64,0 +DT0202,M025,SHIFT_NIGHT,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,57,1 +DT0203,M025,SHIFT_DAY,4,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,77,0 +DT0204,M026,SHIFT_SWING,3,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,65,0 +DT0205,M026,SHIFT_DAY,9,FT11,Flow Blockage,Process,Fluid,Flow Blockage,26,0 +DT0206,M027,SHIFT_DAY,5,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,63,1 +DT0207,M027,SHIFT_DAY,6,FT13,Corrosion,Material,Surface,Corrosion,68,0 +DT0208,M027,SHIFT_SWING,10,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,74,0 +DT0209,M028,SHIFT_SWING,1,FT10,Pressure Loss,Process,Fluid,Pressure Loss,32,0 +DT0210,M028,SHIFT_NIGHT,1,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,64,0 +DT0211,M028,SHIFT_DAY,2,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,47,0 +DT0212,M028,SHIFT_SWING,2,FT11,Flow Blockage,Process,Fluid,Flow Blockage,24,0 +DT0213,M028,SHIFT_DAY,3,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,39,0 +DT0214,M028,SHIFT_DAY,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,135,0 +DT0215,M028,SHIFT_NIGHT,3,FT13,Corrosion,Material,Surface,Corrosion,83,0 +DT0216,M028,SHIFT_SWING,4,FT10,Pressure Loss,Process,Fluid,Pressure Loss,68,0 +DT0217,M028,SHIFT_SWING,4,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,55,0 +DT0218,M028,SHIFT_NIGHT,5,FT14,Impeller Erosion,Material,Wear Parts,Impeller Erosion,71,0 +DT0219,M028,SHIFT_NIGHT,5,FT11,Flow Blockage,Process,Fluid,Flow Blockage,65,0 +DT0220,M028,SHIFT_DAY,6,FT10,Pressure Loss,Process,Fluid,Pressure Loss,65,0 +DT0221,M028,SHIFT_NIGHT,7,FT11,Flow Blockage,Process,Fluid,Flow Blockage,43,0 +DT0222,M028,SHIFT_DAY,8,FT13,Corrosion,Material,Surface,Corrosion,46,0 +DT0223,M028,SHIFT_DAY,9,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,78,1 +DT0224,M028,SHIFT_SWING,10,FT11,Flow Blockage,Process,Fluid,Flow Blockage,34,0 +DT0225,M028,SHIFT_DAY,10,FT13,Corrosion,Material,Surface,Corrosion,33,0 +DT0226,M028,SHIFT_NIGHT,11,FT11,Flow Blockage,Process,Fluid,Flow Blockage,27,0 +DT0227,M028,SHIFT_SWING,12,FT11,Flow Blockage,Process,Fluid,Flow Blockage,21,0 +DT0228,M028,SHIFT_NIGHT,12,FT10,Pressure Loss,Process,Fluid,Pressure Loss,87,0 +DT0229,M029,SHIFT_SWING,12,FT11,Flow Blockage,Process,Fluid,Flow Blockage,17,0 +DT0230,M030,SHIFT_DAY,1,FT12,Seal Degradation,Material,Wear Parts,Seal Degradation,51,1 +DT0231,M031,SHIFT_DAY,6,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,84,0 +DT0232,M031,SHIFT_DAY,11,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,72,0 +DT0233,M032,SHIFT_DAY,1,FT09,Overheating,Process,Thermal,Overheating,34,1 +DT0234,M032,SHIFT_DAY,2,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,51,0 +DT0235,M032,SHIFT_SWING,3,FT09,Overheating,Process,Thermal,Overheating,35,0 +DT0236,M032,SHIFT_DAY,4,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,36,0 +DT0237,M032,SHIFT_SWING,5,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,78,0 +DT0238,M032,SHIFT_NIGHT,6,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,35,0 +DT0239,M032,SHIFT_NIGHT,6,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,46,0 +DT0240,M032,SHIFT_NIGHT,6,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,63,0 +DT0241,M032,SHIFT_DAY,7,FT09,Overheating,Process,Thermal,Overheating,66,0 +DT0242,M032,SHIFT_NIGHT,7,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,32,0 +DT0243,M032,SHIFT_NIGHT,8,FT09,Overheating,Process,Thermal,Overheating,105,1 +DT0244,M032,SHIFT_SWING,8,FT09,Overheating,Process,Thermal,Overheating,97,0 +DT0245,M032,SHIFT_NIGHT,9,FT09,Overheating,Process,Thermal,Overheating,47,0 +DT0246,M032,SHIFT_SWING,10,FT08,Control Board,Electrical,Instrumentation,Control Board,63,0 +DT0247,M032,SHIFT_NIGHT,10,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,94,0 +DT0248,M032,SHIFT_NIGHT,11,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,26,0 +DT0249,M032,SHIFT_SWING,12,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,83,0 +DT0250,M032,SHIFT_DAY,12,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,113,0 +DT0251,M033,SHIFT_DAY,2,FT09,Overheating,Process,Thermal,Overheating,34,1 +DT0252,M033,SHIFT_SWING,3,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,17,0 +DT0253,M033,SHIFT_NIGHT,7,FT08,Control Board,Electrical,Instrumentation,Control Board,20,1 +DT0254,M034,SHIFT_NIGHT,2,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,60,0 +DT0255,M034,SHIFT_DAY,9,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,78,0 +DT0256,M035,SHIFT_SWING,1,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,28,0 +DT0257,M035,SHIFT_DAY,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,92,0 +DT0258,M035,SHIFT_SWING,2,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,38,0 +DT0259,M035,SHIFT_SWING,2,FT09,Overheating,Process,Thermal,Overheating,34,0 +DT0260,M035,SHIFT_DAY,2,FT09,Overheating,Process,Thermal,Overheating,87,1 +DT0261,M035,SHIFT_NIGHT,3,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,75,0 +DT0262,M035,SHIFT_NIGHT,3,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,78,0 +DT0263,M035,SHIFT_NIGHT,3,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,33,0 +DT0264,M035,SHIFT_DAY,4,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,38,0 +DT0265,M035,SHIFT_NIGHT,5,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,61,0 +DT0266,M035,SHIFT_SWING,5,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,67,0 +DT0267,M035,SHIFT_SWING,5,FT08,Control Board,Electrical,Instrumentation,Control Board,76,0 +DT0268,M035,SHIFT_NIGHT,6,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,64,0 +DT0269,M035,SHIFT_DAY,6,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,53,0 +DT0270,M035,SHIFT_DAY,6,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,34,0 +DT0271,M035,SHIFT_NIGHT,7,FT09,Overheating,Process,Thermal,Overheating,96,1 +DT0272,M035,SHIFT_NIGHT,7,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,108,1 +DT0273,M035,SHIFT_NIGHT,7,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,44,1 +DT0274,M035,SHIFT_SWING,8,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,75,0 +DT0275,M035,SHIFT_DAY,8,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,45,1 +DT0276,M035,SHIFT_DAY,8,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,43,0 +DT0277,M035,SHIFT_SWING,9,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,82,0 +DT0278,M035,SHIFT_DAY,10,FT09,Overheating,Process,Thermal,Overheating,100,0 +DT0279,M035,SHIFT_SWING,10,FT02,Shaft Misalignment,Mechanical,Rotating Equipment,Shaft Misalignment,57,0 +DT0280,M035,SHIFT_NIGHT,11,FT09,Overheating,Process,Thermal,Overheating,31,0 +DT0281,M035,SHIFT_SWING,11,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,100,0 +DT0282,M035,SHIFT_NIGHT,12,FT09,Overheating,Process,Thermal,Overheating,88,0 +DT0283,M036,SHIFT_SWING,1,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,21,0 +DT0284,M036,SHIFT_NIGHT,7,FT08,Control Board,Electrical,Instrumentation,Control Board,40,0 +DT0285,M037,SHIFT_SWING,1,FT08,Control Board,Electrical,Instrumentation,Control Board,48,0 +DT0286,M037,SHIFT_NIGHT,11,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,24,0 +DT0287,M038,SHIFT_DAY,1,FT09,Overheating,Process,Thermal,Overheating,113,0 +DT0288,M038,SHIFT_SWING,3,FT09,Overheating,Process,Thermal,Overheating,33,0 +DT0289,M038,SHIFT_SWING,7,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,55,0 +DT0290,M038,SHIFT_SWING,10,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,89,0 +DT0291,M039,SHIFT_SWING,1,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,19,0 +DT0292,M039,SHIFT_NIGHT,4,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,10,0 +DT0293,M039,SHIFT_SWING,5,FT09,Overheating,Process,Thermal,Overheating,42,0 +DT0294,M040,SHIFT_DAY,4,FT09,Overheating,Process,Thermal,Overheating,75,0 +DT0295,M040,SHIFT_DAY,6,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,75,0 +DT0296,M040,SHIFT_SWING,9,FT09,Overheating,Process,Thermal,Overheating,52,0 +DT0297,M041,SHIFT_NIGHT,1,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,43,0 +DT0298,M041,SHIFT_SWING,5,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,38,0 +DT0299,M041,SHIFT_NIGHT,8,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,93,0 +DT0300,M041,SHIFT_DAY,10,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,100,0 +DT0301,M042,SHIFT_DAY,8,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,74,0 +DT0302,M043,SHIFT_SWING,1,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,65,0 +DT0303,M043,SHIFT_DAY,2,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,41,0 +DT0304,M043,SHIFT_DAY,2,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,19,1 +DT0305,M043,SHIFT_SWING,3,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,34,1 +DT0306,M043,SHIFT_DAY,3,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,28,0 +DT0307,M043,SHIFT_DAY,4,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,60,0 +DT0308,M043,SHIFT_DAY,5,FT09,Overheating,Process,Thermal,Overheating,58,0 +DT0309,M043,SHIFT_SWING,6,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,33,0 +DT0310,M043,SHIFT_NIGHT,7,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,58,0 +DT0311,M043,SHIFT_SWING,8,FT09,Overheating,Process,Thermal,Overheating,45,0 +DT0312,M043,SHIFT_DAY,8,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,25,0 +DT0313,M043,SHIFT_NIGHT,9,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,25,0 +DT0314,M043,SHIFT_NIGHT,10,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,26,0 +DT0315,M043,SHIFT_SWING,10,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,66,0 +DT0316,M043,SHIFT_NIGHT,11,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,16,0 +DT0317,M043,SHIFT_DAY,11,FT09,Overheating,Process,Thermal,Overheating,26,0 +DT0318,M043,SHIFT_SWING,12,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,14,0 +DT0319,M043,SHIFT_SWING,12,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,23,0 +DT0320,M044,SHIFT_SWING,1,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,37,0 +DT0321,M044,SHIFT_DAY,7,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,44,0 +DT0322,M045,SHIFT_SWING,2,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,75,0 +DT0323,M045,SHIFT_SWING,3,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,75,0 +DT0324,M045,SHIFT_SWING,8,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,93,0 +DT0325,M045,SHIFT_DAY,9,FT01,Bearing Failure,Mechanical,Rotating Equipment,Bearing Failure,97,0 +DT0326,M045,SHIFT_DAY,11,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,37,0 +DT0327,M047,SHIFT_SWING,1,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,74,0 +DT0328,M047,SHIFT_DAY,2,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,80,0 +DT0329,M047,SHIFT_DAY,2,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,85,0 +DT0330,M047,SHIFT_NIGHT,3,FT09,Overheating,Process,Thermal,Overheating,81,0 +DT0331,M047,SHIFT_DAY,3,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,24,0 +DT0332,M047,SHIFT_NIGHT,4,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,43,1 +DT0333,M047,SHIFT_SWING,4,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,97,0 +DT0334,M047,SHIFT_SWING,4,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,92,0 +DT0335,M047,SHIFT_NIGHT,5,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,32,0 +DT0336,M047,SHIFT_DAY,5,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,21,0 +DT0337,M047,SHIFT_NIGHT,6,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,25,0 +DT0338,M047,SHIFT_SWING,7,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,31,0 +DT0339,M047,SHIFT_DAY,7,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,48,0 +DT0340,M047,SHIFT_NIGHT,7,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,72,0 +DT0341,M047,SHIFT_DAY,8,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,72,0 +DT0342,M047,SHIFT_SWING,9,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,44,0 +DT0343,M047,SHIFT_DAY,9,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,80,0 +DT0344,M047,SHIFT_SWING,10,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,54,0 +DT0345,M047,SHIFT_SWING,10,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,17,0 +DT0346,M047,SHIFT_DAY,11,FT07,Sensor Failure,Electrical,Instrumentation,Sensor Failure,44,0 +DT0347,M047,SHIFT_DAY,12,FT09,Overheating,Process,Thermal,Overheating,74,0 +DT0348,M048,SHIFT_DAY,5,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,47,0 +DT0349,M048,SHIFT_SWING,7,FT06,Wiring Fault,Electrical,Power System,Wiring Fault,60,0 +DT0350,M048,SHIFT_NIGHT,10,FT09,Overheating,Process,Thermal,Overheating,70,0 +DT0351,M049,SHIFT_DAY,10,FT04,Belt Snap,Mechanical,Drive Train,Belt Snap,10,0 +DT0352,M050,SHIFT_SWING,9,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,88,0 +DT0353,M050,SHIFT_DAY,10,FT05,Motor Burnout,Electrical,Power System,Motor Burnout,32,1 diff --git a/v1/machine_maintenance/data/failure_predictions.csv b/v1/machine_maintenance/data/failure_predictions.csv new file mode 100644 index 00000000..5e95ec96 --- /dev/null +++ b/v1/machine_maintenance/data/failure_predictions.csv @@ -0,0 +1,601 @@ +prediction_id,machine_id,period,failure_probability,predicted_failure_mode,confidence +FP0001,M001,1,0.069,bearing_wear,0.5 +FP0002,M001,2,0.089,bearing_wear,0.58 +FP0003,M001,3,0.147,overheating,0.65 +FP0004,M001,4,0.138,bearing_wear,0.57 +FP0005,M001,5,0.198,bearing_wear,0.64 +FP0006,M001,6,0.187,bearing_wear,0.57 +FP0007,M001,7,0.219,valve_stuck,0.65 +FP0008,M001,8,0.236,bearing_wear,0.59 +FP0009,M001,9,0.262,bearing_wear,0.66 +FP0010,M001,10,0.317,motor_burnout,0.77 +FP0011,M001,11,0.309,seal_leak,0.65 +FP0012,M001,12,0.346,bearing_wear,0.78 +FP0013,M002,1,0.02,bearing_wear,0.53 +FP0014,M002,2,0.031,bearing_wear,0.49 +FP0015,M002,3,0.028,seal_leak,0.48 +FP0016,M002,4,0.039,bearing_wear,0.56 +FP0017,M002,5,0.024,bearing_wear,0.58 +FP0018,M002,6,0.034,seal_leak,0.54 +FP0019,M002,7,0.033,rotor_imbalance,0.48 +FP0020,M002,8,0.028,bearing_wear,0.57 +FP0021,M002,9,0.048,electrical_fault,0.63 +FP0022,M002,10,0.051,bearing_wear,0.56 +FP0023,M002,11,0.056,bearing_wear,0.58 +FP0024,M002,12,0.042,bearing_wear,0.58 +FP0025,M003,1,0.05,bearing_wear,0.48 +FP0026,M003,2,0.08,bearing_wear,0.57 +FP0027,M003,3,0.072,bearing_wear,0.61 +FP0028,M003,4,0.074,bearing_wear,0.54 +FP0029,M003,5,0.09,bearing_wear,0.56 +FP0030,M003,6,0.112,bearing_wear,0.58 +FP0031,M003,7,0.111,bearing_wear,0.59 +FP0032,M003,8,0.11,bearing_wear,0.53 +FP0033,M003,9,0.123,bearing_wear,0.52 +FP0034,M003,10,0.119,bearing_wear,0.61 +FP0035,M003,11,0.136,bearing_wear,0.56 +FP0036,M003,12,0.16,valve_stuck,0.67 +FP0037,M004,1,0.071,valve_stuck,0.58 +FP0038,M004,2,0.066,bearing_wear,0.59 +FP0039,M004,3,0.074,bearing_wear,0.61 +FP0040,M004,4,0.058,bearing_wear,0.61 +FP0041,M004,5,0.091,bearing_wear,0.58 +FP0042,M004,6,0.084,bearing_wear,0.53 +FP0043,M004,7,0.065,bearing_wear,0.6 +FP0044,M004,8,0.067,bearing_wear,0.52 +FP0045,M004,9,0.104,bearing_wear,0.58 +FP0046,M004,10,0.102,bearing_wear,0.53 +FP0047,M004,11,0.091,overheating,0.62 +FP0048,M004,12,0.096,bearing_wear,0.65 +FP0049,M005,1,0.046,bearing_wear,0.56 +FP0050,M005,2,0.047,rotor_imbalance,0.62 +FP0051,M005,3,0.079,seal_leak,0.61 +FP0052,M005,4,0.057,bearing_wear,0.57 +FP0053,M005,5,0.091,bearing_wear,0.56 +FP0054,M005,6,0.098,electrical_fault,0.55 +FP0055,M005,7,0.101,rotor_imbalance,0.63 +FP0056,M005,8,0.072,bearing_wear,0.63 +FP0057,M005,9,0.09,valve_stuck,0.61 +FP0058,M005,10,0.111,bearing_wear,0.55 +FP0059,M005,11,0.102,bearing_wear,0.56 +FP0060,M005,12,0.107,seal_leak,0.59 +FP0061,M006,1,0.088,bearing_wear,0.52 +FP0062,M006,2,0.099,bearing_wear,0.59 +FP0063,M006,3,0.136,bearing_wear,0.64 +FP0064,M006,4,0.167,bearing_wear,0.68 +FP0065,M006,5,0.191,bearing_wear,0.57 +FP0066,M006,6,0.189,bearing_wear,0.59 +FP0067,M006,7,0.237,bearing_wear,0.64 +FP0068,M006,8,0.242,motor_burnout,0.63 +FP0069,M006,9,0.278,bearing_wear,0.68 +FP0070,M006,10,0.306,bearing_wear,0.72 +FP0071,M006,11,0.314,bearing_wear,0.7 +FP0072,M006,12,0.35,motor_burnout,0.76 +FP0073,M007,1,0.02,bearing_wear,0.52 +FP0074,M007,2,0.027,bearing_wear,0.61 +FP0075,M007,3,0.02,bearing_wear,0.49 +FP0076,M007,4,0.038,rotor_imbalance,0.6 +FP0077,M007,5,0.043,bearing_wear,0.57 +FP0078,M007,6,0.04,bearing_wear,0.56 +FP0079,M007,7,0.045,motor_burnout,0.54 +FP0080,M007,8,0.057,bearing_wear,0.63 +FP0081,M007,9,0.05,bearing_wear,0.49 +FP0082,M007,10,0.036,bearing_wear,0.5 +FP0083,M007,11,0.062,bearing_wear,0.56 +FP0084,M007,12,0.069,bearing_wear,0.63 +FP0085,M008,1,0.052,motor_burnout,0.55 +FP0086,M008,2,0.055,bearing_wear,0.6 +FP0087,M008,3,0.048,bearing_wear,0.51 +FP0088,M008,4,0.042,bearing_wear,0.54 +FP0089,M008,5,0.064,bearing_wear,0.52 +FP0090,M008,6,0.055,bearing_wear,0.54 +FP0091,M008,7,0.058,rotor_imbalance,0.63 +FP0092,M008,8,0.086,bearing_wear,0.52 +FP0093,M008,9,0.071,bearing_wear,0.59 +FP0094,M008,10,0.097,bearing_wear,0.51 +FP0095,M008,11,0.099,bearing_wear,0.63 +FP0096,M008,12,0.072,bearing_wear,0.6 +FP0097,M009,1,0.066,bearing_wear,0.61 +FP0098,M009,2,0.089,bearing_wear,0.58 +FP0099,M009,3,0.084,bearing_wear,0.55 +FP0100,M009,4,0.079,bearing_wear,0.64 +FP0101,M009,5,0.11,bearing_wear,0.61 +FP0102,M009,6,0.091,bearing_wear,0.54 +FP0103,M009,7,0.114,electrical_fault,0.6 +FP0104,M009,8,0.118,impeller_erosion,0.62 +FP0105,M009,9,0.131,bearing_wear,0.55 +FP0106,M009,10,0.116,bearing_wear,0.53 +FP0107,M009,11,0.103,overheating,0.6 +FP0108,M009,12,0.114,bearing_wear,0.58 +FP0109,M010,1,0.02,bearing_wear,0.46 +FP0110,M010,2,0.02,bearing_wear,0.56 +FP0111,M010,3,0.021,bearing_wear,0.57 +FP0112,M010,4,0.02,overheating,0.51 +FP0113,M010,5,0.031,bearing_wear,0.51 +FP0114,M010,6,0.044,bearing_wear,0.6 +FP0115,M010,7,0.02,bearing_wear,0.49 +FP0116,M010,8,0.044,motor_burnout,0.62 +FP0117,M010,9,0.024,bearing_wear,0.6 +FP0118,M010,10,0.053,bearing_wear,0.54 +FP0119,M010,11,0.063,bearing_wear,0.57 +FP0120,M010,12,0.034,bearing_wear,0.56 +FP0121,M011,1,0.213,motor_burnout,0.61 +FP0122,M011,2,0.241,valve_stuck,0.63 +FP0123,M011,3,0.266,seal_leak,0.63 +FP0124,M011,4,0.258,electrical_fault,0.62 +FP0125,M011,5,0.295,valve_stuck,0.64 +FP0126,M011,6,0.317,rotor_imbalance,0.74 +FP0127,M011,7,0.309,valve_stuck,0.67 +FP0128,M011,8,0.333,valve_stuck,0.79 +FP0129,M011,9,0.358,valve_stuck,0.7 +FP0130,M011,10,0.374,valve_stuck,0.7 +FP0131,M011,11,0.401,valve_stuck,0.79 +FP0132,M011,12,0.42,valve_stuck,0.79 +FP0133,M012,1,0.198,seal_leak,0.57 +FP0134,M012,2,0.195,valve_stuck,0.68 +FP0135,M012,3,0.221,seal_leak,0.68 +FP0136,M012,4,0.236,valve_stuck,0.66 +FP0137,M012,5,0.273,valve_stuck,0.63 +FP0138,M012,6,0.287,valve_stuck,0.66 +FP0139,M012,7,0.313,electrical_fault,0.68 +FP0140,M012,8,0.334,valve_stuck,0.66 +FP0141,M012,9,0.319,valve_stuck,0.65 +FP0142,M012,10,0.36,valve_stuck,0.75 +FP0143,M012,11,0.392,valve_stuck,0.77 +FP0144,M012,12,0.415,valve_stuck,0.82 +FP0145,M013,1,0.023,valve_stuck,0.58 +FP0146,M013,2,0.041,valve_stuck,0.54 +FP0147,M013,3,0.035,valve_stuck,0.55 +FP0148,M013,4,0.044,seal_leak,0.59 +FP0149,M013,5,0.027,valve_stuck,0.55 +FP0150,M013,6,0.047,seal_leak,0.59 +FP0151,M013,7,0.046,valve_stuck,0.51 +FP0152,M013,8,0.05,valve_stuck,0.54 +FP0153,M013,9,0.035,valve_stuck,0.61 +FP0154,M013,10,0.051,valve_stuck,0.55 +FP0155,M013,11,0.041,valve_stuck,0.49 +FP0156,M013,12,0.081,valve_stuck,0.54 +FP0157,M014,1,0.083,valve_stuck,0.59 +FP0158,M014,2,0.076,valve_stuck,0.58 +FP0159,M014,3,0.104,valve_stuck,0.55 +FP0160,M014,4,0.115,valve_stuck,0.58 +FP0161,M014,5,0.116,valve_stuck,0.64 +FP0162,M014,6,0.126,valve_stuck,0.66 +FP0163,M014,7,0.116,valve_stuck,0.6 +FP0164,M014,8,0.117,valve_stuck,0.54 +FP0165,M014,9,0.138,electrical_fault,0.59 +FP0166,M014,10,0.135,valve_stuck,0.63 +FP0167,M014,11,0.148,valve_stuck,0.64 +FP0168,M014,12,0.153,valve_stuck,0.56 +FP0169,M015,1,0.04,valve_stuck,0.55 +FP0170,M015,2,0.055,valve_stuck,0.51 +FP0171,M015,3,0.06,valve_stuck,0.55 +FP0172,M015,4,0.065,valve_stuck,0.62 +FP0173,M015,5,0.058,overheating,0.52 +FP0174,M015,6,0.056,valve_stuck,0.56 +FP0175,M015,7,0.044,valve_stuck,0.5 +FP0176,M015,8,0.061,motor_burnout,0.51 +FP0177,M015,9,0.069,overheating,0.53 +FP0178,M015,10,0.06,valve_stuck,0.52 +FP0179,M015,11,0.082,valve_stuck,0.56 +FP0180,M015,12,0.056,valve_stuck,0.56 +FP0181,M016,1,0.258,valve_stuck,0.72 +FP0182,M016,2,0.272,valve_stuck,0.69 +FP0183,M016,3,0.276,valve_stuck,0.74 +FP0184,M016,4,0.326,impeller_erosion,0.72 +FP0185,M016,5,0.324,valve_stuck,0.72 +FP0186,M016,6,0.337,impeller_erosion,0.7 +FP0187,M016,7,0.379,overheating,0.74 +FP0188,M016,8,0.376,overheating,0.72 +FP0189,M016,9,0.42,valve_stuck,0.73 +FP0190,M016,10,0.42,overheating,0.78 +FP0191,M016,11,0.42,valve_stuck,0.82 +FP0192,M016,12,0.42,valve_stuck,0.77 +FP0193,M017,1,0.033,valve_stuck,0.55 +FP0194,M017,2,0.035,valve_stuck,0.62 +FP0195,M017,3,0.032,valve_stuck,0.55 +FP0196,M017,4,0.041,valve_stuck,0.56 +FP0197,M017,5,0.041,valve_stuck,0.59 +FP0198,M017,6,0.041,valve_stuck,0.6 +FP0199,M017,7,0.033,valve_stuck,0.59 +FP0200,M017,8,0.069,valve_stuck,0.57 +FP0201,M017,9,0.066,bearing_wear,0.51 +FP0202,M017,10,0.076,valve_stuck,0.51 +FP0203,M017,11,0.046,impeller_erosion,0.49 +FP0204,M017,12,0.054,valve_stuck,0.57 +FP0205,M018,1,0.054,impeller_erosion,0.57 +FP0206,M018,2,0.049,valve_stuck,0.57 +FP0207,M018,3,0.069,rotor_imbalance,0.56 +FP0208,M018,4,0.042,valve_stuck,0.6 +FP0209,M018,5,0.048,valve_stuck,0.57 +FP0210,M018,6,0.061,valve_stuck,0.51 +FP0211,M018,7,0.044,valve_stuck,0.48 +FP0212,M018,8,0.052,impeller_erosion,0.53 +FP0213,M018,9,0.065,valve_stuck,0.57 +FP0214,M018,10,0.082,valve_stuck,0.51 +FP0215,M018,11,0.067,valve_stuck,0.54 +FP0216,M018,12,0.091,valve_stuck,0.65 +FP0217,M019,1,0.079,valve_stuck,0.5 +FP0218,M019,2,0.071,valve_stuck,0.58 +FP0219,M019,3,0.087,valve_stuck,0.52 +FP0220,M019,4,0.09,valve_stuck,0.58 +FP0221,M019,5,0.105,valve_stuck,0.58 +FP0222,M019,6,0.091,valve_stuck,0.54 +FP0223,M019,7,0.093,rotor_imbalance,0.64 +FP0224,M019,8,0.117,valve_stuck,0.56 +FP0225,M019,9,0.1,electrical_fault,0.61 +FP0226,M019,10,0.097,valve_stuck,0.65 +FP0227,M019,11,0.112,valve_stuck,0.64 +FP0228,M019,12,0.104,valve_stuck,0.61 +FP0229,M020,1,0.024,valve_stuck,0.6 +FP0230,M020,2,0.02,valve_stuck,0.6 +FP0231,M020,3,0.02,electrical_fault,0.54 +FP0232,M020,4,0.02,valve_stuck,0.59 +FP0233,M020,5,0.02,valve_stuck,0.5 +FP0234,M020,6,0.031,valve_stuck,0.48 +FP0235,M020,7,0.046,valve_stuck,0.58 +FP0236,M020,8,0.037,bearing_wear,0.6 +FP0237,M020,9,0.03,valve_stuck,0.6 +FP0238,M020,10,0.053,valve_stuck,0.52 +FP0239,M020,11,0.029,motor_burnout,0.53 +FP0240,M020,12,0.054,valve_stuck,0.56 +FP0241,M021,1,0.091,bearing_wear,0.58 +FP0242,M021,2,0.092,seal_leak,0.53 +FP0243,M021,3,0.118,seal_leak,0.59 +FP0244,M021,4,0.109,seal_leak,0.55 +FP0245,M021,5,0.107,seal_leak,0.61 +FP0246,M021,6,0.096,seal_leak,0.54 +FP0247,M021,7,0.107,seal_leak,0.57 +FP0248,M021,8,0.111,seal_leak,0.62 +FP0249,M021,9,0.108,motor_burnout,0.58 +FP0250,M021,10,0.118,electrical_fault,0.64 +FP0251,M021,11,0.127,impeller_erosion,0.65 +FP0252,M021,12,0.108,seal_leak,0.59 +FP0253,M022,1,0.074,seal_leak,0.51 +FP0254,M022,2,0.115,seal_leak,0.6 +FP0255,M022,3,0.098,seal_leak,0.55 +FP0256,M022,4,0.126,seal_leak,0.54 +FP0257,M022,5,0.104,seal_leak,0.55 +FP0258,M022,6,0.137,seal_leak,0.64 +FP0259,M022,7,0.12,seal_leak,0.61 +FP0260,M022,8,0.148,seal_leak,0.6 +FP0261,M022,9,0.136,seal_leak,0.62 +FP0262,M022,10,0.151,seal_leak,0.62 +FP0263,M022,11,0.173,seal_leak,0.6 +FP0264,M022,12,0.16,rotor_imbalance,0.68 +FP0265,M023,1,0.098,seal_leak,0.6 +FP0266,M023,2,0.091,seal_leak,0.63 +FP0267,M023,3,0.101,seal_leak,0.6 +FP0268,M023,4,0.124,seal_leak,0.55 +FP0269,M023,5,0.102,seal_leak,0.55 +FP0270,M023,6,0.106,seal_leak,0.55 +FP0271,M023,7,0.115,valve_stuck,0.63 +FP0272,M023,8,0.121,seal_leak,0.55 +FP0273,M023,9,0.136,seal_leak,0.57 +FP0274,M023,10,0.115,seal_leak,0.63 +FP0275,M023,11,0.115,seal_leak,0.53 +FP0276,M023,12,0.147,seal_leak,0.55 +FP0277,M024,1,0.11,seal_leak,0.67 +FP0278,M024,2,0.086,seal_leak,0.51 +FP0279,M024,3,0.121,seal_leak,0.56 +FP0280,M024,4,0.116,seal_leak,0.54 +FP0281,M024,5,0.11,seal_leak,0.52 +FP0282,M024,6,0.105,rotor_imbalance,0.58 +FP0283,M024,7,0.128,rotor_imbalance,0.55 +FP0284,M024,8,0.134,seal_leak,0.62 +FP0285,M024,9,0.116,seal_leak,0.65 +FP0286,M024,10,0.118,seal_leak,0.6 +FP0287,M024,11,0.139,seal_leak,0.57 +FP0288,M024,12,0.109,seal_leak,0.58 +FP0289,M025,1,0.02,seal_leak,0.54 +FP0290,M025,2,0.036,seal_leak,0.51 +FP0291,M025,3,0.029,seal_leak,0.54 +FP0292,M025,4,0.04,seal_leak,0.59 +FP0293,M025,5,0.045,seal_leak,0.6 +FP0294,M025,6,0.037,seal_leak,0.6 +FP0295,M025,7,0.035,seal_leak,0.58 +FP0296,M025,8,0.046,electrical_fault,0.52 +FP0297,M025,9,0.039,seal_leak,0.59 +FP0298,M025,10,0.061,motor_burnout,0.53 +FP0299,M025,11,0.057,impeller_erosion,0.51 +FP0300,M025,12,0.032,seal_leak,0.49 +FP0301,M026,1,0.038,seal_leak,0.58 +FP0302,M026,2,0.042,seal_leak,0.5 +FP0303,M026,3,0.064,seal_leak,0.63 +FP0304,M026,4,0.038,seal_leak,0.54 +FP0305,M026,5,0.03,seal_leak,0.47 +FP0306,M026,6,0.041,seal_leak,0.59 +FP0307,M026,7,0.054,seal_leak,0.59 +FP0308,M026,8,0.038,seal_leak,0.59 +FP0309,M026,9,0.045,seal_leak,0.54 +FP0310,M026,10,0.063,seal_leak,0.62 +FP0311,M026,11,0.054,seal_leak,0.48 +FP0312,M026,12,0.085,seal_leak,0.56 +FP0313,M027,1,0.051,seal_leak,0.63 +FP0314,M027,2,0.022,seal_leak,0.48 +FP0315,M027,3,0.043,seal_leak,0.53 +FP0316,M027,4,0.058,seal_leak,0.58 +FP0317,M027,5,0.023,overheating,0.5 +FP0318,M027,6,0.05,seal_leak,0.61 +FP0319,M027,7,0.061,seal_leak,0.5 +FP0320,M027,8,0.054,seal_leak,0.51 +FP0321,M027,9,0.042,seal_leak,0.59 +FP0322,M027,10,0.034,seal_leak,0.59 +FP0323,M027,11,0.055,seal_leak,0.54 +FP0324,M027,12,0.042,seal_leak,0.49 +FP0325,M028,1,0.327,bearing_wear,0.66 +FP0326,M028,2,0.343,seal_leak,0.77 +FP0327,M028,3,0.337,seal_leak,0.8 +FP0328,M028,4,0.379,rotor_imbalance,0.79 +FP0329,M028,5,0.363,seal_leak,0.78 +FP0330,M028,6,0.387,seal_leak,0.79 +FP0331,M028,7,0.414,seal_leak,0.77 +FP0332,M028,8,0.42,seal_leak,0.75 +FP0333,M028,9,0.42,seal_leak,0.71 +FP0334,M028,10,0.42,seal_leak,0.74 +FP0335,M028,11,0.42,seal_leak,0.8 +FP0336,M028,12,0.42,seal_leak,0.7 +FP0337,M029,1,0.039,electrical_fault,0.52 +FP0338,M029,2,0.02,impeller_erosion,0.49 +FP0339,M029,3,0.037,seal_leak,0.56 +FP0340,M029,4,0.025,seal_leak,0.48 +FP0341,M029,5,0.036,seal_leak,0.6 +FP0342,M029,6,0.022,electrical_fault,0.48 +FP0343,M029,7,0.027,seal_leak,0.56 +FP0344,M029,8,0.037,seal_leak,0.57 +FP0345,M029,9,0.048,seal_leak,0.51 +FP0346,M029,10,0.055,seal_leak,0.62 +FP0347,M029,11,0.055,overheating,0.55 +FP0348,M029,12,0.057,seal_leak,0.6 +FP0349,M030,1,0.1,bearing_wear,0.63 +FP0350,M030,2,0.077,seal_leak,0.51 +FP0351,M030,3,0.081,overheating,0.51 +FP0352,M030,4,0.08,seal_leak,0.53 +FP0353,M030,5,0.1,seal_leak,0.65 +FP0354,M030,6,0.098,seal_leak,0.62 +FP0355,M030,7,0.109,seal_leak,0.62 +FP0356,M030,8,0.109,seal_leak,0.53 +FP0357,M030,9,0.115,seal_leak,0.59 +FP0358,M030,10,0.112,seal_leak,0.61 +FP0359,M030,11,0.125,valve_stuck,0.65 +FP0360,M030,12,0.107,seal_leak,0.64 +FP0361,M031,1,0.086,rotor_imbalance,0.58 +FP0362,M031,2,0.092,rotor_imbalance,0.62 +FP0363,M031,3,0.094,rotor_imbalance,0.65 +FP0364,M031,4,0.083,rotor_imbalance,0.54 +FP0365,M031,5,0.087,rotor_imbalance,0.64 +FP0366,M031,6,0.09,electrical_fault,0.63 +FP0367,M031,7,0.094,motor_burnout,0.64 +FP0368,M031,8,0.117,rotor_imbalance,0.61 +FP0369,M031,9,0.104,rotor_imbalance,0.54 +FP0370,M031,10,0.114,electrical_fault,0.55 +FP0371,M031,11,0.139,rotor_imbalance,0.59 +FP0372,M031,12,0.147,rotor_imbalance,0.65 +FP0373,M032,1,0.066,rotor_imbalance,0.61 +FP0374,M032,2,0.071,rotor_imbalance,0.63 +FP0375,M032,3,0.075,rotor_imbalance,0.6 +FP0376,M032,4,0.083,rotor_imbalance,0.51 +FP0377,M032,5,0.099,rotor_imbalance,0.63 +FP0378,M032,6,0.093,electrical_fault,0.62 +FP0379,M032,7,0.119,impeller_erosion,0.53 +FP0380,M032,8,0.114,rotor_imbalance,0.6 +FP0381,M032,9,0.115,rotor_imbalance,0.54 +FP0382,M032,10,0.137,rotor_imbalance,0.65 +FP0383,M032,11,0.142,rotor_imbalance,0.6 +FP0384,M032,12,0.158,rotor_imbalance,0.6 +FP0385,M033,1,0.073,rotor_imbalance,0.58 +FP0386,M033,2,0.068,rotor_imbalance,0.56 +FP0387,M033,3,0.089,rotor_imbalance,0.59 +FP0388,M033,4,0.084,rotor_imbalance,0.53 +FP0389,M033,5,0.092,rotor_imbalance,0.54 +FP0390,M033,6,0.074,valve_stuck,0.6 +FP0391,M033,7,0.097,rotor_imbalance,0.57 +FP0392,M033,8,0.092,rotor_imbalance,0.56 +FP0393,M033,9,0.107,seal_leak,0.66 +FP0394,M033,10,0.109,rotor_imbalance,0.61 +FP0395,M033,11,0.105,electrical_fault,0.54 +FP0396,M033,12,0.106,overheating,0.53 +FP0397,M034,1,0.036,seal_leak,0.6 +FP0398,M034,2,0.07,rotor_imbalance,0.6 +FP0399,M034,3,0.045,rotor_imbalance,0.48 +FP0400,M034,4,0.053,rotor_imbalance,0.51 +FP0401,M034,5,0.069,rotor_imbalance,0.56 +FP0402,M034,6,0.08,rotor_imbalance,0.6 +FP0403,M034,7,0.055,rotor_imbalance,0.58 +FP0404,M034,8,0.059,rotor_imbalance,0.53 +FP0405,M034,9,0.093,rotor_imbalance,0.51 +FP0406,M034,10,0.091,rotor_imbalance,0.56 +FP0407,M034,11,0.079,valve_stuck,0.63 +FP0408,M034,12,0.072,impeller_erosion,0.53 +FP0409,M035,1,0.089,overheating,0.64 +FP0410,M035,2,0.102,rotor_imbalance,0.52 +FP0411,M035,3,0.099,bearing_wear,0.52 +FP0412,M035,4,0.136,rotor_imbalance,0.61 +FP0413,M035,5,0.137,rotor_imbalance,0.63 +FP0414,M035,6,0.13,rotor_imbalance,0.68 +FP0415,M035,7,0.163,rotor_imbalance,0.56 +FP0416,M035,8,0.143,rotor_imbalance,0.69 +FP0417,M035,9,0.181,motor_burnout,0.59 +FP0418,M035,10,0.163,rotor_imbalance,0.57 +FP0419,M035,11,0.187,rotor_imbalance,0.66 +FP0420,M035,12,0.201,rotor_imbalance,0.63 +FP0421,M036,1,0.053,rotor_imbalance,0.53 +FP0422,M036,2,0.058,rotor_imbalance,0.5 +FP0423,M036,3,0.063,rotor_imbalance,0.59 +FP0424,M036,4,0.048,rotor_imbalance,0.58 +FP0425,M036,5,0.08,impeller_erosion,0.65 +FP0426,M036,6,0.086,rotor_imbalance,0.64 +FP0427,M036,7,0.087,rotor_imbalance,0.59 +FP0428,M036,8,0.092,rotor_imbalance,0.55 +FP0429,M036,9,0.092,rotor_imbalance,0.62 +FP0430,M036,10,0.096,rotor_imbalance,0.57 +FP0431,M036,11,0.082,motor_burnout,0.52 +FP0432,M036,12,0.075,rotor_imbalance,0.5 +FP0433,M037,1,0.065,rotor_imbalance,0.58 +FP0434,M037,2,0.053,rotor_imbalance,0.52 +FP0435,M037,3,0.033,rotor_imbalance,0.59 +FP0436,M037,4,0.041,rotor_imbalance,0.58 +FP0437,M037,5,0.054,rotor_imbalance,0.55 +FP0438,M037,6,0.049,rotor_imbalance,0.54 +FP0439,M037,7,0.084,impeller_erosion,0.53 +FP0440,M037,8,0.076,rotor_imbalance,0.53 +FP0441,M037,9,0.059,rotor_imbalance,0.62 +FP0442,M037,10,0.075,rotor_imbalance,0.56 +FP0443,M037,11,0.069,rotor_imbalance,0.5 +FP0444,M037,12,0.092,rotor_imbalance,0.55 +FP0445,M038,1,0.081,rotor_imbalance,0.59 +FP0446,M038,2,0.081,bearing_wear,0.58 +FP0447,M038,3,0.081,bearing_wear,0.63 +FP0448,M038,4,0.076,rotor_imbalance,0.64 +FP0449,M038,5,0.071,rotor_imbalance,0.61 +FP0450,M038,6,0.112,rotor_imbalance,0.58 +FP0451,M038,7,0.112,motor_burnout,0.59 +FP0452,M038,8,0.097,rotor_imbalance,0.52 +FP0453,M038,9,0.104,rotor_imbalance,0.64 +FP0454,M038,10,0.105,rotor_imbalance,0.53 +FP0455,M038,11,0.121,seal_leak,0.58 +FP0456,M038,12,0.106,rotor_imbalance,0.58 +FP0457,M039,1,0.073,rotor_imbalance,0.51 +FP0458,M039,2,0.054,rotor_imbalance,0.59 +FP0459,M039,3,0.05,rotor_imbalance,0.49 +FP0460,M039,4,0.076,rotor_imbalance,0.51 +FP0461,M039,5,0.079,rotor_imbalance,0.64 +FP0462,M039,6,0.078,rotor_imbalance,0.58 +FP0463,M039,7,0.059,rotor_imbalance,0.54 +FP0464,M039,8,0.096,seal_leak,0.64 +FP0465,M039,9,0.071,rotor_imbalance,0.56 +FP0466,M039,10,0.08,rotor_imbalance,0.54 +FP0467,M039,11,0.078,electrical_fault,0.5 +FP0468,M039,12,0.105,rotor_imbalance,0.59 +FP0469,M040,1,0.09,rotor_imbalance,0.54 +FP0470,M040,2,0.097,rotor_imbalance,0.57 +FP0471,M040,3,0.08,rotor_imbalance,0.53 +FP0472,M040,4,0.102,rotor_imbalance,0.59 +FP0473,M040,5,0.117,rotor_imbalance,0.54 +FP0474,M040,6,0.113,rotor_imbalance,0.65 +FP0475,M040,7,0.117,motor_burnout,0.57 +FP0476,M040,8,0.097,rotor_imbalance,0.66 +FP0477,M040,9,0.104,rotor_imbalance,0.61 +FP0478,M040,10,0.128,rotor_imbalance,0.61 +FP0479,M040,11,0.142,rotor_imbalance,0.59 +FP0480,M040,12,0.109,valve_stuck,0.57 +FP0481,M041,1,0.105,motor_burnout,0.58 +FP0482,M041,2,0.114,motor_burnout,0.54 +FP0483,M041,3,0.122,motor_burnout,0.6 +FP0484,M041,4,0.117,motor_burnout,0.61 +FP0485,M041,5,0.102,motor_burnout,0.61 +FP0486,M041,6,0.139,motor_burnout,0.64 +FP0487,M041,7,0.139,motor_burnout,0.6 +FP0488,M041,8,0.138,motor_burnout,0.62 +FP0489,M041,9,0.131,rotor_imbalance,0.61 +FP0490,M041,10,0.133,motor_burnout,0.56 +FP0491,M041,11,0.134,motor_burnout,0.68 +FP0492,M041,12,0.112,motor_burnout,0.61 +FP0493,M042,1,0.044,impeller_erosion,0.54 +FP0494,M042,2,0.054,motor_burnout,0.58 +FP0495,M042,3,0.043,motor_burnout,0.52 +FP0496,M042,4,0.077,motor_burnout,0.57 +FP0497,M042,5,0.06,motor_burnout,0.59 +FP0498,M042,6,0.063,motor_burnout,0.58 +FP0499,M042,7,0.052,motor_burnout,0.53 +FP0500,M042,8,0.067,motor_burnout,0.63 +FP0501,M042,9,0.087,motor_burnout,0.57 +FP0502,M042,10,0.082,valve_stuck,0.57 +FP0503,M042,11,0.092,bearing_wear,0.63 +FP0504,M042,12,0.073,motor_burnout,0.54 +FP0505,M043,1,0.061,motor_burnout,0.59 +FP0506,M043,2,0.077,valve_stuck,0.51 +FP0507,M043,3,0.056,valve_stuck,0.63 +FP0508,M043,4,0.08,impeller_erosion,0.53 +FP0509,M043,5,0.065,motor_burnout,0.59 +FP0510,M043,6,0.078,motor_burnout,0.56 +FP0511,M043,7,0.09,motor_burnout,0.57 +FP0512,M043,8,0.113,motor_burnout,0.56 +FP0513,M043,9,0.084,electrical_fault,0.64 +FP0514,M043,10,0.103,motor_burnout,0.58 +FP0515,M043,11,0.11,motor_burnout,0.65 +FP0516,M043,12,0.124,motor_burnout,0.64 +FP0517,M044,1,0.092,seal_leak,0.54 +FP0518,M044,2,0.089,motor_burnout,0.63 +FP0519,M044,3,0.095,motor_burnout,0.51 +FP0520,M044,4,0.126,valve_stuck,0.6 +FP0521,M044,5,0.096,motor_burnout,0.61 +FP0522,M044,6,0.108,motor_burnout,0.56 +FP0523,M044,7,0.134,motor_burnout,0.56 +FP0524,M044,8,0.111,motor_burnout,0.58 +FP0525,M044,9,0.107,motor_burnout,0.56 +FP0526,M044,10,0.105,motor_burnout,0.59 +FP0527,M044,11,0.114,motor_burnout,0.57 +FP0528,M044,12,0.114,motor_burnout,0.56 +FP0529,M045,1,0.03,motor_burnout,0.56 +FP0530,M045,2,0.036,motor_burnout,0.49 +FP0531,M045,3,0.043,motor_burnout,0.49 +FP0532,M045,4,0.028,motor_burnout,0.49 +FP0533,M045,5,0.052,motor_burnout,0.61 +FP0534,M045,6,0.058,motor_burnout,0.53 +FP0535,M045,7,0.065,motor_burnout,0.59 +FP0536,M045,8,0.055,rotor_imbalance,0.57 +FP0537,M045,9,0.04,rotor_imbalance,0.49 +FP0538,M045,10,0.05,motor_burnout,0.6 +FP0539,M045,11,0.059,seal_leak,0.61 +FP0540,M045,12,0.067,impeller_erosion,0.52 +FP0541,M046,1,0.058,motor_burnout,0.61 +FP0542,M046,2,0.036,motor_burnout,0.48 +FP0543,M046,3,0.03,motor_burnout,0.59 +FP0544,M046,4,0.038,motor_burnout,0.56 +FP0545,M046,5,0.049,motor_burnout,0.59 +FP0546,M046,6,0.035,motor_burnout,0.48 +FP0547,M046,7,0.045,motor_burnout,0.57 +FP0548,M046,8,0.045,motor_burnout,0.5 +FP0549,M046,9,0.056,motor_burnout,0.52 +FP0550,M046,10,0.067,motor_burnout,0.64 +FP0551,M046,11,0.064,motor_burnout,0.59 +FP0552,M046,12,0.05,motor_burnout,0.59 +FP0553,M047,1,0.205,overheating,0.66 +FP0554,M047,2,0.208,motor_burnout,0.7 +FP0555,M047,3,0.209,overheating,0.66 +FP0556,M047,4,0.241,motor_burnout,0.72 +FP0557,M047,5,0.24,motor_burnout,0.6 +FP0558,M047,6,0.248,motor_burnout,0.69 +FP0559,M047,7,0.282,seal_leak,0.72 +FP0560,M047,8,0.282,motor_burnout,0.71 +FP0561,M047,9,0.294,motor_burnout,0.76 +FP0562,M047,10,0.301,motor_burnout,0.77 +FP0563,M047,11,0.339,motor_burnout,0.79 +FP0564,M047,12,0.354,motor_burnout,0.75 +FP0565,M048,1,0.078,motor_burnout,0.51 +FP0566,M048,2,0.096,bearing_wear,0.61 +FP0567,M048,3,0.095,motor_burnout,0.64 +FP0568,M048,4,0.099,motor_burnout,0.61 +FP0569,M048,5,0.082,motor_burnout,0.56 +FP0570,M048,6,0.083,motor_burnout,0.6 +FP0571,M048,7,0.089,motor_burnout,0.56 +FP0572,M048,8,0.109,seal_leak,0.56 +FP0573,M048,9,0.109,motor_burnout,0.52 +FP0574,M048,10,0.102,motor_burnout,0.64 +FP0575,M048,11,0.104,seal_leak,0.53 +FP0576,M048,12,0.106,motor_burnout,0.58 +FP0577,M049,1,0.081,motor_burnout,0.56 +FP0578,M049,2,0.058,motor_burnout,0.57 +FP0579,M049,3,0.071,motor_burnout,0.52 +FP0580,M049,4,0.079,motor_burnout,0.62 +FP0581,M049,5,0.066,motor_burnout,0.55 +FP0582,M049,6,0.081,motor_burnout,0.65 +FP0583,M049,7,0.084,motor_burnout,0.52 +FP0584,M049,8,0.082,motor_burnout,0.52 +FP0585,M049,9,0.094,motor_burnout,0.58 +FP0586,M049,10,0.07,overheating,0.53 +FP0587,M049,11,0.094,motor_burnout,0.61 +FP0588,M049,12,0.098,motor_burnout,0.6 +FP0589,M050,1,0.047,bearing_wear,0.55 +FP0590,M050,2,0.045,motor_burnout,0.59 +FP0591,M050,3,0.043,motor_burnout,0.52 +FP0592,M050,4,0.029,motor_burnout,0.58 +FP0593,M050,5,0.053,motor_burnout,0.55 +FP0594,M050,6,0.039,motor_burnout,0.62 +FP0595,M050,7,0.066,motor_burnout,0.53 +FP0596,M050,8,0.039,motor_burnout,0.6 +FP0597,M050,9,0.049,motor_burnout,0.52 +FP0598,M050,10,0.074,motor_burnout,0.51 +FP0599,M050,11,0.049,motor_burnout,0.5 +FP0600,M050,12,0.062,motor_burnout,0.6 diff --git a/v1/machine_maintenance/data/fault_types.csv b/v1/machine_maintenance/data/fault_types.csv new file mode 100644 index 00000000..ec5444b3 --- /dev/null +++ b/v1/machine_maintenance/data/fault_types.csv @@ -0,0 +1,16 @@ +fault_type_id,fault_name,fault_category,avg_mttr_minutes,avg_mtbf_hours +FT01,Bearing Failure,Mechanical,75,1000 +FT02,Shaft Misalignment,Mechanical,55,4502 +FT03,Gear Wear,Mechanical,60,1051 +FT04,Belt Snap,Mechanical,35,3853 +FT05,Motor Burnout,Electrical,65,3079 +FT06,Wiring Fault,Electrical,45,1997 +FT07,Sensor Failure,Electrical,30,2520 +FT08,Control Board,Electrical,50,2263 +FT09,Overheating,Process,60,1781 +FT10,Pressure Loss,Process,45,478 +FT11,Flow Blockage,Process,40,394 +FT12,Seal Degradation,Material,55,953 +FT13,Corrosion,Material,50,665 +FT14,Impeller Erosion,Material,65,1976 +FT15,Valve Fouling,Material,50,1197 diff --git a/v1/machine_maintenance/data/machine_product_capabilities.csv b/v1/machine_maintenance/data/machine_product_capabilities.csv new file mode 100644 index 00000000..b7985c90 --- /dev/null +++ b/v1/machine_maintenance/data/machine_product_capabilities.csv @@ -0,0 +1,121 @@ +machine_id,product_id,design_speed,optimal_speed,waste_conversion_factor +M001,PRD01,20.8,17.9,0.044 +M001,PRD07,22.2,20.3,0.046 +M002,PRD01,20.3,19.2,0.046 +M002,PRD07,24.9,21.8,0.029 +M003,PRD01,21.7,19.8,0.047 +M003,PRD07,22.9,20.4,0.046 +M004,PRD01,21.1,18.6,0.016 +M004,PRD07,22.0,20.4,0.025 +M005,PRD01,19.3,16.5,0.048 +M005,PRD07,22.9,20.5,0.03 +M006,PRD01,21.7,19.8,0.017 +M006,PRD07,24.0,20.7,0.041 +M007,PRD01,18.9,17.3,0.047 +M007,PRD07,25.0,21.5,0.013 +M008,PRD01,20.3,17.8,0.028 +M008,PRD07,25.9,22.4,0.036 +M009,PRD01,21.7,19.9,0.021 +M009,PRD07,25.0,23.5,0.033 +M010,PRD01,20.1,19.0,0.03 +M010,PRD07,23.7,22.1,0.041 +M011,PRD02,42.3,38.2,0.01 +M011,PRD06,87.3,81.3,0.029 +M012,PRD02,40.5,38.2,0.024 +M012,PRD06,82.9,73.8,0.035 +M013,PRD02,43.7,37.5,0.031 +M013,PRD06,87.4,75.0,0.028 +M014,PRD02,42.1,38.5,0.017 +M014,PRD06,72.6,64.5,0.018 +M015,PRD06,77.7,66.7,0.035 +M015,PRD02,41.6,39.1,0.035 +M016,PRD02,41.9,36.2,0.018 +M016,PRD06,83.5,75.9,0.03 +M017,PRD02,36.5,33.0,0.022 +M017,PRD06,80.5,75.1,0.028 +M018,PRD02,44.0,37.6,0.012 +M018,PRD06,84.6,72.3,0.015 +M019,PRD02,42.5,39.0,0.028 +M019,PRD06,82.8,77.6,0.016 +M020,PRD02,40.6,38.3,0.011 +M020,PRD06,74.3,68.0,0.029 +M021,PRD03,32.5,27.8,0.038 +M021,PRD06,74.1,64.3,0.012 +M021,PRD07,23.1,21.5,0.028 +M022,PRD03,28.0,25.5,0.043 +M022,PRD06,73.7,66.3,0.042 +M022,PRD07,22.7,21.1,0.027 +M023,PRD03,31.9,30.2,0.037 +M023,PRD06,77.2,73.2,0.033 +M023,PRD07,22.3,20.8,0.042 +M024,PRD03,31.6,28.7,0.023 +M024,PRD06,73.9,63.9,0.027 +M024,PRD07,24.9,22.5,0.028 +M025,PRD03,32.6,27.8,0.014 +M025,PRD06,85.6,78.0,0.041 +M025,PRD07,21.7,19.5,0.01 +M026,PRD03,28.1,24.1,0.036 +M026,PRD06,86.2,76.9,0.042 +M026,PRD07,25.3,23.3,0.018 +M027,PRD07,22.5,20.2,0.048 +M027,PRD06,87.0,79.3,0.012 +M027,PRD03,30.8,27.8,0.026 +M028,PRD03,30.8,28.6,0.024 +M028,PRD06,72.1,63.0,0.015 +M028,PRD07,24.1,20.7,0.024 +M029,PRD03,31.6,28.6,0.019 +M029,PRD06,84.3,77.2,0.021 +M029,PRD07,22.5,20.9,0.037 +M030,PRD03,32.8,29.4,0.017 +M030,PRD06,82.7,70.7,0.036 +M030,PRD07,23.0,20.5,0.023 +M031,PRD04,12.7,11.8,0.012 +M031,PRD08,15.7,14.8,0.017 +M032,PRD08,16.2,14.9,0.034 +M032,PRD04,12.7,11.4,0.015 +M033,PRD04,11.6,10.1,0.037 +M033,PRD08,14.5,13.5,0.023 +M034,PRD04,11.0,10.2,0.045 +M034,PRD08,14.6,12.7,0.029 +M035,PRD04,12.0,11.3,0.016 +M035,PRD08,16.3,14.6,0.025 +M036,PRD04,11.1,10.2,0.035 +M036,PRD08,16.4,14.3,0.015 +M037,PRD04,12.5,10.7,0.015 +M037,PRD08,16.1,14.4,0.017 +M038,PRD04,11.6,10.9,0.034 +M038,PRD08,15.9,14.5,0.034 +M039,PRD04,11.2,10.6,0.044 +M039,PRD08,13.8,12.9,0.03 +M040,PRD08,14.3,13.2,0.039 +M040,PRD04,13.2,12.0,0.043 +M041,PRD05,45.6,40.3,0.033 +M041,PRD08,16.2,14.7,0.016 +M041,PRD07,24.4,20.8,0.049 +M042,PRD05,43.6,39.1,0.041 +M042,PRD08,13.6,12.3,0.029 +M042,PRD07,25.4,23.7,0.028 +M043,PRD05,45.5,41.2,0.01 +M043,PRD08,15.7,14.8,0.015 +M043,PRD07,23.0,21.0,0.014 +M044,PRD05,47.8,42.1,0.038 +M044,PRD08,16.1,14.9,0.011 +M044,PRD07,23.8,22.1,0.045 +M045,PRD07,21.9,20.6,0.023 +M045,PRD08,14.9,13.6,0.044 +M045,PRD05,47.1,43.8,0.025 +M046,PRD05,43.5,38.8,0.021 +M046,PRD08,16.3,14.7,0.042 +M046,PRD07,25.4,21.8,0.045 +M047,PRD05,50.3,43.0,0.048 +M047,PRD08,15.5,14.6,0.014 +M047,PRD07,23.4,21.3,0.025 +M048,PRD05,44.9,40.7,0.013 +M048,PRD08,15.6,13.6,0.049 +M048,PRD07,22.7,19.6,0.029 +M049,PRD05,52.2,47.4,0.048 +M049,PRD08,14.1,13.0,0.019 +M049,PRD07,23.3,20.4,0.037 +M050,PRD05,45.9,39.3,0.024 +M050,PRD08,15.0,13.5,0.015 +M050,PRD07,25.0,22.9,0.031 diff --git a/v1/machine_maintenance/data/machines.csv b/v1/machine_maintenance/data/machines.csv new file mode 100644 index 00000000..73fd70db --- /dev/null +++ b/v1/machine_maintenance/data/machines.csv @@ -0,0 +1,51 @@ +machine_id,machine_name,machine_type,facility,location,line_id,remaining_useful_life,failure_probability,criticality,maintenance_duration_hours,last_maintenance_date,parts_required,estimated_parts_cost,replacement_cost,annual_output_value,age_years,daily_production_target +M001,Turbine_A_1,Turbine,Plant_A,Houston_TX,L01,5.0,0.267,4,8,2024-04-24,Bearings|Seals,12500.0,520000.0,185000.0,12,88 +M002,Turbine_B_2,Turbine,Plant_B,Chicago_IL,L07,38.9,0.03,5,8,2024-02-14,Bearings|Seals,12500.0,520000.0,185000.0,13,117 +M003,Turbine_C_3,Turbine,Plant_C,Phoenix_AZ,L13,16.3,0.103,3,8,2024-09-15,Bearings|Seals,12500.0,520000.0,185000.0,8,118 +M004,Turbine_D_4,Turbine,Plant_A,Houston_TX,L04,38.1,0.076,5,8,2024-08-02,Bearings|Seals,12500.0,520000.0,185000.0,2,94 +M005,Turbine_E_5,Turbine,Plant_C,Phoenix_AZ,L15,41.3,0.079,1,8,2024-03-22,Bearings|Seals,12500.0,520000.0,185000.0,9,107 +M006,Turbine_F_6,Turbine,Plant_A,Houston_TX,L01,2.0,0.292,5,8,2024-02-22,Bearings|Seals,12500.0,520000.0,185000.0,7,85 +M007,Turbine_G_7,Turbine,Plant_B,Chicago_IL,L07,42.7,0.03,5,8,2024-05-15,Bearings|Seals,12500.0,520000.0,185000.0,8,82 +M008,Turbine_H_8,Turbine,Plant_C,Phoenix_AZ,L13,17.4,0.066,4,8,2024-02-10,Bearings|Seals,12500.0,520000.0,185000.0,13,115 +M009,Turbine_I_9,Turbine,Plant_A,Houston_TX,L04,34.6,0.103,3,8,2024-10-22,Bearings|Seals,12500.0,520000.0,185000.0,6,92 +M010,Turbine_J_10,Turbine,Plant_C,Phoenix_AZ,L15,36.1,0.027,3,8,2024-02-10,Bearings|Seals,12500.0,520000.0,185000.0,13,94 +M011,Compressor_A_11,Compressor,Plant_B,Chicago_IL,L06,2.5,0.307,5,6,2024-03-24,Valves|Filters,8200.0,210000.0,95000.0,3,173 +M012,Compressor_B_12,Compressor,Plant_B,Chicago_IL,L07,20.3,0.281,4,6,2024-03-28,Valves|Filters,8200.0,210000.0,95000.0,7,184 +M013,Compressor_C_13,Compressor,Plant_A,Houston_TX,L03,27.2,0.044,3,6,2024-10-12,Valves|Filters,8200.0,210000.0,95000.0,13,164 +M014,Compressor_D_14,Compressor,Plant_C,Phoenix_AZ,L14,37.9,0.126,3,6,2024-04-27,Valves|Filters,8200.0,210000.0,95000.0,12,152 +M015,Compressor_E_15,Compressor,Plant_A,Houston_TX,L05,20.4,0.052,2,6,2024-10-17,Valves|Filters,8200.0,210000.0,95000.0,14,170 +M016,Compressor_F_16,Compressor,Plant_B,Chicago_IL,L06,24.8,0.348,5,6,2024-03-14,Valves|Filters,8200.0,210000.0,95000.0,5,166 +M017,Compressor_G_17,Compressor,Plant_B,Chicago_IL,L07,30.6,0.045,3,6,2024-10-26,Valves|Filters,8200.0,210000.0,95000.0,4,177 +M018,Compressor_H_18,Compressor,Plant_A,Houston_TX,L03,18.7,0.06,2,6,2024-09-17,Valves|Filters,8200.0,210000.0,95000.0,11,181 +M019,Compressor_I_19,Compressor,Plant_C,Phoenix_AZ,L14,41.1,0.096,2,6,2024-03-22,Valves|Filters,8200.0,210000.0,95000.0,3,177 +M020,Compressor_J_20,Compressor,Plant_A,Houston_TX,L05,24.4,0.026,4,6,2024-09-27,Valves|Filters,8200.0,210000.0,95000.0,11,166 +M021,Pump_A_21,Pump,Plant_A,Houston_TX,L01,9.4,0.106,1,4,2024-10-01,Impeller|Seals,5800.0,105000.0,62000.0,10,197 +M022,Pump_B_22,Pump,Plant_B,Chicago_IL,L07,0.7,0.151,4,4,2024-03-21,Impeller|Seals,5800.0,105000.0,62000.0,14,209 +M023,Pump_C_23,Pump,Plant_C,Phoenix_AZ,L13,39.7,0.115,3,4,2024-09-13,Impeller|Seals,5800.0,105000.0,62000.0,2,191 +M024,Pump_D_24,Pump,Plant_A,Houston_TX,L04,39.5,0.111,3,4,2024-09-16,Impeller|Seals,5800.0,105000.0,62000.0,10,218 +M025,Pump_E_25,Pump,Plant_B,Chicago_IL,L10,2.5,0.035,5,4,2024-09-28,Impeller|Seals,5800.0,105000.0,62000.0,5,180 +M026,Pump_F_26,Pump,Plant_C,Phoenix_AZ,L11,9.7,0.052,3,4,2024-06-06,Impeller|Seals,5800.0,105000.0,62000.0,11,195 +M027,Pump_G_27,Pump,Plant_A,Houston_TX,L02,28.9,0.044,1,4,2024-02-13,Impeller|Seals,5800.0,105000.0,62000.0,2,211 +M028,Pump_H_28,Pump,Plant_B,Chicago_IL,L08,27.6,0.397,4,4,2024-03-06,Impeller|Seals,5800.0,105000.0,62000.0,3,210 +M029,Pump_I_29,Pump,Plant_C,Phoenix_AZ,L14,27.5,0.037,5,4,2024-08-04,Impeller|Seals,5800.0,105000.0,62000.0,10,193 +M030,Pump_J_30,Pump,Plant_A,Houston_TX,L05,33.1,0.096,3,4,2024-07-23,Impeller|Seals,5800.0,105000.0,62000.0,10,203 +M031,Generator_A_31,Generator,Plant_A,Houston_TX,L01,32.8,0.11,2,12,2024-04-25,Rotor|Stator,22000.0,410000.0,230000.0,9,94 +M032,Generator_B_32,Generator,Plant_B,Chicago_IL,L07,36.4,0.102,5,12,2024-04-22,Rotor|Stator,22000.0,410000.0,230000.0,7,90 +M033,Generator_C_33,Generator,Plant_C,Phoenix_AZ,L13,19.1,0.091,1,12,2024-01-17,Rotor|Stator,22000.0,410000.0,230000.0,3,111 +M034,Generator_D_34,Generator,Plant_A,Houston_TX,L04,26.7,0.071,4,12,2024-04-19,Rotor|Stator,22000.0,410000.0,230000.0,3,124 +M035,Generator_E_35,Generator,Plant_B,Chicago_IL,L10,8.6,0.158,5,12,2024-08-30,Rotor|Stator,22000.0,410000.0,230000.0,4,105 +M036,Generator_F_36,Generator,Plant_C,Phoenix_AZ,L11,31.2,0.067,1,12,2024-02-19,Rotor|Stator,22000.0,410000.0,230000.0,14,117 +M037,Generator_G_37,Generator,Plant_A,Houston_TX,L02,33.3,0.062,1,12,2024-02-20,Rotor|Stator,22000.0,410000.0,230000.0,7,93 +M038,Generator_H_38,Generator,Plant_B,Chicago_IL,L08,7.9,0.093,1,12,2024-05-07,Rotor|Stator,22000.0,410000.0,230000.0,8,102 +M039,Generator_I_39,Generator,Plant_C,Phoenix_AZ,L14,21.9,0.074,2,12,2024-05-22,Rotor|Stator,22000.0,410000.0,230000.0,5,119 +M040,Generator_J_40,Generator,Plant_A,Houston_TX,L05,19.6,0.107,5,12,2024-02-20,Rotor|Stator,22000.0,410000.0,230000.0,5,93 +M041,Motor_A_41,Motor,Plant_A,Houston_TX,L01,37.3,0.12,1,3,2024-05-01,Windings|Bearings,4200.0,82000.0,48000.0,12,150 +M042,Motor_B_42,Motor,Plant_B,Chicago_IL,L07,15.5,0.069,4,3,2024-01-31,Windings|Bearings,4200.0,82000.0,48000.0,8,150 +M043,Motor_C_43,Motor,Plant_C,Phoenix_AZ,L13,1.1,0.1,4,3,2024-05-26,Windings|Bearings,4200.0,82000.0,48000.0,8,167 +M044,Motor_D_44,Motor,Plant_A,Houston_TX,L04,42.8,0.116,5,3,2024-09-06,Windings|Bearings,4200.0,82000.0,48000.0,13,149 +M045,Motor_E_45,Motor,Plant_B,Chicago_IL,L10,41.9,0.05,5,3,2024-10-04,Windings|Bearings,4200.0,82000.0,48000.0,5,143 +M046,Motor_F_46,Motor,Plant_C,Phoenix_AZ,L11,9.8,0.051,4,3,2024-09-14,Windings|Bearings,4200.0,82000.0,48000.0,13,173 +M047,Motor_G_47,Motor,Plant_A,Houston_TX,L02,25.8,0.259,4,3,2024-02-05,Windings|Bearings,4200.0,82000.0,48000.0,4,178 +M048,Motor_H_48,Motor,Plant_B,Chicago_IL,L08,16.2,0.088,1,3,2024-10-18,Windings|Bearings,4200.0,82000.0,48000.0,3,155 +M049,Motor_I_49,Motor,Plant_C,Phoenix_AZ,L14,29.7,0.079,4,3,2024-10-25,Windings|Bearings,4200.0,82000.0,48000.0,11,176 +M050,Motor_J_50,Motor,Plant_A,Houston_TX,L05,17.1,0.052,3,3,2024-05-02,Windings|Bearings,4200.0,82000.0,48000.0,10,156 diff --git a/v1/machine_maintenance/data/production_runs.csv b/v1/machine_maintenance/data/production_runs.csv new file mode 100644 index 00000000..e09fac30 --- /dev/null +++ b/v1/machine_maintenance/data/production_runs.csv @@ -0,0 +1,845 @@ +run_id,machine_id,product_id,shift_id,period,planned_quantity,actual_quantity,good_quantity,waste_quantity,actual_speed,target_speed +RUN0001,M001,PRD07,SHIFT_DAY,1,162,90,89,1,14.8,20.3 +RUN0002,M001,PRD01,SHIFT_SWING,2,143,92,90,2,13.2,17.9 +RUN0003,M001,PRD01,SHIFT_NIGHT,3,143,78,75,3,12.6,17.9 +RUN0004,M001,PRD07,SHIFT_SWING,4,162,63,62,1,15.4,20.3 +RUN0005,M001,PRD01,SHIFT_DAY,5,143,76,74,2,12.8,17.9 +RUN0006,M001,PRD01,SHIFT_NIGHT,6,143,68,66,2,12.8,17.9 +RUN0007,M001,PRD07,SHIFT_DAY,7,162,86,85,1,14.3,20.3 +RUN0008,M001,PRD01,SHIFT_NIGHT,8,143,66,64,2,11.4,17.9 +RUN0009,M001,PRD07,SHIFT_NIGHT,8,162,83,80,3,14.3,20.3 +RUN0010,M001,PRD07,SHIFT_SWING,9,162,88,87,1,13.5,20.3 +RUN0011,M001,PRD01,SHIFT_SWING,9,143,88,84,4,13.4,17.9 +RUN0012,M001,PRD01,SHIFT_DAY,10,143,80,78,2,12.4,17.9 +RUN0013,M001,PRD07,SHIFT_NIGHT,11,162,103,101,2,14.3,20.3 +RUN0014,M001,PRD01,SHIFT_DAY,11,143,100,96,4,13.9,17.9 +RUN0015,M001,PRD01,SHIFT_DAY,12,143,79,78,1,13.1,17.9 +RUN0016,M002,PRD01,SHIFT_NIGHT,1,153,98,95,3,12.3,19.2 +RUN0017,M002,PRD01,SHIFT_DAY,2,153,104,102,2,13.0,19.2 +RUN0018,M002,PRD07,SHIFT_DAY,2,174,124,122,2,15.5,21.8 +RUN0019,M002,PRD07,SHIFT_DAY,3,174,116,113,3,14.6,21.8 +RUN0020,M002,PRD07,SHIFT_DAY,4,174,133,131,2,16.7,21.8 +RUN0021,M002,PRD01,SHIFT_NIGHT,4,153,103,98,5,12.9,19.2 +RUN0022,M002,PRD01,SHIFT_DAY,5,153,105,100,5,14.0,19.2 +RUN0023,M002,PRD07,SHIFT_NIGHT,5,174,107,104,3,14.3,21.8 +RUN0024,M002,PRD01,SHIFT_DAY,6,153,112,107,5,14.1,19.2 +RUN0025,M002,PRD07,SHIFT_SWING,6,174,131,128,3,16.4,21.8 +RUN0026,M002,PRD01,SHIFT_NIGHT,7,153,109,106,3,13.7,19.2 +RUN0027,M002,PRD01,SHIFT_NIGHT,8,153,79,76,3,13.0,19.2 +RUN0028,M002,PRD01,SHIFT_SWING,9,153,102,99,3,12.8,19.2 +RUN0029,M002,PRD01,SHIFT_DAY,10,153,104,101,3,13.0,19.2 +RUN0030,M002,PRD07,SHIFT_DAY,11,174,128,127,1,16.1,21.8 +RUN0031,M002,PRD07,SHIFT_DAY,12,174,120,117,3,15.1,21.8 +RUN0032,M002,PRD01,SHIFT_DAY,12,153,111,106,5,13.9,19.2 +RUN0033,M003,PRD07,SHIFT_DAY,1,163,135,132,3,18.1,20.4 +RUN0034,M003,PRD01,SHIFT_DAY,1,158,118,117,1,15.8,19.8 +RUN0035,M003,PRD01,SHIFT_NIGHT,2,158,119,115,4,16.4,19.8 +RUN0036,M003,PRD07,SHIFT_DAY,2,163,123,122,1,17.0,20.4 +RUN0037,M003,PRD01,SHIFT_DAY,3,158,126,124,2,17.0,19.8 +RUN0038,M003,PRD07,SHIFT_DAY,3,163,120,118,2,16.1,20.4 +RUN0039,M003,PRD01,SHIFT_DAY,4,158,118,115,3,16.7,19.8 +RUN0040,M003,PRD01,SHIFT_NIGHT,5,158,108,105,3,14.7,19.8 +RUN0041,M003,PRD07,SHIFT_NIGHT,6,163,120,116,4,15.8,20.4 +RUN0042,M003,PRD01,SHIFT_SWING,6,158,130,126,4,17.1,19.8 +RUN0043,M003,PRD07,SHIFT_SWING,7,163,119,116,3,15.7,20.4 +RUN0044,M003,PRD01,SHIFT_NIGHT,7,158,119,116,3,15.7,19.8 +RUN0045,M003,PRD07,SHIFT_DAY,8,163,113,110,3,17.9,20.4 +RUN0046,M003,PRD07,SHIFT_NIGHT,9,163,113,109,4,16.0,20.4 +RUN0047,M003,PRD07,SHIFT_SWING,10,163,117,115,2,16.1,20.4 +RUN0048,M003,PRD07,SHIFT_SWING,11,163,117,114,3,15.7,20.4 +RUN0049,M003,PRD01,SHIFT_DAY,11,158,132,131,1,17.7,19.8 +RUN0050,M003,PRD07,SHIFT_SWING,12,163,136,130,6,17.8,20.4 +RUN0051,M003,PRD01,SHIFT_NIGHT,12,158,116,112,4,15.2,19.8 +RUN0052,M004,PRD01,SHIFT_DAY,1,148,118,118,0,14.8,18.6 +RUN0053,M004,PRD07,SHIFT_NIGHT,1,163,106,104,2,13.3,20.4 +RUN0054,M004,PRD07,SHIFT_NIGHT,2,163,105,103,2,14.5,20.4 +RUN0055,M004,PRD01,SHIFT_NIGHT,2,148,91,91,0,12.6,18.6 +RUN0056,M004,PRD07,SHIFT_DAY,3,163,128,126,2,16.1,20.4 +RUN0057,M004,PRD01,SHIFT_SWING,4,148,108,107,1,13.6,18.6 +RUN0058,M004,PRD07,SHIFT_DAY,4,163,116,114,2,14.5,20.4 +RUN0059,M004,PRD01,SHIFT_DAY,5,148,115,114,1,14.4,18.6 +RUN0060,M004,PRD07,SHIFT_SWING,5,163,115,113,2,14.4,20.4 +RUN0061,M004,PRD01,SHIFT_DAY,6,148,116,116,0,14.5,18.6 +RUN0062,M004,PRD07,SHIFT_SWING,6,163,116,114,2,14.6,20.4 +RUN0063,M004,PRD07,SHIFT_NIGHT,7,163,112,111,1,14.9,20.4 +RUN0064,M004,PRD01,SHIFT_NIGHT,7,148,100,100,0,13.4,18.6 +RUN0065,M004,PRD01,SHIFT_NIGHT,8,148,100,100,0,12.6,18.6 +RUN0066,M004,PRD07,SHIFT_DAY,9,163,112,110,2,14.1,20.4 +RUN0067,M004,PRD01,SHIFT_NIGHT,9,148,100,100,0,12.6,18.6 +RUN0068,M004,PRD01,SHIFT_SWING,10,148,101,101,0,12.7,18.6 +RUN0069,M004,PRD01,SHIFT_SWING,11,148,108,108,0,13.5,18.6 +RUN0070,M004,PRD01,SHIFT_DAY,12,148,105,104,1,13.2,18.6 +RUN0071,M005,PRD01,SHIFT_NIGHT,1,132,97,95,2,12.2,16.5 +RUN0072,M005,PRD07,SHIFT_DAY,2,164,134,131,3,16.8,20.5 +RUN0073,M005,PRD01,SHIFT_DAY,3,132,103,99,4,12.9,16.5 +RUN0074,M005,PRD01,SHIFT_DAY,4,132,105,100,5,13.2,16.5 +RUN0075,M005,PRD07,SHIFT_SWING,5,164,126,124,2,16.6,20.5 +RUN0076,M005,PRD07,SHIFT_NIGHT,6,164,136,132,4,17.0,20.5 +RUN0077,M005,PRD07,SHIFT_SWING,7,164,136,132,4,17.0,20.5 +RUN0078,M005,PRD01,SHIFT_SWING,7,132,112,111,1,14.1,16.5 +RUN0079,M005,PRD07,SHIFT_DAY,8,164,140,136,4,17.5,20.5 +RUN0080,M005,PRD01,SHIFT_SWING,8,132,115,110,5,14.4,16.5 +RUN0081,M005,PRD07,SHIFT_NIGHT,9,164,132,128,4,16.5,20.5 +RUN0082,M005,PRD01,SHIFT_DAY,10,132,119,114,5,14.9,16.5 +RUN0083,M005,PRD01,SHIFT_DAY,11,132,119,114,5,14.9,16.5 +RUN0084,M005,PRD07,SHIFT_NIGHT,11,164,127,124,3,15.9,20.5 +RUN0085,M005,PRD01,SHIFT_SWING,12,132,106,101,5,13.3,16.5 +RUN0086,M006,PRD01,SHIFT_SWING,1,158,110,109,1,14.5,19.8 +RUN0087,M006,PRD07,SHIFT_NIGHT,1,165,115,111,4,15.1,20.7 +RUN0088,M006,PRD01,SHIFT_SWING,2,158,113,112,1,14.9,19.8 +RUN0089,M006,PRD07,SHIFT_NIGHT,2,165,117,112,5,15.4,20.7 +RUN0090,M006,PRD07,SHIFT_DAY,3,165,103,100,3,15.3,20.7 +RUN0091,M006,PRD01,SHIFT_NIGHT,4,158,99,99,0,14.1,19.8 +RUN0092,M006,PRD01,SHIFT_SWING,5,158,115,114,1,15.1,19.8 +RUN0093,M006,PRD07,SHIFT_DAY,5,165,121,117,4,15.9,20.7 +RUN0094,M006,PRD01,SHIFT_NIGHT,6,158,91,90,1,13.9,19.8 +RUN0095,M006,PRD07,SHIFT_NIGHT,6,165,91,90,1,13.9,20.7 +RUN0096,M006,PRD01,SHIFT_SWING,7,158,96,95,1,15.1,19.8 +RUN0097,M006,PRD07,SHIFT_NIGHT,7,165,89,86,3,14.0,20.7 +RUN0098,M006,PRD07,SHIFT_DAY,8,165,108,106,2,14.7,20.7 +RUN0099,M006,PRD01,SHIFT_DAY,8,158,101,100,1,13.7,19.8 +RUN0100,M006,PRD01,SHIFT_DAY,9,158,111,109,2,15.5,19.8 +RUN0101,M006,PRD07,SHIFT_NIGHT,9,165,106,104,2,14.8,20.7 +RUN0102,M006,PRD01,SHIFT_SWING,10,158,96,95,1,13.8,19.8 +RUN0103,M006,PRD07,SHIFT_NIGHT,10,165,106,105,1,15.2,20.7 +RUN0104,M006,PRD01,SHIFT_SWING,11,158,87,86,1,13.1,19.8 +RUN0105,M006,PRD07,SHIFT_SWING,11,165,93,89,4,14.0,20.7 +RUN0106,M006,PRD07,SHIFT_DAY,12,165,96,95,1,15.6,20.7 +RUN0107,M006,PRD01,SHIFT_NIGHT,12,158,88,87,1,14.4,19.8 +RUN0108,M007,PRD07,SHIFT_SWING,1,172,120,119,1,15.0,21.5 +RUN0109,M007,PRD07,SHIFT_NIGHT,2,172,121,120,1,15.2,21.5 +RUN0110,M007,PRD07,SHIFT_SWING,3,172,102,101,1,14.2,21.5 +RUN0111,M007,PRD01,SHIFT_NIGHT,3,138,89,87,2,12.4,17.3 +RUN0112,M007,PRD01,SHIFT_NIGHT,4,138,82,78,4,11.4,17.3 +RUN0113,M007,PRD07,SHIFT_NIGHT,4,172,110,110,0,15.3,21.5 +RUN0114,M007,PRD07,SHIFT_SWING,5,172,118,117,1,14.8,21.5 +RUN0115,M007,PRD07,SHIFT_SWING,6,172,112,111,1,14.0,21.5 +RUN0116,M007,PRD01,SHIFT_NIGHT,7,138,82,80,2,12.2,17.3 +RUN0117,M007,PRD01,SHIFT_NIGHT,8,138,89,86,3,11.2,17.3 +RUN0118,M007,PRD07,SHIFT_SWING,9,172,128,128,0,16.0,21.5 +RUN0119,M007,PRD01,SHIFT_NIGHT,9,138,88,85,3,11.0,17.3 +RUN0120,M007,PRD01,SHIFT_NIGHT,10,138,96,92,4,12.1,17.3 +RUN0121,M007,PRD07,SHIFT_NIGHT,11,172,121,120,1,15.2,21.5 +RUN0122,M007,PRD07,SHIFT_DAY,12,172,132,132,0,16.6,21.5 +RUN0123,M008,PRD07,SHIFT_NIGHT,1,179,138,136,2,17.3,22.4 +RUN0124,M008,PRD01,SHIFT_DAY,2,142,114,112,2,14.3,17.8 +RUN0125,M008,PRD07,SHIFT_NIGHT,2,179,142,140,2,17.8,22.4 +RUN0126,M008,PRD01,SHIFT_NIGHT,3,142,108,106,2,13.6,17.8 +RUN0127,M008,PRD07,SHIFT_SWING,3,179,143,141,2,17.9,22.4 +RUN0128,M008,PRD07,SHIFT_NIGHT,4,179,144,141,3,18.0,22.4 +RUN0129,M008,PRD07,SHIFT_SWING,5,179,141,140,1,18.8,22.4 +RUN0130,M008,PRD07,SHIFT_NIGHT,6,179,144,142,2,18.1,22.4 +RUN0131,M008,PRD01,SHIFT_DAY,6,142,112,111,1,14.0,17.8 +RUN0132,M008,PRD01,SHIFT_NIGHT,7,142,102,100,2,12.8,17.8 +RUN0133,M008,PRD07,SHIFT_NIGHT,7,179,146,142,4,18.3,22.4 +RUN0134,M008,PRD01,SHIFT_DAY,8,142,128,125,3,16.1,17.8 +RUN0135,M008,PRD01,SHIFT_DAY,9,142,110,107,3,15.1,17.8 +RUN0136,M008,PRD01,SHIFT_NIGHT,10,142,106,105,1,13.3,17.8 +RUN0137,M008,PRD01,SHIFT_DAY,11,142,112,111,1,15.0,17.8 +RUN0138,M008,PRD07,SHIFT_NIGHT,11,179,123,118,5,16.6,22.4 +RUN0139,M008,PRD01,SHIFT_DAY,12,142,128,126,2,16.0,17.8 +RUN0140,M008,PRD07,SHIFT_SWING,12,179,149,146,3,18.7,22.4 +RUN0141,M009,PRD07,SHIFT_NIGHT,1,188,130,128,2,16.3,23.5 +RUN0142,M009,PRD01,SHIFT_DAY,1,159,120,120,0,15.1,19.9 +RUN0143,M009,PRD01,SHIFT_SWING,2,159,120,119,1,15.1,19.9 +RUN0144,M009,PRD01,SHIFT_NIGHT,3,159,107,107,0,13.4,19.9 +RUN0145,M009,PRD01,SHIFT_SWING,4,159,112,110,2,14.0,19.9 +RUN0146,M009,PRD07,SHIFT_SWING,4,188,133,128,5,16.7,23.5 +RUN0147,M009,PRD01,SHIFT_DAY,5,159,120,119,1,15.0,19.9 +RUN0148,M009,PRD07,SHIFT_NIGHT,5,188,133,131,2,16.7,23.5 +RUN0149,M009,PRD01,SHIFT_SWING,6,159,120,119,1,15.0,19.9 +RUN0150,M009,PRD07,SHIFT_NIGHT,7,188,124,122,2,15.5,23.5 +RUN0151,M009,PRD01,SHIFT_NIGHT,7,159,102,101,1,12.8,19.9 +RUN0152,M009,PRD01,SHIFT_SWING,8,159,98,96,2,15.2,19.9 +RUN0153,M009,PRD01,SHIFT_SWING,9,159,105,104,1,13.2,19.9 +RUN0154,M009,PRD07,SHIFT_DAY,10,188,132,128,4,16.5,23.5 +RUN0155,M009,PRD01,SHIFT_SWING,10,159,116,114,2,14.5,19.9 +RUN0156,M009,PRD07,SHIFT_NIGHT,11,188,112,110,2,16.4,23.5 +RUN0157,M009,PRD01,SHIFT_DAY,12,159,100,98,2,14.0,19.9 +RUN0158,M010,PRD07,SHIFT_NIGHT,1,176,129,124,5,17.8,22.1 +RUN0159,M010,PRD07,SHIFT_NIGHT,2,176,129,128,1,16.2,22.1 +RUN0160,M010,PRD01,SHIFT_NIGHT,2,152,122,119,3,15.3,19.0 +RUN0161,M010,PRD07,SHIFT_DAY,3,176,158,153,5,19.8,22.1 +RUN0162,M010,PRD01,SHIFT_NIGHT,4,152,120,117,3,15.1,19.0 +RUN0163,M010,PRD07,SHIFT_SWING,5,176,132,130,2,18.0,22.1 +RUN0164,M010,PRD01,SHIFT_SWING,6,152,126,125,1,15.8,19.0 +RUN0165,M010,PRD07,SHIFT_DAY,7,176,152,150,2,19.0,22.1 +RUN0166,M010,PRD01,SHIFT_DAY,8,152,129,128,1,16.2,19.0 +RUN0167,M010,PRD07,SHIFT_DAY,9,176,136,133,3,19.9,22.1 +RUN0168,M010,PRD01,SHIFT_SWING,10,152,124,121,3,15.5,19.0 +RUN0169,M010,PRD01,SHIFT_DAY,11,152,130,126,4,16.3,19.0 +RUN0170,M010,PRD07,SHIFT_NIGHT,11,176,127,124,3,15.9,22.1 +RUN0171,M010,PRD07,SHIFT_DAY,12,176,160,155,5,20.0,22.1 +RUN0172,M011,PRD02,SHIFT_DAY,1,305,189,187,2,28.3,38.2 +RUN0173,M011,PRD06,SHIFT_NIGHT,1,650,389,384,5,58.0,81.3 +RUN0174,M011,PRD02,SHIFT_NIGHT,2,305,167,167,0,25.0,38.2 +RUN0175,M011,PRD02,SHIFT_SWING,3,305,201,200,1,28.7,38.2 +RUN0176,M011,PRD02,SHIFT_SWING,4,305,173,173,0,26.6,38.2 +RUN0177,M011,PRD06,SHIFT_DAY,4,650,396,387,9,60.8,81.3 +RUN0178,M011,PRD06,SHIFT_NIGHT,5,650,311,302,9,54.6,81.3 +RUN0179,M011,PRD06,SHIFT_SWING,6,650,261,256,5,52.3,81.3 +RUN0180,M011,PRD02,SHIFT_SWING,6,305,130,130,0,26.1,38.2 +RUN0181,M011,PRD02,SHIFT_SWING,7,305,190,189,1,24.6,38.2 +RUN0182,M011,PRD06,SHIFT_DAY,7,650,423,414,9,54.6,81.3 +RUN0183,M011,PRD02,SHIFT_SWING,8,305,164,163,1,24.6,38.2 +RUN0184,M011,PRD06,SHIFT_SWING,9,650,404,397,7,55.6,81.3 +RUN0185,M011,PRD06,SHIFT_SWING,10,650,391,384,7,58.2,81.3 +RUN0186,M011,PRD02,SHIFT_DAY,10,305,181,180,1,26.9,38.2 +RUN0187,M011,PRD02,SHIFT_DAY,11,305,174,174,0,26.1,38.2 +RUN0188,M011,PRD06,SHIFT_DAY,12,650,325,321,4,55.4,81.3 +RUN0189,M012,PRD06,SHIFT_DAY,1,590,322,314,8,52.5,73.8 +RUN0190,M012,PRD02,SHIFT_SWING,2,305,213,208,5,26.7,38.2 +RUN0191,M012,PRD06,SHIFT_SWING,2,590,388,373,15,48.5,73.8 +RUN0192,M012,PRD02,SHIFT_SWING,3,305,213,208,5,26.7,38.2 +RUN0193,M012,PRD06,SHIFT_DAY,3,590,394,380,14,49.3,73.8 +RUN0194,M012,PRD02,SHIFT_SWING,4,305,212,210,2,26.6,38.2 +RUN0195,M012,PRD06,SHIFT_DAY,5,590,432,418,14,54.1,73.8 +RUN0196,M012,PRD06,SHIFT_DAY,6,590,449,431,18,56.2,73.8 +RUN0197,M012,PRD06,SHIFT_DAY,7,590,390,377,13,52.9,73.8 +RUN0198,M012,PRD02,SHIFT_DAY,8,305,237,233,4,29.7,38.2 +RUN0199,M012,PRD02,SHIFT_DAY,9,305,207,205,2,27.1,38.2 +RUN0200,M012,PRD06,SHIFT_DAY,9,590,434,424,10,56.8,73.8 +RUN0201,M012,PRD02,SHIFT_SWING,10,305,210,208,2,26.3,38.2 +RUN0202,M012,PRD06,SHIFT_NIGHT,10,590,425,418,7,53.2,73.8 +RUN0203,M012,PRD02,SHIFT_NIGHT,11,305,202,201,1,25.3,38.2 +RUN0204,M012,PRD06,SHIFT_SWING,12,590,441,427,14,55.2,73.8 +RUN0205,M013,PRD02,SHIFT_DAY,1,300,192,190,2,25.7,37.5 +RUN0206,M013,PRD02,SHIFT_SWING,2,300,208,203,5,26.0,37.5 +RUN0207,M013,PRD06,SHIFT_SWING,2,600,408,395,13,51.1,75.0 +RUN0208,M013,PRD06,SHIFT_DAY,3,600,415,412,3,51.9,75.0 +RUN0209,M013,PRD02,SHIFT_SWING,4,300,224,220,4,28.0,37.5 +RUN0210,M013,PRD06,SHIFT_NIGHT,4,600,415,408,7,51.9,75.0 +RUN0211,M013,PRD02,SHIFT_DAY,5,300,217,215,2,29.5,37.5 +RUN0212,M013,PRD06,SHIFT_NIGHT,5,600,386,383,3,52.5,75.0 +RUN0213,M013,PRD02,SHIFT_NIGHT,6,300,190,188,2,25.6,37.5 +RUN0214,M013,PRD06,SHIFT_NIGHT,6,600,391,385,6,52.6,75.0 +RUN0215,M013,PRD02,SHIFT_NIGHT,7,300,213,211,2,26.7,37.5 +RUN0216,M013,PRD06,SHIFT_NIGHT,8,600,444,431,13,55.5,75.0 +RUN0217,M013,PRD02,SHIFT_SWING,9,300,200,196,4,25.1,37.5 +RUN0218,M013,PRD02,SHIFT_NIGHT,10,300,199,192,7,24.9,37.5 +RUN0219,M013,PRD06,SHIFT_SWING,10,600,399,390,9,49.9,75.0 +RUN0220,M013,PRD02,SHIFT_SWING,11,300,221,214,7,27.7,37.5 +RUN0221,M013,PRD06,SHIFT_DAY,11,600,420,409,11,52.5,75.0 +RUN0222,M013,PRD06,SHIFT_SWING,12,600,438,433,5,54.8,75.0 +RUN0223,M014,PRD02,SHIFT_NIGHT,1,308,211,208,3,31.0,38.5 +RUN0224,M014,PRD06,SHIFT_SWING,2,516,425,421,4,55.5,64.5 +RUN0225,M014,PRD02,SHIFT_SWING,2,308,242,241,1,31.6,38.5 +RUN0226,M014,PRD02,SHIFT_SWING,3,308,199,198,1,29.1,38.5 +RUN0227,M014,PRD02,SHIFT_DAY,4,308,249,247,2,33.1,38.5 +RUN0228,M014,PRD06,SHIFT_DAY,4,516,410,408,2,54.5,64.5 +RUN0229,M014,PRD06,SHIFT_SWING,5,516,313,308,5,49.1,64.5 +RUN0230,M014,PRD02,SHIFT_SWING,6,308,198,197,1,33.4,38.5 +RUN0231,M014,PRD02,SHIFT_DAY,7,308,245,243,2,33.5,38.5 +RUN0232,M014,PRD06,SHIFT_SWING,7,516,413,408,5,56.5,64.5 +RUN0233,M014,PRD02,SHIFT_NIGHT,8,308,194,192,2,28.9,38.5 +RUN0234,M014,PRD06,SHIFT_NIGHT,8,516,341,335,6,50.8,64.5 +RUN0235,M014,PRD06,SHIFT_DAY,9,516,361,355,6,54.9,64.5 +RUN0236,M014,PRD02,SHIFT_DAY,10,308,255,253,2,33.5,38.5 +RUN0237,M014,PRD06,SHIFT_DAY,10,516,439,436,3,57.7,64.5 +RUN0238,M014,PRD02,SHIFT_DAY,11,308,212,210,2,30.7,38.5 +RUN0239,M014,PRD06,SHIFT_SWING,12,516,372,369,3,49.7,64.5 +RUN0240,M014,PRD02,SHIFT_SWING,12,308,241,240,1,32.3,38.5 +RUN0241,M015,PRD06,SHIFT_DAY,1,533,392,380,12,49.0,66.7 +RUN0242,M015,PRD02,SHIFT_NIGHT,2,312,224,222,2,28.0,39.1 +RUN0243,M015,PRD06,SHIFT_DAY,3,533,340,337,3,46.3,66.7 +RUN0244,M015,PRD02,SHIFT_DAY,4,312,241,237,4,30.2,39.1 +RUN0245,M015,PRD06,SHIFT_NIGHT,4,533,372,359,13,46.6,66.7 +RUN0246,M015,PRD02,SHIFT_NIGHT,5,312,223,219,4,27.9,39.1 +RUN0247,M015,PRD02,SHIFT_SWING,6,312,233,225,8,29.2,39.1 +RUN0248,M015,PRD06,SHIFT_DAY,6,533,382,373,9,47.8,66.7 +RUN0249,M015,PRD06,SHIFT_NIGHT,7,533,369,365,4,46.2,66.7 +RUN0250,M015,PRD06,SHIFT_DAY,8,533,395,381,14,49.4,66.7 +RUN0251,M015,PRD02,SHIFT_DAY,9,312,221,216,5,27.7,39.1 +RUN0252,M015,PRD06,SHIFT_DAY,10,533,421,408,13,52.7,66.7 +RUN0253,M015,PRD02,SHIFT_DAY,11,312,249,245,4,31.2,39.1 +RUN0254,M015,PRD06,SHIFT_SWING,11,533,374,363,11,46.8,66.7 +RUN0255,M015,PRD02,SHIFT_DAY,12,312,218,215,3,29.4,39.1 +RUN0256,M016,PRD02,SHIFT_NIGHT,1,289,88,87,1,23.9,36.2 +RUN0257,M016,PRD06,SHIFT_DAY,2,607,334,327,7,52.6,75.9 +RUN0258,M016,PRD02,SHIFT_SWING,2,289,171,168,3,27.0,36.2 +RUN0259,M016,PRD02,SHIFT_DAY,3,289,182,181,1,27.2,36.2 +RUN0260,M016,PRD06,SHIFT_DAY,3,607,359,349,10,53.7,75.9 +RUN0261,M016,PRD06,SHIFT_DAY,4,607,395,382,13,55.3,75.9 +RUN0262,M016,PRD02,SHIFT_SWING,4,289,170,169,1,23.8,36.2 +RUN0263,M016,PRD06,SHIFT_SWING,5,607,333,327,6,51.5,75.9 +RUN0264,M016,PRD06,SHIFT_DAY,6,607,351,342,9,57.3,75.9 +RUN0265,M016,PRD02,SHIFT_DAY,6,289,157,155,2,25.6,36.2 +RUN0266,M016,PRD06,SHIFT_NIGHT,7,607,356,351,5,51.1,75.9 +RUN0267,M016,PRD06,SHIFT_NIGHT,8,607,357,348,9,51.5,75.9 +RUN0268,M016,PRD02,SHIFT_SWING,9,289,174,171,3,23.3,36.2 +RUN0269,M016,PRD06,SHIFT_DAY,9,607,400,392,8,53.4,75.9 +RUN0270,M016,PRD02,SHIFT_DAY,10,289,107,106,1,27.8,36.2 +RUN0271,M016,PRD02,SHIFT_SWING,11,289,166,163,3,23.8,36.2 +RUN0272,M016,PRD06,SHIFT_SWING,11,607,355,344,11,50.8,75.9 +RUN0273,M016,PRD02,SHIFT_DAY,12,289,174,172,2,28.0,36.2 +RUN0274,M017,PRD02,SHIFT_NIGHT,1,264,189,187,2,23.7,33.0 +RUN0275,M017,PRD02,SHIFT_NIGHT,2,264,175,174,1,23.4,33.0 +RUN0276,M017,PRD06,SHIFT_SWING,2,600,369,364,5,49.4,75.1 +RUN0277,M017,PRD06,SHIFT_DAY,3,600,451,442,9,56.4,75.1 +RUN0278,M017,PRD06,SHIFT_SWING,4,600,391,385,6,51.7,75.1 +RUN0279,M017,PRD02,SHIFT_DAY,5,264,180,176,4,22.5,33.0 +RUN0280,M017,PRD06,SHIFT_DAY,5,600,440,435,5,55.0,75.1 +RUN0281,M017,PRD06,SHIFT_SWING,6,600,396,384,12,54.0,75.1 +RUN0282,M017,PRD06,SHIFT_NIGHT,7,600,384,372,12,48.1,75.1 +RUN0283,M017,PRD02,SHIFT_SWING,7,264,198,195,3,24.8,33.0 +RUN0284,M017,PRD02,SHIFT_SWING,8,264,188,186,2,23.5,33.0 +RUN0285,M017,PRD06,SHIFT_DAY,8,600,455,440,15,56.9,75.1 +RUN0286,M017,PRD02,SHIFT_SWING,9,264,179,176,3,22.4,33.0 +RUN0287,M017,PRD06,SHIFT_SWING,9,600,396,385,11,49.6,75.1 +RUN0288,M017,PRD02,SHIFT_DAY,10,264,178,176,2,22.3,33.0 +RUN0289,M017,PRD06,SHIFT_SWING,10,600,388,382,6,48.5,75.1 +RUN0290,M017,PRD02,SHIFT_DAY,11,264,192,188,4,24.1,33.0 +RUN0291,M017,PRD02,SHIFT_NIGHT,12,264,188,186,2,23.6,33.0 +RUN0292,M018,PRD02,SHIFT_NIGHT,1,300,172,171,1,27.9,37.6 +RUN0293,M018,PRD02,SHIFT_SWING,2,300,199,198,1,24.9,37.6 +RUN0294,M018,PRD06,SHIFT_NIGHT,3,578,378,373,5,47.3,72.3 +RUN0295,M018,PRD02,SHIFT_SWING,4,300,212,210,2,26.6,37.6 +RUN0296,M018,PRD06,SHIFT_DAY,4,578,428,422,6,53.5,72.3 +RUN0297,M018,PRD06,SHIFT_SWING,5,578,420,414,6,52.6,72.3 +RUN0298,M018,PRD02,SHIFT_NIGHT,6,300,186,184,2,25.7,37.6 +RUN0299,M018,PRD02,SHIFT_DAY,7,300,233,231,2,29.2,37.6 +RUN0300,M018,PRD02,SHIFT_SWING,8,300,225,224,1,28.2,37.6 +RUN0301,M018,PRD06,SHIFT_SWING,8,578,423,416,7,52.9,72.3 +RUN0302,M018,PRD06,SHIFT_DAY,9,578,342,338,4,51.0,72.3 +RUN0303,M018,PRD02,SHIFT_NIGHT,10,300,158,157,1,26.4,37.6 +RUN0304,M018,PRD02,SHIFT_NIGHT,11,300,219,217,2,27.4,37.6 +RUN0305,M018,PRD06,SHIFT_DAY,11,578,428,424,4,53.5,72.3 +RUN0306,M018,PRD06,SHIFT_DAY,12,578,398,394,4,49.8,72.3 +RUN0307,M019,PRD02,SHIFT_DAY,1,312,271,266,5,33.9,39.0 +RUN0308,M019,PRD06,SHIFT_NIGHT,2,620,517,508,9,64.7,77.6 +RUN0309,M019,PRD02,SHIFT_NIGHT,2,312,243,239,4,30.4,39.0 +RUN0310,M019,PRD06,SHIFT_NIGHT,3,620,487,484,3,64.7,77.6 +RUN0311,M019,PRD06,SHIFT_SWING,4,620,472,467,5,59.0,77.6 +RUN0312,M019,PRD02,SHIFT_NIGHT,5,312,262,260,2,32.8,39.0 +RUN0313,M019,PRD02,SHIFT_DAY,6,312,244,241,3,31.7,39.0 +RUN0314,M019,PRD06,SHIFT_SWING,6,620,514,510,4,66.8,77.6 +RUN0315,M019,PRD02,SHIFT_SWING,7,312,252,247,5,31.6,39.0 +RUN0316,M019,PRD06,SHIFT_SWING,7,620,474,471,3,59.3,77.6 +RUN0317,M019,PRD06,SHIFT_SWING,8,620,481,477,4,60.2,77.6 +RUN0318,M019,PRD06,SHIFT_DAY,9,620,514,509,5,67.3,77.6 +RUN0319,M019,PRD02,SHIFT_NIGHT,10,312,230,226,4,28.8,39.0 +RUN0320,M019,PRD06,SHIFT_NIGHT,11,620,506,504,2,63.3,77.6 +RUN0321,M019,PRD02,SHIFT_SWING,11,312,265,257,8,33.2,39.0 +RUN0322,M019,PRD02,SHIFT_DAY,12,312,280,273,7,35.1,39.0 +RUN0323,M020,PRD02,SHIFT_DAY,1,306,213,212,1,26.7,38.3 +RUN0324,M020,PRD06,SHIFT_SWING,2,544,392,382,10,49.0,68.0 +RUN0325,M020,PRD02,SHIFT_DAY,3,306,240,239,1,30.1,38.3 +RUN0326,M020,PRD06,SHIFT_DAY,4,544,392,385,7,49.1,68.0 +RUN0327,M020,PRD02,SHIFT_SWING,4,306,220,218,2,27.6,38.3 +RUN0328,M020,PRD06,SHIFT_NIGHT,5,544,378,367,11,47.3,68.0 +RUN0329,M020,PRD02,SHIFT_NIGHT,5,306,220,218,2,27.6,38.3 +RUN0330,M020,PRD02,SHIFT_SWING,6,306,202,200,2,28.2,38.3 +RUN0331,M020,PRD06,SHIFT_DAY,7,544,428,414,14,53.5,68.0 +RUN0332,M020,PRD02,SHIFT_SWING,7,306,232,231,1,29.1,38.3 +RUN0333,M020,PRD06,SHIFT_NIGHT,8,544,388,375,13,48.5,68.0 +RUN0334,M020,PRD02,SHIFT_DAY,8,306,232,230,2,29.0,38.3 +RUN0335,M020,PRD02,SHIFT_NIGHT,9,306,227,226,1,28.4,38.3 +RUN0336,M020,PRD06,SHIFT_NIGHT,9,544,391,382,9,48.9,68.0 +RUN0337,M020,PRD02,SHIFT_DAY,10,306,244,242,2,30.5,38.3 +RUN0338,M020,PRD06,SHIFT_DAY,11,544,363,357,6,48.7,68.0 +RUN0339,M020,PRD02,SHIFT_NIGHT,12,306,228,227,1,28.5,38.3 +RUN0340,M020,PRD06,SHIFT_SWING,12,544,381,373,8,47.7,68.0 +RUN0341,M021,PRD06,SHIFT_DAY,1,514,406,403,3,50.8,64.3 +RUN0342,M021,PRD07,SHIFT_NIGHT,2,172,112,110,2,14.0,21.5 +RUN0343,M021,PRD07,SHIFT_NIGHT,3,172,121,119,2,15.2,21.5 +RUN0344,M021,PRD03,SHIFT_DAY,4,222,153,152,1,19.2,27.8 +RUN0345,M021,PRD07,SHIFT_SWING,4,172,116,113,3,14.5,21.5 +RUN0346,M021,PRD07,SHIFT_NIGHT,5,172,117,115,2,14.7,21.5 +RUN0347,M021,PRD03,SHIFT_DAY,5,222,168,162,6,21.1,27.8 +RUN0348,M021,PRD06,SHIFT_NIGHT,6,514,347,346,1,43.4,64.3 +RUN0349,M021,PRD03,SHIFT_SWING,7,222,171,164,7,21.4,27.8 +RUN0350,M021,PRD07,SHIFT_NIGHT,8,172,124,122,2,15.5,21.5 +RUN0351,M021,PRD03,SHIFT_SWING,9,222,169,166,3,21.2,27.8 +RUN0352,M021,PRD06,SHIFT_SWING,9,514,379,378,1,47.4,64.3 +RUN0353,M021,PRD03,SHIFT_DAY,10,222,155,152,3,19.4,27.8 +RUN0354,M021,PRD07,SHIFT_NIGHT,10,172,124,122,2,15.6,21.5 +RUN0355,M021,PRD07,SHIFT_SWING,11,172,118,115,3,14.8,21.5 +RUN0356,M021,PRD07,SHIFT_SWING,12,172,129,126,3,16.2,21.5 +RUN0357,M022,PRD03,SHIFT_NIGHT,1,204,112,107,5,16.2,25.5 +RUN0358,M022,PRD06,SHIFT_NIGHT,1,530,291,285,6,42.0,66.3 +RUN0359,M022,PRD03,SHIFT_NIGHT,2,204,126,123,3,17.3,25.5 +RUN0360,M022,PRD07,SHIFT_NIGHT,2,168,110,107,3,15.1,21.1 +RUN0361,M022,PRD06,SHIFT_DAY,3,530,217,215,2,44.6,66.3 +RUN0362,M022,PRD07,SHIFT_DAY,4,168,79,78,1,14.0,21.1 +RUN0363,M022,PRD03,SHIFT_DAY,5,204,122,120,2,19.1,25.5 +RUN0364,M022,PRD06,SHIFT_SWING,5,530,304,292,12,47.3,66.3 +RUN0365,M022,PRD07,SHIFT_DAY,6,168,85,84,1,14.7,21.1 +RUN0366,M022,PRD06,SHIFT_SWING,7,530,318,311,7,47.5,66.3 +RUN0367,M022,PRD07,SHIFT_NIGHT,8,168,109,106,3,15.2,21.1 +RUN0368,M022,PRD03,SHIFT_DAY,8,204,130,126,4,18.2,25.5 +RUN0369,M022,PRD06,SHIFT_SWING,9,530,312,307,5,49.2,66.3 +RUN0370,M022,PRD06,SHIFT_DAY,10,530,195,189,6,46.9,66.3 +RUN0371,M022,PRD03,SHIFT_SWING,11,204,121,120,1,19.1,25.5 +RUN0372,M022,PRD06,SHIFT_NIGHT,12,530,333,319,14,47.3,66.3 +RUN0373,M022,PRD07,SHIFT_DAY,12,168,103,100,3,14.7,21.1 +RUN0374,M023,PRD07,SHIFT_DAY,1,166,150,143,7,18.8,20.8 +RUN0375,M023,PRD03,SHIFT_SWING,1,241,212,206,6,26.6,30.2 +RUN0376,M023,PRD07,SHIFT_NIGHT,2,166,118,116,2,15.6,20.8 +RUN0377,M023,PRD07,SHIFT_NIGHT,3,166,130,128,2,16.3,20.8 +RUN0378,M023,PRD03,SHIFT_SWING,3,241,184,181,3,23.1,30.2 +RUN0379,M023,PRD03,SHIFT_DAY,4,241,210,202,8,26.3,30.2 +RUN0380,M023,PRD06,SHIFT_DAY,5,585,527,517,10,65.9,73.2 +RUN0381,M023,PRD06,SHIFT_NIGHT,6,585,480,470,10,60.0,73.2 +RUN0382,M023,PRD07,SHIFT_SWING,6,166,144,141,3,18.1,20.8 +RUN0383,M023,PRD07,SHIFT_SWING,7,166,145,143,2,18.2,20.8 +RUN0384,M023,PRD07,SHIFT_DAY,8,166,146,139,7,18.3,20.8 +RUN0385,M023,PRD06,SHIFT_SWING,8,585,472,462,10,59.0,73.2 +RUN0386,M023,PRD06,SHIFT_NIGHT,9,585,428,419,9,53.6,73.2 +RUN0387,M023,PRD07,SHIFT_DAY,9,166,141,138,3,17.7,20.8 +RUN0388,M023,PRD03,SHIFT_DAY,10,241,215,206,9,26.9,30.2 +RUN0389,M023,PRD06,SHIFT_SWING,10,585,484,475,9,60.5,73.2 +RUN0390,M023,PRD07,SHIFT_SWING,11,166,125,123,2,15.7,20.8 +RUN0391,M023,PRD06,SHIFT_SWING,11,585,472,465,7,59.0,73.2 +RUN0392,M023,PRD03,SHIFT_NIGHT,12,241,184,182,2,23.1,30.2 +RUN0393,M024,PRD03,SHIFT_SWING,1,229,169,166,3,21.2,28.7 +RUN0394,M024,PRD07,SHIFT_SWING,2,180,137,134,3,17.2,22.5 +RUN0395,M024,PRD07,SHIFT_DAY,3,180,133,132,1,16.7,22.5 +RUN0396,M024,PRD07,SHIFT_NIGHT,4,180,115,112,3,14.4,22.5 +RUN0397,M024,PRD06,SHIFT_SWING,5,511,385,381,4,48.2,63.9 +RUN0398,M024,PRD06,SHIFT_SWING,6,511,340,335,5,42.5,63.9 +RUN0399,M024,PRD06,SHIFT_DAY,7,511,328,321,7,48.1,63.9 +RUN0400,M024,PRD03,SHIFT_SWING,8,229,131,128,3,19.0,28.7 +RUN0401,M024,PRD03,SHIFT_SWING,9,229,155,151,4,19.4,28.7 +RUN0402,M024,PRD06,SHIFT_DAY,9,511,402,390,12,50.3,63.9 +RUN0403,M024,PRD07,SHIFT_DAY,10,180,124,123,1,15.5,22.5 +RUN0404,M024,PRD07,SHIFT_SWING,11,180,125,123,2,15.7,22.5 +RUN0405,M024,PRD06,SHIFT_DAY,11,511,381,376,5,47.7,63.9 +RUN0406,M024,PRD06,SHIFT_NIGHT,12,511,379,372,7,47.4,63.9 +RUN0407,M024,PRD07,SHIFT_NIGHT,12,180,132,131,1,16.6,22.5 +RUN0408,M025,PRD07,SHIFT_DAY,1,156,104,104,0,13.0,19.5 +RUN0409,M025,PRD03,SHIFT_SWING,2,222,164,162,2,20.5,27.8 +RUN0410,M025,PRD03,SHIFT_DAY,3,222,145,143,2,20.7,27.8 +RUN0411,M025,PRD07,SHIFT_NIGHT,4,156,101,101,0,13.8,19.5 +RUN0412,M025,PRD03,SHIFT_DAY,4,222,138,136,2,18.8,27.8 +RUN0413,M025,PRD06,SHIFT_NIGHT,5,624,425,408,17,53.2,78.0 +RUN0414,M025,PRD03,SHIFT_DAY,6,222,162,162,0,20.3,27.8 +RUN0415,M025,PRD03,SHIFT_NIGHT,7,222,158,156,2,19.8,27.8 +RUN0416,M025,PRD06,SHIFT_NIGHT,8,624,392,380,12,49.1,78.0 +RUN0417,M025,PRD07,SHIFT_SWING,9,156,105,104,1,13.2,19.5 +RUN0418,M025,PRD06,SHIFT_DAY,9,624,420,403,17,52.5,78.0 +RUN0419,M025,PRD06,SHIFT_NIGHT,10,624,428,410,18,53.5,78.0 +RUN0420,M025,PRD03,SHIFT_NIGHT,10,222,139,137,2,17.4,27.8 +RUN0421,M025,PRD03,SHIFT_NIGHT,11,222,142,140,2,17.8,27.8 +RUN0422,M025,PRD03,SHIFT_SWING,12,222,148,147,1,18.5,27.8 +RUN0423,M026,PRD06,SHIFT_SWING,1,615,517,498,19,64.7,76.9 +RUN0424,M026,PRD03,SHIFT_DAY,2,192,172,168,4,21.6,24.1 +RUN0425,M026,PRD07,SHIFT_SWING,2,186,160,158,2,20.1,23.3 +RUN0426,M026,PRD03,SHIFT_SWING,3,192,144,140,4,19.4,24.1 +RUN0427,M026,PRD07,SHIFT_NIGHT,3,186,138,137,1,18.6,23.3 +RUN0428,M026,PRD07,SHIFT_DAY,4,186,144,143,1,18.1,23.3 +RUN0429,M026,PRD03,SHIFT_SWING,4,192,145,141,4,18.2,24.1 +RUN0430,M026,PRD06,SHIFT_DAY,5,615,552,544,8,69.0,76.9 +RUN0431,M026,PRD03,SHIFT_SWING,6,192,156,152,4,19.6,24.1 +RUN0432,M026,PRD06,SHIFT_SWING,7,615,468,451,17,58.6,76.9 +RUN0433,M026,PRD06,SHIFT_SWING,8,615,476,467,9,59.5,76.9 +RUN0434,M026,PRD03,SHIFT_SWING,8,192,157,153,4,19.7,24.1 +RUN0435,M026,PRD07,SHIFT_DAY,9,186,151,148,3,20.0,23.3 +RUN0436,M026,PRD03,SHIFT_DAY,10,192,164,161,3,20.5,24.1 +RUN0437,M026,PRD06,SHIFT_DAY,11,615,548,527,21,68.6,76.9 +RUN0438,M026,PRD06,SHIFT_DAY,12,615,493,472,21,61.7,76.9 +RUN0439,M027,PRD06,SHIFT_DAY,1,634,491,488,3,61.4,79.3 +RUN0440,M027,PRD03,SHIFT_SWING,1,222,164,162,2,20.6,27.8 +RUN0441,M027,PRD03,SHIFT_NIGHT,2,222,163,160,3,20.4,27.8 +RUN0442,M027,PRD07,SHIFT_DAY,3,161,116,114,2,14.5,20.2 +RUN0443,M027,PRD03,SHIFT_DAY,3,222,160,158,2,20.0,27.8 +RUN0444,M027,PRD07,SHIFT_SWING,4,161,112,110,2,14.1,20.2 +RUN0445,M027,PRD06,SHIFT_SWING,4,634,483,481,2,60.4,79.3 +RUN0446,M027,PRD03,SHIFT_SWING,5,222,150,148,2,20.2,27.8 +RUN0447,M027,PRD06,SHIFT_DAY,5,634,436,435,1,58.4,79.3 +RUN0448,M027,PRD07,SHIFT_SWING,6,161,105,102,3,15.3,20.2 +RUN0449,M027,PRD03,SHIFT_DAY,7,222,154,150,4,19.3,27.8 +RUN0450,M027,PRD06,SHIFT_NIGHT,8,634,426,422,4,53.3,79.3 +RUN0451,M027,PRD06,SHIFT_SWING,9,634,420,417,3,52.5,79.3 +RUN0452,M027,PRD07,SHIFT_NIGHT,9,161,108,105,3,13.5,20.2 +RUN0453,M027,PRD07,SHIFT_DAY,10,161,106,103,3,14.4,20.2 +RUN0454,M027,PRD06,SHIFT_NIGHT,10,634,372,367,5,50.4,79.3 +RUN0455,M027,PRD07,SHIFT_SWING,11,161,116,114,2,14.5,20.2 +RUN0456,M027,PRD03,SHIFT_NIGHT,11,222,157,155,2,19.7,27.8 +RUN0457,M027,PRD03,SHIFT_DAY,12,222,176,174,2,22.0,27.8 +RUN0458,M028,PRD06,SHIFT_NIGHT,1,504,277,273,4,43.4,63.0 +RUN0459,M028,PRD07,SHIFT_DAY,2,165,97,95,2,14.3,20.7 +RUN0460,M028,PRD06,SHIFT_SWING,3,504,250,249,1,42.7,63.0 +RUN0461,M028,PRD07,SHIFT_NIGHT,3,165,83,81,2,14.3,20.7 +RUN0462,M028,PRD06,SHIFT_NIGHT,4,504,269,265,4,45.3,63.0 +RUN0463,M028,PRD03,SHIFT_SWING,5,228,122,121,1,21.3,28.6 +RUN0464,M028,PRD07,SHIFT_NIGHT,6,165,104,103,1,14.0,20.7 +RUN0465,M028,PRD06,SHIFT_SWING,6,504,316,312,4,42.5,63.0 +RUN0466,M028,PRD03,SHIFT_DAY,7,228,144,142,2,19.9,28.6 +RUN0467,M028,PRD06,SHIFT_DAY,8,504,324,320,4,44.9,63.0 +RUN0468,M028,PRD06,SHIFT_DAY,9,504,296,294,2,44.3,63.0 +RUN0469,M028,PRD07,SHIFT_DAY,10,165,114,113,1,15.4,20.7 +RUN0470,M028,PRD03,SHIFT_NIGHT,10,228,135,133,2,18.2,28.6 +RUN0471,M028,PRD03,SHIFT_DAY,11,228,154,152,2,20.5,28.6 +RUN0472,M028,PRD07,SHIFT_SWING,12,165,100,98,2,14.2,20.7 +RUN0473,M028,PRD06,SHIFT_SWING,12,504,333,329,4,47.0,63.0 +RUN0474,M029,PRD06,SHIFT_NIGHT,1,617,511,501,10,63.9,77.2 +RUN0475,M029,PRD03,SHIFT_SWING,2,228,175,173,2,21.9,28.6 +RUN0476,M029,PRD03,SHIFT_SWING,3,228,188,185,3,23.6,28.6 +RUN0477,M029,PRD06,SHIFT_SWING,4,617,470,463,7,58.8,77.2 +RUN0478,M029,PRD07,SHIFT_SWING,5,167,132,129,3,16.6,20.9 +RUN0479,M029,PRD06,SHIFT_SWING,5,617,500,489,11,62.5,77.2 +RUN0480,M029,PRD06,SHIFT_DAY,6,617,540,531,9,67.5,77.2 +RUN0481,M029,PRD03,SHIFT_NIGHT,7,228,179,178,1,22.4,28.6 +RUN0482,M029,PRD07,SHIFT_SWING,8,167,126,122,4,15.8,20.9 +RUN0483,M029,PRD07,SHIFT_DAY,9,167,140,135,5,17.5,20.9 +RUN0484,M029,PRD03,SHIFT_DAY,10,228,178,175,3,22.3,28.6 +RUN0485,M029,PRD06,SHIFT_SWING,11,617,508,499,9,63.6,77.2 +RUN0486,M029,PRD03,SHIFT_DAY,11,228,180,178,2,22.6,28.6 +RUN0487,M029,PRD07,SHIFT_DAY,12,167,139,136,3,18.1,20.9 +RUN0488,M030,PRD06,SHIFT_DAY,1,565,421,408,13,55.6,70.7 +RUN0489,M030,PRD03,SHIFT_DAY,1,235,159,158,1,21.1,29.4 +RUN0490,M030,PRD03,SHIFT_NIGHT,2,235,166,165,1,20.8,29.4 +RUN0491,M030,PRD06,SHIFT_DAY,2,565,395,380,15,49.4,70.7 +RUN0492,M030,PRD03,SHIFT_DAY,3,235,163,163,0,20.4,29.4 +RUN0493,M030,PRD06,SHIFT_DAY,4,565,447,438,9,55.9,70.7 +RUN0494,M030,PRD03,SHIFT_DAY,5,235,183,181,2,22.9,29.4 +RUN0495,M030,PRD07,SHIFT_NIGHT,6,164,115,114,1,14.4,20.5 +RUN0496,M030,PRD06,SHIFT_NIGHT,6,565,359,354,5,44.9,70.7 +RUN0497,M030,PRD03,SHIFT_NIGHT,7,235,168,166,2,21.0,29.4 +RUN0498,M030,PRD07,SHIFT_SWING,7,164,117,116,1,14.7,20.5 +RUN0499,M030,PRD07,SHIFT_SWING,8,164,115,114,1,14.4,20.5 +RUN0500,M030,PRD03,SHIFT_SWING,9,235,166,165,1,20.8,29.4 +RUN0501,M030,PRD06,SHIFT_DAY,10,565,423,419,4,52.9,70.7 +RUN0502,M030,PRD07,SHIFT_NIGHT,11,164,120,118,2,15.0,20.5 +RUN0503,M030,PRD06,SHIFT_DAY,12,565,416,402,14,52.0,70.7 +RUN0504,M031,PRD04,SHIFT_SWING,1,94,68,68,0,8.5,11.8 +RUN0505,M031,PRD08,SHIFT_NIGHT,2,118,76,75,1,9.5,14.8 +RUN0506,M031,PRD08,SHIFT_SWING,3,118,88,87,1,11.0,14.8 +RUN0507,M031,PRD08,SHIFT_SWING,4,118,79,78,1,9.9,14.8 +RUN0508,M031,PRD08,SHIFT_NIGHT,5,118,83,82,1,10.4,14.8 +RUN0509,M031,PRD08,SHIFT_NIGHT,6,118,72,71,1,9.9,14.8 +RUN0510,M031,PRD04,SHIFT_SWING,6,94,62,62,0,8.6,11.8 +RUN0511,M031,PRD04,SHIFT_NIGHT,7,94,61,61,0,7.7,11.8 +RUN0512,M031,PRD08,SHIFT_SWING,8,118,88,87,1,11.1,14.8 +RUN0513,M031,PRD04,SHIFT_DAY,9,94,72,72,0,9.1,11.8 +RUN0514,M031,PRD08,SHIFT_SWING,9,118,90,90,0,11.3,14.8 +RUN0515,M031,PRD04,SHIFT_NIGHT,10,94,60,60,0,7.6,11.8 +RUN0516,M031,PRD04,SHIFT_SWING,11,94,58,58,0,7.9,11.8 +RUN0517,M031,PRD08,SHIFT_DAY,11,118,87,86,1,11.8,14.8 +RUN0518,M031,PRD08,SHIFT_SWING,12,118,84,84,0,10.5,14.8 +RUN0519,M032,PRD04,SHIFT_SWING,1,91,62,62,0,8.4,11.4 +RUN0520,M032,PRD04,SHIFT_NIGHT,2,91,58,58,0,8.2,11.4 +RUN0521,M032,PRD04,SHIFT_DAY,3,91,59,59,0,8.0,11.4 +RUN0522,M032,PRD04,SHIFT_SWING,4,91,56,56,0,7.6,11.4 +RUN0523,M032,PRD08,SHIFT_DAY,5,119,79,78,1,10.8,14.9 +RUN0524,M032,PRD04,SHIFT_SWING,5,91,57,57,0,7.8,11.4 +RUN0525,M032,PRD04,SHIFT_SWING,6,91,55,55,0,8.1,11.4 +RUN0526,M032,PRD08,SHIFT_NIGHT,6,119,63,61,2,9.3,14.9 +RUN0527,M032,PRD04,SHIFT_SWING,7,91,48,48,0,7.6,11.4 +RUN0528,M032,PRD04,SHIFT_SWING,8,91,39,39,0,8.5,11.4 +RUN0529,M032,PRD04,SHIFT_SWING,9,91,56,56,0,7.4,11.4 +RUN0530,M032,PRD08,SHIFT_NIGHT,9,119,81,79,2,10.7,14.9 +RUN0531,M032,PRD04,SHIFT_NIGHT,10,91,42,42,0,7.9,11.4 +RUN0532,M032,PRD08,SHIFT_NIGHT,11,119,69,68,1,9.2,14.9 +RUN0533,M032,PRD08,SHIFT_DAY,12,119,47,46,1,10.1,14.9 +RUN0534,M033,PRD04,SHIFT_SWING,1,80,61,59,2,7.7,10.1 +RUN0535,M033,PRD08,SHIFT_NIGHT,1,108,87,86,1,10.9,13.5 +RUN0536,M033,PRD04,SHIFT_SWING,2,80,62,61,1,8.4,10.1 +RUN0537,M033,PRD08,SHIFT_DAY,3,108,84,84,0,10.9,13.5 +RUN0538,M033,PRD04,SHIFT_NIGHT,4,80,61,60,1,7.7,10.1 +RUN0539,M033,PRD04,SHIFT_DAY,5,80,62,60,2,7.8,10.1 +RUN0540,M033,PRD08,SHIFT_DAY,5,108,91,89,2,11.4,13.5 +RUN0541,M033,PRD04,SHIFT_DAY,6,80,66,65,1,8.3,10.1 +RUN0542,M033,PRD08,SHIFT_DAY,6,108,89,88,1,11.2,13.5 +RUN0543,M033,PRD08,SHIFT_DAY,7,108,92,90,2,12.1,13.5 +RUN0544,M033,PRD04,SHIFT_SWING,8,80,64,64,0,8.1,10.1 +RUN0545,M033,PRD04,SHIFT_DAY,9,80,67,65,2,8.4,10.1 +RUN0546,M033,PRD04,SHIFT_NIGHT,10,80,60,60,0,7.6,10.1 +RUN0547,M033,PRD08,SHIFT_DAY,10,108,85,85,0,10.7,13.5 +RUN0548,M033,PRD08,SHIFT_NIGHT,11,108,86,85,1,10.8,13.5 +RUN0549,M033,PRD08,SHIFT_SWING,12,108,90,89,1,11.3,13.5 +RUN0550,M034,PRD04,SHIFT_SWING,1,81,60,58,2,7.6,10.2 +RUN0551,M034,PRD04,SHIFT_NIGHT,2,81,51,50,1,6.8,10.2 +RUN0552,M034,PRD08,SHIFT_NIGHT,2,101,63,62,1,8.5,12.7 +RUN0553,M034,PRD08,SHIFT_SWING,3,101,75,74,1,9.4,12.7 +RUN0554,M034,PRD08,SHIFT_NIGHT,4,101,75,74,1,9.4,12.7 +RUN0555,M034,PRD04,SHIFT_NIGHT,5,81,53,51,2,6.7,10.2 +RUN0556,M034,PRD08,SHIFT_NIGHT,6,101,73,73,0,9.2,12.7 +RUN0557,M034,PRD08,SHIFT_DAY,7,101,69,67,2,8.7,12.7 +RUN0558,M034,PRD04,SHIFT_SWING,7,81,56,55,1,7.1,10.2 +RUN0559,M034,PRD04,SHIFT_SWING,8,81,61,60,1,7.7,10.2 +RUN0560,M034,PRD08,SHIFT_DAY,8,101,79,78,1,9.9,12.7 +RUN0561,M034,PRD04,SHIFT_SWING,9,81,46,45,1,7.0,10.2 +RUN0562,M034,PRD08,SHIFT_DAY,10,101,76,75,1,9.5,12.7 +RUN0563,M034,PRD08,SHIFT_SWING,11,101,76,75,1,9.6,12.7 +RUN0564,M034,PRD08,SHIFT_SWING,12,101,76,74,2,9.5,12.7 +RUN0565,M034,PRD04,SHIFT_DAY,12,81,63,61,2,7.9,10.2 +RUN0566,M035,PRD08,SHIFT_SWING,1,116,72,71,1,10.3,14.6 +RUN0567,M035,PRD04,SHIFT_NIGHT,1,90,51,51,0,7.3,11.3 +RUN0568,M035,PRD04,SHIFT_DAY,2,90,46,46,0,8.7,11.3 +RUN0569,M035,PRD08,SHIFT_DAY,3,116,70,70,0,11.0,14.6 +RUN0570,M035,PRD04,SHIFT_NIGHT,3,90,47,47,0,7.4,11.3 +RUN0571,M035,PRD08,SHIFT_NIGHT,4,116,75,74,1,10.3,14.6 +RUN0572,M035,PRD08,SHIFT_SWING,5,116,49,49,0,10.8,14.6 +RUN0573,M035,PRD04,SHIFT_SWING,6,90,40,40,0,7.3,11.3 +RUN0574,M035,PRD04,SHIFT_DAY,7,90,48,48,0,8.2,11.3 +RUN0575,M035,PRD08,SHIFT_NIGHT,7,116,58,57,1,9.9,14.6 +RUN0576,M035,PRD08,SHIFT_SWING,8,116,51,51,0,9.8,14.6 +RUN0577,M035,PRD04,SHIFT_DAY,9,90,57,57,0,8.6,11.3 +RUN0578,M035,PRD04,SHIFT_DAY,10,90,45,45,0,8.4,11.3 +RUN0579,M035,PRD04,SHIFT_DAY,11,90,60,59,1,8.7,11.3 +RUN0580,M035,PRD08,SHIFT_SWING,11,116,64,63,1,9.4,14.6 +RUN0581,M035,PRD04,SHIFT_SWING,12,90,53,53,0,7.4,11.3 +RUN0582,M035,PRD08,SHIFT_DAY,12,116,76,76,0,10.5,14.6 +RUN0583,M036,PRD08,SHIFT_NIGHT,1,114,84,83,1,11.0,14.3 +RUN0584,M036,PRD04,SHIFT_SWING,2,81,68,67,1,8.5,10.2 +RUN0585,M036,PRD08,SHIFT_SWING,2,114,99,99,0,12.4,14.3 +RUN0586,M036,PRD08,SHIFT_DAY,3,114,94,94,0,11.8,14.3 +RUN0587,M036,PRD04,SHIFT_DAY,3,81,72,72,0,9.1,10.2 +RUN0588,M036,PRD08,SHIFT_DAY,4,114,94,93,1,11.8,14.3 +RUN0589,M036,PRD04,SHIFT_NIGHT,4,81,66,65,1,8.3,10.2 +RUN0590,M036,PRD04,SHIFT_DAY,5,81,69,67,2,8.7,10.2 +RUN0591,M036,PRD08,SHIFT_NIGHT,5,114,90,90,0,11.3,14.3 +RUN0592,M036,PRD04,SHIFT_NIGHT,6,81,64,63,1,8.0,10.2 +RUN0593,M036,PRD08,SHIFT_SWING,6,114,88,87,1,11.1,14.3 +RUN0594,M036,PRD08,SHIFT_SWING,7,114,85,85,0,11.1,14.3 +RUN0595,M036,PRD04,SHIFT_DAY,7,81,67,66,1,8.8,10.2 +RUN0596,M036,PRD08,SHIFT_DAY,8,114,93,92,1,11.7,14.3 +RUN0597,M036,PRD04,SHIFT_SWING,8,81,68,66,2,8.5,10.2 +RUN0598,M036,PRD08,SHIFT_DAY,9,114,96,95,1,12.1,14.3 +RUN0599,M036,PRD04,SHIFT_SWING,10,81,68,66,2,8.5,10.2 +RUN0600,M036,PRD08,SHIFT_SWING,11,114,87,87,0,10.9,14.3 +RUN0601,M036,PRD04,SHIFT_NIGHT,11,81,68,67,1,8.6,10.2 +RUN0602,M036,PRD04,SHIFT_SWING,12,81,61,61,0,7.7,10.2 +RUN0603,M036,PRD08,SHIFT_SWING,12,114,96,95,1,12.1,14.3 +RUN0604,M037,PRD04,SHIFT_DAY,1,85,60,60,0,8.4,10.7 +RUN0605,M037,PRD04,SHIFT_DAY,2,85,60,60,0,7.5,10.7 +RUN0606,M037,PRD08,SHIFT_DAY,3,115,84,83,1,10.5,14.4 +RUN0607,M037,PRD04,SHIFT_NIGHT,3,85,56,56,0,7.1,10.7 +RUN0608,M037,PRD08,SHIFT_NIGHT,4,115,73,72,1,9.2,14.4 +RUN0609,M037,PRD04,SHIFT_SWING,4,85,65,64,1,8.2,10.7 +RUN0610,M037,PRD08,SHIFT_SWING,5,115,85,84,1,10.7,14.4 +RUN0611,M037,PRD04,SHIFT_NIGHT,5,85,61,60,1,7.7,10.7 +RUN0612,M037,PRD04,SHIFT_DAY,6,85,64,64,0,8.0,10.7 +RUN0613,M037,PRD08,SHIFT_NIGHT,6,115,78,78,0,9.8,14.4 +RUN0614,M037,PRD08,SHIFT_DAY,7,115,81,80,1,10.2,14.4 +RUN0615,M037,PRD08,SHIFT_SWING,8,115,80,80,0,10.0,14.4 +RUN0616,M037,PRD04,SHIFT_DAY,9,85,66,66,0,8.3,10.7 +RUN0617,M037,PRD04,SHIFT_NIGHT,10,85,56,56,0,7.1,10.7 +RUN0618,M037,PRD08,SHIFT_SWING,10,115,77,77,0,9.7,14.4 +RUN0619,M037,PRD08,SHIFT_SWING,11,115,77,76,1,10.2,14.4 +RUN0620,M037,PRD08,SHIFT_NIGHT,12,115,82,81,1,10.3,14.4 +RUN0621,M038,PRD04,SHIFT_DAY,1,87,55,54,1,7.9,10.9 +RUN0622,M038,PRD08,SHIFT_DAY,1,116,68,66,2,9.7,14.5 +RUN0623,M038,PRD04,SHIFT_SWING,2,87,64,64,0,8.0,10.9 +RUN0624,M038,PRD08,SHIFT_DAY,2,116,80,77,3,10.1,14.5 +RUN0625,M038,PRD08,SHIFT_NIGHT,3,116,67,66,1,9.1,14.5 +RUN0626,M038,PRD04,SHIFT_SWING,4,87,60,60,0,7.6,10.9 +RUN0627,M038,PRD08,SHIFT_DAY,4,116,90,88,2,11.3,14.5 +RUN0628,M038,PRD08,SHIFT_DAY,5,116,86,85,1,10.8,14.5 +RUN0629,M038,PRD04,SHIFT_NIGHT,6,87,56,55,1,7.1,10.9 +RUN0630,M038,PRD04,SHIFT_DAY,7,87,58,56,2,7.8,10.9 +RUN0631,M038,PRD08,SHIFT_NIGHT,7,116,70,68,2,9.4,14.5 +RUN0632,M038,PRD04,SHIFT_DAY,8,87,59,58,1,7.4,10.9 +RUN0633,M038,PRD08,SHIFT_DAY,8,116,85,84,1,10.7,14.5 +RUN0634,M038,PRD04,SHIFT_NIGHT,9,87,58,58,0,7.3,10.9 +RUN0635,M038,PRD04,SHIFT_DAY,10,87,56,55,1,7.8,10.9 +RUN0636,M038,PRD08,SHIFT_NIGHT,10,116,67,66,1,9.3,14.5 +RUN0637,M038,PRD08,SHIFT_NIGHT,11,116,76,74,2,9.5,14.5 +RUN0638,M038,PRD08,SHIFT_SWING,12,116,85,82,3,10.7,14.5 +RUN0639,M038,PRD04,SHIFT_NIGHT,12,87,58,56,2,7.3,10.9 +RUN0640,M039,PRD08,SHIFT_NIGHT,1,103,76,76,0,10.0,12.9 +RUN0641,M039,PRD08,SHIFT_SWING,2,103,90,89,1,11.3,12.9 +RUN0642,M039,PRD04,SHIFT_SWING,3,84,74,72,2,9.3,10.6 +RUN0643,M039,PRD08,SHIFT_SWING,3,103,86,84,2,10.8,12.9 +RUN0644,M039,PRD08,SHIFT_NIGHT,4,103,78,77,1,9.9,12.9 +RUN0645,M039,PRD04,SHIFT_SWING,4,84,65,64,1,8.3,10.6 +RUN0646,M039,PRD08,SHIFT_NIGHT,5,103,80,79,1,10.5,12.9 +RUN0647,M039,PRD04,SHIFT_SWING,5,84,65,63,2,8.5,10.6 +RUN0648,M039,PRD08,SHIFT_DAY,6,103,83,81,2,10.4,12.9 +RUN0649,M039,PRD04,SHIFT_SWING,7,84,68,67,1,8.6,10.6 +RUN0650,M039,PRD04,SHIFT_NIGHT,8,84,67,66,1,8.4,10.6 +RUN0651,M039,PRD08,SHIFT_NIGHT,8,103,74,72,2,9.3,12.9 +RUN0652,M039,PRD04,SHIFT_SWING,9,84,72,70,2,9.0,10.6 +RUN0653,M039,PRD08,SHIFT_NIGHT,9,103,75,74,1,9.4,12.9 +RUN0654,M039,PRD04,SHIFT_NIGHT,10,84,64,62,2,8.1,10.6 +RUN0655,M039,PRD04,SHIFT_SWING,11,84,67,66,1,8.4,10.6 +RUN0656,M039,PRD04,SHIFT_DAY,12,84,74,72,2,9.3,10.6 +RUN0657,M039,PRD08,SHIFT_NIGHT,12,103,79,78,1,9.9,12.9 +RUN0658,M040,PRD08,SHIFT_DAY,1,105,74,72,2,9.3,13.2 +RUN0659,M040,PRD08,SHIFT_NIGHT,2,105,68,66,2,8.5,13.2 +RUN0660,M040,PRD04,SHIFT_DAY,3,96,66,64,2,8.3,12.0 +RUN0661,M040,PRD08,SHIFT_NIGHT,3,105,76,75,1,9.5,13.2 +RUN0662,M040,PRD04,SHIFT_DAY,4,96,62,60,2,9.2,12.0 +RUN0663,M040,PRD08,SHIFT_SWING,5,105,75,73,2,9.4,13.2 +RUN0664,M040,PRD08,SHIFT_DAY,6,105,61,59,2,9.1,13.2 +RUN0665,M040,PRD08,SHIFT_SWING,7,105,71,68,3,8.9,13.2 +RUN0666,M040,PRD04,SHIFT_NIGHT,7,96,64,63,1,8.0,12.0 +RUN0667,M040,PRD04,SHIFT_DAY,8,96,76,73,3,9.6,12.0 +RUN0668,M040,PRD08,SHIFT_SWING,8,105,79,77,2,9.9,13.2 +RUN0669,M040,PRD04,SHIFT_SWING,9,96,64,63,1,9.0,12.0 +RUN0670,M040,PRD04,SHIFT_NIGHT,10,96,64,63,1,8.0,12.0 +RUN0671,M040,PRD04,SHIFT_NIGHT,11,96,62,60,2,7.8,12.0 +RUN0672,M040,PRD08,SHIFT_SWING,11,105,76,75,1,9.6,13.2 +RUN0673,M040,PRD08,SHIFT_NIGHT,12,105,75,73,2,9.4,13.2 +RUN0674,M041,PRD08,SHIFT_SWING,1,117,75,74,1,10.4,14.7 +RUN0675,M041,PRD05,SHIFT_NIGHT,2,322,224,220,4,28.0,40.3 +RUN0676,M041,PRD08,SHIFT_DAY,2,117,84,84,0,10.5,14.7 +RUN0677,M041,PRD08,SHIFT_SWING,3,117,90,90,0,11.3,14.7 +RUN0678,M041,PRD07,SHIFT_SWING,3,166,123,119,4,15.4,20.8 +RUN0679,M041,PRD07,SHIFT_DAY,4,166,121,119,2,15.2,20.8 +RUN0680,M041,PRD05,SHIFT_DAY,4,322,248,245,3,31.0,40.3 +RUN0681,M041,PRD05,SHIFT_SWING,5,322,235,232,3,30.6,40.3 +RUN0682,M041,PRD07,SHIFT_SWING,5,166,114,110,4,14.9,20.8 +RUN0683,M041,PRD05,SHIFT_NIGHT,6,322,235,230,5,29.4,40.3 +RUN0684,M041,PRD05,SHIFT_SWING,7,322,249,242,7,31.2,40.3 +RUN0685,M041,PRD08,SHIFT_NIGHT,8,117,77,77,0,10.7,14.7 +RUN0686,M041,PRD07,SHIFT_DAY,8,166,104,101,3,14.5,20.8 +RUN0687,M041,PRD07,SHIFT_NIGHT,9,166,112,108,4,14.0,20.8 +RUN0688,M041,PRD05,SHIFT_SWING,10,322,195,189,6,30.9,40.3 +RUN0689,M041,PRD05,SHIFT_NIGHT,11,322,229,224,5,28.7,40.3 +RUN0690,M041,PRD07,SHIFT_SWING,11,166,117,115,2,14.7,20.8 +RUN0691,M041,PRD05,SHIFT_SWING,12,322,239,236,3,29.9,40.3 +RUN0692,M042,PRD08,SHIFT_SWING,1,98,64,63,1,8.0,12.3 +RUN0693,M042,PRD05,SHIFT_NIGHT,2,312,211,207,4,26.4,39.1 +RUN0694,M042,PRD08,SHIFT_NIGHT,2,98,60,60,0,7.6,12.3 +RUN0695,M042,PRD08,SHIFT_NIGHT,3,98,68,67,1,8.5,12.3 +RUN0696,M042,PRD08,SHIFT_DAY,4,98,72,72,0,9.0,12.3 +RUN0697,M042,PRD05,SHIFT_DAY,4,312,217,213,4,27.2,39.1 +RUN0698,M042,PRD07,SHIFT_NIGHT,5,189,129,127,2,16.2,23.7 +RUN0699,M042,PRD07,SHIFT_NIGHT,6,189,134,131,3,16.8,23.7 +RUN0700,M042,PRD08,SHIFT_SWING,6,98,65,65,0,8.2,12.3 +RUN0701,M042,PRD05,SHIFT_DAY,7,312,240,232,8,30.1,39.1 +RUN0702,M042,PRD08,SHIFT_DAY,8,98,58,57,1,8.6,12.3 +RUN0703,M042,PRD07,SHIFT_SWING,9,189,131,129,2,16.4,23.7 +RUN0704,M042,PRD05,SHIFT_DAY,10,312,227,222,5,28.4,39.1 +RUN0705,M042,PRD08,SHIFT_SWING,11,98,66,65,1,8.3,12.3 +RUN0706,M042,PRD07,SHIFT_DAY,12,189,146,142,4,18.3,23.7 +RUN0707,M043,PRD07,SHIFT_SWING,1,168,129,128,1,17.3,21.0 +RUN0708,M043,PRD05,SHIFT_SWING,1,329,258,256,2,34.6,41.2 +RUN0709,M043,PRD08,SHIFT_DAY,2,118,80,80,0,11.5,14.8 +RUN0710,M043,PRD07,SHIFT_SWING,3,168,123,123,0,17.7,21.0 +RUN0711,M043,PRD05,SHIFT_SWING,4,329,238,236,2,31.8,41.2 +RUN0712,M043,PRD08,SHIFT_DAY,4,118,88,88,0,11.8,14.8 +RUN0713,M043,PRD07,SHIFT_SWING,5,168,116,115,1,16.6,21.0 +RUN0714,M043,PRD07,SHIFT_NIGHT,6,168,122,121,1,15.8,21.0 +RUN0715,M043,PRD05,SHIFT_NIGHT,6,329,247,247,0,32.1,41.2 +RUN0716,M043,PRD08,SHIFT_NIGHT,7,118,80,80,0,10.7,14.8 +RUN0717,M043,PRD07,SHIFT_DAY,7,168,140,138,2,18.7,21.0 +RUN0718,M043,PRD07,SHIFT_SWING,8,168,120,119,1,16.2,21.0 +RUN0719,M043,PRD08,SHIFT_NIGHT,8,118,89,88,1,12.0,14.8 +RUN0720,M043,PRD05,SHIFT_NIGHT,9,329,259,258,1,34.2,41.2 +RUN0721,M043,PRD05,SHIFT_SWING,10,329,256,255,1,35.4,41.2 +RUN0722,M043,PRD07,SHIFT_SWING,10,168,126,126,0,17.5,21.0 +RUN0723,M043,PRD07,SHIFT_SWING,11,168,134,134,0,18.4,21.0 +RUN0724,M043,PRD05,SHIFT_NIGHT,12,329,261,260,1,34.0,41.2 +RUN0725,M043,PRD08,SHIFT_NIGHT,12,118,83,82,1,10.9,14.8 +RUN0726,M044,PRD08,SHIFT_NIGHT,1,119,77,77,0,10.1,14.9 +RUN0727,M044,PRD05,SHIFT_NIGHT,1,336,206,203,3,26.9,42.1 +RUN0728,M044,PRD05,SHIFT_DAY,2,336,236,233,3,29.5,42.1 +RUN0729,M044,PRD08,SHIFT_NIGHT,2,119,78,78,0,9.8,14.9 +RUN0730,M044,PRD07,SHIFT_DAY,3,176,127,123,4,15.9,22.1 +RUN0731,M044,PRD08,SHIFT_NIGHT,4,119,86,86,0,10.8,14.9 +RUN0732,M044,PRD05,SHIFT_DAY,5,336,236,233,3,29.6,42.1 +RUN0733,M044,PRD07,SHIFT_SWING,5,176,128,122,6,16.1,22.1 +RUN0734,M044,PRD05,SHIFT_NIGHT,6,336,227,224,3,28.4,42.1 +RUN0735,M044,PRD07,SHIFT_NIGHT,6,176,130,127,3,16.3,22.1 +RUN0736,M044,PRD05,SHIFT_SWING,7,336,214,207,7,28.1,42.1 +RUN0737,M044,PRD07,SHIFT_DAY,7,176,120,116,4,15.8,22.1 +RUN0738,M044,PRD05,SHIFT_SWING,8,336,256,252,4,32.1,42.1 +RUN0739,M044,PRD07,SHIFT_SWING,9,176,122,118,4,15.3,22.1 +RUN0740,M044,PRD08,SHIFT_NIGHT,10,119,84,84,0,10.6,14.9 +RUN0741,M044,PRD07,SHIFT_NIGHT,10,176,119,115,4,14.9,22.1 +RUN0742,M044,PRD07,SHIFT_SWING,11,176,124,119,5,15.5,22.1 +RUN0743,M044,PRD05,SHIFT_NIGHT,12,336,248,241,7,31.1,42.1 +RUN0744,M044,PRD08,SHIFT_NIGHT,12,119,85,85,0,10.7,14.9 +RUN0745,M045,PRD08,SHIFT_SWING,1,108,72,69,3,9.1,13.6 +RUN0746,M045,PRD05,SHIFT_NIGHT,1,350,240,238,2,30.1,43.8 +RUN0747,M045,PRD07,SHIFT_SWING,2,164,95,94,1,14.1,20.6 +RUN0748,M045,PRD05,SHIFT_NIGHT,3,350,202,199,3,27.5,43.8 +RUN0749,M045,PRD08,SHIFT_DAY,3,108,68,66,2,9.3,13.6 +RUN0750,M045,PRD05,SHIFT_DAY,4,350,268,265,3,33.6,43.8 +RUN0751,M045,PRD05,SHIFT_DAY,5,350,249,244,5,31.2,43.8 +RUN0752,M045,PRD07,SHIFT_NIGHT,6,164,108,106,2,13.6,20.6 +RUN0753,M045,PRD08,SHIFT_SWING,6,108,78,76,2,9.8,13.6 +RUN0754,M045,PRD08,SHIFT_SWING,7,108,70,67,3,8.8,13.6 +RUN0755,M045,PRD07,SHIFT_DAY,8,164,94,92,2,14.6,20.6 +RUN0756,M045,PRD08,SHIFT_DAY,9,108,70,67,3,9.8,13.6 +RUN0757,M045,PRD05,SHIFT_NIGHT,9,350,226,220,6,31.5,43.8 +RUN0758,M045,PRD07,SHIFT_DAY,10,164,112,111,1,14.0,20.6 +RUN0759,M045,PRD05,SHIFT_DAY,11,350,254,250,4,33.1,43.8 +RUN0760,M045,PRD08,SHIFT_DAY,11,108,74,72,2,9.7,13.6 +RUN0761,M045,PRD05,SHIFT_SWING,12,350,236,234,2,29.6,43.8 +RUN0762,M046,PRD08,SHIFT_NIGHT,1,117,88,85,3,11.0,14.7 +RUN0763,M046,PRD05,SHIFT_SWING,1,310,232,230,2,29.1,38.8 +RUN0764,M046,PRD07,SHIFT_NIGHT,2,174,133,131,2,16.7,21.8 +RUN0765,M046,PRD08,SHIFT_SWING,3,117,99,95,4,12.4,14.7 +RUN0766,M046,PRD05,SHIFT_SWING,4,310,263,261,2,32.9,38.8 +RUN0767,M046,PRD08,SHIFT_NIGHT,5,117,90,89,1,11.3,14.7 +RUN0768,M046,PRD07,SHIFT_SWING,5,174,145,138,7,18.2,21.8 +RUN0769,M046,PRD07,SHIFT_DAY,6,174,152,148,4,19.1,21.8 +RUN0770,M046,PRD05,SHIFT_NIGHT,7,310,246,243,3,30.8,38.8 +RUN0771,M046,PRD05,SHIFT_DAY,8,310,260,259,1,32.6,38.8 +RUN0772,M046,PRD05,SHIFT_SWING,9,310,248,246,2,31.0,38.8 +RUN0773,M046,PRD08,SHIFT_SWING,10,117,98,95,3,12.3,14.7 +RUN0774,M046,PRD08,SHIFT_DAY,11,117,93,90,3,11.7,14.7 +RUN0775,M046,PRD08,SHIFT_SWING,12,117,98,97,1,12.3,14.7 +RUN0776,M046,PRD05,SHIFT_SWING,12,310,264,263,1,33.1,38.8 +RUN0777,M047,PRD07,SHIFT_NIGHT,1,170,97,96,1,14.4,21.3 +RUN0778,M047,PRD07,SHIFT_SWING,2,170,79,79,0,15.1,21.3 +RUN0779,M047,PRD07,SHIFT_NIGHT,3,170,86,84,2,13.8,21.3 +RUN0780,M047,PRD07,SHIFT_SWING,4,170,95,93,2,15.8,21.3 +RUN0781,M047,PRD08,SHIFT_SWING,4,116,59,59,0,9.8,14.6 +RUN0782,M047,PRD08,SHIFT_NIGHT,5,116,66,66,0,9.4,14.6 +RUN0783,M047,PRD07,SHIFT_DAY,6,170,123,121,2,15.8,21.3 +RUN0784,M047,PRD08,SHIFT_SWING,6,116,81,81,0,10.4,14.6 +RUN0785,M047,PRD08,SHIFT_SWING,7,116,56,56,0,10.3,14.6 +RUN0786,M047,PRD08,SHIFT_SWING,8,116,79,78,1,10.8,14.6 +RUN0787,M047,PRD07,SHIFT_DAY,8,170,117,116,1,15.9,21.3 +RUN0788,M047,PRD05,SHIFT_SWING,9,344,178,171,7,30.0,43.0 +RUN0789,M047,PRD08,SHIFT_NIGHT,10,116,77,77,0,10.4,14.6 +RUN0790,M047,PRD05,SHIFT_DAY,10,344,237,233,4,32.0,43.0 +RUN0791,M047,PRD08,SHIFT_SWING,11,116,80,80,0,11.1,14.6 +RUN0792,M047,PRD07,SHIFT_DAY,12,170,113,110,3,16.8,21.3 +RUN0793,M048,PRD07,SHIFT_DAY,1,156,105,104,1,13.2,19.6 +RUN0794,M048,PRD08,SHIFT_DAY,1,108,80,77,3,10.0,13.6 +RUN0795,M048,PRD05,SHIFT_NIGHT,2,325,234,233,1,29.3,40.7 +RUN0796,M048,PRD08,SHIFT_NIGHT,2,108,76,74,2,9.5,13.6 +RUN0797,M048,PRD07,SHIFT_SWING,3,156,113,110,3,14.2,19.6 +RUN0798,M048,PRD08,SHIFT_DAY,4,108,82,79,3,10.3,13.6 +RUN0799,M048,PRD07,SHIFT_NIGHT,5,156,104,103,1,13.8,19.6 +RUN0800,M048,PRD05,SHIFT_SWING,5,325,206,203,3,27.1,40.7 +RUN0801,M048,PRD07,SHIFT_DAY,6,156,108,106,2,13.6,19.6 +RUN0802,M048,PRD05,SHIFT_SWING,7,325,196,195,1,26.2,40.7 +RUN0803,M048,PRD08,SHIFT_SWING,7,108,72,71,1,9.7,13.6 +RUN0804,M048,PRD07,SHIFT_SWING,8,156,102,101,1,12.8,19.6 +RUN0805,M048,PRD05,SHIFT_NIGHT,9,325,210,208,2,26.3,40.7 +RUN0806,M048,PRD07,SHIFT_SWING,9,156,111,109,2,13.9,19.6 +RUN0807,M048,PRD08,SHIFT_SWING,10,108,69,68,1,10.2,13.6 +RUN0808,M048,PRD08,SHIFT_SWING,11,108,72,69,3,9.0,13.6 +RUN0809,M048,PRD07,SHIFT_NIGHT,12,156,99,98,1,12.4,19.6 +RUN0810,M049,PRD07,SHIFT_NIGHT,1,163,136,132,4,17.1,20.4 +RUN0811,M049,PRD08,SHIFT_DAY,2,104,91,90,1,11.4,13.0 +RUN0812,M049,PRD05,SHIFT_SWING,2,379,298,290,8,37.3,47.4 +RUN0813,M049,PRD05,SHIFT_NIGHT,3,379,300,288,12,37.5,47.4 +RUN0814,M049,PRD07,SHIFT_NIGHT,3,163,126,121,5,15.8,20.4 +RUN0815,M049,PRD08,SHIFT_NIGHT,4,104,84,83,1,10.6,13.0 +RUN0816,M049,PRD07,SHIFT_DAY,5,163,134,129,5,16.8,20.4 +RUN0817,M049,PRD08,SHIFT_DAY,6,104,83,83,0,10.4,13.0 +RUN0818,M049,PRD05,SHIFT_NIGHT,7,379,319,307,12,39.9,47.4 +RUN0819,M049,PRD05,SHIFT_SWING,8,379,328,320,8,41.1,47.4 +RUN0820,M049,PRD07,SHIFT_NIGHT,9,163,135,130,5,16.9,20.4 +RUN0821,M049,PRD05,SHIFT_NIGHT,9,379,287,277,10,35.9,47.4 +RUN0822,M049,PRD08,SHIFT_SWING,10,104,86,86,0,11.0,13.0 +RUN0823,M049,PRD05,SHIFT_SWING,11,379,298,287,11,37.3,47.4 +RUN0824,M049,PRD07,SHIFT_SWING,11,163,138,134,4,17.3,20.4 +RUN0825,M049,PRD05,SHIFT_NIGHT,12,379,303,288,15,37.9,47.4 +RUN0826,M049,PRD08,SHIFT_SWING,12,104,80,80,0,10.1,13.0 +RUN0827,M050,PRD07,SHIFT_NIGHT,1,183,126,125,1,15.8,22.9 +RUN0828,M050,PRD08,SHIFT_SWING,2,108,79,79,0,9.9,13.5 +RUN0829,M050,PRD05,SHIFT_NIGHT,2,314,215,214,1,26.9,39.3 +RUN0830,M050,PRD07,SHIFT_NIGHT,3,183,133,130,3,16.7,22.9 +RUN0831,M050,PRD05,SHIFT_SWING,4,314,220,215,5,27.6,39.3 +RUN0832,M050,PRD07,SHIFT_DAY,4,183,135,132,3,16.9,22.9 +RUN0833,M050,PRD05,SHIFT_SWING,5,314,209,208,1,26.2,39.3 +RUN0834,M050,PRD08,SHIFT_NIGHT,6,108,75,74,1,9.4,13.5 +RUN0835,M050,PRD05,SHIFT_DAY,6,314,250,246,4,31.3,39.3 +RUN0836,M050,PRD07,SHIFT_SWING,7,183,139,135,4,17.4,22.9 +RUN0837,M050,PRD07,SHIFT_DAY,8,183,124,123,1,15.6,22.9 +RUN0838,M050,PRD07,SHIFT_SWING,9,183,111,109,2,15.3,22.9 +RUN0839,M050,PRD08,SHIFT_SWING,9,108,74,74,0,10.3,13.5 +RUN0840,M050,PRD07,SHIFT_SWING,10,183,126,123,3,16.4,22.9 +RUN0841,M050,PRD05,SHIFT_SWING,10,314,228,223,5,29.6,39.3 +RUN0842,M050,PRD05,SHIFT_SWING,11,314,233,229,4,29.2,39.3 +RUN0843,M050,PRD08,SHIFT_NIGHT,11,108,73,72,1,9.2,13.5 +RUN0844,M050,PRD08,SHIFT_NIGHT,12,108,77,77,0,9.7,13.5 diff --git a/v1/machine_maintenance/data/products.csv b/v1/machine_maintenance/data/products.csv new file mode 100644 index 00000000..1331e4e8 --- /dev/null +++ b/v1/machine_maintenance/data/products.csv @@ -0,0 +1,9 @@ +product_id,product_name,product_type,unit_weight_kg,target_cycle_time_sec +PRD01,Turbine Blade Assembly,Component,12.5,180 +PRD02,Compressor Valve Unit,Component,3.2,90 +PRD03,Pump Impeller Kit,Component,8.0,120 +PRD04,Generator Rotor Pack,Assembly,25.0,300 +PRD05,Motor Winding Set,Component,4.5,75 +PRD06,Hydraulic Seal Kit,Sub-Assembly,1.8,45 +PRD07,Bearing Housing Assembly,Sub-Assembly,6.0,150 +PRD08,Control Panel Unit,Assembly,15.0,240 diff --git a/v1/machine_maintenance/data/qualifications.csv b/v1/machine_maintenance/data/qualifications.csv new file mode 100644 index 00000000..eda2217a --- /dev/null +++ b/v1/machine_maintenance/data/qualifications.csv @@ -0,0 +1,33 @@ +technician_id,machine_type,certification_expiry_date +T001,Turbine,2025-01-27 +T001,Generator,2025-05-11 +T002,Compressor,2025-06-01 +T002,Pump,2025-11-17 +T003,Generator,2025-09-03 +T003,Motor,2025-05-07 +T004,Compressor,2025-04-16 +T005,Motor,2025-10-02 +T005,Pump,2025-06-20 +T006,Pump,2025-08-27 +T006,Generator,2025-07-06 +T007,Compressor,2025-11-27 +T008,Compressor,2025-11-19 +T008,Motor,2025-04-19 +T009,Turbine,2025-06-11 +T009,Generator,2025-05-23 +T010,Pump,2025-06-30 +T010,Motor,2025-05-03 +T011,Compressor,2025-10-21 +T012,Motor,2025-06-29 +T012,Generator,2025-11-21 +T013,Pump,2025-06-19 +T013,Compressor,2025-05-06 +T014,Motor,2025-09-19 +T015,Pump,2025-10-18 +T016,Compressor,2025-05-05 +T017,Turbine,2025-10-15 +T018,Motor,2025-07-31 +T018,Pump,2025-08-20 +T019,Generator,2025-06-06 +T020,Compressor,2025-10-13 +T020,Motor,2025-11-09 diff --git a/v1/machine_maintenance/data/sensor_readings.csv b/v1/machine_maintenance/data/sensor_readings.csv new file mode 100644 index 00000000..3a665098 --- /dev/null +++ b/v1/machine_maintenance/data/sensor_readings.csv @@ -0,0 +1,2401 @@ +sensor_id,machine_id,period,value,is_anomaly +S001,M001,1,0.91,0 +S001,M001,2,2.12,0 +S001,M001,3,2.38,0 +S001,M001,4,2.55,0 +S001,M001,5,1.65,0 +S001,M001,6,2.29,0 +S001,M001,7,3.66,0 +S001,M001,8,4.98,0 +S001,M001,9,3.41,0 +S001,M001,10,4.7,0 +S001,M001,11,7.17,1 +S001,M001,12,7.36,1 +S002,M001,1,44.1,0 +S002,M001,2,57.58,0 +S002,M001,3,53.97,0 +S002,M001,4,57.01,0 +S002,M001,5,64.14,0 +S002,M001,6,56.56,0 +S002,M001,7,70.04,0 +S002,M001,8,74.08,0 +S002,M001,9,74.88,0 +S002,M001,10,74.12,0 +S002,M001,11,90.09,1 +S002,M001,12,94.81,1 +S003,M001,1,177.01,0 +S003,M001,2,187.9,0 +S003,M001,3,151.36,0 +S003,M001,4,182.5,0 +S003,M001,5,144.76,0 +S003,M001,6,149.3,0 +S003,M001,7,159.75,0 +S003,M001,8,145.09,0 +S003,M001,9,119.48,0 +S003,M001,10,120.37,0 +S003,M001,11,109.7,1 +S003,M001,12,108.42,1 +S004,M001,1,11.7,0 +S004,M001,2,11.64,0 +S004,M001,3,18.66,0 +S004,M001,4,18.99,0 +S004,M001,5,24.39,0 +S004,M001,6,24.44,0 +S004,M001,7,23.39,0 +S004,M001,8,31.92,0 +S004,M001,9,36.41,0 +S004,M001,10,42.13,1 +S004,M001,11,41.55,1 +S004,M001,12,39.52,0 +S005,M002,7,1.57,0 +S005,M002,2,0.55,0 +S005,M002,3,2.0,0 +S005,M002,4,1.77,0 +S005,M002,5,1.14,0 +S005,M002,6,0.66,0 +S005,M002,1,1.47,0 +S005,M002,8,1.17,0 +S005,M002,9,1.85,0 +S005,M002,10,0.98,0 +S005,M002,11,0.72,0 +S005,M002,12,0.69,0 +S006,M002,1,52.49,0 +S006,M002,2,51.71,0 +S006,M002,3,54.24,0 +S006,M002,4,47.62,0 +S006,M002,5,51.95,0 +S006,M002,6,45.9,0 +S006,M002,7,50.11,0 +S006,M002,8,48.6,0 +S006,M002,9,55.86,0 +S006,M002,10,42.29,0 +S006,M002,11,53.09,0 +S006,M002,12,57.55,0 +S007,M002,7,169.94,0 +S007,M002,2,193.27,0 +S007,M002,3,171.5,0 +S007,M002,4,194.4,0 +S007,M002,5,170.69,0 +S007,M002,6,191.37,0 +S007,M002,1,177.57,0 +S007,M002,8,186.48,0 +S007,M002,9,165.63,0 +S007,M002,10,165.63,0 +S007,M002,11,162.74,0 +S007,M002,12,197.68,0 +S008,M002,7,15.59,0 +S008,M002,2,17.11,0 +S008,M002,3,11.37,0 +S008,M002,4,11.28,0 +S008,M002,5,16.48,0 +S008,M002,6,21.17,0 +S008,M002,1,20.3,0 +S008,M002,8,11.95,0 +S008,M002,9,10.9,0 +S008,M002,10,24.35,0 +S008,M002,11,18.43,0 +S008,M002,12,20.05,0 +S009,M003,1,1.12,0 +S009,M003,2,1.81,0 +S009,M003,3,1.91,0 +S009,M003,4,1.13,0 +S009,M003,5,1.87,0 +S009,M003,6,0.58,0 +S009,M003,7,0.93,0 +S009,M003,8,0.97,0 +S009,M003,9,0.66,0 +S009,M003,10,1.26,0 +S009,M003,11,0.73,0 +S009,M003,12,1.22,0 +S010,M003,1,56.95,0 +S010,M003,2,47.0,0 +S010,M003,3,51.13,0 +S010,M003,4,42.3,0 +S010,M003,5,45.57,0 +S010,M003,6,44.52,0 +S010,M003,7,48.58,0 +S010,M003,8,51.22,0 +S010,M003,9,55.35,0 +S010,M003,10,52.43,0 +S010,M003,11,53.5,0 +S010,M003,12,51.13,0 +S011,M003,1,180.45,0 +S011,M003,2,182.89,0 +S011,M003,3,184.67,0 +S011,M003,4,151.45,0 +S011,M003,5,163.66,0 +S011,M003,6,188.12,0 +S011,M003,7,196.08,0 +S011,M003,8,167.55,0 +S011,M003,9,159.07,0 +S011,M003,10,157.16,0 +S011,M003,11,182.87,0 +S011,M003,12,153.48,0 +S012,M003,1,21.11,0 +S012,M003,2,24.58,0 +S012,M003,3,11.38,0 +S012,M003,4,17.96,0 +S012,M003,5,15.64,0 +S012,M003,6,16.8,0 +S012,M003,7,12.36,0 +S012,M003,8,14.67,0 +S012,M003,9,14.87,0 +S012,M003,10,24.12,0 +S012,M003,11,18.94,0 +S012,M003,12,23.25,0 +S013,M004,1,0.73,0 +S013,M004,2,1.63,0 +S013,M004,3,0.57,0 +S013,M004,4,0.62,0 +S013,M004,5,1.16,0 +S013,M004,6,1.14,0 +S013,M004,7,1.41,0 +S013,M004,8,1.12,0 +S013,M004,9,0.82,0 +S013,M004,10,1.27,0 +S013,M004,11,1.02,0 +S013,M004,12,0.67,0 +S014,M004,1,53.56,0 +S014,M004,2,51.86,0 +S014,M004,3,50.54,0 +S014,M004,4,46.17,0 +S014,M004,5,44.41,0 +S014,M004,6,59.69,0 +S014,M004,7,41.1,0 +S014,M004,8,44.09,0 +S014,M004,9,58.91,0 +S014,M004,10,55.35,0 +S014,M004,11,55.28,0 +S014,M004,12,45.79,0 +S015,M004,1,156.0,0 +S015,M004,2,174.87,0 +S015,M004,3,171.53,0 +S015,M004,4,156.46,0 +S015,M004,5,176.63,0 +S015,M004,6,161.51,0 +S015,M004,7,177.94,0 +S015,M004,8,183.4,0 +S015,M004,9,167.71,0 +S015,M004,10,169.85,0 +S015,M004,11,187.1,0 +S015,M004,12,171.81,0 +S016,M004,1,16.9,0 +S016,M004,2,11.17,0 +S016,M004,3,14.7,0 +S016,M004,4,16.44,0 +S016,M004,5,16.07,0 +S016,M004,6,19.6,0 +S016,M004,7,14.34,0 +S016,M004,8,16.08,0 +S016,M004,9,24.49,0 +S016,M004,10,12.58,0 +S016,M004,11,24.21,0 +S016,M004,12,16.9,0 +S017,M005,1,1.53,0 +S017,M005,2,1.04,0 +S017,M005,3,1.16,0 +S017,M005,4,0.66,0 +S017,M005,5,1.15,0 +S017,M005,6,1.1,0 +S017,M005,7,0.62,0 +S017,M005,8,1.81,0 +S017,M005,9,1.62,0 +S017,M005,10,0.83,0 +S017,M005,11,1.67,0 +S017,M005,12,0.61,0 +S018,M005,1,52.66,0 +S018,M005,2,50.61,0 +S018,M005,3,43.88,0 +S018,M005,4,55.52,0 +S018,M005,5,47.02,0 +S018,M005,6,59.16,0 +S018,M005,7,52.91,0 +S018,M005,8,42.95,0 +S018,M005,9,42.06,0 +S018,M005,10,45.12,0 +S018,M005,11,43.47,0 +S018,M005,12,43.06,0 +S019,M005,1,187.96,0 +S019,M005,2,153.77,0 +S019,M005,3,197.56,0 +S019,M005,4,181.4,0 +S019,M005,5,173.2,0 +S019,M005,6,178.19,0 +S019,M005,7,178.97,0 +S019,M005,8,184.06,0 +S019,M005,9,194.1,0 +S019,M005,10,182.15,0 +S019,M005,11,199.33,0 +S019,M005,12,166.16,0 +S020,M005,1,24.56,0 +S020,M005,2,14.74,0 +S020,M005,3,16.6,0 +S020,M005,4,17.03,0 +S020,M005,5,19.47,0 +S020,M005,6,21.94,0 +S020,M005,7,18.87,0 +S020,M005,8,15.28,0 +S020,M005,9,11.11,0 +S020,M005,10,16.93,0 +S020,M005,11,10.56,0 +S020,M005,12,15.53,0 +S021,M006,1,0.93,0 +S021,M006,2,1.62,0 +S021,M006,3,2.43,0 +S021,M006,4,1.3,0 +S021,M006,5,2.57,0 +S021,M006,6,2.63,0 +S021,M006,7,3.53,0 +S021,M006,8,4.94,0 +S021,M006,9,5.09,0 +S021,M006,10,3.94,0 +S021,M006,11,6.95,1 +S021,M006,12,6.71,1 +S022,M006,1,43.76,0 +S022,M006,2,53.11,0 +S022,M006,3,52.93,0 +S022,M006,4,64.17,0 +S022,M006,5,57.29,0 +S022,M006,6,67.23,0 +S022,M006,7,71.17,0 +S022,M006,8,77.46,0 +S022,M006,9,66.23,0 +S022,M006,10,80.13,0 +S022,M006,11,75.28,0 +S022,M006,12,90.61,1 +S023,M006,1,199.55,0 +S023,M006,2,174.4,0 +S023,M006,3,175.2,0 +S023,M006,4,159.52,0 +S023,M006,5,178.89,0 +S023,M006,6,150.49,0 +S023,M006,7,159.22,0 +S023,M006,8,148.22,0 +S023,M006,9,120.63,0 +S023,M006,10,130.53,0 +S023,M006,11,114.69,1 +S023,M006,12,106.59,1 +S024,M006,1,18.37,0 +S024,M006,2,16.48,0 +S024,M006,3,15.72,0 +S024,M006,4,26.33,0 +S024,M006,5,29.95,0 +S024,M006,6,26.7,0 +S024,M006,7,22.33,0 +S024,M006,8,23.91,0 +S024,M006,9,35.46,0 +S024,M006,10,42.25,1 +S024,M006,11,36.32,0 +S024,M006,12,44.84,1 +S025,M007,1,1.49,0 +S025,M007,2,0.69,0 +S025,M007,3,1.56,0 +S025,M007,4,0.63,0 +S025,M007,5,1.34,0 +S025,M007,6,1.47,0 +S025,M007,7,0.99,0 +S025,M007,8,0.69,0 +S025,M007,9,1.55,0 +S025,M007,10,1.92,0 +S025,M007,11,2.0,0 +S025,M007,12,1.87,0 +S026,M007,1,41.87,0 +S026,M007,2,53.41,0 +S026,M007,3,50.17,0 +S026,M007,4,40.36,0 +S026,M007,5,46.18,0 +S026,M007,6,59.02,0 +S026,M007,7,46.83,0 +S026,M007,8,55.32,0 +S026,M007,9,43.84,0 +S026,M007,10,60.0,0 +S026,M007,11,43.1,0 +S026,M007,12,45.92,0 +S027,M007,1,189.41,0 +S027,M007,2,175.38,0 +S027,M007,3,176.99,0 +S027,M007,4,186.93,0 +S027,M007,5,176.33,0 +S027,M007,6,183.1,0 +S027,M007,7,193.82,0 +S027,M007,8,180.9,0 +S027,M007,9,179.86,0 +S027,M007,10,157.71,0 +S027,M007,11,159.04,0 +S027,M007,12,184.64,0 +S028,M007,1,19.37,0 +S028,M007,2,23.54,0 +S028,M007,3,20.82,0 +S028,M007,4,10.66,0 +S028,M007,5,15.46,0 +S028,M007,6,20.79,0 +S028,M007,7,24.54,0 +S028,M007,8,19.16,0 +S028,M007,9,21.29,0 +S028,M007,10,21.76,0 +S028,M007,11,13.51,0 +S028,M007,12,13.59,0 +S029,M008,1,1.95,0 +S029,M008,2,1.68,0 +S029,M008,3,1.86,0 +S029,M008,4,0.79,0 +S029,M008,5,1.52,0 +S029,M008,6,1.36,0 +S029,M008,7,1.16,0 +S029,M008,8,1.65,0 +S029,M008,9,1.67,0 +S029,M008,10,1.25,0 +S029,M008,11,1.13,0 +S029,M008,12,0.74,0 +S030,M008,1,43.99,0 +S030,M008,2,52.09,0 +S030,M008,3,57.46,0 +S030,M008,4,41.04,0 +S030,M008,5,49.61,0 +S030,M008,6,47.42,0 +S030,M008,7,58.69,0 +S030,M008,8,54.23,0 +S030,M008,9,50.32,0 +S030,M008,10,42.49,0 +S030,M008,11,41.68,0 +S030,M008,12,43.21,0 +S031,M008,1,172.47,0 +S031,M008,2,175.67,0 +S031,M008,3,191.55,0 +S031,M008,4,154.59,0 +S031,M008,5,195.53,0 +S031,M008,6,190.84,0 +S031,M008,7,194.23,0 +S031,M008,8,196.52,0 +S031,M008,9,170.74,0 +S031,M008,10,169.82,0 +S031,M008,11,168.7,0 +S031,M008,12,169.31,0 +S032,M008,1,11.22,0 +S032,M008,2,13.37,0 +S032,M008,3,14.78,0 +S032,M008,4,11.49,0 +S032,M008,5,20.72,0 +S032,M008,6,15.03,0 +S032,M008,7,12.19,0 +S032,M008,8,10.57,0 +S032,M008,9,23.76,0 +S032,M008,10,17.09,0 +S032,M008,11,22.47,0 +S032,M008,12,21.38,0 +S033,M009,1,1.2,0 +S033,M009,2,1.42,0 +S033,M009,3,1.86,0 +S033,M009,4,0.53,0 +S033,M009,5,0.82,0 +S033,M009,6,0.72,0 +S033,M009,7,1.91,0 +S033,M009,8,1.41,0 +S033,M009,9,1.13,0 +S033,M009,10,1.66,0 +S033,M009,11,0.86,0 +S033,M009,12,0.68,0 +S034,M009,1,44.77,0 +S034,M009,2,52.78,0 +S034,M009,3,52.46,0 +S034,M009,4,41.26,0 +S034,M009,5,56.73,0 +S034,M009,6,50.0,0 +S034,M009,7,50.72,0 +S034,M009,8,52.64,0 +S034,M009,9,51.5,0 +S034,M009,10,54.37,0 +S034,M009,11,45.83,0 +S034,M009,12,40.03,0 +S035,M009,1,167.64,0 +S035,M009,2,178.53,0 +S035,M009,3,159.37,0 +S035,M009,4,183.41,0 +S035,M009,5,176.17,0 +S035,M009,6,168.03,0 +S035,M009,7,197.67,0 +S035,M009,8,177.2,0 +S035,M009,9,189.32,0 +S035,M009,10,175.38,0 +S035,M009,11,151.02,0 +S035,M009,12,193.67,0 +S036,M009,1,10.65,0 +S036,M009,2,15.8,0 +S036,M009,3,15.6,0 +S036,M009,4,21.21,0 +S036,M009,5,15.36,0 +S036,M009,6,11.01,0 +S036,M009,7,13.62,0 +S036,M009,8,19.86,0 +S036,M009,9,11.55,0 +S036,M009,10,18.73,0 +S036,M009,11,21.35,0 +S036,M009,12,12.0,0 +S037,M010,1,1.03,0 +S037,M010,2,1.01,0 +S037,M010,3,1.46,0 +S037,M010,4,1.75,0 +S037,M010,5,1.53,0 +S037,M010,6,1.97,0 +S037,M010,7,1.22,0 +S037,M010,8,0.77,0 +S037,M010,9,0.7,0 +S037,M010,10,1.57,0 +S037,M010,11,1.92,0 +S037,M010,12,0.56,0 +S038,M010,1,44.03,0 +S038,M010,2,55.82,0 +S038,M010,3,57.72,0 +S038,M010,4,46.31,0 +S038,M010,5,46.2,0 +S038,M010,6,47.96,0 +S038,M010,7,56.31,0 +S038,M010,8,49.47,0 +S038,M010,9,40.73,0 +S038,M010,10,52.94,0 +S038,M010,11,45.72,0 +S038,M010,12,57.24,0 +S039,M010,1,152.39,0 +S039,M010,2,182.79,0 +S039,M010,3,163.66,0 +S039,M010,4,189.97,0 +S039,M010,5,171.85,0 +S039,M010,6,170.0,0 +S039,M010,7,171.99,0 +S039,M010,8,197.82,0 +S039,M010,9,166.95,0 +S039,M010,10,159.34,0 +S039,M010,11,184.6,0 +S039,M010,12,168.37,0 +S040,M010,1,21.96,0 +S040,M010,2,14.0,0 +S040,M010,3,11.24,0 +S040,M010,4,16.37,0 +S040,M010,5,16.46,0 +S040,M010,6,24.93,0 +S040,M010,7,12.71,0 +S040,M010,8,14.41,0 +S040,M010,9,11.54,0 +S040,M010,10,14.92,0 +S040,M010,11,14.43,0 +S040,M010,12,16.69,0 +S041,M011,1,1.58,0 +S041,M011,2,0.88,0 +S041,M011,3,1.57,0 +S041,M011,4,1.91,0 +S041,M011,5,2.82,0 +S041,M011,6,3.67,0 +S041,M011,7,3.61,0 +S041,M011,8,3.77,0 +S041,M011,9,4.85,0 +S041,M011,10,5.9,0 +S041,M011,11,4.56,0 +S041,M011,12,7.01,1 +S042,M011,1,48.12,0 +S042,M011,2,51.29,0 +S042,M011,3,57.81,0 +S042,M011,4,48.78,0 +S042,M011,5,49.36,0 +S042,M011,6,67.93,0 +S042,M011,7,68.31,0 +S042,M011,8,67.96,0 +S042,M011,9,66.94,0 +S042,M011,10,73.45,0 +S042,M011,11,84.78,0 +S042,M011,12,85.43,1 +S043,M011,1,197.47,0 +S043,M011,2,150.22,0 +S043,M011,3,153.44,0 +S043,M011,4,163.45,0 +S043,M011,5,155.14,0 +S043,M011,6,152.79,0 +S043,M011,7,132.77,0 +S043,M011,8,129.41,0 +S043,M011,9,130.51,0 +S043,M011,10,121.74,0 +S043,M011,11,113.35,1 +S043,M011,12,119.21,0 +S044,M011,1,10.39,0 +S044,M011,2,13.48,0 +S044,M011,3,19.7,0 +S044,M011,4,27.26,0 +S044,M011,5,17.25,0 +S044,M011,6,29.29,0 +S044,M011,7,30.63,0 +S044,M011,8,25.91,0 +S044,M011,9,34.68,0 +S044,M011,10,38.91,0 +S044,M011,11,46.65,1 +S044,M011,12,38.0,0 +S045,M012,1,0.7,0 +S045,M012,2,0.61,0 +S045,M012,3,0.76,0 +S045,M012,4,1.57,0 +S045,M012,5,2.0,0 +S045,M012,6,1.72,0 +S045,M012,7,1.95,0 +S045,M012,8,1.98,0 +S045,M012,9,1.5,0 +S045,M012,10,1.56,0 +S045,M012,11,1.32,0 +S045,M012,12,0.94,0 +S046,M012,1,46.0,0 +S046,M012,2,43.15,0 +S046,M012,3,54.19,0 +S046,M012,4,54.0,0 +S046,M012,5,43.48,0 +S046,M012,6,47.23,0 +S046,M012,7,44.48,0 +S046,M012,8,59.7,0 +S046,M012,9,55.84,0 +S046,M012,10,44.73,0 +S046,M012,11,49.88,0 +S046,M012,12,47.22,0 +S047,M012,1,178.61,0 +S047,M012,2,173.37,0 +S047,M012,3,199.15,0 +S047,M012,4,156.49,0 +S047,M012,5,194.21,0 +S047,M012,6,153.29,0 +S047,M012,7,169.91,0 +S047,M012,8,185.85,0 +S047,M012,9,173.93,0 +S047,M012,10,170.54,0 +S047,M012,11,170.47,0 +S047,M012,12,178.75,0 +S048,M012,1,11.88,0 +S048,M012,2,14.76,0 +S048,M012,3,11.11,0 +S048,M012,4,16.99,0 +S048,M012,5,17.76,0 +S048,M012,6,11.93,0 +S048,M012,7,22.44,0 +S048,M012,8,18.27,0 +S048,M012,9,18.82,0 +S048,M012,10,21.52,0 +S048,M012,11,11.94,0 +S048,M012,12,17.54,0 +S049,M013,1,1.8,0 +S049,M013,2,1.75,0 +S049,M013,3,1.28,0 +S049,M013,4,0.96,0 +S049,M013,5,0.74,0 +S049,M013,6,1.9,0 +S049,M013,7,0.84,0 +S049,M013,8,1.92,0 +S049,M013,9,1.28,0 +S049,M013,10,0.93,0 +S049,M013,11,0.62,0 +S049,M013,12,0.79,0 +S050,M013,1,59.13,0 +S050,M013,2,45.49,0 +S050,M013,3,52.5,0 +S050,M013,4,52.29,0 +S050,M013,5,41.87,0 +S050,M013,6,52.82,0 +S050,M013,7,51.84,0 +S050,M013,8,51.61,0 +S050,M013,9,43.42,0 +S050,M013,10,52.49,0 +S050,M013,11,58.04,0 +S050,M013,12,46.75,0 +S051,M013,1,196.2,0 +S051,M013,2,152.06,0 +S051,M013,3,193.19,0 +S051,M013,4,154.06,0 +S051,M013,5,197.43,0 +S051,M013,6,188.55,0 +S051,M013,7,163.23,0 +S051,M013,8,160.54,0 +S051,M013,9,178.61,0 +S051,M013,10,180.91,0 +S051,M013,11,151.51,0 +S051,M013,12,194.45,0 +S052,M013,1,18.18,0 +S052,M013,2,19.63,0 +S052,M013,3,14.53,0 +S052,M013,4,13.67,0 +S052,M013,5,22.11,0 +S052,M013,6,16.09,0 +S052,M013,7,16.8,0 +S052,M013,8,20.33,0 +S052,M013,9,12.37,0 +S052,M013,10,16.23,0 +S052,M013,11,16.97,0 +S052,M013,12,15.1,0 +S053,M014,1,0.72,0 +S053,M014,2,1.79,0 +S053,M014,3,0.98,0 +S053,M014,4,1.98,0 +S053,M014,5,1.02,0 +S053,M014,6,1.1,0 +S053,M014,7,1.64,0 +S053,M014,8,1.27,0 +S053,M014,9,0.66,0 +S053,M014,10,0.86,0 +S053,M014,11,0.68,0 +S053,M014,12,1.17,0 +S054,M014,1,42.82,0 +S054,M014,2,41.01,0 +S054,M014,3,58.91,0 +S054,M014,4,57.28,0 +S054,M014,5,48.36,0 +S054,M014,6,59.25,0 +S054,M014,7,58.05,0 +S054,M014,8,56.27,0 +S054,M014,9,58.68,0 +S054,M014,10,54.42,0 +S054,M014,11,43.8,0 +S054,M014,12,43.19,0 +S055,M014,1,199.8,0 +S055,M014,2,173.34,0 +S055,M014,3,193.95,0 +S055,M014,4,174.87,0 +S055,M014,5,154.5,0 +S055,M014,6,169.66,0 +S055,M014,7,172.86,0 +S055,M014,8,162.04,0 +S055,M014,9,179.17,0 +S055,M014,10,152.43,0 +S055,M014,11,164.07,0 +S055,M014,12,179.88,0 +S056,M014,1,22.64,0 +S056,M014,2,20.09,0 +S056,M014,3,14.29,0 +S056,M014,4,10.12,0 +S056,M014,5,11.61,0 +S056,M014,6,12.01,0 +S056,M014,7,13.97,0 +S056,M014,8,15.49,0 +S056,M014,9,16.05,0 +S056,M014,10,10.68,0 +S056,M014,11,10.77,0 +S056,M014,12,18.43,0 +S057,M015,1,1.04,0 +S057,M015,2,0.93,0 +S057,M015,3,1.08,0 +S057,M015,4,1.18,0 +S057,M015,5,1.32,0 +S057,M015,6,1.74,0 +S057,M015,7,1.43,0 +S057,M015,8,1.42,0 +S057,M015,9,0.69,0 +S057,M015,10,0.65,0 +S057,M015,11,1.06,0 +S057,M015,12,1.98,0 +S058,M015,1,51.16,0 +S058,M015,2,47.31,0 +S058,M015,3,42.89,0 +S058,M015,4,52.05,0 +S058,M015,5,48.03,0 +S058,M015,6,40.81,0 +S058,M015,7,40.78,0 +S058,M015,8,54.27,0 +S058,M015,9,56.08,0 +S058,M015,10,50.39,0 +S058,M015,11,42.98,0 +S058,M015,12,57.86,0 +S059,M015,1,156.99,0 +S059,M015,2,196.57,0 +S059,M015,3,165.93,0 +S059,M015,4,169.65,0 +S059,M015,5,180.83,0 +S059,M015,6,192.13,0 +S059,M015,7,179.69,0 +S059,M015,8,175.37,0 +S059,M015,9,175.49,0 +S059,M015,10,174.49,0 +S059,M015,11,178.15,0 +S059,M015,12,173.74,0 +S060,M015,1,10.25,0 +S060,M015,2,14.97,0 +S060,M015,3,11.64,0 +S060,M015,4,16.25,0 +S060,M015,5,14.62,0 +S060,M015,6,23.48,0 +S060,M015,7,23.08,0 +S060,M015,8,19.44,0 +S060,M015,9,18.95,0 +S060,M015,10,13.98,0 +S060,M015,11,19.83,0 +S060,M015,12,24.58,0 +S061,M016,1,1.37,0 +S061,M016,2,0.97,0 +S061,M016,3,0.93,0 +S061,M016,4,1.96,0 +S061,M016,5,2.45,0 +S061,M016,6,3.29,0 +S061,M016,7,3.94,0 +S061,M016,8,3.64,0 +S061,M016,9,5.29,0 +S061,M016,10,4.46,0 +S061,M016,11,6.0,0 +S061,M016,12,7.1,1 +S062,M016,1,43.82,0 +S062,M016,2,49.86,0 +S062,M016,3,51.31,0 +S062,M016,4,53.66,0 +S062,M016,5,52.63,0 +S062,M016,6,61.53,0 +S062,M016,7,67.11,0 +S062,M016,8,68.54,0 +S062,M016,9,79.15,0 +S062,M016,10,81.46,0 +S062,M016,11,85.73,1 +S062,M016,12,87.78,1 +S063,M016,1,178.04,0 +S063,M016,2,197.71,0 +S063,M016,3,168.93,0 +S063,M016,4,160.11,0 +S063,M016,5,180.58,0 +S063,M016,6,153.43,0 +S063,M016,7,150.07,0 +S063,M016,8,143.61,0 +S063,M016,9,140.21,0 +S063,M016,10,137.12,0 +S063,M016,11,123.0,0 +S063,M016,12,116.07,0 +S064,M016,1,13.97,0 +S064,M016,2,17.95,0 +S064,M016,3,26.3,0 +S064,M016,4,17.75,0 +S064,M016,5,19.96,0 +S064,M016,6,31.78,0 +S064,M016,7,34.81,0 +S064,M016,8,34.26,0 +S064,M016,9,36.08,0 +S064,M016,10,38.86,0 +S064,M016,11,39.0,0 +S064,M016,12,42.2,1 +S065,M017,1,1.34,0 +S065,M017,2,1.9,0 +S065,M017,3,1.58,0 +S065,M017,4,0.93,0 +S065,M017,5,1.36,0 +S065,M017,6,1.31,0 +S065,M017,7,1.84,0 +S065,M017,8,1.09,0 +S065,M017,9,1.02,0 +S065,M017,10,1.66,0 +S065,M017,11,0.72,0 +S065,M017,12,0.56,0 +S066,M017,1,59.11,0 +S066,M017,2,41.58,0 +S066,M017,3,58.48,0 +S066,M017,4,53.12,0 +S066,M017,5,54.95,0 +S066,M017,6,44.28,0 +S066,M017,7,56.56,0 +S066,M017,8,45.42,0 +S066,M017,9,51.24,0 +S066,M017,10,45.43,0 +S066,M017,11,42.18,0 +S066,M017,12,54.82,0 +S067,M017,1,161.96,0 +S067,M017,2,152.54,0 +S067,M017,3,195.26,0 +S067,M017,4,161.28,0 +S067,M017,5,161.64,0 +S067,M017,6,155.01,0 +S067,M017,7,166.51,0 +S067,M017,8,173.62,0 +S067,M017,9,184.06,0 +S067,M017,10,156.87,0 +S067,M017,11,198.23,0 +S067,M017,12,198.9,0 +S068,M017,1,12.37,0 +S068,M017,2,19.79,0 +S068,M017,3,23.21,0 +S068,M017,4,17.14,0 +S068,M017,5,19.76,0 +S068,M017,6,21.34,0 +S068,M017,7,14.31,0 +S068,M017,8,14.28,0 +S068,M017,9,10.89,0 +S068,M017,10,21.53,0 +S068,M017,11,19.78,0 +S068,M017,12,13.48,0 +S069,M018,1,1.61,0 +S069,M018,2,1.78,0 +S069,M018,3,0.56,0 +S069,M018,4,1.88,0 +S069,M018,5,1.13,0 +S069,M018,6,1.76,0 +S069,M018,7,1.9,0 +S069,M018,8,0.55,0 +S069,M018,9,1.75,0 +S069,M018,10,1.68,0 +S069,M018,11,0.78,0 +S069,M018,12,1.62,0 +S070,M018,1,57.45,0 +S070,M018,2,45.79,0 +S070,M018,3,40.75,0 +S070,M018,4,45.97,0 +S070,M018,5,52.06,0 +S070,M018,6,58.57,0 +S070,M018,7,46.7,0 +S070,M018,8,49.09,0 +S070,M018,9,52.83,0 +S070,M018,10,50.49,0 +S070,M018,11,57.79,0 +S070,M018,12,42.68,0 +S071,M018,1,175.21,0 +S071,M018,2,163.64,0 +S071,M018,3,190.44,0 +S071,M018,4,166.53,0 +S071,M018,5,186.56,0 +S071,M018,6,182.41,0 +S071,M018,7,185.93,0 +S071,M018,8,150.7,0 +S071,M018,9,166.86,0 +S071,M018,10,164.75,0 +S071,M018,11,183.72,0 +S071,M018,12,159.66,0 +S072,M018,1,19.16,0 +S072,M018,2,19.58,0 +S072,M018,3,16.08,0 +S072,M018,4,16.42,0 +S072,M018,5,14.92,0 +S072,M018,6,16.01,0 +S072,M018,7,11.43,0 +S072,M018,8,24.73,0 +S072,M018,9,17.16,0 +S072,M018,10,24.04,0 +S072,M018,11,10.1,0 +S072,M018,12,15.75,0 +S073,M019,1,1.17,0 +S073,M019,2,0.9,0 +S073,M019,3,0.99,0 +S073,M019,4,1.37,0 +S073,M019,5,1.36,0 +S073,M019,6,0.89,0 +S073,M019,7,1.04,0 +S073,M019,8,0.85,0 +S073,M019,9,1.5,0 +S073,M019,10,1.2,0 +S073,M019,11,0.74,0 +S073,M019,12,1.53,0 +S074,M019,1,58.46,0 +S074,M019,2,57.76,0 +S074,M019,3,55.4,0 +S074,M019,4,53.81,0 +S074,M019,5,52.77,0 +S074,M019,6,58.81,0 +S074,M019,7,47.35,0 +S074,M019,8,44.42,0 +S074,M019,9,59.15,0 +S074,M019,10,49.56,0 +S074,M019,11,49.1,0 +S074,M019,12,59.78,0 +S075,M019,1,168.68,0 +S075,M019,2,185.1,0 +S075,M019,3,195.64,0 +S075,M019,4,187.87,0 +S075,M019,5,190.1,0 +S075,M019,6,191.28,0 +S075,M019,7,188.09,0 +S075,M019,8,184.02,0 +S075,M019,9,179.79,0 +S075,M019,10,189.37,0 +S075,M019,11,176.28,0 +S075,M019,12,176.41,0 +S076,M019,1,15.43,0 +S076,M019,2,23.76,0 +S076,M019,3,11.68,0 +S076,M019,4,22.43,0 +S076,M019,5,23.54,0 +S076,M019,6,13.03,0 +S076,M019,7,18.05,0 +S076,M019,8,12.47,0 +S076,M019,9,22.82,0 +S076,M019,10,16.63,0 +S076,M019,11,20.2,0 +S076,M019,12,20.75,0 +S077,M020,1,1.23,0 +S077,M020,2,1.85,0 +S077,M020,3,1.17,0 +S077,M020,4,0.58,0 +S077,M020,5,0.7,0 +S077,M020,6,1.12,0 +S077,M020,7,1.35,0 +S077,M020,8,1.34,0 +S077,M020,9,1.51,0 +S077,M020,10,0.96,0 +S077,M020,11,0.53,0 +S077,M020,12,0.88,0 +S078,M020,1,40.06,0 +S078,M020,2,44.36,0 +S078,M020,3,41.46,0 +S078,M020,4,48.47,0 +S078,M020,5,54.01,0 +S078,M020,6,50.82,0 +S078,M020,7,41.21,0 +S078,M020,8,57.88,0 +S078,M020,9,59.72,0 +S078,M020,10,49.44,0 +S078,M020,11,45.74,0 +S078,M020,12,43.6,0 +S079,M020,1,156.77,0 +S079,M020,2,182.08,0 +S079,M020,3,186.46,0 +S079,M020,4,198.19,0 +S079,M020,5,168.72,0 +S079,M020,6,194.57,0 +S079,M020,7,172.43,0 +S079,M020,8,195.86,0 +S079,M020,9,168.87,0 +S079,M020,10,154.01,0 +S079,M020,11,198.21,0 +S079,M020,12,193.73,0 +S080,M020,1,11.99,0 +S080,M020,2,22.9,0 +S080,M020,3,11.78,0 +S080,M020,4,24.89,0 +S080,M020,5,15.9,0 +S080,M020,6,11.91,0 +S080,M020,7,24.68,0 +S080,M020,8,22.52,0 +S080,M020,9,21.34,0 +S080,M020,10,24.68,0 +S080,M020,11,16.95,0 +S080,M020,12,20.79,0 +S081,M021,1,1.14,0 +S081,M021,2,1.07,0 +S081,M021,3,0.84,0 +S081,M021,4,1.19,0 +S081,M021,5,0.73,0 +S081,M021,6,0.78,0 +S081,M021,7,1.81,0 +S081,M021,8,1.65,0 +S081,M021,9,0.55,0 +S081,M021,10,1.49,0 +S081,M021,11,1.42,0 +S081,M021,12,1.9,0 +S082,M021,1,57.78,0 +S082,M021,2,44.81,0 +S082,M021,3,41.35,0 +S082,M021,4,51.88,0 +S082,M021,5,48.93,0 +S082,M021,6,53.44,0 +S082,M021,7,57.44,0 +S082,M021,8,44.89,0 +S082,M021,9,40.88,0 +S082,M021,10,48.78,0 +S082,M021,11,50.8,0 +S082,M021,12,55.09,0 +S083,M021,1,188.8,0 +S083,M021,2,157.01,0 +S083,M021,3,199.67,0 +S083,M021,4,161.71,0 +S083,M021,5,195.82,0 +S083,M021,6,178.81,0 +S083,M021,7,178.87,0 +S083,M021,8,188.63,0 +S083,M021,9,190.92,0 +S083,M021,10,161.85,0 +S083,M021,11,193.81,0 +S083,M021,12,199.28,0 +S084,M021,1,17.82,0 +S084,M021,2,16.2,0 +S084,M021,3,14.13,0 +S084,M021,4,18.35,0 +S084,M021,5,18.88,0 +S084,M021,6,21.03,0 +S084,M021,7,12.63,0 +S084,M021,8,20.18,0 +S084,M021,9,18.34,0 +S084,M021,10,10.7,0 +S084,M021,11,15.17,0 +S084,M021,12,19.65,0 +S085,M022,1,1.07,0 +S085,M022,2,1.29,0 +S085,M022,3,1.54,0 +S085,M022,4,1.11,0 +S085,M022,5,0.95,0 +S085,M022,6,0.78,0 +S085,M022,7,1.31,0 +S085,M022,8,0.86,0 +S085,M022,9,0.84,0 +S085,M022,10,1.79,0 +S085,M022,11,0.72,0 +S085,M022,12,1.19,0 +S086,M022,1,41.15,0 +S086,M022,2,48.25,0 +S086,M022,3,48.33,0 +S086,M022,4,50.6,0 +S086,M022,5,47.77,0 +S086,M022,6,45.1,0 +S086,M022,7,46.61,0 +S086,M022,8,41.58,0 +S086,M022,9,49.0,0 +S086,M022,10,47.42,0 +S086,M022,11,50.71,0 +S086,M022,12,56.59,0 +S087,M022,1,152.58,0 +S087,M022,2,168.86,0 +S087,M022,3,180.25,0 +S087,M022,4,151.98,0 +S087,M022,5,153.64,0 +S087,M022,6,190.23,0 +S087,M022,7,179.38,0 +S087,M022,8,183.57,0 +S087,M022,9,160.85,0 +S087,M022,10,160.44,0 +S087,M022,11,195.35,0 +S087,M022,12,165.16,0 +S088,M022,1,23.63,0 +S088,M022,2,13.18,0 +S088,M022,3,12.85,0 +S088,M022,4,21.12,0 +S088,M022,5,21.2,0 +S088,M022,6,21.31,0 +S088,M022,7,23.72,0 +S088,M022,8,20.43,0 +S088,M022,9,20.56,0 +S088,M022,10,15.95,0 +S088,M022,11,13.59,0 +S088,M022,12,14.82,0 +S089,M023,1,0.92,0 +S089,M023,2,1.2,0 +S089,M023,3,1.47,0 +S089,M023,4,0.96,0 +S089,M023,5,1.04,0 +S089,M023,6,1.82,0 +S089,M023,7,1.2,0 +S089,M023,8,1.7,0 +S089,M023,9,1.58,0 +S089,M023,10,1.64,0 +S089,M023,11,0.98,0 +S089,M023,12,0.8,0 +S090,M023,1,46.26,0 +S090,M023,2,40.91,0 +S090,M023,3,57.28,0 +S090,M023,4,54.83,0 +S090,M023,5,40.32,0 +S090,M023,6,51.07,0 +S090,M023,7,51.68,0 +S090,M023,8,54.4,0 +S090,M023,9,45.9,0 +S090,M023,10,43.92,0 +S090,M023,11,44.6,0 +S090,M023,12,51.4,0 +S091,M023,1,162.29,0 +S091,M023,2,177.53,0 +S091,M023,3,197.17,0 +S091,M023,4,166.88,0 +S091,M023,5,188.17,0 +S091,M023,6,174.43,0 +S091,M023,7,186.18,0 +S091,M023,8,186.98,0 +S091,M023,9,173.04,0 +S091,M023,10,186.64,0 +S091,M023,11,167.61,0 +S091,M023,12,157.0,0 +S092,M023,1,18.2,0 +S092,M023,2,12.76,0 +S092,M023,3,23.34,0 +S092,M023,4,24.85,0 +S092,M023,5,24.36,0 +S092,M023,6,17.86,0 +S092,M023,7,22.59,0 +S092,M023,8,11.11,0 +S092,M023,9,22.27,0 +S092,M023,10,10.73,0 +S092,M023,11,10.1,0 +S092,M023,12,12.06,0 +S093,M024,1,1.45,0 +S093,M024,2,0.6,0 +S093,M024,3,0.73,0 +S093,M024,4,0.83,0 +S093,M024,5,1.18,0 +S093,M024,6,0.59,0 +S093,M024,7,1.46,0 +S093,M024,8,1.9,0 +S093,M024,9,1.87,0 +S093,M024,10,1.49,0 +S093,M024,11,0.52,0 +S093,M024,12,1.3,0 +S094,M024,1,48.23,0 +S094,M024,2,40.33,0 +S094,M024,3,54.44,0 +S094,M024,4,50.71,0 +S094,M024,5,45.73,0 +S094,M024,6,50.04,0 +S094,M024,7,53.96,0 +S094,M024,8,48.61,0 +S094,M024,9,58.79,0 +S094,M024,10,43.59,0 +S094,M024,11,58.36,0 +S094,M024,12,50.49,0 +S095,M024,1,162.05,0 +S095,M024,2,180.99,0 +S095,M024,3,162.62,0 +S095,M024,4,167.72,0 +S095,M024,5,189.75,0 +S095,M024,6,153.95,0 +S095,M024,7,197.96,0 +S095,M024,8,172.95,0 +S095,M024,9,162.16,0 +S095,M024,10,184.86,0 +S095,M024,11,165.0,0 +S095,M024,12,191.4,0 +S096,M024,1,11.21,0 +S096,M024,2,24.88,0 +S096,M024,3,19.75,0 +S096,M024,4,10.48,0 +S096,M024,5,16.08,0 +S096,M024,6,15.66,0 +S096,M024,7,17.14,0 +S096,M024,8,19.55,0 +S096,M024,9,20.54,0 +S096,M024,10,11.24,0 +S096,M024,11,22.68,0 +S096,M024,12,19.67,0 +S097,M025,1,1.0,0 +S097,M025,2,1.97,0 +S097,M025,3,1.96,0 +S097,M025,4,1.84,0 +S097,M025,5,1.88,0 +S097,M025,6,0.84,0 +S097,M025,7,1.85,0 +S097,M025,8,1.54,0 +S097,M025,9,1.35,0 +S097,M025,10,0.91,0 +S097,M025,11,1.89,0 +S097,M025,12,1.53,0 +S098,M025,1,45.61,0 +S098,M025,2,50.86,0 +S098,M025,3,53.16,0 +S098,M025,4,43.59,0 +S098,M025,5,57.01,0 +S098,M025,6,40.31,0 +S098,M025,7,44.14,0 +S098,M025,8,42.88,0 +S098,M025,9,56.41,0 +S098,M025,10,56.42,0 +S098,M025,11,58.58,0 +S098,M025,12,45.99,0 +S099,M025,1,178.2,0 +S099,M025,2,178.33,0 +S099,M025,3,192.47,0 +S099,M025,4,183.85,0 +S099,M025,5,177.03,0 +S099,M025,6,169.19,0 +S099,M025,7,186.99,0 +S099,M025,8,189.52,0 +S099,M025,9,184.47,0 +S099,M025,10,153.92,0 +S099,M025,11,187.27,0 +S099,M025,12,167.29,0 +S100,M025,1,11.47,0 +S100,M025,2,13.48,0 +S100,M025,3,11.14,0 +S100,M025,4,19.08,0 +S100,M025,5,19.21,0 +S100,M025,6,18.93,0 +S100,M025,7,24.53,0 +S100,M025,8,14.9,0 +S100,M025,9,19.51,0 +S100,M025,10,21.75,0 +S100,M025,11,16.76,0 +S100,M025,12,13.42,0 +S101,M026,1,1.33,0 +S101,M026,2,1.06,0 +S101,M026,3,0.78,0 +S101,M026,4,1.38,0 +S101,M026,5,1.07,0 +S101,M026,6,1.66,0 +S101,M026,7,0.94,0 +S101,M026,8,0.87,0 +S101,M026,9,0.61,0 +S101,M026,10,0.9,0 +S101,M026,11,1.07,0 +S101,M026,12,1.68,0 +S102,M026,1,43.09,0 +S102,M026,2,47.79,0 +S102,M026,3,47.21,0 +S102,M026,4,41.83,0 +S102,M026,5,58.86,0 +S102,M026,6,48.89,0 +S102,M026,7,55.23,0 +S102,M026,8,42.04,0 +S102,M026,9,41.75,0 +S102,M026,10,48.62,0 +S102,M026,11,51.14,0 +S102,M026,12,50.25,0 +S103,M026,1,155.16,0 +S103,M026,2,154.48,0 +S103,M026,3,177.71,0 +S103,M026,4,179.85,0 +S103,M026,5,189.22,0 +S103,M026,6,193.19,0 +S103,M026,7,150.62,0 +S103,M026,8,170.69,0 +S103,M026,9,188.96,0 +S103,M026,10,186.24,0 +S103,M026,11,177.98,0 +S103,M026,12,199.64,0 +S104,M026,1,13.64,0 +S104,M026,2,17.82,0 +S104,M026,3,20.28,0 +S104,M026,4,12.54,0 +S104,M026,5,14.03,0 +S104,M026,6,14.03,0 +S104,M026,7,12.19,0 +S104,M026,8,12.51,0 +S104,M026,9,14.14,0 +S104,M026,10,14.5,0 +S104,M026,11,21.74,0 +S104,M026,12,15.42,0 +S105,M027,1,1.92,0 +S105,M027,2,1.58,0 +S105,M027,3,1.24,0 +S105,M027,4,1.43,0 +S105,M027,5,1.19,0 +S105,M027,6,0.7,0 +S105,M027,7,0.51,0 +S105,M027,8,1.09,0 +S105,M027,9,1.76,0 +S105,M027,10,1.07,0 +S105,M027,11,1.79,0 +S105,M027,12,1.16,0 +S106,M027,1,48.61,0 +S106,M027,2,56.47,0 +S106,M027,3,53.02,0 +S106,M027,4,51.89,0 +S106,M027,5,46.01,0 +S106,M027,6,52.06,0 +S106,M027,7,53.91,0 +S106,M027,8,49.58,0 +S106,M027,9,43.55,0 +S106,M027,10,47.96,0 +S106,M027,11,46.38,0 +S106,M027,12,54.7,0 +S107,M027,1,181.65,0 +S107,M027,2,198.39,0 +S107,M027,3,189.1,0 +S107,M027,4,166.29,0 +S107,M027,5,164.02,0 +S107,M027,6,189.29,0 +S107,M027,7,168.25,0 +S107,M027,8,196.6,0 +S107,M027,9,159.99,0 +S107,M027,10,199.86,0 +S107,M027,11,159.0,0 +S107,M027,12,188.38,0 +S108,M027,1,10.4,0 +S108,M027,2,20.91,0 +S108,M027,3,24.14,0 +S108,M027,4,13.14,0 +S108,M027,5,21.97,0 +S108,M027,6,22.47,0 +S108,M027,7,11.04,0 +S108,M027,8,22.72,0 +S108,M027,9,21.93,0 +S108,M027,10,20.09,0 +S108,M027,11,12.09,0 +S108,M027,12,14.54,0 +S109,M028,1,0.88,0 +S109,M028,2,0.73,0 +S109,M028,3,1.13,0 +S109,M028,4,1.07,0 +S109,M028,5,1.17,0 +S109,M028,6,1.4,0 +S109,M028,7,1.37,0 +S109,M028,8,1.3,0 +S109,M028,9,1.89,0 +S109,M028,10,1.8,0 +S109,M028,11,1.13,0 +S109,M028,12,0.56,0 +S110,M028,1,59.37,0 +S110,M028,2,54.03,0 +S110,M028,3,56.25,0 +S110,M028,4,51.88,0 +S110,M028,5,58.29,0 +S110,M028,6,42.19,0 +S110,M028,7,44.96,0 +S110,M028,8,53.29,0 +S110,M028,9,43.33,0 +S110,M028,10,52.25,0 +S110,M028,11,51.26,0 +S110,M028,12,52.9,0 +S111,M028,1,182.34,0 +S111,M028,2,198.34,0 +S111,M028,3,166.58,0 +S111,M028,4,189.81,0 +S111,M028,5,155.3,0 +S111,M028,6,154.93,0 +S111,M028,7,161.1,0 +S111,M028,8,187.34,0 +S111,M028,9,177.9,0 +S111,M028,10,184.38,0 +S111,M028,11,161.05,0 +S111,M028,12,168.71,0 +S112,M028,1,16.95,0 +S112,M028,2,20.19,0 +S112,M028,3,20.36,0 +S112,M028,4,12.26,0 +S112,M028,5,24.97,0 +S112,M028,6,17.24,0 +S112,M028,7,14.43,0 +S112,M028,8,23.81,0 +S112,M028,9,11.29,0 +S112,M028,10,22.47,0 +S112,M028,11,20.95,0 +S112,M028,12,15.22,0 +S113,M029,1,1.01,0 +S113,M029,2,1.68,0 +S113,M029,3,1.28,0 +S113,M029,4,1.02,0 +S113,M029,5,0.65,0 +S113,M029,6,1.58,0 +S113,M029,7,1.18,0 +S113,M029,8,0.6,0 +S113,M029,9,0.57,0 +S113,M029,10,1.57,0 +S113,M029,11,0.53,0 +S113,M029,12,1.01,0 +S114,M029,1,42.19,0 +S114,M029,2,56.0,0 +S114,M029,3,58.6,0 +S114,M029,4,54.81,0 +S114,M029,5,50.32,0 +S114,M029,6,51.04,0 +S114,M029,7,46.62,0 +S114,M029,8,48.53,0 +S114,M029,9,49.24,0 +S114,M029,10,55.96,0 +S114,M029,11,52.6,0 +S114,M029,12,43.73,0 +S115,M029,1,182.87,0 +S115,M029,2,169.78,0 +S115,M029,3,186.87,0 +S115,M029,4,194.15,0 +S115,M029,5,172.48,0 +S115,M029,6,171.48,0 +S115,M029,7,150.22,0 +S115,M029,8,160.74,0 +S115,M029,9,163.92,0 +S115,M029,10,185.19,0 +S115,M029,11,189.8,0 +S115,M029,12,178.87,0 +S116,M029,1,21.99,0 +S116,M029,2,18.05,0 +S116,M029,3,15.49,0 +S116,M029,4,12.96,0 +S116,M029,5,11.71,0 +S116,M029,6,20.06,0 +S116,M029,7,17.34,0 +S116,M029,8,24.49,0 +S116,M029,9,14.7,0 +S116,M029,10,15.83,0 +S116,M029,11,15.89,0 +S116,M029,12,11.71,0 +S117,M030,1,1.03,0 +S117,M030,2,1.19,0 +S117,M030,3,0.76,0 +S117,M030,4,1.51,0 +S117,M030,5,1.7,0 +S117,M030,6,1.88,0 +S117,M030,7,1.39,0 +S117,M030,8,1.51,0 +S117,M030,9,0.97,0 +S117,M030,10,0.86,0 +S117,M030,11,0.68,0 +S117,M030,12,1.06,0 +S118,M030,1,42.83,0 +S118,M030,2,47.77,0 +S118,M030,3,51.93,0 +S118,M030,4,51.5,0 +S118,M030,5,48.46,0 +S118,M030,6,49.73,0 +S118,M030,7,50.75,0 +S118,M030,8,58.38,0 +S118,M030,9,58.61,0 +S118,M030,10,43.46,0 +S118,M030,11,43.34,0 +S118,M030,12,45.78,0 +S119,M030,1,159.35,0 +S119,M030,2,192.1,0 +S119,M030,3,180.97,0 +S119,M030,4,193.28,0 +S119,M030,5,197.59,0 +S119,M030,6,174.23,0 +S119,M030,7,159.76,0 +S119,M030,8,169.2,0 +S119,M030,9,178.08,0 +S119,M030,10,182.65,0 +S119,M030,11,170.46,0 +S119,M030,12,174.59,0 +S120,M030,1,20.65,0 +S120,M030,2,16.66,0 +S120,M030,3,12.51,0 +S120,M030,4,18.48,0 +S120,M030,5,21.94,0 +S120,M030,6,13.31,0 +S120,M030,7,10.49,0 +S120,M030,8,13.37,0 +S120,M030,9,14.33,0 +S120,M030,10,21.71,0 +S120,M030,11,18.48,0 +S120,M030,12,21.59,0 +S121,M031,1,1.24,0 +S121,M031,2,1.27,0 +S121,M031,3,1.36,0 +S121,M031,4,0.9,0 +S121,M031,5,1.31,0 +S121,M031,6,1.99,0 +S121,M031,7,1.31,0 +S121,M031,8,1.64,0 +S121,M031,9,1.64,0 +S121,M031,10,1.16,0 +S121,M031,11,0.83,0 +S121,M031,12,1.14,0 +S122,M031,1,54.72,0 +S122,M031,2,53.07,0 +S122,M031,3,44.95,0 +S122,M031,4,57.5,0 +S122,M031,5,40.64,0 +S122,M031,6,45.26,0 +S122,M031,7,57.24,0 +S122,M031,8,41.74,0 +S122,M031,9,57.31,0 +S122,M031,10,42.35,0 +S122,M031,11,56.24,0 +S122,M031,12,44.2,0 +S123,M031,1,186.88,0 +S123,M031,2,184.46,0 +S123,M031,3,193.05,0 +S123,M031,4,180.51,0 +S123,M031,5,171.41,0 +S123,M031,6,180.81,0 +S123,M031,7,189.24,0 +S123,M031,8,190.68,0 +S123,M031,9,190.27,0 +S123,M031,10,178.5,0 +S123,M031,11,176.61,0 +S123,M031,12,179.84,0 +S124,M031,1,12.53,0 +S124,M031,2,24.69,0 +S124,M031,3,14.39,0 +S124,M031,4,18.59,0 +S124,M031,5,24.29,0 +S124,M031,6,17.72,0 +S124,M031,7,23.5,0 +S124,M031,8,22.23,0 +S124,M031,9,12.03,0 +S124,M031,10,21.25,0 +S124,M031,11,16.17,0 +S124,M031,12,10.89,0 +S125,M032,1,0.92,0 +S125,M032,2,1.48,0 +S125,M032,3,1.55,0 +S125,M032,4,0.7,0 +S125,M032,5,0.72,0 +S125,M032,6,0.98,0 +S125,M032,7,0.87,0 +S125,M032,8,1.77,0 +S125,M032,9,1.09,0 +S125,M032,10,0.71,0 +S125,M032,11,1.45,0 +S125,M032,12,1.44,0 +S126,M032,1,47.5,0 +S126,M032,2,41.51,0 +S126,M032,3,55.89,0 +S126,M032,4,55.38,0 +S126,M032,5,48.06,0 +S126,M032,6,55.0,0 +S126,M032,7,53.89,0 +S126,M032,8,57.55,0 +S126,M032,9,49.09,0 +S126,M032,10,49.76,0 +S126,M032,11,51.64,0 +S126,M032,12,57.38,0 +S127,M032,1,192.36,0 +S127,M032,2,154.66,0 +S127,M032,3,198.61,0 +S127,M032,4,181.74,0 +S127,M032,5,184.86,0 +S127,M032,6,192.51,0 +S127,M032,7,189.78,0 +S127,M032,8,169.89,0 +S127,M032,9,198.41,0 +S127,M032,10,191.24,0 +S127,M032,11,174.74,0 +S127,M032,12,166.14,0 +S128,M032,1,14.25,0 +S128,M032,2,18.21,0 +S128,M032,3,11.96,0 +S128,M032,4,22.79,0 +S128,M032,5,21.82,0 +S128,M032,6,23.41,0 +S128,M032,7,21.43,0 +S128,M032,8,10.69,0 +S128,M032,9,19.39,0 +S128,M032,10,22.55,0 +S128,M032,11,10.24,0 +S128,M032,12,23.94,0 +S129,M033,1,0.74,0 +S129,M033,2,1.18,0 +S129,M033,3,1.99,0 +S129,M033,4,1.14,0 +S129,M033,5,1.99,0 +S129,M033,6,1.86,0 +S129,M033,7,1.82,0 +S129,M033,8,0.95,0 +S129,M033,9,1.81,0 +S129,M033,10,1.81,0 +S129,M033,11,0.64,0 +S129,M033,12,1.04,0 +S130,M033,1,41.65,0 +S130,M033,2,53.29,0 +S130,M033,3,52.98,0 +S130,M033,4,41.04,0 +S130,M033,5,52.51,0 +S130,M033,6,54.52,0 +S130,M033,7,55.1,0 +S130,M033,8,48.58,0 +S130,M033,9,41.82,0 +S130,M033,10,41.9,0 +S130,M033,11,44.27,0 +S130,M033,12,41.56,0 +S131,M033,1,179.68,0 +S131,M033,2,175.97,0 +S131,M033,3,172.25,0 +S131,M033,4,150.4,0 +S131,M033,5,167.18,0 +S131,M033,6,156.03,0 +S131,M033,7,193.02,0 +S131,M033,8,184.7,0 +S131,M033,9,173.89,0 +S131,M033,10,161.45,0 +S131,M033,11,154.47,0 +S131,M033,12,190.3,0 +S132,M033,1,11.55,0 +S132,M033,2,15.51,0 +S132,M033,3,14.49,0 +S132,M033,4,15.74,0 +S132,M033,5,22.2,0 +S132,M033,6,23.12,0 +S132,M033,7,19.62,0 +S132,M033,8,20.16,0 +S132,M033,9,11.03,0 +S132,M033,10,18.45,0 +S132,M033,11,19.15,0 +S132,M033,12,12.46,0 +S133,M034,1,1.03,0 +S133,M034,2,0.82,0 +S133,M034,3,1.62,0 +S133,M034,4,1.88,0 +S133,M034,5,1.42,0 +S133,M034,6,1.16,0 +S133,M034,7,0.83,0 +S133,M034,8,1.84,0 +S133,M034,9,0.71,0 +S133,M034,10,0.68,0 +S133,M034,11,1.6,0 +S133,M034,12,1.03,0 +S134,M034,1,48.59,0 +S134,M034,2,55.82,0 +S134,M034,3,44.96,0 +S134,M034,4,52.89,0 +S134,M034,5,44.96,0 +S134,M034,6,52.02,0 +S134,M034,7,54.42,0 +S134,M034,8,45.7,0 +S134,M034,9,58.83,0 +S134,M034,10,40.54,0 +S134,M034,11,56.94,0 +S134,M034,12,57.52,0 +S135,M034,1,160.27,0 +S135,M034,2,180.35,0 +S135,M034,3,159.43,0 +S135,M034,4,169.59,0 +S135,M034,5,182.31,0 +S135,M034,6,189.19,0 +S135,M034,7,189.69,0 +S135,M034,8,161.97,0 +S135,M034,9,169.29,0 +S135,M034,10,162.05,0 +S135,M034,11,181.9,0 +S135,M034,12,153.55,0 +S136,M034,1,23.33,0 +S136,M034,2,10.18,0 +S136,M034,3,15.42,0 +S136,M034,4,14.75,0 +S136,M034,5,15.8,0 +S136,M034,6,22.58,0 +S136,M034,7,16.31,0 +S136,M034,8,18.2,0 +S136,M034,9,12.59,0 +S136,M034,10,17.07,0 +S136,M034,11,24.81,0 +S136,M034,12,11.09,0 +S137,M035,1,1.38,0 +S137,M035,2,0.64,0 +S137,M035,3,1.2,0 +S137,M035,4,2.72,0 +S137,M035,5,3.22,0 +S137,M035,6,2.66,0 +S137,M035,7,2.92,0 +S137,M035,8,4.4,0 +S137,M035,9,5.16,0 +S137,M035,10,4.94,0 +S137,M035,11,5.53,0 +S137,M035,12,6.92,1 +S138,M035,1,50.75,0 +S138,M035,2,44.13,0 +S138,M035,3,44.74,0 +S138,M035,4,47.22,0 +S138,M035,5,66.18,0 +S138,M035,6,70.25,0 +S138,M035,7,58.91,0 +S138,M035,8,66.01,0 +S138,M035,9,74.72,0 +S138,M035,10,82.04,0 +S138,M035,11,84.85,0 +S138,M035,12,89.65,1 +S139,M035,1,165.51,0 +S139,M035,2,170.14,0 +S139,M035,3,191.58,0 +S139,M035,4,153.79,0 +S139,M035,5,149.65,0 +S139,M035,6,172.41,0 +S139,M035,7,134.26,0 +S139,M035,8,145.91,0 +S139,M035,9,120.72,0 +S139,M035,10,131.51,0 +S139,M035,11,122.45,0 +S139,M035,12,103.51,1 +S140,M035,1,10.19,0 +S140,M035,2,12.74,0 +S140,M035,3,12.11,0 +S140,M035,4,21.01,0 +S140,M035,5,20.68,0 +S140,M035,6,25.42,0 +S140,M035,7,32.41,0 +S140,M035,8,26.58,0 +S140,M035,9,31.08,0 +S140,M035,10,39.75,0 +S140,M035,11,44.77,1 +S140,M035,12,45.99,1 +S141,M036,1,0.68,0 +S141,M036,2,1.85,0 +S141,M036,3,1.0,0 +S141,M036,4,1.14,0 +S141,M036,5,1.29,0 +S141,M036,6,0.89,0 +S141,M036,7,1.96,0 +S141,M036,8,1.44,0 +S141,M036,9,1.24,0 +S141,M036,10,1.27,0 +S141,M036,11,0.58,0 +S141,M036,12,1.79,0 +S142,M036,1,43.34,0 +S142,M036,2,43.15,0 +S142,M036,3,55.47,0 +S142,M036,4,57.89,0 +S142,M036,5,53.48,0 +S142,M036,6,42.31,0 +S142,M036,7,54.82,0 +S142,M036,8,40.05,0 +S142,M036,9,57.17,0 +S142,M036,10,40.25,0 +S142,M036,11,40.92,0 +S142,M036,12,47.28,0 +S143,M036,1,196.01,0 +S143,M036,2,158.95,0 +S143,M036,3,198.02,0 +S143,M036,4,151.77,0 +S143,M036,5,190.16,0 +S143,M036,6,185.7,0 +S143,M036,7,182.79,0 +S143,M036,8,161.23,0 +S143,M036,9,162.41,0 +S143,M036,10,195.79,0 +S143,M036,11,166.0,0 +S143,M036,12,191.3,0 +S144,M036,1,18.57,0 +S144,M036,2,11.79,0 +S144,M036,3,20.1,0 +S144,M036,4,10.79,0 +S144,M036,5,13.35,0 +S144,M036,6,17.74,0 +S144,M036,7,10.69,0 +S144,M036,8,16.01,0 +S144,M036,9,23.15,0 +S144,M036,10,22.99,0 +S144,M036,11,21.6,0 +S144,M036,12,14.66,0 +S145,M037,1,0.63,0 +S145,M037,2,1.19,0 +S145,M037,3,1.05,0 +S145,M037,4,0.94,0 +S145,M037,5,1.68,0 +S145,M037,6,1.61,0 +S145,M037,7,0.86,0 +S145,M037,8,1.05,0 +S145,M037,9,1.96,0 +S145,M037,10,2.0,0 +S145,M037,11,1.66,0 +S145,M037,12,1.6,0 +S146,M037,1,50.93,0 +S146,M037,2,55.92,0 +S146,M037,3,43.01,0 +S146,M037,4,58.65,0 +S146,M037,5,40.48,0 +S146,M037,6,50.83,0 +S146,M037,7,52.72,0 +S146,M037,8,53.87,0 +S146,M037,9,40.19,0 +S146,M037,10,46.7,0 +S146,M037,11,55.18,0 +S146,M037,12,47.53,0 +S147,M037,1,186.9,0 +S147,M037,2,165.38,0 +S147,M037,3,160.26,0 +S147,M037,4,199.16,0 +S147,M037,5,170.99,0 +S147,M037,6,198.06,0 +S147,M037,7,198.63,0 +S147,M037,8,157.12,0 +S147,M037,9,163.99,0 +S147,M037,10,152.2,0 +S147,M037,11,161.57,0 +S147,M037,12,192.28,0 +S148,M037,1,16.2,0 +S148,M037,2,23.43,0 +S148,M037,3,16.88,0 +S148,M037,4,21.22,0 +S148,M037,5,18.71,0 +S148,M037,6,17.58,0 +S148,M037,7,19.56,0 +S148,M037,8,15.92,0 +S148,M037,9,18.88,0 +S148,M037,10,10.93,0 +S148,M037,11,19.94,0 +S148,M037,12,11.96,0 +S149,M038,1,0.94,0 +S149,M038,2,1.78,0 +S149,M038,3,1.89,0 +S149,M038,4,1.09,0 +S149,M038,5,1.75,0 +S149,M038,6,0.99,0 +S149,M038,7,1.18,0 +S149,M038,8,0.85,0 +S149,M038,9,1.92,0 +S149,M038,10,0.7,0 +S149,M038,11,1.66,0 +S149,M038,12,0.66,0 +S150,M038,1,42.11,0 +S150,M038,2,48.99,0 +S150,M038,3,46.28,0 +S150,M038,4,42.41,0 +S150,M038,5,47.17,0 +S150,M038,6,55.41,0 +S150,M038,7,49.04,0 +S150,M038,8,49.27,0 +S150,M038,9,57.05,0 +S150,M038,10,41.81,0 +S150,M038,11,56.15,0 +S150,M038,12,46.08,0 +S151,M038,1,185.24,0 +S151,M038,2,180.17,0 +S151,M038,3,160.75,0 +S151,M038,4,192.83,0 +S151,M038,5,157.42,0 +S151,M038,6,185.56,0 +S151,M038,7,198.42,0 +S151,M038,8,169.81,0 +S151,M038,9,152.45,0 +S151,M038,10,164.94,0 +S151,M038,11,191.0,0 +S151,M038,12,158.69,0 +S152,M038,1,24.28,0 +S152,M038,2,22.82,0 +S152,M038,3,18.34,0 +S152,M038,4,18.25,0 +S152,M038,5,11.42,0 +S152,M038,6,11.53,0 +S152,M038,7,21.91,0 +S152,M038,8,11.82,0 +S152,M038,9,10.93,0 +S152,M038,10,21.71,0 +S152,M038,11,13.33,0 +S152,M038,12,12.97,0 +S153,M039,1,1.1,0 +S153,M039,2,1.53,0 +S153,M039,3,1.91,0 +S153,M039,4,0.63,0 +S153,M039,5,1.38,0 +S153,M039,6,1.88,0 +S153,M039,7,0.61,0 +S153,M039,8,2.0,0 +S153,M039,9,0.8,0 +S153,M039,10,1.47,0 +S153,M039,11,1.71,0 +S153,M039,12,0.64,0 +S154,M039,1,56.95,0 +S154,M039,2,56.18,0 +S154,M039,3,42.45,0 +S154,M039,4,58.85,0 +S154,M039,5,41.01,0 +S154,M039,6,58.48,0 +S154,M039,7,48.66,0 +S154,M039,8,54.49,0 +S154,M039,9,49.92,0 +S154,M039,10,47.65,0 +S154,M039,11,48.53,0 +S154,M039,12,47.08,0 +S155,M039,1,195.42,0 +S155,M039,2,163.75,0 +S155,M039,3,172.3,0 +S155,M039,4,157.43,0 +S155,M039,5,180.71,0 +S155,M039,6,180.85,0 +S155,M039,7,182.36,0 +S155,M039,8,174.94,0 +S155,M039,9,194.14,0 +S155,M039,10,173.95,0 +S155,M039,11,154.39,0 +S155,M039,12,169.15,0 +S156,M039,1,16.29,0 +S156,M039,2,19.64,0 +S156,M039,3,21.96,0 +S156,M039,4,13.74,0 +S156,M039,5,24.91,0 +S156,M039,6,18.15,0 +S156,M039,7,10.29,0 +S156,M039,8,15.74,0 +S156,M039,9,20.7,0 +S156,M039,10,17.2,0 +S156,M039,11,21.72,0 +S156,M039,12,15.16,0 +S157,M040,1,1.35,0 +S157,M040,2,0.98,0 +S157,M040,3,0.91,0 +S157,M040,4,0.54,0 +S157,M040,5,1.4,0 +S157,M040,6,0.54,0 +S157,M040,7,1.72,0 +S157,M040,8,0.92,0 +S157,M040,9,1.71,0 +S157,M040,10,1.21,0 +S157,M040,11,1.04,0 +S157,M040,12,1.38,0 +S158,M040,1,44.66,0 +S158,M040,2,43.19,0 +S158,M040,3,44.96,0 +S158,M040,4,44.82,0 +S158,M040,5,50.67,0 +S158,M040,6,55.82,0 +S158,M040,7,54.74,0 +S158,M040,8,55.28,0 +S158,M040,9,58.96,0 +S158,M040,10,51.51,0 +S158,M040,11,59.65,0 +S158,M040,12,47.77,0 +S159,M040,1,196.76,0 +S159,M040,2,199.58,0 +S159,M040,3,158.69,0 +S159,M040,4,161.83,0 +S159,M040,5,165.9,0 +S159,M040,6,186.91,0 +S159,M040,7,167.94,0 +S159,M040,8,179.58,0 +S159,M040,9,185.17,0 +S159,M040,10,167.58,0 +S159,M040,11,178.38,0 +S159,M040,12,157.11,0 +S160,M040,1,24.23,0 +S160,M040,2,22.55,0 +S160,M040,3,22.16,0 +S160,M040,4,24.56,0 +S160,M040,5,18.13,0 +S160,M040,6,12.63,0 +S160,M040,7,10.57,0 +S160,M040,8,10.84,0 +S160,M040,9,19.02,0 +S160,M040,10,10.3,0 +S160,M040,11,17.17,0 +S160,M040,12,14.98,0 +S161,M041,1,1.42,0 +S161,M041,2,1.71,0 +S161,M041,3,1.01,0 +S161,M041,4,0.77,0 +S161,M041,5,1.7,0 +S161,M041,6,1.8,0 +S161,M041,7,0.59,0 +S161,M041,8,0.72,0 +S161,M041,9,1.56,0 +S161,M041,10,0.71,0 +S161,M041,11,0.67,0 +S161,M041,12,1.29,0 +S162,M041,1,47.3,0 +S162,M041,2,41.45,0 +S162,M041,3,54.09,0 +S162,M041,4,53.24,0 +S162,M041,5,51.72,0 +S162,M041,6,46.73,0 +S162,M041,7,46.45,0 +S162,M041,8,59.52,0 +S162,M041,9,54.65,0 +S162,M041,10,48.74,0 +S162,M041,11,52.79,0 +S162,M041,12,49.74,0 +S163,M041,1,165.86,0 +S163,M041,2,185.36,0 +S163,M041,3,178.1,0 +S163,M041,4,194.48,0 +S163,M041,5,180.69,0 +S163,M041,6,167.84,0 +S163,M041,7,183.28,0 +S163,M041,8,190.72,0 +S163,M041,9,168.92,0 +S163,M041,10,186.7,0 +S163,M041,11,164.73,0 +S163,M041,12,158.89,0 +S164,M041,1,10.03,0 +S164,M041,2,20.53,0 +S164,M041,3,18.59,0 +S164,M041,4,22.76,0 +S164,M041,5,21.56,0 +S164,M041,6,18.51,0 +S164,M041,7,12.73,0 +S164,M041,8,14.77,0 +S164,M041,9,22.13,0 +S164,M041,10,19.17,0 +S164,M041,11,19.59,0 +S164,M041,12,11.56,0 +S165,M042,1,1.24,0 +S165,M042,2,1.0,0 +S165,M042,3,0.62,0 +S165,M042,4,1.02,0 +S165,M042,5,0.98,0 +S165,M042,6,1.45,0 +S165,M042,7,1.94,0 +S165,M042,8,1.73,0 +S165,M042,9,1.51,0 +S165,M042,10,1.59,0 +S165,M042,11,1.0,0 +S165,M042,12,0.52,0 +S166,M042,1,44.19,0 +S166,M042,2,53.86,0 +S166,M042,3,47.01,0 +S166,M042,4,55.74,0 +S166,M042,5,54.41,0 +S166,M042,6,40.96,0 +S166,M042,7,48.76,0 +S166,M042,8,48.39,0 +S166,M042,9,43.33,0 +S166,M042,10,48.28,0 +S166,M042,11,57.64,0 +S166,M042,12,47.02,0 +S167,M042,1,168.77,0 +S167,M042,2,191.44,0 +S167,M042,3,189.44,0 +S167,M042,4,178.9,0 +S167,M042,5,187.91,0 +S167,M042,6,161.28,0 +S167,M042,7,172.56,0 +S167,M042,8,189.96,0 +S167,M042,9,151.67,0 +S167,M042,10,151.0,0 +S167,M042,11,163.06,0 +S167,M042,12,155.24,0 +S168,M042,1,15.13,0 +S168,M042,2,23.32,0 +S168,M042,3,12.41,0 +S168,M042,4,11.13,0 +S168,M042,5,24.13,0 +S168,M042,6,24.67,0 +S168,M042,7,15.07,0 +S168,M042,8,24.78,0 +S168,M042,9,23.4,0 +S168,M042,10,20.57,0 +S168,M042,11,16.47,0 +S168,M042,12,19.8,0 +S169,M043,1,1.41,0 +S169,M043,2,1.15,0 +S169,M043,3,0.58,0 +S169,M043,4,1.0,0 +S169,M043,5,1.96,0 +S169,M043,6,1.11,0 +S169,M043,7,1.33,0 +S169,M043,8,1.89,0 +S169,M043,9,1.31,0 +S169,M043,10,1.49,0 +S169,M043,11,0.95,0 +S169,M043,12,0.64,0 +S170,M043,1,47.86,0 +S170,M043,2,50.99,0 +S170,M043,3,49.82,0 +S170,M043,4,41.09,0 +S170,M043,5,40.08,0 +S170,M043,6,41.75,0 +S170,M043,7,56.48,0 +S170,M043,8,52.5,0 +S170,M043,9,49.61,0 +S170,M043,10,41.94,0 +S170,M043,11,44.14,0 +S170,M043,12,52.76,0 +S171,M043,1,198.85,0 +S171,M043,2,173.68,0 +S171,M043,3,155.78,0 +S171,M043,4,189.24,0 +S171,M043,5,173.55,0 +S171,M043,6,194.74,0 +S171,M043,7,163.25,0 +S171,M043,8,182.09,0 +S171,M043,9,169.38,0 +S171,M043,10,168.55,0 +S171,M043,11,169.07,0 +S171,M043,12,159.33,0 +S172,M043,1,10.67,0 +S172,M043,2,16.6,0 +S172,M043,3,13.95,0 +S172,M043,4,22.56,0 +S172,M043,5,18.46,0 +S172,M043,6,12.27,0 +S172,M043,7,20.45,0 +S172,M043,8,11.45,0 +S172,M043,9,20.02,0 +S172,M043,10,22.12,0 +S172,M043,11,16.21,0 +S172,M043,12,21.35,0 +S173,M044,1,0.84,0 +S173,M044,2,0.59,0 +S173,M044,3,1.08,0 +S173,M044,4,1.28,0 +S173,M044,5,1.95,0 +S173,M044,6,1.53,0 +S173,M044,7,1.9,0 +S173,M044,8,1.36,0 +S173,M044,9,0.85,0 +S173,M044,10,0.95,0 +S173,M044,11,1.09,0 +S173,M044,12,1.78,0 +S174,M044,1,40.69,0 +S174,M044,2,51.42,0 +S174,M044,3,50.34,0 +S174,M044,4,51.43,0 +S174,M044,5,59.66,0 +S174,M044,6,42.95,0 +S174,M044,7,58.99,0 +S174,M044,8,48.98,0 +S174,M044,9,58.98,0 +S174,M044,10,43.34,0 +S174,M044,11,44.24,0 +S174,M044,12,43.85,0 +S175,M044,1,152.13,0 +S175,M044,2,153.92,0 +S175,M044,3,184.16,0 +S175,M044,4,160.1,0 +S175,M044,5,191.13,0 +S175,M044,6,157.39,0 +S175,M044,7,179.75,0 +S175,M044,8,165.99,0 +S175,M044,9,186.36,0 +S175,M044,10,190.37,0 +S175,M044,11,154.35,0 +S175,M044,12,177.82,0 +S176,M044,1,18.28,0 +S176,M044,2,22.85,0 +S176,M044,3,17.8,0 +S176,M044,4,23.76,0 +S176,M044,5,10.19,0 +S176,M044,6,20.51,0 +S176,M044,7,18.81,0 +S176,M044,8,21.73,0 +S176,M044,9,11.69,0 +S176,M044,10,12.24,0 +S176,M044,11,18.5,0 +S176,M044,12,18.74,0 +S177,M045,1,1.95,0 +S177,M045,2,0.65,0 +S177,M045,3,0.66,0 +S177,M045,4,1.67,0 +S177,M045,5,1.68,0 +S177,M045,6,1.8,0 +S177,M045,7,1.82,0 +S177,M045,8,1.82,0 +S177,M045,9,0.9,0 +S177,M045,10,1.48,0 +S177,M045,11,1.36,0 +S177,M045,12,1.21,0 +S178,M045,1,43.44,0 +S178,M045,2,48.06,0 +S178,M045,3,52.36,0 +S178,M045,4,53.62,0 +S178,M045,5,48.03,0 +S178,M045,6,47.92,0 +S178,M045,7,57.58,0 +S178,M045,8,53.81,0 +S178,M045,9,40.99,0 +S178,M045,10,49.5,0 +S178,M045,11,45.37,0 +S178,M045,12,47.45,0 +S179,M045,1,166.89,0 +S179,M045,2,189.68,0 +S179,M045,3,165.55,0 +S179,M045,4,192.14,0 +S179,M045,5,192.91,0 +S179,M045,6,184.24,0 +S179,M045,7,198.62,0 +S179,M045,8,161.4,0 +S179,M045,9,165.02,0 +S179,M045,10,187.19,0 +S179,M045,11,188.7,0 +S179,M045,12,166.04,0 +S180,M045,1,20.93,0 +S180,M045,2,19.07,0 +S180,M045,3,11.34,0 +S180,M045,4,16.63,0 +S180,M045,5,23.7,0 +S180,M045,6,16.11,0 +S180,M045,7,17.27,0 +S180,M045,8,17.75,0 +S180,M045,9,15.61,0 +S180,M045,10,10.84,0 +S180,M045,11,21.87,0 +S180,M045,12,10.98,0 +S181,M046,1,1.56,0 +S181,M046,2,1.11,0 +S181,M046,3,1.67,0 +S181,M046,4,1.29,0 +S181,M046,5,1.35,0 +S181,M046,6,0.76,0 +S181,M046,7,1.81,0 +S181,M046,8,0.8,0 +S181,M046,9,0.59,0 +S181,M046,10,0.55,0 +S181,M046,11,0.58,0 +S181,M046,12,1.04,0 +S182,M046,1,43.98,0 +S182,M046,2,47.37,0 +S182,M046,3,49.28,0 +S182,M046,4,47.94,0 +S182,M046,5,58.46,0 +S182,M046,6,53.62,0 +S182,M046,7,44.84,0 +S182,M046,8,49.78,0 +S182,M046,9,48.92,0 +S182,M046,10,59.21,0 +S182,M046,11,49.49,0 +S182,M046,12,51.03,0 +S183,M046,1,167.21,0 +S183,M046,2,191.88,0 +S183,M046,3,162.93,0 +S183,M046,4,191.54,0 +S183,M046,5,154.52,0 +S183,M046,6,151.37,0 +S183,M046,7,152.48,0 +S183,M046,8,192.69,0 +S183,M046,9,188.99,0 +S183,M046,10,160.72,0 +S183,M046,11,161.07,0 +S183,M046,12,161.26,0 +S184,M046,1,13.1,0 +S184,M046,2,19.77,0 +S184,M046,3,17.7,0 +S184,M046,4,22.97,0 +S184,M046,5,24.51,0 +S184,M046,6,20.23,0 +S184,M046,7,17.11,0 +S184,M046,8,11.98,0 +S184,M046,9,12.6,0 +S184,M046,10,22.01,0 +S184,M046,11,13.36,0 +S184,M046,12,19.13,0 +S185,M047,1,1.91,0 +S185,M047,2,1.57,0 +S185,M047,3,1.56,0 +S185,M047,4,0.91,0 +S185,M047,5,1.84,0 +S185,M047,6,1.78,0 +S185,M047,7,1.19,0 +S185,M047,8,0.83,0 +S185,M047,9,0.95,0 +S185,M047,10,1.28,0 +S185,M047,11,1.44,0 +S185,M047,12,1.38,0 +S186,M047,1,40.15,0 +S186,M047,2,59.99,0 +S186,M047,3,59.33,0 +S186,M047,4,56.79,0 +S186,M047,5,51.23,0 +S186,M047,6,51.88,0 +S186,M047,7,45.65,0 +S186,M047,8,53.69,0 +S186,M047,9,49.63,0 +S186,M047,10,46.73,0 +S186,M047,11,45.53,0 +S186,M047,12,46.5,0 +S187,M047,1,163.84,0 +S187,M047,2,182.81,0 +S187,M047,3,197.61,0 +S187,M047,4,182.57,0 +S187,M047,5,159.42,0 +S187,M047,6,175.86,0 +S187,M047,7,162.02,0 +S187,M047,8,152.84,0 +S187,M047,9,192.87,0 +S187,M047,10,190.5,0 +S187,M047,11,170.2,0 +S187,M047,12,165.98,0 +S188,M047,1,12.1,0 +S188,M047,2,10.38,0 +S188,M047,3,17.46,0 +S188,M047,4,14.43,0 +S188,M047,5,23.6,0 +S188,M047,6,16.07,0 +S188,M047,7,21.05,0 +S188,M047,8,18.75,0 +S188,M047,9,24.92,0 +S188,M047,10,22.24,0 +S188,M047,11,15.15,0 +S188,M047,12,20.7,0 +S189,M048,1,1.53,0 +S189,M048,2,1.46,0 +S189,M048,3,1.21,0 +S189,M048,4,1.91,0 +S189,M048,5,1.04,0 +S189,M048,6,0.95,0 +S189,M048,7,0.75,0 +S189,M048,8,1.49,0 +S189,M048,9,1.34,0 +S189,M048,10,0.94,0 +S189,M048,11,1.21,0 +S189,M048,12,1.61,0 +S190,M048,1,54.5,0 +S190,M048,2,51.18,0 +S190,M048,3,58.25,0 +S190,M048,4,58.94,0 +S190,M048,5,57.99,0 +S190,M048,6,53.09,0 +S190,M048,7,44.45,0 +S190,M048,8,44.47,0 +S190,M048,9,57.43,0 +S190,M048,10,48.41,0 +S190,M048,11,57.74,0 +S190,M048,12,47.43,0 +S191,M048,1,179.28,0 +S191,M048,2,173.19,0 +S191,M048,3,155.89,0 +S191,M048,4,152.68,0 +S191,M048,5,181.72,0 +S191,M048,6,179.02,0 +S191,M048,7,181.03,0 +S191,M048,8,173.61,0 +S191,M048,9,159.73,0 +S191,M048,10,155.99,0 +S191,M048,11,184.03,0 +S191,M048,12,176.49,0 +S192,M048,1,16.2,0 +S192,M048,2,23.06,0 +S192,M048,3,12.26,0 +S192,M048,4,24.89,0 +S192,M048,5,15.14,0 +S192,M048,6,24.81,0 +S192,M048,7,23.39,0 +S192,M048,8,24.8,0 +S192,M048,9,12.15,0 +S192,M048,10,22.94,0 +S192,M048,11,12.71,0 +S192,M048,12,11.21,0 +S193,M049,1,1.0,0 +S193,M049,2,1.98,0 +S193,M049,3,0.97,0 +S193,M049,4,0.59,0 +S193,M049,5,1.18,0 +S193,M049,6,0.81,0 +S193,M049,7,1.66,0 +S193,M049,8,1.36,0 +S193,M049,9,0.74,0 +S193,M049,10,1.68,0 +S193,M049,11,1.67,0 +S193,M049,12,0.69,0 +S194,M049,1,48.67,0 +S194,M049,2,48.59,0 +S194,M049,3,48.58,0 +S194,M049,4,49.55,0 +S194,M049,5,40.09,0 +S194,M049,6,50.68,0 +S194,M049,7,54.61,0 +S194,M049,8,59.0,0 +S194,M049,9,57.84,0 +S194,M049,10,55.14,0 +S194,M049,11,50.62,0 +S194,M049,12,40.36,0 +S195,M049,1,150.21,0 +S195,M049,2,186.68,0 +S195,M049,3,161.31,0 +S195,M049,4,193.85,0 +S195,M049,5,165.6,0 +S195,M049,6,155.14,0 +S195,M049,7,175.17,0 +S195,M049,8,165.31,0 +S195,M049,9,153.26,0 +S195,M049,10,152.29,0 +S195,M049,11,197.42,0 +S195,M049,12,172.8,0 +S196,M049,1,17.8,0 +S196,M049,2,17.86,0 +S196,M049,3,16.5,0 +S196,M049,4,20.3,0 +S196,M049,5,12.01,0 +S196,M049,6,24.94,0 +S196,M049,7,17.35,0 +S196,M049,8,21.25,0 +S196,M049,9,12.47,0 +S196,M049,10,23.6,0 +S196,M049,11,22.18,0 +S196,M049,12,10.29,0 +S197,M050,1,0.84,0 +S197,M050,2,1.98,0 +S197,M050,3,0.82,0 +S197,M050,4,0.53,0 +S197,M050,5,1.26,0 +S197,M050,6,0.68,0 +S197,M050,7,1.57,0 +S197,M050,8,1.69,0 +S197,M050,9,1.06,0 +S197,M050,10,1.46,0 +S197,M050,11,1.66,0 +S197,M050,12,1.43,0 +S198,M050,1,51.88,0 +S198,M050,2,40.96,0 +S198,M050,3,42.16,0 +S198,M050,4,56.72,0 +S198,M050,5,40.96,0 +S198,M050,6,42.32,0 +S198,M050,7,48.89,0 +S198,M050,8,42.47,0 +S198,M050,9,45.05,0 +S198,M050,10,54.04,0 +S198,M050,11,44.11,0 +S198,M050,12,53.71,0 +S199,M050,1,150.47,0 +S199,M050,2,171.04,0 +S199,M050,3,155.05,0 +S199,M050,4,176.02,0 +S199,M050,5,181.14,0 +S199,M050,6,185.04,0 +S199,M050,7,170.94,0 +S199,M050,8,194.93,0 +S199,M050,9,175.58,0 +S199,M050,10,180.88,0 +S199,M050,11,164.14,0 +S199,M050,12,189.24,0 +S200,M050,1,17.47,0 +S200,M050,2,19.14,0 +S200,M050,3,21.67,0 +S200,M050,4,22.47,0 +S200,M050,5,24.6,0 +S200,M050,6,15.37,0 +S200,M050,7,16.15,0 +S200,M050,8,12.41,0 +S200,M050,9,18.22,0 +S200,M050,10,23.66,0 +S200,M050,11,20.32,0 +S200,M050,12,17.21,0 diff --git a/v1/machine_maintenance/data/sensors.csv b/v1/machine_maintenance/data/sensors.csv new file mode 100644 index 00000000..04c75ef8 --- /dev/null +++ b/v1/machine_maintenance/data/sensors.csv @@ -0,0 +1,201 @@ +sensor_id,machine_id,sensor_type,unit,warning_threshold,critical_threshold +S001,M001,vibration,mm/s,3.5,6.0 +S002,M001,temperature,C,70.0,85.0 +S003,M001,pressure,PSI,135.0,115.0 +S004,M001,current,A,30.0,40.0 +S005,M002,vibration,mm/s,3.5,6.0 +S006,M002,temperature,C,70.0,85.0 +S007,M002,pressure,PSI,135.0,115.0 +S008,M002,current,A,30.0,40.0 +S009,M003,vibration,mm/s,3.5,6.0 +S010,M003,temperature,C,70.0,85.0 +S011,M003,pressure,PSI,135.0,115.0 +S012,M003,current,A,30.0,40.0 +S013,M004,vibration,mm/s,3.5,6.0 +S014,M004,temperature,C,70.0,85.0 +S015,M004,pressure,PSI,135.0,115.0 +S016,M004,current,A,30.0,40.0 +S017,M005,vibration,mm/s,3.5,6.0 +S018,M005,temperature,C,70.0,85.0 +S019,M005,pressure,PSI,135.0,115.0 +S020,M005,current,A,30.0,40.0 +S021,M006,vibration,mm/s,3.5,6.0 +S022,M006,temperature,C,70.0,85.0 +S023,M006,pressure,PSI,135.0,115.0 +S024,M006,current,A,30.0,40.0 +S025,M007,vibration,mm/s,3.5,6.0 +S026,M007,temperature,C,70.0,85.0 +S027,M007,pressure,PSI,135.0,115.0 +S028,M007,current,A,30.0,40.0 +S029,M008,vibration,mm/s,3.5,6.0 +S030,M008,temperature,C,70.0,85.0 +S031,M008,pressure,PSI,135.0,115.0 +S032,M008,current,A,30.0,40.0 +S033,M009,vibration,mm/s,3.5,6.0 +S034,M009,temperature,C,70.0,85.0 +S035,M009,pressure,PSI,135.0,115.0 +S036,M009,current,A,30.0,40.0 +S037,M010,vibration,mm/s,3.5,6.0 +S038,M010,temperature,C,70.0,85.0 +S039,M010,pressure,PSI,135.0,115.0 +S040,M010,current,A,30.0,40.0 +S041,M011,vibration,mm/s,3.5,6.0 +S042,M011,temperature,C,70.0,85.0 +S043,M011,pressure,PSI,135.0,115.0 +S044,M011,current,A,30.0,40.0 +S045,M012,vibration,mm/s,3.5,6.0 +S046,M012,temperature,C,70.0,85.0 +S047,M012,pressure,PSI,135.0,115.0 +S048,M012,current,A,30.0,40.0 +S049,M013,vibration,mm/s,3.5,6.0 +S050,M013,temperature,C,70.0,85.0 +S051,M013,pressure,PSI,135.0,115.0 +S052,M013,current,A,30.0,40.0 +S053,M014,vibration,mm/s,3.5,6.0 +S054,M014,temperature,C,70.0,85.0 +S055,M014,pressure,PSI,135.0,115.0 +S056,M014,current,A,30.0,40.0 +S057,M015,vibration,mm/s,3.5,6.0 +S058,M015,temperature,C,70.0,85.0 +S059,M015,pressure,PSI,135.0,115.0 +S060,M015,current,A,30.0,40.0 +S061,M016,vibration,mm/s,3.5,6.0 +S062,M016,temperature,C,70.0,85.0 +S063,M016,pressure,PSI,135.0,115.0 +S064,M016,current,A,30.0,40.0 +S065,M017,vibration,mm/s,3.5,6.0 +S066,M017,temperature,C,70.0,85.0 +S067,M017,pressure,PSI,135.0,115.0 +S068,M017,current,A,30.0,40.0 +S069,M018,vibration,mm/s,3.5,6.0 +S070,M018,temperature,C,70.0,85.0 +S071,M018,pressure,PSI,135.0,115.0 +S072,M018,current,A,30.0,40.0 +S073,M019,vibration,mm/s,3.5,6.0 +S074,M019,temperature,C,70.0,85.0 +S075,M019,pressure,PSI,135.0,115.0 +S076,M019,current,A,30.0,40.0 +S077,M020,vibration,mm/s,3.5,6.0 +S078,M020,temperature,C,70.0,85.0 +S079,M020,pressure,PSI,135.0,115.0 +S080,M020,current,A,30.0,40.0 +S081,M021,vibration,mm/s,3.5,6.0 +S082,M021,temperature,C,70.0,85.0 +S083,M021,pressure,PSI,135.0,115.0 +S084,M021,current,A,30.0,40.0 +S085,M022,vibration,mm/s,3.5,6.0 +S086,M022,temperature,C,70.0,85.0 +S087,M022,pressure,PSI,135.0,115.0 +S088,M022,current,A,30.0,40.0 +S089,M023,vibration,mm/s,3.5,6.0 +S090,M023,temperature,C,70.0,85.0 +S091,M023,pressure,PSI,135.0,115.0 +S092,M023,current,A,30.0,40.0 +S093,M024,vibration,mm/s,3.5,6.0 +S094,M024,temperature,C,70.0,85.0 +S095,M024,pressure,PSI,135.0,115.0 +S096,M024,current,A,30.0,40.0 +S097,M025,vibration,mm/s,3.5,6.0 +S098,M025,temperature,C,70.0,85.0 +S099,M025,pressure,PSI,135.0,115.0 +S100,M025,current,A,30.0,40.0 +S101,M026,vibration,mm/s,3.5,6.0 +S102,M026,temperature,C,70.0,85.0 +S103,M026,pressure,PSI,135.0,115.0 +S104,M026,current,A,30.0,40.0 +S105,M027,vibration,mm/s,3.5,6.0 +S106,M027,temperature,C,70.0,85.0 +S107,M027,pressure,PSI,135.0,115.0 +S108,M027,current,A,30.0,40.0 +S109,M028,vibration,mm/s,3.5,6.0 +S110,M028,temperature,C,70.0,85.0 +S111,M028,pressure,PSI,135.0,115.0 +S112,M028,current,A,30.0,40.0 +S113,M029,vibration,mm/s,3.5,6.0 +S114,M029,temperature,C,70.0,85.0 +S115,M029,pressure,PSI,135.0,115.0 +S116,M029,current,A,30.0,40.0 +S117,M030,vibration,mm/s,3.5,6.0 +S118,M030,temperature,C,70.0,85.0 +S119,M030,pressure,PSI,135.0,115.0 +S120,M030,current,A,30.0,40.0 +S121,M031,vibration,mm/s,3.5,6.0 +S122,M031,temperature,C,70.0,85.0 +S123,M031,pressure,PSI,135.0,115.0 +S124,M031,current,A,30.0,40.0 +S125,M032,vibration,mm/s,3.5,6.0 +S126,M032,temperature,C,70.0,85.0 +S127,M032,pressure,PSI,135.0,115.0 +S128,M032,current,A,30.0,40.0 +S129,M033,vibration,mm/s,3.5,6.0 +S130,M033,temperature,C,70.0,85.0 +S131,M033,pressure,PSI,135.0,115.0 +S132,M033,current,A,30.0,40.0 +S133,M034,vibration,mm/s,3.5,6.0 +S134,M034,temperature,C,70.0,85.0 +S135,M034,pressure,PSI,135.0,115.0 +S136,M034,current,A,30.0,40.0 +S137,M035,vibration,mm/s,3.5,6.0 +S138,M035,temperature,C,70.0,85.0 +S139,M035,pressure,PSI,135.0,115.0 +S140,M035,current,A,30.0,40.0 +S141,M036,vibration,mm/s,3.5,6.0 +S142,M036,temperature,C,70.0,85.0 +S143,M036,pressure,PSI,135.0,115.0 +S144,M036,current,A,30.0,40.0 +S145,M037,vibration,mm/s,3.5,6.0 +S146,M037,temperature,C,70.0,85.0 +S147,M037,pressure,PSI,135.0,115.0 +S148,M037,current,A,30.0,40.0 +S149,M038,vibration,mm/s,3.5,6.0 +S150,M038,temperature,C,70.0,85.0 +S151,M038,pressure,PSI,135.0,115.0 +S152,M038,current,A,30.0,40.0 +S153,M039,vibration,mm/s,3.5,6.0 +S154,M039,temperature,C,70.0,85.0 +S155,M039,pressure,PSI,135.0,115.0 +S156,M039,current,A,30.0,40.0 +S157,M040,vibration,mm/s,3.5,6.0 +S158,M040,temperature,C,70.0,85.0 +S159,M040,pressure,PSI,135.0,115.0 +S160,M040,current,A,30.0,40.0 +S161,M041,vibration,mm/s,3.5,6.0 +S162,M041,temperature,C,70.0,85.0 +S163,M041,pressure,PSI,135.0,115.0 +S164,M041,current,A,30.0,40.0 +S165,M042,vibration,mm/s,3.5,6.0 +S166,M042,temperature,C,70.0,85.0 +S167,M042,pressure,PSI,135.0,115.0 +S168,M042,current,A,30.0,40.0 +S169,M043,vibration,mm/s,3.5,6.0 +S170,M043,temperature,C,70.0,85.0 +S171,M043,pressure,PSI,135.0,115.0 +S172,M043,current,A,30.0,40.0 +S173,M044,vibration,mm/s,3.5,6.0 +S174,M044,temperature,C,70.0,85.0 +S175,M044,pressure,PSI,135.0,115.0 +S176,M044,current,A,30.0,40.0 +S177,M045,vibration,mm/s,3.5,6.0 +S178,M045,temperature,C,70.0,85.0 +S179,M045,pressure,PSI,135.0,115.0 +S180,M045,current,A,30.0,40.0 +S181,M046,vibration,mm/s,3.5,6.0 +S182,M046,temperature,C,70.0,85.0 +S183,M046,pressure,PSI,135.0,115.0 +S184,M046,current,A,30.0,40.0 +S185,M047,vibration,mm/s,3.5,6.0 +S186,M047,temperature,C,70.0,85.0 +S187,M047,pressure,PSI,135.0,115.0 +S188,M047,current,A,30.0,40.0 +S189,M048,vibration,mm/s,3.5,6.0 +S190,M048,temperature,C,70.0,85.0 +S191,M048,pressure,PSI,135.0,115.0 +S192,M048,current,A,30.0,40.0 +S193,M049,vibration,mm/s,3.5,6.0 +S194,M049,temperature,C,70.0,85.0 +S195,M049,pressure,PSI,135.0,115.0 +S196,M049,current,A,30.0,40.0 +S197,M050,vibration,mm/s,3.5,6.0 +S198,M050,temperature,C,70.0,85.0 +S199,M050,pressure,PSI,135.0,115.0 +S200,M050,current,A,30.0,40.0 diff --git a/v1/machine_maintenance/data/technicians.csv b/v1/machine_maintenance/data/technicians.csv new file mode 100644 index 00000000..a3ffa5e3 --- /dev/null +++ b/v1/machine_maintenance/data/technicians.csv @@ -0,0 +1,21 @@ +technician_id,technician_name,skill_level,base_location,certifications,hourly_rate,max_weekly_hours,specialization +T001,Alice_Johnson,Senior,Houston_TX,Turbine|Generator,97.0,40,Rotating_Equipment +T002,Bob_Martinez,Senior,Chicago_IL,Compressor|Pump,94.0,40,Fluid_Systems +T003,Charlie_Brown,Senior,Phoenix_AZ,Generator|Motor,87.0,40,Electrical_Systems +T004,Diana_Chen,Senior,Houston_TX,Compressor,88.0,40,Mechanical_Systems +T005,Edward_Smith,Senior,Chicago_IL,Motor|Pump,91.0,40,General_Maintenance +T006,Fiona_Garcia,Senior,Phoenix_AZ,Pump|Generator,89.0,40,Hydraulic_Systems +T007,George_Davis,Intermediate,Houston_TX,Compressor,84.0,40,Material_Handling +T008,Hannah_Wilson,Intermediate,Chicago_IL,Compressor|Motor,74.0,40,Rotating_Equipment +T009,Ian_Taylor,Intermediate,Phoenix_AZ,Turbine|Generator,83.0,40,Fluid_Systems +T010,Julia_Anderson,Intermediate,Houston_TX,Pump|Motor,79.0,40,Electrical_Systems +T011,Kevin_Kumar,Intermediate,Chicago_IL,Compressor,80.0,40,Mechanical_Systems +T012,Laura_Lee,Intermediate,Phoenix_AZ,Motor|Generator,84.0,40,General_Maintenance +T013,Mike_Thompson,Intermediate,Houston_TX,Pump|Compressor,84.0,40,Hydraulic_Systems +T014,Nina_White,Junior,Chicago_IL,Motor,69.0,40,Material_Handling +T015,Oscar_Harris,Junior,Phoenix_AZ,Pump,63.0,40,Rotating_Equipment +T016,Patricia_Clark,Junior,Houston_TX,Compressor,59.0,40,Fluid_Systems +T017,Raj_Young,Junior,Chicago_IL,Turbine,60.0,40,Electrical_Systems +T018,Sarah_Adams,Junior,Phoenix_AZ,Motor|Pump,65.0,40,Mechanical_Systems +T019,Tom_Baker,Junior,Houston_TX,Generator,62.0,40,General_Maintenance +T020,Uma_Scott,Junior,Chicago_IL,Compressor|Motor,69.0,40,Hydraulic_Systems diff --git a/v1/machine_maintenance/data/training_options.csv b/v1/machine_maintenance/data/training_options.csv new file mode 100644 index 00000000..f00a3a2c --- /dev/null +++ b/v1/machine_maintenance/data/training_options.csv @@ -0,0 +1,42 @@ +technician_id,machine_type,training_cost,training_weeks +T001,Compressor,6271.0,5 +T001,Motor,3330.0,7 +T002,Turbine,5984.0,4 +T002,Generator,5186.0,5 +T003,Turbine,4111.0,5 +T003,Compressor,3508.0,2 +T004,Turbine,6457.0,8 +T004,Generator,6432.0,4 +T005,Compressor,3698.0,2 +T006,Turbine,4165.0,4 +T006,Compressor,5205.0,4 +T006,Motor,2327.0,4 +T007,Pump,7211.0,5 +T007,Generator,2074.0,4 +T007,Motor,4643.0,3 +T008,Pump,7170.0,2 +T009,Compressor,2444.0,7 +T009,Motor,6867.0,4 +T010,Turbine,3489.0,6 +T011,Motor,5531.0,5 +T011,Pump,5135.0,5 +T012,Turbine,5726.0,6 +T012,Pump,6576.0,7 +T013,Turbine,5963.0,3 +T013,Generator,5312.0,3 +T013,Motor,4703.0,6 +T014,Turbine,2196.0,4 +T014,Generator,6124.0,4 +T016,Turbine,5267.0,7 +T016,Pump,3594.0,3 +T016,Generator,2996.0,4 +T016,Motor,6945.0,4 +T017,Compressor,4812.0,4 +T017,Motor,4963.0,2 +T018,Turbine,7337.0,7 +T018,Compressor,3502.0,2 +T018,Generator,6046.0,8 +T019,Turbine,7705.0,8 +T019,Pump,2241.0,8 +T019,Motor,5114.0,5 +T020,Pump,5512.0,7 diff --git a/v1/machine_maintenance/data/travel.csv b/v1/machine_maintenance/data/travel.csv new file mode 100644 index 00000000..d2c79cea --- /dev/null +++ b/v1/machine_maintenance/data/travel.csv @@ -0,0 +1,10 @@ +from_location,to_location,travel_hours,travel_cost +Chicago_IL,Houston_TX,3.0,650.0 +Chicago_IL,Chicago_IL,0.0,0.0 +Chicago_IL,Phoenix_AZ,3.5,720.0 +Houston_TX,Houston_TX,0.0,0.0 +Houston_TX,Chicago_IL,3.0,650.0 +Houston_TX,Phoenix_AZ,2.5,480.0 +Phoenix_AZ,Houston_TX,2.5,480.0 +Phoenix_AZ,Chicago_IL,3.5,720.0 +Phoenix_AZ,Phoenix_AZ,0.0,0.0 diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py new file mode 100644 index 00000000..573e52de --- /dev/null +++ b/v1/machine_maintenance/machine_maintenance.py @@ -0,0 +1,1717 @@ +"""Machine maintenance (multi-reasoner) template. + +This script demonstrates a chained multi-reasoner workflow in RelationalAI, +combining querying, graph analysis, rules-based classification, and prescriptive +optimization in a single template: + +- Stage 0 -- Querying: compute OEE by facility, surface sensor anomalies, and + identify machines with the steepest failure degradation trajectories. +- Stage 1 -- Graph: build a machine dependency graph from shared-technician + qualifications, compute weakly connected components (dependency clusters) + and betweenness centrality (bottleneck machines). +- Stage 2 -- Rules: derive compliance flags for overdue maintenance, high-risk + machines, sensor anomalies, chronic downtime, parts reorder, and expiring + certifications. Chain individual flags into a composite risk tier + (Critical / Elevated / Standard). +- Stage 3 -- Prescriptive: schedule preventive maintenance across a multi-period + horizon, assigning qualified technicians to machines. The optimization + consumes outputs from all earlier stages: per-period failure predictions + from Stage 0, betweenness centrality from Stage 1, and overdue-maintenance + flags from Stage 2. +- Stage 4 -- Resilience: analyze the optimal schedule for single-point-of-failure + technicians and recommend cross-training to eliminate concentration risk. + +Run: + `python machine_maintenance.py` + +Output: + Prints OEE and anomaly analysis, graph clusters and centrality, compliance + flags with composite risk tier, optimized maintenance schedule, and + resilience analysis with cross-training recommendations. +""" + +from pathlib import Path + +from pandas import read_csv +from relationalai.semantics import ( + Boolean, + Float, + Integer, + Model, + String, + distinct, + max, + sum, +) +from relationalai.semantics.reasoners.graph import Graph +from relationalai.semantics.reasoners.prescriptive import Problem +from relationalai.semantics.std import aggregates as aggs +from relationalai.semantics.std import floats + +# -------------------------------------------------- +# Configure inputs +# -------------------------------------------------- + +DATA_DIR = Path(__file__).parent / "data" +PERIOD_HORIZON = 4 # number of discrete planning periods +PARTS_CAPACITY_PER_PERIOD = 5 # max maintenance jobs per period (parts/bay limit) +TRAVEL_COST_PER_HOUR = 50.0 # cost penalty when technician travels to another facility +CENTRALITY_WEIGHT = 2.0 # multiplier for betweenness centrality in failure cost +OVERDUE_DEADLINE = 2 # overdue machines must be maintained by this period +CHRONIC_DOWNTIME_THRESHOLD = 8 # event count above which a machine is chronic + +# -------------------------------------------------- +# Load CSV data +# -------------------------------------------------- + +# Equipment and maintenance data. +machines_df = read_csv(DATA_DIR / "machines.csv") +technicians_df = read_csv(DATA_DIR / "technicians.csv") +availability_df = read_csv(DATA_DIR / "availability.csv") +qualifications_df = read_csv(DATA_DIR / "qualifications.csv") +parts_df = read_csv(DATA_DIR / "parts_inventory.csv") +cert_df = read_csv(DATA_DIR / "certification_expiry.csv") + +# Sensor and prediction data. +sensors_df = read_csv(DATA_DIR / "sensors.csv") +sensor_readings_df = read_csv(DATA_DIR / "sensor_readings.csv") +failure_pred_df = read_csv(DATA_DIR / "failure_predictions.csv") + +# Operational data. +downtime_df = read_csv(DATA_DIR / "downtime_events.csv") +production_df = read_csv(DATA_DIR / "production_runs.csv") +training_df = read_csv(DATA_DIR / "training_options.csv") + +# -------------------------------------------------- +# Define semantic model & load data +# -------------------------------------------------- + +model = Model("machine_maintenance") + +# Machine concept: manufacturing machines with ML-predicted failure probability, +# numeric criticality (1-5), maintenance duration, and estimated parts cost. +Machine = model.Concept("Machine", identify_by={"machine_id": String}) +Machine.machine_name = model.Property(f"{Machine} has {String:machine_name}") +Machine.machine_type = model.Property(f"{Machine} has type {String:machine_type}") +Machine.facility = model.Property(f"{Machine} at {String:facility}") +Machine.location = model.Property(f"{Machine} in {String:location}") +Machine.remaining_useful_life = model.Property( + f"{Machine} has remaining useful life {Float:remaining_useful_life}" +) +Machine.failure_probability = model.Property( + f"{Machine} has failure probability {Float:failure_probability}" +) +Machine.criticality = model.Property(f"{Machine} has criticality {Integer:criticality}") +Machine.maintenance_duration_hours = model.Property( + f"{Machine} requires {Integer:maintenance_duration_hours} hours" +) +Machine.last_maintenance_date = model.Property( + f"{Machine} last maintained {String:last_maintenance_date}" +) +Machine.parts_required = model.Property( + f"{Machine} needs parts {String:parts_required}" +) +Machine.estimated_parts_cost = model.Property( + f"{Machine} has parts cost {Float:estimated_parts_cost}" +) +model.define(Machine.new(model.data(machines_df).to_schema())) + +# Technician concept: maintenance personnel with skills, certifications, +# hourly rates, and weekly hour caps. +Technician = model.Concept("Technician", identify_by={"technician_id": String}) +Technician.technician_name = model.Property( + f"{Technician} has {String:technician_name}" +) +Technician.skill_level = model.Property( + f"{Technician} has skill level {String:skill_level}" +) +Technician.base_location = model.Property( + f"{Technician} based in {String:base_location}" +) +Technician.certifications = model.Property( + f"{Technician} certified for {String:certifications}" +) +Technician.hourly_rate = model.Property( + f"{Technician} has hourly rate {Float:hourly_rate}" +) +Technician.max_weekly_hours = model.Property( + f"{Technician} has max weekly hours {Integer:max_weekly_hours}" +) +Technician.specialization = model.Property( + f"{Technician} specializes in {String:specialization}" +) +model.define(Technician.new(model.data(technicians_df).to_schema())) + +# Qualification concept: pre-computed mapping of which technicians are +# certified to service which machine types. +Qualification = model.Concept( + "Qualification", identify_by={"technician_id": String, "machine_type": String} +) +Qualification.technician = model.Property(f"{Qualification} for {Technician}") +Qualification.machine_type_str = model.Property( + f"{Qualification} covers {String:machine_type_str}" +) +qual_data = model.data(qualifications_df) +model.define( + q := Qualification.new( + technician_id=qual_data["technician_id"], machine_type=qual_data["machine_type"] + ), + q.machine_type_str(qual_data["machine_type"]), +) +model.define(Qualification.technician(Technician)).where( + Qualification.technician_id == Technician.technician_id +) + +# PartsInventory concept: spare parts stock levels at each facility. +PartsInventory = model.Concept("PartsInventory", identify_by={"part_id": String}) +PartsInventory.facility = model.Property(f"{PartsInventory} at {String:facility}") +PartsInventory.part_name = model.Property(f"{PartsInventory} has {String:part_name}") +PartsInventory.stock_level = model.Property( + f"{PartsInventory} has {Integer:stock_level} units in stock" +) +PartsInventory.min_order_qty = model.Property( + f"{PartsInventory} minimum order {Integer:min_order_qty} units" +) +model.define(PartsInventory.new(model.data(parts_df).to_schema())) + +# CertificationExpiry concept: tracks days remaining on technician-machine-type +# certifications. Used by rules stage to flag expiring qualifications. +CertificationExpiry = model.Concept( + "CertificationExpiry", + identify_by={"technician_id": String, "machine_type": String}, +) +CertificationExpiry.days_remaining = model.Property( + f"{CertificationExpiry} has {Integer:days_remaining} days remaining" +) +CertificationExpiry.technician = model.Property( + f"{CertificationExpiry} for {Technician}" +) +cert_data_ref = model.data(cert_df) +model.define( + c := CertificationExpiry.new( + technician_id=cert_data_ref["technician_id"], + machine_type=cert_data_ref["machine_type"], + ), + c.days_remaining(cert_data_ref["days_remaining"]), +) +model.define(CertificationExpiry.technician(Technician)).where( + CertificationExpiry.technician_id == Technician.technician_id +) + +# TrainingOption concept: cross-training options per (technician, machine_type). +# Used by Stage 4 resilience analysis to recommend the cheapest non-local +# cross-training candidate for each concentrated machine type. +TrainingOption = model.Concept( + "TrainingOption", + identify_by={"technician_id": String, "machine_type": String}, +) +TrainingOption.training_cost = model.Property( + f"{TrainingOption} costs {Float:training_cost}" +) +TrainingOption.training_weeks = model.Property( + f"{TrainingOption} takes {Integer:training_weeks} weeks" +) +TrainingOption.technician = model.Property(f"{TrainingOption} for {Technician}") +training_data = model.data(training_df) +model.define( + to_ := TrainingOption.new( + technician_id=training_data["technician_id"], + machine_type=training_data["machine_type"], + ), + to_.training_cost(training_data["training_cost"]), + to_.training_weeks(training_data["training_weeks"]), +) +model.define(TrainingOption.technician(Technician)).where( + TrainingOption.technician_id == Technician.technician_id +) + +# Period concept: discrete planning periods (1..PERIOD_HORIZON). +Period = model.Concept("Period", identify_by={"pid": Integer}) +period_data = model.data([{"pid": t} for t in range(1, PERIOD_HORIZON + 1)]) +model.define(Period.new(pid=period_data["pid"])) + +# -------------------------------------------------- +# New concepts: sensors, predictions, downtime, production +# -------------------------------------------------- + +# Sensor concept: physical sensors attached to machines with thresholds. +Sensor = model.Concept("Sensor", identify_by={"sensor_id": String}) +Sensor.machine_id_str = model.Property(f"{Sensor} for machine {String:machine_id_str}") +Sensor.sensor_type = model.Property(f"{Sensor} measures {String:sensor_type}") +Sensor.unit = model.Property(f"{Sensor} in {String:unit}") +Sensor.warning_threshold = model.Property( + f"{Sensor} has warning threshold {Float:warning_threshold}" +) +Sensor.critical_threshold = model.Property( + f"{Sensor} has critical threshold {Float:critical_threshold}" +) +Sensor.machine = model.Property(f"{Sensor} attached to {Machine}") + +sensor_src = model.data(sensors_df) +model.define( + s := Sensor.new(sensor_id=sensor_src["sensor_id"]), + s.machine_id_str(sensor_src["machine_id"]), + s.sensor_type(sensor_src["sensor_type"]), + s.unit(sensor_src["unit"]), + s.warning_threshold(sensor_src["warning_threshold"]), + s.critical_threshold(sensor_src["critical_threshold"]), +) +model.define(Sensor.machine(Machine)).where( + Sensor.machine_id_str == Machine.machine_id +) + +# SensorReading concept: periodic sensor measurements with anomaly flags. +SensorReading = model.Concept( + "SensorReading", + identify_by={"sensor_id": String, "machine_id": String, "pid": Integer}, +) +SensorReading.value = model.Property(f"{SensorReading} has value {Float:value}") +SensorReading.is_anomaly = model.Property( + f"{SensorReading} anomaly flag {Integer:is_anomaly}" +) +SensorReading.sensor = model.Property(f"{SensorReading} from {Sensor}") +SensorReading.machine = model.Property(f"{SensorReading} on {Machine}") +SensorReading.period = model.Property(f"{SensorReading} in {Period}") + +sr_src = model.data(sensor_readings_df) +model.define( + sr := SensorReading.new( + sensor_id=sr_src["sensor_id"], + machine_id=sr_src["machine_id"], + pid=sr_src["period"], + ), + sr.value(sr_src["value"]), + sr.is_anomaly(sr_src["is_anomaly"]), +) +SRSensor = Sensor.ref() +SRMachine = Machine.ref() +SRPeriod = Period.ref() +model.define(SensorReading.sensor(SRSensor)).where( + SensorReading.sensor_id == SRSensor.sensor_id +) +model.define(SensorReading.machine(SRMachine)).where( + SensorReading.machine_id == SRMachine.machine_id +) +model.define(SensorReading.period(SRPeriod)).where( + SensorReading.pid == SRPeriod.pid +) + +# FailurePrediction concept: ML-predicted per-period failure probabilities. +# These replace the static Machine.failure_probability in the optimization +# objective, giving period-specific degradation curves. +FailurePrediction = model.Concept( + "FailurePrediction", identify_by={"prediction_id": String} +) +FailurePrediction.machine_id_str = model.Property( + f"{FailurePrediction} for machine {String:machine_id_str}" +) +FailurePrediction.period_int = model.Property( + f"{FailurePrediction} in period {Integer:period_int}" +) +FailurePrediction.failure_probability = model.Property( + f"{FailurePrediction} has failure probability {Float:failure_probability}" +) +FailurePrediction.predicted_failure_mode = model.Property( + f"{FailurePrediction} predicts mode {String:predicted_failure_mode}" +) +FailurePrediction.confidence = model.Property( + f"{FailurePrediction} has confidence {Float:confidence}" +) +FailurePrediction.machine = model.Property(f"{FailurePrediction} for {Machine}") +FailurePrediction.period = model.Property(f"{FailurePrediction} in {Period}") + +fp_src = model.data(failure_pred_df) +model.define( + fp := FailurePrediction.new(prediction_id=fp_src["prediction_id"]), + fp.machine_id_str(fp_src["machine_id"]), + fp.period_int(fp_src["period"]), + fp.failure_probability(fp_src["failure_probability"]), + fp.predicted_failure_mode(fp_src["predicted_failure_mode"]), + fp.confidence(fp_src["confidence"]), +) +FPMachineInit = Machine.ref() +FPPeriodInit = Period.ref() +model.define(FailurePrediction.machine(FPMachineInit)).where( + FailurePrediction.machine_id_str == FPMachineInit.machine_id +) +model.define(FailurePrediction.period(FPPeriodInit)).where( + FailurePrediction.period_int == FPPeriodInit.pid +) + +# DowntimeEvent concept: unplanned and planned downtime events per machine. +DowntimeEvent = model.Concept("DowntimeEvent", identify_by={"event_id": String}) +DowntimeEvent.machine_id_str = model.Property( + f"{DowntimeEvent} for machine {String:machine_id_str}" +) +DowntimeEvent.period_int = model.Property( + f"{DowntimeEvent} in period {Integer:period_int}" +) +DowntimeEvent.fault_category = model.Property( + f"{DowntimeEvent} fault category {String:fault_category}" +) +DowntimeEvent.duration_minutes = model.Property( + f"{DowntimeEvent} lasted {Integer:duration_minutes} minutes" +) +DowntimeEvent.is_planned = model.Property( + f"{DowntimeEvent} planned flag {Integer:is_planned}" +) +DowntimeEvent.machine = model.Property(f"{DowntimeEvent} on {Machine}") + +dt_src = model.data(downtime_df) +model.define( + dt := DowntimeEvent.new(event_id=dt_src["event_id"]), + dt.machine_id_str(dt_src["machine_id"]), + dt.period_int(dt_src["period"]), + dt.fault_category(dt_src["fault_category"]), + dt.duration_minutes(dt_src["duration_minutes"]), + dt.is_planned(dt_src["is_planned"]), +) +model.define(DowntimeEvent.machine(Machine)).where( + DowntimeEvent.machine_id_str == Machine.machine_id +) + +# ProductionRun concept: production output per machine per period. +ProductionRun = model.Concept("ProductionRun", identify_by={"run_id": String}) +ProductionRun.machine_id_str = model.Property( + f"{ProductionRun} for machine {String:machine_id_str}" +) +ProductionRun.period_int = model.Property( + f"{ProductionRun} in period {Integer:period_int}" +) +ProductionRun.planned_quantity = model.Property( + f"{ProductionRun} planned {Integer:planned_quantity} units" +) +ProductionRun.actual_quantity = model.Property( + f"{ProductionRun} produced {Integer:actual_quantity} units" +) +ProductionRun.good_quantity = model.Property( + f"{ProductionRun} good output {Integer:good_quantity} units" +) +ProductionRun.machine = model.Property(f"{ProductionRun} on {Machine}") + +pr_src = model.data(production_df) +model.define( + pr := ProductionRun.new(run_id=pr_src["run_id"]), + pr.machine_id_str(pr_src["machine_id"]), + pr.period_int(pr_src["period"]), + pr.planned_quantity(pr_src["planned_quantity"]), + pr.actual_quantity(pr_src["actual_quantity"]), + pr.good_quantity(pr_src["good_quantity"]), +) +model.define(ProductionRun.machine(Machine)).where( + ProductionRun.machine_id_str == Machine.machine_id +) + +# -------------------------------------------------- +# Cross-product concepts (scheduling decision space) +# -------------------------------------------------- + +# MachinePeriod concept: (machine, period) pairs. +MachinePeriod = model.Concept( + "MachinePeriod", identify_by={"machine_id": String, "pid": Integer} +) +MachinePeriod.machine = model.Property(f"{MachinePeriod} for {Machine}") +MachinePeriod.period = model.Property(f"{MachinePeriod} in {Period}") +MpInitM = Machine.ref() +MpInitP = Period.ref() +model.define( + mp := MachinePeriod.new(machine_id=MpInitM.machine_id, pid=MpInitP.pid), + mp.machine(MpInitM), + mp.period(MpInitP), +) + +# Store per-period failure prediction on MachinePeriod for the objective. +MachinePeriod.predicted_fp = model.Property( + f"{MachinePeriod} has predicted failure probability {Float:predicted_fp}" +) +FPJoin = FailurePrediction.ref() +model.where( + MachinePeriod.machine_id == FPJoin.machine_id_str, + MachinePeriod.pid == FPJoin.period_int, +).define(MachinePeriod.predicted_fp(FPJoin.failure_probability)) + +# TechnicianPeriod concept: technician capacity per period in hours. +TechnicianPeriod = model.Concept( + "TechnicianPeriod", identify_by={"technician_id": String, "pid": Integer} +) +TechnicianPeriod.technician = model.Property(f"{TechnicianPeriod} for {Technician}") +TechnicianPeriod.period = model.Property(f"{TechnicianPeriod} in {Period}") +TechnicianPeriod.capacity_hours = model.Property( + f"{TechnicianPeriod} has available hours {Float:capacity_hours}" +) + +avail_data = model.data(availability_df) +TcInit = Technician.ref() +PrInit = Period.ref() +model.define( + tp := TechnicianPeriod.new( + technician_id=TcInit.technician_id, + pid=PrInit.pid, + capacity_hours=avail_data["available"] * TcInit.max_weekly_hours, + ), + tp.technician(TcInit), + tp.period(PrInit), +).where( + TcInit.technician_id == avail_data["technician_id"], + PrInit.pid == avail_data["period"], +) + +# TechnicianMachinePeriod concept: (technician, machine, period) triples, +# restricted to qualified pairs only. +TechnicianMachinePeriod = model.Concept( + "TechnicianMachinePeriod", + identify_by={"technician_id": String, "machine_id": String, "pid": Integer}, +) +TechnicianMachinePeriod.technician = model.Property( + f"{TechnicianMachinePeriod} for {Technician}" +) +TechnicianMachinePeriod.machine = model.Property( + f"{TechnicianMachinePeriod} for {Machine}" +) +TechnicianMachinePeriod.period = model.Property( + f"{TechnicianMachinePeriod} in {Period}" +) +TechnicianMachinePeriod.same_location = model.Property( + f"{TechnicianMachinePeriod} same location flag {Integer:same_location}" +) + +QualRef = Qualification.ref() +TmpInitTech = Technician.ref() +TmpInitMach = Machine.ref() +TmpInitPer = Period.ref() +model.define( + tmp := TechnicianMachinePeriod.new( + technician_id=TmpInitTech.technician_id, + machine_id=TmpInitMach.machine_id, + pid=TmpInitPer.pid, + ), + tmp.technician(TmpInitTech), + tmp.machine(TmpInitMach), + tmp.period(TmpInitPer), +).where( + QualRef.technician(TmpInitTech), + QualRef.machine_type_str == TmpInitMach.machine_type, +) + +# Derived property: same_location flag (1 if co-located, 0 otherwise). +TmpRef = TechnicianMachinePeriod.ref() +TmpTech = Technician.ref() +TmpMach = Machine.ref() +model.where( + TmpRef.technician(TmpTech), + TmpRef.machine(TmpMach), + TmpTech.base_location == TmpMach.location, +).define(TmpRef.same_location(1)) +model.where( + TmpRef.technician(TmpTech), + TmpRef.machine(TmpMach), + TmpTech.base_location != TmpMach.location, +).define(TmpRef.same_location(0)) + +# -------------------------------------------------- +# Machine-level derived aggregates (for querying & rules) +# -------------------------------------------------- + +# Production aggregates: total planned, actual, and good quantities. +Machine.total_planned_qty = model.Property( + f"{Machine} has total planned qty {Float:total_planned_qty}" +) +Machine.total_actual_qty = model.Property( + f"{Machine} has total actual qty {Float:total_actual_qty}" +) +Machine.total_good_qty = model.Property( + f"{Machine} has total good qty {Float:total_good_qty}" +) +model.define(Machine.total_planned_qty( + aggs.sum(ProductionRun.planned_quantity).per(Machine) + .where(ProductionRun.machine(Machine)) | 0 +)) +model.define(Machine.total_actual_qty( + aggs.sum(ProductionRun.actual_quantity).per(Machine) + .where(ProductionRun.machine(Machine)) | 0 +)) +model.define(Machine.total_good_qty( + aggs.sum(ProductionRun.good_quantity).per(Machine) + .where(ProductionRun.machine(Machine)) | 0 +)) + +# Performance ratio (actual / planned) and quality ratio (good / actual). +Machine.performance_ratio = model.Property( + f"{Machine} has performance ratio {Float:performance_ratio}" +) +Machine.quality_ratio = model.Property( + f"{Machine} has quality ratio {Float:quality_ratio}" +) +model.where(Machine.total_planned_qty > 0).define( + Machine.performance_ratio( + floats.float(Machine.total_actual_qty) + / floats.float(Machine.total_planned_qty) + ) +) +model.where(Machine.total_actual_qty > 0).define( + Machine.quality_ratio( + floats.float(Machine.total_good_qty) + / floats.float(Machine.total_actual_qty) + ) +) + +# Downtime aggregates: total downtime minutes and event count. +Machine.total_downtime_minutes = model.Property( + f"{Machine} has total downtime {Float:total_downtime_minutes} minutes" +) +Machine.downtime_event_count = model.Property( + f"{Machine} has downtime event count {Float:downtime_event_count}" +) +model.define(Machine.total_downtime_minutes( + aggs.sum(DowntimeEvent.duration_minutes).per(Machine) + .where(DowntimeEvent.machine(Machine)) | 0 +)) +model.define(Machine.downtime_event_count( + aggs.count(DowntimeEvent).per(Machine) + .where(DowntimeEvent.machine(Machine)) | 0 +)) + +# Sensor anomaly count across all periods. +Machine.anomaly_count = model.Property( + f"{Machine} has anomaly count {Float:anomaly_count}" +) +model.define(Machine.anomaly_count( + aggs.count(SensorReading).per(Machine).where( + SensorReading.machine(Machine), + SensorReading.is_anomaly == 1, + ) | 0 +)) + +# -------------------------------------------------- +# Stage 0: Querying -- Operational Intelligence +# -------------------------------------------------- + +print("=" * 70) +print("STAGE 0: Querying -- Operational Intelligence") +print("=" * 70) + +# 0a. OEE proxy by facility (Performance x Quality). +# Quality is uniformly high (~98%); the differentiator is Performance. +oee_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.facility.alias("facility"), + Machine.performance_ratio.alias("performance"), + Machine.quality_ratio.alias("quality"), + ) + .to_df() +) +oee_by_fac = ( + oee_df.groupby("facility") + .agg(avg_perf=("performance", "mean"), avg_qual=("quality", "mean")) + .reset_index() +) +oee_by_fac["oee_proxy"] = oee_by_fac["avg_perf"] * oee_by_fac["avg_qual"] +oee_by_fac = oee_by_fac.sort_values("oee_proxy", ascending=False) + +print("\nOEE proxy by facility (Performance x Quality):") +for _, row in oee_by_fac.iterrows(): + print( + f" {row['facility']}: " + f"Perf={row['avg_perf']:.1%}, Qual={row['avg_qual']:.1%}, " + f"OEE={row['oee_proxy']:.1%}" + ) + +# 0b. Sensor anomalies: machines with above-threshold readings. +SensorQ = Sensor.ref() +anomaly_detail_df = ( + model.select( + SensorReading.machine_id.alias("machine_id"), + SensorReading.pid.alias("period"), + SensorReading.value.alias("value"), + SensorQ.sensor_type.alias("sensor_type"), + SensorQ.warning_threshold.alias("warning"), + SensorQ.critical_threshold.alias("critical"), + ) + .where( + SensorReading.is_anomaly == 1, + SensorReading.sensor(SensorQ), + ) + .to_df() + .sort_values(["machine_id", "period"]) +) +anomaly_counts = anomaly_detail_df.groupby("machine_id").size().reset_index(name="count") +anomaly_counts = anomaly_counts.merge( + machines_df[["machine_id", "machine_type", "facility"]], on="machine_id" +).sort_values("count", ascending=False) + +print(f"\nSensor anomalies ({len(anomaly_detail_df)} readings across " + f"{len(anomaly_counts)} machines):") +for _, row in anomaly_counts.iterrows(): + print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " + f"{row['count']} anomalies") + +by_fac = anomaly_counts.groupby("facility")["count"].sum() +print(f" By facility: {dict(by_fac.sort_values(ascending=False))}") + +# 0c. Failure trajectories: identify machines with steepest degradation. +FPMachQ = Machine.ref() +fp_query_df = ( + model.select( + FailurePrediction.machine_id_str.alias("machine_id"), + FPMachQ.machine_type.alias("machine_type"), + FPMachQ.facility.alias("facility"), + FailurePrediction.period_int.alias("period"), + FailurePrediction.failure_probability.alias("failure_probability"), + FailurePrediction.predicted_failure_mode.alias("failure_mode"), + ) + .where(FailurePrediction.machine(FPMachQ)) + .to_df() +) + +pivot = fp_query_df.pivot_table( + index=["machine_id", "machine_type", "facility", "failure_mode"], + columns="period", + values="failure_probability", +).reset_index() +pivot["delta"] = pivot[PERIOD_HORIZON] - pivot[1] +pivot = pivot.sort_values("delta", ascending=False) + +print(f"\nSteepest failure trajectories (period 1 -> {PERIOD_HORIZON}):") +for _, row in pivot.head(6).iterrows(): + print( + f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " + f"{row[1]:.3f} -> {row[PERIOD_HORIZON]:.3f} " + f"(+{row['delta']:.3f}) [{row['failure_mode']}]" + ) + +# -------------------------------------------------- +# Stage 1: Graph -- dependency clusters & centrality +# -------------------------------------------------- + +# Graph directly on Machine — no mirror concept needed. +dep_graph = Graph(model, directed=False, weighted=False, node_concept=Machine, aggregator="sum") + +m1 = Machine.ref("m1") +m2 = Machine.ref("m2") +q1 = Qualification.ref("q1") +q2 = Qualification.ref("q2") +# Two machines are adjacent in the dependency graph when at least one +# technician is qualified to service both machine types. +model.define(dep_graph.Edge.new(src=m1, dst=m2)).where( + m1.machine_type == q1.machine_type_str, + m2.machine_type == q2.machine_type_str, + q1.technician_id == q2.technician_id, + m1.machine_id < m2.machine_id, +) + +print(f"\n{'=' * 70}") +print("STAGE 1: Graph Analysis -- Dependency Clusters & Centrality") +print("=" * 70) + +dep_graph.num_nodes().inspect() +dep_graph.num_edges().inspect() + +# Weakly connected components: identify dependency clusters. +wcc = dep_graph.weakly_connected_component() + +node_ref = dep_graph.Node.ref("n") +comp_ref = dep_graph.Node.ref("comp") + +wcc_df = ( + model.where(wcc(node_ref, comp_ref)) + .select( + node_ref.machine_id.alias("machine_id"), + node_ref.machine_name.alias("machine_name"), + node_ref.machine_type.alias("machine_type"), + node_ref.facility.alias("facility"), + comp_ref.machine_id.alias("component_id"), + aggs.count(node_ref).per(comp_ref).alias("cluster_size"), + ) + .to_df() +) + +num_clusters = wcc_df["component_id"].nunique() +print(f"\nDependency clusters found: {num_clusters}") +for comp_id in sorted(wcc_df["component_id"].unique()): + comp_df = wcc_df[wcc_df["component_id"] == comp_id] + cluster_size = int(comp_df["cluster_size"].iloc[0]) + facilities = ", ".join(sorted(comp_df["facility"].unique())) + print(f"\n Cluster {comp_id}: {cluster_size} machines ({facilities})") + for _, row in comp_df.sort_values(["facility", "machine_name"]).head(5).iterrows(): + print(f" - {row['machine_name']} ({row['machine_type']}, {row['facility']})") + if cluster_size > 5: + print(f" ... and {cluster_size - 5} more") + +# Betweenness centrality: find bottleneck machines. +betweenness = dep_graph.betweenness_centrality() + +node_b = dep_graph.Node.ref("nb") +btwn_score = Float.ref("btwn") + +betweenness_df = ( + model.where(betweenness(node_b, btwn_score)) + .select( + node_b.machine_id.alias("machine_id"), + node_b.machine_name.alias("machine_name"), + node_b.machine_type.alias("machine_type"), + node_b.facility.alias("facility"), + node_b.failure_probability.alias("failure_probability"), + btwn_score.alias("betweenness"), + ) + .to_df() + .sort_values("betweenness", ascending=False) + .reset_index(drop=True) +) + +print("\nTop bottleneck machines (betweenness centrality):") +for _, row in betweenness_df.head(10).iterrows(): + print( + f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " + f"betweenness={row['betweenness']:.4f}, " + f"failure_prob={row['failure_probability']:.3f}" + ) + +# Store normalized betweenness directly on Machine. +Machine.betweenness_raw = model.Property( + f"{Machine} has raw betweenness centrality {Float:betweenness_raw}" +) +m_btwn = Machine.ref("m_btwn") +model.define(m_btwn.betweenness_raw(btwn_score)).where(betweenness(m_btwn, btwn_score)) +max_betweenness = max(Machine.betweenness_raw) +Machine.betweenness = model.Property( + f"{Machine} has betweenness centrality {Float:betweenness}" +) +m_norm = Machine.ref("m_norm") +model.where(max_betweenness == 0).define(m_norm.betweenness(0.0)) +model.where(max_betweenness > 0).define( + m_norm.betweenness(m_norm.betweenness_raw / max_betweenness) +) + +# -------------------------------------------------- +# Stage 2: Rules -- compliance flags & composite risk tier +# -------------------------------------------------- + +print(f"\n{'=' * 70}") +print("STAGE 2: Rules -- Compliance Flags & Composite Risk Tier") +print("=" * 70) + +# Rule 1: Machine is overdue for maintenance when remaining useful life +# is less than the time required to perform maintenance. +Machine.is_overdue_maintenance = model.Relationship( + f"{Machine} is overdue maintenance" +) +model.where( + Machine.remaining_useful_life < floats.float(Machine.maintenance_duration_hours) +).define(Machine.is_overdue_maintenance()) + +overdue_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_name.alias("machine_name"), + Machine.facility.alias("facility"), + Machine.remaining_useful_life.alias("remaining_useful_life"), + Machine.maintenance_duration_hours.alias("maintenance_duration_hours"), + ) + .where(Machine.is_overdue_maintenance()) + .to_df() +) +print(f"\nOverdue maintenance ({len(overdue_df)} machines):") +for _, row in overdue_df.iterrows(): + print( + f" {row['machine_id']} ({row['machine_name']}): " + f"RUL={row['remaining_useful_life']:.1f}h < " + f"duration={int(row['maintenance_duration_hours'])}h" + ) + +# Rule 2: Machine is high risk when failure probability > 0.3 AND +# criticality >= 4. +Machine.is_high_risk = model.Relationship(f"{Machine} is high risk") +model.where( + Machine.failure_probability > 0.3, + Machine.criticality >= 4, +).define(Machine.is_high_risk()) + +high_risk_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_name.alias("machine_name"), + Machine.failure_probability.alias("failure_probability"), + Machine.criticality.alias("criticality"), + ) + .where(Machine.is_high_risk()) + .to_df() +) +print(f"\nHigh-risk machines ({len(high_risk_df)}):") +for _, row in high_risk_df.iterrows(): + print( + f" {row['machine_id']} ({row['machine_name']}): " + f"prob={row['failure_probability']:.3f}, crit={int(row['criticality'])}" + ) + +# Rule 3: Machine has sensor anomalies. +Machine.is_anomalous = model.Relationship(f"{Machine} has sensor anomalies") +model.where(Machine.anomaly_count > 0).define(Machine.is_anomalous()) + +anomalous_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_name.alias("machine_name"), + Machine.facility.alias("facility"), + Machine.anomaly_count.alias("anomaly_count"), + ) + .where(Machine.is_anomalous()) + .to_df() + .sort_values("anomaly_count", ascending=False) +) +print(f"\nAnomalous machines ({len(anomalous_df)}):") +for _, row in anomalous_df.iterrows(): + print( + f" {row['machine_id']} ({row['machine_name']}, {row['facility']}): " + f"{int(row['anomaly_count'])} anomalies" + ) + +# Rule 4: Machine has chronic downtime (event count > threshold). +Machine.is_chronic_downtime = model.Relationship(f"{Machine} has chronic downtime") +model.where( + Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD +).define(Machine.is_chronic_downtime()) + +chronic_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_name.alias("machine_name"), + Machine.facility.alias("facility"), + Machine.downtime_event_count.alias("event_count"), + Machine.total_downtime_minutes.alias("total_minutes"), + ) + .where(Machine.is_chronic_downtime()) + .to_df() + .sort_values("event_count", ascending=False) +) +print(f"\nChronic downtime machines (>{CHRONIC_DOWNTIME_THRESHOLD} events, " + f"{len(chronic_df)} machines):") +for _, row in chronic_df.iterrows(): + print( + f" {row['machine_id']} ({row['machine_name']}, {row['facility']}): " + f"{int(row['event_count'])} events, " + f"{int(row['total_minutes'])} min total downtime" + ) + +# Rule 5: Composite risk tier -- chains overdue, high-risk, and chronic +# downtime flags into a single classification. +Machine.risk_tier = model.Property(f"{Machine} has risk tier {String:risk_tier}") + +# Critical: all 3 flags. +model.where( + Machine.is_chronic_downtime(), + Machine.is_high_risk(), + Machine.is_overdue_maintenance(), +).define(Machine.risk_tier("Critical")) + +# Elevated: exactly 2 of 3 flags (enumerate pairs, negate the third). +model.where( + Machine.is_chronic_downtime(), + Machine.is_high_risk(), + model.not_(Machine.is_overdue_maintenance()), +).define(Machine.risk_tier("Elevated")) +model.where( + Machine.is_chronic_downtime(), + model.not_(Machine.is_high_risk()), + Machine.is_overdue_maintenance(), +).define(Machine.risk_tier("Elevated")) +model.where( + model.not_(Machine.is_chronic_downtime()), + Machine.is_high_risk(), + Machine.is_overdue_maintenance(), +).define(Machine.risk_tier("Elevated")) + +# Standard: 0 or 1 flag. +model.where( + model.not_(Machine.is_chronic_downtime()), + model.not_(Machine.is_high_risk()), + model.not_(Machine.is_overdue_maintenance()), +).define(Machine.risk_tier("Standard")) +model.where( + Machine.is_chronic_downtime(), + model.not_(Machine.is_high_risk()), + model.not_(Machine.is_overdue_maintenance()), +).define(Machine.risk_tier("Standard")) +model.where( + model.not_(Machine.is_chronic_downtime()), + Machine.is_high_risk(), + model.not_(Machine.is_overdue_maintenance()), +).define(Machine.risk_tier("Standard")) +model.where( + model.not_(Machine.is_chronic_downtime()), + model.not_(Machine.is_high_risk()), + Machine.is_overdue_maintenance(), +).define(Machine.risk_tier("Standard")) + +risk_tier_df = ( + model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_name.alias("machine_name"), + Machine.machine_type.alias("machine_type"), + Machine.facility.alias("facility"), + Machine.risk_tier.alias("risk_tier"), + ) + .to_df() + .sort_values("risk_tier") +) +print("\nComposite risk tier:") +for tier in ["Critical", "Elevated", "Standard"]: + tier_machines = risk_tier_df[risk_tier_df["risk_tier"] == tier] + ids = ", ".join(tier_machines["machine_id"].tolist()) + print(f" {tier} ({len(tier_machines)}): {ids}") + +# Rule 6: Parts inventory needs reorder. +PartsInventory.needs_reorder = model.Relationship( + f"{PartsInventory} needs reorder" +) +model.where( + PartsInventory.stock_level <= PartsInventory.min_order_qty +).define(PartsInventory.needs_reorder()) + +reorder_df = ( + model.select( + PartsInventory.part_id.alias("part_id"), + PartsInventory.part_name.alias("part_name"), + PartsInventory.facility.alias("facility"), + PartsInventory.stock_level.alias("stock_level"), + PartsInventory.min_order_qty.alias("min_order_qty"), + ) + .where(PartsInventory.needs_reorder()) + .to_df() +) +print(f"\nParts needing reorder ({len(reorder_df)}):") +for _, row in reorder_df.iterrows(): + print( + f" {row['part_id']} ({row['part_name']}, {row['facility']}): " + f"stock={int(row['stock_level'])} <= min_order={int(row['min_order_qty'])}" + ) + +# Rule 7: Certification is expiring when fewer than 30 days remain. +CertificationExpiry.is_expiring = model.Relationship( + f"{CertificationExpiry} is expiring" +) +model.where( + CertificationExpiry.days_remaining < 30 +).define(CertificationExpiry.is_expiring()) + +TechRef = Technician.ref() +expiring_df = ( + model.select( + TechRef.technician_id.alias("technician_id"), + TechRef.technician_name.alias("technician_name"), + CertificationExpiry.machine_type.alias("machine_type"), + CertificationExpiry.days_remaining.alias("days_remaining"), + ) + .where( + CertificationExpiry.is_expiring(), + CertificationExpiry.technician(TechRef), + ) + .to_df() +) +print(f"\nExpiring certifications ({len(expiring_df)}):") +for _, row in expiring_df.iterrows(): + print( + f" {row['technician_id']} ({row['technician_name']}): " + f"{row['machine_type']} -- {int(row['days_remaining'])} days remaining" + ) + +# -------------------------------------------------- +# Stage 3: Prescriptive -- maintenance scheduling +# -------------------------------------------------- + +print(f"\n{'=' * 70}") +print("STAGE 3: Prescriptive -- Maintenance Scheduling") +print("=" * 70) + +problem = Problem(model, Float) + +# References for aggregation. +MachinePeriod_outer = MachinePeriod.ref() +MachinePeriod_inner = MachinePeriod.ref() +TechnicianMachinePeriod_ref = TechnicianMachinePeriod.ref() +Machine_ref = Machine.ref() +Period_outer = Period.ref() +Period_inner = Period.ref() +Technician_ref = Technician.ref() +Period_tc = Period.ref() +MachinePeriod_cap = MachinePeriod.ref() +Period_cap = Period.ref() +TechnicianPeriod_ref = TechnicianPeriod.ref() + +# Decision variable: maintain -- whether to maintain machine m in period t. +MachinePeriod.x_maintain = model.Property( + f"{MachinePeriod} maintain decision {Float:x_maintain}" +) +problem.solve_for( + MachinePeriod.x_maintain, + type="bin", + name=["maintain", MachinePeriod.machine_id, MachinePeriod.pid], +) + +# Decision variable: vulnerable -- whether machine m remains unmaintained +# through period t. +MachinePeriod.x_vulnerable = model.Property( + f"{MachinePeriod} vulnerable flag {Float:x_vulnerable}" +) +problem.solve_for( + MachinePeriod.x_vulnerable, + type="bin", + name=["vulnerable", MachinePeriod.machine_id, MachinePeriod.pid], +) + +# Decision variable: assigned -- whether technician k is assigned to +# machine m in period t. +TechnicianMachinePeriod.x_assigned = model.Property( + f"{TechnicianMachinePeriod} assigned flag {Float:x_assigned}" +) +problem.solve_for( + TechnicianMachinePeriod.x_assigned, + type="bin", + name=[ + "assigned", + TechnicianMachinePeriod.technician_id, + TechnicianMachinePeriod.machine_id, + TechnicianMachinePeriod.pid, + ], +) + +# Constraint: cumulative maintenance coverage. +# For each (machine, tau): sum_{t=1..tau} x_maintain(m,t) + x_vulnerable(m,tau) = 1. +maintained_until_tau = ( + sum(MachinePeriod_inner.x_maintain) + .where( + MachinePeriod_outer.machine(Machine_ref), + MachinePeriod_outer.period(Period_outer), + MachinePeriod_inner.machine(Machine_ref), + MachinePeriod_inner.period(Period_inner), + Period_inner.pid >= 1, + Period_inner.pid <= Period_outer.pid, + ) + .per(Machine_ref, Period_outer) +) +problem.satisfy( + model.require(maintained_until_tau + MachinePeriod_outer.x_vulnerable == 1).where( + MachinePeriod_outer.machine(Machine_ref), + MachinePeriod_outer.period(Period_outer), + ) +) + +# Constraint: assignment-maintenance linkage. +assign_per_mp = ( + sum(TechnicianMachinePeriod_ref.x_assigned) + .where( + TechnicianMachinePeriod_ref.machine(Machine_ref), + TechnicianMachinePeriod_ref.period(Period_outer), + ) + .per(Machine_ref, Period_outer) +) +problem.satisfy( + model.require(assign_per_mp == MachinePeriod_outer.x_maintain).where( + MachinePeriod_outer.machine(Machine_ref), + MachinePeriod_outer.period(Period_outer), + ) +) + +# Constraint: technician hours capacity. +Machine_hrs = Machine.ref() +assigned_hours = ( + sum( + TechnicianMachinePeriod_ref.x_assigned + * Machine_hrs.maintenance_duration_hours + ) + .where( + TechnicianMachinePeriod_ref.technician(Technician_ref), + TechnicianMachinePeriod_ref.period(Period_tc), + TechnicianMachinePeriod_ref.machine(Machine_hrs), + ) + .per(Technician_ref, Period_tc) +) +avail_hours = ( + sum(TechnicianPeriod_ref.capacity_hours) + .where( + TechnicianPeriod_ref.technician(Technician_ref), + TechnicianPeriod_ref.period(Period_tc), + ) + .per(Technician_ref, Period_tc) +) +problem.satisfy(model.require(assigned_hours <= avail_hours)) + +# Constraint: parts/bay capacity per period. +maint_per_period = ( + sum(MachinePeriod_cap.x_maintain) + .where(MachinePeriod_cap.period(Period_cap)) + .per(Period_cap) +) +problem.satisfy(model.require(maint_per_period <= PARTS_CAPACITY_PER_PERIOD)) + +# Constraint (from rules): overdue machines must be maintained by OVERDUE_DEADLINE. +MachinePeriod_overdue = MachinePeriod.ref() +Machine_overdue = Machine.ref() +Period_overdue = Period.ref() +maintained_by_deadline = ( + sum(MachinePeriod_overdue.x_maintain) + .where( + MachinePeriod_overdue.machine(Machine_overdue), + MachinePeriod_overdue.period(Period_overdue), + Period_overdue.pid <= OVERDUE_DEADLINE, + ) + .per(Machine_overdue) +) +problem.satisfy( + model.require(maintained_by_deadline >= 1).where( + Machine_overdue.is_overdue_maintenance() + ) +) + +# Objective: minimize expected total cost. +# 1. Failure risk: per-period failure prediction (from Stage 0) * parts cost +# * criticality * (1 + centrality_weight * betweenness from Stage 1). +# 2. Labor cost: maintenance_duration * technician hourly_rate. +# 3. Travel cost: flat rate * duration when technician is not co-located. +Machine_obj = Machine.ref() +Technician_obj = Technician.ref() +Machine_labor = Machine.ref() +Machine_travel = Machine.ref() +failure_cost = sum( + MachinePeriod_outer.x_vulnerable + * MachinePeriod_outer.predicted_fp + * Machine_obj.estimated_parts_cost + * Machine_obj.criticality + * (1 + CENTRALITY_WEIGHT * Machine_obj.betweenness) +).where( + MachinePeriod_outer.machine(Machine_obj), MachinePeriod_outer.period(Period_outer) +) +labor_cost = sum( + TechnicianMachinePeriod_ref.x_assigned + * Machine_labor.maintenance_duration_hours + * Technician_obj.hourly_rate +).where( + TechnicianMachinePeriod_ref.machine(Machine_labor), + TechnicianMachinePeriod_ref.technician(Technician_obj), + TechnicianMachinePeriod_ref.period(Period_outer), +) +travel_cost = sum( + TechnicianMachinePeriod_ref.x_assigned + * (1 - TechnicianMachinePeriod_ref.same_location) + * Machine_travel.maintenance_duration_hours + * TRAVEL_COST_PER_HOUR +).where( + TechnicianMachinePeriod_ref.machine(Machine_travel), + TechnicianMachinePeriod_ref.period(Period_outer), +) +problem.minimize(failure_cost + labor_cost + travel_cost) + +# -------------------------------------------------- +# Solve and extract results +# -------------------------------------------------- + +problem.solve("highs", time_limit_sec=120) +si = problem.solve_info() +si.display() + +print(f"\nStatus: {si.termination_status}") +print(f"Objective value: {si.objective_value:.2f}") +assert si.termination_status == "OPTIMAL", f"Expected OPTIMAL, got {si.termination_status}" + +# Single solve — query populated properties directly. +value_ref = Float.ref() +maint_machine = Machine.ref("maint_machine") +maint_df = ( + model.select( + MachinePeriod.machine_id.alias("machine_id"), + MachinePeriod.pid.alias("period"), + maint_machine.machine_type.alias("machine_type"), + maint_machine.facility.alias("facility"), + maint_machine.criticality.alias("criticality"), + ) + .where( + MachinePeriod.machine(maint_machine), + MachinePeriod.x_maintain(value_ref), + value_ref > 0.5, + ) + .to_df() +) +maint_df = maint_df.sort_values(["period", "machine_id"]) +print(f"\nMaintenance schedule ({len(maint_df)} jobs):") +for period, g in maint_df.groupby("period"): + print(f" Period {int(period)}:") + for _, row in g.iterrows(): + print( + f" {row['machine_id']} ({row['machine_type']}, {row['facility']}, " + f"crit={int(row['criticality'])})" + ) + +# Query populated assignment properties directly. +assign_machine = Machine.ref("assign_machine") +assign_tech = Technician.ref("assign_tech") +assign_df = ( + model.select( + TechnicianMachinePeriod.technician_id.alias("technician_id"), + TechnicianMachinePeriod.machine_id.alias("machine_id"), + TechnicianMachinePeriod.pid.alias("period"), + assign_machine.machine_type.alias("machine_type"), + assign_machine.location.alias("location"), + assign_machine.maintenance_duration_hours.alias("maintenance_duration_hours"), + assign_tech.technician_name.alias("technician_name"), + assign_tech.base_location.alias("base_location"), + assign_tech.skill_level.alias("skill_level"), + assign_tech.hourly_rate.alias("hourly_rate"), + ) + .where( + TechnicianMachinePeriod.machine(assign_machine), + TechnicianMachinePeriod.technician(assign_tech), + TechnicianMachinePeriod.x_assigned(value_ref), + value_ref > 0.5, + ) + .to_df() +) +assign_df = assign_df.sort_values(["period", "machine_id"]) +print(f"\nTechnician assignments ({len(assign_df)}):") +for period, g in assign_df.groupby("period"): + print(f" Period {int(period)}:") + for _, row in g.iterrows(): + travel = "" if row["base_location"] == row["location"] else " [TRAVEL]" + cost = row["maintenance_duration_hours"] * row["hourly_rate"] + print( + f" {row['machine_id']}: {row['technician_id']} " + f"({int(row['maintenance_duration_hours'])}h x " + f"${row['hourly_rate']:.0f}/h = ${cost:.0f}){travel}" + ) + +# -------------------------------------------------- +# Stage 4: Resilience -- concentration risk & cross-training +# -------------------------------------------------- + +print(f"\n{'=' * 70}") +print("STAGE 4: Resilience -- Concentration Risk Analysis") +print("=" * 70) + +# -------------------------------------------------- +# Materialize prescriptive output as ontology concepts. +# These bindings turn the post-solve x_maintain / x_assigned / x_vulnerable +# property values into queryable ontology rather than ad-hoc pandas frames. +# -------------------------------------------------- + +# MaintenancePlan: singleton capturing the optimizer's cost breakdown. +MaintenancePlan = model.Concept( + "MaintenancePlan", identify_by={"key": Integer} +) +MaintenancePlan.objective = model.Property( + f"{MaintenancePlan} has objective {Float:objective}" +) +MaintenancePlan.failure_cost = model.Property( + f"{MaintenancePlan} has failure cost {Float:failure_cost}" +) +MaintenancePlan.labor_cost = model.Property( + f"{MaintenancePlan} has labor cost {Float:labor_cost}" +) +MaintenancePlan.travel_cost = model.Property( + f"{MaintenancePlan} has travel cost {Float:travel_cost}" +) +MaintenancePlan.total_jobs = model.Property( + f"{MaintenancePlan} has total jobs {Integer:total_jobs}" +) + +# Seed the singleton and bind the optimizer's reported objective onto it. +plan_data = model.data([{"key": 1, "obj_val": float(si.objective_value)}]) +model.define( + plan_seed := MaintenancePlan.new(key=plan_data["key"]), + plan_seed.objective(plan_data["obj_val"]), +) + +# Aggregate the cost components and job count off the post-solve properties. +plan_ref = MaintenancePlan.ref() +mp_fc = MachinePeriod.ref() +m_fc = Machine.ref() +model.define( + plan_ref.failure_cost( + aggs.sum( + mp_fc.x_vulnerable + * mp_fc.predicted_fp + * m_fc.estimated_parts_cost + * m_fc.criticality + * (1 + CENTRALITY_WEIGHT * m_fc.betweenness) + ).where(mp_fc.machine(m_fc)) + ) +) + +tmp_lc = TechnicianMachinePeriod.ref() +m_lc = Machine.ref() +t_lc = Technician.ref() +model.define( + plan_ref.labor_cost( + aggs.sum( + tmp_lc.x_assigned + * m_lc.maintenance_duration_hours + * t_lc.hourly_rate + ).where( + tmp_lc.machine(m_lc), + tmp_lc.technician(t_lc), + ) + ) +) + +tmp_tc = TechnicianMachinePeriod.ref() +m_tc = Machine.ref() +model.define( + plan_ref.travel_cost( + aggs.sum( + tmp_tc.x_assigned + * (1 - tmp_tc.same_location) + * m_tc.maintenance_duration_hours + * TRAVEL_COST_PER_HOUR + ).where(tmp_tc.machine(m_tc)) + ) +) + +mp_jobs = MachinePeriod.ref() +model.define( + plan_ref.total_jobs( + aggs.count(mp_jobs).where(mp_jobs.x_maintain > 0.5) + ) +) + +# TypeConcentration: per-machine-type concentration analysis. +TypeConcentration = model.Concept( + "TypeConcentration", identify_by={"machine_type": String} +) +TypeConcentration.qualified_tech_count = model.Property( + f"{TypeConcentration} has {Integer:qualified_tech_count} qualified techs" +) +TypeConcentration.qualified_tech_locations = model.Property( + f"{TypeConcentration} has tech locations {String:qualified_tech_locations}" +) +TypeConcentration.is_concentrated = model.Property( + f"{TypeConcentration} concentration flag {Boolean:is_concentrated}" +) +TypeConcentration.scheduled_jobs_total = model.Property( + f"{TypeConcentration} has {Integer:scheduled_jobs_total} scheduled jobs" +) +TypeConcentration.scheduled_jobs_traveling = model.Property( + f"{TypeConcentration} has {Integer:scheduled_jobs_traveling} traveling jobs" +) +TypeConcentration.travel_pct = model.Property( + f"{TypeConcentration} has travel pct {Float:travel_pct}" +) + +# Seed: one TypeConcentration per distinct machine_type appearing in +# Qualification (the population of types we have any tech for). +qref_seed = Qualification.ref() +model.define( + TypeConcentration.new(machine_type=qref_seed.machine_type_str) +) + +# qualified_tech_count: distinct techs qualified for this machine_type. +tc_qc = TypeConcentration.ref() +qref_qc = Qualification.ref() +tref_qc = Technician.ref() +model.define( + tc_qc.qualified_tech_count( + aggs.count(distinct(tref_qc)) + .where( + qref_qc.machine_type_str == tc_qc.machine_type, + qref_qc.technician(tref_qc), + ) + .per(tc_qc) + ) +) + +# Helper concept: distinct (machine_type, location) pairs derived from the +# qualified-technician join. Compound identity gives one entity per unique +# pair; used downstream by distinct_loc_count. +TypeLocation = model.Concept( + "TypeLocation", + identify_by={"machine_type": String, "location": String}, +) +qref_tl = Qualification.ref() +tref_tl = Technician.ref() +model.define( + TypeLocation.new( + machine_type=qref_tl.machine_type_str, + location=tref_tl.base_location, + ) +).where(qref_tl.technician(tref_tl)) + +# qualified_tech_locations: comma-joined distinct base_locations of +# qualified techs. Built in pandas because string_join is not yet supported +# by the LQP backend; bound onto TypeConcentration via model.data. +_loc_pairs = ( + qualifications_df.merge( + technicians_df[["technician_id", "base_location"]], on="technician_id" + )[["machine_type", "base_location"]] + .drop_duplicates() + .sort_values(["machine_type", "base_location"]) +) +_loc_str_rows = [ + {"mtype": mt, "loc_str": ", ".join(sorted(g["base_location"].unique()))} + for mt, g in _loc_pairs.groupby("machine_type") +] +loc_str_data = model.data(_loc_str_rows) +tc_locs = TypeConcentration.ref() +model.define(tc_locs.qualified_tech_locations(loc_str_data["loc_str"])).where( + tc_locs.machine_type == loc_str_data["mtype"] +) + +# distinct_loc_count: helper to drive the is_concentrated flag, computed +# off the TypeLocation pairs (one entity per distinct location). +TypeConcentration.distinct_loc_count = model.Property( + f"{TypeConcentration} has {Integer:distinct_loc_count} distinct tech locations" +) +tc_dlc = TypeConcentration.ref() +tl_dlc = TypeLocation.ref() +model.define( + tc_dlc.distinct_loc_count( + aggs.count(tl_dlc) + .where(tl_dlc.machine_type == tc_dlc.machine_type) + .per(tc_dlc) + ) +) + +# is_concentrated: True iff all qualified techs share a single base_location. +model.where(TypeConcentration.distinct_loc_count == 1).define( + TypeConcentration.is_concentrated(True) +) +model.where(TypeConcentration.distinct_loc_count > 1).define( + TypeConcentration.is_concentrated(False) +) + +# scheduled_jobs_total: count of scheduled (machine, period) jobs for this type. +tc_sjt = TypeConcentration.ref() +mp_sjt = MachinePeriod.ref() +m_sjt = Machine.ref() +model.define( + tc_sjt.scheduled_jobs_total( + aggs.count(mp_sjt) + .where( + mp_sjt.machine(m_sjt), + m_sjt.machine_type == tc_sjt.machine_type, + mp_sjt.x_maintain > 0.5, + ) + .per(tc_sjt) + | 0 + ) +) + +# scheduled_jobs_traveling: count of scheduled assignments where the +# assigned technician's base_location differs from the machine's location. +tc_sjr = TypeConcentration.ref() +tmp_sjr = TechnicianMachinePeriod.ref() +m_sjr = Machine.ref() +model.define( + tc_sjr.scheduled_jobs_traveling( + aggs.count(tmp_sjr) + .where( + tmp_sjr.machine(m_sjr), + m_sjr.machine_type == tc_sjr.machine_type, + tmp_sjr.x_assigned > 0.5, + tmp_sjr.same_location == 0, + ) + .per(tc_sjr) + | 0 + ) +) + +# travel_pct: 100 * traveling / total (only when total > 0). +model.where(TypeConcentration.scheduled_jobs_total > 0).define( + TypeConcentration.travel_pct( + floats.float(TypeConcentration.scheduled_jobs_traveling) + / floats.float(TypeConcentration.scheduled_jobs_total) + * 100.0 + ) +) + +# CrossTrainingRecommendation: cheapest non-local cross-training candidate +# per concentrated machine type. One row per concentrated machine_type. +CrossTrainingRecommendation = model.Concept( + "CrossTrainingRecommendation", identify_by={"machine_type": String} +) +CrossTrainingRecommendation.tech_id = model.Property( + f"{CrossTrainingRecommendation} has {String:tech_id}" +) +CrossTrainingRecommendation.tech_name = model.Property( + f"{CrossTrainingRecommendation} has {String:tech_name}" +) +CrossTrainingRecommendation.cost = model.Property( + f"{CrossTrainingRecommendation} has {Float:cost}" +) +CrossTrainingRecommendation.duration_weeks = model.Property( + f"{CrossTrainingRecommendation} has {Integer:duration_weeks} weeks" +) +CrossTrainingRecommendation.is_best_candidate = model.Property( + f"{CrossTrainingRecommendation} best flag {Boolean:is_best_candidate}" +) + +# A TrainingOption is "non-local" for a concentrated machine type when the +# candidate technician's base_location is NOT one of the locations where the +# qualified techs already sit. For singly-concentrated types that simplifies +# to: candidate.base_location != the (single) qualified-tech location string. +# Pre-compute the cheapest non-local cost per concentrated type as a derived +# property on TypeConcentration so the recommendation seeding stays simple. +TypeConcentration.min_nonlocal_cost = model.Property( + f"{TypeConcentration} has {Float:min_nonlocal_cost} cheapest non-local training cost" +) +tc_min = TypeConcentration.ref() +to_min = TrainingOption.ref() +t_min = Technician.ref() +model.where(tc_min.is_concentrated == True).define( + tc_min.min_nonlocal_cost( + aggs.min(to_min.training_cost) + .where( + to_min.machine_type == tc_min.machine_type, + to_min.technician(t_min), + t_min.base_location != tc_min.qualified_tech_locations, + ) + .per(tc_min) + ) +) + +# Seed CrossTrainingRecommendation: one row per concentrated type that has +# at least one non-local candidate (min_nonlocal_cost is populated). +tc_seed_ctr = TypeConcentration.ref() +model.where(tc_seed_ctr.min_nonlocal_cost).define( + CrossTrainingRecommendation.new(machine_type=tc_seed_ctr.machine_type) +) + +# Bind cheapest-candidate attributes onto each recommendation by joining the +# TrainingOption whose cost matches the pre-computed min_nonlocal_cost AND +# whose tech sits outside the concentrated location. +ctr_bind = CrossTrainingRecommendation.ref() +tc_bind = TypeConcentration.ref() +to_bind = TrainingOption.ref() +t_bind = Technician.ref() +model.where( + ctr_bind.machine_type == tc_bind.machine_type, + to_bind.machine_type == ctr_bind.machine_type, + to_bind.technician(t_bind), + t_bind.base_location != tc_bind.qualified_tech_locations, + to_bind.training_cost == tc_bind.min_nonlocal_cost, +).define( + ctr_bind.tech_id(t_bind.technician_id), + ctr_bind.tech_name(t_bind.technician_name), + ctr_bind.cost(to_bind.training_cost), + ctr_bind.duration_weeks(to_bind.training_weeks), + ctr_bind.is_best_candidate(True), +) + +# 4a. Technician utilization from the optimal schedule. +tech_assignments = ( + assign_df.groupby( + ["technician_id", "technician_name", "base_location", "skill_level"], + as_index=False, + ) + .agg( + assignment_count=("machine_id", "count"), + machines=("machine_id", list), + ) + .sort_values("assignment_count", ascending=False) +) + +print("\nTechnician utilization in optimal schedule:") +total_assignments = len(assign_df) +for _, row in tech_assignments.iterrows(): + pct = row["assignment_count"] / total_assignments * 100 + print( + f" {row['technician_id']} ({row['technician_name']}, " + f"{row['skill_level']}, {row['base_location']}): " + f"{row['assignment_count']} assignments ({pct:.0f}%)" + ) + +# 4b. MaintenancePlan singleton: cost breakdown from the optimizer. +plan_df = ( + model.select( + MaintenancePlan.objective.alias("objective"), + MaintenancePlan.failure_cost.alias("failure_cost"), + MaintenancePlan.labor_cost.alias("labor_cost"), + MaintenancePlan.travel_cost.alias("travel_cost"), + MaintenancePlan.total_jobs.alias("total_jobs"), + ) + .to_df() +) +plan_row = plan_df.iloc[0] +print("\nMaintenancePlan (cost breakdown):") +print(f" Objective: ${plan_row['objective']:.2f}") +print(f" Failure cost: ${plan_row['failure_cost']:.2f}") +print(f" Labor cost: ${plan_row['labor_cost']:.2f}") +print(f" Travel cost: ${plan_row['travel_cost']:.2f}") +print(f" Total jobs: {int(plan_row['total_jobs'])}") + +# 4c. TypeConcentration: per-machine-type concentration analysis. +type_conc_df = ( + model.select( + TypeConcentration.machine_type.alias("machine_type"), + TypeConcentration.qualified_tech_count.alias("qualified_tech_count"), + TypeConcentration.qualified_tech_locations.alias("qualified_tech_locations"), + TypeConcentration.is_concentrated.alias("is_concentrated"), + TypeConcentration.scheduled_jobs_total.alias("scheduled_jobs_total"), + TypeConcentration.scheduled_jobs_traveling.alias("scheduled_jobs_traveling"), + TypeConcentration.travel_pct.alias("travel_pct"), + ) + .to_df() + .sort_values("machine_type") +) + +print("\nQualification coverage by machine type:") +for _, row in type_conc_df.iterrows(): + tag = ( + f"CONCENTRATED -- all {int(row['qualified_tech_count'])} techs in " + f"{row['qualified_tech_locations']}" + if row["is_concentrated"] + else "OK" + ) + print( + f" {row['machine_type']}: {int(row['qualified_tech_count'])} techs " + f"in {row['qualified_tech_locations']} -- {tag}" + ) + +concentrated_df = type_conc_df[type_conc_df["is_concentrated"]] +if not concentrated_df.empty: + print("\nConcentration risk detail:") + for _, row in concentrated_df.iterrows(): + total_jobs = int(row["scheduled_jobs_total"]) if row["scheduled_jobs_total"] else 0 + travel_jobs = ( + int(row["scheduled_jobs_traveling"]) if row["scheduled_jobs_traveling"] else 0 + ) + pct = float(row["travel_pct"]) if total_jobs else 0.0 + print( + f"\n {row['machine_type']}: all {int(row['qualified_tech_count'])} " + f"qualified techs in {row['qualified_tech_locations']}" + ) + if total_jobs: + print( + f" Scheduled {row['machine_type']} jobs: {total_jobs}, " + f"of which {travel_jobs} require travel ({pct:.0f}%)" + ) + else: + print(f" Scheduled {row['machine_type']} jobs: 0") + + # 4d. CrossTrainingRecommendation: cheapest non-local candidate per type. + print(f"\n{'=' * 70}") + print("RECOMMENDATION: Cross-Training to Eliminate Concentration Risk") + print("=" * 70) + + rec_df = ( + model.select( + CrossTrainingRecommendation.machine_type.alias("machine_type"), + CrossTrainingRecommendation.tech_id.alias("tech_id"), + CrossTrainingRecommendation.tech_name.alias("tech_name"), + CrossTrainingRecommendation.cost.alias("cost"), + CrossTrainingRecommendation.duration_weeks.alias("duration_weeks"), + CrossTrainingRecommendation.is_best_candidate.alias("is_best_candidate"), + ) + .to_df() + .sort_values("machine_type") + ) + + if rec_df.empty: + print("\n No non-local cross-training options available.") + for _, row in rec_df.iterrows(): + conc_loc = concentrated_df[ + concentrated_df["machine_type"] == row["machine_type"] + ]["qualified_tech_locations"].iloc[0] + print(f"\n {row['machine_type']} -- add coverage outside {conc_loc}:") + print( + f" Best candidate: {row['tech_id']} ({row['tech_name']}): " + f"${int(row['cost']):,}, {int(row['duration_weeks'])} weeks" + ) +else: + print("\nNo geographic concentration risk detected.") diff --git a/v1/machine_maintenance/pyproject.toml b/v1/machine_maintenance/pyproject.toml new file mode 100644 index 00000000..6398eca0 --- /dev/null +++ b/v1/machine_maintenance/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "rai-template-machine-maintenance" +version = "0.0.0" +description = "RelationalAI template: machine_maintenance (PyRel v1)" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "relationalai==1.0.14", + "pandas>=2.0", +] + +[tool.setuptools] +packages = [] diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md new file mode 100644 index 00000000..3c950942 --- /dev/null +++ b/v1/machine_maintenance/runbook.md @@ -0,0 +1,166 @@ +# Runbook: Machine Maintenance — Multi-Reasoner Walkthrough + +Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufacturing operation. OEE alone misranks the plants; downtime totals don't say what will fail next; rules flag risky machines but don't allocate scarce technician time; the optimizer produces a feasible schedule but can't see that all on-site Turbine coverage funnels through a single technician per plant. The chain threads querying, graph, rules, and prescriptive reasoners through one ontology so each stage's enrichments feed the next. + +> **Data provenance.** Every figure below is computed from the bundled `data/*.csv`, which is the real `MANUFACTURING.PUBLIC` dataset (50 machines, 20 technicians, 8 products, 12 weekly periods across Plant_A/Plant_B/Plant_C). The same dataset backs the reasoner-workflow eval suite, so the walkthrough doubles as a reproducibility check against 13 known-answer questions. +> +> **Status (draft).** Querying (Q1–Q5, Q7) and Rules (Q9) below are verified against the real data and reproduce the eval's expected answers exactly. Graph (Q8), Predictive (Q6, Q13), and Prescriptive (Q10–Q12) are wired in the script and their numbers will be filled from the live engine run — they are marked _pending live run_ until then. + +## The chain + +``` + ───────────────────────────────────────────────────────────────── + STAGE 1 Querying ──► OEE by plant, downtime drivers, failure + /rai-querying ranking, waste rates, tech coverage + Plant_C 78.3% > Plant_A 68.0% > Plant_B 63.3% + Bearing Failure = 19.4% of all downtime. + Turbines have only 3 qualified technicians. + ───────────────────────────────────────────────────────────────── + STAGE 2 Rules ──► Machine.risk_tier (50) + /rai-rules-authoring 3 Critical · 6 Elevated · 41 Standard + Critical: M001, M006 (Turbine/Plant_A), M011. + ───────────────────────────────────────────────────────────────── + STAGE 3 Graph ──► Machine producibility bottlenecks + /rai-graph-analysis (pending live run) + ───────────────────────────────────────────────────────────────── + STAGE 4 Predictive ──► Per-machine failure risk & mode, 12 periods + /rai-predictive-modeling (pre-computed predictions; GNN pending) + ───────────────────────────────────────────────────────────────── + STAGE 5 Prescriptive ──► Preventive-maintenance schedule + what-if + /rai-prescriptive-* (pending live run) + ───────────────────────────────────────────────────────────────── +``` + +## Workflow + +> **How to use this walkthrough.** Each section is a Prompt you paste into a single RAI session; state accumulates across steps so later reasoners read the properties earlier ones wrote. Responses show what the template produces against the bundled real data. + +### 1. Build ontology + +**Prompt** + +``` +/rai-build-starter-ontology Build a manufacturing maintenance ontology from the CSVs in data/. Scope it for preventive-maintenance scheduling over a multi-period horizon — introduce a Period concept (1..12 from the `period` column) alongside the source-bound concepts (machines, technicians, qualifications, products, production runs, downtime events, failure predictions, sensors, machine-product capabilities). +``` + +**Response** + +Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine/Generator/Pump/Compressor/Motor), `Technician` (20), `Qualification` (32), `Product` (8), `ProductionRun` (844), `DowntimeEvent` (353), `FailurePrediction` (600), `Sensor` (200), `SensorReading` (2400), `MachineProductCapability` (120), and a generated `Period` (1..12). Junction concepts (`MachinePeriod`, `TechnicianMachinePeriod`) are deferred to the prescriptive stage. + +### 2. Diagnose plant operations — OEE _(eval Q1)_ + +**Prompt** + +``` +/rai-querying What's the OEE by plant, broken into Availability (planned time = production runs × 480 min per run; downtime = unplanned events only, is_planned = 0), Performance (avg of actual_speed / target_speed per run), and Quality (good_quantity / actual_quantity)? Compute OEE from the unrounded components, then round to one decimal. +``` + +**Response** _(verified)_ + +| Plant | Availability | Performance | Quality | OEE | +|---|---|---|---|---| +| Plant_C | 97.7% | 81.7% | 98.2% | **78.3%** | +| Plant_A | 96.4% | 71.8% | 98.3% | **68.0%** | +| Plant_B | 92.5% | 69.8% | 98.1% | **63.3%** | + +Plant_C leads; Plant_B trails — driven by its low Availability (most unplanned downtime) and Performance. + +### 3. Find the downtime drivers _(eval Q2, Q3)_ + +**Prompt** + +``` +/rai-querying What are the top causes of downtime by specific fault name and their percent of total downtime? And which plant carries the most downtime? +``` + +**Response** _(verified)_ + +Top fault names: **Bearing Failure 3,905 min (19.4%)**, Overheating 3,183 (15.8%), Motor Burnout 2,344 (11.7%), Seal Degradation 2,328 (11.6%), Shaft Misalignment 1,600 (8.0%). By plant: **Plant_B 10,494 min (52.2%)**, Plant_A 6,576 (32.7%), Plant_C 3,033 (15.1%). Total downtime = 20,103 min. + +### 4. Rank forward failure risk _(eval Q4)_ + +**Prompt** + +``` +/rai-querying Which machines are most likely to fail by the end of the planning horizon (period 12), and for what predicted reason? +``` + +**Response** _(verified)_ + +M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). + +### 5. Surface the worst waste _(eval Q5)_ + +**Prompt** + +``` +/rai-querying Which machine-product combinations have the worst waste rates (waste_quantity / actual_quantity), to one decimal? +``` + +**Response** _(verified)_ + +M025 + Hydraulic Seal Kit (3.8%), M005 + Turbine Blade Assembly (3.7%), M002 + Turbine Blade Assembly (3.6%), M049 + Motor Winding Set (3.6%), M045 + Control Panel Unit (3.5%). + +### 6. Check technician coverage _(eval Q7)_ + +**Prompt** + +``` +/rai-querying Which machine types have the fewest qualified technicians? +``` + +**Response** _(verified)_ + +Turbines are most constrained — only **3** qualified technicians (T001, T009, T017). Generators have 6; Pumps 7; Compressors and Motors 8 each. This concentration is what later makes Turbine coverage fragile in the schedule. + +### 7. Classify machine risk _(eval Q9)_ + +**Prompt** + +``` +/rai-rules-authoring Rate each machine's risk from three flags: chronic = more than 15 downtime events; high-risk = failure probability above 0.20 AND criticality 4 or higher; maintenance-overdue = remaining useful life of 9 or less. All three flags → Critical; exactly two → Elevated; otherwise Standard. +``` + +**Response** _(verified)_ + +**3 Critical** — M001 (Turbine, Plant_A), M006 (Turbine, Plant_A), M011 (Compressor, Plant_B); **6 Elevated**; **41 Standard**. The two Critical Turbines sit in the same plant that the coverage query already flagged as thin on Turbine technicians. + +### 8. Find producibility bottlenecks _(eval Q8)_ + +**Prompt** + +``` +/rai-graph-analysis Build a machine-product bipartite graph from machine_product_capabilities and find the biggest connectivity bottlenecks — machines tied to products with the fewest alternative producers. +``` + +**Response** _(pending live run)_ + +Graph stage is wired in `machine_maintenance.py`; bottleneck centralities will be filled from the live engine run. + +### 9. Predict failures _(eval Q6, Q13)_ + +**Prompt** + +``` +/rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each, given sensor readings, downtime history, and machine attributes? +``` + +**Response** _(pre-computed; GNN pending)_ + +The bundled `failure_predictions` supply per-machine, per-period probabilities and predicted modes (the source of the step-4 ranking). A GNN formulation over sensor/downtime history is wired for the predictive reasoner; its trained-model results will be added from the live run. + +### 10. Schedule preventive maintenance + stress-test _(eval Q10, Q11, Q12)_ + +**Prompt** + +``` +/rai-prescriptive-problem-formulation Schedule preventive maintenance across the 50 machines and 12 periods: at most 5 jobs per period; each maintained machine needs a qualified technician, with Turbine work covered by an on-site technician at the same plant; prioritize high failure-probability × high-criticality and earlier periods for the riskiest. Then re-solve with T001 unavailable and report the coverage impact. +``` + +**Response** _(pending live run)_ + +Prescriptive formulation (decision variables, ≤5/period cap, qualified + on-site Turbine assignment, expected-failure-cost objective) and the T001-unavailable what-if are wired in `machine_maintenance.py`; the optimal schedule, objective, and concentration/cross-training findings will be filled from the live solve. + +## Data + +Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants × 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. All stages run in `machine_maintenance.py`. From 29c40b8aea908dca47fa4bf64acb5810991f52e6 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 13:32:47 -0700 Subject: [PATCH 02/12] Rebuild machine_maintenance script and finalize runbook on real data Rebuild machine_maintenance.py as a four-stage multi-reasoner pipeline (querying, graph, rules, prescriptive) over the 50-machine MANUFACTURING.PUBLIC data; the full script runs OPTIMAL end-to-end. Querying and rules reproduce the eval's expected answers exactly (OEE 78.3/68.0/63.3, downtime drivers, risk tiers 3/6/41); graph and prescriptive use the template's own sound formulations and corroborate the eval's structural findings (Pumps/Motors bottlenecks; the T001 what-if drops the four Plant_A Turbines, 46/50 scheduled). Finalize runbook with the real figures, rewrite README for the 50-machine dataset, pin relationalai==1.15.0, add .gitignore. --- v1/machine_maintenance/.gitignore | 13 + v1/machine_maintenance/README.md | 551 +---- v1/machine_maintenance/machine_maintenance.py | 2132 ++++------------- v1/machine_maintenance/pyproject.toml | 2 +- v1/machine_maintenance/runbook.md | 25 +- 5 files changed, 596 insertions(+), 2127 deletions(-) create mode 100644 v1/machine_maintenance/.gitignore diff --git a/v1/machine_maintenance/.gitignore b/v1/machine_maintenance/.gitignore new file mode 100644 index 00000000..b6651b36 --- /dev/null +++ b/v1/machine_maintenance/.gitignore @@ -0,0 +1,13 @@ +# Python +__pycache__/ +*.pyc +.venv/ +*.egg-info/ +build/ + +# RAI / engine local artifacts +raiconfig.yaml +metadata.json +debug.jsonl +spans.jsonl +dev_run/ diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index b117961f..406bd614 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -1,10 +1,11 @@ --- title: "Machine Maintenance" -description: "A multi-reasoner template that chains querying, graph analysis, rules-based classification, and prescriptive optimization to schedule preventive maintenance, surface hidden operational risk, and recommend cross-training to eliminate concentration vulnerabilities." +description: "A multi-reasoner template that chains querying, graph analysis, rules-based classification, and prescriptive optimization to diagnose plant performance, surface producibility bottlenecks, classify machine risk, and schedule preventive maintenance under technician-coverage constraints." featured: false experience_level: intermediate industry: "Manufacturing" reasoning_types: + - Querying - Graph - Rules-based - Prescriptive @@ -14,89 +15,44 @@ tags: - Scheduling - Maintenance - Manufacturing - - Assignment - OEE - - Sensor Anomalies - Risk Classification + - Bottleneck Analysis --- # Machine Maintenance ## What this template is for -Manufacturing facilities must schedule preventive maintenance for machines with ML-predicted failure probabilities. The challenge is that surface-level metrics (like OEE) can mask structural vulnerabilities -- a plant that looks mid-tier on performance may actually carry the highest concentration risk, discoverable only by chaining multiple analytical layers. +This template models a **50-machine, 3-plant, 12-period** manufacturing operation and threads four RelationalAI reasoners through a single ontology, with each stage's enrichments feeding the next: -This template uses RelationalAI's **querying**, **graph analysis**, **rules-based classification**, and **prescriptive reasoning (optimization)** capabilities in a five-stage multi-reasoner workflow: +1. **Querying** — diagnose plant performance: OEE (availability × performance × quality), downtime drivers, forward failure risk, waste rates, and technician coverage. +2. **Graph** — build a machine-product bipartite graph and rank machines by betweenness centrality to find producibility bottlenecks. +3. **Rules** — classify each machine into a risk tier (Critical / Elevated / Standard) from chronic-downtime, high-risk, and maintenance-overdue flags. +4. **Prescriptive** — schedule preventive maintenance across machines and periods under a per-period bay limit and technician-coverage feasibility (Turbine work needs an on-site qualified technician), then stress-test the schedule against the loss of a key technician. -1. **Querying** computes OEE by facility, surfaces sensor anomalies, and identifies machines with the steepest failure degradation trajectories. Plant_B looks worst at 61.4% OEE -- but Plant_A, at 68.2%, has 7 of 9 sensor anomalies and the 3 steepest degradation curves. -2. **Graph analysis** builds a machine dependency graph from shared-technician qualifications. All 30 machines form a single connected cluster, and Pump-type machines score highest on betweenness centrality (24.0) as the most constrained scheduling bottlenecks. -3. **Rules** derive seven compliance flags and chain three of them (chronic downtime, high-risk, overdue) into a composite risk tier. M013 (Pump, Plant_A) is the only Critical-tier machine -- it triggers all three flags. -4. **Prescriptive optimization** schedules 20 maintenance jobs across 4 periods at $605K total cost, assigning qualified technicians. The optimizer consumes per-period failure predictions from Stage 0, betweenness centrality from Stage 1, and overdue-maintenance flags from Stage 2. -5. **Resilience analysis** reveals that all 3 Turbine-qualified technicians are in Houston_TX, forcing 67% of scheduled Turbine jobs to require travel. Cross-training T006 (Senior, Chicago_IL) for $3,200 over 5 weeks eliminates this geographic concentration risk. - -Each stage enriches the shared ontology, and downstream stages consume those enrichments -- this is the **accretive ontology enrichment** pattern. No Python dicts carry state between stages; the ontology is the single source of truth: - -- **Stage 0 writes** `Machine.performance_ratio`, `Machine.quality_ratio`, `Machine.anomaly_count`, `MachinePeriod.predicted_fp` -- consumed by Stage 2's rules AND Stage 3's objective. Both downstream reasoners see the same derived signals. -- **Stage 1 writes** `Machine.betweenness` (normalized centrality) -- consumed by Stage 3's failure cost term. Bottleneck machines are more expensive to leave vulnerable. -- **Stage 2 writes** `Machine.is_overdue_maintenance`, `Machine.is_high_risk`, `Machine.is_chronic_downtime`, `Machine.risk_tier` -- the overdue flag feeds a hard scheduling constraint in Stage 3 (overdue machines must be maintained by period 2). -- **Stage 3 writes** `x_maintain`, `x_vulnerable`, `x_assigned` (decision variables) -- parsed in Stage 4 to analyze technician utilization and concentration risk. - -### Reasoner overview - -| Stage | Reasoner | Reads from ontology | Writes to ontology | Role | -|-------|----------|---------------------|--------------------|------| -| 0 | Querying | ProductionRun, SensorReading, FailurePrediction | Machine.performance_ratio, Machine.quality_ratio, Machine.anomaly_count, MachinePeriod.predicted_fp | Plant_C leads at 79.8% OEE; Plant_A mid at 68.2% but has 7 of 9 sensor anomalies and the 3 steepest failure trajectories (M001 +0.230, M013 +0.228, M016 +0.219). | -| 1 | Graph | Qualification, Machine (as `node_concept`) | Machine.betweenness (normalized centrality) | All 30 machines form 1 connected cluster. Pump-type machines are the top bottlenecks (betweenness=24.0). Centrality scores feed the failure cost multiplier in Stage 3. | -| 2 | Rules | Machine (all derived properties from Stages 0-1) | Machine.is_overdue_maintenance, Machine.is_high_risk, Machine.is_chronic_downtime, Machine.risk_tier | 6 overdue, 1 high-risk, 3 chronic downtime. Composite tier: M013 is Critical (all 3 flags), M016 is Elevated (2 of 3). Overdue flag becomes a hard constraint in Stage 3. | -| 3 | Prescriptive | MachinePeriod.predicted_fp, Machine.betweenness, Machine.is_overdue_maintenance | x_maintain, x_vulnerable, x_assigned (decision variables) | 20 jobs across 4 periods at $605K total cost. Per-period failure predictions (not static probability) weight the objective. Overdue machines scheduled by period 2. | -| 4 | Analysis | Solution variables, Qualification, TrainingOption | (terminal -- prints recommendations) | All 3 Turbine techs in Houston_TX -- 67% of Turbine jobs require travel. Best cross-training: T006 (Chicago_IL, Senior) at $3,200 / 5 weeks. | - -## Why this problem matters - -OEE dashboards and failure-probability rankings are how most plants prioritize maintenance today. But these metrics evaluate machines in isolation -- they miss structural dependencies between machines, technicians, and locations that create cascading risk. A plant where all Turbine-qualified technicians happen to work from the same office looks fine on every individual metric. The concentration risk is invisible until someone leaves, a certification expires, or a weather event disrupts the location -- at which point multiple machines lose coverage simultaneously. - -The multi-reasoner approach is necessary because no single analytical technique surfaces this risk. Querying reveals sensor anomalies that OEE masks. Graph analysis exposes which machines share technician pools. Rules chain individual flags into composite risk tiers. Optimization produces a schedule, and resilience analysis stress-tests that schedule against the qualification structure. Each layer reveals something the previous one missed. - -### Key design patterns demonstrated - -- **Accretive ontology enrichment** -- each stage writes derived properties (betweenness, risk_tier, predicted_fp) that downstream stages consume, building a progressively richer model -- **Rules chaining** -- three boolean flags (is_chronic_downtime, is_high_risk, is_overdue_maintenance) are composed into a single risk_tier property using exhaustive enumeration with `model.not_()` -- **Graph directly on domain concept** -- the Graph reasoner uses `Machine` directly as `node_concept`, so centrality scores are stored as Machine properties without a mirror concept -- **Per-period failure predictions** -- the optimization objective uses `MachinePeriod.predicted_fp` (period-specific) rather than static `Machine.failure_probability`, giving the solver time-varying cost information -- **Post-solve resilience analysis** -- Stage 4 inspects the solution and qualification structure to identify concentration risk, producing actionable cross-training recommendations without re-solving +The point is the chain: OEE alone misranks the plants, downtime totals don't say what will fail next, rules flag risky machines but don't allocate scarce technician time, and the optimizer produces a feasible schedule but can't see that on-site Turbine coverage funnels through a single technician per plant. ## Who this is for -- Manufacturing and plant managers scheduling preventive maintenance -- Operations researchers exploring multi-reasoner pipelines in RelationalAI -- Developers learning how to chain querying, graph, rules, and optimization in a single model +- Data scientists and analysts learning to chain multiple RelationalAI reasoners over one ontology +- Manufacturing and reliability teams building preventive-maintenance and risk-classification workflows +- Anyone wanting a worked multi-reasoner example on a realistic operational dataset ## What you'll build -- Machine-level production aggregates, OEE components, and anomaly counts as derived properties -- A machine dependency graph with cluster detection and centrality scoring -- Seven compliance rules as derived Relationships and Properties, including a composite risk tier that chains three boolean flags -- Binary decision variables for maintenance timing, vulnerability tracking, and technician assignment -- Cumulative coverage, capacity, and overdue-deadline constraints -- A cost minimization objective that incorporates per-period failure predictions and graph centrality -- Geographic concentration risk analysis with cross-training recommendations +- An ontology over machines, technicians, qualifications, products, production runs, downtime events, failure predictions, and machine-product capabilities +- Querying-stage metrics: OEE by plant, downtime by fault and plant, failure ranking, waste rates, technician coverage +- A betweenness-centrality bottleneck ranking over the machine-product graph +- A per-machine `risk_tier` derived from business rules +- A preventive-maintenance schedule plus a technician-availability what-if ## What's included -- `machine_maintenance.py` -- Main script with five chained reasoning stages -- `data/machines.csv` -- 30 machines with failure probability, criticality (1-5), duration, and parts cost -- `data/technicians.csv` -- 10 technicians with skill levels, certifications, hourly rates, and locations -- `data/availability.csv` -- Technician availability across the 4-period planning horizon -- `data/qualifications.csv` -- Mapping of which technicians can service which machine types -- `data/parts_inventory.csv` -- Spare parts stock levels at each facility -- `data/certification_expiry.csv` -- Days remaining on technician certifications per machine type -- `data/sensors.csv` -- 60 sensors (2 per machine) with warning and critical thresholds -- `data/sensor_readings.csv` -- 240 periodic sensor measurements with anomaly flags -- `data/failure_predictions.csv` -- 120 per-period failure probability trajectories with predicted failure modes -- `data/downtime_events.csv` -- 129 downtime events with fault categories and durations -- `data/production_runs.csv` -- 120 production runs with planned, actual, and good quantities -- `data/training_options.csv` -- 13 cross-training options with costs and durations -- `pyproject.toml` -- Python project configuration with dependencies +- `machine_maintenance.py` — the four-stage multi-reasoner script +- `runbook.md` — a prompt-by-prompt walkthrough mapped to 13 reasoner questions, with the real figures each stage produces +- `data/` — the bundled `MANUFACTURING.PUBLIC` sample (15 CSVs) +- `pyproject.toml` — package configuration and dependencies ## Prerequisites @@ -106,483 +62,128 @@ The multi-reasoner approach is necessary because no single analytical technique ### Tools - Python >= 3.10 -- RelationalAI Python SDK (`relationalai`) >= 1.0.14 ## Quickstart -1. Download the ZIP file for this template and extract it: - +1. Download ZIP: ```bash curl -O https://docs.relational.ai/templates/zips/v1/machine_maintenance.zip unzip machine_maintenance.zip cd machine_maintenance ``` - > [!TIP] > You can also download the template ZIP using the "Download ZIP" button at the top of this page. -2. Create a virtual environment and activate it: - +2. Create venv: ```bash python -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip ``` -3. Install dependencies: - +3. Install: ```bash python -m pip install . ``` -4. Configure your RAI connection: - +4. Configure: ```bash rai init ``` -5. Run the template: - +5. Run: ```bash python machine_maintenance.py ``` -6. Expected output: - ```text - ====================================================================== - STAGE 0: Querying -- Operational Intelligence - ====================================================================== - - OEE proxy by facility (Performance x Quality): - Plant_C: Perf=81.3%, Qual=98.1%, OEE=79.8% - Plant_A: Perf=69.8%, Qual=97.8%, OEE=68.2% - Plant_B: Perf=62.6%, Qual=98.1%, OEE=61.4% - - Sensor anomalies (9 readings across 5 machines): - M013 (Pump, Plant_A): 3 anomalies - M001 (Turbine, Plant_A): 2 anomalies - M016 (Turbine, Plant_A): 2 anomalies - M002 (Compressor, Plant_B): 1 anomalies - M006 (Turbine, Plant_C): 1 anomalies - By facility: {'Plant_A': 7, 'Plant_B': 1, 'Plant_C': 1} - - Steepest failure trajectories (period 1 -> 4): - M001 (Turbine, Plant_A): 0.102 -> 0.332 (+0.230) [bearing_wear] - M013 (Pump, Plant_A): 0.435 -> 0.663 (+0.228) [impeller_erosion] - M016 (Turbine, Plant_A): 0.263 -> 0.482 (+0.219) [bearing_wear] - ... - - ====================================================================== - STAGE 1: Graph Analysis -- Dependency Clusters & Centrality - ====================================================================== - - Dependency clusters found: 1 - - Top bottleneck machines (betweenness centrality): - M003 (Pump, Plant_C): betweenness=24.0000, failure_prob=0.089 - M008 (Pump, Plant_B): betweenness=24.0000, failure_prob=0.076 - M013 (Pump, Plant_A): betweenness=24.0000, failure_prob=0.435 - ... - - ====================================================================== - STAGE 2: Rules -- Compliance Flags & Composite Risk Tier - ====================================================================== - - Overdue maintenance (6 machines): - M002 (Compressor_Beta_1): RUL=3.7h < duration=6h - M006 (Turbine_Alpha_2): RUL=3.4h < duration=8h - M013 (Pump_Gamma_3): RUL=2.3h < duration=4h - ... - - High-risk machines (1): - M013 (Pump_Gamma_3): prob=0.435, crit=4 - - Anomalous machines (5): - M013 (Pump_Gamma_3, Plant_A): 3 anomalies - M001 (Turbine_Alpha_1, Plant_A): 2 anomalies - M016 (Turbine_Alpha_4, Plant_A): 2 anomalies - ... - - Chronic downtime machines (>8 events, 3 machines): - M001 (Turbine_Alpha_1, Plant_A): 12 events, 1635 min total downtime - M016 (Turbine_Alpha_4, Plant_A): 11 events, 1314 min total downtime - M013 (Pump_Gamma_3, Plant_A): 10 events, 1272 min total downtime - - Composite risk tier: - Critical (1): M013 - Elevated (1): M016 - Standard (28): M001, M002, ... - - Parts needing reorder (4): - P001 (Spindle Bearings, Plant_A): stock=25 <= min_order=50 - ... - - Expiring certifications (5): - T001 (Alice_Johnson): Compressor -- 22 days remaining - T004 (Diana_Chen): Pump -- 8 days remaining - ... - - ====================================================================== - STAGE 3: Prescriptive -- Maintenance Scheduling - ====================================================================== - - Status: OPTIMAL - Objective value: 605240.61 - - Maintenance schedule (20 jobs): - Period 1: - M002 (Compressor, Plant_B, crit=5) - M006 (Turbine, Plant_C, crit=5) - M013 (Pump, Plant_A, crit=4) - M016 (Turbine, Plant_A, crit=3) - ... - Period 2: ... - Period 3: ... - Period 4: ... - - Technician assignments (20): - Period 1: - M002: T003 (6h x $65/h = $390) [TRAVEL] - M013: T006 (4h x $88/h = $352) [TRAVEL] - ... - - ====================================================================== - STAGE 4: Resilience -- Concentration Risk Analysis - ====================================================================== - - Technician utilization in optimal schedule: - T003 (Charlie_Brown, Junior, Houston_TX): 5 assignments (25%) - T004 (Diana_Chen, Junior, Chicago_IL): 5 assignments (25%) - ... - - Qualification coverage by machine type: - Compressor: 3 techs in Chicago_IL, Houston_TX -- OK - Generator: 3 techs in Chicago_IL, Phoenix_AZ -- OK - Motor: 4 techs in Chicago_IL, Phoenix_AZ -- OK - Pump: 3 techs in Chicago_IL, Phoenix_AZ -- OK - Turbine: 3 techs in Houston_TX -- CONCENTRATED -- all 3 techs in Houston_TX - - Concentration risk detail: - - Turbine: all 3 qualified techs in Houston_TX - Scheduled Turbine jobs: 3, of which 2 require travel (67%) - - ====================================================================== - RECOMMENDATION: Cross-Training to Eliminate Concentration Risk - ====================================================================== - - Turbine -- add coverage outside Houston_TX: - Best candidate: T006 (Fiona_Garcia): $3,200, 5 weeks - ``` + Each stage prints its findings — OEE by plant, downtime drivers, the bottleneck ranking, risk tiers, and the maintenance schedule with its what-if. ## Template structure ```text . ├── README.md +├── runbook.md ├── pyproject.toml ├── machine_maintenance.py └── data/ ├── machines.csv ├── technicians.csv - ├── availability.csv ├── qualifications.csv - ├── parts_inventory.csv - ├── certification_expiry.csv + ├── products.csv + ├── production_runs.csv + ├── machine_product_capabilities.csv + ├── downtime_events.csv + ├── fault_types.csv + ├── failure_predictions.csv ├── sensors.csv ├── sensor_readings.csv - ├── failure_predictions.csv - ├── downtime_events.csv - ├── production_runs.csv - └── training_options.csv -``` - -## How it works - -This section walks through the highlights in `machine_maintenance.py`. - -### Define concepts and load CSV data - -The model defines concepts for machines (with ML-predicted failure probability and numeric criticality), technicians (with skills and hourly rates), qualifications linking technicians to machine types, parts inventory, certification expiry, sensors, sensor readings, failure predictions, downtime events, and production runs. All data is loaded from CSV files: - -```python -Machine = model.Concept("Machine", identify_by={"machine_id": String}) -Machine.failure_probability = model.Property( - f"{Machine} has failure probability {Float:failure_probability}") -Machine.criticality = model.Property(f"{Machine} has criticality {Integer:criticality}") - -Technician = model.Concept("Technician", identify_by={"technician_id": String}) -Qualification = model.Concept( - "Qualification", identify_by={"technician_id": String, "machine_type": String}) - -Sensor = model.Concept("Sensor", identify_by={"sensor_id": String}) -SensorReading = model.Concept( - "SensorReading", - identify_by={"sensor_id": String, "machine_id": String, "pid": Integer}) -FailurePrediction = model.Concept( - "FailurePrediction", identify_by={"prediction_id": String}) -DowntimeEvent = model.Concept("DowntimeEvent", identify_by={"event_id": String}) -ProductionRun = model.Concept("ProductionRun", identify_by={"run_id": String}) -``` - -Machine-level derived aggregates are computed from the loaded data using `aggs.sum` and `aggs.count`, providing production ratios, downtime counts, and anomaly counts as derived properties: - -```python -Machine.total_planned_qty = model.Property( - f"{Machine} has total planned qty {Float:total_planned_qty}") -model.define(Machine.total_planned_qty( - aggs.sum(ProductionRun.planned_quantity).per(Machine) - .where(ProductionRun.machine(Machine)) | 0 -)) - -model.where(Machine.total_planned_qty > 0).define( - Machine.performance_ratio( - floats.float(Machine.total_actual_qty) - / floats.float(Machine.total_planned_qty) - ) -) -``` - -Cross-product concepts define the scheduling decision space. `MachinePeriod` pairs each machine with each planning period and stores per-period failure predictions. `TechnicianMachinePeriod` is restricted to qualified pairs -- technicians can only be assigned to machine types they are certified for: - -```python -MachinePeriod.predicted_fp = model.Property( - f"{MachinePeriod} has predicted failure probability {Float:predicted_fp}") -FPJoin = FailurePrediction.ref() -model.where( - MachinePeriod.machine_id == FPJoin.machine_id_str, - MachinePeriod.pid == FPJoin.period_int, -).define(MachinePeriod.predicted_fp(FPJoin.failure_probability)) -``` - -### Stage 0: Querying -- operational intelligence - -The querying stage computes OEE proxy (Performance x Quality) by facility, surfaces machines with above-threshold sensor readings, and identifies the steepest failure degradation trajectories. All queries use `model.select` with derived properties: - -```python -oee_df = ( - model.select( - Machine.machine_id.alias("machine_id"), - Machine.facility.alias("facility"), - Machine.performance_ratio.alias("performance"), - Machine.quality_ratio.alias("quality"), - ) - .to_df() -) -``` - -### Stage 1: Graph -- dependency clusters and centrality - -The Graph reasoner uses `Machine` directly as `node_concept` -- no mirror concept needed. Edges connect machines when at least one technician is qualified for both machine types: - -```python -dep_graph = Graph( - model, directed=False, weighted=False, node_concept=Machine, aggregator="sum" -) -``` - -Weakly connected components identify dependency clusters (groups of machines that compete for the same technicians). Betweenness centrality scores bottleneck machines -- those whose maintenance blocks the most scheduling options. The scores are normalized and stored directly on `Machine`: - -```python -Machine.betweenness_raw = model.Property( - f"{Machine} has raw betweenness centrality {Float:betweenness_raw}") -m_btwn = Machine.ref("m_btwn") -model.define(m_btwn.betweenness_raw(btwn_score)).where(betweenness(m_btwn, btwn_score)) -max_betweenness = max(Machine.betweenness_raw) -Machine.betweenness = model.Property( - f"{Machine} has betweenness centrality {Float:betweenness}") -m_norm = Machine.ref("m_norm") -model.where(max_betweenness == 0).define(m_norm.betweenness(0.0)) -model.where(max_betweenness > 0).define( - m_norm.betweenness(m_norm.betweenness_raw / max_betweenness) -) -``` - -### Stage 2: Rules -- compliance flags and composite risk tier - -Seven derived Relationships and Properties flag compliance issues. Each rule is a pure logic derivation using `model.where(...).define(...)`: - -```python -Machine.is_overdue_maintenance = model.Relationship( - f"{Machine} is overdue maintenance") -model.where( - Machine.remaining_useful_life < floats.float(Machine.maintenance_duration_hours) -).define(Machine.is_overdue_maintenance()) - -Machine.is_chronic_downtime = model.Relationship(f"{Machine} has chronic downtime") -model.where( - Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD -).define(Machine.is_chronic_downtime()) -``` - -Individual flags are chained into a composite risk tier using `model.not_()` for negation. This exhaustively enumerates all eight combinations of three boolean flags: - -```python -Machine.risk_tier = model.Property(f"{Machine} has risk tier {String:risk_tier}") - -# Critical: all 3 flags. -model.where( - Machine.is_chronic_downtime(), - Machine.is_high_risk(), - Machine.is_overdue_maintenance(), -).define(Machine.risk_tier("Critical")) - -# Elevated: exactly 2 of 3 flags. -model.where( - Machine.is_chronic_downtime(), - Machine.is_high_risk(), - model.not_(Machine.is_overdue_maintenance()), -).define(Machine.risk_tier("Elevated")) -``` - -### Stage 3: Define decision variables, constraints, and objective - -Three binary decision variables control the schedule: whether to maintain a machine in a period, whether it remains vulnerable, and whether a technician is assigned. The formulation includes four standard constraints (cumulative coverage, assignment linkage, technician capacity, parts/bay capacity) plus a hard constraint from Stage 2's overdue flag: - -```python -maintained_by_deadline = ( - sum(MachinePeriod_overdue.x_maintain) - .where( - MachinePeriod_overdue.machine(Machine_overdue), - MachinePeriod_overdue.period(Period_overdue), - Period_overdue.pid <= OVERDUE_DEADLINE, - ) - .per(Machine_overdue) -) -problem.satisfy( - model.require(maintained_by_deadline >= 1).where( - Machine_overdue.is_overdue_maintenance() - ) -) -``` - -The objective minimizes expected total cost with three components. The failure cost term incorporates per-period failure predictions from Stage 0 and betweenness centrality from Stage 1, making it more expensive to leave bottleneck machines vulnerable in periods where their predicted failure probability is highest: - -```python -failure_cost = sum( - MachinePeriod_outer.x_vulnerable - * MachinePeriod_outer.predicted_fp - * Machine_obj.estimated_parts_cost - * Machine_obj.criticality - * (1 + CENTRALITY_WEIGHT * Machine_obj.betweenness) -).where( - MachinePeriod_outer.machine(Machine_obj), MachinePeriod_outer.period(Period_outer) -) -``` - -### Solve and extract results - -The model is solved using the HiGHS solver with a two-minute time limit. Assignment decisions are parsed from the solution to build the maintenance schedule: - -```python -problem.solve("highs", time_limit_sec=120) -si = problem.solve_info() -assert si.termination_status == "OPTIMAL" + ├── travel.csv + ├── training_options.csv + ├── availability.csv + └── degradation.csv ``` -### Stage 4: Resilience analysis and cross-training +## Sample data -After solving, the script analyzes qualification coverage by machine type and location. For each machine type, it checks whether all qualified technicians are concentrated in a single location -- a geographic single-point-of-failure invisible to the optimizer: +The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: -```python -for mtype in machine_types: - qual_techs = qualifications_df[ - qualifications_df["machine_type"] == mtype - ]["technician_id"].tolist() - tech_info = technicians_df[technicians_df["technician_id"].isin(qual_techs)] - locations = tech_info["base_location"].unique().tolist() - if len(locations) == 1: - concentrated_types.append((mtype, locations[0], len(qual_techs))) -``` +| File | Rows | Description | +|---|---|---| +| `machines.csv` | 50 | Machines across 3 plants × 5 types (Turbine, Generator, Pump, Compressor, Motor) | +| `technicians.csv` | 20 | Technicians with skill level, base location, and rate | +| `qualifications.csv` | 32 | Which technicians are qualified for which machine type | +| `products.csv` | 8 | Products manufactured | +| `production_runs.csv` | 844 | Per-run planned/actual/good/waste quantities and speeds | +| `machine_product_capabilities.csv` | 120 | Which machines can produce which products | +| `downtime_events.csv` | 353 | Downtime events with fault name, duration, and planned flag | +| `fault_types.csv` | 15 | Fault catalog (name, category, MTTR/MTBF) | +| `failure_predictions.csv` | 600 | Per-machine, per-period failure probability and predicted mode | +| `sensors.csv` / `sensor_readings.csv` | 200 / 2,400 | Sensor catalog and readings with anomaly flags | +| `travel.csv` | 9 | Inter-location travel hours and cost | +| `training_options.csv` | 41 | Cross-training cost and duration per technician/type | +| `availability.csv` | 240 | Per-technician, per-period availability | +| `degradation.csv` | 5 | Per-type degradation rate and maintenance reset factor | -For concentrated types, the script queries `training_options.csv` to recommend the cheapest candidate at a different location, producing a specific, costed action item (e.g., "Cross-train T006 for Turbine at $3,200 / 5 weeks"). +## Model overview -### Stage 5: Inspect the model schema (post-pipeline) +Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period` (1..12). The prescriptive stage adds a `MachinePeriod` decision space (machine × period). -Templates that chain multiple reasoners over a rich ontology benefit from a quick schema dump once the pipeline has run. `relationalai.semantics.inspect` (available in `relationalai>=1.0.14`) returns a typed view of every registered concept, property, and relationship -- handy for confirming that decision variables, derived aggregates, and cross-product concepts all registered correctly across the five-stage pipeline. +## How it works -Once `Problem(...)` plus `solve_for` / `satisfy` / `minimize` / `maximize` have run in Stage 3, the prescriptive reasoner registers root concepts named `Variable`, `Expression`, `Constraint`, and `Objective` (plus per-solve `Variable_` / `Constraint_` / `Objective_` subconcepts) on the shared model. Graph(model, node_concept=Machine) from Stage 1 also registers an edge concept (e.g. `graph_Edge`). These are noise for user-facing introspection, so filter them out by exact name and reasoner-name prefix -- an underscore check won't catch them since the names don't start with `_`: +### 1. Querying +Per-plant OEE is built from production runs (performance = avg of actual/target speed; quality = good/actual quantity) and downtime events (availability from unplanned downtime against an 480-minute-per-run planned base). Additional queries rank downtime by fault and plant, surface the highest forward failure risk, compute waste rates by machine-product, and count qualified technicians per machine type. -```python -from relationalai.semantics import inspect +### 2. Graph +A bipartite machine-product graph is built from `machine_product_capabilities` (edge = machine can produce product). `betweenness_centrality()` ranks machines by how much production-routing flows through them — the producibility bottlenecks. -schema = inspect.schema(model) -reasoner_names = {"Variable", "Expression", "Constraint", "Objective"} -user_concepts = [ - c for c in schema.concepts - if c.name not in reasoner_names - and not any(c.name.startswith(p + "_") for p in reasoner_names) - and not c.name.startswith("graph") # Graph reasoner registers e.g. graph_Edge -] -print(f"User concepts: {len(user_concepts)}") -for c in user_concepts: - print(f" {c.name}: {len(c.properties)} properties, {len(c.relationships)} relationships") -``` +### 3. Rules +Three boolean flags — chronic downtime (> 15 events), high-risk (failure probability > 0.20 **and** criticality ≥ 4), and maintenance-overdue (remaining useful life ≤ 9) — combine into `Machine.risk_tier`: all three → Critical, exactly two → Elevated, otherwise Standard. -Print `inspect.schema(model)` first to see the actual names registered in your model, then extend the filter list if other reasoner-owned concepts appear. +### 4. Prescriptive +A binary `MachinePeriod.x_maintain` decides which machine is maintained in which period. Each machine gets at most one slot and only if coverage is feasible (Turbine work requires an on-site qualified technician); each period is capped at 5 jobs. The objective prioritizes high failure-probability × criticality work in earlier periods. A second solve removes a key technician (T001) to show which machines lose coverage. ## Customize this template -- **Adjust centrality weight** via `CENTRALITY_WEIGHT` to control how strongly graph bottleneck scores influence scheduling priority. -- **Change the overdue deadline** via `OVERDUE_DEADLINE` to give more or fewer periods for overdue machines. -- **Extend the planning horizon** by adding more periods to the availability and failure prediction data and increasing `PERIOD_HORIZON`. -- **Adjust capacity limits** via `PARTS_CAPACITY_PER_PERIOD` to see how tighter constraints shift scheduling priorities. -- **Tune travel cost** via `TRAVEL_COST_PER_HOUR` to control preference for local vs. cross-facility assignments. -- **Add rule thresholds** -- adjust `failure_probability > 0.3` or `criticality >= 4` in the high-risk rule to match your risk tolerance. -- **Change chronic downtime threshold** via `CHRONIC_DOWNTIME_THRESHOLD` to control which machines are flagged. -- **Add sensor types** -- extend `sensors.csv` with new sensor types and adjust `sensor_readings.csv` with corresponding measurements. -- **Add training options** -- extend `training_options.csv` to explore different cross-training strategies. - -## Troubleshooting - -
-Status: INFEASIBLE - -- The overdue-maintenance constraint requires certain machines to be scheduled in early periods. If technician capacity is too tight, this can cause infeasibility. -- Try increasing `OVERDUE_DEADLINE` from 2 to 3, or increase `PARTS_CAPACITY_PER_PERIOD`. -- Check that technician hours capacity across all periods can accommodate all machines. -
- -
-All machines maintained in period 1 - -- The solver minimizes total cost. If capacity allows, it may schedule all maintenance early to avoid vulnerability costs. -- Tighten `PARTS_CAPACITY_PER_PERIOD` to spread maintenance across periods. -
- -
-Graph shows 0 edges +### Use your own data +Replace the CSVs in `data/` with your own machines, technicians, production, and downtime records (matching the column headers). Concept definitions bind directly to the CSV columns. -- This means no two machines share a qualified technician. Check that `qualifications.csv` has overlapping machine types across technicians. -- The graph edge construction uses type-based joins: two machines connect if any technician is qualified for both their `machine_type` values. -
+### Tune parameters +The thresholds at the top of `machine_maintenance.py` — period horizon, per-period bay limit, chronic/high-risk/overdue cutoffs — are constants you can adjust to your operation. -
-input definition is too large +### Extend the model +Add reasoners or stages: cluster machines by shared technicians, train a GNN on the sensor/downtime history for failure prediction, or add cross-training recommendations from `training_options` to relieve the coverage bottlenecks the what-if surfaces. -- This occurs with large cross-products. The qualification-filtered assignment space avoids this issue for the default 30-machine dataset. -- If you scale up significantly, consider reducing data size or querying solver - results via `Variable.values(...)` instead of broad `model.select(...)` - patterns. -
+## Troubleshooting
ModuleNotFoundError -- Make sure you activated the virtual environment and ran `python -m pip install .` from the template directory. -- The `pyproject.toml` declares the required dependencies. +Make sure you activated the virtual environment and ran `python -m pip install .` to install dependencies listed in `pyproject.toml`.
Connection or authentication errors -- Run `rai init` to configure your Snowflake connection. -- Verify that the RAI Native App is installed and your user has the required permissions. -
- -
-No concentration risk detected in Stage 4 - -- This means all machine types have qualified technicians in multiple locations. The resilience analysis examines geographic diversity of the qualification pool, not individual assignment redundancy. -- Try modifying `qualifications.csv` to concentrate a machine type's technicians in one location to see how the analysis surfaces this risk. +Run `rai init` to configure your Snowflake connection. Verify that the RAI Native App is installed and your user has the required permissions.
diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 573e52de..0019a3e8 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -1,48 +1,27 @@ -"""Machine maintenance (multi-reasoner) template. - -This script demonstrates a chained multi-reasoner workflow in RelationalAI, -combining querying, graph analysis, rules-based classification, and prescriptive -optimization in a single template: - -- Stage 0 -- Querying: compute OEE by facility, surface sensor anomalies, and - identify machines with the steepest failure degradation trajectories. -- Stage 1 -- Graph: build a machine dependency graph from shared-technician - qualifications, compute weakly connected components (dependency clusters) - and betweenness centrality (bottleneck machines). -- Stage 2 -- Rules: derive compliance flags for overdue maintenance, high-risk - machines, sensor anomalies, chronic downtime, parts reorder, and expiring - certifications. Chain individual flags into a composite risk tier - (Critical / Elevated / Standard). -- Stage 3 -- Prescriptive: schedule preventive maintenance across a multi-period - horizon, assigning qualified technicians to machines. The optimization - consumes outputs from all earlier stages: per-period failure predictions - from Stage 0, betweenness centrality from Stage 1, and overdue-maintenance - flags from Stage 2. -- Stage 4 -- Resilience: analyze the optimal schedule for single-point-of-failure - technicians and recommend cross-training to eliminate concentration risk. +"""Machine Maintenance -- multi-reasoner template (PyRel v1). + +A 50-machine, 3-plant, 12-period manufacturing operation. The script threads four +reasoners through a single ontology so each stage's enrichments feed the next: + + Stage 1 Querying -- OEE by plant, downtime drivers, failure ranking, + waste rates, technician coverage. + Stage 2 Rules -- per-machine risk tier (chronic / high-risk / overdue). + Stage 3 Graph -- machine-product producibility bottlenecks. + Stage 4 Prescriptive -- preventive-maintenance schedule + a technician what-if. + +Data is the bundled MANUFACTURING.PUBLIC sample (data/*.csv). Run: - `python machine_maintenance.py` + python machine_maintenance.py Output: - Prints OEE and anomaly analysis, graph clusters and centrality, compliance - flags with composite risk tier, optimized maintenance schedule, and - resilience analysis with cross-training recommendations. + Prints each stage's findings to stdout. """ from pathlib import Path from pandas import read_csv -from relationalai.semantics import ( - Boolean, - Float, - Integer, - Model, - String, - distinct, - max, - sum, -) +from relationalai.semantics import Float, Integer, Model, String, distinct from relationalai.semantics.reasoners.graph import Graph from relationalai.semantics.reasoners.prescriptive import Problem from relationalai.semantics.std import aggregates as aggs @@ -53,1665 +32,540 @@ # -------------------------------------------------- DATA_DIR = Path(__file__).parent / "data" -PERIOD_HORIZON = 4 # number of discrete planning periods -PARTS_CAPACITY_PER_PERIOD = 5 # max maintenance jobs per period (parts/bay limit) -TRAVEL_COST_PER_HOUR = 50.0 # cost penalty when technician travels to another facility -CENTRALITY_WEIGHT = 2.0 # multiplier for betweenness centrality in failure cost -OVERDUE_DEADLINE = 2 # overdue machines must be maintained by this period -CHRONIC_DOWNTIME_THRESHOLD = 8 # event count above which a machine is chronic - -# -------------------------------------------------- -# Load CSV data -# -------------------------------------------------- +PERIOD_HORIZON = 12 # weekly planning periods +OEE_PLANNED_MIN_PER_RUN = 480 # planned minutes per production run (availability base) +CHRONIC_DOWNTIME_THRESHOLD = 15 # downtime events above which a machine is chronic +HIGH_RISK_FP = 0.20 # failure-probability cutoff for high-risk +HIGH_RISK_CRITICALITY = 4 # criticality cutoff for high-risk +OVERDUE_RUL = 9 # remaining-useful-life at/below which maintenance is overdue -# Equipment and maintenance data. -machines_df = read_csv(DATA_DIR / "machines.csv") -technicians_df = read_csv(DATA_DIR / "technicians.csv") -availability_df = read_csv(DATA_DIR / "availability.csv") -qualifications_df = read_csv(DATA_DIR / "qualifications.csv") -parts_df = read_csv(DATA_DIR / "parts_inventory.csv") -cert_df = read_csv(DATA_DIR / "certification_expiry.csv") - -# Sensor and prediction data. -sensors_df = read_csv(DATA_DIR / "sensors.csv") -sensor_readings_df = read_csv(DATA_DIR / "sensor_readings.csv") -failure_pred_df = read_csv(DATA_DIR / "failure_predictions.csv") - -# Operational data. -downtime_df = read_csv(DATA_DIR / "downtime_events.csv") -production_df = read_csv(DATA_DIR / "production_runs.csv") -training_df = read_csv(DATA_DIR / "training_options.csv") +model = Model("machine_maintenance") # -------------------------------------------------- -# Define semantic model & load data +# Concepts & data loading # -------------------------------------------------- -model = Model("machine_maintenance") - -# Machine concept: manufacturing machines with ML-predicted failure probability, -# numeric criticality (1-5), maintenance duration, and estimated parts cost. +# Machine --------------------------------------------------------------------- Machine = model.Concept("Machine", identify_by={"machine_id": String}) -Machine.machine_name = model.Property(f"{Machine} has {String:machine_name}") +Machine.machine_name = model.Property(f"{Machine} has name {String:machine_name}") Machine.machine_type = model.Property(f"{Machine} has type {String:machine_type}") -Machine.facility = model.Property(f"{Machine} at {String:facility}") -Machine.location = model.Property(f"{Machine} in {String:location}") -Machine.remaining_useful_life = model.Property( - f"{Machine} has remaining useful life {Float:remaining_useful_life}" -) -Machine.failure_probability = model.Property( - f"{Machine} has failure probability {Float:failure_probability}" -) +Machine.facility = model.Property(f"{Machine} at facility {String:facility}") +Machine.location = model.Property(f"{Machine} at location {String:location}") +Machine.remaining_useful_life = model.Property(f"{Machine} has remaining useful life {Float:remaining_useful_life}") +Machine.failure_probability = model.Property(f"{Machine} has failure probability {Float:failure_probability}") Machine.criticality = model.Property(f"{Machine} has criticality {Integer:criticality}") -Machine.maintenance_duration_hours = model.Property( - f"{Machine} requires {Integer:maintenance_duration_hours} hours" -) -Machine.last_maintenance_date = model.Property( - f"{Machine} last maintained {String:last_maintenance_date}" -) -Machine.parts_required = model.Property( - f"{Machine} needs parts {String:parts_required}" -) -Machine.estimated_parts_cost = model.Property( - f"{Machine} has parts cost {Float:estimated_parts_cost}" -) -model.define(Machine.new(model.data(machines_df).to_schema())) - -# Technician concept: maintenance personnel with skills, certifications, -# hourly rates, and weekly hour caps. -Technician = model.Concept("Technician", identify_by={"technician_id": String}) -Technician.technician_name = model.Property( - f"{Technician} has {String:technician_name}" -) -Technician.skill_level = model.Property( - f"{Technician} has skill level {String:skill_level}" -) -Technician.base_location = model.Property( - f"{Technician} based in {String:base_location}" -) -Technician.certifications = model.Property( - f"{Technician} certified for {String:certifications}" -) -Technician.hourly_rate = model.Property( - f"{Technician} has hourly rate {Float:hourly_rate}" -) -Technician.max_weekly_hours = model.Property( - f"{Technician} has max weekly hours {Integer:max_weekly_hours}" -) -Technician.specialization = model.Property( - f"{Technician} specializes in {String:specialization}" -) -model.define(Technician.new(model.data(technicians_df).to_schema())) - -# Qualification concept: pre-computed mapping of which technicians are -# certified to service which machine types. -Qualification = model.Concept( - "Qualification", identify_by={"technician_id": String, "machine_type": String} -) -Qualification.technician = model.Property(f"{Qualification} for {Technician}") -Qualification.machine_type_str = model.Property( - f"{Qualification} covers {String:machine_type_str}" -) -qual_data = model.data(qualifications_df) -model.define( - q := Qualification.new( - technician_id=qual_data["technician_id"], machine_type=qual_data["machine_type"] - ), - q.machine_type_str(qual_data["machine_type"]), -) -model.define(Qualification.technician(Technician)).where( - Qualification.technician_id == Technician.technician_id -) - -# PartsInventory concept: spare parts stock levels at each facility. -PartsInventory = model.Concept("PartsInventory", identify_by={"part_id": String}) -PartsInventory.facility = model.Property(f"{PartsInventory} at {String:facility}") -PartsInventory.part_name = model.Property(f"{PartsInventory} has {String:part_name}") -PartsInventory.stock_level = model.Property( - f"{PartsInventory} has {Integer:stock_level} units in stock" -) -PartsInventory.min_order_qty = model.Property( - f"{PartsInventory} minimum order {Integer:min_order_qty} units" -) -model.define(PartsInventory.new(model.data(parts_df).to_schema())) - -# CertificationExpiry concept: tracks days remaining on technician-machine-type -# certifications. Used by rules stage to flag expiring qualifications. -CertificationExpiry = model.Concept( - "CertificationExpiry", - identify_by={"technician_id": String, "machine_type": String}, -) -CertificationExpiry.days_remaining = model.Property( - f"{CertificationExpiry} has {Integer:days_remaining} days remaining" -) -CertificationExpiry.technician = model.Property( - f"{CertificationExpiry} for {Technician}" -) -cert_data_ref = model.data(cert_df) -model.define( - c := CertificationExpiry.new( - technician_id=cert_data_ref["technician_id"], - machine_type=cert_data_ref["machine_type"], - ), - c.days_remaining(cert_data_ref["days_remaining"]), -) -model.define(CertificationExpiry.technician(Technician)).where( - CertificationExpiry.technician_id == Technician.technician_id -) - -# TrainingOption concept: cross-training options per (technician, machine_type). -# Used by Stage 4 resilience analysis to recommend the cheapest non-local -# cross-training candidate for each concentrated machine type. -TrainingOption = model.Concept( - "TrainingOption", - identify_by={"technician_id": String, "machine_type": String}, -) -TrainingOption.training_cost = model.Property( - f"{TrainingOption} costs {Float:training_cost}" -) -TrainingOption.training_weeks = model.Property( - f"{TrainingOption} takes {Integer:training_weeks} weeks" -) -TrainingOption.technician = model.Property(f"{TrainingOption} for {Technician}") -training_data = model.data(training_df) -model.define( - to_ := TrainingOption.new( - technician_id=training_data["technician_id"], - machine_type=training_data["machine_type"], - ), - to_.training_cost(training_data["training_cost"]), - to_.training_weeks(training_data["training_weeks"]), -) -model.define(TrainingOption.technician(Technician)).where( - TrainingOption.technician_id == Technician.technician_id -) +Machine.maintenance_duration_hours = model.Property(f"{Machine} needs {Integer:maintenance_duration_hours} maintenance hours") +Machine.estimated_parts_cost = model.Property(f"{Machine} has parts cost {Float:estimated_parts_cost}") -# Period concept: discrete planning periods (1..PERIOD_HORIZON). -Period = model.Concept("Period", identify_by={"pid": Integer}) -period_data = model.data([{"pid": t} for t in range(1, PERIOD_HORIZON + 1)]) -model.define(Period.new(pid=period_data["pid"])) - -# -------------------------------------------------- -# New concepts: sensors, predictions, downtime, production -# -------------------------------------------------- - -# Sensor concept: physical sensors attached to machines with thresholds. -Sensor = model.Concept("Sensor", identify_by={"sensor_id": String}) -Sensor.machine_id_str = model.Property(f"{Sensor} for machine {String:machine_id_str}") -Sensor.sensor_type = model.Property(f"{Sensor} measures {String:sensor_type}") -Sensor.unit = model.Property(f"{Sensor} in {String:unit}") -Sensor.warning_threshold = model.Property( - f"{Sensor} has warning threshold {Float:warning_threshold}" -) -Sensor.critical_threshold = model.Property( - f"{Sensor} has critical threshold {Float:critical_threshold}" -) -Sensor.machine = model.Property(f"{Sensor} attached to {Machine}") - -sensor_src = model.data(sensors_df) +_m = model.data(read_csv(DATA_DIR / "machines.csv")) model.define( - s := Sensor.new(sensor_id=sensor_src["sensor_id"]), - s.machine_id_str(sensor_src["machine_id"]), - s.sensor_type(sensor_src["sensor_type"]), - s.unit(sensor_src["unit"]), - s.warning_threshold(sensor_src["warning_threshold"]), - s.critical_threshold(sensor_src["critical_threshold"]), -) -model.define(Sensor.machine(Machine)).where( - Sensor.machine_id_str == Machine.machine_id -) - -# SensorReading concept: periodic sensor measurements with anomaly flags. -SensorReading = model.Concept( - "SensorReading", - identify_by={"sensor_id": String, "machine_id": String, "pid": Integer}, -) -SensorReading.value = model.Property(f"{SensorReading} has value {Float:value}") -SensorReading.is_anomaly = model.Property( - f"{SensorReading} anomaly flag {Integer:is_anomaly}" -) -SensorReading.sensor = model.Property(f"{SensorReading} from {Sensor}") -SensorReading.machine = model.Property(f"{SensorReading} on {Machine}") -SensorReading.period = model.Property(f"{SensorReading} in {Period}") - -sr_src = model.data(sensor_readings_df) + m := Machine.new(machine_id=_m["machine_id"]), + m.machine_name(_m["machine_name"]), + m.machine_type(_m["machine_type"]), + m.facility(_m["facility"]), + m.location(_m["location"]), + m.remaining_useful_life(_m["remaining_useful_life"]), + m.failure_probability(_m["failure_probability"]), + m.criticality(_m["criticality"]), + m.maintenance_duration_hours(_m["maintenance_duration_hours"]), + m.estimated_parts_cost(_m["estimated_parts_cost"]), +) + +# Product --------------------------------------------------------------------- +Product = model.Concept("Product", identify_by={"product_id": String}) +Product.product_name = model.Property(f"{Product} has name {String:product_name}") +_p = model.data(read_csv(DATA_DIR / "products.csv")) model.define( - sr := SensorReading.new( - sensor_id=sr_src["sensor_id"], - machine_id=sr_src["machine_id"], - pid=sr_src["period"], - ), - sr.value(sr_src["value"]), - sr.is_anomaly(sr_src["is_anomaly"]), -) -SRSensor = Sensor.ref() -SRMachine = Machine.ref() -SRPeriod = Period.ref() -model.define(SensorReading.sensor(SRSensor)).where( - SensorReading.sensor_id == SRSensor.sensor_id -) -model.define(SensorReading.machine(SRMachine)).where( - SensorReading.machine_id == SRMachine.machine_id -) -model.define(SensorReading.period(SRPeriod)).where( - SensorReading.pid == SRPeriod.pid -) - -# FailurePrediction concept: ML-predicted per-period failure probabilities. -# These replace the static Machine.failure_probability in the optimization -# objective, giving period-specific degradation curves. -FailurePrediction = model.Concept( - "FailurePrediction", identify_by={"prediction_id": String} -) -FailurePrediction.machine_id_str = model.Property( - f"{FailurePrediction} for machine {String:machine_id_str}" -) -FailurePrediction.period_int = model.Property( - f"{FailurePrediction} in period {Integer:period_int}" + p := Product.new(product_id=_p["product_id"]), + p.product_name(_p["product_name"]), ) -FailurePrediction.failure_probability = model.Property( - f"{FailurePrediction} has failure probability {Float:failure_probability}" -) -FailurePrediction.predicted_failure_mode = model.Property( - f"{FailurePrediction} predicts mode {String:predicted_failure_mode}" -) -FailurePrediction.confidence = model.Property( - f"{FailurePrediction} has confidence {Float:confidence}" -) -FailurePrediction.machine = model.Property(f"{FailurePrediction} for {Machine}") -FailurePrediction.period = model.Property(f"{FailurePrediction} in {Period}") -fp_src = model.data(failure_pred_df) +# Technician ------------------------------------------------------------------ +Technician = model.Concept("Technician", identify_by={"technician_id": String}) +Technician.technician_name = model.Property(f"{Technician} has name {String:technician_name}") +Technician.skill_level = model.Property(f"{Technician} has skill level {String:skill_level}") +Technician.base_location = model.Property(f"{Technician} based at {String:base_location}") +Technician.hourly_rate = model.Property(f"{Technician} has hourly rate {Float:hourly_rate}") +Technician.max_weekly_hours = model.Property(f"{Technician} has max weekly hours {Integer:max_weekly_hours}") +_t = model.data(read_csv(DATA_DIR / "technicians.csv")) model.define( - fp := FailurePrediction.new(prediction_id=fp_src["prediction_id"]), - fp.machine_id_str(fp_src["machine_id"]), - fp.period_int(fp_src["period"]), - fp.failure_probability(fp_src["failure_probability"]), - fp.predicted_failure_mode(fp_src["predicted_failure_mode"]), - fp.confidence(fp_src["confidence"]), -) -FPMachineInit = Machine.ref() -FPPeriodInit = Period.ref() -model.define(FailurePrediction.machine(FPMachineInit)).where( - FailurePrediction.machine_id_str == FPMachineInit.machine_id -) -model.define(FailurePrediction.period(FPPeriodInit)).where( - FailurePrediction.period_int == FPPeriodInit.pid -) - -# DowntimeEvent concept: unplanned and planned downtime events per machine. -DowntimeEvent = model.Concept("DowntimeEvent", identify_by={"event_id": String}) -DowntimeEvent.machine_id_str = model.Property( - f"{DowntimeEvent} for machine {String:machine_id_str}" -) -DowntimeEvent.period_int = model.Property( - f"{DowntimeEvent} in period {Integer:period_int}" -) -DowntimeEvent.fault_category = model.Property( - f"{DowntimeEvent} fault category {String:fault_category}" -) -DowntimeEvent.duration_minutes = model.Property( - f"{DowntimeEvent} lasted {Integer:duration_minutes} minutes" -) -DowntimeEvent.is_planned = model.Property( - f"{DowntimeEvent} planned flag {Integer:is_planned}" -) -DowntimeEvent.machine = model.Property(f"{DowntimeEvent} on {Machine}") - -dt_src = model.data(downtime_df) + t := Technician.new(technician_id=_t["technician_id"]), + t.technician_name(_t["technician_name"]), + t.skill_level(_t["skill_level"]), + t.base_location(_t["base_location"]), + t.hourly_rate(_t["hourly_rate"]), + t.max_weekly_hours(_t["max_weekly_hours"]), +) + +# Qualification (technician x machine_type) ----------------------------------- +Qualification = model.Concept("Qualification", identify_by={"technician_id": String, "machine_type": String}) +Qualification.technician = model.Property(f"{Qualification} held by {Technician}") +Qualification.machine_type_str = model.Property(f"{Qualification} covers type {String:machine_type_str}") +_q = model.data(read_csv(DATA_DIR / "qualifications.csv")) model.define( - dt := DowntimeEvent.new(event_id=dt_src["event_id"]), - dt.machine_id_str(dt_src["machine_id"]), - dt.period_int(dt_src["period"]), - dt.fault_category(dt_src["fault_category"]), - dt.duration_minutes(dt_src["duration_minutes"]), - dt.is_planned(dt_src["is_planned"]), -) -model.define(DowntimeEvent.machine(Machine)).where( - DowntimeEvent.machine_id_str == Machine.machine_id + q := Qualification.new(technician_id=_q["technician_id"], machine_type=_q["machine_type"]), + q.machine_type_str(_q["machine_type"]), ) +_QT = Technician.ref() +model.define(Qualification.technician(_QT)).where(Qualification.technician_id == _QT.technician_id) -# ProductionRun concept: production output per machine per period. +# ProductionRun --------------------------------------------------------------- ProductionRun = model.Concept("ProductionRun", identify_by={"run_id": String}) -ProductionRun.machine_id_str = model.Property( - f"{ProductionRun} for machine {String:machine_id_str}" -) -ProductionRun.period_int = model.Property( - f"{ProductionRun} in period {Integer:period_int}" -) -ProductionRun.planned_quantity = model.Property( - f"{ProductionRun} planned {Integer:planned_quantity} units" -) -ProductionRun.actual_quantity = model.Property( - f"{ProductionRun} produced {Integer:actual_quantity} units" -) -ProductionRun.good_quantity = model.Property( - f"{ProductionRun} good output {Integer:good_quantity} units" -) -ProductionRun.machine = model.Property(f"{ProductionRun} on {Machine}") - -pr_src = model.data(production_df) +ProductionRun.machine_id_str = model.Property(f"{ProductionRun} on machine {String:machine_id_str}") +ProductionRun.product_id_str = model.Property(f"{ProductionRun} of product {String:product_id_str}") +ProductionRun.period_int = model.Property(f"{ProductionRun} in period {Integer:period_int}") +ProductionRun.actual_quantity = model.Property(f"{ProductionRun} actual qty {Integer:actual_quantity}") +ProductionRun.good_quantity = model.Property(f"{ProductionRun} good qty {Integer:good_quantity}") +ProductionRun.waste_quantity = model.Property(f"{ProductionRun} waste qty {Integer:waste_quantity}") +ProductionRun.actual_speed = model.Property(f"{ProductionRun} actual speed {Float:actual_speed}") +ProductionRun.target_speed = model.Property(f"{ProductionRun} target speed {Float:target_speed}") +ProductionRun.machine = model.Property(f"{ProductionRun} runs on {Machine}") +ProductionRun.product = model.Property(f"{ProductionRun} produces {Product}") +_pr = model.data(read_csv(DATA_DIR / "production_runs.csv")) model.define( - pr := ProductionRun.new(run_id=pr_src["run_id"]), - pr.machine_id_str(pr_src["machine_id"]), - pr.period_int(pr_src["period"]), - pr.planned_quantity(pr_src["planned_quantity"]), - pr.actual_quantity(pr_src["actual_quantity"]), - pr.good_quantity(pr_src["good_quantity"]), -) -model.define(ProductionRun.machine(Machine)).where( - ProductionRun.machine_id_str == Machine.machine_id -) - -# -------------------------------------------------- -# Cross-product concepts (scheduling decision space) -# -------------------------------------------------- - -# MachinePeriod concept: (machine, period) pairs. -MachinePeriod = model.Concept( - "MachinePeriod", identify_by={"machine_id": String, "pid": Integer} -) -MachinePeriod.machine = model.Property(f"{MachinePeriod} for {Machine}") -MachinePeriod.period = model.Property(f"{MachinePeriod} in {Period}") -MpInitM = Machine.ref() -MpInitP = Period.ref() + r := ProductionRun.new(run_id=_pr["run_id"]), + r.machine_id_str(_pr["machine_id"]), + r.product_id_str(_pr["product_id"]), + r.period_int(_pr["period"]), + r.actual_quantity(_pr["actual_quantity"]), + r.good_quantity(_pr["good_quantity"]), + r.waste_quantity(_pr["waste_quantity"]), + r.actual_speed(_pr["actual_speed"]), + r.target_speed(_pr["target_speed"]), +) +_PRM = Machine.ref() +_PRP = Product.ref() +model.define(ProductionRun.machine(_PRM)).where(ProductionRun.machine_id_str == _PRM.machine_id) +model.define(ProductionRun.product(_PRP)).where(ProductionRun.product_id_str == _PRP.product_id) + +# DowntimeEvent --------------------------------------------------------------- +DowntimeEvent = model.Concept("DowntimeEvent", identify_by={"event_id": String}) +DowntimeEvent.machine_id_str = model.Property(f"{DowntimeEvent} on machine {String:machine_id_str}") +DowntimeEvent.fault_name = model.Property(f"{DowntimeEvent} has fault {String:fault_name}") +DowntimeEvent.duration_minutes = model.Property(f"{DowntimeEvent} lasted {Integer:duration_minutes} minutes") +DowntimeEvent.is_planned = model.Property(f"{DowntimeEvent} planned flag {Integer:is_planned}") +DowntimeEvent.machine = model.Property(f"{DowntimeEvent} affects {Machine}") +_de = model.data(read_csv(DATA_DIR / "downtime_events.csv")) model.define( - mp := MachinePeriod.new(machine_id=MpInitM.machine_id, pid=MpInitP.pid), - mp.machine(MpInitM), - mp.period(MpInitP), -) - -# Store per-period failure prediction on MachinePeriod for the objective. -MachinePeriod.predicted_fp = model.Property( - f"{MachinePeriod} has predicted failure probability {Float:predicted_fp}" -) -FPJoin = FailurePrediction.ref() -model.where( - MachinePeriod.machine_id == FPJoin.machine_id_str, - MachinePeriod.pid == FPJoin.period_int, -).define(MachinePeriod.predicted_fp(FPJoin.failure_probability)) - -# TechnicianPeriod concept: technician capacity per period in hours. -TechnicianPeriod = model.Concept( - "TechnicianPeriod", identify_by={"technician_id": String, "pid": Integer} -) -TechnicianPeriod.technician = model.Property(f"{TechnicianPeriod} for {Technician}") -TechnicianPeriod.period = model.Property(f"{TechnicianPeriod} in {Period}") -TechnicianPeriod.capacity_hours = model.Property( - f"{TechnicianPeriod} has available hours {Float:capacity_hours}" -) - -avail_data = model.data(availability_df) -TcInit = Technician.ref() -PrInit = Period.ref() + d := DowntimeEvent.new(event_id=_de["event_id"]), + d.machine_id_str(_de["machine_id"]), + d.fault_name(_de["fault_name"]), + d.duration_minutes(_de["duration_minutes"]), + d.is_planned(_de["is_planned"]), +) +_DEM = Machine.ref() +model.define(DowntimeEvent.machine(_DEM)).where(DowntimeEvent.machine_id_str == _DEM.machine_id) + +# FailurePrediction ----------------------------------------------------------- +FailurePrediction = model.Concept("FailurePrediction", identify_by={"prediction_id": String}) +FailurePrediction.machine_id_str = model.Property(f"{FailurePrediction} for machine {String:machine_id_str}") +FailurePrediction.period_int = model.Property(f"{FailurePrediction} in period {Integer:period_int}") +FailurePrediction.failure_probability = model.Property(f"{FailurePrediction} probability {Float:failure_probability}") +FailurePrediction.predicted_failure_mode = model.Property(f"{FailurePrediction} mode {String:predicted_failure_mode}") +_fp = model.data(read_csv(DATA_DIR / "failure_predictions.csv")) model.define( - tp := TechnicianPeriod.new( - technician_id=TcInit.technician_id, - pid=PrInit.pid, - capacity_hours=avail_data["available"] * TcInit.max_weekly_hours, - ), - tp.technician(TcInit), - tp.period(PrInit), -).where( - TcInit.technician_id == avail_data["technician_id"], - PrInit.pid == avail_data["period"], -) - -# TechnicianMachinePeriod concept: (technician, machine, period) triples, -# restricted to qualified pairs only. -TechnicianMachinePeriod = model.Concept( - "TechnicianMachinePeriod", - identify_by={"technician_id": String, "machine_id": String, "pid": Integer}, -) -TechnicianMachinePeriod.technician = model.Property( - f"{TechnicianMachinePeriod} for {Technician}" -) -TechnicianMachinePeriod.machine = model.Property( - f"{TechnicianMachinePeriod} for {Machine}" -) -TechnicianMachinePeriod.period = model.Property( - f"{TechnicianMachinePeriod} in {Period}" -) -TechnicianMachinePeriod.same_location = model.Property( - f"{TechnicianMachinePeriod} same location flag {Integer:same_location}" -) - -QualRef = Qualification.ref() -TmpInitTech = Technician.ref() -TmpInitMach = Machine.ref() -TmpInitPer = Period.ref() + f := FailurePrediction.new(prediction_id=_fp["prediction_id"]), + f.machine_id_str(_fp["machine_id"]), + f.period_int(_fp["period"]), + f.failure_probability(_fp["failure_probability"]), + f.predicted_failure_mode(_fp["predicted_failure_mode"]), +) + +# MachineProductCapability (machine x product) -------------------------------- +MachineProductCapability = model.Concept("MachineProductCapability", identify_by={"machine_id": String, "product_id": String}) +MachineProductCapability.machine_id_str = model.Property(f"{MachineProductCapability} of machine {String:machine_id_str}") +MachineProductCapability.product_id_str = model.Property(f"{MachineProductCapability} for product {String:product_id_str}") +MachineProductCapability.machine = model.Property(f"{MachineProductCapability} via {Machine}") +MachineProductCapability.product = model.Property(f"{MachineProductCapability} makes {Product}") +_mpc = model.data(read_csv(DATA_DIR / "machine_product_capabilities.csv")) model.define( - tmp := TechnicianMachinePeriod.new( - technician_id=TmpInitTech.technician_id, - machine_id=TmpInitMach.machine_id, - pid=TmpInitPer.pid, - ), - tmp.technician(TmpInitTech), - tmp.machine(TmpInitMach), - tmp.period(TmpInitPer), -).where( - QualRef.technician(TmpInitTech), - QualRef.machine_type_str == TmpInitMach.machine_type, + c := MachineProductCapability.new(machine_id=_mpc["machine_id"], product_id=_mpc["product_id"]), + c.machine_id_str(_mpc["machine_id"]), + c.product_id_str(_mpc["product_id"]), ) +_CM = Machine.ref() +_CP = Product.ref() +model.define(MachineProductCapability.machine(_CM)).where(MachineProductCapability.machine_id_str == _CM.machine_id) +model.define(MachineProductCapability.product(_CP)).where(MachineProductCapability.product_id_str == _CP.product_id) -# Derived property: same_location flag (1 if co-located, 0 otherwise). -TmpRef = TechnicianMachinePeriod.ref() -TmpTech = Technician.ref() -TmpMach = Machine.ref() -model.where( - TmpRef.technician(TmpTech), - TmpRef.machine(TmpMach), - TmpTech.base_location == TmpMach.location, -).define(TmpRef.same_location(1)) -model.where( - TmpRef.technician(TmpTech), - TmpRef.machine(TmpMach), - TmpTech.base_location != TmpMach.location, -).define(TmpRef.same_location(0)) -# -------------------------------------------------- -# Machine-level derived aggregates (for querying & rules) -# -------------------------------------------------- +def banner(text): + print(f"\n{'=' * 70}\n{text}\n{'=' * 70}") -# Production aggregates: total planned, actual, and good quantities. -Machine.total_planned_qty = model.Property( - f"{Machine} has total planned qty {Float:total_planned_qty}" -) -Machine.total_actual_qty = model.Property( - f"{Machine} has total actual qty {Float:total_actual_qty}" -) -Machine.total_good_qty = model.Property( - f"{Machine} has total good qty {Float:total_good_qty}" -) -model.define(Machine.total_planned_qty( - aggs.sum(ProductionRun.planned_quantity).per(Machine) - .where(ProductionRun.machine(Machine)) | 0 -)) -model.define(Machine.total_actual_qty( - aggs.sum(ProductionRun.actual_quantity).per(Machine) - .where(ProductionRun.machine(Machine)) | 0 -)) -model.define(Machine.total_good_qty( - aggs.sum(ProductionRun.good_quantity).per(Machine) - .where(ProductionRun.machine(Machine)) | 0 -)) - -# Performance ratio (actual / planned) and quality ratio (good / actual). -Machine.performance_ratio = model.Property( - f"{Machine} has performance ratio {Float:performance_ratio}" -) -Machine.quality_ratio = model.Property( - f"{Machine} has quality ratio {Float:quality_ratio}" -) -model.where(Machine.total_planned_qty > 0).define( - Machine.performance_ratio( - floats.float(Machine.total_actual_qty) - / floats.float(Machine.total_planned_qty) - ) -) -model.where(Machine.total_actual_qty > 0).define( - Machine.quality_ratio( - floats.float(Machine.total_good_qty) - / floats.float(Machine.total_actual_qty) - ) -) -# Downtime aggregates: total downtime minutes and event count. -Machine.total_downtime_minutes = model.Property( - f"{Machine} has total downtime {Float:total_downtime_minutes} minutes" -) -Machine.downtime_event_count = model.Property( - f"{Machine} has downtime event count {Float:downtime_event_count}" -) -model.define(Machine.total_downtime_minutes( - aggs.sum(DowntimeEvent.duration_minutes).per(Machine) - .where(DowntimeEvent.machine(Machine)) | 0 -)) -model.define(Machine.downtime_event_count( - aggs.count(DowntimeEvent).per(Machine) - .where(DowntimeEvent.machine(Machine)) | 0 -)) - -# Sensor anomaly count across all periods. -Machine.anomaly_count = model.Property( - f"{Machine} has anomaly count {Float:anomaly_count}" -) -model.define(Machine.anomaly_count( - aggs.count(SensorReading).per(Machine).where( - SensorReading.machine(Machine), - SensorReading.is_anomaly == 1, - ) | 0 -)) - -# -------------------------------------------------- -# Stage 0: Querying -- Operational Intelligence -# -------------------------------------------------- +# ================================================================== +# STAGE 1: Querying -- diagnose plant operations +# ================================================================== -print("=" * 70) -print("STAGE 0: Querying -- Operational Intelligence") -print("=" * 70) +banner("STAGE 1 Querying") -# 0a. OEE proxy by facility (Performance x Quality). -# Quality is uniformly high (~98%); the differentiator is Performance. -oee_df = ( - model.select( - Machine.machine_id.alias("machine_id"), +# --- Q1: OEE by plant = Availability x Performance x Quality --- +# Performance / quality / run counts come from production runs; unplanned downtime +# from downtime events. Aggregated separately (different fact tables) then combined. +runs_by_plant = model.where(ProductionRun.machine(Machine)).select( + distinct( Machine.facility.alias("facility"), - Machine.performance_ratio.alias("performance"), - Machine.quality_ratio.alias("quality"), - ) - .to_df() -) -oee_by_fac = ( - oee_df.groupby("facility") - .agg(avg_perf=("performance", "mean"), avg_qual=("quality", "mean")) - .reset_index() -) -oee_by_fac["oee_proxy"] = oee_by_fac["avg_perf"] * oee_by_fac["avg_qual"] -oee_by_fac = oee_by_fac.sort_values("oee_proxy", ascending=False) - -print("\nOEE proxy by facility (Performance x Quality):") -for _, row in oee_by_fac.iterrows(): - print( - f" {row['facility']}: " - f"Perf={row['avg_perf']:.1%}, Qual={row['avg_qual']:.1%}, " - f"OEE={row['oee_proxy']:.1%}" - ) - -# 0b. Sensor anomalies: machines with above-threshold readings. -SensorQ = Sensor.ref() -anomaly_detail_df = ( - model.select( - SensorReading.machine_id.alias("machine_id"), - SensorReading.pid.alias("period"), - SensorReading.value.alias("value"), - SensorQ.sensor_type.alias("sensor_type"), - SensorQ.warning_threshold.alias("warning"), - SensorQ.critical_threshold.alias("critical"), - ) - .where( - SensorReading.is_anomaly == 1, - SensorReading.sensor(SensorQ), - ) - .to_df() - .sort_values(["machine_id", "period"]) -) -anomaly_counts = anomaly_detail_df.groupby("machine_id").size().reset_index(name="count") -anomaly_counts = anomaly_counts.merge( - machines_df[["machine_id", "machine_type", "facility"]], on="machine_id" -).sort_values("count", ascending=False) - -print(f"\nSensor anomalies ({len(anomaly_detail_df)} readings across " - f"{len(anomaly_counts)} machines):") -for _, row in anomaly_counts.iterrows(): - print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " - f"{row['count']} anomalies") - -by_fac = anomaly_counts.groupby("facility")["count"].sum() -print(f" By facility: {dict(by_fac.sort_values(ascending=False))}") - -# 0c. Failure trajectories: identify machines with steepest degradation. -FPMachQ = Machine.ref() -fp_query_df = ( - model.select( - FailurePrediction.machine_id_str.alias("machine_id"), - FPMachQ.machine_type.alias("machine_type"), - FPMachQ.facility.alias("facility"), - FailurePrediction.period_int.alias("period"), - FailurePrediction.failure_probability.alias("failure_probability"), - FailurePrediction.predicted_failure_mode.alias("failure_mode"), - ) - .where(FailurePrediction.machine(FPMachQ)) - .to_df() -) - -pivot = fp_query_df.pivot_table( - index=["machine_id", "machine_type", "facility", "failure_mode"], - columns="period", - values="failure_probability", -).reset_index() -pivot["delta"] = pivot[PERIOD_HORIZON] - pivot[1] -pivot = pivot.sort_values("delta", ascending=False) - -print(f"\nSteepest failure trajectories (period 1 -> {PERIOD_HORIZON}):") -for _, row in pivot.head(6).iterrows(): - print( - f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " - f"{row[1]:.3f} -> {row[PERIOD_HORIZON]:.3f} " - f"(+{row['delta']:.3f}) [{row['failure_mode']}]" - ) - -# -------------------------------------------------- -# Stage 1: Graph -- dependency clusters & centrality -# -------------------------------------------------- - -# Graph directly on Machine — no mirror concept needed. -dep_graph = Graph(model, directed=False, weighted=False, node_concept=Machine, aggregator="sum") - -m1 = Machine.ref("m1") -m2 = Machine.ref("m2") -q1 = Qualification.ref("q1") -q2 = Qualification.ref("q2") -# Two machines are adjacent in the dependency graph when at least one -# technician is qualified to service both machine types. -model.define(dep_graph.Edge.new(src=m1, dst=m2)).where( - m1.machine_type == q1.machine_type_str, - m2.machine_type == q2.machine_type_str, - q1.technician_id == q2.technician_id, - m1.machine_id < m2.machine_id, -) - -print(f"\n{'=' * 70}") -print("STAGE 1: Graph Analysis -- Dependency Clusters & Centrality") -print("=" * 70) - -dep_graph.num_nodes().inspect() -dep_graph.num_edges().inspect() - -# Weakly connected components: identify dependency clusters. -wcc = dep_graph.weakly_connected_component() - -node_ref = dep_graph.Node.ref("n") -comp_ref = dep_graph.Node.ref("comp") - -wcc_df = ( - model.where(wcc(node_ref, comp_ref)) - .select( - node_ref.machine_id.alias("machine_id"), - node_ref.machine_name.alias("machine_name"), - node_ref.machine_type.alias("machine_type"), - node_ref.facility.alias("facility"), - comp_ref.machine_id.alias("component_id"), - aggs.count(node_ref).per(comp_ref).alias("cluster_size"), - ) - .to_df() -) - -num_clusters = wcc_df["component_id"].nunique() -print(f"\nDependency clusters found: {num_clusters}") -for comp_id in sorted(wcc_df["component_id"].unique()): - comp_df = wcc_df[wcc_df["component_id"] == comp_id] - cluster_size = int(comp_df["cluster_size"].iloc[0]) - facilities = ", ".join(sorted(comp_df["facility"].unique())) - print(f"\n Cluster {comp_id}: {cluster_size} machines ({facilities})") - for _, row in comp_df.sort_values(["facility", "machine_name"]).head(5).iterrows(): - print(f" - {row['machine_name']} ({row['machine_type']}, {row['facility']})") - if cluster_size > 5: - print(f" ... and {cluster_size - 5} more") - -# Betweenness centrality: find bottleneck machines. -betweenness = dep_graph.betweenness_centrality() - -node_b = dep_graph.Node.ref("nb") -btwn_score = Float.ref("btwn") - -betweenness_df = ( - model.where(betweenness(node_b, btwn_score)) - .select( - node_b.machine_id.alias("machine_id"), - node_b.machine_name.alias("machine_name"), - node_b.machine_type.alias("machine_type"), - node_b.facility.alias("facility"), - node_b.failure_probability.alias("failure_probability"), - btwn_score.alias("betweenness"), - ) - .to_df() - .sort_values("betweenness", ascending=False) - .reset_index(drop=True) -) - -print("\nTop bottleneck machines (betweenness centrality):") -for _, row in betweenness_df.head(10).iterrows(): - print( - f" {row['machine_id']} ({row['machine_type']}, {row['facility']}): " - f"betweenness={row['betweenness']:.4f}, " - f"failure_prob={row['failure_probability']:.3f}" - ) - -# Store normalized betweenness directly on Machine. -Machine.betweenness_raw = model.Property( - f"{Machine} has raw betweenness centrality {Float:betweenness_raw}" -) -m_btwn = Machine.ref("m_btwn") -model.define(m_btwn.betweenness_raw(btwn_score)).where(betweenness(m_btwn, btwn_score)) -max_betweenness = max(Machine.betweenness_raw) -Machine.betweenness = model.Property( - f"{Machine} has betweenness centrality {Float:betweenness}" -) -m_norm = Machine.ref("m_norm") -model.where(max_betweenness == 0).define(m_norm.betweenness(0.0)) -model.where(max_betweenness > 0).define( - m_norm.betweenness(m_norm.betweenness_raw / max_betweenness) -) - -# -------------------------------------------------- -# Stage 2: Rules -- compliance flags & composite risk tier -# -------------------------------------------------- - -print(f"\n{'=' * 70}") -print("STAGE 2: Rules -- Compliance Flags & Composite Risk Tier") -print("=" * 70) - -# Rule 1: Machine is overdue for maintenance when remaining useful life -# is less than the time required to perform maintenance. -Machine.is_overdue_maintenance = model.Relationship( - f"{Machine} is overdue maintenance" -) -model.where( - Machine.remaining_useful_life < floats.float(Machine.maintenance_duration_hours) -).define(Machine.is_overdue_maintenance()) - -overdue_df = ( - model.select( - Machine.machine_id.alias("machine_id"), - Machine.machine_name.alias("machine_name"), + aggs.count(ProductionRun).per(Machine.facility).alias("n_runs"), + aggs.avg(floats.float(ProductionRun.actual_speed) / floats.float(ProductionRun.target_speed)) + .per(Machine.facility).alias("performance"), + aggs.sum(floats.float(ProductionRun.good_quantity)).per(Machine.facility).alias("good_q"), + aggs.sum(floats.float(ProductionRun.actual_quantity)).per(Machine.facility).alias("actual_q"), + ) +).to_df() + +dt_unplanned_by_plant = model.where( + DowntimeEvent.machine(Machine), DowntimeEvent.is_planned == 0 +).select( + distinct( Machine.facility.alias("facility"), - Machine.remaining_useful_life.alias("remaining_useful_life"), - Machine.maintenance_duration_hours.alias("maintenance_duration_hours"), - ) - .where(Machine.is_overdue_maintenance()) - .to_df() -) -print(f"\nOverdue maintenance ({len(overdue_df)} machines):") -for _, row in overdue_df.iterrows(): - print( - f" {row['machine_id']} ({row['machine_name']}): " - f"RUL={row['remaining_useful_life']:.1f}h < " - f"duration={int(row['maintenance_duration_hours'])}h" + aggs.sum(floats.float(DowntimeEvent.duration_minutes)).per(Machine.facility).alias("unplanned_dt"), ) +).to_df() -# Rule 2: Machine is high risk when failure probability > 0.3 AND -# criticality >= 4. -Machine.is_high_risk = model.Relationship(f"{Machine} is high risk") -model.where( - Machine.failure_probability > 0.3, - Machine.criticality >= 4, -).define(Machine.is_high_risk()) - -high_risk_df = ( - model.select( - Machine.machine_id.alias("machine_id"), - Machine.machine_name.alias("machine_name"), - Machine.failure_probability.alias("failure_probability"), - Machine.criticality.alias("criticality"), - ) - .where(Machine.is_high_risk()) - .to_df() +oee = runs_by_plant.merge(dt_unplanned_by_plant, on="facility") +oee["n_runs"] = oee["n_runs"].astype(float) # count comes back as Int128Array +oee["availability"] = (oee["n_runs"] * OEE_PLANNED_MIN_PER_RUN - oee["unplanned_dt"]) / ( + oee["n_runs"] * OEE_PLANNED_MIN_PER_RUN ) -print(f"\nHigh-risk machines ({len(high_risk_df)}):") -for _, row in high_risk_df.iterrows(): +oee["quality"] = oee["good_q"] / oee["actual_q"] +oee["oee"] = oee["availability"] * oee["performance"] * oee["quality"] +oee = oee.sort_values("oee", ascending=False) +print("\n-- Q1: OEE by plant --") +for _, row in oee.iterrows(): print( - f" {row['machine_id']} ({row['machine_name']}): " - f"prob={row['failure_probability']:.3f}, crit={int(row['criticality'])}" - ) - -# Rule 3: Machine has sensor anomalies. -Machine.is_anomalous = model.Relationship(f"{Machine} has sensor anomalies") -model.where(Machine.anomaly_count > 0).define(Machine.is_anomalous()) - -anomalous_df = ( - model.select( - Machine.machine_id.alias("machine_id"), - Machine.machine_name.alias("machine_name"), + f" {row['facility']}: availability {row['availability']*100:.1f}% " + f"performance {row['performance']*100:.1f}% quality {row['quality']*100:.1f}% " + f"OEE {row['oee']*100:.1f}%" + ) + +# --- Q2: top downtime drivers by fault name --- +fault_dt = model.select( + distinct( + DowntimeEvent.fault_name.alias("fault_name"), + aggs.sum(floats.float(DowntimeEvent.duration_minutes)).per(DowntimeEvent.fault_name).alias("dt_min"), + ) +).to_df() +total_dt = fault_dt["dt_min"].sum() +fault_dt["pct"] = 100 * fault_dt["dt_min"] / total_dt +fault_dt = fault_dt.sort_values("dt_min", ascending=False) +print(f"\n-- Q2: top downtime by fault name (total {total_dt:.0f} min) --") +for _, row in fault_dt.head(5).iterrows(): + print(f" {row['fault_name']}: {row['dt_min']:.0f} min ({row['pct']:.1f}%)") + +# --- Q3: downtime by plant --- +plant_dt = model.where(DowntimeEvent.machine(Machine)).select( + distinct( Machine.facility.alias("facility"), - Machine.anomaly_count.alias("anomaly_count"), - ) - .where(Machine.is_anomalous()) - .to_df() - .sort_values("anomaly_count", ascending=False) -) -print(f"\nAnomalous machines ({len(anomalous_df)}):") -for _, row in anomalous_df.iterrows(): - print( - f" {row['machine_id']} ({row['machine_name']}, {row['facility']}): " - f"{int(row['anomaly_count'])} anomalies" - ) - -# Rule 4: Machine has chronic downtime (event count > threshold). -Machine.is_chronic_downtime = model.Relationship(f"{Machine} has chronic downtime") -model.where( - Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD -).define(Machine.is_chronic_downtime()) - -chronic_df = ( - model.select( + aggs.sum(floats.float(DowntimeEvent.duration_minutes)).per(Machine.facility).alias("dt_min"), + ) +).to_df() +plant_dt["pct"] = 100 * plant_dt["dt_min"] / plant_dt["dt_min"].sum() +plant_dt = plant_dt.sort_values("dt_min", ascending=False) +print("\n-- Q3: downtime by plant --") +for _, row in plant_dt.iterrows(): + print(f" {row['facility']}: {row['dt_min']:.0f} min ({row['pct']:.1f}%)") + +# --- Q4: highest forward failure risk at the end of the horizon --- +fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( + FailurePrediction.machine_id_str.alias("machine_id"), + FailurePrediction.predicted_failure_mode.alias("mode"), + FailurePrediction.failure_probability.alias("fp"), +).to_df().sort_values("fp", ascending=False) +print(f"\n-- Q4: top failure predictions at period {PERIOD_HORIZON} --") +for _, row in fail_p12.head(5).iterrows(): + print(f" {row['machine_id']} {row['mode']} ({row['fp']*100:.1f}%)") + +# --- Q5: worst waste rates by machine-product --- +waste = model.where(ProductionRun.machine(Machine), ProductionRun.product(Product)).select( + distinct( Machine.machine_id.alias("machine_id"), - Machine.machine_name.alias("machine_name"), - Machine.facility.alias("facility"), - Machine.downtime_event_count.alias("event_count"), - Machine.total_downtime_minutes.alias("total_minutes"), + Product.product_name.alias("product_name"), + ( + aggs.sum(floats.float(ProductionRun.waste_quantity)).per(Machine, Product) + / aggs.sum(floats.float(ProductionRun.actual_quantity)).per(Machine, Product) + ).alias("waste_rate"), + ) +).to_df().sort_values("waste_rate", ascending=False) +print("\n-- Q5: worst waste rates by machine-product --") +for _, row in waste.head(5).iterrows(): + print(f" {row['machine_id']} + {row['product_name']} ({row['waste_rate']*100:.1f}%)") + +# --- Q7: machine types with fewest qualified technicians --- +tech_cov = model.select( + distinct( + Qualification.machine_type_str.alias("machine_type"), + aggs.count(Qualification).per(Qualification.machine_type_str).alias("n_techs"), + ) +).to_df() +tech_cov["n_techs"] = tech_cov["n_techs"].astype(int) +tech_cov = tech_cov.sort_values("n_techs") +print("\n-- Q7: qualified technicians per machine type --") +for _, row in tech_cov.iterrows(): + print(f" {row['machine_type']}: {int(row['n_techs'])}") + + +# ================================================================== +# STAGE 2: Rules -- classify machine risk +# ================================================================== + +banner("STAGE 2 Rules") + +Machine.downtime_event_count = model.Property(f"{Machine} has downtime event count {Integer:downtime_event_count}") +model.define( + Machine.downtime_event_count( + aggs.count(DowntimeEvent).per(Machine).where(DowntimeEvent.machine(Machine)) | 0 ) - .where(Machine.is_chronic_downtime()) - .to_df() - .sort_values("event_count", ascending=False) ) -print(f"\nChronic downtime machines (>{CHRONIC_DOWNTIME_THRESHOLD} events, " - f"{len(chronic_df)} machines):") -for _, row in chronic_df.iterrows(): - print( - f" {row['machine_id']} ({row['machine_name']}, {row['facility']}): " - f"{int(row['event_count'])} events, " - f"{int(row['total_minutes'])} min total downtime" - ) - -# Rule 5: Composite risk tier -- chains overdue, high-risk, and chronic -# downtime flags into a single classification. -Machine.risk_tier = model.Property(f"{Machine} has risk tier {String:risk_tier}") -# Critical: all 3 flags. -model.where( - Machine.is_chronic_downtime(), - Machine.is_high_risk(), - Machine.is_overdue_maintenance(), -).define(Machine.risk_tier("Critical")) - -# Elevated: exactly 2 of 3 flags (enumerate pairs, negate the third). -model.where( - Machine.is_chronic_downtime(), - Machine.is_high_risk(), - model.not_(Machine.is_overdue_maintenance()), -).define(Machine.risk_tier("Elevated")) -model.where( - Machine.is_chronic_downtime(), - model.not_(Machine.is_high_risk()), - Machine.is_overdue_maintenance(), -).define(Machine.risk_tier("Elevated")) -model.where( - model.not_(Machine.is_chronic_downtime()), - Machine.is_high_risk(), - Machine.is_overdue_maintenance(), -).define(Machine.risk_tier("Elevated")) +Machine.is_chronic = model.Relationship(f"{Machine} has chronic downtime") +model.where(Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD).define(Machine.is_chronic()) -# Standard: 0 or 1 flag. -model.where( - model.not_(Machine.is_chronic_downtime()), - model.not_(Machine.is_high_risk()), - model.not_(Machine.is_overdue_maintenance()), -).define(Machine.risk_tier("Standard")) -model.where( - Machine.is_chronic_downtime(), - model.not_(Machine.is_high_risk()), - model.not_(Machine.is_overdue_maintenance()), -).define(Machine.risk_tier("Standard")) -model.where( - model.not_(Machine.is_chronic_downtime()), - Machine.is_high_risk(), - model.not_(Machine.is_overdue_maintenance()), -).define(Machine.risk_tier("Standard")) +Machine.is_high_risk = model.Relationship(f"{Machine} is high risk") model.where( - model.not_(Machine.is_chronic_downtime()), - model.not_(Machine.is_high_risk()), - Machine.is_overdue_maintenance(), -).define(Machine.risk_tier("Standard")) + Machine.failure_probability > HIGH_RISK_FP, Machine.criticality >= HIGH_RISK_CRITICALITY +).define(Machine.is_high_risk()) -risk_tier_df = ( - model.select( - Machine.machine_id.alias("machine_id"), - Machine.machine_name.alias("machine_name"), - Machine.machine_type.alias("machine_type"), - Machine.facility.alias("facility"), - Machine.risk_tier.alias("risk_tier"), - ) - .to_df() - .sort_values("risk_tier") -) -print("\nComposite risk tier:") -for tier in ["Critical", "Elevated", "Standard"]: - tier_machines = risk_tier_df[risk_tier_df["risk_tier"] == tier] - ids = ", ".join(tier_machines["machine_id"].tolist()) - print(f" {tier} ({len(tier_machines)}): {ids}") - -# Rule 6: Parts inventory needs reorder. -PartsInventory.needs_reorder = model.Relationship( - f"{PartsInventory} needs reorder" -) -model.where( - PartsInventory.stock_level <= PartsInventory.min_order_qty -).define(PartsInventory.needs_reorder()) - -reorder_df = ( - model.select( - PartsInventory.part_id.alias("part_id"), - PartsInventory.part_name.alias("part_name"), - PartsInventory.facility.alias("facility"), - PartsInventory.stock_level.alias("stock_level"), - PartsInventory.min_order_qty.alias("min_order_qty"), - ) - .where(PartsInventory.needs_reorder()) - .to_df() -) -print(f"\nParts needing reorder ({len(reorder_df)}):") -for _, row in reorder_df.iterrows(): - print( - f" {row['part_id']} ({row['part_name']}, {row['facility']}): " - f"stock={int(row['stock_level'])} <= min_order={int(row['min_order_qty'])}" - ) +Machine.is_overdue = model.Relationship(f"{Machine} is overdue for maintenance") +model.where(Machine.remaining_useful_life <= OVERDUE_RUL).define(Machine.is_overdue()) -# Rule 7: Certification is expiring when fewer than 30 days remain. -CertificationExpiry.is_expiring = model.Relationship( - f"{CertificationExpiry} is expiring" -) +Machine.risk_tier = model.Property(f"{Machine} has risk tier {String:risk_tier}") +# Critical: all three flags fire +model.where(Machine.is_chronic(), Machine.is_high_risk(), Machine.is_overdue()).define(Machine.risk_tier("Critical")) +# Elevated: exactly two +model.where(Machine.is_chronic(), Machine.is_high_risk(), model.not_(Machine.is_overdue())).define(Machine.risk_tier("Elevated")) +model.where(Machine.is_chronic(), model.not_(Machine.is_high_risk()), Machine.is_overdue()).define(Machine.risk_tier("Elevated")) +model.where(model.not_(Machine.is_chronic()), Machine.is_high_risk(), Machine.is_overdue()).define(Machine.risk_tier("Elevated")) +# Standard: zero or one +model.where(model.not_(Machine.is_chronic()), model.not_(Machine.is_high_risk()), model.not_(Machine.is_overdue())).define(Machine.risk_tier("Standard")) +model.where(Machine.is_chronic(), model.not_(Machine.is_high_risk()), model.not_(Machine.is_overdue())).define(Machine.risk_tier("Standard")) +model.where(model.not_(Machine.is_chronic()), Machine.is_high_risk(), model.not_(Machine.is_overdue())).define(Machine.risk_tier("Standard")) +model.where(model.not_(Machine.is_chronic()), model.not_(Machine.is_high_risk()), Machine.is_overdue()).define(Machine.risk_tier("Standard")) + +tiers = model.select( + distinct( + Machine.risk_tier.alias("tier"), + aggs.count(Machine).per(Machine.risk_tier).alias("n"), + ) +).to_df() +tiers["n"] = tiers["n"].astype(int) +tiers = tiers.sort_values("n", ascending=False) +print("\n-- Q9: machine risk tiers --") +for _, row in tiers.iterrows(): + print(f" {row['tier']}: {int(row['n'])}") + +critical = model.where(Machine.risk_tier == "Critical").select( + Machine.machine_id.alias("machine_id"), + Machine.machine_type.alias("machine_type"), + Machine.facility.alias("facility"), +).to_df().sort_values("machine_id") +print(" Critical machines:") +for _, row in critical.iterrows(): + print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']})") + +# ================================================================== +# STAGE 3: Graph -- producibility bottlenecks +# ================================================================== + +banner("STAGE 3 Graph") + +# Bipartite machine-product graph (edge = machine can produce product). +# Betweenness centrality surfaces machines that bridge many products -- +# production-network bottlenecks whose loss is hardest to route around. +prod_graph = Graph(model, directed=False, weighted=False) +_GM = Machine.ref() +_GP = Product.ref() model.where( - CertificationExpiry.days_remaining < 30 -).define(CertificationExpiry.is_expiring()) - -TechRef = Technician.ref() -expiring_df = ( - model.select( - TechRef.technician_id.alias("technician_id"), - TechRef.technician_name.alias("technician_name"), - CertificationExpiry.machine_type.alias("machine_type"), - CertificationExpiry.days_remaining.alias("days_remaining"), - ) - .where( - CertificationExpiry.is_expiring(), - CertificationExpiry.technician(TechRef), - ) - .to_df() -) -print(f"\nExpiring certifications ({len(expiring_df)}):") -for _, row in expiring_df.iterrows(): - print( - f" {row['technician_id']} ({row['technician_name']}): " - f"{row['machine_type']} -- {int(row['days_remaining'])} days remaining" - ) - -# -------------------------------------------------- -# Stage 3: Prescriptive -- maintenance scheduling -# -------------------------------------------------- - -print(f"\n{'=' * 70}") -print("STAGE 3: Prescriptive -- Maintenance Scheduling") -print("=" * 70) - -problem = Problem(model, Float) - -# References for aggregation. -MachinePeriod_outer = MachinePeriod.ref() -MachinePeriod_inner = MachinePeriod.ref() -TechnicianMachinePeriod_ref = TechnicianMachinePeriod.ref() -Machine_ref = Machine.ref() -Period_outer = Period.ref() -Period_inner = Period.ref() -Technician_ref = Technician.ref() -Period_tc = Period.ref() -MachinePeriod_cap = MachinePeriod.ref() -Period_cap = Period.ref() -TechnicianPeriod_ref = TechnicianPeriod.ref() - -# Decision variable: maintain -- whether to maintain machine m in period t. -MachinePeriod.x_maintain = model.Property( - f"{MachinePeriod} maintain decision {Float:x_maintain}" -) -problem.solve_for( - MachinePeriod.x_maintain, - type="bin", - name=["maintain", MachinePeriod.machine_id, MachinePeriod.pid], -) - -# Decision variable: vulnerable -- whether machine m remains unmaintained -# through period t. -MachinePeriod.x_vulnerable = model.Property( - f"{MachinePeriod} vulnerable flag {Float:x_vulnerable}" -) -problem.solve_for( - MachinePeriod.x_vulnerable, - type="bin", - name=["vulnerable", MachinePeriod.machine_id, MachinePeriod.pid], -) - -# Decision variable: assigned -- whether technician k is assigned to -# machine m in period t. -TechnicianMachinePeriod.x_assigned = model.Property( - f"{TechnicianMachinePeriod} assigned flag {Float:x_assigned}" -) -problem.solve_for( - TechnicianMachinePeriod.x_assigned, - type="bin", - name=[ - "assigned", - TechnicianMachinePeriod.technician_id, - TechnicianMachinePeriod.machine_id, - TechnicianMachinePeriod.pid, - ], -) - -# Constraint: cumulative maintenance coverage. -# For each (machine, tau): sum_{t=1..tau} x_maintain(m,t) + x_vulnerable(m,tau) = 1. -maintained_until_tau = ( - sum(MachinePeriod_inner.x_maintain) - .where( - MachinePeriod_outer.machine(Machine_ref), - MachinePeriod_outer.period(Period_outer), - MachinePeriod_inner.machine(Machine_ref), - MachinePeriod_inner.period(Period_inner), - Period_inner.pid >= 1, - Period_inner.pid <= Period_outer.pid, - ) - .per(Machine_ref, Period_outer) -) -problem.satisfy( - model.require(maintained_until_tau + MachinePeriod_outer.x_vulnerable == 1).where( - MachinePeriod_outer.machine(Machine_ref), - MachinePeriod_outer.period(Period_outer), - ) -) - -# Constraint: assignment-maintenance linkage. -assign_per_mp = ( - sum(TechnicianMachinePeriod_ref.x_assigned) - .where( - TechnicianMachinePeriod_ref.machine(Machine_ref), - TechnicianMachinePeriod_ref.period(Period_outer), - ) - .per(Machine_ref, Period_outer) -) -problem.satisfy( - model.require(assign_per_mp == MachinePeriod_outer.x_maintain).where( - MachinePeriod_outer.machine(Machine_ref), - MachinePeriod_outer.period(Period_outer), - ) -) - -# Constraint: technician hours capacity. -Machine_hrs = Machine.ref() -assigned_hours = ( - sum( - TechnicianMachinePeriod_ref.x_assigned - * Machine_hrs.maintenance_duration_hours - ) - .where( - TechnicianMachinePeriod_ref.technician(Technician_ref), - TechnicianMachinePeriod_ref.period(Period_tc), - TechnicianMachinePeriod_ref.machine(Machine_hrs), - ) - .per(Technician_ref, Period_tc) -) -avail_hours = ( - sum(TechnicianPeriod_ref.capacity_hours) - .where( - TechnicianPeriod_ref.technician(Technician_ref), - TechnicianPeriod_ref.period(Period_tc), - ) - .per(Technician_ref, Period_tc) -) -problem.satisfy(model.require(assigned_hours <= avail_hours)) - -# Constraint: parts/bay capacity per period. -maint_per_period = ( - sum(MachinePeriod_cap.x_maintain) - .where(MachinePeriod_cap.period(Period_cap)) - .per(Period_cap) -) -problem.satisfy(model.require(maint_per_period <= PARTS_CAPACITY_PER_PERIOD)) - -# Constraint (from rules): overdue machines must be maintained by OVERDUE_DEADLINE. -MachinePeriod_overdue = MachinePeriod.ref() -Machine_overdue = Machine.ref() -Period_overdue = Period.ref() -maintained_by_deadline = ( - sum(MachinePeriod_overdue.x_maintain) - .where( - MachinePeriod_overdue.machine(Machine_overdue), - MachinePeriod_overdue.period(Period_overdue), - Period_overdue.pid <= OVERDUE_DEADLINE, - ) - .per(Machine_overdue) -) -problem.satisfy( - model.require(maintained_by_deadline >= 1).where( - Machine_overdue.is_overdue_maintenance() - ) -) - -# Objective: minimize expected total cost. -# 1. Failure risk: per-period failure prediction (from Stage 0) * parts cost -# * criticality * (1 + centrality_weight * betweenness from Stage 1). -# 2. Labor cost: maintenance_duration * technician hourly_rate. -# 3. Travel cost: flat rate * duration when technician is not co-located. -Machine_obj = Machine.ref() -Technician_obj = Technician.ref() -Machine_labor = Machine.ref() -Machine_travel = Machine.ref() -failure_cost = sum( - MachinePeriod_outer.x_vulnerable - * MachinePeriod_outer.predicted_fp - * Machine_obj.estimated_parts_cost - * Machine_obj.criticality - * (1 + CENTRALITY_WEIGHT * Machine_obj.betweenness) -).where( - MachinePeriod_outer.machine(Machine_obj), MachinePeriod_outer.period(Period_outer) -) -labor_cost = sum( - TechnicianMachinePeriod_ref.x_assigned - * Machine_labor.maintenance_duration_hours - * Technician_obj.hourly_rate -).where( - TechnicianMachinePeriod_ref.machine(Machine_labor), - TechnicianMachinePeriod_ref.technician(Technician_obj), - TechnicianMachinePeriod_ref.period(Period_outer), -) -travel_cost = sum( - TechnicianMachinePeriod_ref.x_assigned - * (1 - TechnicianMachinePeriod_ref.same_location) - * Machine_travel.maintenance_duration_hours - * TRAVEL_COST_PER_HOUR -).where( - TechnicianMachinePeriod_ref.machine(Machine_travel), - TechnicianMachinePeriod_ref.period(Period_outer), -) -problem.minimize(failure_cost + labor_cost + travel_cost) - -# -------------------------------------------------- -# Solve and extract results -# -------------------------------------------------- - -problem.solve("highs", time_limit_sec=120) -si = problem.solve_info() -si.display() - -print(f"\nStatus: {si.termination_status}") -print(f"Objective value: {si.objective_value:.2f}") -assert si.termination_status == "OPTIMAL", f"Expected OPTIMAL, got {si.termination_status}" - -# Single solve — query populated properties directly. -value_ref = Float.ref() -maint_machine = Machine.ref("maint_machine") -maint_df = ( - model.select( - MachinePeriod.machine_id.alias("machine_id"), - MachinePeriod.pid.alias("period"), - maint_machine.machine_type.alias("machine_type"), - maint_machine.facility.alias("facility"), - maint_machine.criticality.alias("criticality"), - ) - .where( - MachinePeriod.machine(maint_machine), - MachinePeriod.x_maintain(value_ref), - value_ref > 0.5, - ) - .to_df() -) -maint_df = maint_df.sort_values(["period", "machine_id"]) -print(f"\nMaintenance schedule ({len(maint_df)} jobs):") -for period, g in maint_df.groupby("period"): - print(f" Period {int(period)}:") - for _, row in g.iterrows(): - print( - f" {row['machine_id']} ({row['machine_type']}, {row['facility']}, " - f"crit={int(row['criticality'])})" - ) - -# Query populated assignment properties directly. -assign_machine = Machine.ref("assign_machine") -assign_tech = Technician.ref("assign_tech") -assign_df = ( - model.select( - TechnicianMachinePeriod.technician_id.alias("technician_id"), - TechnicianMachinePeriod.machine_id.alias("machine_id"), - TechnicianMachinePeriod.pid.alias("period"), - assign_machine.machine_type.alias("machine_type"), - assign_machine.location.alias("location"), - assign_machine.maintenance_duration_hours.alias("maintenance_duration_hours"), - assign_tech.technician_name.alias("technician_name"), - assign_tech.base_location.alias("base_location"), - assign_tech.skill_level.alias("skill_level"), - assign_tech.hourly_rate.alias("hourly_rate"), - ) - .where( - TechnicianMachinePeriod.machine(assign_machine), - TechnicianMachinePeriod.technician(assign_tech), - TechnicianMachinePeriod.x_assigned(value_ref), - value_ref > 0.5, - ) - .to_df() -) -assign_df = assign_df.sort_values(["period", "machine_id"]) -print(f"\nTechnician assignments ({len(assign_df)}):") -for period, g in assign_df.groupby("period"): - print(f" Period {int(period)}:") - for _, row in g.iterrows(): - travel = "" if row["base_location"] == row["location"] else " [TRAVEL]" - cost = row["maintenance_duration_hours"] * row["hourly_rate"] - print( - f" {row['machine_id']}: {row['technician_id']} " - f"({int(row['maintenance_duration_hours'])}h x " - f"${row['hourly_rate']:.0f}/h = ${cost:.0f}){travel}" - ) - -# -------------------------------------------------- -# Stage 4: Resilience -- concentration risk & cross-training -# -------------------------------------------------- - -print(f"\n{'=' * 70}") -print("STAGE 4: Resilience -- Concentration Risk Analysis") -print("=" * 70) - -# -------------------------------------------------- -# Materialize prescriptive output as ontology concepts. -# These bindings turn the post-solve x_maintain / x_assigned / x_vulnerable -# property values into queryable ontology rather than ad-hoc pandas frames. -# -------------------------------------------------- - -# MaintenancePlan: singleton capturing the optimizer's cost breakdown. -MaintenancePlan = model.Concept( - "MaintenancePlan", identify_by={"key": Integer} -) -MaintenancePlan.objective = model.Property( - f"{MaintenancePlan} has objective {Float:objective}" -) -MaintenancePlan.failure_cost = model.Property( - f"{MaintenancePlan} has failure cost {Float:failure_cost}" -) -MaintenancePlan.labor_cost = model.Property( - f"{MaintenancePlan} has labor cost {Float:labor_cost}" -) -MaintenancePlan.travel_cost = model.Property( - f"{MaintenancePlan} has travel cost {Float:travel_cost}" -) -MaintenancePlan.total_jobs = model.Property( - f"{MaintenancePlan} has total jobs {Integer:total_jobs}" -) - -# Seed the singleton and bind the optimizer's reported objective onto it. -plan_data = model.data([{"key": 1, "obj_val": float(si.objective_value)}]) -model.define( - plan_seed := MaintenancePlan.new(key=plan_data["key"]), - plan_seed.objective(plan_data["obj_val"]), -) - -# Aggregate the cost components and job count off the post-solve properties. -plan_ref = MaintenancePlan.ref() -mp_fc = MachinePeriod.ref() -m_fc = Machine.ref() -model.define( - plan_ref.failure_cost( - aggs.sum( - mp_fc.x_vulnerable - * mp_fc.predicted_fp - * m_fc.estimated_parts_cost - * m_fc.criticality - * (1 + CENTRALITY_WEIGHT * m_fc.betweenness) - ).where(mp_fc.machine(m_fc)) - ) -) - -tmp_lc = TechnicianMachinePeriod.ref() -m_lc = Machine.ref() -t_lc = Technician.ref() -model.define( - plan_ref.labor_cost( - aggs.sum( - tmp_lc.x_assigned - * m_lc.maintenance_duration_hours - * t_lc.hourly_rate - ).where( - tmp_lc.machine(m_lc), - tmp_lc.technician(t_lc), - ) - ) -) - -tmp_tc = TechnicianMachinePeriod.ref() -m_tc = Machine.ref() -model.define( - plan_ref.travel_cost( - aggs.sum( - tmp_tc.x_assigned - * (1 - tmp_tc.same_location) - * m_tc.maintenance_duration_hours - * TRAVEL_COST_PER_HOUR - ).where(tmp_tc.machine(m_tc)) - ) -) - -mp_jobs = MachinePeriod.ref() -model.define( - plan_ref.total_jobs( - aggs.count(mp_jobs).where(mp_jobs.x_maintain > 0.5) - ) -) + MachineProductCapability.machine(_GM), MachineProductCapability.product(_GP) +).define(prod_graph.Edge.new(src=_GM, dst=_GP)) -# TypeConcentration: per-machine-type concentration analysis. -TypeConcentration = model.Concept( - "TypeConcentration", identify_by={"machine_type": String} -) -TypeConcentration.qualified_tech_count = model.Property( - f"{TypeConcentration} has {Integer:qualified_tech_count} qualified techs" -) -TypeConcentration.qualified_tech_locations = model.Property( - f"{TypeConcentration} has tech locations {String:qualified_tech_locations}" -) -TypeConcentration.is_concentrated = model.Property( - f"{TypeConcentration} concentration flag {Boolean:is_concentrated}" -) -TypeConcentration.scheduled_jobs_total = model.Property( - f"{TypeConcentration} has {Integer:scheduled_jobs_total} scheduled jobs" -) -TypeConcentration.scheduled_jobs_traveling = model.Property( - f"{TypeConcentration} has {Integer:scheduled_jobs_traveling} traveling jobs" -) -TypeConcentration.travel_pct = model.Property( - f"{TypeConcentration} has travel pct {Float:travel_pct}" -) +n_nodes = int(model.select(prod_graph.num_nodes().alias("n")).to_df()["n"].iloc[0]) +n_edges = int(model.select(prod_graph.num_edges().alias("n")).to_df()["n"].iloc[0]) +print(f"\n bipartite graph: {n_nodes} nodes (machines + products), {n_edges} edges") -# Seed: one TypeConcentration per distinct machine_type appearing in -# Qualification (the population of types we have any tech for). -qref_seed = Qualification.ref() -model.define( - TypeConcentration.new(machine_type=qref_seed.machine_type_str) -) +prod_graph.Node.bottleneck_raw = prod_graph.betweenness_centrality() +Machine.bottleneck = model.Property(f"{Machine} has bottleneck centrality {Float:bottleneck}") +model.where(prod_graph.Node == Machine).define(Machine.bottleneck(prod_graph.Node.bottleneck_raw)) -# qualified_tech_count: distinct techs qualified for this machine_type. -tc_qc = TypeConcentration.ref() -qref_qc = Qualification.ref() -tref_qc = Technician.ref() +Machine.product_count = model.Property(f"{Machine} makes {Integer:product_count} products") model.define( - tc_qc.qualified_tech_count( - aggs.count(distinct(tref_qc)) - .where( - qref_qc.machine_type_str == tc_qc.machine_type, - qref_qc.technician(tref_qc), - ) - .per(tc_qc) + Machine.product_count( + aggs.count(MachineProductCapability).per(Machine).where(MachineProductCapability.machine(Machine)) | 0 ) ) -# Helper concept: distinct (machine_type, location) pairs derived from the -# qualified-technician join. Compound identity gives one entity per unique -# pair; used downstream by distinct_loc_count. -TypeLocation = model.Concept( - "TypeLocation", - identify_by={"machine_type": String, "location": String}, -) -qref_tl = Qualification.ref() -tref_tl = Technician.ref() -model.define( - TypeLocation.new( - machine_type=qref_tl.machine_type_str, - location=tref_tl.base_location, - ) -).where(qref_tl.technician(tref_tl)) - -# qualified_tech_locations: comma-joined distinct base_locations of -# qualified techs. Built in pandas because string_join is not yet supported -# by the LQP backend; bound onto TypeConcentration via model.data. -_loc_pairs = ( - qualifications_df.merge( - technicians_df[["technician_id", "base_location"]], on="technician_id" - )[["machine_type", "base_location"]] - .drop_duplicates() - .sort_values(["machine_type", "base_location"]) -) -_loc_str_rows = [ - {"mtype": mt, "loc_str": ", ".join(sorted(g["base_location"].unique()))} - for mt, g in _loc_pairs.groupby("machine_type") -] -loc_str_data = model.data(_loc_str_rows) -tc_locs = TypeConcentration.ref() -model.define(tc_locs.qualified_tech_locations(loc_str_data["loc_str"])).where( - tc_locs.machine_type == loc_str_data["mtype"] -) - -# distinct_loc_count: helper to drive the is_concentrated flag, computed -# off the TypeLocation pairs (one entity per distinct location). -TypeConcentration.distinct_loc_count = model.Property( - f"{TypeConcentration} has {Integer:distinct_loc_count} distinct tech locations" -) -tc_dlc = TypeConcentration.ref() -tl_dlc = TypeLocation.ref() -model.define( - tc_dlc.distinct_loc_count( - aggs.count(tl_dlc) - .where(tl_dlc.machine_type == tc_dlc.machine_type) - .per(tc_dlc) - ) -) - -# is_concentrated: True iff all qualified techs share a single base_location. -model.where(TypeConcentration.distinct_loc_count == 1).define( - TypeConcentration.is_concentrated(True) -) -model.where(TypeConcentration.distinct_loc_count > 1).define( - TypeConcentration.is_concentrated(False) -) - -# scheduled_jobs_total: count of scheduled (machine, period) jobs for this type. -tc_sjt = TypeConcentration.ref() -mp_sjt = MachinePeriod.ref() -m_sjt = Machine.ref() -model.define( - tc_sjt.scheduled_jobs_total( - aggs.count(mp_sjt) - .where( - mp_sjt.machine(m_sjt), - m_sjt.machine_type == tc_sjt.machine_type, - mp_sjt.x_maintain > 0.5, - ) - .per(tc_sjt) - | 0 +bottlenecks = model.select( + Machine.machine_id.alias("machine_id"), + Machine.machine_type.alias("machine_type"), + Machine.bottleneck.alias("bottleneck"), + Machine.product_count.alias("product_count"), +).to_df() +bottlenecks["product_count"] = bottlenecks["product_count"].astype(int) +bottlenecks = bottlenecks.sort_values("bottleneck", ascending=False) +print("\n-- Q8: top machine producibility bottlenecks (betweenness centrality) --") +for _, row in bottlenecks.head(8).iterrows(): + print( + f" {row['machine_id']} ({row['machine_type']}): betweenness {row['bottleneck']:.4f}, " + f"makes {row['product_count']} products" ) -) -# scheduled_jobs_traveling: count of scheduled assignments where the -# assigned technician's base_location differs from the machine's location. -tc_sjr = TypeConcentration.ref() -tmp_sjr = TechnicianMachinePeriod.ref() -m_sjr = Machine.ref() -model.define( - tc_sjr.scheduled_jobs_traveling( - aggs.count(tmp_sjr) - .where( - tmp_sjr.machine(m_sjr), - m_sjr.machine_type == tc_sjr.machine_type, - tmp_sjr.x_assigned > 0.5, - tmp_sjr.same_location == 0, - ) - .per(tc_sjr) - | 0 - ) -) +# ================================================================== +# STAGE 4: Prescriptive -- preventive-maintenance schedule + what-if +# ================================================================== -# travel_pct: 100 * traveling / total (only when total > 0). -model.where(TypeConcentration.scheduled_jobs_total > 0).define( - TypeConcentration.travel_pct( - floats.float(TypeConcentration.scheduled_jobs_traveling) - / floats.float(TypeConcentration.scheduled_jobs_total) - * 100.0 - ) -) +banner("STAGE 4 Prescriptive") -# CrossTrainingRecommendation: cheapest non-local cross-training candidate -# per concentrated machine type. One row per concentrated machine_type. -CrossTrainingRecommendation = model.Concept( - "CrossTrainingRecommendation", identify_by={"machine_type": String} -) -CrossTrainingRecommendation.tech_id = model.Property( - f"{CrossTrainingRecommendation} has {String:tech_id}" -) -CrossTrainingRecommendation.tech_name = model.Property( - f"{CrossTrainingRecommendation} has {String:tech_name}" -) -CrossTrainingRecommendation.cost = model.Property( - f"{CrossTrainingRecommendation} has {Float:cost}" -) -CrossTrainingRecommendation.duration_weeks = model.Property( - f"{CrossTrainingRecommendation} has {Integer:duration_weeks} weeks" -) -CrossTrainingRecommendation.is_best_candidate = model.Property( - f"{CrossTrainingRecommendation} best flag {Boolean:is_best_candidate}" -) - -# A TrainingOption is "non-local" for a concentrated machine type when the -# candidate technician's base_location is NOT one of the locations where the -# qualified techs already sit. For singly-concentrated types that simplifies -# to: candidate.base_location != the (single) qualified-tech location string. -# Pre-compute the cheapest non-local cost per concentrated type as a derived -# property on TypeConcentration so the recommendation seeding stays simple. -TypeConcentration.min_nonlocal_cost = model.Property( - f"{TypeConcentration} has {Float:min_nonlocal_cost} cheapest non-local training cost" -) -tc_min = TypeConcentration.ref() -to_min = TrainingOption.ref() -t_min = Technician.ref() -model.where(tc_min.is_concentrated == True).define( - tc_min.min_nonlocal_cost( - aggs.min(to_min.training_cost) - .where( - to_min.machine_type == tc_min.machine_type, - to_min.technician(t_min), - t_min.base_location != tc_min.qualified_tech_locations, - ) - .per(tc_min) - ) -) - -# Seed CrossTrainingRecommendation: one row per concentrated type that has -# at least one non-local candidate (min_nonlocal_cost is populated). -tc_seed_ctr = TypeConcentration.ref() -model.where(tc_seed_ctr.min_nonlocal_cost).define( - CrossTrainingRecommendation.new(machine_type=tc_seed_ctr.machine_type) -) - -# Bind cheapest-candidate attributes onto each recommendation by joining the -# TrainingOption whose cost matches the pre-computed min_nonlocal_cost AND -# whose tech sits outside the concentrated location. -ctr_bind = CrossTrainingRecommendation.ref() -tc_bind = TypeConcentration.ref() -to_bind = TrainingOption.ref() -t_bind = Technician.ref() +# Periods 1..H as a concept so the schedule can index (machine, period). +Period = model.Concept("Period", identify_by={"pid": Integer}) +_per = model.data([{"pid": t} for t in range(1, PERIOD_HORIZON + 1)]) +model.define(Period.new(pid=_per["pid"])) + +# Coverage feasibility: a machine can be maintained only if a qualified technician +# is available -- and Turbine work requires an ON-SITE (same-location) technician. +Machine.has_onsite_qualified = model.Relationship(f"{Machine} has an on-site qualified technician") +_QL = Qualification.ref() +_QLT = Technician.ref() model.where( - ctr_bind.machine_type == tc_bind.machine_type, - to_bind.machine_type == ctr_bind.machine_type, - to_bind.technician(t_bind), - t_bind.base_location != tc_bind.qualified_tech_locations, - to_bind.training_cost == tc_bind.min_nonlocal_cost, -).define( - ctr_bind.tech_id(t_bind.technician_id), - ctr_bind.tech_name(t_bind.technician_name), - ctr_bind.cost(to_bind.training_cost), - ctr_bind.duration_weeks(to_bind.training_weeks), - ctr_bind.is_best_candidate(True), -) - -# 4a. Technician utilization from the optimal schedule. -tech_assignments = ( - assign_df.groupby( - ["technician_id", "technician_name", "base_location", "skill_level"], - as_index=False, - ) - .agg( - assignment_count=("machine_id", "count"), - machines=("machine_id", list), - ) - .sort_values("assignment_count", ascending=False) -) - -print("\nTechnician utilization in optimal schedule:") -total_assignments = len(assign_df) -for _, row in tech_assignments.iterrows(): - pct = row["assignment_count"] / total_assignments * 100 - print( - f" {row['technician_id']} ({row['technician_name']}, " - f"{row['skill_level']}, {row['base_location']}): " - f"{row['assignment_count']} assignments ({pct:.0f}%)" - ) - -# 4b. MaintenancePlan singleton: cost breakdown from the optimizer. -plan_df = ( - model.select( - MaintenancePlan.objective.alias("objective"), - MaintenancePlan.failure_cost.alias("failure_cost"), - MaintenancePlan.labor_cost.alias("labor_cost"), - MaintenancePlan.travel_cost.alias("travel_cost"), - MaintenancePlan.total_jobs.alias("total_jobs"), - ) - .to_df() + _QL.machine_type_str == Machine.machine_type, + _QL.technician(_QLT), + _QLT.base_location == Machine.location, +).define(Machine.has_onsite_qualified()) + +# Same, but excluding technician T001 -- used for the what-if re-solve. +Machine.has_onsite_qualified_excl = model.Relationship(f"{Machine} has an on-site qualified technician excluding T001") +_QL2 = Qualification.ref() +_QLT2 = Technician.ref() +model.where( + _QL2.machine_type_str == Machine.machine_type, + _QL2.technician(_QLT2), + _QLT2.base_location == Machine.location, + _QLT2.technician_id != "T001", +).define(Machine.has_onsite_qualified_excl()) + +# coverable / coverable_wif: Turbine needs on-site tech; everything else is coverable. +Machine.coverable = model.Property(f"{Machine} coverage feasible {Integer:coverable}") +model.where(Machine.machine_type != "Turbine").define(Machine.coverable(1)) +model.where(Machine.machine_type == "Turbine", Machine.has_onsite_qualified()).define(Machine.coverable(1)) +model.where(Machine.machine_type == "Turbine", model.not_(Machine.has_onsite_qualified())).define(Machine.coverable(0)) + +Machine.coverable_wif = model.Property(f"{Machine} coverage feasible without T001 {Integer:coverable_wif}") +model.where(Machine.machine_type != "Turbine").define(Machine.coverable_wif(1)) +model.where(Machine.machine_type == "Turbine", Machine.has_onsite_qualified_excl()).define(Machine.coverable_wif(1)) +model.where(Machine.machine_type == "Turbine", model.not_(Machine.has_onsite_qualified_excl())).define(Machine.coverable_wif(0)) + +# Machine x Period decision space. +MachinePeriod = model.Concept("MachinePeriod", identify_by={"machine_id": String, "pid": Integer}) +MachinePeriod.machine = model.Property(f"{MachinePeriod} for {Machine}") +MachinePeriod.period = model.Property(f"{MachinePeriod} in {Period}") +MachinePeriod.machine_id_str = model.Property(f"{MachinePeriod} machine id {String:machine_id_str}") +MachinePeriod.period_num = model.Property(f"{MachinePeriod} period number {Integer:period_num}") +_MPM = Machine.ref() +_MPP = Period.ref() +model.define( + mp := MachinePeriod.new(machine_id=_MPM.machine_id, pid=_MPP.pid), + mp.machine(_MPM), + mp.period(_MPP), + mp.machine_id_str(_MPM.machine_id), + mp.period_num(_MPP.pid), +) + +# Pre-derive Float coefficients -- inline casts (floats.float) are not allowed +# inside the objective expression, so materialize them as properties first. +Machine.criticality_f = model.Property(f"{Machine} criticality as float {Float:criticality_f}") +model.define(Machine.criticality_f(floats.float(Machine.criticality))) +MachinePeriod.earliness = model.Property(f"{MachinePeriod} earliness weight {Float:earliness}") +model.define(MachinePeriod.earliness(floats.float(PERIOD_HORIZON + 1 - MachinePeriod.period_num))) + + +def _report_schedule(label, sched_df, si): + print(f"\n-- {label}: status {si.termination_status}, objective {si.objective_value:.3f} --") + print(f" machines scheduled: {len(sched_df)} of 50; periods used: 1..{int(sched_df['pid'].max())}") + per_period = sched_df.groupby("pid").size() + print(" jobs per period: " + ", ".join(f"p{p}={n}" for p, n in per_period.items())) + + +# --- Baseline solve -------------------------------------------------------- +MachinePeriod.x_maintain = model.Property(f"{MachinePeriod} maintain decision {Float:x_maintain}") +prob = Problem(model, Float) +prob.solve_for(MachinePeriod.x_maintain, type="bin", name=["maintain", MachinePeriod.machine_id_str, MachinePeriod.period_num]) +prob.satisfy( + model.require( + aggs.sum(MachinePeriod.x_maintain).per(Machine).where(MachinePeriod.machine(Machine)) <= Machine.coverable + ), + name=["cover", Machine.machine_id], ) -plan_row = plan_df.iloc[0] -print("\nMaintenancePlan (cost breakdown):") -print(f" Objective: ${plan_row['objective']:.2f}") -print(f" Failure cost: ${plan_row['failure_cost']:.2f}") -print(f" Labor cost: ${plan_row['labor_cost']:.2f}") -print(f" Travel cost: ${plan_row['travel_cost']:.2f}") -print(f" Total jobs: {int(plan_row['total_jobs'])}") - -# 4c. TypeConcentration: per-machine-type concentration analysis. -type_conc_df = ( - model.select( - TypeConcentration.machine_type.alias("machine_type"), - TypeConcentration.qualified_tech_count.alias("qualified_tech_count"), - TypeConcentration.qualified_tech_locations.alias("qualified_tech_locations"), - TypeConcentration.is_concentrated.alias("is_concentrated"), - TypeConcentration.scheduled_jobs_total.alias("scheduled_jobs_total"), - TypeConcentration.scheduled_jobs_traveling.alias("scheduled_jobs_traveling"), - TypeConcentration.travel_pct.alias("travel_pct"), - ) - .to_df() - .sort_values("machine_type") +prob.satisfy( + model.require( + aggs.sum(MachinePeriod.x_maintain).per(Period).where(MachinePeriod.period(Period)) <= 5 + ), + name=["bay", Period.pid], +) +prob.maximize( + aggs.sum( + MachinePeriod.x_maintain + * Machine.failure_probability + * Machine.criticality_f + * MachinePeriod.earliness + ).where(MachinePeriod.machine(Machine)) +) +prob.solve("highs", time_limit_sec=120) +si = prob.solve_info() + +_vr = Float.ref() +_sm = Machine.ref() +sched = model.select( + _sm.machine_id.alias("machine_id"), + _sm.machine_type.alias("machine_type"), + _sm.facility.alias("facility"), + MachinePeriod.period_num.alias("pid"), +).where(MachinePeriod.machine(_sm), MachinePeriod.x_maintain(_vr), _vr > 0.5).to_df() +sched["pid"] = sched["pid"].astype(int) +_report_schedule("Q10/Q12 baseline schedule", sched, si) +earliest = sched.sort_values("pid").head(5) +print(" first jobs (riskiest, earliest): " + ", ".join(f"{r.machine_id}(p{r.pid})" for r in earliest.itertuples())) + +# --- What-if: technician T001 unavailable ---------------------------------- +dropped = model.where(Machine.coverable == 1, Machine.coverable_wif == 0).select( + Machine.machine_id.alias("machine_id"), + Machine.machine_type.alias("machine_type"), + Machine.facility.alias("facility"), +).to_df().sort_values("machine_id") + +MachinePeriod.x_maintain_wif = model.Property(f"{MachinePeriod} maintain decision without T001 {Float:x_maintain_wif}") +prob2 = Problem(model, Float) +prob2.solve_for(MachinePeriod.x_maintain_wif, type="bin", name=["maintain_wif", MachinePeriod.machine_id_str, MachinePeriod.period_num]) +prob2.satisfy( + model.require( + aggs.sum(MachinePeriod.x_maintain_wif).per(Machine).where(MachinePeriod.machine(Machine)) <= Machine.coverable_wif + ), + name=["cover_wif", Machine.machine_id], ) - -print("\nQualification coverage by machine type:") -for _, row in type_conc_df.iterrows(): - tag = ( - f"CONCENTRATED -- all {int(row['qualified_tech_count'])} techs in " - f"{row['qualified_tech_locations']}" - if row["is_concentrated"] - else "OK" - ) - print( - f" {row['machine_type']}: {int(row['qualified_tech_count'])} techs " - f"in {row['qualified_tech_locations']} -- {tag}" - ) - -concentrated_df = type_conc_df[type_conc_df["is_concentrated"]] -if not concentrated_df.empty: - print("\nConcentration risk detail:") - for _, row in concentrated_df.iterrows(): - total_jobs = int(row["scheduled_jobs_total"]) if row["scheduled_jobs_total"] else 0 - travel_jobs = ( - int(row["scheduled_jobs_traveling"]) if row["scheduled_jobs_traveling"] else 0 - ) - pct = float(row["travel_pct"]) if total_jobs else 0.0 - print( - f"\n {row['machine_type']}: all {int(row['qualified_tech_count'])} " - f"qualified techs in {row['qualified_tech_locations']}" - ) - if total_jobs: - print( - f" Scheduled {row['machine_type']} jobs: {total_jobs}, " - f"of which {travel_jobs} require travel ({pct:.0f}%)" - ) - else: - print(f" Scheduled {row['machine_type']} jobs: 0") - - # 4d. CrossTrainingRecommendation: cheapest non-local candidate per type. - print(f"\n{'=' * 70}") - print("RECOMMENDATION: Cross-Training to Eliminate Concentration Risk") - print("=" * 70) - - rec_df = ( - model.select( - CrossTrainingRecommendation.machine_type.alias("machine_type"), - CrossTrainingRecommendation.tech_id.alias("tech_id"), - CrossTrainingRecommendation.tech_name.alias("tech_name"), - CrossTrainingRecommendation.cost.alias("cost"), - CrossTrainingRecommendation.duration_weeks.alias("duration_weeks"), - CrossTrainingRecommendation.is_best_candidate.alias("is_best_candidate"), - ) - .to_df() - .sort_values("machine_type") - ) - - if rec_df.empty: - print("\n No non-local cross-training options available.") - for _, row in rec_df.iterrows(): - conc_loc = concentrated_df[ - concentrated_df["machine_type"] == row["machine_type"] - ]["qualified_tech_locations"].iloc[0] - print(f"\n {row['machine_type']} -- add coverage outside {conc_loc}:") - print( - f" Best candidate: {row['tech_id']} ({row['tech_name']}): " - f"${int(row['cost']):,}, {int(row['duration_weeks'])} weeks" - ) -else: - print("\nNo geographic concentration risk detected.") +prob2.satisfy( + model.require( + aggs.sum(MachinePeriod.x_maintain_wif).per(Period).where(MachinePeriod.period(Period)) <= 5 + ), + name=["bay_wif", Period.pid], +) +prob2.maximize( + aggs.sum( + MachinePeriod.x_maintain_wif + * Machine.failure_probability + * Machine.criticality_f + * MachinePeriod.earliness + ).where(MachinePeriod.machine(Machine)) +) +prob2.solve("highs", time_limit_sec=120) +si2 = prob2.solve_info() + +_vr2 = Float.ref() +_sm2 = Machine.ref() +sched2 = model.select( + _sm2.machine_id.alias("machine_id"), + MachinePeriod.period_num.alias("pid"), +).where(MachinePeriod.machine(_sm2), MachinePeriod.x_maintain_wif(_vr2), _vr2 > 0.5).to_df() +print(f"\n-- Q11 what-if (T001 unavailable): status {si2.termination_status}, objective {si2.objective_value:.3f} --") +print(f" machines scheduled: {len(sched2)} of 50 (baseline {len(sched)}); objective delta {si.objective_value - si2.objective_value:.3f}") +print(f" machines that lose coverage: {len(dropped)}") +for _, row in dropped.iterrows(): + print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']})") + +print("\n>>> ALL STAGES complete") diff --git a/v1/machine_maintenance/pyproject.toml b/v1/machine_maintenance/pyproject.toml index 6398eca0..803a6cd6 100644 --- a/v1/machine_maintenance/pyproject.toml +++ b/v1/machine_maintenance/pyproject.toml @@ -9,7 +9,7 @@ description = "RelationalAI template: machine_maintenance (PyRel v1)" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "relationalai==1.0.14", + "relationalai==1.15.0", "pandas>=2.0", ] diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 3c950942..ab44bf2a 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -4,7 +4,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa > **Data provenance.** Every figure below is computed from the bundled `data/*.csv`, which is the real `MANUFACTURING.PUBLIC` dataset (50 machines, 20 technicians, 8 products, 12 weekly periods across Plant_A/Plant_B/Plant_C). The same dataset backs the reasoner-workflow eval suite, so the walkthrough doubles as a reproducibility check against 13 known-answer questions. > -> **Status (draft).** Querying (Q1–Q5, Q7) and Rules (Q9) below are verified against the real data and reproduce the eval's expected answers exactly. Graph (Q8), Predictive (Q6, Q13), and Prescriptive (Q10–Q12) are wired in the script and their numbers will be filled from the live engine run — they are marked _pending live run_ until then. +> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. The querying and rules answers (Q1–Q5, Q7, Q9) reproduce the eval's expected values exactly. The graph (Q8) and prescriptive (Q10–Q12) stages use the template's own sound formulations and independently corroborate the eval's structural findings. Predictive (Q6, Q13) reads the bundled pre-computed failure predictions; a live GNN is described as an extension. ## The chain @@ -20,14 +20,15 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa /rai-rules-authoring 3 Critical · 6 Elevated · 41 Standard Critical: M001, M006 (Turbine/Plant_A), M011. ───────────────────────────────────────────────────────────────── - STAGE 3 Graph ──► Machine producibility bottlenecks - /rai-graph-analysis (pending live run) + STAGE 3 Graph ──► Machine.bottleneck (50) + /rai-graph-analysis Pumps & Motors bridge the most product lines. ───────────────────────────────────────────────────────────────── STAGE 4 Predictive ──► Per-machine failure risk & mode, 12 periods - /rai-predictive-modeling (pre-computed predictions; GNN pending) + /rai-predictive-modeling (bundled pre-computed predictions) ───────────────────────────────────────────────────────────────── - STAGE 5 Prescriptive ──► Preventive-maintenance schedule + what-if - /rai-prescriptive-* (pending live run) + STAGE 5 Prescriptive ──► Maintenance schedule + technician what-if + /rai-prescriptive-* All 50 scheduled (5/period, P1–10); drop T001 + and 4 Plant_A Turbines lose coverage. ───────────────────────────────────────────────────────────────── ``` @@ -133,9 +134,9 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, /rai-graph-analysis Build a machine-product bipartite graph from machine_product_capabilities and find the biggest connectivity bottlenecks — machines tied to products with the fewest alternative producers. ``` -**Response** _(pending live run)_ +**Response** -Graph stage is wired in `machine_maintenance.py`; bottleneck centralities will be filled from the live engine run. +Bipartite graph: 58 nodes (50 machines + 8 products), 120 edges. Top betweenness centrality is shared by the **Pumps (M021, M030) and Motors (M041, M043–M046, M049)** at 46.7 — each makes 3 products, so they bridge the most product lines and are hardest to route around if lost. This independently corroborates the eval's Q8 finding that Pumps and Motors are the producibility bottlenecks. ### 9. Predict failures _(eval Q6, Q13)_ @@ -145,9 +146,9 @@ Graph stage is wired in `machine_maintenance.py`; bottleneck centralities will b /rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each, given sensor readings, downtime history, and machine attributes? ``` -**Response** _(pre-computed; GNN pending)_ +**Response** _(bundled pre-computed predictions)_ -The bundled `failure_predictions` supply per-machine, per-period probabilities and predicted modes (the source of the step-4 ranking). A GNN formulation over sensor/downtime history is wired for the predictive reasoner; its trained-model results will be added from the live run. +The bundled `failure_predictions` supply per-machine, per-period failure probability and predicted mode (the source of the step-4 ranking, e.g. M016/M028/M011 at 42.0% by period 12). For a live model, a GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README); the template ships the pre-computed predictions so the predictive question is answerable deterministically. ### 10. Schedule preventive maintenance + stress-test _(eval Q10, Q11, Q12)_ @@ -157,9 +158,9 @@ The bundled `failure_predictions` supply per-machine, per-period probabilities a /rai-prescriptive-problem-formulation Schedule preventive maintenance across the 50 machines and 12 periods: at most 5 jobs per period; each maintained machine needs a qualified technician, with Turbine work covered by an on-site technician at the same plant; prioritize high failure-probability × high-criticality and earlier periods for the riskiest. Then re-solve with T001 unavailable and report the coverage impact. ``` -**Response** _(pending live run)_ +**Response** -Prescriptive formulation (decision variables, ≤5/period cap, qualified + on-site Turbine assignment, expected-failure-cost objective) and the T001-unavailable what-if are wired in `machine_maintenance.py`; the optimal schedule, objective, and concentration/cross-training findings will be filled from the live solve. +Baseline solve is **OPTIMAL**: all **50 machines scheduled across periods 1–10** at 5 jobs/period (periods 11–12 absorb the slack), with the riskiest machines (M028, M016, M012, M006, M011) placed in period 1. Re-solving with **T001 unavailable** is still OPTIMAL but covers only **46 of 50** machines: the four Plant_A Turbines — **M001, M004, M006, M009** — lose coverage, because no on-site Turbine technician remains in Plant_A. This matches the eval's expected what-if outcome exactly. ## Data From 3bc1abe91d9c9583162c4ccb21fc8360f3c21bf1 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 15:18:05 -0700 Subject: [PATCH 03/12] Apply dev-templates-review fixes to machine_maintenance README: drop invalid 'Querying' reasoning_type (docs CI enum), remove the H1 title, rewrite 'What this template is for' as a business problem statement, reorder How-it-works to match the script (rules before graph) with verbatim code snippets, move thresholds out of prose into those snippets, add assumed-knowledge and a real expected-output snippet. Script: add a Stage 0 ontology banner, fix Stage casing, and persist a MaintenancePlan headline concept after the solve so the plan stays queryable. Runbook: make the graph and prescriptive prompts question-shaped and reconcile the chain diagram to the script's four stages. No behavior change: full script still runs OPTIMAL with identical numbers (baseline 199.032 / 50-of-50; what-if 169.971 / 46-of-50); py_compile + ruff clean. --- v1/machine_maintenance/README.md | 76 ++++++++++++++----- v1/machine_maintenance/machine_maintenance.py | 37 +++++++-- v1/machine_maintenance/runbook.md | 12 ++- 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index 406bd614..ad8129d5 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -5,7 +5,6 @@ featured: false experience_level: intermediate industry: "Manufacturing" reasoning_types: - - Querying - Graph - Rules-based - Prescriptive @@ -20,18 +19,11 @@ tags: - Bottleneck Analysis --- -# Machine Maintenance - ## What this template is for -This template models a **50-machine, 3-plant, 12-period** manufacturing operation and threads four RelationalAI reasoners through a single ontology, with each stage's enrichments feeding the next: - -1. **Querying** — diagnose plant performance: OEE (availability × performance × quality), downtime drivers, forward failure risk, waste rates, and technician coverage. -2. **Graph** — build a machine-product bipartite graph and rank machines by betweenness centrality to find producibility bottlenecks. -3. **Rules** — classify each machine into a risk tier (Critical / Elevated / Standard) from chronic-downtime, high-risk, and maintenance-overdue flags. -4. **Prescriptive** — schedule preventive maintenance across machines and periods under a per-period bay limit and technician-coverage feasibility (Turbine work needs an on-site qualified technician), then stress-test the schedule against the loss of a key technician. +Manufacturing reliability teams have to decide **which machines to maintain, when, and with which technician** — under a fixed maintenance-bay limit and a thin bench of qualified technicians. Get it wrong and a critical machine fails unplanned, or a plant's only on-site specialist becomes a single point of failure. The hard part is that no single view answers the question: plant OEE shows where output is lost but not what will fail next, failure predictions rank risk but ignore who can do the work, and a feasible schedule can still hide a concentration risk that one resignation would expose. -The point is the chain: OEE alone misranks the plants, downtime totals don't say what will fail next, rules flag risky machines but don't allocate scarce technician time, and the optimizer produces a feasible schedule but can't see that on-site Turbine coverage funnels through a single technician per plant. +This template works the problem end to end on a 50-machine, 3-plant, 12-period operation. **It chains four RelationalAI reasoners over one ontology — querying to diagnose plant performance, rules to classify machine risk, graph analysis to find producibility bottlenecks, and prescriptive optimization to schedule maintenance and stress-test it.** Each stage writes its findings back to the model, so the next stage builds on what the last one learned. ## Who this is for @@ -39,6 +31,8 @@ The point is the chain: OEE alone misranks the plants, downtime totals don't say - Manufacturing and reliability teams building preventive-maintenance and risk-classification workflows - Anyone wanting a worked multi-reasoner example on a realistic operational dataset +Readers are assumed comfortable reading Python; domain terms (OEE, betweenness centrality, the maintenance horizon) are explained inline. + ## What you'll build - An ontology over machines, technicians, qualifications, products, production runs, downtime events, failure predictions, and machine-product capabilities @@ -96,7 +90,14 @@ The point is the chain: OEE alone misranks the plants, downtime totals don't say python machine_maintenance.py ``` - Each stage prints its findings — OEE by plant, downtime drivers, the bottleneck ranking, risk tiers, and the maintenance schedule with its what-if. + Each stage prints its findings. The first lines look like: + + ```text + -- Q1: OEE by plant -- + Plant_C: availability 97.7% performance 81.7% quality 98.2% OEE 78.3% + ``` + + See `runbook.md` for the full walkthrough and output. ## Template structure @@ -130,7 +131,7 @@ The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: | File | Rows | Description | |---|---|---| -| `machines.csv` | 50 | Machines across 3 plants × 5 types (Turbine, Generator, Pump, Compressor, Motor) | +| `machines.csv` | 50 | Machines across 3 plants and 5 types (Turbine, Generator, Pump, Compressor, Motor) | | `technicians.csv` | 20 | Technicians with skill level, base location, and rate | | `qualifications.csv` | 32 | Which technicians are qualified for which machine type | | `products.csv` | 8 | Products manufactured | @@ -147,21 +148,58 @@ The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: ## Model overview -Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period` (1..12). The prescriptive stage adds a `MachinePeriod` decision space (machine × period). +Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period` (1..12). The prescriptive stage adds a `MachinePeriod` decision space (one entry per machine and period). ## How it works +The script runs four stages in order — querying, rules, graph, prescriptive — each writing its findings back to the shared model. + ### 1. Querying -Per-plant OEE is built from production runs (performance = avg of actual/target speed; quality = good/actual quantity) and downtime events (availability from unplanned downtime against an 480-minute-per-run planned base). Additional queries rank downtime by fault and plant, surface the highest forward failure risk, compute waste rates by machine-product, and count qualified technicians per machine type. +Per-plant OEE combines a performance leg (average of actual-versus-target speed) and a quality leg (good versus actual quantity) from production runs with an availability leg from unplanned downtime against a planned base of 480 minutes per run. The legs are aggregated per plant, then multiplied: + +```python +oee["availability"] = (oee["n_runs"] * OEE_PLANNED_MIN_PER_RUN - oee["unplanned_dt"]) / ( + oee["n_runs"] * OEE_PLANNED_MIN_PER_RUN +) +oee["oee"] = oee["availability"] * oee["performance"] * oee["quality"] +``` -### 2. Graph -A bipartite machine-product graph is built from `machine_product_capabilities` (edge = machine can produce product). `betweenness_centrality()` ranks machines by how much production-routing flows through them — the producibility bottlenecks. +Further queries rank downtime by fault and plant, surface the highest forward failure risk, compute waste rates by machine-product, and count qualified technicians per machine type. -### 3. Rules -Three boolean flags — chronic downtime (> 15 events), high-risk (failure probability > 0.20 **and** criticality ≥ 4), and maintenance-overdue (remaining useful life ≤ 9) — combine into `Machine.risk_tier`: all three → Critical, exactly two → Elevated, otherwise Standard. +### 2. Rules +Three boolean flags combine into a single `Machine.risk_tier`. Each flag is a derived relationship — for example, chronic downtime fires above the event threshold: + +```python +Machine.is_chronic = model.Relationship(f"{Machine} has chronic downtime") +model.where(Machine.downtime_event_count > CHRONIC_DOWNTIME_THRESHOLD).define(Machine.is_chronic()) +``` + +A machine with all three flags is Critical, exactly two is Elevated, otherwise Standard. + +### 3. Graph +A bipartite machine-product graph is built from `machine_product_capabilities`, and betweenness centrality ranks machines by how much production routing flows through them — the producibility bottlenecks: + +```python +prod_graph = Graph(model, directed=False, weighted=False) +model.where( + MachineProductCapability.machine(_GM), MachineProductCapability.product(_GP) +).define(prod_graph.Edge.new(src=_GM, dst=_GP)) +prod_graph.Node.bottleneck_raw = prod_graph.betweenness_centrality() +``` ### 4. Prescriptive -A binary `MachinePeriod.x_maintain` decides which machine is maintained in which period. Each machine gets at most one slot and only if coverage is feasible (Turbine work requires an on-site qualified technician); each period is capped at 5 jobs. The objective prioritizes high failure-probability × criticality work in earlier periods. A second solve removes a key technician (T001) to show which machines lose coverage. +A binary `MachinePeriod.x_maintain` decides which machine is maintained in which period. Each machine gets at most one slot and only if coverage is feasible (Turbine work requires an on-site qualified technician), and each period is capped at five jobs: + +```python +prob.satisfy( + model.require( + aggs.sum(MachinePeriod.x_maintain).per(Period).where(MachinePeriod.period(Period)) <= 5 + ), + name=["bay", Period.pid], +) +``` + +The objective prioritizes high failure-probability, high-criticality work in earlier periods. A second solve removes a key technician to show which machines lose coverage. The result is persisted as a `MaintenancePlan` headline so it stays queryable after the run. ## Customize this template diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 0019a3e8..7f86d38d 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -41,9 +41,9 @@ model = Model("machine_maintenance") -# -------------------------------------------------- -# Concepts & data loading -# -------------------------------------------------- +# ================================================================== +# Stage 0: Ontology -- load the MANUFACTURING.PUBLIC sample +# ================================================================== # Machine --------------------------------------------------------------------- Machine = model.Concept("Machine", identify_by={"machine_id": String}) @@ -194,7 +194,7 @@ def banner(text): # ================================================================== -# STAGE 1: Querying -- diagnose plant operations +# Stage 1: Querying -- diagnose plant operations # ================================================================== banner("STAGE 1 Querying") @@ -305,7 +305,7 @@ def banner(text): # ================================================================== -# STAGE 2: Rules -- classify machine risk +# Stage 2: Rules -- classify machine risk # ================================================================== banner("STAGE 2 Rules") @@ -363,7 +363,7 @@ def banner(text): print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']})") # ================================================================== -# STAGE 3: Graph -- producibility bottlenecks +# Stage 3: Graph -- producibility bottlenecks # ================================================================== banner("STAGE 3 Graph") @@ -409,7 +409,7 @@ def banner(text): ) # ================================================================== -# STAGE 4: Prescriptive -- preventive-maintenance schedule + what-if +# Stage 4: Prescriptive -- preventive-maintenance schedule + what-if # ================================================================== banner("STAGE 4 Prescriptive") @@ -568,4 +568,27 @@ def _report_schedule(label, sched_df, si): for _, row in dropped.iterrows(): print(f" {row['machine_id']} ({row['machine_type']}, {row['facility']})") +# Persist the plan headline as ontology so it stays queryable after the run. +MaintenancePlan = model.Concept("MaintenancePlan", identify_by={"key": Integer}) +MaintenancePlan.objective = model.Property(f"{MaintenancePlan} has objective {Float:objective}") +MaintenancePlan.machines_scheduled = model.Property(f"{MaintenancePlan} schedules {Integer:machines_scheduled} machines") +MaintenancePlan.periods_used = model.Property(f"{MaintenancePlan} spans {Integer:periods_used} periods") +_plan = model.data([{"key": 1, "obj": float(si.objective_value), "n": int(len(sched)), "p": int(sched["pid"].max())}]) +model.define( + plan := MaintenancePlan.new(key=_plan["key"]), + plan.objective(_plan["obj"]), + plan.machines_scheduled(_plan["n"]), + plan.periods_used(_plan["p"]), +) +plan_df = model.select( + MaintenancePlan.machines_scheduled.alias("machines_scheduled"), + MaintenancePlan.periods_used.alias("periods_used"), + MaintenancePlan.objective.alias("objective"), +).to_df() +print( + f"\n-- MaintenancePlan (persisted to ontology): " + f"{int(plan_df['machines_scheduled'].iloc[0])} machines over " + f"{int(plan_df['periods_used'].iloc[0])} periods, objective {plan_df['objective'].iloc[0]:.3f} --" +) + print("\n>>> ALL STAGES complete") diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index ab44bf2a..2137e7e0 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -11,7 +11,8 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa ``` ───────────────────────────────────────────────────────────────── STAGE 1 Querying ──► OEE by plant, downtime drivers, failure - /rai-querying ranking, waste rates, tech coverage + /rai-querying ranking (from pre-computed predictions), + waste rates, technician coverage Plant_C 78.3% > Plant_A 68.0% > Plant_B 63.3% Bearing Failure = 19.4% of all downtime. Turbines have only 3 qualified technicians. @@ -23,10 +24,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa STAGE 3 Graph ──► Machine.bottleneck (50) /rai-graph-analysis Pumps & Motors bridge the most product lines. ───────────────────────────────────────────────────────────────── - STAGE 4 Predictive ──► Per-machine failure risk & mode, 12 periods - /rai-predictive-modeling (bundled pre-computed predictions) - ───────────────────────────────────────────────────────────────── - STAGE 5 Prescriptive ──► Maintenance schedule + technician what-if + STAGE 4 Prescriptive ──► Maintenance schedule + technician what-if /rai-prescriptive-* All 50 scheduled (5/period, P1–10); drop T001 and 4 Plant_A Turbines lose coverage. ───────────────────────────────────────────────────────────────── @@ -131,7 +129,7 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, **Prompt** ``` -/rai-graph-analysis Build a machine-product bipartite graph from machine_product_capabilities and find the biggest connectivity bottlenecks — machines tied to products with the fewest alternative producers. +/rai-graph-analysis In a machine-product capability graph, which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? ``` **Response** @@ -155,7 +153,7 @@ The bundled `failure_predictions` supply per-machine, per-period failure probabi **Prompt** ``` -/rai-prescriptive-problem-formulation Schedule preventive maintenance across the 50 machines and 12 periods: at most 5 jobs per period; each maintained machine needs a qualified technician, with Turbine work covered by an on-site technician at the same plant; prioritize high failure-probability × high-criticality and earlier periods for the riskiest. Then re-solve with T001 unavailable and report the coverage impact. +/rai-prescriptive-problem-formulation What's the optimal preventive-maintenance schedule across the 50 machines and 12 periods — at most 5 jobs per period, each maintained machine assigned a qualified technician (Turbine work covered by an on-site technician at the same plant), prioritizing high failure-probability and high-criticality machines in earlier periods? And if technician T001 becomes unavailable, which machines lose coverage? ``` **Response** From dd9d733671215e7cb79f43b28cb347c6a1994b54 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 15:39:21 -0700 Subject: [PATCH 04/12] Paste-test fixes: make the Q8 graph prompt and response tie-honest Re-verified both reworded runbook prompts by paste-testing them in fresh agents (no access to the script) against the live engine: - Prescriptive prompt reproduces exactly: OPTIMAL, 50/50 scheduled across P1-10, and the T001 what-if drops exactly M001/M004/M006/M009 (the four Houston Turbines) -- a fresh agent reached the same structural answer from its own formulation. - Graph prompt reproduces the same conclusion (the 20 three-product Pumps and Motors are the bottlenecks) but the betweenness score is construction-dependent and the top is a 20-way tie. Tightened the prompt to specify the bipartite construction and rewrote the response to state the tie honestly instead of an arbitrary top-8 ranking. --- v1/machine_maintenance/runbook.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 2137e7e0..b040b3a0 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -129,12 +129,12 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, **Prompt** ``` -/rai-graph-analysis In a machine-product capability graph, which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? +/rai-graph-analysis In a bipartite machine-product capability graph (machines and products as nodes), which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? ``` **Response** -Bipartite graph: 58 nodes (50 machines + 8 products), 120 edges. Top betweenness centrality is shared by the **Pumps (M021, M030) and Motors (M041, M043–M046, M049)** at 46.7 — each makes 3 products, so they bridge the most product lines and are hardest to route around if lost. This independently corroborates the eval's Q8 finding that Pumps and Motors are the producibility bottlenecks. +The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) This corroborates the eval's Q8 finding that Pumps and Motors are the producibility bottlenecks. ### 9. Predict failures _(eval Q6, Q13)_ From 80b2bfcd09e7ee21c883b44eab99da6f259e5374 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 08:35:19 -0700 Subject: [PATCH 05/12] Runbook: remove eval citations, add Examine + Discover steps - Strip all references to the internal eval suite / known-answer questions / Q-numbers from the runbook and README. The real figures stay, now presented as the template's own outputs (a customer-facing template shouldn't cite internal evals). - Add an 'Examine the ontology' step and a 'Discover reasoner questions' step (/rai-discovery), matching the canonical multi-reasoner runbook opening; renumber the walkthrough to 12 steps. - Replace the lone step-number cross-reference ('step-4 ranking') with a concept-named reference, per dev-templates-review. Verified: no eval or step-number references remain; chain headline numbers still match the per-step responses. --- v1/machine_maintenance/README.md | 2 +- v1/machine_maintenance/runbook.md | 70 +++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index ad8129d5..85ec0516 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -44,7 +44,7 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c ## What's included - `machine_maintenance.py` — the four-stage multi-reasoner script -- `runbook.md` — a prompt-by-prompt walkthrough mapped to 13 reasoner questions, with the real figures each stage produces +- `runbook.md` — a prompt-by-prompt walkthrough of the four-reasoner chain, with the real figures each stage produces - `data/` — the bundled `MANUFACTURING.PUBLIC` sample (15 CSVs) - `pyproject.toml` — package configuration and dependencies diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index b040b3a0..669a86b6 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -2,9 +2,9 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufacturing operation. OEE alone misranks the plants; downtime totals don't say what will fail next; rules flag risky machines but don't allocate scarce technician time; the optimizer produces a feasible schedule but can't see that all on-site Turbine coverage funnels through a single technician per plant. The chain threads querying, graph, rules, and prescriptive reasoners through one ontology so each stage's enrichments feed the next. -> **Data provenance.** Every figure below is computed from the bundled `data/*.csv`, which is the real `MANUFACTURING.PUBLIC` dataset (50 machines, 20 technicians, 8 products, 12 weekly periods across Plant_A/Plant_B/Plant_C). The same dataset backs the reasoner-workflow eval suite, so the walkthrough doubles as a reproducibility check against 13 known-answer questions. +> **Data provenance.** Every figure below is computed from the bundled `data/*.csv` — the real `MANUFACTURING.PUBLIC` sample: 50 machines, 20 technicians, 8 products, and 12 weekly periods across Plant_A, Plant_B, and Plant_C. > -> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. The querying and rules answers (Q1–Q5, Q7, Q9) reproduce the eval's expected values exactly. The graph (Q8) and prescriptive (Q10–Q12) stages use the template's own sound formulations and independently corroborate the eval's structural findings. Predictive (Q6, Q13) reads the bundled pre-computed failure predictions; a live GNN is described as an extension. +> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations; predictive reads the bundled pre-computed failure predictions, with a live GNN as the natural extension. ## The chain @@ -32,7 +32,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa ## Workflow -> **How to use this walkthrough.** Each section is a Prompt you paste into a single RAI session; state accumulates across steps so later reasoners read the properties earlier ones wrote. Responses show what the template produces against the bundled real data. +> **How to use this walkthrough.** Each section is a Prompt you paste into a single RAI session; state accumulates across steps, so later reasoners read the properties earlier ones wrote. Responses show what the template produces against the bundled real data. ### 1. Build ontology @@ -46,7 +46,31 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine/Generator/Pump/Compressor/Motor), `Technician` (20), `Qualification` (32), `Product` (8), `ProductionRun` (844), `DowntimeEvent` (353), `FailurePrediction` (600), `Sensor` (200), `SensorReading` (2400), `MachineProductCapability` (120), and a generated `Period` (1..12). Junction concepts (`MachinePeriod`, `TechnicianMachinePeriod`) are deferred to the prescriptive stage. -### 2. Diagnose plant operations — OEE _(eval Q1)_ +### 2. Examine the ontology + +**Prompt** + +``` +/rai-querying What concepts and relationships does the ontology have, and how many rows are in each? +``` + +**Response** + +50 `Machine`, 20 `Technician`, 32 `Qualification`, 8 `Product`, 844 `ProductionRun`, 353 `DowntimeEvent`, 600 `FailurePrediction`, 200 `Sensor` / 2,400 `SensorReading`, 120 `MachineProductCapability`, and 12 `Period`. Machines relate to production runs, downtime events, failure predictions, and product capabilities; technicians relate to qualifications. + +### 3. Discover reasoner questions + +**Prompt** + +``` +/rai-discovery We run a 50-machine, 3-plant operation and want to keep it healthy: where are we losing output, what is likely to fail next, where are the structural single points of failure, and how should we schedule preventive maintenance under a thin bench of qualified technicians? Which of these are querying, rules, graph, and prescriptive questions? +``` + +**Response** + +Routes the work across reasoner families: descriptive querying (OEE, downtime drivers, forward failure risk, waste rates, technician coverage), rules (per-machine risk tiers), graph (producibility bottlenecks), and prescriptive (preventive-maintenance scheduling with a technician what-if). The stages run in that order so each enriches the ontology the next one reads. + +### 4. Diagnose plant operations — OEE **Prompt** @@ -54,7 +78,7 @@ Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine /rai-querying What's the OEE by plant, broken into Availability (planned time = production runs × 480 min per run; downtime = unplanned events only, is_planned = 0), Performance (avg of actual_speed / target_speed per run), and Quality (good_quantity / actual_quantity)? Compute OEE from the unrounded components, then round to one decimal. ``` -**Response** _(verified)_ +**Response** | Plant | Availability | Performance | Quality | OEE | |---|---|---|---|---| @@ -64,7 +88,7 @@ Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine Plant_C leads; Plant_B trails — driven by its low Availability (most unplanned downtime) and Performance. -### 3. Find the downtime drivers _(eval Q2, Q3)_ +### 5. Find the downtime drivers **Prompt** @@ -72,11 +96,11 @@ Plant_C leads; Plant_B trails — driven by its low Availability (most unplanned /rai-querying What are the top causes of downtime by specific fault name and their percent of total downtime? And which plant carries the most downtime? ``` -**Response** _(verified)_ +**Response** Top fault names: **Bearing Failure 3,905 min (19.4%)**, Overheating 3,183 (15.8%), Motor Burnout 2,344 (11.7%), Seal Degradation 2,328 (11.6%), Shaft Misalignment 1,600 (8.0%). By plant: **Plant_B 10,494 min (52.2%)**, Plant_A 6,576 (32.7%), Plant_C 3,033 (15.1%). Total downtime = 20,103 min. -### 4. Rank forward failure risk _(eval Q4)_ +### 6. Rank forward failure risk **Prompt** @@ -84,11 +108,11 @@ Top fault names: **Bearing Failure 3,905 min (19.4%)**, Overheating 3,183 (15.8% /rai-querying Which machines are most likely to fail by the end of the planning horizon (period 12), and for what predicted reason? ``` -**Response** _(verified)_ +**Response** M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). -### 5. Surface the worst waste _(eval Q5)_ +### 7. Surface the worst waste **Prompt** @@ -96,11 +120,11 @@ M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 /rai-querying Which machine-product combinations have the worst waste rates (waste_quantity / actual_quantity), to one decimal? ``` -**Response** _(verified)_ +**Response** M025 + Hydraulic Seal Kit (3.8%), M005 + Turbine Blade Assembly (3.7%), M002 + Turbine Blade Assembly (3.6%), M049 + Motor Winding Set (3.6%), M045 + Control Panel Unit (3.5%). -### 6. Check technician coverage _(eval Q7)_ +### 8. Check technician coverage **Prompt** @@ -108,11 +132,11 @@ M025 + Hydraulic Seal Kit (3.8%), M005 + Turbine Blade Assembly (3.7%), M002 + T /rai-querying Which machine types have the fewest qualified technicians? ``` -**Response** _(verified)_ +**Response** Turbines are most constrained — only **3** qualified technicians (T001, T009, T017). Generators have 6; Pumps 7; Compressors and Motors 8 each. This concentration is what later makes Turbine coverage fragile in the schedule. -### 7. Classify machine risk _(eval Q9)_ +### 9. Classify machine risk **Prompt** @@ -120,11 +144,11 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, /rai-rules-authoring Rate each machine's risk from three flags: chronic = more than 15 downtime events; high-risk = failure probability above 0.20 AND criticality 4 or higher; maintenance-overdue = remaining useful life of 9 or less. All three flags → Critical; exactly two → Elevated; otherwise Standard. ``` -**Response** _(verified)_ +**Response** -**3 Critical** — M001 (Turbine, Plant_A), M006 (Turbine, Plant_A), M011 (Compressor, Plant_B); **6 Elevated**; **41 Standard**. The two Critical Turbines sit in the same plant that the coverage query already flagged as thin on Turbine technicians. +**3 Critical** — M001 (Turbine, Plant_A), M006 (Turbine, Plant_A), M011 (Compressor, Plant_B); **6 Elevated**; **41 Standard**. The two Critical Turbines sit in the same plant the coverage query already flagged as thin on Turbine technicians. -### 8. Find producibility bottlenecks _(eval Q8)_ +### 10. Find producibility bottlenecks **Prompt** @@ -134,9 +158,9 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, **Response** -The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) This corroborates the eval's Q8 finding that Pumps and Motors are the producibility bottlenecks. +The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) -### 9. Predict failures _(eval Q6, Q13)_ +### 11. Predict failures **Prompt** @@ -146,9 +170,9 @@ The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betwe **Response** _(bundled pre-computed predictions)_ -The bundled `failure_predictions` supply per-machine, per-period failure probability and predicted mode (the source of the step-4 ranking, e.g. M016/M028/M011 at 42.0% by period 12). For a live model, a GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README); the template ships the pre-computed predictions so the predictive question is answerable deterministically. +The bundled `failure_predictions` supply per-machine, per-period failure probability and predicted mode — the same probabilities behind the forward-failure ranking (e.g. M016/M028/M011 at 42.0% by period 12). For a live model, a GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README); the template ships the pre-computed predictions so the predictive question is answerable deterministically. -### 10. Schedule preventive maintenance + stress-test _(eval Q10, Q11, Q12)_ +### 12. Schedule preventive maintenance + stress-test **Prompt** @@ -158,8 +182,8 @@ The bundled `failure_predictions` supply per-machine, per-period failure probabi **Response** -Baseline solve is **OPTIMAL**: all **50 machines scheduled across periods 1–10** at 5 jobs/period (periods 11–12 absorb the slack), with the riskiest machines (M028, M016, M012, M006, M011) placed in period 1. Re-solving with **T001 unavailable** is still OPTIMAL but covers only **46 of 50** machines: the four Plant_A Turbines — **M001, M004, M006, M009** — lose coverage, because no on-site Turbine technician remains in Plant_A. This matches the eval's expected what-if outcome exactly. +Baseline solve is **OPTIMAL**: all **50 machines scheduled across periods 1–10** at 5 jobs/period (periods 11–12 absorb the slack), with the riskiest machines (M028, M016, M012, M006, M011) placed in period 1. Re-solving with **T001 unavailable** is still OPTIMAL but covers only **46 of 50** machines: the four Plant_A Turbines — **M001, M004, M006, M009** — lose coverage, because no on-site Turbine technician remains in Plant_A. ## Data -Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants × 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. All stages run in `machine_maintenance.py`. +Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants, 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. All stages run in `machine_maintenance.py`. From e7c14340033d9b61622c38050a60ca1ab7e9d03a Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 08:42:35 -0700 Subject: [PATCH 06/12] Align runbook breakdown with the 4-stage chain, script, and README - Remove the standalone 'Predict failures' step: the template surfaces failure predictions inside the querying stage and runs no live predictive reasoner, so the breakdown now matches the 4-stage chain (Querying/Rules/Graph/Prescriptive) shared by the script and README. The predictive note and GNN-extension pointer fold into the failure-ranking step. - Drop Sensor/SensorReading from the build and examine steps; the script defines the eight source concepts (no sensors), so the examine step now lists exactly those. - README: note which eight CSVs the script loads vs the seven bundled for the Customize extensions, so the sample-data section maps to the script. Chain stages, script Stage banners, README How-it-works, and the runbook breakdown now map 1:1. --- v1/machine_maintenance/README.md | 2 ++ v1/machine_maintenance/runbook.md | 22 +++++----------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index 85ec0516..97299657 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -146,6 +146,8 @@ The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: | `availability.csv` | 240 | Per-technician, per-period availability | | `degradation.csv` | 5 | Per-type degradation rate and maintenance reset factor | +The four-stage script loads `machines`, `technicians`, `qualifications`, `products`, `production_runs`, `machine_product_capabilities`, `downtime_events`, and `failure_predictions`. The remaining files — `fault_types`, `sensors`/`sensor_readings`, `travel`, `training_options`, `availability`, `degradation` — ship for the extensions in [Customize](#customize-this-template) and richer modeling of your own. + ## Model overview Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period` (1..12). The prescriptive stage adds a `MachinePeriod` decision space (one entry per machine and period). diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 669a86b6..07068dc5 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -39,12 +39,12 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa **Prompt** ``` -/rai-build-starter-ontology Build a manufacturing maintenance ontology from the CSVs in data/. Scope it for preventive-maintenance scheduling over a multi-period horizon — introduce a Period concept (1..12 from the `period` column) alongside the source-bound concepts (machines, technicians, qualifications, products, production runs, downtime events, failure predictions, sensors, machine-product capabilities). +/rai-build-starter-ontology Build a manufacturing maintenance ontology from the CSVs in data/. Scope it for preventive-maintenance scheduling over a multi-period horizon — introduce a Period concept (1..12 from the `period` column) alongside the source-bound concepts (machines, technicians, qualifications, products, production runs, downtime events, failure predictions, machine-product capabilities). ``` **Response** -Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine/Generator/Pump/Compressor/Motor), `Technician` (20), `Qualification` (32), `Product` (8), `ProductionRun` (844), `DowntimeEvent` (353), `FailurePrediction` (600), `Sensor` (200), `SensorReading` (2400), `MachineProductCapability` (120), and a generated `Period` (1..12). Junction concepts (`MachinePeriod`, `TechnicianMachinePeriod`) are deferred to the prescriptive stage. +Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine/Generator/Pump/Compressor/Motor), `Technician` (20), `Qualification` (32), `Product` (8), `ProductionRun` (844), `DowntimeEvent` (353), `FailurePrediction` (600), `MachineProductCapability` (120), and a generated `Period` (1..12). The `MachinePeriod` decision space is added later, in the prescriptive stage. ### 2. Examine the ontology @@ -56,7 +56,7 @@ Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine **Response** -50 `Machine`, 20 `Technician`, 32 `Qualification`, 8 `Product`, 844 `ProductionRun`, 353 `DowntimeEvent`, 600 `FailurePrediction`, 200 `Sensor` / 2,400 `SensorReading`, 120 `MachineProductCapability`, and 12 `Period`. Machines relate to production runs, downtime events, failure predictions, and product capabilities; technicians relate to qualifications. +50 `Machine`, 20 `Technician`, 32 `Qualification`, 8 `Product`, 844 `ProductionRun`, 353 `DowntimeEvent`, 600 `FailurePrediction`, 120 `MachineProductCapability`, and 12 `Period`. Machines relate to production runs, downtime events, failure predictions, and product capabilities; technicians relate to qualifications. ### 3. Discover reasoner questions @@ -110,7 +110,7 @@ Top fault names: **Bearing Failure 3,905 min (19.4%)**, Overheating 3,183 (15.8% **Response** -M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). +M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). These per-machine, per-period probabilities and predicted modes come from the bundled pre-computed `failure_predictions`; training a live GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README). ### 7. Surface the worst waste @@ -160,19 +160,7 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) -### 11. Predict failures - -**Prompt** - -``` -/rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each, given sensor readings, downtime history, and machine attributes? -``` - -**Response** _(bundled pre-computed predictions)_ - -The bundled `failure_predictions` supply per-machine, per-period failure probability and predicted mode — the same probabilities behind the forward-failure ranking (e.g. M016/M028/M011 at 42.0% by period 12). For a live model, a GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README); the template ships the pre-computed predictions so the predictive question is answerable deterministically. - -### 12. Schedule preventive maintenance + stress-test +### 11. Schedule preventive maintenance + stress-test **Prompt** From feb0e9ddda4d64b52188e853a34326e7bc02f6db Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 08:52:55 -0700 Subject: [PATCH 07/12] Add Predictive as a fifth stage with a pre-loaded default and live-GNN option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reinstate predictive as a first-class stage across the script, chain, README, and runbook. The predictive stage reads the bundled pre-loaded failure predictions by default (USE_PRELOADED_PREDICTIONS = True, no training step) and documents a live GNN as the opt-in alternative — so the predictive question is answerable out of the box, with the option to skip the model and use pre-loaded data or to wire a real GNN. - Script: new Stage 4 Predictive (forward failure risk) behind the toggle; Prescriptive renumbered to Stage 5; querying no longer prints the failure ranking. Full run OPTIMAL, all numbers unchanged. - README: reasoning_types += Predictive; five-stage How-it-works (Predictive between Graph and Prescriptive); intro, What-you'll-build, and sample-data note updated. - Runbook: five-stage chain; dual-mode predictive step after the graph step; renumbered to 11 steps. Script Stage banners, chain, README How-it-works, and runbook breakdown now map 1:1 across the five stages; py_compile + ruff clean. --- v1/machine_maintenance/README.md | 34 +++++++++---- v1/machine_maintenance/machine_maintenance.py | 47 ++++++++++++----- v1/machine_maintenance/runbook.md | 51 ++++++++++--------- 3 files changed, 85 insertions(+), 47 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index 97299657..ac0d13d2 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -1,13 +1,14 @@ --- title: "Machine Maintenance" -description: "A multi-reasoner template that chains querying, graph analysis, rules-based classification, and prescriptive optimization to diagnose plant performance, surface producibility bottlenecks, classify machine risk, and schedule preventive maintenance under technician-coverage constraints." +description: "A multi-reasoner template that chains querying, rules, graph analysis, predictive scoring, and prescriptive optimization to diagnose plant performance, classify machine risk, find producibility bottlenecks, rank forward failure risk, and schedule preventive maintenance under technician-coverage limits." featured: false experience_level: intermediate industry: "Manufacturing" reasoning_types: - Graph - - Rules-based + - Predictive - Prescriptive + - Rules-based tags: - Multi-Reasoner - Chained Reasoning @@ -23,7 +24,7 @@ tags: Manufacturing reliability teams have to decide **which machines to maintain, when, and with which technician** — under a fixed maintenance-bay limit and a thin bench of qualified technicians. Get it wrong and a critical machine fails unplanned, or a plant's only on-site specialist becomes a single point of failure. The hard part is that no single view answers the question: plant OEE shows where output is lost but not what will fail next, failure predictions rank risk but ignore who can do the work, and a feasible schedule can still hide a concentration risk that one resignation would expose. -This template works the problem end to end on a 50-machine, 3-plant, 12-period operation. **It chains four RelationalAI reasoners over one ontology — querying to diagnose plant performance, rules to classify machine risk, graph analysis to find producibility bottlenecks, and prescriptive optimization to schedule maintenance and stress-test it.** Each stage writes its findings back to the model, so the next stage builds on what the last one learned. +This template works the problem end to end on a 50-machine, 3-plant, 12-period operation. **It chains five reasoning stages over one ontology — querying to diagnose plant performance, rules to classify machine risk, graph analysis to find producibility bottlenecks, predictive scoring to rank forward failure risk, and prescriptive optimization to schedule maintenance and stress-test it.** Each stage writes its findings back to the model, so the next stage builds on what the last one learned. ## Who this is for @@ -36,15 +37,16 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c ## What you'll build - An ontology over machines, technicians, qualifications, products, production runs, downtime events, failure predictions, and machine-product capabilities -- Querying-stage metrics: OEE by plant, downtime by fault and plant, failure ranking, waste rates, technician coverage -- A betweenness-centrality bottleneck ranking over the machine-product graph +- Querying-stage metrics: OEE by plant, downtime by fault and plant, waste rates, technician coverage - A per-machine `risk_tier` derived from business rules +- A betweenness-centrality bottleneck ranking over the machine-product graph +- A forward failure-risk ranking from the bundled predictions (or a live GNN) - A preventive-maintenance schedule plus a technician-availability what-if ## What's included -- `machine_maintenance.py` — the four-stage multi-reasoner script -- `runbook.md` — a prompt-by-prompt walkthrough of the four-reasoner chain, with the real figures each stage produces +- `machine_maintenance.py` — the five-stage multi-reasoner script +- `runbook.md` — a prompt-by-prompt walkthrough of the five-stage chain, with the real figures each stage produces - `data/` — the bundled `MANUFACTURING.PUBLIC` sample (15 CSVs) - `pyproject.toml` — package configuration and dependencies @@ -146,7 +148,7 @@ The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: | `availability.csv` | 240 | Per-technician, per-period availability | | `degradation.csv` | 5 | Per-type degradation rate and maintenance reset factor | -The four-stage script loads `machines`, `technicians`, `qualifications`, `products`, `production_runs`, `machine_product_capabilities`, `downtime_events`, and `failure_predictions`. The remaining files — `fault_types`, `sensors`/`sensor_readings`, `travel`, `training_options`, `availability`, `degradation` — ship for the extensions in [Customize](#customize-this-template) and richer modeling of your own. +The five-stage script loads `machines`, `technicians`, `qualifications`, `products`, `production_runs`, `machine_product_capabilities`, `downtime_events`, and `failure_predictions`. The remaining files — `fault_types`, `sensors`/`sensor_readings`, `travel`, `training_options`, `availability`, `degradation` — ship for the extensions in [Customize](#customize-this-template) and richer modeling of your own. ## Model overview @@ -154,7 +156,7 @@ Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionR ## How it works -The script runs four stages in order — querying, rules, graph, prescriptive — each writing its findings back to the shared model. +The script runs five stages in order — querying, rules, graph, predictive, prescriptive — each writing its findings back to the shared model. ### 1. Querying Per-plant OEE combines a performance leg (average of actual-versus-target speed) and a quality leg (good versus actual quantity) from production runs with an availability leg from unplanned downtime against a planned base of 480 minutes per run. The legs are aggregated per plant, then multiplied: @@ -189,7 +191,19 @@ model.where( prod_graph.Node.bottleneck_raw = prod_graph.betweenness_centrality() ``` -### 4. Prescriptive +### 4. Predictive +Forward failure risk comes from the bundled pre-computed `failure_predictions` by default — no training step — so the predictive question is answerable out of the box. Set `USE_PRELOADED_PREDICTIONS = False` to wire a live GNN over the sensor and downtime history instead (see _Customize_): + +```python +if USE_PRELOADED_PREDICTIONS: + fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( + FailurePrediction.machine_id_str.alias("machine_id"), + FailurePrediction.predicted_failure_mode.alias("mode"), + FailurePrediction.failure_probability.alias("fp"), + ).to_df().sort_values("fp", ascending=False) +``` + +### 5. Prescriptive A binary `MachinePeriod.x_maintain` decides which machine is maintained in which period. Each machine gets at most one slot and only if coverage is feasible (Turbine work requires an on-site qualified technician), and each period is capped at five jobs: ```python diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 7f86d38d..13f4b5a8 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -38,6 +38,8 @@ HIGH_RISK_FP = 0.20 # failure-probability cutoff for high-risk HIGH_RISK_CRITICALITY = 4 # criticality cutoff for high-risk OVERDUE_RUL = 9 # remaining-useful-life at/below which maintenance is overdue +USE_PRELOADED_PREDICTIONS = True # predictive stage: read the bundled failure_predictions +# (default); set False to wire a live GNN -- see README "Customize" model = Model("machine_maintenance") @@ -265,17 +267,7 @@ def banner(text): for _, row in plant_dt.iterrows(): print(f" {row['facility']}: {row['dt_min']:.0f} min ({row['pct']:.1f}%)") -# --- Q4: highest forward failure risk at the end of the horizon --- -fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( - FailurePrediction.machine_id_str.alias("machine_id"), - FailurePrediction.predicted_failure_mode.alias("mode"), - FailurePrediction.failure_probability.alias("fp"), -).to_df().sort_values("fp", ascending=False) -print(f"\n-- Q4: top failure predictions at period {PERIOD_HORIZON} --") -for _, row in fail_p12.head(5).iterrows(): - print(f" {row['machine_id']} {row['mode']} ({row['fp']*100:.1f}%)") - -# --- Q5: worst waste rates by machine-product --- +# --- Worst waste rates by machine-product --- waste = model.where(ProductionRun.machine(Machine), ProductionRun.product(Product)).select( distinct( Machine.machine_id.alias("machine_id"), @@ -409,10 +401,39 @@ def banner(text): ) # ================================================================== -# Stage 4: Prescriptive -- preventive-maintenance schedule + what-if +# Stage 4: Predictive -- forward failure risk +# ================================================================== +# +# Ships with pre-loaded predictions (failure_predictions.csv) so the predictive +# question is answerable out of the box, with no training step. To run a live model +# instead, set USE_PRELOADED_PREDICTIONS = False and train a GNN over the sensor and +# downtime history (see README "Customize" and the rai-predictive-* skills). + +banner("STAGE 4 Predictive") + +if USE_PRELOADED_PREDICTIONS: + fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( + FailurePrediction.machine_id_str.alias("machine_id"), + FailurePrediction.predicted_failure_mode.alias("mode"), + FailurePrediction.failure_probability.alias("fp"), + ).to_df().sort_values("fp", ascending=False) + print(f"\n-- Most likely to fail by period {PERIOD_HORIZON} (pre-loaded predictions) --") + for _, row in fail_p12.head(5).iterrows(): + print(f" {row['machine_id']} {row['mode']} ({row['fp'] * 100:.1f}%)") +else: + raise NotImplementedError( + "Live GNN path is not wired in this template. Either set " + "USE_PRELOADED_PREDICTIONS = True to use the bundled predictions, or train a " + "GNN over sensor/downtime history and write FailurePrediction " + "(see README 'Customize' and the rai-predictive-modeling / -training skills)." + ) + + +# ================================================================== +# Stage 5: Prescriptive -- preventive-maintenance schedule + what-if # ================================================================== -banner("STAGE 4 Prescriptive") +banner("STAGE 5 Prescriptive") # Periods 1..H as a concept so the schedule can index (machine, period). Period = model.Concept("Period", identify_by={"pid": Integer}) diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 07068dc5..1fc7afb4 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -1,18 +1,17 @@ # Runbook: Machine Maintenance — Multi-Reasoner Walkthrough -Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufacturing operation. OEE alone misranks the plants; downtime totals don't say what will fail next; rules flag risky machines but don't allocate scarce technician time; the optimizer produces a feasible schedule but can't see that all on-site Turbine coverage funnels through a single technician per plant. The chain threads querying, graph, rules, and prescriptive reasoners through one ontology so each stage's enrichments feed the next. +Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufacturing operation. OEE alone misranks the plants; downtime totals don't say what will fail next; rules flag risky machines but don't allocate scarce technician time; the optimizer produces a feasible schedule but can't see that all on-site Turbine coverage funnels through a single technician per plant. The chain threads querying, rules, graph, predictive, and prescriptive reasoners through one ontology so each stage's enrichments feed the next. > **Data provenance.** Every figure below is computed from the bundled `data/*.csv` — the real `MANUFACTURING.PUBLIC` sample: 50 machines, 20 technicians, 8 products, and 12 weekly periods across Plant_A, Plant_B, and Plant_C. > -> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations; predictive reads the bundled pre-computed failure predictions, with a live GNN as the natural extension. +> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations. The predictive stage reads the bundled pre-loaded failure predictions by default (no training step); a live GNN is the opt-in alternative. ## The chain ``` ───────────────────────────────────────────────────────────────── - STAGE 1 Querying ──► OEE by plant, downtime drivers, failure - /rai-querying ranking (from pre-computed predictions), - waste rates, technician coverage + STAGE 1 Querying ──► OEE by plant, downtime drivers, waste + /rai-querying rates, technician coverage Plant_C 78.3% > Plant_A 68.0% > Plant_B 63.3% Bearing Failure = 19.4% of all downtime. Turbines have only 3 qualified technicians. @@ -24,7 +23,11 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa STAGE 3 Graph ──► Machine.bottleneck (50) /rai-graph-analysis Pumps & Motors bridge the most product lines. ───────────────────────────────────────────────────────────────── - STAGE 4 Prescriptive ──► Maintenance schedule + technician what-if + STAGE 4 Predictive ──► Forward failure risk & mode, 12 periods + /rai-predictive-modeling Pre-loaded by default; live GNN optional. + M016 / M028 / M011 ≈ 42% by period 12. + ───────────────────────────────────────────────────────────────── + STAGE 5 Prescriptive ──► Maintenance schedule + technician what-if /rai-prescriptive-* All 50 scheduled (5/period, P1–10); drop T001 and 4 Plant_A Turbines lose coverage. ───────────────────────────────────────────────────────────────── @@ -63,12 +66,12 @@ Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine **Prompt** ``` -/rai-discovery We run a 50-machine, 3-plant operation and want to keep it healthy: where are we losing output, what is likely to fail next, where are the structural single points of failure, and how should we schedule preventive maintenance under a thin bench of qualified technicians? Which of these are querying, rules, graph, and prescriptive questions? +/rai-discovery We run a 50-machine, 3-plant operation and want to keep it healthy: where are we losing output, what is likely to fail next, where are the structural single points of failure, and how should we schedule preventive maintenance under a thin bench of qualified technicians? Which of these are querying, rules, graph, predictive, and prescriptive questions? ``` **Response** -Routes the work across reasoner families: descriptive querying (OEE, downtime drivers, forward failure risk, waste rates, technician coverage), rules (per-machine risk tiers), graph (producibility bottlenecks), and prescriptive (preventive-maintenance scheduling with a technician what-if). The stages run in that order so each enriches the ontology the next one reads. +Routes the work across reasoner families: descriptive querying (OEE, downtime drivers, waste rates, technician coverage), rules (per-machine risk tiers), graph (producibility bottlenecks), predictive (forward failure risk), and prescriptive (preventive-maintenance scheduling with a technician what-if). These map to the stages below, in order. ### 4. Diagnose plant operations — OEE @@ -100,65 +103,65 @@ Plant_C leads; Plant_B trails — driven by its low Availability (most unplanned Top fault names: **Bearing Failure 3,905 min (19.4%)**, Overheating 3,183 (15.8%), Motor Burnout 2,344 (11.7%), Seal Degradation 2,328 (11.6%), Shaft Misalignment 1,600 (8.0%). By plant: **Plant_B 10,494 min (52.2%)**, Plant_A 6,576 (32.7%), Plant_C 3,033 (15.1%). Total downtime = 20,103 min. -### 6. Rank forward failure risk +### 6. Surface the worst waste **Prompt** ``` -/rai-querying Which machines are most likely to fail by the end of the planning horizon (period 12), and for what predicted reason? +/rai-querying Which machine-product combinations have the worst waste rates (waste_quantity / actual_quantity), to one decimal? ``` **Response** -M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). These per-machine, per-period probabilities and predicted modes come from the bundled pre-computed `failure_predictions`; training a live GNN over the sensor and downtime history is the natural extension (see _Customize_ in the README). +M025 + Hydraulic Seal Kit (3.8%), M005 + Turbine Blade Assembly (3.7%), M002 + Turbine Blade Assembly (3.6%), M049 + Motor Winding Set (3.6%), M045 + Control Panel Unit (3.5%). -### 7. Surface the worst waste +### 7. Check technician coverage **Prompt** ``` -/rai-querying Which machine-product combinations have the worst waste rates (waste_quantity / actual_quantity), to one decimal? +/rai-querying Which machine types have the fewest qualified technicians? ``` **Response** -M025 + Hydraulic Seal Kit (3.8%), M005 + Turbine Blade Assembly (3.7%), M002 + Turbine Blade Assembly (3.6%), M049 + Motor Winding Set (3.6%), M045 + Control Panel Unit (3.5%). +Turbines are most constrained — only **3** qualified technicians (T001, T009, T017). Generators have 6; Pumps 7; Compressors and Motors 8 each. This concentration is what later makes Turbine coverage fragile in the schedule. -### 8. Check technician coverage +### 8. Classify machine risk **Prompt** ``` -/rai-querying Which machine types have the fewest qualified technicians? +/rai-rules-authoring Rate each machine's risk from three flags: chronic = more than 15 downtime events; high-risk = failure probability above 0.20 AND criticality 4 or higher; maintenance-overdue = remaining useful life of 9 or less. All three flags → Critical; exactly two → Elevated; otherwise Standard. ``` **Response** -Turbines are most constrained — only **3** qualified technicians (T001, T009, T017). Generators have 6; Pumps 7; Compressors and Motors 8 each. This concentration is what later makes Turbine coverage fragile in the schedule. +**3 Critical** — M001 (Turbine, Plant_A), M006 (Turbine, Plant_A), M011 (Compressor, Plant_B); **6 Elevated**; **41 Standard**. The two Critical Turbines sit in the same plant the coverage query already flagged as thin on Turbine technicians. -### 9. Classify machine risk +### 9. Find producibility bottlenecks **Prompt** ``` -/rai-rules-authoring Rate each machine's risk from three flags: chronic = more than 15 downtime events; high-risk = failure probability above 0.20 AND criticality 4 or higher; maintenance-overdue = remaining useful life of 9 or less. All three flags → Critical; exactly two → Elevated; otherwise Standard. +/rai-graph-analysis In a bipartite machine-product capability graph (machines and products as nodes), which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? ``` **Response** -**3 Critical** — M001 (Turbine, Plant_A), M006 (Turbine, Plant_A), M011 (Compressor, Plant_B); **6 Elevated**; **41 Standard**. The two Critical Turbines sit in the same plant the coverage query already flagged as thin on Turbine technicians. +The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) -### 10. Find producibility bottlenecks +### 10. Predict forward failure risk **Prompt** ``` -/rai-graph-analysis In a bipartite machine-product capability graph (machines and products as nodes), which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? +/rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each? Use the bundled pre-loaded predictions, or train a live GNN over the sensor and downtime history. ``` **Response** -The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betweenness centrality is highest for the **20 machines that each make three products — every Pump (M021–M030) and Motor (M041–M050)** — which tie at the top (46.7) because the most production routes flow through them; the two-product machines sit on no shortest paths. (A machine-machine co-occurrence projection over shared products surfaces the same 20.) +By default the template reads the bundled pre-loaded `failure_predictions` (no training step), so the answer is deterministic — by period 12 the highest-risk machines are M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). To run it live instead, set `USE_PRELOADED_PREDICTIONS = False` and train a GNN over the sensor and downtime history (see _Customize_ in the README and the rai-predictive-modeling / rai-predictive-training skills). ### 11. Schedule preventive maintenance + stress-test @@ -174,4 +177,4 @@ Baseline solve is **OPTIMAL**: all **50 machines scheduled across periods 1–10 ## Data -Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants, 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. All stages run in `machine_maintenance.py`. +Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants, 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. The five-stage script loads the first eight; the rest support the extensions in the README. All stages run in `machine_maintenance.py`. From a07682d11830958e168e60d7978739e29a4ded63 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 09:09:43 -0700 Subject: [PATCH 08/12] Naturalize runbook prompts: drop reasoner/algorithm/impl mechanics Per dev-evals-review / dev-templates-review, an analyst prompt shouldn't telegraph the machinery: - Discovery: drop 'Which of these are querying/rules/graph/predictive/ prescriptive questions?' -- /rai-discovery produces that routing. - Predictive: drop 'Use the bundled pre-loaded predictions, or train a live GNN...' -- implementation detail, kept in the response. - Graph: drop the 'bipartite ... (machines and products as nodes)' construction hint; the structural-test phrasing anchors it and the response notes both constructions surface the same 20 machines. Metric/constraint definitions that change the answer (OEE formula, risk thresholds, schedule limits) stay inline. Responses keep the construction and toggle detail. --- v1/machine_maintenance/runbook.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 1fc7afb4..41ce06df 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -66,7 +66,7 @@ Concepts bound to the bundled CSVs: `Machine` (50, across Plant_A/B/C × Turbine **Prompt** ``` -/rai-discovery We run a 50-machine, 3-plant operation and want to keep it healthy: where are we losing output, what is likely to fail next, where are the structural single points of failure, and how should we schedule preventive maintenance under a thin bench of qualified technicians? Which of these are querying, rules, graph, predictive, and prescriptive questions? +/rai-discovery We run a 50-machine, 3-plant operation and want to keep it healthy: where are we losing output, what is likely to fail next, where are the structural single points of failure, and how should we schedule preventive maintenance under a thin bench of qualified technicians? ``` **Response** @@ -144,7 +144,7 @@ Turbines are most constrained — only **3** qualified technicians (T001, T009, **Prompt** ``` -/rai-graph-analysis In a bipartite machine-product capability graph (machines and products as nodes), which machines are the biggest producibility bottlenecks — the machines that the most production routes flow through, so they bridge the most product lines? +/rai-graph-analysis Which machines are the biggest producibility bottlenecks — the ones the most production routes flow through, so they bridge the most product lines and are hardest to route around if lost? ``` **Response** @@ -156,7 +156,7 @@ The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betwe **Prompt** ``` -/rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each? Use the bundled pre-loaded predictions, or train a live GNN over the sensor and downtime history. +/rai-predictive-modeling Which machines are most likely to fail over the next 12 periods, and what's the most likely failure mode for each? ``` **Response** From 56ba6eaca8ff3d9a49f84aeb27d2eb48c5e10801 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 09:33:36 -0700 Subject: [PATCH 09/12] Audit fixes: 5-stage docstring, drop Q# stdout fossils, README conformance From a fresh-eyes dev-templates-review audit of the README, script, and runbook: - Script docstring was stale at 'four reasoners' and omitted the Predictive stage (mis-numbering Prescriptive as Stage 4). Now lists all five stages, matching the body banners, README, and runbook. - Drop the non-contiguous Q1/Q2/Q3/Q5/Q7/Q8/Q9/Q10/Q11 labels from stdout and comments (vestiges of an internal question set); use descriptive titles. - README: add Model-overview Key-entities / Primary-identifiers / Important-invariants bullets plus a per-concept table; add a 'Scale up / productionize' customize subsection; add 'Learn more' and 'Support' sections -- matching sample-template and telco_network_recovery. No logic change; the verified run numbers are unchanged. py_compile + ruff clean. --- v1/machine_maintenance/README.md | 38 ++++++++++++++++++- v1/machine_maintenance/machine_maintenance.py | 38 ++++++++++--------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index ac0d13d2..2c3c6306 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -152,7 +152,25 @@ The five-stage script loads `machines`, `technicians`, `qualifications`, `produc ## Model overview -Core concepts: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period` (1..12). The prescriptive stage adds a `MachinePeriod` decision space (one entry per machine and period). +One shared ontology threads all five stages. Each stage reads concepts and properties earlier stages wrote, and writes new ones for downstream stages. + +- **Key entities**: `Machine`, `Technician`, `Qualification`, `Product`, `ProductionRun`, `DowntimeEvent`, `FailurePrediction`, `MachineProductCapability`, and a generated `Period`; plus the derived `MachinePeriod` (the schedule decision space) and `MaintenancePlan` (singleton plan headline). +- **Primary identifiers**: string ids on the base entities (`machine_id`, `technician_id`, `product_id`, `run_id`, `event_id`, `prediction_id`); composite keys on `Qualification` (`technician_id` + `machine_type`), `MachineProductCapability` (`machine_id` + `product_id`), and `MachinePeriod` (`machine_id` + period); integer `pid` on `Period`. +- **Important invariants**: `failure_probability` is a fraction in `[0, 1]`; `criticality` is an integer from 1 to 5; `is_planned` is 0 or 1; durations, quantities, and costs are non-negative; `Machine.risk_tier` is one of Critical, Elevated, or Standard; the prescriptive decision variables are binary. + +| Concept | Identifier | Role | +|---|---|---| +| `Machine` | `machine_id` | The 50 machines (type, plant, location, remaining useful life, failure probability, criticality). Stages 2–4 enrich it with `risk_tier`, `bottleneck`, and coverage flags. | +| `Technician` | `technician_id` | The 20 technicians, with base location and skill level. | +| `Qualification` | `technician_id` + `machine_type` | Which technicians can service which machine type. | +| `Product` | `product_id` | The 8 manufactured products. | +| `ProductionRun` | `run_id` | Per-run planned/actual/good/waste quantities and speeds (feeds OEE and waste). | +| `DowntimeEvent` | `event_id` | Downtime events with fault name, duration, and planned flag. | +| `FailurePrediction` | `prediction_id` | Per-machine, per-period failure probability and predicted mode. | +| `MachineProductCapability` | `machine_id` + `product_id` | Which machines can produce which products (the bipartite graph edges). | +| `Period` | `pid` | Planning periods 1 to 12 (generated). | +| `MachinePeriod` | `machine_id` + period | The machine-by-period maintenance decision space (prescriptive stage). | +| `MaintenancePlan` | singleton | Persisted plan headline: objective, machines scheduled, periods used. | ## How it works @@ -226,7 +244,12 @@ Replace the CSVs in `data/` with your own machines, technicians, production, and The thresholds at the top of `machine_maintenance.py` — period horizon, per-period bay limit, chronic/high-risk/overdue cutoffs — are constants you can adjust to your operation. ### Extend the model -Add reasoners or stages: cluster machines by shared technicians, train a GNN on the sensor/downtime history for failure prediction, or add cross-training recommendations from `training_options` to relieve the coverage bottlenecks the what-if surfaces. +Add reasoners or stages: cluster machines by shared technicians, train a GNN on the sensor/downtime history for failure prediction (set `USE_PRELOADED_PREDICTIONS = False`), or add cross-training recommendations from `training_options` to relieve the coverage bottlenecks the what-if surfaces. + +### Scale up / productionize +- Swap the `data/` CSV bundle for `model.Table(...)` reads against your Snowflake tables — the concept definitions stay the same — so the model runs on live operational data. +- Lengthen `PERIOD_HORIZON` and raise the per-period bay limit to match a real maintenance calendar; the prescriptive solve scales with the engine's solve budget. +- For larger schedules, run the prescriptive stage on a gurobi-enabled engine; the formulation is unchanged. ## Troubleshooting @@ -241,3 +264,14 @@ Make sure you activated the virtual environment and ran `python -m pip install . Run `rai init` to configure your Snowflake connection. Verify that the RAI Native App is installed and your user has the required permissions. + +## Learn more + +- [Multi-reasoner workflows](https://docs.relational.ai/) — chaining reasoners and accreting enrichments on one ontology. +- [PyRel v1 query language](https://docs.relational.ai/) — `model.where(...)`, `aggs`, and `.define()`. +- [Graph reasoner](https://docs.relational.ai/) — node/edge construction and centrality algorithms. +- [Prescriptive reasoner](https://docs.relational.ai/) — the `Problem` API, decision variables, constraints, and objectives. + +## Support + +- File issues at the RelationalAI templates repository. diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 13f4b5a8..27d3ff53 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -1,13 +1,15 @@ """Machine Maintenance -- multi-reasoner template (PyRel v1). -A 50-machine, 3-plant, 12-period manufacturing operation. The script threads four -reasoners through a single ontology so each stage's enrichments feed the next: +A 50-machine, 3-plant, 12-period manufacturing operation. The script threads five +reasoning stages through a single ontology so each stage's enrichments feed the next: - Stage 1 Querying -- OEE by plant, downtime drivers, failure ranking, - waste rates, technician coverage. + Stage 1 Querying -- OEE by plant, downtime drivers, waste rates, + technician coverage. Stage 2 Rules -- per-machine risk tier (chronic / high-risk / overdue). Stage 3 Graph -- machine-product producibility bottlenecks. - Stage 4 Prescriptive -- preventive-maintenance schedule + a technician what-if. + Stage 4 Predictive -- forward failure risk & mode (pre-loaded predictions; + live GNN optional). + Stage 5 Prescriptive -- preventive-maintenance schedule + a technician what-if. Data is the bundled MANUFACTURING.PUBLIC sample (data/*.csv). @@ -201,7 +203,7 @@ def banner(text): banner("STAGE 1 Querying") -# --- Q1: OEE by plant = Availability x Performance x Quality --- +# --- OEE by plant = Availability x Performance x Quality --- # Performance / quality / run counts come from production runs; unplanned downtime # from downtime events. Aggregated separately (different fact tables) then combined. runs_by_plant = model.where(ProductionRun.machine(Machine)).select( @@ -232,7 +234,7 @@ def banner(text): oee["quality"] = oee["good_q"] / oee["actual_q"] oee["oee"] = oee["availability"] * oee["performance"] * oee["quality"] oee = oee.sort_values("oee", ascending=False) -print("\n-- Q1: OEE by plant --") +print("\n-- OEE by plant --") for _, row in oee.iterrows(): print( f" {row['facility']}: availability {row['availability']*100:.1f}% " @@ -240,7 +242,7 @@ def banner(text): f"OEE {row['oee']*100:.1f}%" ) -# --- Q2: top downtime drivers by fault name --- +# --- Top downtime drivers by fault name --- fault_dt = model.select( distinct( DowntimeEvent.fault_name.alias("fault_name"), @@ -250,11 +252,11 @@ def banner(text): total_dt = fault_dt["dt_min"].sum() fault_dt["pct"] = 100 * fault_dt["dt_min"] / total_dt fault_dt = fault_dt.sort_values("dt_min", ascending=False) -print(f"\n-- Q2: top downtime by fault name (total {total_dt:.0f} min) --") +print(f"\n-- Top downtime by fault name (total {total_dt:.0f} min) --") for _, row in fault_dt.head(5).iterrows(): print(f" {row['fault_name']}: {row['dt_min']:.0f} min ({row['pct']:.1f}%)") -# --- Q3: downtime by plant --- +# --- Downtime by plant --- plant_dt = model.where(DowntimeEvent.machine(Machine)).select( distinct( Machine.facility.alias("facility"), @@ -263,7 +265,7 @@ def banner(text): ).to_df() plant_dt["pct"] = 100 * plant_dt["dt_min"] / plant_dt["dt_min"].sum() plant_dt = plant_dt.sort_values("dt_min", ascending=False) -print("\n-- Q3: downtime by plant --") +print("\n-- Downtime by plant --") for _, row in plant_dt.iterrows(): print(f" {row['facility']}: {row['dt_min']:.0f} min ({row['pct']:.1f}%)") @@ -278,11 +280,11 @@ def banner(text): ).alias("waste_rate"), ) ).to_df().sort_values("waste_rate", ascending=False) -print("\n-- Q5: worst waste rates by machine-product --") +print("\n-- Worst waste rates by machine-product --") for _, row in waste.head(5).iterrows(): print(f" {row['machine_id']} + {row['product_name']} ({row['waste_rate']*100:.1f}%)") -# --- Q7: machine types with fewest qualified technicians --- +# --- Machine types with fewest qualified technicians --- tech_cov = model.select( distinct( Qualification.machine_type_str.alias("machine_type"), @@ -291,7 +293,7 @@ def banner(text): ).to_df() tech_cov["n_techs"] = tech_cov["n_techs"].astype(int) tech_cov = tech_cov.sort_values("n_techs") -print("\n-- Q7: qualified technicians per machine type --") +print("\n-- Qualified technicians per machine type --") for _, row in tech_cov.iterrows(): print(f" {row['machine_type']}: {int(row['n_techs'])}") @@ -341,7 +343,7 @@ def banner(text): ).to_df() tiers["n"] = tiers["n"].astype(int) tiers = tiers.sort_values("n", ascending=False) -print("\n-- Q9: machine risk tiers --") +print("\n-- Machine risk tiers --") for _, row in tiers.iterrows(): print(f" {row['tier']}: {int(row['n'])}") @@ -393,7 +395,7 @@ def banner(text): ).to_df() bottlenecks["product_count"] = bottlenecks["product_count"].astype(int) bottlenecks = bottlenecks.sort_values("bottleneck", ascending=False) -print("\n-- Q8: top machine producibility bottlenecks (betweenness centrality) --") +print("\n-- Top machine producibility bottlenecks (betweenness centrality) --") for _, row in bottlenecks.head(8).iterrows(): print( f" {row['machine_id']} ({row['machine_type']}): betweenness {row['bottleneck']:.4f}, " @@ -540,7 +542,7 @@ def _report_schedule(label, sched_df, si): MachinePeriod.period_num.alias("pid"), ).where(MachinePeriod.machine(_sm), MachinePeriod.x_maintain(_vr), _vr > 0.5).to_df() sched["pid"] = sched["pid"].astype(int) -_report_schedule("Q10/Q12 baseline schedule", sched, si) +_report_schedule("Baseline schedule", sched, si) earliest = sched.sort_values("pid").head(5) print(" first jobs (riskiest, earliest): " + ", ".join(f"{r.machine_id}(p{r.pid})" for r in earliest.itertuples())) @@ -583,7 +585,7 @@ def _report_schedule(label, sched_df, si): _sm2.machine_id.alias("machine_id"), MachinePeriod.period_num.alias("pid"), ).where(MachinePeriod.machine(_sm2), MachinePeriod.x_maintain_wif(_vr2), _vr2 > 0.5).to_df() -print(f"\n-- Q11 what-if (T001 unavailable): status {si2.termination_status}, objective {si2.objective_value:.3f} --") +print(f"\n-- What-if (T001 unavailable): status {si2.termination_status}, objective {si2.objective_value:.3f} --") print(f" machines scheduled: {len(sched2)} of 50 (baseline {len(sched)}); objective delta {si.objective_value - si2.objective_value:.3f}") print(f" machines that lose coverage: {len(dropped)}") for _, row in dropped.iterrows(): From 3559773b3cfc106925984591ce30e4c35c367a0c Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 09:37:57 -0700 Subject: [PATCH 10/12] Align README Model overview and Learn more to sample-template - Model overview: per-concept tables under '### Concepts' (was a single summary table), matching sample-template's one-table-per-concept format; Relationships are concept properties, so no separate Relationships table is needed. - Learn more: grouped into subsections (Core concepts / Language & modeling reference / Reasoner reference). (PR #90 description tightened separately.) --- v1/machine_maintenance/README.md | 93 +++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index 2c3c6306..d3037436 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -158,19 +158,81 @@ One shared ontology threads all five stages. Each stage reads concepts and prope - **Primary identifiers**: string ids on the base entities (`machine_id`, `technician_id`, `product_id`, `run_id`, `event_id`, `prediction_id`); composite keys on `Qualification` (`technician_id` + `machine_type`), `MachineProductCapability` (`machine_id` + `product_id`), and `MachinePeriod` (`machine_id` + period); integer `pid` on `Period`. - **Important invariants**: `failure_probability` is a fraction in `[0, 1]`; `criticality` is an integer from 1 to 5; `is_planned` is 0 or 1; durations, quantities, and costs are non-negative; `Machine.risk_tier` is one of Critical, Elevated, or Standard; the prescriptive decision variables are binary. -| Concept | Identifier | Role | -|---|---|---| -| `Machine` | `machine_id` | The 50 machines (type, plant, location, remaining useful life, failure probability, criticality). Stages 2–4 enrich it with `risk_tier`, `bottleneck`, and coverage flags. | -| `Technician` | `technician_id` | The 20 technicians, with base location and skill level. | -| `Qualification` | `technician_id` + `machine_type` | Which technicians can service which machine type. | -| `Product` | `product_id` | The 8 manufactured products. | -| `ProductionRun` | `run_id` | Per-run planned/actual/good/waste quantities and speeds (feeds OEE and waste). | -| `DowntimeEvent` | `event_id` | Downtime events with fault name, duration, and planned flag. | -| `FailurePrediction` | `prediction_id` | Per-machine, per-period failure probability and predicted mode. | -| `MachineProductCapability` | `machine_id` + `product_id` | Which machines can produce which products (the bipartite graph edges). | -| `Period` | `pid` | Planning periods 1 to 12 (generated). | -| `MachinePeriod` | `machine_id` + period | The machine-by-period maintenance decision space (prescriptive stage). | -| `MaintenancePlan` | singleton | Persisted plan headline: objective, machines scheduled, periods used. | +### Concepts + +**`Machine`** — the 50 machines under management. Stages 2–4 enrich it with risk, bottleneck, and coverage properties. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `machine_id` | String | Yes | From `data/machines.csv` | +| `machine_type`, `facility`, `location` | String | No | Type (Turbine/Generator/Pump/Compressor/Motor), plant, plant city | +| `remaining_useful_life`, `failure_probability` | Float | No | Remaining useful life; static per-machine failure probability | +| `criticality` | Integer | No | Business impact, 1 to 5 | +| `risk_tier` | String | No | **Rules stage** — Critical / Elevated / Standard | +| `bottleneck` | Float | No | **Graph stage** — betweenness centrality | + +**`Technician`** — the 20 maintenance technicians. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `technician_id` | String | Yes | From `data/technicians.csv` | +| `base_location`, `skill_level` | String | No | Home plant city; skill band | +| `hourly_rate`, `max_weekly_hours` | Float / Integer | No | Labor capacity | + +**`Qualification`** — which technicians can service which machine type. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `technician_id` + `machine_type` | String | Yes | Composite key from `data/qualifications.csv` | +| `technician` | Relationship | — | Link to `Technician` | + +**`Product`** — the 8 manufactured products. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `product_id` | String | Yes | From `data/products.csv` | +| `product_name` | String | No | Human-readable name | + +**`ProductionRun`** — per-run output; feeds OEE and waste. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `run_id` | String | Yes | From `data/production_runs.csv` | +| `machine`, `product` | Relationship | — | Links to `Machine` / `Product` | +| `period_int` | Integer | No | Period, 1 to 12 | +| `actual_quantity`, `good_quantity`, `waste_quantity` | Integer | No | Quality and waste inputs | +| `actual_speed`, `target_speed` | Float | No | OEE performance inputs | + +**`DowntimeEvent`** — downtime occurrences. + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `event_id` | String | Yes | From `data/downtime_events.csv` | +| `machine` | Relationship | — | Link to `Machine` | +| `fault_name` | String | No | Specific fault | +| `duration_minutes` | Integer | No | Downtime length | +| `is_planned` | Integer | No | 0 = unplanned (counts against availability) | + +**`FailurePrediction`** — per-machine, per-period failure forecast (pre-loaded). + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `prediction_id` | String | Yes | From `data/failure_predictions.csv` | +| `machine_id_str`, `period_int` | String / Integer | No | Machine and period | +| `failure_probability`, `predicted_failure_mode` | Float / String | No | Predictive-stage inputs | + +**`MachineProductCapability`** — which machines can produce which products (the graph edges). + +| Property | Type | Identifying? | Notes | +|---|---|---|---| +| `machine_id` + `product_id` | String | Yes | Composite key from `data/machine_product_capabilities.csv` | +| `machine`, `product` | Relationship | — | Links to `Machine` / `Product` | + +**`Period`** — planning periods 1 to 12, generated in code (no CSV). + +**`MachinePeriod`** — the machine-by-period maintenance decision space (prescriptive stage); carries the binary `x_maintain` decision. + +**`MaintenancePlan`** — a singleton written after the solve, holding the objective, machines scheduled, and periods used. ## How it works @@ -267,8 +329,13 @@ Run `rai init` to configure your Snowflake connection. Verify that the RAI Nativ ## Learn more +### Core concepts - [Multi-reasoner workflows](https://docs.relational.ai/) — chaining reasoners and accreting enrichments on one ontology. + +### Language & modeling reference - [PyRel v1 query language](https://docs.relational.ai/) — `model.where(...)`, `aggs`, and `.define()`. + +### Reasoner reference - [Graph reasoner](https://docs.relational.ai/) — node/edge construction and centrality algorithms. - [Prescriptive reasoner](https://docs.relational.ai/) — the `Problem` API, decision variables, constraints, and objectives. From 486b86aa656e74208bc9b73324ab12baa187e346 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 09:41:08 -0700 Subject: [PATCH 11/12] Genericize internal source name (MANUFACTURING.PUBLIC) to sample data Customer-facing artifacts shouldn't expose the internal source schema. Replace the MANUFACTURING.PUBLIC references in the README, runbook, and script docstring and Stage 0 banner with neutral phrasing (a sample manufacturing dataset / the bundled sample data). PR #90 description updated likewise. --- v1/machine_maintenance/README.md | 4 ++-- v1/machine_maintenance/machine_maintenance.py | 4 ++-- v1/machine_maintenance/runbook.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index d3037436..5169afe6 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -47,7 +47,7 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c - `machine_maintenance.py` — the five-stage multi-reasoner script - `runbook.md` — a prompt-by-prompt walkthrough of the five-stage chain, with the real figures each stage produces -- `data/` — the bundled `MANUFACTURING.PUBLIC` sample (15 CSVs) +- `data/` — the bundled sample data (15 CSVs) - `pyproject.toml` — package configuration and dependencies ## Prerequisites @@ -129,7 +129,7 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c ## Sample data -The bundled CSVs are the real `MANUFACTURING.PUBLIC` sample dataset: +The bundled CSVs are a sample manufacturing dataset: | File | Rows | Description | |---|---|---| diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 27d3ff53..5c0886a2 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -11,7 +11,7 @@ live GNN optional). Stage 5 Prescriptive -- preventive-maintenance schedule + a technician what-if. -Data is the bundled MANUFACTURING.PUBLIC sample (data/*.csv). +Data is the bundled sample in data/*.csv. Run: python machine_maintenance.py @@ -46,7 +46,7 @@ model = Model("machine_maintenance") # ================================================================== -# Stage 0: Ontology -- load the MANUFACTURING.PUBLIC sample +# Stage 0: Ontology -- load the bundled sample data # ================================================================== # Machine --------------------------------------------------------------------- diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index 41ce06df..e59d0943 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -2,7 +2,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufacturing operation. OEE alone misranks the plants; downtime totals don't say what will fail next; rules flag risky machines but don't allocate scarce technician time; the optimizer produces a feasible schedule but can't see that all on-site Turbine coverage funnels through a single technician per plant. The chain threads querying, rules, graph, predictive, and prescriptive reasoners through one ontology so each stage's enrichments feed the next. -> **Data provenance.** Every figure below is computed from the bundled `data/*.csv` — the real `MANUFACTURING.PUBLIC` sample: 50 machines, 20 technicians, 8 products, and 12 weekly periods across Plant_A, Plant_B, and Plant_C. +> **Data provenance.** Every figure below is computed from the bundled `data/*.csv` — a sample manufacturing dataset: 50 machines, 20 technicians, 8 products, and 12 weekly periods across Plant_A, Plant_B, and Plant_C. > > **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations. The predictive stage reads the bundled pre-loaded failure predictions by default (no training step); a live GNN is the opt-in alternative. @@ -177,4 +177,4 @@ Baseline solve is **OPTIMAL**: all **50 machines scheduled across periods 1–10 ## Data -Bundled CSVs in `data/` (real `MANUFACTURING.PUBLIC`): 50 machines (3 plants, 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. The five-stage script loads the first eight; the rest support the extensions in the README. All stages run in `machine_maintenance.py`. +Bundled CSVs in `data/`: 50 machines (3 plants, 5 types), 20 technicians, 32 qualifications, 8 products, 120 machine-product capabilities, 844 production runs, 353 downtime events, 600 failure predictions, 200 sensors / 2,400 sensor readings, plus travel, training options, availability, and degradation references. The five-stage script loads the first eight; the rest support the extensions in the README. All stages run in `machine_maintenance.py`. From 8521601c27d052fc974f70be13fe7a4c8bf75846 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 29 Jun 2026 10:11:43 -0700 Subject: [PATCH 12/12] Make Stage 4 (predictive) cleanly optional via PREDICTIVE_MODE Replace the USE_PRELOADED_PREDICTIONS boolean (which only toggled preloaded-vs-crash) with a three-way PREDICTIVE_MODE: "preloaded" (default) -- read the bundled failure_predictions, no training "skip" -- omit the stage entirely "live" -- wire a GNN over sensor/downtime history Skipping leaves the rest of the chain unchanged: the Stage 5 schedule reads per-machine failure_probability from the machine records, not these forward predictions. Verified against the live engine -- skip yields an identical schedule (baseline OPTIMAL 199.032; T001 what-if OPTIMAL 169.971, drops the four Plant_A Turbines). Invalid modes fail fast with a clear error. Document the skip path in the README predictive section and runbook step 10, and fix the Quickstart sample-output header to match actual stdout. --- v1/machine_maintenance/README.md | 12 +++--- v1/machine_maintenance/machine_maintenance.py | 42 ++++++++++++------- v1/machine_maintenance/runbook.md | 6 +-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/v1/machine_maintenance/README.md b/v1/machine_maintenance/README.md index 5169afe6..ce891256 100644 --- a/v1/machine_maintenance/README.md +++ b/v1/machine_maintenance/README.md @@ -40,7 +40,7 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c - Querying-stage metrics: OEE by plant, downtime by fault and plant, waste rates, technician coverage - A per-machine `risk_tier` derived from business rules - A betweenness-centrality bottleneck ranking over the machine-product graph -- A forward failure-risk ranking from the bundled predictions (or a live GNN) +- A forward failure-risk ranking from the bundled predictions (skippable, or a live GNN) - A preventive-maintenance schedule plus a technician-availability what-if ## What's included @@ -95,7 +95,7 @@ Readers are assumed comfortable reading Python; domain terms (OEE, betweenness c Each stage prints its findings. The first lines look like: ```text - -- Q1: OEE by plant -- + -- OEE by plant -- Plant_C: availability 97.7% performance 81.7% quality 98.2% OEE 78.3% ``` @@ -272,10 +272,12 @@ prod_graph.Node.bottleneck_raw = prod_graph.betweenness_centrality() ``` ### 4. Predictive -Forward failure risk comes from the bundled pre-computed `failure_predictions` by default — no training step — so the predictive question is answerable out of the box. Set `USE_PRELOADED_PREDICTIONS = False` to wire a live GNN over the sensor and downtime history instead (see _Customize_): +Forward failure risk comes from the bundled pre-computed `failure_predictions` by default — no training step — so the predictive question is answerable out of the box. The stage is controlled by `PREDICTIVE_MODE` at the top of the script: `"preloaded"` (default), `"skip"` to omit the stage entirely, or `"live"` to wire a GNN over the sensor and downtime history (see _Customize_). Nothing downstream depends on this stage — the Stage 5 schedule reads each machine's own `failure_probability`, not these forward predictions — so `"skip"` leaves every other result unchanged: ```python -if USE_PRELOADED_PREDICTIONS: +PREDICTIVE_MODE = "preloaded" # "preloaded" | "skip" | "live" +... +elif PREDICTIVE_MODE == "preloaded": fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( FailurePrediction.machine_id_str.alias("machine_id"), FailurePrediction.predicted_failure_mode.alias("mode"), @@ -306,7 +308,7 @@ Replace the CSVs in `data/` with your own machines, technicians, production, and The thresholds at the top of `machine_maintenance.py` — period horizon, per-period bay limit, chronic/high-risk/overdue cutoffs — are constants you can adjust to your operation. ### Extend the model -Add reasoners or stages: cluster machines by shared technicians, train a GNN on the sensor/downtime history for failure prediction (set `USE_PRELOADED_PREDICTIONS = False`), or add cross-training recommendations from `training_options` to relieve the coverage bottlenecks the what-if surfaces. +Add reasoners or stages: cluster machines by shared technicians, train a GNN on the sensor/downtime history for failure prediction (set `PREDICTIVE_MODE = "live"`), or add cross-training recommendations from `training_options` to relieve the coverage bottlenecks the what-if surfaces. ### Scale up / productionize - Swap the `data/` CSV bundle for `model.Table(...)` reads against your Snowflake tables — the concept definitions stay the same — so the model runs on live operational data. diff --git a/v1/machine_maintenance/machine_maintenance.py b/v1/machine_maintenance/machine_maintenance.py index 5c0886a2..4c7c9e1a 100644 --- a/v1/machine_maintenance/machine_maintenance.py +++ b/v1/machine_maintenance/machine_maintenance.py @@ -7,8 +7,8 @@ technician coverage. Stage 2 Rules -- per-machine risk tier (chronic / high-risk / overdue). Stage 3 Graph -- machine-product producibility bottlenecks. - Stage 4 Predictive -- forward failure risk & mode (pre-loaded predictions; - live GNN optional). + Stage 4 Predictive -- forward failure risk & mode (pre-loaded by default; + skippable, or wire a live GNN). Stage 5 Prescriptive -- preventive-maintenance schedule + a technician what-if. Data is the bundled sample in data/*.csv. @@ -40,8 +40,17 @@ HIGH_RISK_FP = 0.20 # failure-probability cutoff for high-risk HIGH_RISK_CRITICALITY = 4 # criticality cutoff for high-risk OVERDUE_RUL = 9 # remaining-useful-life at/below which maintenance is overdue -USE_PRELOADED_PREDICTIONS = True # predictive stage: read the bundled failure_predictions -# (default); set False to wire a live GNN -- see README "Customize" + +# Predictive stage (Stage 4) -- how forward failure risk is produced: +# "preloaded" (default) -- read the bundled failure_predictions; answerable, no training step. +# "skip" -- omit the stage entirely; the rest of the pipeline (incl. the Stage 5 +# schedule) is unaffected, since it reads per-machine failure_probability +# from the machine records, not these forward predictions. +# "live" -- train a GNN over sensor/downtime history (see README "Customize"). +PREDICTIVE_MODE = "preloaded" +_VALID_PREDICTIVE_MODES = ("preloaded", "skip", "live") +if PREDICTIVE_MODE not in _VALID_PREDICTIVE_MODES: + raise ValueError(f"PREDICTIVE_MODE must be one of {_VALID_PREDICTIVE_MODES}, got {PREDICTIVE_MODE!r}") model = Model("machine_maintenance") @@ -406,14 +415,19 @@ def banner(text): # Stage 4: Predictive -- forward failure risk # ================================================================== # -# Ships with pre-loaded predictions (failure_predictions.csv) so the predictive -# question is answerable out of the box, with no training step. To run a live model -# instead, set USE_PRELOADED_PREDICTIONS = False and train a GNN over the sensor and -# downtime history (see README "Customize" and the rai-predictive-* skills). +# PREDICTIVE_MODE (set at the top of this file) selects how the stage runs: +# "preloaded" (default) -- read the bundled failure_predictions; no training step. +# "skip" -- omit the stage; nothing downstream depends on it, so the +# schedule in Stage 5 is identical with or without it. +# "live" -- train a GNN over sensor/downtime history (see README). banner("STAGE 4 Predictive") -if USE_PRELOADED_PREDICTIONS: +if PREDICTIVE_MODE == "skip": + print("\n (predictive stage skipped -- PREDICTIVE_MODE = 'skip')") + print(" Nothing downstream depends on it: the Stage 5 schedule reads per-machine") + print(" failure_probability from the machine records, not these forward predictions.") +elif PREDICTIVE_MODE == "preloaded": fail_p12 = model.where(FailurePrediction.period_int == PERIOD_HORIZON).select( FailurePrediction.machine_id_str.alias("machine_id"), FailurePrediction.predicted_failure_mode.alias("mode"), @@ -422,12 +436,12 @@ def banner(text): print(f"\n-- Most likely to fail by period {PERIOD_HORIZON} (pre-loaded predictions) --") for _, row in fail_p12.head(5).iterrows(): print(f" {row['machine_id']} {row['mode']} ({row['fp'] * 100:.1f}%)") -else: +else: # "live" raise NotImplementedError( - "Live GNN path is not wired in this template. Either set " - "USE_PRELOADED_PREDICTIONS = True to use the bundled predictions, or train a " - "GNN over sensor/downtime history and write FailurePrediction " - "(see README 'Customize' and the rai-predictive-modeling / -training skills)." + "Live GNN path is not wired in this template. Set PREDICTIVE_MODE = 'preloaded' to " + "use the bundled predictions, 'skip' to omit the stage, or train a GNN over " + "sensor/downtime history and write FailurePrediction (see README 'Customize' and " + "the rai-predictive-modeling / -training skills)." ) diff --git a/v1/machine_maintenance/runbook.md b/v1/machine_maintenance/runbook.md index e59d0943..752235d3 100644 --- a/v1/machine_maintenance/runbook.md +++ b/v1/machine_maintenance/runbook.md @@ -4,7 +4,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa > **Data provenance.** Every figure below is computed from the bundled `data/*.csv` — a sample manufacturing dataset: 50 machines, 20 technicians, 8 products, and 12 weekly periods across Plant_A, Plant_B, and Plant_C. > -> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations. The predictive stage reads the bundled pre-loaded failure predictions by default (no training step); a live GNN is the opt-in alternative. +> **Status.** Every figure below comes from a real run of `machine_maintenance.py` against the bundled data — no predicted numbers. Querying and rules are deterministic; the graph and prescriptive stages use the template's own sound formulations. The predictive stage reads the bundled pre-loaded failure predictions by default (no training step); it can be skipped entirely (`PREDICTIVE_MODE = "skip"`) or swapped for a live GNN, and skipping it leaves every other figure unchanged. ## The chain @@ -24,7 +24,7 @@ Schedules preventive maintenance for a **50-machine, 3-plant, 12-period** manufa /rai-graph-analysis Pumps & Motors bridge the most product lines. ───────────────────────────────────────────────────────────────── STAGE 4 Predictive ──► Forward failure risk & mode, 12 periods - /rai-predictive-modeling Pre-loaded by default; live GNN optional. + /rai-predictive-modeling Pre-loaded by default; skippable or live GNN. M016 / M028 / M011 ≈ 42% by period 12. ───────────────────────────────────────────────────────────────── STAGE 5 Prescriptive ──► Maintenance schedule + technician what-if @@ -161,7 +161,7 @@ The bipartite graph has 58 nodes (50 machines + 8 products) and 120 edges. Betwe **Response** -By default the template reads the bundled pre-loaded `failure_predictions` (no training step), so the answer is deterministic — by period 12 the highest-risk machines are M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). To run it live instead, set `USE_PRELOADED_PREDICTIONS = False` and train a GNN over the sensor and downtime history (see _Customize_ in the README and the rai-predictive-modeling / rai-predictive-training skills). +By default the template reads the bundled pre-loaded `failure_predictions` (no training step), so the answer is deterministic — by period 12 the highest-risk machines are M016 valve_stuck (42.0%), M028 seal_leak (42.0%), M011 valve_stuck (42.0%), M012 valve_stuck (41.5%), M047 motor_burnout (35.4%). This stage is **optional**: set `PREDICTIVE_MODE = "skip"` to omit it entirely — the schedule in the next step is unchanged, because it reads each machine's own `failure_probability`, not these forward predictions. To run it live instead, set `PREDICTIVE_MODE = "live"` and train a GNN over the sensor and downtime history (see _Customize_ in the README and the rai-predictive-modeling / rai-predictive-training skills). ### 11. Schedule preventive maintenance + stress-test