Skip to content

Cement plant and associated technologies - #845

Merged
Manish-Khanra merged 2 commits into
mainfrom
new_dsm_unit
Aug 6, 2026
Merged

Cement plant and associated technologies#845
Manish-Khanra merged 2 commits into
mainfrom
new_dsm_unit

Conversation

@Manish-Khanra

@Manish-Khanra Manish-Khanra commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

User description

Related Issue

Description

Adds a cement plant demand-side unit, modelled as a fuel-switchable kiln line, with full rolling-horizon support.

New DSM unit: CementPlant (assume/units/cement_plant.py)

  • Kiln line of preheater, calciner, and kiln, each independently fuel-switchable between electricity, a natural-gas/coal mix, or hydrogen (ThermalProcessStage base class + subclasses in dst_components.py).
  • Waste heat recovered from the kiln reduces the fuel the preheater needs.
  • Optional electrolyser producing hydrogen on site for the calciner/kiln burners, with the split between burners tracked explicitly.
  • Optional thermal storage (ThermalStorage, new short-term_with_generator mode) acting as an electric-heater-charged buffer (E-TES) that displaces calciner burner heat in expensive hours, the plant's main source of demand-side flexibility.
  • Optional raw material mill and cement mill (GrindingMill): electric grinding steps. A cement mill attached to a kiln line grinds that line's clinker as before; either mill with no kiln line at all runs as a standalone grinding operation, where the declared demand targets that mill's own ground tonnage directly instead of clinker.
  • New CementForecaster (assume/common/forecaster.py)

Rolling-horizon

  • Fixed a bug where the rolling-horizon min_demand strategy detection hard-coded steel_demand_per_timestep instead of using the unit's own _demand_attr_suffix, so it silently fell back to the wrong (already-window-sliced) series for any non-steel-plant unit using the min_demand strategy in rolling-horizon mode.
  • Added a generic _component_power_expr() fallback so the shared flexibility measures (peak_load_shifting, renewable_utilisation, etc.) work for any DSM unit's component set, not just steel-plant-shaped ones.

Bug fix: ramp constraint at the first time step (assume/units/dst_components.py)

Found while stress-testing the new unit: the first time step of every ramp-limited quantity was capped by min(ramp_up, ramp_down) instead of just ramp_up, because ramp_down_constraint's own first-step branch also applied an absolute cap (value[0] <= ramp_down) even though there is no previous value to decrease from. A tight ramp_down could therefore artificially choke off a legitimately high first-step value.

Fixed in all four places this exact pattern was duplicated: the shared add_ramping_constraints helper, GenericStorage's inline charge/discharge ramps, ChargingStation's inline ramps, and Boiler's natural-gas/hydrogen-gas fuel paths. ramp_down_constraint now skips the first step entirely instead of applying a spurious cap.

Tests

tests/test_cement_plant.py

Checklist

  • Documentation updated (docstrings, READMEs, user guides, inline comments, docs folder updates, etc.)
  • New unit/integration tests added (if applicable)
  • Changes noted in release notes (if any)
  • Consent to release this PR's code under the GNU Affero General Public License v3.0

PR Type

Enhancement, Bug fix, Tests, Documentation


Description

  • Add fuel-switchable cement plant DSM model

  • Support mills, electrolyser, E-TES flexibility

  • Add cement forecasts and CSV loading

  • Cover operations with extensive tests


Diagram Walkthrough

flowchart LR
  cfg["CSV unit configuration"]
  forecaster["CementForecaster"]
  plant["CementPlant DSM model"]
  components["Kiln line, mills, storage, electrolyser"]
  optimisation["Flex and rolling-horizon optimisation"]
  tests["Integration and behavior tests"]
  cfg -- "loads forecasts" --> forecaster
  forecaster -- "provides prices and demand" --> plant
  components -- "compose model" --> plant
  plant -- "solves schedules" --> optimisation
  optimisation -- "validated by" --> tests
Loading

File Walkthrough

