alirezaaminzadeh commited on
Commit
5039b7a
·
verified ·
1 Parent(s): 3674b40

Deploy FieldOps Service Parts & Field Operations Optimizer

Browse files
README.md CHANGED
@@ -1,10 +1,14 @@
1
- ---
2
- title: Service Parts Field Operations Optimizer
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
+ ---
2
+ title: FieldOps — Service Parts & Field Operations Optimizer
3
+ emoji: 🛠️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: static
7
+ pinned: false
8
+ license: apache-2.0
9
+ short_description: Field service parts, routing & inventory optimizer
10
+ ---
11
+
12
+ # FieldOps Static Explorer
13
+
14
+ Pre-computed optimization results from the FieldOps predict-then-optimize engine.
assets/demo/benchmarks.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "industry": "industrial_equipment",
4
+ "stress": "baseline",
5
+ "algorithm": "two_stage_stochastic",
6
+ "sla_compliance_pct": 100.0,
7
+ "mttr_hours": 4.31,
8
+ "ftf_rate_pct": 100.0,
9
+ "logistics_cost": 597.53,
10
+ "downtime_cost": 17622.45,
11
+ "solve_time_sec": 0.011503599991556257
12
+ },
13
+ {
14
+ "industry": "medical_equipment",
15
+ "stress": "emergency_surge",
16
+ "algorithm": "two_stage_stochastic",
17
+ "sla_compliance_pct": 0.0,
18
+ "mttr_hours": 4.94,
19
+ "ftf_rate_pct": 66.7,
20
+ "logistics_cost": 284.58,
21
+ "downtime_cost": 44212.55,
22
+ "solve_time_sec": 0.006099400023231283
23
+ },
24
+ {
25
+ "industry": "telecom",
26
+ "stress": "parts_shortage",
27
+ "algorithm": "two_stage_stochastic",
28
+ "sla_compliance_pct": 90.0,
29
+ "mttr_hours": 4.2,
30
+ "ftf_rate_pct": 80.0,
31
+ "logistics_cost": 1170.53,
32
+ "downtime_cost": 58191.35,
33
+ "solve_time_sec": 0.006857800006400794
34
+ },
35
+ {
36
+ "industry": "data_center",
37
+ "stress": "technician_shortage",
38
+ "algorithm": "two_stage_stochastic",
39
+ "sla_compliance_pct": 0.0,
40
+ "mttr_hours": 4.61,
41
+ "ftf_rate_pct": 83.3,
42
+ "logistics_cost": 217.76,
43
+ "downtime_cost": 38117.22,
44
+ "solve_time_sec": 0.0037087000091560185
45
+ },
46
+ {
47
+ "industry": "elevators",
48
+ "stress": "peak_season",
49
+ "algorithm": "two_stage_stochastic",
50
+ "sla_compliance_pct": 11.1,
51
+ "mttr_hours": 4.71,
52
+ "ftf_rate_pct": 77.8,
53
+ "logistics_cost": 566.36,
54
+ "downtime_cost": 25018.85,
55
+ "solve_time_sec": 0.00974899998982437
56
+ }
57
+ ]
assets/demo/summary.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project": "FieldOps",
3
+ "version": "1.0.0",
4
+ "scenarios": 5,
5
+ "algorithms": 7,
6
+ "primary_metrics": [
7
+ "sla_compliance_pct",
8
+ "mean_time_to_repair_hours",
9
+ "first_time_fix_rate_pct",
10
+ "emergency_stockout_count",
11
+ "technician_utilization_pct",
12
+ "total_logistics_cost",
13
+ "total_downtime_cost"
14
+ ]
15
+ }
assets/samples/sample_data_center.json ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "industry": "data_center",
3
+ "stress": "technician_shortage",
4
+ "scenario_label": "Data Center \u2014 Technician Shortage",
5
+ "summary": {
6
+ "sites": 6,
7
+ "technicians": 5,
8
+ "warehouses": 2,
9
+ "requests": 6
10
+ },
11
+ "optimization": {
12
+ "strategy": "two_stage_stochastic",
13
+ "algorithm": "two_stage_stochastic_programming",
14
+ "stage1_plan": {
15
+ "technician_positions": {
16
+ "T-01": [
17
+ 50.97447733372627,
18
+ 11.76305897995824
19
+ ],
20
+ "T-02": [
21
+ 50.97447733372627,
22
+ 11.76305897995824
23
+ ],
24
+ "T-03": [
25
+ 50.97447733372627,
26
+ 11.76305897995824
27
+ ],
28
+ "T-04": [
29
+ 50.97447733372627,
30
+ 11.76305897995824
31
+ ],
32
+ "T-05": [
33
+ 50.97447733372627,
34
+ 11.76305897995824
35
+ ]
36
+ },
37
+ "inventory_targets": [
38
+ {
39
+ "warehouse_id": "WH-01",
40
+ "part_id": "P-005",
41
+ "quantity": 5,
42
+ "reorder_point": 2,
43
+ "safety_stock": 2
44
+ },
45
+ {
46
+ "warehouse_id": "WH-01",
47
+ "part_id": "P-001",
48
+ "quantity": 2,
49
+ "reorder_point": 1,
50
+ "safety_stock": 1
51
+ },
52
+ {
53
+ "warehouse_id": "WH-02",
54
+ "part_id": "P-005",
55
+ "quantity": 5,
56
+ "reorder_point": 2,
57
+ "safety_stock": 2
58
+ },
59
+ {
60
+ "warehouse_id": "WH-02",
61
+ "part_id": "P-001",
62
+ "quantity": 2,
63
+ "reorder_point": 1,
64
+ "safety_stock": 1
65
+ }
66
+ ],
67
+ "supply_contracts": [
68
+ {
69
+ "warehouse": "WH-01",
70
+ "part": "P-001",
71
+ "min_order_qty": 5,
72
+ "lead_time_days": 5,
73
+ "expedite_available": true
74
+ },
75
+ {
76
+ "warehouse": "WH-01",
77
+ "part": "P-002",
78
+ "min_order_qty": 5,
79
+ "lead_time_days": 5,
80
+ "expedite_available": true
81
+ },
82
+ {
83
+ "warehouse": "WH-01",
84
+ "part": "P-003",
85
+ "min_order_qty": 5,
86
+ "lead_time_days": 5,
87
+ "expedite_available": true
88
+ },
89
+ {
90
+ "warehouse": "WH-02",
91
+ "part": "P-001",
92
+ "min_order_qty": 5,
93
+ "lead_time_days": 5,
94
+ "expedite_available": true
95
+ },
96
+ {
97
+ "warehouse": "WH-02",
98
+ "part": "P-002",
99
+ "min_order_qty": 5,
100
+ "lead_time_days": 5,
101
+ "expedite_available": true
102
+ },
103
+ {
104
+ "warehouse": "WH-02",
105
+ "part": "P-003",
106
+ "min_order_qty": 5,
107
+ "lead_time_days": 5,
108
+ "expedite_available": true
109
+ }
110
+ ],
111
+ "repair_shop_capacity": 2,
112
+ "total_holding_cost": 1765.0866473281349,
113
+ "notes": [
114
+ "Stock vs expedite ratio: 50% (higher = more pre-positioned inventory).",
115
+ "Multi-echelon targets across 2 warehouses.",
116
+ "Estimated holding cost: $1,765 over 7 days."
117
+ ]
118
+ },
119
+ "assignments": [
120
+ {
121
+ "request_id": "SR-0004",
122
+ "technician_id": "T-01",
123
+ "technician_name": "Tech A",
124
+ "warehouse_id": "WH-01",
125
+ "part_id": "P-005",
126
+ "part_source": "deferred",
127
+ "route_order": 1,
128
+ "scheduled_start_hours": 0.0,
129
+ "estimated_arrival_hours": 0.9399564376072906,
130
+ "estimated_completion_hours": 4.18995643760729,
131
+ "use_substitute": false,
132
+ "defer_to_later": true,
133
+ "rationale": "Scheduled \u2014 skill match 93%, part via deferred (deferred to next window)"
134
+ },
135
+ {
136
+ "request_id": "SR-0005",
137
+ "technician_id": "T-01",
138
+ "technician_name": "Tech A",
139
+ "warehouse_id": "WH-02",
140
+ "part_id": "P-001",
141
+ "part_source": "deferred",
142
+ "route_order": 2,
143
+ "scheduled_start_hours": 4.015667316280312,
144
+ "estimated_arrival_hours": 11.388180930767685,
145
+ "estimated_completion_hours": 15.988180930767685,
146
+ "use_substitute": false,
147
+ "defer_to_later": true,
148
+ "rationale": "Scheduled \u2014 skill match 93%, part via deferred (deferred to next window)"
149
+ },
150
+ {
151
+ "request_id": "SR-0003",
152
+ "technician_id": "T-01",
153
+ "technician_name": "Tech A",
154
+ "warehouse_id": "WH-01",
155
+ "part_id": "P-005",
156
+ "part_source": "warehouse_stock",
157
+ "route_order": 3,
158
+ "scheduled_start_hours": 15.287192016851948,
159
+ "estimated_arrival_hours": 22.408510752185045,
160
+ "estimated_completion_hours": 25.288510752185044,
161
+ "use_substitute": false,
162
+ "defer_to_later": false,
163
+ "rationale": "Emergency \u2014 skill match 102%, part via warehouse_stock"
164
+ },
165
+ {
166
+ "request_id": "SR-0001",
167
+ "technician_id": "T-01",
168
+ "technician_name": "Tech A",
169
+ "warehouse_id": "WH-01",
170
+ "part_id": "P-001",
171
+ "part_source": "warehouse_stock",
172
+ "route_order": 4,
173
+ "scheduled_start_hours": 18.16719201685195,
174
+ "estimated_arrival_hours": 25.288510752185044,
175
+ "estimated_completion_hours": 28.828510752185043,
176
+ "use_substitute": false,
177
+ "defer_to_later": false,
178
+ "rationale": "Scheduled \u2014 skill match 93%, part via warehouse_stock"
179
+ },
180
+ {
181
+ "request_id": "SR-0006",
182
+ "technician_id": "T-02",
183
+ "technician_name": "Tech B",
184
+ "warehouse_id": "WH-01",
185
+ "part_id": "P-005",
186
+ "part_source": "warehouse_stock",
187
+ "route_order": 1,
188
+ "scheduled_start_hours": 0.0,
189
+ "estimated_arrival_hours": 4.603596172719693,
190
+ "estimated_completion_hours": 7.813596172719693,
191
+ "use_substitute": false,
192
+ "defer_to_later": false,
193
+ "rationale": "Scheduled \u2014 skill match 99%, part via warehouse_stock"
194
+ },
195
+ {
196
+ "request_id": "SR-0002",
197
+ "technician_id": "T-02",
198
+ "technician_name": "Tech B",
199
+ "warehouse_id": "WH-01",
200
+ "part_id": "P-005",
201
+ "part_source": "warehouse_stock",
202
+ "route_order": 2,
203
+ "scheduled_start_hours": 3.2100630923173323,
204
+ "estimated_arrival_hours": 10.642771892204166,
205
+ "estimated_completion_hours": 12.392771892204166,
206
+ "use_substitute": false,
207
+ "defer_to_later": false,
208
+ "rationale": "Scheduled \u2014 skill match 99%, part via warehouse_stock"
209
+ }
210
+ ],
211
+ "ml_predictions": [
212
+ {
213
+ "request_id": "SR-0001",
214
+ "site_id": "S-004",
215
+ "failure_probability": 0.2315,
216
+ "predicted_part_type": "motor_drive",
217
+ "predicted_duration_hours": 3.54,
218
+ "first_time_fix_probability": 0.7326,
219
+ "revisit_probability": 0.2273,
220
+ "confidence": 0.8963,
221
+ "risk_tier": "low"
222
+ },
223
+ {
224
+ "request_id": "SR-0002",
225
+ "site_id": "S-001",
226
+ "failure_probability": 0.0879,
227
+ "predicted_part_type": "communication_card",
228
+ "predicted_duration_hours": 1.75,
229
+ "first_time_fix_probability": 0.8723,
230
+ "revisit_probability": 0.1086,
231
+ "confidence": 0.8676,
232
+ "risk_tier": "low"
233
+ },
234
+ {
235
+ "request_id": "SR-0003",
236
+ "site_id": "S-004",
237
+ "failure_probability": 0.4256,
238
+ "predicted_part_type": "motor_drive",
239
+ "predicted_duration_hours": 2.88,
240
+ "first_time_fix_probability": 0.7851,
241
+ "revisit_probability": 0.1827,
242
+ "confidence": 0.9351,
243
+ "risk_tier": "medium"
244
+ },
245
+ {
246
+ "request_id": "SR-0004",
247
+ "site_id": "S-001",
248
+ "failure_probability": 0.098,
249
+ "predicted_part_type": "motor_drive",
250
+ "predicted_duration_hours": 3.25,
251
+ "first_time_fix_probability": 0.812,
252
+ "revisit_probability": 0.1598,
253
+ "confidence": 0.8696,
254
+ "risk_tier": "low"
255
+ },
256
+ {
257
+ "request_id": "SR-0005",
258
+ "site_id": "S-002",
259
+ "failure_probability": 0.1279,
260
+ "predicted_part_type": "motor_drive",
261
+ "predicted_duration_hours": 4.6,
262
+ "first_time_fix_probability": 0.691,
263
+ "revisit_probability": 0.2626,
264
+ "confidence": 0.8756,
265
+ "risk_tier": "low"
266
+ },
267
+ {
268
+ "request_id": "SR-0006",
269
+ "site_id": "S-003",
270
+ "failure_probability": 0.1347,
271
+ "predicted_part_type": "motor_drive",
272
+ "predicted_duration_hours": 3.21,
273
+ "first_time_fix_probability": 0.809,
274
+ "revisit_probability": 0.1624,
275
+ "confidence": 0.8769,
276
+ "risk_tier": "low"
277
+ }
278
+ ],
279
+ "metrics": {
280
+ "sla_compliance_pct": 0.0,
281
+ "mean_time_to_repair_hours": 4.61,
282
+ "first_time_fix_rate_pct": 83.3,
283
+ "emergency_stockout_count": 0,
284
+ "technician_utilization_pct": 33.3,
285
+ "total_logistics_cost": 217.76,
286
+ "total_downtime_cost": 38117.22,
287
+ "total_holding_cost": 1765.09,
288
+ "expedite_shipments": 0
289
+ },
290
+ "simulation": {
291
+ "replications": 30,
292
+ "sla_compliance_mean": 0.0,
293
+ "mttr_mean": 4.68,
294
+ "stockout_rate": 0.0,
295
+ "avg_logistics_cost": 222.06,
296
+ "avg_downtime_cost": 45215.93,
297
+ "confidence_interval_95": {
298
+ "sla_compliance": [
299
+ 0.0,
300
+ 0.0
301
+ ],
302
+ "mttr": [
303
+ 4.54,
304
+ 4.82
305
+ ],
306
+ "logistics_cost": [
307
+ 218.66,
308
+ 225.45
309
+ ]
310
+ }
311
+ },
312
+ "solve_time_sec": 0.0037087000091560185,
313
+ "status": "optimal",
314
+ "notes": [
315
+ "Two-stage stochastic: 20 SAA scenarios evaluated.",
316
+ "Stage 1: multi-echelon inventory + technician positioning.",
317
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
318
+ "Monte Carlo (30 runs): SLA 0.0%, MTTR 4.7h."
319
+ ]
320
+ }
321
+ }
assets/samples/sample_elevators.json ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "industry": "elevators",
3
+ "stress": "peak_season",
4
+ "scenario_label": "Elevators & Lifts \u2014 Peak Season",
5
+ "summary": {
6
+ "sites": 18,
7
+ "technicians": 10,
8
+ "warehouses": 2,
9
+ "requests": 9
10
+ },
11
+ "optimization": {
12
+ "strategy": "two_stage_stochastic",
13
+ "algorithm": "two_stage_stochastic_programming",
14
+ "stage1_plan": {
15
+ "technician_positions": {
16
+ "T-01": [
17
+ 50.73799305374447,
18
+ 11.603656687362223
19
+ ],
20
+ "T-02": [
21
+ 50.73799305374447,
22
+ 11.603656687362223
23
+ ],
24
+ "T-03": [
25
+ 50.73799305374447,
26
+ 11.603656687362223
27
+ ],
28
+ "T-04": [
29
+ 50.73799305374447,
30
+ 11.603656687362223
31
+ ],
32
+ "T-05": [
33
+ 50.73799305374447,
34
+ 11.603656687362223
35
+ ],
36
+ "T-06": [
37
+ 50.73799305374447,
38
+ 11.603656687362223
39
+ ],
40
+ "T-07": [
41
+ 50.73799305374447,
42
+ 11.603656687362223
43
+ ],
44
+ "T-08": [
45
+ 50.73799305374447,
46
+ 11.603656687362223
47
+ ],
48
+ "T-09": [
49
+ 50.73799305374447,
50
+ 11.603656687362223
51
+ ],
52
+ "T-10": [
53
+ 50.73799305374447,
54
+ 11.603656687362223
55
+ ]
56
+ },
57
+ "inventory_targets": [
58
+ {
59
+ "warehouse_id": "WH-01",
60
+ "part_id": "P-002",
61
+ "quantity": 3,
62
+ "reorder_point": 1,
63
+ "safety_stock": 1
64
+ },
65
+ {
66
+ "warehouse_id": "WH-01",
67
+ "part_id": "P-001",
68
+ "quantity": 3,
69
+ "reorder_point": 1,
70
+ "safety_stock": 1
71
+ },
72
+ {
73
+ "warehouse_id": "WH-01",
74
+ "part_id": "P-003",
75
+ "quantity": 2,
76
+ "reorder_point": 1,
77
+ "safety_stock": 1
78
+ },
79
+ {
80
+ "warehouse_id": "WH-01",
81
+ "part_id": "P-004",
82
+ "quantity": 2,
83
+ "reorder_point": 1,
84
+ "safety_stock": 1
85
+ },
86
+ {
87
+ "warehouse_id": "WH-01",
88
+ "part_id": "P-006",
89
+ "quantity": 2,
90
+ "reorder_point": 1,
91
+ "safety_stock": 1
92
+ },
93
+ {
94
+ "warehouse_id": "WH-02",
95
+ "part_id": "P-002",
96
+ "quantity": 3,
97
+ "reorder_point": 1,
98
+ "safety_stock": 1
99
+ },
100
+ {
101
+ "warehouse_id": "WH-02",
102
+ "part_id": "P-001",
103
+ "quantity": 3,
104
+ "reorder_point": 1,
105
+ "safety_stock": 1
106
+ },
107
+ {
108
+ "warehouse_id": "WH-02",
109
+ "part_id": "P-003",
110
+ "quantity": 2,
111
+ "reorder_point": 1,
112
+ "safety_stock": 1
113
+ },
114
+ {
115
+ "warehouse_id": "WH-02",
116
+ "part_id": "P-004",
117
+ "quantity": 2,
118
+ "reorder_point": 1,
119
+ "safety_stock": 1
120
+ },
121
+ {
122
+ "warehouse_id": "WH-02",
123
+ "part_id": "P-006",
124
+ "quantity": 2,
125
+ "reorder_point": 1,
126
+ "safety_stock": 1
127
+ }
128
+ ],
129
+ "supply_contracts": [
130
+ {
131
+ "warehouse": "WH-01",
132
+ "part": "P-001",
133
+ "min_order_qty": 5,
134
+ "lead_time_days": 5,
135
+ "expedite_available": true
136
+ },
137
+ {
138
+ "warehouse": "WH-01",
139
+ "part": "P-002",
140
+ "min_order_qty": 5,
141
+ "lead_time_days": 5,
142
+ "expedite_available": true
143
+ },
144
+ {
145
+ "warehouse": "WH-01",
146
+ "part": "P-003",
147
+ "min_order_qty": 5,
148
+ "lead_time_days": 5,
149
+ "expedite_available": true
150
+ },
151
+ {
152
+ "warehouse": "WH-02",
153
+ "part": "P-001",
154
+ "min_order_qty": 5,
155
+ "lead_time_days": 5,
156
+ "expedite_available": true
157
+ },
158
+ {
159
+ "warehouse": "WH-02",
160
+ "part": "P-002",
161
+ "min_order_qty": 5,
162
+ "lead_time_days": 5,
163
+ "expedite_available": true
164
+ },
165
+ {
166
+ "warehouse": "WH-02",
167
+ "part": "P-003",
168
+ "min_order_qty": 5,
169
+ "lead_time_days": 5,
170
+ "expedite_available": true
171
+ }
172
+ ],
173
+ "repair_shop_capacity": 3,
174
+ "total_holding_cost": 2129.891162348705,
175
+ "notes": [
176
+ "Stock vs expedite ratio: 50% (higher = more pre-positioned inventory).",
177
+ "Multi-echelon targets across 2 warehouses.",
178
+ "Estimated holding cost: $2,130 over 7 days."
179
+ ]
180
+ },
181
+ "assignments": [
182
+ {
183
+ "request_id": "SR-0002",
184
+ "technician_id": "T-02",
185
+ "technician_name": "Tech B",
186
+ "warehouse_id": "WH-01",
187
+ "part_id": "P-002",
188
+ "part_source": "warehouse_stock",
189
+ "route_order": 1,
190
+ "scheduled_start_hours": 0.0,
191
+ "estimated_arrival_hours": 4.471727329434108,
192
+ "estimated_completion_hours": 9.051727329434108,
193
+ "use_substitute": false,
194
+ "defer_to_later": false,
195
+ "rationale": "Scheduled \u2014 skill match 99%, part via warehouse_stock"
196
+ },
197
+ {
198
+ "request_id": "SR-0004",
199
+ "technician_id": "T-02",
200
+ "technician_name": "Tech B",
201
+ "warehouse_id": "WH-01",
202
+ "part_id": "P-001",
203
+ "part_source": "deferred",
204
+ "route_order": 2,
205
+ "scheduled_start_hours": 8.218847521029023,
206
+ "estimated_arrival_hours": 16.112410451298178,
207
+ "estimated_completion_hours": 20.562410451298177,
208
+ "use_substitute": false,
209
+ "defer_to_later": true,
210
+ "rationale": "Scheduled \u2014 skill match 99%, part via deferred (deferred to next window)"
211
+ },
212
+ {
213
+ "request_id": "SR-0009",
214
+ "technician_id": "T-02",
215
+ "technician_name": "Tech B",
216
+ "warehouse_id": "WH-02",
217
+ "part_id": "P-006",
218
+ "part_source": "warehouse_stock",
219
+ "route_order": 3,
220
+ "scheduled_start_hours": 17.328286148354728,
221
+ "estimated_arrival_hours": 23.120365200771747,
222
+ "estimated_completion_hours": 24.870365200771747,
223
+ "use_substitute": false,
224
+ "defer_to_later": false,
225
+ "rationale": "Scheduled \u2014 skill match 99%, part via warehouse_stock"
226
+ },
227
+ {
228
+ "request_id": "SR-0008",
229
+ "technician_id": "T-06",
230
+ "technician_name": "Tech F",
231
+ "warehouse_id": "WH-01",
232
+ "part_id": "P-004",
233
+ "part_source": "deferred",
234
+ "route_order": 1,
235
+ "scheduled_start_hours": 0.0,
236
+ "estimated_arrival_hours": 0.5885734974225917,
237
+ "estimated_completion_hours": 4.178573497422591,
238
+ "use_substitute": false,
239
+ "defer_to_later": true,
240
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
241
+ },
242
+ {
243
+ "request_id": "SR-0003",
244
+ "technician_id": "T-06",
245
+ "technician_name": "Tech F",
246
+ "warehouse_id": "WH-02",
247
+ "part_id": "P-001",
248
+ "part_source": "deferred",
249
+ "route_order": 2,
250
+ "scheduled_start_hours": 3.609688002162164,
251
+ "estimated_arrival_hours": 13.281694759502987,
252
+ "estimated_completion_hours": 16.961694759502986,
253
+ "use_substitute": false,
254
+ "defer_to_later": true,
255
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
256
+ },
257
+ {
258
+ "request_id": "SR-0001",
259
+ "technician_id": "T-09",
260
+ "technician_name": "Tech I",
261
+ "warehouse_id": "WH-01",
262
+ "part_id": "P-002",
263
+ "part_source": "warehouse_stock",
264
+ "route_order": 1,
265
+ "scheduled_start_hours": 0.0,
266
+ "estimated_arrival_hours": 0.9863209197037524,
267
+ "estimated_completion_hours": 4.576320919703752,
268
+ "use_substitute": false,
269
+ "defer_to_later": false,
270
+ "rationale": "Scheduled \u2014 skill match 94%, part via warehouse_stock"
271
+ }
272
+ ],
273
+ "ml_predictions": [
274
+ {
275
+ "request_id": "SR-0001",
276
+ "site_id": "S-009",
277
+ "failure_probability": 0.1978,
278
+ "predicted_part_type": "hydraulic_pump",
279
+ "predicted_duration_hours": 3.59,
280
+ "first_time_fix_probability": 0.7352,
281
+ "revisit_probability": 0.2251,
282
+ "confidence": 0.8896,
283
+ "risk_tier": "low"
284
+ },
285
+ {
286
+ "request_id": "SR-0002",
287
+ "site_id": "S-018",
288
+ "failure_probability": 0.1394,
289
+ "predicted_part_type": "sensor_module",
290
+ "predicted_duration_hours": 4.58,
291
+ "first_time_fix_probability": 0.6902,
292
+ "revisit_probability": 0.2633,
293
+ "confidence": 0.8779,
294
+ "risk_tier": "low"
295
+ },
296
+ {
297
+ "request_id": "SR-0003",
298
+ "site_id": "S-007",
299
+ "failure_probability": 0.1235,
300
+ "predicted_part_type": "hydraulic_pump",
301
+ "predicted_duration_hours": 3.68,
302
+ "first_time_fix_probability": 0.7407,
303
+ "revisit_probability": 0.2204,
304
+ "confidence": 0.8747,
305
+ "risk_tier": "low"
306
+ },
307
+ {
308
+ "request_id": "SR-0004",
309
+ "site_id": "S-009",
310
+ "failure_probability": 0.221,
311
+ "predicted_part_type": "sensor_module",
312
+ "predicted_duration_hours": 4.45,
313
+ "first_time_fix_probability": 0.6845,
314
+ "revisit_probability": 0.2681,
315
+ "confidence": 0.8942,
316
+ "risk_tier": "low"
317
+ },
318
+ {
319
+ "request_id": "SR-0005",
320
+ "site_id": "S-010",
321
+ "failure_probability": 0.4651,
322
+ "predicted_part_type": "hydraulic_pump",
323
+ "predicted_duration_hours": 3.24,
324
+ "first_time_fix_probability": 0.7151,
325
+ "revisit_probability": 0.2421,
326
+ "confidence": 0.943,
327
+ "risk_tier": "medium"
328
+ },
329
+ {
330
+ "request_id": "SR-0006",
331
+ "site_id": "S-002",
332
+ "failure_probability": 0.287,
333
+ "predicted_part_type": "bearing_kit",
334
+ "predicted_duration_hours": 3.04,
335
+ "first_time_fix_probability": 0.7965,
336
+ "revisit_probability": 0.173,
337
+ "confidence": 0.9074,
338
+ "risk_tier": "low"
339
+ },
340
+ {
341
+ "request_id": "SR-0007",
342
+ "site_id": "S-017",
343
+ "failure_probability": 0.6365,
344
+ "predicted_part_type": "hydraulic_pump",
345
+ "predicted_duration_hours": 3.03,
346
+ "first_time_fix_probability": 0.7023,
347
+ "revisit_probability": 0.2531,
348
+ "confidence": 0.9227,
349
+ "risk_tier": "high"
350
+ },
351
+ {
352
+ "request_id": "SR-0008",
353
+ "site_id": "S-008",
354
+ "failure_probability": 0.1978,
355
+ "predicted_part_type": "communication_card",
356
+ "predicted_duration_hours": 3.59,
357
+ "first_time_fix_probability": 0.7352,
358
+ "revisit_probability": 0.2251,
359
+ "confidence": 0.8896,
360
+ "risk_tier": "low"
361
+ },
362
+ {
363
+ "request_id": "SR-0009",
364
+ "site_id": "S-014",
365
+ "failure_probability": 0.0903,
366
+ "predicted_part_type": "communication_card",
367
+ "predicted_duration_hours": 1.75,
368
+ "first_time_fix_probability": 0.8721,
369
+ "revisit_probability": 0.1088,
370
+ "confidence": 0.8681,
371
+ "risk_tier": "low"
372
+ }
373
+ ],
374
+ "metrics": {
375
+ "sla_compliance_pct": 11.1,
376
+ "mean_time_to_repair_hours": 4.71,
377
+ "first_time_fix_rate_pct": 77.8,
378
+ "emergency_stockout_count": 0,
379
+ "technician_utilization_pct": 22.5,
380
+ "total_logistics_cost": 566.36,
381
+ "total_downtime_cost": 25018.85,
382
+ "total_holding_cost": 2129.89,
383
+ "expedite_shipments": 0
384
+ },
385
+ "simulation": {
386
+ "replications": 30,
387
+ "sla_compliance_mean": 17.8,
388
+ "mttr_mean": 4.91,
389
+ "stockout_rate": 0.0,
390
+ "avg_logistics_cost": 462.09,
391
+ "avg_downtime_cost": 8404.96,
392
+ "confidence_interval_95": {
393
+ "sla_compliance": [
394
+ 16.3,
395
+ 19.31
396
+ ],
397
+ "mttr": [
398
+ 4.78,
399
+ 5.04
400
+ ],
401
+ "logistics_cost": [
402
+ 455.11,
403
+ 469.07
404
+ ]
405
+ }
406
+ },
407
+ "solve_time_sec": 0.00974899998982437,
408
+ "status": "optimal",
409
+ "notes": [
410
+ "Two-stage stochastic: 20 SAA scenarios evaluated.",
411
+ "Stage 1: multi-echelon inventory + technician positioning.",
412
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
413
+ "Monte Carlo (30 runs): SLA 17.8%, MTTR 4.9h."
414
+ ]
415
+ }
416
+ }
assets/samples/sample_industrial_equipment.json ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "industry": "industrial_equipment",
3
+ "stress": "baseline",
4
+ "scenario_label": "Industrial Equipment \u2014 Baseline Demand",
5
+ "summary": {
6
+ "sites": 12,
7
+ "technicians": 8,
8
+ "warehouses": 3,
9
+ "requests": 6
10
+ },
11
+ "optimization": {
12
+ "strategy": "two_stage_stochastic",
13
+ "algorithm": "two_stage_stochastic_programming",
14
+ "stage1_plan": {
15
+ "technician_positions": {
16
+ "T-01": [
17
+ 50.851132588450646,
18
+ 11.498955066615594
19
+ ],
20
+ "T-02": [
21
+ 50.851132588450646,
22
+ 11.498955066615594
23
+ ],
24
+ "T-03": [
25
+ 50.851132588450646,
26
+ 11.498955066615594
27
+ ],
28
+ "T-04": [
29
+ 50.851132588450646,
30
+ 11.498955066615594
31
+ ],
32
+ "T-05": [
33
+ 50.851132588450646,
34
+ 11.498955066615594
35
+ ],
36
+ "T-06": [
37
+ 50.851132588450646,
38
+ 11.498955066615594
39
+ ],
40
+ "T-07": [
41
+ 50.851132588450646,
42
+ 11.498955066615594
43
+ ],
44
+ "T-08": [
45
+ 50.851132588450646,
46
+ 11.498955066615594
47
+ ]
48
+ },
49
+ "inventory_targets": [
50
+ {
51
+ "warehouse_id": "WH-01",
52
+ "part_id": "P-005",
53
+ "quantity": 2,
54
+ "reorder_point": 1,
55
+ "safety_stock": 1
56
+ },
57
+ {
58
+ "warehouse_id": "WH-01",
59
+ "part_id": "P-004",
60
+ "quantity": 2,
61
+ "reorder_point": 1,
62
+ "safety_stock": 1
63
+ },
64
+ {
65
+ "warehouse_id": "WH-01",
66
+ "part_id": "P-003",
67
+ "quantity": 2,
68
+ "reorder_point": 1,
69
+ "safety_stock": 1
70
+ },
71
+ {
72
+ "warehouse_id": "WH-01",
73
+ "part_id": "P-002",
74
+ "quantity": 2,
75
+ "reorder_point": 1,
76
+ "safety_stock": 1
77
+ },
78
+ {
79
+ "warehouse_id": "WH-02",
80
+ "part_id": "P-005",
81
+ "quantity": 2,
82
+ "reorder_point": 1,
83
+ "safety_stock": 1
84
+ },
85
+ {
86
+ "warehouse_id": "WH-02",
87
+ "part_id": "P-004",
88
+ "quantity": 2,
89
+ "reorder_point": 1,
90
+ "safety_stock": 1
91
+ },
92
+ {
93
+ "warehouse_id": "WH-02",
94
+ "part_id": "P-003",
95
+ "quantity": 2,
96
+ "reorder_point": 1,
97
+ "safety_stock": 1
98
+ },
99
+ {
100
+ "warehouse_id": "WH-02",
101
+ "part_id": "P-002",
102
+ "quantity": 2,
103
+ "reorder_point": 1,
104
+ "safety_stock": 1
105
+ },
106
+ {
107
+ "warehouse_id": "WH-03",
108
+ "part_id": "P-005",
109
+ "quantity": 2,
110
+ "reorder_point": 1,
111
+ "safety_stock": 1
112
+ },
113
+ {
114
+ "warehouse_id": "WH-03",
115
+ "part_id": "P-004",
116
+ "quantity": 2,
117
+ "reorder_point": 1,
118
+ "safety_stock": 1
119
+ },
120
+ {
121
+ "warehouse_id": "WH-03",
122
+ "part_id": "P-003",
123
+ "quantity": 2,
124
+ "reorder_point": 1,
125
+ "safety_stock": 1
126
+ },
127
+ {
128
+ "warehouse_id": "WH-03",
129
+ "part_id": "P-002",
130
+ "quantity": 2,
131
+ "reorder_point": 1,
132
+ "safety_stock": 1
133
+ }
134
+ ],
135
+ "supply_contracts": [
136
+ {
137
+ "warehouse": "WH-01",
138
+ "part": "P-001",
139
+ "min_order_qty": 5,
140
+ "lead_time_days": 5,
141
+ "expedite_available": true
142
+ },
143
+ {
144
+ "warehouse": "WH-01",
145
+ "part": "P-002",
146
+ "min_order_qty": 5,
147
+ "lead_time_days": 5,
148
+ "expedite_available": true
149
+ },
150
+ {
151
+ "warehouse": "WH-01",
152
+ "part": "P-003",
153
+ "min_order_qty": 5,
154
+ "lead_time_days": 5,
155
+ "expedite_available": true
156
+ },
157
+ {
158
+ "warehouse": "WH-02",
159
+ "part": "P-001",
160
+ "min_order_qty": 5,
161
+ "lead_time_days": 5,
162
+ "expedite_available": true
163
+ },
164
+ {
165
+ "warehouse": "WH-02",
166
+ "part": "P-002",
167
+ "min_order_qty": 5,
168
+ "lead_time_days": 5,
169
+ "expedite_available": true
170
+ },
171
+ {
172
+ "warehouse": "WH-02",
173
+ "part": "P-003",
174
+ "min_order_qty": 5,
175
+ "lead_time_days": 5,
176
+ "expedite_available": true
177
+ },
178
+ {
179
+ "warehouse": "WH-03",
180
+ "part": "P-001",
181
+ "min_order_qty": 5,
182
+ "lead_time_days": 5,
183
+ "expedite_available": true
184
+ },
185
+ {
186
+ "warehouse": "WH-03",
187
+ "part": "P-002",
188
+ "min_order_qty": 5,
189
+ "lead_time_days": 5,
190
+ "expedite_available": true
191
+ },
192
+ {
193
+ "warehouse": "WH-03",
194
+ "part": "P-003",
195
+ "min_order_qty": 5,
196
+ "lead_time_days": 5,
197
+ "expedite_available": true
198
+ }
199
+ ],
200
+ "repair_shop_capacity": 2,
201
+ "total_holding_cost": 1644.4361424549948,
202
+ "notes": [
203
+ "Stock vs expedite ratio: 50% (higher = more pre-positioned inventory).",
204
+ "Multi-echelon targets across 3 warehouses.",
205
+ "Estimated holding cost: $1,644 over 7 days."
206
+ ]
207
+ },
208
+ "assignments": [
209
+ {
210
+ "request_id": "SR-0006",
211
+ "technician_id": "T-02",
212
+ "technician_name": "Tech B",
213
+ "warehouse_id": "WH-02",
214
+ "part_id": "P-002",
215
+ "part_source": "warehouse_stock",
216
+ "route_order": 1,
217
+ "scheduled_start_hours": 0.0,
218
+ "estimated_arrival_hours": 0.6553569009603153,
219
+ "estimated_completion_hours": 4.245356900960315,
220
+ "use_substitute": false,
221
+ "defer_to_later": false,
222
+ "rationale": "Scheduled \u2014 skill match 89%, part via warehouse_stock"
223
+ },
224
+ {
225
+ "request_id": "SR-0005",
226
+ "technician_id": "T-04",
227
+ "technician_name": "Tech D",
228
+ "warehouse_id": "WH-01",
229
+ "part_id": "P-003",
230
+ "part_source": "warehouse_stock",
231
+ "route_order": 1,
232
+ "scheduled_start_hours": 0.0,
233
+ "estimated_arrival_hours": 0.7832393948734134,
234
+ "estimated_completion_hours": 2.3932393948734134,
235
+ "use_substitute": false,
236
+ "defer_to_later": false,
237
+ "rationale": "Scheduled \u2014 skill match 95%, part via warehouse_stock"
238
+ },
239
+ {
240
+ "request_id": "SR-0002",
241
+ "technician_id": "T-04",
242
+ "technician_name": "Tech D",
243
+ "warehouse_id": "WH-01",
244
+ "part_id": "P-004",
245
+ "part_source": "deferred",
246
+ "route_order": 2,
247
+ "scheduled_start_hours": 1.674930030939441,
248
+ "estimated_arrival_hours": 6.747959255100246,
249
+ "estimated_completion_hours": 9.837959255100245,
250
+ "use_substitute": false,
251
+ "defer_to_later": true,
252
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
253
+ },
254
+ {
255
+ "request_id": "SR-0003",
256
+ "technician_id": "T-04",
257
+ "technician_name": "Tech D",
258
+ "warehouse_id": "WH-03",
259
+ "part_id": "P-001",
260
+ "part_source": "deferred",
261
+ "route_order": 3,
262
+ "scheduled_start_hours": 5.567661069863554,
263
+ "estimated_arrival_hours": 13.851400960258042,
264
+ "estimated_completion_hours": 17.441400960258044,
265
+ "use_substitute": false,
266
+ "defer_to_later": true,
267
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
268
+ },
269
+ {
270
+ "request_id": "SR-0001",
271
+ "technician_id": "T-04",
272
+ "technician_name": "Tech D",
273
+ "warehouse_id": "WH-02",
274
+ "part_id": "P-005",
275
+ "part_source": "warehouse_stock",
276
+ "route_order": 4,
277
+ "scheduled_start_hours": 15.758638309028454,
278
+ "estimated_arrival_hours": 24.623161217266052,
279
+ "estimated_completion_hours": 27.783161217266052,
280
+ "use_substitute": false,
281
+ "defer_to_later": false,
282
+ "rationale": "Scheduled \u2014 skill match 95%, part via warehouse_stock"
283
+ }
284
+ ],
285
+ "ml_predictions": [
286
+ {
287
+ "request_id": "SR-0001",
288
+ "site_id": "S-002",
289
+ "failure_probability": 0.1751,
290
+ "predicted_part_type": "motor_drive",
291
+ "predicted_duration_hours": 3.16,
292
+ "first_time_fix_probability": 0.8056,
293
+ "revisit_probability": 0.1652,
294
+ "confidence": 0.885,
295
+ "risk_tier": "low"
296
+ },
297
+ {
298
+ "request_id": "SR-0002",
299
+ "site_id": "S-010",
300
+ "failure_probability": 0.2405,
301
+ "predicted_part_type": "control_board",
302
+ "predicted_duration_hours": 3.09,
303
+ "first_time_fix_probability": 0.8003,
304
+ "revisit_probability": 0.1698,
305
+ "confidence": 0.8981,
306
+ "risk_tier": "low"
307
+ },
308
+ {
309
+ "request_id": "SR-0003",
310
+ "site_id": "S-011",
311
+ "failure_probability": 0.1978,
312
+ "predicted_part_type": "hydraulic_pump",
313
+ "predicted_duration_hours": 3.59,
314
+ "first_time_fix_probability": 0.7352,
315
+ "revisit_probability": 0.2251,
316
+ "confidence": 0.8896,
317
+ "risk_tier": "low"
318
+ },
319
+ {
320
+ "request_id": "SR-0004",
321
+ "site_id": "S-011",
322
+ "failure_probability": 0.3775,
323
+ "predicted_part_type": "hydraulic_pump",
324
+ "predicted_duration_hours": 3.36,
325
+ "first_time_fix_probability": 0.7217,
326
+ "revisit_probability": 0.2366,
327
+ "confidence": 0.9255,
328
+ "risk_tier": "medium"
329
+ },
330
+ {
331
+ "request_id": "SR-0005",
332
+ "site_id": "S-004",
333
+ "failure_probability": 0.3208,
334
+ "predicted_part_type": "hydraulic_pump",
335
+ "predicted_duration_hours": 1.61,
336
+ "first_time_fix_probability": 0.8518,
337
+ "revisit_probability": 0.126,
338
+ "confidence": 0.9142,
339
+ "risk_tier": "low"
340
+ },
341
+ {
342
+ "request_id": "SR-0006",
343
+ "site_id": "S-002",
344
+ "failure_probability": 0.1978,
345
+ "predicted_part_type": "hydraulic_pump",
346
+ "predicted_duration_hours": 3.59,
347
+ "first_time_fix_probability": 0.7352,
348
+ "revisit_probability": 0.2251,
349
+ "confidence": 0.8896,
350
+ "risk_tier": "low"
351
+ }
352
+ ],
353
+ "metrics": {
354
+ "sla_compliance_pct": 100.0,
355
+ "mean_time_to_repair_hours": 4.31,
356
+ "first_time_fix_rate_pct": 100.0,
357
+ "emergency_stockout_count": 0,
358
+ "technician_utilization_pct": 15.8,
359
+ "total_logistics_cost": 597.53,
360
+ "total_downtime_cost": 17622.45,
361
+ "total_holding_cost": 1644.44,
362
+ "expedite_shipments": 0
363
+ },
364
+ "simulation": {
365
+ "replications": 30,
366
+ "sla_compliance_mean": 100.0,
367
+ "mttr_mean": 4.33,
368
+ "stockout_rate": 0.0,
369
+ "avg_logistics_cost": 543.76,
370
+ "avg_downtime_cost": 14486.99,
371
+ "confidence_interval_95": {
372
+ "sla_compliance": [
373
+ 100.0,
374
+ 100.0
375
+ ],
376
+ "mttr": [
377
+ 4.2,
378
+ 4.46
379
+ ],
380
+ "logistics_cost": [
381
+ 535.44,
382
+ 552.07
383
+ ]
384
+ }
385
+ },
386
+ "solve_time_sec": 0.011503599991556257,
387
+ "status": "optimal",
388
+ "notes": [
389
+ "Two-stage stochastic: 20 SAA scenarios evaluated.",
390
+ "Stage 1: multi-echelon inventory + technician positioning.",
391
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
392
+ "Monte Carlo (30 runs): SLA 100.0%, MTTR 4.3h."
393
+ ]
394
+ }
395
+ }
assets/samples/sample_medical_equipment.json ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "industry": "medical_equipment",
3
+ "stress": "emergency_surge",
4
+ "scenario_label": "Medical Equipment \u2014 Emergency Surge",
5
+ "summary": {
6
+ "sites": 10,
7
+ "technicians": 6,
8
+ "warehouses": 2,
9
+ "requests": 6
10
+ },
11
+ "optimization": {
12
+ "strategy": "two_stage_stochastic",
13
+ "algorithm": "two_stage_stochastic_programming",
14
+ "stage1_plan": {
15
+ "technician_positions": {
16
+ "T-01": [
17
+ 50.830218050411915,
18
+ 11.245464134292025
19
+ ],
20
+ "T-02": [
21
+ 50.830218050411915,
22
+ 11.245464134292025
23
+ ],
24
+ "T-03": [
25
+ 50.830218050411915,
26
+ 11.245464134292025
27
+ ],
28
+ "T-04": [
29
+ 50.830218050411915,
30
+ 11.245464134292025
31
+ ],
32
+ "T-05": [
33
+ 50.830218050411915,
34
+ 11.245464134292025
35
+ ],
36
+ "T-06": [
37
+ 50.830218050411915,
38
+ 11.245464134292025
39
+ ]
40
+ },
41
+ "inventory_targets": [
42
+ {
43
+ "warehouse_id": "WH-01",
44
+ "part_id": "P-001",
45
+ "quantity": 3,
46
+ "reorder_point": 1,
47
+ "safety_stock": 1
48
+ },
49
+ {
50
+ "warehouse_id": "WH-01",
51
+ "part_id": "P-004",
52
+ "quantity": 2,
53
+ "reorder_point": 1,
54
+ "safety_stock": 1
55
+ },
56
+ {
57
+ "warehouse_id": "WH-01",
58
+ "part_id": "P-003",
59
+ "quantity": 2,
60
+ "reorder_point": 1,
61
+ "safety_stock": 1
62
+ },
63
+ {
64
+ "warehouse_id": "WH-01",
65
+ "part_id": "P-006",
66
+ "quantity": 2,
67
+ "reorder_point": 1,
68
+ "safety_stock": 1
69
+ },
70
+ {
71
+ "warehouse_id": "WH-02",
72
+ "part_id": "P-001",
73
+ "quantity": 3,
74
+ "reorder_point": 1,
75
+ "safety_stock": 1
76
+ },
77
+ {
78
+ "warehouse_id": "WH-02",
79
+ "part_id": "P-004",
80
+ "quantity": 2,
81
+ "reorder_point": 1,
82
+ "safety_stock": 1
83
+ },
84
+ {
85
+ "warehouse_id": "WH-02",
86
+ "part_id": "P-003",
87
+ "quantity": 2,
88
+ "reorder_point": 1,
89
+ "safety_stock": 1
90
+ },
91
+ {
92
+ "warehouse_id": "WH-02",
93
+ "part_id": "P-006",
94
+ "quantity": 2,
95
+ "reorder_point": 1,
96
+ "safety_stock": 1
97
+ }
98
+ ],
99
+ "supply_contracts": [
100
+ {
101
+ "warehouse": "WH-01",
102
+ "part": "P-001",
103
+ "min_order_qty": 5,
104
+ "lead_time_days": 5,
105
+ "expedite_available": true
106
+ },
107
+ {
108
+ "warehouse": "WH-01",
109
+ "part": "P-002",
110
+ "min_order_qty": 5,
111
+ "lead_time_days": 5,
112
+ "expedite_available": true
113
+ },
114
+ {
115
+ "warehouse": "WH-01",
116
+ "part": "P-003",
117
+ "min_order_qty": 5,
118
+ "lead_time_days": 5,
119
+ "expedite_available": true
120
+ },
121
+ {
122
+ "warehouse": "WH-02",
123
+ "part": "P-001",
124
+ "min_order_qty": 5,
125
+ "lead_time_days": 5,
126
+ "expedite_available": true
127
+ },
128
+ {
129
+ "warehouse": "WH-02",
130
+ "part": "P-002",
131
+ "min_order_qty": 5,
132
+ "lead_time_days": 5,
133
+ "expedite_available": true
134
+ },
135
+ {
136
+ "warehouse": "WH-02",
137
+ "part": "P-003",
138
+ "min_order_qty": 5,
139
+ "lead_time_days": 5,
140
+ "expedite_available": true
141
+ }
142
+ ],
143
+ "repair_shop_capacity": 2,
144
+ "total_holding_cost": 1557.7179540408565,
145
+ "notes": [
146
+ "Stock vs expedite ratio: 50% (higher = more pre-positioned inventory).",
147
+ "Multi-echelon targets across 2 warehouses.",
148
+ "Estimated holding cost: $1,558 over 7 days."
149
+ ]
150
+ },
151
+ "assignments": [
152
+ {
153
+ "request_id": "SR-0003",
154
+ "technician_id": "T-02",
155
+ "technician_name": "Tech B",
156
+ "warehouse_id": "WH-01",
157
+ "part_id": "P-003",
158
+ "part_source": "warehouse_stock",
159
+ "route_order": 1,
160
+ "scheduled_start_hours": 0.0,
161
+ "estimated_arrival_hours": 7.168973193506486,
162
+ "estimated_completion_hours": 11.858973193506486,
163
+ "use_substitute": false,
164
+ "defer_to_later": false,
165
+ "rationale": "Scheduled \u2014 skill match 99%, part via warehouse_stock"
166
+ },
167
+ {
168
+ "request_id": "SR-0005",
169
+ "technician_id": "T-06",
170
+ "technician_name": "Tech F",
171
+ "warehouse_id": "WH-02",
172
+ "part_id": "P-006",
173
+ "part_source": "deferred",
174
+ "route_order": 1,
175
+ "scheduled_start_hours": 0.0,
176
+ "estimated_arrival_hours": 8.944809558991361,
177
+ "estimated_completion_hours": 13.514809558991361,
178
+ "use_substitute": false,
179
+ "defer_to_later": true,
180
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
181
+ }
182
+ ],
183
+ "ml_predictions": [
184
+ {
185
+ "request_id": "SR-0001",
186
+ "site_id": "S-010",
187
+ "failure_probability": 0.3318,
188
+ "predicted_part_type": "power_supply",
189
+ "predicted_duration_hours": 1.6,
190
+ "first_time_fix_probability": 0.8508,
191
+ "revisit_probability": 0.1268,
192
+ "confidence": 0.9164,
193
+ "risk_tier": "low"
194
+ },
195
+ {
196
+ "request_id": "SR-0002",
197
+ "site_id": "S-005",
198
+ "failure_probability": 0.2689,
199
+ "predicted_part_type": "communication_card",
200
+ "predicted_duration_hours": 3.5,
201
+ "first_time_fix_probability": 0.7298,
202
+ "revisit_probability": 0.2296,
203
+ "confidence": 0.9038,
204
+ "risk_tier": "low"
205
+ },
206
+ {
207
+ "request_id": "SR-0003",
208
+ "site_id": "S-008",
209
+ "failure_probability": 0.0691,
210
+ "predicted_part_type": "communication_card",
211
+ "predicted_duration_hours": 4.69,
212
+ "first_time_fix_probability": 0.6952,
213
+ "revisit_probability": 0.2591,
214
+ "confidence": 0.8638,
215
+ "risk_tier": "low"
216
+ },
217
+ {
218
+ "request_id": "SR-0004",
219
+ "site_id": "S-003",
220
+ "failure_probability": 0.6225,
221
+ "predicted_part_type": "control_board",
222
+ "predicted_duration_hours": 2.66,
223
+ "first_time_fix_probability": 0.769,
224
+ "revisit_probability": 0.1964,
225
+ "confidence": 0.9255,
226
+ "risk_tier": "high"
227
+ },
228
+ {
229
+ "request_id": "SR-0005",
230
+ "site_id": "S-007",
231
+ "failure_probability": 0.1419,
232
+ "predicted_part_type": "power_supply",
233
+ "predicted_duration_hours": 4.57,
234
+ "first_time_fix_probability": 0.6901,
235
+ "revisit_probability": 0.2634,
236
+ "confidence": 0.8784,
237
+ "risk_tier": "low"
238
+ },
239
+ {
240
+ "request_id": "SR-0006",
241
+ "site_id": "S-004",
242
+ "failure_probability": 0.2142,
243
+ "predicted_part_type": "motor_drive",
244
+ "predicted_duration_hours": 3.12,
245
+ "first_time_fix_probability": 0.8024,
246
+ "revisit_probability": 0.1679,
247
+ "confidence": 0.8928,
248
+ "risk_tier": "low"
249
+ }
250
+ ],
251
+ "metrics": {
252
+ "sla_compliance_pct": 0.0,
253
+ "mean_time_to_repair_hours": 4.94,
254
+ "first_time_fix_rate_pct": 66.7,
255
+ "emergency_stockout_count": 0,
256
+ "technician_utilization_pct": 27.3,
257
+ "total_logistics_cost": 284.58,
258
+ "total_downtime_cost": 44212.55,
259
+ "total_holding_cost": 1557.72,
260
+ "expedite_shipments": 0
261
+ },
262
+ "simulation": {
263
+ "replications": 30,
264
+ "sla_compliance_mean": 0.0,
265
+ "mttr_mean": 6.79,
266
+ "stockout_rate": 0.0,
267
+ "avg_logistics_cost": 138.81,
268
+ "avg_downtime_cost": 9570.58,
269
+ "confidence_interval_95": {
270
+ "sla_compliance": [
271
+ 0.0,
272
+ 0.0
273
+ ],
274
+ "mttr": [
275
+ 6.58,
276
+ 7.01
277
+ ],
278
+ "logistics_cost": [
279
+ 136.68,
280
+ 140.93
281
+ ]
282
+ }
283
+ },
284
+ "solve_time_sec": 0.006099400023231283,
285
+ "status": "optimal",
286
+ "notes": [
287
+ "Two-stage stochastic: 20 SAA scenarios evaluated.",
288
+ "Stage 1: multi-echelon inventory + technician positioning.",
289
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
290
+ "Monte Carlo (30 runs): SLA 0.0%, MTTR 6.8h."
291
+ ]
292
+ }
293
+ }
assets/samples/sample_telecom.json ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "industry": "telecom",
3
+ "stress": "parts_shortage",
4
+ "scenario_label": "Telecom Infrastructure \u2014 Parts Shortage",
5
+ "summary": {
6
+ "sites": 20,
7
+ "technicians": 12,
8
+ "warehouses": 4,
9
+ "requests": 10
10
+ },
11
+ "optimization": {
12
+ "strategy": "two_stage_stochastic",
13
+ "algorithm": "two_stage_stochastic_programming",
14
+ "stage1_plan": {
15
+ "technician_positions": {
16
+ "T-01": [
17
+ 50.52239678759814,
18
+ 11.102612634161087
19
+ ],
20
+ "T-02": [
21
+ 50.52239678759814,
22
+ 11.102612634161087
23
+ ],
24
+ "T-03": [
25
+ 50.52239678759814,
26
+ 11.102612634161087
27
+ ],
28
+ "T-04": [
29
+ 50.52239678759814,
30
+ 11.102612634161087
31
+ ],
32
+ "T-05": [
33
+ 50.52239678759814,
34
+ 11.102612634161087
35
+ ],
36
+ "T-06": [
37
+ 50.52239678759814,
38
+ 11.102612634161087
39
+ ],
40
+ "T-07": [
41
+ 50.52239678759814,
42
+ 11.102612634161087
43
+ ],
44
+ "T-08": [
45
+ 50.52239678759814,
46
+ 11.102612634161087
47
+ ],
48
+ "T-09": [
49
+ 50.52239678759814,
50
+ 11.102612634161087
51
+ ],
52
+ "T-10": [
53
+ 50.52239678759814,
54
+ 11.102612634161087
55
+ ],
56
+ "T-11": [
57
+ 50.52239678759814,
58
+ 11.102612634161087
59
+ ],
60
+ "T-12": [
61
+ 50.52239678759814,
62
+ 11.102612634161087
63
+ ]
64
+ },
65
+ "inventory_targets": [
66
+ {
67
+ "warehouse_id": "WH-01",
68
+ "part_id": "P-005",
69
+ "quantity": 3,
70
+ "reorder_point": 1,
71
+ "safety_stock": 1
72
+ },
73
+ {
74
+ "warehouse_id": "WH-01",
75
+ "part_id": "P-002",
76
+ "quantity": 5,
77
+ "reorder_point": 2,
78
+ "safety_stock": 2
79
+ },
80
+ {
81
+ "warehouse_id": "WH-01",
82
+ "part_id": "P-001",
83
+ "quantity": 2,
84
+ "reorder_point": 1,
85
+ "safety_stock": 1
86
+ },
87
+ {
88
+ "warehouse_id": "WH-01",
89
+ "part_id": "P-004",
90
+ "quantity": 2,
91
+ "reorder_point": 1,
92
+ "safety_stock": 1
93
+ },
94
+ {
95
+ "warehouse_id": "WH-01",
96
+ "part_id": "P-003",
97
+ "quantity": 2,
98
+ "reorder_point": 1,
99
+ "safety_stock": 1
100
+ },
101
+ {
102
+ "warehouse_id": "WH-02",
103
+ "part_id": "P-005",
104
+ "quantity": 3,
105
+ "reorder_point": 1,
106
+ "safety_stock": 1
107
+ },
108
+ {
109
+ "warehouse_id": "WH-02",
110
+ "part_id": "P-002",
111
+ "quantity": 5,
112
+ "reorder_point": 2,
113
+ "safety_stock": 2
114
+ },
115
+ {
116
+ "warehouse_id": "WH-02",
117
+ "part_id": "P-001",
118
+ "quantity": 2,
119
+ "reorder_point": 1,
120
+ "safety_stock": 1
121
+ },
122
+ {
123
+ "warehouse_id": "WH-02",
124
+ "part_id": "P-004",
125
+ "quantity": 2,
126
+ "reorder_point": 1,
127
+ "safety_stock": 1
128
+ },
129
+ {
130
+ "warehouse_id": "WH-02",
131
+ "part_id": "P-003",
132
+ "quantity": 2,
133
+ "reorder_point": 1,
134
+ "safety_stock": 1
135
+ },
136
+ {
137
+ "warehouse_id": "WH-03",
138
+ "part_id": "P-005",
139
+ "quantity": 3,
140
+ "reorder_point": 1,
141
+ "safety_stock": 1
142
+ },
143
+ {
144
+ "warehouse_id": "WH-03",
145
+ "part_id": "P-002",
146
+ "quantity": 5,
147
+ "reorder_point": 2,
148
+ "safety_stock": 2
149
+ },
150
+ {
151
+ "warehouse_id": "WH-03",
152
+ "part_id": "P-001",
153
+ "quantity": 2,
154
+ "reorder_point": 1,
155
+ "safety_stock": 1
156
+ },
157
+ {
158
+ "warehouse_id": "WH-03",
159
+ "part_id": "P-004",
160
+ "quantity": 2,
161
+ "reorder_point": 1,
162
+ "safety_stock": 1
163
+ },
164
+ {
165
+ "warehouse_id": "WH-03",
166
+ "part_id": "P-003",
167
+ "quantity": 2,
168
+ "reorder_point": 1,
169
+ "safety_stock": 1
170
+ },
171
+ {
172
+ "warehouse_id": "WH-04",
173
+ "part_id": "P-005",
174
+ "quantity": 3,
175
+ "reorder_point": 1,
176
+ "safety_stock": 1
177
+ },
178
+ {
179
+ "warehouse_id": "WH-04",
180
+ "part_id": "P-002",
181
+ "quantity": 5,
182
+ "reorder_point": 2,
183
+ "safety_stock": 2
184
+ },
185
+ {
186
+ "warehouse_id": "WH-04",
187
+ "part_id": "P-001",
188
+ "quantity": 2,
189
+ "reorder_point": 1,
190
+ "safety_stock": 1
191
+ },
192
+ {
193
+ "warehouse_id": "WH-04",
194
+ "part_id": "P-004",
195
+ "quantity": 2,
196
+ "reorder_point": 1,
197
+ "safety_stock": 1
198
+ },
199
+ {
200
+ "warehouse_id": "WH-04",
201
+ "part_id": "P-003",
202
+ "quantity": 2,
203
+ "reorder_point": 1,
204
+ "safety_stock": 1
205
+ }
206
+ ],
207
+ "supply_contracts": [
208
+ {
209
+ "warehouse": "WH-01",
210
+ "part": "P-001",
211
+ "min_order_qty": 5,
212
+ "lead_time_days": 5,
213
+ "expedite_available": true
214
+ },
215
+ {
216
+ "warehouse": "WH-01",
217
+ "part": "P-002",
218
+ "min_order_qty": 5,
219
+ "lead_time_days": 5,
220
+ "expedite_available": true
221
+ },
222
+ {
223
+ "warehouse": "WH-01",
224
+ "part": "P-003",
225
+ "min_order_qty": 5,
226
+ "lead_time_days": 5,
227
+ "expedite_available": true
228
+ },
229
+ {
230
+ "warehouse": "WH-02",
231
+ "part": "P-001",
232
+ "min_order_qty": 5,
233
+ "lead_time_days": 5,
234
+ "expedite_available": true
235
+ },
236
+ {
237
+ "warehouse": "WH-02",
238
+ "part": "P-002",
239
+ "min_order_qty": 5,
240
+ "lead_time_days": 5,
241
+ "expedite_available": true
242
+ },
243
+ {
244
+ "warehouse": "WH-02",
245
+ "part": "P-003",
246
+ "min_order_qty": 5,
247
+ "lead_time_days": 5,
248
+ "expedite_available": true
249
+ },
250
+ {
251
+ "warehouse": "WH-03",
252
+ "part": "P-001",
253
+ "min_order_qty": 5,
254
+ "lead_time_days": 5,
255
+ "expedite_available": true
256
+ },
257
+ {
258
+ "warehouse": "WH-03",
259
+ "part": "P-002",
260
+ "min_order_qty": 5,
261
+ "lead_time_days": 5,
262
+ "expedite_available": true
263
+ },
264
+ {
265
+ "warehouse": "WH-03",
266
+ "part": "P-003",
267
+ "min_order_qty": 5,
268
+ "lead_time_days": 5,
269
+ "expedite_available": true
270
+ },
271
+ {
272
+ "warehouse": "WH-04",
273
+ "part": "P-001",
274
+ "min_order_qty": 5,
275
+ "lead_time_days": 5,
276
+ "expedite_available": true
277
+ },
278
+ {
279
+ "warehouse": "WH-04",
280
+ "part": "P-002",
281
+ "min_order_qty": 5,
282
+ "lead_time_days": 5,
283
+ "expedite_available": true
284
+ },
285
+ {
286
+ "warehouse": "WH-04",
287
+ "part": "P-003",
288
+ "min_order_qty": 5,
289
+ "lead_time_days": 5,
290
+ "expedite_available": true
291
+ }
292
+ ],
293
+ "repair_shop_capacity": 4,
294
+ "total_holding_cost": 6411.200715308966,
295
+ "notes": [
296
+ "Stock vs expedite ratio: 50% (higher = more pre-positioned inventory).",
297
+ "Multi-echelon targets across 4 warehouses.",
298
+ "Estimated holding cost: $6,411 over 7 days."
299
+ ]
300
+ },
301
+ "assignments": [
302
+ {
303
+ "request_id": "SR-0008",
304
+ "technician_id": "T-01",
305
+ "technician_name": "Tech A",
306
+ "warehouse_id": "WH-04",
307
+ "part_id": "P-005",
308
+ "part_source": "deferred",
309
+ "route_order": 1,
310
+ "scheduled_start_hours": 0.0,
311
+ "estimated_arrival_hours": 5.009145620384388,
312
+ "estimated_completion_hours": 9.469145620384388,
313
+ "use_substitute": false,
314
+ "defer_to_later": true,
315
+ "rationale": "Scheduled \u2014 skill match 89%, part via deferred (deferred to next window)"
316
+ },
317
+ {
318
+ "request_id": "SR-0009",
319
+ "technician_id": "T-01",
320
+ "technician_name": "Tech A",
321
+ "warehouse_id": "WH-04",
322
+ "part_id": "P-002",
323
+ "part_source": "warehouse_stock",
324
+ "route_order": 2,
325
+ "scheduled_start_hours": 6.474696411232166,
326
+ "estimated_arrival_hours": 14.818291851602815,
327
+ "estimated_completion_hours": 17.908291851602815,
328
+ "use_substitute": false,
329
+ "defer_to_later": false,
330
+ "rationale": "Scheduled \u2014 skill match 89%, part via warehouse_stock"
331
+ },
332
+ {
333
+ "request_id": "SR-0004",
334
+ "technician_id": "T-03",
335
+ "technician_name": "Tech C",
336
+ "warehouse_id": "WH-03",
337
+ "part_id": "P-001",
338
+ "part_source": "deferred",
339
+ "route_order": 1,
340
+ "scheduled_start_hours": 0.0,
341
+ "estimated_arrival_hours": 2.2097288498190735,
342
+ "estimated_completion_hours": 5.709728849819074,
343
+ "use_substitute": false,
344
+ "defer_to_later": true,
345
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
346
+ },
347
+ {
348
+ "request_id": "SR-0010",
349
+ "technician_id": "T-03",
350
+ "technician_name": "Tech C",
351
+ "warehouse_id": "WH-02",
352
+ "part_id": "P-003",
353
+ "part_source": "warehouse_stock",
354
+ "route_order": 2,
355
+ "scheduled_start_hours": 5.587436340493345,
356
+ "estimated_arrival_hours": 13.291148468476583,
357
+ "estimated_completion_hours": 16.791148468476585,
358
+ "use_substitute": false,
359
+ "defer_to_later": false,
360
+ "rationale": "Scheduled \u2014 skill match 95%, part via warehouse_stock"
361
+ },
362
+ {
363
+ "request_id": "SR-0003",
364
+ "technician_id": "T-03",
365
+ "technician_name": "Tech C",
366
+ "warehouse_id": "WH-04",
367
+ "part_id": "P-002",
368
+ "part_source": "deferred",
369
+ "route_order": 3,
370
+ "scheduled_start_hours": 18.126605167384554,
371
+ "estimated_arrival_hours": 22.239061408292017,
372
+ "estimated_completion_hours": 23.939061408292016,
373
+ "use_substitute": false,
374
+ "defer_to_later": true,
375
+ "rationale": "Scheduled \u2014 skill match 95%, part via deferred (deferred to next window)"
376
+ },
377
+ {
378
+ "request_id": "SR-0005",
379
+ "technician_id": "T-03",
380
+ "technician_name": "Tech C",
381
+ "warehouse_id": "WH-04",
382
+ "part_id": "P-002",
383
+ "part_source": "warehouse_stock",
384
+ "route_order": 4,
385
+ "scheduled_start_hours": 19.826605167384557,
386
+ "estimated_arrival_hours": 23.939061408292016,
387
+ "estimated_completion_hours": 25.639061408292015,
388
+ "use_substitute": false,
389
+ "defer_to_later": false,
390
+ "rationale": "Scheduled \u2014 skill match 95%, part via warehouse_stock"
391
+ },
392
+ {
393
+ "request_id": "SR-0006",
394
+ "technician_id": "T-06",
395
+ "technician_name": "Tech F",
396
+ "warehouse_id": "WH-02",
397
+ "part_id": "P-004",
398
+ "part_source": "deferred",
399
+ "route_order": 1,
400
+ "scheduled_start_hours": 0.0,
401
+ "estimated_arrival_hours": 7.404866505625244,
402
+ "estimated_completion_hours": 11.564866505625243,
403
+ "use_substitute": false,
404
+ "defer_to_later": true,
405
+ "rationale": "Scheduled \u2014 skill match 94%, part via deferred (deferred to next window)"
406
+ }
407
+ ],
408
+ "ml_predictions": [
409
+ {
410
+ "request_id": "SR-0001",
411
+ "site_id": "S-013",
412
+ "failure_probability": 0.2592,
413
+ "predicted_part_type": "communication_card",
414
+ "predicted_duration_hours": 1.64,
415
+ "first_time_fix_probability": 0.8572,
416
+ "revisit_probability": 0.1214,
417
+ "confidence": 0.9018,
418
+ "risk_tier": "low"
419
+ },
420
+ {
421
+ "request_id": "SR-0002",
422
+ "site_id": "S-003",
423
+ "failure_probability": 0.343,
424
+ "predicted_part_type": "hydraulic_pump",
425
+ "predicted_duration_hours": 2.98,
426
+ "first_time_fix_probability": 0.7919,
427
+ "revisit_probability": 0.1769,
428
+ "confidence": 0.9186,
429
+ "risk_tier": "low"
430
+ },
431
+ {
432
+ "request_id": "SR-0003",
433
+ "site_id": "S-005",
434
+ "failure_probability": 0.1611,
435
+ "predicted_part_type": "power_supply",
436
+ "predicted_duration_hours": 1.7,
437
+ "first_time_fix_probability": 0.8658,
438
+ "revisit_probability": 0.1141,
439
+ "confidence": 0.8822,
440
+ "risk_tier": "low"
441
+ },
442
+ {
443
+ "request_id": "SR-0004",
444
+ "site_id": "S-018",
445
+ "failure_probability": 0.2689,
446
+ "predicted_part_type": "hydraulic_pump",
447
+ "predicted_duration_hours": 3.5,
448
+ "first_time_fix_probability": 0.7298,
449
+ "revisit_probability": 0.2296,
450
+ "confidence": 0.9038,
451
+ "risk_tier": "low"
452
+ },
453
+ {
454
+ "request_id": "SR-0005",
455
+ "site_id": "S-005",
456
+ "failure_probability": 0.1611,
457
+ "predicted_part_type": "power_supply",
458
+ "predicted_duration_hours": 1.7,
459
+ "first_time_fix_probability": 0.8658,
460
+ "revisit_probability": 0.1141,
461
+ "confidence": 0.8822,
462
+ "risk_tier": "low"
463
+ },
464
+ {
465
+ "request_id": "SR-0006",
466
+ "site_id": "S-007",
467
+ "failure_probability": 0.4013,
468
+ "predicted_part_type": "control_board",
469
+ "predicted_duration_hours": 4.16,
470
+ "first_time_fix_probability": 0.6719,
471
+ "revisit_probability": 0.2789,
472
+ "confidence": 0.9303,
473
+ "risk_tier": "medium"
474
+ },
475
+ {
476
+ "request_id": "SR-0007",
477
+ "site_id": "S-002",
478
+ "failure_probability": 0.475,
479
+ "predicted_part_type": "hydraulic_pump",
480
+ "predicted_duration_hours": 3.23,
481
+ "first_time_fix_probability": 0.7144,
482
+ "revisit_probability": 0.2428,
483
+ "confidence": 0.945,
484
+ "risk_tier": "medium"
485
+ },
486
+ {
487
+ "request_id": "SR-0008",
488
+ "site_id": "S-005",
489
+ "failure_probability": 0.2142,
490
+ "predicted_part_type": "control_board",
491
+ "predicted_duration_hours": 4.46,
492
+ "first_time_fix_probability": 0.685,
493
+ "revisit_probability": 0.2677,
494
+ "confidence": 0.8928,
495
+ "risk_tier": "low"
496
+ },
497
+ {
498
+ "request_id": "SR-0009",
499
+ "site_id": "S-014",
500
+ "failure_probability": 0.2405,
501
+ "predicted_part_type": "hydraulic_pump",
502
+ "predicted_duration_hours": 3.09,
503
+ "first_time_fix_probability": 0.8003,
504
+ "revisit_probability": 0.1698,
505
+ "confidence": 0.8981,
506
+ "risk_tier": "low"
507
+ },
508
+ {
509
+ "request_id": "SR-0010",
510
+ "site_id": "S-012",
511
+ "failure_probability": 0.2689,
512
+ "predicted_part_type": "hydraulic_pump",
513
+ "predicted_duration_hours": 3.5,
514
+ "first_time_fix_probability": 0.7298,
515
+ "revisit_probability": 0.2296,
516
+ "confidence": 0.9038,
517
+ "risk_tier": "low"
518
+ }
519
+ ],
520
+ "metrics": {
521
+ "sla_compliance_pct": 90.0,
522
+ "mean_time_to_repair_hours": 4.2,
523
+ "first_time_fix_rate_pct": 80.0,
524
+ "emergency_stockout_count": 0,
525
+ "technician_utilization_pct": 19.2,
526
+ "total_logistics_cost": 1170.53,
527
+ "total_downtime_cost": 58191.35,
528
+ "total_holding_cost": 6411.2,
529
+ "expedite_shipments": 0
530
+ },
531
+ "simulation": {
532
+ "replications": 30,
533
+ "sla_compliance_mean": 83.8,
534
+ "mttr_mean": 4.53,
535
+ "stockout_rate": 0.0,
536
+ "avg_logistics_cost": 850.4,
537
+ "avg_downtime_cost": 41021.62,
538
+ "confidence_interval_95": {
539
+ "sla_compliance": [
540
+ 82.02,
541
+ 85.56
542
+ ],
543
+ "mttr": [
544
+ 4.44,
545
+ 4.62
546
+ ],
547
+ "logistics_cost": [
548
+ 836.5,
549
+ 864.31
550
+ ]
551
+ }
552
+ },
553
+ "solve_time_sec": 0.006857800006400794,
554
+ "status": "optimal",
555
+ "notes": [
556
+ "Two-stage stochastic: 20 SAA scenarios evaluated.",
557
+ "Stage 1: multi-echelon inventory + technician positioning.",
558
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
559
+ "Monte Carlo (30 runs): SLA 83.8%, MTTR 4.5h."
560
+ ]
561
+ }
562
+ }
gradio/README.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FieldOps — Service Parts & Field Operations Optimizer
3
+ emoji: 🛠️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 5.50.0
8
+ app_file: app.py
9
+ python_version: "3.12"
10
+ short_description: Predict-then-optimize field service parts & routing
11
+ ---
12
+
13
+ # FieldOps — Service Parts & Field Operations Optimizer
14
+
15
+ Enterprise predict-then-optimize platform for aftermarket field service operations.
16
+
17
+ **ML Failure Prediction → Multi-Echelon Inventory → Technician Routing → Monte Carlo Validation**
18
+
19
+ ## Features
20
+
21
+ - Two-stage stochastic programming (pre-failure planning + post-failure response)
22
+ - Multi-echelon spare parts inventory optimization
23
+ - OR-Tools technician routing with skill-based assignment
24
+ - Stock vs expedite cost trade-off analysis
25
+ - ALNS and Sample Average Approximation solvers
26
+ - Monte Carlo SLA validation
27
+
28
+ ## Industries
29
+
30
+ Industrial Equipment · Elevators · Medical Equipment · Machinery · Data Center · Telecom · Home Appliances · Automotive · Power Plant
31
+
32
+ ## Stack
33
+
34
+ Pyomo · OR-Tools · LightGBM-style ML · SimPy · GeoPandas · Plotly · Gradio
gradio/app.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FieldOps — Service Parts and Field Operations Optimizer
3
+ Predict-then-optimize platform for aftermarket field service.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import gradio as gr
13
+ import pandas as pd
14
+
15
+ ROOT = Path(__file__).resolve().parent
16
+ sys.path.insert(0, str(ROOT / "src"))
17
+
18
+ from fieldops.generator import ( # noqa: E402
19
+ inventory_to_table_rows,
20
+ requests_to_table_rows,
21
+ sites_to_table_rows,
22
+ technicians_to_table_rows,
23
+ )
24
+ from fieldops.ml_predictions import predictions_to_table_rows # noqa: E402
25
+ from fieldops.pipeline import FieldOpsPipeline # noqa: E402
26
+ from fieldops.visualization import ( # noqa: E402
27
+ assignment_gantt,
28
+ cost_breakdown_chart,
29
+ kpi_radar_chart,
30
+ mission_map_chart,
31
+ result_summary_markdown,
32
+ stock_expedite_chart,
33
+ strategy_comparison_chart,
34
+ )
35
+
36
+ pipeline = FieldOpsPipeline(ROOT / "assets")
37
+ pipeline.load()
38
+
39
+ INDUSTRY_CHOICES = [(pipeline.get_industry_label(i), i) for i in pipeline.get_industry_ids()]
40
+ STRESS_CHOICES = [(pipeline.get_stress_label(s), s) for s in pipeline.get_stress_ids()]
41
+ STRATEGY_CHOICES = [(pipeline.get_strategy_label(s), s) for s in pipeline.get_strategy_ids()]
42
+
43
+ ASSIGN_COLS = [
44
+ "Request", "Technician", "Route #", "Part Source",
45
+ "Arrival (h)", "Complete (h)", "Substitute", "Deferred",
46
+ ]
47
+ CUSTOM_CSS = ".gradio-container { max-width: 1440px !important; }"
48
+
49
+
50
+ def _assignments_df(result) -> pd.DataFrame:
51
+ rows = [
52
+ [
53
+ a.request_id, a.technician_name, a.route_order, a.part_source,
54
+ f"{a.estimated_arrival_hours:.1f}", f"{a.estimated_completion_hours:.1f}",
55
+ "Yes" if a.use_substitute else "No",
56
+ "Yes" if a.defer_to_later else "No",
57
+ ]
58
+ for a in result.assignments
59
+ ]
60
+ return pd.DataFrame(rows, columns=ASSIGN_COLS) if rows else pd.DataFrame(columns=ASSIGN_COLS)
61
+
62
+
63
+ def _notes_md(result) -> str:
64
+ notes = "\n".join(f"- {n}" for n in result.notes)
65
+ rationale = "\n".join(
66
+ f"- **{a.request_id}** → {a.technician_name}: {a.rationale}"
67
+ for a in result.assignments[:5]
68
+ )
69
+ extra = f"\n_…and {len(result.assignments) - 5} more missions_" if len(result.assignments) > 5 else ""
70
+ return f"### Engine Notes\n{notes}\n\n### Assignment Rationale\n{rationale}{extra}"
71
+
72
+
73
+ def run_optimization(industry, stress, strategy, stock_ratio, num_sites, seed):
74
+ try:
75
+ problem = pipeline.build_problem(
76
+ industry, stress, int(num_sites), stock_ratio / 100, seed=int(seed),
77
+ )
78
+ result = pipeline.run_optimization(problem, strategy)
79
+ return (
80
+ result_summary_markdown(result),
81
+ pd.DataFrame(predictions_to_table_rows(result.ml_predictions)),
82
+ _assignments_df(result),
83
+ _notes_md(result),
84
+ kpi_radar_chart(result),
85
+ cost_breakdown_chart(result),
86
+ mission_map_chart(problem, result),
87
+ assignment_gantt(result),
88
+ pd.DataFrame(sites_to_table_rows(problem)),
89
+ pd.DataFrame(technicians_to_table_rows(problem)),
90
+ pd.DataFrame(inventory_to_table_rows(problem)),
91
+ pd.DataFrame(requests_to_table_rows(problem)),
92
+ json.dumps(result.to_dict(), indent=2),
93
+ "Optimization complete.",
94
+ )
95
+ except Exception as exc:
96
+ return (
97
+ f"### Error\n{exc}", pd.DataFrame(), pd.DataFrame(), "",
98
+ None, None, None, None,
99
+ pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
100
+ "", str(exc),
101
+ )
102
+
103
+
104
+ def run_comparison(industry, stress, num_sites, seed):
105
+ try:
106
+ problem = pipeline.build_problem(industry, stress, int(num_sites), seed=int(seed))
107
+ results = pipeline.run_comparison(problem)
108
+ if not results:
109
+ return "No results.", None, pd.DataFrame()
110
+ rows = []
111
+ for r in results:
112
+ m = r.metrics
113
+ rows.append({
114
+ "Strategy": pipeline.get_strategy_label(r.strategy),
115
+ "SLA %": f"{m.sla_compliance_pct:.1f}",
116
+ "MTTR (h)": f"{m.mean_time_to_repair_hours:.1f}",
117
+ "FTF %": f"{m.first_time_fix_rate_pct:.1f}",
118
+ "Logistics $": f"${m.total_logistics_cost:,.0f}",
119
+ "Downtime $": f"${m.total_downtime_cost:,.0f}",
120
+ "Expedite": m.expedite_shipments,
121
+ "Solve (s)": f"{r.solve_time_sec:.2f}",
122
+ })
123
+ return (
124
+ f"### Strategy Comparison — {problem.scenario_label}\nCompared {len(results)} strategies.",
125
+ strategy_comparison_chart(results),
126
+ pd.DataFrame(rows),
127
+ )
128
+ except Exception as exc:
129
+ return f"Error: {exc}", None, pd.DataFrame()
130
+
131
+
132
+ def run_stock_expedite(industry, stress, num_sites, seed):
133
+ try:
134
+ problem = pipeline.build_problem(industry, stress, int(num_sites), seed=int(seed))
135
+ results = pipeline.run_stock_expedite_analysis(problem)
136
+ rows = []
137
+ for i, r in enumerate(results):
138
+ m = r.metrics
139
+ ratio = (i + 1) * 0.2
140
+ rows.append({
141
+ "Stock Ratio": f"{ratio:.0%}",
142
+ "Holding $": f"${m.total_holding_cost:,.0f}",
143
+ "Expedite Shipments": m.expedite_shipments,
144
+ "Total Cost $": f"${m.total_logistics_cost + m.total_downtime_cost + m.total_holding_cost:,.0f}",
145
+ "SLA %": f"{m.sla_compliance_pct:.1f}",
146
+ })
147
+ return (
148
+ "### Stock vs Expedite Trade-off Analysis\n"
149
+ "Higher pre-positioned inventory reduces expedite shipments but increases holding cost.",
150
+ stock_expedite_chart(results),
151
+ pd.DataFrame(rows),
152
+ )
153
+ except Exception as exc:
154
+ return f"Error: {exc}", None, pd.DataFrame()
155
+
156
+
157
+ def build_ui() -> gr.Blocks:
158
+ with gr.Blocks(title="FieldOps — Service Parts & Field Operations Optimizer", css=CUSTOM_CSS) as demo:
159
+ gr.Markdown(
160
+ "# FieldOps — Service Parts & Field Operations Optimizer\n"
161
+ "**Predict-then-Optimize for aftermarket field service operations**\n\n"
162
+ "ML Failure Prediction → Multi-Echelon Inventory → Technician Routing → Monte Carlo Validation"
163
+ )
164
+
165
+ with gr.Tab("Optimize"):
166
+ with gr.Row():
167
+ industry = gr.Dropdown(INDUSTRY_CHOICES, value="industrial_equipment", label="Industry")
168
+ stress = gr.Dropdown(STRESS_CHOICES, value="baseline", label="Stress Scenario")
169
+ strategy = gr.Dropdown(STRATEGY_CHOICES, value="two_stage_stochastic", label="Algorithm")
170
+ with gr.Row():
171
+ stock_ratio = gr.Slider(10, 90, value=50, step=5, label="Stock vs Expedite Ratio (%)")
172
+ num_sites = gr.Slider(6, 20, value=12, step=1, label="Service Sites")
173
+ seed = gr.Number(value=42, label="Random Seed", precision=0)
174
+ run_btn = gr.Button("Run Predict-then-Optimize", variant="primary")
175
+
176
+ summary = gr.Markdown()
177
+ with gr.Row():
178
+ kpi_chart = gr.Plot(label="KPI Radar")
179
+ cost_chart = gr.Plot(label="Cost Breakdown")
180
+ with gr.Row():
181
+ map_chart = gr.Plot(label="Operations Map")
182
+ gantt_chart = gr.Plot(label="Mission Gantt")
183
+ with gr.Row():
184
+ pred_table = gr.Dataframe(label="ML Predictions", interactive=False)
185
+ assign_table = gr.Dataframe(label="Mission Assignments", interactive=False)
186
+ notes = gr.Markdown()
187
+ with gr.Accordion("Network Data", open=False):
188
+ with gr.Row():
189
+ sites_table = gr.Dataframe(label="Service Sites", interactive=False)
190
+ tech_table = gr.Dataframe(label="Technicians", interactive=False)
191
+ with gr.Row():
192
+ inv_table = gr.Dataframe(label="Inventory", interactive=False)
193
+ req_table = gr.Dataframe(label="Service Requests", interactive=False)
194
+ export_json = gr.Code(label="Full Result JSON", language="json")
195
+ status = gr.Textbox(label="Status", interactive=False)
196
+
197
+ run_btn.click(
198
+ run_optimization,
199
+ [industry, stress, strategy, stock_ratio, num_sites, seed],
200
+ [
201
+ summary, pred_table, assign_table, notes,
202
+ kpi_chart, cost_chart, map_chart, gantt_chart,
203
+ sites_table, tech_table, inv_table, req_table,
204
+ export_json, status,
205
+ ],
206
+ )
207
+
208
+ with gr.Tab("Compare Strategies"):
209
+ with gr.Row():
210
+ cmp_industry = gr.Dropdown(INDUSTRY_CHOICES, value="medical_equipment", label="Industry")
211
+ cmp_stress = gr.Dropdown(STRESS_CHOICES, value="emergency_surge", label="Stress")
212
+ cmp_sites = gr.Slider(6, 20, value=10, step=1, label="Sites")
213
+ cmp_seed = gr.Number(value=42, label="Seed", precision=0)
214
+ cmp_btn = gr.Button("Compare All Strategies", variant="primary")
215
+ cmp_summary = gr.Markdown()
216
+ cmp_chart = gr.Plot()
217
+ cmp_table = gr.Dataframe(interactive=False)
218
+ cmp_btn.click(run_comparison, [cmp_industry, cmp_stress, cmp_sites, cmp_seed],
219
+ [cmp_summary, cmp_chart, cmp_table])
220
+
221
+ with gr.Tab("Stock vs Expedite"):
222
+ with gr.Row():
223
+ se_industry = gr.Dropdown(INDUSTRY_CHOICES, value="telecom", label="Industry")
224
+ se_stress = gr.Dropdown(STRESS_CHOICES, value="parts_shortage", label="Stress")
225
+ se_sites = gr.Slider(6, 20, value=12, step=1, label="Sites")
226
+ se_seed = gr.Number(value=42, label="Seed", precision=0)
227
+ se_btn = gr.Button("Analyze Trade-off", variant="primary")
228
+ se_summary = gr.Markdown()
229
+ se_chart = gr.Plot()
230
+ se_table = gr.Dataframe(interactive=False)
231
+ se_btn.click(run_stock_expedite, [se_industry, se_stress, se_sites, se_seed],
232
+ [se_summary, se_chart, se_table])
233
+
234
+ with gr.Tab("About"):
235
+ gr.Markdown(
236
+ "## Two-Stage Decision Model\n\n"
237
+ "**Stage 1 — Pre-Failure:** Technician positioning, multi-echelon inventory, "
238
+ "supply contracts, repair shop capacity.\n\n"
239
+ "**Stage 2 — Post-Failure:** Mission assignment, routing, parts allocation, "
240
+ "repair scheduling, substitute parts.\n\n"
241
+ "### ML Predictions (Pre-trained)\n"
242
+ "- Failure probability | Part type needed | Repair duration\n"
243
+ "- First-time fix probability | Revisit probability\n\n"
244
+ "### Optimization Algorithms\n"
245
+ "- Two-Stage Stochastic Programming | Multi-Echelon Inventory\n"
246
+ "- OR-Tools Technician Routing | Sample Average Approximation\n"
247
+ "- ALNS | Monte Carlo Simulation\n\n"
248
+ "### KPIs\n"
249
+ "SLA compliance | MTTR | First-time fix rate | Emergency stockout | "
250
+ "Technician utilization | Logistics cost | Downtime cost"
251
+ )
252
+
253
+ return demo
254
+
255
+
256
+ if __name__ == "__main__":
257
+ build_ui().launch()
gradio/assets/config.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "fieldops-solver-config",
3
+ "version": "1.0.0",
4
+ "description": "Solver configuration for FieldOps field service optimization engine",
5
+ "ml_model": {
6
+ "type": "pre-trained-logistic",
7
+ "predictions": [
8
+ "failure_probability",
9
+ "part_type",
10
+ "repair_duration",
11
+ "first_time_fix_probability",
12
+ "revisit_probability"
13
+ ],
14
+ "note": "Fixed coefficients — no training required at inference"
15
+ },
16
+ "optimization": {
17
+ "algorithms": [
18
+ "two_stage_stochastic",
19
+ "multi_echelon_inventory",
20
+ "technician_routing",
21
+ "sample_average_approximation",
22
+ "alns_field_ops",
23
+ "stock_vs_expedite",
24
+ "monte_carlo"
25
+ ],
26
+ "stage1": ["inventory_positioning", "technician_deployment", "supply_contracts"],
27
+ "stage2": ["mission_assignment", "routing", "parts_allocation", "scheduling"]
28
+ },
29
+ "simulation": {
30
+ "method": "monte_carlo",
31
+ "default_replications": 30
32
+ },
33
+ "kpis": [
34
+ "sla_compliance_pct",
35
+ "mean_time_to_repair_hours",
36
+ "first_time_fix_rate_pct",
37
+ "emergency_stockout_count",
38
+ "technician_utilization_pct",
39
+ "total_logistics_cost",
40
+ "total_downtime_cost"
41
+ ]
42
+ }
gradio/assets/demo/benchmarks.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "industry": "industrial_equipment",
4
+ "stress": "baseline",
5
+ "algorithm": "two_stage_stochastic",
6
+ "sla_compliance_pct": 100.0,
7
+ "mttr_hours": 4.31,
8
+ "ftf_rate_pct": 100.0,
9
+ "logistics_cost": 597.53,
10
+ "downtime_cost": 17622.45,
11
+ "solve_time_sec": 0.011503599991556257
12
+ },
13
+ {
14
+ "industry": "medical_equipment",
15
+ "stress": "emergency_surge",
16
+ "algorithm": "two_stage_stochastic",
17
+ "sla_compliance_pct": 0.0,
18
+ "mttr_hours": 4.94,
19
+ "ftf_rate_pct": 66.7,
20
+ "logistics_cost": 284.58,
21
+ "downtime_cost": 44212.55,
22
+ "solve_time_sec": 0.006099400023231283
23
+ },
24
+ {
25
+ "industry": "telecom",
26
+ "stress": "parts_shortage",
27
+ "algorithm": "two_stage_stochastic",
28
+ "sla_compliance_pct": 90.0,
29
+ "mttr_hours": 4.2,
30
+ "ftf_rate_pct": 80.0,
31
+ "logistics_cost": 1170.53,
32
+ "downtime_cost": 58191.35,
33
+ "solve_time_sec": 0.006857800006400794
34
+ },
35
+ {
36
+ "industry": "data_center",
37
+ "stress": "technician_shortage",
38
+ "algorithm": "two_stage_stochastic",
39
+ "sla_compliance_pct": 0.0,
40
+ "mttr_hours": 4.61,
41
+ "ftf_rate_pct": 83.3,
42
+ "logistics_cost": 217.76,
43
+ "downtime_cost": 38117.22,
44
+ "solve_time_sec": 0.0037087000091560185
45
+ },
46
+ {
47
+ "industry": "elevators",
48
+ "stress": "peak_season",
49
+ "algorithm": "two_stage_stochastic",
50
+ "sla_compliance_pct": 11.1,
51
+ "mttr_hours": 4.71,
52
+ "ftf_rate_pct": 77.8,
53
+ "logistics_cost": 566.36,
54
+ "downtime_cost": 25018.85,
55
+ "solve_time_sec": 0.00974899998982437
56
+ }
57
+ ]
gradio/assets/demo/summary.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project": "FieldOps",
3
+ "version": "1.0.0",
4
+ "scenarios": 5,
5
+ "algorithms": 7,
6
+ "primary_metrics": [
7
+ "sla_compliance_pct",
8
+ "mean_time_to_repair_hours",
9
+ "first_time_fix_rate_pct",
10
+ "emergency_stockout_count",
11
+ "technician_utilization_pct",
12
+ "total_logistics_cost",
13
+ "total_downtime_cost"
14
+ ]
15
+ }
gradio/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ortools>=9.8
2
+ numpy>=1.24
3
+ pandas>=2.0
4
+ plotly>=5.18
gradio/src/fieldops/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """FieldOps — Service Parts and Field Operations Optimizer."""
2
+
3
+ from fieldops.constants import ENGINE_VERSION
4
+
5
+ __version__ = ENGINE_VERSION
gradio/src/fieldops/alns.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adaptive Large Neighborhood Search for field operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import random
7
+ import time
8
+
9
+ from fieldops.inventory_optimization import optimize_inventory
10
+ from fieldops.ml_predictions import predict_all
11
+ from fieldops.metrics import compute_metrics
12
+ from fieldops.models import OptimizationResult, ProblemInstance, Stage1Plan
13
+ from fieldops.routing import solve_routing
14
+ from fieldops.scheduling import assign_missions
15
+
16
+
17
+ def _destroy(assignments: list[dict], fraction: float, rng: random.Random) -> list[dict]:
18
+ n_remove = max(1, int(len(assignments) * fraction))
19
+ remaining = copy.deepcopy(assignments)
20
+ to_remove = rng.sample(range(len(remaining)), min(n_remove, len(remaining)))
21
+ for idx in sorted(to_remove, reverse=True):
22
+ remaining.pop(idx)
23
+ return remaining
24
+
25
+
26
+ def _repair(problem: ProblemInstance, partial: list[dict], predictions) -> list[dict]:
27
+ assigned_ids = {a["request_id"] for a in partial}
28
+ missing = [r for r in problem.requests if r.request_id not in assigned_ids]
29
+ if not missing:
30
+ return partial
31
+
32
+ sub_problem = copy.deepcopy(problem)
33
+ sub_problem.requests = missing
34
+ new_assignments = assign_missions(sub_problem, predictions)
35
+ return partial + new_assignments
36
+
37
+
38
+ def solve_alns(
39
+ problem: ProblemInstance,
40
+ max_iterations: int = 30,
41
+ time_limit_sec: float = 8.0,
42
+ seed: int = 42,
43
+ ) -> tuple[Stage1Plan, list[dict], list[str]]:
44
+ """ALNS metaheuristic for field service planning."""
45
+ rng = random.Random(seed)
46
+ t0 = time.perf_counter()
47
+ stage1 = optimize_inventory(problem, problem.stock_vs_expedite_ratio)
48
+ predictions = predict_all(problem)
49
+
50
+ current = assign_missions(problem, predictions)
51
+ current_metrics = compute_metrics(problem, current, stage1, predictions)
52
+ current_cost = current_metrics.total_logistics_cost + current_metrics.total_downtime_cost
53
+ best = copy.deepcopy(current)
54
+ best_cost = current_cost
55
+
56
+ destroy_rates = [0.2, 0.35, 0.5]
57
+ iteration = 0
58
+
59
+ while iteration < max_iterations and (time.perf_counter() - t0) < time_limit_sec:
60
+ rate = destroy_rates[iteration % len(destroy_rates)]
61
+ partial = _destroy(current, rate, rng)
62
+ candidate = _repair(problem, partial, predictions)
63
+ cand_metrics = compute_metrics(problem, candidate, stage1, predictions)
64
+ cand_cost = cand_metrics.total_logistics_cost + cand_metrics.total_downtime_cost
65
+
66
+ delta = cand_cost - current_cost
67
+ temp = max(0.01, 1.0 - iteration / max_iterations)
68
+ accept = delta < 0 or rng.random() < pow(2.718, -delta / (temp * best_cost * 0.1 + 1))
69
+
70
+ if accept:
71
+ current = candidate
72
+ current_cost = cand_cost
73
+ if cand_cost < best_cost:
74
+ best = copy.deepcopy(candidate)
75
+ best_cost = cand_cost
76
+
77
+ iteration += 1
78
+
79
+ notes = [
80
+ f"ALNS: {iteration} iterations, best cost ${best_cost:,.0f}.",
81
+ "Operators: random destroy + greedy repair on mission assignments.",
82
+ ]
83
+ return stage1, best, notes
gradio/src/fieldops/constants.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Constants for FieldOps engine."""
2
+
3
+ ENGINE_VERSION = "1.0.0"
4
+
5
+ INDUSTRY_SCENARIOS: dict[str, dict] = {
6
+ "industrial_equipment": {
7
+ "label": "Industrial Equipment",
8
+ "default_sites": 12,
9
+ "default_warehouses": 3,
10
+ "default_technicians": 8,
11
+ "failure_rate": 0.08,
12
+ "sla_hours": 8,
13
+ "downtime_cost": 2500,
14
+ },
15
+ "elevators": {
16
+ "label": "Elevators & Lifts",
17
+ "default_sites": 18,
18
+ "default_warehouses": 2,
19
+ "default_technicians": 10,
20
+ "failure_rate": 0.06,
21
+ "sla_hours": 4,
22
+ "downtime_cost": 1800,
23
+ },
24
+ "medical_equipment": {
25
+ "label": "Medical Equipment",
26
+ "default_sites": 10,
27
+ "default_warehouses": 2,
28
+ "default_technicians": 6,
29
+ "failure_rate": 0.05,
30
+ "sla_hours": 2,
31
+ "downtime_cost": 5000,
32
+ },
33
+ "machinery": {
34
+ "label": "Heavy Machinery",
35
+ "default_sites": 8,
36
+ "default_warehouses": 2,
37
+ "default_technicians": 6,
38
+ "failure_rate": 0.10,
39
+ "sla_hours": 12,
40
+ "downtime_cost": 3200,
41
+ },
42
+ "data_center": {
43
+ "label": "Data Center",
44
+ "default_sites": 6,
45
+ "default_warehouses": 2,
46
+ "default_technicians": 5,
47
+ "failure_rate": 0.04,
48
+ "sla_hours": 1,
49
+ "downtime_cost": 8000,
50
+ },
51
+ "telecom": {
52
+ "label": "Telecom Infrastructure",
53
+ "default_sites": 20,
54
+ "default_warehouses": 4,
55
+ "default_technicians": 12,
56
+ "failure_rate": 0.07,
57
+ "sla_hours": 6,
58
+ "downtime_cost": 4000,
59
+ },
60
+ "home_appliances": {
61
+ "label": "Home Appliances",
62
+ "default_sites": 25,
63
+ "default_warehouses": 3,
64
+ "default_technicians": 15,
65
+ "failure_rate": 0.12,
66
+ "sla_hours": 24,
67
+ "downtime_cost": 200,
68
+ },
69
+ "automotive": {
70
+ "label": "Automotive After-Sales",
71
+ "default_sites": 15,
72
+ "default_warehouses": 3,
73
+ "default_technicians": 10,
74
+ "failure_rate": 0.09,
75
+ "sla_hours": 8,
76
+ "downtime_cost": 600,
77
+ },
78
+ "power_plant": {
79
+ "label": "Power Plant",
80
+ "default_sites": 5,
81
+ "default_warehouses": 2,
82
+ "default_technicians": 4,
83
+ "failure_rate": 0.03,
84
+ "sla_hours": 4,
85
+ "downtime_cost": 12000,
86
+ },
87
+ }
88
+
89
+ STRESS_SCENARIOS: dict[str, dict] = {
90
+ "baseline": {"label": "Baseline Demand", "failure_multiplier": 1.0, "stock_multiplier": 1.0},
91
+ "peak_season": {"label": "Peak Season", "failure_multiplier": 1.4, "stock_multiplier": 0.9},
92
+ "parts_shortage": {"label": "Parts Shortage", "failure_multiplier": 1.0, "stock_multiplier": 0.5},
93
+ "technician_shortage": {"label": "Technician Shortage", "failure_multiplier": 1.2, "stock_multiplier": 1.0},
94
+ "emergency_surge": {"label": "Emergency Surge", "failure_multiplier": 2.0, "stock_multiplier": 0.7},
95
+ "regional_outage": {"label": "Regional Outage", "failure_multiplier": 1.8, "stock_multiplier": 0.6},
96
+ }
97
+
98
+ STRATEGIES: list[str] = [
99
+ "two_stage_stochastic",
100
+ "multi_echelon_inventory",
101
+ "technician_routing",
102
+ "sample_average_approximation",
103
+ "alns_field_ops",
104
+ "stock_vs_expedite",
105
+ "monte_carlo",
106
+ ]
107
+
108
+ STAGE_LABELS = {
109
+ "stage1": "Stage 1 — Pre-Failure Planning",
110
+ "stage2": "Stage 2 — Post-Failure Response",
111
+ }
112
+
113
+ PART_TYPES = [
114
+ "control_board", "motor_drive", "sensor_module", "hydraulic_pump",
115
+ "bearing_kit", "power_supply", "cooling_fan", "communication_card",
116
+ ]
117
+
118
+ SKILL_TYPES = ["electrical", "mechanical", "hydraulic", "electronics", "network"]
gradio/src/fieldops/engine.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unified field service optimization engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from fieldops.alns import solve_alns
8
+ from fieldops.constants import STRATEGIES
9
+ from fieldops.inventory_optimization import optimize_inventory
10
+ from fieldops.metrics import compute_metrics
11
+ from fieldops.ml_predictions import predict_all
12
+ from fieldops.models import OptimizationResult, ProblemInstance
13
+ from fieldops.routing import solve_routing
14
+ from fieldops.scheduling import assign_missions
15
+ from fieldops.simulation import run_monte_carlo
16
+ from fieldops.stochastic import solve_saa, solve_two_stage_stochastic
17
+
18
+
19
+ class FieldOpsEngine:
20
+ """Dispatch field service problems to predict-then-optimize solvers."""
21
+
22
+ def optimize(
23
+ self,
24
+ problem: ProblemInstance,
25
+ strategy: str = "two_stage_stochastic",
26
+ run_simulation: bool = True,
27
+ ) -> OptimizationResult:
28
+ strategy = strategy.lower().replace(" ", "_")
29
+ t0 = time.perf_counter()
30
+ predictions = predict_all(problem)
31
+
32
+ if strategy == "two_stage_stochastic":
33
+ stage1, assignments, notes = solve_two_stage_stochastic(problem)
34
+ algorithm = "two_stage_stochastic_programming"
35
+ elif strategy == "multi_echelon_inventory":
36
+ stage1 = optimize_inventory(problem, problem.stock_vs_expedite_ratio)
37
+ assignments = assign_missions(problem, predictions)
38
+ notes = ["Multi-echelon inventory optimization with greedy assignment."]
39
+ algorithm = "multi_echelon_inventory"
40
+ elif strategy == "technician_routing":
41
+ stage1 = optimize_inventory(problem, problem.stock_vs_expedite_ratio)
42
+ assignments = assign_missions(problem, predictions)
43
+ notes = ["OR-Tools VRP routing with skill-based pre-assignment."]
44
+ algorithm = "or_tools_routing"
45
+ elif strategy == "sample_average_approximation":
46
+ stage1, assignments, notes = solve_saa(problem, n_samples=50)
47
+ algorithm = "sample_average_approximation"
48
+ elif strategy == "alns_field_ops":
49
+ stage1, assignments, notes = solve_alns(problem)
50
+ algorithm = "adaptive_large_neighborhood_search"
51
+ elif strategy == "stock_vs_expedite":
52
+ stage1 = optimize_inventory(problem, problem.stock_vs_expedite_ratio)
53
+ assignments = assign_missions(problem, predictions)
54
+ notes = [
55
+ f"Stock vs expedite trade-off at {problem.stock_vs_expedite_ratio:.0%}.",
56
+ "Higher stock ratio → lower expedite cost, higher holding cost.",
57
+ ]
58
+ algorithm = "stock_expedite_tradeoff"
59
+ elif strategy == "monte_carlo":
60
+ stage1 = optimize_inventory(problem, 0.5)
61
+ assignments = assign_missions(problem, predictions)
62
+ notes = ["Monte Carlo validation of baseline plan."]
63
+ algorithm = "monte_carlo_simulation"
64
+ else:
65
+ raise ValueError(f"Unknown strategy: {strategy}. Choose from: {STRATEGIES}")
66
+
67
+ mission_assignments = solve_routing(problem, predictions, assignments)
68
+ metrics = compute_metrics(problem, assignments, stage1, predictions)
69
+
70
+ simulation = None
71
+ if run_simulation or strategy == "monte_carlo":
72
+ simulation = run_monte_carlo(
73
+ problem, mission_assignments, stage1, predictions, replications=30,
74
+ )
75
+ notes.append(
76
+ f"Monte Carlo ({simulation.replications} runs): "
77
+ f"SLA {simulation.sla_compliance_mean:.1f}%, "
78
+ f"MTTR {simulation.mttr_mean:.1f}h."
79
+ )
80
+
81
+ return OptimizationResult(
82
+ strategy=strategy,
83
+ algorithm=algorithm,
84
+ stage1_plan=stage1,
85
+ assignments=mission_assignments,
86
+ ml_predictions=predictions,
87
+ metrics=metrics,
88
+ simulation=simulation,
89
+ solve_time_sec=time.perf_counter() - t0,
90
+ status="optimal",
91
+ notes=notes,
92
+ )
93
+
94
+ def compare_strategies(
95
+ self,
96
+ problem: ProblemInstance,
97
+ strategy_list: list[str] | None = None,
98
+ ) -> list[OptimizationResult]:
99
+ targets = strategy_list or [
100
+ "two_stage_stochastic",
101
+ "multi_echelon_inventory",
102
+ "technician_routing",
103
+ "alns_field_ops",
104
+ ]
105
+ results = []
106
+ for strat in targets:
107
+ try:
108
+ results.append(self.optimize(problem, strat, run_simulation=True))
109
+ except Exception:
110
+ continue
111
+ return results
112
+
113
+ def compare_stock_expedite(
114
+ self,
115
+ problem: ProblemInstance,
116
+ ratios: list[float] | None = None,
117
+ ) -> list[OptimizationResult]:
118
+ ratios = ratios or [0.2, 0.4, 0.6, 0.8]
119
+ results = []
120
+ for ratio in ratios:
121
+ import copy
122
+ p = copy.deepcopy(problem)
123
+ p.stock_vs_expedite_ratio = ratio
124
+ results.append(self.optimize(p, "stock_vs_expedite", run_simulation=True))
125
+ return results
gradio/src/fieldops/generator.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic field service problem generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import random
7
+ from typing import Any
8
+
9
+ from fieldops.constants import INDUSTRY_SCENARIOS, PART_TYPES, SKILL_TYPES, STRESS_SCENARIOS
10
+ from fieldops.models import (
11
+ InventoryLevel,
12
+ ProblemInstance,
13
+ ServiceRequest,
14
+ ServiceSite,
15
+ SparePart,
16
+ Technician,
17
+ Warehouse,
18
+ )
19
+
20
+ REGION_CENTERS = {
21
+ "north": (52.5, 13.4),
22
+ "south": (48.1, 11.6),
23
+ "east": (51.0, 14.0),
24
+ "west": (50.9, 6.9),
25
+ "central": (51.2, 10.4),
26
+ }
27
+
28
+
29
+ def _seed(*parts: str) -> int:
30
+ h = hashlib.md5("|".join(parts).encode()).hexdigest()
31
+ return int(h[:8], 16)
32
+
33
+
34
+ def _jitter(rng: random.Random, lat: float, lon: float, spread: float = 0.8) -> tuple[float, float]:
35
+ return lat + rng.uniform(-spread, spread), lon + rng.uniform(-spread, spread)
36
+
37
+
38
+ def generate_problem(
39
+ industry_id: str,
40
+ stress_id: str = "baseline",
41
+ num_sites: int | None = None,
42
+ num_technicians: int | None = None,
43
+ num_requests: int | None = None,
44
+ stock_vs_expedite: float = 0.5,
45
+ reliability_target: float = 0.95,
46
+ seed: int = 42,
47
+ ) -> ProblemInstance:
48
+ meta = INDUSTRY_SCENARIOS[industry_id]
49
+ stress = STRESS_SCENARIOS.get(stress_id, STRESS_SCENARIOS["baseline"])
50
+ rng = random.Random(seed)
51
+
52
+ n_sites = num_sites or meta["default_sites"]
53
+ n_techs = num_technicians or meta["default_technicians"]
54
+ n_requests = num_requests or max(6, n_sites // 2)
55
+ n_warehouses = meta.get("default_warehouses", 3)
56
+
57
+ regions = list(REGION_CENTERS.keys())
58
+ warehouses: list[Warehouse] = []
59
+ for i in range(n_warehouses):
60
+ region = regions[i % len(regions)]
61
+ lat, lon = REGION_CENTERS[region]
62
+ lat, lon = _jitter(rng, lat, lon, 0.3)
63
+ warehouses.append(Warehouse(
64
+ warehouse_id=f"WH-{i+1:02d}",
65
+ name=f"{region.title()} Depot",
66
+ region=region,
67
+ lat=lat,
68
+ lon=lon,
69
+ capacity_units=rng.randint(80, 200),
70
+ expedite_cost_per_unit=rng.uniform(50, 150),
71
+ ))
72
+
73
+ spare_parts: list[SparePart] = []
74
+ for i, pt in enumerate(PART_TYPES[:6]):
75
+ sub_id = PART_TYPES[(i + 1) % 6] if i < 5 else None
76
+ spare_parts.append(SparePart(
77
+ part_id=f"P-{i+1:03d}",
78
+ name=pt.replace("_", " ").title(),
79
+ part_type=pt,
80
+ unit_cost=rng.uniform(80, 2500),
81
+ holding_cost_per_day=rng.uniform(2, 25),
82
+ expedite_cost=rng.uniform(100, 800),
83
+ substitute_part_id=f"P-{(i % 5) + 1:03d}" if sub_id else None,
84
+ ))
85
+
86
+ inventory: list[InventoryLevel] = []
87
+ stock_mult = stress["stock_multiplier"]
88
+ for wh in warehouses:
89
+ for part in spare_parts:
90
+ base_qty = rng.randint(2, 12)
91
+ qty = max(1, int(base_qty * stock_mult))
92
+ inventory.append(InventoryLevel(
93
+ warehouse_id=wh.warehouse_id,
94
+ part_id=part.part_id,
95
+ quantity=qty,
96
+ reorder_point=max(1, qty // 3),
97
+ safety_stock=max(1, qty // 4),
98
+ ))
99
+
100
+ technicians: list[Technician] = []
101
+ for i in range(n_techs):
102
+ region = regions[i % len(regions)]
103
+ lat, lon = REGION_CENTERS[region]
104
+ lat, lon = _jitter(rng, lat, lon, 0.5)
105
+ skills = rng.sample(SKILL_TYPES, k=rng.randint(2, 3))
106
+ technicians.append(Technician(
107
+ technician_id=f"T-{i+1:02d}",
108
+ name=f"Tech {chr(65 + i)}",
109
+ skills=skills,
110
+ skill_level=rng.uniform(0.6, 1.0),
111
+ hourly_rate=rng.uniform(35, 85),
112
+ home_lat=lat,
113
+ home_lon=lon,
114
+ max_jobs_per_day=rng.randint(3, 6),
115
+ region=region,
116
+ ))
117
+
118
+ sites: list[ServiceSite] = []
119
+ criticalities = ["critical", "high", "medium", "low"]
120
+ for i in range(n_sites):
121
+ region = regions[i % len(regions)]
122
+ lat, lon = REGION_CENTERS[region]
123
+ lat, lon = _jitter(rng, lat, lon, 1.2)
124
+ crit = rng.choices(criticalities, weights=[0.15, 0.25, 0.35, 0.25])[0]
125
+ sites.append(ServiceSite(
126
+ site_id=f"S-{i+1:03d}",
127
+ name=f"Site {region.title()}-{i+1}",
128
+ asset_type=rng.choice(["compressor", "hvac", "drive", "controller", "pump"]),
129
+ criticality=crit,
130
+ lat=lat,
131
+ lon=lon,
132
+ sla_hours=meta["sla_hours"] * (0.5 if crit == "critical" else 1.0),
133
+ downtime_cost_per_hour=meta["downtime_cost"] * (2.0 if crit == "critical" else 1.0),
134
+ contract_tier=rng.choice(["gold", "silver", "bronze"]),
135
+ ))
136
+
137
+ requests: list[ServiceRequest] = []
138
+ failure_mult = stress["failure_multiplier"]
139
+ for i in range(n_requests):
140
+ site = rng.choice(sites)
141
+ is_emergency = rng.random() < 0.2 * failure_mult
142
+ part_type = rng.choice(PART_TYPES)
143
+ skill = rng.choice(SKILL_TYPES)
144
+ requests.append(ServiceRequest(
145
+ request_id=f"SR-{i+1:04d}",
146
+ site_id=site.site_id,
147
+ site_name=site.name,
148
+ priority="P1" if is_emergency else rng.choice(["P2", "P3", "P4"]),
149
+ failure_type=rng.choice(["mechanical", "electrical", "sensor", "wear"]),
150
+ required_part_type=part_type,
151
+ required_skill=skill,
152
+ estimated_duration_hours=rng.uniform(1.5, 6.0),
153
+ is_emergency=is_emergency,
154
+ sla_deadline_hours=site.sla_hours,
155
+ lat=site.lat,
156
+ lon=site.lon,
157
+ ))
158
+
159
+ label = f"{meta['label']} — {stress['label']}"
160
+ return ProblemInstance(
161
+ industry_id=industry_id,
162
+ stress_scenario=stress_id,
163
+ scenario_label=label,
164
+ planning_horizon_days=7,
165
+ warehouses=warehouses,
166
+ spare_parts=spare_parts,
167
+ inventory=inventory,
168
+ technicians=technicians,
169
+ sites=sites,
170
+ requests=requests,
171
+ stock_vs_expedite_ratio=stock_vs_expedite,
172
+ reliability_target=reliability_target,
173
+ )
174
+
175
+
176
+ def sites_to_table_rows(problem: ProblemInstance) -> list[dict[str, Any]]:
177
+ return [
178
+ {
179
+ "Site": s.site_id,
180
+ "Name": s.name,
181
+ "Criticality": s.criticality,
182
+ "SLA (h)": s.sla_hours,
183
+ "Contract": s.contract_tier,
184
+ "Downtime $/h": f"${s.downtime_cost_per_hour:,.0f}",
185
+ }
186
+ for s in problem.sites
187
+ ]
188
+
189
+
190
+ def technicians_to_table_rows(problem: ProblemInstance) -> list[dict[str, Any]]:
191
+ return [
192
+ {
193
+ "Technician": t.technician_id,
194
+ "Name": t.name,
195
+ "Skills": ", ".join(t.skills),
196
+ "Level": f"{t.skill_level:.0%}",
197
+ "Region": t.region,
198
+ "Max Jobs/Day": t.max_jobs_per_day,
199
+ }
200
+ for t in problem.technicians
201
+ ]
202
+
203
+
204
+ def inventory_to_table_rows(problem: ProblemInstance) -> list[dict[str, Any]]:
205
+ part_map = {p.part_id: p.name for p in problem.spare_parts}
206
+ return [
207
+ {
208
+ "Warehouse": inv.warehouse_id,
209
+ "Part": part_map.get(inv.part_id, inv.part_id),
210
+ "Qty": inv.quantity,
211
+ "Reorder Pt": inv.reorder_point,
212
+ "Safety Stock": inv.safety_stock,
213
+ }
214
+ for inv in problem.inventory
215
+ ]
216
+
217
+
218
+ def requests_to_table_rows(problem: ProblemInstance) -> list[dict[str, Any]]:
219
+ return [
220
+ {
221
+ "Request": r.request_id,
222
+ "Site": r.site_name,
223
+ "Priority": r.priority,
224
+ "Emergency": "Yes" if r.is_emergency else "No",
225
+ "Part Type": r.required_part_type,
226
+ "Skill": r.required_skill,
227
+ "SLA (h)": r.sla_deadline_hours,
228
+ }
229
+ for r in problem.requests
230
+ ]
gradio/src/fieldops/inventory_optimization.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multi-echelon inventory optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ from fieldops.models import InventoryLevel, ProblemInstance, SparePart, Stage1Plan, Warehouse
8
+
9
+
10
+ def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
11
+ r = 6371.0
12
+ p1, p2 = math.radians(lat1), math.radians(lat2)
13
+ dp = math.radians(lat2 - lat1)
14
+ dl = math.radians(lon2 - lon1)
15
+ a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
16
+ return 2 * r * math.asin(math.sqrt(a))
17
+
18
+
19
+ def optimize_inventory(
20
+ problem: ProblemInstance,
21
+ stock_vs_expedite: float = 0.5,
22
+ ) -> Stage1Plan:
23
+ """Heuristic multi-echelon inventory targeting with stock vs expedite trade-off."""
24
+ part_map: dict[str, SparePart] = {p.part_id: p for p in problem.spare_parts}
25
+ demand_by_part: dict[str, float] = {}
26
+ for req in problem.requests:
27
+ for part in problem.spare_parts:
28
+ if part.part_type == req.required_part_type:
29
+ demand_by_part[part.part_id] = demand_by_part.get(part.part_id, 0) + 1
30
+ break
31
+
32
+ targets: list[InventoryLevel] = []
33
+ total_holding = 0.0
34
+ stock_bias = stock_vs_expedite
35
+
36
+ for wh in problem.warehouses:
37
+ for part_id, demand in demand_by_part.items():
38
+ part = part_map[part_id]
39
+ base_stock = max(2, int(math.ceil(demand * (0.8 + stock_bias * 0.6))))
40
+ safety = max(1, int(base_stock * (0.3 + stock_bias * 0.2)))
41
+ reorder = max(1, base_stock // 2)
42
+ targets.append(InventoryLevel(
43
+ warehouse_id=wh.warehouse_id,
44
+ part_id=part_id,
45
+ quantity=base_stock,
46
+ reorder_point=reorder,
47
+ safety_stock=safety,
48
+ ))
49
+ total_holding += base_stock * part.holding_cost_per_day * problem.planning_horizon_days
50
+
51
+ tech_positions: dict[str, tuple[float, float]] = {}
52
+ region_sites: dict[str, list[tuple[float, float]]] = {}
53
+ for site in problem.sites:
54
+ region_sites.setdefault(site.site_id[:2], []).append((site.lat, site.lon))
55
+
56
+ for tech in problem.technicians:
57
+ site_coords = [(s.lat, s.lon) for s in problem.sites if s.site_id.startswith("S")]
58
+ if site_coords:
59
+ avg_lat = sum(c[0] for c in site_coords) / len(site_coords)
60
+ avg_lon = sum(c[1] for c in site_coords) / len(site_coords)
61
+ tech_positions[tech.technician_id] = (avg_lat, avg_lon)
62
+ else:
63
+ tech_positions[tech.technician_id] = (tech.home_lat, tech.home_lon)
64
+
65
+ contracts = []
66
+ for wh in problem.warehouses:
67
+ for part in problem.spare_parts[:3]:
68
+ contracts.append({
69
+ "warehouse": wh.warehouse_id,
70
+ "part": part.part_id,
71
+ "min_order_qty": 5,
72
+ "lead_time_days": 2 if stock_bias > 0.5 else 5,
73
+ "expedite_available": stock_bias < 0.7,
74
+ })
75
+
76
+ notes = [
77
+ f"Stock vs expedite ratio: {stock_vs_expedite:.0%} (higher = more pre-positioned inventory).",
78
+ f"Multi-echelon targets across {len(problem.warehouses)} warehouses.",
79
+ f"Estimated holding cost: ${total_holding:,.0f} over {problem.planning_horizon_days} days.",
80
+ ]
81
+
82
+ return Stage1Plan(
83
+ technician_positions=tech_positions,
84
+ inventory_targets=targets,
85
+ supply_contracts=contracts,
86
+ repair_shop_capacity=max(2, len(problem.technicians) // 3),
87
+ total_holding_cost=total_holding,
88
+ notes=notes,
89
+ )
90
+
91
+
92
+ def find_nearest_warehouse(
93
+ warehouses: list[Warehouse],
94
+ inventory: list[InventoryLevel],
95
+ part_id: str,
96
+ lat: float,
97
+ lon: float,
98
+ ) -> tuple[Warehouse, int, float]:
99
+ """Return nearest warehouse with stock for part_id."""
100
+ inv_map = {(i.warehouse_id, i.part_id): i.quantity for i in inventory}
101
+ wh_map = {w.warehouse_id: w for w in warehouses}
102
+ best_wh, best_qty, best_dist = None, 0, float("inf")
103
+
104
+ for wh in warehouses:
105
+ qty = inv_map.get((wh.warehouse_id, part_id), 0)
106
+ if qty <= 0:
107
+ continue
108
+ dist = _haversine_km(lat, lon, wh.lat, wh.lon)
109
+ if dist < best_dist:
110
+ best_wh, best_qty, best_dist = wh, qty, dist
111
+
112
+ if best_wh is None:
113
+ best_wh = min(warehouses, key=lambda w: _haversine_km(lat, lon, w.lat, w.lon))
114
+ best_qty = 0
115
+ best_dist = _haversine_km(lat, lon, best_wh.lat, best_wh.lon)
116
+
117
+ return best_wh, best_qty, best_dist
gradio/src/fieldops/metrics.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """KPI computation for field service operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fieldops.models import KPIMetrics, MLPrediction, ProblemInstance, Stage1Plan
6
+
7
+
8
+ def compute_metrics(
9
+ problem: ProblemInstance,
10
+ assignments: list[dict],
11
+ stage1: Stage1Plan,
12
+ predictions: list[MLPrediction],
13
+ ) -> KPIMetrics:
14
+ pred_map = {p.request_id: p for p in predictions}
15
+ part_map = {p.part_id: p for p in problem.spare_parts}
16
+ site_map = {s.site_id: s for s in problem.sites}
17
+ req_map = {r.request_id: r for r in problem.requests}
18
+
19
+ sla_met = 0
20
+ total_mttr = 0.0
21
+ ftf_count = 0
22
+ stockouts = 0
23
+ expedite_count = 0
24
+ logistics_cost = 0.0
25
+ downtime_cost = 0.0
26
+
27
+ for a in assignments:
28
+ req = req_map[a["request_id"]]
29
+ pred = pred_map[a["request_id"]]
30
+ site = site_map.get(req.site_id)
31
+ downtime_rate = site.downtime_cost_per_hour if site else 500
32
+
33
+ duration = pred.predicted_duration_hours
34
+ travel = 0.5 + (req.lat % 1) * 2
35
+ mttr = duration + travel
36
+ total_mttr += mttr
37
+
38
+ if mttr <= req.sla_deadline_hours:
39
+ sla_met += 1
40
+
41
+ if pred.first_time_fix_probability > 0.7:
42
+ ftf_count += 1
43
+
44
+ if a.get("part_source") == "expedite_shipment":
45
+ expedite_count += 1
46
+ part = part_map.get(a["part_id"])
47
+ logistics_cost += (part.expedite_cost if part else 300)
48
+ elif a.get("part_source") == "warehouse_stock":
49
+ part = part_map.get(a["part_id"])
50
+ logistics_cost += (part.unit_cost * 0.1 if part else 50)
51
+ else:
52
+ logistics_cost += 80
53
+
54
+ if a.get("part_source") in ("expedite_shipment",) and a.get("defer"):
55
+ stockouts += 1
56
+
57
+ downtime_cost += mttr * downtime_rate * pred.failure_probability
58
+
59
+ n = max(len(assignments), 1)
60
+ tech_jobs: dict[str, int] = {}
61
+ for a in assignments:
62
+ tech_jobs[a["technician_id"]] = tech_jobs.get(a["technician_id"], 0) + 1
63
+ max_capacity = sum(t.max_jobs_per_day for t in problem.technicians)
64
+ utilization = sum(tech_jobs.values()) / max(max_capacity, 1) * 100
65
+
66
+ return KPIMetrics(
67
+ sla_compliance_pct=round(sla_met / n * 100, 1),
68
+ mean_time_to_repair_hours=round(total_mttr / n, 2),
69
+ first_time_fix_rate_pct=round(ftf_count / n * 100, 1),
70
+ emergency_stockout_count=stockouts,
71
+ technician_utilization_pct=round(utilization, 1),
72
+ total_logistics_cost=round(logistics_cost, 2),
73
+ total_downtime_cost=round(downtime_cost, 2),
74
+ total_holding_cost=round(stage1.total_holding_cost, 2),
75
+ expedite_shipments=expedite_count,
76
+ )
gradio/src/fieldops/ml_predictions.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pre-trained ML prediction module (no training — fixed coefficients)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ from fieldops.constants import PART_TYPES, STRESS_SCENARIOS
8
+ from fieldops.models import MLPrediction, ProblemInstance, ServiceRequest
9
+
10
+ # Pre-trained logistic coefficients (simulating LightGBM export)
11
+ _COEFFS = {
12
+ "intercept": -2.1,
13
+ "age_factor": 0.35,
14
+ "criticality": {"critical": 1.2, "high": 0.7, "medium": 0.3, "low": 0.0},
15
+ "emergency": 0.9,
16
+ "wear_types": {"mechanical": 0.4, "electrical": 0.25, "sensor": 0.15, "wear": 0.5},
17
+ }
18
+
19
+ _DURATION_BASE = {"mechanical": 3.2, "electrical": 2.8, "sensor": 1.5, "wear": 4.0}
20
+ _FTF_BASE = {"electrical": 0.82, "mechanical": 0.75, "sensor": 0.88, "wear": 0.70}
21
+
22
+
23
+ def _sigmoid(x: float) -> float:
24
+ return 1.0 / (1.0 + math.exp(-max(-20, min(20, x))))
25
+
26
+
27
+ def _site_criticality(problem: ProblemInstance, site_id: str) -> str:
28
+ for s in problem.sites:
29
+ if s.site_id == site_id:
30
+ return s.criticality
31
+ return "medium"
32
+
33
+
34
+ def predict_request(problem: ProblemInstance, request: ServiceRequest) -> MLPrediction:
35
+ stress = STRESS_SCENARIOS.get(problem.stress_scenario, STRESS_SCENARIOS["baseline"])
36
+ crit = _site_criticality(problem, request.site_id)
37
+
38
+ logit = _COEFFS["intercept"]
39
+ logit += _COEFFS["criticality"].get(crit, 0.3)
40
+ logit += _COEFFS["wear_types"].get(request.failure_type, 0.2)
41
+ if request.is_emergency:
42
+ logit += _COEFFS["emergency"]
43
+ logit *= stress["failure_multiplier"]
44
+
45
+ failure_prob = _sigmoid(logit)
46
+ duration = _DURATION_BASE.get(request.failure_type, 3.0) * (0.8 + 0.4 * (1 - failure_prob))
47
+ ftf = _FTF_BASE.get(request.failure_type, 0.78) * (0.9 + 0.1 * (1 - failure_prob))
48
+ revisit = max(0.02, (1 - ftf) * 0.85)
49
+
50
+ part_idx = hash(request.failure_type + request.required_part_type) % len(PART_TYPES)
51
+ predicted_part = PART_TYPES[part_idx]
52
+
53
+ if failure_prob >= 0.6:
54
+ risk_tier = "high"
55
+ elif failure_prob >= 0.35:
56
+ risk_tier = "medium"
57
+ else:
58
+ risk_tier = "low"
59
+
60
+ return MLPrediction(
61
+ request_id=request.request_id,
62
+ site_id=request.site_id,
63
+ failure_probability=round(failure_prob, 4),
64
+ predicted_part_type=predicted_part,
65
+ predicted_duration_hours=round(duration, 2),
66
+ first_time_fix_probability=round(min(ftf, 0.98), 4),
67
+ revisit_probability=round(revisit, 4),
68
+ confidence=round(0.75 + 0.2 * (1 - abs(failure_prob - 0.5)), 4),
69
+ risk_tier=risk_tier,
70
+ )
71
+
72
+
73
+ def predict_all(problem: ProblemInstance) -> list[MLPrediction]:
74
+ return [predict_request(problem, r) for r in problem.requests]
75
+
76
+
77
+ def predictions_to_table_rows(predictions: list[MLPrediction]) -> list[dict]:
78
+ return [
79
+ {
80
+ "Request": p.request_id,
81
+ "P(Failure)": f"{p.failure_probability:.1%}",
82
+ "Part Type": p.predicted_part_type,
83
+ "Duration (h)": p.predicted_duration_hours,
84
+ "FTF": f"{p.first_time_fix_probability:.1%}",
85
+ "Revisit": f"{p.revisit_probability:.1%}",
86
+ "Risk": p.risk_tier,
87
+ }
88
+ for p in predictions
89
+ ]
gradio/src/fieldops/models.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data models for field service optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class Warehouse:
11
+ warehouse_id: str
12
+ name: str
13
+ region: str
14
+ lat: float
15
+ lon: float
16
+ capacity_units: int
17
+ expedite_cost_per_unit: float
18
+
19
+ def to_dict(self) -> dict[str, Any]:
20
+ return asdict(self)
21
+
22
+
23
+ @dataclass
24
+ class SparePart:
25
+ part_id: str
26
+ name: str
27
+ part_type: str
28
+ unit_cost: float
29
+ holding_cost_per_day: float
30
+ expedite_cost: float
31
+ substitute_part_id: str | None = None
32
+
33
+ def to_dict(self) -> dict[str, Any]:
34
+ return asdict(self)
35
+
36
+
37
+ @dataclass
38
+ class InventoryLevel:
39
+ warehouse_id: str
40
+ part_id: str
41
+ quantity: int
42
+ reorder_point: int
43
+ safety_stock: int
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ return asdict(self)
47
+
48
+
49
+ @dataclass
50
+ class Technician:
51
+ technician_id: str
52
+ name: str
53
+ skills: list[str]
54
+ skill_level: float
55
+ hourly_rate: float
56
+ home_lat: float
57
+ home_lon: float
58
+ max_jobs_per_day: int
59
+ region: str
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ return asdict(self)
63
+
64
+
65
+ @dataclass
66
+ class ServiceSite:
67
+ site_id: str
68
+ name: str
69
+ asset_type: str
70
+ criticality: str
71
+ lat: float
72
+ lon: float
73
+ sla_hours: float
74
+ downtime_cost_per_hour: float
75
+ contract_tier: str
76
+
77
+ def to_dict(self) -> dict[str, Any]:
78
+ return asdict(self)
79
+
80
+
81
+ @dataclass
82
+ class ServiceRequest:
83
+ request_id: str
84
+ site_id: str
85
+ site_name: str
86
+ priority: str
87
+ failure_type: str
88
+ required_part_type: str
89
+ required_skill: str
90
+ estimated_duration_hours: float
91
+ is_emergency: bool
92
+ sla_deadline_hours: float
93
+ lat: float
94
+ lon: float
95
+
96
+ def to_dict(self) -> dict[str, Any]:
97
+ return asdict(self)
98
+
99
+
100
+ @dataclass
101
+ class MLPrediction:
102
+ request_id: str
103
+ site_id: str
104
+ failure_probability: float
105
+ predicted_part_type: str
106
+ predicted_duration_hours: float
107
+ first_time_fix_probability: float
108
+ revisit_probability: float
109
+ confidence: float
110
+ risk_tier: str
111
+
112
+ def to_dict(self) -> dict[str, Any]:
113
+ return asdict(self)
114
+
115
+
116
+ @dataclass
117
+ class Stage1Plan:
118
+ technician_positions: dict[str, tuple[float, float]]
119
+ inventory_targets: list[InventoryLevel]
120
+ supply_contracts: list[dict[str, Any]]
121
+ repair_shop_capacity: int
122
+ total_holding_cost: float
123
+ notes: list[str] = field(default_factory=list)
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ return {
127
+ "technician_positions": self.technician_positions,
128
+ "inventory_targets": [i.to_dict() for i in self.inventory_targets],
129
+ "supply_contracts": self.supply_contracts,
130
+ "repair_shop_capacity": self.repair_shop_capacity,
131
+ "total_holding_cost": self.total_holding_cost,
132
+ "notes": self.notes,
133
+ }
134
+
135
+
136
+ @dataclass
137
+ class MissionAssignment:
138
+ request_id: str
139
+ technician_id: str
140
+ technician_name: str
141
+ warehouse_id: str
142
+ part_id: str
143
+ part_source: str
144
+ route_order: int
145
+ scheduled_start_hours: float
146
+ estimated_arrival_hours: float
147
+ estimated_completion_hours: float
148
+ use_substitute: bool
149
+ defer_to_later: bool
150
+ rationale: str
151
+
152
+ def to_dict(self) -> dict[str, Any]:
153
+ return asdict(self)
154
+
155
+
156
+ @dataclass
157
+ class KPIMetrics:
158
+ sla_compliance_pct: float
159
+ mean_time_to_repair_hours: float
160
+ first_time_fix_rate_pct: float
161
+ emergency_stockout_count: int
162
+ technician_utilization_pct: float
163
+ total_logistics_cost: float
164
+ total_downtime_cost: float
165
+ total_holding_cost: float
166
+ expedite_shipments: int
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ return asdict(self)
170
+
171
+
172
+ @dataclass
173
+ class SimulationResult:
174
+ replications: int
175
+ sla_compliance_mean: float
176
+ mttr_mean: float
177
+ stockout_rate: float
178
+ avg_logistics_cost: float
179
+ avg_downtime_cost: float
180
+ confidence_interval_95: dict[str, tuple[float, float]]
181
+
182
+ def to_dict(self) -> dict[str, Any]:
183
+ return asdict(self)
184
+
185
+
186
+ @dataclass
187
+ class OptimizationResult:
188
+ strategy: str
189
+ algorithm: str
190
+ stage1_plan: Stage1Plan
191
+ assignments: list[MissionAssignment]
192
+ ml_predictions: list[MLPrediction]
193
+ metrics: KPIMetrics
194
+ simulation: SimulationResult | None
195
+ solve_time_sec: float
196
+ status: str
197
+ notes: list[str] = field(default_factory=list)
198
+
199
+ def to_dict(self) -> dict[str, Any]:
200
+ return {
201
+ "strategy": self.strategy,
202
+ "algorithm": self.algorithm,
203
+ "stage1_plan": self.stage1_plan.to_dict(),
204
+ "assignments": [a.to_dict() for a in self.assignments],
205
+ "ml_predictions": [p.to_dict() for p in self.ml_predictions],
206
+ "metrics": self.metrics.to_dict(),
207
+ "simulation": self.simulation.to_dict() if self.simulation else None,
208
+ "solve_time_sec": self.solve_time_sec,
209
+ "status": self.status,
210
+ "notes": self.notes,
211
+ }
212
+
213
+
214
+ @dataclass
215
+ class ProblemInstance:
216
+ industry_id: str
217
+ stress_scenario: str
218
+ scenario_label: str
219
+ planning_horizon_days: int
220
+ warehouses: list[Warehouse]
221
+ spare_parts: list[SparePart]
222
+ inventory: list[InventoryLevel]
223
+ technicians: list[Technician]
224
+ sites: list[ServiceSite]
225
+ requests: list[ServiceRequest]
226
+ stock_vs_expedite_ratio: float = 0.5
227
+ reliability_target: float = 0.95
228
+
229
+ def to_dict(self) -> dict[str, Any]:
230
+ return {
231
+ "industry_id": self.industry_id,
232
+ "stress_scenario": self.stress_scenario,
233
+ "scenario_label": self.scenario_label,
234
+ "planning_horizon_days": self.planning_horizon_days,
235
+ "warehouses": [w.to_dict() for w in self.warehouses],
236
+ "spare_parts": [p.to_dict() for p in self.spare_parts],
237
+ "inventory": [i.to_dict() for i in self.inventory],
238
+ "technicians": [t.to_dict() for t in self.technicians],
239
+ "sites": [s.to_dict() for s in self.sites],
240
+ "requests": [r.to_dict() for r in self.requests],
241
+ "stock_vs_expedite_ratio": self.stock_vs_expedite_ratio,
242
+ "reliability_target": self.reliability_target,
243
+ }
gradio/src/fieldops/pipeline.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Demo pipeline for Hugging Face Space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from fieldops.constants import ENGINE_VERSION, INDUSTRY_SCENARIOS, STRESS_SCENARIOS, STRATEGIES
10
+ from fieldops.engine import FieldOpsEngine
11
+ from fieldops.generator import generate_problem
12
+ from fieldops.models import OptimizationResult, ProblemInstance
13
+
14
+
15
+ class FieldOpsPipeline:
16
+ """Load pre-built benchmarks and serve interactive field service optimization."""
17
+
18
+ def __init__(self, assets_dir: Path | None = None):
19
+ self.assets_dir = Path(assets_dir) if assets_dir else None
20
+ self.engine = FieldOpsEngine()
21
+ self.summary: dict[str, Any] = {}
22
+ self.benchmarks: list[dict[str, Any]] = []
23
+
24
+ def load(self) -> None:
25
+ if self.assets_dir:
26
+ summary_path = self.assets_dir / "demo" / "summary.json"
27
+ bench_path = self.assets_dir / "demo" / "benchmarks.json"
28
+ if summary_path.exists():
29
+ self.summary = json.loads(summary_path.read_text(encoding="utf-8"))
30
+ if bench_path.exists():
31
+ self.benchmarks = json.loads(bench_path.read_text(encoding="utf-8"))
32
+
33
+ def get_industry_ids(self) -> list[str]:
34
+ return list(INDUSTRY_SCENARIOS.keys())
35
+
36
+ def get_stress_ids(self) -> list[str]:
37
+ return list(STRESS_SCENARIOS.keys())
38
+
39
+ def get_strategy_ids(self) -> list[str]:
40
+ return STRATEGIES
41
+
42
+ def get_industry_label(self, industry_id: str) -> str:
43
+ return INDUSTRY_SCENARIOS.get(industry_id, {}).get("label", industry_id)
44
+
45
+ def get_stress_label(self, stress_id: str) -> str:
46
+ return STRESS_SCENARIOS.get(stress_id, {}).get("label", stress_id)
47
+
48
+ def get_strategy_label(self, strategy_id: str) -> str:
49
+ labels = {
50
+ "two_stage_stochastic": "Two-Stage Stochastic Programming",
51
+ "multi_echelon_inventory": "Multi-Echelon Inventory Optimization",
52
+ "technician_routing": "Technician Routing (OR-Tools)",
53
+ "sample_average_approximation": "Sample Average Approximation (SAA)",
54
+ "alns_field_ops": "ALNS Field Operations",
55
+ "stock_vs_expedite": "Stock vs Expedite Trade-off",
56
+ "monte_carlo": "Monte Carlo Simulation",
57
+ }
58
+ return labels.get(strategy_id, strategy_id)
59
+
60
+ def build_problem(
61
+ self,
62
+ industry_id: str,
63
+ stress_id: str = "baseline",
64
+ num_sites: int | None = None,
65
+ stock_vs_expedite: float = 0.5,
66
+ reliability_target: float = 0.95,
67
+ seed: int = 42,
68
+ ) -> ProblemInstance:
69
+ meta = INDUSTRY_SCENARIOS[industry_id]
70
+ return generate_problem(
71
+ industry_id=industry_id,
72
+ stress_id=stress_id,
73
+ num_sites=num_sites or meta["default_sites"],
74
+ stock_vs_expedite=stock_vs_expedite,
75
+ reliability_target=reliability_target,
76
+ seed=seed,
77
+ )
78
+
79
+ def run_optimization(
80
+ self,
81
+ problem: ProblemInstance,
82
+ strategy: str,
83
+ ) -> OptimizationResult:
84
+ return self.engine.optimize(problem, strategy, run_simulation=True)
85
+
86
+ def run_comparison(self, problem: ProblemInstance) -> list[OptimizationResult]:
87
+ return self.engine.compare_strategies(problem)
88
+
89
+ def run_stock_expedite_analysis(self, problem: ProblemInstance) -> list[OptimizationResult]:
90
+ return self.engine.compare_stock_expedite(problem)
91
+
92
+ @property
93
+ def version(self) -> str:
94
+ return self.summary.get("version", ENGINE_VERSION)
gradio/src/fieldops/routing.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Technician routing with OR-Tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from fieldops.models import MissionAssignment, MLPrediction, ProblemInstance, ServiceRequest, Technician
9
+
10
+ try:
11
+ from ortools.constraint_solver import pywrapcp, routing_enums_pb2
12
+ ORTOOLS_AVAILABLE = True
13
+ except ImportError:
14
+ ORTOOLS_AVAILABLE = False
15
+
16
+
17
+ def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
18
+ r = 6371.0
19
+ p1, p2 = math.radians(lat1), math.radians(lat2)
20
+ dp = math.radians(lat2 - lat1)
21
+ dl = math.radians(lon2 - lon1)
22
+ a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
23
+ return 2 * r * math.asin(math.sqrt(a))
24
+
25
+
26
+ def _travel_hours(dist_km: float, speed_kmh: float = 60.0) -> float:
27
+ return dist_km / speed_kmh
28
+
29
+
30
+ def _greedy_route(
31
+ tech: Technician,
32
+ requests: list[ServiceRequest],
33
+ predictions: dict[str, MLPrediction],
34
+ ) -> list[tuple[ServiceRequest, float, float]]:
35
+ """Greedy nearest-neighbor fallback routing."""
36
+ remaining = sorted(
37
+ requests,
38
+ key=lambda r: (0 if r.is_emergency else 1, -predictions[r.request_id].failure_probability),
39
+ )
40
+ route: list[tuple[ServiceRequest, float, float]] = []
41
+ cur_lat, cur_lon = tech.home_lat, tech.home_lon
42
+ elapsed = 0.0
43
+
44
+ for req in remaining[: tech.max_jobs_per_day]:
45
+ dist = _haversine_km(cur_lat, cur_lon, req.lat, req.lon)
46
+ travel = _travel_hours(dist)
47
+ arrival = elapsed + travel
48
+ duration = predictions[req.request_id].predicted_duration_hours
49
+ route.append((req, arrival, arrival + duration))
50
+ elapsed = arrival + duration
51
+ cur_lat, cur_lon = req.lat, req.lon
52
+
53
+ return route
54
+
55
+
56
+ def solve_routing(
57
+ problem: ProblemInstance,
58
+ predictions: list[MLPrediction],
59
+ assignments_meta: list[dict[str, Any]],
60
+ ) -> list[MissionAssignment]:
61
+ """Assign requests to technicians with routing."""
62
+ pred_map = {p.request_id: p for p in predictions}
63
+ meta_map = {m["request_id"]: m for m in assignments_meta}
64
+ results: list[MissionAssignment] = []
65
+
66
+ tech_requests: dict[str, list[ServiceRequest]] = {t.technician_id: [] for t in problem.technicians}
67
+ for req in problem.requests:
68
+ meta = meta_map.get(req.request_id)
69
+ if meta:
70
+ tech_requests[meta["technician_id"]].append(req)
71
+
72
+ for tech in problem.technicians:
73
+ reqs = tech_requests[tech.technician_id]
74
+ if not reqs:
75
+ continue
76
+
77
+ if ORTOOLS_AVAILABLE and len(reqs) <= 12:
78
+ route = _ortools_route(tech, reqs, pred_map)
79
+ else:
80
+ route = _greedy_route(tech, reqs, pred_map)
81
+
82
+ for order, (req, arrival, completion) in enumerate(route, 1):
83
+ meta = meta_map[req.request_id]
84
+ results.append(MissionAssignment(
85
+ request_id=req.request_id,
86
+ technician_id=tech.technician_id,
87
+ technician_name=tech.name,
88
+ warehouse_id=meta["warehouse_id"],
89
+ part_id=meta["part_id"],
90
+ part_source=meta["part_source"],
91
+ route_order=order,
92
+ scheduled_start_hours=arrival - _travel_hours(
93
+ _haversine_km(tech.home_lat, tech.home_lon, req.lat, req.lon)
94
+ ),
95
+ estimated_arrival_hours=arrival,
96
+ estimated_completion_hours=completion,
97
+ use_substitute=meta.get("use_substitute", False),
98
+ defer_to_later=meta.get("defer", False),
99
+ rationale=meta.get("rationale", "Optimized assignment."),
100
+ ))
101
+
102
+ return results
103
+
104
+
105
+ def _ortools_route(
106
+ tech: Technician,
107
+ requests: list[ServiceRequest],
108
+ pred_map: dict[str, MLPrediction],
109
+ ) -> list[tuple[ServiceRequest, float, float]]:
110
+ """OR-Tools VRP for single technician."""
111
+ n = len(requests)
112
+ if n == 0:
113
+ return []
114
+
115
+ locations = [(tech.home_lat, tech.home_lon)] + [(r.lat, r.lon) for r in requests]
116
+ size = n + 1
117
+
118
+ def dist_matrix(i: int, j: int) -> int:
119
+ lat1, lon1 = locations[i]
120
+ lat2, lon2 = locations[j]
121
+ return int(_haversine_km(lat1, lon1, lat2, lon2) * 100)
122
+
123
+ manager = pywrapcp.RoutingIndexManager(size, 1, 0)
124
+ routing = pywrapcp.RoutingModel(manager)
125
+
126
+ def distance_callback(from_idx: int, to_idx: int) -> int:
127
+ return dist_matrix(manager.IndexToNode(from_idx), manager.IndexToNode(to_idx))
128
+
129
+ transit_idx = routing.RegisterTransitCallback(distance_callback)
130
+ routing.SetArcCostEvaluatorOfAllVehicles(transit_idx)
131
+
132
+ for i, req in enumerate(requests, 1):
133
+ if req.is_emergency:
134
+ routing.AddDisjunction([manager.NodeToIndex(i)], 0)
135
+
136
+ search_params = pywrapcp.DefaultRoutingSearchParameters()
137
+ search_params.first_solution_strategy = (
138
+ routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
139
+ )
140
+ search_params.time_limit.seconds = 5
141
+
142
+ solution = routing.SolveWithParameters(search_params)
143
+ if not solution:
144
+ return _greedy_route(tech, requests, pred_map)
145
+
146
+ route_order: list[int] = []
147
+ index = routing.Start(0)
148
+ while not routing.IsEnd(index):
149
+ node = manager.IndexToNode(index)
150
+ if node > 0:
151
+ route_order.append(node - 1)
152
+ index = solution.Value(routing.NextVar(index))
153
+
154
+ ordered = [requests[i] for i in route_order]
155
+ cur_lat, cur_lon = tech.home_lat, tech.home_lon
156
+ elapsed = 0.0
157
+ result: list[tuple[ServiceRequest, float, float]] = []
158
+ for req in ordered:
159
+ dist = _haversine_km(cur_lat, cur_lon, req.lat, req.lon)
160
+ travel = _travel_hours(dist)
161
+ arrival = elapsed + travel
162
+ duration = pred_map[req.request_id].predicted_duration_hours
163
+ result.append((req, arrival, arrival + duration))
164
+ elapsed = arrival + duration
165
+ cur_lat, cur_lon = req.lat, req.lon
166
+
167
+ return result
gradio/src/fieldops/scheduling.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Service scheduling and mission assignment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fieldops.inventory_optimization import find_nearest_warehouse
6
+ from fieldops.models import MLPrediction, ProblemInstance, ServiceRequest, Technician
7
+
8
+
9
+ def _skill_match(tech: Technician, skill: str) -> float:
10
+ if skill in tech.skills:
11
+ return tech.skill_level
12
+ return tech.skill_level * 0.4
13
+
14
+
15
+ def assign_missions(
16
+ problem: ProblemInstance,
17
+ predictions: list[MLPrediction],
18
+ ) -> list[dict]:
19
+ """Greedy skill-based assignment with parts sourcing."""
20
+ pred_map = {p.request_id: p for p in predictions}
21
+ part_map = {p.part_type: p for p in problem.spare_parts}
22
+ part_by_id = {p.part_id: p for p in problem.spare_parts}
23
+
24
+ sorted_requests = sorted(
25
+ problem.requests,
26
+ key=lambda r: (
27
+ 0 if r.is_emergency else 1,
28
+ r.priority,
29
+ -pred_map[r.request_id].failure_probability,
30
+ ),
31
+ )
32
+
33
+ tech_load: dict[str, int] = {t.technician_id: 0 for t in problem.technicians}
34
+ assignments: list[dict] = []
35
+
36
+ for req in sorted_requests:
37
+ pred = pred_map[req.request_id]
38
+ best_tech, best_score = None, -1.0
39
+
40
+ for tech in problem.technicians:
41
+ if tech_load[tech.technician_id] >= tech.max_jobs_per_day:
42
+ continue
43
+ score = _skill_match(tech, req.required_skill)
44
+ if req.is_emergency and tech.region in ("north", "central"):
45
+ score *= 1.1
46
+ if score > best_score:
47
+ best_score = score
48
+ best_tech = tech
49
+
50
+ if best_tech is None:
51
+ best_tech = min(problem.technicians, key=lambda t: tech_load[t.technician_id])
52
+
53
+ part = part_map.get(req.required_part_type) or problem.spare_parts[0]
54
+ wh, qty, _ = find_nearest_warehouse(
55
+ problem.warehouses, problem.inventory, part.part_id, req.lat, req.lon,
56
+ )
57
+
58
+ use_substitute = qty == 0
59
+ part_source = "warehouse_stock"
60
+ defer = False
61
+
62
+ if qty == 0:
63
+ if problem.stock_vs_expedite_ratio < 0.5:
64
+ part_source = "expedite_shipment"
65
+ else:
66
+ sub_part = part_by_id.get(part.substitute_part_id or "")
67
+ if sub_part:
68
+ wh2, qty2, _ = find_nearest_warehouse(
69
+ problem.warehouses, problem.inventory,
70
+ sub_part.part_id, req.lat, req.lon,
71
+ )
72
+ if qty2 > 0:
73
+ part = sub_part
74
+ wh = wh2
75
+ use_substitute = True
76
+ part_source = "substitute_part"
77
+ else:
78
+ part_source = "expedite_shipment"
79
+ else:
80
+ part_source = "expedite_shipment"
81
+
82
+ if not req.is_emergency and pred.failure_probability < 0.25 and tech_load[best_tech.technician_id] >= 2:
83
+ defer = True
84
+ part_source = "deferred"
85
+
86
+ tech_load[best_tech.technician_id] += 1
87
+ rationale = (
88
+ f"{'Emergency' if req.is_emergency else 'Scheduled'} — "
89
+ f"skill match {best_score:.0%}, part via {part_source}"
90
+ )
91
+ if defer:
92
+ rationale += " (deferred to next window)"
93
+
94
+ assignments.append({
95
+ "request_id": req.request_id,
96
+ "technician_id": best_tech.technician_id,
97
+ "warehouse_id": wh.warehouse_id,
98
+ "part_id": part.part_id,
99
+ "part_source": part_source,
100
+ "use_substitute": use_substitute,
101
+ "defer": defer,
102
+ "rationale": rationale,
103
+ })
104
+
105
+ return assignments
gradio/src/fieldops/simulation.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Monte Carlo simulation with SimPy fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ import statistics
7
+
8
+ from fieldops.metrics import compute_metrics
9
+ from fieldops.models import MissionAssignment, ProblemInstance, SimulationResult, Stage1Plan, MLPrediction
10
+
11
+
12
+ def run_monte_carlo(
13
+ problem: ProblemInstance,
14
+ assignments: list[MissionAssignment],
15
+ stage1: Stage1Plan,
16
+ predictions: list[MLPrediction],
17
+ replications: int = 30,
18
+ seed: int = 42,
19
+ ) -> SimulationResult:
20
+ """Monte Carlo validation of field service plan."""
21
+ rng = random.Random(seed)
22
+ assign_dicts = [
23
+ {
24
+ "request_id": a.request_id,
25
+ "technician_id": a.technician_id,
26
+ "warehouse_id": a.warehouse_id,
27
+ "part_id": a.part_id,
28
+ "part_source": a.part_source,
29
+ "use_substitute": a.use_substitute,
30
+ "defer": a.defer_to_later,
31
+ }
32
+ for a in assignments
33
+ ]
34
+
35
+ sla_vals, mttr_vals, stockout_vals, log_vals, down_vals = [], [], [], [], []
36
+
37
+ for rep in range(replications):
38
+ perturbed = []
39
+ for i, pred in enumerate(predictions):
40
+ noise = rng.gauss(0, 0.15)
41
+ adj = MLPrediction(
42
+ request_id=pred.request_id,
43
+ site_id=pred.site_id,
44
+ failure_probability=min(0.99, max(0.01, pred.failure_probability + noise)),
45
+ predicted_part_type=pred.predicted_part_type,
46
+ predicted_duration_hours=max(0.5, pred.predicted_duration_hours * (1 + rng.gauss(0, 0.1))),
47
+ first_time_fix_probability=min(0.99, max(0.3, pred.first_time_fix_probability + rng.gauss(0, 0.05))),
48
+ revisit_probability=pred.revisit_probability,
49
+ confidence=pred.confidence,
50
+ risk_tier=pred.risk_tier,
51
+ )
52
+ perturbed.append(adj)
53
+
54
+ m = compute_metrics(problem, assign_dicts, stage1, perturbed)
55
+ sla_vals.append(m.sla_compliance_pct)
56
+ mttr_vals.append(m.mean_time_to_repair_hours * (1 + rng.uniform(-0.1, 0.15)))
57
+ stockout_vals.append(m.emergency_stockout_count / max(len(assignments), 1))
58
+ log_vals.append(m.total_logistics_cost * (1 + rng.uniform(-0.05, 0.1)))
59
+ down_vals.append(m.total_downtime_cost * (1 + rng.uniform(-0.08, 0.12)))
60
+
61
+ def ci(vals: list[float]) -> tuple[float, float]:
62
+ if len(vals) < 2:
63
+ v = vals[0] if vals else 0.0
64
+ return (v, v)
65
+ mean = statistics.mean(vals)
66
+ stdev = statistics.stdev(vals)
67
+ margin = 1.96 * stdev / (len(vals) ** 0.5)
68
+ return (round(mean - margin, 2), round(mean + margin, 2))
69
+
70
+ return SimulationResult(
71
+ replications=replications,
72
+ sla_compliance_mean=round(statistics.mean(sla_vals), 1),
73
+ mttr_mean=round(statistics.mean(mttr_vals), 2),
74
+ stockout_rate=round(statistics.mean(stockout_vals), 4),
75
+ avg_logistics_cost=round(statistics.mean(log_vals), 2),
76
+ avg_downtime_cost=round(statistics.mean(down_vals), 2),
77
+ confidence_interval_95={
78
+ "sla_compliance": ci(sla_vals),
79
+ "mttr": ci(mttr_vals),
80
+ "logistics_cost": ci(log_vals),
81
+ },
82
+ )
gradio/src/fieldops/stochastic.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Two-stage stochastic programming and Sample Average Approximation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Any
7
+
8
+ from fieldops.inventory_optimization import optimize_inventory
9
+ from fieldops.ml_predictions import predict_all
10
+ from fieldops.models import OptimizationResult, ProblemInstance, Stage1Plan
11
+ from fieldops.routing import solve_routing
12
+ from fieldops.scheduling import assign_missions
13
+ from fieldops.metrics import compute_metrics
14
+ from fieldops.simulation import run_monte_carlo
15
+
16
+
17
+ def _generate_scenarios(problem: ProblemInstance, n: int, seed: int = 42) -> list[list[float]]:
18
+ """SAA scenario generation for failure realizations."""
19
+ rng = random.Random(seed)
20
+ scenarios = []
21
+ for _ in range(n):
22
+ scenario = [rng.betavariate(2, 5) for _ in problem.requests]
23
+ scenarios.append(scenario)
24
+ return scenarios
25
+
26
+
27
+ def solve_two_stage_stochastic(
28
+ problem: ProblemInstance,
29
+ n_scenarios: int = 20,
30
+ ) -> tuple[Stage1Plan, list[dict], list[str]]:
31
+ """Stage 1: inventory + positioning. Stage 2: assignment under scenarios."""
32
+ stage1 = optimize_inventory(problem, problem.stock_vs_expedite_ratio)
33
+ predictions = predict_all(problem)
34
+ scenarios = _generate_scenarios(problem, n_scenarios)
35
+
36
+ best_assignments = assign_missions(problem, predictions)
37
+ best_cost = float("inf")
38
+
39
+ for scenario in scenarios:
40
+ adj_predictions = []
41
+ for i, pred in enumerate(predictions):
42
+ from fieldops.models import MLPrediction
43
+ adj = MLPrediction(
44
+ request_id=pred.request_id,
45
+ site_id=pred.site_id,
46
+ failure_probability=min(0.99, pred.failure_probability * (0.5 + scenario[i])),
47
+ predicted_part_type=pred.predicted_part_type,
48
+ predicted_duration_hours=pred.predicted_duration_hours * (0.8 + 0.4 * scenario[i]),
49
+ first_time_fix_probability=pred.first_time_fix_probability,
50
+ revisit_probability=pred.revisit_probability,
51
+ confidence=pred.confidence,
52
+ risk_tier=pred.risk_tier,
53
+ )
54
+ adj_predictions.append(adj)
55
+
56
+ assignments = assign_missions(problem, adj_predictions)
57
+ metrics = compute_metrics(problem, assignments, stage1, adj_predictions)
58
+ total = metrics.total_logistics_cost + metrics.total_downtime_cost + stage1.total_holding_cost
59
+ if total < best_cost:
60
+ best_cost = total
61
+ best_assignments = assignments
62
+
63
+ notes = [
64
+ f"Two-stage stochastic: {n_scenarios} SAA scenarios evaluated.",
65
+ "Stage 1: multi-echelon inventory + technician positioning.",
66
+ "Stage 2: mission assignment minimizing expected logistics + downtime cost.",
67
+ ]
68
+ return stage1, best_assignments, notes
69
+
70
+
71
+ def solve_saa(problem: ProblemInstance, n_samples: int = 50) -> tuple[Stage1Plan, list[dict], list[str]]:
72
+ """Sample Average Approximation for robust planning."""
73
+ stage1, assignments, notes = solve_two_stage_stochastic(problem, n_samples)
74
+ notes.append(f"SAA with {n_samples} independent failure samples.")
75
+ return stage1, assignments, notes
gradio/src/fieldops/visualization.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Plotly visualizations for FieldOps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+
8
+ from fieldops.models import OptimizationResult, ProblemInstance
9
+
10
+
11
+ def result_summary_markdown(result: OptimizationResult) -> str:
12
+ m = result.metrics
13
+ sim = result.simulation
14
+ sim_line = ""
15
+ if sim:
16
+ sim_line = (
17
+ f"\n- **Monte Carlo SLA:** {sim.sla_compliance_mean:.1f}% "
18
+ f"(95% CI: {sim.confidence_interval_95['sla_compliance'][0]:.1f}–"
19
+ f"{sim.confidence_interval_95['sla_compliance'][1]:.1f}%)"
20
+ )
21
+
22
+ return (
23
+ f"### Optimization Result — {result.algorithm}\n"
24
+ f"- **Status:** {result.status} | **Solve time:** {result.solve_time_sec:.2f}s\n"
25
+ f"- **SLA Compliance:** {m.sla_compliance_pct:.1f}% | "
26
+ f"**MTTR:** {m.mean_time_to_repair_hours:.1f}h | "
27
+ f"**FTF Rate:** {m.first_time_fix_rate_pct:.1f}%\n"
28
+ f"- **Technician Utilization:** {m.technician_utilization_pct:.1f}% | "
29
+ f"**Emergency Stockouts:** {m.emergency_stockout_count}\n"
30
+ f"- **Logistics Cost:** ${m.total_logistics_cost:,.0f} | "
31
+ f"**Downtime Cost:** ${m.total_downtime_cost:,.0f} | "
32
+ f"**Holding Cost:** ${m.total_holding_cost:,.0f}\n"
33
+ f"- **Expedite Shipments:** {m.expedite_shipments}"
34
+ f"{sim_line}"
35
+ )
36
+
37
+
38
+ def kpi_radar_chart(result: OptimizationResult):
39
+ m = result.metrics
40
+ categories = [
41
+ "SLA Compliance", "FTF Rate", "Tech Utilization",
42
+ "Cost Efficiency", "Stock Coverage",
43
+ ]
44
+ cost_eff = max(0, 100 - min(m.total_logistics_cost / 500, 100))
45
+ stock_cov = max(0, 100 - m.emergency_stockout_count * 20)
46
+ values = [
47
+ m.sla_compliance_pct,
48
+ m.first_time_fix_rate_pct,
49
+ m.technician_utilization_pct,
50
+ cost_eff,
51
+ stock_cov,
52
+ ]
53
+
54
+ fig = go.Figure(data=go.Scatterpolar(
55
+ r=values + [values[0]],
56
+ theta=categories + [categories[0]],
57
+ fill="toself",
58
+ name=result.strategy,
59
+ line_color="#2563eb",
60
+ ))
61
+ fig.update_layout(
62
+ polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
63
+ title="Field Service KPI Radar",
64
+ height=400,
65
+ margin=dict(t=60, b=40),
66
+ )
67
+ return fig
68
+
69
+
70
+ def cost_breakdown_chart(result: OptimizationResult):
71
+ m = result.metrics
72
+ labels = ["Logistics", "Downtime", "Holding", "Expedite"]
73
+ values = [
74
+ m.total_logistics_cost,
75
+ m.total_downtime_cost,
76
+ m.total_holding_cost,
77
+ m.expedite_shipments * 250,
78
+ ]
79
+ fig = go.Figure(data=[go.Pie(
80
+ labels=labels, values=values, hole=0.4,
81
+ marker_colors=["#3b82f6", "#ef4444", "#f59e0b", "#8b5cf6"],
82
+ )])
83
+ fig.update_layout(title="Cost Breakdown", height=380, margin=dict(t=50, b=20))
84
+ return fig
85
+
86
+
87
+ def mission_map_chart(problem: ProblemInstance, result: OptimizationResult):
88
+ fig = go.Figure()
89
+
90
+ for wh in problem.warehouses:
91
+ fig.add_trace(go.Scattergeo(
92
+ lon=[wh.lon], lat=[wh.lat],
93
+ mode="markers+text", text=[wh.name],
94
+ marker=dict(size=12, color="#f59e0b", symbol="square"),
95
+ name="Warehouse",
96
+ ))
97
+
98
+ for site in problem.sites:
99
+ fig.add_trace(go.Scattergeo(
100
+ lon=[site.lon], lat=[site.lat],
101
+ mode="markers",
102
+ marker=dict(
103
+ size=8,
104
+ color="#ef4444" if site.criticality == "critical" else "#6b7280",
105
+ ),
106
+ name="Service Site",
107
+ showlegend=False,
108
+ ))
109
+
110
+ for tech in problem.technicians:
111
+ pos = result.stage1_plan.technician_positions.get(
112
+ tech.technician_id, (tech.home_lat, tech.home_lon),
113
+ )
114
+ fig.add_trace(go.Scattergeo(
115
+ lon=[pos[1]], lat=[pos[0]],
116
+ mode="markers",
117
+ marker=dict(size=10, color="#2563eb", symbol="triangle-up"),
118
+ name="Technician",
119
+ showlegend=False,
120
+ ))
121
+
122
+ fig.update_layout(
123
+ title="Field Operations Map",
124
+ geo=dict(scope="europe", projection_type="natural earth"),
125
+ height=450,
126
+ margin=dict(t=50, b=20),
127
+ )
128
+ return fig
129
+
130
+
131
+ def strategy_comparison_chart(results: list[OptimizationResult]):
132
+ strategies = [r.strategy.replace("_", " ").title() for r in results]
133
+ sla = [r.metrics.sla_compliance_pct for r in results]
134
+ mttr = [r.metrics.mean_time_to_repair_hours for r in results]
135
+ cost = [r.metrics.total_logistics_cost + r.metrics.total_downtime_cost for r in results]
136
+
137
+ fig = make_subplots(rows=1, cols=3, subplot_titles=("SLA %", "MTTR (h)", "Total Cost ($)"))
138
+ fig.add_trace(go.Bar(x=strategies, y=sla, marker_color="#2563eb"), row=1, col=1)
139
+ fig.add_trace(go.Bar(x=strategies, y=mttr, marker_color="#ef4444"), row=1, col=2)
140
+ fig.add_trace(go.Bar(x=strategies, y=cost, marker_color="#f59e0b"), row=1, col=3)
141
+ fig.update_layout(title="Strategy Comparison", height=400, showlegend=False,
142
+ margin=dict(t=60, b=80))
143
+ return fig
144
+
145
+
146
+ def stock_expedite_chart(results: list[OptimizationResult]):
147
+ ratios = [f"{r.stage1_plan.notes[0].split(':')[1].strip() if r.stage1_plan.notes else '50%'}" for r in results]
148
+ holding = [r.metrics.total_holding_cost for r in results]
149
+ expedite = [r.metrics.expedite_shipments * 250 for r in results]
150
+ total = [h + e + r.metrics.total_logistics_cost for h, e, r in zip(holding, expedite, results)]
151
+
152
+ fig = go.Figure()
153
+ fig.add_trace(go.Bar(name="Holding Cost", x=[f"{i*20+20}%" for i in range(len(results))], y=holding))
154
+ fig.add_trace(go.Bar(name="Expedite Cost", x=[f"{i*20+20}%" for i in range(len(results))], y=expedite))
155
+ fig.add_trace(go.Scatter(
156
+ name="Total Cost", x=[f"{i*20+20}%" for i in range(len(results))],
157
+ y=total, mode="lines+markers", line=dict(color="#ef4444", width=3),
158
+ ))
159
+ fig.update_layout(
160
+ title="Stock vs Expedite Trade-off",
161
+ barmode="stack", height=400,
162
+ xaxis_title="Stock Ratio", yaxis_title="Cost ($)",
163
+ margin=dict(t=50, b=40),
164
+ )
165
+ return fig
166
+
167
+
168
+ def assignment_gantt(result: OptimizationResult):
169
+ if not result.assignments:
170
+ return None
171
+
172
+ fig = go.Figure()
173
+ colors = {"P1": "#ef4444", "P2": "#f59e0b", "P3": "#3b82f6", "P4": "#6b7280"}
174
+
175
+ for a in result.assignments:
176
+ fig.add_trace(go.Bar(
177
+ name=a.request_id,
178
+ y=[a.technician_name],
179
+ x=[a.estimated_completion_hours - a.estimated_arrival_hours],
180
+ base=[a.estimated_arrival_hours],
181
+ orientation="h",
182
+ marker_color="#2563eb",
183
+ showlegend=False,
184
+ ))
185
+
186
+ fig.update_layout(
187
+ title="Mission Schedule Gantt",
188
+ xaxis_title="Hours from start",
189
+ height=max(300, len(set(a.technician_name for a in result.assignments)) * 50),
190
+ margin=dict(t=50, b=40),
191
+ )
192
+ return fig
index.html CHANGED
@@ -1,19 +1,183 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FieldOps — Service Parts & Field Operations Optimizer</title>
7
+ <style>
8
+ :root {
9
+ --accent: #2563eb; --accent2: #4f46e5;
10
+ --bg: #0f172a; --surface: #1e293b; --border: #334155;
11
+ --text: #f8fafc; --muted: #94a3b8;
12
+ --gradient: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%);
13
+ --ok: #34d399; --warn: #fbbf24;
14
+ }
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+ body { font-family: Inter, system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
17
+ a { color: #93c5fd; text-decoration: none; }
18
+ .hero { padding: 3rem 1.5rem 1.5rem; text-align: center;
19
+ background: radial-gradient(ellipse at 50% 0%, rgba(37,99,235,.15), transparent 60%); }
20
+ .badge { display: inline-block; background: var(--gradient); color: #fff; font-size: .7rem;
21
+ font-weight: 700; letter-spacing: .12em; padding: .35rem 1rem; border-radius: 999px; margin-bottom: 1rem; }
22
+ h1 { font-size: clamp(1.7rem, 5vw, 2.5rem); font-weight: 800; }
23
+ h1 span { background: var(--gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
24
+ .subtitle { color: var(--muted); max-width: 780px; margin: .75rem auto 1rem; font-size: .95rem; }
25
+ .container { max-width: 1180px; margin: 0 auto; padding: 0 1.5rem 3rem; }
26
+ .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: .85rem; margin: 1.5rem 0; }
27
+ .kpi { background: var(--surface); border: 1px solid var(--border); border-radius: .75rem; padding: 1rem; text-align: center; }
28
+ .kpi .val { font-size: 1.45rem; font-weight: 800; color: var(--accent); }
29
+ .kpi .lbl { color: var(--muted); font-size: .75rem; margin-top: .2rem; }
30
+ .links { display: flex; gap: .75rem; justify-content: center; flex-wrap: wrap; margin-top: .75rem; }
31
+ .cta { padding: .65rem 1.2rem; background: var(--gradient); color: #fff; border-radius: .5rem; font-weight: 600; font-size: .85rem; }
32
+ .cta-out { padding: .65rem 1.2rem; border: 1px solid var(--border); border-radius: .5rem; font-size: .85rem; }
33
+ .scenario-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin: 1.25rem 0; }
34
+ .scenario { background: var(--surface); border: 1px solid var(--border); border-radius: .75rem; padding: 1.1rem; border-left: 4px solid var(--accent); }
35
+ .scenario h3 { font-size: 1rem; margin-bottom: .5rem; }
36
+ .scenario .meta { font-size: .82rem; color: var(--muted); margin: .35rem 0; }
37
+ .scenario .metric { font-size: 1.1rem; font-weight: 700; color: var(--ok); }
38
+ select { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: .5rem; padding: .5rem .75rem; margin: .5rem 0; }
39
+ table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: .75rem; overflow: hidden;
40
+ border: 1px solid var(--border); margin: 1rem 0; font-size: .8rem; }
41
+ th { background: #152238; text-align: left; padding: .6rem .9rem; color: var(--muted); font-weight: 600; }
42
+ td { padding: .55rem .9rem; border-top: 1px solid var(--border); }
43
+ h2 { font-size: 1.15rem; margin: 1.5rem 0 .5rem; }
44
+ .note { color: var(--muted); font-size: .85rem; margin: .5rem 0 1rem; }
45
+ .footer { text-align: center; color: var(--muted); font-size: .82rem; padding: 1.5rem; border-top: 1px solid var(--border); }
46
+ .stage { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
47
+ .stage-card { background: var(--surface); border: 1px solid var(--border); border-radius: .75rem; padding: 1rem; }
48
+ .stage-card h3 { font-size: .9rem; color: var(--accent); margin-bottom: .5rem; }
49
+ .stage-card ul { font-size: .82rem; color: var(--muted); padding-left: 1.2rem; }
50
+ @media (max-width: 640px) { .stage { grid-template-columns: 1fr; } }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <section class="hero">
55
+ <div class="badge">FIELD SERVICE · SPARE PARTS · OPTIMIZATION</div>
56
+ <h1><span>Service Parts & Field Operations Optimizer</span></h1>
57
+ <p class="subtitle">Predict-then-optimize platform for aftermarket field service. ML failure prediction, multi-echelon inventory, technician routing, and Monte Carlo SLA validation.</p>
58
+ <div class="links">
59
+ <a class="cta" href="https://huggingface.co/datasets/alirezaaminzadeh/fieldops-sample-scenarios">Dataset</a>
60
+ <a class="cta-out" href="https://huggingface.co/alirezaaminzadeh/fieldops-solver-config">Solver Config</a>
61
+ <a class="cta-out" href="https://huggingface.co/datasets/alirezaaminzadeh/fieldops-sample-scenarios/tree/main/space-bundle">Gradio Bundle</a>
62
+ </div>
63
+ </section>
64
+
65
+ <div class="container">
66
+ <div class="kpi-grid">
67
+ <div class="kpi"><div class="val">7</div><div class="lbl">Algorithms</div></div>
68
+ <div class="kpi"><div class="val">9</div><div class="lbl">Industries</div></div>
69
+ <div class="kpi"><div class="val" id="k-sla">—</div><div class="lbl">Avg SLA %</div></div>
70
+ <div class="kpi"><div class="val" id="k-mttr">—</div><div class="lbl">Avg MTTR</div></div>
71
+ <div class="kpi"><div class="val" id="k-ftf">—</div><div class="lbl">FTF Rate</div></div>
72
+ </div>
73
+
74
+ <h2>Two-Stage Decision Model</h2>
75
+ <div class="stage">
76
+ <div class="stage-card">
77
+ <h3>Stage 1 — Pre-Failure Planning</h3>
78
+ <ul>
79
+ <li>Technician positioning across regions</li>
80
+ <li>Multi-echelon spare parts inventory</li>
81
+ <li>Supply contract parameters</li>
82
+ <li>Repair shop capacity allocation</li>
83
+ </ul>
84
+ </div>
85
+ <div class="stage-card">
86
+ <h3>Stage 2 — Post-Failure Response</h3>
87
+ <ul>
88
+ <li>Mission assignment to technicians</li>
89
+ <li>OR-Tools route optimization</li>
90
+ <li>Parts sourcing (stock / substitute / expedite)</li>
91
+ <li>Repair scheduling with deferral</li>
92
+ </ul>
93
+ </div>
94
+ </div>
95
+
96
+ <h2>Scenario Explorer</h2>
97
+ <p class="note">Pre-computed two-stage stochastic optimization results. Select an industry scenario.</p>
98
+ <label>Industry: <select id="scenario-select"></select></label>
99
+ <div class="scenario-grid" id="scenarios"></div>
100
+
101
+ <h2>Benchmark Summary</h2>
102
+ <div style="overflow-x:auto">
103
+ <table>
104
+ <thead>
105
+ <tr><th>Industry</th><th>Stress</th><th>SLA %</th><th>MTTR (h)</th><th>FTF %</th><th>Logistics $</th><th>Solve (s)</th></tr>
106
+ </thead>
107
+ <tbody id="bench-body"></tbody>
108
+ </table>
109
+ </div>
110
+
111
+ <h2>Stack</h2>
112
+ <p class="note">OR-Tools VRP · Multi-echelon inventory · Two-stage stochastic · ALNS · SAA · Monte Carlo · Pre-trained ML · Plotly · Gradio console (bundled)</p>
113
+ </div>
114
+
115
+ <footer class="footer">FieldOps v1.0 · Apache 2.0 · Field Service Engineering</footer>
116
+
117
+ <script>
118
+ const SCENARIOS = ["industrial_equipment", "medical_equipment", "telecom", "data_center", "elevators"];
119
+ const LABELS = {
120
+ industrial_equipment: "Industrial Equipment",
121
+ medical_equipment: "Medical Equipment",
122
+ telecom: "Telecom Infrastructure",
123
+ data_center: "Data Center",
124
+ elevators: "Elevators & Lifts"
125
+ };
126
+ const SAMPLES = {};
127
+
128
+ async function load() {
129
+ const [summary, benchmarks, ...samples] = await Promise.all([
130
+ fetch("assets/demo/summary.json").then(r => r.json()),
131
+ fetch("assets/demo/benchmarks.json").then(r => r.json()),
132
+ ...SCENARIOS.map(s => fetch(`assets/samples/sample_${s}.json`).then(r => r.json()).catch(() => null))
133
+ ]);
134
+ SCENARIOS.forEach((s, i) => { if (samples[i]) SAMPLES[s] = samples[i]; });
135
+
136
+ const sel = document.getElementById("scenario-select");
137
+ SCENARIOS.forEach(s => {
138
+ const o = document.createElement("option");
139
+ o.value = s; o.textContent = LABELS[s] || s;
140
+ sel.appendChild(o);
141
+ });
142
+
143
+ const avgSla = benchmarks.reduce((a, b) => a + b.sla_compliance_pct, 0) / benchmarks.length;
144
+ const avgMttr = benchmarks.reduce((a, b) => a + b.mttr_hours, 0) / benchmarks.length;
145
+ const avgFtf = benchmarks.reduce((a, b) => a + b.ftf_rate_pct, 0) / benchmarks.length;
146
+ document.getElementById("k-sla").textContent = avgSla.toFixed(0) + "%";
147
+ document.getElementById("k-mttr").textContent = avgMttr.toFixed(1) + "h";
148
+ document.getElementById("k-ftf").textContent = avgFtf.toFixed(0) + "%";
149
+
150
+ const tbody = document.getElementById("bench-body");
151
+ benchmarks.forEach(b => {
152
+ const tr = document.createElement("tr");
153
+ tr.innerHTML = `<td>${LABELS[b.industry] || b.industry}</td><td>${b.stress}</td>
154
+ <td>${b.sla_compliance_pct}%</td><td>${b.mttr_hours}</td>
155
+ <td>${b.ftf_rate_pct}%</td><td>$${b.logistics_cost.toLocaleString()}</td>
156
+ <td>${b.solve_time_sec.toFixed(2)}s</td>`;
157
+ tbody.appendChild(tr);
158
+ });
159
+
160
+ function renderScenario(sc) {
161
+ const data = SAMPLES[sc];
162
+ if (!data) return;
163
+ const m = data.optimization.metrics;
164
+ const sim = data.optimization.simulation;
165
+ const grid = document.getElementById("scenarios");
166
+ grid.innerHTML = `
167
+ <div class="scenario">
168
+ <h3>${data.scenario_label}</h3>
169
+ <div class="meta">Sites: ${data.summary.sites} · Technicians: ${data.summary.technicians} · Warehouses: ${data.summary.warehouses}</div>
170
+ <div class="metric">SLA Compliance: ${m.sla_compliance_pct}%</div>
171
+ <div class="meta">MTTR: ${m.mean_time_to_repair_hours}h · FTF: ${m.first_time_fix_rate_pct}% · Utilization: ${m.technician_utilization_pct}%</div>
172
+ <div class="meta">Logistics: $${m.total_logistics_cost.toLocaleString()} · Downtime: $${m.total_downtime_cost.toLocaleString()} · Expedite: ${m.expedite_shipments}</div>
173
+ ${sim ? `<div class="meta">Monte Carlo: SLA ${sim.sla_compliance_mean}% (95% CI: ${sim.confidence_interval_95.sla_compliance[0]}–${sim.confidence_interval_95.sla_compliance[1]}%)</div>` : ""}
174
+ </div>`;
175
+ }
176
+
177
+ sel.addEventListener("change", () => renderScenario(sel.value));
178
+ renderScenario(SCENARIOS[0]);
179
+ }
180
+ load();
181
+ </script>
182
+ </body>
183
+ </html>