Conversation
chore: sync develop-v4
* Add glossary * All 184 tests pass. Here's what was done: Created (10 new files) src/tsam/pipeline/ ├── __init__.py — run_pipeline() orchestrator (replaces createTypicalPeriods) ├── types.py — NormalizedData, PeriodProfiles, PipelineResult dataclasses ├── normalize.py — normalize(), denormalize() ├── periods.py — unstack_to_periods(), add_period_sum_features() ├── clustering.py — cluster_periods(), cluster_sorted_periods(), use_predefined_assignments() ├── represent.py — compute_representatives() (wraps representations.py) ├── extremes.py — count_extreme_periods(), add_extreme_periods() ├── rescale.py — rescale_representatives() ├── segment.py — segment_typical_periods() (wraps utils/segmentation.py) └── accuracy.py — reconstruct(), compute_accuracy() Modified (4 files) - src/tsam/api.py — Now calls run_pipeline(). Removed _build_old_params(), rewrote _build_clustering_result() to take PipelineResult. - src/tsam/result.py — Replaced _aggregation: TimeSeriesAggregation with _original_data, _reconstructed_data, _time_index, _segmented_df explicit fields. - src/tsam/config.py — Rewrote ClusteringResult.apply() to call run_pipeline() directly. - test/test_reconstruct_samemean_segmentation.py — Updated _aggregation.timeSeries → result.original (public API). Key technical decisions 1. sameMean fix: The monolith had an in-place side effect where _postProcessTimeSeries mutated normalizedTypicalPeriods. The pipeline avoids this by working on copies, so the compensating division in predictOriginalData was removed. 2. Column order: Pipeline sorts columns internally (matching monolith) but restores original column order in all output DataFrames. 3. Zero monolith coupling: api.py, result.py, and config.py no longer import from timeseriesaggregation.py. The monolith is only imported by __init__.py (legacy export) and old test files. * Refactor pipeline to use config objects and canonical method names - run_pipeline() now accepts ClusterConfig, ExtremeConfig, SegmentConfig directly instead of ~20 unpacked keyword arguments - Bundle 6 predef_* transfer kwargs into a PredefParams dataclass - Use short canonical names (e.g. "kmeans", "medoid", "new_cluster") throughout pipeline, with alias dicts in representations.py and periodAggregation.py for backward compat with the monolith - Remove METHOD_MAPPING, REPRESENTATION_MAPPING, EXTREME_METHOD_MAPPING translation dicts from config.py and api.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move ClusteringResult construction into pipeline, unify AggregationResult assembly Build ClusteringResult inside run_pipeline() so PipelineResult carries it directly. Replace the duplicated post-processing in aggregate() and ClusteringResult.apply() with a single _build_aggregation_result() helper, eliminating ~110 lines of repeated code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 1. Deleted represent.py — dead code removed 2. Removed distribution_period_wise from all 4 pipeline wrapper signatures; hardcoded True at the 4 underlying call sites 3. Unified return order — cluster_sorted_periods now returns (centers, center_indices, order) matching cluster_periods; caller destructuring is now identical for both branches 4. Dropped profiles from PeriodProfiles — replaced 2 usages with profiles_dataframe.values 5. Removed eager config unpacking — 30 lines of locals deleted; accessing cluster.method, extremes.max_value, segments.n_segments etc. directly 6. Slimmed PipelineResult from 22 fields to 11 — removed all intermediates that were only consumed during the pipeline * Here's a summary of what changed: ┌───────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ File │ Change │ ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/types.py │ Added weights and normalize_column_means fields to NormalizedData │ ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/normalize.py │ normalize() stores the two new fields; denormalize() signature reduced from 5 params to 2 + apply_weights kwarg; round_decimals removed │ ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/accuracy.py │ reconstruct() lost 3 params (normalize_column_means, weights, round_decimals); compute_accuracy() takes norm_data instead of weights │ ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/rescale.py │ rescale_representatives() lost 4 params (original_data, columns, normalize_column_means, weights), replaced by norm_data │ ├───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/__init__.py │ All 4 call sites updated; round_decimals now applied after denormalize/reconstruct return │ └───────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ * Extract some helper methods * Add documentation * Summary of what was removed: ┌──────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Location │ Removed │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ config.py ExtremeConfig │ preserve_n_clusters field, __post_init__ FutureWarning, _effective_preserve_n_clusters property, serialization in to_dict/from_dict │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/__init__.py │ Step 4 (warning + count_extreme_periods + n_extremes + effective_n_clusters); n_clusters now passed directly to clustering │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ pipeline/extremes.py │ count_extreme_periods() function deleted entirely │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ timeseriesaggregation.py │ self.extremePreserveNumClusters attribute, _countExtremePeriods() method, preserve_n_clusters logic block in createTypicalPeriods(); effective_n_clusters → self.noTypicalPeriods │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ test_extremePeriods.py │ 8 test_preserve_n_clusters_* tests removed │ ├──────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ docs/ │ extreme_periods_methods.md deleted, glossary.md row removed, pipeline_dataflow.md step 4 removed + renumbered, newsDoc.rst updated │ └──────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ * important: Rename all camelCase identifiers to snake_case across entire codebase This is a comprehensive naming convention migration for v3.0.0, converting all camelCase function names, method names, parameters, attributes, local variables, dict keys, and comments to snake_case. The legacy monolith is deprecated and this is already a breaking release, so its public API is renamed as well. File renames: - periodAggregation.py → period_aggregation.py - utils/durationRepresentation.py → utils/duration_representation.py Core modules renamed (functions + all parameters + locals): - period_aggregation.py: aggregatePeriods → aggregate_periods - representations.py: meanRepresentation → mean_representation, etc. - utils/duration_representation.py: durationRepresentation → duration_representation - utils/segmentation.py: all params (normalizedTypicalPeriods → normalized_typical_periods, etc.) - utils/k_maxoids.py: doLogarithmic → do_logarithmic, inertiaTempPrime → inertia_best, etc. Legacy monolith (timeseriesaggregation.py): - 27 __init__ parameters renamed (noTypicalPeriods → no_typical_periods, etc.) - 13 methods renamed (createTypicalPeriods → create_typical_periods, etc.) - 6 properties renamed (clusterOrder → cluster_order, etc.) - All internal attributes and ~50+ local variables renamed - Error messages updated to reference new parameter names - Dict keys renamed: stepNo → step_no, clusterNo → cluster_no, newClusterNo → new_cluster_no Hyperparameter tuning (hyperparametertuning.py): - 2 module functions renamed (getNoPeriodsForDataReduction → get_no_periods_for_data_reduction, etc.) - 4 methods renamed (identifyOptimalSegmentPeriodCombination → identify_optimal_segment_period_combination, etc.) - All parameters, attributes, and locals renamed Pipeline modules: - Updated all developer comments referencing old monolith method names - Warning messages updated (numericalTolerance → numerical_tolerance) - Error messages updated (timeSeries → time_series) Documentation: - Updated legacy code examples in __init__.py, gettingStartedDoc.rst, newsDoc.rst - Updated function references in pipeline_dataflow.md - Updated performance section method names in newsDoc.rst Test files (22 files updated): - All monolith kwargs, method calls, property accesses, and attribute accesses renamed to snake_case - Error message regex patterns updated in test_assert_raises.py - Hyperparametertuning function/method calls updated Intentionally NOT renamed: - Pyomo rule names in k_medoids_exact.py (objRule, candToClusterRule, etc.) - Class names (PascalCase is correct: TimeSeriesAggregation, HyperTunedAggregations) - _ALIASES dict string keys (backward compat: "meanRepresentation" → "mean") - REPRESENTATION_METHODS list string values (backward compat for user input) - unstackToPeriods backward compat alias in __init__.py All 176 tests pass, 1 skipped (kmedoids dependency). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * src/tsam/timeseriesaggregation.py - Added _PARAM_ALIASES dict (28 entries mapping old camelCase → new snake_case) - Added **kwargs to __init__, with translation loop that emits FutureWarning for each deprecated kwarg, plus kwargs.pop() lines to apply overrides, and TypeError for unknown kwargs - Made time_series param default to None (needed for timeSeries= kwarg to work) - Added 6 method aliases: createTypicalPeriods, predictOriginalData, accuracyIndicators, totalAccuracyIndicators, prepareEnersysInput, indexMatching - Added 6 property aliases: stepIdx, clusterPeriodIdx, clusterOrder, clusterPeriodNoOccur, clusterPeriodDict, segmentDurationDict src/tsam/hyperparametertuning.py - Added 2 function aliases: getNoPeriodsForDataReduction, getNoSegmentsForDataReduction - Added **kwargs to HyperTunedAggregations.__init__ with saveAggregationHistory translation - Added 2 method aliases: identifyOptimalSegmentPeriodCombination, identifyParetoOptimalAggregation - Added 4 property aliases: aggregationHistory, _RMSEHistory, _segmentHistory, _periodHistory test/test_backward_compat.py (new) - 6 tests covering old kwargs with FutureWarning, unknown kwarg rejection, old method names, old property names, old hyperparameter function names, and old saveAggregationHistory kwarg Result: 182 passed, 1 skipped (the kmedoids test). * Re-add stable sort and update golden test data * Add test for backwards compat * Update e2e fixtures as well * Remove preserve_n_clusters * Revert back to pandas based sort * Use remaining config options taht were previously missing in v3 * perf: properly vectorize sorting without any pandas loops * Revert to v2.3.9 golden files * Regenerate golden files * Work the new config objects into the pipeline * All done. Here's a summary of the changes: src/tsam/pipeline/extremes.py — add_extreme_periods now takes extremes: ExtremeConfig instead of 4 separate params (extreme_method, add_peak_max, add_peak_min, add_mean_max, add_mean_min). Reads .method, .max_value, .min_value, .max_period, .min_period inside. src/tsam/pipeline/clustering.py — Three functions refactored: - cluster_periods: takes cluster: ClusterConfig instead of cluster_method + solver + representation_method - cluster_sorted_periods: takes period_profiles: PeriodProfiles + cluster: ClusterConfig instead of profiles_values + n_columns + cluster_method + solver + representation_method - use_predefined_assignments: takes predef: PredefParams instead of cluster_order + center_indices src/tsam/pipeline/segment.py — segment_typical_periods takes segments: SegmentConfig + predef: PredefParams | None instead of n_segments + representation_method + 3 predef params. src/tsam/pipeline/__init__.py — All 5 call sites simplified. Removed the representation_method and segment_rep local variables. All 481 tests pass. The 4 hierarchical_weighted/testdata failures are pre-existing (fail on the stashed original code too). ✻ Baked for 22m 42s ⏺ Background command "Run tests to verify refactor" failed with exit code 1 ⏺ That was the initial test run that caught the pre-existing hierarchical_weighted/testdata failure. I already investigated it — confirmed it fails on the original code too (pre-existing), then re-ran with that case excluded and all 481 tests passed. No action needed. * src/tsam/pipeline/__init__.py: 1. Item 1 — Segmentation no longer overwrites normalized_typical_periods. Two explicit variables (denorm_source, reconstruct_source) route the right DataFrame to denormalize and reconstruct. 2. Item 2 — Removed columns from the add_extreme_periods call site. 3. Item 3 — Extracted _extract_segment_data() helper (34 lines) from _build_clustering_result, which now calls it in one line. 4. Item 4 — After rescale_representatives, cluster_periods_list is coerced to list. _representatives_to_dataframe now expects list only (isinstance guard removed). 5. Item 5 — Extracted _build_representation_dict() helper. The 17-line inline block is now a single call. 6. Item 6 — Renamed del_cluster_params → n_feature_cols with positive semantics (candidates.shape[1] before augmentation, candidates.shape[1] - n_extra after). 7. Item 7 — Steps renumbered 1–16 sequentially (gap at step 4 removed). src/tsam/pipeline/accuracy.py: - reconstruct simplified from 6 params to 4 (removed segmentation_active and predicted_segmented_df). src/tsam/pipeline/extremes.py: - add_extreme_periods derives columns internally from profiles_df.columns.get_level_values(0).unique() instead of receiving it as a parameter. * Fix cross-platform determinism in distributionPeriodWise=False path Add stable sorting and rounded means to the global distribution path, matching the fixes already applied to the per-cluster (periodWise=True) path. * Regenerate test fixtures and golden data * Add piepline guide * tests: dont compare column order as we dont sort anymore * fix: divide by weight * Add Release notes * Disambiguate 'weights' across the codebase (#27) * ⏺ All done. Here's a summary of what was changed: 1. Renamed internal cluster-weight variables + alias public API - pipeline/types.py: PipelineResult.cluster_weights → PipelineResult.cluster_counts (type fixed to dict[int, float]) - pipeline/__init__.py: cluster_period_no_occur → cluster_counts (local variable + PipelineResult construction) - api.py: reads result.cluster_counts instead of result.cluster_weights - result.py: primary field renamed to cluster_counts: dict[int, float], added deprecated cluster_weights property emitting FutureWarning 2. Fixed dict[int, int] type → dict[int, float] - Both PipelineResult and AggregationResult now correctly annotate as dict[int, float] since partial-period adjustment makes values fractional 3. Stored column weights in ClusteringResult + replay during apply() - Added column_weights: tuple[tuple[str, float], ...] | None field to ClusteringResult - Populated in _build_clustering_result() from cluster_config.weights - apply() now passes weights=dict(self.column_weights) to ClusterConfig, so transferred results preserve column weighting - Serialization: to_dict() converts to dict, from_dict() converts back to sorted tuple-of-pairs 4. Documented column-weight side effects - Added note to ClusterConfig.weights docstring explaining that weights affect rescaling bounds, not just clustering distance - Added inline comment at rescale.py:79-83 explaining why scale_ub is multiplied by the weight Tests: 633 tests pass (485 + 148 golden), 75 skipped. * Core pipeline (6 files): 1. src/tsam/pipeline/normalize.py — Removed weights parameter from normalize(), removed apply_weights parameter from denormalize(), removed MIN_WEIGHT constant. Normalization now produces purely unweighted data. 2. src/tsam/pipeline/types.py — Removed weights field from NormalizedData dataclass. 3. src/tsam/period_aggregation.py — Added representation_candidates parameter to aggregate_periods(). When provided, representations() uses this array instead of the clustering candidates, allowing representatives to be computed from unweighted data while clustering uses weighted data. 4. src/tsam/pipeline/clustering.py — Added weighted_candidates parameter to cluster_periods() and cluster_sorted_periods(). When provided, weighted data is passed to aggregate_periods() for distance computation while unweighted data is used for representation. 5. src/tsam/pipeline/__init__.py — Added _weight_candidates() helper and _build_weight_vector(). Creates weighted candidates after unstacking, passes them to clustering functions. Updated rescale_representatives() call to pass original_data and normalize_column_means directly. Updated compute_accuracy() call to remove norm_data. 6. src/tsam/pipeline/rescale.py — Changed signature to accept original_data and normalize_column_means directly instead of NormalizedData. Removed the weight compensation scale_ub *= weights[column] block. 7. src/tsam/pipeline/accuracy.py — Removed weight-undo block from reconstruct(), simplified denormalize() call, simplified compute_accuracy() to compare directly without weight division. Tests (3 files): 8. test/test_api_equivalence.py — Changed test_with_weights to compare cluster assignments instead of exact DataFrame equality (medoid selection intentionally differs). 9. test/test_old_new_equivalence.py — Added _SKIP_EQUIVALENCE set and skip_equivalence flag on EquivalenceCase. Tests for representatives, accuracy, and reconstruction skip for weighted configs; cluster assignment test still runs. 10. test/test_golden_regression.py — test_update_golden generates golden from new API for divergent configs. test_old_api_matches_golden skips for divergent configs. 11. test/data/golden/hierarchical_weighted/testdata.csv — Regenerated from new API. Test results 629 passed, 79 skipped, 0 failed (including test_weightingFactors.py which validates the equal-weight invariant on the old API). * Update Changelog * ⏺ Fixed three issues: 1. src/tsam/config.py — ClusterConfig.weights docstring: removed the stale claim that weights affect rescaling bounds. Now correctly states weights apply to a separate copy used only for clustering distance. 2. docs/source/glossary.md — Section 3 "Normalization & Weighting": - Column Weight: updated definition to say "applied to a separate copy", explicitly lists what weights do not affect. - Weighted Profile → Weighted Candidates: renamed to match the actual code variable name, updated definition to say "used exclusively as the distance matrix for clustering". 3. docs/source/glossary.md — Section 7 "Rescaling": - Rescale Upper Bound: removed "column weight" from derivation, now says "derived from mean-normalization settings only". 4. docs/source/glossary.md — Naming conventions and ambiguities resolved: weighted_profiles → weighted_candidates in both places. * Test results: 644 passed, 79 skipped (up from 629 — the 15 new tests in test_weight_decoupling.py all pass) PR review items addressed: - Bug 1 (partial weight dict): Fixed — _build_weight_vector now emits entries for ALL columns, defaulting unlisted to 1.0 - Bug 2 (cluster_sorted_periods ignoring weighted_candidates): Fixed — sorts weighted candidates separately and passes representation_candidates - print() → warnings.warn(): Fixed in _build_weight_vector - _count_occurrences float cast: Fixed int() → float() - Stale 3.1.2 changelog: Removed stale weight-rescaling bullet - Test gaps: All 15 new tests pass (unit tests for helpers, partial weights, duration curves + weights, deprecated property warning, JSON roundtrip) Remaining PR review items not requiring action: - to_dict() backward compat: AggregationResult.to_dict() has no from_dict() counterpart, and the rename happened before any release, so no migration path needed - column_weights vs weights naming: This is a naming nit — ClusteringResult.column_weights stores the serialized weight tuple, while ClusterConfig.weights is the user-facing dict. The two serve different roles so the naming difference is intentional * Feature/rewrite/remove timeseriesaggregation.py (#28) * ⏺ All done. Here's a summary of what was changed: 1. Renamed internal cluster-weight variables + alias public API - pipeline/types.py: PipelineResult.cluster_weights → PipelineResult.cluster_counts (type fixed to dict[int, float]) - pipeline/__init__.py: cluster_period_no_occur → cluster_counts (local variable + PipelineResult construction) - api.py: reads result.cluster_counts instead of result.cluster_weights - result.py: primary field renamed to cluster_counts: dict[int, float], added deprecated cluster_weights property emitting FutureWarning 2. Fixed dict[int, int] type → dict[int, float] - Both PipelineResult and AggregationResult now correctly annotate as dict[int, float] since partial-period adjustment makes values fractional 3. Stored column weights in ClusteringResult + replay during apply() - Added column_weights: tuple[tuple[str, float], ...] | None field to ClusteringResult - Populated in _build_clustering_result() from cluster_config.weights - apply() now passes weights=dict(self.column_weights) to ClusterConfig, so transferred results preserve column weighting - Serialization: to_dict() converts to dict, from_dict() converts back to sorted tuple-of-pairs 4. Documented column-weight side effects - Added note to ClusterConfig.weights docstring explaining that weights affect rescaling bounds, not just clustering distance - Added inline comment at rescale.py:79-83 explaining why scale_ub is multiplied by the weight Tests: 633 tests pass (485 + 148 golden), 75 skipped. * Core pipeline (6 files): 1. src/tsam/pipeline/normalize.py — Removed weights parameter from normalize(), removed apply_weights parameter from denormalize(), removed MIN_WEIGHT constant. Normalization now produces purely unweighted data. 2. src/tsam/pipeline/types.py — Removed weights field from NormalizedData dataclass. 3. src/tsam/period_aggregation.py — Added representation_candidates parameter to aggregate_periods(). When provided, representations() uses this array instead of the clustering candidates, allowing representatives to be computed from unweighted data while clustering uses weighted data. 4. src/tsam/pipeline/clustering.py — Added weighted_candidates parameter to cluster_periods() and cluster_sorted_periods(). When provided, weighted data is passed to aggregate_periods() for distance computation while unweighted data is used for representation. 5. src/tsam/pipeline/__init__.py — Added _weight_candidates() helper and _build_weight_vector(). Creates weighted candidates after unstacking, passes them to clustering functions. Updated rescale_representatives() call to pass original_data and normalize_column_means directly. Updated compute_accuracy() call to remove norm_data. 6. src/tsam/pipeline/rescale.py — Changed signature to accept original_data and normalize_column_means directly instead of NormalizedData. Removed the weight compensation scale_ub *= weights[column] block. 7. src/tsam/pipeline/accuracy.py — Removed weight-undo block from reconstruct(), simplified denormalize() call, simplified compute_accuracy() to compare directly without weight division. Tests (3 files): 8. test/test_api_equivalence.py — Changed test_with_weights to compare cluster assignments instead of exact DataFrame equality (medoid selection intentionally differs). 9. test/test_old_new_equivalence.py — Added _SKIP_EQUIVALENCE set and skip_equivalence flag on EquivalenceCase. Tests for representatives, accuracy, and reconstruction skip for weighted configs; cluster assignment test still runs. 10. test/test_golden_regression.py — test_update_golden generates golden from new API for divergent configs. test_old_api_matches_golden skips for divergent configs. 11. test/data/golden/hierarchical_weighted/testdata.csv — Regenerated from new API. Test results 629 passed, 79 skipped, 0 failed (including test_weightingFactors.py which validates the equal-weight invariant on the old API). * Update Changelog * ⏺ Fixed three issues: 1. src/tsam/config.py — ClusterConfig.weights docstring: removed the stale claim that weights affect rescaling bounds. Now correctly states weights apply to a separate copy used only for clustering distance. 2. docs/source/glossary.md — Section 3 "Normalization & Weighting": - Column Weight: updated definition to say "applied to a separate copy", explicitly lists what weights do not affect. - Weighted Profile → Weighted Candidates: renamed to match the actual code variable name, updated definition to say "used exclusively as the distance matrix for clustering". 3. docs/source/glossary.md — Section 7 "Rescaling": - Rescale Upper Bound: removed "column weight" from derivation, now says "derived from mean-normalization settings only". 4. docs/source/glossary.md — Naming conventions and ambiguities resolved: weighted_profiles → weighted_candidates in both places. * Test results: 644 passed, 79 skipped (up from 629 — the 15 new tests in test_weight_decoupling.py all pass) PR review items addressed: - Bug 1 (partial weight dict): Fixed — _build_weight_vector now emits entries for ALL columns, defaulting unlisted to 1.0 - Bug 2 (cluster_sorted_periods ignoring weighted_candidates): Fixed — sorts weighted candidates separately and passes representation_candidates - print() → warnings.warn(): Fixed in _build_weight_vector - _count_occurrences float cast: Fixed int() → float() - Stale 3.1.2 changelog: Removed stale weight-rescaling bullet - Test gaps: All 15 new tests pass (unit tests for helpers, partial weights, duration curves + weights, deprecated property warning, JSON roundtrip) Remaining PR review items not requiring action: - to_dict() backward compat: AggregationResult.to_dict() has no from_dict() counterpart, and the rename happened before any release, so no migration path needed - column_weights vs weights naming: This is a naming nit — ClusteringResult.column_weights stores the serialized weight tuple, while ClusterConfig.weights is the user-facing dict. The two serve different roles so the naming difference is intentional * Add Release notes * Convert TimeSeriesAggregation from monolith to thin wrapper over run_pipeline() Replace ~700 lines of duplicated algorithm code (normalize, cluster, rescale, reconstruct, accuracy) with delegation to the pipeline. Both old and new APIs now share a single code path. - Replace create_typical_periods() body with run_pipeline() call + state extraction - Replace predict_original_data() and accuracy_indicators() with pipeline result reads - Add _translate_representation() and _build_pipeline_args() for parameter translation - Delete _normalize/_unnormalize/_pre_process/_post_process/_add_extreme_periods/ _rescale_cluster_periods/_cluster_sorted_periods internal methods - Keep all __init__ validation, properties, index_matching(), and camelCase aliases - Re-export aggregate_periods and representations for backward compat - Delete test_preprocess.py (tested removed private methods) - Remove normalized_time_series access from test_samemean.py - Clear _SKIP_EQUIVALENCE set (old API now matches new API for weighted configs) - Regenerate hierarchical_weighted golden CSV for weight-decoupled pipeline 647 passed, 75 skipped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Merge Changelog * Merge Changelog --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Improve code quality across pipeline and representation modules - Fix if→elif chain in aggregate_periods() to prevent fallthrough bugs and add else clause for unknown cluster methods - Add type hints to representations.py and period_aggregation.py - Rename 'indice' → 'indices', 'cluster_rep' → 'cluster_representation', 'a' → 'iteration', '_alt_centers' → '_' - Remove stale monolith line-number references from all pipeline modules - DRY up extreme period detection via _detect_extreme() helper - Precompute rep_values in minmax_mean_representation for performance - Tighten return types in clustering.py (list→list[np.ndarray], etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Restructure pipeline: unified weights, ClusteringResult.from_pipeline(), phase decomposition - Add src/tsam/weights.py with validate_weights() consolidating column-existence checks and MIN_WEIGHT clamping; wire into api.py and timeseriesaggregation.py (old wrapper now rejects invalid weight column names) - Move _build_clustering_result(), _extract_segment_data(), _infer_resolution() from pipeline/__init__.py to ClusteringResult classmethods/staticmethods in config.py; refactor apply() to reuse _infer_resolution() - Decompose run_pipeline() into 4 phase functions (_prepare_data, _cluster_and_postprocess, _format_and_reconstruct, _assemble_result) with PreparedData, ClusteringOutput, FormattedOutput intermediate dataclasses - Add TestValidateWeights tests in test_weight_decoupling.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: evalSumPeriods crash: strip extra columns before representations() (#29) Period-sum features (include_period_sums/evalSumPeriods) are appended to the candidate matrix for clustering distance only. They must not reach representations() which expects original time-series columns. Fix: route augmented candidates through weighted_candidates so cluster_periods() uses them for distance but passes unaugmented candidates to representations(). Duration-curve clustering skips period sums entirely (not sortable columns). Also: minor type cast fix in result.py residuals property. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix changelog to note weight decoupling affects both APIs The legacy TimeSeriesAggregation class now uses the same pipeline internally, so the weight semantics change applies to both APIs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: pipeline architecture improvements - Add PipelineConfig dataclass to bundle 13 loose run_pipeline() params - Add tsam.options module for runtime-tunable constants (rescale_max_iterations, rescale_tolerance, min_weight) replacing hardcoded magic numbers - Rename normalize_column_means → scale_by_column_means (old name still works via backward-compat alias with FutureWarning) - Remove implicit column sorting from normalize() — columns now preserve user's original order (old API still sorts explicitly) - Remove NormalizedData.original_data redundancy — original data now stored once in PreparedData - Fix non-deterministic extreme period reassignment in new_cluster method — now picks closest extreme by distance instead of last-wins loop order - Add warning when column weights are silently ignored for duration-curve clustering with include_period_sums=True - Fix type annotations throughout pipeline types (list → list[np.ndarray], dict → dict[str, dict], cluster_order always np.ndarray) - Add @cached_property to result.assignments and result.plot - Fix averaging method duplication in period_aggregation.py - Remove unused _resolve_representation() from config.py - Remove dead _apply_representation_params() from api.py - Export tsam.options in __init__.py - Regenerate golden files for new column order behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add version field to ClusteringResult JSON + document behavioral changes - ClusteringResult.to_dict() now includes a "version" field recording the tsam version that created the clustering. Old JSON files without a version field load with version=None for backward compat. - Document extreme period determinism fix and DC+weights warning in changelog Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf: lazy accuracy computation — eliminates 2x benchmark regression Accuracy indicators (RMSE, MAE, RMSE_duration) are now computed on first access instead of eagerly during pipeline execution. This removes a per-column sklearn loop from the hot path of createTypicalPeriods(), which was the sole cause of a 1.5–2.5x slowdown vs develop on all benchmark configs. PipelineResult.accuracy_indicators and AggregationResult.accuracy are now cached_property fields. The two DataFrames needed for the computation (_norm_values, _normalized_predicted) are stored as private fields and the result is cached after first access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update optimization_input notebook to use new AggregationResult API Replace references to removed `result._aggregation` (old TimeSeriesAggregation object) with the new public API equivalents: - `result.assignments` instead of `result._aggregation.indexMatching()` - `result.cluster_representatives.loc[...]` instead of `clusterPeriodDict` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: eliminate dual candidate arrays, bake weights into candidates early Remove _weight_candidates() and weighted_candidates field. Weights are applied directly to candidates via vectorized np.repeat/multiply. The weight_vector (np.ndarray) is stored on PreparedData for reuse in the step 5 unweighting. Clustering functions no longer receive a separate weighted_candidates parameter — they operate on already-weighted candidates. Weights affect clustering distance, medoid/maxoid selection, and segmentation boundaries. This is the intended design: "solar matters 100× more" means it matters everywhere. Updated comments throughout the pipeline to reflect this decision and removed "remove this for parity" language. Updated ClusterConfig.weights docstring to document all three effects and note that mean/distribution/minmaxmean are weight-invariant. * docs: update weight handling docs for baked-in candidates refactor Replace all references to weighted_candidates, _weight_candidates(), and has_period_sums with the new weight_vector/baked-in pattern across pipeline_guide, pipeline_dataflow, and glossary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: detect extreme periods in weighted space to match develop Move unweighting from before to after add_extreme_periods, so extremes are detected and extracted in weighted space (matching develop's behavior where normalizedPeriodlyProfiles carries weights). All representatives (regular + extreme) are then unweighted uniformly before rescale/denorm. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: move unweighting to step 6b (after extremes), add weight-invariance note Extremes now run in weighted space. Detection (per-column idxmax/idxmin) is weight-invariant; only new_cluster distance reassignment is affected. Unweighting moved from step 5 to step 6b so all representatives (regular + extreme) are unweighted uniformly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: re-apply v4 content to upstream's mkdocs format - Add v3→v4 migration section to migration-guide.md - Add v4.0.0 changelog entry - Rename cluster_weights → cluster_counts in glossary and migration guide - Add pipeline_guide.md and pipeline_dataflow.md under Learn nav - Add pipeline docs to mkdocs.yml navigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: merge pipeline_dataflow into pipeline_guide, update weights references - Remove standalone pipeline_dataflow.md (-574 lines) - Add key dataflow tables as collapsible sections in pipeline_guide.md - Update weights references to reflect top-level parameter (not ClusterConfig) - Remove pipeline_dataflow from mkdocs nav Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remove stale API reference snapshots (auto-generated at build time) gen_ref_pages.py regenerates these from src/ on every mkdocs build. The static files referenced old module names and were missing pipeline/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore v3 weighted duration-curve clustering and fix weight roundtrip test cluster_sorted_periods now sorts and clusters on weighted candidates (matching v3 behaviour) instead of unweighted profiles. Also fix test_json_roundtrip_preserves_weights to use .weights instead of the non-existent .column_weights property. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make ClusterConfig picklable and use to_dict/from_dict in tuning serialization ClusterConfig's custom __setattr__ immutability guard prevented pickle from deserializing instances in worker subprocesses, breaking parallel find_pareto_front. Added __getstate__/__setstate__ to bypass the guard. Also replaced dataclasses.asdict() (which failed since ClusterConfig is not a stdlib dataclass) with .to_dict()/.from_dict() for correct serialization roundtrips, and removed duplicate :param: docstrings that caused griffe warnings during mkdocs build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: hoist weighted profiles into PreparedData and simplify clustering interface Move weighted DataFrame construction into _prepare_data so it's computed once and stored in PreparedData. Strip period-sum columns before calling cluster_periods instead of inside it, and pass n_columns directly to cluster_sorted_periods instead of the full PeriodProfiles object. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove manual Changelog entry --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
@julian-belina I merged my rewrite into here. |
|
Thank you for your work, @FBumann! The general structure looks very promising. However, it's a big change. To improve my confidence in the change, I think I need to create some simple examples in which you can manually verify that the method worked as expected. This will be necessary for the core methods in Fine, at least: distribution, representation, and hierarchy. I'm not sure when I'll have time to build these. Regarding feedback on the API, I will also ask @jo-omoyele and @phil-fzj for their input. |
|
@julian-belina Thanks! I know its a big change. Im very confident due to the golden regression tests we have! There are only minor follow ups needed imo. All being design decisions we need to discuss AFTER you agree on the PR. |
|
@julian-belina I wanted to share: I created a tsam wrapper package that allows to use it with multi-dimensional data using xarray |
|
@FBumann, this looks like a good addition. Perhaps we can integrate it directly into Tsam once V4 is released. |
|
Im not sure if this is a good idea tbh.
I would rather link it prominently. As it matures, maybe we can think about including it directly. |
|
There is no time pressure, but reading from Xarray formats is considerably faster than reading from CSV formats, which would be great for a large energy system model. It's an incentive worth considering, in my opinion. However, I also see the efforts. We can discuss this after the 4.0 release, once I have a better understanding of the new structure. |
If it's about data read and write, switch to .parquet. |
* fix: make LegacyAPIWarning visible by default before v4 removal (#236) (#237) Change LegacyAPIWarning base class from DeprecationWarning to FutureWarning. DeprecationWarning is silenced by default in Python (since 3.2) for library code, so users never saw the deprecation notice. FutureWarning is always shown. Also update all warning messages to explicitly mention removal in v4.0. Closes #236 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: AccuracyMetrics now exposes weighted_rmse, weighted_mae, and weighted_rmse_duration as pre-computed scalars (#238) * feat: add weighted aggregate metrics to AccuracyMetrics (#228) Store pre-computed weighted_rmse, weighted_mae, and weighted_rmse_duration on AccuracyMetrics so callers (including tuning) can read a single scalar instead of computing ad-hoc. Default weight=1 collapses to unweighted mean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add weighted aggregate metrics to AccuracyMetrics (#228) Add three pre-computed scalar fields to AccuracyMetrics: - weighted_rmse: weighted RMS of per-column RMSE - weighted_mae: weighted arithmetic mean of per-column MAE - weighted_rmse_duration: weighted RMS of per-column duration-curve RMSE RMSE metrics use root-mean-square aggregation (preserves variance interpretation), MAE uses plain weighted mean. Default weight=1 for all columns collapses to unweighted aggregation, matching the old totalAccuracyIndicators() values. Tuning now reads result.accuracy.weighted_rmse directly instead of computing ad-hoc via the removed _compute_rmse() helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: add codecov coverage reporting (#243) * CI: add codecov coverage reporting (#242) Run pytest with --cov on a single matrix leg (self-hosted, Python 3.10) to avoid overhead on all runs, then upload the XML report to Codecov. Also adds codecov.yml with project/patch coverage status checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update codecov badge to track develop branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * CI: run pytest only once by conditionally adding coverage flags Use GITHUB_ENV to set --cov flags on the coverage leg, so a single pytest invocation handles both testing and coverage measurement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * CI: migrate to v1 reusable workflows and add coverage reporting (#244) - Migrate all shared workflow references from legacy `_*` workflows to their v1 equivalents from FZJ-IEK3-VSA/.github@v1 (cleaner top-level paths, consolidated installer variants) - Remove inline coverage from ci-develop; move to standalone coverage.yaml that runs pytest --cov on push to develop and uploads to Codecov - Replace inline push_to_jugit steps with the shared sync_jugit.yml reusable workflow - Clean up codecov.yml: remove unused patch status block, raise drop threshold to 5% Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: use GitHub App token in release workflow to trigger publish (#246) The default GITHUB_TOKEN cannot trigger downstream workflows (GitHub's anti-cascade rule). This caused v3.2.1's tag push from release-please to silently not trigger the Publish workflow, so it never reached PyPI. Use actions/create-github-app-token to mint a short-lived token from a GitHub App, which *does* trigger downstream workflows. Requires two repository secrets: - RELEASE_APP_ID - RELEASE_APP_PRIVATE_KEY Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com> * chore(deps): Update github-actions (#251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update dependency numpy to >=1.22.4,<=2.4.4 (#247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com> * chore(deps): Update dependency ruff to <=0.15.8 (#248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * feat: add disaggregate() method (#245) * feat: add disaggregate() to expand typical periods back to full time series Adds `ClusteringResult.disaggregate()` and `AggregationResult.disaggregate()` to map arbitrary typical-period data (e.g. optimization results) back to the original time series length using stored cluster assignments. - Non-segmented: expands periods by cluster order, matches .reconstructed - Segmented: expands segments to timesteps (NaN between segment starts), then expands periods. Users can .ffill() for step-function behavior. - Survives IO: works after to_json()/from_json() roundtrip - Extracts _expand_periods helper, shared with predictOriginalData() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: detect segmented vs timestep input from data, not clustering disaggregate() now accepts both (cluster, timestep) and (cluster, segment, duration) input regardless of whether the clustering used segmentation. Segment expansion only happens when the input actually has segment-level indices (3+ levels). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: strict input validation — segmented data requires segmented clustering and vice versa Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review — level=-1 for MultiIndex columns, per-cluster validation, numpy import - _expand_periods: stack(level=-1) instead of level=1 to correctly handle MultiIndex columns (avoids silent data corruption) - _validate_disaggregate_input: per-cluster segment/timestep count check instead of global nunique - _expand_segments_to_timesteps: dict-based duration lookup by cluster ID instead of positional indexing - Move numpy import to module level - Update AggregationResult.disaggregate docstring to mention segmented input - Add test for MultiIndex columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add edge case coverage for disaggregate - Padded last period (non-divisible length) - Single cluster (n_clusters=1) - Extreme periods (append, new_cluster) - kmeans clustering method - Segmented NaN at correct positions (segment starts have values) - Regression: predictOriginalData still matches via shared _expand_periods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add disaggregation notebook Demonstrates: - Basic disaggregation (matches .reconstructed) - Disaggregating arbitrary data (optimization results) - IO roundtrip (save/load clustering, then disaggregate) - Segmented data with NaN and ffill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: link disaggregation notebook in mkdocs nav Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: stop tracking clustering.json notebook artifact Generated by clustering_transfer.ipynb and disaggregation.ipynb at notebook execution time. Should not be in version control. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com> * ci: build docs in Renovate dependency tests (#253) Adds docs_build_command to the Renovate test job so documentation is also built when Renovate updates dependencies, catching breakage early. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * ci: renovate regex not matching `<` constraints in environment.yml (#254) * fix: renovate regex not matching `<` constraints in environment.yml The custom regex manager only matched `<=` upper bounds, so dependencies using strict `<` (e.g. mkdocs-jupyter `<0.26`) were silently skipped. Widen the regex to `<=?` and normalise mkdocs-jupyter to `<=0.25.1`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use replace strategy for Python version in setup-python The global `rangeStrategy: "widen"` caused Renovate to propose `"3.12 || 3.14"` instead of simply replacing with `"3.14"`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: update Python to 3.14 in docs and publish jobs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * chore(deps): Update dependency mkdocs-jupyter to >=0.25,<0.27 (#249) * chore(deps): Update dependency mkdocs-jupyter to >=0.25,<0.27 * Update pyproject.toml --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com> * chore(master): release 3.3.0 * fix(ci): gracefully skip GitHub release when already created by release-please (#260) The publish workflow's `github-release` job conflicted with release-please, which already creates releases for stable tags. Now the job attempts to create the release and silently skips if it already exists. The job is kept because manual prereleases (e.g. `v3.3.0rc1`) are not handled by release-please — this job provides their changelog via `--generate-notes`. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Julian Belina <56728940+julian-belina@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: fzj-iek3-vsa-release-bot[bot] <272156123+fzj-iek3-vsa-release-bot[bot]@users.noreply.github.com>
# Conflicts: # CHANGELOG.md # src/tsam/api.py # src/tsam/config.py # src/tsam/timeseriesaggregation.py
The v4 pipeline bakes weights into candidates (step 2b), matching the old API's behavior. Weighted configs no longer diverge between old and new APIs, so equivalence tests should run for them too. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add _weights field to AggregationResult - Pass clustering weights through _build_aggregation_result - Lazy accuracy property now uses actual column weights for weighted_rmse, weighted_mae, weighted_rmse_duration - Update test_weight_handling.py from camelCase to snake_case v4 API Refs: #256 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@julian-belina Any updates? |
|
@FBumann, it's on my to-do list, but I'm super busy unfortunately. I hope I can get to it by the end of the week or next week. It's definitely not forgotten! |
|
@julian-belina No worries, just checking back. I wont have time either the next few weeks to tackle those issues. |
|
This looks really promising! |
|
@l-kotzur great to hear! Due to the existing sophisticated testing im also very confident it preserves behaviour entirely! |
Motivation
The v3 functional API (
tsam.aggregate()) was a thin wrapper around the legacyTimeSeriesAggregationclass — all the actual logic still lived in a 1,200-line monolith with deeply entangled state. This made bugs hard to trace, behavior hard to test in isolation, and the codebase hard to contribute to.What this PR does
Architecture: pipeline decomposition
Replaces the monolithic
TimeSeriesAggregationinternals with a stateless pipeline of pure functions insrc/tsam/pipeline/:normalize.pyperiods.pyclustering.pyextremes.pyrescale.pysegment.pyaccuracy.pytypes.pyNormalizedData,PeriodProfiles,PipelineResult)Each function takes explicit inputs and returns explicit outputs — no shared mutable state.
Weight decoupling
In v3, weights were multiplied into the normalized data during preprocessing, requiring every downstream step to compensate. The pipeline now applies weights only to the candidate arrays used for clustering distance, representation selection (medoid/maxoid), and extreme period detection. Normalization, rescaling, reconstruction, and accuracy computation are weight-free.
Numerical impact: Across all 111 golden regression tests, the only change is
kmeans_weighted/testdata.csv— a single value differs by less than 1e-8 (floating-point rounding). All other configurations produce bit-identical results to v3.Performance: lazy accuracy computation
AggregationResult.accuracyis now computed on first access instead of eagerly during aggregation, eliminating unnecessary work when accuracy metrics aren't needed.Code quality: snake_case throughout
All public and internal identifiers renamed from camelCase to snake_case. The legacy
TimeSeriesAggregationclass retains its original parameter names for backward compatibility.Golden regression tests
Extensive golden-file tests lock down numerical output across all major configuration combinations, catching regressions from refactoring.
Breaking changes
cluster_weights→cluster_counts(new API): renamed property (old name emitsFutureWarning).See the v3 to v4 migration guide for details.
Test plan
pytest test/ -q)test_weight_decoupling.pyverifies weights affect only clustering distancetest_old_new_equivalence.pyconfirms legacy API still worksfind_pareto_frontwithn_jobs=-1runs without serialization errors🤖 Generated with Claude Code