Skip to content

odys.optimization.formulations.electric_vehicle

The electric vehicle formulation: the shared battery model (see storage) with trip energy drawn while driving, plus the driving and departure-SOC constraints. Vehicles charge and discharge only through chargers (see charging).

The variables, constraints and objective terms are written out in the Electric vehicles and chargers and Batteries formulation pages.

See also ElectricVehicle for the domain model and entity_arrays for the parameter arrays.

Model formulation of electric vehicles.

ElectricVehicleFormulation

Bases: VariableFormulation[StorageVariables]

Electric vehicles: the shared battery model with trip energy drawn from the battery while driving.

A vehicle cannot charge or discharge while driving and must reach its departure SOC before each trip. It charges and discharges only through a charger (ChargingFormulation).

Source code in src/odys/optimization/formulations/electric_vehicle.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
class ElectricVehicleFormulation(VariableFormulation[StorageVariables]):
    """Electric vehicles: the shared battery model with trip energy drawn from the battery while driving.

    A vehicle cannot charge or discharge while driving and must reach its departure SOC
    before each trip. It charges and discharges only through a charger (`ChargingFormulation`).
    """

    dimension = EV
    entity_type = ElectricVehicle
    power_in_name: ClassVar[str] = f"{EV}_power_in"
    net_power_name: ClassVar[str] = f"{EV}_net_power"
    soc_name: ClassVar[str] = f"{EV}_soc"
    charge_mode_name: ClassVar[str] = f"{EV}_charge_mode"

    def __init__(self, electric_vehicles: Sequence[ElectricVehicle], context: ModelContext) -> None:
        """Initialize with the electric vehicles of the system.

        Args:
            electric_vehicles: The electric vehicles, at least one.
            context: The shared indexing of the problem.
        """
        super().__init__(electric_vehicles, context)
        arrays = electric_vehicle_arrays(electric_vehicles, self.coordinates, context)
        self.trips: ElectricVehicleTripArrays = arrays.trips
        self.storage = StorageFormulation(
            self.coordinates,
            arrays.battery,
            arrays.states,
            context,
            soc_drop=arrays.trips.trip_energy / arrays.battery.capacity,
        )

    @classmethod
    def build(cls, inputs: FormulationInputs) -> Self | None:
        """Return the formulation of the electric vehicles, or None if there are none."""
        electric_vehicles = inputs.of_type(ElectricVehicle)
        return cls(electric_vehicles, inputs.context) if electric_vehicles else None

    def _create_variables(self, model: linopy.Model) -> StorageVariables:
        """Add the battery variables of every electric vehicle."""
        return self.storage.create_variables(model)

    @constraint
    def _get_ev_max_charge_constraint(self) -> ModelConstraint:
        """Charging power is limited by `max_charge_power`, and only in charging mode."""
        return self.storage.max_charge_constraint(self.variables)

    @constraint
    def _get_ev_max_discharge_constraint(self) -> ModelConstraint:
        """Discharging power is limited by `max_discharge_power`, and only outside charging mode."""
        return self.storage.max_discharge_constraint(self.variables)

    @constraint
    def _get_ev_soc_dynamics_constraint(self) -> ModelConstraint:
        """SOC follows self-discharge, charging, discharging and trip energy from step to step."""
        return self.storage.soc_dynamics_constraint(self.variables)

    @constraint
    def _get_ev_soc_start_constraint(self) -> ModelConstraint:
        """The SOC after the first step starts from the initial state of charge, less a trip departing at t=0."""
        return self.storage.soc_start_constraint(self.variables)

    @constraint
    def _get_ev_soc_end_constraint(self) -> list[ModelConstraint]:
        """The SOC at the last step equals the vehicle's final state of charge, where set."""
        return self.storage.soc_end_constraint(self.variables)

    @constraint
    def _get_ev_soc_min_constraint(self) -> ModelConstraint:
        """The SOC never falls below `soc_min`."""
        return self.storage.soc_min_constraint(self.variables)

    @constraint
    def _get_ev_soc_max_constraint(self) -> ModelConstraint:
        """The SOC never exceeds `soc_max`."""
        return self.storage.soc_max_constraint(self.variables)

    @constraint
    def _get_ev_capacity_constraint(self) -> ModelConstraint:
        """The SOC never exceeds a full battery."""
        return self.storage.capacity_constraint(self.variables)

    @constraint
    def _get_ev_driving_constraint(self) -> ModelConstraint:
        """A vehicle cannot charge or discharge while driving."""
        battery = self.storage.battery
        max_power = battery.max_charge_power + battery.max_discharge_power
        return ModelConstraint(
            constraint=self.variables.power_in + self.variables.power_out <= max_power * (1 - self.trips.is_driving),
            name="ev_driving_constraint",
        )

    @constraint
    def _get_ev_min_soc_departure_constraint(self) -> ModelConstraint:
        """A vehicle reaches its departure SOC by the end of the step before the trip."""
        min_soc_before_departure = self.trips.min_soc_at_departure.shift({ModelDimension.Time: -1}, fill_value=0)
        return ModelConstraint(
            constraint=self.variables.soc >= min_soc_before_departure,
            name="ev_min_soc_departure_constraint",
        )

    @constraint
    def _get_ev_net_power_constraint(self) -> ModelConstraint:
        """Net power is charging minus discharging power."""
        return self.storage.net_power_constraint(self.variables)

    def power_injection(self) -> linopy.LinearExpression:
        """Return discharging minus charging power (V2G minus charging), summed over the vehicles."""
        return self.storage.power_injection(self.variables)

    def profit(self) -> linopy.LinearExpression:
        """Return minus the battery degradation cost per scenario."""
        profit: linopy.LinearExpression = -self.storage.degradation_cost(self.variables)
        return profit

    def dispatch(self, solution: xr.Dataset) -> ElectricVehicleDispatch:
        """Return the net power, state of charge and charge mode of the electric vehicles."""
        return ElectricVehicleDispatch(self.storage.dispatch_data(solution), EV)

