Skip to content

feat!: batched element models (stacked on the v8 API removals)#744

Closed
FBumann wants to merge 400 commits into
remove-deprecated-apifrom
feature/element-data-classes-v8
Closed

feat!: batched element models (stacked on the v8 API removals)#744
FBumann wants to merge 400 commits into
remove-deprecated-apifrom
feature/element-data-classes-v8

Conversation

@FBumann

@FBumann FBumann commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

Stacked PR — base is remove-deprecated-api (#741), not main. This is a duplicate of #602's synced state with #741 merged in, so the diff here shows only the batched-modeling work on top of the v8 cleanup. #602/#610/#611/#614 remain untouched as fallback while the merge strategy is undecided.

What this is

The batched/vectorized model-building rework (up to 67× faster builds): type-level FlowsModel/StoragesModel/BusesModel replacing per-element loops, pre-computed *Data containers, mask-based variables, and the flow|rate-style batched solution naming with a LegacySolutionWrapper bridge.

What merging #741 into it bought

The deprecated surface #602 had been maintaining against the batched world is now deleted instead: the batched-adapted Optimization/Results classes, normalize_weights plumbing, the network-method wrappers on the rewritten FlowSystem, and the kwarg-rename bridge. from_old_dataset() (kept per #741) works in the batched world — verified by the real-file regression tests, including the pinned end-to-end objective.

Verification

Full suite on this branch: 1450 passed, 0 failed (linopy 0.7.0).

Relationship to the existing stack

PR Status
#741 base of this PR — the v8 deprecation removals
this PR #602 (synced) + #741, conflicts resolved
#602 original, untouched fallback
#610/#611/#614 id-rename / dataclasses layers — to be restacked on this branch if this route is chosen

🤖 Generated with Claude Code

  FlowsData (batched.py):
  1. Added categorizations: with_flow_hours, with_load_factor
  2. Renamed: size_minimum → effective_size_lower, size_maximum → effective_size_upper
  3. Properties now only include relevant flows (no NaN padding):
    - flow_hours_minimum/maximum → only with_flow_hours
    - flow_hours_minimum/maximum_over_periods → only with_flow_hours_over_periods
    - load_factor_minimum/maximum → only with_load_factor
  4. Added absolute_lower_bounds, absolute_upper_bounds for all flows
  5. Added _stack_values_for_subset() helper

  FlowsModel (elements.py):
  1. Removed hours and hours_over_periods variables - not needed
  2. Simplified constraints to compute inline:
    - constraint_flow_hours() - directly constrains sum_temporal(rate)
    - constraint_flow_hours_over_periods() - directly constrains weighted sum
    - constraint_load_factor_min/max() - compute hours inline
  3. rate variable uses self.data.absolute_lower_bounds/upper_bounds directly
  4. Removed obsolete bound collection methods

  Benefits:
  - Cleaner separation: data in FlowsData, constraints in FlowsModel
  - No NaN handling needed - properties only include relevant flows
  - Fewer variables in the model
  - More explicit about which flows have which constraints
  1. Added Status Data Properties to FlowsData (batched.py)

  Added new cached properties for status-related bounds:
  - min_uptime, max_uptime - uptime bounds for flows with uptime tracking
  - min_downtime, max_downtime - downtime bounds for flows with downtime tracking
  - startup_limit_values - startup limits for flows with startup limit
  - previous_uptime, previous_downtime - computed previous durations using StatusHelpers.compute_previous_duration()

  2. Simplified FlowsModel Variable Creation (elements.py)

  Refactored uptime, downtime, and startup_count methods to use the new FlowsData properties instead of inline computation:
  - uptime: Now uses self.data.min_uptime, self.data.max_uptime, self.data.previous_uptime
  - downtime: Now uses self.data.min_downtime, self.data.max_downtime, self.data.previous_downtime
  - startup_count: Now uses self.data.startup_limit_values

  3. Kept active_hours (Plan Adjustment)

  The original plan called for removing active_hours, but functional tests (test_on_total_max, test_on_total_bounds) demonstrated that active_hours is required to enforce active_hours_min and active_hours_max parameters. Without it, the optimizer would ignore those constraints.

  Verification

  All tests pass:
  - pytest tests/test_functional.py -v - 26 tests passed
  - pytest tests/test_flow.py -v -k "time_only" - 22 tests passed
  - pytest tests/test_component.py -v -k "time_only" - 9 tests passed
  1. Combined min/max pairs

  - Created _uptime_bounds and _downtime_bounds cached properties that compute both min and max in a single iteration
  - Individual properties (min_uptime, max_uptime, etc.) now delegate to these cached tuples

  2. Added helper methods

  - _build_status_bounds(flow_ids, min_attr, max_attr) - builds both bounds in one pass
  - _build_previous_durations(flow_ids, target_state, min_attr) - consolidates previous duration logic

  3. Used more efficient patterns

  - Pre-allocated numpy arrays (np.empty, np.full) instead of Python list appends
  - Cached dict lookups - params = self.status_params at loop start instead of repeated self.status_params[fid]
  - Reduced redundant iterations - accessing min/max uptime now only iterates once instead of twice
  - status_effects_per_startup → effects_per_startup
  1. compute_previous_duration - Simple helper for computing previous duration (used by FlowsData)
  2. add_batched_duration_tracking - Creates duration tracking constraints (used by FlowsModel)
  3. create_status_features - Used by ComponentsModel (separate code path, not part of FlowsModel refactoring)

  Removed:
  - collect_status_effects - replaced with simpler _build_status_effects helper directly in FlowsData

  The effect building is now consistent - effects_per_active_hour and effects_per_startup use the same pattern as effects_per_flow_hour:

  # Simple, direct approach - no intermediate dict
  def _build_status_effects(self, attr: str) -> xr.DataArray | None:
      flow_factors = [
          xr.concat(
              [xr.DataArray(getattr(params[fid], attr).get(eff, np.nan)) for eff in effect_ids],
              dim='effect',
              coords='minimal',
          ).assign_coords(effect=effect_ids)
          for fid in flow_ids
      ]
      return concat_with_coords(flow_factors, 'flow', flow_ids)
FBumann and others added 21 commits February 6, 2026 08:59
…red element names but didn't reset the model's _is_built flag, so get_status() would still report MODEL_BUILT. Added fs.model._is_built = False when fs.model is not None.
…taArray() (a scalar with no dims) when empty, breaking downstream .dims checks. Changed to xr.DataArray(dims=['case'], coords={'case': []}) so the 'case' dimension is always present.
  - PiecewiseBuilder.create_piecewise_constraints — removed the zero_point parameter entirely. It now always creates sum(inside_piece) <= 1.
  - Callers (FlowsModel and StoragesModel) — add the tighter <= invested constraint separately, only for optional IDs that exist in invested_var. No coord mismatch possible.
  - ConvertersModel — was already passing None, just cleaned up the dead code.
…erseded/math/ directory. Here's a summary of the changes made:

  Summary of Updated Tests

  test_flow.py (88 tests)

  - Updated variable names: flow|rate, flow|size, flow|invested, flow|status, flow|active_hours
  - Updated constraint names: share|temporal(costs), share|periodic(costs) instead of 'ComponentName->effect(temporal)'
  - Updated uptime/downtime constraints: flow|uptime|forward, flow|uptime|backward, flow|uptime|min instead of flow|uptime|fwd/bwd/lb
  - Updated switch constraints: flow|switch_transition instead of flow|switch
  - Removed non-existent flow|fixed constraint check (fixed profile uses flow|invest_lb/ub)

  test_storage.py (48 tests)

  - Updated variable names: storage|charge, storage|netto, storage|size, storage|invested
  - Updated constraint names: storage|balance, storage|netto_eq, storage|initial_equals_final
  - Updated status variable from status|status to flow|status
  - Updated prevent simultaneous constraint: storage|prevent_simultaneous
  - Fixed effects_of_investment syntax to use dict: {'costs': 100}

  test_component.py (40 tests)

  - Updated status variables: component|status, flow|status instead of status|status
  - Updated active_hours variables: component|active_hours
  - Updated uptime variables: component|uptime
  - Updated constraints: component|status|lb/ub/eq, component|uptime|initial
  - Removed non-existent flow|total_flow_hours checks

  test_linear_converter.py (36 tests)

  - Updated constraint names: converter|conversion (no index suffix)
  - Updated status variables: component|status, component|active_hours
  - Updated share constraints: share|temporal(costs)
  - Made piecewise tests more flexible with pattern matching

  test_effect.py (26 tests)

  - No changes needed - tests already working
  1. Storage charge state scalar bounds (batched.py): Added .astype(float) after expand_dims().copy() to prevent silent int→float truncation when assigning final charge state
   overrides (0.5 was being truncated to 0 on an int64 array).
  2. SourceAndSink deserialization (components.py): Convert inputs/outputs from dict to list before + concatenation in __init__, fixing TypeError: unsupported operand type(s)
   for +: 'dict' and 'dict' during NetCDF save/reload.
  3. Legacy config leaking between test modules (test_math/conftest.py, superseded/math/conftest.py, test_legacy_solution_access.py): Converted module-level
  fx.CONFIG.Legacy.solution_access = True to autouse fixtures that restore the original value after each test, preventing the plotting isinstance(solution, xr.Dataset) test
  from failing.
* Here's a summary of everything that was done:

  Summary

  Phase 1: TransmissionsData

  - Added flow_ids parameter to TransmissionsData.__init__
  - Moved 12 cached properties from TransmissionsModel to TransmissionsData: bidirectional_ids, balanced_ids, _build_flow_mask(), in1_mask, out1_mask, in2_mask, out2_mask,
  relative_losses, absolute_losses, has_absolute_losses_mask, transmissions_with_abs_losses
  - Updated TransmissionsModel.create_constraints() to use self.data.*
  - Updated BatchedAccessor.transmissions to pass flow_ids

  Phase 2: BusesData

  - Added balance_coefficients cached property to BusesData
  - Updated BusesModel.create_constraints() to use self.data.balance_coefficients

  Phase 3: ConvertersData

  - Added flow_ids and timesteps parameters to ConvertersData.__init__
  - Moved 13 cached properties from ConvertersModel to ConvertersData: factor_element_ids, max_equations, equation_mask, signed_coefficients, n_equations_per_converter,
  piecewise_element_ids, piecewise_segment_counts_dict, piecewise_max_segments, piecewise_segment_mask, piecewise_flow_breakpoints, piecewise_segment_counts_array,
  piecewise_breakpoints
  - Updated ConvertersModel methods to use self.data.*
  - Removed unused defaultdict and stack_along_dim imports from elements.py

  Phase 4: ComponentsData

  - Added flows_data, effect_ids, timestep_duration parameters to ComponentsData.__init__
  - Moved 6 cached properties from ComponentsModel to ComponentsData: with_prevent_simultaneous, status_params, previous_status_dict, status_data, flow_mask, flow_count
  - Moved _get_previous_status_for_component() helper to ComponentsData
  - Updated ComponentsModel to use self.data.* throughout

  Bug Fix

  - Discovered a stale cache issue: FlowsData.previous_states could be cached before all previous_flow_rate values were set (e.g., in from_old_results). Fixed by having
  ComponentsData._get_previous_status_for_component() compute previous status directly from flow attributes instead of going through the potentially-stale
  FlowsData.previous_states cache.

* 1. _build_flow_mask exposure: Added balanced_in1_mask and balanced_in2_mask cached properties to TransmissionsData. TransmissionsModel now uses these instead of calling the
  private d._build_flow_mask().
  2. EffectsModel/elements: Confirmed not a bug — EffectsModel does not inherit from TypeModel and never accesses .elements on its data, so EffectsData not having elements is
  fine.
  3. FlowsData.dim_name: Changed from @cached_property to @Property to match all other Data classes.
…classes

# Conflicts:
#	CHANGELOG.md
#	tests/deprecated/test_config.py
- StatusData.with_effects_per_active_hour and StatusData.with_effects_per_startup — categorization lists that were never accessed. The actual effect values
  (effects_per_active_hour, effects_per_startup) are applied correctly via a different path.

  Removed passthrough properties (components.py — StoragesModel)

  9 properties eliminated, replaced with direct self.data.X at all call sites:
  - with_investment, with_optional_investment, with_mandatory_investment
  - storages_with_investment, storages_with_optional_investment
  - optional_investment_ids, mandatory_investment_ids
  - invest_params, _investment_data

  Kept investment_ids — it has 4 external callers in optimization.py.

  Removed passthrough property (elements.py — FlowsModel)

  - _previous_status → replaced 3 call sites with self.data.previous_states
# Conflicts:
#	CHANGELOG.md
#	flixopt/io.py
#	flixopt/statistics_accessor.py
#	flixopt/transform_accessor.py
#	tests/deprecated/conftest.py
#	tests/deprecated/examples/03_Optimization_modes/example_optimization_modes.py
#	tests/test_clustering/test_cluster_reduce_expand.py
#	tests/test_math/test_clustering.py
- stats.flow_sizes/storage_sizes return empty arrays when the solution has no
  size variables instead of raising KeyError
- transform.fix_sizes() accepts the element-dim DataArray that batched
  stats.sizes returns by splitting it into per-element variables
- cyclic clustered-storage SOC assertions made degeneracy-robust (both the
  level and the charge timing are solver-dependent, see #733)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests

- from_dataset: connect the network before restoring the solution — restoring
  first sets status SOLVED, which made connect_and_transform() skip and left
  bus wiring empty, crashing any rebuild of a reloaded system
- solution expansion tolerates FlowSystems without a solution
- main-era clustering tests adapted to batched solution variable names
- manual SOC decay comparison gets an atol for near-zero values

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ure/element-data-classes-v8

# Conflicts:
#	CHANGELOG.md
#	flixopt/__init__.py
#	flixopt/flow_system.py
#	flixopt/optimization.py
#	flixopt/results.py
#	flixopt/structure.py
#	flixopt/topology_accessor.py
#	tests/io/test_io_conversion.py
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fd76910-9975-4983-9890-10ea90e00dd0

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/element-data-classes-v8

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.

FBumann and others added 4 commits July 23, 2026 14:03
…s, and the docs notebooks

Library fixes surfaced by executing the docs notebooks (the only coverage for
several clustered/plotting paths):

- InterclusterStoragesModel.create_effect_shares called the removed tuple-based
  InvestmentBuilder.collect_effects — clustered storage with investment effects
  crashed at build. Rewritten to the push-based add_periodic_contribution
  pattern (mirroring StoragesModel); dead collect_effects removed.
- stats.storage_sizes / sizes now include intercluster storages
  (intercluster_storage|size, renamed onto the storage dim).
- CONFIG.Legacy.solution_access defaults to True for the v8 transition: the
  LegacySolutionWrapper bridges old access patterns (with DeprecationWarning)
  by default; strict mode is one config line away.
- LegacySolutionWrapper: selections drop the scalar coord (old per-element
  variables carried none) and cover component-level status/startup/shutdown/
  uptime/downtime plus flow-level status-tracking variables.
- stats.plot.storage resolves charge state from the batched variables
  (regular and intercluster) instead of per-element names.
- stats.plot.effects(effect=...) keeps a one-element effect dim so single-
  effect plots render one bar instead of failing on a scalar; threshold
  filtering picks the breakdown dim unless there are multiple effects.
- stats.plot.balance names its data so .to_dataframe() works.

Notebooks adapted to the batched idioms where they taught the old API
(stats.sizes.sel(element=...), flow_rates.sel(flow=...), batched linopy
variable names in the custom-constraint example).

Full suite: 1450 passed. All fast notebooks execute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the v8 transition compat layer from the solution to the statistics
accessors. When CONFIG.Legacy.solution_access is on (the default),
flow_rates / flow_hours / sizes / charge_states are wrapped in
LegacyElementAccess, which keeps v7 Dataset-style patterns working with a
DeprecationWarning:

    flow_rates['Boiler(Q_th)']  ->  .sel(flow='Boiler(Q_th)')
    flow_hours.items()          ->  per-element (label, DataArray) pairs
    sizes.data_vars             ->  {label: DataArray} mapping

Everything else proxies to the wrapped DataArray (including arithmetic and
abs() via explicit dunders, since special methods bypass __getattr__).
Internal composition uses the raw arrays; fix_sizes unwraps before its
isinstance dispatch. LegacySolutionWrapper.__contains__ now answers True for
translatable legacy keys. Strict-mode contract tests toggle the flag off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build the mapping directly instead of routing through items(), which emits
its own DeprecationWarning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@FBumann

FBumann commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Collected into the v8 integration branch (see #747) — v8 was created from this branch's head. Review context for the batched work lives here and in #602.

@FBumann FBumann closed this Jul 23, 2026
FBumann added a commit that referenced this pull request Jul 23, 2026
Squash of the element-data-classes work rebased onto the v8 removals.

Batched/vectorized model building — up to 67x faster for large systems:
type-level FlowsModel/StoragesModel/BusesModel replace per-element loops,
*Data containers pre-compute element parameters as element-dimensioned
DataArrays, variables use linopy masks, and the solution uses batched
naming ('flow|rate' with a flow dim instead of per-element variables).

v7 compatibility layer (CONFIG.Legacy.solution_access, default on for the
v8 transition, sunset v9):
- LegacySolutionWrapper translates old solution keys (flows, storages,
  effects, component status family) with DeprecationWarnings attributed to
  user code; membership checks translate too
- LegacyElementAccess keeps Dataset-style stats patterns working:
  flow_rates['Boiler(Q_th)'], .items(), .data_vars
- verified against real v7 result files: load, raw access and re-optimize
  work; stats accessors need one re-optimize

Also: clustered-storage investment effects fixed (dead collect_effects path),
intercluster sizes included in stats.sizes, docs notebooks adapted to the
batched idioms, expansion machinery reconciled with the tsam_xarray world.

BREAKING CHANGE: solution and linopy variables use batched names; statistics
accessors return element-dimensioned DataArrays. The legacy access layer
bridges common v7 patterns during the transition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant