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,365 @@
|
|
|
1
|
+
"""Pure net-load clustering-matrix math (no database access).
|
|
2
|
+
|
|
3
|
+
Every function here operates on a :class:`~flextool.representative_periods.
|
|
4
|
+
netload_inputs.NetloadInputs` value (built by ``netload_inputs.read_netload_inputs``)
|
|
5
|
+
plus caller-supplied ``timestep_keys`` — mirroring the pure caller-passes-data
|
|
6
|
+
split used by :mod:`flextool.representative_periods.force_include`. This keeps
|
|
7
|
+
each step independently unit-testable.
|
|
8
|
+
|
|
9
|
+
The signal is a real-MW **net load** per aggregation unit ``g``:
|
|
10
|
+
|
|
11
|
+
``net_load[g, h] = Σ_{demand node in g} demand_h
|
|
12
|
+
− Σ_{VRE unit in g} cap[u] · avail[profile(u)][h]``
|
|
13
|
+
|
|
14
|
+
with ``demand_h = −inflow_h (time-varying) + |scalar level|`` (positive-demand
|
|
15
|
+
convention — a demand node's inflow is negative in FlexTool, so ``−inflow_h`` is
|
|
16
|
+
positive demand and a scalar demand level enters as ``+|value|``; this matches
|
|
17
|
+
``force_include.build_netload_hourly``). Each aggregation unit's series is then
|
|
18
|
+
min-max normalized to ``[0, 1]`` exactly as
|
|
19
|
+
``preprocess._build_clustering_matrix`` does per feature.
|
|
20
|
+
|
|
21
|
+
The iteration-0 VRE capacities come from a **demand-match** default: size the
|
|
22
|
+
investable VRE of each aggregation unit so its energy plus the existing VRE
|
|
23
|
+
energy just covers the unit's demand energy (pure energy balance, curtailment
|
|
24
|
+
ignored).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from flextool.representative_periods.netload_inputs import NetloadInputs
|
|
32
|
+
|
|
33
|
+
# Series / availability whose scale is below this are treated as carrying no
|
|
34
|
+
# usable capacity-per-energy information (avoids dividing by a near-zero mean
|
|
35
|
+
# availability).
|
|
36
|
+
_EPS = 1e-12
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _series_lookup(series: list[tuple[str, float]]) -> dict[str, float]:
|
|
40
|
+
"""A ``{timestep_key: value}`` lookup from a ``[(key, value), ...]`` list."""
|
|
41
|
+
return {k: float(v) for k, v in series}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _covers(lookup: dict[str, float], timestep_keys: list[str]) -> bool:
|
|
45
|
+
"""Whether ``lookup`` has a value at *every* key in ``timestep_keys``.
|
|
46
|
+
|
|
47
|
+
Matches the partial-coverage policy of ``preprocess._build_clustering_matrix``
|
|
48
|
+
and ``force_include._series_matrix``: a series that does not cover every used
|
|
49
|
+
timestep is skipped by the caller (never zero-filled).
|
|
50
|
+
"""
|
|
51
|
+
return all(k in lookup for k in timestep_keys)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _duration_array(
|
|
55
|
+
step_durations: dict[str, float], timestep_keys: list[str]
|
|
56
|
+
) -> np.ndarray:
|
|
57
|
+
"""Duration per key over ``timestep_keys`` (missing key → duration 1.0)."""
|
|
58
|
+
return np.array(
|
|
59
|
+
[step_durations.get(k, 1.0) for k in timestep_keys], dtype=np.float64
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def demand_match_default_caps(
|
|
64
|
+
inputs: NetloadInputs,
|
|
65
|
+
timestep_keys: list[str],
|
|
66
|
+
vre_penetration: float = 1.0,
|
|
67
|
+
) -> dict[str, float]:
|
|
68
|
+
"""Iteration-0 demand-match capacities for every investable VRE unit.
|
|
69
|
+
|
|
70
|
+
For each aggregation unit ``g`` (``inputs.units_by_group``):
|
|
71
|
+
|
|
72
|
+
* ``E_demand = Σ_{node in g}( |scalar| + Σ_h |min(inflow_h, 0)| · dur_h )`` —
|
|
73
|
+
the scalar demand level (magnitude) plus the DURATION-WEIGHTED energy of the
|
|
74
|
+
time-varying *demand* (negative-inflow) part. A time-varying inflow series
|
|
75
|
+
that does not cover every key in ``timestep_keys`` is SKIPPED with a warning
|
|
76
|
+
(never zero-filled), matching ``preprocess._build_clustering_matrix``.
|
|
77
|
+
* ``E_existing_VRE = Σ_{VRE unit in g} existing_cap · Σ_h avail_h · dur_h``
|
|
78
|
+
over ALL of ``g``'s VRE units (investable and not) — the true
|
|
79
|
+
duration-weighted profiled energy, correct for UNEQUAL step durations. A VRE
|
|
80
|
+
unit whose availability profile does not cover every used timestep is
|
|
81
|
+
SKIPPED with a warning (its contribution is not subtracted).
|
|
82
|
+
* ``target = max(0, vre_penetration · E_demand − E_existing_VRE)`` — the
|
|
83
|
+
(scaled) demand energy the existing VRE fleet does not already cover.
|
|
84
|
+
``vre_penetration`` (default ``1.0`` → 100% energy match) scales the demand
|
|
85
|
+
target: e.g. ``0.5`` sizes the investable fleet to a half-energy VRE share.
|
|
86
|
+
* ``target`` is split as EQUAL ENERGY SHARES across ``g``'s ``k`` investable
|
|
87
|
+
VRE units that have a usable profile energy
|
|
88
|
+
(``w_u = Σ_h avail_h · dur_h ≥ eps``). Each such unit's invested capacity is
|
|
89
|
+
``(target/k) / w_u``, i.e. the capacity whose profiled energy
|
|
90
|
+
``cap · Σ_h avail_h · dur_h`` equals its ``target/k`` share EXACTLY.
|
|
91
|
+
|
|
92
|
+
Contract of the returned dict (documented, and pinned by the capacity-contract
|
|
93
|
+
test):
|
|
94
|
+
|
|
95
|
+
* Keys are **investable VRE units only**; existing-only (non-investable)
|
|
96
|
+
units are absent (their capacity is their existing cap, applied by
|
|
97
|
+
:func:`build_group_capacities`).
|
|
98
|
+
* Each value is the unit's **TOTAL** iteration-0 capacity =
|
|
99
|
+
``existing_cap + invested_share``. Because ``target`` already subtracts the
|
|
100
|
+
existing VRE energy, adding it back per unit keeps the group's total VRE
|
|
101
|
+
energy equal to ``vre_penetration · E_demand`` (when ``target > 0`` and
|
|
102
|
+
every investable unit is usable) — a clean, energy-consistent total the
|
|
103
|
+
net-load builder can multiply by availability directly. The invested VRE
|
|
104
|
+
energy ``Σ_u invested_u · w_u`` equals ``target`` to machine precision, for
|
|
105
|
+
equal AND unequal step durations alike.
|
|
106
|
+
* An investable unit with ``w_u < eps`` (no usable profile), or a group whose
|
|
107
|
+
``target`` is 0 or has no usable investable unit, gets its ``existing_cap``
|
|
108
|
+
(no useful invest possible) — never a divide-by-near-zero blow-up.
|
|
109
|
+
|
|
110
|
+
Curtailment is ignored (pure energy balance).
|
|
111
|
+
"""
|
|
112
|
+
dur = _duration_array(inputs.step_durations, timestep_keys)
|
|
113
|
+
caps: dict[str, float] = {}
|
|
114
|
+
|
|
115
|
+
for group in sorted(inputs.units_by_group):
|
|
116
|
+
node_set = set(inputs.units_by_group[group])
|
|
117
|
+
|
|
118
|
+
# Demand energy of the group (duration-weighted).
|
|
119
|
+
e_demand = 0.0
|
|
120
|
+
for node in sorted(node_set):
|
|
121
|
+
e_demand += abs(inputs.demand_scalar.get(node, 0.0))
|
|
122
|
+
ts = inputs.demand_ts.get(node)
|
|
123
|
+
if not ts:
|
|
124
|
+
continue
|
|
125
|
+
lookup = _series_lookup(ts)
|
|
126
|
+
if not _covers(lookup, timestep_keys):
|
|
127
|
+
print(
|
|
128
|
+
f" Net-load: skipping inflow of node '{node}' "
|
|
129
|
+
f"(demand-match): does not cover all used timesteps."
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
inflow = np.array(
|
|
133
|
+
[lookup[k] for k in timestep_keys], dtype=np.float64
|
|
134
|
+
)
|
|
135
|
+
demand_part = np.where(inflow < 0.0, -inflow, 0.0)
|
|
136
|
+
e_demand += float(np.dot(demand_part, dur))
|
|
137
|
+
|
|
138
|
+
# VRE units of the group, with their duration-weighted profile energy.
|
|
139
|
+
vre_units = sorted(
|
|
140
|
+
u for u, vu in inputs.vre.items() if vu.node in node_set
|
|
141
|
+
)
|
|
142
|
+
weight: dict[str, float] = {}
|
|
143
|
+
e_existing = 0.0
|
|
144
|
+
for u in vre_units:
|
|
145
|
+
vu = inputs.vre[u]
|
|
146
|
+
lookup = _series_lookup(inputs.profiles.get(vu.profile, []))
|
|
147
|
+
if not _covers(lookup, timestep_keys):
|
|
148
|
+
print(
|
|
149
|
+
f" Net-load: skipping VRE unit '{u}' (demand-match): "
|
|
150
|
+
f"profile '{vu.profile}' does not cover all used timesteps."
|
|
151
|
+
)
|
|
152
|
+
weight[u] = 0.0
|
|
153
|
+
continue
|
|
154
|
+
avail = np.array(
|
|
155
|
+
[lookup[k] for k in timestep_keys], dtype=np.float64
|
|
156
|
+
)
|
|
157
|
+
w_u = float(np.dot(avail, dur))
|
|
158
|
+
weight[u] = w_u
|
|
159
|
+
e_existing += vu.existing_cap * w_u
|
|
160
|
+
|
|
161
|
+
target = max(0.0, vre_penetration * e_demand - e_existing)
|
|
162
|
+
|
|
163
|
+
# Investable units with usable profile energy share the target energy.
|
|
164
|
+
usable = [
|
|
165
|
+
u
|
|
166
|
+
for u in vre_units
|
|
167
|
+
if inputs.vre[u].investable and weight[u] >= _EPS
|
|
168
|
+
]
|
|
169
|
+
k = len(usable)
|
|
170
|
+
|
|
171
|
+
for u in vre_units:
|
|
172
|
+
vu = inputs.vre[u]
|
|
173
|
+
if not vu.investable:
|
|
174
|
+
continue # existing-only units are not in this dict.
|
|
175
|
+
w_u = weight[u]
|
|
176
|
+
if k == 0 or target <= 0.0 or w_u < _EPS:
|
|
177
|
+
# No useful invest possible: total cap is just the existing cap.
|
|
178
|
+
caps[u] = vu.existing_cap
|
|
179
|
+
else:
|
|
180
|
+
invested = (target / k) / w_u
|
|
181
|
+
caps[u] = vu.existing_cap + invested
|
|
182
|
+
|
|
183
|
+
return {u: caps[u] for u in sorted(caps)}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_group_capacities(
|
|
187
|
+
inputs: NetloadInputs,
|
|
188
|
+
default_caps: dict[str, float],
|
|
189
|
+
solved_caps: dict[str, float] | None,
|
|
190
|
+
) -> dict[str, float]:
|
|
191
|
+
"""Resolve the per-VRE-unit capacity used to build the net-load signal.
|
|
192
|
+
|
|
193
|
+
Contract (existing vs default vs solved), one rule per VRE unit:
|
|
194
|
+
|
|
195
|
+
* **Non-investable (existing-only)** unit → always its ``existing_cap``. Its
|
|
196
|
+
capacity is fixed data; neither the demand-match default nor a solve can
|
|
197
|
+
change it.
|
|
198
|
+
* **Investable** unit → the *total* capacity for this iteration:
|
|
199
|
+
``solved_caps[unit]`` when ``solved_caps`` is provided and contains the
|
|
200
|
+
unit (a later iteration feeding back a solve's realized capacity), else
|
|
201
|
+
``default_caps[unit]`` (the iteration-0 demand-match total from
|
|
202
|
+
:func:`demand_match_default_caps`). Both sources are totals, so no existing
|
|
203
|
+
capacity is added here. A unit absent from the chosen source falls back to
|
|
204
|
+
its ``existing_cap`` (defensive; the demand-match default always emits
|
|
205
|
+
every investable unit).
|
|
206
|
+
|
|
207
|
+
Returns a capacity for every VRE unit in ``inputs.vre`` (sorted).
|
|
208
|
+
"""
|
|
209
|
+
caps: dict[str, float] = {}
|
|
210
|
+
for u in sorted(inputs.vre):
|
|
211
|
+
vu = inputs.vre[u]
|
|
212
|
+
if not vu.investable:
|
|
213
|
+
caps[u] = vu.existing_cap
|
|
214
|
+
continue
|
|
215
|
+
if solved_caps is not None and u in solved_caps:
|
|
216
|
+
caps[u] = float(solved_caps[u])
|
|
217
|
+
elif u in default_caps:
|
|
218
|
+
caps[u] = float(default_caps[u])
|
|
219
|
+
else:
|
|
220
|
+
caps[u] = vu.existing_cap
|
|
221
|
+
return caps
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_netload_matrix(
|
|
225
|
+
inputs: NetloadInputs,
|
|
226
|
+
caps: dict[str, float],
|
|
227
|
+
timestep_keys: list[str],
|
|
228
|
+
period_length: int,
|
|
229
|
+
) -> tuple[np.ndarray, int, list[str]]:
|
|
230
|
+
"""Build the net-load clustering matrix ``C`` from per-unit capacities.
|
|
231
|
+
|
|
232
|
+
For each aggregation unit ``g`` (sorted): compute the hourly net load
|
|
233
|
+
``demand_h − Σ VRE cap·avail_h``, min-max normalize the series to ``[0, 1]``
|
|
234
|
+
(same convention as ``preprocess._build_clustering_matrix``; a constant /
|
|
235
|
+
all-zero series normalizes to all zeros and is KEPT — it simply carries no
|
|
236
|
+
information), reshape to ``(n_base_periods, period_length)``, and stack.
|
|
237
|
+
|
|
238
|
+
Period geometry copies ``preprocess._build_clustering_matrix``: drop the
|
|
239
|
+
tail so ``n_base_periods = len(timestep_keys) // period_length`` whole
|
|
240
|
+
periods remain (a positive drop prints a warning; ``n_base_periods == 0``
|
|
241
|
+
raises).
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
inputs: The net-load inputs (aggregation units, demand, VRE, profiles).
|
|
245
|
+
caps: Per-VRE-unit capacity (from :func:`build_group_capacities`); a unit
|
|
246
|
+
absent from the map is treated as 0 capacity.
|
|
247
|
+
timestep_keys: Ordered timestep keys defining the horizon.
|
|
248
|
+
period_length: Timesteps per aligned base period.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
``(C, n_base_periods, agg_unit_names)`` where ``C`` has shape
|
|
252
|
+
``(n_agg · period_length, n_base_periods)`` and ``agg_unit_names`` is the
|
|
253
|
+
sorted list of aggregation-unit names (block order along ``C``'s rows).
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
ValueError: If ``n_base_periods == 0``, or if there are no aggregation
|
|
257
|
+
units at all (mirrors ``_build_clustering_matrix``'s "no valid time
|
|
258
|
+
series" guard).
|
|
259
|
+
"""
|
|
260
|
+
n_total = len(timestep_keys)
|
|
261
|
+
n_base_periods = n_total // period_length
|
|
262
|
+
n_dropped = n_total - n_base_periods * period_length
|
|
263
|
+
|
|
264
|
+
if n_base_periods == 0:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Timeline has {n_total} timesteps but period_length is "
|
|
267
|
+
f"{period_length}. Need at least {period_length} timesteps."
|
|
268
|
+
)
|
|
269
|
+
if n_dropped > 0:
|
|
270
|
+
print(f"Warning: Dropping {n_dropped} timesteps from end of timeline")
|
|
271
|
+
|
|
272
|
+
n_used = n_base_periods * period_length
|
|
273
|
+
used_keys = timestep_keys[:n_used]
|
|
274
|
+
|
|
275
|
+
agg_names = sorted(inputs.units_by_group)
|
|
276
|
+
if not agg_names:
|
|
277
|
+
raise ValueError("No aggregation units found for net-load clustering.")
|
|
278
|
+
|
|
279
|
+
# Pre-build availability arrays (over the used keys) for every profile
|
|
280
|
+
# referenced by a VRE unit. A profile that does not cover all used timesteps
|
|
281
|
+
# is cached as ``None`` (skip policy of ``preprocess._build_clustering_matrix``
|
|
282
|
+
# / ``force_include._series_matrix`` — never zero-filled).
|
|
283
|
+
avail_cache: dict[str, np.ndarray | None] = {}
|
|
284
|
+
|
|
285
|
+
def _avail(profile: str) -> np.ndarray | None:
|
|
286
|
+
if profile not in avail_cache:
|
|
287
|
+
lookup = _series_lookup(inputs.profiles.get(profile, []))
|
|
288
|
+
if _covers(lookup, used_keys):
|
|
289
|
+
avail_cache[profile] = np.array(
|
|
290
|
+
[lookup[k] for k in used_keys], dtype=np.float64
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
avail_cache[profile] = None
|
|
294
|
+
return avail_cache[profile]
|
|
295
|
+
|
|
296
|
+
feature_blocks: list[np.ndarray] = []
|
|
297
|
+
for group in agg_names:
|
|
298
|
+
node_set = set(inputs.units_by_group[group])
|
|
299
|
+
|
|
300
|
+
# Demand term: Σ nodes ( −inflow_h(time-varying) + |scalar| ).
|
|
301
|
+
demand = np.zeros(n_used, dtype=np.float64)
|
|
302
|
+
has_demand = False
|
|
303
|
+
for node in sorted(node_set):
|
|
304
|
+
scalar = inputs.demand_scalar.get(node, 0.0)
|
|
305
|
+
if scalar:
|
|
306
|
+
demand += abs(scalar)
|
|
307
|
+
has_demand = True
|
|
308
|
+
ts = inputs.demand_ts.get(node)
|
|
309
|
+
if ts:
|
|
310
|
+
lookup = _series_lookup(ts)
|
|
311
|
+
if not _covers(lookup, used_keys):
|
|
312
|
+
print(
|
|
313
|
+
f" Net-load: skipping inflow of node '{node}': "
|
|
314
|
+
f"does not cover all used timesteps."
|
|
315
|
+
)
|
|
316
|
+
continue
|
|
317
|
+
demand -= np.array(
|
|
318
|
+
[lookup[k] for k in used_keys], dtype=np.float64
|
|
319
|
+
)
|
|
320
|
+
has_demand = True
|
|
321
|
+
|
|
322
|
+
# VRE term: Σ VRE units cap · avail_h.
|
|
323
|
+
vre_supply = np.zeros(n_used, dtype=np.float64)
|
|
324
|
+
has_vre = False
|
|
325
|
+
for u in sorted(u for u, vu in inputs.vre.items() if vu.node in node_set):
|
|
326
|
+
has_vre = True
|
|
327
|
+
avail = _avail(inputs.vre[u].profile)
|
|
328
|
+
if avail is None:
|
|
329
|
+
print(
|
|
330
|
+
f" Net-load: skipping VRE unit '{u}': profile "
|
|
331
|
+
f"'{inputs.vre[u].profile}' does not cover all used timesteps."
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
cap = float(caps.get(u, 0.0))
|
|
335
|
+
if cap:
|
|
336
|
+
vre_supply += cap * avail
|
|
337
|
+
|
|
338
|
+
# Co-location check: a group with VRE capacity but NO demand (neither
|
|
339
|
+
# time-varying inflow nor a scalar level) yields a pure-negative "net
|
|
340
|
+
# load" with nothing to net against — usually the region's load lives on
|
|
341
|
+
# a different node/group. Warn and suggest a co-locating region-group.
|
|
342
|
+
# (Demand-but-no-VRE, a pure load region, is normal and NOT warned.)
|
|
343
|
+
if has_vre and not has_demand:
|
|
344
|
+
print(
|
|
345
|
+
f" Net-load: aggregation unit '{group}' has VRE capacity but "
|
|
346
|
+
f"no demand (time-varying or scalar) — its net load is pure "
|
|
347
|
+
f"negative VRE. Define a region-group "
|
|
348
|
+
f"(use_for_representative_periods) to co-locate demand and VRE."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
series = demand - vre_supply
|
|
352
|
+
|
|
353
|
+
# Min-max normalize to [0, 1]; a constant series → all zeros (kept).
|
|
354
|
+
s_min = series.min()
|
|
355
|
+
s_max = series.max()
|
|
356
|
+
if s_max > s_min:
|
|
357
|
+
series = (series - s_min) / (s_max - s_min)
|
|
358
|
+
else:
|
|
359
|
+
series = np.zeros_like(series)
|
|
360
|
+
|
|
361
|
+
feature_blocks.append(series.reshape(n_base_periods, period_length))
|
|
362
|
+
|
|
363
|
+
feature_matrix = np.hstack(feature_blocks) # (n_base, n_agg * PL)
|
|
364
|
+
C = feature_matrix.T # (n_agg * PL, n_base)
|
|
365
|
+
return C, n_base_periods, agg_names
|