Skip to content

Your data

Attach the model from Your first model to data in files, and keep what a solve produces. Only the data changes. Every block runs from the examples/ folder of the repository.

The files

generators.csv has one row for each generator:

tutorial/data/generators.csv
generator,p_max,cost,p_min,no_load_cost
wind,80.0,10.0,0.0,0.0
solar,0.0,25.0,0.0,0.0
gas,200.0,50.0,50.0,500.0

load.csv has the load in each snapshot:

tutorial/data/load.csv
snapshot,value
0,60.0
1,120.0
2,180.0
3,90.0

The columns p_min and no_load_cost wait for a later tutorial.

From files to sources

Read both files with polars:

import polars as pl

import specsolve as sps

generators = pl.read_csv('tutorial/data/generators.csv')
load = pl.read_csv('tutorial/data/load.csv')
print(generators)
shape: (3, 5)
┌───────────┬───────┬──────┬───────┬──────────────┐
│ generator ┆ p_max ┆ cost ┆ p_min ┆ no_load_cost │
│ ---       ┆ ---   ┆ ---  ┆ ---   ┆ ---          │
│ str       ┆ f64   ┆ f64  ┆ f64   ┆ f64          │
╞═══════════╪═══════╪══════╪═══════╪══════════════╡
│ wind      ┆ 80.0  ┆ 10.0 ┆ 0.0   ┆ 0.0          │
│ solar     ┆ 0.0   ┆ 25.0 ┆ 0.0   ┆ 0.0          │
│ gas       ┆ 200.0 ┆ 50.0 ┆ 50.0  ┆ 500.0        │
└───────────┴───────┴──────┴───────┴──────────────┘

Each parameter needs its own table with a value column, so you pick it out of generators with select. load.csv already has that shape:

sources = {
    'generator': generators['generator'],
    'snapshot': load['snapshot'],
    'p_max': generators.select('generator', value='p_max'),
    'cost': generators.select('generator', value='cost'),
    'load': load,
}

A source can also be a pandas frame or a parquet path; see the data contract.

Solve

result = sps.solve('dispatch.yaml', sources)
print(result.objective)
10500.0

The data is the same as before, so the cost is 10500 again.

Work with the answer

The answer is a polars table. Join it to the cost to see what each generator costs over the four snapshots:

spend = (
    result.primal('p')
    .join(generators, on='generator')
    .group_by('generator')
    .agg(spend=(pl.col('value') * pl.col('cost')).sum())
    .sort('generator')
)
print(spend)
shape: (2, 2)
┌───────────┬────────┐
│ generator ┆ spend  │
│ ---       ┆ ---    │
│ str       ┆ f64    │
╞═══════════╪════════╡
│ gas       ┆ 7500.0 │
│ wind      ┆ 3000.0 │
└───────────┴────────┘

Gas costs 7500 and wind 3000, which add up to the objective.

Keep the run

archive= writes the spec, the sources and the answer to a folder:

import tempfile
from pathlib import Path

run = Path(tempfile.mkdtemp()) / 'first_run'
sps.solve('dispatch.yaml', sources, archive=run)

for file in sorted(run.rglob('*.parquet')):
    print(file.relative_to(run))
answer/dual/power_balance.parquet
answer/metrics.parquet
answer/primal/p.parquet
answer/record.parquet
catalog.parquet
sources/cost.parquet
sources/generator.parquet
sources/load.parquet
sources/p_max.parquet
sources/snapshot.parquet

sources/ holds each table as the solve read it, and answer/ holds one file for each variable and constraint.

load_archive reads the answer back without solving again:

archived = sps.load_archive(run)
print(archived.result.objective)
10500.0

To zip an archive, see Archiving a solve. To query many as one table, see Reading a directory of runs.

Next

In the next tutorial you change the data and solve again.