flextool 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- flextool-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
"""Main orchestrator for representative periods pre-processing.
|
|
2
|
+
|
|
3
|
+
Reads time series from a FlexTool Spine database, runs greedy convex hull
|
|
4
|
+
clustering, computes convex weights, and writes results back to the database.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import spinedb_api as api
|
|
14
|
+
from spinedb_api import DatabaseMapping, import_data, Map
|
|
15
|
+
|
|
16
|
+
from flextool.engine_polars._db_reader import DictMode, params_to_dict
|
|
17
|
+
from flextool.representative_periods import force_include
|
|
18
|
+
from flextool.representative_periods.clustering import greedy_convex_hull_clustering
|
|
19
|
+
from flextool.representative_periods.netload import (
|
|
20
|
+
build_group_capacities,
|
|
21
|
+
build_netload_matrix,
|
|
22
|
+
demand_match_default_caps,
|
|
23
|
+
)
|
|
24
|
+
from flextool.representative_periods.netload_inputs import read_netload_inputs
|
|
25
|
+
from flextool.representative_periods.weights import compute_weight_matrix
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_time_series(
|
|
29
|
+
db: DatabaseMapping,
|
|
30
|
+
) -> tuple[
|
|
31
|
+
dict[str, list[tuple[str, float]]],
|
|
32
|
+
dict[str, list[tuple[str, float]]],
|
|
33
|
+
dict[str, float],
|
|
34
|
+
]:
|
|
35
|
+
"""Read profile and inflow time series from the database.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of ``(profiles, inflows, demand_scalars)``. ``profiles`` and
|
|
39
|
+
``inflows`` each map entity name to a list of ``(timestep_key, value)``
|
|
40
|
+
pairs (the time-varying series that feed clustering). ``demand_scalars``
|
|
41
|
+
maps node name to its *scalar* (constant) inflow value as a float — the
|
|
42
|
+
constant demand levels dropped from clustering but needed by
|
|
43
|
+
force-include as per-region demand weights ``D_r``.
|
|
44
|
+
|
|
45
|
+
Time-varying series are the ``isinstance(v, list)`` entries.
|
|
46
|
+
``params_to_dict`` returns a scalar float as a *string* (see
|
|
47
|
+
``db_reader.py`` ``params_to_dict`` line ~117), and the downstream
|
|
48
|
+
clustering matrix builder iterates each entry's value as
|
|
49
|
+
``for k, v in ts_data:`` which crashes on the per-character unpack, so
|
|
50
|
+
scalars are excluded from ``inflows``. A constant-inflow node has zero
|
|
51
|
+
variance so it would be skipped by the constant-series filter further down
|
|
52
|
+
anyway; dropping it here makes the skip explicit and matches the existing
|
|
53
|
+
"no matching timesteps" semantics. The scalar inflows are instead collected
|
|
54
|
+
into ``demand_scalars`` (coerced from the string via ``float(...)``, skipping
|
|
55
|
+
any value that does not coerce).
|
|
56
|
+
"""
|
|
57
|
+
raw_profiles = params_to_dict(
|
|
58
|
+
db=db, cl="profile", par="profile", mode=DictMode.DICT
|
|
59
|
+
)
|
|
60
|
+
raw_inflows = params_to_dict(
|
|
61
|
+
db=db, cl="node", par="inflow", mode=DictMode.DICT
|
|
62
|
+
)
|
|
63
|
+
profiles: dict[str, list[tuple[str, float]]] = {
|
|
64
|
+
k: v for k, v in raw_profiles.items() if isinstance(v, list)
|
|
65
|
+
}
|
|
66
|
+
inflows: dict[str, list[tuple[str, float]]] = {
|
|
67
|
+
k: v for k, v in raw_inflows.items() if isinstance(v, list)
|
|
68
|
+
}
|
|
69
|
+
# Collect the dropped scalar inflows as demand levels. A scalar comes back
|
|
70
|
+
# as a string from params_to_dict; coerce and skip anything non-numeric.
|
|
71
|
+
demand_scalars: dict[str, float] = {}
|
|
72
|
+
for k, v in raw_inflows.items():
|
|
73
|
+
if isinstance(v, list):
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
demand_scalars[k] = float(v)
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
continue
|
|
79
|
+
dropped_profiles = len(raw_profiles) - len(profiles)
|
|
80
|
+
dropped_inflows = len(raw_inflows) - len(inflows)
|
|
81
|
+
if dropped_profiles or dropped_inflows:
|
|
82
|
+
print(
|
|
83
|
+
f" Skipping {dropped_profiles} scalar profile(s) and "
|
|
84
|
+
f"{dropped_inflows} scalar inflow(s) — clustering only "
|
|
85
|
+
f"considers time-varying series."
|
|
86
|
+
)
|
|
87
|
+
return profiles, inflows, demand_scalars
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_region_maps(
|
|
91
|
+
db: DatabaseMapping,
|
|
92
|
+
region_groups: list[str],
|
|
93
|
+
demand_scalars: dict[str, float],
|
|
94
|
+
) -> tuple[dict[str, list[str]], dict[str, float], dict[str, list[str]]]:
|
|
95
|
+
"""Build node-group demand-weighting maps for force-include (structural).
|
|
96
|
+
|
|
97
|
+
For each named region group, resolve its member nodes via ``group__node``
|
|
98
|
+
membership, then:
|
|
99
|
+
|
|
100
|
+
* ``region_profiles[r]`` = profile names attached (via ``unit__node__profile``)
|
|
101
|
+
to a node that is a member of region ``r``.
|
|
102
|
+
* ``region_demand[r]`` = ``Σ|demand_scalars[node]|`` over region ``r``'s
|
|
103
|
+
member nodes (the scalar demand already collected by ``_read_time_series``).
|
|
104
|
+
* ``region_nodes[r]`` = the member node names of region ``r`` (used by the
|
|
105
|
+
per-region force-include mode to filter inflow / demand series per region).
|
|
106
|
+
|
|
107
|
+
Purely structural — no name parsing. ``group__node`` ``element_name_list`` is
|
|
108
|
+
``(group, node)`` (dimension order group=0, node=1); ``unit__node__profile``
|
|
109
|
+
is ``(unit, node, profile)`` (node=1, profile=2).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
db: Scenario-filtered database mapping.
|
|
113
|
+
region_groups: Names of the region node-groups to weight by.
|
|
114
|
+
demand_scalars: Node name -> scalar demand level (from ``_read_time_series``).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tuple of ``(region_profiles, region_demand, region_nodes)``.
|
|
118
|
+
"""
|
|
119
|
+
wanted = set(region_groups)
|
|
120
|
+
|
|
121
|
+
# region -> set of member node names
|
|
122
|
+
region_node_sets: dict[str, set[str]] = {r: set() for r in region_groups}
|
|
123
|
+
for item in db.get_entity_items(entity_class_name="group__node"):
|
|
124
|
+
group_name, node_name = item["element_name_list"]
|
|
125
|
+
if group_name in wanted:
|
|
126
|
+
region_node_sets[group_name].add(node_name)
|
|
127
|
+
|
|
128
|
+
# node -> profiles attached to it
|
|
129
|
+
node_profiles: dict[str, list[str]] = {}
|
|
130
|
+
for item in db.get_entity_items(entity_class_name="unit__node__profile"):
|
|
131
|
+
_unit, node_name, profile_name = item["element_name_list"]
|
|
132
|
+
node_profiles.setdefault(node_name, []).append(profile_name)
|
|
133
|
+
|
|
134
|
+
region_profiles: dict[str, list[str]] = {}
|
|
135
|
+
region_demand: dict[str, float] = {}
|
|
136
|
+
region_nodes: dict[str, list[str]] = {}
|
|
137
|
+
for region in region_groups:
|
|
138
|
+
nodes = region_node_sets[region]
|
|
139
|
+
profs: list[str] = []
|
|
140
|
+
for node_name in nodes:
|
|
141
|
+
profs.extend(node_profiles.get(node_name, []))
|
|
142
|
+
region_profiles[region] = profs
|
|
143
|
+
region_nodes[region] = sorted(nodes)
|
|
144
|
+
region_demand[region] = sum(
|
|
145
|
+
abs(demand_scalars[n]) for n in nodes if n in demand_scalars
|
|
146
|
+
)
|
|
147
|
+
print(
|
|
148
|
+
f" Region '{region}': {len(nodes)} member node(s), "
|
|
149
|
+
f"{len(profs)} profile(s), D_r = {region_demand[region]:.3f}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return region_profiles, region_demand, region_nodes
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_timeline_keys(db: DatabaseMapping) -> list[str]:
|
|
156
|
+
"""Determine the main timeline and return ordered timestep keys.
|
|
157
|
+
|
|
158
|
+
Reads all timelines and returns the timestep keys from the first one found.
|
|
159
|
+
"""
|
|
160
|
+
timelines = params_to_dict(
|
|
161
|
+
db=db, cl="timeline", par="timestep_duration", mode=DictMode.DICT
|
|
162
|
+
)
|
|
163
|
+
if not timelines:
|
|
164
|
+
raise ValueError("No timeline found in the database.")
|
|
165
|
+
|
|
166
|
+
# Use the first timeline available
|
|
167
|
+
timeline_name = next(iter(timelines))
|
|
168
|
+
timeline_data = timelines[timeline_name]
|
|
169
|
+
print(f"Using timeline: '{timeline_name}' with {len(timeline_data)} timesteps")
|
|
170
|
+
|
|
171
|
+
# timeline_data is a list of (timestep_key, duration) pairs
|
|
172
|
+
timestep_keys = [entry[0] for entry in timeline_data]
|
|
173
|
+
return timestep_keys
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _build_clustering_matrix(
|
|
177
|
+
profiles: dict[str, list[tuple[str, float]]],
|
|
178
|
+
inflows: dict[str, list[tuple[str, float]]],
|
|
179
|
+
timestep_keys: list[str],
|
|
180
|
+
period_length: int,
|
|
181
|
+
) -> tuple[np.ndarray, int]:
|
|
182
|
+
"""Build the clustering matrix from time series data.
|
|
183
|
+
|
|
184
|
+
Each time series is split into periods, normalized to [0,1],
|
|
185
|
+
and stacked as features.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
profiles: Profile time series (name -> [(key, value), ...]).
|
|
189
|
+
inflows: Inflow time series (name -> [(key, value), ...]).
|
|
190
|
+
timestep_keys: Ordered list of timestep keys from the timeline.
|
|
191
|
+
period_length: Number of timesteps per period.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Tuple of (C, n_base_periods) where C has shape
|
|
195
|
+
(n_features * period_length, n_base_periods).
|
|
196
|
+
"""
|
|
197
|
+
n_total = len(timestep_keys)
|
|
198
|
+
n_base_periods = n_total // period_length
|
|
199
|
+
n_dropped = n_total - n_base_periods * period_length
|
|
200
|
+
|
|
201
|
+
if n_base_periods == 0:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Timeline has {n_total} timesteps but period_length is {period_length}. "
|
|
204
|
+
f"Need at least {period_length} timesteps."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if n_dropped > 0:
|
|
208
|
+
print(f"Warning: Dropping {n_dropped} timesteps from end of timeline")
|
|
209
|
+
|
|
210
|
+
n_used = n_base_periods * period_length
|
|
211
|
+
key_set = set(timestep_keys[:n_used])
|
|
212
|
+
|
|
213
|
+
# Collect all time series into a list of 1D arrays
|
|
214
|
+
all_series: list[np.ndarray] = []
|
|
215
|
+
series_names: list[str] = []
|
|
216
|
+
|
|
217
|
+
combined_ts: dict[str, list[tuple[str, float]]] = {}
|
|
218
|
+
for name, data in profiles.items():
|
|
219
|
+
combined_ts[f"profile:{name}"] = data
|
|
220
|
+
for name, data in inflows.items():
|
|
221
|
+
combined_ts[f"inflow:{name}"] = data
|
|
222
|
+
|
|
223
|
+
for ts_name, ts_data in combined_ts.items():
|
|
224
|
+
# Build a lookup from key to value
|
|
225
|
+
ts_dict = {k: v for k, v in ts_data}
|
|
226
|
+
|
|
227
|
+
# Check if this time series covers the timeline
|
|
228
|
+
available_keys = key_set & ts_dict.keys()
|
|
229
|
+
if len(available_keys) < n_used:
|
|
230
|
+
if len(available_keys) == 0:
|
|
231
|
+
print(f" Skipping '{ts_name}': no matching timesteps")
|
|
232
|
+
continue
|
|
233
|
+
print(
|
|
234
|
+
f" Warning: Skipping '{ts_name}': only {len(available_keys)}/{n_used} "
|
|
235
|
+
f"timesteps available"
|
|
236
|
+
)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Extract values in timeline order
|
|
240
|
+
values = np.array([ts_dict[k] for k in timestep_keys[:n_used]], dtype=np.float64)
|
|
241
|
+
|
|
242
|
+
# Skip constant time series (no variation, adds no information)
|
|
243
|
+
val_min = values.min()
|
|
244
|
+
val_max = values.max()
|
|
245
|
+
if val_max == val_min:
|
|
246
|
+
print(f" Skipping '{ts_name}': constant value ({val_min})")
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Normalize to [0, 1]
|
|
250
|
+
values = (values - val_min) / (val_max - val_min)
|
|
251
|
+
|
|
252
|
+
all_series.append(values)
|
|
253
|
+
series_names.append(ts_name)
|
|
254
|
+
|
|
255
|
+
if not all_series:
|
|
256
|
+
raise ValueError("No valid time series found for clustering.")
|
|
257
|
+
|
|
258
|
+
print(f"Using {len(all_series)} time series features for clustering")
|
|
259
|
+
for name in series_names:
|
|
260
|
+
print(f" - {name}")
|
|
261
|
+
|
|
262
|
+
# Stack features: each base period d has a feature vector of length
|
|
263
|
+
# (n_features * period_length)
|
|
264
|
+
# Reshape each series to (n_base_periods, period_length) then stack
|
|
265
|
+
feature_blocks: list[np.ndarray] = []
|
|
266
|
+
for values in all_series:
|
|
267
|
+
# Reshape to (n_base_periods, period_length)
|
|
268
|
+
reshaped = values[:n_used].reshape(n_base_periods, period_length)
|
|
269
|
+
feature_blocks.append(reshaped)
|
|
270
|
+
|
|
271
|
+
# Stack along feature dimension: (n_base_periods, n_features * period_length)
|
|
272
|
+
feature_matrix = np.hstack(feature_blocks) # (n_base_periods, n_features * period_length)
|
|
273
|
+
|
|
274
|
+
# Transpose to get C of shape (n_features * period_length, n_base_periods)
|
|
275
|
+
C = feature_matrix.T
|
|
276
|
+
|
|
277
|
+
print(
|
|
278
|
+
f"Clustering matrix shape: {C.shape} "
|
|
279
|
+
f"({len(all_series)} features x {period_length} timesteps, {n_base_periods} periods)"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return C, n_base_periods
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_timeset_duration_map(
|
|
286
|
+
rep_indices: list[int],
|
|
287
|
+
timestep_keys: list[str],
|
|
288
|
+
period_length: int,
|
|
289
|
+
) -> Map:
|
|
290
|
+
"""Build a Map of representative period start keys to period durations."""
|
|
291
|
+
keys = [timestep_keys[idx * period_length] for idx in rep_indices]
|
|
292
|
+
values = [float(period_length)] * len(rep_indices)
|
|
293
|
+
return Map(keys, values)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _build_weights_map(
|
|
297
|
+
W: np.ndarray,
|
|
298
|
+
rep_indices: list[int],
|
|
299
|
+
timestep_keys: list[str],
|
|
300
|
+
period_length: int,
|
|
301
|
+
n_base_periods: int,
|
|
302
|
+
) -> Map:
|
|
303
|
+
"""Build a nested Map of representative_period_weights.
|
|
304
|
+
|
|
305
|
+
Outer keys = base period starting timesteps.
|
|
306
|
+
Inner keys = representative period starting timesteps.
|
|
307
|
+
Inner values = weights (only non-zero entries are included).
|
|
308
|
+
|
|
309
|
+
Both levels carry an ``index_name`` (``base_period`` / ``representative_period``)
|
|
310
|
+
so the self-describing-xlsx export labels the two index columns
|
|
311
|
+
meaningfully instead of falling back to the stochastic-sheet defaults
|
|
312
|
+
(``forecast`` / ``branch_time``); the names round-trip with the Map.
|
|
313
|
+
"""
|
|
314
|
+
rep_start_keys = [timestep_keys[idx * period_length] for idx in rep_indices]
|
|
315
|
+
|
|
316
|
+
outer_keys: list[str] = []
|
|
317
|
+
outer_values: list[Map] = []
|
|
318
|
+
|
|
319
|
+
for d in range(n_base_periods):
|
|
320
|
+
base_start_key = timestep_keys[d * period_length]
|
|
321
|
+
|
|
322
|
+
# Only include non-zero weights (sparse)
|
|
323
|
+
inner_keys: list[str] = []
|
|
324
|
+
inner_values: list[float] = []
|
|
325
|
+
for r_idx, rep_key in enumerate(rep_start_keys):
|
|
326
|
+
weight = float(W[d, r_idx])
|
|
327
|
+
if weight > 1e-10:
|
|
328
|
+
inner_keys.append(rep_key)
|
|
329
|
+
inner_values.append(weight)
|
|
330
|
+
|
|
331
|
+
if inner_keys:
|
|
332
|
+
outer_keys.append(base_start_key)
|
|
333
|
+
outer_values.append(
|
|
334
|
+
Map(inner_keys, inner_values, index_name="representative_period")
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return Map(outer_keys, outer_values, index_name="base_period")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _write_results_to_db(
|
|
341
|
+
db_url: str,
|
|
342
|
+
timeset_name: str,
|
|
343
|
+
alternative_name: str,
|
|
344
|
+
timeline_name: str,
|
|
345
|
+
timeset_duration_map: Map,
|
|
346
|
+
weights_map: Map,
|
|
347
|
+
solve_period_timesets: dict,
|
|
348
|
+
alternative_description: str | None = None,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Write clustering results to the database in a new alternative.
|
|
351
|
+
|
|
352
|
+
Also updates each solve's period_timeset to point to the new RP timeset.
|
|
353
|
+
Opens a NEW connection WITHOUT scenario filter. ``alternative_description``
|
|
354
|
+
overrides the alternative's description text; ``None`` (default) keeps the
|
|
355
|
+
byte-parity ``Representative periods: <timeset>`` string.
|
|
356
|
+
"""
|
|
357
|
+
# Clean overwrite (separate connection, committed BEFORE the import below).
|
|
358
|
+
# Spine's ``import_data`` MERGES the indexes of a Map value into any existing
|
|
359
|
+
# value for the same (entity, parameter, alternative) rather than replacing
|
|
360
|
+
# it. Without this purge, re-running the generator into an existing timeset
|
|
361
|
+
# would UNION the fresh representative periods with the stale ones (leaving
|
|
362
|
+
# orphaned periods in ``timeset_duration`` / ``representative_period_weights``).
|
|
363
|
+
# The purge must be its own committed session: a same-session
|
|
364
|
+
# remove-then-import restores the removed item and merges anyway. Scalar
|
|
365
|
+
# params (``timeline``) overwrite cleanly and need no purge.
|
|
366
|
+
with DatabaseMapping(db_url) as db:
|
|
367
|
+
db.fetch_all("parameter_value")
|
|
368
|
+
_purged = False
|
|
369
|
+
for _map_param in ("timeset_duration", "representative_period_weights"):
|
|
370
|
+
try:
|
|
371
|
+
_existing = list(
|
|
372
|
+
db.find_parameter_values(
|
|
373
|
+
entity_class_name="timeset",
|
|
374
|
+
parameter_definition_name=_map_param,
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
except Exception:
|
|
378
|
+
# Parameter definition not present yet (fresh DB) → nothing to purge.
|
|
379
|
+
_existing = []
|
|
380
|
+
for _pv in _existing:
|
|
381
|
+
if (
|
|
382
|
+
_pv["entity_name"] == timeset_name
|
|
383
|
+
and _pv["alternative_name"] == alternative_name
|
|
384
|
+
):
|
|
385
|
+
_pv.remove()
|
|
386
|
+
_purged = True
|
|
387
|
+
if _purged:
|
|
388
|
+
db.commit_session(
|
|
389
|
+
f"Clear stale representative periods before rewrite: {timeset_name}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
with DatabaseMapping(db_url) as db:
|
|
393
|
+
# Ensure parameter definition exists for representative_period_weights
|
|
394
|
+
parameter_definitions = [
|
|
395
|
+
("timeset", "representative_period_weights"),
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
# Create alternative
|
|
399
|
+
description = (
|
|
400
|
+
alternative_description
|
|
401
|
+
if alternative_description
|
|
402
|
+
else f"Representative periods: {timeset_name}"
|
|
403
|
+
)
|
|
404
|
+
alternatives = [(alternative_name, description)]
|
|
405
|
+
|
|
406
|
+
# Create timeset entity
|
|
407
|
+
entities = [("timeset", timeset_name)]
|
|
408
|
+
|
|
409
|
+
# Set parameter values (5-tuple: class, entity, param, value, alternative)
|
|
410
|
+
parameter_values = [
|
|
411
|
+
("timeset", timeset_name, "timeline", timeline_name, alternative_name),
|
|
412
|
+
("timeset", timeset_name, "timeset_duration", timeset_duration_map, alternative_name),
|
|
413
|
+
("timeset", timeset_name, "representative_period_weights", weights_map, alternative_name),
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
# Update each solve's period_timeset to use the new RP timeset
|
|
417
|
+
entity_alternatives = [
|
|
418
|
+
("timeset", timeset_name, alternative_name, True),
|
|
419
|
+
]
|
|
420
|
+
for solve_name, pts_data in solve_period_timesets.items():
|
|
421
|
+
# pts_data is a list of (period, timeset) tuples or a Map
|
|
422
|
+
if isinstance(pts_data, list):
|
|
423
|
+
periods = [entry[0] for entry in pts_data]
|
|
424
|
+
elif isinstance(pts_data, api.Map):
|
|
425
|
+
periods = list(pts_data.indexes)
|
|
426
|
+
else:
|
|
427
|
+
continue
|
|
428
|
+
# Replace all timesets with the new RP timeset
|
|
429
|
+
new_map = Map(
|
|
430
|
+
[str(p) for p in periods],
|
|
431
|
+
[timeset_name] * len(periods),
|
|
432
|
+
)
|
|
433
|
+
parameter_values.append(
|
|
434
|
+
("solve", solve_name, "period_timeset", new_map, alternative_name)
|
|
435
|
+
)
|
|
436
|
+
entity_alternatives.append(
|
|
437
|
+
("solve", solve_name, alternative_name, True)
|
|
438
|
+
)
|
|
439
|
+
print(f" Updated solve '{solve_name}' period_timeset -> '{timeset_name}' for periods {[str(p) for p in periods]}")
|
|
440
|
+
|
|
441
|
+
count, errors = import_data(
|
|
442
|
+
db,
|
|
443
|
+
parameter_definitions=parameter_definitions,
|
|
444
|
+
alternatives=alternatives,
|
|
445
|
+
entities=entities,
|
|
446
|
+
parameter_values=parameter_values,
|
|
447
|
+
entity_alternatives=entity_alternatives,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if errors:
|
|
451
|
+
for err in errors:
|
|
452
|
+
print(f" DB import error: {err}")
|
|
453
|
+
raise RuntimeError(f"Failed to write results: {len(errors)} errors")
|
|
454
|
+
|
|
455
|
+
# Tag the lazily-created parameter so it lands under the
|
|
456
|
+
# solve_advanced group in group-filtered exports. No-op on
|
|
457
|
+
# pre-v44 DBs that don't have parameter_groups yet.
|
|
458
|
+
if db.item(db.mapped_table("parameter_group"), name="solve_advanced") is not None:
|
|
459
|
+
db.add_update_item(
|
|
460
|
+
"parameter_definition",
|
|
461
|
+
entity_class_name="timeset",
|
|
462
|
+
name="representative_period_weights",
|
|
463
|
+
parameter_group_name="solve_advanced",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
db.commit_session(f"Add representative periods: {timeset_name}")
|
|
467
|
+
print(f"Wrote {count} items to database")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def preprocess_representative_periods(
|
|
471
|
+
db_url: str,
|
|
472
|
+
scenario_name: str,
|
|
473
|
+
n_rp: int,
|
|
474
|
+
period_length: int,
|
|
475
|
+
*,
|
|
476
|
+
force_peak_load: bool = False,
|
|
477
|
+
force_highest_net_load: bool = False,
|
|
478
|
+
force_window: int | None = None,
|
|
479
|
+
vg_weight: float = 0.5,
|
|
480
|
+
region_groups: list[str] | None = None,
|
|
481
|
+
force_region_scope: bool = False,
|
|
482
|
+
force_region_budget: int | None = None,
|
|
483
|
+
solves: list[str] | None = None,
|
|
484
|
+
alternative_name: str | None = None,
|
|
485
|
+
alternative_description: str | None = None,
|
|
486
|
+
netload_clustering: bool = False,
|
|
487
|
+
vre_penetration: float = 1.0,
|
|
488
|
+
solved_caps: dict[str, float] | None = None,
|
|
489
|
+
) -> str:
|
|
490
|
+
"""Select representative periods and write results to database.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
db_url: Spine database URL (e.g., 'sqlite:///path.sqlite')
|
|
494
|
+
scenario_name: Name of the scenario to read time series from
|
|
495
|
+
n_rp: Number of representative periods to select
|
|
496
|
+
period_length: Length of each period in timesteps (typically hours)
|
|
497
|
+
force_peak_load: Force-include the peak-net-load base period (Flag A).
|
|
498
|
+
force_highest_net_load: Force-include the sustained-net-load base
|
|
499
|
+
period (Flag B — the energy-adequacy fix).
|
|
500
|
+
force_window: Sub-window length (timesteps) for Flag B; ``None`` means
|
|
501
|
+
the whole-period mean.
|
|
502
|
+
vg_weight: Convex blend weight in [0, 1] on the VG-shortfall term of the
|
|
503
|
+
net-load signal; the inflow-demand term gets ``1 - vg_weight``.
|
|
504
|
+
region_groups: Optional list of node-group names for demand-weighting
|
|
505
|
+
the VG term (``Σ_r D_r · mean_{p∈r}(1 - avail)``). ``None`` (default)
|
|
506
|
+
keeps the unweighted system aggregate — the byte-parity path.
|
|
507
|
+
force_region_scope: Opt in to the per-region budgeted force-include mode
|
|
508
|
+
(requires ``region_groups``). ``False`` (default) keeps the
|
|
509
|
+
single system-coincident signal — the byte-parity path. When ``True``,
|
|
510
|
+
net load is scored independently per region group and the forced set
|
|
511
|
+
is chosen by greedy budgeted coverage of each region's worst lull.
|
|
512
|
+
force_region_budget: Cap on the number of forced periods under region
|
|
513
|
+
scope. ``None`` (default) derives a sane cap from ``n_rp`` as
|
|
514
|
+
``max(1, n_rp // 2)`` so forced periods never dominate the
|
|
515
|
+
representative set; ignored when ``force_region_scope`` is ``False``.
|
|
516
|
+
solves: Optional subset of solve names whose ``period_timeset`` should
|
|
517
|
+
be repointed at the new RP timeset. ``None`` (default) updates every
|
|
518
|
+
solve that carries a ``period_timeset`` param — the current
|
|
519
|
+
behaviour. A non-``None`` list restricts the update (and the
|
|
520
|
+
per-solve print) to exactly those solves; a name not present in the
|
|
521
|
+
database raises ``ValueError``.
|
|
522
|
+
alternative_name: Optional override for the output alternative name. It
|
|
523
|
+
also names the ``timeset`` entity (kept equal, as in the default
|
|
524
|
+
path), so distinct overrides yield distinct alternatives *and*
|
|
525
|
+
distinct timeset entities — the guard against two scenarios sharing
|
|
526
|
+
an ``n_rp``/``period_length`` colliding on the same names. ``None``
|
|
527
|
+
(default) derives both from ``n_rp``/``period_length`` (plus any
|
|
528
|
+
force suffix), byte-identical to the current behaviour.
|
|
529
|
+
alternative_description: Optional override for the alternative's
|
|
530
|
+
description text. ``None`` (default) keeps the byte-parity
|
|
531
|
+
``Representative periods: <timeset>`` string.
|
|
532
|
+
netload_clustering: Opt in to the real-MW net-load clustering matrix
|
|
533
|
+
(``netload.build_netload_matrix`` over demand-match-sized VRE caps)
|
|
534
|
+
instead of the per-series normalized profile/inflow stack. ``False``
|
|
535
|
+
(default) keeps ``_build_clustering_matrix`` — the byte-parity path.
|
|
536
|
+
Single-shot only: iteration-0 demand-match capacities, no solve
|
|
537
|
+
feedback (that is Phase 4). When ``True`` the default output name is
|
|
538
|
+
``netload_{n_rp}rp_{period_length}h`` so it never collides with the
|
|
539
|
+
hull path's ``hull_*`` timeset/alternative.
|
|
540
|
+
vre_penetration: Energy-share target passed to
|
|
541
|
+
``demand_match_default_caps`` when ``netload_clustering`` is ``True``
|
|
542
|
+
(``1.0`` = full-energy VRE match). Ignored on the default path.
|
|
543
|
+
solved_caps: Optional per-investable-VRE-unit TOTAL capacities from a
|
|
544
|
+
prior solve, threaded into ``build_group_capacities`` so the
|
|
545
|
+
net-load signal is built over the *solved* fleet instead of the
|
|
546
|
+
iteration-0 demand-match default. ``None`` (default) reproduces the
|
|
547
|
+
Phase-3 single-shot behaviour byte-for-byte (demand-match caps
|
|
548
|
+
only). Only consulted when ``netload_clustering`` is ``True``; the
|
|
549
|
+
solve-iteration driver (``netload_iterate``) feeds each iteration's
|
|
550
|
+
invested caps back through this argument.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Name of the created timeset entity.
|
|
554
|
+
|
|
555
|
+
With no force flag set (the default) the forced-index set is empty and both
|
|
556
|
+
the selected ``rep_indices`` and the emitted Maps are byte-identical to the
|
|
557
|
+
pure-hull path — this is the opt-in byte-parity contract.
|
|
558
|
+
"""
|
|
559
|
+
# ------------------------------------------------------------------
|
|
560
|
+
# 1. Read from DB with scenario filter
|
|
561
|
+
# ------------------------------------------------------------------
|
|
562
|
+
print(f"Reading time series from database (scenario: '{scenario_name}')...")
|
|
563
|
+
scen_config = api.filters.scenario_filter.scenario_filter_config(scenario_name)
|
|
564
|
+
with DatabaseMapping(db_url) as db:
|
|
565
|
+
api.filters.scenario_filter.scenario_filter_from_dict(db, scen_config)
|
|
566
|
+
db.fetch_all("parameter_value")
|
|
567
|
+
|
|
568
|
+
profiles, inflows, demand_scalars = _read_time_series(db)
|
|
569
|
+
print(
|
|
570
|
+
f" Found {len(profiles)} profiles, {len(inflows)} node inflows, "
|
|
571
|
+
f"{len(demand_scalars)} scalar demand level(s)"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
# 2. Determine timeline
|
|
576
|
+
# ------------------------------------------------------------------
|
|
577
|
+
timestep_keys = _get_timeline_keys(db)
|
|
578
|
+
|
|
579
|
+
# Also read the timeline name for later use
|
|
580
|
+
timelines = params_to_dict(
|
|
581
|
+
db=db, cl="timeline", par="timestep_duration", mode=DictMode.DICT
|
|
582
|
+
)
|
|
583
|
+
timeline_name = next(iter(timelines))
|
|
584
|
+
|
|
585
|
+
# Read solve period_timeset mappings so we can update them
|
|
586
|
+
solve_period_timesets: dict = params_to_dict(
|
|
587
|
+
db=db, cl="solve", par="period_timeset", mode=DictMode.DICT
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Optionally restrict the update to a named subset of solves. A
|
|
591
|
+
# requested solve that has no period_timeset in the DB is a user error
|
|
592
|
+
# (typo / wrong scenario) — fail loudly rather than silently no-op.
|
|
593
|
+
if solves is not None:
|
|
594
|
+
missing = [s for s in solves if s not in solve_period_timesets]
|
|
595
|
+
if missing:
|
|
596
|
+
raise ValueError(
|
|
597
|
+
f"--solves named solve(s) with no period_timeset in the "
|
|
598
|
+
f"database: {missing}. Available solves: "
|
|
599
|
+
f"{sorted(solve_period_timesets)}."
|
|
600
|
+
)
|
|
601
|
+
solve_period_timesets = {
|
|
602
|
+
s: solve_period_timesets[s] for s in solves
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
# Node-group demand-weighting / per-region maps (only when region
|
|
606
|
+
# groups requested).
|
|
607
|
+
region_profiles: dict[str, list[str]] | None = None
|
|
608
|
+
region_demand: dict[str, float] | None = None
|
|
609
|
+
region_nodes: dict[str, list[str]] | None = None
|
|
610
|
+
if region_groups:
|
|
611
|
+
region_profiles, region_demand, region_nodes = _read_region_maps(
|
|
612
|
+
db, region_groups, demand_scalars
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Net-load inputs are read INSIDE the scenario filter so the builder
|
|
616
|
+
# sees scenario-specific existing caps / invest_method / group flag —
|
|
617
|
+
# the same filtered mapping the profile/inflow reads use above. The
|
|
618
|
+
# returned NetloadInputs dataclass captures the data, so it survives
|
|
619
|
+
# after the mapping closes.
|
|
620
|
+
netload_inputs = read_netload_inputs(db) if netload_clustering else None
|
|
621
|
+
|
|
622
|
+
# ------------------------------------------------------------------
|
|
623
|
+
# 3. Build clustering matrix
|
|
624
|
+
# ------------------------------------------------------------------
|
|
625
|
+
print("Building clustering matrix...")
|
|
626
|
+
if netload_clustering:
|
|
627
|
+
# Net-load mode: size the investable VRE fleet by the iteration-0
|
|
628
|
+
# demand-match default UNLESS the caller feeds back a prior solve's
|
|
629
|
+
# capacities via ``solved_caps`` (the solve-iteration driver), then
|
|
630
|
+
# build the real-MW net-load matrix. Same C shape
|
|
631
|
+
# (n_features · period_length, n_base_periods) the hull path emits, so
|
|
632
|
+
# everything downstream (clustering, force-include seeding, weights,
|
|
633
|
+
# DB write) is unchanged.
|
|
634
|
+
default_caps = demand_match_default_caps(
|
|
635
|
+
netload_inputs, timestep_keys, vre_penetration
|
|
636
|
+
)
|
|
637
|
+
caps = build_group_capacities(
|
|
638
|
+
netload_inputs, default_caps, solved_caps=solved_caps
|
|
639
|
+
)
|
|
640
|
+
C, n_base_periods, _agg_names = build_netload_matrix(
|
|
641
|
+
netload_inputs, caps, timestep_keys, period_length
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
C, n_base_periods = _build_clustering_matrix(
|
|
645
|
+
profiles, inflows, timestep_keys, period_length
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# ------------------------------------------------------------------
|
|
649
|
+
# 4. Force-include adequacy-critical base periods (opt-in) — computed
|
|
650
|
+
# BEFORE clustering so the greedy hull can be SEEDED with them.
|
|
651
|
+
# ------------------------------------------------------------------
|
|
652
|
+
# Under region scope, resolve the None budget to an n_rp-derived cap so
|
|
653
|
+
# forced periods never dominate the representative set (documented default).
|
|
654
|
+
effective_region_budget = force_region_budget
|
|
655
|
+
if force_region_scope and effective_region_budget is None:
|
|
656
|
+
effective_region_budget = max(1, n_rp // 2)
|
|
657
|
+
# Force-include composes independently of the clustering matrix: it operates
|
|
658
|
+
# on the same profiles/inflows/demand_scalars, so it runs unchanged in
|
|
659
|
+
# netload mode. NOTE: force-include's net-load definition here
|
|
660
|
+
# (``force_include.build_netload_hourly``, a capacity-UNWEIGHTED
|
|
661
|
+
# ``1 − mean(avail)`` VG signal) DIFFERS from the capacity-weighted real-MW
|
|
662
|
+
# net load the netload clustering matrix uses. Unifying the two into one
|
|
663
|
+
# signal is a deliberate open question left to a later phase; for now each
|
|
664
|
+
# keeps its own definition on purpose.
|
|
665
|
+
forced_indices = force_include.compute_forced_indices(
|
|
666
|
+
profiles,
|
|
667
|
+
inflows,
|
|
668
|
+
demand_scalars,
|
|
669
|
+
timestep_keys,
|
|
670
|
+
period_length,
|
|
671
|
+
n_base_periods,
|
|
672
|
+
force_peak_load=force_peak_load,
|
|
673
|
+
force_highest_net_load=force_highest_net_load,
|
|
674
|
+
force_window=force_window,
|
|
675
|
+
vg_weight=vg_weight,
|
|
676
|
+
region_profiles=region_profiles,
|
|
677
|
+
region_demand=region_demand,
|
|
678
|
+
force_region_scope=force_region_scope,
|
|
679
|
+
force_region_budget=effective_region_budget,
|
|
680
|
+
region_nodes=region_nodes,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# ------------------------------------------------------------------
|
|
684
|
+
# 4b. Run clustering (seeded by the forced periods when forcing is active)
|
|
685
|
+
# ------------------------------------------------------------------
|
|
686
|
+
# The pure (unseeded) hull is always computed: with no force flag it IS the
|
|
687
|
+
# representative set (byte-parity default path); when forcing is active it is
|
|
688
|
+
# only the naming baseline (how many forced periods displaced a clustered
|
|
689
|
+
# pick). Empty forced set → rep_indices == pure_hull → byte-parity default.
|
|
690
|
+
print(
|
|
691
|
+
f"Running greedy convex hull clustering (selecting {n_rp} from "
|
|
692
|
+
f"{n_base_periods} periods)..."
|
|
693
|
+
)
|
|
694
|
+
pure_hull = sorted(greedy_convex_hull_clustering(C, n_rp))
|
|
695
|
+
print(f"Selected representative period indices: {pure_hull}")
|
|
696
|
+
|
|
697
|
+
if not forced_indices:
|
|
698
|
+
rep_indices = pure_hull
|
|
699
|
+
n_forced = 0
|
|
700
|
+
else:
|
|
701
|
+
# Seed the greedy hull with the forced periods so the remaining
|
|
702
|
+
# (n_rp - |forced|) picks are chosen to COMPLEMENT them — the clustering
|
|
703
|
+
# never spends budget on a period a forced extreme already represents.
|
|
704
|
+
# The total stays pinned at n_rp: forced periods consume part of the
|
|
705
|
+
# budget, they never grow it beyond n_rp.
|
|
706
|
+
rep_indices = sorted(
|
|
707
|
+
greedy_convex_hull_clustering(C, n_rp, seed_indices=forced_indices)
|
|
708
|
+
)
|
|
709
|
+
# Suffix count = forced periods that displaced a pure-hull pick (i.e.
|
|
710
|
+
# were not already selected by the unseeded clustering). Zero leaves the
|
|
711
|
+
# pure-hull name untouched, preserving the default-path name.
|
|
712
|
+
n_forced = len(set(forced_indices) - set(pure_hull))
|
|
713
|
+
|
|
714
|
+
if forced_indices:
|
|
715
|
+
forced_start_keys = [
|
|
716
|
+
timestep_keys[idx * period_length] for idx in forced_indices
|
|
717
|
+
]
|
|
718
|
+
print(
|
|
719
|
+
f"Force-include (seeded): forced base periods "
|
|
720
|
+
f"{forced_indices} (starts {forced_start_keys}); "
|
|
721
|
+
f"{n_forced} displaced a clustered pick."
|
|
722
|
+
)
|
|
723
|
+
print(f"Final representative period indices: {rep_indices}")
|
|
724
|
+
|
|
725
|
+
# ------------------------------------------------------------------
|
|
726
|
+
# 5. Compute weights
|
|
727
|
+
# ------------------------------------------------------------------
|
|
728
|
+
print("Computing convex weights...")
|
|
729
|
+
W = compute_weight_matrix(C, rep_indices)
|
|
730
|
+
|
|
731
|
+
# Compute projection errors for summary
|
|
732
|
+
R = C[:, rep_indices]
|
|
733
|
+
errors = np.array([
|
|
734
|
+
np.linalg.norm(R @ W[d, :] - C[:, d]) for d in range(n_base_periods)
|
|
735
|
+
])
|
|
736
|
+
mean_error = float(errors.mean())
|
|
737
|
+
max_error = float(errors.max())
|
|
738
|
+
|
|
739
|
+
# ------------------------------------------------------------------
|
|
740
|
+
# 6. Build output
|
|
741
|
+
# ------------------------------------------------------------------
|
|
742
|
+
# Naming rule: base is the pure-hull name ``hull_{n_rp}rp_{PL}h``. Append a
|
|
743
|
+
# ``+f{n_forced}`` suffix whenever forced periods displaced a clustered pick
|
|
744
|
+
# (``n_forced > 0`` = forced periods the unseeded hull would not have
|
|
745
|
+
# selected). When ``n_forced == 0`` (default, or a forced index that was
|
|
746
|
+
# already a hull pick) the name is unchanged, preserving byte-parity of the
|
|
747
|
+
# default path and not overwriting the pure-hull timeset.
|
|
748
|
+
suffix = f"+f{n_forced}" if n_forced > 0 else ""
|
|
749
|
+
# Net-load mode gets a distinct ``netload_*`` prefix so its timeset /
|
|
750
|
+
# alternative never collides with the hull path's ``hull_*`` name.
|
|
751
|
+
name_prefix = "netload" if netload_clustering else "hull"
|
|
752
|
+
default_name = f"{name_prefix}_{n_rp}rp_{period_length}h{suffix}"
|
|
753
|
+
if alternative_name is None:
|
|
754
|
+
# Byte-parity default: derive both names from n_rp/period_length.
|
|
755
|
+
alternative_name = default_name
|
|
756
|
+
# Tie the timeset ENTITY name to the alternative so distinct callers never
|
|
757
|
+
# collide on the entity either (the entity is not alternative-scoped).
|
|
758
|
+
timeset_name = alternative_name
|
|
759
|
+
|
|
760
|
+
timeset_duration_map = _build_timeset_duration_map(
|
|
761
|
+
rep_indices, timestep_keys, period_length
|
|
762
|
+
)
|
|
763
|
+
weights_map = _build_weights_map(
|
|
764
|
+
W, rep_indices, timestep_keys, period_length, n_base_periods
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# ------------------------------------------------------------------
|
|
768
|
+
# 7. Write to DB (new connection, no scenario filter)
|
|
769
|
+
# ------------------------------------------------------------------
|
|
770
|
+
print("Writing results to database...")
|
|
771
|
+
_write_results_to_db(
|
|
772
|
+
db_url,
|
|
773
|
+
timeset_name,
|
|
774
|
+
alternative_name,
|
|
775
|
+
timeline_name,
|
|
776
|
+
timeset_duration_map,
|
|
777
|
+
weights_map,
|
|
778
|
+
solve_period_timesets,
|
|
779
|
+
alternative_description=alternative_description,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# ------------------------------------------------------------------
|
|
783
|
+
# 8. Print summary
|
|
784
|
+
# ------------------------------------------------------------------
|
|
785
|
+
print("\n--- Representative Periods Summary ---")
|
|
786
|
+
print(f" Representative periods selected: {n_rp}")
|
|
787
|
+
print(f" Mean projection error: {mean_error:.6f}")
|
|
788
|
+
print(f" Max projection error: {max_error:.6f}")
|
|
789
|
+
print(f" Timeset name: '{timeset_name}'")
|
|
790
|
+
print(f" Alternative name: '{alternative_name}'")
|
|
791
|
+
|
|
792
|
+
rep_start_keys = [timestep_keys[idx * period_length] for idx in rep_indices]
|
|
793
|
+
print(f" Representative period starts: {rep_start_keys}")
|
|
794
|
+
|
|
795
|
+
return timeset_name
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def main() -> None:
|
|
799
|
+
"""CLI entry point for representative periods preprocessing."""
|
|
800
|
+
parser = argparse.ArgumentParser(
|
|
801
|
+
description="Select representative periods using greedy convex hull clustering "
|
|
802
|
+
"and write results to a FlexTool Spine database.",
|
|
803
|
+
)
|
|
804
|
+
parser.add_argument(
|
|
805
|
+
"db_url",
|
|
806
|
+
help="Spine database URL (e.g., 'sqlite:///path/to/db.sqlite')",
|
|
807
|
+
)
|
|
808
|
+
parser.add_argument(
|
|
809
|
+
"scenario",
|
|
810
|
+
help="Name of the scenario to read time series from",
|
|
811
|
+
)
|
|
812
|
+
parser.add_argument(
|
|
813
|
+
"n_rp",
|
|
814
|
+
type=int,
|
|
815
|
+
help="Number of representative periods to select",
|
|
816
|
+
)
|
|
817
|
+
parser.add_argument(
|
|
818
|
+
"period_length",
|
|
819
|
+
type=int,
|
|
820
|
+
help="Length of each period in timesteps (e.g., 24 for daily, 168 for weekly)",
|
|
821
|
+
)
|
|
822
|
+
parser.add_argument(
|
|
823
|
+
"--force-peak-load",
|
|
824
|
+
action="store_true",
|
|
825
|
+
help="Force-include the base period with the highest instantaneous "
|
|
826
|
+
"net load (Flag A, capacity-adequacy).",
|
|
827
|
+
)
|
|
828
|
+
parser.add_argument(
|
|
829
|
+
"--force-highest-net-load",
|
|
830
|
+
action="store_true",
|
|
831
|
+
help="Force-include the base period with the greatest sustained net "
|
|
832
|
+
"load (Flag B, energy-adequacy — the multi-carrier fix).",
|
|
833
|
+
)
|
|
834
|
+
parser.add_argument(
|
|
835
|
+
"--force-window",
|
|
836
|
+
type=int,
|
|
837
|
+
default=None,
|
|
838
|
+
help="Sub-window length in timesteps for the sustained net-load score "
|
|
839
|
+
"(default: whole period).",
|
|
840
|
+
)
|
|
841
|
+
parser.add_argument(
|
|
842
|
+
"--vg-weight",
|
|
843
|
+
type=float,
|
|
844
|
+
default=0.5,
|
|
845
|
+
help="Convex blend weight in [0,1] on the VG-shortfall term of the "
|
|
846
|
+
"net-load signal; the inflow-demand term gets 1 - vg_weight (default: 0.5).",
|
|
847
|
+
)
|
|
848
|
+
parser.add_argument(
|
|
849
|
+
"--region-groups",
|
|
850
|
+
type=str,
|
|
851
|
+
default=None,
|
|
852
|
+
help="Comma-separated node-group names to demand-weight the net-load "
|
|
853
|
+
"VG term (e.g. 'decomp_AUS,decomp_JAP,decomp_KOR'). Omitted → unweighted "
|
|
854
|
+
"system aggregate (byte-parity default).",
|
|
855
|
+
)
|
|
856
|
+
parser.add_argument(
|
|
857
|
+
"--force-region-scope",
|
|
858
|
+
action="store_true",
|
|
859
|
+
help="Score net load PER region group and greedily force each region's "
|
|
860
|
+
"worst lull under a budget cap, instead of forcing the single "
|
|
861
|
+
"system-coincident worst period. Requires --region-groups.",
|
|
862
|
+
)
|
|
863
|
+
parser.add_argument(
|
|
864
|
+
"--force-region-budget",
|
|
865
|
+
type=int,
|
|
866
|
+
default=None,
|
|
867
|
+
help="Max forced periods under --force-region-scope (default: derived "
|
|
868
|
+
"from n_rp as max(1, n_rp // 2)).",
|
|
869
|
+
)
|
|
870
|
+
parser.add_argument(
|
|
871
|
+
"--solves",
|
|
872
|
+
type=str,
|
|
873
|
+
default=None,
|
|
874
|
+
help="Comma-separated solve names whose period_timeset should point at "
|
|
875
|
+
"the new RP timeset (e.g. 'invest_solve'). Omitted → update every solve "
|
|
876
|
+
"with a period_timeset. A name absent from the DB is an error.",
|
|
877
|
+
)
|
|
878
|
+
parser.add_argument(
|
|
879
|
+
"--alternative-name",
|
|
880
|
+
type=str,
|
|
881
|
+
default=None,
|
|
882
|
+
help="Override the output alternative name (also names the timeset "
|
|
883
|
+
"entity). Give distinct scenarios distinct names so they don't collide "
|
|
884
|
+
"on the alternative or the timeset entity. Omitted → derive from "
|
|
885
|
+
"n_rp/period_length (plus any force suffix).",
|
|
886
|
+
)
|
|
887
|
+
parser.add_argument(
|
|
888
|
+
"--alternative-description",
|
|
889
|
+
type=str,
|
|
890
|
+
default=None,
|
|
891
|
+
help="Override the created alternative's description text. Omitted → "
|
|
892
|
+
"'Representative periods: <timeset>'.",
|
|
893
|
+
)
|
|
894
|
+
parser.add_argument(
|
|
895
|
+
"--netload-clustering",
|
|
896
|
+
action="store_true",
|
|
897
|
+
help="Cluster on the real-MW net-load signal (demand − VRE·avail over "
|
|
898
|
+
"demand-match-sized caps) instead of the per-series normalized "
|
|
899
|
+
"profile/inflow stack. Output name defaults to netload_{n_rp}rp_{PL}h. "
|
|
900
|
+
"Single-shot (iteration-0 caps, no solve feedback).",
|
|
901
|
+
)
|
|
902
|
+
parser.add_argument(
|
|
903
|
+
"--vre-penetration",
|
|
904
|
+
type=float,
|
|
905
|
+
default=1.0,
|
|
906
|
+
help="Energy-share target for the demand-match VRE sizing under "
|
|
907
|
+
"--netload-clustering (1.0 = full-energy match; default 1.0). Ignored "
|
|
908
|
+
"without --netload-clustering.",
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
args = parser.parse_args()
|
|
912
|
+
region_groups = (
|
|
913
|
+
[g.strip() for g in args.region_groups.split(",") if g.strip()]
|
|
914
|
+
if args.region_groups
|
|
915
|
+
else None
|
|
916
|
+
)
|
|
917
|
+
solves = (
|
|
918
|
+
[s.strip() for s in args.solves.split(",") if s.strip()]
|
|
919
|
+
if args.solves
|
|
920
|
+
else None
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
try:
|
|
924
|
+
preprocess_representative_periods(
|
|
925
|
+
db_url=args.db_url,
|
|
926
|
+
scenario_name=args.scenario,
|
|
927
|
+
n_rp=args.n_rp,
|
|
928
|
+
period_length=args.period_length,
|
|
929
|
+
force_peak_load=args.force_peak_load,
|
|
930
|
+
force_highest_net_load=args.force_highest_net_load,
|
|
931
|
+
force_window=args.force_window,
|
|
932
|
+
vg_weight=args.vg_weight,
|
|
933
|
+
region_groups=region_groups,
|
|
934
|
+
force_region_scope=args.force_region_scope,
|
|
935
|
+
force_region_budget=args.force_region_budget,
|
|
936
|
+
solves=solves,
|
|
937
|
+
alternative_name=args.alternative_name,
|
|
938
|
+
alternative_description=args.alternative_description,
|
|
939
|
+
netload_clustering=args.netload_clustering,
|
|
940
|
+
vre_penetration=args.vre_penetration,
|
|
941
|
+
)
|
|
942
|
+
except Exception as e:
|
|
943
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
944
|
+
sys.exit(1)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
if __name__ == "__main__":
|
|
948
|
+
main()
|