Skip to content

Conversation

@FBumann
Copy link
Member

@FBumann FBumann commented Nov 30, 2025

Description

Brief description of the changes in this PR.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Code refactoring

Related Issues

Closes #(issue number)

Testing

  • I have tested my changes
  • Existing tests still pass

Checklist

  • My code follows the project style
  • I have updated documentation if needed
  • I have added tests for new functionality (if applicable)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added SizingParameters and InvestmentParameters classes to public API for enhanced sizing and investment modeling.
    • Introduced investment timing controls including lifecycle tracking for investment, decommissioning, and size changes.
    • Added vintage dimension support for modeling capacity additions over time.
  • Documentation

    • New comprehensive guides for InvestmentParameters and SizingParameters with usage examples and mathematical constraints.
  • Refactor

    • Standardized sizing and investment terminology across the API with updated parameter naming and structure.

✏️ Tip: You can customize this high-level summary in your review settings.

…. Here's a summary of what was implemented:

  Summary of Changes

  1. core.py - Added 'vintage' to FlowSystemDimensions

  - Updated FlowSystemDimensions type to include 'vintage'

  2. types.py - Added vintage type aliases

  - Numeric_VS - Vintage, Scenario dimensions
  - Numeric_PVS - Period, Vintage, Scenario dimensions
  - Numeric_TPVS - Time, Period, Vintage, Scenario dimensions
  - Bool_VS, Bool_PVS - Boolean variants
  - Effect_VS, Effect_PVS - Effect dict variants

  3. flow_system.py - Added vintages and vintage_weights

  - vintages property derived from periods (same values, different name)
  - vintage_weights defaults to 1.0 for all vintages (one-time costs not scaled)
  - all_coords property that includes vintage

  4. structure.py - Added vintage_weights property

  - vintage_weights property on FlowSystemModel
  - Updated get_coords to work with vintage dimension when explicitly requested

  5. effects.py - Major updates

  - Added vintage parameters: vintage_weights, share_from_vintage, minimum_vintage, maximum_vintage, minimum_over_vintages, maximum_over_vintages,
   minimum_overall, maximum_overall
  - Added minimum_total/maximum_total for per-period total bounds
  - Added vintage ShareAllocationModel (only when periods are defined)
  - Updated objective function to weight temporal/periodic and vintage separately
  - Added vintage target to add_share_to_effects
  - Added vintage cross-effect shares in _add_share_between_effects
  - Kept total variable for backwards compatibility (temporal + periodic only)

  The remaining tasks (SizingParameters and InvestmentParameters) are separate from the vintage dimension work and would require further
  implementation to split the current InvestParameters into distinct sizing vs investment classes with the active matrix logic.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 30, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The pull request introduces comprehensive vintage dimension support throughout the flixopt modeling framework. It adds new SizingParameters and InvestmentParameters classes with a shared _SizeModel base, extends effects with vintage-aware constraints and sharing, updates the type system with vintage-related aliases, and adds corresponding documentation and test updates.

Changes