Relevant files
Tests
1 files
test_cement_plant.py
Add comprehensive cement plant model tests                             
+1634/-0
Enhancement
4 files
dst_components.py
Add cement components and storage generator mode                 
+782/-21
forecaster.py
Add dedicated cement plant forecaster                                       
+126/-0 
__init__.py
Register cement plant unit type                                                   
+2/-0     
cement_plant.py
Implement cement plant DSM unit                                                   
+670/-0 
Configuration changes
1 files
loader_csv.py
Load cement plant forecast inputs                                               
+37/-0   
Bug fix
1 files
dsm_load_shift.py
Generalize DSM flexibility and rolling horizon                     
+80/-45 
Documentation
3 files
demand_side_agent.rst
Document cement plant demand-side unit                                     
+77/-0   
release_notes.rst
Add cement plant release notes                                                     
+11/-0   
unit_forecasts.rst
Document cement forecast configuration fields                       
+1/-0     

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit bf7f7bd)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

components defaults to None, but the constructor calls components.keys(). Instantiating CementPlant without explicitly passing a components dict will raise AttributeError instead of creating an empty plant or producing the intended validation error. Normalize components to {} before using it.

    components: dict[str, dict] = None,
    technology: str = "cement_plant",
    objective: str = "min_variable_cost",
    flexibility_measure: str = "cost_based_load_shift",
    demand: float = 0,
    cost_tolerance: float = 10,
    congestion_threshold: float = 0,
    peak_load_cap: float = 0,
    load_profile_deviation: float = 1.0,
    raw_meal_to_clinker_ratio: float = 1.55,
    waste_heat_per_t_clinker: float = 0.22,
    waste_heat_utilisation: float = 0.90,
    node: str = "node0",
    location: tuple[float, float] = (0.0, 0.0),
    **kwargs,
):
    super().__init__(
        id=id,
        unit_operator=unit_operator,
        technology=technology,
        components=components,
        bidding_strategies=bidding_strategies,
        forecaster=forecaster,
        node=node,
        location=location,
        **kwargs,
    )
    if not isinstance(forecaster, CementForecaster):
        raise TypeError(f"forecaster must be of type {CementForecaster.__name__}")

    # check if the required components are present in the components dictionary
    for component in self.required_technologies:
        if component not in components.keys():
            raise ValueError(
                f"Component {component} is required for the cement plant unit."
            )

    # check if the provided components are valid and do not contain any unknown components
    for component in components.keys():
Attribute Error

self.components["thermal_storage"] is treated as a dict elsewhere, but this constraint accesses .storage_type as an attribute. Any plant configured with thermal_storage will fail during model construction with AttributeError; use dict access such as .get("storage_type", "short-term").

if self.components["thermal_storage"].storage_type != (
    "short-term_with_generator"
):
    effective_heat -= storage.charge[t]

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to bf7f7bd

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Cap effective process heat

Add a capacity constraint for effective_heat_in in Calciner. Because thermal storage
discharge is added to heat_out, the current model can let effective_heat_in exceed
max_heat_out, allowing clinker throughput above the calciner's configured capacity.

assume/units/dst_components.py [2688-2690]

 @model_block.Constraint(self.time_steps)
 def throughput_from_heat(b, t):
     return b.clinker_out[t] == b.effective_heat_in[t] / b.specific_heat_demand
 
+@model_block.Constraint(self.time_steps)
+def effective_heat_capacity_constraint(b, t):
+    return b.effective_heat_in[t] <= b.max_heat_out
+
Suggestion importance[1-10]: 8

__

Why: This addresses a meaningful modelling bug: using effective_heat_in for clinker_out without also capping it can let storage discharge increase calciner throughput beyond max_heat_out. Adding effective_heat_in[t] <= b.max_heat_out correctly preserves the calciner capacity while allowing storage to displace burner heat.

Medium
Fix storage configuration access

self.components["thermal_storage"] is treated as a dictionary elsewhere, so
accessing .storage_type will raise AttributeError. Use dictionary access with the
same default used during initialization.

assume/units/cement_plant.py [551-554]

