Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions client/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,18 @@ components:
maximum: 2
default: 0
description: Charging and discharging priority 0..2 compared to other batteries. 2 = highest priority.
eta_c:
type: number
minimum: 0
maximum: 1
description: Charging efficiency (0 to 1). Overrides the top level eta_c for this battery.
example: 0.95
eta_d:
type: number
minimum: 0
maximum: 1
description: Discharging efficiency (0 to 1). Overrides the top level eta_d for this battery.
example: 0.95

TimeSeries:
type: object
Expand Down Expand Up @@ -342,14 +354,14 @@ components:
minimum: 0
maximum: 1
default: 0.95
description: Charging efficiency (0 to 1)
description: Default charging efficiency (0 to 1) for batteries without their own eta_c
example: 0.95
eta_d:
type: number
minimum: 0
maximum: 1
default: 0.95
description: Discharging efficiency (0 to 1)
description: Default discharging efficiency (0 to 1) for batteries without their own eta_d
example: 0.95

BatteryResult:
Expand Down
14 changes: 9 additions & 5 deletions src/optimizer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def handle_validation_error(error):
'c_max': fields.Float(required=True, description='Maximum charge power (W)'),
'd_max': fields.Float(required=True, description='Maximum discharge power (W)'),
'p_a': fields.Float(required=True, description='Monetary value per Wh at end of the optimization horizon'),
'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.')
'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.'),
'eta_c': fields.Float(required=False, description='Charging efficiency (0 to 1), overrides the top level default'),
'eta_d': fields.Float(required=False, description='Discharging efficiency (0 to 1), overrides the top level default')
})

time_series_model = api.model('TimeSeries', {
Expand All @@ -96,8 +98,8 @@ def handle_validation_error(error):
'grid': fields.Nested(grid_model, required=False, description='Grid import and export configuration'),
'batteries': fields.List(fields.Nested(battery_config_model), required=True, description='Battery configurations'),
'time_series': fields.Nested(time_series_model, required=True, description='Time series data'),
'eta_c': fields.Float(required=False, default=0.95, description='Charging efficiency'),
'eta_d': fields.Float(required=False, default=0.95, description='Discharging efficiency'),
'eta_c': fields.Float(required=False, default=0.95, description='Default charging efficiency for batteries without their own eta_c'),
'eta_d': fields.Float(required=False, default=0.95, description='Default discharging efficiency for batteries without their own eta_d'),
})

# Output models
Expand Down Expand Up @@ -155,6 +157,8 @@ def post(self):
)

# Parse battery configurations
eta_c_default = data.get('eta_c', 0.95)
eta_d_default = data.get('eta_d', 0.95)
batteries = []
for bat_data in data['batteries']:
batteries.append(BatteryConfig(
Expand All @@ -171,6 +175,8 @@ def post(self):
d_max=bat_data['d_max'],
p_a=bat_data['p_a'],
c_priority=bat_data.get('c_priority', 0),
eta_c=bat_data.get('eta_c', eta_c_default),
eta_d=bat_data.get('eta_d', eta_d_default),
))

# Parse time series data
Expand Down Expand Up @@ -209,8 +215,6 @@ def post(self):
grid=grid,
batteries=batteries,
time_series=time_series,
eta_c=data.get('eta_c', 0.95),
eta_d=data.get('eta_d', 0.95),
M=1e6
)

Expand Down
14 changes: 7 additions & 7 deletions src/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class BatteryConfig:
p_demand: Optional[List[float]] = None # Minimum charge demand (Wh)
s_goal: Optional[List[float]] = None # Goal state of charge (Wh)
c_priority: int = 0
eta_c: float = 0.95 # Charging efficiency (0 to 1)
eta_d: float = 0.95 # Discharging efficiency (0 to 1)


@dataclass
Expand All @@ -54,7 +56,7 @@ class Optimizer:
"""

def __init__(self, strategy: OptimizationStrategy, grid: GridConfig, batteries: List[BatteryConfig], time_series: TimeSeriesData,
eta_c: float = 0.95, eta_d: float = 0.95, M: float = 1e6, optimizer_settings: OptimizerSettings | None = None):
M: float = 1e6, optimizer_settings: OptimizerSettings | None = None):
"""
Optimizer Constructor
"""
Expand All @@ -65,8 +67,6 @@ def __init__(self, strategy: OptimizationStrategy, grid: GridConfig, batteries:
self.grid = grid
self.batteries = batteries
self.time_series = time_series
self.eta_c = eta_c
self.eta_d = eta_d
self.M = M
# number of time steps
self.T = len(time_series.gt)
Expand Down Expand Up @@ -427,15 +427,15 @@ def _add_battery_constraints(self):
if len(self.time_steps) > 0:
self.problem += (self.variables['s'][i][0]
== bat.s_initial
+ self.eta_c * self.variables['c'][i][0]
- (1 / self.eta_d) * self.variables['d'][i][0])
+ bat.eta_c * self.variables['c'][i][0]
- (1 / bat.eta_d) * self.variables['d'][i][0])

# State of charge evolution
for t in range(1, self.T):
self.problem += (self.variables['s'][i][t]
== self.variables['s'][i][t - 1]
+ self.eta_c * self.variables['c'][i][t]
- (1 / self.eta_d) * self.variables['d'][i][t])
+ bat.eta_c * self.variables['c'][i][t]
- (1 / bat.eta_d) * self.variables['d'][i][t])

# Constraint (6): Battery SOC goal constraints (for t > 0)
if bat.s_goal is not None:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ def test_optimizer(test_case: pathlib.Path):
f"objective value: {actual_objective_value}, expected was: {expected_objective_value}"


def test_per_battery_efficiency():
# both batteries are forced to charge the same energy, so their state of charge
# gain must scale with their individual charging efficiency
client = app.test_client()
battery = {
"charge_from_grid": True,
"s_min": 0, "s_max": 10000, "s_initial": 0,
"c_min": 0, "c_max": 2000, "d_max": 0, "p_a": 0.01,
"p_demand": [1000, 0],
}
request = {
"batteries": [
{**battery, "eta_c": 1.0},
{**battery, "eta_c": 0.5},
],
"time_series": {
"dt": [3600, 3600],
"gt": [0, 0],
"ft": [0, 0],
"p_N": [0.3, 0.3],
"p_E": [0.3, 0.3],
},
}
response = client.post("/optimize/charge-schedule", json=request)

assert response.status_code == 200
assert response.json["status"] == "Optimal"
assert numpy.isclose(response.json["batteries"][0]["state_of_charge"][0], 1000)
assert numpy.isclose(response.json["batteries"][1]["state_of_charge"][0], 500)


def test_abort_returns_json_message():
# message-only api.abort(400, ...) must return a JSON body, not an empty response
client = app.test_client()
Expand Down
Loading