Skip to content

Feature/battery internal#258

Open
paulf81 wants to merge 8 commits into
NatLabRockies:developfrom
paulf81:feature/battery_internal
Open

Feature/battery internal#258
paulf81 wants to merge 8 commits into
NatLabRockies:developfrom
paulf81:feature/battery_internal

Conversation

@paulf81
Copy link
Copy Markdown
Collaborator

@paulf81 paulf81 commented May 13, 2026

This PR changes the battery simple model from a convention where the input value energy_capacity defines the energy capacity of the output (that is after efficiency losses) rather than internally. The reason is that I believe it's more conventional that if a battery is specified as 1 MW / 4 hour, it's assumed the internal size will be sized to ensure the battery can deliver 1 MW for 4 hours.

A follow-on change are the that min_SOC and max_SOC are meant to be deviations from nameplate performance, such that if they are not their default values of 0 and 1, then the battery won't be able to deliver the same 1 MW for 4 hours. These can be used therefore to model impacts of degredation but should not be used to represent a new properly functioning battery. In other words, if the battery has a physical ability to further charge or discharge that can never be accessed, even when the battery is new, we don't count this as min/max SOC.

A new internal variable internal_energy_capacity is defined accounting for this efficiency loss. A new test is added to make sure the convention of specifying the output power is respected. A regression test is updated to this new convention but all remaining tests continue to pass (no impact if RTE=1.0)

I guess one outstanding question is should I have also included min_SOC / max_SOC in defining internal_energy_capacity.

paulf81 and others added 3 commits May 13, 2026 10:49
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates BatterySimple so configured energy_capacity represents deliverable output energy by sizing an internal capacity that accounts for discharge efficiency.

Changes:

  • Adds internal_energy_capacity and uses it for energy limits, initialization, and SOC updates.
  • Updates battery usage regression expectations for the new convention.
  • Adds a discharge-duration test for non-unity roundtrip efficiency.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
hercules/plant_components/battery_simple.py Implements internal capacity scaling for BatterySimple.
tests/battery_simple_test.py Adds a duration invariant test for RTE < 1.
tests/regression_tests/battery_regression_test.py Updates regression baselines affected by the capacity convention.
Comments suppressed due to low confidence (2)

hercules/plant_components/battery_simple.py:152

  • The new deliverable-capacity invariant only holds when min_SOC is 0 and max_SOC is 1. Because E_min/E_max are still set to fractions of this internal capacity, a configuration using the existing 0.1/0.9 SOC window can deliver only (max_SOC - min_SOC) * energy_capacity before hitting the limits, not energy_capacity. If energy_capacity is meant to represent usable deliverable energy, this needs to account for the SOC window; otherwise the API/comment should explicitly say reserves reduce the deliverable duration.
        self.internal_energy_capacity = self.energy_capacity / self.eta_discharge

        # Charge (Energy) limits [kJ]
        self.E_min = kWh2kJ(self.SOC_min * self.internal_energy_capacity)
        self.E_max = kWh2kJ(self.SOC_max * self.internal_energy_capacity)

hercules/plant_components/battery_simple.py:148

  • This divides by eta_discharge without validating it is positive. A configured roundtrip_efficiency of 0 (or an invalid negative value that produces NaN) now makes internal_energy_capacity, energy limits, and initial state invalid during initialization, so the model can silently run with infinities/NaNs. Add validation for 0 < roundtrip_efficiency <= 1 before deriving the internal capacity.
        self.internal_energy_capacity = self.energy_capacity / self.eta_discharge

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +440 to +473
def test_SB_discharge_duration_invariant():
"""Test that discharge duration equals energy_capacity / discharge_rate regardless of RTE.

A 4 MWh battery with 1 MW discharge rate should take exactly 4 hours to fully
discharge, even when roundtrip efficiency < 1. The RTE losses should not reduce
the deliverable energy duration.
"""
test_h_dict = copy.deepcopy(h_dict_simple_battery)
test_h_dict["battery"]["energy_capacity"] = 4000 # 4 MWh
test_h_dict["battery"]["charge_rate"] = 1000 # 1 MW
test_h_dict["battery"]["discharge_rate"] = 1000 # 1 MW
test_h_dict["battery"]["roundtrip_efficiency"] = 0.9
test_h_dict["battery"]["max_SOC"] = 1.0
test_h_dict["battery"]["min_SOC"] = 0.0
test_h_dict["battery"]["initial_conditions"] = {"SOC": 1.0}
test_h_dict["battery"]["allow_grid_power_consumption"] = True
test_h_dict["dt"] = 1.0

SB = BatterySimple(test_h_dict, "battery")

# Discharge at full rated power and count how many steps until empty
expected_steps = 14400 # 4 hours * 3600 s/hr / 1 s/step
steps_taken = 0
for i in range(expected_steps + 1000): # Run extra to find when it empties
SB.step(step_inputs(P_avail=0, P_signal=-1000))
steps_taken += 1
if SB.SOC <= 0.0:
break

# The battery should discharge in exactly 4 hours (14400 steps), not sooner
assert abs(steps_taken - expected_steps) <= 1, (
f"Battery should take {expected_steps} steps (4 hours) to fully discharge "
f"at 1 MW from 4 MWh with RTE=0.9, but took {steps_taken} steps "
f"({steps_taken / 3600:.3f} hours)."
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Believe this is clarified by changes pushed since this comment made

Comment on lines +145 to +148
# Internal energy capacity accounts for efficiency losses so that
# discharge duration = energy_capacity / discharge_rate regardless of RTE.
# energy_capacity is the user-specified deliverable energy.
self.internal_energy_capacity = self.energy_capacity / self.eta_discharge
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should now be addressed

paulf81 and others added 5 commits May 13, 2026 15:18
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
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.

2 participants