calliope-project / calliope

A multi-scale energy systems modelling framework
https://www.callio.pe
Apache License 2.0
276 stars 89 forks source link

Specify "ordered sets" to simplify synthax #605

Closed irm-codebase closed 2 weeks ago

irm-codebase commented 2 weeks ago

What can be improved?

Calliope makes use of roll() and get_val_at_index() in cases were the position of a set matters. This is often used for clustering and storage balancing constraints.

Although this works, I think the approach has two weaknesses:

Proposed improvement

Some optimization libraries, like pyomo, allow you to specify "ordered sets". I think this makes a lot of sense in our case, and would enable easier syntax and checks.

We currently do not define the dimensions directly, but I think something like this would be good:

dimensions:
  nodes:
  techs:
  carriers:
  timesteps:
    ordered: true
  investsteps:
    ordered: true

Syntax: allowed ONLY for ordered dimensions

Then, a constraint like

link_storage_level:
  description: Link the storage level at the end of one investment step to the start of the next.
  foreach: [nodes, techs, investsteps]
  where: storage AND NOT investsteps=get_val_at_index(investsteps=0)
  equations:
    - expression: >-
        storage[timesteps=$initial_step] == roll(
          storage[timesteps=$final_step] * (
            (1 - storage_loss) ** timestep_resolution[timesteps=$final_step]
          ),
          investsteps=1
        )
  slices:
    initial_step:
      - expression: get_val_at_index(timesteps=0)
    final_step:
      - expression: get_val_at_index(timesteps=-1)

May turn to:

link_storage_level:
  description: Link storage level... but easier this time!
  foreach: [nodes, techs, investsteps]
  where: storage AND NOT investsteps == investsteps[0]
  equations:
  - expressions: >-
      storage[timesteps[0]] == storage[timesteps[-1], investsteps<<1] * (
        (1-  storage_loss)**timestep_resolution[timesteps[-1]]
      )

or (my least favourite)

link_storage_level:
  description: Link storage level... but easier this time!
  foreach: [nodes, techs, investsteps]
  where: storage AND NOT investsteps[0]
  equations:
    - expression: >-
        storage[timesteps=$initial_step] == storage[timesteps=$final_step, investsteps<<1] * (
            (1 - storage_loss) ** timestep_resolution[timesteps=$final_step]
          )
  slices:
    initial_step:
      - expression: timesteps[0]
    final_step:
      - expression: timesteps[-1]

Version

v0.6.10

irm-codebase commented 2 weeks ago

Defining ordered sets in theory should also allow us to perform more complicated summations with conditionals (like "sum new capacity installed for all investsteps lower than this one").

I've yet to land on an approach that is easy to read for these cases, though...

brynpickering commented 2 weeks ago

I'm not keen to add all this extra core syntax math - the helper functions are there to extend the math in a way that remains modular. Although they are wordy, they are relatively explicit (they can certainly be given better names!). "<<" and ">>" are creating a whole new syntax, and "[-1]" and "[0]" overlap with existing slicing syntax which will make them difficult to parse.

You're right that those two helpers are specific to ordered sets and we have no way of controlling for someone applying them in the wrong context. We could have some check in the helpers for either user-defined metadata (ordered: true) or it being a datetime/numeric dtype. As a user, you could feasibly ensure the ordering of your sets by reorganising the input data dimensions, we just need to provide an API for that.

irm-codebase commented 2 weeks ago

I'm fine with not implementing the extra syntax :) However, a personal wish is to use ordered sets for numeric comparisons. Kind of: sum over this dimension while it is lower than [some position]. Would be a god send for pathways math.

I will close this issue as "not planned", and rewrite another with your feedback in mind.