CarlyneB commited on
Commit
818a3c4
·
1 Parent(s): 87fce1c

[FIX] Model: Updating the model calculation when a method is removed from tab one

Browse files
optimization/optimization.py CHANGED
@@ -43,19 +43,22 @@ def run_optimization(
43
  custom_batch_info = {} # {batch_name: {"group": ref, "amount": Q}}
44
  for batch in (custom_resources or []):
45
  custom_batch_info[batch["name"]] = {
46
- "group": batch["group"],
47
- "amount": float(batch["amount"]),
 
48
  }
49
  custom_names = set(custom_batch_info)
50
  ref_to_batches = {} # {standard_resource: [batch_name, ...]}
51
  for bname, info in custom_batch_info.items():
52
  ref_to_batches.setdefault(info["group"], []).append(bname)
53
-
54
- # ── Inject primary batch coefficients (fallback) ─────────────────────────
55
- # Tab 3 pre-init may have set the coefficient to 0 if it classified the
56
- # link as secondary (or if the key already existed at 0 from a prior run).
57
- # For methods explicitly listed in batch["methods"] (primary dialog links),
58
- # fall back to the batch median so the LP can actually use the resource.
 
 
59
  for batch in (custom_resources or []):
60
  bname = batch["name"]
61
  for method, coefs in batch["methods"].items():
@@ -84,22 +87,22 @@ def run_optimization(
84
  return False, {"message": "No active methods."}
85
 
86
  # ── Split-variable list ───────────────────────────────────────────────────
87
- # z_{m,n} exists when BOTH c_s[m] > 0 AND c_n[m] > 0.
88
- # It represents CO₂ attributed to custom pool n for method m, substituting
89
- # for an equal amount of output that would otherwise consume standard pool s.
90
- #
91
- # Guard: if the standard resource cap is 0 (user left it unset), the custom
92
- # batch is also inactive — a batch supplements an existing resource, it doesn't
93
- # enable a resource type that the user hasn't allocated.
94
  z_pairs = []
95
  for bname, info in custom_batch_info.items():
96
- ref = info["group"]
97
- if resource_caps.get(ref, 0.0) <= 0:
98
- continue # standard resource unavailable → custom batch inactive too
 
99
  for m in methods:
100
- c_s = method_costs.get(m, {}).get(ref, 0.0)
 
101
  c_n = method_costs.get(m, {}).get(bname, 0.0)
102
- if c_s > 0 and c_n > 0:
103
  z_pairs.append((m, bname))
104
 
105
  Z = len(z_pairs)
@@ -139,31 +142,27 @@ def run_optimization(
139
  A_rows.append(row)
140
  b_rows.append(resource_caps[r])
141
 
142
- # (C) Custom batch capacity constraints
143
- # For substitutable pairs (m, n) in z_idx → c_n[m] * z_{m,n}
144
- # For non-substitutable (c_s = 0, c_n > 0) → c_n[m] * y_m (method fully
145
- # relies on this batch since it has no access to the standard resource)
146
- # Skip entirely when the standard resource cap is 0 (batch is inactive).
147
  for bname, info in custom_batch_info.items():
148
- ref = info["group"]
149
- if resource_caps.get(ref, 0.0) <= 0:
150
  continue
151
  row = np.zeros(N)
152
  for j, m in enumerate(methods):
153
- c_n_m = method_costs.get(m, {}).get(bname, 0.0)
154
- if c_n_m <= 0:
155
  continue
156
- c_s_m = method_costs.get(m, {}).get(ref, 0.0)
157
- if c_s_m > 0 and (m, bname) in z_idx:
158
  row[z_idx[(m, bname)]] = c_n_m
159
- else:
160
- row[j] = c_n_m # entire y_m draws from this batch
161
  if np.any(row != 0):
162
  A_rows.append(row)
163
- b_rows.append(info["amount"])
164
-
165
- # (D) Coupling constraints per (method, standard-resource-type)
166
- # Ensure Σ_{n: ref(n)=s} z_{m,n} ≤ y_m so standard usage stays ≥ 0
 
 
 
 
167
  coupling_done = set()
168
  for (m, bname) in z_pairs:
169
  ref = custom_batch_info[bname]["group"]
@@ -171,15 +170,27 @@ def run_optimization(
171
  if key in coupling_done:
172
  continue
173
  coupling_done.add(key)
174
- j = method_idx[m]
 
 
 
 
175
  row = np.zeros(N)
176
  row[j] = -1.0
177
- for bn in ref_to_batches.get(ref, []):
178
- if (m, bn) in z_idx:
179
- row[z_idx[(m, bn)]] = 1.0
180
  A_rows.append(row)
181
  b_rows.append(0.0)
182
 
 
 
 
 
 
 
 
 
 
183
  # (E) Method cap constraints (on y_m)
184
  for j, m in enumerate(methods):
185
  constraint = method_constraints.get(m, {})
@@ -242,18 +253,20 @@ def run_optimization(
242
  if used > 1e-9:
243
  resource_usage[r][m] = float(used)
244
 
245
- # Custom batches
246
  for bname, info in custom_batch_info.items():
247
- ref = info["group"]
 
248
  for j, m in enumerate(methods):
 
 
249
  c_n_m = method_costs.get(m, {}).get(bname, 0.0)
250
  if c_n_m <= 0:
251
  continue
252
- c_s_m = method_costs.get(m, {}).get(ref, 0.0)
253
- if c_s_m > 0 and (m, bname) in z_idx:
254
  used = c_n_m * sol[z_idx[(m, bname)]]
255
  else:
256
- used = c_n_m * y[j]
257
  if used > 1e-9:
258
  resource_usage[bname][m] = float(used)
259
 
 
43
  custom_batch_info = {} # {batch_name: {"group": ref, "amount": Q}}
44
  for batch in (custom_resources or []):
45
  custom_batch_info[batch["name"]] = {
46
+ "group": batch["group"],
47
+ "amount": float(batch["amount"]),
48
+ "primary_methods": set(batch.get("methods", {}).keys()),
49
  }
50
  custom_names = set(custom_batch_info)
51
  ref_to_batches = {} # {standard_resource: [batch_name, ...]}
52
  for bname, info in custom_batch_info.items():
53
  ref_to_batches.setdefault(info["group"], []).append(bname)
54
+ if bname not in resource_caps:
55
+ resource_caps[bname] = info["amount"]
56
+
57
+ # Inject primary batch coefficients (fallback) ─────────────────────────────
58
+ # Tab 3 pre-init may have set the coefficient to 0; fall back to batch median.
59
+ # Custom pools are INDEPENDENT of the standard resource enable/disable toggle:
60
+ # a method can access a custom batch even if it is disabled for the standard
61
+ # reference resource — the D2 coupling constraint (y_m ≤ Σ z) bounds the LP.
62
  for batch in (custom_resources or []):
63
  bname = batch["name"]
64
  for method, coefs in batch["methods"].items():
 
87
  return False, {"message": "No active methods."}
88
 
89
  # ── Split-variable list ───────────────────────────────────────────────────
90
+ # z_{m,n} is the CO₂ from method m drawn from custom pool n.
91
+ # y_m - Σ_n z_{m,n} is what method m draws from the standard pool.
92
+ # Custom pools are INDEPENDENT: a method can use a custom pool even when
93
+ # the standard resource is 0 or disabled for that method.
94
+ # Guard: skip batch if the batch itself has no capacity (Q_n = 0).
 
 
95
  z_pairs = []
96
  for bname, info in custom_batch_info.items():
97
+ ref = info["group"]
98
+ primary_methods = info["primary_methods"]
99
+ if resource_caps.get(bname, 0.0) <= 0:
100
+ continue # custom batch has no capacity → inactive
101
  for m in methods:
102
+ if m not in primary_methods:
103
+ continue # only methods explicitly linked to this batch in the dialog
104
  c_n = method_costs.get(m, {}).get(bname, 0.0)
105
+ if c_n > 0:
106
  z_pairs.append((m, bname))
107
 
108
  Z = len(z_pairs)
 
142
  A_rows.append(row)
143
  b_rows.append(resource_caps[r])
144
 
145
+ # (C) Custom batch capacity constraints: Σ_m c_n[m] * z_{m,n} ≤ Q_n
 
 
 
 
146
  for bname, info in custom_batch_info.items():
147
+ if resource_caps.get(bname, 0.0) <= 0:
 
148
  continue
149
  row = np.zeros(N)
150
  for j, m in enumerate(methods):
151
+ if (m, bname) not in z_idx:
 
152
  continue
153
+ c_n_m = method_costs.get(m, {}).get(bname, 0.0)
154
+ if c_n_m > 0:
155
  row[z_idx[(m, bname)]] = c_n_m
 
 
156
  if np.any(row != 0):
157
  A_rows.append(row)
158
+ b_rows.append(resource_caps.get(bname, 0.0))
159
+
160
+ # (D1) Coupling lower bound: Σ_{n: ref(n)=r} z_{m,n} ≤ y_m
161
+ # Keeps standard resource usage non-negative.
162
+ # (D2) Coupling upper bound when c_s = 0: y_m ≤ Σ z_{m,n}
163
+ # When a method has no access to the standard resource (disabled or coef=0),
164
+ # its entire output must come from custom batches — prevents y_m from being
165
+ # unbounded. Together with D1 this forces y_m = Σ z when c_s = 0.
166
  coupling_done = set()
167
  for (m, bname) in z_pairs:
168
  ref = custom_batch_info[bname]["group"]
 
170
  if key in coupling_done:
171
  continue
172
  coupling_done.add(key)
173
+ j = method_idx[m]
174
+ c_s_m = method_costs.get(m, {}).get(ref, 0.0)
175
+ batch_cols = [z_idx[(m, bn)] for bn in ref_to_batches.get(ref, []) if (m, bn) in z_idx]
176
+
177
+ # D1: Σ z ≤ y_m → Σ z - y_m ≤ 0
178
  row = np.zeros(N)
179
  row[j] = -1.0
180
+ for col in batch_cols:
181
+ row[col] = 1.0
 
182
  A_rows.append(row)
183
  b_rows.append(0.0)
184
 
185
+ # D2 (only when c_s = 0): y_m ≤ Σ z → y_m - Σ z ≤ 0
186
+ if c_s_m == 0:
187
+ row2 = np.zeros(N)
188
+ row2[j] = 1.0
189
+ for col in batch_cols:
190
+ row2[col] = -1.0
191
+ A_rows.append(row2)
192
+ b_rows.append(0.0)
193
+
194
  # (E) Method cap constraints (on y_m)
195
  for j, m in enumerate(methods):
196
  constraint = method_constraints.get(m, {})
 
253
  if used > 1e-9:
254
  resource_usage[r][m] = float(used)
255
 
256
+ # Custom batches — only for methods explicitly linked in the dialog
257
  for bname, info in custom_batch_info.items():
258
+ ref = info["group"]
259
+ primary_methods = info["primary_methods"]
260
  for j, m in enumerate(methods):
261
+ if m not in primary_methods:
262
+ continue # ignore stale session-state coefficients
263
  c_n_m = method_costs.get(m, {}).get(bname, 0.0)
264
  if c_n_m <= 0:
265
  continue
266
+ if (m, bname) in z_idx:
 
267
  used = c_n_m * sol[z_idx[(m, bname)]]
268
  else:
269
+ used = 0.0
270
  if used > 1e-9:
271
  resource_usage[bname][m] = float(used)
272
 
tabs/tab1_inputs.py CHANGED
@@ -18,6 +18,8 @@ def init_custom_resources():
18
  st.session_state.custom_resources = []
19
  if "enabled_methods" not in st.session_state:
20
  st.session_state.enabled_methods = {}
 
 
21
  for batch in st.session_state.custom_resources:
22
  try:
23
  batch["amount"] = float(batch["amount"])
@@ -678,16 +680,38 @@ def render_tab(cost_sliders):
678
  if ok:
679
  method_active_resource_count[m] += 1
680
 
 
 
 
 
 
 
 
 
 
 
681
  methods_with_no_active_resource = [
682
  m for m in method_to_primary_resources
683
  if method_active_resource_count.get(m, 0) == 0
684
  ]
685
 
 
 
 
 
 
 
 
 
 
 
 
686
  newly_disabled = []
687
  for m in methods_with_no_active_resource:
688
  if st.session_state.constraints.get(m, {}).get("active", True):
689
  st.session_state.constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
690
  st.session_state.constraints[m]["active"] = False
 
691
  newly_disabled.append(m)
692
  toggle_key = f"toggle_{m}"
693
  if toggle_key in st.session_state:
 
18
  st.session_state.custom_resources = []
19
  if "enabled_methods" not in st.session_state:
20
  st.session_state.enabled_methods = {}
21
+ if "auto_disabled_methods" not in st.session_state:
22
+ st.session_state.auto_disabled_methods = set()
23
  for batch in st.session_state.custom_resources:
24
  try:
25
  batch["amount"] = float(batch["amount"])
 
680
  if ok:
681
  method_active_resource_count[m] += 1
682
 
683
+ # Custom batches are INDEPENDENT of the standard resource toggle: a method
684
+ # linked to a custom batch with capacity counts as having an active resource,
685
+ # even if it is disabled for the standard reference resource.
686
+ for batch in st.session_state.get("custom_resources", []):
687
+ if float(batch.get("amount") or 0) <= 0:
688
+ continue
689
+ for m_name in batch.get("methods", {}).keys():
690
+ method_active_resource_count.setdefault(m_name, 0)
691
+ method_active_resource_count[m_name] += 1
692
+
693
  methods_with_no_active_resource = [
694
  m for m in method_to_primary_resources
695
  if method_active_resource_count.get(m, 0) == 0
696
  ]
697
 
698
+ # Re-enable methods that were auto-disabled but now have at least one active resource
699
+ for m in list(st.session_state.auto_disabled_methods):
700
+ if method_active_resource_count.get(m, 0) > 0:
701
+ if m in st.session_state.constraints:
702
+ st.session_state.constraints[m]["active"] = True
703
+ toggle_key = f"toggle_{m}"
704
+ if toggle_key in st.session_state:
705
+ st.session_state[toggle_key] = True
706
+ st.session_state.auto_disabled_methods.discard(m)
707
+
708
+ # Auto-disable methods that now have no active primary resource
709
  newly_disabled = []
710
  for m in methods_with_no_active_resource:
711
  if st.session_state.constraints.get(m, {}).get("active", True):
712
  st.session_state.constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
713
  st.session_state.constraints[m]["active"] = False
714
+ st.session_state.auto_disabled_methods.add(m)
715
  newly_disabled.append(m)
716
  toggle_key = f"toggle_{m}"
717
  if toggle_key in st.session_state:
tabs/tab3_coefficients.py CHANGED
@@ -27,24 +27,12 @@ def is_method_enabled(method, resource, enabled_methods):
27
 
28
 
29
  def _enforce_disabled_coefficients(method_order_, resource_order_, custom_resources, enabled_methods):
30
- """Zero out st.session_state.costs[method][resource] for every (method, resource)
31
- pair the user disabled in tab1 — even if that pair was never opened in this tab.
32
- Without this, a disabled pair silently keeps its last/default coefficient and
33
- gets exported/optimised as if it were still active."""
34
- for r in resource_order_:
35
- disallowed = {m for m, ok in enabled_methods.get(r, {}).items() if not ok}
36
- if not disallowed:
37
- continue
38
- for m in disallowed:
39
- if m in st.session_state.costs and r in st.session_state.costs[m]:
40
- st.session_state.costs[m][r] = 0.0
41
-
42
- for batch in custom_resources or []:
43
- r = batch["name"]
44
- disallowed = {m for m, ok in enabled_methods.get(r, {}).items() if not ok}
45
- for m in disallowed:
46
- if m in st.session_state.costs and r in st.session_state.costs[m]:
47
- st.session_state.costs[m][r] = 0.0
48
 
49
 
50
  def render_tab(cost_sliders):
@@ -142,6 +130,21 @@ def render_tab(cost_sliders):
142
  sub_group = custom_resource_to_sub_group.get(custom_r)
143
  return sub_group is not None and (method_name, sub_group) in secondary_resources
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  # Pre-initialize costs for custom resources so the summary table shows the
146
  # correct value immediately, without needing to open the expander first.
147
  # Secondary custom resources start at 0 (like standard secondary resources).
@@ -202,7 +205,6 @@ def render_tab(cost_sliders):
202
  enabled = is_method_enabled(method, r, enabled_methods)
203
  if not enabled:
204
  st.info(f"INFO: Method disabled for **{r}** (see Resource Inputs tab)")
205
- st.session_state.costs[method][r] = 0.0
206
  continue
207
  conf = method_sliders[r]
208
  display_label = resource_display.get(r, r)
@@ -259,7 +261,6 @@ def render_tab(cost_sliders):
259
  enabled = is_method_enabled(method, r, enabled_methods)
260
  if not enabled:
261
  st.info("INFO: Method disabled for this resource (see Resource Inputs tab)")
262
- st.session_state.costs.setdefault(method, {})[r] = 0.0
263
  continue
264
 
265
  _is_cust_secondary = is_custom_secondary(method, r)
 
27
 
28
 
29
  def _enforce_disabled_coefficients(method_order_, resource_order_, custom_resources, enabled_methods):
30
+ # Intentionally a no-op: disabling a method for a resource must NOT permanently
31
+ # zero the stored coefficient in session state, because re-enabling would then
32
+ # see an existing key at 0 and the pre-init would skip restoration.
33
+ # The LP in optimization.py already applies enabled_methods at call time
34
+ # (zeroing only for that run, without mutating session state).
35
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  def render_tab(cost_sliders):
 
130
  sub_group = custom_resource_to_sub_group.get(custom_r)
131
  return sub_group is not None and (method_name, sub_group) in secondary_resources
132
 
133
+ # Build reverse index: custom resource name → set of valid methods
134
+ valid_methods_for_custom = {}
135
+ for m, resources in custom_sliders_by_method.items():
136
+ for r in resources:
137
+ valid_methods_for_custom.setdefault(r, set()).add(m)
138
+
139
+ # Remove stale session state entries: a method should not carry a custom
140
+ # resource coefficient if it is not linked to that batch (neither as
141
+ # explicit primary nor as secondary auto-injection). Stale values can
142
+ # persist across reruns and cause the LP to create spurious z-pairs.
143
+ for r_name, valid in valid_methods_for_custom.items():
144
+ for m in list(st.session_state.get("costs", {}).keys()):
145
+ if r_name in st.session_state.costs.get(m, {}) and m not in valid:
146
+ del st.session_state.costs[m][r_name]
147
+
148
  # Pre-initialize costs for custom resources so the summary table shows the
149
  # correct value immediately, without needing to open the expander first.
150
  # Secondary custom resources start at 0 (like standard secondary resources).
 
205
  enabled = is_method_enabled(method, r, enabled_methods)
206
  if not enabled:
207
  st.info(f"INFO: Method disabled for **{r}** (see Resource Inputs tab)")
 
208
  continue
209
  conf = method_sliders[r]
210
  display_label = resource_display.get(r, r)
 
261
  enabled = is_method_enabled(method, r, enabled_methods)
262
  if not enabled:
263
  st.info("INFO: Method disabled for this resource (see Resource Inputs tab)")
 
264
  continue
265
 
266
  _is_cust_secondary = is_custom_secondary(method, r)
tabs/tab4_optimization.py CHANGED
@@ -330,8 +330,27 @@ def run_and_store_result():
330
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
331
  return
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  safe_constraints = neutralize_zero_coefficient_methods(
334
- st.session_state.costs, st.session_state.constraints
335
  )
336
  neutralized = [
337
  m for m in safe_constraints
@@ -341,7 +360,7 @@ def run_and_store_result():
341
 
342
  success, result = run_optimization(
343
  resource_caps=st.session_state.resource_caps,
344
- method_constraints=st.session_state.constraints,
345
  method_costs=st.session_state.costs,
346
  custom_resources=custom_resources,
347
  enabled_methods=enabled_methods,
 
330
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
331
  return
332
 
333
+ # Apply enabled_methods overrides to a temporary copy so that
334
+ # neutralize_zero_coefficient_methods sees the effective coefficients
335
+ # (a method fully disabled for its only resource has all-zero coefficients
336
+ # and must be deactivated before the LP is built).
337
+ costs_effective = {m: dict(v) for m, v in st.session_state.costs.items()}
338
+ for _m, _resources in costs_effective.items():
339
+ for _r in list(_resources):
340
+ if not enabled_methods.get(_r, {}).get(_m, True):
341
+ costs_effective[_m][_r] = 0.0
342
+
343
+ # Inject custom batch coefficients into costs_effective so neutralize does
344
+ # not wrongly deactivate a method that only has custom-batch access.
345
+ # Custom pools are INDEPENDENT of the standard resource enable/disable toggle.
346
+ for _batch in custom_resources:
347
+ _bname = _batch["name"]
348
+ for _m, _coefs in _batch.get("methods", {}).items():
349
+ if _m in costs_effective and costs_effective[_m].get(_bname, 0.0) == 0.0:
350
+ costs_effective[_m][_bname] = _coefs.get("median", 0.0)
351
+
352
  safe_constraints = neutralize_zero_coefficient_methods(
353
+ costs_effective, st.session_state.constraints
354
  )
355
  neutralized = [
356
  m for m in safe_constraints
 
360
 
361
  success, result = run_optimization(
362
  resource_caps=st.session_state.resource_caps,
363
+ method_constraints=safe_constraints,
364
  method_costs=st.session_state.costs,
365
  custom_resources=custom_resources,
366
  enabled_methods=enabled_methods,