Skip to content

Your first model

Every odys model is built from two objects. An AssetPortfolio holds what you own and operate. An EnergySystem takes that portfolio and adds the rest: the markets you trade on, the futures you plan for, the horizon and where each asset starts. Calling .optimize() on it returns the plan.

Basic usage

Let's build a first system, a small industrial site: a 100 MW CCGT serving a factory over four hours.

from datetime import timedelta

from odys import AssetPortfolio, EnergySystem, FixedLoad, Generator, LoadProfile, Scenario

ccgt = Generator(name="ccgt", nominal_power=100.0, variable_cost=50.0)
factory = FixedLoad(name="factory")

energy_system = EnergySystem(
    portfolio=AssetPortfolio([ccgt, factory]),
    scenarios=Scenario(profiles=(LoadProfile(load=factory, values=[60, 90, 40, 70]),)),
    timestep=timedelta(hours=1),
    number_of_steps=4,
)

result = energy_system.optimize()
print(result.generators.power)
time  generator
0     ccgt         60.0
1     ccgt         90.0
2     ccgt         40.0
3     ccgt         70.0
Name: power, dtype: float64

The CCGT is the only supply, so it follows the factory's demand exactly. The optimizer maximizes profit, and here the profit is only the fuel cost: 260 MWh at 50 $/MWh is 13,000, so the objective is negative:

print(result.objective_value)
-13000.0

The rest of the User Guide adds to this site one piece at a time.

Units

odys doesn't convert units. Powers are in MW and held for the whole step, energies are in MWh, and prices and variable costs are per MWh, in whatever currency your prices are in. For a small asset, convert to MW yourself: a 22 kW charger is 0.022.

A few fields depend on the step length, such as ramp limits and self-discharge. Time and units lists how each one reads it.

The portfolio

An AssetPortfolio takes the assets you own and operate: generators, fixed and flexible loads, battery storage, electric vehicles and chargers. Building it checks two rules, and raises an OdysValidationError when one fails:

  • Asset names are unique within the portfolio.
  • Chargers and electric vehicles come together: a portfolio with vehicles but no charger, or chargers but no vehicle, is rejected.

Markets aren't assets, so they don't go in the portfolio: pass them to the EnergySystem with markets=... (see EnergyMarket).

When you build portfolios in a script, you can read the assets back by name or by type:

portfolio = AssetPortfolio([ccgt, factory])

portfolio.get_asset("ccgt")  # the Generator named "ccgt"
portfolio.assets_of(Generator)  # (ccgt,), in the order you added them
portfolio.assets  # every asset, keyed by name, read-only

EnergySystem parameters

Parameter Type Required Default Description
portfolio AssetPortfolio Yes - The assets you own and operate
scenarios Scenario or list[Scenario] Yes - The futures you plan for: demand, available capacity and prices
timestep timedelta Yes - Length of each step
number_of_steps int Yes - How many steps the horizon has
markets EnergyMarket or list[EnergyMarket] or None No None The markets you buy from and sell to
objective Objective No Objective() What to maximize: expected profit by default
initial_conditions sequence of initial states No () Where each asset starts, just before the first step
final_conditions sequence of final states No () Where an asset has to be at the last step

Each one is covered on its own page:

Creating the EnergySystem checks the whole configuration before anything is solved, and raises an OdysValidationError on the first problem. See Validation and errors.

Initial and final conditions

Some assets carry state from one step to the next: a generator's output, a battery's charge. An initial condition says where such an asset starts, and a final condition where it has to end. Say you're planning tomorrow's dispatch: the CCGT running tonight and the battery's charge at midnight are where tomorrow starts.

Conditions go to the EnergySystem, not to a Scenario, because they're the same in every future: you know the starting state when you plan, and a final state is a target you choose. Each asset has at most one of each.

Asset Initial condition Final condition Initial condition required
Generator GeneratorInitialState GeneratorFinalState When its start matters: ramp limits, minimum up or down times, start-up or shut-down costs
BatteryStorage StorageInitialState StorageFinalState Always
ElectricVehicle ElectricVehicleInitialState ElectricVehicleFinalState Always

Final conditions are always optional. Loads, chargers and markets carry no state, so they take none. Each asset page shows its conditions in code.

One bus

odys connects every asset and every market to a single bus. At every step and in every scenario, the power fed in equals the power taken out:

Feeds the bus Draws from the bus
Generator output FixedLoad demand
Battery discharge, from battery storage and vehicles with V2G FlexibleLoad consumption, within its range
EnergyMarket purchases Battery charging
EnergyMarket sales

There's no network: no lines, no congestion and no losses between assets, so power fed in anywhere serves demand anywhere. Chargers aren't on the bus themselves: the vehicles plugged into them are.

There's no unserved energy either. All demand has to be met, so a system whose supply can't cover it has no solution. odys catches the obvious cases when you create the EnergySystem (see Validation and errors). See PB-1.

Optimizing

.optimize() builds the model as a mixed-integer linear program and solves it with HiGHS, unless you choose another solver with a SolverConfig (see Solvers). It returns the results, one container per asset type; see Results. The exact model is written out in the Mathematical Formulation.

Next steps

The factory's demand was a list of four numbers in a Scenario. Next, see what a Scenario describes: the availability, demand and prices your system faces at each step.