Feature/battery internal#258
Open
paulf81 wants to merge 8 commits into
Open
Conversation
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Contributor
There was a problem hiding this comment.
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_capacityand 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_SOCis 0 andmax_SOCis 1. BecauseE_min/E_maxare 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_capacitybefore hitting the limits, notenergy_capacity. Ifenergy_capacityis 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_dischargewithout validating it is positive. A configuredroundtrip_efficiencyof 0 (or an invalid negative value that produces NaN) now makesinternal_energy_capacity, energy limits, and initial state invalid during initialization, so the model can silently run with infinities/NaNs. Add validation for0 < roundtrip_efficiency <= 1before 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)." |
Collaborator
Author
There was a problem hiding this comment.
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 |
Collaborator
Author
There was a problem hiding this comment.
This should now be addressed
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR changes the battery simple model from a convention where the input value
energy_capacitydefines 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_SOCandmax_SOCare 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_capacityis 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_SOCin defininginternal_energy_capacity.