Cohort / File(s) Summary
Documentation
docs/user-guide/mathematical-notation/features/InvestmentParameters.md, docs/user-guide/mathematical-notation/features/SizingParameters.md
New feature documentation for InvestmentParameters detailing timing and lifetime concepts; updated SizingParameters documentation with renamed terminology and refactored API examples.
Public API & Exports
flixopt/__init__.py
Added InvestmentParameters and SizingParameters to module-level exports and all list.
Type System
flixopt/types.py, flixopt/core.py
Introduced vintage-related type aliases (Numeric_VS, Numeric_PVS, Bool_VS, Bool_PVS, Effect_VS, Effect_PVS) and new PeriodicData/PeriodicEffects types; added 'vintage' to FlowSystemDimensions.
Flow System Infrastructure
flixopt/flow_system.py, flixopt/structure.py
Added vintages attribute, vintage_weights attribute, and all_coords property to FlowSystem; added vintage_weights property to FlowSystemModel.
Parameter System
flixopt/interface.py
Introduced _SizeParameters base class, new SizingParameters class, new InvestmentParameters class with timing/lifetime support, and deprecated InvestParameters wrapper with backward-compatibility mapping.
Feature Modeling
flixopt/features.py
Introduced _SizeModel base class with sizing variable/constraint creation, added SizingModel subclass, refactored InvestmentModel to inherit from _SizeModel with new lifecycle tracking (investment_occurs, decommissioning_occurs, size_increase/decrease), and added public properties.
Effects Modeling
flixopt/effects.py
Extended Effect with vintage-related parameters (vintage_weights, share_from_vintage, minimum/maximum_vintage, minimum/maximum_over_vintages, minimum/maximum_overall); added vintage_weights property to EffectModel; introduced vintage constraint groups (_add_over_vintages_constraints, _add_overall_constraints); extended cross-effect sharing to support vintage targets.
Element Modeling
flixopt/elements.py
Updated mandatory flag check in absolute_flow_rate_bounds to require all elements of size.mandatory be True instead of partial truthiness.
Tests
tests/test_effect.py, tests/test_scenarios.py
Updated test expectations to include conditional vintage variable/constraint generation; expanded objective calculation to separately weight temporal and periodic components.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • flixopt/interface.py: Major redesign with three parameter classes (_SizeParameters, SizingParameters, InvestmentParameters); backward-compatibility mapping for deprecated InvestParameters; validation and transform logic for new timing/lifetime concepts
  • flixopt/features.py: Introduction of _SizeModel base class architecture; refactored InvestmentModel with new lifecycle tracking variables and constraints; complex interaction between sizing, investment timing, and effect application
  • flixopt/effects.py: Extensive vintage dimension integration across Effect initialization, constraint generation, and cross-effect sharing; multiple new constraint groups and objective weighting paths
  • flixopt/flow_system.py & flixopt/structure.py: Vintage coordinate propagation and property defaults; coordinate retrieval changes affecting filtering behavior
  • Test updates: Conditional vintage handling and expanded objective computation may mask edge cases if not thoroughly validated

Possibly related PRs

  • PR #469: Modifies flixopt/types.py by adding and expanding numeric/boolean/effect type aliases with dimension unions, directly supporting the new vintage type system introduced here
  • PR #486: Extends flixopt/effects.py with vintage-aware modeling and constraint generation, overlapping with the effects subsystem expansion in this PR
  • PR #380: Modifies investment/sizing subsystem in flixopt/interface.py and flixopt/features.py, addressing related parameter and variable lifecycle patterns

Poem

🐰 Vintage times we now shall track,
Sizing forward, never back,
Dims expanded, weights aligned,
Parameters redesigned!
From investment's old to new found way,
Model's logic blooms today! 🌱

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is a blank template with no substantive content filled in; only checkboxes are marked without providing actual description, type of change details, or related issues. Fill in the description section with a summary of changes, select the appropriate type of change, reference related issues if applicable, and confirm testing status and checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 63.79% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature/vintage investment timing' clearly summarizes the main change—introducing vintage dimension support for investment timing, as evidenced by new vintage-related parameters, types, and documentation across multiple files.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FBumann
Copy link
Member Author

FBumann commented Nov 30, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 30, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
flixopt/elements.py (2)

815-829: Lower bound logic for mandatory investments looks sound but assumes array-like mandatory

Using self.element.size.mandatory.all() so that the minimum-relative bound is only enforced when all mandatory flags are true avoids over-constraining partially-optional investments and is consistent with scenario/period arrays. This relies on mandatory always being an array-like (e.g. xarray/NumPy) rather than a plain bool; if plain booleans are still possible, consider normalizing them earlier or wrapping with bool(np.asarray(self.element.size.mandatory).all()) to avoid surprises.


451-455: with_investment only checks InvestParameters despite new sizing classes

