diff --git a/client/client.gen.go b/client/client.gen.go index 37e49d44f..802fa0c09 100644 --- a/client/client.gen.go +++ b/client/client.gen.go @@ -110,6 +110,11 @@ type Error struct { // GridConfig defines model for GridConfig. type GridConfig struct { + // PMaxAbsImp 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. + PMaxAbsImp float32 `json:"p_max_abs_imp,omitempty"` + // PMaxExp Maximum grid export power in W PMaxExp float32 `json:"p_max_exp,omitempty"` diff --git a/openapi.yaml b/openapi.yaml index 7b70bd4d8..02c71809a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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: diff --git a/src/optimizer/app.py b/src/optimizer/app.py index a7a823f6b..b0b33a147 100644 --- a/src/optimizer/app.py +++ b/src/optimizer/app.py @@ -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. ') }) @@ -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) ) @@ -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)}") diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index 6cac33ee6..8e5e60d2d 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -17,6 +17,7 @@ class OptimizationStrategy: @dataclass class GridConfig: p_max_imp: float + p_max_abs_imp: float p_max_exp: float prc_p_exc_imp: float @@ -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 @@ -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.