Skip to content

odys.optimization.formulations.charging

The charging formulation: the assignment of vehicles to chargers and the charger power limits. It receives the electric vehicle formulation it couples to, and is outside the power balance.

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

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

Model formulation of chargers: which electric vehicle each charger serves, and at what power.

ChargingFormulation

Bases: VariableFormulation[ChargingVariables]

Chargers: assign electric vehicles to chargers and cap their power by the charger's rating.

It couples to the ElectricVehicleFormulation it receives: the vehicles' power variables and trip schedule. Chargers inject nothing into the power balance; the vehicles do.

Source code in src/odys/optimization/formulations/charging.py
 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
class ChargingFormulation(VariableFormulation[ChargingVariables]):
    """Chargers: assign electric vehicles to chargers and cap their power by the charger's rating.

    It couples to the `ElectricVehicleFormulation` it receives: the vehicles' power
    variables and trip schedule. Chargers inject nothing into the power balance;
    the vehicles do.
    """

    dimension = CHARGER
    entity_type = Charger
    assignment_name: ClassVar[str] = "charger_ev_assignment"

    def __init__(
        self,
        chargers: Sequence[Charger],
        context: ModelContext,
        electric_vehicles: ElectricVehicleFormulation,
    ) -> None:
        """Initialize with the chargers of the system and the electric vehicles they serve.

        Args:
            chargers: The chargers, at least one.
            context: The shared indexing of the problem.
            electric_vehicles: The formulation of the vehicles the chargers serve.
        """
        super().__init__(chargers, context)
        self.arrays = vectorize(ChargerArrays, chargers, self.coordinates)
        self.electric_vehicles = electric_vehicles

    @classmethod
    def build(cls, inputs: FormulationInputs) -> Self | None:
        """Return the formulation of the chargers, or None if there are none.

        Raises:
            OdysError: If there are chargers but no electric vehicle formulation was built before.
        """
        chargers = inputs.of_type(Charger)
        if not chargers:
            return None
        electric_vehicles = inputs.formulation_of(ElectricVehicleFormulation)
        if electric_vehicles is None:
            msg = "ChargingFormulation must be built after an ElectricVehicleFormulation."
            raise OdysError(msg)
        return cls(chargers, inputs.context, electric_vehicles)

    def _create_variables(self, model: linopy.Model) -> ChargingVariables:
        """Add the binary assignment of each vehicle to each charger, per scenario and time."""
        coords = self.context.variable_coords(self.coordinates, self.electric_vehicles.coordinates)
        return ChargingVariables(
            assignment=model.add_variables(name=self.assignment_name, coords=coords, binary=True),
        )

    @constraint
    def _get_charger_one_ev_per_charger_constraint(self) -> ModelConstraint:
        """Each charger serves at most one vehicle at a time."""
        return ModelConstraint(
            constraint=self.variables.assignment.sum(EV) <= 1,
            name="charger_one_ev_per_charger_constraint",
        )

    @constraint
    def _get_charger_one_charger_per_ev_constraint(self) -> ModelConstraint:
        """Each vehicle is connected to at most one charger at a time."""
        return ModelConstraint(
            constraint=self.variables.assignment.sum(CHARGER) <= 1,
            name="charger_one_charger_per_ev_constraint",
        )

    @constraint
    def _get_charger_no_assignment_while_driving_constraint(self) -> ModelConstraint:
        """A driving vehicle cannot be assigned to a charger."""
        return ModelConstraint(
            constraint=self.variables.assignment <= 1 - self.electric_vehicles.trips.is_driving,
            name="charger_no_assignment_while_driving_constraint",
        )

    @constraint
    def _get_charger_power_limit_constraint(self) -> ModelConstraint:
        """A vehicle's charging and discharging power is capped by its assigned charger's `max_power`.

        With no charger assigned the cap is 0, so a vehicle charges or discharges (V2G) only through a charger.
        """
        available_power = (self.variables.assignment * self.arrays.max_power).sum(CHARGER)
        vehicles = self.electric_vehicles.variables
        return ModelConstraint(
            constraint=vehicles.power_in + vehicles.power_out <= available_power,
            name="charger_power_limit_constraint",
        )

    def power_injection(self) -> None:
        """Return None: chargers are outside the power balance; the vehicles they serve inject."""

    def dispatch(self, solution: xr.Dataset) -> ChargerDispatch:
        """Return the assignment of vehicles to chargers and the power each charger delivers."""
        assignment = solution[self.assignment_name]
        power: xr.DataArray = (assignment * solution[ElectricVehicleFormulation.power_in_name]).sum(EV)
        return ChargerDispatch(xr.Dataset({"assignment": assignment, "power": power}), CHARGER)

__init__(chargers, context, electric_vehicles)

Initialize with the chargers of the system and the electric vehicles they serve.

Parameters:

Name Type Description Default
chargers Sequence[Charger]

The chargers, at least one.

required
context ModelContext

The shared indexing of the problem.

required
electric_vehicles ElectricVehicleFormulation

The formulation of the vehicles the chargers serve.

required
Source code in src/odys/optimization/formulations/charging.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def __init__(
    self,
    chargers: Sequence[Charger],
    context: ModelContext,
    electric_vehicles: ElectricVehicleFormulation,
) -> None:
    """Initialize with the chargers of the system and the electric vehicles they serve.

    Args:
        chargers: The chargers, at least one.
        context: The shared indexing of the problem.
        electric_vehicles: The formulation of the vehicles the chargers serve.
    """
    super().__init__(chargers, context)
    self.arrays = vectorize(ChargerArrays, chargers, self.coordinates)
    self.electric_vehicles = electric_vehicles

build(inputs) classmethod

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

Raises:

Type Description
OdysError

If there are chargers but no electric vehicle formulation was built before.

Source code in src/odys/optimization/formulations/charging.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@classmethod
def build(cls, inputs: FormulationInputs) -> Self | None:
    """Return the formulation of the chargers, or None if there are none.

    Raises:
        OdysError: If there are chargers but no electric vehicle formulation was built before.
    """
    chargers = inputs.of_type(Charger)
    if not chargers:
        return None
    electric_vehicles = inputs.formulation_of(ElectricVehicleFormulation)
    if electric_vehicles is None:
        msg = "ChargingFormulation must be built after an ElectricVehicleFormulation."
        raise OdysError(msg)
    return cls(chargers, inputs.context, electric_vehicles)

dispatch(solution)

Return the assignment of vehicles to chargers and the power each charger delivers.

Source code in src/odys/optimization/formulations/charging.py
125
126
127
128
129
def dispatch(self, solution: xr.Dataset) -> ChargerDispatch:
    """Return the assignment of vehicles to chargers and the power each charger delivers."""
    assignment = solution[self.assignment_name]
    power: xr.DataArray = (assignment * solution[ElectricVehicleFormulation.power_in_name]).sum(EV)
    return ChargerDispatch(xr.Dataset({"assignment": assignment, "power": power}), CHARGER)

power_injection()

Return None: chargers are outside the power balance; the vehicles they serve inject.

Source code in src/odys/optimization/formulations/charging.py
122
123
def power_injection(self) -> None:
    """Return None: chargers are outside the power balance; the vehicles they serve inject."""

ChargingVariables

Bases: BaseModel

Decision variables of the chargers.

Source code in src/odys/optimization/formulations/charging.py
25
26
27
28
29
30
class ChargingVariables(BaseModel):
    """Decision variables of the chargers."""

    model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)

    assignment: linopy.Variable