Skip to content

feat(cluster): add cluster_on for true subset clustering#724

Open
FBumann wants to merge 3 commits into
mainfrom
feat/cluster-on-subset
Open

feat(cluster): add cluster_on for true subset clustering#724
FBumann wants to merge 3 commits into
mainfrom
feat/cluster-on-subset

Conversation

@FBumann

@FBumann FBumann commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Adds transform.cluster(cluster_on=[...]) — "cluster on these variables only",
restoring the ergonomics of the removed v5/v6 data_vars. The clustering is computed
on the listed subset and the assignments are applied to the full dataset, so excluded
variables are aggregated but have no influence on the assignment.

Built on tsam_xarray 0.6.1, which adds aggregate(cluster_on=): flixopt delegates
straight to it (single aggregate() call, no subset+apply plumbing on our side).

Why not weights={var: 0}?

A 0 weight is not genuine exclusion — tsam clamps it up to a minimal tolerable
weighting
(with a warning), so the variable still nudges the assignment, and you'd have
to enumerate every column to zero the rest. cluster_on is a hard structural selection;
weights stays purely about relative importance among the kept variables.

# v5/v6
fs.transform.cluster(n_clusters=8, cluster_duration='1D',
                     data_vars=['HeatDemand(Q)|fixed_relative_profile'])
# v7 (this PR)
fs.transform.cluster(n_clusters=8, cluster_duration='1D',
                     cluster_on=['HeatDemand(Q)|fixed_relative_profile'])

cluster_on composes with ClusterConfig(weights=...) (weights may not reference an
excluded variable). Not a performance feature — subset-then-apply is a second pass, on
par with a full clustering.

Dependency

  • tsam_xarray >= 0.6.1 (was >= 0.6.0); dev pin == 0.6.1. 0.6.1 adds
    aggregate(cluster_on=) and validates extremes that reference excluded variables.

