Skip to content
Draft
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
5 changes: 5 additions & 0 deletions client/client.gen.go

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

9 changes: 8 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,15 @@ components:
type: number
minimum: 0
description: |
price per W to consider in case the import limit is exceeded.
price per W to consider in case the import limit is exceeded.
If not specified, the limit will be protected by a hard constraint.
p_max_abs_imp:
type: number
minimum: 0
description: |
Absolute physical grid import limit in W. Hard cap that can never be
exceeded, regardless of price penalty. Use to model fuse rating or
cable capacity.
BatteryConfig:
type: object
required:
Expand Down
4 changes: 4 additions & 0 deletions src/optimizer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def handle_validation_error(error):

grid_model = api.model('GridConfig', {
'p_max_imp': fields.Float(required=False, description='Maximum grid import power in W'),
'p_max_abs_imp': fields.Float(required=False, description='Absolute physical grid import limit in W. Hard cap that can never be exceeded.'),
'p_max_exp': fields.Float(required=False, description='Maximum grid export power in W'),
'prc_p_exc_imp': fields.Float(required=False, description='price per W to consider in case the import limit is exceeded. ')
})
Expand Down Expand Up @@ -146,6 +147,7 @@ def post(self):
grid_data = data.get('grid', {})
grid = GridConfig(
p_max_imp=grid_data.get('p_max_imp', None),
p_max_abs_imp=grid_data.get('p_max_abs_imp', None),
p_max_exp=grid_data.get('p_max_exp', None),
prc_p_exc_imp=grid_data.get('prc_p_exc_imp', None)
)
Expand Down Expand Up @@ -212,6 +214,8 @@ def post(self):
result = optimizer.solve()
return result

except ValueError as e:
api.abort(400, f"Invalid configuration: {str(e)}")
except Exception as e:
api.abort(500, f"Optimization failed: {str(e)}")

Expand Down
15 changes: 15 additions & 0 deletions src/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class OptimizationStrategy:
@dataclass
class GridConfig:
p_max_imp: float
p_max_abs_imp: float
Comment on lines 18 to +20

Choose a reason for hiding this comment

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

suggestion: Consider making p_max_abs_imp (and possibly p_max_imp) explicitly optional since they can be None in practice.

In app.py, GridConfig is constructed with grid_data.get(..., None) for these fields, so they may be None at runtime despite being annotated as float. This can mislead type checkers and hide None-handling bugs. Please either annotate them as Optional[float] (and do the same for other nullable fields) or ensure None is never passed in.

Suggested implementation:

from dataclasses import dataclass
from typing import Optional
@dataclass
class GridConfig:
    p_max_imp: Optional[float]
    p_max_abs_imp: Optional[float]
    p_max_exp: float
    prc_p_exc_imp: float
            # hard physical cap on how far above p_max_imp the import can go
            if self.grid.p_max_abs_imp is not None and self.grid.p_max_imp is not None:
                self.problem += self.variables['p_max_imp_exc'] \
                    <= self.grid.p_max_abs_imp - self.grid.p_max_imp

Other configuration fields that can be None in practice and are currently typed as float (or other non-Optional types) should also be updated to Optional[...] in GridConfig and any similar config dataclasses. Any usage sites that perform arithmetic or comparisons on these fields should either:

  1. Be guarded with is not None checks (as done above), or
  2. Enforce non-nullability earlier (e.g. validate and coerce defaults when constructing GridConfig instead of passing None through).
    You may also want to adjust the construction in app.py to avoid using get(..., None) where a value is required, or add explicit validation if None is not acceptable.

p_max_exp: float
prc_p_exc_imp: float

Expand Down Expand Up @@ -98,6 +99,15 @@ def __init__(self, strategy: OptimizationStrategy, grid: GridConfig, batteries:
if self.grid.p_max_imp is not None and self.grid.prc_p_exc_imp is not None:
self.is_grid_demand_rate_active = True

# validate p_max_abs_imp configuration
if self.grid.p_max_abs_imp is not None:
if self.grid.p_max_imp is None:
raise ValueError("p_max_abs_imp requires p_max_imp to be set")
if self.grid.p_max_abs_imp < self.grid.p_max_imp:
raise ValueError(
f"p_max_abs_imp ({self.grid.p_max_abs_imp}) must be >= p_max_imp ({self.grid.p_max_imp})"
)

def create_model(self):
"""
Create and initialize the MILP model
Expand Down Expand Up @@ -392,6 +402,11 @@ def _add_energy_balance_constraints(self):
self.problem += self.variables['e_imp_lim_exc'][t] \
<= self.variables['p_max_imp_exc'] * self.time_series.dt[t] / 3600

# hard physical cap on how far above p_max_imp the import can go
if self.grid.p_max_abs_imp is not None:
self.problem += self.variables['p_max_imp_exc'] \
<= self.grid.p_max_abs_imp - self.grid.p_max_imp

def _add_battery_constraints(self):
"""
Add constraints related to battery behavior to the model.
Expand Down
Loading