__init__(electric_vehicles, context)

Initialize with the electric vehicles of the system.

Parameters:

Name Type Description Default
electric_vehicles Sequence[ElectricVehicle]

The electric vehicles, at least one.

required
context ModelContext

The shared indexing of the problem.

required
Source code in src/odys/optimization/formulations/electric_vehicle.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def __init__(self, electric_vehicles: Sequence[ElectricVehicle], context: ModelContext) -> None:
    """Initialize with the electric vehicles of the system.

    Args:
        electric_vehicles: The electric vehicles, at least one.
        context: The shared indexing of the problem.
    """
    super().__init__(electric_vehicles, context)
    arrays = electric_vehicle_arrays(electric_vehicles, self.coordinates, context)
    self.trips: ElectricVehicleTripArrays = arrays.trips
    self.storage = StorageFormulation(
        self.coordinates,
        arrays.battery,
        arrays.states,
        context,
        soc_drop=arrays.trips.trip_energy / arrays.battery.capacity,
    )

build(inputs) classmethod

Return the formulation of the electric vehicles, or None if there are none.

Source code in src/odys/optimization/formulations/electric_vehicle.py
54
55
56
57
58
@classmethod
def build(cls, inputs: FormulationInputs) -> Self | None:
    """Return the formulation of the electric vehicles, or None if there are none."""
    electric_vehicles = inputs.of_type(ElectricVehicle)
    return cls(electric_vehicles, inputs.context) if electric_vehicles else None

dispatch(solution)

Return the net power, state of charge and charge mode of the electric vehicles.

Source code in src/odys/optimization/formulations/electric_vehicle.py
137
138
139
def dispatch(self, solution: xr.Dataset) -> ElectricVehicleDispatch:
    """Return the net power, state of charge and charge mode of the electric vehicles."""
    return ElectricVehicleDispatch(self.storage.dispatch_data(solution), EV)

power_injection()

Return discharging minus charging power (V2G minus charging), summed over the vehicles.

Source code in src/odys/optimization/formulations/electric_vehicle.py
128
129
130
def power_injection(self) -> linopy.LinearExpression:
    """Return discharging minus charging power (V2G minus charging), summed over the vehicles."""
    return self.storage.power_injection(self.variables)

profit()

Return minus the battery degradation cost per scenario.

Source code in src/odys/optimization/formulations/electric_vehicle.py
132
133
134
135
def profit(self) -> linopy.LinearExpression:
    """Return minus the battery degradation cost per scenario."""
    profit: linopy.LinearExpression = -self.storage.degradation_cost(self.variables)
    return profit