Skip to content

Add configurable demand resolution and DemandActivity mode#274

Open
SutubraResearch wants to merge 1 commit intoTemoaProject:unstablefrom
SutubraResearch:pr/demand-formulation
Open

Add configurable demand resolution and DemandActivity mode#274
SutubraResearch wants to merge 1 commit intoTemoaProject:unstablefrom
SutubraResearch:pr/demand-formulation

Conversation

@SutubraResearch
Copy link
Collaborator

@SutubraResearch SutubraResearch commented Mar 11, 2026

Summary

Restructure the demand constraint formulation for significantly better barrier solve performance:

  • Timeslice-level demand: Replace the annual-level demand constraint (where every timeslice variable appeared in a single summed constraint creating a dense matrix column) with a timeslice-level formulation that directly sets v_flow_out per (region, period, season, day) slice. This eliminates the dense column and improves barrier solve performance by 25-43% on national-scale models.
  • Reference-timeslice DemandActivity: Rewrite DemandActivity to use a reference-timeslice formulation that ties each technology's timeslice output to its annual total via a single representative timeslice, rather than requiring all timeslice outputs to match demand shares simultaneously.

The annual formulation created a dense column in the constraint matrix because every timeslice flow variable for a demand commodity appeared in one summed row. Barrier methods (Cholesky factorization) are sensitive to such dense columns. The timeslice-level formulation produces a sparser matrix with equivalent feasible region.

Files changed

File Change
temoa/components/commodities.py Demand constraint + DemandActivity rewrite
temoa/components/flows.py Restrict annual flow vars to tech_annual
temoa/core/model.py New demand_constraint_rpsd_dem set
tests/legacy_test_values.py Updated constraint/variable counts
tests/test_full_runs.py Updated test assertions
tests/test_material_results.py Updated test assertions

Performance

Tested on a 16-region, 52-week national model:

  • Period 1 (2027): 2,456s vs 3,698s upstream (1.5x faster)
  • Combined two-period myopic: 6,162s vs 7,683s (1.25x faster)
  • The demand formulation change accounts for 25-43% of the improvement

Test plan

  • All 190 tests pass
  • Results validated against mip-dev reference (all gen shares within ±0.23pp nationally)
  • Set cache JSON files regenerated

Summary by CodeRabbit

Release Notes

  • Optimization

    • Enhanced demand constraint handling for single-technology scenarios, reducing overall constraint counts and improving model efficiency.
  • Refactor

    • Streamlined initialization code formatting in core model components for better readability and maintainability.
  • Tests

    • Updated baseline constraint counts across test scenarios to reflect optimization improvements.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Walkthrough

The main change optimizes demand-activity constraint generation in commodities.py by explicitly handling a special case: single non-annual upstream techs with exactly one input and no annual co-techs. In such cases, flow values are directly fixed and constraint indices are omitted. Supporting changes include formatting updates to Set initialization in model.py and constraint count adjustments in test expectations.

Changes

Cohort / File(s) Summary
Demand Activity Constraint Optimization
temoa/components/commodities.py
Refactored demand_activity_constraint_indices from set-comprehension to explicit loop. Detects single non-annual upstream with one input and no annual co-techs, directly fixing flow values instead of creating constraints. Includes docstring documenting the optimization for single-tech demand drops.
Set Initialization Formatting
temoa/core/model.py
Updated commodity_sink and commodity_carrier Set declarations from multi-line to single-line initialization format. No semantic changes, purely formatting adjustment.
Test Constraint Count Updates
tests/legacy_test_values.py
Updated expected constraint counts reflecting the demand-activity constraint reduction: utopia (1486 → 1474), mediumville (240 → 232), seasonal_storage (183 → 175). Each includes comment noting reduction due to single-tech demand optimization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