Flow.size is typed as Numeric_PS | InvestParameters, and with_investment is implemented as isinstance(self.element.size, InvestParameters). With the introduction of SizingParameters and InvestmentParameters (and InvestParameters now being a deprecation wrapper), flows configured directly with the new classes may not be treated as “with investment” unless those classes subclass InvestParameters.

It’s worth double-checking the hierarchy and, if needed, broadening the check, for example:

-from .interface import InvestParameters, StatusParameters
+from .interface import InvestParameters, InvestmentParameters, SizingParameters, StatusParameters
...
     @property
     def with_investment(self) -> bool:
-        return isinstance(self.element.size, InvestParameters)
+        return isinstance(
+            self.element.size,
+            (InvestParameters, SizingParameters, InvestmentParameters),
+        )

and optionally updating the type hint on size accordingly. Based on relevant code snippets from flixopt.interface, where SizingParameters and InvestmentParameters are the new primary APIs.

Also applies to: 747-748

flixopt/flow_system.py (1)

180-207: Vintages, vintage weights, and coordinate helpers integrate cleanly

  • Initializing self.vintages as periods.rename('vintage') and defaulting self.vintage_weights to ones over that index gives a clear semantics: vintages mirror periods but are used specifically for one‑time investment decisions.
  • When no periods are defined, both vintages and vintage_weights remain None, so vintage-aware code can short‑circuit cleanly.
  • The updated coords/all_coords split—coords for the traditional (time, period, scenario) set and all_coords adding vintage—is consistent with the new docstring comments and the FlowSystemModel.get_coords implementation.

If you later allow user-defined vintage weights via the public API, you may want to thread them through __init__ and serialization similarly to scenario_weights, but for the current behavior this wiring is consistent.

Also applies to: 1088-1111

flixopt/interface.py (1)

1036-1040: Warning for lifetime exceeding horizon is helpful.

The warning when lifetime > max_horizon helps users understand potential issues. However, the warning message could be improved for clarity.

Consider slightly improving the warning message:

             if (self.lifetime > max_horizon).any():
                 logger.warning(
-                    f'Fixed lifetime ({self.lifetime.values}) if Investment exceeds model horizon ({max_horizon}). '
+                    f'Lifetime ({self.lifetime.values}) exceeds model horizon ({max_horizon}). '
+                    f'Investment in late periods may extend beyond the optimization timeframe.'
                 )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1cb20e and da43796.

📒 Files selected for processing (13)
  • docs/user-guide/mathematical-notation/features/InvestmentParameters.md (1 hunks)
  • docs/user-guide/mathematical-notation/features/SizingParameters.md (7 hunks)
  • flixopt/__init__.py (2 hunks)
  • flixopt/core.py (1 hunks)
  • flixopt/effects.py (13 hunks)
  • flixopt/elements.py (1 hunks)
  • flixopt/features.py (3 hunks)
  • flixopt/flow_system.py (3 hunks)
  • flixopt/interface.py (2 hunks)
  • flixopt/structure.py (3 hunks)
  • flixopt/types.py (4 hunks)
  • tests/test_effect.py (5 hunks)
  • tests/test_scenarios.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
flixopt/flow_system.py (2)
flixopt/effects.py (1)
  • vintage_weights (431-447)
flixopt/structure.py (1)
  • vintage_weights (218-233)
tests/test_scenarios.py (2)
flixopt/effects.py (3)
  • objective_effect (768-771)
  • objective_effect (774-783)
  • penalty_effect (786-805)
flixopt/results.py (1)
  • objective (300-307)
tests/test_effect.py (2)
flixopt/structure.py (8)
  • flow_system (350-368)
  • add (1087-1095)
  • add (1620-1622)
  • variables (1520-1526)
  • constraints (1511-1517)
  • add_variables (1407-1416)
  • get_coords (245-277)
  • get_coords (1466-1471)
