Skip to content

Commit c6c49d0

Browse files
committed
fixup! Implement initial storage support
1 parent 6458257 commit c6c49d0

File tree

3 files changed

+60
-35
lines changed

3 files changed

+60
-35
lines changed

docs/sphinx/source/user_guide/storage.rst

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,23 @@ The simplest way is to start with some battery specifications from a datasheet:
115115
.. ipython:: python
116116
117117
parameters = {
118-
"brand": "BYD",
119-
"model": "HVS 5.1",
120-
"width": 0.585,
121-
"height": 0.712,
122-
"depth": 0.298,
123-
"weight": 91,
118+
"brand": "Sonnen",
119+
"model": "sonnenBatterie 10/5,5",
120+
"width": 0.690,
121+
"height": 1.840,
122+
"depth": 0.270,
123+
"weight": 98,
124124
"chemistry": "LFP",
125-
"modules": 2,
126-
"energy_wh": 5120,
127-
"voltage": 204,
128-
"max_power_w": 5100,
125+
"mode": "AC",
126+
"charge_efficiency": 0.96,
127+
"discharge_efficiency": 0.96,
128+
"min_soc_percent": 5,
129+
"max_soc_percent": 95,
130+
"dc_modules": 1,
131+
"dc_modules_in_series": 1,
132+
"dc_energy_wh": 5500,
133+
"dc_nominal_voltage": 102.4,
134+
"dc_max_power_w": 3400,
129135
}
130136
131137
You can then use this information to build a model that can be used to run
@@ -358,6 +364,9 @@ following assumptions:
358364
- When the excess power from the system (after feeding the load) is not fed
359365
into the battery, it will be fed into the grid
360366

367+
- The battery can only charge from the system and discharge to the load (i.e.:
368+
battery-to-grid and grid-to-battery power flow is always zero)
369+
361370
- The grid will provide power to the system if required (i.e.: during night
362371
hours)
363372

pvlib/battery.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def fit_boc(model):
8080
datasheet.
8181
"""
8282
params = {
83-
"soc": 50,
83+
"soc_percent": 50,
8484
}
8585
model.update(params)
8686
return model
@@ -117,27 +117,36 @@ def boc(model, dispatch):
117117
- SOC. [%]
118118
- TODO: other? (i.e.: Energy/capacity)
119119
"""
120-
min_soc = 10
121-
max_soc = 90
120+
min_soc = model.get("min_soc_percent", 10)
121+
max_soc = model.get("max_soc_percent", 90)
122122
factor = offset_to_hours(dispatch.index.freq)
123123

124124
states = []
125-
current_energy = model["energy_wh"] * model["soc"] / 100
126-
max_energy = model["energy_wh"] * max_soc / 100
127-
min_energy = model["energy_wh"] * min_soc / 100
125+
current_energy = model["dc_energy_wh"] * model["soc_percent"] / 100
126+
max_energy = model["dc_energy_wh"] * max_soc / 100
127+
min_energy = model["dc_energy_wh"] * min_soc / 100
128+
129+
dispatch = power.copy()
130+
discharge_efficiency = model.get("discharge_efficiency", 1.0)
131+
charge_efficiency = model.get("charge_efficiency", 1.0)
132+
dispatch.loc[power < 0] *= discharge_efficiency
133+
dispatch.loc[power > 0] /= charge_efficiency
134+
128135
for power in dispatch:
129136
if power > 0:
130-
power = min(power, model["max_power_w"])
137+
power = min(power, model["dc_max_power_w"])
131138
energy = power * factor
132139
available = current_energy - min_energy
133140
energy = min(energy, available)
141+
power = energy / factor * discharge_efficiency
134142
else:
135-
power = max(power, -model["max_power_w"])
143+
power = max(power, -model["dc_max_power_w"])
136144
energy = power * factor
137145
available = current_energy - max_energy
138146
energy = max(energy, available)
147+
power = energy / factor / charge_efficiency
139148
current_energy -= energy
140-
soc = current_energy / model["energy_wh"] * 100
149+
soc = current_energy / model["dc_energy_wh"] * 100
141150
power = energy / factor
142151
states.append((power, soc))
143152

