Modeling Electric Vehicles

Dear community,

Is there a way to constrain a GenericStorage component to only charge when it is not discharging? This will be a key element in modeling Electric Vehicles. I can imagine there has already been discussion about this, so a reference to a thread about this would also be great :slight_smile: Thanks!

Hi @TimKaasjager,

there are several ways to model electric vehicles. I assume you want one GenericStorage per vehicle. One way to do so is the constraint limit_active_flow_count. However, as you probably have pre-calculated time series for departure and arrival, setting the “max” of the charging flow to zero between these points in time promises faster optimisation times.

Thanks for your considerations. I have added this condition as follows:

    # Add charging constraint for EV
    for t, timestep in enumerate(energysystem.timeindex):
        # node_index_battery = energysystem.nodes.index(storage)
        # for i in range(model_data['num_movers']):
            
            if data_mover['Charging State'].iloc[t-1]:
                model.flows[ bel, barn_mover_battery].max = 100e10
            else:
                model.flows[ bel, barn_mover_battery].max = 0

The data_mover[‘Charging State’] is a boolean Pandas series that is False when the load is >0 (i.e. it is driving). Despite setting the max to zero between the electricity bus (bel, label ‘electricity_bus’) and the battery (barn_mover_battery, label ‘barn_mover_battery’), there is still a flow value when the load >0 (see figures

Figure 2024-02-06 124136
Figure 2024-02-06 124130

This is the flow when printed,

EdgeLabel(input="<oemof.solph.buses._bus.Bus: 'electricity_bus'>", output="<oemof.solph.components._generic_storage.GenericStorage: 'barn_mover_battery'>")

so I’m confident it’s the right direction. Am I setting the max wrong?

Your feeling is right. In your code, you are resetting the max value for every iteration of the loop. So, you end up setting every entry to 0 (if the last charge state is False) or 100e10 (else). I think, the easiest solution would look like:

barn_mover_battery = GenericStorage(
    ...
    inputs={bel: Flow(
        nominal_value=10e10,
        max=data_mover['Charging State'].replace({True: 1, False: 0}),
    )},
    ...
)

@pschoen thanks again for your help with this. Is the nominal value here a good way to emulate the maximum charging current of the battery? And doesn’t setting the max to 1 conflict with the nominal value of 10e10? It looks like with this you are setting the max to a value of 1?

The nominal_value marks the capacity (I suggested to rename the parameter, but there was not much feedback, yet: Rename "nominal_value" of Flow · Issue #991 · oemof/oemof-solph · GitHub), min, fix, and max are all in relative units, meaning typically in the range [0:1]. (You can exceed 1, think of it as overload. But the default is max == 1.)

1 Like

I see thanks! ‘Nominal’ is in the eye of the beholder :wink:

By the way, is limit_active_flow_count_by_keyword still a functional constraint? I get this issue:

    constraints.limit_active_flow_count_by_keyword(

  File ~\.conda\envs\energysystem\Lib\site-packages\oemof\solph\constraints\flow_count_limit.py:133 in limit_active_flow_count_by_keyword

    for i, o in model.NonConvexFlowBlock.NONCONVEX_FLOWS:

  File ~\.conda\envs\energysystem\Lib\site-packages\pyomo\core\base\block.py:559 in __getattr__
    raise AttributeError(

AttributeError: 'NonConvexFlowBlock' object has no attribute 'NONCONVEX_FLOWS'

The error handling with limit_active_flow_count is nice , because it reads

10:51:57-ERROR-Constructing component 'one_tractor_battery_constraint_build' from data=None failed:

indicating that no data was generated. Below i have a very simple model where only one of the two batteries is allowed to charge the load on the bev bus. My guess is that something is wrong with the format of the flows, which I assumed to be as it is mentioned in the documentation:

flows (list of flows) – flows (have to be NonConvex) in the format [(in, out)]

def saturating_storage_example():
    # create an energy system
    idx = pd.date_range("1/1/2023", periods=100, freq="H")
    es = solph.EnergySystem(timeindex=idx, infer_last_interval=False)

    # power bus
    bel = solph.Bus(label="bel")
    bev = solph.Bus(label ='bev')
    es.add(bel,bev)
    

    es.add(
        solph.components.Source(
            label="source_el",
            outputs={bel: solph.Flow(nominal_value=1, fix=1)},
        )
    )

    es.add(
        solph.components.Sink(
            label="sink_el",
            inputs={
                bev: solph.Flow(
                    nominal_value=1,
                    variable_costs=1,
                )
            },
        )
    )

    # Electric Storage

    inflow_capacity = 0.5
    full_charging_limit = 0.4
    storage_capacity = 10
    battery = solph.components.GenericStorage(
        label="battery_1",
        nominal_storage_capacity=storage_capacity,
        inputs={bel: solph.Flow(nominal_value=inflow_capacity)},
        outputs={bev: solph.Flow(variable_costs=2)},
        initial_storage_level=0,
        balanced=False,
        loss_rate=0.0001,
    )
    es.add(battery)
    
    battery = solph.components.GenericStorage(
        label="battery_2",
        nominal_storage_capacity=storage_capacity,
        inputs={bel: solph.Flow(nominal_value=inflow_capacity)},
        outputs={bev: solph.Flow(variable_costs=2)},
        initial_storage_level=0,
        balanced=False,
        loss_rate=0.0001,
    )
    es.add(battery)

    # create an optimization problem and solve it
    model = solph.Model(es)
    battery1 = es.groups['battery_1']
    battery2 = es.groups['battery_2']
        
    # only one of the tractor batteries may be actively discharging at a time
    solph.constraints.limit_active_flow_count(
        model=model, constraint_name = 'one_tractor_battery', flows = [('battery_1','bev'),('battery_2','bev')], lower_limit=0, upper_limit=1
    )

Oops, forgot to add nonconvex = NonConvex() in the flow