alirezaaminzadeh commited on
Commit
3372dac
·
verified ·
1 Parent(s): 1a9b65f

Deploy PortTower mega-project portfolio control tower

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +13 -10
  2. assets/demo/benchmarks.json +86 -0
  3. assets/demo/summary.json +18 -0
  4. assets/demo/what_if_results.json +26 -0
  5. assets/samples/sample_defense_program.json +265 -0
  6. assets/samples/sample_engineering_epc.json +235 -0
  7. assets/samples/sample_government_program.json +205 -0
  8. assets/samples/sample_infrastructure_mega.json +1622 -0
  9. assets/samples/sample_oil_gas_field.json +1537 -0
  10. assets/samples/sample_pharma_rd.json +1759 -0
  11. assets/samples/sample_software_development.json +921 -0
  12. gradio/README.md +22 -0
  13. gradio/app.py +258 -0
  14. gradio/assets/demo/benchmarks.json +86 -0
  15. gradio/assets/demo/summary.json +18 -0
  16. gradio/assets/demo/what_if_results.json +26 -0
  17. gradio/requirements.txt +5 -0
  18. gradio/src/portfoliowave/__init__.py +3 -0
  19. gradio/src/portfoliowave/cashflow.py +38 -0
  20. gradio/src/portfoliowave/constants.py +85 -0
  21. gradio/src/portfoliowave/cpsat_solver.py +164 -0
  22. gradio/src/portfoliowave/critical_chain.py +87 -0
  23. gradio/src/portfoliowave/disruptions.py +80 -0
  24. gradio/src/portfoliowave/engine.py +64 -0
  25. gradio/src/portfoliowave/generator.py +209 -0
  26. gradio/src/portfoliowave/models.py +185 -0
  27. gradio/src/portfoliowave/monte_carlo.py +62 -0
  28. gradio/src/portfoliowave/network.py +74 -0
  29. gradio/src/portfoliowave/nsga2_solver.py +94 -0
  30. gradio/src/portfoliowave/pipeline.py +101 -0
  31. gradio/src/portfoliowave/portfolio_selection.py +44 -0
  32. gradio/src/portfoliowave/rescheduling.py +73 -0
  33. gradio/src/portfoliowave/resource_leveling.py +57 -0
  34. gradio/src/portfoliowave/visualization.py +235 -0
  35. gradio/src/porttower/__init__.py +3 -0
  36. gradio/src/porttower/cashflow.py +38 -0
  37. gradio/src/porttower/constants.py +162 -0
  38. gradio/src/porttower/cpsat_solver.py +164 -0
  39. gradio/src/porttower/critical_chain.py +87 -0
  40. gradio/src/porttower/disruptions.py +80 -0
  41. gradio/src/porttower/engine.py +64 -0
  42. gradio/src/porttower/generator.py +231 -0
  43. gradio/src/porttower/models.py +194 -0
  44. gradio/src/porttower/monte_carlo.py +161 -0
  45. gradio/src/porttower/network.py +74 -0
  46. gradio/src/porttower/nsga2_solver.py +94 -0
  47. gradio/src/porttower/pipeline.py +117 -0
  48. gradio/src/porttower/portfolio_selection.py +44 -0
  49. gradio/src/porttower/rescheduling.py +73 -0
  50. gradio/src/porttower/resource_leveling.py +57 -0
README.md CHANGED
@@ -1,10 +1,13 @@
1
- ---
2
- title: Mega Project Portfolio Control Tower
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: pink
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: PortTower
3
+ emoji: 🏗️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: static
7
+ app_file: index.html
8
+ short_description: Mega-project portfolio control tower explorer
9
+ ---
10
+
11
+ Static explorer for the PortTower mega-project portfolio control tower.
12
+
13
+ Open the Gradio console from the embedded bundle or the dataset `space-bundle/` folder.
assets/demo/benchmarks.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "scenario": "engineering_epc",
4
+ "uncertainty": "baseline",
5
+ "algorithm": "cp_sat_rcpsp",
6
+ "portfolio_npv": 0.0,
7
+ "makespan": 0,
8
+ "total_tardiness": 0,
9
+ "on_time_delivery_pct": 0.0,
10
+ "resource_leveling_index": 0.0,
11
+ "cash_flow_risk": 0.0,
12
+ "solve_time_sec": 0.001298599992878735
13
+ },
14
+ {
15
+ "scenario": "infrastructure_mega",
16
+ "uncertainty": "resource_shortage",
17
+ "algorithm": "critical_chain",
18
+ "portfolio_npv": 0,
19
+ "makespan": 0,
20
+ "total_tardiness": 0,
21
+ "on_time_delivery_pct": 0.0,
22
+ "resource_leveling_index": 0.0,
23
+ "cash_flow_risk": 0.0,
24
+ "solve_time_sec": 0.0
25
+ },
26
+ {
27
+ "scenario": "oil_gas_field",
28
+ "uncertainty": "supply_delay",
29
+ "algorithm": "scenario_monte_carlo",
30
+ "portfolio_npv": 0,
31
+ "makespan": 0,
32
+ "total_tardiness": 0,
33
+ "on_time_delivery_pct": 0.0,
34
+ "resource_leveling_index": 0.0,
35
+ "cash_flow_risk": 0.0,
36
+ "solve_time_sec": 0.108
37
+ },
38
+ {
39
+ "scenario": "pharma_rd",
40
+ "uncertainty": "scope_volatility",
41
+ "algorithm": "nsga2_portfolio",
42
+ "portfolio_npv": 0,
43
+ "makespan": 0,
44
+ "total_tardiness": 0,
45
+ "on_time_delivery_pct": 0.0,
46
+ "resource_leveling_index": 0.0,
47
+ "cash_flow_risk": 0.0,
48
+ "solve_time_sec": 0.045
49
+ },
50
+ {
51
+ "scenario": "defense_program",
52
+ "uncertainty": "baseline",
53
+ "algorithm": "cp_sat_rcpsp",
54
+ "portfolio_npv": 0.0,
55
+ "makespan": 0,
56
+ "total_tardiness": 0,
57
+ "on_time_delivery_pct": 0.0,
58
+ "resource_leveling_index": 0.0,
59
+ "cash_flow_risk": 0.0,
60
+ "solve_time_sec": 0.00035180000122636557
61
+ },
62
+ {
63
+ "scenario": "software_development",
64
+ "uncertainty": "scope_volatility",
65
+ "algorithm": "critical_chain",
66
+ "portfolio_npv": 0,
67
+ "makespan": 0,
68
+ "total_tardiness": 0,
69
+ "on_time_delivery_pct": 0.0,
70
+ "resource_leveling_index": 0.0,
71
+ "cash_flow_risk": 0.0,
72
+ "solve_time_sec": 0.0
73
+ },
74
+ {
75
+ "scenario": "government_program",
76
+ "uncertainty": "cash_crunch",
77
+ "algorithm": "rolling_horizon",
78
+ "portfolio_npv": 0.0,
79
+ "makespan": 0,
80
+ "total_tardiness": 0,
81
+ "on_time_delivery_pct": 0.0,
82
+ "resource_leveling_index": 0.0,
83
+ "cash_flow_risk": 0.0,
84
+ "solve_time_sec": 0.002
85
+ }
86
+ ]
assets/demo/summary.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project": "PortTower",
3
+ "version": "1.0.0",
4
+ "scenarios": 7,
5
+ "algorithms": 5,
6
+ "primary_metrics": [
7
+ "portfolio_npv",
8
+ "total_tardiness",
9
+ "makespan",
10
+ "resource_leveling_index",
11
+ "cash_flow_risk",
12
+ "on_time_probability_pct",
13
+ "monte_carlo_p50_cost",
14
+ "monte_carlo_p90_cost",
15
+ "liquidity_shortfall_prob_pct",
16
+ "milestone_delay_prob_pct"
17
+ ]
18
+ }
assets/demo/what_if_results.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "scenario": "engineering_epc",
4
+ "resource": "engineering_team",
5
+ "npv_delta": 0,
6
+ "portfolio_value_change_pct": 0.0,
7
+ "projects_faster_count": 0,
8
+ "on_time_delta_pct": 0.0
9
+ },
10
+ {
11
+ "scenario": "infrastructure_mega",
12
+ "resource": "engineering_team",
13
+ "npv_delta": 0,
14
+ "portfolio_value_change_pct": 0.0,
15
+ "projects_faster_count": 0,
16
+ "on_time_delta_pct": 0.0
17
+ },
18
+ {
19
+ "scenario": "defense_program",
20
+ "resource": "engineering_team",
21
+ "npv_delta": 0,
22
+ "portfolio_value_change_pct": 0.0,
23
+ "projects_faster_count": 0,
24
+ "on_time_delta_pct": 0.0
25
+ }
26
+ ]
assets/samples/sample_defense_program.json ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "defense_program",
3
+ "uncertainty": "baseline",
4
+ "algorithm": "cp_sat_rcpsp",
5
+ "scenario_label": "Defense Systems Portfolio",
6
+ "summary": {
7
+ "projects": 5,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 210,
11
+ "budget_musd": 72.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "cp_sat_rcpsp",
15
+ "scenario": "defense_program",
16
+ "uncertainty": "baseline",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0.0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.00035180000122636557,
34
+ "status": "infeasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {},
46
+ "cash_flow_profile": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0,
228
+ 0.0,
229
+ 0.0,
230
+ 0.0,
231
+ 0.0,
232
+ 0.0,
233
+ 0.0,
234
+ 0.0,
235
+ 0.0,
236
+ 0.0,
237
+ 0.0,
238
+ 0.0,
239
+ 0.0,
240
+ 0.0,
241
+ 0.0,
242
+ 0.0,
243
+ 0.0,
244
+ 0.0,
245
+ 0.0,
246
+ 0.0,
247
+ 0.0,
248
+ 0.0,
249
+ 0.0,
250
+ 0.0,
251
+ 0.0,
252
+ 0.0,
253
+ 0.0,
254
+ 0.0,
255
+ 0.0,
256
+ 0.0,
257
+ 0.0
258
+ ],
259
+ "selected_projects": [],
260
+ "suspended_projects": [],
261
+ "simulation_runs": [],
262
+ "pareto_front": [],
263
+ "risk_contributors": []
264
+ }
265
+ }
assets/samples/sample_engineering_epc.json ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "engineering_epc",
3
+ "uncertainty": "baseline",
4
+ "algorithm": "cp_sat_rcpsp",
5
+ "scenario_label": "Engineering & Construction EPC",
6
+ "summary": {
7
+ "projects": 6,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 180,
11
+ "budget_musd": 48.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "cp_sat_rcpsp",
15
+ "scenario": "engineering_epc",
16
+ "uncertainty": "baseline",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0.0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.001298599992878735,
34
+ "status": "infeasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {},
46
+ "cash_flow_profile": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0
228
+ ],
229
+ "selected_projects": [],
230
+ "suspended_projects": [],
231
+ "simulation_runs": [],
232
+ "pareto_front": [],
233
+ "risk_contributors": []
234
+ }
235
+ }
assets/samples/sample_government_program.json ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "government_program",
3
+ "uncertainty": "cash_crunch",
4
+ "algorithm": "rolling_horizon",
5
+ "scenario_label": "Government Multi-Program Office",
6
+ "summary": {
7
+ "projects": 7,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 150,
11
+ "budget_musd": 35.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "rolling_horizon",
15
+ "scenario": "government_program",
16
+ "uncertainty": "cash_crunch",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0.0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.002,
34
+ "status": "infeasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {},
46
+ "cash_flow_profile": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0
198
+ ],
199
+ "selected_projects": [],
200
+ "suspended_projects": [],
201
+ "simulation_runs": [],
202
+ "pareto_front": [],
203
+ "risk_contributors": []
204
+ }
205
+ }
assets/samples/sample_infrastructure_mega.json ADDED
@@ -0,0 +1,1622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "infrastructure_mega",
3
+ "uncertainty": "resource_shortage",
4
+ "algorithm": "critical_chain",
5
+ "scenario_label": "National Infrastructure Program",
6
+ "summary": {
7
+ "projects": 7,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 220,
11
+ "budget_musd": 95.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "critical_chain",
15
+ "scenario": "infrastructure_mega",
16
+ "uncertainty": "resource_shortage",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.0,
34
+ "status": "feasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {
46
+ "res-human": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0,
228
+ 0.0,
229
+ 0.0,
230
+ 0.0,
231
+ 0.0,
232
+ 0.0,
233
+ 0.0,
234
+ 0.0,
235
+ 0.0,
236
+ 0.0,
237
+ 0.0,
238
+ 0.0,
239
+ 0.0,
240
+ 0.0,
241
+ 0.0,
242
+ 0.0,
243
+ 0.0,
244
+ 0.0,
245
+ 0.0,
246
+ 0.0,
247
+ 0.0,
248
+ 0.0,
249
+ 0.0,
250
+ 0.0,
251
+ 0.0,
252
+ 0.0,
253
+ 0.0,
254
+ 0.0,
255
+ 0.0,
256
+ 0.0,
257
+ 0.0,
258
+ 0.0,
259
+ 0.0,
260
+ 0.0,
261
+ 0.0,
262
+ 0.0,
263
+ 0.0,
264
+ 0.0,
265
+ 0.0,
266
+ 0.0,
267
+ 0.0
268
+ ],
269
+ "res-machinery": [
270
+ 0.0,
271
+ 0.0,
272
+ 0.0,
273
+ 0.0,
274
+ 0.0,
275
+ 0.0,
276
+ 0.0,
277
+ 0.0,
278
+ 0.0,
279
+ 0.0,
280
+ 0.0,
281
+ 0.0,
282
+ 0.0,
283
+ 0.0,
284
+ 0.0,
285
+ 0.0,
286
+ 0.0,
287
+ 0.0,
288
+ 0.0,
289
+ 0.0,
290
+ 0.0,
291
+ 0.0,
292
+ 0.0,
293
+ 0.0,
294
+ 0.0,
295
+ 0.0,
296
+ 0.0,
297
+ 0.0,
298
+ 0.0,
299
+ 0.0,
300
+ 0.0,
301
+ 0.0,
302
+ 0.0,
303
+ 0.0,
304
+ 0.0,
305
+ 0.0,
306
+ 0.0,
307
+ 0.0,
308
+ 0.0,
309
+ 0.0,
310
+ 0.0,
311
+ 0.0,
312
+ 0.0,
313
+ 0.0,
314
+ 0.0,
315
+ 0.0,
316
+ 0.0,
317
+ 0.0,
318
+ 0.0,
319
+ 0.0,
320
+ 0.0,
321
+ 0.0,
322
+ 0.0,
323
+ 0.0,
324
+ 0.0,
325
+ 0.0,
326
+ 0.0,
327
+ 0.0,
328
+ 0.0,
329
+ 0.0,
330
+ 0.0,
331
+ 0.0,
332
+ 0.0,
333
+ 0.0,
334
+ 0.0,
335
+ 0.0,
336
+ 0.0,
337
+ 0.0,
338
+ 0.0,
339
+ 0.0,
340
+ 0.0,
341
+ 0.0,
342
+ 0.0,
343
+ 0.0,
344
+ 0.0,
345
+ 0.0,
346
+ 0.0,
347
+ 0.0,
348
+ 0.0,
349
+ 0.0,
350
+ 0.0,
351
+ 0.0,
352
+ 0.0,
353
+ 0.0,
354
+ 0.0,
355
+ 0.0,
356
+ 0.0,
357
+ 0.0,
358
+ 0.0,
359
+ 0.0,
360
+ 0.0,
361
+ 0.0,
362
+ 0.0,
363
+ 0.0,
364
+ 0.0,
365
+ 0.0,
366
+ 0.0,
367
+ 0.0,
368
+ 0.0,
369
+ 0.0,
370
+ 0.0,
371
+ 0.0,
372
+ 0.0,
373
+ 0.0,
374
+ 0.0,
375
+ 0.0,
376
+ 0.0,
377
+ 0.0,
378
+ 0.0,
379
+ 0.0,
380
+ 0.0,
381
+ 0.0,
382
+ 0.0,
383
+ 0.0,
384
+ 0.0,
385
+ 0.0,
386
+ 0.0,
387
+ 0.0,
388
+ 0.0,
389
+ 0.0,
390
+ 0.0,
391
+ 0.0,
392
+ 0.0,
393
+ 0.0,
394
+ 0.0,
395
+ 0.0,
396
+ 0.0,
397
+ 0.0,
398
+ 0.0,
399
+ 0.0,
400
+ 0.0,
401
+ 0.0,
402
+ 0.0,
403
+ 0.0,
404
+ 0.0,
405
+ 0.0,
406
+ 0.0,
407
+ 0.0,
408
+ 0.0,
409
+ 0.0,
410
+ 0.0,
411
+ 0.0,
412
+ 0.0,
413
+ 0.0,
414
+ 0.0,
415
+ 0.0,
416
+ 0.0,
417
+ 0.0,
418
+ 0.0,
419
+ 0.0,
420
+ 0.0,
421
+ 0.0,
422
+ 0.0,
423
+ 0.0,
424
+ 0.0,
425
+ 0.0,
426
+ 0.0,
427
+ 0.0,
428
+ 0.0,
429
+ 0.0,
430
+ 0.0,
431
+ 0.0,
432
+ 0.0,
433
+ 0.0,
434
+ 0.0,
435
+ 0.0,
436
+ 0.0,
437
+ 0.0,
438
+ 0.0,
439
+ 0.0,
440
+ 0.0,
441
+ 0.0,
442
+ 0.0,
443
+ 0.0,
444
+ 0.0,
445
+ 0.0,
446
+ 0.0,
447
+ 0.0,
448
+ 0.0,
449
+ 0.0,
450
+ 0.0,
451
+ 0.0,
452
+ 0.0,
453
+ 0.0,
454
+ 0.0,
455
+ 0.0,
456
+ 0.0,
457
+ 0.0,
458
+ 0.0,
459
+ 0.0,
460
+ 0.0,
461
+ 0.0,
462
+ 0.0,
463
+ 0.0,
464
+ 0.0,
465
+ 0.0,
466
+ 0.0,
467
+ 0.0,
468
+ 0.0,
469
+ 0.0,
470
+ 0.0,
471
+ 0.0,
472
+ 0.0,
473
+ 0.0,
474
+ 0.0,
475
+ 0.0,
476
+ 0.0,
477
+ 0.0,
478
+ 0.0,
479
+ 0.0,
480
+ 0.0,
481
+ 0.0,
482
+ 0.0,
483
+ 0.0,
484
+ 0.0,
485
+ 0.0,
486
+ 0.0,
487
+ 0.0,
488
+ 0.0,
489
+ 0.0,
490
+ 0.0
491
+ ],
492
+ "res-contractor": [
493
+ 0.0,
494
+ 0.0,
495
+ 0.0,
496
+ 0.0,
497
+ 0.0,
498
+ 0.0,
499
+ 0.0,
500
+ 0.0,
501
+ 0.0,
502
+ 0.0,
503
+ 0.0,
504
+ 0.0,
505
+ 0.0,
506
+ 0.0,
507
+ 0.0,
508
+ 0.0,
509
+ 0.0,
510
+ 0.0,
511
+ 0.0,
512
+ 0.0,
513
+ 0.0,
514
+ 0.0,
515
+ 0.0,
516
+ 0.0,
517
+ 0.0,
518
+ 0.0,
519
+ 0.0,
520
+ 0.0,
521
+ 0.0,
522
+ 0.0,
523
+ 0.0,
524
+ 0.0,
525
+ 0.0,
526
+ 0.0,
527
+ 0.0,
528
+ 0.0,
529
+ 0.0,
530
+ 0.0,
531
+ 0.0,
532
+ 0.0,
533
+ 0.0,
534
+ 0.0,
535
+ 0.0,
536
+ 0.0,
537
+ 0.0,
538
+ 0.0,
539
+ 0.0,
540
+ 0.0,
541
+ 0.0,
542
+ 0.0,
543
+ 0.0,
544
+ 0.0,
545
+ 0.0,
546
+ 0.0,
547
+ 0.0,
548
+ 0.0,
549
+ 0.0,
550
+ 0.0,
551
+ 0.0,
552
+ 0.0,
553
+ 0.0,
554
+ 0.0,
555
+ 0.0,
556
+ 0.0,
557
+ 0.0,
558
+ 0.0,
559
+ 0.0,
560
+ 0.0,
561
+ 0.0,
562
+ 0.0,
563
+ 0.0,
564
+ 0.0,
565
+ 0.0,
566
+ 0.0,
567
+ 0.0,
568
+ 0.0,
569
+ 0.0,
570
+ 0.0,
571
+ 0.0,
572
+ 0.0,
573
+ 0.0,
574
+ 0.0,
575
+ 0.0,
576
+ 0.0,
577
+ 0.0,
578
+ 0.0,
579
+ 0.0,
580
+ 0.0,
581
+ 0.0,
582
+ 0.0,
583
+ 0.0,
584
+ 0.0,
585
+ 0.0,
586
+ 0.0,
587
+ 0.0,
588
+ 0.0,
589
+ 0.0,
590
+ 0.0,
591
+ 0.0,
592
+ 0.0,
593
+ 0.0,
594
+ 0.0,
595
+ 0.0,
596
+ 0.0,
597
+ 0.0,
598
+ 0.0,
599
+ 0.0,
600
+ 0.0,
601
+ 0.0,
602
+ 0.0,
603
+ 0.0,
604
+ 0.0,
605
+ 0.0,
606
+ 0.0,
607
+ 0.0,
608
+ 0.0,
609
+ 0.0,
610
+ 0.0,
611
+ 0.0,
612
+ 0.0,
613
+ 0.0,
614
+ 0.0,
615
+ 0.0,
616
+ 0.0,
617
+ 0.0,
618
+ 0.0,
619
+ 0.0,
620
+ 0.0,
621
+ 0.0,
622
+ 0.0,
623
+ 0.0,
624
+ 0.0,
625
+ 0.0,
626
+ 0.0,
627
+ 0.0,
628
+ 0.0,
629
+ 0.0,
630
+ 0.0,
631
+ 0.0,
632
+ 0.0,
633
+ 0.0,
634
+ 0.0,
635
+ 0.0,
636
+ 0.0,
637
+ 0.0,
638
+ 0.0,
639
+ 0.0,
640
+ 0.0,
641
+ 0.0,
642
+ 0.0,
643
+ 0.0,
644
+ 0.0,
645
+ 0.0,
646
+ 0.0,
647
+ 0.0,
648
+ 0.0,
649
+ 0.0,
650
+ 0.0,
651
+ 0.0,
652
+ 0.0,
653
+ 0.0,
654
+ 0.0,
655
+ 0.0,
656
+ 0.0,
657
+ 0.0,
658
+ 0.0,
659
+ 0.0,
660
+ 0.0,
661
+ 0.0,
662
+ 0.0,
663
+ 0.0,
664
+ 0.0,
665
+ 0.0,
666
+ 0.0,
667
+ 0.0,
668
+ 0.0,
669
+ 0.0,
670
+ 0.0,
671
+ 0.0,
672
+ 0.0,
673
+ 0.0,
674
+ 0.0,
675
+ 0.0,
676
+ 0.0,
677
+ 0.0,
678
+ 0.0,
679
+ 0.0,
680
+ 0.0,
681
+ 0.0,
682
+ 0.0,
683
+ 0.0,
684
+ 0.0,
685
+ 0.0,
686
+ 0.0,
687
+ 0.0,
688
+ 0.0,
689
+ 0.0,
690
+ 0.0,
691
+ 0.0,
692
+ 0.0,
693
+ 0.0,
694
+ 0.0,
695
+ 0.0,
696
+ 0.0,
697
+ 0.0,
698
+ 0.0,
699
+ 0.0,
700
+ 0.0,
701
+ 0.0,
702
+ 0.0,
703
+ 0.0,
704
+ 0.0,
705
+ 0.0,
706
+ 0.0,
707
+ 0.0,
708
+ 0.0,
709
+ 0.0,
710
+ 0.0,
711
+ 0.0,
712
+ 0.0,
713
+ 0.0
714
+ ],
715
+ "res-equipment": [
716
+ 0.0,
717
+ 0.0,
718
+ 0.0,
719
+ 0.0,
720
+ 0.0,
721
+ 0.0,
722
+ 0.0,
723
+ 0.0,
724
+ 0.0,
725
+ 0.0,
726
+ 0.0,
727
+ 0.0,
728
+ 0.0,
729
+ 0.0,
730
+ 0.0,
731
+ 0.0,
732
+ 0.0,
733
+ 0.0,
734
+ 0.0,
735
+ 0.0,
736
+ 0.0,
737
+ 0.0,
738
+ 0.0,
739
+ 0.0,
740
+ 0.0,
741
+ 0.0,
742
+ 0.0,
743
+ 0.0,
744
+ 0.0,
745
+ 0.0,
746
+ 0.0,
747
+ 0.0,
748
+ 0.0,
749
+ 0.0,
750
+ 0.0,
751
+ 0.0,
752
+ 0.0,
753
+ 0.0,
754
+ 0.0,
755
+ 0.0,
756
+ 0.0,
757
+ 0.0,
758
+ 0.0,
759
+ 0.0,
760
+ 0.0,
761
+ 0.0,
762
+ 0.0,
763
+ 0.0,
764
+ 0.0,
765
+ 0.0,
766
+ 0.0,
767
+ 0.0,
768
+ 0.0,
769
+ 0.0,
770
+ 0.0,
771
+ 0.0,
772
+ 0.0,
773
+ 0.0,
774
+ 0.0,
775
+ 0.0,
776
+ 0.0,
777
+ 0.0,
778
+ 0.0,
779
+ 0.0,
780
+ 0.0,
781
+ 0.0,
782
+ 0.0,
783
+ 0.0,
784
+ 0.0,
785
+ 0.0,
786
+ 0.0,
787
+ 0.0,
788
+ 0.0,
789
+ 0.0,
790
+ 0.0,
791
+ 0.0,
792
+ 0.0,
793
+ 0.0,
794
+ 0.0,
795
+ 0.0,
796
+ 0.0,
797
+ 0.0,
798
+ 0.0,
799
+ 0.0,
800
+ 0.0,
801
+ 0.0,
802
+ 0.0,
803
+ 0.0,
804
+ 0.0,
805
+ 0.0,
806
+ 0.0,
807
+ 0.0,
808
+ 0.0,
809
+ 0.0,
810
+ 0.0,
811
+ 0.0,
812
+ 0.0,
813
+ 0.0,
814
+ 0.0,
815
+ 0.0,
816
+ 0.0,
817
+ 0.0,
818
+ 0.0,
819
+ 0.0,
820
+ 0.0,
821
+ 0.0,
822
+ 0.0,
823
+ 0.0,
824
+ 0.0,
825
+ 0.0,
826
+ 0.0,
827
+ 0.0,
828
+ 0.0,
829
+ 0.0,
830
+ 0.0,
831
+ 0.0,
832
+ 0.0,
833
+ 0.0,
834
+ 0.0,
835
+ 0.0,
836
+ 0.0,
837
+ 0.0,
838
+ 0.0,
839
+ 0.0,
840
+ 0.0,
841
+ 0.0,
842
+ 0.0,
843
+ 0.0,
844
+ 0.0,
845
+ 0.0,
846
+ 0.0,
847
+ 0.0,
848
+ 0.0,
849
+ 0.0,
850
+ 0.0,
851
+ 0.0,
852
+ 0.0,
853
+ 0.0,
854
+ 0.0,
855
+ 0.0,
856
+ 0.0,
857
+ 0.0,
858
+ 0.0,
859
+ 0.0,
860
+ 0.0,
861
+ 0.0,
862
+ 0.0,
863
+ 0.0,
864
+ 0.0,
865
+ 0.0,
866
+ 0.0,
867
+ 0.0,
868
+ 0.0,
869
+ 0.0,
870
+ 0.0,
871
+ 0.0,
872
+ 0.0,
873
+ 0.0,
874
+ 0.0,
875
+ 0.0,
876
+ 0.0,
877
+ 0.0,
878
+ 0.0,
879
+ 0.0,
880
+ 0.0,
881
+ 0.0,
882
+ 0.0,
883
+ 0.0,
884
+ 0.0,
885
+ 0.0,
886
+ 0.0,
887
+ 0.0,
888
+ 0.0,
889
+ 0.0,
890
+ 0.0,
891
+ 0.0,
892
+ 0.0,
893
+ 0.0,
894
+ 0.0,
895
+ 0.0,
896
+ 0.0,
897
+ 0.0,
898
+ 0.0,
899
+ 0.0,
900
+ 0.0,
901
+ 0.0,
902
+ 0.0,
903
+ 0.0,
904
+ 0.0,
905
+ 0.0,
906
+ 0.0,
907
+ 0.0,
908
+ 0.0,
909
+ 0.0,
910
+ 0.0,
911
+ 0.0,
912
+ 0.0,
913
+ 0.0,
914
+ 0.0,
915
+ 0.0,
916
+ 0.0,
917
+ 0.0,
918
+ 0.0,
919
+ 0.0,
920
+ 0.0,
921
+ 0.0,
922
+ 0.0,
923
+ 0.0,
924
+ 0.0,
925
+ 0.0,
926
+ 0.0,
927
+ 0.0,
928
+ 0.0,
929
+ 0.0,
930
+ 0.0,
931
+ 0.0,
932
+ 0.0,
933
+ 0.0,
934
+ 0.0,
935
+ 0.0,
936
+ 0.0
937
+ ],
938
+ "res-laboratory": [
939
+ 0.0,
940
+ 0.0,
941
+ 0.0,
942
+ 0.0,
943
+ 0.0,
944
+ 0.0,
945
+ 0.0,
946
+ 0.0,
947
+ 0.0,
948
+ 0.0,
949
+ 0.0,
950
+ 0.0,
951
+ 0.0,
952
+ 0.0,
953
+ 0.0,
954
+ 0.0,
955
+ 0.0,
956
+ 0.0,
957
+ 0.0,
958
+ 0.0,
959
+ 0.0,
960
+ 0.0,
961
+ 0.0,
962
+ 0.0,
963
+ 0.0,
964
+ 0.0,
965
+ 0.0,
966
+ 0.0,
967
+ 0.0,
968
+ 0.0,
969
+ 0.0,
970
+ 0.0,
971
+ 0.0,
972
+ 0.0,
973
+ 0.0,
974
+ 0.0,
975
+ 0.0,
976
+ 0.0,
977
+ 0.0,
978
+ 0.0,
979
+ 0.0,
980
+ 0.0,
981
+ 0.0,
982
+ 0.0,
983
+ 0.0,
984
+ 0.0,
985
+ 0.0,
986
+ 0.0,
987
+ 0.0,
988
+ 0.0,
989
+ 0.0,
990
+ 0.0,
991
+ 0.0,
992
+ 0.0,
993
+ 0.0,
994
+ 0.0,
995
+ 0.0,
996
+ 0.0,
997
+ 0.0,
998
+ 0.0,
999
+ 0.0,
1000
+ 0.0,
1001
+ 0.0,
1002
+ 0.0,
1003
+ 0.0,
1004
+ 0.0,
1005
+ 0.0,
1006
+ 0.0,
1007
+ 0.0,
1008
+ 0.0,
1009
+ 0.0,
1010
+ 0.0,
1011
+ 0.0,
1012
+ 0.0,
1013
+ 0.0,
1014
+ 0.0,
1015
+ 0.0,
1016
+ 0.0,
1017
+ 0.0,
1018
+ 0.0,
1019
+ 0.0,
1020
+ 0.0,
1021
+ 0.0,
1022
+ 0.0,
1023
+ 0.0,
1024
+ 0.0,
1025
+ 0.0,
1026
+ 0.0,
1027
+ 0.0,
1028
+ 0.0,
1029
+ 0.0,
1030
+ 0.0,
1031
+ 0.0,
1032
+ 0.0,
1033
+ 0.0,
1034
+ 0.0,
1035
+ 0.0,
1036
+ 0.0,
1037
+ 0.0,
1038
+ 0.0,
1039
+ 0.0,
1040
+ 0.0,
1041
+ 0.0,
1042
+ 0.0,
1043
+ 0.0,
1044
+ 0.0,
1045
+ 0.0,
1046
+ 0.0,
1047
+ 0.0,
1048
+ 0.0,
1049
+ 0.0,
1050
+ 0.0,
1051
+ 0.0,
1052
+ 0.0,
1053
+ 0.0,
1054
+ 0.0,
1055
+ 0.0,
1056
+ 0.0,
1057
+ 0.0,
1058
+ 0.0,
1059
+ 0.0,
1060
+ 0.0,
1061
+ 0.0,
1062
+ 0.0,
1063
+ 0.0,
1064
+ 0.0,
1065
+ 0.0,
1066
+ 0.0,
1067
+ 0.0,
1068
+ 0.0,
1069
+ 0.0,
1070
+ 0.0,
1071
+ 0.0,
1072
+ 0.0,
1073
+ 0.0,
1074
+ 0.0,
1075
+ 0.0,
1076
+ 0.0,
1077
+ 0.0,
1078
+ 0.0,
1079
+ 0.0,
1080
+ 0.0,
1081
+ 0.0,
1082
+ 0.0,
1083
+ 0.0,
1084
+ 0.0,
1085
+ 0.0,
1086
+ 0.0,
1087
+ 0.0,
1088
+ 0.0,
1089
+ 0.0,
1090
+ 0.0,
1091
+ 0.0,
1092
+ 0.0,
1093
+ 0.0,
1094
+ 0.0,
1095
+ 0.0,
1096
+ 0.0,
1097
+ 0.0,
1098
+ 0.0,
1099
+ 0.0,
1100
+ 0.0,
1101
+ 0.0,
1102
+ 0.0,
1103
+ 0.0,
1104
+ 0.0,
1105
+ 0.0,
1106
+ 0.0,
1107
+ 0.0,
1108
+ 0.0,
1109
+ 0.0,
1110
+ 0.0,
1111
+ 0.0,
1112
+ 0.0,
1113
+ 0.0,
1114
+ 0.0,
1115
+ 0.0,
1116
+ 0.0,
1117
+ 0.0,
1118
+ 0.0,
1119
+ 0.0,
1120
+ 0.0,
1121
+ 0.0,
1122
+ 0.0,
1123
+ 0.0,
1124
+ 0.0,
1125
+ 0.0,
1126
+ 0.0,
1127
+ 0.0,
1128
+ 0.0,
1129
+ 0.0,
1130
+ 0.0,
1131
+ 0.0,
1132
+ 0.0,
1133
+ 0.0,
1134
+ 0.0,
1135
+ 0.0,
1136
+ 0.0,
1137
+ 0.0,
1138
+ 0.0,
1139
+ 0.0,
1140
+ 0.0,
1141
+ 0.0,
1142
+ 0.0,
1143
+ 0.0,
1144
+ 0.0,
1145
+ 0.0,
1146
+ 0.0,
1147
+ 0.0,
1148
+ 0.0,
1149
+ 0.0,
1150
+ 0.0,
1151
+ 0.0,
1152
+ 0.0,
1153
+ 0.0,
1154
+ 0.0,
1155
+ 0.0,
1156
+ 0.0,
1157
+ 0.0,
1158
+ 0.0,
1159
+ 0.0
1160
+ ],
1161
+ "res-specialist": [
1162
+ 0.0,
1163
+ 0.0,
1164
+ 0.0,
1165
+ 0.0,
1166
+ 0.0,
1167
+ 0.0,
1168
+ 0.0,
1169
+ 0.0,
1170
+ 0.0,
1171
+ 0.0,
1172
+ 0.0,
1173
+ 0.0,
1174
+ 0.0,
1175
+ 0.0,
1176
+ 0.0,
1177
+ 0.0,
1178
+ 0.0,
1179
+ 0.0,
1180
+ 0.0,
1181
+ 0.0,
1182
+ 0.0,
1183
+ 0.0,
1184
+ 0.0,
1185
+ 0.0,
1186
+ 0.0,
1187
+ 0.0,
1188
+ 0.0,
1189
+ 0.0,
1190
+ 0.0,
1191
+ 0.0,
1192
+ 0.0,
1193
+ 0.0,
1194
+ 0.0,
1195
+ 0.0,
1196
+ 0.0,
1197
+ 0.0,
1198
+ 0.0,
1199
+ 0.0,
1200
+ 0.0,
1201
+ 0.0,
1202
+ 0.0,
1203
+ 0.0,
1204
+ 0.0,
1205
+ 0.0,
1206
+ 0.0,
1207
+ 0.0,
1208
+ 0.0,
1209
+ 0.0,
1210
+ 0.0,
1211
+ 0.0,
1212
+ 0.0,
1213
+ 0.0,
1214
+ 0.0,
1215
+ 0.0,
1216
+ 0.0,
1217
+ 0.0,
1218
+ 0.0,
1219
+ 0.0,
1220
+ 0.0,
1221
+ 0.0,
1222
+ 0.0,
1223
+ 0.0,
1224
+ 0.0,
1225
+ 0.0,
1226
+ 0.0,
1227
+ 0.0,
1228
+ 0.0,
1229
+ 0.0,
1230
+ 0.0,
1231
+ 0.0,
1232
+ 0.0,
1233
+ 0.0,
1234
+ 0.0,
1235
+ 0.0,
1236
+ 0.0,
1237
+ 0.0,
1238
+ 0.0,
1239
+ 0.0,
1240
+ 0.0,
1241
+ 0.0,
1242
+ 0.0,
1243
+ 0.0,
1244
+ 0.0,
1245
+ 0.0,
1246
+ 0.0,
1247
+ 0.0,
1248
+ 0.0,
1249
+ 0.0,
1250
+ 0.0,
1251
+ 0.0,
1252
+ 0.0,
1253
+ 0.0,
1254
+ 0.0,
1255
+ 0.0,
1256
+ 0.0,
1257
+ 0.0,
1258
+ 0.0,
1259
+ 0.0,
1260
+ 0.0,
1261
+ 0.0,
1262
+ 0.0,
1263
+ 0.0,
1264
+ 0.0,
1265
+ 0.0,
1266
+ 0.0,
1267
+ 0.0,
1268
+ 0.0,
1269
+ 0.0,
1270
+ 0.0,
1271
+ 0.0,
1272
+ 0.0,
1273
+ 0.0,
1274
+ 0.0,
1275
+ 0.0,
1276
+ 0.0,
1277
+ 0.0,
1278
+ 0.0,
1279
+ 0.0,
1280
+ 0.0,
1281
+ 0.0,
1282
+ 0.0,
1283
+ 0.0,
1284
+ 0.0,
1285
+ 0.0,
1286
+ 0.0,
1287
+ 0.0,
1288
+ 0.0,
1289
+ 0.0,
1290
+ 0.0,
1291
+ 0.0,
1292
+ 0.0,
1293
+ 0.0,
1294
+ 0.0,
1295
+ 0.0,
1296
+ 0.0,
1297
+ 0.0,
1298
+ 0.0,
1299
+ 0.0,
1300
+ 0.0,
1301
+ 0.0,
1302
+ 0.0,
1303
+ 0.0,
1304
+ 0.0,
1305
+ 0.0,
1306
+ 0.0,
1307
+ 0.0,
1308
+ 0.0,
1309
+ 0.0,
1310
+ 0.0,
1311
+ 0.0,
1312
+ 0.0,
1313
+ 0.0,
1314
+ 0.0,
1315
+ 0.0,
1316
+ 0.0,
1317
+ 0.0,
1318
+ 0.0,
1319
+ 0.0,
1320
+ 0.0,
1321
+ 0.0,
1322
+ 0.0,
1323
+ 0.0,
1324
+ 0.0,
1325
+ 0.0,
1326
+ 0.0,
1327
+ 0.0,
1328
+ 0.0,
1329
+ 0.0,
1330
+ 0.0,
1331
+ 0.0,
1332
+ 0.0,
1333
+ 0.0,
1334
+ 0.0,
1335
+ 0.0,
1336
+ 0.0,
1337
+ 0.0,
1338
+ 0.0,
1339
+ 0.0,
1340
+ 0.0,
1341
+ 0.0,
1342
+ 0.0,
1343
+ 0.0,
1344
+ 0.0,
1345
+ 0.0,
1346
+ 0.0,
1347
+ 0.0,
1348
+ 0.0,
1349
+ 0.0,
1350
+ 0.0,
1351
+ 0.0,
1352
+ 0.0,
1353
+ 0.0,
1354
+ 0.0,
1355
+ 0.0,
1356
+ 0.0,
1357
+ 0.0,
1358
+ 0.0,
1359
+ 0.0,
1360
+ 0.0,
1361
+ 0.0,
1362
+ 0.0,
1363
+ 0.0,
1364
+ 0.0,
1365
+ 0.0,
1366
+ 0.0,
1367
+ 0.0,
1368
+ 0.0,
1369
+ 0.0,
1370
+ 0.0,
1371
+ 0.0,
1372
+ 0.0,
1373
+ 0.0,
1374
+ 0.0,
1375
+ 0.0,
1376
+ 0.0,
1377
+ 0.0,
1378
+ 0.0,
1379
+ 0.0,
1380
+ 0.0,
1381
+ 0.0,
1382
+ 0.0
1383
+ ]
1384
+ },
1385
+ "cash_flow_profile": [
1386
+ 0.0,
1387
+ 0.0,
1388
+ 0.0,
1389
+ 0.0,
1390
+ 0.0,
1391
+ 0.0,
1392
+ 0.0,
1393
+ 0.0,
1394
+ 0.0,
1395
+ 0.0,
1396
+ 0.0,
1397
+ 0.0,
1398
+ 0.0,
1399
+ 0.0,
1400
+ 0.0,
1401
+ 0.0,
1402
+ 0.0,
1403
+ 0.0,
1404
+ 0.0,
1405
+ 0.0,
1406
+ 0.0,
1407
+ 0.0,
1408
+ 0.0,
1409
+ 0.0,
1410
+ 0.0,
1411
+ 0.0,
1412
+ 0.0,
1413
+ 0.0,
1414
+ 0.0,
1415
+ 0.0,
1416
+ 0.0,
1417
+ 0.0,
1418
+ 0.0,
1419
+ 0.0,
1420
+ 0.0,
1421
+ 0.0,
1422
+ 0.0,
1423
+ 0.0,
1424
+ 0.0,
1425
+ 0.0,
1426
+ 0.0,
1427
+ 0.0,
1428
+ 0.0,
1429
+ 0.0,
1430
+ 0.0,
1431
+ 0.0,
1432
+ 0.0,
1433
+ 0.0,
1434
+ 0.0,
1435
+ 0.0,
1436
+ 0.0,
1437
+ 0.0,
1438
+ 0.0,
1439
+ 0.0,
1440
+ 0.0,
1441
+ 0.0,
1442
+ 0.0,
1443
+ 0.0,
1444
+ 0.0,
1445
+ 0.0,
1446
+ 0.0,
1447
+ 0.0,
1448
+ 0.0,
1449
+ 0.0,
1450
+ 0.0,
1451
+ 0.0,
1452
+ 0.0,
1453
+ 0.0,
1454
+ 0.0,
1455
+ 0.0,
1456
+ 0.0,
1457
+ 0.0,
1458
+ 0.0,
1459
+ 0.0,
1460
+ 0.0,
1461
+ 0.0,
1462
+ 0.0,
1463
+ 0.0,
1464
+ 0.0,
1465
+ 0.0,
1466
+ 0.0,
1467
+ 0.0,
1468
+ 0.0,
1469
+ 0.0,
1470
+ 0.0,
1471
+ 0.0,
1472
+ 0.0,
1473
+ 0.0,
1474
+ 0.0,
1475
+ 0.0,
1476
+ 0.0,
1477
+ 0.0,
1478
+ 0.0,
1479
+ 0.0,
1480
+ 0.0,
1481
+ 0.0,
1482
+ 0.0,
1483
+ 0.0,
1484
+ 0.0,
1485
+ 0.0,
1486
+ 0.0,
1487
+ 0.0,
1488
+ 0.0,
1489
+ 0.0,
1490
+ 0.0,
1491
+ 0.0,
1492
+ 0.0,
1493
+ 0.0,
1494
+ 0.0,
1495
+ 0.0,
1496
+ 0.0,
1497
+ 0.0,
1498
+ 0.0,
1499
+ 0.0,
1500
+ 0.0,
1501
+ 0.0,
1502
+ 0.0,
1503
+ 0.0,
1504
+ 0.0,
1505
+ 0.0,
1506
+ 0.0,
1507
+ 0.0,
1508
+ 0.0,
1509
+ 0.0,
1510
+ 0.0,
1511
+ 0.0,
1512
+ 0.0,
1513
+ 0.0,
1514
+ 0.0,
1515
+ 0.0,
1516
+ 0.0,
1517
+ 0.0,
1518
+ 0.0,
1519
+ 0.0,
1520
+ 0.0,
1521
+ 0.0,
1522
+ 0.0,
1523
+ 0.0,
1524
+ 0.0,
1525
+ 0.0,
1526
+ 0.0,
1527
+ 0.0,
1528
+ 0.0,
1529
+ 0.0,
1530
+ 0.0,
1531
+ 0.0,
1532
+ 0.0,
1533
+ 0.0,
1534
+ 0.0,
1535
+ 0.0,
1536
+ 0.0,
1537
+ 0.0,
1538
+ 0.0,
1539
+ 0.0,
1540
+ 0.0,
1541
+ 0.0,
1542
+ 0.0,
1543
+ 0.0,
1544
+ 0.0,
1545
+ 0.0,
1546
+ 0.0,
1547
+ 0.0,
1548
+ 0.0,
1549
+ 0.0,
1550
+ 0.0,
1551
+ 0.0,
1552
+ 0.0,
1553
+ 0.0,
1554
+ 0.0,
1555
+ 0.0,
1556
+ 0.0,
1557
+ 0.0,
1558
+ 0.0,
1559
+ 0.0,
1560
+ 0.0,
1561
+ 0.0,
1562
+ 0.0,
1563
+ 0.0,
1564
+ 0.0,
1565
+ 0.0,
1566
+ 0.0,
1567
+ 0.0,
1568
+ 0.0,
1569
+ 0.0,
1570
+ 0.0,
1571
+ 0.0,
1572
+ 0.0,
1573
+ 0.0,
1574
+ 0.0,
1575
+ 0.0,
1576
+ 0.0,
1577
+ 0.0,
1578
+ 0.0,
1579
+ 0.0,
1580
+ 0.0,
1581
+ 0.0,
1582
+ 0.0,
1583
+ 0.0,
1584
+ 0.0,
1585
+ 0.0,
1586
+ 0.0,
1587
+ 0.0,
1588
+ 0.0,
1589
+ 0.0,
1590
+ 0.0,
1591
+ 0.0,
1592
+ 0.0,
1593
+ 0.0,
1594
+ 0.0,
1595
+ 0.0,
1596
+ 0.0,
1597
+ 0.0,
1598
+ 0.0,
1599
+ 0.0,
1600
+ 0.0,
1601
+ 0.0,
1602
+ 0.0,
1603
+ 0.0,
1604
+ 0.0,
1605
+ 0.0,
1606
+ 0.0
1607
+ ],
1608
+ "selected_projects": [],
1609
+ "suspended_projects": [
1610
+ "proj-000",
1611
+ "proj-001",
1612
+ "proj-002",
1613
+ "proj-003",
1614
+ "proj-004",
1615
+ "proj-005",
1616
+ "proj-006"
1617
+ ],
1618
+ "simulation_runs": [],
1619
+ "pareto_front": [],
1620
+ "risk_contributors": []
1621
+ }
1622
+ }
assets/samples/sample_oil_gas_field.json ADDED
@@ -0,0 +1,1537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "oil_gas_field",
3
+ "uncertainty": "supply_delay",
4
+ "algorithm": "scenario_monte_carlo",
5
+ "scenario_label": "Oil & Gas Field Development",
6
+ "summary": {
7
+ "projects": 5,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 200,
11
+ "budget_musd": 120.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "scenario_monte_carlo",
15
+ "scenario": "oil_gas_field",
16
+ "uncertainty": "supply_delay",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.108,
34
+ "status": "feasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0,
38
+ "monte_carlo_p90_cost": 0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 100.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {
46
+ "res-human": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0,
228
+ 0.0,
229
+ 0.0,
230
+ 0.0,
231
+ 0.0,
232
+ 0.0,
233
+ 0.0,
234
+ 0.0,
235
+ 0.0,
236
+ 0.0,
237
+ 0.0,
238
+ 0.0,
239
+ 0.0,
240
+ 0.0,
241
+ 0.0,
242
+ 0.0,
243
+ 0.0,
244
+ 0.0,
245
+ 0.0,
246
+ 0.0,
247
+ 0.0
248
+ ],
249
+ "res-machinery": [
250
+ 0.0,
251
+ 0.0,
252
+ 0.0,
253
+ 0.0,
254
+ 0.0,
255
+ 0.0,
256
+ 0.0,
257
+ 0.0,
258
+ 0.0,
259
+ 0.0,
260
+ 0.0,
261
+ 0.0,
262
+ 0.0,
263
+ 0.0,
264
+ 0.0,
265
+ 0.0,
266
+ 0.0,
267
+ 0.0,
268
+ 0.0,
269
+ 0.0,
270
+ 0.0,
271
+ 0.0,
272
+ 0.0,
273
+ 0.0,
274
+ 0.0,
275
+ 0.0,
276
+ 0.0,
277
+ 0.0,
278
+ 0.0,
279
+ 0.0,
280
+ 0.0,
281
+ 0.0,
282
+ 0.0,
283
+ 0.0,
284
+ 0.0,
285
+ 0.0,
286
+ 0.0,
287
+ 0.0,
288
+ 0.0,
289
+ 0.0,
290
+ 0.0,
291
+ 0.0,
292
+ 0.0,
293
+ 0.0,
294
+ 0.0,
295
+ 0.0,
296
+ 0.0,
297
+ 0.0,
298
+ 0.0,
299
+ 0.0,
300
+ 0.0,
301
+ 0.0,
302
+ 0.0,
303
+ 0.0,
304
+ 0.0,
305
+ 0.0,
306
+ 0.0,
307
+ 0.0,
308
+ 0.0,
309
+ 0.0,
310
+ 0.0,
311
+ 0.0,
312
+ 0.0,
313
+ 0.0,
314
+ 0.0,
315
+ 0.0,
316
+ 0.0,
317
+ 0.0,
318
+ 0.0,
319
+ 0.0,
320
+ 0.0,
321
+ 0.0,
322
+ 0.0,
323
+ 0.0,
324
+ 0.0,
325
+ 0.0,
326
+ 0.0,
327
+ 0.0,
328
+ 0.0,
329
+ 0.0,
330
+ 0.0,
331
+ 0.0,
332
+ 0.0,
333
+ 0.0,
334
+ 0.0,
335
+ 0.0,
336
+ 0.0,
337
+ 0.0,
338
+ 0.0,
339
+ 0.0,
340
+ 0.0,
341
+ 0.0,
342
+ 0.0,
343
+ 0.0,
344
+ 0.0,
345
+ 0.0,
346
+ 0.0,
347
+ 0.0,
348
+ 0.0,
349
+ 0.0,
350
+ 0.0,
351
+ 0.0,
352
+ 0.0,
353
+ 0.0,
354
+ 0.0,
355
+ 0.0,
356
+ 0.0,
357
+ 0.0,
358
+ 0.0,
359
+ 0.0,
360
+ 0.0,
361
+ 0.0,
362
+ 0.0,
363
+ 0.0,
364
+ 0.0,
365
+ 0.0,
366
+ 0.0,
367
+ 0.0,
368
+ 0.0,
369
+ 0.0,
370
+ 0.0,
371
+ 0.0,
372
+ 0.0,
373
+ 0.0,
374
+ 0.0,
375
+ 0.0,
376
+ 0.0,
377
+ 0.0,
378
+ 0.0,
379
+ 0.0,
380
+ 0.0,
381
+ 0.0,
382
+ 0.0,
383
+ 0.0,
384
+ 0.0,
385
+ 0.0,
386
+ 0.0,
387
+ 0.0,
388
+ 0.0,
389
+ 0.0,
390
+ 0.0,
391
+ 0.0,
392
+ 0.0,
393
+ 0.0,
394
+ 0.0,
395
+ 0.0,
396
+ 0.0,
397
+ 0.0,
398
+ 0.0,
399
+ 0.0,
400
+ 0.0,
401
+ 0.0,
402
+ 0.0,
403
+ 0.0,
404
+ 0.0,
405
+ 0.0,
406
+ 0.0,
407
+ 0.0,
408
+ 0.0,
409
+ 0.0,
410
+ 0.0,
411
+ 0.0,
412
+ 0.0,
413
+ 0.0,
414
+ 0.0,
415
+ 0.0,
416
+ 0.0,
417
+ 0.0,
418
+ 0.0,
419
+ 0.0,
420
+ 0.0,
421
+ 0.0,
422
+ 0.0,
423
+ 0.0,
424
+ 0.0,
425
+ 0.0,
426
+ 0.0,
427
+ 0.0,
428
+ 0.0,
429
+ 0.0,
430
+ 0.0,
431
+ 0.0,
432
+ 0.0,
433
+ 0.0,
434
+ 0.0,
435
+ 0.0,
436
+ 0.0,
437
+ 0.0,
438
+ 0.0,
439
+ 0.0,
440
+ 0.0,
441
+ 0.0,
442
+ 0.0,
443
+ 0.0,
444
+ 0.0,
445
+ 0.0,
446
+ 0.0,
447
+ 0.0,
448
+ 0.0,
449
+ 0.0,
450
+ 0.0
451
+ ],
452
+ "res-contractor": [
453
+ 0.0,
454
+ 0.0,
455
+ 0.0,
456
+ 0.0,
457
+ 0.0,
458
+ 0.0,
459
+ 0.0,
460
+ 0.0,
461
+ 0.0,
462
+ 0.0,
463
+ 0.0,
464
+ 0.0,
465
+ 0.0,
466
+ 0.0,
467
+ 0.0,
468
+ 0.0,
469
+ 0.0,
470
+ 0.0,
471
+ 0.0,
472
+ 0.0,
473
+ 0.0,
474
+ 0.0,
475
+ 0.0,
476
+ 0.0,
477
+ 0.0,
478
+ 0.0,
479
+ 0.0,
480
+ 0.0,
481
+ 0.0,
482
+ 0.0,
483
+ 0.0,
484
+ 0.0,
485
+ 0.0,
486
+ 0.0,
487
+ 0.0,
488
+ 0.0,
489
+ 0.0,
490
+ 0.0,
491
+ 0.0,
492
+ 0.0,
493
+ 0.0,
494
+ 0.0,
495
+ 0.0,
496
+ 0.0,
497
+ 0.0,
498
+ 0.0,
499
+ 0.0,
500
+ 0.0,
501
+ 0.0,
502
+ 0.0,
503
+ 0.0,
504
+ 0.0,
505
+ 0.0,
506
+ 0.0,
507
+ 0.0,
508
+ 0.0,
509
+ 0.0,
510
+ 0.0,
511
+ 0.0,
512
+ 0.0,
513
+ 0.0,
514
+ 0.0,
515
+ 0.0,
516
+ 0.0,
517
+ 0.0,
518
+ 0.0,
519
+ 0.0,
520
+ 0.0,
521
+ 0.0,
522
+ 0.0,
523
+ 0.0,
524
+ 0.0,
525
+ 0.0,
526
+ 0.0,
527
+ 0.0,
528
+ 0.0,
529
+ 0.0,
530
+ 0.0,
531
+ 0.0,
532
+ 0.0,
533
+ 0.0,
534
+ 0.0,
535
+ 0.0,
536
+ 0.0,
537
+ 0.0,
538
+ 0.0,
539
+ 0.0,
540
+ 0.0,
541
+ 0.0,
542
+ 0.0,
543
+ 0.0,
544
+ 0.0,
545
+ 0.0,
546
+ 0.0,
547
+ 0.0,
548
+ 0.0,
549
+ 0.0,
550
+ 0.0,
551
+ 0.0,
552
+ 0.0,
553
+ 0.0,
554
+ 0.0,
555
+ 0.0,
556
+ 0.0,
557
+ 0.0,
558
+ 0.0,
559
+ 0.0,
560
+ 0.0,
561
+ 0.0,
562
+ 0.0,
563
+ 0.0,
564
+ 0.0,
565
+ 0.0,
566
+ 0.0,
567
+ 0.0,
568
+ 0.0,
569
+ 0.0,
570
+ 0.0,
571
+ 0.0,
572
+ 0.0,
573
+ 0.0,
574
+ 0.0,
575
+ 0.0,
576
+ 0.0,
577
+ 0.0,
578
+ 0.0,
579
+ 0.0,
580
+ 0.0,
581
+ 0.0,
582
+ 0.0,
583
+ 0.0,
584
+ 0.0,
585
+ 0.0,
586
+ 0.0,
587
+ 0.0,
588
+ 0.0,
589
+ 0.0,
590
+ 0.0,
591
+ 0.0,
592
+ 0.0,
593
+ 0.0,
594
+ 0.0,
595
+ 0.0,
596
+ 0.0,
597
+ 0.0,
598
+ 0.0,
599
+ 0.0,
600
+ 0.0,
601
+ 0.0,
602
+ 0.0,
603
+ 0.0,
604
+ 0.0,
605
+ 0.0,
606
+ 0.0,
607
+ 0.0,
608
+ 0.0,
609
+ 0.0,
610
+ 0.0,
611
+ 0.0,
612
+ 0.0,
613
+ 0.0,
614
+ 0.0,
615
+ 0.0,
616
+ 0.0,
617
+ 0.0,
618
+ 0.0,
619
+ 0.0,
620
+ 0.0,
621
+ 0.0,
622
+ 0.0,
623
+ 0.0,
624
+ 0.0,
625
+ 0.0,
626
+ 0.0,
627
+ 0.0,
628
+ 0.0,
629
+ 0.0,
630
+ 0.0,
631
+ 0.0,
632
+ 0.0,
633
+ 0.0,
634
+ 0.0,
635
+ 0.0,
636
+ 0.0,
637
+ 0.0,
638
+ 0.0,
639
+ 0.0,
640
+ 0.0,
641
+ 0.0,
642
+ 0.0,
643
+ 0.0,
644
+ 0.0,
645
+ 0.0,
646
+ 0.0,
647
+ 0.0,
648
+ 0.0,
649
+ 0.0,
650
+ 0.0,
651
+ 0.0,
652
+ 0.0,
653
+ 0.0
654
+ ],
655
+ "res-equipment": [
656
+ 0.0,
657
+ 0.0,
658
+ 0.0,
659
+ 0.0,
660
+ 0.0,
661
+ 0.0,
662
+ 0.0,
663
+ 0.0,
664
+ 0.0,
665
+ 0.0,
666
+ 0.0,
667
+ 0.0,
668
+ 0.0,
669
+ 0.0,
670
+ 0.0,
671
+ 0.0,
672
+ 0.0,
673
+ 0.0,
674
+ 0.0,
675
+ 0.0,
676
+ 0.0,
677
+ 0.0,
678
+ 0.0,
679
+ 0.0,
680
+ 0.0,
681
+ 0.0,
682
+ 0.0,
683
+ 0.0,
684
+ 0.0,
685
+ 0.0,
686
+ 0.0,
687
+ 0.0,
688
+ 0.0,
689
+ 0.0,
690
+ 0.0,
691
+ 0.0,
692
+ 0.0,
693
+ 0.0,
694
+ 0.0,
695
+ 0.0,
696
+ 0.0,
697
+ 0.0,
698
+ 0.0,
699
+ 0.0,
700
+ 0.0,
701
+ 0.0,
702
+ 0.0,
703
+ 0.0,
704
+ 0.0,
705
+ 0.0,
706
+ 0.0,
707
+ 0.0,
708
+ 0.0,
709
+ 0.0,
710
+ 0.0,
711
+ 0.0,
712
+ 0.0,
713
+ 0.0,
714
+ 0.0,
715
+ 0.0,
716
+ 0.0,
717
+ 0.0,
718
+ 0.0,
719
+ 0.0,
720
+ 0.0,
721
+ 0.0,
722
+ 0.0,
723
+ 0.0,
724
+ 0.0,
725
+ 0.0,
726
+ 0.0,
727
+ 0.0,
728
+ 0.0,
729
+ 0.0,
730
+ 0.0,
731
+ 0.0,
732
+ 0.0,
733
+ 0.0,
734
+ 0.0,
735
+ 0.0,
736
+ 0.0,
737
+ 0.0,
738
+ 0.0,
739
+ 0.0,
740
+ 0.0,
741
+ 0.0,
742
+ 0.0,
743
+ 0.0,
744
+ 0.0,
745
+ 0.0,
746
+ 0.0,
747
+ 0.0,
748
+ 0.0,
749
+ 0.0,
750
+ 0.0,
751
+ 0.0,
752
+ 0.0,
753
+ 0.0,
754
+ 0.0,
755
+ 0.0,
756
+ 0.0,
757
+ 0.0,
758
+ 0.0,
759
+ 0.0,
760
+ 0.0,
761
+ 0.0,
762
+ 0.0,
763
+ 0.0,
764
+ 0.0,
765
+ 0.0,
766
+ 0.0,
767
+ 0.0,
768
+ 0.0,
769
+ 0.0,
770
+ 0.0,
771
+ 0.0,
772
+ 0.0,
773
+ 0.0,
774
+ 0.0,
775
+ 0.0,
776
+ 0.0,
777
+ 0.0,
778
+ 0.0,
779
+ 0.0,
780
+ 0.0,
781
+ 0.0,
782
+ 0.0,
783
+ 0.0,
784
+ 0.0,
785
+ 0.0,
786
+ 0.0,
787
+ 0.0,
788
+ 0.0,
789
+ 0.0,
790
+ 0.0,
791
+ 0.0,
792
+ 0.0,
793
+ 0.0,
794
+ 0.0,
795
+ 0.0,
796
+ 0.0,
797
+ 0.0,
798
+ 0.0,
799
+ 0.0,
800
+ 0.0,
801
+ 0.0,
802
+ 0.0,
803
+ 0.0,
804
+ 0.0,
805
+ 0.0,
806
+ 0.0,
807
+ 0.0,
808
+ 0.0,
809
+ 0.0,
810
+ 0.0,
811
+ 0.0,
812
+ 0.0,
813
+ 0.0,
814
+ 0.0,
815
+ 0.0,
816
+ 0.0,
817
+ 0.0,
818
+ 0.0,
819
+ 0.0,
820
+ 0.0,
821
+ 0.0,
822
+ 0.0,
823
+ 0.0,
824
+ 0.0,
825
+ 0.0,
826
+ 0.0,
827
+ 0.0,
828
+ 0.0,
829
+ 0.0,
830
+ 0.0,
831
+ 0.0,
832
+ 0.0,
833
+ 0.0,
834
+ 0.0,
835
+ 0.0,
836
+ 0.0,
837
+ 0.0,
838
+ 0.0,
839
+ 0.0,
840
+ 0.0,
841
+ 0.0,
842
+ 0.0,
843
+ 0.0,
844
+ 0.0,
845
+ 0.0,
846
+ 0.0,
847
+ 0.0,
848
+ 0.0,
849
+ 0.0,
850
+ 0.0,
851
+ 0.0,
852
+ 0.0,
853
+ 0.0,
854
+ 0.0,
855
+ 0.0,
856
+ 0.0
857
+ ],
858
+ "res-laboratory": [
859
+ 0.0,
860
+ 0.0,
861
+ 0.0,
862
+ 0.0,
863
+ 0.0,
864
+ 0.0,
865
+ 0.0,
866
+ 0.0,
867
+ 0.0,
868
+ 0.0,
869
+ 0.0,
870
+ 0.0,
871
+ 0.0,
872
+ 0.0,
873
+ 0.0,
874
+ 0.0,
875
+ 0.0,
876
+ 0.0,
877
+ 0.0,
878
+ 0.0,
879
+ 0.0,
880
+ 0.0,
881
+ 0.0,
882
+ 0.0,
883
+ 0.0,
884
+ 0.0,
885
+ 0.0,
886
+ 0.0,
887
+ 0.0,
888
+ 0.0,
889
+ 0.0,
890
+ 0.0,
891
+ 0.0,
892
+ 0.0,
893
+ 0.0,
894
+ 0.0,
895
+ 0.0,
896
+ 0.0,
897
+ 0.0,
898
+ 0.0,
899
+ 0.0,
900
+ 0.0,
901
+ 0.0,
902
+ 0.0,
903
+ 0.0,
904
+ 0.0,
905
+ 0.0,
906
+ 0.0,
907
+ 0.0,
908
+ 0.0,
909
+ 0.0,
910
+ 0.0,
911
+ 0.0,
912
+ 0.0,
913
+ 0.0,
914
+ 0.0,
915
+ 0.0,
916
+ 0.0,
917
+ 0.0,
918
+ 0.0,
919
+ 0.0,
920
+ 0.0,
921
+ 0.0,
922
+ 0.0,
923
+ 0.0,
924
+ 0.0,
925
+ 0.0,
926
+ 0.0,
927
+ 0.0,
928
+ 0.0,
929
+ 0.0,
930
+ 0.0,
931
+ 0.0,
932
+ 0.0,
933
+ 0.0,
934
+ 0.0,
935
+ 0.0,
936
+ 0.0,
937
+ 0.0,
938
+ 0.0,
939
+ 0.0,
940
+ 0.0,
941
+ 0.0,
942
+ 0.0,
943
+ 0.0,
944
+ 0.0,
945
+ 0.0,
946
+ 0.0,
947
+ 0.0,
948
+ 0.0,
949
+ 0.0,
950
+ 0.0,
951
+ 0.0,
952
+ 0.0,
953
+ 0.0,
954
+ 0.0,
955
+ 0.0,
956
+ 0.0,
957
+ 0.0,
958
+ 0.0,
959
+ 0.0,
960
+ 0.0,
961
+ 0.0,
962
+ 0.0,
963
+ 0.0,
964
+ 0.0,
965
+ 0.0,
966
+ 0.0,
967
+ 0.0,
968
+ 0.0,
969
+ 0.0,
970
+ 0.0,
971
+ 0.0,
972
+ 0.0,
973
+ 0.0,
974
+ 0.0,
975
+ 0.0,
976
+ 0.0,
977
+ 0.0,
978
+ 0.0,
979
+ 0.0,
980
+ 0.0,
981
+ 0.0,
982
+ 0.0,
983
+ 0.0,
984
+ 0.0,
985
+ 0.0,
986
+ 0.0,
987
+ 0.0,
988
+ 0.0,
989
+ 0.0,
990
+ 0.0,
991
+ 0.0,
992
+ 0.0,
993
+ 0.0,
994
+ 0.0,
995
+ 0.0,
996
+ 0.0,
997
+ 0.0,
998
+ 0.0,
999
+ 0.0,
1000
+ 0.0,
1001
+ 0.0,
1002
+ 0.0,
1003
+ 0.0,
1004
+ 0.0,
1005
+ 0.0,
1006
+ 0.0,
1007
+ 0.0,
1008
+ 0.0,
1009
+ 0.0,
1010
+ 0.0,
1011
+ 0.0,
1012
+ 0.0,
1013
+ 0.0,
1014
+ 0.0,
1015
+ 0.0,
1016
+ 0.0,
1017
+ 0.0,
1018
+ 0.0,
1019
+ 0.0,
1020
+ 0.0,
1021
+ 0.0,
1022
+ 0.0,
1023
+ 0.0,
1024
+ 0.0,
1025
+ 0.0,
1026
+ 0.0,
1027
+ 0.0,
1028
+ 0.0,
1029
+ 0.0,
1030
+ 0.0,
1031
+ 0.0,
1032
+ 0.0,
1033
+ 0.0,
1034
+ 0.0,
1035
+ 0.0,
1036
+ 0.0,
1037
+ 0.0,
1038
+ 0.0,
1039
+ 0.0,
1040
+ 0.0,
1041
+ 0.0,
1042
+ 0.0,
1043
+ 0.0,
1044
+ 0.0,
1045
+ 0.0,
1046
+ 0.0,
1047
+ 0.0,
1048
+ 0.0,
1049
+ 0.0,
1050
+ 0.0,
1051
+ 0.0,
1052
+ 0.0,
1053
+ 0.0,
1054
+ 0.0,
1055
+ 0.0,
1056
+ 0.0,
1057
+ 0.0,
1058
+ 0.0,
1059
+ 0.0
1060
+ ],
1061
+ "res-specialist": [
1062
+ 0.0,
1063
+ 0.0,
1064
+ 0.0,
1065
+ 0.0,
1066
+ 0.0,
1067
+ 0.0,
1068
+ 0.0,
1069
+ 0.0,
1070
+ 0.0,
1071
+ 0.0,
1072
+ 0.0,
1073
+ 0.0,
1074
+ 0.0,
1075
+ 0.0,
1076
+ 0.0,
1077
+ 0.0,
1078
+ 0.0,
1079
+ 0.0,
1080
+ 0.0,
1081
+ 0.0,
1082
+ 0.0,
1083
+ 0.0,
1084
+ 0.0,
1085
+ 0.0,
1086
+ 0.0,
1087
+ 0.0,
1088
+ 0.0,
1089
+ 0.0,
1090
+ 0.0,
1091
+ 0.0,
1092
+ 0.0,
1093
+ 0.0,
1094
+ 0.0,
1095
+ 0.0,
1096
+ 0.0,
1097
+ 0.0,
1098
+ 0.0,
1099
+ 0.0,
1100
+ 0.0,
1101
+ 0.0,
1102
+ 0.0,
1103
+ 0.0,
1104
+ 0.0,
1105
+ 0.0,
1106
+ 0.0,
1107
+ 0.0,
1108
+ 0.0,
1109
+ 0.0,
1110
+ 0.0,
1111
+ 0.0,
1112
+ 0.0,
1113
+ 0.0,
1114
+ 0.0,
1115
+ 0.0,
1116
+ 0.0,
1117
+ 0.0,
1118
+ 0.0,
1119
+ 0.0,
1120
+ 0.0,
1121
+ 0.0,
1122
+ 0.0,
1123
+ 0.0,
1124
+ 0.0,
1125
+ 0.0,
1126
+ 0.0,
1127
+ 0.0,
1128
+ 0.0,
1129
+ 0.0,
1130
+ 0.0,
1131
+ 0.0,
1132
+ 0.0,
1133
+ 0.0,
1134
+ 0.0,
1135
+ 0.0,
1136
+ 0.0,
1137
+ 0.0,
1138
+ 0.0,
1139
+ 0.0,
1140
+ 0.0,
1141
+ 0.0,
1142
+ 0.0,
1143
+ 0.0,
1144
+ 0.0,
1145
+ 0.0,
1146
+ 0.0,
1147
+ 0.0,
1148
+ 0.0,
1149
+ 0.0,
1150
+ 0.0,
1151
+ 0.0,
1152
+ 0.0,
1153
+ 0.0,
1154
+ 0.0,
1155
+ 0.0,
1156
+ 0.0,
1157
+ 0.0,
1158
+ 0.0,
1159
+ 0.0,
1160
+ 0.0,
1161
+ 0.0,
1162
+ 0.0,
1163
+ 0.0,
1164
+ 0.0,
1165
+ 0.0,
1166
+ 0.0,
1167
+ 0.0,
1168
+ 0.0,
1169
+ 0.0,
1170
+ 0.0,
1171
+ 0.0,
1172
+ 0.0,
1173
+ 0.0,
1174
+ 0.0,
1175
+ 0.0,
1176
+ 0.0,
1177
+ 0.0,
1178
+ 0.0,
1179
+ 0.0,
1180
+ 0.0,
1181
+ 0.0,
1182
+ 0.0,
1183
+ 0.0,
1184
+ 0.0,
1185
+ 0.0,
1186
+ 0.0,
1187
+ 0.0,
1188
+ 0.0,
1189
+ 0.0,
1190
+ 0.0,
1191
+ 0.0,
1192
+ 0.0,
1193
+ 0.0,
1194
+ 0.0,
1195
+ 0.0,
1196
+ 0.0,
1197
+ 0.0,
1198
+ 0.0,
1199
+ 0.0,
1200
+ 0.0,
1201
+ 0.0,
1202
+ 0.0,
1203
+ 0.0,
1204
+ 0.0,
1205
+ 0.0,
1206
+ 0.0,
1207
+ 0.0,
1208
+ 0.0,
1209
+ 0.0,
1210
+ 0.0,
1211
+ 0.0,
1212
+ 0.0,
1213
+ 0.0,
1214
+ 0.0,
1215
+ 0.0,
1216
+ 0.0,
1217
+ 0.0,
1218
+ 0.0,
1219
+ 0.0,
1220
+ 0.0,
1221
+ 0.0,
1222
+ 0.0,
1223
+ 0.0,
1224
+ 0.0,
1225
+ 0.0,
1226
+ 0.0,
1227
+ 0.0,
1228
+ 0.0,
1229
+ 0.0,
1230
+ 0.0,
1231
+ 0.0,
1232
+ 0.0,
1233
+ 0.0,
1234
+ 0.0,
1235
+ 0.0,
1236
+ 0.0,
1237
+ 0.0,
1238
+ 0.0,
1239
+ 0.0,
1240
+ 0.0,
1241
+ 0.0,
1242
+ 0.0,
1243
+ 0.0,
1244
+ 0.0,
1245
+ 0.0,
1246
+ 0.0,
1247
+ 0.0,
1248
+ 0.0,
1249
+ 0.0,
1250
+ 0.0,
1251
+ 0.0,
1252
+ 0.0,
1253
+ 0.0,
1254
+ 0.0,
1255
+ 0.0,
1256
+ 0.0,
1257
+ 0.0,
1258
+ 0.0,
1259
+ 0.0,
1260
+ 0.0,
1261
+ 0.0,
1262
+ 0.0
1263
+ ]
1264
+ },
1265
+ "cash_flow_profile": [
1266
+ 0.0,
1267
+ 0.0,
1268
+ 0.0,
1269
+ 0.0,
1270
+ 0.0,
1271
+ 0.0,
1272
+ 0.0,
1273
+ 0.0,
1274
+ 0.0,
1275
+ 0.0,
1276
+ 0.0,
1277
+ 0.0,
1278
+ 0.0,
1279
+ 0.0,
1280
+ 0.0,
1281
+ 0.0,
1282
+ 0.0,
1283
+ 0.0,
1284
+ 0.0,
1285
+ 0.0,
1286
+ 0.0,
1287
+ 0.0,
1288
+ 0.0,
1289
+ 0.0,
1290
+ 0.0,
1291
+ 0.0,
1292
+ 0.0,
1293
+ 0.0,
1294
+ 0.0,
1295
+ 0.0,
1296
+ 0.0,
1297
+ 0.0,
1298
+ 0.0,
1299
+ 0.0,
1300
+ 0.0,
1301
+ 0.0,
1302
+ 0.0,
1303
+ 0.0,
1304
+ 0.0,
1305
+ 0.0,
1306
+ 0.0,
1307
+ 0.0,
1308
+ 0.0,
1309
+ 0.0,
1310
+ 0.0,
1311
+ 0.0,
1312
+ 0.0,
1313
+ 0.0,
1314
+ 0.0,
1315
+ 0.0,
1316
+ 0.0,
1317
+ 0.0,
1318
+ 0.0,
1319
+ 0.0,
1320
+ 0.0,
1321
+ 0.0,
1322
+ 0.0,
1323
+ 0.0,
1324
+ 0.0,
1325
+ 0.0,
1326
+ 0.0,
1327
+ 0.0,
1328
+ 0.0,
1329
+ 0.0,
1330
+ 0.0,
1331
+ 0.0,
1332
+ 0.0,
1333
+ 0.0,
1334
+ 0.0,
1335
+ 0.0,
1336
+ 0.0,
1337
+ 0.0,
1338
+ 0.0,
1339
+ 0.0,
1340
+ 0.0,
1341
+ 0.0,
1342
+ 0.0,
1343
+ 0.0,
1344
+ 0.0,
1345
+ 0.0,
1346
+ 0.0,
1347
+ 0.0,
1348
+ 0.0,
1349
+ 0.0,
1350
+ 0.0,
1351
+ 0.0,
1352
+ 0.0,
1353
+ 0.0,
1354
+ 0.0,
1355
+ 0.0,
1356
+ 0.0,
1357
+ 0.0,
1358
+ 0.0,
1359
+ 0.0,
1360
+ 0.0,
1361
+ 0.0,
1362
+ 0.0,
1363
+ 0.0,
1364
+ 0.0,
1365
+ 0.0,
1366
+ 0.0,
1367
+ 0.0,
1368
+ 0.0,
1369
+ 0.0,
1370
+ 0.0,
1371
+ 0.0,
1372
+ 0.0,
1373
+ 0.0,
1374
+ 0.0,
1375
+ 0.0,
1376
+ 0.0,
1377
+ 0.0,
1378
+ 0.0,
1379
+ 0.0,
1380
+ 0.0,
1381
+ 0.0,
1382
+ 0.0,
1383
+ 0.0,
1384
+ 0.0,
1385
+ 0.0,
1386
+ 0.0,
1387
+ 0.0,
1388
+ 0.0,
1389
+ 0.0,
1390
+ 0.0,
1391
+ 0.0,
1392
+ 0.0,
1393
+ 0.0,
1394
+ 0.0,
1395
+ 0.0,
1396
+ 0.0,
1397
+ 0.0,
1398
+ 0.0,
1399
+ 0.0,
1400
+ 0.0,
1401
+ 0.0,
1402
+ 0.0,
1403
+ 0.0,
1404
+ 0.0,
1405
+ 0.0,
1406
+ 0.0,
1407
+ 0.0,
1408
+ 0.0,
1409
+ 0.0,
1410
+ 0.0,
1411
+ 0.0,
1412
+ 0.0,
1413
+ 0.0,
1414
+ 0.0,
1415
+ 0.0,
1416
+ 0.0,
1417
+ 0.0,
1418
+ 0.0,
1419
+ 0.0,
1420
+ 0.0,
1421
+ 0.0,
1422
+ 0.0,
1423
+ 0.0,
1424
+ 0.0,
1425
+ 0.0,
1426
+ 0.0,
1427
+ 0.0,
1428
+ 0.0,
1429
+ 0.0,
1430
+ 0.0,
1431
+ 0.0,
1432
+ 0.0,
1433
+ 0.0,
1434
+ 0.0,
1435
+ 0.0,
1436
+ 0.0,
1437
+ 0.0,
1438
+ 0.0,
1439
+ 0.0,
1440
+ 0.0,
1441
+ 0.0,
1442
+ 0.0,
1443
+ 0.0,
1444
+ 0.0,
1445
+ 0.0,
1446
+ 0.0,
1447
+ 0.0,
1448
+ 0.0,
1449
+ 0.0,
1450
+ 0.0,
1451
+ 0.0,
1452
+ 0.0,
1453
+ 0.0,
1454
+ 0.0,
1455
+ 0.0,
1456
+ 0.0,
1457
+ 0.0,
1458
+ 0.0,
1459
+ 0.0,
1460
+ 0.0,
1461
+ 0.0,
1462
+ 0.0,
1463
+ 0.0,
1464
+ 0.0,
1465
+ 0.0,
1466
+ 0.0
1467
+ ],
1468
+ "selected_projects": [],
1469
+ "suspended_projects": [
1470
+ "proj-000",
1471
+ "proj-001",
1472
+ "proj-002",
1473
+ "proj-003",
1474
+ "proj-004"
1475
+ ],
1476
+ "simulation_runs": [
1477
+ {
1478
+ "run": 1,
1479
+ "makespan": 0,
1480
+ "cost_musd": 0,
1481
+ "tardiness": 0,
1482
+ "on_time_pct": 0.0
1483
+ },
1484
+ {
1485
+ "run": 2,
1486
+ "makespan": 0,
1487
+ "cost_musd": 0,
1488
+ "tardiness": 0,
1489
+ "on_time_pct": 0.0
1490
+ },
1491
+ {
1492
+ "run": 3,
1493
+ "makespan": 0,
1494
+ "cost_musd": 0,
1495
+ "tardiness": 0,
1496
+ "on_time_pct": 0.0
1497
+ },
1498
+ {
1499
+ "run": 4,
1500
+ "makespan": 0,
1501
+ "cost_musd": 0,
1502
+ "tardiness": 0,
1503
+ "on_time_pct": 0.0
1504
+ },
1505
+ {
1506
+ "run": 5,
1507
+ "makespan": 0,
1508
+ "cost_musd": 0,
1509
+ "tardiness": 0,
1510
+ "on_time_pct": 0.0
1511
+ },
1512
+ {
1513
+ "run": 6,
1514
+ "makespan": 0,
1515
+ "cost_musd": 0,
1516
+ "tardiness": 0,
1517
+ "on_time_pct": 0.0
1518
+ },
1519
+ {
1520
+ "run": 7,
1521
+ "makespan": 0,
1522
+ "cost_musd": 0,
1523
+ "tardiness": 0,
1524
+ "on_time_pct": 0.0
1525
+ },
1526
+ {
1527
+ "run": 8,
1528
+ "makespan": 0,
1529
+ "cost_musd": 0,
1530
+ "tardiness": 0,
1531
+ "on_time_pct": 0.0
1532
+ }
1533
+ ],
1534
+ "pareto_front": [],
1535
+ "risk_contributors": []
1536
+ }
1537
+ }
assets/samples/sample_pharma_rd.json ADDED
@@ -0,0 +1,1759 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "pharma_rd",
3
+ "uncertainty": "scope_volatility",
4
+ "algorithm": "nsga2_portfolio",
5
+ "scenario_label": "Pharmaceutical R&D Pipeline",
6
+ "summary": {
7
+ "projects": 4,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 240,
11
+ "budget_musd": 85.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "nsga2_portfolio",
15
+ "scenario": "pharma_rd",
16
+ "uncertainty": "scope_volatility",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.045,
34
+ "status": "feasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {
46
+ "res-human": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0,
168
+ 0.0,
169
+ 0.0,
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0,
228
+ 0.0,
229
+ 0.0,
230
+ 0.0,
231
+ 0.0,
232
+ 0.0,
233
+ 0.0,
234
+ 0.0,
235
+ 0.0,
236
+ 0.0,
237
+ 0.0,
238
+ 0.0,
239
+ 0.0,
240
+ 0.0,
241
+ 0.0,
242
+ 0.0,
243
+ 0.0,
244
+ 0.0,
245
+ 0.0,
246
+ 0.0,
247
+ 0.0,
248
+ 0.0,
249
+ 0.0,
250
+ 0.0,
251
+ 0.0,
252
+ 0.0,
253
+ 0.0,
254
+ 0.0,
255
+ 0.0,
256
+ 0.0,
257
+ 0.0,
258
+ 0.0,
259
+ 0.0,
260
+ 0.0,
261
+ 0.0,
262
+ 0.0,
263
+ 0.0,
264
+ 0.0,
265
+ 0.0,
266
+ 0.0,
267
+ 0.0,
268
+ 0.0,
269
+ 0.0,
270
+ 0.0,
271
+ 0.0,
272
+ 0.0,
273
+ 0.0,
274
+ 0.0,
275
+ 0.0,
276
+ 0.0,
277
+ 0.0,
278
+ 0.0,
279
+ 0.0,
280
+ 0.0,
281
+ 0.0,
282
+ 0.0,
283
+ 0.0,
284
+ 0.0,
285
+ 0.0,
286
+ 0.0,
287
+ 0.0
288
+ ],
289
+ "res-machinery": [
290
+ 0.0,
291
+ 0.0,
292
+ 0.0,
293
+ 0.0,
294
+ 0.0,
295
+ 0.0,
296
+ 0.0,
297
+ 0.0,
298
+ 0.0,
299
+ 0.0,
300
+ 0.0,
301
+ 0.0,
302
+ 0.0,
303
+ 0.0,
304
+ 0.0,
305
+ 0.0,
306
+ 0.0,
307
+ 0.0,
308
+ 0.0,
309
+ 0.0,
310
+ 0.0,
311
+ 0.0,
312
+ 0.0,
313
+ 0.0,
314
+ 0.0,
315
+ 0.0,
316
+ 0.0,
317
+ 0.0,
318
+ 0.0,
319
+ 0.0,
320
+ 0.0,
321
+ 0.0,
322
+ 0.0,
323
+ 0.0,
324
+ 0.0,
325
+ 0.0,
326
+ 0.0,
327
+ 0.0,
328
+ 0.0,
329
+ 0.0,
330
+ 0.0,
331
+ 0.0,
332
+ 0.0,
333
+ 0.0,
334
+ 0.0,
335
+ 0.0,
336
+ 0.0,
337
+ 0.0,
338
+ 0.0,
339
+ 0.0,
340
+ 0.0,
341
+ 0.0,
342
+ 0.0,
343
+ 0.0,
344
+ 0.0,
345
+ 0.0,
346
+ 0.0,
347
+ 0.0,
348
+ 0.0,
349
+ 0.0,
350
+ 0.0,
351
+ 0.0,
352
+ 0.0,
353
+ 0.0,
354
+ 0.0,
355
+ 0.0,
356
+ 0.0,
357
+ 0.0,
358
+ 0.0,
359
+ 0.0,
360
+ 0.0,
361
+ 0.0,
362
+ 0.0,
363
+ 0.0,
364
+ 0.0,
365
+ 0.0,
366
+ 0.0,
367
+ 0.0,
368
+ 0.0,
369
+ 0.0,
370
+ 0.0,
371
+ 0.0,
372
+ 0.0,
373
+ 0.0,
374
+ 0.0,
375
+ 0.0,
376
+ 0.0,
377
+ 0.0,
378
+ 0.0,
379
+ 0.0,
380
+ 0.0,
381
+ 0.0,
382
+ 0.0,
383
+ 0.0,
384
+ 0.0,
385
+ 0.0,
386
+ 0.0,
387
+ 0.0,
388
+ 0.0,
389
+ 0.0,
390
+ 0.0,
391
+ 0.0,
392
+ 0.0,
393
+ 0.0,
394
+ 0.0,
395
+ 0.0,
396
+ 0.0,
397
+ 0.0,
398
+ 0.0,
399
+ 0.0,
400
+ 0.0,
401
+ 0.0,
402
+ 0.0,
403
+ 0.0,
404
+ 0.0,
405
+ 0.0,
406
+ 0.0,
407
+ 0.0,
408
+ 0.0,
409
+ 0.0,
410
+ 0.0,
411
+ 0.0,
412
+ 0.0,
413
+ 0.0,
414
+ 0.0,
415
+ 0.0,
416
+ 0.0,
417
+ 0.0,
418
+ 0.0,
419
+ 0.0,
420
+ 0.0,
421
+ 0.0,
422
+ 0.0,
423
+ 0.0,
424
+ 0.0,
425
+ 0.0,
426
+ 0.0,
427
+ 0.0,
428
+ 0.0,
429
+ 0.0,
430
+ 0.0,
431
+ 0.0,
432
+ 0.0,
433
+ 0.0,
434
+ 0.0,
435
+ 0.0,
436
+ 0.0,
437
+ 0.0,
438
+ 0.0,
439
+ 0.0,
440
+ 0.0,
441
+ 0.0,
442
+ 0.0,
443
+ 0.0,
444
+ 0.0,
445
+ 0.0,
446
+ 0.0,
447
+ 0.0,
448
+ 0.0,
449
+ 0.0,
450
+ 0.0,
451
+ 0.0,
452
+ 0.0,
453
+ 0.0,
454
+ 0.0,
455
+ 0.0,
456
+ 0.0,
457
+ 0.0,
458
+ 0.0,
459
+ 0.0,
460
+ 0.0,
461
+ 0.0,
462
+ 0.0,
463
+ 0.0,
464
+ 0.0,
465
+ 0.0,
466
+ 0.0,
467
+ 0.0,
468
+ 0.0,
469
+ 0.0,
470
+ 0.0,
471
+ 0.0,
472
+ 0.0,
473
+ 0.0,
474
+ 0.0,
475
+ 0.0,
476
+ 0.0,
477
+ 0.0,
478
+ 0.0,
479
+ 0.0,
480
+ 0.0,
481
+ 0.0,
482
+ 0.0,
483
+ 0.0,
484
+ 0.0,
485
+ 0.0,
486
+ 0.0,
487
+ 0.0,
488
+ 0.0,
489
+ 0.0,
490
+ 0.0,
491
+ 0.0,
492
+ 0.0,
493
+ 0.0,
494
+ 0.0,
495
+ 0.0,
496
+ 0.0,
497
+ 0.0,
498
+ 0.0,
499
+ 0.0,
500
+ 0.0,
501
+ 0.0,
502
+ 0.0,
503
+ 0.0,
504
+ 0.0,
505
+ 0.0,
506
+ 0.0,
507
+ 0.0,
508
+ 0.0,
509
+ 0.0,
510
+ 0.0,
511
+ 0.0,
512
+ 0.0,
513
+ 0.0,
514
+ 0.0,
515
+ 0.0,
516
+ 0.0,
517
+ 0.0,
518
+ 0.0,
519
+ 0.0,
520
+ 0.0,
521
+ 0.0,
522
+ 0.0,
523
+ 0.0,
524
+ 0.0,
525
+ 0.0,
526
+ 0.0,
527
+ 0.0,
528
+ 0.0,
529
+ 0.0,
530
+ 0.0
531
+ ],
532
+ "res-contractor": [
533
+ 0.0,
534
+ 0.0,
535
+ 0.0,
536
+ 0.0,
537
+ 0.0,
538
+ 0.0,
539
+ 0.0,
540
+ 0.0,
541
+ 0.0,
542
+ 0.0,
543
+ 0.0,
544
+ 0.0,
545
+ 0.0,
546
+ 0.0,
547
+ 0.0,
548
+ 0.0,
549
+ 0.0,
550
+ 0.0,
551
+ 0.0,
552
+ 0.0,
553
+ 0.0,
554
+ 0.0,
555
+ 0.0,
556
+ 0.0,
557
+ 0.0,
558
+ 0.0,
559
+ 0.0,
560
+ 0.0,
561
+ 0.0,
562
+ 0.0,
563
+ 0.0,
564
+ 0.0,
565
+ 0.0,
566
+ 0.0,
567
+ 0.0,
568
+ 0.0,
569
+ 0.0,
570
+ 0.0,
571
+ 0.0,
572
+ 0.0,
573
+ 0.0,
574
+ 0.0,
575
+ 0.0,
576
+ 0.0,
577
+ 0.0,
578
+ 0.0,
579
+ 0.0,
580
+ 0.0,
581
+ 0.0,
582
+ 0.0,
583
+ 0.0,
584
+ 0.0,
585
+ 0.0,
586
+ 0.0,
587
+ 0.0,
588
+ 0.0,
589
+ 0.0,
590
+ 0.0,
591
+ 0.0,
592
+ 0.0,
593
+ 0.0,
594
+ 0.0,
595
+ 0.0,
596
+ 0.0,
597
+ 0.0,
598
+ 0.0,
599
+ 0.0,
600
+ 0.0,
601
+ 0.0,
602
+ 0.0,
603
+ 0.0,
604
+ 0.0,
605
+ 0.0,
606
+ 0.0,
607
+ 0.0,
608
+ 0.0,
609
+ 0.0,
610
+ 0.0,
611
+ 0.0,
612
+ 0.0,
613
+ 0.0,
614
+ 0.0,
615
+ 0.0,
616
+ 0.0,
617
+ 0.0,
618
+ 0.0,
619
+ 0.0,
620
+ 0.0,
621
+ 0.0,
622
+ 0.0,
623
+ 0.0,
624
+ 0.0,
625
+ 0.0,
626
+ 0.0,
627
+ 0.0,
628
+ 0.0,
629
+ 0.0,
630
+ 0.0,
631
+ 0.0,
632
+ 0.0,
633
+ 0.0,
634
+ 0.0,
635
+ 0.0,
636
+ 0.0,
637
+ 0.0,
638
+ 0.0,
639
+ 0.0,
640
+ 0.0,
641
+ 0.0,
642
+ 0.0,
643
+ 0.0,
644
+ 0.0,
645
+ 0.0,
646
+ 0.0,
647
+ 0.0,
648
+ 0.0,
649
+ 0.0,
650
+ 0.0,
651
+ 0.0,
652
+ 0.0,
653
+ 0.0,
654
+ 0.0,
655
+ 0.0,
656
+ 0.0,
657
+ 0.0,
658
+ 0.0,
659
+ 0.0,
660
+ 0.0,
661
+ 0.0,
662
+ 0.0,
663
+ 0.0,
664
+ 0.0,
665
+ 0.0,
666
+ 0.0,
667
+ 0.0,
668
+ 0.0,
669
+ 0.0,
670
+ 0.0,
671
+ 0.0,
672
+ 0.0,
673
+ 0.0,
674
+ 0.0,
675
+ 0.0,
676
+ 0.0,
677
+ 0.0,
678
+ 0.0,
679
+ 0.0,
680
+ 0.0,
681
+ 0.0,
682
+ 0.0,
683
+ 0.0,
684
+ 0.0,
685
+ 0.0,
686
+ 0.0,
687
+ 0.0,
688
+ 0.0,
689
+ 0.0,
690
+ 0.0,
691
+ 0.0,
692
+ 0.0,
693
+ 0.0,
694
+ 0.0,
695
+ 0.0,
696
+ 0.0,
697
+ 0.0,
698
+ 0.0,
699
+ 0.0,
700
+ 0.0,
701
+ 0.0,
702
+ 0.0,
703
+ 0.0,
704
+ 0.0,
705
+ 0.0,
706
+ 0.0,
707
+ 0.0,
708
+ 0.0,
709
+ 0.0,
710
+ 0.0,
711
+ 0.0,
712
+ 0.0,
713
+ 0.0,
714
+ 0.0,
715
+ 0.0,
716
+ 0.0,
717
+ 0.0,
718
+ 0.0,
719
+ 0.0,
720
+ 0.0,
721
+ 0.0,
722
+ 0.0,
723
+ 0.0,
724
+ 0.0,
725
+ 0.0,
726
+ 0.0,
727
+ 0.0,
728
+ 0.0,
729
+ 0.0,
730
+ 0.0,
731
+ 0.0,
732
+ 0.0,
733
+ 0.0,
734
+ 0.0,
735
+ 0.0,
736
+ 0.0,
737
+ 0.0,
738
+ 0.0,
739
+ 0.0,
740
+ 0.0,
741
+ 0.0,
742
+ 0.0,
743
+ 0.0,
744
+ 0.0,
745
+ 0.0,
746
+ 0.0,
747
+ 0.0,
748
+ 0.0,
749
+ 0.0,
750
+ 0.0,
751
+ 0.0,
752
+ 0.0,
753
+ 0.0,
754
+ 0.0,
755
+ 0.0,
756
+ 0.0,
757
+ 0.0,
758
+ 0.0,
759
+ 0.0,
760
+ 0.0,
761
+ 0.0,
762
+ 0.0,
763
+ 0.0,
764
+ 0.0,
765
+ 0.0,
766
+ 0.0,
767
+ 0.0,
768
+ 0.0,
769
+ 0.0,
770
+ 0.0,
771
+ 0.0,
772
+ 0.0,
773
+ 0.0
774
+ ],
775
+ "res-equipment": [
776
+ 0.0,
777
+ 0.0,
778
+ 0.0,
779
+ 0.0,
780
+ 0.0,
781
+ 0.0,
782
+ 0.0,
783
+ 0.0,
784
+ 0.0,
785
+ 0.0,
786
+ 0.0,
787
+ 0.0,
788
+ 0.0,
789
+ 0.0,
790
+ 0.0,
791
+ 0.0,
792
+ 0.0,
793
+ 0.0,
794
+ 0.0,
795
+ 0.0,
796
+ 0.0,
797
+ 0.0,
798
+ 0.0,
799
+ 0.0,
800
+ 0.0,
801
+ 0.0,
802
+ 0.0,
803
+ 0.0,
804
+ 0.0,
805
+ 0.0,
806
+ 0.0,
807
+ 0.0,
808
+ 0.0,
809
+ 0.0,
810
+ 0.0,
811
+ 0.0,
812
+ 0.0,
813
+ 0.0,
814
+ 0.0,
815
+ 0.0,
816
+ 0.0,
817
+ 0.0,
818
+ 0.0,
819
+ 0.0,
820
+ 0.0,
821
+ 0.0,
822
+ 0.0,
823
+ 0.0,
824
+ 0.0,
825
+ 0.0,
826
+ 0.0,
827
+ 0.0,
828
+ 0.0,
829
+ 0.0,
830
+ 0.0,
831
+ 0.0,
832
+ 0.0,
833
+ 0.0,
834
+ 0.0,
835
+ 0.0,
836
+ 0.0,
837
+ 0.0,
838
+ 0.0,
839
+ 0.0,
840
+ 0.0,
841
+ 0.0,
842
+ 0.0,
843
+ 0.0,
844
+ 0.0,
845
+ 0.0,
846
+ 0.0,
847
+ 0.0,
848
+ 0.0,
849
+ 0.0,
850
+ 0.0,
851
+ 0.0,
852
+ 0.0,
853
+ 0.0,
854
+ 0.0,
855
+ 0.0,
856
+ 0.0,
857
+ 0.0,
858
+ 0.0,
859
+ 0.0,
860
+ 0.0,
861
+ 0.0,
862
+ 0.0,
863
+ 0.0,
864
+ 0.0,
865
+ 0.0,
866
+ 0.0,
867
+ 0.0,
868
+ 0.0,
869
+ 0.0,
870
+ 0.0,
871
+ 0.0,
872
+ 0.0,
873
+ 0.0,
874
+ 0.0,
875
+ 0.0,
876
+ 0.0,
877
+ 0.0,
878
+ 0.0,
879
+ 0.0,
880
+ 0.0,
881
+ 0.0,
882
+ 0.0,
883
+ 0.0,
884
+ 0.0,
885
+ 0.0,
886
+ 0.0,
887
+ 0.0,
888
+ 0.0,
889
+ 0.0,
890
+ 0.0,
891
+ 0.0,
892
+ 0.0,
893
+ 0.0,
894
+ 0.0,
895
+ 0.0,
896
+ 0.0,
897
+ 0.0,
898
+ 0.0,
899
+ 0.0,
900
+ 0.0,
901
+ 0.0,
902
+ 0.0,
903
+ 0.0,
904
+ 0.0,
905
+ 0.0,
906
+ 0.0,
907
+ 0.0,
908
+ 0.0,
909
+ 0.0,
910
+ 0.0,
911
+ 0.0,
912
+ 0.0,
913
+ 0.0,
914
+ 0.0,
915
+ 0.0,
916
+ 0.0,
917
+ 0.0,
918
+ 0.0,
919
+ 0.0,
920
+ 0.0,
921
+ 0.0,
922
+ 0.0,
923
+ 0.0,
924
+ 0.0,
925
+ 0.0,
926
+ 0.0,
927
+ 0.0,
928
+ 0.0,
929
+ 0.0,
930
+ 0.0,
931
+ 0.0,
932
+ 0.0,
933
+ 0.0,
934
+ 0.0,
935
+ 0.0,
936
+ 0.0,
937
+ 0.0,
938
+ 0.0,
939
+ 0.0,
940
+ 0.0,
941
+ 0.0,
942
+ 0.0,
943
+ 0.0,
944
+ 0.0,
945
+ 0.0,
946
+ 0.0,
947
+ 0.0,
948
+ 0.0,
949
+ 0.0,
950
+ 0.0,
951
+ 0.0,
952
+ 0.0,
953
+ 0.0,
954
+ 0.0,
955
+ 0.0,
956
+ 0.0,
957
+ 0.0,
958
+ 0.0,
959
+ 0.0,
960
+ 0.0,
961
+ 0.0,
962
+ 0.0,
963
+ 0.0,
964
+ 0.0,
965
+ 0.0,
966
+ 0.0,
967
+ 0.0,
968
+ 0.0,
969
+ 0.0,
970
+ 0.0,
971
+ 0.0,
972
+ 0.0,
973
+ 0.0,
974
+ 0.0,
975
+ 0.0,
976
+ 0.0,
977
+ 0.0,
978
+ 0.0,
979
+ 0.0,
980
+ 0.0,
981
+ 0.0,
982
+ 0.0,
983
+ 0.0,
984
+ 0.0,
985
+ 0.0,
986
+ 0.0,
987
+ 0.0,
988
+ 0.0,
989
+ 0.0,
990
+ 0.0,
991
+ 0.0,
992
+ 0.0,
993
+ 0.0,
994
+ 0.0,
995
+ 0.0,
996
+ 0.0,
997
+ 0.0,
998
+ 0.0,
999
+ 0.0,
1000
+ 0.0,
1001
+ 0.0,
1002
+ 0.0,
1003
+ 0.0,
1004
+ 0.0,
1005
+ 0.0,
1006
+ 0.0,
1007
+ 0.0,
1008
+ 0.0,
1009
+ 0.0,
1010
+ 0.0,
1011
+ 0.0,
1012
+ 0.0,
1013
+ 0.0,
1014
+ 0.0,
1015
+ 0.0,
1016
+ 0.0
1017
+ ],
1018
+ "res-laboratory": [
1019
+ 0.0,
1020
+ 0.0,
1021
+ 0.0,
1022
+ 0.0,
1023
+ 0.0,
1024
+ 0.0,
1025
+ 0.0,
1026
+ 0.0,
1027
+ 0.0,
1028
+ 0.0,
1029
+ 0.0,
1030
+ 0.0,
1031
+ 0.0,
1032
+ 0.0,
1033
+ 0.0,
1034
+ 0.0,
1035
+ 0.0,
1036
+ 0.0,
1037
+ 0.0,
1038
+ 0.0,
1039
+ 0.0,
1040
+ 0.0,
1041
+ 0.0,
1042
+ 0.0,
1043
+ 0.0,
1044
+ 0.0,
1045
+ 0.0,
1046
+ 0.0,
1047
+ 0.0,
1048
+ 0.0,
1049
+ 0.0,
1050
+ 0.0,
1051
+ 0.0,
1052
+ 0.0,
1053
+ 0.0,
1054
+ 0.0,
1055
+ 0.0,
1056
+ 0.0,
1057
+ 0.0,
1058
+ 0.0,
1059
+ 0.0,
1060
+ 0.0,
1061
+ 0.0,
1062
+ 0.0,
1063
+ 0.0,
1064
+ 0.0,
1065
+ 0.0,
1066
+ 0.0,
1067
+ 0.0,
1068
+ 0.0,
1069
+ 0.0,
1070
+ 0.0,
1071
+ 0.0,
1072
+ 0.0,
1073
+ 0.0,
1074
+ 0.0,
1075
+ 0.0,
1076
+ 0.0,
1077
+ 0.0,
1078
+ 0.0,
1079
+ 0.0,
1080
+ 0.0,
1081
+ 0.0,
1082
+ 0.0,
1083
+ 0.0,
1084
+ 0.0,
1085
+ 0.0,
1086
+ 0.0,
1087
+ 0.0,
1088
+ 0.0,
1089
+ 0.0,
1090
+ 0.0,
1091
+ 0.0,
1092
+ 0.0,
1093
+ 0.0,
1094
+ 0.0,
1095
+ 0.0,
1096
+ 0.0,
1097
+ 0.0,
1098
+ 0.0,
1099
+ 0.0,
1100
+ 0.0,
1101
+ 0.0,
1102
+ 0.0,
1103
+ 0.0,
1104
+ 0.0,
1105
+ 0.0,
1106
+ 0.0,
1107
+ 0.0,
1108
+ 0.0,
1109
+ 0.0,
1110
+ 0.0,
1111
+ 0.0,
1112
+ 0.0,
1113
+ 0.0,
1114
+ 0.0,
1115
+ 0.0,
1116
+ 0.0,
1117
+ 0.0,
1118
+ 0.0,
1119
+ 0.0,
1120
+ 0.0,
1121
+ 0.0,
1122
+ 0.0,
1123
+ 0.0,
1124
+ 0.0,
1125
+ 0.0,
1126
+ 0.0,
1127
+ 0.0,
1128
+ 0.0,
1129
+ 0.0,
1130
+ 0.0,
1131
+ 0.0,
1132
+ 0.0,
1133
+ 0.0,
1134
+ 0.0,
1135
+ 0.0,
1136
+ 0.0,
1137
+ 0.0,
1138
+ 0.0,
1139
+ 0.0,
1140
+ 0.0,
1141
+ 0.0,
1142
+ 0.0,
1143
+ 0.0,
1144
+ 0.0,
1145
+ 0.0,
1146
+ 0.0,
1147
+ 0.0,
1148
+ 0.0,
1149
+ 0.0,
1150
+ 0.0,
1151
+ 0.0,
1152
+ 0.0,
1153
+ 0.0,
1154
+ 0.0,
1155
+ 0.0,
1156
+ 0.0,
1157
+ 0.0,
1158
+ 0.0,
1159
+ 0.0,
1160
+ 0.0,
1161
+ 0.0,
1162
+ 0.0,
1163
+ 0.0,
1164
+ 0.0,
1165
+ 0.0,
1166
+ 0.0,
1167
+ 0.0,
1168
+ 0.0,
1169
+ 0.0,
1170
+ 0.0,
1171
+ 0.0,
1172
+ 0.0,
1173
+ 0.0,
1174
+ 0.0,
1175
+ 0.0,
1176
+ 0.0,
1177
+ 0.0,
1178
+ 0.0,
1179
+ 0.0,
1180
+ 0.0,
1181
+ 0.0,
1182
+ 0.0,
1183
+ 0.0,
1184
+ 0.0,
1185
+ 0.0,
1186
+ 0.0,
1187
+ 0.0,
1188
+ 0.0,
1189
+ 0.0,
1190
+ 0.0,
1191
+ 0.0,
1192
+ 0.0,
1193
+ 0.0,
1194
+ 0.0,
1195
+ 0.0,
1196
+ 0.0,
1197
+ 0.0,
1198
+ 0.0,
1199
+ 0.0,
1200
+ 0.0,
1201
+ 0.0,
1202
+ 0.0,
1203
+ 0.0,
1204
+ 0.0,
1205
+ 0.0,
1206
+ 0.0,
1207
+ 0.0,
1208
+ 0.0,
1209
+ 0.0,
1210
+ 0.0,
1211
+ 0.0,
1212
+ 0.0,
1213
+ 0.0,
1214
+ 0.0,
1215
+ 0.0,
1216
+ 0.0,
1217
+ 0.0,
1218
+ 0.0,
1219
+ 0.0,
1220
+ 0.0,
1221
+ 0.0,
1222
+ 0.0,
1223
+ 0.0,
1224
+ 0.0,
1225
+ 0.0,
1226
+ 0.0,
1227
+ 0.0,
1228
+ 0.0,
1229
+ 0.0,
1230
+ 0.0,
1231
+ 0.0,
1232
+ 0.0,
1233
+ 0.0,
1234
+ 0.0,
1235
+ 0.0,
1236
+ 0.0,
1237
+ 0.0,
1238
+ 0.0,
1239
+ 0.0,
1240
+ 0.0,
1241
+ 0.0,
1242
+ 0.0,
1243
+ 0.0,
1244
+ 0.0,
1245
+ 0.0,
1246
+ 0.0,
1247
+ 0.0,
1248
+ 0.0,
1249
+ 0.0,
1250
+ 0.0,
1251
+ 0.0,
1252
+ 0.0,
1253
+ 0.0,
1254
+ 0.0,
1255
+ 0.0,
1256
+ 0.0,
1257
+ 0.0,
1258
+ 0.0,
1259
+ 0.0
1260
+ ],
1261
+ "res-specialist": [
1262
+ 0.0,
1263
+ 0.0,
1264
+ 0.0,
1265
+ 0.0,
1266
+ 0.0,
1267
+ 0.0,
1268
+ 0.0,
1269
+ 0.0,
1270
+ 0.0,
1271
+ 0.0,
1272
+ 0.0,
1273
+ 0.0,
1274
+ 0.0,
1275
+ 0.0,
1276
+ 0.0,
1277
+ 0.0,
1278
+ 0.0,
1279
+ 0.0,
1280
+ 0.0,
1281
+ 0.0,
1282
+ 0.0,
1283
+ 0.0,
1284
+ 0.0,
1285
+ 0.0,
1286
+ 0.0,
1287
+ 0.0,
1288
+ 0.0,
1289
+ 0.0,
1290
+ 0.0,
1291
+ 0.0,
1292
+ 0.0,
1293
+ 0.0,
1294
+ 0.0,
1295
+ 0.0,
1296
+ 0.0,
1297
+ 0.0,
1298
+ 0.0,
1299
+ 0.0,
1300
+ 0.0,
1301
+ 0.0,
1302
+ 0.0,
1303
+ 0.0,
1304
+ 0.0,
1305
+ 0.0,
1306
+ 0.0,
1307
+ 0.0,
1308
+ 0.0,
1309
+ 0.0,
1310
+ 0.0,
1311
+ 0.0,
1312
+ 0.0,
1313
+ 0.0,
1314
+ 0.0,
1315
+ 0.0,
1316
+ 0.0,
1317
+ 0.0,
1318
+ 0.0,
1319
+ 0.0,
1320
+ 0.0,
1321
+ 0.0,
1322
+ 0.0,
1323
+ 0.0,
1324
+ 0.0,
1325
+ 0.0,
1326
+ 0.0,
1327
+ 0.0,
1328
+ 0.0,
1329
+ 0.0,
1330
+ 0.0,
1331
+ 0.0,
1332
+ 0.0,
1333
+ 0.0,
1334
+ 0.0,
1335
+ 0.0,
1336
+ 0.0,
1337
+ 0.0,
1338
+ 0.0,
1339
+ 0.0,
1340
+ 0.0,
1341
+ 0.0,
1342
+ 0.0,
1343
+ 0.0,
1344
+ 0.0,
1345
+ 0.0,
1346
+ 0.0,
1347
+ 0.0,
1348
+ 0.0,
1349
+ 0.0,
1350
+ 0.0,
1351
+ 0.0,
1352
+ 0.0,
1353
+ 0.0,
1354
+ 0.0,
1355
+ 0.0,
1356
+ 0.0,
1357
+ 0.0,
1358
+ 0.0,
1359
+ 0.0,
1360
+ 0.0,
1361
+ 0.0,
1362
+ 0.0,
1363
+ 0.0,
1364
+ 0.0,
1365
+ 0.0,
1366
+ 0.0,
1367
+ 0.0,
1368
+ 0.0,
1369
+ 0.0,
1370
+ 0.0,
1371
+ 0.0,
1372
+ 0.0,
1373
+ 0.0,
1374
+ 0.0,
1375
+ 0.0,
1376
+ 0.0,
1377
+ 0.0,
1378
+ 0.0,
1379
+ 0.0,
1380
+ 0.0,
1381
+ 0.0,
1382
+ 0.0,
1383
+ 0.0,
1384
+ 0.0,
1385
+ 0.0,
1386
+ 0.0,
1387
+ 0.0,
1388
+ 0.0,
1389
+ 0.0,
1390
+ 0.0,
1391
+ 0.0,
1392
+ 0.0,
1393
+ 0.0,
1394
+ 0.0,
1395
+ 0.0,
1396
+ 0.0,
1397
+ 0.0,
1398
+ 0.0,
1399
+ 0.0,
1400
+ 0.0,
1401
+ 0.0,
1402
+ 0.0,
1403
+ 0.0,
1404
+ 0.0,
1405
+ 0.0,
1406
+ 0.0,
1407
+ 0.0,
1408
+ 0.0,
1409
+ 0.0,
1410
+ 0.0,
1411
+ 0.0,
1412
+ 0.0,
1413
+ 0.0,
1414
+ 0.0,
1415
+ 0.0,
1416
+ 0.0,
1417
+ 0.0,
1418
+ 0.0,
1419
+ 0.0,
1420
+ 0.0,
1421
+ 0.0,
1422
+ 0.0,
1423
+ 0.0,
1424
+ 0.0,
1425
+ 0.0,
1426
+ 0.0,
1427
+ 0.0,
1428
+ 0.0,
1429
+ 0.0,
1430
+ 0.0,
1431
+ 0.0,
1432
+ 0.0,
1433
+ 0.0,
1434
+ 0.0,
1435
+ 0.0,
1436
+ 0.0,
1437
+ 0.0,
1438
+ 0.0,
1439
+ 0.0,
1440
+ 0.0,
1441
+ 0.0,
1442
+ 0.0,
1443
+ 0.0,
1444
+ 0.0,
1445
+ 0.0,
1446
+ 0.0,
1447
+ 0.0,
1448
+ 0.0,
1449
+ 0.0,
1450
+ 0.0,
1451
+ 0.0,
1452
+ 0.0,
1453
+ 0.0,
1454
+ 0.0,
1455
+ 0.0,
1456
+ 0.0,
1457
+ 0.0,
1458
+ 0.0,
1459
+ 0.0,
1460
+ 0.0,
1461
+ 0.0,
1462
+ 0.0,
1463
+ 0.0,
1464
+ 0.0,
1465
+ 0.0,
1466
+ 0.0,
1467
+ 0.0,
1468
+ 0.0,
1469
+ 0.0,
1470
+ 0.0,
1471
+ 0.0,
1472
+ 0.0,
1473
+ 0.0,
1474
+ 0.0,
1475
+ 0.0,
1476
+ 0.0,
1477
+ 0.0,
1478
+ 0.0,
1479
+ 0.0,
1480
+ 0.0,
1481
+ 0.0,
1482
+ 0.0,
1483
+ 0.0,
1484
+ 0.0,
1485
+ 0.0,
1486
+ 0.0,
1487
+ 0.0,
1488
+ 0.0,
1489
+ 0.0,
1490
+ 0.0,
1491
+ 0.0,
1492
+ 0.0,
1493
+ 0.0,
1494
+ 0.0,
1495
+ 0.0,
1496
+ 0.0,
1497
+ 0.0,
1498
+ 0.0,
1499
+ 0.0,
1500
+ 0.0,
1501
+ 0.0,
1502
+ 0.0
1503
+ ]
1504
+ },
1505
+ "cash_flow_profile": [
1506
+ 0.0,
1507
+ 0.0,
1508
+ 0.0,
1509
+ 0.0,
1510
+ 0.0,
1511
+ 0.0,
1512
+ 0.0,
1513
+ 0.0,
1514
+ 0.0,
1515
+ 0.0,
1516
+ 0.0,
1517
+ 0.0,
1518
+ 0.0,
1519
+ 0.0,
1520
+ 0.0,
1521
+ 0.0,
1522
+ 0.0,
1523
+ 0.0,
1524
+ 0.0,
1525
+ 0.0,
1526
+ 0.0,
1527
+ 0.0,
1528
+ 0.0,
1529
+ 0.0,
1530
+ 0.0,
1531
+ 0.0,
1532
+ 0.0,
1533
+ 0.0,
1534
+ 0.0,
1535
+ 0.0,
1536
+ 0.0,
1537
+ 0.0,
1538
+ 0.0,
1539
+ 0.0,
1540
+ 0.0,
1541
+ 0.0,
1542
+ 0.0,
1543
+ 0.0,
1544
+ 0.0,
1545
+ 0.0,
1546
+ 0.0,
1547
+ 0.0,
1548
+ 0.0,
1549
+ 0.0,
1550
+ 0.0,
1551
+ 0.0,
1552
+ 0.0,
1553
+ 0.0,
1554
+ 0.0,
1555
+ 0.0,
1556
+ 0.0,
1557
+ 0.0,
1558
+ 0.0,
1559
+ 0.0,
1560
+ 0.0,
1561
+ 0.0,
1562
+ 0.0,
1563
+ 0.0,
1564
+ 0.0,
1565
+ 0.0,
1566
+ 0.0,
1567
+ 0.0,
1568
+ 0.0,
1569
+ 0.0,
1570
+ 0.0,
1571
+ 0.0,
1572
+ 0.0,
1573
+ 0.0,
1574
+ 0.0,
1575
+ 0.0,
1576
+ 0.0,
1577
+ 0.0,
1578
+ 0.0,
1579
+ 0.0,
1580
+ 0.0,
1581
+ 0.0,
1582
+ 0.0,
1583
+ 0.0,
1584
+ 0.0,
1585
+ 0.0,
1586
+ 0.0,
1587
+ 0.0,
1588
+ 0.0,
1589
+ 0.0,
1590
+ 0.0,
1591
+ 0.0,
1592
+ 0.0,
1593
+ 0.0,
1594
+ 0.0,
1595
+ 0.0,
1596
+ 0.0,
1597
+ 0.0,
1598
+ 0.0,
1599
+ 0.0,
1600
+ 0.0,
1601
+ 0.0,
1602
+ 0.0,
1603
+ 0.0,
1604
+ 0.0,
1605
+ 0.0,
1606
+ 0.0,
1607
+ 0.0,
1608
+ 0.0,
1609
+ 0.0,
1610
+ 0.0,
1611
+ 0.0,
1612
+ 0.0,
1613
+ 0.0,
1614
+ 0.0,
1615
+ 0.0,
1616
+ 0.0,
1617
+ 0.0,
1618
+ 0.0,
1619
+ 0.0,
1620
+ 0.0,
1621
+ 0.0,
1622
+ 0.0,
1623
+ 0.0,
1624
+ 0.0,
1625
+ 0.0,
1626
+ 0.0,
1627
+ 0.0,
1628
+ 0.0,
1629
+ 0.0,
1630
+ 0.0,
1631
+ 0.0,
1632
+ 0.0,
1633
+ 0.0,
1634
+ 0.0,
1635
+ 0.0,
1636
+ 0.0,
1637
+ 0.0,
1638
+ 0.0,
1639
+ 0.0,
1640
+ 0.0,
1641
+ 0.0,
1642
+ 0.0,
1643
+ 0.0,
1644
+ 0.0,
1645
+ 0.0,
1646
+ 0.0,
1647
+ 0.0,
1648
+ 0.0,
1649
+ 0.0,
1650
+ 0.0,
1651
+ 0.0,
1652
+ 0.0,
1653
+ 0.0,
1654
+ 0.0,
1655
+ 0.0,
1656
+ 0.0,
1657
+ 0.0,
1658
+ 0.0,
1659
+ 0.0,
1660
+ 0.0,
1661
+ 0.0,
1662
+ 0.0,
1663
+ 0.0,
1664
+ 0.0,
1665
+ 0.0,
1666
+ 0.0,
1667
+ 0.0,
1668
+ 0.0,
1669
+ 0.0,
1670
+ 0.0,
1671
+ 0.0,
1672
+ 0.0,
1673
+ 0.0,
1674
+ 0.0,
1675
+ 0.0,
1676
+ 0.0,
1677
+ 0.0,
1678
+ 0.0,
1679
+ 0.0,
1680
+ 0.0,
1681
+ 0.0,
1682
+ 0.0,
1683
+ 0.0,
1684
+ 0.0,
1685
+ 0.0,
1686
+ 0.0,
1687
+ 0.0,
1688
+ 0.0,
1689
+ 0.0,
1690
+ 0.0,
1691
+ 0.0,
1692
+ 0.0,
1693
+ 0.0,
1694
+ 0.0,
1695
+ 0.0,
1696
+ 0.0,
1697
+ 0.0,
1698
+ 0.0,
1699
+ 0.0,
1700
+ 0.0,
1701
+ 0.0,
1702
+ 0.0,
1703
+ 0.0,
1704
+ 0.0,
1705
+ 0.0,
1706
+ 0.0,
1707
+ 0.0,
1708
+ 0.0,
1709
+ 0.0,
1710
+ 0.0,
1711
+ 0.0,
1712
+ 0.0,
1713
+ 0.0,
1714
+ 0.0,
1715
+ 0.0,
1716
+ 0.0,
1717
+ 0.0,
1718
+ 0.0,
1719
+ 0.0,
1720
+ 0.0,
1721
+ 0.0,
1722
+ 0.0,
1723
+ 0.0,
1724
+ 0.0,
1725
+ 0.0,
1726
+ 0.0,
1727
+ 0.0,
1728
+ 0.0,
1729
+ 0.0,
1730
+ 0.0,
1731
+ 0.0,
1732
+ 0.0,
1733
+ 0.0,
1734
+ 0.0,
1735
+ 0.0,
1736
+ 0.0,
1737
+ 0.0,
1738
+ 0.0,
1739
+ 0.0,
1740
+ 0.0,
1741
+ 0.0,
1742
+ 0.0,
1743
+ 0.0,
1744
+ 0.0,
1745
+ 0.0,
1746
+ 0.0
1747
+ ],
1748
+ "selected_projects": [],
1749
+ "suspended_projects": [
1750
+ "proj-000",
1751
+ "proj-001",
1752
+ "proj-002",
1753
+ "proj-003"
1754
+ ],
1755
+ "simulation_runs": [],
1756
+ "pareto_front": [],
1757
+ "risk_contributors": []
1758
+ }
1759
+ }
assets/samples/sample_software_development.json ADDED
@@ -0,0 +1,921 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenario": "software_development",
3
+ "uncertainty": "scope_volatility",
4
+ "algorithm": "critical_chain",
5
+ "scenario_label": "Technology Product Portfolio",
6
+ "summary": {
7
+ "projects": 6,
8
+ "active_projects": 0,
9
+ "activities": 0,
10
+ "horizon": 120,
11
+ "budget_musd": 12.0
12
+ },
13
+ "optimization": {
14
+ "algorithm": "critical_chain",
15
+ "scenario": "software_development",
16
+ "uncertainty": "scope_volatility",
17
+ "disruption": "none",
18
+ "schedule": [],
19
+ "metrics": {
20
+ "portfolio_npv": 0,
21
+ "total_tardiness": 0,
22
+ "max_tardiness": 0,
23
+ "makespan": 0,
24
+ "resource_peak_variance": 0.0,
25
+ "resource_leveling_index": 0.0,
26
+ "cash_flow_risk": 0.0,
27
+ "reschedule_cost": 0.0,
28
+ "projects_active": 0,
29
+ "projects_suspended": 0,
30
+ "activities_scheduled": 0,
31
+ "budget_utilization_pct": 0.0,
32
+ "on_time_delivery_pct": 0.0,
33
+ "solve_time_sec": 0.0,
34
+ "status": "feasible",
35
+ "monte_carlo_p50_makespan": 0.0,
36
+ "monte_carlo_p90_makespan": 0.0,
37
+ "monte_carlo_p50_cost": 0.0,
38
+ "monte_carlo_p90_cost": 0.0,
39
+ "on_time_probability_pct": 0.0,
40
+ "completion_by_deadline_prob_pct": 0.0,
41
+ "liquidity_shortfall_prob_pct": 0.0,
42
+ "milestone_delay_prob_pct": 0.0,
43
+ "strategic_value_score": 0.0
44
+ },
45
+ "resource_profile": {
46
+ "res-human": [
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ 0.0,
68
+ 0.0,
69
+ 0.0,
70
+ 0.0,
71
+ 0.0,
72
+ 0.0,
73
+ 0.0,
74
+ 0.0,
75
+ 0.0,
76
+ 0.0,
77
+ 0.0,
78
+ 0.0,
79
+ 0.0,
80
+ 0.0,
81
+ 0.0,
82
+ 0.0,
83
+ 0.0,
84
+ 0.0,
85
+ 0.0,
86
+ 0.0,
87
+ 0.0,
88
+ 0.0,
89
+ 0.0,
90
+ 0.0,
91
+ 0.0,
92
+ 0.0,
93
+ 0.0,
94
+ 0.0,
95
+ 0.0,
96
+ 0.0,
97
+ 0.0,
98
+ 0.0,
99
+ 0.0,
100
+ 0.0,
101
+ 0.0,
102
+ 0.0,
103
+ 0.0,
104
+ 0.0,
105
+ 0.0,
106
+ 0.0,
107
+ 0.0,
108
+ 0.0,
109
+ 0.0,
110
+ 0.0,
111
+ 0.0,
112
+ 0.0,
113
+ 0.0,
114
+ 0.0,
115
+ 0.0,
116
+ 0.0,
117
+ 0.0,
118
+ 0.0,
119
+ 0.0,
120
+ 0.0,
121
+ 0.0,
122
+ 0.0,
123
+ 0.0,
124
+ 0.0,
125
+ 0.0,
126
+ 0.0,
127
+ 0.0,
128
+ 0.0,
129
+ 0.0,
130
+ 0.0,
131
+ 0.0,
132
+ 0.0,
133
+ 0.0,
134
+ 0.0,
135
+ 0.0,
136
+ 0.0,
137
+ 0.0,
138
+ 0.0,
139
+ 0.0,
140
+ 0.0,
141
+ 0.0,
142
+ 0.0,
143
+ 0.0,
144
+ 0.0,
145
+ 0.0,
146
+ 0.0,
147
+ 0.0,
148
+ 0.0,
149
+ 0.0,
150
+ 0.0,
151
+ 0.0,
152
+ 0.0,
153
+ 0.0,
154
+ 0.0,
155
+ 0.0,
156
+ 0.0,
157
+ 0.0,
158
+ 0.0,
159
+ 0.0,
160
+ 0.0,
161
+ 0.0,
162
+ 0.0,
163
+ 0.0,
164
+ 0.0,
165
+ 0.0,
166
+ 0.0,
167
+ 0.0
168
+ ],
169
+ "res-machinery": [
170
+ 0.0,
171
+ 0.0,
172
+ 0.0,
173
+ 0.0,
174
+ 0.0,
175
+ 0.0,
176
+ 0.0,
177
+ 0.0,
178
+ 0.0,
179
+ 0.0,
180
+ 0.0,
181
+ 0.0,
182
+ 0.0,
183
+ 0.0,
184
+ 0.0,
185
+ 0.0,
186
+ 0.0,
187
+ 0.0,
188
+ 0.0,
189
+ 0.0,
190
+ 0.0,
191
+ 0.0,
192
+ 0.0,
193
+ 0.0,
194
+ 0.0,
195
+ 0.0,
196
+ 0.0,
197
+ 0.0,
198
+ 0.0,
199
+ 0.0,
200
+ 0.0,
201
+ 0.0,
202
+ 0.0,
203
+ 0.0,
204
+ 0.0,
205
+ 0.0,
206
+ 0.0,
207
+ 0.0,
208
+ 0.0,
209
+ 0.0,
210
+ 0.0,
211
+ 0.0,
212
+ 0.0,
213
+ 0.0,
214
+ 0.0,
215
+ 0.0,
216
+ 0.0,
217
+ 0.0,
218
+ 0.0,
219
+ 0.0,
220
+ 0.0,
221
+ 0.0,
222
+ 0.0,
223
+ 0.0,
224
+ 0.0,
225
+ 0.0,
226
+ 0.0,
227
+ 0.0,
228
+ 0.0,
229
+ 0.0,
230
+ 0.0,
231
+ 0.0,
232
+ 0.0,
233
+ 0.0,
234
+ 0.0,
235
+ 0.0,
236
+ 0.0,
237
+ 0.0,
238
+ 0.0,
239
+ 0.0,
240
+ 0.0,
241
+ 0.0,
242
+ 0.0,
243
+ 0.0,
244
+ 0.0,
245
+ 0.0,
246
+ 0.0,
247
+ 0.0,
248
+ 0.0,
249
+ 0.0,
250
+ 0.0,
251
+ 0.0,
252
+ 0.0,
253
+ 0.0,
254
+ 0.0,
255
+ 0.0,
256
+ 0.0,
257
+ 0.0,
258
+ 0.0,
259
+ 0.0,
260
+ 0.0,
261
+ 0.0,
262
+ 0.0,
263
+ 0.0,
264
+ 0.0,
265
+ 0.0,
266
+ 0.0,
267
+ 0.0,
268
+ 0.0,
269
+ 0.0,
270
+ 0.0,
271
+ 0.0,
272
+ 0.0,
273
+ 0.0,
274
+ 0.0,
275
+ 0.0,
276
+ 0.0,
277
+ 0.0,
278
+ 0.0,
279
+ 0.0,
280
+ 0.0,
281
+ 0.0,
282
+ 0.0,
283
+ 0.0,
284
+ 0.0,
285
+ 0.0,
286
+ 0.0,
287
+ 0.0,
288
+ 0.0,
289
+ 0.0,
290
+ 0.0
291
+ ],
292
+ "res-contractor": [
293
+ 0.0,
294
+ 0.0,
295
+ 0.0,
296
+ 0.0,
297
+ 0.0,
298
+ 0.0,
299
+ 0.0,
300
+ 0.0,
301
+ 0.0,
302
+ 0.0,
303
+ 0.0,
304
+ 0.0,
305
+ 0.0,
306
+ 0.0,
307
+ 0.0,
308
+ 0.0,
309
+ 0.0,
310
+ 0.0,
311
+ 0.0,
312
+ 0.0,
313
+ 0.0,
314
+ 0.0,
315
+ 0.0,
316
+ 0.0,
317
+ 0.0,
318
+ 0.0,
319
+ 0.0,
320
+ 0.0,
321
+ 0.0,
322
+ 0.0,
323
+ 0.0,
324
+ 0.0,
325
+ 0.0,
326
+ 0.0,
327
+ 0.0,
328
+ 0.0,
329
+ 0.0,
330
+ 0.0,
331
+ 0.0,
332
+ 0.0,
333
+ 0.0,
334
+ 0.0,
335
+ 0.0,
336
+ 0.0,
337
+ 0.0,
338
+ 0.0,
339
+ 0.0,
340
+ 0.0,
341
+ 0.0,
342
+ 0.0,
343
+ 0.0,
344
+ 0.0,
345
+ 0.0,
346
+ 0.0,
347
+ 0.0,
348
+ 0.0,
349
+ 0.0,
350
+ 0.0,
351
+ 0.0,
352
+ 0.0,
353
+ 0.0,
354
+ 0.0,
355
+ 0.0,
356
+ 0.0,
357
+ 0.0,
358
+ 0.0,
359
+ 0.0,
360
+ 0.0,
361
+ 0.0,
362
+ 0.0,
363
+ 0.0,
364
+ 0.0,
365
+ 0.0,
366
+ 0.0,
367
+ 0.0,
368
+ 0.0,
369
+ 0.0,
370
+ 0.0,
371
+ 0.0,
372
+ 0.0,
373
+ 0.0,
374
+ 0.0,
375
+ 0.0,
376
+ 0.0,
377
+ 0.0,
378
+ 0.0,
379
+ 0.0,
380
+ 0.0,
381
+ 0.0,
382
+ 0.0,
383
+ 0.0,
384
+ 0.0,
385
+ 0.0,
386
+ 0.0,
387
+ 0.0,
388
+ 0.0,
389
+ 0.0,
390
+ 0.0,
391
+ 0.0,
392
+ 0.0,
393
+ 0.0,
394
+ 0.0,
395
+ 0.0,
396
+ 0.0,
397
+ 0.0,
398
+ 0.0,
399
+ 0.0,
400
+ 0.0,
401
+ 0.0,
402
+ 0.0,
403
+ 0.0,
404
+ 0.0,
405
+ 0.0,
406
+ 0.0,
407
+ 0.0,
408
+ 0.0,
409
+ 0.0,
410
+ 0.0,
411
+ 0.0,
412
+ 0.0,
413
+ 0.0
414
+ ],
415
+ "res-equipment": [
416
+ 0.0,
417
+ 0.0,
418
+ 0.0,
419
+ 0.0,
420
+ 0.0,
421
+ 0.0,
422
+ 0.0,
423
+ 0.0,
424
+ 0.0,
425
+ 0.0,
426
+ 0.0,
427
+ 0.0,
428
+ 0.0,
429
+ 0.0,
430
+ 0.0,
431
+ 0.0,
432
+ 0.0,
433
+ 0.0,
434
+ 0.0,
435
+ 0.0,
436
+ 0.0,
437
+ 0.0,
438
+ 0.0,
439
+ 0.0,
440
+ 0.0,
441
+ 0.0,
442
+ 0.0,
443
+ 0.0,
444
+ 0.0,
445
+ 0.0,
446
+ 0.0,
447
+ 0.0,
448
+ 0.0,
449
+ 0.0,
450
+ 0.0,
451
+ 0.0,
452
+ 0.0,
453
+ 0.0,
454
+ 0.0,
455
+ 0.0,
456
+ 0.0,
457
+ 0.0,
458
+ 0.0,
459
+ 0.0,
460
+ 0.0,
461
+ 0.0,
462
+ 0.0,
463
+ 0.0,
464
+ 0.0,
465
+ 0.0,
466
+ 0.0,
467
+ 0.0,
468
+ 0.0,
469
+ 0.0,
470
+ 0.0,
471
+ 0.0,
472
+ 0.0,
473
+ 0.0,
474
+ 0.0,
475
+ 0.0,
476
+ 0.0,
477
+ 0.0,
478
+ 0.0,
479
+ 0.0,
480
+ 0.0,
481
+ 0.0,
482
+ 0.0,
483
+ 0.0,
484
+ 0.0,
485
+ 0.0,
486
+ 0.0,
487
+ 0.0,
488
+ 0.0,
489
+ 0.0,
490
+ 0.0,
491
+ 0.0,
492
+ 0.0,
493
+ 0.0,
494
+ 0.0,
495
+ 0.0,
496
+ 0.0,
497
+ 0.0,
498
+ 0.0,
499
+ 0.0,
500
+ 0.0,
501
+ 0.0,
502
+ 0.0,
503
+ 0.0,
504
+ 0.0,
505
+ 0.0,
506
+ 0.0,
507
+ 0.0,
508
+ 0.0,
509
+ 0.0,
510
+ 0.0,
511
+ 0.0,
512
+ 0.0,
513
+ 0.0,
514
+ 0.0,
515
+ 0.0,
516
+ 0.0,
517
+ 0.0,
518
+ 0.0,
519
+ 0.0,
520
+ 0.0,
521
+ 0.0,
522
+ 0.0,
523
+ 0.0,
524
+ 0.0,
525
+ 0.0,
526
+ 0.0,
527
+ 0.0,
528
+ 0.0,
529
+ 0.0,
530
+ 0.0,
531
+ 0.0,
532
+ 0.0,
533
+ 0.0,
534
+ 0.0,
535
+ 0.0,
536
+ 0.0
537
+ ],
538
+ "res-laboratory": [
539
+ 0.0,
540
+ 0.0,
541
+ 0.0,
542
+ 0.0,
543
+ 0.0,
544
+ 0.0,
545
+ 0.0,
546
+ 0.0,
547
+ 0.0,
548
+ 0.0,
549
+ 0.0,
550
+ 0.0,
551
+ 0.0,
552
+ 0.0,
553
+ 0.0,
554
+ 0.0,
555
+ 0.0,
556
+ 0.0,
557
+ 0.0,
558
+ 0.0,
559
+ 0.0,
560
+ 0.0,
561
+ 0.0,
562
+ 0.0,
563
+ 0.0,
564
+ 0.0,
565
+ 0.0,
566
+ 0.0,
567
+ 0.0,
568
+ 0.0,
569
+ 0.0,
570
+ 0.0,
571
+ 0.0,
572
+ 0.0,
573
+ 0.0,
574
+ 0.0,
575
+ 0.0,
576
+ 0.0,
577
+ 0.0,
578
+ 0.0,
579
+ 0.0,
580
+ 0.0,
581
+ 0.0,
582
+ 0.0,
583
+ 0.0,
584
+ 0.0,
585
+ 0.0,
586
+ 0.0,
587
+ 0.0,
588
+ 0.0,
589
+ 0.0,
590
+ 0.0,
591
+ 0.0,
592
+ 0.0,
593
+ 0.0,
594
+ 0.0,
595
+ 0.0,
596
+ 0.0,
597
+ 0.0,
598
+ 0.0,
599
+ 0.0,
600
+ 0.0,
601
+ 0.0,
602
+ 0.0,
603
+ 0.0,
604
+ 0.0,
605
+ 0.0,
606
+ 0.0,
607
+ 0.0,
608
+ 0.0,
609
+ 0.0,
610
+ 0.0,
611
+ 0.0,
612
+ 0.0,
613
+ 0.0,
614
+ 0.0,
615
+ 0.0,
616
+ 0.0,
617
+ 0.0,
618
+ 0.0,
619
+ 0.0,
620
+ 0.0,
621
+ 0.0,
622
+ 0.0,
623
+ 0.0,
624
+ 0.0,
625
+ 0.0,
626
+ 0.0,
627
+ 0.0,
628
+ 0.0,
629
+ 0.0,
630
+ 0.0,
631
+ 0.0,
632
+ 0.0,
633
+ 0.0,
634
+ 0.0,
635
+ 0.0,
636
+ 0.0,
637
+ 0.0,
638
+ 0.0,
639
+ 0.0,
640
+ 0.0,
641
+ 0.0,
642
+ 0.0,
643
+ 0.0,
644
+ 0.0,
645
+ 0.0,
646
+ 0.0,
647
+ 0.0,
648
+ 0.0,
649
+ 0.0,
650
+ 0.0,
651
+ 0.0,
652
+ 0.0,
653
+ 0.0,
654
+ 0.0,
655
+ 0.0,
656
+ 0.0,
657
+ 0.0,
658
+ 0.0,
659
+ 0.0
660
+ ],
661
+ "res-specialist": [
662
+ 0.0,
663
+ 0.0,
664
+ 0.0,
665
+ 0.0,
666
+ 0.0,
667
+ 0.0,
668
+ 0.0,
669
+ 0.0,
670
+ 0.0,
671
+ 0.0,
672
+ 0.0,
673
+ 0.0,
674
+ 0.0,
675
+ 0.0,
676
+ 0.0,
677
+ 0.0,
678
+ 0.0,
679
+ 0.0,
680
+ 0.0,
681
+ 0.0,
682
+ 0.0,
683
+ 0.0,
684
+ 0.0,
685
+ 0.0,
686
+ 0.0,
687
+ 0.0,
688
+ 0.0,
689
+ 0.0,
690
+ 0.0,
691
+ 0.0,
692
+ 0.0,
693
+ 0.0,
694
+ 0.0,
695
+ 0.0,
696
+ 0.0,
697
+ 0.0,
698
+ 0.0,
699
+ 0.0,
700
+ 0.0,
701
+ 0.0,
702
+ 0.0,
703
+ 0.0,
704
+ 0.0,
705
+ 0.0,
706
+ 0.0,
707
+ 0.0,
708
+ 0.0,
709
+ 0.0,
710
+ 0.0,
711
+ 0.0,
712
+ 0.0,
713
+ 0.0,
714
+ 0.0,
715
+ 0.0,
716
+ 0.0,
717
+ 0.0,
718
+ 0.0,
719
+ 0.0,
720
+ 0.0,
721
+ 0.0,
722
+ 0.0,
723
+ 0.0,
724
+ 0.0,
725
+ 0.0,
726
+ 0.0,
727
+ 0.0,
728
+ 0.0,
729
+ 0.0,
730
+ 0.0,
731
+ 0.0,
732
+ 0.0,
733
+ 0.0,
734
+ 0.0,
735
+ 0.0,
736
+ 0.0,
737
+ 0.0,
738
+ 0.0,
739
+ 0.0,
740
+ 0.0,
741
+ 0.0,
742
+ 0.0,
743
+ 0.0,
744
+ 0.0,
745
+ 0.0,
746
+ 0.0,
747
+ 0.0,
748
+ 0.0,
749
+ 0.0,
750
+ 0.0,
751
+ 0.0,
752
+ 0.0,
753
+ 0.0,
754
+ 0.0,
755
+ 0.0,
756
+ 0.0,
757
+ 0.0,
758
+ 0.0,
759
+ 0.0,
760
+ 0.0,
761
+ 0.0,
762
+ 0.0,
763
+ 0.0,
764
+ 0.0,
765
+ 0.0,
766
+ 0.0,
767
+ 0.0,
768
+ 0.0,
769
+ 0.0,
770
+ 0.0,
771
+ 0.0,
772
+ 0.0,
773
+ 0.0,
774
+ 0.0,
775
+ 0.0,
776
+ 0.0,
777
+ 0.0,
778
+ 0.0,
779
+ 0.0,
780
+ 0.0,
781
+ 0.0,
782
+ 0.0
783
+ ]
784
+ },
785
+ "cash_flow_profile": [
786
+ 0.0,
787
+ 0.0,
788
+ 0.0,
789
+ 0.0,
790
+ 0.0,
791
+ 0.0,
792
+ 0.0,
793
+ 0.0,
794
+ 0.0,
795
+ 0.0,
796
+ 0.0,
797
+ 0.0,
798
+ 0.0,
799
+ 0.0,
800
+ 0.0,
801
+ 0.0,
802
+ 0.0,
803
+ 0.0,
804
+ 0.0,
805
+ 0.0,
806
+ 0.0,
807
+ 0.0,
808
+ 0.0,
809
+ 0.0,
810
+ 0.0,
811
+ 0.0,
812
+ 0.0,
813
+ 0.0,
814
+ 0.0,
815
+ 0.0,
816
+ 0.0,
817
+ 0.0,
818
+ 0.0,
819
+ 0.0,
820
+ 0.0,
821
+ 0.0,
822
+ 0.0,
823
+ 0.0,
824
+ 0.0,
825
+ 0.0,
826
+ 0.0,
827
+ 0.0,
828
+ 0.0,
829
+ 0.0,
830
+ 0.0,
831
+ 0.0,
832
+ 0.0,
833
+ 0.0,
834
+ 0.0,
835
+ 0.0,
836
+ 0.0,
837
+ 0.0,
838
+ 0.0,
839
+ 0.0,
840
+ 0.0,
841
+ 0.0,
842
+ 0.0,
843
+ 0.0,
844
+ 0.0,
845
+ 0.0,
846
+ 0.0,
847
+ 0.0,
848
+ 0.0,
849
+ 0.0,
850
+ 0.0,
851
+ 0.0,
852
+ 0.0,
853
+ 0.0,
854
+ 0.0,
855
+ 0.0,
856
+ 0.0,
857
+ 0.0,
858
+ 0.0,
859
+ 0.0,
860
+ 0.0,
861
+ 0.0,
862
+ 0.0,
863
+ 0.0,
864
+ 0.0,
865
+ 0.0,
866
+ 0.0,
867
+ 0.0,
868
+ 0.0,
869
+ 0.0,
870
+ 0.0,
871
+ 0.0,
872
+ 0.0,
873
+ 0.0,
874
+ 0.0,
875
+ 0.0,
876
+ 0.0,
877
+ 0.0,
878
+ 0.0,
879
+ 0.0,
880
+ 0.0,
881
+ 0.0,
882
+ 0.0,
883
+ 0.0,
884
+ 0.0,
885
+ 0.0,
886
+ 0.0,
887
+ 0.0,
888
+ 0.0,
889
+ 0.0,
890
+ 0.0,
891
+ 0.0,
892
+ 0.0,
893
+ 0.0,
894
+ 0.0,
895
+ 0.0,
896
+ 0.0,
897
+ 0.0,
898
+ 0.0,
899
+ 0.0,
900
+ 0.0,
901
+ 0.0,
902
+ 0.0,
903
+ 0.0,
904
+ 0.0,
905
+ 0.0,
906
+ 0.0
907
+ ],
908
+ "selected_projects": [],
909
+ "suspended_projects": [
910
+ "proj-000",
911
+ "proj-001",
912
+ "proj-002",
913
+ "proj-003",
914
+ "proj-004",
915
+ "proj-005"
916
+ ],
917
+ "simulation_runs": [],
918
+ "pareto_front": [],
919
+ "risk_contributors": []
920
+ }
921
+ }
gradio/README.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PortTower
3
+ emoji: 🏗️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 5.50.0
8
+ app_file: app.py
9
+ short_description: Mega-project portfolio control tower with what-if analysis
10
+ python_version: "3.12"
11
+ ---
12
+
13
+ # PortTower — Mega-Project Portfolio Control Tower
14
+
15
+ Interactive console for multi-project portfolio selection, RCPSP scheduling, Monte Carlo risk analysis, and what-if resource decisions.
16
+
17
+ **Tabs:** Control Tower · What-If Resource Analysis · Disruption & Reschedule · Algorithm Comparison
18
+
19
+ **Related artifacts:**
20
+ - [Dataset: porttower-portfolio-scenarios](https://huggingface.co/datasets/alirezaaminzadeh/porttower-portfolio-scenarios)
21
+ - [Benchmark: porttower-benchmark-results](https://huggingface.co/datasets/alirezaaminzadeh/porttower-benchmark-results)
22
+ - [Model: porttower-control-tower-config](https://huggingface.co/alirezaaminzadeh/porttower-control-tower-config)
gradio/app.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PortTower — Mega-Project Portfolio Control Tower
3
+ Portfolio selection, RCPSP scheduling, Monte Carlo risk, and what-if resource analysis.
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 porttower.generator import projects_to_table_rows, resources_to_table_rows # noqa: E402
19
+ from porttower.pipeline import PortfolioPipeline # noqa: E402
20
+ from porttower.visualization import ( # noqa: E402
21
+ algorithm_comparison_chart,
22
+ cash_flow_chart,
23
+ disruption_delta_markdown,
24
+ gantt_chart,
25
+ kpi_radar_chart,
26
+ resource_profile_chart,
27
+ result_summary_markdown,
28
+ risk_contribution_chart,
29
+ schedule_comparison_chart,
30
+ schedule_table_rows,
31
+ what_if_summary_markdown,
32
+ )
33
+
34
+ pipeline = PortfolioPipeline(ROOT / "assets")
35
+ pipeline.load()
36
+
37
+ SCENARIO_CHOICES = [(pipeline.get_scenario_label(s), s) for s in pipeline.get_scenario_ids()]
38
+ UNCERTAINTY_CHOICES = [(pipeline.get_uncertainty_label(u), u) for u in pipeline.get_uncertainty_ids()]
39
+ ALGO_CHOICES = [(pipeline.get_algorithm_label(a), a) for a in pipeline.get_algorithm_ids()]
40
+ DISRUPTION_CHOICES = [(pipeline.get_disruption_label(d), d) for d in pipeline.get_disruption_ids()]
41
+ WHATIF_CHOICES = [(pipeline.get_what_if_label(k), k) for k in pipeline.get_what_if_ids()]
42
+
43
+ CUSTOM_CSS = ".gradio-container { max-width: 1440px !important; }"
44
+
45
+
46
+ def run_schedule(scenario, uncertainty, algorithm, num_projects, seed):
47
+ try:
48
+ portfolio = pipeline.build_portfolio(scenario, uncertainty, int(num_projects), seed=int(seed))
49
+ result = pipeline.run_schedule(portfolio, algorithm)
50
+ risk_plot = risk_contribution_chart(result) if algorithm == "scenario_monte_carlo" else None
51
+ return (
52
+ result_summary_markdown(result),
53
+ pd.DataFrame(schedule_table_rows(result)),
54
+ gantt_chart(result, f"Gantt — {pipeline.get_scenario_label(scenario)}"),
55
+ resource_profile_chart(result, portfolio),
56
+ cash_flow_chart(result),
57
+ kpi_radar_chart(result),
58
+ risk_plot,
59
+ pd.DataFrame(projects_to_table_rows(portfolio)),
60
+ pd.DataFrame(resources_to_table_rows(portfolio)),
61
+ json.dumps(result.to_dict(), indent=2),
62
+ "Scheduling complete.",
63
+ )
64
+ except Exception as exc:
65
+ return (
66
+ f"### Error\n{exc}", pd.DataFrame(), None, None, None, None, None,
67
+ pd.DataFrame(), pd.DataFrame(), "", str(exc),
68
+ )
69
+
70
+
71
+ def run_what_if(scenario, uncertainty, resource_key, seed):
72
+ try:
73
+ portfolio = pipeline.build_portfolio(scenario, uncertainty, seed=int(seed))
74
+ wi = pipeline.run_what_if(portfolio, resource_key)
75
+ faster_rows = wi.projects_faster or []
76
+ return (
77
+ what_if_summary_markdown(wi),
78
+ schedule_comparison_chart(wi.baseline, wi.revised),
79
+ gantt_chart(wi.baseline, "Baseline Schedule"),
80
+ gantt_chart(wi.revised, f"After {pipeline.get_what_if_label(resource_key)}"),
81
+ pd.DataFrame(faster_rows) if faster_rows else pd.DataFrame(
82
+ columns=["project", "days_saved", "baseline_finish", "revised_finish"]
83
+ ),
84
+ json.dumps(wi.to_dict(), indent=2),
85
+ "What-if analysis complete.",
86
+ )
87
+ except Exception as exc:
88
+ return f"Error: {exc}", None, None, None, pd.DataFrame(), "", str(exc)
89
+
90
+
91
+ def run_disruption(scenario, uncertainty, algorithm, disruption, delay_days, budget_factor, seed):
92
+ try:
93
+ portfolio = pipeline.build_portfolio(scenario, uncertainty, seed=int(seed))
94
+ params = {}
95
+ if disruption == "activity_delay":
96
+ params = {"delay_days": int(delay_days), "project_index": 0, "activity_index": 1}
97
+ elif disruption == "key_person_removal":
98
+ params = {"skill": "specialist", "capacity_factor": 0.5}
99
+ elif disruption == "budget_reduction":
100
+ params = {"budget_factor": float(budget_factor), "suspend_count": 1}
101
+ elif disruption == "new_project":
102
+ params = {"project_name": "Emergency Scope Addition"}
103
+ elif disruption == "scope_change":
104
+ params = {"cost_factor": 1.25, "duration_factor": 1.15}
105
+
106
+ baseline, revised = pipeline.run_disruption_compare(portfolio, algorithm, disruption, params)
107
+ return (
108
+ disruption_delta_markdown(baseline, revised),
109
+ schedule_comparison_chart(baseline, revised),
110
+ gantt_chart(baseline, "Baseline Schedule"),
111
+ gantt_chart(revised, "Revised Schedule After Disruption"),
112
+ pd.DataFrame(schedule_table_rows(revised)),
113
+ json.dumps({"baseline": baseline.to_dict(), "revised": revised.to_dict()}, indent=2),
114
+ "Disruption analysis complete.",
115
+ )
116
+ except Exception as exc:
117
+ return f"Error: {exc}", None, None, None, pd.DataFrame(), "", str(exc)
118
+
119
+
120
+ def run_algo_compare(scenario, uncertainty, seed):
121
+ try:
122
+ portfolio = pipeline.build_portfolio(scenario, uncertainty, seed=int(seed))
123
+ results = pipeline.run_algorithm_comparison(portfolio)
124
+ rows = []
125
+ for r in results:
126
+ m = r.metrics
127
+ rows.append({
128
+ "Algorithm": pipeline.get_algorithm_label(r.algorithm),
129
+ "NPV (M$)": f"{m.portfolio_npv:.1f}",
130
+ "Makespan": m.makespan,
131
+ "Tardiness": m.total_tardiness,
132
+ "On-Time %": f"{m.on_time_delivery_pct:.1f}",
133
+ "Leveling": f"{m.resource_leveling_index:.4f}",
134
+ "Cash Risk": f"{m.cash_flow_risk:.1f}",
135
+ "Solve (s)": f"{m.solve_time_sec:.2f}",
136
+ })
137
+ return (
138
+ "### Algorithm Comparison",
139
+ algorithm_comparison_chart(results),
140
+ pd.DataFrame(rows),
141
+ )
142
+ except Exception as exc:
143
+ return f"Error: {exc}", None, pd.DataFrame()
144
+
145
+
146
+ with gr.Blocks(title="PortTower", css=CUSTOM_CSS) as demo:
147
+ gr.Markdown(
148
+ "# PortTower — Mega-Project Portfolio Control Tower\n"
149
+ "Portfolio Selection · Multi-Mode RCPSP · Cash-Flow Scheduling · Monte Carlo Risk · What-If Resource Analysis"
150
+ )
151
+
152
+ with gr.Tab("Control Tower"):
153
+ with gr.Row():
154
+ scenario_dd = gr.Dropdown(SCENARIO_CHOICES, value="engineering_epc", label="Industry Scenario")
155
+ unc_dd = gr.Dropdown(UNCERTAINTY_CHOICES, value="baseline", label="Uncertainty Profile")
156
+ algo_dd = gr.Dropdown(ALGO_CHOICES, value="cp_sat_rcpsp", label="Algorithm")
157
+ with gr.Row():
158
+ proj_num = gr.Slider(3, 8, value=6, step=1, label="Candidate Projects")
159
+ seed_num = gr.Number(value=42, label="Seed", precision=0)
160
+ run_btn = gr.Button("Run Control Tower", variant="primary")
161
+ summary_md = gr.Markdown()
162
+ with gr.Row():
163
+ schedule_tbl = gr.Dataframe(label="Activity Schedule", interactive=False)
164
+ with gr.Row():
165
+ gantt_plot = gr.Plot(label="Gantt Chart")
166
+ radar_plot = gr.Plot(label="KPI Radar")
167
+ with gr.Row():
168
+ resource_plot = gr.Plot(label="Resource Profile")
169
+ cash_plot = gr.Plot(label="Cash Flow")
170
+ risk_plot = gr.Plot(label="Risk Contributors (Monte Carlo)")
171
+ with gr.Row():
172
+ projects_tbl = gr.Dataframe(label="Portfolio Selection", interactive=False)
173
+ resources_tbl = gr.Dataframe(label="Shared Resources", interactive=False)
174
+ json_out = gr.JSON(label="Full Result")
175
+ status_txt = gr.Textbox(label="Status", interactive=False)
176
+
177
+ run_btn.click(
178
+ run_schedule,
179
+ [scenario_dd, unc_dd, algo_dd, proj_num, seed_num],
180
+ [summary_md, schedule_tbl, gantt_plot, resource_plot, cash_plot, radar_plot, risk_plot,
181
+ projects_tbl, resources_tbl, json_out, status_txt],
182
+ )
183
+
184
+ with gr.Tab("What-If Resource Analysis"):
185
+ gr.Markdown(
186
+ "Add capacity (e.g. an engineering team) and **re-optimize** the full portfolio. "
187
+ "The system reports which projects finish faster and how total portfolio NPV changes."
188
+ )
189
+ with gr.Row():
190
+ w_scenario = gr.Dropdown(SCENARIO_CHOICES, value="infrastructure_mega", label="Scenario")
191
+ w_unc = gr.Dropdown(UNCERTAINTY_CHOICES, value="resource_shortage", label="Uncertainty")
192
+ w_resource = gr.Dropdown(WHATIF_CHOICES, value="engineering_team", label="Resource to Add")
193
+ w_seed = gr.Number(value=42, label="Seed", precision=0)
194
+ w_btn = gr.Button("Run What-If Analysis", variant="primary")
195
+ w_summary = gr.Markdown()
196
+ with gr.Row():
197
+ w_compare = gr.Plot(label="Baseline vs Revised")
198
+ with gr.Row():
199
+ w_base_gantt = gr.Plot(label="Baseline Gantt")
200
+ w_rev_gantt = gr.Plot(label="Revised Gantt")
201
+ w_faster_tbl = gr.Dataframe(label="Projects Finishing Faster")
202
+ w_json = gr.JSON(label="What-If JSON")
203
+ w_status = gr.Textbox(label="Status", interactive=False)
204
+
205
+ w_btn.click(
206
+ run_what_if,
207
+ [w_scenario, w_unc, w_resource, w_seed],
208
+ [w_summary, w_compare, w_base_gantt, w_rev_gantt, w_faster_tbl, w_json, w_status],
209
+ )
210
+
211
+ with gr.Tab("Disruption & Reschedule"):
212
+ gr.Markdown("Apply a disruption and compare baseline vs rolling-horizon revised plan.")
213
+ with gr.Row():
214
+ d_scenario = gr.Dropdown(SCENARIO_CHOICES, value="oil_gas_field", label="Scenario")
215
+ d_unc = gr.Dropdown(UNCERTAINTY_CHOICES, value="scope_volatility", label="Uncertainty")
216
+ d_algo = gr.Dropdown(ALGO_CHOICES, value="rolling_horizon", label="Algorithm")
217
+ d_type = gr.Dropdown(DISRUPTION_CHOICES, value="activity_delay", label="Disruption Type")
218
+ with gr.Row():
219
+ d_delay = gr.Slider(3, 30, value=10, step=1, label="Delay Days")
220
+ d_budget = gr.Slider(0.5, 0.95, value=0.75, step=0.05, label="Budget Factor")
221
+ d_seed = gr.Number(value=42, label="Seed", precision=0)
222
+ d_btn = gr.Button("Apply Disruption", variant="primary")
223
+ d_delta = gr.Markdown()
224
+ with gr.Row():
225
+ d_compare = gr.Plot(label="Baseline vs Revised")
226
+ with gr.Row():
227
+ d_base_gantt = gr.Plot(label="Baseline Gantt")
228
+ d_rev_gantt = gr.Plot(label="Revised Gantt")
229
+ d_tbl = gr.Dataframe(label="Revised Schedule")
230
+ d_json = gr.JSON(label="Comparison JSON")
231
+ d_status = gr.Textbox(label="Status", interactive=False)
232
+
233
+ d_btn.click(
234
+ run_disruption,
235
+ [d_scenario, d_unc, d_algo, d_type, d_delay, d_budget, d_seed],
236
+ [d_delta, d_compare, d_base_gantt, d_rev_gantt, d_tbl, d_json, d_status],
237
+ )
238
+
239
+ with gr.Tab("Algorithm Comparison"):
240
+ with gr.Row():
241
+ c_scenario = gr.Dropdown(SCENARIO_CHOICES, value="defense_program", label="Scenario")
242
+ c_unc = gr.Dropdown(UNCERTAINTY_CHOICES, value="supply_delay", label="Uncertainty")
243
+ c_seed = gr.Number(value=42, label="Seed", precision=0)
244
+ c_btn = gr.Button("Compare Algorithms", variant="primary")
245
+ c_summary = gr.Markdown()
246
+ c_chart = gr.Plot(label="Comparison Chart")
247
+ c_tbl = gr.Dataframe(label="Results")
248
+
249
+ c_btn.click(run_algo_compare, [c_scenario, c_unc, c_seed], [c_summary, c_chart, c_tbl])
250
+
251
+ gr.Markdown(
252
+ "---\n"
253
+ "**Stack:** OR-Tools CP-SAT · Critical Chain · NSGA-II · Monte Carlo · NetworkX · Plotly · Gradio\n\n"
254
+ "**Industries:** Construction · Infrastructure · Oil & Gas · Pharma R&D · Defense · Technology · Government"
255
+ )
256
+
257
+ if __name__ == "__main__":
258
+ demo.launch()
gradio/assets/demo/benchmarks.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "scenario": "engineering_epc",
4
+ "uncertainty": "baseline",
5
+ "algorithm": "cp_sat_rcpsp",
6
+ "portfolio_npv": 0.0,
7
+ "makespan": 0,
8
+ "total_tardiness": 0,
9
+ "on_time_delivery_pct": 0.0,
10
+ "resource_leveling_index": 0.0,
11
+ "cash_flow_risk": 0.0,
12
+ "solve_time_sec": 0.001298599992878735
13
+ },
14
+ {
15
+ "scenario": "infrastructure_mega",
16
+ "uncertainty": "resource_shortage",
17
+ "algorithm": "critical_chain",
18
+ "portfolio_npv": 0,
19
+ "makespan": 0,
20
+ "total_tardiness": 0,
21
+ "on_time_delivery_pct": 0.0,
22
+ "resource_leveling_index": 0.0,
23
+ "cash_flow_risk": 0.0,
24
+ "solve_time_sec": 0.0
25
+ },
26
+ {
27
+ "scenario": "oil_gas_field",
28
+ "uncertainty": "supply_delay",
29
+ "algorithm": "scenario_monte_carlo",
30
+ "portfolio_npv": 0,
31
+ "makespan": 0,
32
+ "total_tardiness": 0,
33
+ "on_time_delivery_pct": 0.0,
34
+ "resource_leveling_index": 0.0,
35
+ "cash_flow_risk": 0.0,
36
+ "solve_time_sec": 0.108
37
+ },
38
+ {
39
+ "scenario": "pharma_rd",
40
+ "uncertainty": "scope_volatility",
41
+ "algorithm": "nsga2_portfolio",
42
+ "portfolio_npv": 0,
43
+ "makespan": 0,
44
+ "total_tardiness": 0,
45
+ "on_time_delivery_pct": 0.0,
46
+ "resource_leveling_index": 0.0,
47
+ "cash_flow_risk": 0.0,
48
+ "solve_time_sec": 0.045
49
+ },
50
+ {
51
+ "scenario": "defense_program",
52
+ "uncertainty": "baseline",
53
+ "algorithm": "cp_sat_rcpsp",
54
+ "portfolio_npv": 0.0,
55
+ "makespan": 0,
56
+ "total_tardiness": 0,
57
+ "on_time_delivery_pct": 0.0,
58
+ "resource_leveling_index": 0.0,
59
+ "cash_flow_risk": 0.0,
60
+ "solve_time_sec": 0.00035180000122636557
61
+ },
62
+ {
63
+ "scenario": "software_development",
64
+ "uncertainty": "scope_volatility",
65
+ "algorithm": "critical_chain",
66
+ "portfolio_npv": 0,
67
+ "makespan": 0,
68
+ "total_tardiness": 0,
69
+ "on_time_delivery_pct": 0.0,
70
+ "resource_leveling_index": 0.0,
71
+ "cash_flow_risk": 0.0,
72
+ "solve_time_sec": 0.0
73
+ },
74
+ {
75
+ "scenario": "government_program",
76
+ "uncertainty": "cash_crunch",
77
+ "algorithm": "rolling_horizon",
78
+ "portfolio_npv": 0.0,
79
+ "makespan": 0,
80
+ "total_tardiness": 0,
81
+ "on_time_delivery_pct": 0.0,
82
+ "resource_leveling_index": 0.0,
83
+ "cash_flow_risk": 0.0,
84
+ "solve_time_sec": 0.002
85
+ }
86
+ ]
gradio/assets/demo/summary.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project": "PortTower",
3
+ "version": "1.0.0",
4
+ "scenarios": 7,
5
+ "algorithms": 5,
6
+ "primary_metrics": [
7
+ "portfolio_npv",
8
+ "total_tardiness",
9
+ "makespan",
10
+ "resource_leveling_index",
11
+ "cash_flow_risk",
12
+ "on_time_probability_pct",
13
+ "monte_carlo_p50_cost",
14
+ "monte_carlo_p90_cost",
15
+ "liquidity_shortfall_prob_pct",
16
+ "milestone_delay_prob_pct"
17
+ ]
18
+ }
gradio/assets/demo/what_if_results.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "scenario": "engineering_epc",
4
+ "resource": "engineering_team",
5
+ "npv_delta": 0,
6
+ "portfolio_value_change_pct": 0.0,
7
+ "projects_faster_count": 0,
8
+ "on_time_delta_pct": 0.0
9
+ },
10
+ {
11
+ "scenario": "infrastructure_mega",
12
+ "resource": "engineering_team",
13
+ "npv_delta": 0,
14
+ "portfolio_value_change_pct": 0.0,
15
+ "projects_faster_count": 0,
16
+ "on_time_delta_pct": 0.0
17
+ },
18
+ {
19
+ "scenario": "defense_program",
20
+ "resource": "engineering_team",
21
+ "npv_delta": 0,
22
+ "portfolio_value_change_pct": 0.0,
23
+ "projects_faster_count": 0,
24
+ "on_time_delta_pct": 0.0
25
+ }
26
+ ]
gradio/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ortools>=9.10
2
+ networkx>=3.2
3
+ plotly>=5.18
4
+ pandas>=2.1
5
+ numpy>=1.26
gradio/src/portfoliowave/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """porttower — multi-project portfolio scheduling under uncertainty."""
2
+
3
+ __version__ = "1.0.0"
gradio/src/portfoliowave/cashflow.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cash-flow scheduling and liquidity risk metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.models import ActivitySchedule, PortfolioInstance
6
+
7
+
8
+ def build_cash_flow_profile(
9
+ portfolio: PortfolioInstance,
10
+ schedule: list[ActivitySchedule],
11
+ ) -> list[float]:
12
+ horizon = max((s.end for s in schedule), default=portfolio.horizon) + 1
13
+ cash = [0.0] * horizon
14
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
15
+
16
+ for s in schedule:
17
+ act = act_map.get(s.activity_id)
18
+ if act and s.start < horizon:
19
+ cash[s.start] -= act.cost
20
+
21
+ for project in portfolio.active_projects:
22
+ for period, amount in project.cash_flows:
23
+ if 0 <= period < horizon:
24
+ cash[period] += amount
25
+
26
+ cumulative = []
27
+ running = 0.0
28
+ for c in cash:
29
+ running += c
30
+ cumulative.append(round(running, 2))
31
+ return cumulative
32
+
33
+
34
+ def cash_flow_risk(cumulative: list[float]) -> float:
35
+ """Minimum cumulative cash position — lower (more negative) = higher risk."""
36
+ if not cumulative:
37
+ return 0.0
38
+ return round(min(cumulative), 2)
gradio/src/portfoliowave/constants.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Scenario and algorithm metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ENGINE_VERSION = "1.0.0"
6
+
7
+ PORTFOLIO_SCENARIOS: dict[str, dict] = {
8
+ "engineering_epc": {
9
+ "label": "Engineering & Construction EPC",
10
+ "industry": "Construction",
11
+ "default_projects": 5,
12
+ "default_horizon": 180,
13
+ "budget_musd": 48.0,
14
+ },
15
+ "software_development": {
16
+ "label": "Software Product Portfolio",
17
+ "industry": "Technology",
18
+ "default_projects": 6,
19
+ "default_horizon": 120,
20
+ "budget_musd": 12.0,
21
+ },
22
+ "pharma_rd": {
23
+ "label": "Pharmaceutical R&D Pipeline",
24
+ "industry": "Pharma",
25
+ "default_projects": 4,
26
+ "default_horizon": 240,
27
+ "budget_musd": 85.0,
28
+ },
29
+ "oil_gas_field": {
30
+ "label": "Oil & Gas Field Development",
31
+ "industry": "Energy",
32
+ "default_projects": 5,
33
+ "default_horizon": 200,
34
+ "budget_musd": 120.0,
35
+ },
36
+ "government_program": {
37
+ "label": "Government Multi-Program Office",
38
+ "industry": "Public Sector",
39
+ "default_projects": 7,
40
+ "default_horizon": 150,
41
+ "budget_musd": 35.0,
42
+ },
43
+ }
44
+
45
+ UNCERTAINTY_PROFILES: dict[str, dict] = {
46
+ "baseline": {"label": "Baseline (Low Uncertainty)", "duration_cv": 0.08, "cost_cv": 0.05},
47
+ "supply_delay": {"label": "Supply Chain Delays", "duration_cv": 0.18, "cost_cv": 0.12},
48
+ "resource_shortage": {"label": "Resource Shortage", "duration_cv": 0.15, "cost_cv": 0.10},
49
+ "scope_volatility": {"label": "Scope Volatility", "duration_cv": 0.22, "cost_cv": 0.18},
50
+ "cash_crunch": {"label": "Cash Flow Pressure", "duration_cv": 0.12, "cost_cv": 0.20},
51
+ }
52
+
53
+ ALGORITHMS = [
54
+ "cp_sat_rcpsp",
55
+ "critical_chain",
56
+ "nsga2_portfolio",
57
+ "scenario_monte_carlo",
58
+ "rolling_horizon",
59
+ ]
60
+
61
+ ALGORITHM_LABELS: dict[str, str] = {
62
+ "cp_sat_rcpsp": "CP-SAT RCPSP",
63
+ "critical_chain": "Critical Chain",
64
+ "nsga2_portfolio": "NSGA-II Multi-Objective",
65
+ "scenario_monte_carlo": "Scenario Monte Carlo",
66
+ "rolling_horizon": "Rolling Horizon Reschedule",
67
+ }
68
+
69
+ DISRUPTION_TYPES: dict[str, str] = {
70
+ "none": "No Disruption",
71
+ "activity_delay": "Activity Delay",
72
+ "key_person_removal": "Key Person Removal",
73
+ "budget_reduction": "Budget Reduction",
74
+ "new_project": "New Project Injection",
75
+ }
76
+
77
+ RESOURCE_CATEGORIES = [
78
+ "human",
79
+ "machinery",
80
+ "budget",
81
+ "contractor",
82
+ "equipment",
83
+ "laboratory",
84
+ "specialist",
85
+ ]
gradio/src/portfoliowave/cpsat_solver.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OR-Tools CP-SAT solver for multi-project RCPSP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from ortools.sat.python import cp_model
8
+
9
+ from porttower.models import (
10
+ ActivitySchedule,
11
+ PortfolioInstance,
12
+ PortfolioMetrics,
13
+ ScheduleResult,
14
+ )
15
+ from porttower.network import mark_critical_activities
16
+ from porttower.resource_leveling import build_resource_profile, resource_leveling_index
17
+
18
+
19
+ def solve_cp_sat_rcpsp(portfolio: PortfolioInstance) -> ScheduleResult:
20
+ t0 = time.perf_counter()
21
+ mark_critical_activities(portfolio)
22
+ model = cp_model.CpModel()
23
+ horizon = portfolio.horizon
24
+ all_activities = []
25
+ for project in portfolio.active_projects:
26
+ all_activities.extend(project.activities)
27
+
28
+ if not all_activities:
29
+ return ScheduleResult(
30
+ algorithm="cp_sat_rcpsp",
31
+ scenario=portfolio.scenario_id,
32
+ uncertainty=portfolio.uncertainty_id,
33
+ disruption=portfolio.disruption_type,
34
+ metrics=PortfolioMetrics(status="infeasible", solve_time_sec=time.perf_counter() - t0),
35
+ )
36
+
37
+ starts: dict[str, cp_model.IntVar] = {}
38
+ ends: dict[str, cp_model.IntVar] = {}
39
+ intervals: dict[str, cp_model.IntervalVar] = {}
40
+
41
+ for act in all_activities:
42
+ s = model.new_int_var(0, horizon, f"s_{act.activity_id}")
43
+ e = model.new_int_var(0, horizon, f"e_{act.activity_id}")
44
+ iv = model.new_interval_var(s, act.duration, e, f"iv_{act.activity_id}")
45
+ starts[act.activity_id] = s
46
+ ends[act.activity_id] = e
47
+ intervals[act.activity_id] = iv
48
+ for pred in act.predecessors:
49
+ if pred in ends:
50
+ model.add(s >= ends[pred])
51
+
52
+ for project in portfolio.active_projects:
53
+ model.add(ends[project.activities[-1].activity_id] <= project.deadline + horizon // 4)
54
+
55
+ res_caps = {r.resource_id: int(r.capacity * 10) for r in portfolio.resources if r.renewable}
56
+ for res_id, cap in res_caps.items():
57
+ interval_list = []
58
+ demand_list = []
59
+ for act in all_activities:
60
+ demand = act.resource_demands.get(res_id, 0)
61
+ if demand > 0:
62
+ interval_list.append(intervals[act.activity_id])
63
+ demand_list.append(max(1, int(demand * 10)))
64
+ if interval_list:
65
+ model.add_cumulative(interval_list, demand_list, cap)
66
+
67
+ tardiness_vars = []
68
+ for project in portfolio.active_projects:
69
+ last = project.activities[-1]
70
+ tard = model.new_int_var(0, horizon, f"tard_{project.project_id}")
71
+ model.add(tard >= ends[last.activity_id] - project.deadline)
72
+ tardiness_vars.append(tard)
73
+
74
+ if tardiness_vars:
75
+ model.minimize(sum(tardiness_vars))
76
+ else:
77
+ model.minimize(sum(ends[a.activity_id] for a in all_activities))
78
+
79
+ solver = cp_model.CpSolver()
80
+ solver.parameters.max_time_in_seconds = 15.0
81
+ solver.parameters.num_search_workers = 4
82
+ status = solver.solve(model)
83
+ solve_time = time.perf_counter() - t0
84
+
85
+ status_map = {
86
+ cp_model.OPTIMAL: "optimal",
87
+ cp_model.FEASIBLE: "feasible",
88
+ cp_model.INFEASIBLE: "infeasible",
89
+ cp_model.UNKNOWN: "unknown",
90
+ }
91
+ st = status_map.get(status, "unknown")
92
+
93
+ schedule: list[ActivitySchedule] = []
94
+ proj_map = {p.project_id: p for p in portfolio.projects}
95
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
96
+
97
+ if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
98
+ for act in all_activities:
99
+ s = solver.value(starts[act.activity_id])
100
+ e = solver.value(ends[act.activity_id])
101
+ proj = proj_map[act.project_id]
102
+ schedule.append(
103
+ ActivitySchedule(
104
+ activity_id=act.activity_id,
105
+ project_id=act.project_id,
106
+ project_name=proj.name,
107
+ activity_name=act.name,
108
+ start=s,
109
+ end=e,
110
+ duration=act.duration,
111
+ resource_assignments=dict(act.resource_demands),
112
+ is_critical=act.is_critical,
113
+ rationale="CP-SAT RCPSP",
114
+ )
115
+ )
116
+
117
+ makespan = max((s.end for s in schedule), default=0)
118
+ total_tard = sum(max(0, s.end - proj_map[s.project_id].deadline) for s in schedule if s.activity_id.endswith(s.activity_id))
119
+ total_tard = 0
120
+ for project in portfolio.active_projects:
121
+ if project.activities:
122
+ last_id = project.activities[-1].activity_id
123
+ for s in schedule:
124
+ if s.activity_id == last_id:
125
+ total_tard += max(0, s.end - project.deadline)
126
+
127
+ profile = build_resource_profile(portfolio, schedule)
128
+ rli = resource_leveling_index(profile)
129
+ peak_var = max(profile.values(), key=lambda v: max(v) if v else 0)
130
+ peak_var_val = max(peak_var) if peak_var else 0.0
131
+
132
+ npv = sum(p.npv for p in portfolio.active_projects)
133
+ metrics = PortfolioMetrics(
134
+ portfolio_npv=npv,
135
+ total_tardiness=total_tard,
136
+ max_tardiness=max((max(0, s.end - proj_map[s.project_id].deadline) for s in schedule if s.activity_id == proj_map[s.project_id].activities[-1].activity_id), default=0),
137
+ makespan=makespan,
138
+ resource_peak_variance=round(peak_var_val, 2),
139
+ resource_leveling_index=rli,
140
+ projects_active=len(portfolio.active_projects),
141
+ activities_scheduled=len(schedule),
142
+ budget_utilization_pct=round(
143
+ sum(a.cost for p in portfolio.active_projects for a in p.activities) / max(portfolio.budget_cap, 0.01) * 100,
144
+ 1,
145
+ ),
146
+ on_time_delivery_pct=round(
147
+ 100 * sum(1 for p in portfolio.active_projects if all(s.end <= p.deadline for s in schedule if s.project_id == p.project_id and s.activity_id == p.activities[-1].activity_id)) / max(len(portfolio.active_projects), 1),
148
+ 1,
149
+ ),
150
+ solve_time_sec=round(solve_time, 3),
151
+ status=st,
152
+ )
153
+
154
+ return ScheduleResult(
155
+ algorithm="cp_sat_rcpsp",
156
+ scenario=portfolio.scenario_id,
157
+ uncertainty=portfolio.uncertainty_id,
158
+ disruption=portfolio.disruption_type,
159
+ schedule=schedule,
160
+ metrics=metrics,
161
+ resource_profile=profile,
162
+ selected_projects=[p.project_id for p in portfolio.active_projects],
163
+ suspended_projects=[p.project_id for p in portfolio.projects if not p.selected],
164
+ )
gradio/src/portfoliowave/critical_chain.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Critical Chain scheduling with resource buffers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
8
+ from porttower.models import ActivitySchedule, PortfolioInstance, PortfolioMetrics, ScheduleResult
9
+ from porttower.network import critical_path_length, mark_critical_activities
10
+ from porttower.resource_leveling import build_resource_profile, resource_leveling_index
11
+
12
+
13
+ def solve_critical_chain(portfolio: PortfolioInstance) -> ScheduleResult:
14
+ t0 = time.perf_counter()
15
+ mark_critical_activities(portfolio)
16
+ schedule: list[ActivitySchedule] = []
17
+ global_start = 0
18
+
19
+ sorted_projects = sorted(portfolio.active_projects, key=lambda p: (-p.priority, p.deadline))
20
+ for project in sorted_projects:
21
+ _, cp = critical_path_length(project)
22
+ cp_set = set(cp)
23
+ act_starts: dict[str, int] = {}
24
+ proj_start = global_start
25
+
26
+ for act in project.activities:
27
+ pred_end = proj_start
28
+ for pred in act.predecessors:
29
+ if pred in act_starts:
30
+ pred_act = next(a for a in project.activities if a.activity_id == pred)
31
+ pred_end = max(pred_end, act_starts[pred] + pred_act.duration)
32
+ buffer = int(act.duration * 0.15) if act.activity_id in cp_set else 0
33
+ start = pred_end
34
+ end = start + act.duration + buffer
35
+ act_starts[act.activity_id] = start
36
+ schedule.append(
37
+ ActivitySchedule(
38
+ activity_id=act.activity_id,
39
+ project_id=project.project_id,
40
+ project_name=project.name,
41
+ activity_name=act.name,
42
+ start=start,
43
+ end=end,
44
+ duration=act.duration + buffer,
45
+ resource_assignments=dict(act.resource_demands),
46
+ is_critical=act.activity_id in cp_set,
47
+ rationale="Critical Chain + feeding buffer" if buffer else "Critical Chain",
48
+ )
49
+ )
50
+ if project.activities:
51
+ last = project.activities[-1]
52
+ global_start = max(global_start, act_starts.get(last.activity_id, 0) + last.duration)
53
+
54
+ makespan = max((s.end for s in schedule), default=0)
55
+ total_tard = 0
56
+ for project in portfolio.active_projects:
57
+ last_id = project.activities[-1].activity_id
58
+ for s in schedule:
59
+ if s.activity_id == last_id:
60
+ total_tard += max(0, s.end - project.deadline)
61
+
62
+ profile = build_resource_profile(portfolio, schedule)
63
+ npv = sum(p.npv for p in portfolio.active_projects)
64
+ solve_time = time.perf_counter() - t0
65
+
66
+ metrics = PortfolioMetrics(
67
+ portfolio_npv=npv,
68
+ total_tardiness=total_tard,
69
+ makespan=makespan,
70
+ resource_leveling_index=resource_leveling_index(profile),
71
+ projects_active=len(portfolio.active_projects),
72
+ activities_scheduled=len(schedule),
73
+ solve_time_sec=round(solve_time, 3),
74
+ status="feasible",
75
+ )
76
+
77
+ return ScheduleResult(
78
+ algorithm="critical_chain",
79
+ scenario=portfolio.scenario_id,
80
+ uncertainty=portfolio.uncertainty_id,
81
+ disruption=portfolio.disruption_type,
82
+ schedule=schedule,
83
+ metrics=metrics,
84
+ resource_profile=profile,
85
+ selected_projects=[p.project_id for p in portfolio.active_projects],
86
+ suspended_projects=[p.project_id for p in portfolio.projects if not p.selected],
87
+ )
gradio/src/portfoliowave/disruptions.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Disruption modeling for portfolio schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from typing import Any
7
+
8
+ from porttower.generator import generate_portfolio
9
+ from porttower.models import PortfolioInstance
10
+ from porttower.portfolio_selection import suspend_low_priority
11
+
12
+
13
+ def apply_disruption(
14
+ portfolio: PortfolioInstance,
15
+ disruption_type: str,
16
+ params: dict[str, Any] | None = None,
17
+ ) -> PortfolioInstance:
18
+ params = params or {}
19
+ p = deepcopy(portfolio)
20
+ p.disruption_type = disruption_type
21
+ p.disruption_params = params
22
+
23
+ if disruption_type == "none":
24
+ return p
25
+
26
+ if disruption_type == "activity_delay":
27
+ delay_days = int(params.get("delay_days", 10))
28
+ project_idx = int(params.get("project_index", 0))
29
+ activity_idx = int(params.get("activity_index", 0))
30
+ active = p.active_projects
31
+ if active and project_idx < len(active):
32
+ acts = active[project_idx].activities
33
+ if activity_idx < len(acts):
34
+ acts[activity_idx].duration += delay_days
35
+ acts[activity_idx].duration_std += delay_days * 0.2
36
+
37
+ elif disruption_type == "key_person_removal":
38
+ skill = params.get("skill", "specialist")
39
+ res_id = f"res-{skill}"
40
+ for res in p.resources:
41
+ if res.resource_id == res_id:
42
+ res.capacity = max(1.0, res.capacity * float(params.get("capacity_factor", 0.5)))
43
+ for project in p.projects:
44
+ for act in project.activities:
45
+ if act.required_skill == skill and res_id in act.resource_demands:
46
+ act.resource_demands[res_id] *= 1.5
47
+
48
+ elif disruption_type == "budget_reduction":
49
+ factor = float(params.get("budget_factor", 0.75))
50
+ p.budget_cap *= factor
51
+ suspend_count = int(params.get("suspend_count", 1))
52
+ suspend_low_priority(p, suspend_count)
53
+
54
+ elif disruption_type == "new_project":
55
+ extra = generate_portfolio(
56
+ p.scenario_id,
57
+ p.uncertainty_id,
58
+ num_projects=1,
59
+ seed=p.seed + 999,
60
+ )
61
+ if extra.projects:
62
+ new_proj = extra.projects[0]
63
+ new_proj.project_id = f"proj-new-{len(p.projects):03d}"
64
+ new_proj.name = params.get("project_name", f"Emergency: {new_proj.name}")
65
+ new_proj.priority = 5
66
+ p.projects.append(new_proj)
67
+
68
+ return p
69
+
70
+
71
+ def disruption_label(disruption_type: str, params: dict[str, Any] | None = None) -> str:
72
+ params = params or {}
73
+ labels = {
74
+ "none": "No disruption",
75
+ "activity_delay": f"Activity delay +{params.get('delay_days', 10)} days",
76
+ "key_person_removal": f"Key {params.get('skill', 'specialist')} capacity reduced",
77
+ "budget_reduction": f"Budget cut to {int(float(params.get('budget_factor', 0.75)) * 100)}%",
78
+ "new_project": f"New project: {params.get('project_name', 'Emergency scope')}",
79
+ }
80
+ return labels.get(disruption_type, disruption_type)
gradio/src/portfoliowave/engine.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unified portfolio scheduling engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.cashflow import build_cash_flow_profile, cash_flow_risk
6
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
7
+ from porttower.critical_chain import solve_critical_chain
8
+ from porttower.models import PortfolioInstance, ScheduleResult
9
+ from porttower.monte_carlo import run_monte_carlo
10
+ from porttower.nsga2_solver import solve_nsga2_portfolio
11
+ from porttower.portfolio_selection import select_portfolio_greedy
12
+ from porttower.rescheduling import solve_rolling_horizon
13
+
14
+
15
+ class PortfolioEngine:
16
+ """Dispatch scheduling to CP-SAT, Critical Chain, NSGA-II, Monte Carlo, or rolling horizon."""
17
+
18
+ def schedule(
19
+ self,
20
+ portfolio: PortfolioInstance,
21
+ algorithm: str,
22
+ run_portfolio_selection: bool = True,
23
+ ) -> ScheduleResult:
24
+ if run_portfolio_selection:
25
+ select_portfolio_greedy(portfolio)
26
+
27
+ algo = algorithm.lower().replace(" ", "_").replace("-", "_")
28
+ solvers = {
29
+ "cp_sat_rcpsp": lambda: solve_cp_sat_rcpsp(portfolio),
30
+ "cp_sat": lambda: solve_cp_sat_rcpsp(portfolio),
31
+ "critical_chain": lambda: solve_critical_chain(portfolio),
32
+ "nsga2_portfolio": lambda: solve_nsga2_portfolio(portfolio),
33
+ "nsga2": lambda: solve_nsga2_portfolio(portfolio),
34
+ "scenario_monte_carlo": lambda: run_monte_carlo(portfolio),
35
+ "monte_carlo": lambda: run_monte_carlo(portfolio),
36
+ "rolling_horizon": lambda: solve_rolling_horizon(
37
+ portfolio,
38
+ portfolio.disruption_type,
39
+ portfolio.disruption_params,
40
+ ),
41
+ }
42
+
43
+ if algo not in solvers:
44
+ raise ValueError(f"Unknown algorithm: {algorithm}")
45
+
46
+ result = solvers[algo]()
47
+ result.cash_flow_profile = build_cash_flow_profile(portfolio, result.schedule)
48
+ result.metrics.cash_flow_risk = cash_flow_risk(result.cash_flow_profile)
49
+ return result
50
+
51
+ def compare_algorithms(self, portfolio: PortfolioInstance) -> list[ScheduleResult]:
52
+ results = []
53
+ for algo in ["cp_sat_rcpsp", "critical_chain", "nsga2_portfolio", "scenario_monte_carlo"]:
54
+ try:
55
+ p = _clone_portfolio(portfolio)
56
+ results.append(self.schedule(p, algo))
57
+ except Exception:
58
+ continue
59
+ return results
60
+
61
+
62
+ def _clone_portfolio(portfolio: PortfolioInstance) -> PortfolioInstance:
63
+ from copy import deepcopy
64
+ return deepcopy(portfolio)
gradio/src/portfoliowave/generator.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic multi-project portfolio scenario generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Any
7
+
8
+ from porttower.constants import PORTFOLIO_SCENARIOS, RESOURCE_CATEGORIES, UNCERTAINTY_PROFILES
9
+ from porttower.models import Activity, ActivityMode, PortfolioInstance, Project, Resource
10
+
11
+
12
+ def _rng(seed: int) -> random.Random:
13
+ return random.Random(seed)
14
+
15
+
16
+ def _base_resources(rng: random.Random, scenario_id: str) -> list[Resource]:
17
+ caps = {
18
+ "engineering_epc": {"human": 45, "machinery": 12, "budget": 48, "contractor": 8, "equipment": 20, "laboratory": 3, "specialist": 6},
19
+ "software_development": {"human": 60, "machinery": 4, "budget": 12, "contractor": 15, "equipment": 8, "laboratory": 2, "specialist": 10},
20
+ "pharma_rd": {"human": 35, "machinery": 6, "budget": 85, "contractor": 5, "equipment": 10, "laboratory": 8, "specialist": 12},
21
+ "oil_gas_field": {"human": 55, "machinery": 18, "budget": 120, "contractor": 12, "equipment": 25, "laboratory": 4, "specialist": 8},
22
+ "government_program": {"human": 40, "machinery": 5, "budget": 35, "contractor": 10, "equipment": 6, "laboratory": 2, "specialist": 5},
23
+ }
24
+ base = caps.get(scenario_id, caps["engineering_epc"])
25
+ resources = []
26
+ for cat in RESOURCE_CATEGORIES:
27
+ cap = base.get(cat, 10)
28
+ resources.append(
29
+ Resource(
30
+ resource_id=f"res-{cat}",
31
+ name=cat.replace("_", " ").title(),
32
+ category=cat,
33
+ capacity=float(cap),
34
+ unit_cost=rng.uniform(0.8, 2.5),
35
+ renewable=cat != "budget",
36
+ )
37
+ )
38
+ return resources
39
+
40
+
41
+ def _activity_templates(scenario_id: str) -> list[dict[str, Any]]:
42
+ common = [
43
+ {"name": "Initiation & Planning", "dur": (5, 12), "skills": ["human", "specialist"]},
44
+ {"name": "Design & Engineering", "dur": (10, 25), "skills": ["human", "specialist", "equipment"]},
45
+ {"name": "Procurement", "dur": (8, 20), "skills": ["human", "contractor", "budget"]},
46
+ {"name": "Core Execution", "dur": (15, 40), "skills": ["human", "machinery", "equipment"]},
47
+ {"name": "Quality Assurance", "dur": (5, 15), "skills": ["human", "laboratory", "specialist"]},
48
+ {"name": "Integration & Testing", "dur": (8, 18), "skills": ["human", "equipment", "laboratory"]},
49
+ {"name": "Commissioning", "dur": (6, 14), "skills": ["human", "machinery", "specialist"]},
50
+ {"name": "Close-out", "dur": (3, 8), "skills": ["human", "budget"]},
51
+ ]
52
+ if scenario_id == "software_development":
53
+ return [
54
+ {"name": "Discovery & Requirements", "dur": (5, 10), "skills": ["human", "specialist"]},
55
+ {"name": "Architecture Design", "dur": (8, 15), "skills": ["human", "specialist"]},
56
+ {"name": "Sprint Development", "dur": (20, 45), "skills": ["human", "equipment"]},
57
+ {"name": "QA & Testing", "dur": (8, 18), "skills": ["human", "laboratory"]},
58
+ {"name": "UAT & Release", "dur": (5, 12), "skills": ["human", "specialist"]},
59
+ {"name": "Post-launch Support", "dur": (4, 10), "skills": ["human", "contractor"]},
60
+ ]
61
+ if scenario_id == "pharma_rd":
62
+ return [
63
+ {"name": "Target Identification", "dur": (15, 30), "skills": ["human", "laboratory", "specialist"]},
64
+ {"name": "Preclinical Studies", "dur": (25, 50), "skills": ["human", "laboratory", "equipment"]},
65
+ {"name": "IND Preparation", "dur": (10, 20), "skills": ["human", "specialist", "budget"]},
66
+ {"name": "Phase I Trial", "dur": (30, 60), "skills": ["human", "laboratory", "contractor"]},
67
+ {"name": "Phase II Trial", "dur": (40, 80), "skills": ["human", "laboratory", "equipment"]},
68
+ {"name": "Regulatory Submission", "dur": (8, 15), "skills": ["human", "specialist"]},
69
+ ]
70
+ return common
71
+
72
+
73
+ def _project_names(scenario_id: str, n: int, rng: random.Random) -> list[str]:
74
+ pools = {
75
+ "engineering_epc": ["Metro Line Extension", "Hospital Wing", "Bridge Rehabilitation", "Industrial Plant", "Data Center Shell", "Airport Terminal", "Highway Section"],
76
+ "software_development": ["CRM Platform", "Mobile Banking App", "IoT Gateway", "Analytics Dashboard", "ERP Module", "API Gateway", "ML Pipeline"],
77
+ "pharma_rd": ["Oncology Candidate A", "Autoimmune Drug B", "Vaccine Platform C", "Rare Disease Therapy D", "Biosimilar E"],
78
+ "oil_gas_field": ["Offshore Platform A", "Pipeline Segment B", "Refinery Upgrade C", "Well Cluster D", "LNG Terminal E", "FPSO Conversion"],
79
+ "government_program": ["Digital Services Portal", "Infrastructure Renewal", "Cybersecurity Upgrade", "Education Platform", "Healthcare IT", "Smart City Pilot", "Defense Logistics"],
80
+ }
81
+ pool = pools.get(scenario_id, pools["engineering_epc"])
82
+ rng.shuffle(pool)
83
+ return pool[:n]
84
+
85
+
86
+ def _build_project(
87
+ project_id: str,
88
+ name: str,
89
+ scenario_id: str,
90
+ horizon: int,
91
+ rng: random.Random,
92
+ uncertainty_cv: float,
93
+ ) -> Project:
94
+ templates = _activity_templates(scenario_id)
95
+ n_acts = rng.randint(max(4, len(templates) - 2), len(templates))
96
+ chosen = templates[:n_acts]
97
+ activities: list[Activity] = []
98
+ prev_id: str | None = None
99
+ total_cost = 0.0
100
+
101
+ for i, tmpl in enumerate(chosen):
102
+ act_id = f"{project_id}-a{i:02d}"
103
+ dur = rng.randint(*tmpl["dur"])
104
+ std = max(1.0, dur * uncertainty_cv)
105
+ demands = {}
106
+ for skill in tmpl["skills"]:
107
+ demands[f"res-{skill}"] = round(rng.uniform(0.3, 2.5), 2)
108
+ cost = round(dur * rng.uniform(0.8, 2.2), 2)
109
+ total_cost += cost
110
+ modes = [
111
+ ActivityMode("fast", max(3, dur - 4), {k: v * 1.3 for k, v in demands.items()}, cost * 1.25),
112
+ ActivityMode("default", dur, demands, cost),
113
+ ActivityMode("lean", dur + 3, {k: v * 0.7 for k, v in demands.items()}, cost * 0.85),
114
+ ]
115
+ activities.append(
116
+ Activity(
117
+ activity_id=act_id,
118
+ project_id=project_id,
119
+ name=tmpl["name"],
120
+ duration=dur,
121
+ duration_std=std,
122
+ predecessors=[prev_id] if prev_id else [],
123
+ resource_demands=demands,
124
+ cost=cost,
125
+ modes=modes,
126
+ required_skill=tmpl["skills"][0],
127
+ )
128
+ )
129
+ prev_id = act_id
130
+
131
+ npv = round(rng.uniform(2.5, 15.0) * (total_cost / 10), 2)
132
+ deadline = rng.randint(int(horizon * 0.55), int(horizon * 0.95))
133
+ cash_flows = []
134
+ for t in range(0, horizon, rng.randint(8, 15)):
135
+ cash_flows.append((t, round(-rng.uniform(0.5, 3.0), 2)))
136
+ cash_flows.append((horizon, round(npv, 2)))
137
+
138
+ return Project(
139
+ project_id=project_id,
140
+ name=name,
141
+ npv=npv,
142
+ priority=rng.randint(1, 5),
143
+ deadline=deadline,
144
+ activities=activities,
145
+ cash_flows=cash_flows,
146
+ selected=True,
147
+ suspendable=rng.random() > 0.25,
148
+ )
149
+
150
+
151
+ def generate_portfolio(
152
+ scenario_id: str,
153
+ uncertainty_id: str = "baseline",
154
+ num_projects: int | None = None,
155
+ seed: int = 42,
156
+ ) -> PortfolioInstance:
157
+ meta = PORTFOLIO_SCENARIOS[scenario_id]
158
+ unc = UNCERTAINTY_PROFILES[uncertainty_id]
159
+ rng = _rng(seed)
160
+ n = num_projects or meta["default_projects"]
161
+ horizon = meta["default_horizon"]
162
+ budget = meta["budget_musd"]
163
+ resources = _base_resources(rng, scenario_id)
164
+ names = _project_names(scenario_id, n, rng)
165
+ projects = [
166
+ _build_project(f"proj-{i:03d}", names[i], scenario_id, horizon, rng, unc["duration_cv"])
167
+ for i in range(n)
168
+ ]
169
+ return PortfolioInstance(
170
+ scenario_id=scenario_id,
171
+ scenario_label=meta["label"],
172
+ uncertainty_id=uncertainty_id,
173
+ uncertainty_label=unc["label"],
174
+ horizon=horizon,
175
+ budget_cap=budget,
176
+ resources=resources,
177
+ projects=projects,
178
+ seed=seed,
179
+ )
180
+
181
+
182
+ def projects_to_table_rows(portfolio: PortfolioInstance) -> list[dict[str, Any]]:
183
+ rows = []
184
+ for p in portfolio.projects:
185
+ total_dur = sum(a.duration for a in p.activities)
186
+ rows.append({
187
+ "Project": p.name,
188
+ "NPV (M$)": f"{p.npv:.1f}",
189
+ "Priority": p.priority,
190
+ "Deadline": p.deadline,
191
+ "Activities": len(p.activities),
192
+ "Duration": total_dur,
193
+ "Selected": "Yes" if p.selected else "No",
194
+ "Suspendable": "Yes" if p.suspendable else "No",
195
+ })
196
+ return rows
197
+
198
+
199
+ def resources_to_table_rows(portfolio: PortfolioInstance) -> list[dict[str, Any]]:
200
+ return [
201
+ {
202
+ "Resource": r.name,
203
+ "Category": r.category,
204
+ "Capacity": f"{r.capacity:.1f}",
205
+ "Unit Cost": f"${r.unit_cost:.2f}",
206
+ "Renewable": "Yes" if r.renewable else "No",
207
+ }
208
+ for r in portfolio.resources
209
+ ]
gradio/src/portfoliowave/models.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data models for multi-project portfolio scheduling."""
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 Resource:
11
+ resource_id: str
12
+ name: str
13
+ category: str
14
+ capacity: float
15
+ unit_cost: float = 1.0
16
+ renewable: bool = True
17
+
18
+ def to_dict(self) -> dict[str, Any]:
19
+ return asdict(self)
20
+
21
+
22
+ @dataclass
23
+ class ActivityMode:
24
+ mode_id: str
25
+ duration: int
26
+ resource_demands: dict[str, float]
27
+ cost: float
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return asdict(self)
31
+
32
+
33
+ @dataclass
34
+ class Activity:
35
+ activity_id: str
36
+ project_id: str
37
+ name: str
38
+ duration: int
39
+ duration_std: float
40
+ predecessors: list[str] = field(default_factory=list)
41
+ resource_demands: dict[str, float] = field(default_factory=dict)
42
+ cost: float = 0.0
43
+ modes: list[ActivityMode] = field(default_factory=list)
44
+ required_skill: str = ""
45
+ is_critical: bool = False
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ d = asdict(self)
49
+ d["modes"] = [m.to_dict() for m in self.modes]
50
+ return d
51
+
52
+
53
+ @dataclass
54
+ class Project:
55
+ project_id: str
56
+ name: str
57
+ npv: float
58
+ priority: int
59
+ deadline: int
60
+ activities: list[Activity] = field(default_factory=list)
61
+ cash_flows: list[tuple[int, float]] = field(default_factory=list)
62
+ selected: bool = True
63
+ suspendable: bool = True
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return {
67
+ "project_id": self.project_id,
68
+ "name": self.name,
69
+ "npv": self.npv,
70
+ "priority": self.priority,
71
+ "deadline": self.deadline,
72
+ "activities": [a.to_dict() for a in self.activities],
73
+ "cash_flows": self.cash_flows,
74
+ "selected": self.selected,
75
+ "suspendable": self.suspendable,
76
+ }
77
+
78
+
79
+ @dataclass
80
+ class ActivitySchedule:
81
+ activity_id: str
82
+ project_id: str
83
+ project_name: str
84
+ activity_name: str
85
+ start: int
86
+ end: int
87
+ duration: int
88
+ mode_id: str = "default"
89
+ resource_assignments: dict[str, float] = field(default_factory=dict)
90
+ is_critical: bool = False
91
+ delayed: bool = False
92
+ rationale: str = ""
93
+
94
+ def to_dict(self) -> dict[str, Any]:
95
+ return asdict(self)
96
+
97
+
98
+ @dataclass
99
+ class PortfolioMetrics:
100
+ portfolio_npv: float = 0.0
101
+ total_tardiness: int = 0
102
+ max_tardiness: int = 0
103
+ makespan: int = 0
104
+ resource_peak_variance: float = 0.0
105
+ resource_leveling_index: float = 0.0
106
+ cash_flow_risk: float = 0.0
107
+ reschedule_cost: float = 0.0
108
+ projects_active: int = 0
109
+ projects_suspended: int = 0
110
+ activities_scheduled: int = 0
111
+ budget_utilization_pct: float = 0.0
112
+ on_time_delivery_pct: float = 0.0
113
+ solve_time_sec: float = 0.0
114
+ status: str = "unknown"
115
+ monte_carlo_p90_makespan: float = 0.0
116
+ monte_carlo_p90_cost: float = 0.0
117
+
118
+ def to_dict(self) -> dict[str, Any]:
119
+ return asdict(self)
120
+
121
+
122
+ @dataclass
123
+ class ScheduleResult:
124
+ algorithm: str
125
+ scenario: str
126
+ uncertainty: str
127
+ disruption: str
128
+ schedule: list[ActivitySchedule] = field(default_factory=list)
129
+ metrics: PortfolioMetrics = field(default_factory=PortfolioMetrics)
130
+ resource_profile: dict[str, list[float]] = field(default_factory=dict)
131
+ cash_flow_profile: list[float] = field(default_factory=list)
132
+ selected_projects: list[str] = field(default_factory=list)
133
+ suspended_projects: list[str] = field(default_factory=list)
134
+ simulation_runs: list[dict[str, Any]] = field(default_factory=list)
135
+ pareto_front: list[dict[str, Any]] = field(default_factory=list)
136
+
137
+ def to_dict(self) -> dict[str, Any]:
138
+ return {
139
+ "algorithm": self.algorithm,
140
+ "scenario": self.scenario,
141
+ "uncertainty": self.uncertainty,
142
+ "disruption": self.disruption,
143
+ "schedule": [s.to_dict() for s in self.schedule],
144
+ "metrics": self.metrics.to_dict(),
145
+ "resource_profile": self.resource_profile,
146
+ "cash_flow_profile": self.cash_flow_profile,
147
+ "selected_projects": self.selected_projects,
148
+ "suspended_projects": self.suspended_projects,
149
+ "simulation_runs": self.simulation_runs,
150
+ "pareto_front": self.pareto_front,
151
+ }
152
+
153
+
154
+ @dataclass
155
+ class PortfolioInstance:
156
+ scenario_id: str
157
+ scenario_label: str
158
+ uncertainty_id: str
159
+ uncertainty_label: str
160
+ horizon: int
161
+ budget_cap: float
162
+ resources: list[Resource]
163
+ projects: list[Project]
164
+ disruption_type: str = "none"
165
+ disruption_params: dict[str, Any] = field(default_factory=dict)
166
+ seed: int = 42
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ return {
170
+ "scenario_id": self.scenario_id,
171
+ "scenario_label": self.scenario_label,
172
+ "uncertainty_id": self.uncertainty_id,
173
+ "uncertainty_label": self.uncertainty_label,
174
+ "horizon": self.horizon,
175
+ "budget_cap": self.budget_cap,
176
+ "resources": [r.to_dict() for r in self.resources],
177
+ "projects": [p.to_dict() for p in self.projects],
178
+ "disruption_type": self.disruption_type,
179
+ "disruption_params": self.disruption_params,
180
+ "seed": self.seed,
181
+ }
182
+
183
+ @property
184
+ def active_projects(self) -> list[Project]:
185
+ return [p for p in self.projects if p.selected]
gradio/src/portfoliowave/monte_carlo.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Monte Carlo simulation for stochastic project scheduling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ import statistics
7
+ import time
8
+ from copy import deepcopy
9
+
10
+ from porttower.critical_chain import solve_critical_chain
11
+ from porttower.models import PortfolioInstance, ScheduleResult
12
+
13
+
14
+ def _perturb_durations(portfolio: PortfolioInstance, rng: random.Random) -> PortfolioInstance:
15
+ p = deepcopy(portfolio)
16
+ for project in p.active_projects:
17
+ for act in project.activities:
18
+ noise = rng.gauss(0, act.duration_std)
19
+ act.duration = max(1, int(act.duration + noise))
20
+ return p
21
+
22
+
23
+ def run_monte_carlo(
24
+ portfolio: PortfolioInstance,
25
+ n_runs: int = 50,
26
+ seed: int | None = None,
27
+ ) -> ScheduleResult:
28
+ t0 = time.perf_counter()
29
+ rng = random.Random(seed if seed is not None else portfolio.seed)
30
+ base = solve_critical_chain(portfolio)
31
+ makespans: list[int] = []
32
+ costs: list[float] = []
33
+ tardiness: list[int] = []
34
+ runs: list[dict] = []
35
+
36
+ for i in range(n_runs):
37
+ perturbed = _perturb_durations(portfolio, rng)
38
+ result = solve_critical_chain(perturbed)
39
+ m = result.metrics
40
+ makespans.append(m.makespan)
41
+ costs.append(sum(a.cost for p in perturbed.active_projects for a in p.activities))
42
+ tardiness.append(m.total_tardiness)
43
+ if i < 5:
44
+ runs.append({
45
+ "run": i + 1,
46
+ "makespan": m.makespan,
47
+ "tardiness": m.total_tardiness,
48
+ "status": m.status,
49
+ })
50
+
51
+ makespans.sort()
52
+ costs.sort()
53
+ p90_idx = int(0.9 * len(makespans)) if makespans else 0
54
+
55
+ base.metrics.monte_carlo_p90_makespan = makespans[p90_idx] if makespans else 0
56
+ base.metrics.monte_carlo_p90_cost = costs[p90_idx] if costs else 0
57
+ base.metrics.cash_flow_risk = round(statistics.mean(tardiness), 1) if tardiness else 0
58
+ base.algorithm = "scenario_monte_carlo"
59
+ base.simulation_runs = runs
60
+ base.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
61
+ base.metrics.status = "feasible"
62
+ return base
gradio/src/portfoliowave/network.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Activity network utilities using NetworkX."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import networkx as nx
6
+
7
+ from porttower.models import Activity, PortfolioInstance, Project
8
+
9
+
10
+ def build_project_dag(project: Project) -> nx.DiGraph:
11
+ g = nx.DiGraph()
12
+ for act in project.activities:
13
+ g.add_node(
14
+ act.activity_id,
15
+ name=act.name,
16
+ duration=act.duration,
17
+ project_id=project.project_id,
18
+ cost=act.cost,
19
+ )
20
+ for pred in act.predecessors:
21
+ g.add_edge(pred, act.activity_id)
22
+ if not nx.is_directed_acyclic_graph(g) and g.nodes:
23
+ return nx.DiGraph(nx.topological_sort(g))
24
+ return g
25
+
26
+
27
+ def build_portfolio_graph(portfolio: PortfolioInstance) -> nx.DiGraph:
28
+ g = nx.DiGraph()
29
+ for project in portfolio.active_projects:
30
+ pg = build_project_dag(project)
31
+ g = nx.compose(g, pg)
32
+ g.nodes[project.project_id] = {"type": "project", "name": project.name}
33
+ roots = [n for n in pg.nodes if pg.in_degree(n) == 0]
34
+ for root in roots:
35
+ g.add_edge(project.project_id, root)
36
+ return g
37
+
38
+
39
+ def critical_path_length(project: Project) -> tuple[int, list[str]]:
40
+ g = build_project_dag(project)
41
+ if not g.nodes:
42
+ return 0, []
43
+ try:
44
+ order = list(nx.topological_sort(g))
45
+ except nx.NetworkXError:
46
+ return 0, []
47
+ dist: dict[str, int] = {n: 0 for n in g.nodes}
48
+ pred: dict[str, str | None] = {n: None for n in g.nodes}
49
+ for node in order:
50
+ dur = g.nodes[node].get("duration", 0)
51
+ for succ in g.successors(node):
52
+ nd = dist[node] + dur
53
+ if nd > dist[succ]:
54
+ dist[succ] = nd
55
+ pred[succ] = node
56
+ if not dist:
57
+ return 0, []
58
+ end = max(dist, key=dist.get)
59
+ total = dist[end] + g.nodes[end].get("duration", 0)
60
+ path = []
61
+ cur: str | None = end
62
+ while cur is not None:
63
+ path.append(cur)
64
+ cur = pred[cur]
65
+ path.reverse()
66
+ return total, path
67
+
68
+
69
+ def mark_critical_activities(portfolio: PortfolioInstance) -> None:
70
+ for project in portfolio.projects:
71
+ _, cp = critical_path_length(project)
72
+ cp_set = set(cp)
73
+ for act in project.activities:
74
+ act.is_critical = act.activity_id in cp_set
gradio/src/portfoliowave/nsga2_solver.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NSGA-II multi-objective portfolio optimization via pymoo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import itertools
6
+ import time
7
+ from copy import deepcopy
8
+
9
+ from porttower.critical_chain import solve_critical_chain
10
+ from porttower.models import PortfolioInstance, ScheduleResult
11
+ from porttower.portfolio_selection import portfolio_selection_score
12
+
13
+
14
+ def _evaluate_selection(portfolio: PortfolioInstance, mask: tuple[int, ...]) -> dict:
15
+ p = deepcopy(portfolio)
16
+ total_cost = 0.0
17
+ for i, proj in enumerate(p.projects):
18
+ proj.selected = bool(mask[i])
19
+ if proj.selected:
20
+ total_cost += sum(a.cost for a in proj.activities)
21
+ result = solve_critical_chain(p)
22
+ npv = sum(pr.npv for pr in p.active_projects)
23
+ return {
24
+ "mask": mask,
25
+ "npv": npv,
26
+ "tardiness": result.metrics.total_tardiness,
27
+ "leveling": result.metrics.resource_leveling_index,
28
+ "cost": total_cost,
29
+ "feasible": total_cost <= portfolio.budget_cap,
30
+ "result": result,
31
+ }
32
+
33
+
34
+ def _is_dominated(a: dict, b: dict) -> bool:
35
+ """True if a is dominated by b (minimize tardiness & leveling, maximize npv)."""
36
+ return (
37
+ b["npv"] >= a["npv"]
38
+ and b["tardiness"] <= a["tardiness"]
39
+ and b["leveling"] <= a["leveling"]
40
+ and (
41
+ b["npv"] > a["npv"]
42
+ or b["tardiness"] < a["tardiness"]
43
+ or b["leveling"] < a["leveling"]
44
+ )
45
+ )
46
+
47
+
48
+ def solve_nsga2_portfolio(portfolio: PortfolioInstance, pop_size: int = 20, n_gen: int = 15) -> ScheduleResult:
49
+ t0 = time.perf_counter()
50
+ n = min(len(portfolio.projects), 6)
51
+ all_masks = list(itertools.product([0, 1], repeat=n))
52
+ evaluations = []
53
+ for mask in all_masks:
54
+ full_mask = mask + tuple(1 for _ in range(len(portfolio.projects) - n))
55
+ evaluations.append(_evaluate_selection(portfolio, full_mask))
56
+ feasible = [e for e in evaluations if e["feasible"] and any(e["mask"])]
57
+
58
+ if not feasible:
59
+ result = solve_critical_chain(portfolio)
60
+ result.algorithm = "nsga2_portfolio"
61
+ result.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
62
+ return result
63
+
64
+ pareto = []
65
+ for e in feasible:
66
+ if not any(_is_dominated(e, other) for other in feasible if other is not e):
67
+ pareto.append({
68
+ "solution": len(pareto) + 1,
69
+ "npv": round(e["npv"], 2),
70
+ "tardiness": round(e["tardiness"], 1),
71
+ "leveling_index": round(e["leveling"], 4),
72
+ "projects_selected": sum(e["mask"]),
73
+ })
74
+
75
+ pareto.sort(key=lambda x: (-x["npv"], x["tardiness"]))
76
+ best = max(
77
+ feasible,
78
+ key=lambda e: (
79
+ sum(portfolio_selection_score(portfolio.projects[i]) for i in range(len(e["mask"])) if e["mask"][i]),
80
+ e["npv"],
81
+ ),
82
+ )
83
+
84
+ p = deepcopy(portfolio)
85
+ for i, proj in enumerate(p.projects):
86
+ proj.selected = bool(best["mask"][i])
87
+
88
+ result = solve_critical_chain(p)
89
+ result.algorithm = "nsga2_portfolio"
90
+ result.pareto_front = pareto[:pop_size]
91
+ result.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
92
+ result.selected_projects = [pr.project_id for pr in p.active_projects]
93
+ result.suspended_projects = [pr.project_id for pr in p.projects if not pr.selected]
94
+ return result
gradio/src/portfoliowave/pipeline.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Demo pipeline for Hugging Face Space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from copy import deepcopy
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from porttower.constants import (
11
+ ALGORITHM_LABELS,
12
+ ALGORITHMS,
13
+ DISRUPTION_TYPES,
14
+ ENGINE_VERSION,
15
+ PORTFOLIO_SCENARIOS,
16
+ UNCERTAINTY_PROFILES,
17
+ )
18
+ from porttower.engine import PortfolioEngine
19
+ from porttower.generator import generate_portfolio
20
+ from porttower.models import PortfolioInstance, ScheduleResult
21
+ from porttower.rescheduling import compare_schedules
22
+
23
+
24
+ class PortfolioPipeline:
25
+ """Load benchmarks and serve interactive portfolio scheduling."""
26
+
27
+ def __init__(self, assets_dir: Path | None = None):
28
+ self.assets_dir = Path(assets_dir) if assets_dir else None
29
+ self.engine = PortfolioEngine()
30
+ self.summary: dict[str, Any] = {}
31
+ self.benchmarks: list[dict[str, Any]] = []
32
+
33
+ def load(self) -> None:
34
+ if self.assets_dir:
35
+ summary_path = self.assets_dir / "demo" / "summary.json"
36
+ bench_path = self.assets_dir / "demo" / "benchmarks.json"
37
+ if summary_path.exists():
38
+ self.summary = json.loads(summary_path.read_text(encoding="utf-8"))
39
+ if bench_path.exists():
40
+ self.benchmarks = json.loads(bench_path.read_text(encoding="utf-8"))
41
+
42
+ def get_scenario_ids(self) -> list[str]:
43
+ return list(PORTFOLIO_SCENARIOS.keys())
44
+
45
+ def get_uncertainty_ids(self) -> list[str]:
46
+ return list(UNCERTAINTY_PROFILES.keys())
47
+
48
+ def get_algorithm_ids(self) -> list[str]:
49
+ return ALGORITHMS
50
+
51
+ def get_disruption_ids(self) -> list[str]:
52
+ return list(DISRUPTION_TYPES.keys())
53
+
54
+ def get_scenario_label(self, scenario_id: str) -> str:
55
+ return PORTFOLIO_SCENARIOS.get(scenario_id, {}).get("label", scenario_id)
56
+
57
+ def get_uncertainty_label(self, unc_id: str) -> str:
58
+ return UNCERTAINTY_PROFILES.get(unc_id, {}).get("label", unc_id)
59
+
60
+ def get_algorithm_label(self, algo_id: str) -> str:
61
+ return ALGORITHM_LABELS.get(algo_id, algo_id)
62
+
63
+ def get_disruption_label(self, dis_id: str) -> str:
64
+ return DISRUPTION_TYPES.get(dis_id, dis_id)
65
+
66
+ def build_portfolio(
67
+ self,
68
+ scenario_id: str,
69
+ uncertainty_id: str = "baseline",
70
+ num_projects: int | None = None,
71
+ seed: int = 42,
72
+ ) -> PortfolioInstance:
73
+ meta = PORTFOLIO_SCENARIOS[scenario_id]
74
+ return generate_portfolio(
75
+ scenario_id=scenario_id,
76
+ uncertainty_id=uncertainty_id,
77
+ num_projects=num_projects or meta["default_projects"],
78
+ seed=seed,
79
+ )
80
+
81
+ def run_schedule(
82
+ self,
83
+ portfolio: PortfolioInstance,
84
+ algorithm: str,
85
+ ) -> ScheduleResult:
86
+ p = deepcopy(portfolio)
87
+ return self.engine.schedule(p, algorithm)
88
+
89
+ def run_disruption_compare(
90
+ self,
91
+ portfolio: PortfolioInstance,
92
+ algorithm: str,
93
+ disruption_type: str,
94
+ disruption_params: dict | None = None,
95
+ ) -> tuple[ScheduleResult, ScheduleResult]:
96
+ p = deepcopy(portfolio)
97
+ return compare_schedules(p, algorithm, disruption_type, disruption_params)
98
+
99
+ def run_algorithm_comparison(self, portfolio: PortfolioInstance) -> list[ScheduleResult]:
100
+ p = deepcopy(portfolio)
101
+ return self.engine.compare_algorithms(p)
gradio/src/portfoliowave/portfolio_selection.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Project portfolio selection under budget and resource caps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.models import PortfolioInstance, Project
6
+
7
+
8
+ def select_portfolio_greedy(portfolio: PortfolioInstance, budget_factor: float = 1.0) -> list[str]:
9
+ """Select projects by NPV/priority score within budget."""
10
+ budget = portfolio.budget_cap * budget_factor
11
+ scored = sorted(
12
+ portfolio.projects,
13
+ key=lambda p: (p.npv / max(p.priority, 1), p.priority),
14
+ reverse=True,
15
+ )
16
+ selected: list[str] = []
17
+ spent = 0.0
18
+ for p in scored:
19
+ cost = sum(a.cost for a in p.activities)
20
+ if spent + cost <= budget:
21
+ p.selected = True
22
+ selected.append(p.project_id)
23
+ spent += cost
24
+ else:
25
+ p.selected = False
26
+ return selected
27
+
28
+
29
+ def suspend_low_priority(portfolio: PortfolioInstance, count: int = 1) -> list[str]:
30
+ """Suspend lowest-priority suspendable projects."""
31
+ suspendable = sorted(
32
+ [p for p in portfolio.projects if p.selected and p.suspendable],
33
+ key=lambda p: (p.priority, p.npv),
34
+ )
35
+ suspended = []
36
+ for p in suspendable[:count]:
37
+ p.selected = False
38
+ suspended.append(p.project_id)
39
+ return suspended
40
+
41
+
42
+ def portfolio_selection_score(project: Project) -> float:
43
+ total_cost = sum(a.cost for a in project.activities) or 1.0
44
+ return project.npv / total_cost / max(project.priority, 1)
gradio/src/portfoliowave/rescheduling.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rolling horizon rescheduling after disruptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from copy import deepcopy
7
+
8
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
9
+ from porttower.disruptions import apply_disruption
10
+ from porttower.models import ActivitySchedule, PortfolioInstance, ScheduleResult
11
+
12
+
13
+ def compute_reschedule_cost(
14
+ baseline: list[ActivitySchedule],
15
+ revised: list[ActivitySchedule],
16
+ ) -> float:
17
+ base_map = {s.activity_id: s for s in baseline}
18
+ cost = 0.0
19
+ for s in revised:
20
+ b = base_map.get(s.activity_id)
21
+ if b is None:
22
+ cost += 5.0
23
+ continue
24
+ shift = abs(s.start - b.start)
25
+ if shift > 0:
26
+ cost += shift * 0.5
27
+ if s.end - s.start != b.end - b.start:
28
+ cost += 2.0
29
+ return round(cost, 2)
30
+
31
+
32
+ def solve_rolling_horizon(
33
+ portfolio: PortfolioInstance,
34
+ disruption_type: str = "none",
35
+ disruption_params: dict | None = None,
36
+ baseline_result: ScheduleResult | None = None,
37
+ ) -> ScheduleResult:
38
+ t0 = time.perf_counter()
39
+ disrupted = apply_disruption(portfolio, disruption_type, disruption_params)
40
+ revised = solve_cp_sat_rcpsp(disrupted)
41
+ revised.algorithm = "rolling_horizon"
42
+
43
+ if baseline_result and baseline_result.schedule:
44
+ revised.metrics.reschedule_cost = compute_reschedule_cost(
45
+ baseline_result.schedule, revised.schedule
46
+ )
47
+
48
+ revised.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
49
+ return revised
50
+
51
+
52
+ def compare_schedules(
53
+ portfolio: PortfolioInstance,
54
+ algorithm: str,
55
+ disruption_type: str,
56
+ disruption_params: dict | None = None,
57
+ ) -> tuple[ScheduleResult, ScheduleResult]:
58
+ """Return (baseline, revised) schedule pair."""
59
+ from porttower.engine import PortfolioEngine
60
+
61
+ engine = PortfolioEngine()
62
+ baseline_portfolio = deepcopy(portfolio)
63
+ baseline_portfolio.disruption_type = "none"
64
+ baseline = engine.schedule(baseline_portfolio, algorithm)
65
+
66
+ revised_portfolio = deepcopy(portfolio)
67
+ revised = solve_rolling_horizon(
68
+ revised_portfolio,
69
+ disruption_type,
70
+ disruption_params,
71
+ baseline_result=baseline,
72
+ )
73
+ return baseline, revised
gradio/src/portfoliowave/resource_leveling.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resource profile and leveling metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+
7
+ from porttower.models import ActivitySchedule, PortfolioInstance
8
+
9
+
10
+ def build_resource_profile(
11
+ portfolio: PortfolioInstance,
12
+ schedule: list[ActivitySchedule],
13
+ ) -> dict[str, list[float]]:
14
+ horizon = max((s.end for s in schedule), default=portfolio.horizon) + 1
15
+ profile: dict[str, list[float]] = {}
16
+ for res in portfolio.resources:
17
+ if not res.renewable:
18
+ continue
19
+ profile[res.resource_id] = [0.0] * horizon
20
+
21
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
22
+ for s in schedule:
23
+ act = act_map.get(s.activity_id)
24
+ if not act:
25
+ continue
26
+ for res_id, demand in act.resource_demands.items():
27
+ if res_id not in profile:
28
+ profile[res_id] = [0.0] * horizon
29
+ for t in range(s.start, min(s.end, horizon)):
30
+ profile[res_id][t] += demand
31
+
32
+ return profile
33
+
34
+
35
+ def resource_leveling_index(profile: dict[str, list[float]]) -> float:
36
+ """Lower is better — coefficient of variation across time periods."""
37
+ if not profile:
38
+ return 0.0
39
+ all_usage = []
40
+ for usage in profile.values():
41
+ if usage:
42
+ all_usage.extend(usage)
43
+ if not all_usage:
44
+ return 0.0
45
+ mean = statistics.mean(all_usage)
46
+ if mean < 1e-6:
47
+ return 0.0
48
+ return round(statistics.stdev(all_usage) / mean, 4)
49
+
50
+
51
+ def peak_utilization(profile: dict[str, list[float]], capacities: dict[str, float]) -> float:
52
+ peaks = []
53
+ for res_id, usage in profile.items():
54
+ cap = capacities.get(res_id, 1.0)
55
+ if usage and cap > 0:
56
+ peaks.append(max(usage) / cap)
57
+ return round(max(peaks) * 100, 1) if peaks else 0.0
gradio/src/portfoliowave/visualization.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Plotly visualizations for portfolio schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+
10
+ from porttower.models import ActivitySchedule, PortfolioInstance, ScheduleResult
11
+
12
+
13
+ COLORS = [
14
+ "#6366f1", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981",
15
+ "#3b82f6", "#ef4444", "#14b8a6", "#f97316", "#a855f7",
16
+ ]
17
+
18
+
19
+ def _project_color_map(schedule: list[ActivitySchedule]) -> dict[str, str]:
20
+ projects = list(dict.fromkeys(s.project_id for s in schedule))
21
+ return {pid: COLORS[i % len(COLORS)] for i, pid in enumerate(projects)}
22
+
23
+
24
+ def gantt_chart(result: ScheduleResult, title: str = "Portfolio Gantt Chart") -> go.Figure:
25
+ schedule = result.schedule
26
+ if not schedule:
27
+ return go.Figure().add_annotation(text="No schedule data", showarrow=False)
28
+
29
+ colors = _project_color_map(schedule)
30
+ fig = go.Figure()
31
+ for s in schedule:
32
+ fig.add_trace(
33
+ go.Bar(
34
+ x=[s.end - s.start],
35
+ y=[f"{s.project_name}: {s.activity_name}"],
36
+ base=[s.start],
37
+ orientation="h",
38
+ marker=dict(
39
+ color=colors.get(s.project_id, "#6366f1"),
40
+ line=dict(color="#ef4444" if s.is_critical else colors.get(s.project_id, "#6366f1"), width=2 if s.is_critical else 0),
41
+ ),
42
+ name=s.project_name,
43
+ hovertemplate=(
44
+ f"<b>{s.activity_name}</b><br>"
45
+ f"Start: {s.start}<br>End: {s.end}<br>"
46
+ f"Duration: {s.duration}<br>"
47
+ f"Critical: {'Yes' if s.is_critical else 'No'}<extra></extra>"
48
+ ),
49
+ showlegend=False,
50
+ )
51
+ )
52
+ fig.update_layout(
53
+ title=title,
54
+ barmode="overlay",
55
+ height=max(400, len(schedule) * 28),
56
+ xaxis_title="Time (days)",
57
+ yaxis=dict(autorange="reversed"),
58
+ template="plotly_dark",
59
+ margin=dict(l=200),
60
+ )
61
+ return fig
62
+
63
+
64
+ def resource_profile_chart(result: ScheduleResult, portfolio: PortfolioInstance) -> go.Figure:
65
+ profile = result.resource_profile
66
+ if not profile:
67
+ return go.Figure().add_annotation(text="No resource profile", showarrow=False)
68
+
69
+ fig = make_subplots(rows=len(profile), cols=1, shared_xaxes=True, subplot_titles=list(profile.keys()))
70
+ caps = {r.resource_id: r.capacity for r in portfolio.resources}
71
+ for i, (res_id, usage) in enumerate(profile.items(), 1):
72
+ cap = caps.get(res_id, 1.0)
73
+ fig.add_trace(
74
+ go.Scatter(y=usage, mode="lines", fill="tozeroy", name=res_id, line=dict(width=2)),
75
+ row=i, col=1,
76
+ )
77
+ fig.add_hline(y=cap, line_dash="dash", line_color="#ef4444", row=i, col=1)
78
+ fig.update_layout(
79
+ title="Resource Utilization Profile",
80
+ height=max(300, 120 * len(profile)),
81
+ template="plotly_dark",
82
+ showlegend=False,
83
+ )
84
+ fig.update_xaxes(title_text="Time (days)", row=len(profile), col=1)
85
+ return fig
86
+
87
+
88
+ def cash_flow_chart(result: ScheduleResult) -> go.Figure:
89
+ cf = result.cash_flow_profile
90
+ if not cf:
91
+ return go.Figure().add_annotation(text="No cash flow data", showarrow=False)
92
+ fig = go.Figure()
93
+ fig.add_trace(go.Scatter(y=cf, mode="lines+markers", fill="tozeroy", name="Cumulative Cash"))
94
+ fig.add_hline(y=0, line_dash="dash", line_color="#ef4444")
95
+ fig.update_layout(
96
+ title="Cumulative Cash Flow",
97
+ xaxis_title="Time (days)",
98
+ yaxis_title="Cash Position (M$)",
99
+ template="plotly_dark",
100
+ height=350,
101
+ )
102
+ return fig
103
+
104
+
105
+ def schedule_comparison_chart(baseline: ScheduleResult, revised: ScheduleResult) -> go.Figure:
106
+ base_map = {s.activity_id: s for s in baseline.schedule}
107
+ fig = go.Figure()
108
+ for s in revised.schedule:
109
+ b = base_map.get(s.activity_id)
110
+ shift = (s.start - b.start) if b else 0
111
+ color = "#ef4444" if shift > 0 else "#10b981" if shift < 0 else "#6366f1"
112
+ fig.add_trace(
113
+ go.Bar(
114
+ x=[s.end - s.start],
115
+ y=[f"{s.project_name}: {s.activity_name}"],
116
+ base=[s.start],
117
+ orientation="h",
118
+ marker_color=color,
119
+ name="Revised",
120
+ hovertemplate=f"Shift: {shift:+d} days<extra></extra>",
121
+ showlegend=False,
122
+ )
123
+ )
124
+ if b:
125
+ fig.add_trace(
126
+ go.Bar(
127
+ x=[b.end - b.start],
128
+ y=[f"{s.project_name}: {s.activity_name}"],
129
+ base=[b.start],
130
+ orientation="h",
131
+ marker=dict(color="rgba(99,102,241,0.3)", line=dict(color="#6366f1", width=1, dash="dot")),
132
+ name="Baseline",
133
+ showlegend=False,
134
+ )
135
+ )
136
+ fig.update_layout(
137
+ title="Baseline (faded) vs Revised Schedule",
138
+ barmode="overlay",
139
+ height=max(400, len(revised.schedule) * 30),
140
+ xaxis_title="Time (days)",
141
+ yaxis=dict(autorange="reversed"),
142
+ template="plotly_dark",
143
+ margin=dict(l=200),
144
+ )
145
+ return fig
146
+
147
+
148
+ def kpi_radar_chart(result: ScheduleResult) -> go.Figure:
149
+ m = result.metrics
150
+ categories = ["NPV", "On-Time %", "Leveling", "Budget Use", "Activities"]
151
+ values = [
152
+ min(100, m.portfolio_npv * 5),
153
+ m.on_time_delivery_pct,
154
+ max(0, 100 - m.resource_leveling_index * 100),
155
+ min(100, m.budget_utilization_pct),
156
+ min(100, m.activities_scheduled * 3),
157
+ ]
158
+ fig = go.Figure()
159
+ fig.add_trace(go.Scatterpolar(r=values + [values[0]], theta=categories + [categories[0]], fill="toself", name="KPIs"))
160
+ fig.update_layout(
161
+ polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
162
+ title="Portfolio KPI Radar",
163
+ template="plotly_dark",
164
+ height=400,
165
+ )
166
+ return fig
167
+
168
+
169
+ def algorithm_comparison_chart(results: list[ScheduleResult]) -> go.Figure:
170
+ names = [r.algorithm for r in results]
171
+ fig = go.Figure()
172
+ fig.add_trace(go.Bar(name="NPV (M$)", x=names, y=[r.metrics.portfolio_npv for r in results]))
173
+ fig.add_trace(go.Bar(name="Tardiness", x=names, y=[r.metrics.total_tardiness for r in results]))
174
+ fig.add_trace(go.Bar(name="Makespan", x=names, y=[r.metrics.makespan for r in results]))
175
+ fig.update_layout(
176
+ title="Algorithm Comparison",
177
+ barmode="group",
178
+ template="plotly_dark",
179
+ height=400,
180
+ )
181
+ return fig
182
+
183
+
184
+ def result_summary_markdown(result: ScheduleResult) -> str:
185
+ m = result.metrics
186
+ return (
187
+ f"### Schedule Summary — {result.algorithm}\n\n"
188
+ f"| Metric | Value |\n|--------|-------|\n"
189
+ f"| Status | **{m.status}** |\n"
190
+ f"| Portfolio NPV | **{m.portfolio_npv:.1f} M$** |\n"
191
+ f"| Makespan | **{m.makespan}** days |\n"
192
+ f"| Total Tardiness | **{m.total_tardiness}** days |\n"
193
+ f"| On-Time Delivery | **{m.on_time_delivery_pct:.1f}%** |\n"
194
+ f"| Resource Leveling Index | **{m.resource_leveling_index:.4f}** |\n"
195
+ f"| Budget Utilization | **{m.budget_utilization_pct:.1f}%** |\n"
196
+ f"| Cash Flow Risk (min) | **{m.cash_flow_risk:.1f} M$** |\n"
197
+ f"| Reschedule Cost | **{m.reschedule_cost:.1f}** |\n"
198
+ f"| Projects Active | **{m.projects_active}** |\n"
199
+ f"| Activities Scheduled | **{m.activities_scheduled}** |\n"
200
+ f"| Solve Time | **{m.solve_time_sec:.2f}s** |\n"
201
+ + (
202
+ f"| Monte Carlo P90 Makespan | **{m.monte_carlo_p90_makespan}** days |\n"
203
+ if m.monte_carlo_p90_makespan
204
+ else ""
205
+ )
206
+ )
207
+
208
+
209
+ def disruption_delta_markdown(baseline: ScheduleResult, revised: ScheduleResult) -> str:
210
+ bm, rm = baseline.metrics, revised.metrics
211
+ return (
212
+ "### Disruption Impact\n\n"
213
+ f"| Metric | Baseline | Revised | Delta |\n"
214
+ f"|--------|----------|---------|-------|\n"
215
+ f"| Makespan | {bm.makespan} | {rm.makespan} | **{rm.makespan - bm.makespan:+d}** |\n"
216
+ f"| Tardiness | {bm.total_tardiness} | {rm.total_tardiness} | **{rm.total_tardiness - bm.total_tardiness:+d}** |\n"
217
+ f"| NPV | {bm.portfolio_npv:.1f} | {rm.portfolio_npv:.1f} | **{rm.portfolio_npv - bm.portfolio_npv:+.1f}** |\n"
218
+ f"| On-Time % | {bm.on_time_delivery_pct:.1f} | {rm.on_time_delivery_pct:.1f} | **{rm.on_time_delivery_pct - bm.on_time_delivery_pct:+.1f}** |\n"
219
+ f"| Reschedule Cost | — | {rm.reschedule_cost:.1f} | — |\n"
220
+ )
221
+
222
+
223
+ def schedule_table_rows(result: ScheduleResult) -> list[dict[str, Any]]:
224
+ return [
225
+ {
226
+ "Project": s.project_name,
227
+ "Activity": s.activity_name,
228
+ "Start": s.start,
229
+ "End": s.end,
230
+ "Duration": s.duration,
231
+ "Critical": "Yes" if s.is_critical else "No",
232
+ "Rationale": s.rationale,
233
+ }
234
+ for s in result.schedule
235
+ ]
gradio/src/porttower/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """porttower — multi-project portfolio scheduling under uncertainty."""
2
+
3
+ __version__ = "1.0.0"
gradio/src/porttower/cashflow.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cash-flow scheduling and liquidity risk metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.models import ActivitySchedule, PortfolioInstance
6
+
7
+
8
+ def build_cash_flow_profile(
9
+ portfolio: PortfolioInstance,
10
+ schedule: list[ActivitySchedule],
11
+ ) -> list[float]:
12
+ horizon = max((s.end for s in schedule), default=portfolio.horizon) + 1
13
+ cash = [0.0] * horizon
14
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
15
+
16
+ for s in schedule:
17
+ act = act_map.get(s.activity_id)
18
+ if act and s.start < horizon:
19
+ cash[s.start] -= act.cost
20
+
21
+ for project in portfolio.active_projects:
22
+ for period, amount in project.cash_flows:
23
+ if 0 <= period < horizon:
24
+ cash[period] += amount
25
+
26
+ cumulative = []
27
+ running = 0.0
28
+ for c in cash:
29
+ running += c
30
+ cumulative.append(round(running, 2))
31
+ return cumulative
32
+
33
+
34
+ def cash_flow_risk(cumulative: list[float]) -> float:
35
+ """Minimum cumulative cash position — lower (more negative) = higher risk."""
36
+ if not cumulative:
37
+ return 0.0
38
+ return round(min(cumulative), 2)
gradio/src/porttower/constants.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Scenario and algorithm metadata for PortTower control tower."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ENGINE_VERSION = "1.0.0"
6
+
7
+ PORTFOLIO_SCENARIOS: dict[str, dict] = {
8
+ "engineering_epc": {
9
+ "label": "Engineering & Construction EPC",
10
+ "industry": "Construction",
11
+ "default_projects": 6,
12
+ "default_horizon": 180,
13
+ "budget_musd": 48.0,
14
+ },
15
+ "infrastructure_mega": {
16
+ "label": "National Infrastructure Program",
17
+ "industry": "Infrastructure",
18
+ "default_projects": 7,
19
+ "default_horizon": 220,
20
+ "budget_musd": 95.0,
21
+ },
22
+ "oil_gas_field": {
23
+ "label": "Oil & Gas Field Development",
24
+ "industry": "Oil & Gas",
25
+ "default_projects": 5,
26
+ "default_horizon": 200,
27
+ "budget_musd": 120.0,
28
+ },
29
+ "pharma_rd": {
30
+ "label": "Pharmaceutical R&D Pipeline",
31
+ "industry": "Pharma",
32
+ "default_projects": 4,
33
+ "default_horizon": 240,
34
+ "budget_musd": 85.0,
35
+ },
36
+ "defense_program": {
37
+ "label": "Defense Systems Portfolio",
38
+ "industry": "Defense",
39
+ "default_projects": 5,
40
+ "default_horizon": 210,
41
+ "budget_musd": 72.0,
42
+ },
43
+ "software_development": {
44
+ "label": "Technology Product Portfolio",
45
+ "industry": "Product Development",
46
+ "default_projects": 6,
47
+ "default_horizon": 120,
48
+ "budget_musd": 12.0,
49
+ },
50
+ "government_program": {
51
+ "label": "Government Multi-Program Office",
52
+ "industry": "Government",
53
+ "default_projects": 7,
54
+ "default_horizon": 150,
55
+ "budget_musd": 35.0,
56
+ },
57
+ }
58
+
59
+ UNCERTAINTY_PROFILES: dict[str, dict] = {
60
+ "baseline": {
61
+ "label": "Baseline (Low Uncertainty)",
62
+ "duration_cv": 0.08,
63
+ "cost_cv": 0.05,
64
+ "rework_prob": 0.04,
65
+ "permit_delay_prob": 0.06,
66
+ "resource_shortage_prob": 0.05,
67
+ },
68
+ "supply_delay": {
69
+ "label": "Supply Chain Delays",
70
+ "duration_cv": 0.18,
71
+ "cost_cv": 0.12,
72
+ "rework_prob": 0.08,
73
+ "permit_delay_prob": 0.14,
74
+ "resource_shortage_prob": 0.10,
75
+ },
76
+ "resource_shortage": {
77
+ "label": "Resource Shortage",
78
+ "duration_cv": 0.15,
79
+ "cost_cv": 0.10,
80
+ "rework_prob": 0.06,
81
+ "permit_delay_prob": 0.08,
82
+ "resource_shortage_prob": 0.22,
83
+ },
84
+ "scope_volatility": {
85
+ "label": "Scope Volatility",
86
+ "duration_cv": 0.22,
87
+ "cost_cv": 0.18,
88
+ "rework_prob": 0.15,
89
+ "permit_delay_prob": 0.10,
90
+ "resource_shortage_prob": 0.08,
91
+ },
92
+ "cash_crunch": {
93
+ "label": "Cash Flow Pressure",
94
+ "duration_cv": 0.12,
95
+ "cost_cv": 0.20,
96
+ "rework_prob": 0.05,
97
+ "permit_delay_prob": 0.07,
98
+ "resource_shortage_prob": 0.12,
99
+ },
100
+ }
101
+
102
+ ALGORITHMS = [
103
+ "cp_sat_rcpsp",
104
+ "critical_chain",
105
+ "nsga2_portfolio",
106
+ "scenario_monte_carlo",
107
+ "rolling_horizon",
108
+ ]
109
+
110
+ ALGORITHM_LABELS: dict[str, str] = {
111
+ "cp_sat_rcpsp": "CP-SAT RCPSP (Multi-Mode)",
112
+ "critical_chain": "Critical Chain",
113
+ "nsga2_portfolio": "NSGA-II Multi-Objective",
114
+ "scenario_monte_carlo": "Monte Carlo Risk Analysis",
115
+ "rolling_horizon": "Rolling Horizon Reschedule",
116
+ }
117
+
118
+ DISRUPTION_TYPES: dict[str, str] = {
119
+ "none": "No Disruption",
120
+ "activity_delay": "Activity Delay",
121
+ "key_person_removal": "Key Person Removal",
122
+ "budget_reduction": "Budget Reduction",
123
+ "new_project": "New Project Injection",
124
+ "scope_change": "Scope Change / Cost Overrun",
125
+ }
126
+
127
+ WHAT_IF_RESOURCES: dict[str, dict] = {
128
+ "engineering_team": {
129
+ "label": "Engineering Team (+capacity)",
130
+ "resource_id": "res-human",
131
+ "capacity_delta": 8.0,
132
+ "cost_per_unit": 0.15,
133
+ },
134
+ "specialist_pool": {
135
+ "label": "Specialist Pool (+capacity)",
136
+ "resource_id": "res-specialist",
137
+ "capacity_delta": 4.0,
138
+ "cost_per_unit": 0.22,
139
+ },
140
+ "contractor_squad": {
141
+ "label": "Contractor Squad (+capacity)",
142
+ "resource_id": "res-contractor",
143
+ "capacity_delta": 6.0,
144
+ "cost_per_unit": 0.18,
145
+ },
146
+ "lab_capacity": {
147
+ "label": "Laboratory Capacity (+capacity)",
148
+ "resource_id": "res-laboratory",
149
+ "capacity_delta": 3.0,
150
+ "cost_per_unit": 0.25,
151
+ },
152
+ }
153
+
154
+ RESOURCE_CATEGORIES = [
155
+ "human",
156
+ "machinery",
157
+ "budget",
158
+ "contractor",
159
+ "equipment",
160
+ "laboratory",
161
+ "specialist",
162
+ ]
gradio/src/porttower/cpsat_solver.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OR-Tools CP-SAT solver for multi-project RCPSP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from ortools.sat.python import cp_model
8
+
9
+ from porttower.models import (
10
+ ActivitySchedule,
11
+ PortfolioInstance,
12
+ PortfolioMetrics,
13
+ ScheduleResult,
14
+ )
15
+ from porttower.network import mark_critical_activities
16
+ from porttower.resource_leveling import build_resource_profile, resource_leveling_index
17
+
18
+
19
+ def solve_cp_sat_rcpsp(portfolio: PortfolioInstance) -> ScheduleResult:
20
+ t0 = time.perf_counter()
21
+ mark_critical_activities(portfolio)
22
+ model = cp_model.CpModel()
23
+ horizon = portfolio.horizon
24
+ all_activities = []
25
+ for project in portfolio.active_projects:
26
+ all_activities.extend(project.activities)
27
+
28
+ if not all_activities:
29
+ return ScheduleResult(
30
+ algorithm="cp_sat_rcpsp",
31
+ scenario=portfolio.scenario_id,
32
+ uncertainty=portfolio.uncertainty_id,
33
+ disruption=portfolio.disruption_type,
34
+ metrics=PortfolioMetrics(status="infeasible", solve_time_sec=time.perf_counter() - t0),
35
+ )
36
+
37
+ starts: dict[str, cp_model.IntVar] = {}
38
+ ends: dict[str, cp_model.IntVar] = {}
39
+ intervals: dict[str, cp_model.IntervalVar] = {}
40
+
41
+ for act in all_activities:
42
+ s = model.new_int_var(0, horizon, f"s_{act.activity_id}")
43
+ e = model.new_int_var(0, horizon, f"e_{act.activity_id}")
44
+ iv = model.new_interval_var(s, act.duration, e, f"iv_{act.activity_id}")
45
+ starts[act.activity_id] = s
46
+ ends[act.activity_id] = e
47
+ intervals[act.activity_id] = iv
48
+ for pred in act.predecessors:
49
+ if pred in ends:
50
+ model.add(s >= ends[pred])
51
+
52
+ for project in portfolio.active_projects:
53
+ model.add(ends[project.activities[-1].activity_id] <= project.deadline + horizon // 4)
54
+
55
+ res_caps = {r.resource_id: int(r.capacity * 10) for r in portfolio.resources if r.renewable}
56
+ for res_id, cap in res_caps.items():
57
+ interval_list = []
58
+ demand_list = []
59
+ for act in all_activities:
60
+ demand = act.resource_demands.get(res_id, 0)
61
+ if demand > 0:
62
+ interval_list.append(intervals[act.activity_id])
63
+ demand_list.append(max(1, int(demand * 10)))
64
+ if interval_list:
65
+ model.add_cumulative(interval_list, demand_list, cap)
66
+
67
+ tardiness_vars = []
68
+ for project in portfolio.active_projects:
69
+ last = project.activities[-1]
70
+ tard = model.new_int_var(0, horizon, f"tard_{project.project_id}")
71
+ model.add(tard >= ends[last.activity_id] - project.deadline)
72
+ tardiness_vars.append(tard)
73
+
74
+ if tardiness_vars:
75
+ model.minimize(sum(tardiness_vars))
76
+ else:
77
+ model.minimize(sum(ends[a.activity_id] for a in all_activities))
78
+
79
+ solver = cp_model.CpSolver()
80
+ solver.parameters.max_time_in_seconds = 15.0
81
+ solver.parameters.num_search_workers = 4
82
+ status = solver.solve(model)
83
+ solve_time = time.perf_counter() - t0
84
+
85
+ status_map = {
86
+ cp_model.OPTIMAL: "optimal",
87
+ cp_model.FEASIBLE: "feasible",
88
+ cp_model.INFEASIBLE: "infeasible",
89
+ cp_model.UNKNOWN: "unknown",
90
+ }
91
+ st = status_map.get(status, "unknown")
92
+
93
+ schedule: list[ActivitySchedule] = []
94
+ proj_map = {p.project_id: p for p in portfolio.projects}
95
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
96
+
97
+ if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
98
+ for act in all_activities:
99
+ s = solver.value(starts[act.activity_id])
100
+ e = solver.value(ends[act.activity_id])
101
+ proj = proj_map[act.project_id]
102
+ schedule.append(
103
+ ActivitySchedule(
104
+ activity_id=act.activity_id,
105
+ project_id=act.project_id,
106
+ project_name=proj.name,
107
+ activity_name=act.name,
108
+ start=s,
109
+ end=e,
110
+ duration=act.duration,
111
+ resource_assignments=dict(act.resource_demands),
112
+ is_critical=act.is_critical,
113
+ rationale="CP-SAT RCPSP",
114
+ )
115
+ )
116
+
117
+ makespan = max((s.end for s in schedule), default=0)
118
+ total_tard = sum(max(0, s.end - proj_map[s.project_id].deadline) for s in schedule if s.activity_id.endswith(s.activity_id))
119
+ total_tard = 0
120
+ for project in portfolio.active_projects:
121
+ if project.activities:
122
+ last_id = project.activities[-1].activity_id
123
+ for s in schedule:
124
+ if s.activity_id == last_id:
125
+ total_tard += max(0, s.end - project.deadline)
126
+
127
+ profile = build_resource_profile(portfolio, schedule)
128
+ rli = resource_leveling_index(profile)
129
+ peak_var = max(profile.values(), key=lambda v: max(v) if v else 0)
130
+ peak_var_val = max(peak_var) if peak_var else 0.0
131
+
132
+ npv = sum(p.npv for p in portfolio.active_projects)
133
+ metrics = PortfolioMetrics(
134
+ portfolio_npv=npv,
135
+ total_tardiness=total_tard,
136
+ max_tardiness=max((max(0, s.end - proj_map[s.project_id].deadline) for s in schedule if s.activity_id == proj_map[s.project_id].activities[-1].activity_id), default=0),
137
+ makespan=makespan,
138
+ resource_peak_variance=round(peak_var_val, 2),
139
+ resource_leveling_index=rli,
140
+ projects_active=len(portfolio.active_projects),
141
+ activities_scheduled=len(schedule),
142
+ budget_utilization_pct=round(
143
+ sum(a.cost for p in portfolio.active_projects for a in p.activities) / max(portfolio.budget_cap, 0.01) * 100,
144
+ 1,
145
+ ),
146
+ on_time_delivery_pct=round(
147
+ 100 * sum(1 for p in portfolio.active_projects if all(s.end <= p.deadline for s in schedule if s.project_id == p.project_id and s.activity_id == p.activities[-1].activity_id)) / max(len(portfolio.active_projects), 1),
148
+ 1,
149
+ ),
150
+ solve_time_sec=round(solve_time, 3),
151
+ status=st,
152
+ )
153
+
154
+ return ScheduleResult(
155
+ algorithm="cp_sat_rcpsp",
156
+ scenario=portfolio.scenario_id,
157
+ uncertainty=portfolio.uncertainty_id,
158
+ disruption=portfolio.disruption_type,
159
+ schedule=schedule,
160
+ metrics=metrics,
161
+ resource_profile=profile,
162
+ selected_projects=[p.project_id for p in portfolio.active_projects],
163
+ suspended_projects=[p.project_id for p in portfolio.projects if not p.selected],
164
+ )
gradio/src/porttower/critical_chain.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Critical Chain scheduling with resource buffers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
8
+ from porttower.models import ActivitySchedule, PortfolioInstance, PortfolioMetrics, ScheduleResult
9
+ from porttower.network import critical_path_length, mark_critical_activities
10
+ from porttower.resource_leveling import build_resource_profile, resource_leveling_index
11
+
12
+
13
+ def solve_critical_chain(portfolio: PortfolioInstance) -> ScheduleResult:
14
+ t0 = time.perf_counter()
15
+ mark_critical_activities(portfolio)
16
+ schedule: list[ActivitySchedule] = []
17
+ global_start = 0
18
+
19
+ sorted_projects = sorted(portfolio.active_projects, key=lambda p: (-p.priority, p.deadline))
20
+ for project in sorted_projects:
21
+ _, cp = critical_path_length(project)
22
+ cp_set = set(cp)
23
+ act_starts: dict[str, int] = {}
24
+ proj_start = global_start
25
+
26
+ for act in project.activities:
27
+ pred_end = proj_start
28
+ for pred in act.predecessors:
29
+ if pred in act_starts:
30
+ pred_act = next(a for a in project.activities if a.activity_id == pred)
31
+ pred_end = max(pred_end, act_starts[pred] + pred_act.duration)
32
+ buffer = int(act.duration * 0.15) if act.activity_id in cp_set else 0
33
+ start = pred_end
34
+ end = start + act.duration + buffer
35
+ act_starts[act.activity_id] = start
36
+ schedule.append(
37
+ ActivitySchedule(
38
+ activity_id=act.activity_id,
39
+ project_id=project.project_id,
40
+ project_name=project.name,
41
+ activity_name=act.name,
42
+ start=start,
43
+ end=end,
44
+ duration=act.duration + buffer,
45
+ resource_assignments=dict(act.resource_demands),
46
+ is_critical=act.activity_id in cp_set,
47
+ rationale="Critical Chain + feeding buffer" if buffer else "Critical Chain",
48
+ )
49
+ )
50
+ if project.activities:
51
+ last = project.activities[-1]
52
+ global_start = max(global_start, act_starts.get(last.activity_id, 0) + last.duration)
53
+
54
+ makespan = max((s.end for s in schedule), default=0)
55
+ total_tard = 0
56
+ for project in portfolio.active_projects:
57
+ last_id = project.activities[-1].activity_id
58
+ for s in schedule:
59
+ if s.activity_id == last_id:
60
+ total_tard += max(0, s.end - project.deadline)
61
+
62
+ profile = build_resource_profile(portfolio, schedule)
63
+ npv = sum(p.npv for p in portfolio.active_projects)
64
+ solve_time = time.perf_counter() - t0
65
+
66
+ metrics = PortfolioMetrics(
67
+ portfolio_npv=npv,
68
+ total_tardiness=total_tard,
69
+ makespan=makespan,
70
+ resource_leveling_index=resource_leveling_index(profile),
71
+ projects_active=len(portfolio.active_projects),
72
+ activities_scheduled=len(schedule),
73
+ solve_time_sec=round(solve_time, 3),
74
+ status="feasible",
75
+ )
76
+
77
+ return ScheduleResult(
78
+ algorithm="critical_chain",
79
+ scenario=portfolio.scenario_id,
80
+ uncertainty=portfolio.uncertainty_id,
81
+ disruption=portfolio.disruption_type,
82
+ schedule=schedule,
83
+ metrics=metrics,
84
+ resource_profile=profile,
85
+ selected_projects=[p.project_id for p in portfolio.active_projects],
86
+ suspended_projects=[p.project_id for p in portfolio.projects if not p.selected],
87
+ )
gradio/src/porttower/disruptions.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Disruption modeling for portfolio schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from typing import Any
7
+
8
+ from porttower.generator import generate_portfolio
9
+ from porttower.models import PortfolioInstance
10
+ from porttower.portfolio_selection import suspend_low_priority
11
+
12
+
13
+ def apply_disruption(
14
+ portfolio: PortfolioInstance,
15
+ disruption_type: str,
16
+ params: dict[str, Any] | None = None,
17
+ ) -> PortfolioInstance:
18
+ params = params or {}
19
+ p = deepcopy(portfolio)
20
+ p.disruption_type = disruption_type
21
+ p.disruption_params = params
22
+
23
+ if disruption_type == "none":
24
+ return p
25
+
26
+ if disruption_type == "activity_delay":
27
+ delay_days = int(params.get("delay_days", 10))
28
+ project_idx = int(params.get("project_index", 0))
29
+ activity_idx = int(params.get("activity_index", 0))
30
+ active = p.active_projects
31
+ if active and project_idx < len(active):
32
+ acts = active[project_idx].activities
33
+ if activity_idx < len(acts):
34
+ acts[activity_idx].duration += delay_days
35
+ acts[activity_idx].duration_std += delay_days * 0.2
36
+
37
+ elif disruption_type == "key_person_removal":
38
+ skill = params.get("skill", "specialist")
39
+ res_id = f"res-{skill}"
40
+ for res in p.resources:
41
+ if res.resource_id == res_id:
42
+ res.capacity = max(1.0, res.capacity * float(params.get("capacity_factor", 0.5)))
43
+ for project in p.projects:
44
+ for act in project.activities:
45
+ if act.required_skill == skill and res_id in act.resource_demands:
46
+ act.resource_demands[res_id] *= 1.5
47
+
48
+ elif disruption_type == "budget_reduction":
49
+ factor = float(params.get("budget_factor", 0.75))
50
+ p.budget_cap *= factor
51
+ suspend_count = int(params.get("suspend_count", 1))
52
+ suspend_low_priority(p, suspend_count)
53
+
54
+ elif disruption_type == "new_project":
55
+ extra = generate_portfolio(
56
+ p.scenario_id,
57
+ p.uncertainty_id,
58
+ num_projects=1,
59
+ seed=p.seed + 999,
60
+ )
61
+ if extra.projects:
62
+ new_proj = extra.projects[0]
63
+ new_proj.project_id = f"proj-new-{len(p.projects):03d}"
64
+ new_proj.name = params.get("project_name", f"Emergency: {new_proj.name}")
65
+ new_proj.priority = 5
66
+ p.projects.append(new_proj)
67
+
68
+ return p
69
+
70
+
71
+ def disruption_label(disruption_type: str, params: dict[str, Any] | None = None) -> str:
72
+ params = params or {}
73
+ labels = {
74
+ "none": "No disruption",
75
+ "activity_delay": f"Activity delay +{params.get('delay_days', 10)} days",
76
+ "key_person_removal": f"Key {params.get('skill', 'specialist')} capacity reduced",
77
+ "budget_reduction": f"Budget cut to {int(float(params.get('budget_factor', 0.75)) * 100)}%",
78
+ "new_project": f"New project: {params.get('project_name', 'Emergency scope')}",
79
+ }
80
+ return labels.get(disruption_type, disruption_type)
gradio/src/porttower/engine.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unified portfolio scheduling engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.cashflow import build_cash_flow_profile, cash_flow_risk
6
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
7
+ from porttower.critical_chain import solve_critical_chain
8
+ from porttower.models import PortfolioInstance, ScheduleResult
9
+ from porttower.monte_carlo import run_monte_carlo
10
+ from porttower.nsga2_solver import solve_nsga2_portfolio
11
+ from porttower.portfolio_selection import select_portfolio_greedy
12
+ from porttower.rescheduling import solve_rolling_horizon
13
+
14
+
15
+ class PortfolioEngine:
16
+ """Dispatch scheduling to CP-SAT, Critical Chain, NSGA-II, Monte Carlo, or rolling horizon."""
17
+
18
+ def schedule(
19
+ self,
20
+ portfolio: PortfolioInstance,
21
+ algorithm: str,
22
+ run_portfolio_selection: bool = True,
23
+ ) -> ScheduleResult:
24
+ if run_portfolio_selection:
25
+ select_portfolio_greedy(portfolio)
26
+
27
+ algo = algorithm.lower().replace(" ", "_").replace("-", "_")
28
+ solvers = {
29
+ "cp_sat_rcpsp": lambda: solve_cp_sat_rcpsp(portfolio),
30
+ "cp_sat": lambda: solve_cp_sat_rcpsp(portfolio),
31
+ "critical_chain": lambda: solve_critical_chain(portfolio),
32
+ "nsga2_portfolio": lambda: solve_nsga2_portfolio(portfolio),
33
+ "nsga2": lambda: solve_nsga2_portfolio(portfolio),
34
+ "scenario_monte_carlo": lambda: run_monte_carlo(portfolio),
35
+ "monte_carlo": lambda: run_monte_carlo(portfolio),
36
+ "rolling_horizon": lambda: solve_rolling_horizon(
37
+ portfolio,
38
+ portfolio.disruption_type,
39
+ portfolio.disruption_params,
40
+ ),
41
+ }
42
+
43
+ if algo not in solvers:
44
+ raise ValueError(f"Unknown algorithm: {algorithm}")
45
+
46
+ result = solvers[algo]()
47
+ result.cash_flow_profile = build_cash_flow_profile(portfolio, result.schedule)
48
+ result.metrics.cash_flow_risk = cash_flow_risk(result.cash_flow_profile)
49
+ return result
50
+
51
+ def compare_algorithms(self, portfolio: PortfolioInstance) -> list[ScheduleResult]:
52
+ results = []
53
+ for algo in ["cp_sat_rcpsp", "critical_chain", "nsga2_portfolio", "scenario_monte_carlo"]:
54
+ try:
55
+ p = _clone_portfolio(portfolio)
56
+ results.append(self.schedule(p, algo))
57
+ except Exception:
58
+ continue
59
+ return results
60
+
61
+
62
+ def _clone_portfolio(portfolio: PortfolioInstance) -> PortfolioInstance:
63
+ from copy import deepcopy
64
+ return deepcopy(portfolio)
gradio/src/porttower/generator.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic multi-project portfolio scenario generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Any
7
+
8
+ from porttower.constants import PORTFOLIO_SCENARIOS, RESOURCE_CATEGORIES, UNCERTAINTY_PROFILES
9
+ from porttower.models import Activity, ActivityMode, PortfolioInstance, Project, Resource
10
+
11
+
12
+ def _rng(seed: int) -> random.Random:
13
+ return random.Random(seed)
14
+
15
+
16
+ def _base_resources(rng: random.Random, scenario_id: str) -> list[Resource]:
17
+ caps = {
18
+ "engineering_epc": {"human": 45, "machinery": 12, "budget": 48, "contractor": 8, "equipment": 20, "laboratory": 3, "specialist": 6},
19
+ "infrastructure_mega": {"human": 70, "machinery": 22, "budget": 95, "contractor": 18, "equipment": 30, "laboratory": 4, "specialist": 10},
20
+ "software_development": {"human": 60, "machinery": 4, "budget": 12, "contractor": 15, "equipment": 8, "laboratory": 2, "specialist": 10},
21
+ "pharma_rd": {"human": 35, "machinery": 6, "budget": 85, "contractor": 5, "equipment": 10, "laboratory": 8, "specialist": 12},
22
+ "oil_gas_field": {"human": 55, "machinery": 18, "budget": 120, "contractor": 12, "equipment": 25, "laboratory": 4, "specialist": 8},
23
+ "defense_program": {"human": 50, "machinery": 10, "budget": 72, "contractor": 14, "equipment": 18, "laboratory": 6, "specialist": 15},
24
+ "government_program": {"human": 40, "machinery": 5, "budget": 35, "contractor": 10, "equipment": 6, "laboratory": 2, "specialist": 5},
25
+ }
26
+ base = caps.get(scenario_id, caps["engineering_epc"])
27
+ resources = []
28
+ for cat in RESOURCE_CATEGORIES:
29
+ cap = base.get(cat, 10)
30
+ resources.append(
31
+ Resource(
32
+ resource_id=f"res-{cat}",
33
+ name=cat.replace("_", " ").title(),
34
+ category=cat,
35
+ capacity=float(cap),
36
+ unit_cost=rng.uniform(0.8, 2.5),
37
+ renewable=cat != "budget",
38
+ )
39
+ )
40
+ return resources
41
+
42
+
43
+ def _activity_templates(scenario_id: str) -> list[dict[str, Any]]:
44
+ common = [
45
+ {"name": "Initiation & Planning", "dur": (5, 12), "skills": ["human", "specialist"]},
46
+ {"name": "Design & Engineering", "dur": (10, 25), "skills": ["human", "specialist", "equipment"]},
47
+ {"name": "Procurement", "dur": (8, 20), "skills": ["human", "contractor", "budget"]},
48
+ {"name": "Core Execution", "dur": (15, 40), "skills": ["human", "machinery", "equipment"]},
49
+ {"name": "Quality Assurance", "dur": (5, 15), "skills": ["human", "laboratory", "specialist"]},
50
+ {"name": "Integration & Testing", "dur": (8, 18), "skills": ["human", "equipment", "laboratory"]},
51
+ {"name": "Commissioning", "dur": (6, 14), "skills": ["human", "machinery", "specialist"]},
52
+ {"name": "Close-out", "dur": (3, 8), "skills": ["human", "budget"]},
53
+ ]
54
+ if scenario_id == "software_development":
55
+ return [
56
+ {"name": "Discovery & Requirements", "dur": (5, 10), "skills": ["human", "specialist"]},
57
+ {"name": "Architecture Design", "dur": (8, 15), "skills": ["human", "specialist"]},
58
+ {"name": "Sprint Development", "dur": (20, 45), "skills": ["human", "equipment"]},
59
+ {"name": "QA & Testing", "dur": (8, 18), "skills": ["human", "laboratory"]},
60
+ {"name": "UAT & Release", "dur": (5, 12), "skills": ["human", "specialist"]},
61
+ {"name": "Post-launch Support", "dur": (4, 10), "skills": ["human", "contractor"]},
62
+ ]
63
+ if scenario_id == "pharma_rd":
64
+ return [
65
+ {"name": "Target Identification", "dur": (15, 30), "skills": ["human", "laboratory", "specialist"]},
66
+ {"name": "Preclinical Studies", "dur": (25, 50), "skills": ["human", "laboratory", "equipment"]},
67
+ {"name": "IND Preparation", "dur": (10, 20), "skills": ["human", "specialist", "budget"]},
68
+ {"name": "Phase I Trial", "dur": (30, 60), "skills": ["human", "laboratory", "contractor"]},
69
+ {"name": "Phase II Trial", "dur": (40, 80), "skills": ["human", "laboratory", "equipment"]},
70
+ {"name": "Regulatory Submission", "dur": (8, 15), "skills": ["human", "specialist"]},
71
+ ]
72
+ if scenario_id == "infrastructure_mega":
73
+ return [
74
+ {"name": "Feasibility & Permitting", "dur": (12, 28), "skills": ["human", "specialist", "budget"]},
75
+ {"name": "Detailed Design", "dur": (18, 35), "skills": ["human", "specialist", "equipment"]},
76
+ {"name": "Procurement & Logistics", "dur": (10, 22), "skills": ["human", "contractor", "machinery"]},
77
+ {"name": "Civil Works", "dur": (25, 55), "skills": ["human", "machinery", "equipment"]},
78
+ {"name": "Systems Integration", "dur": (12, 25), "skills": ["human", "specialist", "equipment"]},
79
+ {"name": "Commissioning & Handover", "dur": (8, 18), "skills": ["human", "machinery", "specialist"]},
80
+ ]
81
+ if scenario_id == "defense_program":
82
+ return [
83
+ {"name": "Requirements & Systems Eng.", "dur": (10, 22), "skills": ["human", "specialist"]},
84
+ {"name": "Prototype Development", "dur": (20, 40), "skills": ["human", "equipment", "laboratory"]},
85
+ {"name": "Qualification Testing", "dur": (15, 30), "skills": ["human", "laboratory", "specialist"]},
86
+ {"name": "Production Ramp-up", "dur": (18, 35), "skills": ["human", "machinery", "contractor"]},
87
+ {"name": "Field Deployment", "dur": (8, 16), "skills": ["human", "contractor", "equipment"]},
88
+ {"name": "Sustainment Planning", "dur": (6, 12), "skills": ["human", "specialist", "budget"]},
89
+ ]
90
+ return common
91
+
92
+
93
+ def _project_names(scenario_id: str, n: int, rng: random.Random) -> list[str]:
94
+ pools = {
95
+ "engineering_epc": ["Metro Line Extension", "Hospital Wing", "Bridge Rehabilitation", "Industrial Plant", "Data Center Shell", "Airport Terminal", "Highway Section"],
96
+ "infrastructure_mega": ["High-Speed Rail Segment", "Port Expansion", "Dam Modernization", "Power Grid Upgrade", "Water Treatment Plant", "Tunnel Boring Phase II", "Urban Transit Hub"],
97
+ "software_development": ["CRM Platform", "Mobile Banking App", "IoT Gateway", "Analytics Dashboard", "ERP Module", "API Gateway", "ML Pipeline"],
98
+ "pharma_rd": ["Oncology Candidate A", "Autoimmune Drug B", "Vaccine Platform C", "Rare Disease Therapy D", "Biosimilar E"],
99
+ "oil_gas_field": ["Offshore Platform A", "Pipeline Segment B", "Refinery Upgrade C", "Well Cluster D", "LNG Terminal E", "FPSO Conversion"],
100
+ "defense_program": ["Radar Modernization", "UAV Platform X", "Secure Comms Suite", "Armored Vehicle Upgrade", "Cyber Defense Module", "Satellite Ground Segment"],
101
+ "government_program": ["Digital Services Portal", "Infrastructure Renewal", "Cybersecurity Upgrade", "Education Platform", "Healthcare IT", "Smart City Pilot", "Defense Logistics"],
102
+ }
103
+ pool = pools.get(scenario_id, pools["engineering_epc"])
104
+ rng.shuffle(pool)
105
+ return pool[:n]
106
+
107
+
108
+ def _build_project(
109
+ project_id: str,
110
+ name: str,
111
+ scenario_id: str,
112
+ horizon: int,
113
+ rng: random.Random,
114
+ uncertainty_cv: float,
115
+ ) -> Project:
116
+ templates = _activity_templates(scenario_id)
117
+ n_acts = rng.randint(max(4, len(templates) - 2), len(templates))
118
+ chosen = templates[:n_acts]
119
+ activities: list[Activity] = []
120
+ prev_id: str | None = None
121
+ total_cost = 0.0
122
+
123
+ for i, tmpl in enumerate(chosen):
124
+ act_id = f"{project_id}-a{i:02d}"
125
+ dur = rng.randint(*tmpl["dur"])
126
+ std = max(1.0, dur * uncertainty_cv)
127
+ demands = {}
128
+ for skill in tmpl["skills"]:
129
+ demands[f"res-{skill}"] = round(rng.uniform(0.3, 2.5), 2)
130
+ cost = round(dur * rng.uniform(0.8, 2.2), 2)
131
+ total_cost += cost
132
+ modes = [
133
+ ActivityMode("fast", max(3, dur - 4), {k: v * 1.3 for k, v in demands.items()}, cost * 1.25),
134
+ ActivityMode("default", dur, demands, cost),
135
+ ActivityMode("lean", dur + 3, {k: v * 0.7 for k, v in demands.items()}, cost * 0.85),
136
+ ]
137
+ activities.append(
138
+ Activity(
139
+ activity_id=act_id,
140
+ project_id=project_id,
141
+ name=tmpl["name"],
142
+ duration=dur,
143
+ duration_std=std,
144
+ predecessors=[prev_id] if prev_id else [],
145
+ resource_demands=demands,
146
+ cost=cost,
147
+ modes=modes,
148
+ required_skill=tmpl["skills"][0],
149
+ )
150
+ )
151
+ prev_id = act_id
152
+
153
+ npv = round(rng.uniform(2.5, 15.0) * (total_cost / 10), 2)
154
+ deadline = rng.randint(int(horizon * 0.55), int(horizon * 0.95))
155
+ cash_flows = []
156
+ for t in range(0, horizon, rng.randint(8, 15)):
157
+ cash_flows.append((t, round(-rng.uniform(0.5, 3.0), 2)))
158
+ cash_flows.append((horizon, round(npv, 2)))
159
+
160
+ return Project(
161
+ project_id=project_id,
162
+ name=name,
163
+ npv=npv,
164
+ priority=rng.randint(1, 5),
165
+ deadline=deadline,
166
+ activities=activities,
167
+ cash_flows=cash_flows,
168
+ selected=True,
169
+ suspendable=rng.random() > 0.25,
170
+ )
171
+
172
+
173
+ def generate_portfolio(
174
+ scenario_id: str,
175
+ uncertainty_id: str = "baseline",
176
+ num_projects: int | None = None,
177
+ seed: int = 42,
178
+ ) -> PortfolioInstance:
179
+ meta = PORTFOLIO_SCENARIOS[scenario_id]
180
+ unc = UNCERTAINTY_PROFILES[uncertainty_id]
181
+ rng = _rng(seed)
182
+ n = num_projects or meta["default_projects"]
183
+ horizon = meta["default_horizon"]
184
+ budget = meta["budget_musd"]
185
+ resources = _base_resources(rng, scenario_id)
186
+ names = _project_names(scenario_id, n, rng)
187
+ projects = [
188
+ _build_project(f"proj-{i:03d}", names[i], scenario_id, horizon, rng, unc["duration_cv"])
189
+ for i in range(n)
190
+ ]
191
+ return PortfolioInstance(
192
+ scenario_id=scenario_id,
193
+ scenario_label=meta["label"],
194
+ uncertainty_id=uncertainty_id,
195
+ uncertainty_label=unc["label"],
196
+ horizon=horizon,
197
+ budget_cap=budget,
198
+ resources=resources,
199
+ projects=projects,
200
+ seed=seed,
201
+ )
202
+
203
+
204
+ def projects_to_table_rows(portfolio: PortfolioInstance) -> list[dict[str, Any]]:
205
+ rows = []
206
+ for p in portfolio.projects:
207
+ total_dur = sum(a.duration for a in p.activities)
208
+ rows.append({
209
+ "Project": p.name,
210
+ "NPV (M$)": f"{p.npv:.1f}",
211
+ "Priority": p.priority,
212
+ "Deadline": p.deadline,
213
+ "Activities": len(p.activities),
214
+ "Duration": total_dur,
215
+ "Selected": "Yes" if p.selected else "No",
216
+ "Suspendable": "Yes" if p.suspendable else "No",
217
+ })
218
+ return rows
219
+
220
+
221
+ def resources_to_table_rows(portfolio: PortfolioInstance) -> list[dict[str, Any]]:
222
+ return [
223
+ {
224
+ "Resource": r.name,
225
+ "Category": r.category,
226
+ "Capacity": f"{r.capacity:.1f}",
227
+ "Unit Cost": f"${r.unit_cost:.2f}",
228
+ "Renewable": "Yes" if r.renewable else "No",
229
+ }
230
+ for r in portfolio.resources
231
+ ]
gradio/src/porttower/models.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data models for multi-project portfolio scheduling."""
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 Resource:
11
+ resource_id: str
12
+ name: str
13
+ category: str
14
+ capacity: float
15
+ unit_cost: float = 1.0
16
+ renewable: bool = True
17
+
18
+ def to_dict(self) -> dict[str, Any]:
19
+ return asdict(self)
20
+
21
+
22
+ @dataclass
23
+ class ActivityMode:
24
+ mode_id: str
25
+ duration: int
26
+ resource_demands: dict[str, float]
27
+ cost: float
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return asdict(self)
31
+
32
+
33
+ @dataclass
34
+ class Activity:
35
+ activity_id: str
36
+ project_id: str
37
+ name: str
38
+ duration: int
39
+ duration_std: float
40
+ predecessors: list[str] = field(default_factory=list)
41
+ resource_demands: dict[str, float] = field(default_factory=dict)
42
+ cost: float = 0.0
43
+ modes: list[ActivityMode] = field(default_factory=list)
44
+ required_skill: str = ""
45
+ is_critical: bool = False
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ d = asdict(self)
49
+ d["modes"] = [m.to_dict() for m in self.modes]
50
+ return d
51
+
52
+
53
+ @dataclass
54
+ class Project:
55
+ project_id: str
56
+ name: str
57
+ npv: float
58
+ priority: int
59
+ deadline: int
60
+ activities: list[Activity] = field(default_factory=list)
61
+ cash_flows: list[tuple[int, float]] = field(default_factory=list)
62
+ selected: bool = True
63
+ suspendable: bool = True
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return {
67
+ "project_id": self.project_id,
68
+ "name": self.name,
69
+ "npv": self.npv,
70
+ "priority": self.priority,
71
+ "deadline": self.deadline,
72
+ "activities": [a.to_dict() for a in self.activities],
73
+ "cash_flows": self.cash_flows,
74
+ "selected": self.selected,
75
+ "suspendable": self.suspendable,
76
+ }
77
+
78
+
79
+ @dataclass
80
+ class ActivitySchedule:
81
+ activity_id: str
82
+ project_id: str
83
+ project_name: str
84
+ activity_name: str
85
+ start: int
86
+ end: int
87
+ duration: int
88
+ mode_id: str = "default"
89
+ resource_assignments: dict[str, float] = field(default_factory=dict)
90
+ is_critical: bool = False
91
+ delayed: bool = False
92
+ rationale: str = ""
93
+
94
+ def to_dict(self) -> dict[str, Any]:
95
+ return asdict(self)
96
+
97
+
98
+ @dataclass
99
+ class PortfolioMetrics:
100
+ portfolio_npv: float = 0.0
101
+ total_tardiness: int = 0
102
+ max_tardiness: int = 0
103
+ makespan: int = 0
104
+ resource_peak_variance: float = 0.0
105
+ resource_leveling_index: float = 0.0
106
+ cash_flow_risk: float = 0.0
107
+ reschedule_cost: float = 0.0
108
+ projects_active: int = 0
109
+ projects_suspended: int = 0
110
+ activities_scheduled: int = 0
111
+ budget_utilization_pct: float = 0.0
112
+ on_time_delivery_pct: float = 0.0
113
+ solve_time_sec: float = 0.0
114
+ status: str = "unknown"
115
+ monte_carlo_p50_makespan: float = 0.0
116
+ monte_carlo_p90_makespan: float = 0.0
117
+ monte_carlo_p50_cost: float = 0.0
118
+ monte_carlo_p90_cost: float = 0.0
119
+ on_time_probability_pct: float = 0.0
120
+ completion_by_deadline_prob_pct: float = 0.0
121
+ liquidity_shortfall_prob_pct: float = 0.0
122
+ milestone_delay_prob_pct: float = 0.0
123
+ strategic_value_score: float = 0.0
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ return asdict(self)
127
+
128
+
129
+ @dataclass
130
+ class ScheduleResult:
131
+ algorithm: str
132
+ scenario: str
133
+ uncertainty: str
134
+ disruption: str
135
+ schedule: list[ActivitySchedule] = field(default_factory=list)
136
+ metrics: PortfolioMetrics = field(default_factory=PortfolioMetrics)
137
+ resource_profile: dict[str, list[float]] = field(default_factory=dict)
138
+ cash_flow_profile: list[float] = field(default_factory=list)
139
+ selected_projects: list[str] = field(default_factory=list)
140
+ suspended_projects: list[str] = field(default_factory=list)
141
+ simulation_runs: list[dict[str, Any]] = field(default_factory=list)
142
+ pareto_front: list[dict[str, Any]] = field(default_factory=list)
143
+ risk_contributors: list[dict[str, Any]] = field(default_factory=list)
144
+
145
+ def to_dict(self) -> dict[str, Any]:
146
+ return {
147
+ "algorithm": self.algorithm,
148
+ "scenario": self.scenario,
149
+ "uncertainty": self.uncertainty,
150
+ "disruption": self.disruption,
151
+ "schedule": [s.to_dict() for s in self.schedule],
152
+ "metrics": self.metrics.to_dict(),
153
+ "resource_profile": self.resource_profile,
154
+ "cash_flow_profile": self.cash_flow_profile,
155
+ "selected_projects": self.selected_projects,
156
+ "suspended_projects": self.suspended_projects,
157
+ "simulation_runs": self.simulation_runs,
158
+ "pareto_front": self.pareto_front,
159
+ "risk_contributors": self.risk_contributors,
160
+ }
161
+
162
+
163
+ @dataclass
164
+ class PortfolioInstance:
165
+ scenario_id: str
166
+ scenario_label: str
167
+ uncertainty_id: str
168
+ uncertainty_label: str
169
+ horizon: int
170
+ budget_cap: float
171
+ resources: list[Resource]
172
+ projects: list[Project]
173
+ disruption_type: str = "none"
174
+ disruption_params: dict[str, Any] = field(default_factory=dict)
175
+ seed: int = 42
176
+
177
+ def to_dict(self) -> dict[str, Any]:
178
+ return {
179
+ "scenario_id": self.scenario_id,
180
+ "scenario_label": self.scenario_label,
181
+ "uncertainty_id": self.uncertainty_id,
182
+ "uncertainty_label": self.uncertainty_label,
183
+ "horizon": self.horizon,
184
+ "budget_cap": self.budget_cap,
185
+ "resources": [r.to_dict() for r in self.resources],
186
+ "projects": [p.to_dict() for p in self.projects],
187
+ "disruption_type": self.disruption_type,
188
+ "disruption_params": self.disruption_params,
189
+ "seed": self.seed,
190
+ }
191
+
192
+ @property
193
+ def active_projects(self) -> list[Project]:
194
+ return [p for p in self.projects if p.selected]
gradio/src/porttower/monte_carlo.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Monte Carlo simulation with risk contribution and liquidity analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ import statistics
7
+ import time
8
+ from copy import deepcopy
9
+
10
+ from porttower.critical_chain import solve_critical_chain
11
+ from porttower.models import PortfolioInstance, ScheduleResult
12
+
13
+
14
+ def _perturb_portfolio(
15
+ portfolio: PortfolioInstance,
16
+ rng: random.Random,
17
+ unc: dict,
18
+ ) -> PortfolioInstance:
19
+ p = deepcopy(portfolio)
20
+ rework_prob = unc.get("rework_prob", 0.05)
21
+ permit_prob = unc.get("permit_delay_prob", 0.06)
22
+ shortage_prob = unc.get("resource_shortage_prob", 0.05)
23
+ cost_cv = unc.get("cost_cv", 0.10)
24
+
25
+ for project in p.active_projects:
26
+ for act in project.activities:
27
+ noise = rng.gauss(0, act.duration_std)
28
+ act.duration = max(1, int(act.duration + noise))
29
+ if rng.random() < rework_prob:
30
+ act.duration = int(act.duration * rng.uniform(1.1, 1.35))
31
+ act.cost *= rng.uniform(1.05, 1.20)
32
+ if rng.random() < permit_prob:
33
+ act.duration += rng.randint(3, 12)
34
+ if rng.random() < shortage_prob:
35
+ act.duration += rng.randint(2, 8)
36
+ act.cost *= max(0.85, 1.0 + rng.gauss(0, cost_cv))
37
+ return p
38
+
39
+
40
+ def _percentile(values: list[float], q: float) -> float:
41
+ if not values:
42
+ return 0.0
43
+ idx = min(len(values) - 1, int(q * len(values)))
44
+ return values[idx]
45
+
46
+
47
+ def _liquidity_shortfall_prob(cash_profiles: list[list[float]], floor: float = -5.0) -> float:
48
+ if not cash_profiles:
49
+ return 0.0
50
+ shortfalls = sum(1 for cp in cash_profiles if min(cp) < floor)
51
+ return round(100.0 * shortfalls / len(cash_profiles), 1)
52
+
53
+
54
+ def _milestone_delay_prob(results: list[ScheduleResult], portfolio: PortfolioInstance) -> float:
55
+ if not results:
56
+ return 0.0
57
+ delayed = 0
58
+ for result in results:
59
+ proj_ends: dict[str, int] = {}
60
+ for s in result.schedule:
61
+ proj_ends[s.project_id] = max(proj_ends.get(s.project_id, 0), s.end)
62
+ for proj in portfolio.active_projects:
63
+ finish = proj_ends.get(proj.project_id, portfolio.horizon)
64
+ if finish > proj.deadline:
65
+ delayed += 1
66
+ total = len(results) * max(len(portfolio.active_projects), 1)
67
+ return round(100.0 * delayed / total, 1)
68
+
69
+
70
+ def _risk_contribution(
71
+ portfolio: PortfolioInstance,
72
+ base_result: ScheduleResult,
73
+ perturbed_results: list[ScheduleResult],
74
+ ) -> list[dict]:
75
+ act_map = {s.activity_id: s for s in base_result.schedule}
76
+ deltas: dict[str, list[int]] = {}
77
+ for result in perturbed_results:
78
+ for s in result.schedule:
79
+ base = act_map.get(s.activity_id)
80
+ if not base:
81
+ continue
82
+ deltas.setdefault(s.activity_id, []).append(s.end - base.end)
83
+ rows = []
84
+ for act_id, vals in deltas.items():
85
+ base = act_map[act_id]
86
+ rows.append({
87
+ "activity_id": act_id,
88
+ "activity": base.activity_name,
89
+ "project": base.project_name,
90
+ "mean_delay": round(statistics.mean(vals), 2),
91
+ "p90_delay": round(_percentile(sorted(vals), 0.9), 2),
92
+ "risk_score": round(statistics.mean(abs(v) for v in vals), 2),
93
+ })
94
+ rows.sort(key=lambda r: r["risk_score"], reverse=True)
95
+ return rows[:10]
96
+
97
+
98
+ def run_monte_carlo(
99
+ portfolio: PortfolioInstance,
100
+ n_runs: int = 60,
101
+ seed: int | None = None,
102
+ ) -> ScheduleResult:
103
+ from porttower.constants import UNCERTAINTY_PROFILES
104
+
105
+ t0 = time.perf_counter()
106
+ rng = random.Random(seed if seed is not None else portfolio.seed)
107
+ unc = UNCERTAINTY_PROFILES.get(portfolio.uncertainty_id, UNCERTAINTY_PROFILES["baseline"])
108
+
109
+ base = solve_critical_chain(portfolio)
110
+ makespans: list[int] = []
111
+ costs: list[float] = []
112
+ tardiness: list[int] = []
113
+ on_time_flags: list[bool] = []
114
+ cash_profiles: list[list[float]] = []
115
+ perturbed_results: list[ScheduleResult] = []
116
+ runs: list[dict] = []
117
+
118
+ for i in range(n_runs):
119
+ perturbed = _perturb_portfolio(portfolio, rng, unc)
120
+ result = solve_critical_chain(perturbed)
121
+ m = result.metrics
122
+ makespans.append(m.makespan)
123
+ total_cost = sum(a.cost for p in perturbed.active_projects for a in p.activities)
124
+ costs.append(total_cost)
125
+ tardiness.append(m.total_tardiness)
126
+ on_time_flags.append(m.on_time_delivery_pct >= 80.0)
127
+ cash_profiles.append(result.cash_flow_profile or [0.0])
128
+ if i < 8:
129
+ perturbed_results.append(result)
130
+ runs.append({
131
+ "run": i + 1,
132
+ "makespan": m.makespan,
133
+ "cost_musd": round(total_cost, 2),
134
+ "tardiness": m.total_tardiness,
135
+ "on_time_pct": m.on_time_delivery_pct,
136
+ })
137
+
138
+ makespans.sort()
139
+ costs.sort()
140
+ horizon = portfolio.horizon
141
+ on_time_prob = round(100.0 * sum(on_time_flags) / max(len(on_time_flags), 1), 1)
142
+
143
+ base.metrics.monte_carlo_p50_makespan = _percentile([float(x) for x in makespans], 0.5)
144
+ base.metrics.monte_carlo_p90_makespan = _percentile([float(x) for x in makespans], 0.9)
145
+ base.metrics.monte_carlo_p50_cost = _percentile(costs, 0.5)
146
+ base.metrics.monte_carlo_p90_cost = _percentile(costs, 0.9)
147
+ base.metrics.monte_carlo_p90_makespan = base.metrics.monte_carlo_p90_makespan
148
+ base.metrics.on_time_probability_pct = on_time_prob
149
+ base.metrics.liquidity_shortfall_prob_pct = _liquidity_shortfall_prob(cash_profiles)
150
+ base.metrics.milestone_delay_prob_pct = _milestone_delay_prob(perturbed_results, portfolio)
151
+ base.metrics.cash_flow_risk = round(statistics.mean(tardiness), 1) if tardiness else 0.0
152
+ base.metrics.completion_by_deadline_prob_pct = round(
153
+ 100.0 * sum(1 for m in makespans if m <= horizon) / max(len(makespans), 1), 1
154
+ )
155
+
156
+ base.algorithm = "scenario_monte_carlo"
157
+ base.simulation_runs = runs
158
+ base.risk_contributors = _risk_contribution(portfolio, base, perturbed_results)
159
+ base.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
160
+ base.metrics.status = "feasible"
161
+ return base
gradio/src/porttower/network.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Activity network utilities using NetworkX."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import networkx as nx
6
+
7
+ from porttower.models import Activity, PortfolioInstance, Project
8
+
9
+
10
+ def build_project_dag(project: Project) -> nx.DiGraph:
11
+ g = nx.DiGraph()
12
+ for act in project.activities:
13
+ g.add_node(
14
+ act.activity_id,
15
+ name=act.name,
16
+ duration=act.duration,
17
+ project_id=project.project_id,
18
+ cost=act.cost,
19
+ )
20
+ for pred in act.predecessors:
21
+ g.add_edge(pred, act.activity_id)
22
+ if not nx.is_directed_acyclic_graph(g) and g.nodes:
23
+ return nx.DiGraph(nx.topological_sort(g))
24
+ return g
25
+
26
+
27
+ def build_portfolio_graph(portfolio: PortfolioInstance) -> nx.DiGraph:
28
+ g = nx.DiGraph()
29
+ for project in portfolio.active_projects:
30
+ pg = build_project_dag(project)
31
+ g = nx.compose(g, pg)
32
+ g.nodes[project.project_id] = {"type": "project", "name": project.name}
33
+ roots = [n for n in pg.nodes if pg.in_degree(n) == 0]
34
+ for root in roots:
35
+ g.add_edge(project.project_id, root)
36
+ return g
37
+
38
+
39
+ def critical_path_length(project: Project) -> tuple[int, list[str]]:
40
+ g = build_project_dag(project)
41
+ if not g.nodes:
42
+ return 0, []
43
+ try:
44
+ order = list(nx.topological_sort(g))
45
+ except nx.NetworkXError:
46
+ return 0, []
47
+ dist: dict[str, int] = {n: 0 for n in g.nodes}
48
+ pred: dict[str, str | None] = {n: None for n in g.nodes}
49
+ for node in order:
50
+ dur = g.nodes[node].get("duration", 0)
51
+ for succ in g.successors(node):
52
+ nd = dist[node] + dur
53
+ if nd > dist[succ]:
54
+ dist[succ] = nd
55
+ pred[succ] = node
56
+ if not dist:
57
+ return 0, []
58
+ end = max(dist, key=dist.get)
59
+ total = dist[end] + g.nodes[end].get("duration", 0)
60
+ path = []
61
+ cur: str | None = end
62
+ while cur is not None:
63
+ path.append(cur)
64
+ cur = pred[cur]
65
+ path.reverse()
66
+ return total, path
67
+
68
+
69
+ def mark_critical_activities(portfolio: PortfolioInstance) -> None:
70
+ for project in portfolio.projects:
71
+ _, cp = critical_path_length(project)
72
+ cp_set = set(cp)
73
+ for act in project.activities:
74
+ act.is_critical = act.activity_id in cp_set
gradio/src/porttower/nsga2_solver.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NSGA-II multi-objective portfolio optimization via pymoo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import itertools
6
+ import time
7
+ from copy import deepcopy
8
+
9
+ from porttower.critical_chain import solve_critical_chain
10
+ from porttower.models import PortfolioInstance, ScheduleResult
11
+ from porttower.portfolio_selection import portfolio_selection_score
12
+
13
+
14
+ def _evaluate_selection(portfolio: PortfolioInstance, mask: tuple[int, ...]) -> dict:
15
+ p = deepcopy(portfolio)
16
+ total_cost = 0.0
17
+ for i, proj in enumerate(p.projects):
18
+ proj.selected = bool(mask[i])
19
+ if proj.selected:
20
+ total_cost += sum(a.cost for a in proj.activities)
21
+ result = solve_critical_chain(p)
22
+ npv = sum(pr.npv for pr in p.active_projects)
23
+ return {
24
+ "mask": mask,
25
+ "npv": npv,
26
+ "tardiness": result.metrics.total_tardiness,
27
+ "leveling": result.metrics.resource_leveling_index,
28
+ "cost": total_cost,
29
+ "feasible": total_cost <= portfolio.budget_cap,
30
+ "result": result,
31
+ }
32
+
33
+
34
+ def _is_dominated(a: dict, b: dict) -> bool:
35
+ """True if a is dominated by b (minimize tardiness & leveling, maximize npv)."""
36
+ return (
37
+ b["npv"] >= a["npv"]
38
+ and b["tardiness"] <= a["tardiness"]
39
+ and b["leveling"] <= a["leveling"]
40
+ and (
41
+ b["npv"] > a["npv"]
42
+ or b["tardiness"] < a["tardiness"]
43
+ or b["leveling"] < a["leveling"]
44
+ )
45
+ )
46
+
47
+
48
+ def solve_nsga2_portfolio(portfolio: PortfolioInstance, pop_size: int = 20, n_gen: int = 15) -> ScheduleResult:
49
+ t0 = time.perf_counter()
50
+ n = min(len(portfolio.projects), 6)
51
+ all_masks = list(itertools.product([0, 1], repeat=n))
52
+ evaluations = []
53
+ for mask in all_masks:
54
+ full_mask = mask + tuple(1 for _ in range(len(portfolio.projects) - n))
55
+ evaluations.append(_evaluate_selection(portfolio, full_mask))
56
+ feasible = [e for e in evaluations if e["feasible"] and any(e["mask"])]
57
+
58
+ if not feasible:
59
+ result = solve_critical_chain(portfolio)
60
+ result.algorithm = "nsga2_portfolio"
61
+ result.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
62
+ return result
63
+
64
+ pareto = []
65
+ for e in feasible:
66
+ if not any(_is_dominated(e, other) for other in feasible if other is not e):
67
+ pareto.append({
68
+ "solution": len(pareto) + 1,
69
+ "npv": round(e["npv"], 2),
70
+ "tardiness": round(e["tardiness"], 1),
71
+ "leveling_index": round(e["leveling"], 4),
72
+ "projects_selected": sum(e["mask"]),
73
+ })
74
+
75
+ pareto.sort(key=lambda x: (-x["npv"], x["tardiness"]))
76
+ best = max(
77
+ feasible,
78
+ key=lambda e: (
79
+ sum(portfolio_selection_score(portfolio.projects[i]) for i in range(len(e["mask"])) if e["mask"][i]),
80
+ e["npv"],
81
+ ),
82
+ )
83
+
84
+ p = deepcopy(portfolio)
85
+ for i, proj in enumerate(p.projects):
86
+ proj.selected = bool(best["mask"][i])
87
+
88
+ result = solve_critical_chain(p)
89
+ result.algorithm = "nsga2_portfolio"
90
+ result.pareto_front = pareto[:pop_size]
91
+ result.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
92
+ result.selected_projects = [pr.project_id for pr in p.active_projects]
93
+ result.suspended_projects = [pr.project_id for pr in p.projects if not pr.selected]
94
+ return result
gradio/src/porttower/pipeline.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Demo pipeline for Hugging Face Space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from copy import deepcopy
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from porttower.constants import (
11
+ ALGORITHM_LABELS,
12
+ ALGORITHMS,
13
+ DISRUPTION_TYPES,
14
+ PORTFOLIO_SCENARIOS,
15
+ UNCERTAINTY_PROFILES,
16
+ WHAT_IF_RESOURCES,
17
+ )
18
+ from porttower.engine import PortfolioEngine
19
+ from porttower.generator import generate_portfolio
20
+ from porttower.models import PortfolioInstance, ScheduleResult
21
+ from porttower.rescheduling import compare_schedules
22
+ from porttower.what_if import WhatIfResult, run_what_if
23
+
24
+
25
+ class PortfolioPipeline:
26
+ """Load benchmarks and serve interactive portfolio scheduling."""
27
+
28
+ def __init__(self, assets_dir: Path | None = None):
29
+ self.assets_dir = Path(assets_dir) if assets_dir else None
30
+ self.engine = PortfolioEngine()
31
+ self.summary: dict[str, Any] = {}
32
+ self.benchmarks: list[dict[str, Any]] = []
33
+
34
+ def load(self) -> None:
35
+ if self.assets_dir:
36
+ summary_path = self.assets_dir / "demo" / "summary.json"
37
+ bench_path = self.assets_dir / "demo" / "benchmarks.json"
38
+ if summary_path.exists():
39
+ self.summary = json.loads(summary_path.read_text(encoding="utf-8"))
40
+ if bench_path.exists():
41
+ self.benchmarks = json.loads(bench_path.read_text(encoding="utf-8"))
42
+
43
+ def get_scenario_ids(self) -> list[str]:
44
+ return list(PORTFOLIO_SCENARIOS.keys())
45
+
46
+ def get_uncertainty_ids(self) -> list[str]:
47
+ return list(UNCERTAINTY_PROFILES.keys())
48
+
49
+ def get_algorithm_ids(self) -> list[str]:
50
+ return ALGORITHMS
51
+
52
+ def get_disruption_ids(self) -> list[str]:
53
+ return list(DISRUPTION_TYPES.keys())
54
+
55
+ def get_scenario_label(self, scenario_id: str) -> str:
56
+ return PORTFOLIO_SCENARIOS.get(scenario_id, {}).get("label", scenario_id)
57
+
58
+ def get_uncertainty_label(self, unc_id: str) -> str:
59
+ return UNCERTAINTY_PROFILES.get(unc_id, {}).get("label", unc_id)
60
+
61
+ def get_algorithm_label(self, algo_id: str) -> str:
62
+ return ALGORITHM_LABELS.get(algo_id, algo_id)
63
+
64
+ def get_disruption_label(self, dis_id: str) -> str:
65
+ return DISRUPTION_TYPES.get(dis_id, dis_id)
66
+
67
+ def get_what_if_ids(self) -> list[str]:
68
+ return list(WHAT_IF_RESOURCES.keys())
69
+
70
+ def get_what_if_label(self, key: str) -> str:
71
+ return WHAT_IF_RESOURCES.get(key, {}).get("label", key)
72
+
73
+ def build_portfolio(
74
+ self,
75
+ scenario_id: str,
76
+ uncertainty_id: str = "baseline",
77
+ num_projects: int | None = None,
78
+ seed: int = 42,
79
+ ) -> PortfolioInstance:
80
+ meta = PORTFOLIO_SCENARIOS[scenario_id]
81
+ return generate_portfolio(
82
+ scenario_id=scenario_id,
83
+ uncertainty_id=uncertainty_id,
84
+ num_projects=num_projects or meta["default_projects"],
85
+ seed=seed,
86
+ )
87
+
88
+ def run_schedule(
89
+ self,
90
+ portfolio: PortfolioInstance,
91
+ algorithm: str,
92
+ ) -> ScheduleResult:
93
+ p = deepcopy(portfolio)
94
+ return self.engine.schedule(p, algorithm)
95
+
96
+ def run_disruption_compare(
97
+ self,
98
+ portfolio: PortfolioInstance,
99
+ algorithm: str,
100
+ disruption_type: str,
101
+ disruption_params: dict | None = None,
102
+ ) -> tuple[ScheduleResult, ScheduleResult]:
103
+ p = deepcopy(portfolio)
104
+ return compare_schedules(p, algorithm, disruption_type, disruption_params)
105
+
106
+ def run_algorithm_comparison(self, portfolio: PortfolioInstance) -> list[ScheduleResult]:
107
+ p = deepcopy(portfolio)
108
+ return self.engine.compare_algorithms(p)
109
+
110
+ def run_what_if(
111
+ self,
112
+ portfolio: PortfolioInstance,
113
+ resource_key: str = "engineering_team",
114
+ algorithm: str = "critical_chain",
115
+ ) -> WhatIfResult:
116
+ p = deepcopy(portfolio)
117
+ return run_what_if(p, resource_key, algorithm)
gradio/src/porttower/portfolio_selection.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Project portfolio selection under budget and resource caps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from porttower.models import PortfolioInstance, Project
6
+
7
+
8
+ def select_portfolio_greedy(portfolio: PortfolioInstance, budget_factor: float = 1.0) -> list[str]:
9
+ """Select projects by NPV/priority score within budget."""
10
+ budget = portfolio.budget_cap * budget_factor
11
+ scored = sorted(
12
+ portfolio.projects,
13
+ key=lambda p: (p.npv / max(p.priority, 1), p.priority),
14
+ reverse=True,
15
+ )
16
+ selected: list[str] = []
17
+ spent = 0.0
18
+ for p in scored:
19
+ cost = sum(a.cost for a in p.activities)
20
+ if spent + cost <= budget:
21
+ p.selected = True
22
+ selected.append(p.project_id)
23
+ spent += cost
24
+ else:
25
+ p.selected = False
26
+ return selected
27
+
28
+
29
+ def suspend_low_priority(portfolio: PortfolioInstance, count: int = 1) -> list[str]:
30
+ """Suspend lowest-priority suspendable projects."""
31
+ suspendable = sorted(
32
+ [p for p in portfolio.projects if p.selected and p.suspendable],
33
+ key=lambda p: (p.priority, p.npv),
34
+ )
35
+ suspended = []
36
+ for p in suspendable[:count]:
37
+ p.selected = False
38
+ suspended.append(p.project_id)
39
+ return suspended
40
+
41
+
42
+ def portfolio_selection_score(project: Project) -> float:
43
+ total_cost = sum(a.cost for a in project.activities) or 1.0
44
+ return project.npv / total_cost / max(project.priority, 1)
gradio/src/porttower/rescheduling.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rolling horizon rescheduling after disruptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from copy import deepcopy
7
+
8
+ from porttower.cpsat_solver import solve_cp_sat_rcpsp
9
+ from porttower.disruptions import apply_disruption
10
+ from porttower.models import ActivitySchedule, PortfolioInstance, ScheduleResult
11
+
12
+
13
+ def compute_reschedule_cost(
14
+ baseline: list[ActivitySchedule],
15
+ revised: list[ActivitySchedule],
16
+ ) -> float:
17
+ base_map = {s.activity_id: s for s in baseline}
18
+ cost = 0.0
19
+ for s in revised:
20
+ b = base_map.get(s.activity_id)
21
+ if b is None:
22
+ cost += 5.0
23
+ continue
24
+ shift = abs(s.start - b.start)
25
+ if shift > 0:
26
+ cost += shift * 0.5
27
+ if s.end - s.start != b.end - b.start:
28
+ cost += 2.0
29
+ return round(cost, 2)
30
+
31
+
32
+ def solve_rolling_horizon(
33
+ portfolio: PortfolioInstance,
34
+ disruption_type: str = "none",
35
+ disruption_params: dict | None = None,
36
+ baseline_result: ScheduleResult | None = None,
37
+ ) -> ScheduleResult:
38
+ t0 = time.perf_counter()
39
+ disrupted = apply_disruption(portfolio, disruption_type, disruption_params)
40
+ revised = solve_cp_sat_rcpsp(disrupted)
41
+ revised.algorithm = "rolling_horizon"
42
+
43
+ if baseline_result and baseline_result.schedule:
44
+ revised.metrics.reschedule_cost = compute_reschedule_cost(
45
+ baseline_result.schedule, revised.schedule
46
+ )
47
+
48
+ revised.metrics.solve_time_sec = round(time.perf_counter() - t0, 3)
49
+ return revised
50
+
51
+
52
+ def compare_schedules(
53
+ portfolio: PortfolioInstance,
54
+ algorithm: str,
55
+ disruption_type: str,
56
+ disruption_params: dict | None = None,
57
+ ) -> tuple[ScheduleResult, ScheduleResult]:
58
+ """Return (baseline, revised) schedule pair."""
59
+ from porttower.engine import PortfolioEngine
60
+
61
+ engine = PortfolioEngine()
62
+ baseline_portfolio = deepcopy(portfolio)
63
+ baseline_portfolio.disruption_type = "none"
64
+ baseline = engine.schedule(baseline_portfolio, algorithm)
65
+
66
+ revised_portfolio = deepcopy(portfolio)
67
+ revised = solve_rolling_horizon(
68
+ revised_portfolio,
69
+ disruption_type,
70
+ disruption_params,
71
+ baseline_result=baseline,
72
+ )
73
+ return baseline, revised
gradio/src/porttower/resource_leveling.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resource profile and leveling metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+
7
+ from porttower.models import ActivitySchedule, PortfolioInstance
8
+
9
+
10
+ def build_resource_profile(
11
+ portfolio: PortfolioInstance,
12
+ schedule: list[ActivitySchedule],
13
+ ) -> dict[str, list[float]]:
14
+ horizon = max((s.end for s in schedule), default=portfolio.horizon) + 1
15
+ profile: dict[str, list[float]] = {}
16
+ for res in portfolio.resources:
17
+ if not res.renewable:
18
+ continue
19
+ profile[res.resource_id] = [0.0] * horizon
20
+
21
+ act_map = {a.activity_id: a for p in portfolio.projects for a in p.activities}
22
+ for s in schedule:
23
+ act = act_map.get(s.activity_id)
24
+ if not act:
25
+ continue
26
+ for res_id, demand in act.resource_demands.items():
27
+ if res_id not in profile:
28
+ profile[res_id] = [0.0] * horizon
29
+ for t in range(s.start, min(s.end, horizon)):
30
+ profile[res_id][t] += demand
31
+
32
+ return profile
33
+
34
+
35
+ def resource_leveling_index(profile: dict[str, list[float]]) -> float:
36
+ """Lower is better — coefficient of variation across time periods."""
37
+ if not profile:
38
+ return 0.0
39
+ all_usage = []
40
+ for usage in profile.values():
41
+ if usage:
42
+ all_usage.extend(usage)
43
+ if not all_usage:
44
+ return 0.0
45
+ mean = statistics.mean(all_usage)
46
+ if mean < 1e-6:
47
+ return 0.0
48
+ return round(statistics.stdev(all_usage) / mean, 4)
49
+
50
+
51
+ def peak_utilization(profile: dict[str, list[float]], capacities: dict[str, float]) -> float:
52
+ peaks = []
53
+ for res_id, usage in profile.items():
54
+ cap = capacities.get(res_id, 1.0)
55
+ if usage and cap > 0:
56
+ peaks.append(max(usage) / cap)
57
+ return round(max(peaks) * 100, 1) if peaks else 0.0