flixopt/flow_system.py (1)
  • coords (1088-1099)
flixopt/features.py (7)
flixopt/interface.py (3)
  • InvestmentParameters (885-1040)
  • SizingParameters (792-848)
  • StatusParameters (1044-1295)
flixopt/structure.py (11)
  • Submodel (1383-1562)
  • add_variables (1407-1416)
  • get_coords (245-277)
  • get_coords (1466-1471)
  • add_constraints (1418-1427)
  • get (1459-1464)
  • get (1624-1626)
  • _do_modeling (1555-1562)
  • flow_system (350-368)
  • label_full (1005-1006)
  • label_full (1497-1498)
flixopt/flow_system.py (2)
  • coords (1088-1099)
  • sel (1284-1312)
flixopt/modeling.py (4)
  • BoundingPatterns (398-772)
  • bounds_with_state (434-476)
  • state_transition_bounds (575-626)
  • link_changes_to_level_with_binaries (698-772)
tests/test_io.py (1)
  • flow_system (27-32)
tests/test_results_plots.py (1)
  • flow_system (15-16)
tests/test_network_app.py (1)
  • flow_system (13-18)
flixopt/interface.py (1)
flixopt/structure.py (9)
  • Interface (300-977)
  • _set_flow_system (334-347)
  • flow_system (350-368)
  • transform_data (318-332)
  • _fit_effect_coords (385-403)
  • _fit_coords (370-383)
  • _plausibility_checks (996-999)
  • values (1330-1335)
  • values (1617-1618)
flixopt/structure.py (2)
flixopt/effects.py (1)
  • vintage_weights (431-447)
flixopt/flow_system.py (2)
  • coords (1088-1099)
  • all_coords (1102-1111)
flixopt/__init__.py (1)
flixopt/interface.py (4)
  • InvestmentParameters (885-1040)
  • InvestParameters (852-875)
  • SizingParameters (792-848)
  • StatusParameters (1044-1295)
🪛 LanguageTool
docs/user-guide/mathematical-notation/features/InvestmentParameters.md

[style] ~4-~4: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 2099 characters long)
Context: ...del WHEN to invest, not just how much. !!! info "Relationship to SizingParameters"...

(EN_EXCESSIVE_EXCLAMATION)

docs/user-guide/mathematical-notation/features/SizingParameters.md