-if self.components["thermal_storage"].storage_type != (
-    "short-term_with_generator"
+if (
+    self.components["thermal_storage"].get("storage_type", "short-term")
+    != "short-term_with_generator"
 ):
     effective_heat -= storage.charge[t]
Suggestion importance[1-10]: 8

__

Why: This is a concrete correctness issue: self.components["thermal_storage"] is used as a config dictionary elsewhere, so .storage_type would likely fail during model construction. Using .get("storage_type", "short-term") matches the surrounding code and fixes thermal storage configurations.

Medium
Allow negative operating costs

Use pyo.Reals for operating_cost instead of pyo.NonNegativeReals. Electricity prices
can be negative, and the current non-negative domain can make otherwise valid
dispatch infeasible whenever power_in * model.electricity_price[t] is negative.

assume/units/dst_components.py [700-702]

 model_block.operating_cost = pyo.Var(
-    self.time_steps, within=pyo.NonNegativeReals
+    self.time_steps, within=pyo.Reals
 )
Suggestion importance[1-10]: 7

__

Why: This is a valid issue because operating_cost[t] == power_in[t] * model.electricity_price[t] can be negative when electricity prices are negative, conflicting with pyo.NonNegativeReals. The fix is important, though the same pattern appears in multiple newly added blocks, so changing only this occurrence may be incomplete.

Medium
Handle absent component configuration

components defaults to None, so using components.keys() can crash when a plant is
created without an explicit component dictionary. Normalize components before
passing it to the base class and validate against self.components.

assume/units/cement_plant.py [184-206]

+components = components or {}
+
 super().__init__(
     id=id,
     unit_operator=unit_operator,
     technology=technology,
     components=components,
     bidding_strategies=bidding_strategies,
     forecaster=forecaster,
     node=node,
     location=location,
     **kwargs,
 )
 ...
 for component in self.required_technologies:
-    if component not in components.keys():
+    if component not in self.components:
         raise ValueError(
             f"Component {component} is required for the cement plant unit."
         )
 
 # check if the provided components are valid and do not contain any unknown components
-for component in components.keys():
+for component in self.components:
Suggestion importance[1-10]: 7

__

Why: The suggestion is valid because components defaults to None, but the constructor later calls components.keys(), which can raise an AttributeError. Normalizing components before super().__init__() and validating against self.components prevents a real instantiation bug.

Medium
Preserve rolling-horizon compatibility

Units that inherit DSMFlex but do not define _demand_attr_suffix will now fail
during rolling-horizon strategy detection. Use a safe fallback so existing DSM units
keep working unless they explicitly override the demand attribute suffix.

assume/units/dsm_load_shift.py [983-985]

+demand_attr_suffix = getattr(self, "_demand_attr_suffix", "steel_demand")
 demand = _fit_to_horizon(
-    f"{self._demand_attr_suffix}_per_timestep", pad_value=0.0
+    f"{demand_attr_suffix}_per_timestep", pad_value=0.0
 )
Suggestion importance[1-10]: 7

__

Why: The suggestion is relevant because replacing the hard-coded steel_demand_per_timestep with self._demand_attr_suffix can break DSMFlex subclasses that do not define _demand_attr_suffix. A getattr fallback preserves the previous steel default while allowing subclasses such as CementPlant to override it.

Medium

Previous suggestions

Suggestions up to commit bf7f7bd
CategorySuggestion                                                                                                                                    Impact
Possible issue
Allow negative operating costs

operating_cost is constrained to be non-negative, but electricity market prices can
be negative. This can make otherwise feasible schedules infeasible whenever power_in
is positive and model.electricity_price[t] is negative; use an unrestricted real
variable for cost variables in the new cement components.

assume/units/dst_components.py [700-702]

 model_block.operating_cost = pyo.Var(
-    self.time_steps, within=pyo.NonNegativeReals
+    self.time_steps, within=pyo.Reals
 )
Suggestion importance[1-10]: 8

__

Why: This is a real feasibility issue because operating_cost[t] == power_in[t] * model.electricity_price[t] can become negative under negative electricity prices while operating_cost is restricted to NonNegativeReals. The fix is relevant, though similar operating_cost declarations elsewhere in the new cement components would also need the same treatment.

Medium
Handle missing components safely

Normalize components before passing it to the parent and before using
components.keys(). As written, instantiating CementPlant without an explicit
components argument can raise an AttributeError.

assume/units/cement_plant.py [162-194]

 def __init__(
     self,
     id: str,
     unit_operator: str,
     bidding_strategies: dict,
     forecaster: CementForecaster,
     components: dict[str, dict] = None,
     ...
 ):
+    components = components or {}
+
     super().__init__(
         id=id,
         unit_operator=unit_operator,
         technology=technology,
         components=components,
         bidding_strategies=bidding_strategies,
         forecaster=forecaster,
         node=node,
         location=location,
         **kwargs,
     )
Suggestion importance[1-10]: 8

__

Why: This is a real constructor bug: components defaults to None but is later used via components.keys(), which can raise when callers rely on the documented default. Normalizing components before super().__init__() is accurate and prevents invalid initialization.

Medium
Fix storage config access

self.components["thermal_storage"] is treated as a config dictionary elsewhere, so
accessing .storage_type can fail at model construction. Read storage_type via .get()
with the same default used earlier.

assume/units/cement_plant.py [551-554]

-if self.components["thermal_storage"].storage_type != (
-    "short-term_with_generator"
-):
+storage_type = self.components["thermal_storage"].get(
+    "storage_type", "short-term"
+)
+if storage_type != "short-term_with_generator":
     effective_heat -= storage.charge[t]
Suggestion importance[1-10]: 8

__

Why: The PR consistently treats self.components["thermal_storage"] as a configuration dictionary elsewhere, so .storage_type is likely to fail for plants with thermal storage. Using .get("storage_type", "short-term") matches the earlier code and fixes a model-construction error.

Medium
Avoid committing infeasible windows

_solve_with_profile_fallback now returns (None, None) for solver exceptions, but a
solver may also report infeasibility through termination_condition without raising.
Return (None, None) for infeasible results after the optional profile retry too,
otherwise the caller can still try to commit values from an infeasible instance.

assume/units/dsm_load_shift.py [1217-1241]

-if (
-    load_profile_added
-    and results.solver.termination_condition
-    in {
-        pyo.TerminationCondition.infeasible,
-        pyo.TerminationCondition.infeasibleOrUnbounded,
-    }
-):
+infeasible_conditions = {
+    pyo.TerminationCondition.infeasible,
+    pyo.TerminationCondition.infeasibleOrUnbounded,
+}
+
+if load_profile_added and results.solver.termination_condition in infeasible_conditions:
     ...
+
+if results.solver.termination_condition in infeasible_conditions:
+    logger.warning(
+        "Window [%d:%d] has no feasible solution; leaving it uncommitted.",
+        window_start,
+        window_end,
+    )
+    return None, None
 
 return instance, results
Suggestion importance[1-10]: 8

__

Why: The new (None, None) path only handles solver exceptions, but infeasible termination conditions can still be returned normally and then committed by _solve_rolling_horizon_next_window. Adding a final infeasibility check is a correct and important safeguard for rolling-horizon operation.

Medium
General
Validate heater efficiency

eta_electric is used in a denominator, but invalid zero or negative values are
silently accepted via max(eta_electric, 1e-6). Reject non-positive efficiencies
instead, otherwise bad input can create unrealistic heater sizing or infeasible
storage behavior.

assume/units/dst_components.py [660-666]

+if eta_electric <= 0:
+    raise ValueError("ThermalStorage requires a positive eta_electric.")
+
 self.eta_electric = eta_electric
 if self.storage_type == "short-term_with_generator":
     self.max_power = (
-        self.max_power_charge / max(eta_electric, 1e-6)
+        self.max_power_charge / eta_electric
         if max_power is None
         else max_power
     )
Suggestion importance[1-10]: 6

__

Why: Rejecting non-positive eta_electric is a sound validation improvement because the current max(eta_electric, 1e-6) can hide invalid input while the actual electric_heater_charge constraint still uses the bad value. This is useful robustness work but mainly input validation rather than a core model bug.

Low
Suggestions up to commit 6c36ceb
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix storage configuration access

self.components["thermal_storage"] is treated as a configuration dictionary
elsewhere, so accessing .storage_type will fail when thermal storage is present. Use
dictionary access with the same default used during initialization.

assume/units/cement_plant.py [551-554]

-if self.components["thermal_storage"].storage_type != (
+if self.components["thermal_storage"].get("storage_type", "short-term") != (
     "short-term_with_generator"
 ):
     effective_heat -= storage.charge[t]
Suggestion importance[1-10]: 8

__

Why: This is a real runtime bug: self.components["thermal_storage"] is handled as a config dict elsewhere, so .storage_type will fail when thermal storage is configured. The proposed .get("storage_type", "short-term") access is consistent with surrounding code.

Medium
Allow negative operating costs

Use pyo.Reals for operating_cost variables whose value is tied to electricity-price
expressions. If model.electricity_price[t] can be negative, the current
NonNegativeReals domain makes otherwise valid operation infeasible whenever the
computed cost is negative.

assume/units/dst_components.py [700-702]

 model_block.operating_cost = pyo.Var(
-    self.time_steps, within=pyo.NonNegativeReals
+    self.time_steps, within=pyo.Reals
 )
Suggestion importance[1-10]: 7

__

Why: This is a valid edge-case fix: operating_cost is constrained to equal power_in * model.electricity_price[t], so negative electricity prices can make a NonNegativeReals cost variable infeasible. The impact is meaningful for market models with negative prices, though the PR tests shown only use positive prices.

Medium
Handle missing component configuration

Normalize components before passing it to the base class and before calling
components.keys(). As written, using the documented default components=None raises
an AttributeError during initialization.

assume/units/cement_plant.py [162-206]

 def __init__(
     self,
     id: str,
     unit_operator: str,
     bidding_strategies: dict,
     forecaster: CementForecaster,
     components: dict[str, dict] = None,
     technology: str = "cement_plant",
 ...
+    components = components or {}
+
     super().__init__(
         id=id,
         unit_operator=unit_operator,
         technology=technology,
         components=components,
 ...
     for component in components.keys():
Suggestion importance[1-10]: 7

__

Why: This correctly identifies that the documented default components=None can fail when components.keys() is called. Normalizing components before super().__init__() is a straightforward fix for an initialization bug.

Medium
Allow negative operating costs

variable_cost can become negative when electricity or fuel prices are negative,
which would make the equality to component operating_cost infeasible. Allow
real-valued costs so the model remains solvable under negative market prices.

assume/units/cement_plant.py [427-429]

 self.model.variable_cost = pyo.Var(
-    self.model.time_steps, within=pyo.NonNegativeReals
+    self.model.time_steps, within=pyo.Reals
 )
Suggestion importance[1-10]: 6

__

Why: The suggestion is plausible because negative electricity prices can make variable_cost negative, making a NonNegativeReals equality infeasible. Its impact depends on whether component-level operating_cost variables also allow negative values, but the plant-level change is directionally correct.

Low
Validate physical efficiencies

Validate efficiency, eta_electric, and eta_fossil as fractions in (0, 1] instead of
only checking positivity or silently clamping denominators. Values above 1 create
mass or energy, and zero/negative thermal efficiencies can produce infeasible or
misleading model bounds.

assume/units/dst_components.py [2175-2365]

-if efficiency <= 0:
-    raise ValueError("GrindingMill requires a positive efficiency.")
+if not 0 < efficiency <= 1:
+    raise ValueError("GrindingMill requires efficiency to be in the interval (0, 1].")
 ...
+if not 0 < eta_electric <= 1:
+    raise ValueError("eta_electric must be in the interval (0, 1].")
+if not 0 < eta_fossil <= 1:
+    raise ValueError("eta_fossil must be in the interval (0, 1].")
+
 self.max_power = (
-    max_heat_out / max(eta_electric, 1e-6) if max_power is None else max_power
+    max_heat_out / eta_electric if max_power is None else max_power
 )
Suggestion importance[1-10]: 5

__

Why: The suggestion is generally correct that efficiency, eta_electric, and eta_fossil should not silently allow nonphysical values or clamp zero denominators. It is mostly validation hardening rather than fixing a demonstrated PR failure, and it spans multiple non-contiguous locations.

Low
General
Enforce meaningful unit commitment

Do not enable minimum up/down constraints with min_power == 0, because
operational_status can remain on while power_in is zero and the intended switching
restriction is bypassed. Either require a positive minimum operating level when
min_operating_steps or min_down_steps is configured, or skip adding those
constraints.

assume/units/dst_components.py [2261-2269]

+if (self.min_operating_steps > 1 or self.min_down_steps > 1) and self.min_power <= 0:
+    raise ValueError(
+        "GrindingMill requires min_power > 0 when min_operating_steps "
+        "or min_down_steps are configured."
+    )
+
 if (
     self.min_operating_steps > 1
     or self.min_down_steps > 1
     or self.min_power > 0
 ):
     add_min_up_down_time_constraints(
         model_block=model_block,
         time_steps=self.time_steps,
     )
Suggestion importance[1-10]: 7

__

Why: This accurately identifies that min_operating_steps/min_down_steps are weak when min_power == 0, because operational_status can satisfy switching constraints without forcing real power_in. Requiring positive min_power for those constraints would prevent misleading configurations and improve model correctness.

Medium

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.59501% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.62%. Comparing base (3ac86e1) to head (bf7f7bd).

Files with missing lines Patch % Lines
assume/units/dsm_load_shift.py 43.47% 13 Missing ⚠️
assume/units/dst_components.py 95.78% 12 Missing ⚠️
assume/common/forecaster.py 63.33% 11 Missing ⚠️
assume/units/cement_plant.py 93.85% 11 Missing ⚠️
assume/scenario/loader_csv.py 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #845      +/-   ##
==========================================
+ Coverage   82.02%   82.62%   +0.59%     
==========================================
  Files          58       59       +1     
  Lines        9738    10229     +491     
==========================================
+ Hits         7988     8452     +464     
- Misses       1750     1777      +27     
Flag Coverage Δ
pytest 82.62% <90.59%> (+0.59%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit bf7f7bd

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit bf7f7bd

@paragpatil39 paragpatil39 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me!

@Manish-Khanra
Manish-Khanra merged commit 4d2cb73 into main Aug 6, 2026
13 checks passed
@Manish-Khanra
Manish-Khanra deleted the new_dsm_unit branch August 7, 2026 07:41

@maurerle maurerle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I left some comments for improvements of this PR

return pyo.quicksum(
m.dsm_blocks[block].power_in[t]
for block in m.dsm_blocks
if hasattr(m.dsm_blocks[block], "power_in")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is it unclear hear if power_in exists? This does not look well

high first-step value the way it would if it were treated as a second, tighter
absolute cap alongside ``ramp_up``.
"""
ramped = getattr(model_block, quantity)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ramped should be passed as value (so model_block.power_in or model_block. to this helper function instead of providing a string here.

So the helper should directly have ramped = model_block.power_in as parameter when called and quantity removed.

That way, we are explicit, we know that the attribute exists and ramped is not None and are sure that this function is not called with a typo.

- **Skip torch seeding when torch is installed but not used**: Irrelevant seeding was performed and a warning was thrown about deterministic PyTorch behavior, even though simulation does not use RL. This is fixed by only setting the PyTorch seeds when learning is active.
- **Fix bug in redispatch mechanism**: Fixed the bug in redispatch evaluation due to PyPSA's version upgrade. In ``PyPSA >= 0.35.2`` (released in February 2025) the sign of load was not taken into account correctly & since the fixed EOM dispatch was modelled as a load with positive sign which was resulting in incorrect redispatch amounts.

0.6.2 - (5th August 2026)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR did add new release notes - of a release which does not exist. Why?

@maurerle maurerle mentioned this pull request Aug 10, 2026
4 tasks
@maurerle

Copy link
Copy Markdown
Member

@Manish-Khanra @paragpatil39 did you look through the PR change suggestions?

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