Skip to content

Fix/greedy time impute refactoring - #39

Open
irm-codebase wants to merge 5 commits into
mainfrom
fix/greedy-time-impute-refactoring
Open

Fix/greedy time impute refactoring#39
irm-codebase wants to merge 5 commits into
mainfrom
fix/greedy-time-impute-refactoring

Conversation

@irm-codebase

@irm-codebase irm-codebase commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #40

Summary of changes in this pull request

  • Refractors impute_ages.py for improved maintainability by standardising the order of operations and making assumptions / priorities clearer for future development.
  • Adds a min-heap to increase efficiency for the capacity_profiles algorithm.
  • Re-arranges things to avoid edge cases placed capacity waaay to early for 'retired' powerplants with no dates.
  • Additionally, fixes Non-contributing powerplants leak into produced datasets #40 (found it after making the schema slightly stronger).

Examples of solved retirement profile edge case

Before, Mexico was pretending to be a wind power pioneer.
This happened because the start-year of powerplants imputed through the retirement profile were derived using the lifetime.

image

After, Mexico is less of a pioneer (although the super old facility is legitimate!)
The new method 'pre-detects' unknown retired plants, and adjusts their individual end-date limit so it can always be decommissioned in time. Then, their start year is solved using the commissioning profile just as any other plant.

image

Disadvantages

The trade-off is that the powerplant might be introduced and immediately retired the year after when the most empty years are near the end of the commissioning profile.
It is a bit hard to see, but this happens for solar in Mexico due to lack of data.

I think it's generally fine, as at least this shields us from getting ahistorical profiles (such as lots of wind / PV in the 1990's).

image

Reviewer checklist

  • There are no pip dependencies in the module's environment files (workflow/envs/).
  • All rules use pathvars (e.g., <results>) in their inputs and outputs.
  • The integration test-suite is successful, including:
    • pre-commit.ci tests pass.
    • tests pass for all relevant OS configurations (linux, osx, windows).
  • Module documentation is up-to-date, including:
    • INTERFACE.yaml mentions all relevant pathvars and wildcards.
    • README.md describes how to use the module and has the necessary citations.

@irm-codebase
irm-codebase marked this pull request as draft August 3, 2026 20:42
@irm-codebase
irm-codebase marked this pull request as ready for review August 4, 2026 20:20
Comment on lines +13 to +25
# A diverse set of statuses to diminish oversimplification during gap filling.
OPERATING = "operating"
RETIRED = "retired"
HISTORICAL = {OPERATING, RETIRED}
PLANNED = {"construction", "pre-construction", "announced"}
SCENARIO_MAP = {
"historical": HISTORICAL,
"construction": HISTORICAL | {"construction"},
"pre_construction": HISTORICAL | {"construction", "pre-construction"},
"announced": HISTORICAL | PLANNED,
}
# Status categorisation shown to users (and accepted in user imputed files).
IMPUTED_STATUS = {"planned", "operating", "retired"}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this here to allow the schema to use it, for higher standardisation.

Comment on lines +134 to +139
@gpa.dataframe_check
def end_after_start(cls, plants: pd.DataFrame):
"""Require ordered dates wherever both years are known."""
known_dates = plants[["start_year", "end_year"]].notna().all(axis="columns")
return ~known_dates | plants["end_year"].gt(plants["start_year"])

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now validate that start_year < end_year.
This should've been there from the start, frankly 😬

known_dates = filtered[["start_year", "end_year"]].notna().all(axis="columns")
zero_duration = known_dates & filtered["start_year"].eq(filtered["end_year"])
return filtered.loc[~zero_duration].copy()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the 'prepare' scripts should use this function to remove powerplants installed and then removed on the same year. They do not contribute to statistics under our convention:

start_year <= operation < end_year (a plant starts at day 1 of start year, and is decommissioned on day 1 of end year).

Comment on lines +211 to +217
def push(year: int) -> None:
"""Add a year to the allocation queue with its current priority."""
year_tie = -year if prefer_later_years else year
# heapq uses a min-heap algorithm
# negating values prioritize the largest deficit and then the largest target.
heapq.heappush(heap, (-remaining.loc[year], -target.loc[year], year_tie, year))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the re-computation per powerplant in the previous script.
https://docs.python.org/3/library/heapq.html

Priority is given in this order: biggest deficit, biggest target powerplant, earlier year (reversible via perfer_later_years).

We negate because the algorithm minimises.

def impute_time(
plants: pd.DataFrame, reference_capacity_df: pd.DataFrame, imputation: Mapping
) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Impute missing dates and return plants plus normalized profile diagnostics."""

@irm-codebase irm-codebase Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lays down the architecture for this stage of the module.

Standard regardless of the selected method:

  1. Adjust statuses to DATASET_YEAR.
  2. Filter to the requested scenario
  3. Run lifetime imputation for plants with known years
  4. Run commissioning windows for start years of future projects with unknown years
  5. Re-run lifetime imputation to add the end year of those future projects

Then, the remaining 'unknown' plants are imputed using a selectable method.
For now, only capacity_profiles is available. In the future we might want to add a heavier optimisation method.

Lastly, the status is simplified for users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-contributing powerplants leak into produced datasets

1 participant