[style] ~4-~4: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1381 characters long)
Context: ...iable — should we build this? How big? !!! note "Naming Change" `SizingParamet...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.18.1)
docs/user-guide/mathematical-notation/features/InvestmentParameters.md

47-47: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


63-63: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


77-77: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


94-94: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


111-111: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

docs/user-guide/mathematical-notation/features/SizingParameters.md

34-34: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


57-57: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


86-86: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


98-98: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
🔇 Additional comments (30)
flixopt/core.py (1)

18-18: FlowSystemDimensions now correctly covers the new vintage axis

Including 'vintage' in FlowSystemDimensions matches the new FlowSystem.all_coords/vintages usage and keeps typing aligned with the extended coordinate set.

docs/user-guide/mathematical-notation/features/SizingParameters.md (1)

1-147: SizingParameters docs align with the renamed API

The examples and symbol table consistently use SizingParameters with effects_per_size, effects_of_size, effects_of_retirement, and piecewise_effects_per_size, which matches the new sizing terminology and expected constructor fields.

flixopt/structure.py (1)

217-234: Vintage weights and coord selection behavior are coherent with FlowSystem’s new vintage axis

  • FlowSystemModel.vintage_weights correctly:
    • returns scalar 1 when the FlowSystem has no vintages,
    • builds a 1‑D ('vintage',) array of ones when vintages exist but no weights are provided, and
    • defers to flow_system.vintage_weights otherwise.
  • get_coords now:
    • keeps the previous default (coords without vintage when dims is None),
    • uses flow_system.all_coords when dims is specified so callers can explicitly request vintage,
    • and guards extra_timestep=True when time is omitted.

This matches the FlowSystem-side changes and offers an explicit opt‑in for vintage in modeling code.

Also applies to: 245-273

tests/test_scenarios.py (1)

241-264: Objective checks now correctly include temporal and periodic parts for both cost and penalty

The updated expected_obj in test_weights and test_weights_io sums:

  • costs_submodel.temporal.total × scenario weights,
  • costs_submodel.periodic.total × scenario weights,
  • penalty_submodel.temporal.total × scenario weights,
  • penalty_submodel.periodic.total × scenario weights,

and compares this against model.objective.expression with assert_lineequal. This matches the decomposition of the objective into temporal and periodic contributions for both the objective and penalty effects and is consistent with how objective_weights are validated earlier in each test.

Also applies to: 267-291

flixopt/__init__.py (1)

31-40: Top-level exports now expose new sizing/investment parameter classes

Re-exporting InvestmentParameters and SizingParameters from .interface and adding them to __all__ (alongside the existing InvestParameters) makes the new APIs available as flixopt.InvestmentParameters / flixopt.SizingParameters while keeping backward compatibility. This is consistent with the documentation updates and test imports.

Also applies to: 43-73

tests/test_effect.py (3)

25-50: Well-structured conditional vintage expectations.

The dynamic construction of expected variables and constraints based on flow_system.vintages presence is clean and maintainable. This pattern correctly adapts tests to both vintage-enabled and non-vintage configurations.


85-89: Good coverage of total constraint validation.

The explicit assertion that total = temporal + periodic aligns with the implementation in effects.py where the total constraint excludes vintage (which has a different dimension).


200-232: Cross-effect share testing is comprehensive.

The test correctly verifies the expected variables and constraints for cross-effect sharing, including the conditional vintage handling. The mapping pattern Effect1(periodic)->Effect2(periodic) is clearly tested.

flixopt/types.py (3)

6-14: Documentation table clearly explains dimension suffixes.

The updated table with _TPVS, _PVS, and _VS suffixes is helpful for understanding the vintage dimension semantics.


74-83: Vintage-related numeric types are well-documented.

The docstrings clearly explain each type's purpose:

  • Numeric_VS: One-time investment parameters
  • Numeric_PVS: Vintage-dependent parameters varying by period
  • Numeric_TPVS: Time-varying vintage-dependent data

140-154: Transformed data types provide useful abstractions.

PeriodicData, PeriodicEffects, and PeriodicEffectsUser clearly distinguish between user input formats and internal transformed representations.

flixopt/interface.py (5)

695-719: Clean base class extraction for sizing parameters.

The _SizeParameters class properly centralizes common sizing logic including effects, bounds, and piecewise handling. The default values from CONFIG.Modeling are appropriate.


759-766: Helpful computed properties for bounds resolution.

minimum_or_fixed_size and maximum_or_fixed_size provide clean abstractions for accessing the effective size bounds regardless of whether fixed_size is set.


851-876: Good deprecation strategy for backwards compatibility.

The InvestParameters wrapper correctly maps old parameter names to new ones and emits a clear deprecation warning with migration guidance. The stacklevel=2 ensures the warning points to user code.


968-970: Validate lifetime parameter early.

The explicit check for None lifetime is good, but consider if a more descriptive error message would help users understand what lifetime means.


1028-1031: The force_investment validation logic is correct.

The check (self.force_investment.sum('period') > 1).any() correctly validates that force_investment can only be True for a single period per scenario. After _fit_coords, the DataConverter explicitly preserves boolean dtype, so the sum operation correctly treats True as 1 and False as 0. This catches violations where a scenario has multiple forced investment periods.

flixopt/features.py (5)

29-51: Good defensive type checking for mandatory parameter.

The explicit dtype check ensures mandatory is boolean, preventing subtle bugs from numeric values being interpreted incorrectly.


59-79: Clear logic for optional vs mandatory sizing.

The binary invested variable is only created when sizing is optional (not mandatory.all()), avoiding unnecessary binary variables when sizing is always required. The constraint invested.where(mandatory) == 1 correctly forces investment where mandatory.


100-113: Retirement effects formula has correct semantics.

The formula -invested * factor + factor = factor * (1 - invested) correctly applies retirement effects only when invested=0. The guard not is_all_mandatory prevents adding these effects when retirement is impossible.


311-329: Investment constraint logic handles force_investment correctly.

The check for force_investment.sum('period') > 1 before adding the constraint ensures consistency with the plausibility check in InvestmentParameters._plausibility_checks().


338-363: Investment-timing effects use dimension renaming correctly.

Renaming period to period_of_investment before summing correctly aggregates effects across potential investment periods. This preserves the original period dimension for the result.

flixopt/effects.py (9)

58-91: Comprehensive vintage parameter documentation.

The docstring clearly explains the new vintage-related parameters including vintage_weights, share_from_vintage, and the various vintage bounds. The distinction between minimum_over_periods and minimum_over_vintages is well-documented.


369-386: Plausibility checks correctly validate dimension requirements.

The checks ensure that vintage-related constraints (minimum_over_vintages, maximum_over_vintages) require the vintage dimension, and overall constraints require the period dimension. This prevents runtime errors from misconfigurations.


430-448: Vintage weights fallback logic mirrors period weights pattern.

The vintage_weights property follows the same pattern as period_weights: effect-specific → FlowSystem default → unitary fallback. This consistency is good.


481-495: Conditional vintage submodel creation is correct.

The vintage ShareAllocationModel is only created when flow_system.vintages is not None, avoiding unnecessary model complexity for non-vintage scenarios.


513-531: Total constraint correctly excludes vintage.

The comment explains the architectural decision: vintage has a different dimension than temporal/periodic, so the total is defined as temporal + periodic for backwards compatibility and per-period constraints. This is a reasonable design choice.


571-595: Overall constraints combine period and vintage contributions.

The _add_overall_constraints method correctly computes the grand total as weighted_periods + weighted_vintages (when vintage exists). This provides users with a way to constrain the true overall effect.


897-922: Objective function correctly weights all effect categories.

The objective combines:

  1. Temporal × period_weights × scenario_weights
  2. Periodic × period_weights × scenario_weights
  3. Vintage × vintage_weights × scenario_weights (if exists)

This ensures proper weighting across all dimensions.


862-874: Vintage target support in add_share_to_effects is well-guarded.

The method correctly checks if vintage submodel exists before adding shares, providing a clear error message if the FlowSystem lacks a vintage dimension.


940-953: Cross-effect vintage shares validation is consistent.

The validation that both source and target effects have vintage submodels before adding shares prevents runtime errors from dimension mismatches.

Comment on lines +29 to +39
## Basic Usage

```python
solar_timing = fx.InvestmentParameters(
lifetime=10, # Investment lasts 10 periods
minimum_size=50,
maximum_size=200,
effects_of_size={'costs': 15000},
effects_per_size={'costs': 1200},
)
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Basic usage example uses sizing parameter names instead of investment-specific ones