@@ -169,20 +178,27 @@ def fit_sam(datasheet):
169178
calculates the model parameters that match the provided information from a
170179
datasheet.
171180
"""
172-
# TODO: validate/normalize datasheet struct
173181
chemistry = {
174182
"LFP": "LFPGraphite",
175183
}
176184
model = sam_default(chemistry[datasheet["chemistry"]])
177185
sam_sizing(
178186
model=model,
179-
desired_power=datasheet["max_power_w"] / 1000,
180-
desired_capacity=datasheet["energy_wh"] / 1000,
181-
desired_voltage=datasheet["voltage"],
187+
desired_power=datasheet["dc_max_power_w"] / 1000,
188+
desired_capacity=datasheet["dc_energy_wh"] / 1000,
189+
desired_voltage=datasheet["dc_nominal_voltage"],
182190
)
183191
model.ParamsCell.initial_SOC = 50
192+
model.ParamsCell.minimum_SOC = datasheet.get("min_soc_percent", 10)
193+
model.ParamsCell.maximum_SOC = datasheet.get("max_soc_percent", 90)
184194
export = model.export()
185195
del export["Controls"]
196+
# TODO: can we use SAM's `calculate_battery_size` to assign these values
197+
# with `batt_dc_ac_efficiency` and `inverter_eff` respectively?
198+
export.update({
199+
"charge_efficiency": datasheet.get("charge_efficiency", 0.96),
200+
"discharge_efficiency": datasheet.get("discharge_efficiency", 0.96),
201+
})
186202
return export
187203

188204

@@ -220,8 +236,11 @@ def sam(model, power):
220236
battery = sam_new()
221237
battery.ParamsCell.assign(model.get("ParamsCell", {}))
222238
battery.ParamsPack.assign(model.get("ParamsPack", {}))
223-
battery.ParamsCell.minimum_SOC = 10
224-
battery.ParamsCell.maximum_SOC = 90
239+
# TODO: min/max SOC should be configurable for each simulation (i.e.: we
240+
# could simulate more conservative cycles than what the battery allows
241+
# (but we probably should not allow more aggressive cycles
242+
#battery.ParamsCell.minimum_SOC = 10
243+
#battery.ParamsCell.maximum_SOC = 90
225244
battery.Controls.replace(
226245
{
227246
"control_mode": 1,
@@ -233,13 +252,19 @@ def sam(model, power):
233252
battery.StateCell.assign(model.get("StateCell", {}))
234253
battery.StatePack.assign(model.get("StatePack", {}))
235254

255+
battery_dispatch = power.copy()
256+
battery_dispatch.loc[power < 0] *= model["discharge_efficiency"]
257+
battery_dispatch.loc[power > 0] /= model["charge_efficiency"]
258+
236259
states = []
237260
for p in power:
238261
battery.Controls.input_power = p / 1000
239262
battery.execute(0)
240263
states.append((battery.StatePack.P * 1000, battery.StatePack.SOC))
241264

242265
results = DataFrame(states, index=power.index, columns=["Power", "SOC"])
266+
results.loc[results["Power"] < 0, "Power"] /= model["charge_efficiency"]
267+
results.loc[results["Power"] > 0, "Power"] *= model["discharge_efficiency"]
243268
export = battery.export()
244269
del export["Controls"]
245270

pvlib/powerflow.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,23 +116,14 @@ def self_consumption_ac_battery_custom_dispatch(
116116
The resulting power flow provided by the system, the grid and the
117117
battery into the system, grid, battery and load. [W]
118118
"""
119-
# TODO: should we move this to the battery model? (i.e.: the dispatch
120-
# series adjustment and the conversion losses
121-
ac_dc_efficiency = 1 - ac_dc_loss / 100
122-
dc_ac_efficiency = 1 - dc_ac_loss / 100
123-
dc_dispatch = dispatch.copy()
124-
dc_dispatch.loc[dispatch < 0] *= dc_ac_efficiency
125-
dc_dispatch.loc[dispatch > 0] /= ac_dc_efficiency
126-
final_state, results = model(battery, dc_dispatch)
119+
final_state, results = model(battery, dispatch)
127120
df = df.copy()
128121
df["System to battery"] = -results["Power"]
129122
df["System to battery"].loc[df["System to battery"] < 0] = 0.0
130-
df["System to battery"] /= ac_dc_efficiency
131123
df["System to battery"] = df[["System to battery", "System to grid"]].min(axis=1)
132124
df["System to grid"] -= df["System to battery"]
133125
df["Battery to load"] = results["Power"]
134126
df["Battery to load"].loc[df["Battery to load"] < 0] = 0.0
135-
df["Battery to load"] *= dc_ac_efficiency
136127
df["Battery to load"] = df[["Battery to load", "Grid to load"]].min(axis=1)
137128
df["Grid to load"] -= df["Battery to load"]
138129
df["Grid"] = df[["Grid to system", "Grid to load"]].sum(

0 commit comments

Comments
 (0)