refactor

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'configurable demand resolution and DemandActivity mode,' but the changeset does not add configurability—it implements a specific fixed restructuring of demand constraints and DemandActivity formulation. Revise the title to reflect the actual change: 'Restructure demand constraints and DemandActivity for improved barrier-solver performance' or similar, without implying configurability that isn't present.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SutubraResearch SutubraResearch force-pushed the pr/demand-formulation branch 3 times, most recently from 1826e11 to a95dbfd Compare March 12, 2026 17:24
@SutubraResearch SutubraResearch changed the title Rewrite demand constraints to timeslice level Add configurable demand resolution and DemandActivity mode Mar 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@temoa/components/commodities.py`:
- Around line 292-303: The supply_annual term is being scaled by
model.segment_fraction but the RHS uses model.demand_specific_distribution,
causing inconsistent timeslice scaling; update the calculation that builds
supply_annual (the expression using model.v_flow_out_annual,
model.commodity_up_stream_process, model.tech_annual, and
model.process_inputs_by_output) to multiply by
value(model.demand_specific_distribution[r, p, s, d, dem]) instead of
value(model.segment_fraction[p, s, d, dem]) so the LHS uses the same demand
share as the RHS and matches the other annual-output handling; keep the call to
demand_constraint_timeslice_error_check(supply + supply_annual, r, p, s, d, dem)
unchanged.
- Around line 162-169: The current fast-path collapses vintages by building
distinct_techs = {t for t, v in non_annual} and skipping DemandActivity when
len(distinct_techs) <= 1; change the skip condition to consider vintages so we
only skip when there is at most one non-annual producer (t,v). Update the check
near the non_annual computation in commodities.py (the block that builds
non_annual from model.commodity_up_stream_process and then does the continue) to
either use len(non_annual) <= 1 or create a set of (t,v) pairs and check its
length <= 1, ensuring DemandActivity rows are emitted whenever multiple vintages
exist for the same technology.

In `@temoa/data_io/hybrid_loader.py`:
- Around line 269-274: The code currently passes config.demand_resolution and
config.demand_activity through to _load_component_data without validation; add
explicit validation before those calls: check (self.config.demand_resolution or
'annual') is one of {'annual','timeslice'} and (self.config.demand_activity or
'on') is one of {'on','off'}, and if not raise a clear ValueError (or
ConfigError) mentioning the invalid value and the expected set; only then call
self._load_component_data for model.demand_resolution and
model.demand_activity_mode so downstream branching in components like
temoa/components/flows.py and temoa/components/commodities.py behaves
predictably.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 07201fc4-4ed2-4625-9227-d517faebaecf

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7ff86 and a95dbfd.

📒 Files selected for processing (11)
  • temoa/components/commodities.py
  • temoa/components/flows.py
  • temoa/core/config.py
  • temoa/core/model.py
  • temoa/data_io/hybrid_loader.py
  • tests/legacy_test_values.py
  • tests/test_full_runs.py
  • tests/test_material_results.py
  • tests/testing_data/mediumville_sets.json
  • tests/testing_data/test_system_sets.json
  • tests/testing_data/utopia_sets.json
💤 Files with no reviewable changes (1)
  • tests/legacy_test_values.py

Comment on lines +52 to +53
demand_resolution: str | None = None,
demand_activity: str | None = None,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate these new demand-mode switches before storing them.

Unlike scenario_mode, these values are accepted verbatim. Downstream code branches on exact literals, and the branching is asymmetric: a bad demand_resolution can make demand_constraint() take the annual path while demand_constraint_timeslice_indices() still builds timeslice rows. Please keep None as “use default” if you want, but normalize and reject any non-None value outside {annual, timeslice} / {on, off}.

🛡️ Suggested validation
         self.save_lp_file = save_lp_file
         self.time_sequencing = time_sequencing
         self.reserve_margin = reserve_margin
+        if demand_resolution is not None:
+            demand_resolution = demand_resolution.lower()
+            if demand_resolution not in {'annual', 'timeslice'}:
+                raise ValueError(
+                    "demand_resolution must be one of {'annual', 'timeslice'}"
+                )
+        if demand_activity is not None:
+            demand_activity = demand_activity.lower()
+            if demand_activity not in {'on', 'off'}:
+                raise ValueError("demand_activity must be one of {'on', 'off'}")
         self.demand_resolution = demand_resolution
         self.demand_activity = demand_activity

Also applies to: 134-135

Comment on lines +269 to +274
self._load_component_data(
data, model.demand_resolution, [(self.config.demand_resolution or 'annual',)]
)
self._load_component_data(
data, model.demand_activity_mode, [(self.config.demand_activity or 'on',)]
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject invalid demand-mode values before loading them.

Lines 269-274 accept any string, but downstream code branches on exact equality. A typo like annnual will not fail here: temoa/components/flows.py falls into the timeslice branch, while temoa/components/commodities.py still leaves the annual demand constraint active and also enables timeslice demand indices. That can change the formulation instead of producing a clear configuration error. Validate both switches here against {'annual', 'timeslice'} and {'on', 'off'} before populating the model sets.

Proposed fix
-        self._load_component_data(
-            data, model.demand_resolution, [(self.config.demand_resolution or 'annual',)]
-        )
-        self._load_component_data(
-            data, model.demand_activity_mode, [(self.config.demand_activity or 'on',)]
-        )
+        demand_resolution = self.config.demand_resolution or 'annual'
+        if demand_resolution not in {'annual', 'timeslice'}:
+            raise ValueError(
+                f"Invalid demand_resolution '{demand_resolution}'. "
+                "Expected 'annual' or 'timeslice'."
+            )
+        self._load_component_data(data, model.demand_resolution, [(demand_resolution,)])
+
+        demand_activity = self.config.demand_activity or 'on'
+        if demand_activity not in {'on', 'off'}:
+            raise ValueError(
+                f"Invalid demand_activity '{demand_activity}'. "
+                "Expected 'on' or 'off'."
+            )
+        self._load_component_data(data, model.demand_activity_mode, [(demand_activity,)])
📝 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
self._load_component_data(
data, model.demand_resolution, [(self.config.demand_resolution or 'annual',)]
)
self._load_component_data(
data, model.demand_activity_mode, [(self.config.demand_activity or 'on',)]
)
demand_resolution = self.config.demand_resolution or 'annual'
if demand_resolution not in {'annual', 'timeslice'}:
raise ValueError(
f"Invalid demand_resolution '{demand_resolution}'. "
"Expected 'annual' or 'timeslice'."
)
self._load_component_data(data, model.demand_resolution, [(demand_resolution,)])
demand_activity = self.config.demand_activity or 'on'
if demand_activity not in {'on', 'off'}:
raise ValueError(
f"Invalid demand_activity '{demand_activity}'. "
"Expected 'on' or 'off'."
)
self._load_component_data(data, model.demand_activity_mode, [(demand_activity,)])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temoa/data_io/hybrid_loader.py` around lines 269 - 274, The code currently
passes config.demand_resolution and config.demand_activity through to
_load_component_data without validation; add explicit validation before those
calls: check (self.config.demand_resolution or 'annual') is one of
{'annual','timeslice'} and (self.config.demand_activity or 'on') is one of
{'on','off'}, and if not raise a clear ValueError (or ConfigError) mentioning
the invalid value and the expected set; only then call self._load_component_data
for model.demand_resolution and model.demand_activity_mode so downstream
branching in components like temoa/components/flows.py and
temoa/components/commodities.py behaves predictably.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@temoa/components/commodities.py`:
- Around line 111-128: The index generator demand_activity_constraint_indices
currently mutates model state by fixing variables (model.v_flow_out_annual and
model.v_flow_out) while iterating model.demand_constraint_rpc and checking
model.process_inputs_by_output, model.tech_annual, model.demand and
model.demand_specific_distribution; extract that side-effecting logic into a
separate BuildAction (or initialization function) that runs during model
construction and performs the same checks (single non-annual upstream, single
input) and calls .fix(...) on model.v_flow_out_annual[...] and
model.v_flow_out[...] using value(model.demand[...]) and
value(model.demand_specific_distribution[...]) so the original
demand_activity_constraint_indices becomes a pure index-returner (or
alternatively rename it to demand_activity_constraint_indices_and_fix if you
prefer to keep behavior together).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a75c3025-de03-455e-aa51-a9f29f6cd907

📥 Commits

Reviewing files that changed from the base of the PR and between 3903bfc and 27dbb14.

📒 Files selected for processing (3)
  • temoa/components/commodities.py
  • temoa/core/model.py
  • tests/legacy_test_values.py

Comment on lines +111 to +128
indices = set()
for r, p, dem in model.demand_constraint_rpc:
upstream = list(model.commodity_up_stream_process[r, p, dem])
non_annual = [(t, v) for t, v in upstream if t not in model.tech_annual]
has_annual = any(t in model.tech_annual for t, _ in upstream)
# Single non-annual (t,v) with one input, no annual co-techs: fix variables
if len(non_annual) == 1 and not has_annual:
t, v = non_annual[0]
inputs = list(model.process_inputs_by_output[r, p, t, v, dem])
if len(inputs) == 1:
i = inputs[0]
dem_val = value(model.demand[r, p, dem])
model.v_flow_out_annual[r, p, i, t, v, dem].fix(dem_val)
for s in model.time_season[p]:
for d in model.time_of_day:
dsd = value(model.demand_specific_distribution[r, p, s, d, dem])
model.v_flow_out[r, p, s, d, i, t, v, dem].fix(dem_val * dsd)
continue
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Side effect in index function: acceptable but worth noting.

Fixing variables within demand_activity_constraint_indices is unconventional since the function name suggests it only returns indices. This works correctly because Pyomo's Set(initialize=...) calls the function exactly once during model construction.

For maintainability, consider either:

  1. Renaming to demand_activity_constraint_indices_and_fix to reflect the dual purpose, or
  2. Extracting the variable-fixing logic to a separate BuildAction.

Not blocking since the current approach is functionally correct and achieves the intended constraint reduction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@temoa/components/commodities.py` around lines 111 - 128, The index generator
demand_activity_constraint_indices currently mutates model state by fixing
variables (model.v_flow_out_annual and model.v_flow_out) while iterating
model.demand_constraint_rpc and checking model.process_inputs_by_output,
model.tech_annual, model.demand and model.demand_specific_distribution; extract
that side-effecting logic into a separate BuildAction (or initialization
function) that runs during model construction and performs the same checks
(single non-annual upstream, single input) and calls .fix(...) on
model.v_flow_out_annual[...] and model.v_flow_out[...] using
value(model.demand[...]) and value(model.demand_specific_distribution[...]) so
the original demand_activity_constraint_indices becomes a pure index-returner
(or alternatively rename it to demand_activity_constraint_indices_and_fix if you
prefer to keep behavior together).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant