Skip to content

fix: keep shared-tank sources active when operating_hours == 0#975

Open
Thimpey wants to merge 1 commit into
davidusb-geek:masterfrom
Thimpey:fix/shared-tank-members-deactivated
Open

fix: keep shared-tank sources active when operating_hours == 0#975
Thimpey wants to merge 1 commit into
davidusb-geek:masterfrom
Thimpey:fix/shared-tank-members-deactivated

Conversation

@Thimpey

@Thimpey Thimpey commented Jun 11, 2026

Copy link
Copy Markdown

Fixes #974.

Shared-tank sources are temperature-driven, but the param_load_active activation loop in perform_optimization only treated k in self.param_thermal as thermal. A shared-tank source is not in param_thermal, so with operating_hours == 0 (the natural setting for a temperature-driven load) it was deactivated and pinned to 0 W for the whole horizon — the tank then cannot hold its min_temperatures band, the problem goes infeasible, the relaxed fallback is infeasible too, and nothing is published.

Master already exempts shared-tank members from the running-lb pinning and the energy constraint, so this just adds the same exemption to the third place that missed it. Same class as #887.

Changes

  • Hoist shared_tank_membership into perform_optimization scope and add it to the is_thermal exemption in the activation loop.
  • Reset the window mask to all-ones (with a warning) for a shared-tank member whose configured window falls entirely outside the horizon — the other 0-W path with the same end state.
  • Two regression tests reproducing both infeasibility paths; both fail on master without the change.

The repro and before/after output are in #974. Full test suite passes locally (the one unrelated test_open_meteo_legacy_pvlib failure is pre-existing on master) and ruff check . is clean.

Happy to adjust the approach if you'd prefer the exemption handled differently.


Co-authored with Claude Code (Anthropic).

Summary by Sourcery

Ensure temperature-driven shared-tank deferrable loads remain active and feasible under zero operating hours and out-of-horizon windows.

Bug Fixes:

  • Prevent shared-tank deferrable sources from being deactivated when operating_hours is set to 0 by treating them as thermal loads in the activation logic.
  • Avoid infeasibility when a shared-tank member’s configured operating window lies entirely outside the optimization horizon by resetting its window mask instead of zeroing it.

Tests:

  • Add regression tests covering shared-tank sources with zero operating hours and with operating windows outside the optimization horizon to verify the optimization stays feasible and dispatches heat.

@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adjusts deferrable-load activation logic so shared-tank thermal sources are never deactivated by operating-hours or window masks, and adds regression tests to cover both infeasibility paths for shared thermal tanks.

File-Level Changes

Change Details Files
Ensure shared-tank member loads are treated as thermal and stay active regardless of operating_hours settings.
  • Load shared-tank membership at the start of perform_optimization so it is available for later logic.
  • Extend is_thermal computation in the param_load_active loop to also treat shared-tank members as thermal loads.
  • Clarify comments to explain that shared-tank sources, like other thermal loads and sequence loads, should not be deactivated based on operating_hours.
src/emhass/optimization.py
Prevent shared-tank members from being pinned off when their configured window lies entirely outside the optimization horizon.
  • Detect shared-tank members whose window is outside the horizon and keep their load active rather than deactivating them.
  • Reset the affected shared-tank members’ window masks to all-ones to allow temperature constraints to drive dispatch.
  • Log a warning when ignoring a shared-tank member’s configured window because it falls outside the horizon.
src/emhass/optimization.py
Add regression tests covering both shared-tank infeasibility paths (zero operating hours and window outside horizon).
  • Introduce a helper _run_shared_tank_no_cap to construct a two-source shared DHW tank scenario with mid-horizon minimum temperature constraints.
  • Add a test verifying shared-tank members remain active and dispatch power when operating_hours == 0 for temperature-driven sources.
  • Add a test verifying optimality and non-zero dispatch when shared-tank member windows are configured entirely outside the horizon with single_constant loads.
tests/test_optimization.py

Assessment against linked issues

Issue Objective Addressed Explanation
#974 Ensure shared-thermal-tank member loads with operating_hours_of_each_deferrable_load == 0 are not deactivated by the param_load_active loop and remain temperature-driven (like thermal_config / thermal_battery loads).
#974 Ensure shared-thermal-tank member loads whose configured window (start_timesteps / end_timesteps) lies entirely outside the optimization horizon are not effectively pinned to 0 W by a zeroed window mask, but instead remain active and are driven by tank temperature constraints.
#974 Add regression tests that reproduce both infeasibility paths (operating_hours == 0 and window outside horizon) and verify that optimization remains feasible and dispatch is non-zero for shared-thermal-tank members.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In perform_optimization, shared_tank_membership is used with k in shared_tank_membership; if this structure can ever be large, consider turning the membership keys into a set once so the repeated in checks stay cheap and explicit.
  • The new warning in the window-mask reset path will fire every horizon where a shared-tank load has an out-of-horizon window; consider either throttling it (e.g. debug-level or once-per-load) or including a clearer hint that this is an expected/benign behavior when using temperature-driven sources.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `perform_optimization`, `shared_tank_membership` is used with `k in shared_tank_membership`; if this structure can ever be large, consider turning the membership keys into a `set` once so the repeated `in` checks stay cheap and explicit.
- The new warning in the window-mask reset path will fire every horizon where a shared-tank load has an out-of-horizon window; consider either throttling it (e.g. debug-level or once-per-load) or including a clearer hint that this is an expected/benign behavior when using temperature-driven sources.

## Individual Comments

### Comment 1
<location path="src/emhass/optimization.py" line_range="3644-3647" />
<code_context>
+                # min_temperatures unreachable (infeasible, then the relaxed
+                # fallback fails too and nothing is published).
+                if k in shared_tank_membership and window_outside_horizon:
+                    if k < len(self.param_window_masks):
+                        self.param_window_masks[k].value = np.ones(n)
+                    self.logger.warning(
+                        "Deferrable load %d is a shared-tank source with a configured "
</code_context>
<issue_to_address>
**suggestion:** Derive the window mask shape from the existing parameter instead of hardcoding np.ones(n).

To better future-proof this, construct the mask from `param_window_masks[k].value`’s shape (e.g. `np.ones_like(self.param_window_masks[k].value)` or `np.ones(self.param_window_masks[k].value.shape)`) rather than assuming a 1D length-`n` vector, so you avoid potential shape mismatches if the mask dimensionality changes.

```suggestion
                if k in shared_tank_membership and window_outside_horizon:
                    if k < len(self.param_window_masks):
                        self.param_window_masks[k].value = np.ones_like(self.param_window_masks[k].value)
                    self.logger.warning(
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +3644 to +3647
if k in shared_tank_membership and window_outside_horizon:
if k < len(self.param_window_masks):
self.param_window_masks[k].value = np.ones(n)
self.logger.warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Derive the window mask shape from the existing parameter instead of hardcoding np.ones(n).

To better future-proof this, construct the mask from param_window_masks[k].value’s shape (e.g. np.ones_like(self.param_window_masks[k].value) or np.ones(self.param_window_masks[k].value.shape)) rather than assuming a 1D length-n vector, so you avoid potential shape mismatches if the mask dimensionality changes.

Suggested change
if k in shared_tank_membership and window_outside_horizon:
if k < len(self.param_window_masks):
self.param_window_masks[k].value = np.ones(n)
self.logger.warning(
if k in shared_tank_membership and window_outside_horizon:
if k < len(self.param_window_masks):
self.param_window_masks[k].value = np.ones_like(self.param_window_masks[k].value)
self.logger.warning(

Shared-tank members are temperature-driven. Master already exempts them
from the running_lb pinning and the energy constraint, but the
param_load_active activation loop still used `is_thermal = k in
self.param_thermal` only. A shared-tank source is not in param_thermal,
so with operating_hours == 0 (the natural setting for a temperature-driven
load) it was deactivated: p_deferrable[k] forced to 0 for the whole
horizon. With every member off the tank cannot hold its min_temperatures
band, the problem goes infeasible, the relaxed fallback is infeasible too,
and nothing is published.

Same fix for the window-mask path: a member whose configured window falls
entirely outside the horizon had its mask zeroed (same 0-W pin); reset it
to all-ones and warn, since temperature constraints drive the load.

Hoists shared_tank_membership into perform_optimization scope and adds it
to the is_thermal exemption, matching the existing energy-constraint
exemption. Two regression tests reproduce both infeasibility paths and
fail on master without this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Thimpey Thimpey force-pushed the fix/shared-tank-members-deactivated branch from 1aa2412 to 952d5c1 Compare June 11, 2026 10:00
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.

Shared-tank sources are deactivated when operating_hours == 0 (tank goes infeasible)

1 participant