Skip to content

Handling vintage availability #10

Description

@brynpickering

What can be improved?

We have to currently input vintage availability manually. Ideally we would input it programatically as part of the math.

I have put together two possible ways we can do it, with different math in the YAML vs in the helper functions. @irm-codebase which do you think is more readable?

Both have this new math to get the year as an integer from the vintagesteps/investsteps (necessary as long as this is still an issue: calliope-project/calliope#567).

YAML math

global_expressions:
  investment_year:
    foreach: [investsteps]
    equations:
      - expression: year(investsteps)

  vintage_year:
    foreach: [vintagesteps]
    equations:
      - expression: year(vintagesteps)
      
  investstep_resolution:
    foreach: [investsteps]
    equations:
      - where: investsteps=get_val_at_index(investsteps, 0)
        expression: get_val_at_index(investment_year, 1) - initial_year
      - where: NOT investsteps=get_val_at_index(investsteps, 0)
        expression: investment_year - roll(investment_year, investsteps=1)

Helper functions

class Year(ParsingHelperFunction):
    #:
    NAME = "year"
    #:
    ALLOWED_IN = ["where", "expression"]

    def as_math_string(self, array: str) -> str:
        return f"year({array})"

    def as_array(self, array: xr.DataArray) -> xr.DataArray:
        return array.dt.year

version 1:

YAML math

global_expressions:
  available_vintages:
    foreach: [vintagesteps, investsteps, techs]
    equations:
      - expression: get_available_vintages(weibull)

Helper functions

class GetVintageAvailability(ParsingHelperFunction):
    #:
    NAME = "get_vintage_availability"
    #:
    ALLOWED_IN = ["expression"]

    def _weibull_func(self, year_diff: xr.DataArray) -> xr.DataArray:
        shape = self._input_data.get("shape", 1)
        gamma = scipy.special.gamma(1 + 1 / shape)
        availability = np.exp(
            -((year_diff / self._input_data.lifetime.fillna(np.inf)) ** shape)
            * (gamma**shape)
        )
        return availability.fillna(0)

    def _linear_func(self, year_diff: xr.DataArray) -> xr.DataArray:
        availability = 1 - (year_diff / self._input_data.lifetime)
        return availability.clip(min=0)

    def _step_func(self, year_diff: xr.DataArray) -> xr.DataArray:
        life_diff = self._input_data.lifetime - year_diff
        availability = (life_diff).clip(min=0) / life_diff
        return availability
    
    def as_math_string(self, method: Literal["weibull", "linear", "step"]) -> str:
        # TODO: implement for each func type
        pass

    def as_array(self, method: Literal["weibull", "linear", "step"]) -> xr.DataArray:
        """For each investment step in pathway optimisation, get the historical capacity additions that now must be decommissioned.

        Args:
            method (str): The method with which to assume technology survival rates.

        Returns:
            xr.DataArray:
        """
        year_diff = (
            self._input_data.investsteps.dt.year - self._input_data.vintagesteps.dt.year
        )
        year_diff_no_negative = year_diff.where(year_diff >= 0)
        if method == "weibull":
            availability = self._weibull_func(year_diff_no_negative)
        elif method == "linear":
            availability = self._linear_func(year_diff_no_negative)
        elif method == "step":
            availability = self._step_func(year_diff_no_negative)
        else:
            raise ValueError(f"Cannot get vintage availability with `method`: {method}")
        return availability.where(year_diff_no_negative.notnull())

version 2:

YAML math

global_expressions:
  available_vintages:
    foreach: [vintagesteps, investsteps, techs]
    equations:
      - where: config.vintage_survival=weibull
        expression: >-
          exponential(
            -(($year_diff / default_if_empty(lifetime, inf)) ** shape)
            * (gamma(1 + 1 / shape) ** shape)
          )
      - where: config.vintage_survival=linear
        expression: >-
           clip(1 - ($year_diff / lifetime), lower=0)
      - where: config.vintage_survival=step
        expression: >-
          clip(lifetime - $year_diff, lower=0) / (lifetime - $year_diff)
    sub_expressions:
      year_diff:
        - expression: investment_year - vintage_year

Helper functions

class Exponential(ParsingHelperFunction):
    #:
    NAME = "exponential"
    #:
    ALLOWED_IN = ["expression"]

    def as_math_string(self, array: str) -> str:
        return rf"\exp^{{{array}}}"

    def as_array(self, array: xr.DataArray) -> xr.DataArray:
        return np.exp(array)


class Gamma(ParsingHelperFunction):
    #:
    NAME = "gamma"
    #:
    ALLOWED_IN = ["expression"]

    def as_math_string(self, array: str) -> str:
        return rf"\Gamma({array})"

    def as_array(self, array: xr.DataArray) -> xr.DataArray:
        return scipy.special.gamma(array)

class Clip(ParsingHelperFunction):
    #:
    NAME = "clip"
    #:
    ALLOWED_IN = ["expression"]

    def as_math_string(self, array: str, lower: Optional[str] = None, upper: Optional[str] = None) -> str:
        base = rf"\text{{clip}}({array}"
        if lower is not None:
            base += rf", \text{{lower}}={lower}"
        if upper is not None:
            base += rf", \text{{upper}}={upper}"
        return base + ")"

    def as_array(self, array: xr.DataArray, lower: Optional[str] = None, upper: Optional[str] = None) -> xr.DataArray:
        return array.clip(min=lower, max=upper)

Version

v0.1.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions