Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'flixopt/**' # notebooks import flixopt; catch library changes that break docs
workflow_dispatch:
inputs:
deploy:
Expand Down
8 changes: 6 additions & 2 deletions docs/notebooks/data/generate_realistic_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,12 @@ def generate(
Electricity demand profile in kW
"""
slp_type = self.CONSUMER_TYPES[consumer_type]
e_slp = bdew.ElecSlp(self.year, holidays=self.holidays)
profile = e_slp.get_scaled_power_profiles({slp_type: annual_demand_kwh})
# demandlib calls warnings.simplefilter("error") internally, which would otherwise
# leak into the global state and turn every later warning (e.g. third-party
# DeprecationWarnings) into a hard error. Contain that side effect here.
with warnings.catch_warnings():
e_slp = bdew.ElecSlp(self.year, holidays=self.holidays)
profile = e_slp.get_scaled_power_profiles({slp_type: annual_demand_kwh})
# Resample to hourly and align with requested timesteps
profile_hourly = profile[slp_type].resample('h').mean()
return profile_hourly.reindex(timesteps, method='ffill').values
Expand Down
39 changes: 23 additions & 16 deletions flixopt/transform_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,12 @@ def _build_cluster_config_with_weights(
cluster: ClusterConfig | None,
auto_weights: dict[str, float],
available_columns: set[str] | None = None,
) -> ClusterConfig:
"""Merge auto-calculated weights into ClusterConfig.
) -> tuple[ClusterConfig, dict[str, float]]:
"""Resolve clustering weights and build a ClusterConfig without weights.

Weights are returned separately so they can be passed as a top-level
argument to ``tsam.aggregate``. Passing weights via ``ClusterConfig`` is
deprecated in tsam (>= 3.4.0) and raises a ``DeprecationWarning``.

Args:
cluster: Optional user-provided ClusterConfig.
Expand All @@ -912,7 +916,7 @@ def _build_cluster_config_with_weights(
(e.g., constant arrays removed before clustering).

Returns:
ClusterConfig with weights set (either user-provided or auto-calculated).
Tuple of (ClusterConfig without weights, resolved weights dict).
"""
from tsam import ClusterConfig

