Skip to content

Add configurable demand resolution and DemandActivity mode#274

Merged
ParticularlyPythonicBS merged 7 commits intoTemoaProject:unstablefrom
SutubraResearch:pr/demand-formulation
Mar 20, 2026
Merged

Add configurable demand resolution and DemandActivity mode#274
ParticularlyPythonicBS merged 7 commits 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

  • Optimization

    • Reduced generated demand constraints and demand-activity entries for single-upstream demand cases, lowering overall constraint counts and improving model runtime.
  • Refactor

    • Minor initialization formatting cleanup in core model declarations.
  • Tests

    • Updated expected constraint counts and trimmed test dataset entries to match the new constraint footprint.
  • Documentation

    • Added a docstring describing the single-technology demand handling behavior.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds detection of “singleton” demands (demands with a single upstream producer) that fixes their annual and time-slice flow variables and omits DemandActivity constraint generation for those demands; registers a BuildAction to run this check during model construction and updates tests/data accordingly.

Changes

Cohort / File(s) Summary
Singleton-demand logic
temoa/components/commodities.py
Added check_singleton_demands(model) which finds demands with exactly one upstream (i,t,v), fixes v_flow_out_annual and time-slice v_flow_out to demand-scalars, records (r,p,dem) in model.singleton_demands, and skips creating demand activity indices for those demands. Also updated imports and added docstring.
Model integration
temoa/core/model.py
Added singleton_demands set and a BuildAction check_singleton_demands = BuildAction(rule=commodities.check_singleton_demands) to run before demand constraints; condensed Set initializations formatting for commodity_sink and commodity_carrier.
Type exports
temoa/types/set_types.py, temoa/types/__init__.py
Added new type alias SingletonDemandsSet = set[tuple[Region, Period, Commodity]] and exported SingletonDemandsSet in __all__.
Tests / test data
tests/legacy_test_values.py, tests/testing_data/mediumville_sets.json, tests/testing_data/utopia_sets.json
Updated expected constraint counts in legacy tests (reduced counts) and removed several demand tuples from mediumville and utopia JSON test sets corresponding to RL/RL1 demands to reflect dropped DACs.

Sequence Diagram(s)

sequenceDiagram
    participant Builder as ModelBuilder
    participant Commod as commodities.check_singleton_demands
    participant Model as PyomoModel
    participant DAC as demand_constraint/DemandActivity

    Builder->>Model: instantiate model, register BuildActions
    Builder->>Commod: invoke check_singleton_demands(model)
    Commod->>Model: scan demand_constraint_rpc indices
    alt single upstream found for (r,p,dem)
        Commod->>Model: set model.v_flow_out_annual[r,p,i,t,v,dem] = demand
        Commod->>Model: set model.v_flow_out[...time-slices...] = demand * distribution
        Commod->>Model: add (r,p,dem) to model.singleton_demands
    end
    Builder->>DAC: trigger demand_constraint build
    DAC->>Model: for each (r,p,dem) check if in singleton_demands
    alt in singleton_demands
        DAC-->>Model: Constraint.Skip (omit DAC and error check)
    else
        DAC-->>Model: build DemandActivity indices and demand_constraint as usual
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

refactor

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title partially addresses the changeset but omits the core optimization: singleton demand singleton detection and constraint skipping that drives the barrier-solver performance gains. Consider a more specific title like 'Optimize demand constraints by detecting and skipping singleton demands' to better reflect the primary technical change.
✅ 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

@ParticularlyPythonicBS ParticularlyPythonicBS merged commit 9773eff into TemoaProject:unstable Mar 20, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants