Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 77 additions & 10 deletions temoa/_internal/table_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import math
import sqlite3
import sys
from collections import defaultdict
Expand Down Expand Up @@ -64,6 +65,13 @@
'output_storage_level',
]

OUTPUT_THRESHOLD_DEFAULTS = {
'capacity': 1e-3,
'activity': 1e-3,
'emission': 1e-3,
'cost': 1e-2,
}

FLOW_SUMMARY_FILE_LOC = (
resources.files('temoa.extensions.modeling_to_generate_alternatives')
/ 'make_flow_summary_table.sql'
Expand All @@ -74,9 +82,8 @@
class TableWriter:
con: sqlite3.Connection | None

def __init__(self, config: TemoaConfig, epsilon: float = 1e-5) -> None:
def __init__(self, config: TemoaConfig) -> None:
self.config = config
self.epsilon = epsilon
self.tech_sectors: dict[str, str] | None = None
self.flow_register: dict[FI, dict[FlowType, float]] = {}
self.emission_register: dict[EI, float] | None = None
Expand All @@ -92,6 +99,17 @@ def __init__(self, config: TemoaConfig, epsilon: float = 1e-5) -> None:
logger.exception('Failed to connect to output database: %s', config.output_database)
sys.exit(-1)

self.output_threshold_capacity = self._resolve_output_threshold('capacity')
self.output_threshold_activity = self._resolve_output_threshold('activity')
self.output_threshold_emission = self._resolve_output_threshold('emission')
self.output_threshold_cost = self._resolve_output_threshold('cost')
self.epsilon = min(
self.output_threshold_capacity,
self.output_threshold_activity,
self.output_threshold_emission,
self.output_threshold_cost,
)

# Unit propagator for populating units in output tables (lazy init)
self._unit_propagator: UnitPropagator | None = None

Expand Down Expand Up @@ -228,6 +246,36 @@ def _bulk_insert(self, table_name: str, records: list[dict[str, Any]]) -> None:

self.connection.executemany(query, rows_to_insert)

@staticmethod
def _validate_threshold(
threshold_name: str,
threshold_value: float | int | None,
) -> float | None:
"""Validate output threshold inputs and normalize to float."""
if threshold_value is None:
return None
numeric_value = float(threshold_value)
if not math.isfinite(numeric_value):
raise ValueError(f'Output threshold "{threshold_name}" must be finite')
if numeric_value < 0:
raise ValueError(f'Output threshold "{threshold_name}" must be non-negative')
return numeric_value

def _resolve_output_threshold(self, threshold_type: str) -> float:
"""Resolve threshold: TOML config if set, else hardcoded default."""
if threshold_type not in OUTPUT_THRESHOLD_DEFAULTS:
raise ValueError(f'Unknown output threshold type: {threshold_type}')

toml_key = f'output_threshold_{threshold_type}'
toml_value = self._validate_threshold(
toml_key,
getattr(self.config, toml_key, None),
)
if toml_value is not None:
return toml_value

return OUTPUT_THRESHOLD_DEFAULTS[threshold_type]

def write_results(
self,
model: TemoaModel,
Expand All @@ -252,7 +300,11 @@ def write_results(
else:
p_0 = None

e_costs, e_flows = poll_emissions(model=model, p_0=value(p_0))
e_costs, e_flows = poll_emissions(
model=model,
p_0=value(p_0),
epsilon=self.output_threshold_emission,
)
self.emission_register = e_flows
self.write_emissions(iteration=iteration)

Expand All @@ -278,7 +330,10 @@ def write_mm_results(self, model: TemoaModel, iteration: int) -> None:
if not self.tech_sectors:
self._set_tech_sectors()
self.write_objective(model, iteration=iteration)
_e_costs, e_flows = poll_emissions(model=model)
_e_costs, e_flows = poll_emissions(
model=model,
epsilon=self.output_threshold_emission,
)
self.emission_register = e_flows
self.write_emissions(iteration=iteration)
finally:
Expand Down Expand Up @@ -400,7 +455,7 @@ def write_emissions(self, iteration: int | None = None) -> None:
records = []

for ei, val in self.emission_register.items():
if abs(val) < self.epsilon:
if abs(val) < self.output_threshold_emission:
continue

row = {
Expand All @@ -425,7 +480,7 @@ def write_emissions(self, iteration: int | None = None) -> None:
self.connection.commit()

def write_capacity_tables(self, model: TemoaModel, iteration: int | None = None) -> None:
cap_data = poll_capacity_results(model=model)
cap_data = poll_capacity_results(model=model, epsilon=self.output_threshold_capacity)
self._insert_capacity_results(cap_data=cap_data, iteration=iteration)

def _insert_capacity_results(self, cap_data: CapData, iteration: int | None) -> None:
Expand Down Expand Up @@ -508,7 +563,7 @@ def write_flow_tables(self, iteration: int | None = None) -> None:
sector = self.tech_sectors.get(fi.t)

for flow_type, val in flows.items():
if abs(val) < self.epsilon:
if abs(val) < self.output_threshold_activity:
continue

table_name = map_flow_to_table.get(flow_type)
Expand Down Expand Up @@ -593,7 +648,7 @@ def _insert_summary_flow_results(

records = []
for (r, p, i, t, v, o), val in output_flows.items():
if abs(val) < self.epsilon:
if abs(val) < self.output_threshold_activity:
continue
records.append(
{
Expand Down Expand Up @@ -643,7 +698,7 @@ def check_flow_balance(self, model: TemoaModel) -> bool:
return all_good

def calculate_flows(self, model: TemoaModel) -> dict[FI, dict[FlowType, float]]:
return poll_flow_results(model, self.epsilon)
return poll_flow_results(model, self.output_threshold_activity)

def write_costs(
self,
Expand All @@ -657,7 +712,7 @@ def write_costs(
else:
p_0 = min(model.time_optimize)

entries, exchange_entries = poll_cost_results(model, value(p_0), self.epsilon)
entries, exchange_entries = poll_cost_results(model, value(p_0), self.output_threshold_cost)
self._insert_cost_results(entries, exchange_entries, emission_entries, iteration)

def _insert_cost_results(
Expand Down Expand Up @@ -697,6 +752,18 @@ def _write_cost_rows(

for r, p, t, v in sorted_keys:
costs = entries[(r, p, t, v)]
row_values = (
costs.get(CostType.D_INVEST, 0),
costs.get(CostType.D_FIXED, 0),
costs.get(CostType.D_VARIABLE, 0),
costs.get(CostType.D_EMISS, 0),
costs.get(CostType.INVEST, 0),
costs.get(CostType.FIXED, 0),
costs.get(CostType.VARIABLE, 0),
costs.get(CostType.EMISS, 0),
)
if all(abs(val) < self.output_threshold_cost for val in row_values):
continue
records.append(
{
'scenario': scenario,
Expand Down
8 changes: 8 additions & 0 deletions temoa/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def __init__(
graphviz_output: bool = False,
cycle_count_limit: int = 100,
cycle_length_limit: int = 1,
output_threshold_capacity: float | None = None,
output_threshold_activity: float | None = None,
output_threshold_emission: float | None = None,
output_threshold_cost: float | None = None,
):
if '-' in scenario:
raise ValueError(
Expand Down Expand Up @@ -148,6 +152,10 @@ def __init__(
self.plot_commodity_network = plot_commodity_network and self.source_trace
self.graphviz_output = graphviz_output
self.stochastic_config = stochastic_config
self.output_threshold_capacity = output_threshold_capacity
self.output_threshold_activity = output_threshold_activity
self.output_threshold_emission = output_threshold_emission
self.output_threshold_cost = output_threshold_cost

# Cycle detection limits
if not isinstance(cycle_count_limit, int) or cycle_count_limit < -1:
Expand Down
22 changes: 17 additions & 5 deletions temoa/extensions/myopic/myopic_sequencer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ class MyopicSequencer:
]

def __init__(self, config: TemoaConfig | None):
self.capacity_epsilon = 1e-5
# Minimum capacity (MW) to carry forward between myopic periods.
# Configurable via [myopic] capacity_threshold in TOML.
default_cap_threshold = 1e-3
if config and config.myopic_inputs:
self.capacity_epsilon = config.myopic_inputs.get(
'capacity_threshold', default_cap_threshold
)
else:
self.capacity_epsilon = default_cap_threshold
Comment on lines +67 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing validation for capacity_threshold from config.

TableWriter._validate_threshold() rejects negative and non-finite values, but here the value from config.myopic_inputs.get('capacity_threshold', ...) is used directly without validation. If a user configures a negative, NaN, or inf value, the SQL query behavior becomes undefined (e.g., ABS(capacity) >= NaN is always false).

Consider adding validation consistent with TableWriter:

♻️ Proposed fix to add validation
+import math
+
+def _validate_capacity_threshold(value: float, default: float) -> float:
+    """Validate capacity threshold, returning default if invalid."""
+    if not isinstance(value, (int, float)):
+        return default
+    if not math.isfinite(value) or value < 0:
+        return default
+    return float(value)
+
 class MyopicSequencer:
     ...
     def __init__(self, config: TemoaConfig | None):
         # Minimum capacity (MW) to carry forward between myopic periods.
         # Configurable via [myopic] capacity_threshold in TOML.
         default_cap_threshold = 1e-3
         if config and config.myopic_inputs:
-            self.capacity_epsilon = config.myopic_inputs.get(
+            raw_threshold = config.myopic_inputs.get(
                 'capacity_threshold', default_cap_threshold
             )
+            self.capacity_epsilon = _validate_capacity_threshold(raw_threshold, default_cap_threshold)
         else:
             self.capacity_epsilon = default_cap_threshold
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Minimum capacity (MW) to carry forward between myopic periods.
# Configurable via [myopic] capacity_threshold in TOML.
default_cap_threshold = 1e-3
if config and config.myopic_inputs:
self.capacity_epsilon = config.myopic_inputs.get(
'capacity_threshold', default_cap_threshold
)
else:
self.capacity_epsilon = default_cap_threshold
# Minimum capacity (MW) to carry forward between myopic periods.
# Configurable via [myopic] capacity_threshold in TOML.
default_cap_threshold = 1e-3
if config and config.myopic_inputs:
raw_threshold = config.myopic_inputs.get(
'capacity_threshold', default_cap_threshold
)
self.capacity_epsilon = _validate_capacity_threshold(raw_threshold, default_cap_threshold)
else:
self.capacity_epsilon = default_cap_threshold
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temoa/extensions/myopic/myopic_sequencer.py` around lines 67 - 75, The value
assigned to self.capacity_epsilon must be validated like
TableWriter._validate_threshold(): retrieve the raw value from
config.myopic_inputs.get('capacity_threshold', default_cap_threshold), ensure it
is finite (not NaN/inf) and >= 0, and either call/reuse
TableWriter._validate_threshold(raw_value) or perform the same checks and raise
ValueError (or fall back to default_cap_threshold) if invalid; update the
assignment of capacity_epsilon accordingly so negative or non-finite config
values cannot be used in later SQL comparisons.

self.debugging = False
self.optimization_periods: list[int] | None = None
self.instance_queue: deque[MyopicIndex] = deque() # a LIFO queue
Expand Down Expand Up @@ -387,7 +395,7 @@ def update_myopic_efficiency_table(self, myopic_index: MyopicIndex, prev_base: i
'DELETE FROM myopic_efficiency '
'WHERE (SELECT region, tech, vintage) '
' NOT IN (SELECT region, tech, vintage FROM output_net_capacity '
' WHERE period = ? AND scenario = ?) '
' WHERE period = ? AND scenario = ? AND ABS(capacity) >= ?) '
'AND tech not in (SELECT tech FROM main.technology where unlim_cap > 0)'
)

Expand All @@ -396,16 +404,20 @@ def update_myopic_efficiency_table(self, myopic_index: MyopicIndex, prev_base: i
'SELECT * FROM myopic_efficiency '
'WHERE (SELECT region, tech, vintage) '
' NOT IN (SELECT region, tech, vintage FROM output_net_capacity '
' WHERE period = ? AND scenario = ?) '
' WHERE period = ? AND scenario = ? AND ABS(capacity) >= ?) '
'AND tech not in (SELECT tech FROM Technology where unlim_cap > 0)'
)
print('\n\n **** Removing these unused region-tech-vintage combos ****')
removals = self.cursor.execute(
debug_query, (last_interval_end, self.config.scenario)
debug_query,
(last_interval_end, self.config.scenario, self.capacity_epsilon),
).fetchall()
for i, removal in enumerate(removals):
print(f'{i}. Removing: {removal}')
self.cursor.execute(delete_qry, (last_interval_end, self.config.scenario))
self.cursor.execute(
delete_qry,
(last_interval_end, self.config.scenario, self.capacity_epsilon),
)
self.output_con.commit()

# 2. Add the new stuff now visible
Expand Down
7 changes: 7 additions & 0 deletions temoa/tutorial_assets/config_sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ save_lp_file = false
# graphviz dot file and svg for network visualization (requires graphviz to be installed separately)
graphviz_output = false

# Optional output filtering thresholds (set to 0 to disable per category)
# Precedence is: TOML value > internal defaults.
output_threshold_capacity = 0.001
output_threshold_activity = 0.001
output_threshold_emission = 0.001
output_threshold_cost = 0.01

# ------------------------------------
# MODEL PARAMETERS
# these are specific to each model
Expand Down
Loading