In the “Basic Usage” snippet you configure:

fx.InvestmentParameters(
    lifetime=10,
    minimum_size=50,
    maximum_size=200,
    effects_of_size={'costs': 15000},
    effects_per_size={'costs': 1200},
)

Per the InvestmentParameters API (and the later “Investment-Period Effects” examples), these fields should be effects_of_investment and effects_of_investment_per_size, not the sizing-style effects_of_size/effects_per_size. Otherwise readers will copy an example that does not match the class signature.

Suggestion:

-    effects_of_size={'costs': 15000},
-    effects_per_size={'costs': 1200},
+    effects_of_investment={'costs': 15000},
+    effects_of_investment_per_size={'costs': 1200},
🤖 Prompt for AI Agents
In docs/user-guide/mathematical-notation/features/InvestmentParameters.md around
lines 29-39, the example uses sizing parameter names effects_of_size and
effects_per_size which do not match the InvestmentParameters API; update the
snippet to use effects_of_investment and effects_of_investment_per_size instead
(preserve values and context) so the example matches the class signature and
later examples.

Comment on lines 283 to 285
previous_level=0 if self.parameters.previous_lifetime == 0 else self.size.isel(period=0),
coord='period',
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential issue with previous_lifetime conditional.

The ternary 0 if self.parameters.previous_lifetime == 0 else self.size.isel(period=0) compares a DataArray to 0. After transform_data, previous_lifetime is a DataArray. This comparison may not work as expected.