Docs

  • New migration-guide-v7.md (v6→v7 tsam_xarray backend); the data_vars section leads
    with cluster_on. Stable { #cluster-inputs } anchor for the cluster_inputs() links.
  • migration-guide-v6.md: removed tsam_xarray content that had been misplaced into the
    v5→v6 guide (it shipped in v7); cross-linked to v7.
  • Registered v7 in the mkdocs nav.

Tests

TestClusterOn (9 cases): equivalence to a manual subset+apply, subset selection drives
assignments, multiperiod per-slice correctness, excluded var retained, no
epsilon-clamp warning, weights combination, and error paths. Full clustering suite: 252
passed. ruff clean.

Review-finding resolutions

  • Fixedcluster_inputs() doc anchor: added explicit { #cluster-inputs } and
    repointed both links (robust across slugify configs; the emoji made the auto-slug fragile).
  • Fixed — nitpick "exact same" → "same semantics".
  • Fixed — added multiperiod cluster_on coverage.
  • Skipped — flixopt-level extremes-vs-cluster_on validation: redundant, tsam_xarray
    0.6.1 already raises a clear ValueError ("extremes references coordinates {...} that
    cluster_on excluded").

Note

tsam_xarray 0.6.1 is released (GitHub tag v0.6.1); at time of writing PyPI was still
ingesting it. Local dev/CI resolves once it lands on PyPI.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a cluster_on option to control which time-varying inputs define clustering assignments, while retaining other time-varying variables in the reduced output via aggregation.
    • Updated clustering execution to use the selected inputs directly and enforce validation (e.g., empty/unknown selections, and incompatible weights).
  • Documentation
    • Expanded the v5→v6 migration guide with additional v6 capabilities and updated clustering metadata details.
    • Added a full v6.x→v7 migration guide and linked it from the support section.
  • Tests
    • Added integration tests for cluster_on, including multi-period consistency and error handling.
  • Chores
    • Updated the tsam_xarray version used for optional installs.

Reintroduce the ergonomics of the removed v5/v6 `data_vars` parameter as
`transform.cluster(cluster_on=[...])`. The clustering is computed on the
listed subset and the resulting assignments are applied to the full dataset,
so excluded variables are aggregated but have no influence on assignment.

This is genuine exclusion, unlike `ClusterConfig(weights={var: 0})`, which
tsam clamps up to a minimal tolerable weight (so a 0-weighted variable still
nudges the assignment). `cluster_on` composes with `weights` for relative
importance among the kept variables and guards against weighting an excluded
variable.

- Implement subset-aggregate + `ClusteringResult.apply()` in cluster()
- Add TestClusterOn (8 cases): equivalence to manual subset+apply, subset
  selection drives assignments, excluded var retained, no epsilon-clamp
  warning, weights combination, and error paths
- Docs: new v6->v7 migration guide (tsam_xarray backend); strip misplaced
  tsam_xarray content out of the v5->v6 guide; register v7 in nav

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b703598d-2f32-4ae8-b641-fe85437749fc

📥 Commits

Reviewing files that changed from the base of the PR and between 66afd09 and 821e21a.

📒 Files selected for processing (1)
  • docs/user-guide/migration-guide-v7.md
📝 Walkthrough

Walkthrough

Adds cluster_on support to clustering, with validation and propagation into tsam_xarray. Updates integration tests and documents the v6/v7 migration path, including the new guide, navigation entry, and revised clustering metadata guidance.

Changes

cluster_on implementation and documentation

Layer / File(s) Summary
v7 migration guide
docs/user-guide/migration-guide-v7.md
Adds the v7 migration guide content covering clustering backend changes, breaking API updates, cluster input enumeration, and migration checklist items.
v6 guide and navigation
docs/user-guide/migration-guide-v6.md, mkdocs.yml
Updates the v6 migration guide with v7 cross-links, new v6 feature notes, revised clustering metadata guidance, and a help link to the v7 guide, while adding the new guide to mkdocs navigation.
cluster_on clustering logic
flixopt/transform_accessor.py, pyproject.toml
Adds the cluster_on parameter to TransformAccessor.cluster(), documents its behavior, validates selection and weight constraints, passes it through to tsam_xarray.aggregate(), and bumps tsam_xarray version constraints.
integration coverage for cluster_on
tests/test_clustering/test_integration.py
Adds integration tests covering subset-driven assignments, multi-period behavior, excluded-variable handling, warning suppression, weight combination, and invalid cluster_on inputs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TransformAccessor
  participant tsam_xarray
  Caller->>TransformAccessor: cluster(cluster_on=[...], weights=...)
  TransformAccessor->>TransformAccessor: validate cluster_on and weights
  TransformAccessor->>tsam_xarray: aggregate(..., cluster_on=cluster_on, weights=weights)
  tsam_xarray-->>TransformAccessor: clustering result
  TransformAccessor-->>Caller: reduced FlowSystem
Loading

Possibly related PRs

  • flixOpt/flixopt#552: Also changes TransformAccessor.cluster() clustering behavior and result handling in the same code path.
  • flixOpt/flixopt#625: Also adjusts clustering weight handling and validation around inputs passed to clustering.
  • flixOpt/flixopt#627: Also adds validation for which variables are eligible for clustering based on user-provided selection or weights.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding cluster_on for subset clustering.
Description check ✅ Passed The description is detailed and covers the change, dependency, docs, tests, and notes, though a few template sections are omitted.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cluster-on-subset

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
docs/user-guide/migration-guide-v7.md (1)

37-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: "exact same" is wordy.

Consider replacing "exact same semantics" with "same semantics".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user-guide/migration-guide-v7.md` around lines 37 - 40, The migration
guide wording in the cluster_on description is a bit wordy; update the phrase in
the relevant section of the guide to use “same semantics” instead of “exact same
semantics” while keeping the rest of the explanation about clustering on the
subset and applying assignments to the full dataset unchanged.

Source: Linters/SAST tools

tests/test_clustering/test_integration.py (1)

536-548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a multi-period cluster_on test.

All TestClusterOn tests use a single-period, single-scenario system. The cluster_on path at transform_accessor.py:1452-1475 calls agg_subset.clustering.apply() without explicit slice_dims, unlike apply_clustering() which reconstructs the ClusteringResult with slice_dims. A test with has_periods=True or has_scenarios=True would verify this path works correctly for multi-slice systems.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_clustering/test_integration.py` around lines 536 - 548, Add a
multi-period or multi-scenario coverage case for the cluster_on path in
TestClusterOn so it exercises the transform.cluster flow on a sliced system, not
just the single-period single-scenario helper. The issue is that
transform_accessor.py’s cluster_on branch calls agg_subset.clustering.apply()
without explicitly passing slice_dims, so add a test using has_periods=True or
has_scenarios=True that compares cluster_on behavior against the expected
clustered result and verifies assignments remain correct across slices. Use the
existing test_subset_selection_drives_assignments and the _two_var_system helper
as the entry point for locating the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/user-guide/migration-guide-v7.md`:
- Around line 61-69: The `cluster_inputs()` links use an invalid fragment that
does not match the generated heading anchor, so update the markdown in the
migration guide to use the exact mkdocs anchor for `transform.cluster_inputs()`
or explicitly define a stable anchor on that heading. Check both occurrences of
the `cluster_inputs()` reference and make them point to the same valid target
used by the `cluster_inputs` section.

In `@flixopt/transform_accessor.py`:
- Around line 1397-1419: Add early validation in the cluster_on handling block
to reject any extremes configured via ExtremeConfig that reference variables not
present in cluster_on. Reuse the existing validation pattern near
weights/cluster_on in transform_accessor.py: inspect the extremes passed through
tsam_kwargs_full before calling tsam_xarray.aggregate(), compare their
min_value/max_value variable names against cluster_on_set, and raise a clear
ValueError listing the excluded names and telling the caller to either remove
them from extremes or add them to cluster_on.

---

Nitpick comments:
In `@docs/user-guide/migration-guide-v7.md`:
- Around line 37-40: The migration guide wording in the cluster_on description
is a bit wordy; update the phrase in the relevant section of the guide to use
“same semantics” instead of “exact same semantics” while keeping the rest of the
explanation about clustering on the subset and applying assignments to the full
dataset unchanged.

In `@tests/test_clustering/test_integration.py`:
- Around line 536-548: Add a multi-period or multi-scenario coverage case for
the cluster_on path in TestClusterOn so it exercises the transform.cluster flow
on a sliced system, not just the single-period single-scenario helper. The issue
is that transform_accessor.py’s cluster_on branch calls
agg_subset.clustering.apply() without explicitly passing slice_dims, so add a
test using has_periods=True or has_scenarios=True that compares cluster_on
behavior against the expected clustered result and verifies assignments remain
correct across slices. Use the existing test_subset_selection_drives_assignments
and the _two_var_system helper as the entry point for locating the behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1dd8582e-76cf-47fc-9673-866ba132b0ae

📥 Commits

Reviewing files that changed from the base of the PR and between 432c463 and e06cb6c.

📒 Files selected for processing (5)
  • docs/user-guide/migration-guide-v6.md
  • docs/user-guide/migration-guide-v7.md
  • flixopt/transform_accessor.py
  • mkdocs.yml
  • tests/test_clustering/test_integration.py

Comment thread docs/user-guide/migration-guide-v7.md
Comment thread flixopt/transform_accessor.py Outdated
Comment on lines +1397 to +1419
# Pass user-specified weights to tsam_xarray (validates unknown keys).
# cluster_on filters *which* variables the clustering runs on; weights set the
# relative importance among the kept ones. Excluded variables are left out of the
# aggregation entirely (below) and only receive the resulting assignments — this
# is true exclusion, unlike a 0 weight, which tsam clamps up to a tiny epsilon.
weights = dict(cluster.weights) if (cluster is not None and cluster.weights is not None) else {}
if cluster_on is not None:
if not cluster_on:
raise ValueError('cluster_on must list at least one variable to cluster on.')
clusterable = list(ds_for_clustering.data_vars)
unknown = [name for name in cluster_on if name not in clusterable]
if unknown:
raise ValueError(
f'cluster_on contains variables that are not clusterable inputs: {unknown}. '
f'Call transform.cluster_inputs() to list the valid names.'
)
cluster_on_set = set(cluster_on)
masked = [name for name in weights if name not in cluster_on_set]
if masked:
raise ValueError(
f'ClusterConfig(weights=...) sets weights for variables excluded by cluster_on: '
f'{masked}. Remove them from weights or add them to cluster_on.'
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing validation for extremes when used with cluster_on.

The validation block correctly checks for empty cluster_on, unknown variables, and weights referencing excluded variables. However, extremes (via tsam_kwargs_full at line 1425) is passed directly to the subset tsam_xarray.aggregate() call at line 1457. If ExtremeConfig(max_value=[...]) or min_value=[...] references a variable excluded by cluster_on, tsam_xarray will receive data that doesn't contain that variable, likely producing a confusing KeyError rather than a clear validation message.

This is inconsistent with the PR's approach of providing clear, early validation for all cluster_on conflict cases.

🛡️ Suggested validation addition
             cluster_on_set = set(cluster_on)
             masked = [name for name in weights if name not in cluster_on_set]
             if masked:
                 raise ValueError(
                     f'ClusterConfig(weights=...) sets weights for variables excluded by cluster_on: '
                     f'{masked}. Remove them from weights or add them to cluster_on.'
                 )
+            if extremes is not None:
+                extreme_vars = list(extremes.max_value or []) + list(extremes.min_value or [])
+                excluded_extremes = [v for v in extreme_vars if v not in cluster_on_set]
+                if excluded_extremes:
+                    raise ValueError(
+                        f'ExtremeConfig references variables excluded by cluster_on: {excluded_extremes}. '
+                        f'Remove them from extremes or add them to cluster_on.'
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Pass user-specified weights to tsam_xarray (validates unknown keys).
# cluster_on filters *which* variables the clustering runs on; weights set the
# relative importance among the kept ones. Excluded variables are left out of the
# aggregation entirely (below) and only receive the resulting assignments — this
# is true exclusion, unlike a 0 weight, which tsam clamps up to a tiny epsilon.
weights = dict(cluster.weights) if (cluster is not None and cluster.weights is not None) else {}
if cluster_on is not None:
if not cluster_on:
raise ValueError('cluster_on must list at least one variable to cluster on.')
clusterable = list(ds_for_clustering.data_vars)
unknown = [name for name in cluster_on if name not in clusterable]
if unknown:
raise ValueError(
f'cluster_on contains variables that are not clusterable inputs: {unknown}. '
f'Call transform.cluster_inputs() to list the valid names.'
)
cluster_on_set = set(cluster_on)
masked = [name for name in weights if name not in cluster_on_set]
if masked:
raise ValueError(
f'ClusterConfig(weights=...) sets weights for variables excluded by cluster_on: '
f'{masked}. Remove them from weights or add them to cluster_on.'
)
# Pass user-specified weights to tsam_xarray (validates unknown keys).
# cluster_on filters *which* variables the clustering runs on; weights set the
# relative importance among the kept ones. Excluded variables are left out of the
# aggregation entirely (below) and only receive the resulting assignments — this
# is true exclusion, unlike a 0 weight, which tsam clamps up to a tiny epsilon.
weights = dict(cluster.weights) if (cluster is not None and cluster.weights is not None) else {}
if cluster_on is not None:
if not cluster_on:
raise ValueError('cluster_on must list at least one variable to cluster on.')
clusterable = list(ds_for_clustering.data_vars)
unknown = [name for name in cluster_on if name not in clusterable]
if unknown:
raise ValueError(
f'cluster_on contains variables that are not clusterable inputs: {unknown}. '
f'Call transform.cluster_inputs() to list the valid names.'
)
cluster_on_set = set(cluster_on)
masked = [name for name in weights if name not in cluster_on_set]
if masked:
raise ValueError(
f'ClusterConfig(weights=...) sets weights for variables excluded by cluster_on: '
f'{masked}. Remove them from weights or add them to cluster_on.'
)
if extremes is not None:
extreme_vars = list(extremes.max_value or []) + list(extremes.min_value or [])
excluded_extremes = [v for v in extreme_vars if v not in cluster_on_set]
if excluded_extremes:
raise ValueError(
f'ExtremeConfig references variables excluded by cluster_on: {excluded_extremes}. '
f'Remove them from extremes or add them to cluster_on.'
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flixopt/transform_accessor.py` around lines 1397 - 1419, Add early validation
in the cluster_on handling block to reject any extremes configured via
ExtremeConfig that reference variables not present in cluster_on. Reuse the
existing validation pattern near weights/cluster_on in transform_accessor.py:
inspect the extremes passed through tsam_kwargs_full before calling
tsam_xarray.aggregate(), compare their min_value/max_value variable names
against cluster_on_set, and raise a clear ValueError listing the excluded names
and telling the caller to either remove them from extremes or add them to
cluster_on.

tsam_xarray 0.6.1 adds aggregate(cluster_on=), so cluster_on is now passed
straight through instead of doing the subset-aggregate + apply dance in
flixopt. Same semantics (cluster on the subset, apply assignments to the full
set), upstream also validates extremes referencing excluded variables.

- Bump tsam_xarray pin to >= 0.6.1 (and dev pin to ==0.6.1)
- Replace the subset+apply branch with cluster_on= on the single aggregate call
- Docs: stable { #cluster-inputs } anchor + fix the two cluster_inputs() links;
  drop "exact same" -> "same semantics"
- Tests: add multiperiod per-slice cluster_on coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FBumann FBumann enabled auto-merge (squash) July 9, 2026 12:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/user-guide/migration-guide-v7.md (1)

28-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document tsam_xarray >= 0.6.1 docs/user-guide/migration-guide-v7.md:28-29 says >= 0.6.0, but pyproject.toml pins tsam_xarray to 0.6.1. Align the documented minimum with the supported version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user-guide/migration-guide-v7.md` around lines 28 - 29, Update the
migration guide dependency note for tsam_xarray so the documented minimum
matches the version actually supported by the project. In the migration-guide-v7
content, adjust the tsam_xarray requirement line to reflect the 0.6.1 minimum
used in pyproject.toml, while leaving the tsam dependency note unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/user-guide/migration-guide-v7.md`:
- Around line 28-29: Update the migration guide dependency note for tsam_xarray
so the documented minimum matches the version actually supported by the project.
In the migration-guide-v7 content, adjust the tsam_xarray requirement line to
reflect the 0.6.1 minimum used in pyproject.toml, while leaving the tsam
dependency note unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b95cc71-a3e0-4769-82d3-1df124907588

📥 Commits

Reviewing files that changed from the base of the PR and between e06cb6c and 66afd09.

📒 Files selected for processing (4)
  • docs/user-guide/migration-guide-v7.md
  • flixopt/transform_accessor.py
  • pyproject.toml
  • tests/test_clustering/test_integration.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_clustering/test_integration.py

Co-Authored-By: Claude Opus 4.8 (1M context) <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