SOC-based charging constraints for District Heating

Dear Oemof Developers,

I’m currently working on a district heating model using oemof and I’m stuck on defining charging constraints for a thermal storage. I’d greatly appreciate any advice or suggestions, eventhough any referenceable code!

The scope of the storage is 50 MWh, and I’d like to implement the following logic:

  1. Charging can only start when SOC < 25 MWh.
  2. Once charging has started, there is no restriction on continuing until SOC reaches 50 MWh.
  3. There’s no limit on the charging rate or on the final SOC (balanced=True).

What I’ve tried:

I defined the thermal storage and used a Big-M binary variable instead of if to control whether charging is allowed at each timestep. But it doesn’t work. The relevant code snippet is below:

from oemof.solph import EnergySystem, Bus, Flow, GenericStorage, Sink
import pyomo.environ as po


— Create buses —

heat_bus = Bus(label=‘heat_bus’)

— Thermal storage —

thermal_storage = GenericStorage(
   label=‘thermal_storage’,
   inputs={heat_bus: Flow(nominal_value=50)},
   outputs={heat_bus:Flow(nominal_value=50)},
   nominal_storage_capacity=50,
   initial_storage_level=0,
   inflow_conversion_factor=1,
   outflow_conversion_factor=1,
   loss_rate=0.01,
   balanced=True,
)

— Heat demand —

heat_sink = Sink(
    label=‘heat_demand’,
    inputs={heat_bus: Flow(nominal_value=1, fix=heat_load)}
)

— Model —

model = Model(energysystem)

— SOC-based discrete charging —

M_flow = 1e3  # Big-M for max flow
threshold = 25  # SOC threshold for starting charge

# Binary variable: 1 if charging allowed
model.charge_mode = po.Var(model.TIMEINDEX, within=po.Binary)

# Constraint: inflow allowed only if charge_mode=1
def charging_activation_rule(m, t):
    inflow_var = m.flow[(heat_bus, thermal_storage)][t]
    return inflow_var <= M_flow * m.charge_mode[t]

model.charging_activation = po.Constraint(model.TIMEINDEX,rule=charging_activation_rule)

# Constraint: activate charge_mode only if SOC < threshold
def soc_limit_rule(m, t):soc = m.GenericStorageBlock.storage_content[thermal_storage, t]return soc <= threshold + M_flow*(1 - m.charge_mode[t])

model.soc_limit = po.Constraint(model.TIMEINDEX, rule=soc_limit_rule)

Looking forward to your any words!

Yours Sincerely

Xinyuan

If you need orientation, we have something along the lines implemented[1], but only allowing flows to be active until a specific SOC. As far as I understand you, you want to allow charging to be continued afterwards. I do not see where this would be defined in your code, though. If you want something like this, you’d need to track the status of the flow and also status changes. We have something to limit the number of startups [2] that can also provide ideas for the implementation.

Hope this already helps.

Bye,
Patrik

1: https://oemof-solph.readthedocs.io/en/stable/\_modules/oemof/solph/constraints/storage_level.html#storage_level_constraint
2: oemof.solph.flows._non_convex_flow_block - v0.6.0