From 0663b94f32cfc8caac64e9e1620a80f679d2b379 Mon Sep 17 00:00:00 2001 From: cafzal Date: Mon, 22 Jun 2026 10:26:25 -0700 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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)_