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.
Files changed (101) hide show
  1. bleecam/__init__.py +0 -0
  2. bleecam/cases/__init__.py +0 -0
  3. bleecam/cases/gallium/__init__.py +0 -0
  4. bleecam/cases/gallium/config_ga.py +122 -0
  5. bleecam/cases/gallium/constraints_ga.py +163 -0
  6. bleecam/cases/gallium/data/gallium/EF_Template.csv +313 -0
  7. bleecam/cases/gallium/data/gallium/EF_Template_Ga_from_LiAISON.csv +261 -0
  8. bleecam/cases/gallium/data/gallium/EF_Template_Gallium_scope_A_material_supply.csv +313 -0
  9. bleecam/cases/gallium/data/gallium/EF_Template_Gallium_scope_B_with_wafer_processing_sensitivity.csv +313 -0
  10. bleecam/cases/gallium/data/gallium/audits/gallium_cost_parameters_jacob_review_changelog.md +90 -0
  11. bleecam/cases/gallium/data/gallium/capacity_template_ga.csv +326 -0
  12. bleecam/cases/gallium/data/gallium/cost_parameters_ga.csv +926 -0
  13. bleecam/cases/gallium/data/gallium/demand_ga.csv +13 -0
  14. bleecam/cases/gallium/data/gallium/lci/gallium_master_lci_liaison_ready.csv +722 -0
  15. bleecam/cases/gallium/data/gallium/lci/gallium_master_lci_liaison_ready.xlsx +0 -0
  16. bleecam/cases/gallium/data/gallium/lcia/README.md +34 -0
  17. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_bauxite_alumina_boundary_audit.csv +7 -0
  18. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_lcia_source_audit.csv +11 -0
  19. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_topology_completeness_audit.csv +316 -0
  20. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_ef_value_sanity_check.csv +261 -0
  21. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_gan_si_wafer_background_patch_audit.csv +5 -0
  22. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_lcia_patch_comparison_summary.csv +53 -0
  23. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_lcia_zero_variant_boundary_audit.csv +12 -0
  24. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_pollutant_profile_generation_audit.csv +53 -0
  25. bleecam/cases/gallium/data/gallium/lcia/audits/gallium_zinc_mining_concentration_boundary_audit.csv +16 -0
  26. bleecam/cases/gallium/data/gallium/lcia/gallium_lcia_node_summary.csv +4213 -0
  27. bleecam/cases/gallium/data/gallium/lcia/gallium_lcia_package_manifest.csv +11 -0
  28. bleecam/cases/gallium/data/gallium/lcia/gallium_pollutant_flow_characterized.csv +676 -0
  29. bleecam/cases/gallium/data/gallium/lcia/gallium_pollutant_profile_summary_by_node.csv +46 -0
  30. bleecam/cases/gallium/data/gallium/lcia/reports/gallium_ef_template_generation_report.md +64 -0
  31. bleecam/cases/gallium/data/gallium/lcia/reports/gallium_lcia_run_report.md +82 -0
  32. bleecam/cases/gallium/data/gallium/lcia/reports/gallium_pollutant_profile_generation_report.md +73 -0
  33. bleecam/cases/gallium/data/gallium/shipping_costs_ga.csv +206 -0
  34. bleecam/cases/gallium/data/gallium/social_lca_template_ga.csv +361 -0
  35. bleecam/cases/gallium/data/gallium/trade_topology_ga.csv +176 -0
  36. bleecam/cases/gallium/data/gallium/yield_ga.csv +316 -0
  37. bleecam/cases/gallium/gallium_border_cost_metrics.py +569 -0
  38. bleecam/cases/gallium/gallium_data_loader.py +1163 -0
  39. bleecam/cases/gallium/gallium_dual_cost_dashboard.py +220 -0
  40. bleecam/cases/gallium/gallium_pareto.py +130 -0
  41. bleecam/cases/gallium/gallium_policy_scenario_dashboard.py +676 -0
  42. bleecam/cases/gallium/gallium_pyomo.py +880 -0
  43. bleecam/cases/gallium/run_gallium_dual_cost_dashboard.py +46 -0
  44. bleecam/cases/gallium/run_gallium_policy_scenarios.py +200 -0
  45. bleecam/cases/rare_earth/REE.py +551 -0
  46. bleecam/cases/rare_earth/__init__.py +0 -0
  47. bleecam/cases/rare_earth/config.py +30 -0
  48. bleecam/cases/rare_earth/constraints.py +836 -0
  49. bleecam/cases/rare_earth/data/Capacity_Template.csv +356 -0
  50. bleecam/cases/rare_earth/data/Capacity_Template_reconciled.csv +356 -0
  51. bleecam/cases/rare_earth/data/Cost_Parameters.csv +656 -0
  52. bleecam/cases/rare_earth/data/Cost_Parameters_reconciled.csv +656 -0
  53. bleecam/cases/rare_earth/data/Demand_Input_Template.csv +27 -0
  54. bleecam/cases/rare_earth/data/EF_Template_with_SLCA.csv +711 -0
  55. bleecam/cases/rare_earth/data/EF_template.csv +711 -0
  56. bleecam/cases/rare_earth/data/Recovery_Rates.csv +26 -0
  57. bleecam/cases/rare_earth/data/Shipping_Costs.csv +361 -0
  58. bleecam/cases/rare_earth/data/Social_LCA_template.csv +711 -0
  59. bleecam/cases/rare_earth/data/Yield_Factor.csv +356 -0
  60. bleecam/cases/rare_earth/data/build_ef_recipe.py +213 -0
  61. bleecam/cases/rare_earth/data/combined_lcia_data.csv +757 -0
  62. bleecam/cases/rare_earth/data/historic_installs.xlsx +0 -0
  63. bleecam/cases/rare_earth/data/lci/mining_2.xlsx +0 -0
  64. bleecam/cases/rare_earth/data/lcia_pipeline_full.py +589 -0
  65. bleecam/cases/rare_earth/data/lcia_results.csv +12277 -0
  66. bleecam/cases/rare_earth/data/market_share_template.csv +41 -0
  67. bleecam/cases/rare_earth/data/model_results_multilocation.csv +131 -0
  68. bleecam/cases/rare_earth/data/provenance/BASELINE_v0.md +77 -0
  69. bleecam/cases/rare_earth/data/provenance/baseline_v0.sha256 +17 -0
  70. bleecam/cases/rare_earth/data/provenance/sensitivity_cost_v0.json +27 -0
  71. bleecam/cases/rare_earth/data/trade_topology.csv +193 -0
  72. bleecam/cases/rare_earth/data/trade_topology_reconciled.csv +124 -0
  73. bleecam/cases/rare_earth/data_loader.py +159 -0
  74. bleecam/cases/rare_earth/html_export.py +133 -0
  75. bleecam/cases/rare_earth/pareto.py +292 -0
  76. bleecam/core/__init__.py +0 -0
  77. bleecam/core/case_config.py +133 -0
  78. bleecam/core/criticality/__init__.py +23 -0
  79. bleecam/core/criticality/catalog.py +113 -0
  80. bleecam/core/criticality/cli.py +55 -0
  81. bleecam/core/criticality/library.py +349 -0
  82. bleecam/core/criticality/registry.py +110 -0
  83. bleecam/core/data_loader.py +174 -0
  84. bleecam/core/lca_import.py +184 -0
  85. bleecam/core/multiobjective.py +136 -0
  86. bleecam/core/network.py +286 -0
  87. bleecam/core/objectives.py +60 -0
  88. bleecam/core/scenario.py +146 -0
  89. bleecam/core/schema.py +83 -0
  90. bleecam/core/sensitivity.py +145 -0
  91. bleecam/core/solve.py +65 -0
  92. bleecam/shared/__init__.py +0 -0
  93. bleecam/shared/bleecam_dashboard.py +269 -0
  94. bleecam/shared/pareto_plot.py +190 -0
  95. bleecam/shared/plots.py +85 -0
  96. bleecam-0.1.0b1.dist-info/METADATA +248 -0
  97. bleecam-0.1.0b1.dist-info/RECORD +101 -0
  98. bleecam-0.1.0b1.dist-info/WHEEL +5 -0
  99. bleecam-0.1.0b1.dist-info/entry_points.txt +7 -0
  100. bleecam-0.1.0b1.dist-info/licenses/LICENSE +661 -0
  101. 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