Skip to content

Commit cb700b6

Browse files
fixes from PR feedback
1 parent 9c0e47f commit cb700b6

File tree

8 files changed

+242
-108
lines changed

8 files changed

+242
-108
lines changed

temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,6 @@ def __init__(self, config: TemoaConfig):
120120
self.mga_weighting.name,
121121
)
122122

123-
def _run_unit_check(self) -> None:
124-
"""Run unit checking on the database if configured."""
125-
if self.config.check_units:
126-
from temoa.model_checking.unit_checking.screener import screen
127-
128-
logger.info('Running units consistency check on input database...')
129-
report_path = self.config.output_path / 'unit_check_reports'
130-
success = screen(self.config.input_database, report_path=report_path)
131-
132-
if not success:
133-
logger.warning(
134-
'Units check found errors. See detailed report at: %s',
135-
report_path,
136-
)
137-
logger.warning('Continuing with model build despite unit check warnings...')
138-
else:
139-
logger.info('Units check completed successfully - no errors found.')
140-
141123
def start(self):
142124
"""Run the sequencer"""
143125
# ==== basic sequence ====
@@ -147,9 +129,6 @@ def start(self):
147129
# 4. Instantiate a Manager to pull in more instances
148130
# 5. Start the re-solve loop
149131

150-
# Run unit checking if configured
151-
self._run_unit_check()
152-
153132
start_time = datetime.now()
154133

155134
# 1. Load data

temoa/extensions/myopic/myopic_sequencer.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -137,28 +137,7 @@ def get_connection(self) -> Connection:
137137

138138
return con
139139

140-
def _run_unit_check(self) -> None:
141-
"""Run unit checking on the database if configured."""
142-
if self.config.check_units:
143-
from temoa.model_checking.unit_checking.screener import screen
144-
145-
logger.info('Running units consistency check on input database...')
146-
report_path = self.config.output_path / 'unit_check_reports'
147-
success = screen(self.config.input_database, report_path=report_path)
148-
149-
if not success:
150-
logger.warning(
151-
'Units check found errors. See detailed report at: %s',
152-
report_path,
153-
)
154-
logger.warning('Continuing with model build despite unit check warnings...')
155-
else:
156-
logger.info('Units check completed successfully - no errors found.')
157-
158140
def start(self):
159-
# Run unit checking if configured
160-
self._run_unit_check()
161-
162141
# load up the instance queue
163142
self.characterize_run()
164143

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
from importlib import resources as importlib_resources
22

33
from pint import UnitRegistry
4+
from pint.errors import DefinitionSyntaxError
45

6+
# UnitRegistry is generic but doesn't require type args at instantiation
57
ureg: UnitRegistry = UnitRegistry() # type: ignore[type-arg]
8+
69
# Load custom unit definitions from the package resources
7-
data = importlib_resources.files('temoa.model_checking.unit_checking').joinpath('temoa_units.txt')
8-
ureg.load_definitions(str(data))
10+
_resource_path = 'temoa.model_checking.unit_checking/temoa_units.txt'
11+
try:
12+
data = importlib_resources.files('temoa.model_checking.unit_checking').joinpath(
13+
'temoa_units.txt'
14+
)
15+
# Ensure we have a real filesystem path (handles zipped resources too)
16+
with importlib_resources.as_file(data) as path:
17+
ureg.load_definitions(path)
18+
except (FileNotFoundError, OSError, DefinitionSyntaxError) as exc:
19+
raise RuntimeError(
20+
f'Failed to load custom Temoa unit definitions from {_resource_path!r}. '
21+
'This may indicate a broken installation or missing resource file.'
22+
) from exc

temoa/model_checking/unit_checking/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class UnitsFormat:
143143
ACCEPTABLE_CHARACTERS = r'^\s*([A-Za-z0-9\*\^\_\s\/\(\)]+?)\s*$'
144144

145145