Expand All @@ -926,19 +930,21 @@ def _build_cluster_config_with_weights(
if available_columns is not None:
weights = {name: w for name, w in weights.items() if name in available_columns}

# No ClusterConfig provided - use defaults with weights
# No ClusterConfig provided - use defaults (weights passed to aggregate())
if cluster is None:
return ClusterConfig(weights=weights)

# ClusterConfig provided - use its settings with (possibly filtered) weights
return ClusterConfig(
method=cluster.method,
representation=cluster.representation,
weights=weights,
normalize_column_means=cluster.normalize_column_means,
use_duration_curves=cluster.use_duration_curves,
include_period_sums=cluster.include_period_sums,
solver=cluster.solver,
return ClusterConfig(), weights

# ClusterConfig provided - preserve its settings; weights passed to aggregate()
return (
ClusterConfig(
method=cluster.method,
representation=cluster.representation,
normalize_column_means=cluster.normalize_column_means,
use_duration_curves=cluster.use_duration_curves,
include_period_sums=cluster.include_period_sums,
solver=cluster.solver,
),
weights,
)
Comment on lines +933 to 948
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm ClusterConfig is a dataclass and weights=None does not warn
rg -nP --no-heading 'tsam' pyproject.toml setup.cfg setup.py requirements*.txt 2>/dev/null
pip install 'tsam==3.4.0' >/dev/null 2>&1 && python - <<'PY'
import dataclasses, warnings
from tsam import ClusterConfig
print("is_dataclass:", dataclasses.is_dataclass(ClusterConfig))
print("fields:", [f.name for f in dataclasses.fields(ClusterConfig)])
base = ClusterConfig(weights={'a': 1.0})
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter('always')
    dataclasses.replace(base, weights=None)
    print("warnings on replace(weights=None):", [str(x.category.__name__) for x in w])
PY

Repository: flixOpt/flixopt

Length of output: 849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect tsam==3.4.0 aggregate signature for weights=
python - <<'PY'
import inspect
import tsam
from tsam import aggregate
print("tsam version:", getattr(tsam, "__version__", "unknown"))
print("aggregate:", aggregate)
print("aggregate signature:", inspect.signature(aggregate))
PY

# 2) Try calling aggregate with weights= kwarg to see if it raises TypeError
python - <<'PY'
import numpy as np
import traceback
from tsam import aggregate

data = np.arange(12, dtype=float).reshape(6,2)  # minimal 2-column time series-like data
kwargs = dict(
    data=data,
    n_clusters=2,
    weights={"x": 1.0},   # if accepted, should not TypeError; if not, should complain
    # keep other params default
)

try:
    aggregate(**kwargs)
    print("aggregate accepted weights= (call did not TypeError)")
except TypeError as e:
    print("TypeError for weights=:", e)
except Exception as e:
    # if it validates weights and fails later, still means weights kwarg is accepted
    print("Non-TypeError raised (weights kwarg likely accepted):", type(e).__name__, e)
    # print a short traceback line
    tb = traceback.format_exc().splitlines()
    print("traceback tail:", "\n".join(tb[-3:]))
PY

# 3) Repo checks around reserved_tsam_keys and cluster()/aggregate() call paths
rg -n "reserved_tsam_keys" -S flixopt/transform_accessor.py
rg -n "aggregate\\(" -S flixopt/transform_accessor.py
rg -n "weights" -S flixopt/transform_accessor.py | head -n 80

# Show the relevant sections (bounded)
python - <<'PY'
from pathlib import Path
p = Path("flixopt/transform_accessor.py")
lines = p.read_text().splitlines()
def show(start, end):
    for i in range(start, end+1):
        print(f"{i}:{lines[i-1]}")
print("\n--- reserved_tsam_keys region ---")
show(1700, 1760)
print("\n--- _build_cluster_config_with_weights region ---")
show(880, 980)
print("\n--- cluster() / aggregate() call region ---")
show(1760, 1860)
PY

Repository: flixOpt/flixopt

Length of output: 18014


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find cluster() definition header and where tsam_kwargs is handled
rg -n "def cluster\\(" -S flixopt/transform_accessor.py
rg -n "tsam_kwargs" -S flixopt/transform_accessor.py

python - <<'PY'
from pathlib import Path
p=Path("flixopt/transform_accessor.py")
lines=p.read_text().splitlines()
# print a window around first occurrence of 'def cluster(' and around where tsam_kwargs is described/used
def show(center, span=80):
    start=max(1, center-span)
    end=min(len(lines), center+span)
    for i in range(start, end+1):
        print(f"{i}:{lines[i-1]}")
for idx,line in enumerate(lines, start=1):
    if "def cluster(" in line:
        print("\n--- def cluster() context ---")
        show(idx, 120)
        break
# find line that contains 'Additional keyword arguments passed'
for idx,line in enumerate(lines, start=1):
    if "Additional keyword arguments passed to" in line:
        print("\n--- tsam_kwargs doc context ---")
        show(idx, 120)
        break
PY

Repository: flixOpt/flixopt

Length of output: 25359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find cluster() definition header and where tsam_kwargs is handled
rg -n "def cluster\\(" -S flixopt/transform_accessor.py
rg -n "tsam_kwargs" -S flixopt/transform_accessor.py

python - <<'PY'
from pathlib import Path
p=Path("flixopt/transform_accessor.py")
lines=p.read_text().splitlines()

def show(center, span=80):
    start=max(1, center-span)
    end=min(len(lines), center+span)
    for i in range(start, end+1):
        print(f"{i}:{lines[i-1]}")

# cluster() context
for idx,line in enumerate(lines, start=1):
    if "def cluster(" in line:
        print("\n--- def cluster() context ---")
        show(idx, 130)
        break

# tsam_kwargs doc context
for idx,line in enumerate(lines, start=1):
    if "Additional keyword arguments passed to ``tsam.aggregate()``" in line:
        print("\n--- tsam_kwargs doc context ---")
        show(idx, 130)
        break

# where tsam_kwargs is likely passed in / constructed (look for **tsam_kwargs usage sites)
for idx,line in enumerate(lines, start=1):
    if "**tsam_kwargs" in line:
        print(f"\n--- **tsam_kwargs usage around line {idx} ---")
        show(idx, 60)
PY

Repository: flixOpt/flixopt

Length of output: 47349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import warnings
import numpy as np
import pandas as pd
from tsam import aggregate, ClusterConfig

# minimal time series-like DataFrame for tsam
df = pd.DataFrame(np.arange(12, dtype=float).reshape(6,2), columns=["x","y"])

base = ClusterConfig(
    method="hierarchical",
    representation="medoid",
    weights={"x": 1.0},  # non-None to show deprecation behavior
)

# 1) Passing weights via ClusterConfig may warn (depending on tsam)
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    try:
        aggregate(
            df,
            n_clusters=2,
            period_duration=24,
            temporal_resolution=None,
            cluster=base,
            weights=None,
        )
    except Exception:
        # ignore validation errors from tsam internals; we only care about DeprecationWarning emission
        pass
    dep_names = sorted({x.category.__name__ for x in w})
    print("warnings categories (cluster_config has weights):", dep_names)

# 2) weights=None in ClusterConfig + top-level weights should not warn
cfg_no_weights = ClusterConfig(
    method=base.method,
    representation=base.representation,
    normalize_column_means=base.normalize_column_means,
    use_duration_curves=base.use_duration_curves,
    include_period_sums=base.include_period_sums,
    solver=base.solver,
    weights=None,
)

with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    try:
        aggregate(
            df,
            n_clusters=2,
            period_duration=24,
            temporal_resolution=None,
            cluster=cfg_no_weights,
            weights={"x": 1.0},
        )
    except Exception:
        pass
    dep_names = sorted({x.category.__name__ for x in w})
    print("warnings categories (cluster_config weights=None):", dep_names)
PY

Repository: flixOpt/flixopt

Length of output: 426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import numpy as np
import pandas as pd
from tsam import aggregate, ClusterConfig

# Create a small hourly time series with a datetime index
idx = pd.date_range("2020-01-01", periods=48, freq="h")  # 2 days
df = pd.DataFrame(
    {
        "x": np.sin(np.linspace(0, 10, len(idx))) + 2.0,
        "y": np.cos(np.linspace(0, 10, len(idx))) + 5.0,
    },
    index=idx,
)

# Case A: top-level weights= accepted
cfg = ClusterConfig(method="hierarchical", representation="medoid", weights=None)
res = aggregate(
    df,
    n_clusters=2,
    period_duration=24,
    temporal_resolution="1h",
    cluster=cfg,
    weights={"x": 1.0, "y": 2.0},
)
print("aggregate(weights=...) succeeded:", type(res).__name__)

# Case B: ensure python doesn't crash due to duplicate weights at call site
try:
    aggregate(
        df,
        n_clusters=2,
        period_duration=24,
        temporal_resolution="1h",
        cluster=cfg,
        weights={"x": 1.0},
        **{"weights": {"x": 2.0}},   # would normally cause "multiple values" at call site
    )
except TypeError as e:
    print("duplicate weights via **tsam_kwargs would TypeError:", e)
PY

Repository: flixOpt/flixopt

Length of output: 496


🏁 Script executed:

rg -n "^import dataclasses\\b|\\bfrom dataclasses import\\b" flixopt/transform_accessor.py

Repository: flixOpt/flixopt

Length of output: 41


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "\\bdataclasses\\b" -S flixopt | head -n 50

Repository: flixOpt/flixopt

Length of output: 369


Refactor ClusterConfig reconstruction + guard tsam_kwargs from passing weights

  • For flixopt/transform_accessor.py _build_cluster_config_with_weights (933-948): tsam.ClusterConfig is a dataclass and dataclasses.replace(cluster, weights=None) does not emit a DeprecationWarning on tsam==3.4.0; prefer this over manually re-listing every field for future-proofing.
♻️ Proposed refactor
+        import dataclasses
+
         # No ClusterConfig provided - use defaults (weights passed to aggregate())
         if cluster is None:
             return ClusterConfig(), weights

-        # ClusterConfig provided - preserve its settings; weights passed to aggregate()
-        return (
-            ClusterConfig(
-                method=cluster.method,
-                representation=cluster.representation,
-                normalize_column_means=cluster.normalize_column_means,
-                use_duration_curves=cluster.use_duration_curves,
-                include_period_sums=cluster.include_period_sums,
-                solver=cluster.solver,
-            ),
-            weights,
-        )
+        # ClusterConfig provided - drop only weights, preserve every other field
+        # (incl. fields added in future tsam versions). weights=None should not
+        # trigger the deprecation warning.
+        return dataclasses.replace(cluster, weights=None), weights
- `tsam.api.aggregate` in `tsam==3.4.0` does accept top-level `weights=...` (so `weights=resolved_weights` at the `tsam.aggregate(...)` call is fine), but `cluster()`’s `reserved_tsam_keys` should also include `'weights'`—otherwise passing `weights` via `**tsam_kwargs` causes `TypeError: got multiple values for keyword argument 'weights'`.
🤖 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 933 - 948, Replace the manual
re-construction of ClusterConfig in _build_cluster_config_with_weights with
dataclasses.replace(cluster, weights=None) to avoid re-listing fields (use
ClusterConfig via dataclasses.replace). Also ensure the code that builds
tsam_kwargs/reserved_tsam_keys excludes top-level 'weights' by adding 'weights'
to reserved_tsam_keys (so weights is passed only via the explicit weights=...
arg to tsam.aggregate and not via **tsam_kwargs), preventing "multiple values
for keyword argument 'weights'".


def sel(
Expand Down Expand Up @@ -1786,7 +1792,7 @@ def to_clean_key(period_label, scenario_label) -> tuple:

# Build ClusterConfig with auto-calculated weights, filtered to available columns
clustering_weights = self._calculate_clustering_weights(ds_slice)
cluster_config = self._build_cluster_config_with_weights(
cluster_config, resolved_weights = self._build_cluster_config_with_weights(
cluster, clustering_weights, available_columns=set(df_for_clustering.columns)
)

Expand All @@ -1797,6 +1803,7 @@ def to_clean_key(period_label, scenario_label) -> tuple:
period_duration=hours_per_cluster,
temporal_resolution=dt,
cluster=cluster_config,
weights=resolved_weights,
extremes=extremes,
segments=segments,
preserve_column_means=preserve_column_means,
Expand Down
Loading