Skip to content

Commit

Permalink
Merge branch 'main' into 398-early-stopping-learning-rate-and-noise-d…
Browse files Browse the repository at this point in the history
…ecay
  • Loading branch information
mthede authored Dec 3, 2024
2 parents 4c1af98 + 16cbea2 commit 25335f0
Show file tree
Hide file tree
Showing 13 changed files with 1,305 additions and 210 deletions.
8 changes: 4 additions & 4 deletions assume/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
flexablePosCRMStorage,
)
from assume.strategies.naive_strategies import (
NaiveDASteelplantStrategy,
NaiveDADSMStrategy,
NaiveProfileStrategy,
NaiveRedispatchSteelplantStrategy,
NaiveRedispatchDSMStrategy,
NaiveRedispatchStrategy,
NaiveSingleBidStrategy,
)
Expand All @@ -38,8 +38,8 @@
"flexable_neg_crm_storage": flexableNegCRMStorage,
"flexable_pos_crm_storage": flexablePosCRMStorage,
"naive_redispatch": NaiveRedispatchStrategy,
"naive_da_steelplant": NaiveDASteelplantStrategy,
"naive_steel_redispatch": NaiveRedispatchSteelplantStrategy,
"naive_da_dsm": NaiveDADSMStrategy,
"naive_redispatch_dsm": NaiveRedispatchDSMStrategy,
"manual_strategy": SimpleManualTerminalStrategy,
"dmas_powerplant": DmasPowerplantStrategy,
"dmas_storage": DmasStorageStrategy,
Expand Down
14 changes: 12 additions & 2 deletions assume/strategies/naive_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ def calculate_bids(
return bids


class NaiveDASteelplantStrategy(BaseStrategy):
class NaiveDADSMStrategy(BaseStrategy):
"""
A naive strategy of a Demand Side Management (DSM) unit. The bid volume is the optimal power requirement of
the unit at the start time of the product. The bid price is the marginal cost of the unit at the start time of the product.
"""

def calculate_bids(
self,
unit: SupportsMinMax,
Expand Down Expand Up @@ -176,7 +181,12 @@ def calculate_bids(
return bids


class NaiveRedispatchSteelplantStrategy(BaseStrategy):
class NaiveRedispatchDSMStrategy(BaseStrategy):
"""
A naive strategy of a Demand Side Management (DSM) unit that bids the available flexibility of the unit on the redispatch market.
The bid volume is the flexible power requirement of the unit at the start time of the product. The bid price is the marginal cost of the unit at the start time of the product.
"""

def calculate_bids(
self,
unit: SupportsMinMax,
Expand Down
2 changes: 2 additions & 0 deletions assume/units/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from assume.units.powerplant import PowerPlant
from assume.units.storage import Storage
from assume.units.steel_plant import SteelPlant
from assume.units.hydrogen_plant import HydrogenPlant
from assume.units.dst_components import demand_side_technologies

unit_types: dict[str, BaseUnit] = {
"power_plant": PowerPlant,
"demand": Demand,
"storage": Storage,
"steel_plant": SteelPlant,
"hydrogen_plant": HydrogenPlant,
}
201 changes: 155 additions & 46 deletions assume/units/dsm_load_shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import logging

import pyomo.environ as pyo
from pyomo.opt import SolverStatus, TerminationCondition

from assume.common.fast_pandas import FastSeries
from assume.units.dst_components import demand_side_technologies

logger = logging.getLogger(__name__)


class DSMFlex:
def __init__(self, components, **kwargs):
Expand Down Expand Up @@ -50,6 +56,137 @@ def initialize_components(self):
self.model, self.model.dsm_blocks[technology]
)

def switch_to_opt(self, instance):
"""
Switches the instance to solve a cost based optimisation problem by deactivating the flexibility constraints and objective.
Args:
instance (pyomo.ConcreteModel): The instance of the Pyomo model.
Returns:
pyomo.ConcreteModel: The modified instance with flexibility constraints and objective deactivated.
"""
# deactivate the flexibility constraints and objective
instance.obj_rule_flex.deactivate()

instance.total_cost_upper_limit.deactivate()
instance.total_power_input_constraint_with_flex.deactivate()

return instance

def switch_to_flex(self, instance):
"""
Switches the instance to flexibility mode by deactivating few constraints and objective function.
Args:
instance (pyomo.ConcreteModel): The instance of the Pyomo model.
Returns:
pyomo.ConcreteModel: The modified instance with optimal constraints and objective deactivated.
"""
# deactivate the optimal constraints and objective
instance.obj_rule_opt.deactivate()
instance.total_power_input_constraint.deactivate()

# fix values of model.total_power_input
for t in instance.time_steps:
instance.total_power_input[t].fix(self.opt_power_requirement.iloc[t])
instance.total_cost = self.total_cost

return instance

def determine_optimal_operation_without_flex(self):
"""
Determines the optimal operation of the steel plant without considering flexibility.
"""
# create an instance of the model
instance = self.model.create_instance()
# switch the instance to the optimal mode by deactivating the flexibility constraints and objective
instance = self.switch_to_opt(instance)
# solve the instance
results = self.solver.solve(instance, options=self.solver_options)

# Check solver status and termination condition
if (results.solver.status == SolverStatus.ok) and (
results.solver.termination_condition == TerminationCondition.optimal
):
logger.debug("The model was solved optimally.")

# Display the Objective Function Value
objective_value = instance.obj_rule_opt()
logger.debug("The value of the objective function is %s.", objective_value)

elif results.solver.termination_condition == TerminationCondition.infeasible:
logger.debug("The model is infeasible.")

else:
logger.debug("Solver Status: ", results.solver.status)
logger.debug(
"Termination Condition: ", results.solver.termination_condition
)

opt_power_requirement = [
pyo.value(instance.total_power_input[t]) for t in instance.time_steps
]
self.opt_power_requirement = FastSeries(
index=self.index, value=opt_power_requirement
)

self.total_cost = sum(
instance.variable_cost[t].value for t in instance.time_steps
)

# Variable cost series
variable_cost = [
pyo.value(instance.variable_cost[t]) for t in instance.time_steps
]
self.variable_cost_series = FastSeries(index=self.index, value=variable_cost)

def determine_optimal_operation_with_flex(self):
"""
Determines the optimal operation of the steel plant without considering flexibility.
"""
# create an instance of the model
instance = self.model.create_instance()
# switch the instance to the flexibility mode by deactivating the optimal constraints and objective
instance = self.switch_to_flex(instance)
# solve the instance
results = self.solver.solve(instance, options=self.solver_options)

# Check solver status and termination condition
if (results.solver.status == SolverStatus.ok) and (
results.solver.termination_condition == TerminationCondition.optimal
):
logger.debug("The model was solved optimally.")

# Display the Objective Function Value
objective_value = instance.obj_rule_flex()
logger.debug("The value of the objective function is %s.", objective_value)

elif results.solver.termination_condition == TerminationCondition.infeasible:
logger.debug("The model is infeasible.")

else:
logger.debug("Solver Status: ", results.solver.status)
logger.debug(
"Termination Condition: ", results.solver.termination_condition
)

flex_power_requirement = [
pyo.value(instance.total_power_input[t]) for t in instance.time_steps
]
self.flex_power_requirement = FastSeries(
index=self.index, value=flex_power_requirement
)

# Variable cost series
flex_variable_cost = [
instance.variable_cost[t].value for t in instance.time_steps
]
self.flex_variable_cost_series = FastSeries(
index=self.index, value=flex_variable_cost
)

def flexibility_cost_tolerance(self, model):
"""
Modify the optimization model to include constraints for flexibility within cost tolerance.
Expand All @@ -69,53 +206,25 @@ def total_cost_upper_limit(m, t):

@model.Constraint(model.time_steps)
def total_power_input_constraint_with_flex(m, t):
if self.has_electrolyser:
# Apply constraints based on the technology type
if self.technology == "hydrogen_plant":
# Hydrogen plant constraint
return (
m.total_power_input[t] - m.load_shift[t]
== self.model.dsm_blocks["electrolyser"].power_in[t]
+ self.model.dsm_blocks["eaf"].power_in[t]
+ self.model.dsm_blocks["dri_plant"].power_in[t]
)
else:
return (
m.total_power_input[t] - m.load_shift[t]
== self.model.dsm_blocks["eaf"].power_in[t]
+ self.model.dsm_blocks["dri_plant"].power_in[t]
)

def recalculate_with_accepted_offers(self, model):
self.reference_power = self.forecaster[f"{self.id}_power"]
self.accepted_pos_capacity = self.forecaster[
f"{self.id}_power_steelneg"
] # _accepted_pos_res

# Parameters
model.reference_power = pyo.Param(
model.time_steps,
initialize={t: value for t, value in enumerate(self.reference_power)},
)

model.accepted_pos_capacity = pyo.Param(
model.time_steps,
initialize={t: value for t, value in enumerate(self.accepted_pos_capacity)},
)

# Variables
model.capacity_upper_bound = pyo.Var(
model.time_steps, within=pyo.NonNegativeReals
)

# Constraints
@model.Constraint(model.time_steps)
def capacity_upper_bound_constraint(m, t):
return (
m.capacity_upper_bound[t]
== m.reference_power[t] - m.accepted_pos_capacity[t]
)

@model.Constraint(model.time_steps)
def total_power_upper_limit(m, t):
if m.accepted_pos_capacity[t] > 0:
return m.total_power_input[t] <= m.capacity_upper_bound[t]
else:
return pyo.Constraint.Skip
elif self.technology == "steel_plant":
# Steel plant constraint with conditional electrolyser inclusion
if self.has_electrolyser:
return (
m.total_power_input[t] - m.load_shift[t]
== self.model.dsm_blocks["electrolyser"].power_in[t]
+ self.model.dsm_blocks["eaf"].power_in[t]
+ self.model.dsm_blocks["dri_plant"].power_in[t]
)
else:
return (
m.total_power_input[t] - m.load_shift[t]
== self.model.dsm_blocks["eaf"].power_in[t]
+ self.model.dsm_blocks["dri_plant"].power_in[t]
)
Loading

0 comments on commit 25335f0

Please sign in to comment.