146-
def consolidate_lines(line_nums: Sequence[str | int]) -> str:
146+
def consolidate_lines(line_nums: Sequence[int]) -> str:
147147
"""A little sand wedge function to prevent lists of many line numbers
148148
and maxing at 5 or 5 + 'more'"""
149149
listed_lines = (

temoa/model_checking/unit_checking/relations_checker.py

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
SINGLE_ELEMENT,
1818
CostTableData,
1919
RelationType,
20+
activity_based_tables,
21+
capacity_based_tables,
22+
commodity_based_tables,
2023
consolidate_lines,
2124
)
2225
from temoa.model_checking.unit_checking.entry_checker import (
@@ -94,8 +97,20 @@ def check_efficiency_table(
9497
continue
9598

9699
# check that our tech matches the units of the connected commodities
97-
invalid_input_flag = input_units != comm_units[ic]
98-
invalid_output_flag = output_units != comm_units[oc]
100+
expected_input = comm_units.get(ic)
101+
expected_output = comm_units.get(oc)
102+
if expected_input is None or expected_output is None:
103+
invalid_rows.append(idx)
104+
logger.warning(
105+
'Missing commodity units for input_comm=%s or output_comm=%s in efficiency row %d',
106+
ic,
107+
oc,
108+
idx,
109+
)
110+
continue
111+
112+
invalid_input_flag = input_units != expected_input
113+
invalid_output_flag = output_units != expected_output
99114
if invalid_input_flag or invalid_output_flag:
100115
logger.warning(
101116
'Efficiency units conflict with associated commodity for Technology %s near row %d',
@@ -107,9 +122,9 @@ def check_efficiency_table(
107122
f'{tech:^20} ----> {f"{oc} [{output_units}]": ^25}'
108123
)
109124
if invalid_input_flag:
110-
msg += f'\n Invalid input units: {comm_units[ic]}'
125+
msg += f'\n Invalid input units: {expected_input}'
111126
if invalid_output_flag:
112-
msg += f'\n Invalid output units: {comm_units[oc]}'
127+
msg += f'\n Invalid output units: {expected_output}'
113128
error_msgs.append(msg)
114129

115130
# check that the output of this technology is consistent in units with
@@ -165,7 +180,12 @@ def check_inter_table_relations(
165180
- Missing tables (e.g., some databases may not have all limit tables)
166181
- Schema variations between v3.1 and v4.0
167182
"""
168-
grouped_errors = defaultdict(list)
183+
# Validate table_name against known safe tables
184+
valid_tables = activity_based_tables + capacity_based_tables + commodity_based_tables
185+
if table_name not in valid_tables:
186+
raise ValueError(f'Invalid table name: {table_name}')
187+
188+
grouped_errors: defaultdict[str, list[int]] = defaultdict(list)
169189

170190
# Build query based on relation type, with robustness checks
171191
match relation_type:
@@ -206,11 +226,9 @@ def check_inter_table_relations(
206226

207227
try:
208228
rows = conn.execute(query).fetchall()
209-
except sqlite3.OperationalError as e:
229+
except sqlite3.OperationalError as _:
210230
# Log the error but don't fail the entire check
211-
logger.error(
212-
'failed to process query: %s when processing table %s: %s', query, table_name, e
213-
)
231+
logger.exception('failed to process query: %s when processing table %s', query, table_name)
214232
msg = (
215233
f'Failed to process table {table_name} due to SQL error. '
216234
f'This may indicate missing columns or incompatible schema. '
@@ -232,7 +250,7 @@ def check_inter_table_relations(
232250
continue
233251
expected_units = io_units.output_units
234252
case RelationType.ACTIVITY:
235-
io_units = tech_lut[tech_or_comm]
253+
io_units = tech_lut.get(tech_or_comm)
236254
if not io_units:
237255
grouped_errors[
238256
f'Unprocessed row (missing reference for tech '
@@ -261,25 +279,28 @@ def check_inter_table_relations(
261279
if c2a_units:
262280
c2a_valid, units_data = validate_units_format(c2a_units, SINGLE_ELEMENT)
263281
if c2a_valid and units_data is not None and len(units_data) >= 1:
264-
# further ensure the conversion is valid and retain the validity
265-
c2a_valid, valid_c2a_units = validate_units_expression(units_data[0])
282+
# further ensure the conversion is valid and retain the appropriate units object
283+
_, valid_c2a_units = validate_units_expression(units_data[0])
284+
if not valid_c2a_units:
285+
grouped_errors[
286+
f'Invalid units or unit format for c2a table: {c2a_units}'
287+
].append(idx)
288+
continue
266289
else:
267-
valid_c2a_units = None
268-
else: # we are in a valid state: no C2A units provided/needed
269-
c2a_valid = True
290+
grouped_errors[f'Invalid units or unit format for c2a table: {c2a_units}'].append(
291+
idx
292+
)
293+
continue
294+
else:
270295
valid_c2a_units = None
271296

272297
if not valid_table_units:
273-
grouped_errors['Unprocessed row (invalid units--see earlier tests)'].append(idx)
274-
if not c2a_valid:
275-
grouped_errors['Unprocessed row (invalid c2a units--see earlier tests)'].append(idx)
276-
if not valid_table_units or not c2a_valid:
298+
grouped_errors[f'Invalid units or unit format: {table_units}'].append(idx)
277299
continue
278300

279301
# if we have valid c2a units, combine them to get the units of activity
280302
if valid_c2a_units:
281303
res_units = valid_table_units * (valid_c2a_units * ureg.year)
282-
283304
else:
284305
res_units = valid_table_units
285306

@@ -356,7 +377,7 @@ def check_cost_tables(
356377
try:
357378
rows = conn.execute(query).fetchall()
358379
except sqlite3.OperationalError:
359-
logger.error(
380+
logger.exception(
360381
'failed to process query: %s when processing table %s', query, ct.table_name
361382
)
362383
msg = f'Failed to process table {ct.table_name}. See log for failed query.'
@@ -385,32 +406,51 @@ def check_cost_tables(
385406
continue
386407

387408
# Test 1: Look for cost commonality
388-
if common_cost_unit is None and cost_units is not None:
389-
# try to establish it
390-
# Check that the units contain currency dimension
391-
# Fixed: Changed from .check('[currency]') which only works for pure currency
392-
# to checking dimensionality dict which works for composite units
393-
# Example: 'Mdollar / (PJ^2/GW)' has currency in dimensionality but fails .check()
394-
try:
395-
if '[currency]' in cost_units.dimensionality:
396-
common_cost_unit = cost_units
397-
else:
398-
# Units don't contain currency dimension - invalid for cost table
399-
error_msgs.append(
400-
f'{ct.table_name}: Unprocessed row '
401-
f'(units lack currency dimension): {cost_units} at row: {idx}'
409+
# extract the cost units
410+
if not cost_units:
411+
label = (
412+
f'{ct.table_name}: Unprocessed row '
413+
f'(missing cost units): {raw_units_expression}'
414+
)
415+
table_grouped_errors[label].append(idx)
416+
continue
417+
418+
# Get cost unit object
419+
# cost_units is already a pint Unit object from validate_units_expression(elements[0])
420+
cost_unit_obj = cost_units
421+
422+
# Check for currency dimension
423+
if '[currency]' not in cost_unit_obj.dimensionality:
424+
label = (
425+
f'{ct.table_name}: Cost units must have currency dimension. '
426+
f'Found: {cost_unit_obj}'
427+
)
428+
table_grouped_errors[label].append(idx)
429+
continue
430+
431+
# Initialize common_cost_unit on first valid row
432+
if common_cost_unit is None:
433+
common_cost_unit = cost_unit_obj
434+
# No need to continue here, as we still need to check measure units
435+
else:
436+
# Validate subsequent rows against the established common unit
437+
if cost_unit_obj != common_cost_unit:
438+
# Try to see if they're equivalent but differently expressed
439+
try:
440+
# Attempt conversion to check if they're compatible
441+
(1.0 * cost_unit_obj).to(common_cost_unit)
442+
except (ValueError, AttributeError, TypeError) as e:
443+
# Not compatible - this is an error
444+
label = (
445+
f'{ct.table_name}: Inconsistent cost units: {cost_unit_obj} '
446+
f'does not match common cost unit {common_cost_unit}. Error: {e}'
402447
)
448+
table_grouped_errors[label].append(idx)
403449
continue
404-
except Exception:
405-
error_msgs.append(
406-
f'{ct.table_name}: Could not check cost units: {cost_units} at row: {idx}'
407-
)
408-
continue
409-
if cost_units != common_cost_unit:
450+
# If compatible but not strictly equal, we still flag it as non-standard
410451
label = (
411452
f'{ct.table_name}: Non-standard cost found '
412453
f'(expected common cost units of {common_cost_unit}) '
413-
f'got: {cost_units}'
414454
)
415455
table_grouped_errors[label].append(idx)
416456

0 commit comments

Comments
 (0)