After transform_data, previous_lifetime is a DataArray. The comparison self.parameters.previous_lifetime == 0 returns a DataArray of booleans, not a single boolean. Consider using .all() or .item():

-            previous_level=0 if self.parameters.previous_lifetime == 0 else self.size.isel(period=0),
+            previous_level=0 if (self.parameters.previous_lifetime == 0).all() else self.size.isel(period=0),

Comment on lines 287 to 309
def _track_lifetime(self):
"""Create constraints that link investment period to decommissioning period based on lifetime."""
periods = self._model.flow_system._fit_coords(
'periods', self._model.flow_system.periods.values, dims=['period', 'scenario']
)

# Calculate decommissioning periods (vectorized)
import xarray as xr

is_first = periods == periods.isel(period=0)
decom_period = periods + self.parameters.lifetime - xr.where(is_first, self.parameters.previous_lifetime, 0)

# Map to available periods (drop invalid ones for sel to work)
valid = decom_period.where(decom_period <= self._model.flow_system.periods.values[-1], drop=True)
avail_decom = periods.sel(period=valid, method='bfill').assign_coords(period=valid.period)

# One constraint per unique decommissioning period
for decom_val in np.unique(avail_decom.values):
mask = (avail_decom == decom_val).reindex_like(periods).fillna(0)
self.add_constraints(
self.investment_occurs.where(mask).sum('period') == self.decommissioning_occurs.sel(period=decom_val),
short_name=f'size|lifetime{int(decom_val)}',
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for tests that exercise the lifetime tracking logic
rg -n "lifetime|_track_lifetime" tests/ --type=py -A 5 -B 2 | head -150

Repository: flixOpt/flixopt

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Also check the full implementation context in features.py
wc -l flixopt/features.py

Repository: flixOpt/flixopt

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Search for any TODOs, FIXMEs, or issues related to _track_lifetime
rg -n "_track_lifetime|lifetime.*constraint" flixopt/features.py -B 2 -A 10

Repository: flixOpt/flixopt

Length of output: 1287


🏁 Script executed:

#!/bin/bash
# Read the complete _track_lifetime method
sed -n '287,309p' flixopt/features.py

Repository: flixOpt/flixopt

Length of output: 1307


🏁 Script executed:

#!/bin/bash
# Search for any test files that might test investment features
fd -e py tests/ | grep -i "invest\|lifetime\|feature" | head -20

Repository: flixOpt/flixopt

Length of output: 388


🏁 Script executed:

#!/bin/bash
# Look for integration tests that might indirectly test this
rg -l "investment\|decommission" tests/ --type=py

Repository: flixOpt/flixopt

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check if there are any validation or test scripts at the repository root
fd -e py . -maxdepth 2 | grep -i test

Repository: flixOpt/flixopt

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Search for test directory structure
find . -type d -name "test*" 2>/dev/null | head -20

Repository: flixOpt/flixopt

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Look at the Investment class to understand parameters
rg -n "class.*Investment|self.parameters.lifetime|self.parameters.previous_lifetime" flixopt/features.py -B 2 -A 5 | head -60

Repository: flixOpt/flixopt

Length of output: 1297


🏁 Script executed:

#!/bin/bash
# Check what decommissioning_occurs and investment_occurs are
rg -n "decommissioning_occurs|investment_occurs" flixopt/features.py -B 2 | head -80

Repository: flixOpt/flixopt

Length of output: 2354


🏁 Script executed:

#!/antml
# Search for any examples or documentation about lifetime
rg -n "lifetime" docs/ README* --type=md 2>/dev/null | head -50

Repository: flixOpt/flixopt

Length of output: 1533


🏁 Script executed:

#!/bin/bash
# List test files to find investment-related tests
ls -la tests/

Repository: flixOpt/flixopt

Length of output: 2087


🏁 Script executed:

#!/bin/bash
# Look for any test that mentions InvestmentModel or lifetime
rg -l "InvestmentModel|lifetime" tests/ --type=py 2>/dev/null

Repository: flixOpt/flixopt

Length of output: 41


🏁 Script executed:

#!/antml
# Read the documentation about the lifetime constraint logic
cat docs/user-guide/mathematical-notation/features/InvestmentParameters.md | head -100

Repository: flixOpt/flixopt

Length of output: 2369


🏁 Script executed:

#!/antml
# Look for issues with the sel() logic - check what valid contains
rg -n "\.sel\(period=" flixopt/features.py -B 3 -A 3

Repository: flixOpt/flixopt

Length of output: 810


🏁 Script executed:

#!/antml
# Search for investment tests in test_component.py
rg -n "investment|Investment|lifetime|Lifetime" tests/test_component.py -A 5 -B 2 | head -150

Repository: flixOpt/flixopt

Length of output: 2216


🏁 Script executed:

#!/antml
# Check test_functional.py which might have functional tests
rg -n "investment|Investment|lifetime|Lifetime" tests/test_functional.py -A 5 -B 2 | head -150

Repository: flixOpt/flixopt

Length of output: 5156


🏁 Script executed:

#!/antml
# Look for any GitHub issues or comments about this specific logic
rg -n "method.*bfill|sel.*period.*valid" flixopt/ --type=py -B 3 -A 3

Repository: flixOpt/flixopt

Length of output: 1217


Lifetime tracking logic lacks test coverage and contains unclear coordinate handling that should be verified.

While the vectorized calculation is sophisticated, there are no dedicated tests for the _track_lifetime() method or the decommissioning period constraint logic. The method='bfill' selector at line 301 maps calculated float decommissioning periods to available periods with backfilling, then reassigns coordinates—this behavior needs clarification through tests or documentation. The constraint at line 307 linking investment_occurs to decommissioning_occurs per unique decommissioning period requires validation that the mask reindexing and coordinate reassignment produce the intended lifetime relationship.

…ndling that needed clarification and test coverage.

  Changes Made

  1. Simplified _track_lifetime() logic (flixopt/features.py:287-336):
    - Rewrote from vectorized xarray operations to explicit for-loop logic
    - Added comprehensive docstring with examples
    - Made the investment-to-decommissioning mapping explicit and readable
  2. Fixed _SizeParameters isinstance checks:
    - Updated elements.py to use _SizeParameters base class for all size parameter checks
    - Updated components.py similarly
    - This ensures InvestmentParameters is properly recognized alongside SizingParameters and deprecated InvestParameters
  3. Added differentiated model selection:
    - _create_investment_model() now selects:
        - InvestmentModel for InvestmentParameters (with lifetime tracking)
      - SizingModel for SizingParameters or deprecated InvestParameters
  4. Fixed parameter name: Changed previous_level to initial_level in BoundingPatterns.link_changes_to_level_with_binaries() call
  5. Added test coverage (tests/test_functional.py:736-831):
    - New test_investment_parameters_with_lifetime test
    - Tests multi-period optimization with InvestmentParameters
    - Verifies investment_occurs and decommissioning_occurs variables exist
    - Validates investment occurs at most once
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.

2 participants