bleecam 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bleecam/__init__.py +0 -0
- bleecam/cases/__init__.py +0 -0
- bleecam/cases/gallium/__init__.py +0 -0
- bleecam/cases/gallium/config_ga.py +122 -0
- bleecam/cases/gallium/constraints_ga.py +163 -0
- bleecam/cases/gallium/data/gallium/EF_Template.csv +313 -0
- bleecam/cases/gallium/data/gallium/EF_Template_Ga_from_LiAISON.csv +261 -0
- bleecam/cases/gallium/data/gallium/EF_Template_Gallium_scope_A_material_supply.csv +313 -0
- bleecam/cases/gallium/data/gallium/EF_Template_Gallium_scope_B_with_wafer_processing_sensitivity.csv +313 -0
- bleecam/cases/gallium/data/gallium/audits/gallium_cost_parameters_jacob_review_changelog.md +90 -0
- bleecam/cases/gallium/data/gallium/capacity_template_ga.csv +326 -0
- bleecam/cases/gallium/data/gallium/cost_parameters_ga.csv +926 -0
- bleecam/cases/gallium/data/gallium/demand_ga.csv +13 -0
- bleecam/cases/gallium/data/gallium/lci/gallium_master_lci_liaison_ready.csv +722 -0
- bleecam/cases/gallium/data/gallium/lci/gallium_master_lci_liaison_ready.xlsx +0 -0
- bleecam/cases/gallium/data/gallium/lcia/README.md +34 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_bauxite_alumina_boundary_audit.csv +7 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_lcia_source_audit.csv +11 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_topology_completeness_audit.csv +316 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_value_sanity_check.csv +261 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_gan_si_wafer_background_patch_audit.csv +5 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_lcia_patch_comparison_summary.csv +53 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_lcia_zero_variant_boundary_audit.csv +12 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_pollutant_profile_generation_audit.csv +53 -0
- bleecam/cases/gallium/data/gallium/lcia/audits/gallium_zinc_mining_concentration_boundary_audit.csv +16 -0
- bleecam/cases/gallium/data/gallium/lcia/gallium_lcia_node_summary.csv +4213 -0
- bleecam/cases/gallium/data/gallium/lcia/gallium_lcia_package_manifest.csv +11 -0
- bleecam/cases/gallium/data/gallium/lcia/gallium_pollutant_flow_characterized.csv +676 -0
- bleecam/cases/gallium/data/gallium/lcia/gallium_pollutant_profile_summary_by_node.csv +46 -0
- bleecam/cases/gallium/data/gallium/lcia/reports/gallium_ef_template_generation_report.md +64 -0
- bleecam/cases/gallium/data/gallium/lcia/reports/gallium_lcia_run_report.md +82 -0
- bleecam/cases/gallium/data/gallium/lcia/reports/gallium_pollutant_profile_generation_report.md +73 -0
- bleecam/cases/gallium/data/gallium/shipping_costs_ga.csv +206 -0
- bleecam/cases/gallium/data/gallium/social_lca_template_ga.csv +361 -0
- bleecam/cases/gallium/data/gallium/trade_topology_ga.csv +176 -0
- bleecam/cases/gallium/data/gallium/yield_ga.csv +316 -0
- bleecam/cases/gallium/gallium_border_cost_metrics.py +569 -0
- bleecam/cases/gallium/gallium_data_loader.py +1163 -0
- bleecam/cases/gallium/gallium_dual_cost_dashboard.py +220 -0
- bleecam/cases/gallium/gallium_pareto.py +130 -0
- bleecam/cases/gallium/gallium_policy_scenario_dashboard.py +676 -0
- bleecam/cases/gallium/gallium_pyomo.py +880 -0
- bleecam/cases/gallium/run_gallium_dual_cost_dashboard.py +46 -0
- bleecam/cases/gallium/run_gallium_policy_scenarios.py +200 -0
- bleecam/cases/rare_earth/REE.py +551 -0
- bleecam/cases/rare_earth/__init__.py +0 -0
- bleecam/cases/rare_earth/config.py +30 -0
- bleecam/cases/rare_earth/constraints.py +836 -0
- bleecam/cases/rare_earth/data/Capacity_Template.csv +356 -0
- bleecam/cases/rare_earth/data/Capacity_Template_reconciled.csv +356 -0
- bleecam/cases/rare_earth/data/Cost_Parameters.csv +656 -0
- bleecam/cases/rare_earth/data/Cost_Parameters_reconciled.csv +656 -0
- bleecam/cases/rare_earth/data/Demand_Input_Template.csv +27 -0
- bleecam/cases/rare_earth/data/EF_Template_with_SLCA.csv +711 -0
- bleecam/cases/rare_earth/data/EF_template.csv +711 -0
- bleecam/cases/rare_earth/data/Recovery_Rates.csv +26 -0
- bleecam/cases/rare_earth/data/Shipping_Costs.csv +361 -0
- bleecam/cases/rare_earth/data/Social_LCA_template.csv +711 -0
- bleecam/cases/rare_earth/data/Yield_Factor.csv +356 -0
- bleecam/cases/rare_earth/data/build_ef_recipe.py +213 -0
- bleecam/cases/rare_earth/data/combined_lcia_data.csv +757 -0
- bleecam/cases/rare_earth/data/historic_installs.xlsx +0 -0
- bleecam/cases/rare_earth/data/lci/mining_2.xlsx +0 -0
- bleecam/cases/rare_earth/data/lcia_pipeline_full.py +589 -0
- bleecam/cases/rare_earth/data/lcia_results.csv +12277 -0
- bleecam/cases/rare_earth/data/market_share_template.csv +41 -0
- bleecam/cases/rare_earth/data/model_results_multilocation.csv +131 -0
- bleecam/cases/rare_earth/data/provenance/BASELINE_v0.md +77 -0
- bleecam/cases/rare_earth/data/provenance/baseline_v0.sha256 +17 -0
- bleecam/cases/rare_earth/data/provenance/sensitivity_cost_v0.json +27 -0
- bleecam/cases/rare_earth/data/trade_topology.csv +193 -0
- bleecam/cases/rare_earth/data/trade_topology_reconciled.csv +124 -0
- bleecam/cases/rare_earth/data_loader.py +159 -0
- bleecam/cases/rare_earth/html_export.py +133 -0
- bleecam/cases/rare_earth/pareto.py +292 -0
- bleecam/core/__init__.py +0 -0
- bleecam/core/case_config.py +133 -0
- bleecam/core/criticality/__init__.py +23 -0
- bleecam/core/criticality/catalog.py +113 -0
- bleecam/core/criticality/cli.py +55 -0
- bleecam/core/criticality/library.py +349 -0
- bleecam/core/criticality/registry.py +110 -0
- bleecam/core/data_loader.py +174 -0
- bleecam/core/lca_import.py +184 -0
- bleecam/core/multiobjective.py +136 -0
- bleecam/core/network.py +286 -0
- bleecam/core/objectives.py +60 -0
- bleecam/core/scenario.py +146 -0
- bleecam/core/schema.py +83 -0
- bleecam/core/sensitivity.py +145 -0
- bleecam/core/solve.py +65 -0
- bleecam/shared/__init__.py +0 -0
- bleecam/shared/bleecam_dashboard.py +269 -0
- bleecam/shared/pareto_plot.py +190 -0
- bleecam/shared/plots.py +85 -0
- bleecam-0.1.0b1.dist-info/METADATA +248 -0
- bleecam-0.1.0b1.dist-info/RECORD +101 -0
- bleecam-0.1.0b1.dist-info/WHEEL +5 -0
- bleecam-0.1.0b1.dist-info/entry_points.txt +7 -0
- bleecam-0.1.0b1.dist-info/licenses/LICENSE +661 -0
- bleecam-0.1.0b1.dist-info/top_level.txt +1 -0
bleecam/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# BLEECAM - Benchmarking Life Cycle Environmental, Economic, and Social Metrics
|
|
2
|
+
# for Critical and Advanced Minerals and Materials
|
|
3
|
+
# Copyright (C) 2026 Alliance for Energy Innovation, LLC
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""Configuration helpers for the Gallium cost-only Pyomo baseline."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from bleecam.core.case_config import load_case_config
|
|
15
|
+
|
|
16
|
+
# Declarations now come from the declarative case file (single source of truth):
|
|
17
|
+
# edit gallium.case.yaml, not these constants. The generic cost logic below stays
|
|
18
|
+
# here for now (it moves to a shared core module in a later phase).
|
|
19
|
+
CASE_CONFIG = load_case_config(Path(__file__).parent / "gallium.case.yaml")
|
|
20
|
+
|
|
21
|
+
TIME_PERIODS = CASE_CONFIG.time_periods
|
|
22
|
+
BASELINE_SCENARIO = CASE_CONFIG.baseline_scenario
|
|
23
|
+
DEMAND_LOCATION = CASE_CONFIG.demand_location
|
|
24
|
+
DEMAND_FLOW = CASE_CONFIG.demand_flow
|
|
25
|
+
DEMAND_SOURCE_PROCESS = CASE_CONFIG.demand_source_process
|
|
26
|
+
DEMAND_SINK_PROCESS = CASE_CONFIG.demand_sink_process
|
|
27
|
+
DEMAND_MATERIALS = CASE_CONFIG.demand_materials
|
|
28
|
+
DEMAND_ARCS = CASE_CONFIG.demand_arcs
|
|
29
|
+
|
|
30
|
+
DEFAULT_INPUT_DIR = Path(".")
|
|
31
|
+
DEFAULT_RESULTS_CSV = CASE_CONFIG.results_csv
|
|
32
|
+
|
|
33
|
+
DEMAND_SLACK_PENALTY_USD_PER_KG = CASE_CONFIG.demand_slack_penalty
|
|
34
|
+
FLOW_TOLERANCE = CASE_CONFIG.flow_tolerance
|
|
35
|
+
|
|
36
|
+
CAPACITY_EXEMPT_PROCESSES = CASE_CONFIG.capacity_exempt_processes
|
|
37
|
+
SOLVER_CANDIDATES = CASE_CONFIG.solver_candidates
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class GalliumCostMaps:
|
|
42
|
+
"""Per-unit cost maps used by the Gallium objective."""
|
|
43
|
+
|
|
44
|
+
process_processing_cost: dict[tuple[int, str, str, str], float]
|
|
45
|
+
process_domestic_transport_cost: dict[tuple[int, str, str, str], float]
|
|
46
|
+
arc_transportation_cost: dict[tuple[int, str, str, str, str, str], float]
|
|
47
|
+
tariff_cost: dict[tuple[int, str, str, str, str, str], float]
|
|
48
|
+
shipping_cost: dict[tuple[int, str, str], float]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def clean_text(value: Any) -> str:
|
|
52
|
+
"""Return loader-compatible stripped text for CSV key fields."""
|
|
53
|
+
|
|
54
|
+
if value is None:
|
|
55
|
+
return ""
|
|
56
|
+
return str(value).strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_cost_maps(loaded_data: dict[str, Any]) -> GalliumCostMaps:
|
|
60
|
+
"""Build process-level and arc-level cost maps from loader output.
|
|
61
|
+
|
|
62
|
+
Rows with blank destination fields are process-level processing/domestic
|
|
63
|
+
transport costs. Rows with destination fields are topology-arc costs.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
cost_df = loaded_data["cost_df"]
|
|
67
|
+
process_processing_cost: dict[tuple[int, str, str, str], float] = {}
|
|
68
|
+
process_domestic_transport_cost: dict[tuple[int, str, str, str], float] = {}
|
|
69
|
+
arc_transportation_cost: dict[tuple[int, str, str, str, str, str], float] = {}
|
|
70
|
+
tariff_cost: dict[tuple[int, str, str, str, str, str], float] = {}
|
|
71
|
+
|
|
72
|
+
for _, row in cost_df.iterrows():
|
|
73
|
+
t = int(row["time_period"])
|
|
74
|
+
source = clean_text(row["source"])
|
|
75
|
+
location = clean_text(row["location"])
|
|
76
|
+
material = clean_text(row["material"])
|
|
77
|
+
destination = clean_text(row["destination"])
|
|
78
|
+
destination_location = clean_text(row["destination_location"])
|
|
79
|
+
processing_cost = float(row["processing cost"])
|
|
80
|
+
transport_cost = float(row["transportation cost"])
|
|
81
|
+
tariff = float(row["tariff_cost"])
|
|
82
|
+
|
|
83
|
+
if destination and destination_location:
|
|
84
|
+
arc_key = (t, source, location, destination, destination_location, material)
|
|
85
|
+
arc_transportation_cost[arc_key] = transport_cost
|
|
86
|
+
if tariff != 0.0:
|
|
87
|
+
tariff_cost[arc_key] = tariff
|
|
88
|
+
else:
|
|
89
|
+
process_key = (t, source, location, material)
|
|
90
|
+
process_processing_cost[process_key] = processing_cost
|
|
91
|
+
process_domestic_transport_cost[process_key] = transport_cost
|
|
92
|
+
|
|
93
|
+
return GalliumCostMaps(
|
|
94
|
+
process_processing_cost=process_processing_cost,
|
|
95
|
+
process_domestic_transport_cost=process_domestic_transport_cost,
|
|
96
|
+
arc_transportation_cost=arc_transportation_cost,
|
|
97
|
+
tariff_cost=tariff_cost,
|
|
98
|
+
shipping_cost=loaded_data["shipping_cost"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def unit_cost_for_arc(
|
|
103
|
+
cost_maps: GalliumCostMaps,
|
|
104
|
+
t: int,
|
|
105
|
+
process_from: str,
|
|
106
|
+
loc_from: str,
|
|
107
|
+
process_to: str,
|
|
108
|
+
loc_to: str,
|
|
109
|
+
material: str,
|
|
110
|
+
) -> tuple[float, float, float, float]:
|
|
111
|
+
"""Return processing, domestic transport, shipping, and tariff costs."""
|
|
112
|
+
|
|
113
|
+
process_key = (int(t), process_from, loc_from, material)
|
|
114
|
+
arc_key = (int(t), process_from, loc_from, process_to, loc_to, material)
|
|
115
|
+
processing = cost_maps.process_processing_cost.get(process_key, 0.0)
|
|
116
|
+
domestic_transport = cost_maps.process_domestic_transport_cost.get(process_key, 0.0)
|
|
117
|
+
domestic_transport += cost_maps.arc_transportation_cost.get(arc_key, 0.0)
|
|
118
|
+
shipping = 0.0
|
|
119
|
+
if loc_from != loc_to:
|
|
120
|
+
shipping = cost_maps.shipping_cost.get((int(t), loc_from, loc_to), 0.0)
|
|
121
|
+
tariff = cost_maps.tariff_cost.get(arc_key, 0.0)
|
|
122
|
+
return processing, domestic_transport, shipping, tariff
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# BLEECAM - Benchmarking Life Cycle Environmental, Economic, and Social Metrics
|
|
2
|
+
# for Critical and Advanced Minerals and Materials
|
|
3
|
+
# Copyright (C) 2026 Alliance for Energy Innovation, LLC
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""Gallium-specific constraints for the cost-only Pyomo baseline."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from typing import Iterable
|
|
12
|
+
|
|
13
|
+
from pyomo.environ import value
|
|
14
|
+
|
|
15
|
+
from .config_ga import CAPACITY_EXEMPT_PROCESSES, DEMAND_FLOW, DEMAND_LOCATION, DEMAND_SOURCE_PROCESS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
FlowKey = tuple[int, str, str, str, str, str]
|
|
19
|
+
ArcKey = tuple[str, str, str, str, str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _flow_terms(model, keys: Iterable[FlowKey]):
|
|
23
|
+
return [model.flowQ[key] for key in keys if key in model.flowQ]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _outgoing_keys(
|
|
27
|
+
model,
|
|
28
|
+
t: int,
|
|
29
|
+
process: str,
|
|
30
|
+
location: str,
|
|
31
|
+
material: str | None = None,
|
|
32
|
+
) -> list[FlowKey]:
|
|
33
|
+
return [
|
|
34
|
+
key
|
|
35
|
+
for key in model.FlowArcs
|
|
36
|
+
if int(key[0]) == int(t)
|
|
37
|
+
and str(key[1]) == process
|
|
38
|
+
and str(key[2]) == location
|
|
39
|
+
and (material is None or str(key[5]) == material)
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _incoming_keys(
|
|
44
|
+
model,
|
|
45
|
+
t: int,
|
|
46
|
+
process: str,
|
|
47
|
+
location: str,
|
|
48
|
+
material: str | None = None,
|
|
49
|
+
) -> list[FlowKey]:
|
|
50
|
+
return [
|
|
51
|
+
key
|
|
52
|
+
for key in model.FlowArcs
|
|
53
|
+
if int(key[0]) == int(t)
|
|
54
|
+
and str(key[3]) == process
|
|
55
|
+
and str(key[4]) == location
|
|
56
|
+
and (material is None or str(key[5]) == material)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def add_capacity_constraints_ga(
|
|
61
|
+
model,
|
|
62
|
+
max_output_capacity: dict[tuple[str, str, str], dict[int, float]],
|
|
63
|
+
*,
|
|
64
|
+
exempt_processes: set[str] | frozenset[str] = CAPACITY_EXEMPT_PROCESSES,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Limit total outgoing flow by process, location, material, and period."""
|
|
67
|
+
|
|
68
|
+
for (process, location, material), period_caps in max_output_capacity.items():
|
|
69
|
+
if process in exempt_processes:
|
|
70
|
+
continue
|
|
71
|
+
for t, capacity in period_caps.items():
|
|
72
|
+
if t not in model.TimePeriods:
|
|
73
|
+
continue
|
|
74
|
+
terms = _flow_terms(model, _outgoing_keys(model, t, process, location, material))
|
|
75
|
+
if terms:
|
|
76
|
+
model.constraints.add(sum(terms) <= float(capacity))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def add_yield_mass_balance_constraints_ga(
|
|
80
|
+
model,
|
|
81
|
+
yield_factor: dict[tuple[int, str, str, str], float],
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Add generic Gallium yield constraints from ``yield_ga.csv``.
|
|
84
|
+
|
|
85
|
+
Source processes with no inbound topology arcs are governed by capacity only.
|
|
86
|
+
For transformation/pass-through nodes, outgoing material requires sufficient
|
|
87
|
+
inbound material. Same-material pass-throughs are constrained material-wise;
|
|
88
|
+
remaining outputs share the process input pool by yield-adjusted equivalents.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
by_process_location: dict[tuple[int, str, str], list[tuple[str, float]]] = defaultdict(list)
|
|
92
|
+
for (t, process, location, material), factor in yield_factor.items():
|
|
93
|
+
if t in model.TimePeriods and factor >= 0:
|
|
94
|
+
by_process_location[(int(t), process, location)].append((material, float(factor)))
|
|
95
|
+
|
|
96
|
+
for (t, process, location), material_factors in by_process_location.items():
|
|
97
|
+
all_incoming = _flow_terms(model, _incoming_keys(model, t, process, location))
|
|
98
|
+
if not all_incoming:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
pooled_output_terms = []
|
|
102
|
+
for material, factor in material_factors:
|
|
103
|
+
outgoing = _flow_terms(model, _outgoing_keys(model, t, process, location, material))
|
|
104
|
+
if not outgoing:
|
|
105
|
+
continue
|
|
106
|
+
if factor <= 0.0:
|
|
107
|
+
model.constraints.add(sum(outgoing) == 0.0)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
same_material_incoming = _flow_terms(
|
|
111
|
+
model, _incoming_keys(model, t, process, location, material)
|
|
112
|
+
)
|
|
113
|
+
if same_material_incoming:
|
|
114
|
+
model.constraints.add(sum(outgoing) <= factor * sum(same_material_incoming))
|
|
115
|
+
else:
|
|
116
|
+
pooled_output_terms.append(sum(outgoing) / factor)
|
|
117
|
+
|
|
118
|
+
if pooled_output_terms:
|
|
119
|
+
model.constraints.add(sum(pooled_output_terms) <= sum(all_incoming))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def add_demand_constraints_ga(model, demand_df) -> None:
|
|
123
|
+
"""Require terminal wafer demand, allowing shortage slack for diagnostics."""
|
|
124
|
+
|
|
125
|
+
for _, row in demand_df.iterrows():
|
|
126
|
+
t = int(row["time_period"])
|
|
127
|
+
location = str(row["location"]).strip()
|
|
128
|
+
material = str(row["material"]).strip()
|
|
129
|
+
flow = str(row["flow"]).strip()
|
|
130
|
+
demand = float(row["demand_kg"])
|
|
131
|
+
if flow != DEMAND_FLOW or location != DEMAND_LOCATION:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
terminal_key = (t, DEMAND_SOURCE_PROCESS, location, flow, location, material)
|
|
135
|
+
terminal_flow = model.flowQ[terminal_key] if terminal_key in model.flowQ else 0.0
|
|
136
|
+
slack_key = (t, location, material)
|
|
137
|
+
expr = terminal_flow + model.demand_slack[slack_key]
|
|
138
|
+
# If the model has a finished-good buffer, demand can be met from production
|
|
139
|
+
# plus a stock draw, and surplus production can be banked (golden-neutral: at
|
|
140
|
+
# baseline, holding cost makes both zero).
|
|
141
|
+
if hasattr(model, "stock_level") and (t, material) in model.stock_level:
|
|
142
|
+
expr = expr + model.flow_from_stock[t, material] - model.flow_to_stock[t, material]
|
|
143
|
+
model.constraints.add(expr == demand)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def summarize_demand_slack(model, tolerance: float = 1e-7) -> list[dict[str, float | int | str]]:
|
|
147
|
+
"""Return nonzero demand slack records from a solved model."""
|
|
148
|
+
|
|
149
|
+
rows = []
|
|
150
|
+
for key in model.DemandKeys:
|
|
151
|
+
raw = value(model.demand_slack[key], exception=False)
|
|
152
|
+
slack = 0.0 if raw is None else float(raw)
|
|
153
|
+
if slack > tolerance:
|
|
154
|
+
t, location, material = key
|
|
155
|
+
rows.append(
|
|
156
|
+
{
|
|
157
|
+
"time_period": int(t),
|
|
158
|
+
"location": str(location),
|
|
159
|
+
"material": str(material),
|
|
160
|
+
"demand_slack_kg": slack,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
return rows
|