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,2186 @@
|
|
|
1
|
+
"""First wave of Direct Param helpers (Γ.1).
|
|
2
|
+
|
|
3
|
+
Each function in this module takes an :class:`InputSource` and returns
|
|
4
|
+
a single :class:`polar_high.Param` (or, for entity-only sets, a
|
|
5
|
+
:class:`polars.DataFrame`) — the trivial Direct port of one Param's
|
|
6
|
+
loader logic. The body of each helper is at most:
|
|
7
|
+
|
|
8
|
+
* ``source.parameter(class, name)`` → polars frame,
|
|
9
|
+
* a column rename / dtype cast,
|
|
10
|
+
* return.
|
|
11
|
+
|
|
12
|
+
Lazy-evaluation pattern
|
|
13
|
+
-----------------------
|
|
14
|
+
``InputSource.parameter()`` already collects internally (the source
|
|
15
|
+
plugin is the boundary). Helpers that compose multiple
|
|
16
|
+
``source.parameter()`` calls use ``.lazy()`` to chain operations
|
|
17
|
+
before a single ``.collect()`` at the end. Single-call helpers stay
|
|
18
|
+
eager (no compositional benefit from going lazy).
|
|
19
|
+
|
|
20
|
+
The full sweep into ``input.py`` is Γ.2/Γ.3; Γ.1 wires only the chosen
|
|
21
|
+
representative subset. Each helper is here so a future helper-by-
|
|
22
|
+
helper migration replaces the corresponding CSV branch in ``input.py``
|
|
23
|
+
with a one-line call to the function below.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
import polars as pl
|
|
30
|
+
|
|
31
|
+
from polar_high import Param
|
|
32
|
+
|
|
33
|
+
from flextool.engine_polars._axis_enums import (cast_frame_axes,
|
|
34
|
+
get_global_axis_enums,
|
|
35
|
+
rename_to_axis)
|
|
36
|
+
from flextool.engine_polars._param_shapes import (
|
|
37
|
+
broadcast_to_period,
|
|
38
|
+
broadcast_to_period_time,
|
|
39
|
+
resolve_param_shape,
|
|
40
|
+
)
|
|
41
|
+
from flextool.engine_polars._solve_state import FlexToolConfigError
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from flextool.engine_polars._input_source import InputSource
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# §5.2.1 — object-class scalars (Direct, scalar default → broadcast)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def p_co2_content_from_source(source: "InputSource") -> Param | None:
|
|
52
|
+
"""``commodity.co2_content`` → ``Param(("c",), [c, value])``.
|
|
53
|
+
|
|
54
|
+
Default ``None`` (§5.2.1) — None-skip per §4.5. Helpers that
|
|
55
|
+
depend on ``p_co2_content`` filter to entities with explicit values.
|
|
56
|
+
"""
|
|
57
|
+
df = source.parameter("commodity", "co2_content")
|
|
58
|
+
if df.height == 0:
|
|
59
|
+
return None
|
|
60
|
+
return Param(("c",),
|
|
61
|
+
df.lazy().rename({"name": "c"}).select("c", "value"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def p_constraint_constant_from_source(source: "InputSource") -> Param | None:
|
|
65
|
+
"""``constraint.constant`` → ``Param(("cn",), [cn, value])``.
|
|
66
|
+
|
|
67
|
+
Default ``0.0`` — broadcast to every constraint via the source.
|
|
68
|
+
|
|
69
|
+
The constraint axis uses the column name ``cn`` (not ``c``) to avoid
|
|
70
|
+
collision with the commodity axis — see
|
|
71
|
+
``schemas/flextool_axis_contract.json`` review note ``c_collision``.
|
|
72
|
+
"""
|
|
73
|
+
df = source.parameter("constraint", "constant")
|
|
74
|
+
if df.height == 0:
|
|
75
|
+
return None
|
|
76
|
+
return Param(("cn",),
|
|
77
|
+
df.lazy().rename({"name": "cn"}).select("cn", "value"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def constraint_sense_from_source(source: "InputSource") -> pl.DataFrame:
|
|
81
|
+
"""Return the constraint sense set as ``[cn, sense]``. Empty frame
|
|
82
|
+
if no constraints carry a ``sense`` value (None-default policy).
|
|
83
|
+
"""
|
|
84
|
+
df = source.parameter("constraint", "sense")
|
|
85
|
+
if df.height == 0:
|
|
86
|
+
return pl.DataFrame(schema={"cn": pl.Utf8, "sense": pl.Utf8})
|
|
87
|
+
return df.lazy().rename({"name": "cn", "value": "sense"}).select("cn", "sense").collect()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# §5.2.7 — method discriminator scalars (string)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def node_node_type_from_source(source: "InputSource") -> pl.DataFrame:
|
|
95
|
+
"""``node.node_type`` → ``[n, value]`` with ``balance`` default
|
|
96
|
+
broadcast to every node.
|
|
97
|
+
"""
|
|
98
|
+
df = source.parameter("node", "node_type")
|
|
99
|
+
if df.height == 0:
|
|
100
|
+
return pl.DataFrame(schema={"n": pl.Utf8, "value": pl.Utf8})
|
|
101
|
+
return df.lazy().rename({"name": "n"}).select("n", "value").collect()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def node_inflow_method_from_source(source: "InputSource") -> pl.DataFrame:
|
|
105
|
+
"""``node.inflow_method`` → ``[n, value]``. None default — only
|
|
106
|
+
nodes with an explicit inflow_method are returned.
|
|
107
|
+
"""
|
|
108
|
+
df = source.parameter("node", "inflow_method")
|
|
109
|
+
if df.height == 0:
|
|
110
|
+
return pl.DataFrame(schema={"n": pl.Utf8, "value": pl.Utf8})
|
|
111
|
+
return df.lazy().rename({"name": "n"}).select("n", "value").collect()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def unit_conversion_method_from_source(source: "InputSource") -> pl.DataFrame:
|
|
115
|
+
"""``unit.conversion_method`` → ``[p, value]``. None default."""
|
|
116
|
+
df = source.parameter("unit", "conversion_method")
|
|
117
|
+
if df.height == 0:
|
|
118
|
+
return pl.DataFrame(schema={"p": pl.Utf8, "value": pl.Utf8})
|
|
119
|
+
return df.lazy().rename({"name": "p"}).select("p", "value").collect()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def connection_transfer_method_from_source(source: "InputSource") -> pl.DataFrame:
|
|
123
|
+
"""``connection.transfer_method`` → ``[p, value]``. None default."""
|
|
124
|
+
df = source.parameter("connection", "transfer_method")
|
|
125
|
+
if df.height == 0:
|
|
126
|
+
return pl.DataFrame(schema={"p": pl.Utf8, "value": pl.Utf8})
|
|
127
|
+
return df.lazy().rename({"name": "p"}).select("p", "value").collect()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# §5.2.1 — penalty (sentinel default ⇒ broadcast)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def penalty_up_scalar_from_source(source: "InputSource") -> pl.DataFrame:
|
|
135
|
+
"""``node.penalty_up`` per-node scalar (pre-broadcast over (d, t)).
|
|
136
|
+
|
|
137
|
+
The sentinel default (large positive value, e.g. 10000.0) is
|
|
138
|
+
applied at the source level (§4.6). The downstream Derived helper
|
|
139
|
+
cross-joins this against ``(d, t)`` to produce ``p_penalty_up``.
|
|
140
|
+
"""
|
|
141
|
+
df = source.parameter("node", "penalty_up")
|
|
142
|
+
return df.lazy().rename({"name": "n"}).select("n", "value").collect()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def penalty_down_scalar_from_source(source: "InputSource") -> pl.DataFrame:
|
|
146
|
+
df = source.parameter("node", "penalty_down")
|
|
147
|
+
return df.lazy().rename({"name": "n"}).select("n", "value").collect()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Existing capacity (object-class scalars, per-class then unioned for `e`)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _union_existing(source: "InputSource", classes: list[str]) -> pl.DataFrame:
|
|
155
|
+
"""Union ``existing`` across the supplied entity classes into one
|
|
156
|
+
long ``[e, value]`` frame. Returns an empty frame if no class
|
|
157
|
+
contributes a row.
|
|
158
|
+
"""
|
|
159
|
+
parts: list[pl.LazyFrame] = []
|
|
160
|
+
for cls in classes:
|
|
161
|
+
try:
|
|
162
|
+
df = source.parameter(cls, "existing")
|
|
163
|
+
except KeyError:
|
|
164
|
+
continue
|
|
165
|
+
if df.height == 0:
|
|
166
|
+
continue
|
|
167
|
+
parts.append(df.lazy().rename({"name": "e"}).select("e", "value"))
|
|
168
|
+
if not parts:
|
|
169
|
+
return pl.DataFrame(schema={"e": pl.Utf8, "value": pl.Float64})
|
|
170
|
+
return pl.concat(parts).collect().sort("e")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def existing_long_from_source(source: "InputSource") -> pl.DataFrame:
|
|
174
|
+
"""Per-(entity, value) ``existing`` across unit/node/connection.
|
|
175
|
+
Mirrors flextool's preprocessed ``p_entity_unitsize.csv`` indirectly
|
|
176
|
+
— but here we expose just the raw scalar.
|
|
177
|
+
"""
|
|
178
|
+
return _union_existing(source, ["unit", "node", "connection"])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Membership relationships (entities-only, no parameter)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def commodity_node_set_from_source(source: "InputSource") -> pl.DataFrame:
|
|
186
|
+
"""``commodity__node`` membership set. Schema: ``[commodity, node]``."""
|
|
187
|
+
return source.entities("commodity__node")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def unit_input_node_set_from_source(source: "InputSource") -> pl.DataFrame:
|
|
191
|
+
"""``unit__inputNode`` membership set. Schema: ``[unit, node]``."""
|
|
192
|
+
return source.entities("unit__inputNode")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def unit_output_node_set_from_source(source: "InputSource") -> pl.DataFrame:
|
|
196
|
+
"""``unit__outputNode`` membership set. Schema: ``[unit, node]``."""
|
|
197
|
+
return source.entities("unit__outputNode")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def connection_node_node_set_from_source(source: "InputSource") -> pl.DataFrame:
|
|
201
|
+
"""``connection__node__node`` membership set. Schema:
|
|
202
|
+
``[connection, node_1, node_2]`` (the dim suffix disambiguates the
|
|
203
|
+
repeated ``node`` dim class).
|
|
204
|
+
"""
|
|
205
|
+
return source.entities("connection__node__node")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def reserve_upDown_group_set_from_source(source: "InputSource") -> pl.DataFrame:
|
|
209
|
+
"""``reserve__upDown__group`` membership set. Schema:
|
|
210
|
+
``[reserve, upDown, group]``.
|
|
211
|
+
"""
|
|
212
|
+
return source.entities("reserve__upDown__group")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# §5.2.3 — relationship 1d_map / scalar (conversion_flow_coeff, etc.)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def unit_input_conversion_flow_coeff_from_source(source: "InputSource") -> Param | None:
|
|
220
|
+
"""``unit__inputNode.conversion_flow_coeff`` → ``Param(("p","source"), …)``.
|
|
221
|
+
|
|
222
|
+
Default 1.0 — broadcast over all (unit, node) entities.
|
|
223
|
+
"""
|
|
224
|
+
df = source.parameter("unit__inputNode", "conversion_flow_coeff")
|
|
225
|
+
if df.height == 0:
|
|
226
|
+
return None
|
|
227
|
+
return Param(("p", "source"),
|
|
228
|
+
df.lazy().rename({"unit": "p", "node": "source"})
|
|
229
|
+
.select("p", "source", "value"))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def unit_output_conversion_flow_coeff_from_source(source: "InputSource") -> Param | None:
|
|
233
|
+
"""``unit__outputNode.conversion_flow_coeff`` → ``Param(("p","sink"), …)``."""
|
|
234
|
+
df = source.parameter("unit__outputNode", "conversion_flow_coeff")
|
|
235
|
+
if df.height == 0:
|
|
236
|
+
return None
|
|
237
|
+
return Param(("p", "sink"),
|
|
238
|
+
df.lazy().rename({"unit": "p", "node": "sink"})
|
|
239
|
+
.select("p", "sink", "value"))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Module catalog (used by the parity test scaffolding to iterate over
|
|
244
|
+
# the first-wave Params). The shape of each entry is:
|
|
245
|
+
#
|
|
246
|
+
# (logical_name, callable_returning_frame, fixture_csv_extractor) where
|
|
247
|
+
#
|
|
248
|
+
# - logical_name: stable label for parametrized test ids.
|
|
249
|
+
# - callable_returning_frame: takes an InputSource → frame.
|
|
250
|
+
# - fixture_csv_extractor: takes the CSV-loaded ``FlexData`` →
|
|
251
|
+
# the corresponding frame (or None) for parity comparison.
|
|
252
|
+
#
|
|
253
|
+
# Only entries whose CSV-side has a clean structural mirror are listed
|
|
254
|
+
# here. Helpers without a direct ``FlexData`` field (e.g. method
|
|
255
|
+
# discriminator strings consumed by Projection) are exercised by the
|
|
256
|
+
# unit-test suite via :class:`InMemoryReader` instead.
|
|
257
|
+
|
|
258
|
+
# Imported lazily inside the test module to avoid a circular import.
|
|
259
|
+
|
|
260
|
+
FIRST_WAVE_PARAMS = (
|
|
261
|
+
"p_co2_content",
|
|
262
|
+
"p_constraint_constant",
|
|
263
|
+
"node_type",
|
|
264
|
+
"inflow_method",
|
|
265
|
+
"conversion_method",
|
|
266
|
+
"transfer_method",
|
|
267
|
+
"penalty_up",
|
|
268
|
+
"penalty_down",
|
|
269
|
+
"commodity_node_set",
|
|
270
|
+
"unit_inputNode_set",
|
|
271
|
+
"unit_outputNode_set",
|
|
272
|
+
"connection_node_node_set",
|
|
273
|
+
"reserve_upDown_group_set",
|
|
274
|
+
"unit_input_flow_coef",
|
|
275
|
+
"unit_output_flow_coef",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Helpers for FlexData-field-targeted overrides
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Accepted ``Map.index_name`` labels for the per-entity constraint-coef
|
|
284
|
+
# parameters. ``"constraint"`` is what ``model_builder`` writes; ``"x"``
|
|
285
|
+
# is spinedb_api's default when a Map is authored without an explicit
|
|
286
|
+
# ``index_name``. Anything else indicates a Spine authoring mistake
|
|
287
|
+
# (typo, wrong axis label) and is surfaced loudly with the offending
|
|
288
|
+
# entity rows so the user can fix the DB.
|
|
289
|
+
_CONSTRAINT_INDEX_LABELS = ("constraint", "x")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _resolve_constraint_index_col(
|
|
293
|
+
df: pl.DataFrame,
|
|
294
|
+
*,
|
|
295
|
+
parameter_name: str,
|
|
296
|
+
entity_class: str,
|
|
297
|
+
) -> str:
|
|
298
|
+
"""Identify the Map's constraint-axis column in *df* (the frame
|
|
299
|
+
returned by :meth:`InputSource.parameter`).
|
|
300
|
+
|
|
301
|
+
Raises a clear ``ValueError`` naming the parameter, entity_class,
|
|
302
|
+
offending column name, and the entity rows that carry the bad Map
|
|
303
|
+
so the user can locate and fix it in Spine.
|
|
304
|
+
"""
|
|
305
|
+
extras = [c for c in df.columns if c not in ("name", "value")]
|
|
306
|
+
if len(extras) == 1 and extras[0] in _CONSTRAINT_INDEX_LABELS:
|
|
307
|
+
return extras[0]
|
|
308
|
+
accepted = ", ".join(repr(s) for s in _CONSTRAINT_INDEX_LABELS)
|
|
309
|
+
entities = df.get_column("name").unique().to_list() if "name" in df.columns else []
|
|
310
|
+
entities_repr = ", ".join(repr(e) for e in entities[:10])
|
|
311
|
+
if len(entities) > 10:
|
|
312
|
+
entities_repr += f", … (+{len(entities) - 10} more)"
|
|
313
|
+
raise ValueError(
|
|
314
|
+
f"Unrecognised Map index column(s) {extras!r} in parameter "
|
|
315
|
+
f"{parameter_name!r} of entity_class {entity_class!r} "
|
|
316
|
+
f"(affected entities: [{entities_repr}]). "
|
|
317
|
+
f"Expected a 1-D Map with index_name in {{{accepted}}}; "
|
|
318
|
+
f"set Map.index_name to 'constraint' in Spine."
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _node_constraint_coef(source: "InputSource", parameter_name: str) -> Param | None:
|
|
323
|
+
"""Return ``Param(("n", "cn"), [n, cn, value])`` for a per-(node,
|
|
324
|
+
constraint) coefficient parameter. ``None`` if the DB has no rows.
|
|
325
|
+
|
|
326
|
+
The constraint axis column is named ``cn`` to disambiguate from the
|
|
327
|
+
commodity axis column ``c`` (the contract's ``c_collision`` decision).
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
df = source.parameter("node", parameter_name)
|
|
331
|
+
except KeyError:
|
|
332
|
+
# Older DBs may not define the parameter on the ``node`` class
|
|
333
|
+
# at all (rather than just having zero rows). Treat the same.
|
|
334
|
+
return None
|
|
335
|
+
if df.height == 0:
|
|
336
|
+
return None
|
|
337
|
+
c_col = _resolve_constraint_index_col(
|
|
338
|
+
df, parameter_name=parameter_name, entity_class="node")
|
|
339
|
+
return Param(
|
|
340
|
+
("n", "cn"),
|
|
341
|
+
df.lazy()
|
|
342
|
+
.rename({"name": "n", c_col: "cn"})
|
|
343
|
+
.select("n", "cn", "value")
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _process_constraint_coef(source: "InputSource",
|
|
348
|
+
parameter_name: str) -> Param | None:
|
|
349
|
+
"""Union the per-(process, constraint) coefficient across the unit +
|
|
350
|
+
connection classes (the ``process`` superclass in flextool's CSV
|
|
351
|
+
output). ``None`` if neither contributes a row.
|
|
352
|
+
"""
|
|
353
|
+
parts: list[pl.LazyFrame] = []
|
|
354
|
+
for cls in ("unit", "connection"):
|
|
355
|
+
try:
|
|
356
|
+
df = source.parameter(cls, parameter_name)
|
|
357
|
+
except KeyError:
|
|
358
|
+
continue
|
|
359
|
+
if df.height == 0:
|
|
360
|
+
continue
|
|
361
|
+
c_col = _resolve_constraint_index_col(
|
|
362
|
+
df, parameter_name=parameter_name, entity_class=cls)
|
|
363
|
+
parts.append(
|
|
364
|
+
df.lazy()
|
|
365
|
+
.rename({"name": "p", c_col: "cn"})
|
|
366
|
+
.select("p", "cn", "value")
|
|
367
|
+
)
|
|
368
|
+
if not parts:
|
|
369
|
+
return None
|
|
370
|
+
return Param(("p", "cn"), pl.concat(parts).sort("p", "cn"))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _entity_methods_pairs(source: "InputSource") -> set[tuple[str, str]]:
|
|
374
|
+
"""Mirror ``entity__invest_method.csv`` — return ``{(e, method)}``.
|
|
375
|
+
|
|
376
|
+
Spine source: ``unit/node/connection.invest_method``. Empty when
|
|
377
|
+
no entity has an explicit method.
|
|
378
|
+
"""
|
|
379
|
+
out: set[tuple[str, str]] = set()
|
|
380
|
+
for cls in ("unit", "node", "connection"):
|
|
381
|
+
try:
|
|
382
|
+
df = source.parameter(cls, "invest_method")
|
|
383
|
+
except KeyError:
|
|
384
|
+
continue
|
|
385
|
+
if df.height == 0:
|
|
386
|
+
continue
|
|
387
|
+
for e, m in df.select("name", "value").iter_rows():
|
|
388
|
+
out.add((str(e), str(m)))
|
|
389
|
+
return out
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Mirror ``invest_method_sets.py:22-27``.
|
|
393
|
+
_INVEST_METHOD_NOT_ALLOWED = frozenset((
|
|
394
|
+
"not_allowed", "retire_period", "retire_total", "retire_no_limit",
|
|
395
|
+
))
|
|
396
|
+
_DIVEST_METHOD_NOT_ALLOWED = frozenset((
|
|
397
|
+
"not_allowed", "invest_period", "invest_total", "invest_no_limit",
|
|
398
|
+
))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _entity_invest_universe(source: "InputSource",
|
|
402
|
+
kind: str = "invest") -> list[str]:
|
|
403
|
+
"""Return ``entityInvest`` (or ``entityDivest``) — entities whose
|
|
404
|
+
invest_method is NOT in the not-allowed set. Ordered by class
|
|
405
|
+
encounter (unit → node → connection), deduplicated, matching
|
|
406
|
+
``invest_method_sets.py:30-46``.
|
|
407
|
+
"""
|
|
408
|
+
not_allowed = (_INVEST_METHOD_NOT_ALLOWED if kind == "invest"
|
|
409
|
+
else _DIVEST_METHOD_NOT_ALLOWED)
|
|
410
|
+
seen: dict[str, None] = {}
|
|
411
|
+
for cls in ("unit", "node", "connection"):
|
|
412
|
+
try:
|
|
413
|
+
df = source.parameter(cls, "invest_method")
|
|
414
|
+
except KeyError:
|
|
415
|
+
continue
|
|
416
|
+
if df.height == 0:
|
|
417
|
+
continue
|
|
418
|
+
for e, m in df.select("name", "value").iter_rows():
|
|
419
|
+
if str(m) not in not_allowed:
|
|
420
|
+
seen.setdefault(str(e), None)
|
|
421
|
+
return list(seen.keys())
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _e_total_param(source: "InputSource", parameter_name: str,
|
|
425
|
+
kind: str = "invest",
|
|
426
|
+
filter_zero: bool = False) -> Param | None:
|
|
427
|
+
"""Per-entity scalar parameter (e.g. ``invest_max_total``) keyed on
|
|
428
|
+
``entityInvest`` (or ``entityDivest`` for divest variants), with 0
|
|
429
|
+
default for entities with no explicit row.
|
|
430
|
+
|
|
431
|
+
Mirrors flextool's ``entity_total_caps.py:_compute_entity_total`` —
|
|
432
|
+
a row is emitted for every entity in ``entityInvest`` (resp.
|
|
433
|
+
``entityDivest``), value defaults to 0 when neither
|
|
434
|
+
``unit/node/connection`` carries an explicit row.
|
|
435
|
+
|
|
436
|
+
When ``filter_zero=True``, rows with ``value == 0`` are dropped
|
|
437
|
+
(mirrors ``input.py::_read_e_param`` for ``invest_min_total`` /
|
|
438
|
+
``retire_min_total`` which use a different reader that filters
|
|
439
|
+
zeros). Returns ``None`` when no rows survive (or
|
|
440
|
+
``entityInvest`` is empty).
|
|
441
|
+
"""
|
|
442
|
+
keys = _entity_invest_universe(source, kind=kind)
|
|
443
|
+
if not keys:
|
|
444
|
+
return None
|
|
445
|
+
explicit: dict[str, float] = {}
|
|
446
|
+
for cls in ("unit", "node", "connection"):
|
|
447
|
+
try:
|
|
448
|
+
df = source.parameter_explicit(cls, parameter_name)
|
|
449
|
+
except (KeyError, AttributeError):
|
|
450
|
+
try:
|
|
451
|
+
df = source.parameter(cls, parameter_name)
|
|
452
|
+
except KeyError:
|
|
453
|
+
continue
|
|
454
|
+
if df is None or df.height == 0:
|
|
455
|
+
continue
|
|
456
|
+
for e, v in df.select("name", "value").iter_rows():
|
|
457
|
+
explicit[str(e)] = float(v) if v is not None else 0.0
|
|
458
|
+
rows = [(e, explicit.get(e, 0.0)) for e in keys]
|
|
459
|
+
if filter_zero:
|
|
460
|
+
rows = [(e, v) for e, v in rows if v != 0.0]
|
|
461
|
+
if not rows:
|
|
462
|
+
return None
|
|
463
|
+
out = pl.DataFrame(rows, schema=["e", "value"], orient="row").sort("e")
|
|
464
|
+
return Param(("e",), out)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
# Δ.4 — second-wave Direct Param helpers.
|
|
469
|
+
#
|
|
470
|
+
# Each helper below covers a single FlexData field whose CSV-loader path
|
|
471
|
+
# in ``input.py`` is a one-CSV ``rename + select`` of an entity scalar /
|
|
472
|
+
# Map / 1d_map parameter. Helpers return ``Param | None`` (None when
|
|
473
|
+
# the DB-side has no explicit row, matching the CSV path's "absent →
|
|
474
|
+
# None" contract). The CSV-side behaviour preserved verbatim — the
|
|
475
|
+
# parity tests must continue to pass.
|
|
476
|
+
|
|
477
|
+
def _entity_scalar_explicit(source: "InputSource", entity_class: str,
|
|
478
|
+
parameter_name: str,
|
|
479
|
+
dim: str) -> Param | None:
|
|
480
|
+
"""Return ``Param((dim,), [<dim>, value])`` for an entity-class
|
|
481
|
+
scalar, mirroring the CSV path's "explicit rows only" semantics.
|
|
482
|
+
|
|
483
|
+
Tolerates a Spine schema that doesn't declare the parameter on the
|
|
484
|
+
class (older fixtures): ``KeyError`` from
|
|
485
|
+
:meth:`InputSource.parameter_explicit` / :meth:`parameter` is
|
|
486
|
+
treated as "no rows".
|
|
487
|
+
"""
|
|
488
|
+
try:
|
|
489
|
+
df = source.parameter_explicit(entity_class, parameter_name)
|
|
490
|
+
except (KeyError, AttributeError):
|
|
491
|
+
try:
|
|
492
|
+
df = source.parameter(entity_class, parameter_name)
|
|
493
|
+
except KeyError:
|
|
494
|
+
return None
|
|
495
|
+
if df is None or df.height == 0:
|
|
496
|
+
return None
|
|
497
|
+
return Param((dim,),
|
|
498
|
+
df.lazy().rename({"name": dim}).select(dim, "value"))
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# §5.2.5 — node scalars sliced from p_node.csv
|
|
502
|
+
def p_state_self_discharge_from_source(source: "InputSource") -> Param | None:
|
|
503
|
+
"""``node.self_discharge_loss`` → ``Param(("n",), [n, value])``.
|
|
504
|
+
|
|
505
|
+
Default 0.0 (schema). CSV path
|
|
506
|
+
(``input.py::_load_storage::_node_param``) returns the explicit
|
|
507
|
+
rows only. We mirror by reading via ``parameter_explicit``.
|
|
508
|
+
"""
|
|
509
|
+
return _entity_scalar_explicit(source, "node", "self_discharge_loss", "n")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def p_state_start_from_source(source: "InputSource") -> Param | None:
|
|
513
|
+
"""``node.storage_state_start`` → ``Param(("n",), [n, value])``.
|
|
514
|
+
|
|
515
|
+
Default ``None`` (schema). Returns explicit rows only.
|
|
516
|
+
"""
|
|
517
|
+
return _entity_scalar_explicit(source, "node", "storage_state_start", "n")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# §5.2.7 — process scalars sliced from p_process.csv
|
|
521
|
+
def p_min_load_from_source(source: "InputSource") -> Param | None:
|
|
522
|
+
"""``unit.min_load`` → ``Param(("p",), [p, value])``.
|
|
523
|
+
|
|
524
|
+
Default 0.0 (schema). CSV path filters by processParam=='min_load'
|
|
525
|
+
and emits rows only when explicit; we mirror via ``parameter_explicit``.
|
|
526
|
+
"""
|
|
527
|
+
return _entity_scalar_explicit(source, "unit", "min_load", "p")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# §5.18 — connection scalars
|
|
531
|
+
def p_connection_susceptance_from_source(source: "InputSource") -> Param | None:
|
|
532
|
+
"""``connection.susceptance`` → ``Param(("p",), [p, value])``.
|
|
533
|
+
|
|
534
|
+
Default ``None``. Used only for DC-power-flow scenarios. Older
|
|
535
|
+
fixtures may not define ``susceptance`` on the class — treated as
|
|
536
|
+
"no rows".
|
|
537
|
+
"""
|
|
538
|
+
return _entity_scalar_explicit(source, "connection", "susceptance", "p")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# §5.19 — commodity scalars
|
|
542
|
+
def p_commodity_unitsize_from_source(source: "InputSource") -> Param | None:
|
|
543
|
+
"""``commodity.unitsize`` → ``Param(("c",), [c, value])``.
|
|
544
|
+
|
|
545
|
+
Default 1.0 (schema). Used by the commodity-price-ladder feature
|
|
546
|
+
only. CSV path emits explicit rows only — mirror that via
|
|
547
|
+
``parameter_explicit``.
|
|
548
|
+
"""
|
|
549
|
+
return _entity_scalar_explicit(source, "commodity", "unitsize", "c")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# ---------------------------------------------------------------------------
|
|
553
|
+
# Δ.4b — third wave Direct Param helpers.
|
|
554
|
+
#
|
|
555
|
+
# Patterns covered below:
|
|
556
|
+
#
|
|
557
|
+
# 1. Indexed (1d_map) entity-class scalars — e.g. ``unit.invest_max_period``
|
|
558
|
+
# (``ed_invest_max_period``) which unrolls to ``[entity, period, value]``.
|
|
559
|
+
# 2. Indexed Map(period→time) parameters — e.g. ``node.availability``
|
|
560
|
+
# (``p_node_availability``) which unrolls to ``[entity, period, t,
|
|
561
|
+
# value]``.
|
|
562
|
+
# 3. Per-(entity, side) relationship scalars — e.g.
|
|
563
|
+
# ``unit__inputNode.ramp_speed_up`` (``p_ramp_speed_up_source``).
|
|
564
|
+
# 4. Multi-class union 1d_map — e.g. invest/divest period caps spanning
|
|
565
|
+
# ``unit`` ∪ ``node`` ∪ ``connection``.
|
|
566
|
+
# 5. Multi-class union Map — e.g. ``other_operational_cost`` for var-cost.
|
|
567
|
+
#
|
|
568
|
+
# All helpers honour the CSV path's "drop zero rows" + "explicit rows
|
|
569
|
+
# only" semantics — failing loudly if the underlying parameter shape
|
|
570
|
+
# doesn't match the expected schema.
|
|
571
|
+
|
|
572
|
+
def _filter_param_by_periods(p: Param | None,
|
|
573
|
+
period_filter: pl.DataFrame | None
|
|
574
|
+
) -> Param | None:
|
|
575
|
+
"""Restrict ``p`` to rows whose ``d`` (and optionally ``t``) column
|
|
576
|
+
matches the active solve's ``period_filter``. Returns the Param
|
|
577
|
+
with the join chained onto its existing LazyFrame — no eager
|
|
578
|
+
collect. Returns ``p`` unchanged when there's nothing to filter
|
|
579
|
+
on (``p`` is None, filter is empty, or the Param has no ``d``/
|
|
580
|
+
``t`` axes to restrict).
|
|
581
|
+
|
|
582
|
+
Shape-aware (Phase E.1): the join is on whichever of ``d`` / ``t``
|
|
583
|
+
are present in ``p.dims``. Under the lazified broadcast contract
|
|
584
|
+
a MAP_TIME-authored field is ``(entity, t)`` and a SCALAR-authored
|
|
585
|
+
field is ``(entity,)``; both are handled — the former joins on
|
|
586
|
+
``t``, the latter passes through unchanged.
|
|
587
|
+
|
|
588
|
+
Mirrors the CSV path's ``solve_data/pdtNode.csv inner-join with
|
|
589
|
+
steps_in_use`` semantic when both ``d`` and ``t`` are present.
|
|
590
|
+
|
|
591
|
+
Reads ``p.lazy`` directly — does NOT trigger ``polar_high.Param``'s
|
|
592
|
+
eager ``.frame`` cache.
|
|
593
|
+
"""
|
|
594
|
+
if p is None or period_filter is None or period_filter.height == 0:
|
|
595
|
+
return p
|
|
596
|
+
p_dims = set(p.dims)
|
|
597
|
+
has_d = "d" in p_dims
|
|
598
|
+
has_t = "t" in p_dims
|
|
599
|
+
if not has_d and not has_t:
|
|
600
|
+
# SCALAR-shape Param (e.g. ``(entity,)``) — nothing to filter on.
|
|
601
|
+
return p
|
|
602
|
+
# Phase 4.8g: the Param's underlying LazyFrame may carry Utf8
|
|
603
|
+
# ``d`` / ``t`` columns from the Direct-Param CSV/scalar path while
|
|
604
|
+
# ``period_filter`` (the ``dt`` frame) is fully Enum-cast under
|
|
605
|
+
# activation. Defensively cast both sides to the live axis enums
|
|
606
|
+
# so the join keys match — without this the join raises
|
|
607
|
+
# ``SchemaError`` (Utf8 ↔ Enum).
|
|
608
|
+
lf = p.lazy
|
|
609
|
+
_enums = get_global_axis_enums()
|
|
610
|
+
if _enums is not None:
|
|
611
|
+
lf = cast_frame_axes(lf, _enums)
|
|
612
|
+
period_filter = cast_frame_axes(period_filter, _enums)
|
|
613
|
+
pf_cols = set(period_filter.columns)
|
|
614
|
+
if has_d and has_t and {"d", "t"}.issubset(pf_cols):
|
|
615
|
+
keep = period_filter.lazy().select("d", "t").unique()
|
|
616
|
+
out_lf = lf.join(keep, on=["d", "t"], how="inner")
|
|
617
|
+
elif has_d and "d" in pf_cols:
|
|
618
|
+
keep = period_filter.lazy().select("d").unique()
|
|
619
|
+
out_lf = lf.join(keep, on="d", how="inner")
|
|
620
|
+
elif has_t and "t" in pf_cols:
|
|
621
|
+
# Phase E.1: MAP_TIME-authored field is ``(entity, t)`` — no
|
|
622
|
+
# ``d`` to filter on. Restrict by ``t`` instead.
|
|
623
|
+
keep = period_filter.lazy().select("t").unique()
|
|
624
|
+
out_lf = lf.join(keep, on="t", how="inner")
|
|
625
|
+
else:
|
|
626
|
+
# Param has a d/t axis but the period_filter doesn't carry the
|
|
627
|
+
# matching column — nothing to filter on.
|
|
628
|
+
return p
|
|
629
|
+
return Param(p.dims, out_lf)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _entity_period_scalar(source: "InputSource", entity_class: str,
|
|
633
|
+
parameter_name: str,
|
|
634
|
+
entity_dim: str,
|
|
635
|
+
*,
|
|
636
|
+
filter_zero: bool = False,
|
|
637
|
+
filter_null: bool = True,
|
|
638
|
+
period_filter: pl.DataFrame | None = None
|
|
639
|
+
) -> Param | None:
|
|
640
|
+
"""Return ``Param((entity_dim, "d"), [<entity_dim>, d, value])`` for a
|
|
641
|
+
``1d_map(period)`` OR scalar parameter on the given entity class.
|
|
642
|
+
|
|
643
|
+
Mirrors the CSV path's slice of ``pdGroup.csv`` / ``pdProcess.csv``
|
|
644
|
+
style files: explicit rows only, optionally filtering out zero /
|
|
645
|
+
null values. Returns ``None`` when no rows survive.
|
|
646
|
+
|
|
647
|
+
Δ.12c-fix gap #3: scalar broadcast. When the source returns a
|
|
648
|
+
scalar shape (``[name, value]`` without a ``period`` column) we
|
|
649
|
+
broadcast the scalar over the periods supplied in ``period_filter``
|
|
650
|
+
— mirroring flextool's preprocessing which expands a group/process
|
|
651
|
+
scalar to one row per (entity, period) before writing the
|
|
652
|
+
``pdGroup.csv`` / ``pd_process.csv`` family. Without
|
|
653
|
+
``period_filter`` we cannot infer the broadcast axis and fall
|
|
654
|
+
through to ``None``.
|
|
655
|
+
|
|
656
|
+
``period_filter``: optional ``[d]`` frame restricting the output to
|
|
657
|
+
a subset of periods (mirrors flextool preprocessing's per-solve
|
|
658
|
+
period filter — Spine ``invest_max_period`` / ``co2_max_period``
|
|
659
|
+
Maps cover ALL declared periods, but the CSV path's
|
|
660
|
+
``pd_group.csv`` etc. is pre-filtered to the active solve's
|
|
661
|
+
periods). Pass ``flex_data.dt`` (already restricted to the active
|
|
662
|
+
solve) to mirror that semantic. Also used as the broadcast axis
|
|
663
|
+
for scalar-shape values.
|
|
664
|
+
"""
|
|
665
|
+
try:
|
|
666
|
+
df = source.parameter_explicit(entity_class, parameter_name)
|
|
667
|
+
except (KeyError, AttributeError):
|
|
668
|
+
try:
|
|
669
|
+
df = source.parameter(entity_class, parameter_name)
|
|
670
|
+
except KeyError:
|
|
671
|
+
return None
|
|
672
|
+
if df is None or df.height == 0:
|
|
673
|
+
return None
|
|
674
|
+
cols = df.columns
|
|
675
|
+
if "value" not in cols:
|
|
676
|
+
return None
|
|
677
|
+
if "period" not in cols:
|
|
678
|
+
# Scalar broadcast cascade — Δ.12c-fix gap #3. Mirrors
|
|
679
|
+
# flextool preprocessing which writes one (entity, period, value)
|
|
680
|
+
# row per period when the source value is a scalar.
|
|
681
|
+
if period_filter is None or period_filter.height == 0:
|
|
682
|
+
return None
|
|
683
|
+
if "name" not in cols:
|
|
684
|
+
return None
|
|
685
|
+
lf = df.lazy().pipe(rename_to_axis, {"name": entity_dim})
|
|
686
|
+
if filter_null:
|
|
687
|
+
lf = lf.filter(pl.col("value").is_not_null())
|
|
688
|
+
if filter_zero:
|
|
689
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
690
|
+
periods = period_filter.lazy().select("d").unique()
|
|
691
|
+
out = (lf.select(entity_dim, "value")
|
|
692
|
+
.join(periods, how="cross")
|
|
693
|
+
.select(entity_dim, "d", "value")
|
|
694
|
+
.collect())
|
|
695
|
+
if out.height == 0:
|
|
696
|
+
return None
|
|
697
|
+
return Param((entity_dim, "d"), out.lazy())
|
|
698
|
+
lf = df.lazy().pipe(rename_to_axis, {"name": entity_dim, "period": "d"})
|
|
699
|
+
if filter_null:
|
|
700
|
+
lf = lf.filter(pl.col("value").is_not_null())
|
|
701
|
+
if filter_zero:
|
|
702
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
703
|
+
if period_filter is not None and period_filter.height > 0:
|
|
704
|
+
lf = lf.join(period_filter.lazy().select("d").unique(), on="d",
|
|
705
|
+
how="inner")
|
|
706
|
+
out = lf.select(entity_dim, "d", "value").collect()
|
|
707
|
+
if out.height == 0:
|
|
708
|
+
return None
|
|
709
|
+
return Param((entity_dim, "d"), out.lazy())
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _entity_period_time_param(source: "InputSource", entity_class: str,
|
|
713
|
+
parameter_name: str,
|
|
714
|
+
entity_dim: str,
|
|
715
|
+
*,
|
|
716
|
+
filter_zero: bool = False,
|
|
717
|
+
period_filter: pl.DataFrame | None = None
|
|
718
|
+
) -> Param | None:
|
|
719
|
+
"""Return ``Param((entity_dim, "d", "t"), [...])`` for a
|
|
720
|
+
``Map(period→time)`` parameter. CSV slice of ``pdtNode.csv`` /
|
|
721
|
+
``pdtCommodity.csv`` / ``pdtGroup.csv`` files.
|
|
722
|
+
|
|
723
|
+
``period_filter`` restricts the output to a subset of periods (per
|
|
724
|
+
the active solve), mirroring CSV preprocessing.
|
|
725
|
+
|
|
726
|
+
Δ.12c-fix gap #1 — broadcast cascade. In addition to the explicit
|
|
727
|
+
``Map(period→time)`` shape (columns ``[name, period, t, value]``)
|
|
728
|
+
the helper now broadcasts:
|
|
729
|
+
* 1d_map(time) — ``[name, t, value]`` → cross-join with periods.
|
|
730
|
+
* 1d_map(period) — ``[name, period, value]`` → cross-join with t.
|
|
731
|
+
* scalar — ``[name, value]`` → cross-join with (period, t).
|
|
732
|
+
Mirrors flextool preprocessing which writes one row per (entity,
|
|
733
|
+
period, t) into the ``pdtX.csv`` family.
|
|
734
|
+
|
|
735
|
+
Broadcast requires ``period_filter`` to carry the (d, t) pairs to
|
|
736
|
+
expand against (typically the active solve's ``dt`` frame). Without
|
|
737
|
+
it the broadcast paths fall through to ``None``.
|
|
738
|
+
"""
|
|
739
|
+
try:
|
|
740
|
+
df = source.parameter_explicit(entity_class, parameter_name)
|
|
741
|
+
except (KeyError, AttributeError):
|
|
742
|
+
try:
|
|
743
|
+
df = source.parameter(entity_class, parameter_name)
|
|
744
|
+
except KeyError:
|
|
745
|
+
return None
|
|
746
|
+
if df is None or df.height == 0:
|
|
747
|
+
return None
|
|
748
|
+
cols = df.columns
|
|
749
|
+
if "value" not in cols or "name" not in cols:
|
|
750
|
+
return None
|
|
751
|
+
has_period = "period" in cols
|
|
752
|
+
has_t = "t" in cols
|
|
753
|
+
if has_period and has_t:
|
|
754
|
+
lf = (df.lazy()
|
|
755
|
+
.pipe(rename_to_axis, {"name": entity_dim, "period": "d"})
|
|
756
|
+
.filter(pl.col("value").is_not_null()))
|
|
757
|
+
if filter_zero:
|
|
758
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
759
|
+
if period_filter is not None and period_filter.height > 0:
|
|
760
|
+
lf = lf.join(period_filter.lazy().select("d").unique(), on="d",
|
|
761
|
+
how="inner")
|
|
762
|
+
out = lf.select(entity_dim, "d", "t", "value").collect()
|
|
763
|
+
if out.height == 0:
|
|
764
|
+
return None
|
|
765
|
+
return Param((entity_dim, "d", "t"), out.lazy())
|
|
766
|
+
# Broadcast paths require period_filter to know the (d, t) axis.
|
|
767
|
+
if period_filter is None or period_filter.height == 0:
|
|
768
|
+
return None
|
|
769
|
+
pf_cols = set(period_filter.columns)
|
|
770
|
+
if not {"d", "t"}.issubset(pf_cols):
|
|
771
|
+
return None
|
|
772
|
+
lf = df.lazy().pipe(rename_to_axis, {"name": entity_dim}).filter(
|
|
773
|
+
pl.col("value").is_not_null())
|
|
774
|
+
if filter_zero:
|
|
775
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
776
|
+
dt_lf = period_filter.lazy().select("d", "t").unique()
|
|
777
|
+
if has_period:
|
|
778
|
+
# 1d_map(period) → broadcast across t per (entity, d).
|
|
779
|
+
lf2 = lf.pipe(rename_to_axis, {"period": "d"}).select(entity_dim, "d", "value")
|
|
780
|
+
out = (lf2.join(dt_lf, on="d", how="inner")
|
|
781
|
+
.select(entity_dim, "d", "t", "value")
|
|
782
|
+
.collect())
|
|
783
|
+
elif has_t:
|
|
784
|
+
# 1d_map(time) → broadcast across d per (entity, t).
|
|
785
|
+
lf2 = lf.select(entity_dim, "t", "value")
|
|
786
|
+
out = (lf2.join(dt_lf, on="t", how="inner")
|
|
787
|
+
.select(entity_dim, "d", "t", "value")
|
|
788
|
+
.collect())
|
|
789
|
+
else:
|
|
790
|
+
# Scalar → broadcast across (d, t) per entity.
|
|
791
|
+
lf2 = lf.select(entity_dim, "value")
|
|
792
|
+
out = (lf2.join(dt_lf, how="cross")
|
|
793
|
+
.select(entity_dim, "d", "t", "value")
|
|
794
|
+
.collect())
|
|
795
|
+
if out.height == 0:
|
|
796
|
+
return None
|
|
797
|
+
return Param((entity_dim, "d", "t"), out.lazy())
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
# §5.14 — group scalars sliced from input/p_group.csv
|
|
801
|
+
def _g_scalar(source: "InputSource", parameter_name: str,
|
|
802
|
+
*, filter_zero: bool = True,
|
|
803
|
+
entity_class: str = "group") -> Param | None:
|
|
804
|
+
"""Group scalar Param ``Param(("g",), [g, value])``, dropping zero
|
|
805
|
+
rows by default (mirrors ``_slice_pgroup``'s ``!= 0`` filter).
|
|
806
|
+
|
|
807
|
+
``entity_class`` defaults to ``"group"`` (all non-flow callers); the
|
|
808
|
+
v58 cumulative-flow callers pass ``"flowGroup"`` since those four
|
|
809
|
+
flow params now live on the dedicated ``flowGroup`` class.
|
|
810
|
+
"""
|
|
811
|
+
try:
|
|
812
|
+
df = source.parameter_explicit(entity_class, parameter_name)
|
|
813
|
+
except (KeyError, AttributeError):
|
|
814
|
+
try:
|
|
815
|
+
df = source.parameter(entity_class, parameter_name)
|
|
816
|
+
except KeyError:
|
|
817
|
+
return None
|
|
818
|
+
if df is None or df.height == 0:
|
|
819
|
+
return None
|
|
820
|
+
if "value" not in df.columns:
|
|
821
|
+
return None
|
|
822
|
+
lf = df.lazy().pipe(rename_to_axis, {"name": "g"}).filter(pl.col("value").is_not_null())
|
|
823
|
+
if filter_zero:
|
|
824
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
825
|
+
# Indexed (period) shapes don't belong here — guard.
|
|
826
|
+
if "period" in df.columns or "t" in df.columns:
|
|
827
|
+
return None
|
|
828
|
+
out = lf.select("g", "value").collect()
|
|
829
|
+
if out.height == 0:
|
|
830
|
+
return None
|
|
831
|
+
return Param(("g",), out.lazy())
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# §1.16 — pdGroup_* (1d_map period) — dropping zeros and nulls
|
|
835
|
+
#
|
|
836
|
+
# Δ.17c-Tier1 — silent-default index migration. These helpers were
|
|
837
|
+
# previously routed through ``_entity_period_scalar``, which keyed the
|
|
838
|
+
# scalar/explicit branch off ``"period" not in cols``. When the source
|
|
839
|
+
# DB authored the Map without ``index_name`` (spinedb_api's silent
|
|
840
|
+
# default ``"x"``), the helper fell into the scalar branch and
|
|
841
|
+
# cross-joined the Map's rows against the period filter — producing
|
|
842
|
+
# ``N_map_rows × N_periods`` duplicate (g, d) keys per entity and
|
|
843
|
+
# destroying the period → value association.
|
|
844
|
+
#
|
|
845
|
+
# Migration template: ``resolve_param_shape`` reads the DB-side shape
|
|
846
|
+
# (with silent-default disambiguation via ``_infer_silent_default_labels``
|
|
847
|
+
# — the {SCALAR, MAP_PERIOD} allow-list resolves the silent default
|
|
848
|
+
# structurally at depth 1) and ``broadcast_to_period`` produces the
|
|
849
|
+
# (entity, d) Param. ``filter_zero=True`` is supported natively by
|
|
850
|
+
# ``broadcast_to_period``, mirroring the legacy
|
|
851
|
+
# ``filter_null=True, filter_zero=True`` semantic.
|
|
852
|
+
def pdGroup_capacity_margin_from_source(source: "InputSource",
|
|
853
|
+
period_filter: pl.DataFrame | None = None,
|
|
854
|
+
) -> Param | None:
|
|
855
|
+
"""``group.capacity_margin`` → ``Param(("g","d"))``.
|
|
856
|
+
|
|
857
|
+
Δ.17c-Tier1 — uses :func:`._param_shapes.resolve_param_shape`.
|
|
858
|
+
Allowed shapes: scalar / 1d_map[period]. CSV path drops zero
|
|
859
|
+
rows; we mirror via ``filter_zero=True``.
|
|
860
|
+
"""
|
|
861
|
+
resolved = resolve_param_shape(
|
|
862
|
+
source, "group", "capacity_margin", period_filter=period_filter)
|
|
863
|
+
return broadcast_to_period(
|
|
864
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def pdGroup_penalty_capacity_margin_from_source(source: "InputSource",
|
|
868
|
+
period_filter: pl.DataFrame | None = None,
|
|
869
|
+
) -> Param | None:
|
|
870
|
+
"""``group.penalty_capacity_margin`` → ``Param(("g","d"))``.
|
|
871
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
872
|
+
"""
|
|
873
|
+
resolved = resolve_param_shape(
|
|
874
|
+
source, "group", "penalty_capacity_margin",
|
|
875
|
+
period_filter=period_filter)
|
|
876
|
+
return broadcast_to_period(
|
|
877
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def pdGroup_inertia_limit_from_source(source: "InputSource",
|
|
881
|
+
period_filter: pl.DataFrame | None = None,
|
|
882
|
+
) -> Param | None:
|
|
883
|
+
"""``group.inertia_limit`` → ``Param(("g","d"))``.
|
|
884
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
885
|
+
"""
|
|
886
|
+
resolved = resolve_param_shape(
|
|
887
|
+
source, "group", "inertia_limit", period_filter=period_filter)
|
|
888
|
+
return broadcast_to_period(
|
|
889
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def pdGroup_penalty_inertia_from_source(source: "InputSource",
|
|
893
|
+
period_filter: pl.DataFrame | None = None,
|
|
894
|
+
) -> Param | None:
|
|
895
|
+
"""``group.penalty_inertia`` → ``Param(("g","d"))``.
|
|
896
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
897
|
+
"""
|
|
898
|
+
resolved = resolve_param_shape(
|
|
899
|
+
source, "group", "penalty_inertia", period_filter=period_filter)
|
|
900
|
+
return broadcast_to_period(
|
|
901
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def pdGroup_non_synchronous_limit_from_source(source: "InputSource",
|
|
905
|
+
period_filter: pl.DataFrame | None = None,
|
|
906
|
+
) -> Param | None:
|
|
907
|
+
"""``group.non_synchronous_limit`` → ``Param(("g","d"))``.
|
|
908
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
909
|
+
"""
|
|
910
|
+
resolved = resolve_param_shape(
|
|
911
|
+
source, "group", "non_synchronous_limit",
|
|
912
|
+
period_filter=period_filter)
|
|
913
|
+
return broadcast_to_period(
|
|
914
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def pdGroup_penalty_non_synchronous_from_source(source: "InputSource",
|
|
918
|
+
period_filter: pl.DataFrame | None = None,
|
|
919
|
+
) -> Param | None:
|
|
920
|
+
"""``group.penalty_non_synchronous`` → ``Param(("g","d"))``.
|
|
921
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
922
|
+
"""
|
|
923
|
+
resolved = resolve_param_shape(
|
|
924
|
+
source, "group", "penalty_non_synchronous",
|
|
925
|
+
period_filter=period_filter)
|
|
926
|
+
return broadcast_to_period(
|
|
927
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
# §1.16 — group invest/divest 1d_map(period) — drop zeros to match CSV
|
|
931
|
+
def p_group_invest_max_period_from_source(source: "InputSource",
|
|
932
|
+
period_filter: pl.DataFrame | None = None,
|
|
933
|
+
) -> Param | None:
|
|
934
|
+
"""``group.invest_max_period`` → ``Param(("g","d"))``.
|
|
935
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
936
|
+
"""
|
|
937
|
+
resolved = resolve_param_shape(
|
|
938
|
+
source, "group", "invest_max_period", period_filter=period_filter)
|
|
939
|
+
return broadcast_to_period(
|
|
940
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def p_group_invest_min_period_from_source(source: "InputSource",
|
|
944
|
+
period_filter: pl.DataFrame | None = None,
|
|
945
|
+
) -> Param | None:
|
|
946
|
+
"""``group.invest_min_period`` → ``Param(("g","d"))``.
|
|
947
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
948
|
+
"""
|
|
949
|
+
resolved = resolve_param_shape(
|
|
950
|
+
source, "group", "invest_min_period", period_filter=period_filter)
|
|
951
|
+
return broadcast_to_period(
|
|
952
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def p_group_retire_max_period_from_source(source: "InputSource",
|
|
956
|
+
period_filter: pl.DataFrame | None = None,
|
|
957
|
+
) -> Param | None:
|
|
958
|
+
"""``group.retire_max_period`` → ``Param(("g","d"))``.
|
|
959
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
960
|
+
"""
|
|
961
|
+
resolved = resolve_param_shape(
|
|
962
|
+
source, "group", "retire_max_period", period_filter=period_filter)
|
|
963
|
+
return broadcast_to_period(
|
|
964
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def p_group_retire_min_period_from_source(source: "InputSource",
|
|
968
|
+
period_filter: pl.DataFrame | None = None,
|
|
969
|
+
) -> Param | None:
|
|
970
|
+
"""``group.retire_min_period`` → ``Param(("g","d"))``.
|
|
971
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
972
|
+
"""
|
|
973
|
+
resolved = resolve_param_shape(
|
|
974
|
+
source, "group", "retire_min_period", period_filter=period_filter)
|
|
975
|
+
return broadcast_to_period(
|
|
976
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def pd_max_cumulative_flow_from_source(source: "InputSource",
|
|
980
|
+
period_filter: pl.DataFrame | None = None,
|
|
981
|
+
) -> Param | None:
|
|
982
|
+
"""``flowGroup.max_cumulative_flow`` → ``Param(("g","d"))``.
|
|
983
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
984
|
+
"""
|
|
985
|
+
resolved = resolve_param_shape(
|
|
986
|
+
source, "flowGroup", "max_cumulative_flow",
|
|
987
|
+
period_filter=period_filter)
|
|
988
|
+
return broadcast_to_period(
|
|
989
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def pd_min_cumulative_flow_from_source(source: "InputSource",
|
|
993
|
+
period_filter: pl.DataFrame | None = None,
|
|
994
|
+
) -> Param | None:
|
|
995
|
+
"""``flowGroup.min_cumulative_flow`` → ``Param(("g","d"))``.
|
|
996
|
+
Δ.17c-Tier1 — uses ``resolve_param_shape`` + ``broadcast_to_period``.
|
|
997
|
+
"""
|
|
998
|
+
resolved = resolve_param_shape(
|
|
999
|
+
source, "flowGroup", "min_cumulative_flow",
|
|
1000
|
+
period_filter=period_filter)
|
|
1001
|
+
return broadcast_to_period(
|
|
1002
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
# §1.16 — group scalar Direct Params (no period dimension)
|
|
1006
|
+
def p_group_invest_max_total_from_source(source: "InputSource") -> Param | None:
|
|
1007
|
+
return _g_scalar(source, "invest_max_total")
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def p_group_invest_min_total_from_source(source: "InputSource") -> Param | None:
|
|
1011
|
+
return _g_scalar(source, "invest_min_total")
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def p_group_retire_max_total_from_source(source: "InputSource") -> Param | None:
|
|
1015
|
+
return _g_scalar(source, "retire_max_total")
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def p_group_retire_min_total_from_source(source: "InputSource") -> Param | None:
|
|
1019
|
+
return _g_scalar(source, "retire_min_total")
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def p_group_invest_max_cumulative_from_source(source: "InputSource") -> Param | None:
|
|
1023
|
+
return _g_scalar(source, "invest_max_cumulative")
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def p_group_invest_min_cumulative_from_source(source: "InputSource") -> Param | None:
|
|
1027
|
+
return _g_scalar(source, "invest_min_cumulative")
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def p_group_max_cumulative_flow_from_source(source: "InputSource") -> Param | None:
|
|
1031
|
+
return _g_scalar(source, "max_cumulative_flow", entity_class="flowGroup")
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def p_group_min_cumulative_flow_from_source(source: "InputSource") -> Param | None:
|
|
1035
|
+
return _g_scalar(source, "min_cumulative_flow", entity_class="flowGroup")
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
# §1.16 — pdtGroup (Map period→time) instant flow caps
|
|
1039
|
+
def pdt_max_instant_flow_from_source(source: "InputSource",
|
|
1040
|
+
period_filter: pl.DataFrame | None = None,
|
|
1041
|
+
) -> Param | None:
|
|
1042
|
+
"""``flowGroup.max_instant_flow`` → ``Param(("g", "d", "t"))``.
|
|
1043
|
+
|
|
1044
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1045
|
+
shapes: scalar / 1d_map[period] / 1d_map[time] / 2d_map[period,time].
|
|
1046
|
+
"""
|
|
1047
|
+
resolved = resolve_param_shape(
|
|
1048
|
+
source, "flowGroup", "max_instant_flow", period_filter=period_filter)
|
|
1049
|
+
return broadcast_to_period_time(
|
|
1050
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def pdt_min_instant_flow_from_source(source: "InputSource",
|
|
1054
|
+
period_filter: pl.DataFrame | None = None,
|
|
1055
|
+
) -> Param | None:
|
|
1056
|
+
"""``flowGroup.min_instant_flow`` → ``Param(("g", "d", "t"))``.
|
|
1057
|
+
|
|
1058
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1059
|
+
shapes: scalar / 1d_map[period] / 1d_map[time] / 2d_map[period,time].
|
|
1060
|
+
"""
|
|
1061
|
+
resolved = resolve_param_shape(
|
|
1062
|
+
source, "flowGroup", "min_instant_flow", period_filter=period_filter)
|
|
1063
|
+
return broadcast_to_period_time(
|
|
1064
|
+
resolved, "g", period_filter, filter_zero=True)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
# ---------------------------------------------------------------------------
|
|
1068
|
+
# Multi-class union helpers — params declared on each of unit/node/connection
|
|
1069
|
+
# (or unit/connection) and merged into one frame keyed on the generic
|
|
1070
|
+
# ``e`` (entity) or ``p`` (process) dim.
|
|
1071
|
+
|
|
1072
|
+
def _e_period_param_union(source: "InputSource",
|
|
1073
|
+
parameter_name: str,
|
|
1074
|
+
*,
|
|
1075
|
+
classes: tuple[str, ...] = ("unit", "node",
|
|
1076
|
+
"connection"),
|
|
1077
|
+
filter_zero: bool = False,
|
|
1078
|
+
period_filter: pl.DataFrame | None = None
|
|
1079
|
+
) -> Param | None:
|
|
1080
|
+
"""Union ``parameter_name`` across the supplied entity classes into a
|
|
1081
|
+
single ``Param(("e", "d"), [...])`` frame.
|
|
1082
|
+
|
|
1083
|
+
Δ.17c-Tier2 — each per-class call routes through
|
|
1084
|
+
:func:`._param_shapes.resolve_param_shape` +
|
|
1085
|
+
:func:`._param_shapes.broadcast_to_period`. This:
|
|
1086
|
+
|
|
1087
|
+
* carries silent-default ``index_name`` disambiguation
|
|
1088
|
+
(spinedb_api's ``"x"`` resolves structurally via the
|
|
1089
|
+
``{SCALAR, MAP_PERIOD}`` allow-list at depth 1; value-domain
|
|
1090
|
+
probing kicks in if the labels are still ambiguous), and
|
|
1091
|
+
* promotes scalar-authored parameters to ``(e, d)`` via the
|
|
1092
|
+
period filter (the pre-migration path silently dropped these
|
|
1093
|
+
because ``period_col`` was ``None``).
|
|
1094
|
+
|
|
1095
|
+
Each per-class ``broadcast_to_period`` call aliases the source's
|
|
1096
|
+
``name`` column to the union ``e`` axis (``cast_frame_axes``
|
|
1097
|
+
resolves ``"e"`` to the entity-union Enum), so the per-class
|
|
1098
|
+
Param frames concat cleanly along the union vocabulary.
|
|
1099
|
+
|
|
1100
|
+
``period_filter`` is REQUIRED for scalar-authored sources (the
|
|
1101
|
+
SCALAR branch in :func:`broadcast_to_period` cross-joins with the
|
|
1102
|
+
period axis to emit ``(e, d)``). Callers that don't have a period
|
|
1103
|
+
filter on hand should default to passing the active solve's ``dt``
|
|
1104
|
+
frame, mirroring Phase 1's pdGroup pattern.
|
|
1105
|
+
"""
|
|
1106
|
+
parts: list[pl.LazyFrame] = []
|
|
1107
|
+
for cls in classes:
|
|
1108
|
+
resolved = resolve_param_shape(
|
|
1109
|
+
source, cls, parameter_name, period_filter=period_filter)
|
|
1110
|
+
part = broadcast_to_period(
|
|
1111
|
+
resolved, "e", period_filter, filter_zero=filter_zero)
|
|
1112
|
+
if part is None:
|
|
1113
|
+
continue
|
|
1114
|
+
# ``broadcast_to_period`` returns a Param whose dims are either
|
|
1115
|
+
# ``(e, d)`` (MAP_PERIOD or SCALAR-broadcast) or ``(e,)`` (SCALAR
|
|
1116
|
+
# with no period_filter — guarded against above). For the
|
|
1117
|
+
# Tier-2 union we require the (e, d) shape; the SCALAR-without-
|
|
1118
|
+
# period-filter case is caller error (helpers below always pass
|
|
1119
|
+
# period_filter=dt).
|
|
1120
|
+
if part.dims != ("e", "d"):
|
|
1121
|
+
raise FlexToolConfigError(
|
|
1122
|
+
f"_e_period_param_union expected ('e', 'd') dims from "
|
|
1123
|
+
f"class {cls!r} on parameter {parameter_name!r}; got "
|
|
1124
|
+
f"{part.dims}. Pass a non-empty period_filter when "
|
|
1125
|
+
"calling the ed_* helpers."
|
|
1126
|
+
)
|
|
1127
|
+
lf = part.frame
|
|
1128
|
+
if not isinstance(lf, pl.LazyFrame):
|
|
1129
|
+
lf = lf.lazy()
|
|
1130
|
+
parts.append(lf.select("e", "d", "value"))
|
|
1131
|
+
if not parts:
|
|
1132
|
+
return None
|
|
1133
|
+
out = pl.concat(parts).collect()
|
|
1134
|
+
if out.height == 0:
|
|
1135
|
+
return None
|
|
1136
|
+
return Param(("e", "d"), out.lazy().sort("e", "d"))
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def ed_invest_max_period_from_source(source: "InputSource",
|
|
1140
|
+
period_filter: pl.DataFrame | None = None,
|
|
1141
|
+
) -> Param | None:
|
|
1142
|
+
"""``unit/node/connection.invest_max_period`` 1d_map(period) →
|
|
1143
|
+
``Param(("e", "d"))``. CSV path keeps zero rows (per
|
|
1144
|
+
``_read_period_cap``); we preserve that.
|
|
1145
|
+
|
|
1146
|
+
Δ.17c-Tier2 — uses ``resolve_param_shape`` + ``broadcast_to_period``
|
|
1147
|
+
per class via :func:`_e_period_param_union`.
|
|
1148
|
+
"""
|
|
1149
|
+
return _e_period_param_union(source, "invest_max_period",
|
|
1150
|
+
filter_zero=False,
|
|
1151
|
+
period_filter=period_filter)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def ed_divest_max_period_from_source(source: "InputSource",
|
|
1155
|
+
period_filter: pl.DataFrame | None = None,
|
|
1156
|
+
) -> Param | None:
|
|
1157
|
+
"""Δ.17c-Tier2 — see :func:`ed_invest_max_period_from_source`."""
|
|
1158
|
+
return _e_period_param_union(source, "retire_max_period",
|
|
1159
|
+
filter_zero=False,
|
|
1160
|
+
period_filter=period_filter)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def ed_invest_min_period_from_source(source: "InputSource",
|
|
1164
|
+
period_filter: pl.DataFrame | None = None,
|
|
1165
|
+
) -> Param | None:
|
|
1166
|
+
"""CSV path drops zero rows (``_read_e_d_param``).
|
|
1167
|
+
Δ.17c-Tier2 — see :func:`ed_invest_max_period_from_source`.
|
|
1168
|
+
"""
|
|
1169
|
+
return _e_period_param_union(source, "invest_min_period",
|
|
1170
|
+
filter_zero=True,
|
|
1171
|
+
period_filter=period_filter)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def ed_divest_min_period_from_source(source: "InputSource",
|
|
1175
|
+
period_filter: pl.DataFrame | None = None,
|
|
1176
|
+
) -> Param | None:
|
|
1177
|
+
"""Δ.17c-Tier2 — see :func:`ed_invest_max_period_from_source`."""
|
|
1178
|
+
return _e_period_param_union(source, "retire_min_period",
|
|
1179
|
+
filter_zero=True,
|
|
1180
|
+
period_filter=period_filter)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def ed_cumulative_max_capacity_from_source(source: "InputSource",
|
|
1184
|
+
period_filter: pl.DataFrame | None = None,
|
|
1185
|
+
) -> Param | None:
|
|
1186
|
+
"""Δ.17c-Tier2 — see :func:`ed_invest_max_period_from_source`."""
|
|
1187
|
+
return _e_period_param_union(source, "cumulative_max_capacity",
|
|
1188
|
+
filter_zero=True,
|
|
1189
|
+
period_filter=period_filter)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def ed_cumulative_min_capacity_from_source(source: "InputSource",
|
|
1193
|
+
period_filter: pl.DataFrame | None = None,
|
|
1194
|
+
) -> Param | None:
|
|
1195
|
+
"""Δ.17c-Tier2 — see :func:`ed_invest_max_period_from_source`."""
|
|
1196
|
+
return _e_period_param_union(source, "cumulative_min_capacity",
|
|
1197
|
+
filter_zero=True,
|
|
1198
|
+
period_filter=period_filter)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
# ---------------------------------------------------------------------------
|
|
1202
|
+
# §1.9, §1.14 — relationship scalars (sink / source side params).
|
|
1203
|
+
#
|
|
1204
|
+
# CSV path (``_read_p_process_side``) reads ``input/p_process_sink.csv`` /
|
|
1205
|
+
# ``input/p_process_source.csv`` and filters ``value != 0``. We mirror.
|
|
1206
|
+
|
|
1207
|
+
def _p_side_scalar(source: "InputSource", entity_class: str,
|
|
1208
|
+
parameter_name: str, side_dim: str,
|
|
1209
|
+
entity_col: str = "unit",
|
|
1210
|
+
*,
|
|
1211
|
+
filter_zero: bool = True) -> Param | None:
|
|
1212
|
+
"""``unit__inputNode`` / ``unit__outputNode`` per-(p, side) scalar.
|
|
1213
|
+
``entity_col`` is the Spine column name for the unit ('unit' or
|
|
1214
|
+
'connection'); ``side_dim`` is 'source' or 'sink'.
|
|
1215
|
+
"""
|
|
1216
|
+
try:
|
|
1217
|
+
df = source.parameter_explicit(entity_class, parameter_name)
|
|
1218
|
+
except (KeyError, AttributeError):
|
|
1219
|
+
try:
|
|
1220
|
+
df = source.parameter(entity_class, parameter_name)
|
|
1221
|
+
except KeyError:
|
|
1222
|
+
return None
|
|
1223
|
+
if df is None or df.height == 0:
|
|
1224
|
+
return None
|
|
1225
|
+
if "value" not in df.columns or entity_col not in df.columns \
|
|
1226
|
+
or "node" not in df.columns:
|
|
1227
|
+
return None
|
|
1228
|
+
lf = (df.lazy()
|
|
1229
|
+
.rename({entity_col: "p", "node": side_dim})
|
|
1230
|
+
.filter(pl.col("value").is_not_null()))
|
|
1231
|
+
if filter_zero:
|
|
1232
|
+
lf = lf.filter(pl.col("value") != 0.0)
|
|
1233
|
+
out = lf.select("p", side_dim, "value").collect()
|
|
1234
|
+
if out.height == 0:
|
|
1235
|
+
return None
|
|
1236
|
+
return Param(("p", side_dim), out.lazy())
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
# ramp_speed (§1.9) — relationship scalar, CSV filters zero
|
|
1240
|
+
def p_ramp_speed_up_sink_from_source(source: "InputSource") -> Param | None:
|
|
1241
|
+
return _p_side_scalar(source, "unit__outputNode", "ramp_speed_up", "sink")
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def p_ramp_speed_down_sink_from_source(source: "InputSource") -> Param | None:
|
|
1245
|
+
return _p_side_scalar(source, "unit__outputNode", "ramp_speed_down", "sink")
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def p_ramp_speed_up_source_from_source(source: "InputSource") -> Param | None:
|
|
1249
|
+
return _p_side_scalar(source, "unit__inputNode", "ramp_speed_up", "source")
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def p_ramp_speed_down_source_from_source(source: "InputSource") -> Param | None:
|
|
1253
|
+
return _p_side_scalar(source, "unit__inputNode", "ramp_speed_down", "source")
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def p_process_sink_min_capacity_coef_from_source(source: "InputSource") -> Param | None:
|
|
1257
|
+
"""``unit__outputNode.capacity_min_coeff`` → ``Param(("p", "sink"))``.
|
|
1258
|
+
|
|
1259
|
+
Default 1.0 (schema). Per-output-arc multiplier on the
|
|
1260
|
+
``minFlow_minload`` floor (``v_online · min_load``), mirroring the
|
|
1261
|
+
.mod's ``p_process_sink_min_capacity_coefficient`` (flextool.mod
|
|
1262
|
+
L3075). Explicit rows only, INCLUDING an authored ``0.0`` ("no floor
|
|
1263
|
+
on this arc") — ``filter_zero=False`` keeps it; the consumer densifies
|
|
1264
|
+
absent arcs to the 1.0 default.
|
|
1265
|
+
"""
|
|
1266
|
+
return _p_side_scalar(source, "unit__outputNode", "capacity_min_coeff",
|
|
1267
|
+
"sink", filter_zero=False)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
# inertia_constant (§1.14) — relationship scalar, CSV filters zero
|
|
1271
|
+
def p_process_sink_inertia_constant_from_source(source: "InputSource") -> Param | None:
|
|
1272
|
+
return _p_side_scalar(source, "unit__outputNode", "inertia_constant", "sink")
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def p_process_source_inertia_constant_from_source(source: "InputSource") -> Param | None:
|
|
1276
|
+
return _p_side_scalar(source, "unit__inputNode", "inertia_constant", "source")
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
# ---------------------------------------------------------------------------
|
|
1280
|
+
# §1.10 — UC: startup_cost (1d_map period)
|
|
1281
|
+
|
|
1282
|
+
def p_startup_cost_from_source(source: "InputSource",
|
|
1283
|
+
period_filter: pl.DataFrame | None = None,
|
|
1284
|
+
) -> Param | None:
|
|
1285
|
+
"""``unit.startup_cost`` → ``Param(("p", "d"))``.
|
|
1286
|
+
|
|
1287
|
+
Δ.17c-Tier1 — uses :func:`._param_shapes.resolve_param_shape`.
|
|
1288
|
+
Allowed shapes: scalar / 1d_map[period]. CSV path filters zero rows
|
|
1289
|
+
(``_load_online``: ``filter(value != 0)``); we mirror via
|
|
1290
|
+
``filter_zero=True``.
|
|
1291
|
+
"""
|
|
1292
|
+
resolved = resolve_param_shape(
|
|
1293
|
+
source, "unit", "startup_cost", period_filter=period_filter)
|
|
1294
|
+
return broadcast_to_period(
|
|
1295
|
+
resolved, "p", period_filter, filter_zero=True)
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
# ---------------------------------------------------------------------------
|
|
1299
|
+
# §1.11 — Storage: Map(period→time) parameters
|
|
1300
|
+
|
|
1301
|
+
def p_node_availability_from_source(source: "InputSource",
|
|
1302
|
+
period_filter: pl.DataFrame | None = None,
|
|
1303
|
+
) -> Param | None:
|
|
1304
|
+
"""``node.availability`` → ``Param(("n", "d", "t"))``.
|
|
1305
|
+
|
|
1306
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1307
|
+
shapes: scalar / 1d_map[period] / 1d_map[time] / 2d_map[period,time].
|
|
1308
|
+
CSV path slices ``pdtNode.csv`` and filters to nodeState entities at
|
|
1309
|
+
the apply site; the helper here returns ALL availability rows. The
|
|
1310
|
+
nodeState filter is applied downstream by ``_load_storage``.
|
|
1311
|
+
"""
|
|
1312
|
+
resolved = resolve_param_shape(
|
|
1313
|
+
source, "node", "availability", period_filter=period_filter)
|
|
1314
|
+
return broadcast_to_period_time(resolved, "n", period_filter)
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def p_storage_state_reference_value_from_source(source: "InputSource",
|
|
1318
|
+
period_filter: pl.DataFrame | None = None,
|
|
1319
|
+
) -> Param | None:
|
|
1320
|
+
"""``node.storage_state_reference_value`` → ``Param(("n", "d", "t"))``.
|
|
1321
|
+
|
|
1322
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1323
|
+
shapes: scalar / 1d_map[period] / 1d_map[time] / 2d_map[period,time].
|
|
1324
|
+
CSV path (``_load_storage``) gates on ``use_reference_value`` set;
|
|
1325
|
+
the gate stays on the consumer side.
|
|
1326
|
+
"""
|
|
1327
|
+
resolved = resolve_param_shape(
|
|
1328
|
+
source, "node", "storage_state_reference_value",
|
|
1329
|
+
period_filter=period_filter)
|
|
1330
|
+
return broadcast_to_period_time(resolved, "n", period_filter)
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
# ---------------------------------------------------------------------------
|
|
1334
|
+
# §1.4 — CO2: group price + period cap
|
|
1335
|
+
|
|
1336
|
+
def p_co2_price_from_source(source: "InputSource",
|
|
1337
|
+
period_filter: pl.DataFrame | None = None,
|
|
1338
|
+
) -> Param | None:
|
|
1339
|
+
"""``group.co2_price`` → ``Param(("g", "d", "t"))``.
|
|
1340
|
+
|
|
1341
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape` to discover
|
|
1342
|
+
the actual shape from the DB and validate against the per-parameter
|
|
1343
|
+
allow-list (scalar / 1d_map[period] / 1d_map[time] /
|
|
1344
|
+
2d_map[period,time]). CSV path slices ``pdtGroup.csv``
|
|
1345
|
+
(param='co2_price'). None default on the schema — explicit rows
|
|
1346
|
+
only.
|
|
1347
|
+
"""
|
|
1348
|
+
resolved = resolve_param_shape(
|
|
1349
|
+
source, "group", "co2_price", period_filter=period_filter)
|
|
1350
|
+
return broadcast_to_period_time(resolved, "g", period_filter)
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def p_co2_max_period_from_source(source: "InputSource",
|
|
1354
|
+
period_filter: pl.DataFrame | None = None,
|
|
1355
|
+
) -> Param | None:
|
|
1356
|
+
"""``group.co2_max_period`` → ``Param(("g", "d"))``.
|
|
1357
|
+
|
|
1358
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1359
|
+
shapes: scalar / 1d_map[period]. CSV path keys this off
|
|
1360
|
+
``inp/pd_group.csv`` slice.
|
|
1361
|
+
|
|
1362
|
+
Gate-coupled note: at the consumer site this Param is only emitted
|
|
1363
|
+
into the LP when the (g, c, n) join with ``commodity_node_co2`` ×
|
|
1364
|
+
``group__node`` is non-empty (see ``_load_co2_cap``). We return the
|
|
1365
|
+
Param unconditionally — the consumer decides whether to wire it.
|
|
1366
|
+
"""
|
|
1367
|
+
resolved = resolve_param_shape(
|
|
1368
|
+
source, "group", "co2_max_period", period_filter=period_filter)
|
|
1369
|
+
return broadcast_to_period(resolved, "g", period_filter)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
# ---------------------------------------------------------------------------
|
|
1373
|
+
# §1.12 — Variable cost (other_operational_cost) Map(period→time)
|
|
1374
|
+
|
|
1375
|
+
def p_pdt_varCost_source_from_source(
|
|
1376
|
+
source: "InputSource",
|
|
1377
|
+
period_filter: pl.DataFrame | None = None,
|
|
1378
|
+
) -> Param | None:
|
|
1379
|
+
"""``unit__inputNode.other_operational_cost`` Map(period→time) →
|
|
1380
|
+
``Param(("p", "source", "d", "t"))``. CSV path filters zero.
|
|
1381
|
+
|
|
1382
|
+
Δ.17c-Tier3 — routes through ``resolve_param_shape`` +
|
|
1383
|
+
``broadcast_to_period_time`` to carry silent-default ``index_name``
|
|
1384
|
+
disambiguation; the per-source-column rename dict maps the
|
|
1385
|
+
relationship class's ``(unit, node)`` columns to ``(p, source)``.
|
|
1386
|
+
"""
|
|
1387
|
+
resolved = resolve_param_shape(
|
|
1388
|
+
source, "unit__inputNode", "other_operational_cost",
|
|
1389
|
+
period_filter=period_filter)
|
|
1390
|
+
return broadcast_to_period_time(
|
|
1391
|
+
resolved, {"unit": "p", "node": "source"},
|
|
1392
|
+
period_filter, filter_zero=True)
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def p_pdt_varCost_sink_from_source(
|
|
1396
|
+
source: "InputSource",
|
|
1397
|
+
period_filter: pl.DataFrame | None = None,
|
|
1398
|
+
) -> Param | None:
|
|
1399
|
+
"""``unit__outputNode.other_operational_cost`` Map(period→time) →
|
|
1400
|
+
``Param(("p", "sink", "d", "t"))``.
|
|
1401
|
+
|
|
1402
|
+
Δ.17c-Tier3 — see :func:`p_pdt_varCost_source_from_source`.
|
|
1403
|
+
"""
|
|
1404
|
+
resolved = resolve_param_shape(
|
|
1405
|
+
source, "unit__outputNode", "other_operational_cost",
|
|
1406
|
+
period_filter=period_filter)
|
|
1407
|
+
return broadcast_to_period_time(
|
|
1408
|
+
resolved, {"unit": "p", "node": "sink"},
|
|
1409
|
+
period_filter, filter_zero=True)
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def p_pdt_varCost_process_from_source(
|
|
1413
|
+
source: "InputSource",
|
|
1414
|
+
period_filter: pl.DataFrame | None = None,
|
|
1415
|
+
) -> Param | None:
|
|
1416
|
+
"""``connection.other_operational_cost`` Map(period→time) →
|
|
1417
|
+
``Param(("p", "d", "t"))``. Single-class helper.
|
|
1418
|
+
|
|
1419
|
+
Δ.17c-Tier3 — the call routes through ``resolve_param_shape`` +
|
|
1420
|
+
``broadcast_to_period_time``. Pre-migration the helper gated on
|
|
1421
|
+
``{"name", "period", "t"}.issubset``, silently dropping silent-
|
|
1422
|
+
default-Map authoring (column ``"x"`` not ``"period"``) and
|
|
1423
|
+
SCALAR / MAP_PERIOD / MAP_TIME shapes.
|
|
1424
|
+
|
|
1425
|
+
SCALAR / MAP_PERIOD / MAP_TIME branches return Params with fewer
|
|
1426
|
+
dims than the canonical ``(p, d, t)``. Results are broadcast up
|
|
1427
|
+
to ``(p, d, t)`` against ``period_filter``'s ``(d, t)`` axis so
|
|
1428
|
+
the output frame is dim-uniform. Callers (the dispatcher) always
|
|
1429
|
+
pass a non-empty ``period_filter``; if the Param is a strict
|
|
1430
|
+
subset of ``(p, d, t)`` AND no period_filter is on hand, we raise
|
|
1431
|
+
rather than emit an incompatible-shape frame.
|
|
1432
|
+
"""
|
|
1433
|
+
parts: list[pl.LazyFrame] = []
|
|
1434
|
+
for cls in ("connection",):
|
|
1435
|
+
resolved = resolve_param_shape(
|
|
1436
|
+
source, cls, "other_operational_cost",
|
|
1437
|
+
period_filter=period_filter)
|
|
1438
|
+
part = broadcast_to_period_time(
|
|
1439
|
+
resolved, {"name": "p"}, period_filter, filter_zero=True)
|
|
1440
|
+
if part is None:
|
|
1441
|
+
continue
|
|
1442
|
+
lf = part.frame
|
|
1443
|
+
if not isinstance(lf, pl.LazyFrame):
|
|
1444
|
+
lf = lf.lazy()
|
|
1445
|
+
if part.dims == ("p", "d", "t"):
|
|
1446
|
+
parts.append(lf.select("p", "d", "t", "value"))
|
|
1447
|
+
continue
|
|
1448
|
+
# Lower-dim Param shapes (SCALAR / MAP_PERIOD / MAP_TIME) — the
|
|
1449
|
+
# union concat requires (p, d, t); broadcast the missing axes
|
|
1450
|
+
# against ``period_filter`` so the multi-class union stays
|
|
1451
|
+
# dim-uniform. Mirrors the legacy concat semantic (the inline
|
|
1452
|
+
# helper accepted only MAP_PERIOD_TIME-shaped sources because
|
|
1453
|
+
# of its column gate; Tier-3 widens that to the full allow-list
|
|
1454
|
+
# by broadcasting the lower-dim shapes here).
|
|
1455
|
+
if period_filter is None or period_filter.height == 0:
|
|
1456
|
+
raise FlexToolConfigError(
|
|
1457
|
+
f"p_pdt_varCost_process_from_source: class {cls!r} "
|
|
1458
|
+
f"resolved to Param dims {part.dims}; cannot broadcast "
|
|
1459
|
+
"to ('p', 'd', 't') without a period_filter. Pass "
|
|
1460
|
+
"period_filter=dt from the dispatcher.")
|
|
1461
|
+
if part.dims == ("p",):
|
|
1462
|
+
# SCALAR — cross-join with full (d, t).
|
|
1463
|
+
dt_lf = period_filter.lazy().select("d", "t").unique()
|
|
1464
|
+
lf = lf.join(dt_lf, how="cross").select("p", "d", "t", "value")
|
|
1465
|
+
elif part.dims == ("p", "d"):
|
|
1466
|
+
# MAP_PERIOD — cross-join across t within each d.
|
|
1467
|
+
t_lf = period_filter.lazy().select("d", "t").unique()
|
|
1468
|
+
lf = (lf.join(t_lf, on="d", how="inner")
|
|
1469
|
+
.select("p", "d", "t", "value"))
|
|
1470
|
+
elif part.dims == ("p", "t"):
|
|
1471
|
+
# MAP_TIME — cross-join across d within each t.
|
|
1472
|
+
d_lf = period_filter.lazy().select("d", "t").unique()
|
|
1473
|
+
lf = (lf.join(d_lf, on="t", how="inner")
|
|
1474
|
+
.select("p", "d", "t", "value"))
|
|
1475
|
+
else: # pragma: no cover — guarded by allow-list above.
|
|
1476
|
+
raise FlexToolConfigError(
|
|
1477
|
+
f"p_pdt_varCost_process_from_source: unexpected per-class "
|
|
1478
|
+
f"Param dims {part.dims}")
|
|
1479
|
+
parts.append(lf)
|
|
1480
|
+
if not parts:
|
|
1481
|
+
return None
|
|
1482
|
+
out = pl.concat(parts).collect()
|
|
1483
|
+
if out.height == 0:
|
|
1484
|
+
return None
|
|
1485
|
+
return Param(("p", "d", "t"), out.lazy().sort("p", "d", "t"))
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
# ---------------------------------------------------------------------------
|
|
1489
|
+
# §1.15 — Reserves
|
|
1490
|
+
|
|
1491
|
+
def pdtReserve_upDown_group_reservation_from_source(
|
|
1492
|
+
source: "InputSource",
|
|
1493
|
+
period_filter: pl.DataFrame | None = None,
|
|
1494
|
+
) -> Param | None:
|
|
1495
|
+
"""``reserve__upDown__group.reservation`` Map(period→time) →
|
|
1496
|
+
``Param(("r", "ud", "g", "d", "t"))``. Default 0.0 (schema) — CSV
|
|
1497
|
+
path emits explicit rows only (zero values are NOT filtered).
|
|
1498
|
+
|
|
1499
|
+
Δ.17c-Tier3 — routes through ``resolve_param_shape`` +
|
|
1500
|
+
``broadcast_to_period_time`` with a per-source-column rename dict
|
|
1501
|
+
mapping the 3-dim relationship class's ``(reserve, upDown, group)``
|
|
1502
|
+
columns to ``(r, ud, g)``. ``filter_zero=False`` because the
|
|
1503
|
+
schema default of 0.0 makes explicit zero rows authoring-
|
|
1504
|
+
meaningful (legacy CSV path preserved zero rows for this param).
|
|
1505
|
+
"""
|
|
1506
|
+
resolved = resolve_param_shape(
|
|
1507
|
+
source, "reserve__upDown__group", "reservation",
|
|
1508
|
+
period_filter=period_filter)
|
|
1509
|
+
return broadcast_to_period_time(
|
|
1510
|
+
resolved,
|
|
1511
|
+
{"reserve": "r", "upDown": "ud", "group": "g"},
|
|
1512
|
+
period_filter, filter_zero=False)
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def p_reserve_upDown_group_penalty_reserve_from_source(source: "InputSource") -> Param | None:
|
|
1516
|
+
"""``reserve__upDown__group.penalty_reserve`` scalar →
|
|
1517
|
+
``Param(("r", "ud", "g"))``. None default — explicit rows only.
|
|
1518
|
+
"""
|
|
1519
|
+
try:
|
|
1520
|
+
df = source.parameter_explicit("reserve__upDown__group", "penalty_reserve")
|
|
1521
|
+
except (KeyError, AttributeError):
|
|
1522
|
+
try:
|
|
1523
|
+
df = source.parameter("reserve__upDown__group", "penalty_reserve")
|
|
1524
|
+
except KeyError:
|
|
1525
|
+
return None
|
|
1526
|
+
if df is None or df.height == 0:
|
|
1527
|
+
return None
|
|
1528
|
+
cols = df.columns
|
|
1529
|
+
if not {"reserve", "upDown", "group", "value"}.issubset(cols):
|
|
1530
|
+
return None
|
|
1531
|
+
lf = (df.lazy()
|
|
1532
|
+
.rename({"reserve": "r", "upDown": "ud", "group": "g"})
|
|
1533
|
+
.filter(pl.col("value").is_not_null()))
|
|
1534
|
+
out = lf.select("r", "ud", "g", "value").collect()
|
|
1535
|
+
if out.height == 0:
|
|
1536
|
+
return None
|
|
1537
|
+
return Param(("r", "ud", "g"), out.lazy())
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _process_reserve_node_param(source: "InputSource",
|
|
1541
|
+
parameter_name: str) -> Param | None:
|
|
1542
|
+
"""Union ``reserve__upDown__unit__node`` ∪
|
|
1543
|
+
``reserve__upDown__connection__node`` for a per-(p, r, ud, n) scalar.
|
|
1544
|
+
|
|
1545
|
+
CSV path (``_read_p_process_reserve_node`` style) emits explicit rows
|
|
1546
|
+
only; we preserve via ``parameter_explicit``.
|
|
1547
|
+
"""
|
|
1548
|
+
parts: list[pl.LazyFrame] = []
|
|
1549
|
+
for cls, ent in (("reserve__upDown__unit__node", "unit"),
|
|
1550
|
+
("reserve__upDown__connection__node", "connection")):
|
|
1551
|
+
try:
|
|
1552
|
+
df = source.parameter_explicit(cls, parameter_name)
|
|
1553
|
+
except (KeyError, AttributeError):
|
|
1554
|
+
try:
|
|
1555
|
+
df = source.parameter(cls, parameter_name)
|
|
1556
|
+
except KeyError:
|
|
1557
|
+
continue
|
|
1558
|
+
if df is None or df.height == 0:
|
|
1559
|
+
continue
|
|
1560
|
+
cols = df.columns
|
|
1561
|
+
if not {"reserve", "upDown", ent, "node", "value"}.issubset(cols):
|
|
1562
|
+
continue
|
|
1563
|
+
parts.append(df.lazy()
|
|
1564
|
+
.rename({ent: "p", "reserve": "r",
|
|
1565
|
+
"upDown": "ud", "node": "n"})
|
|
1566
|
+
.filter(pl.col("value").is_not_null())
|
|
1567
|
+
.select("p", "r", "ud", "n", "value"))
|
|
1568
|
+
if not parts:
|
|
1569
|
+
return None
|
|
1570
|
+
out = pl.concat(parts).collect()
|
|
1571
|
+
if out.height == 0:
|
|
1572
|
+
return None
|
|
1573
|
+
return Param(("p", "r", "ud", "n"), out.lazy().sort("p", "r", "ud", "n"))
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def p_process_reserve_upDown_node_reliability_from_source(source: "InputSource") -> Param | None:
|
|
1577
|
+
"""``reserve__upDown__{unit,connection}__node.reliability`` →
|
|
1578
|
+
``Param(("p", "r", "ud", "n"))``. Default 1.0 (schema) — broadcast.
|
|
1579
|
+
The CSV path uses the broadcast default; here we delegate to
|
|
1580
|
+
``parameter`` (not ``parameter_explicit``) so the default-fill kicks
|
|
1581
|
+
in. Result is the same shape as the CSV path's
|
|
1582
|
+
``Param(("p", "r", "ud", "n"))``.
|
|
1583
|
+
"""
|
|
1584
|
+
parts: list[pl.LazyFrame] = []
|
|
1585
|
+
for cls, ent in (("reserve__upDown__unit__node", "unit"),
|
|
1586
|
+
("reserve__upDown__connection__node", "connection")):
|
|
1587
|
+
try:
|
|
1588
|
+
df = source.parameter(cls, parameter_name="reliability")
|
|
1589
|
+
except KeyError:
|
|
1590
|
+
continue
|
|
1591
|
+
if df is None or df.height == 0:
|
|
1592
|
+
continue
|
|
1593
|
+
cols = df.columns
|
|
1594
|
+
if not {"reserve", "upDown", ent, "node", "value"}.issubset(cols):
|
|
1595
|
+
continue
|
|
1596
|
+
parts.append(df.lazy()
|
|
1597
|
+
.rename({ent: "p", "reserve": "r",
|
|
1598
|
+
"upDown": "ud", "node": "n"})
|
|
1599
|
+
.filter(pl.col("value").is_not_null())
|
|
1600
|
+
.select("p", "r", "ud", "n", "value"))
|
|
1601
|
+
if not parts:
|
|
1602
|
+
return None
|
|
1603
|
+
out = pl.concat(parts).collect()
|
|
1604
|
+
if out.height == 0:
|
|
1605
|
+
return None
|
|
1606
|
+
return Param(("p", "r", "ud", "n"), out.lazy().sort("p", "r", "ud", "n"))
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def p_process_reserve_upDown_node_max_share_from_source(source: "InputSource") -> Param | None:
|
|
1610
|
+
return _process_reserve_node_param(source, "max_share")
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def p_process_reserve_upDown_node_large_failure_ratio_value_from_source(
|
|
1614
|
+
source: "InputSource") -> Param | None:
|
|
1615
|
+
return _process_reserve_node_param(source, "large_failure_ratio")
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def p_process_reserve_upDown_node_increase_reserve_ratio_value_from_source(
|
|
1619
|
+
source: "InputSource") -> Param | None:
|
|
1620
|
+
return _process_reserve_node_param(source, "increase_reserve_ratio")
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
# ---------------------------------------------------------------------------
|
|
1624
|
+
# §1.17 — Delayed processes (process_delayed__duration)
|
|
1625
|
+
|
|
1626
|
+
def process_delayed__duration_from_source(source: "InputSource") -> pl.DataFrame | None:
|
|
1627
|
+
"""``unit/connection.delay`` 1d_map(td) → ``[p, td]`` set frame.
|
|
1628
|
+
|
|
1629
|
+
CSV path (``_delay.load_data``) reads ``solve_data/process_delayed__duration.csv``
|
|
1630
|
+
and returns the (p, td) keys as a DataFrame (not a Param — it's a
|
|
1631
|
+
set, since the duration values are 1.0 or absent).
|
|
1632
|
+
|
|
1633
|
+
Note: this is a *set* in FlexData, not a Param — return
|
|
1634
|
+
type matches the field declared on FlexData.
|
|
1635
|
+
"""
|
|
1636
|
+
parts: list[pl.LazyFrame] = []
|
|
1637
|
+
for cls in ("unit", "connection"):
|
|
1638
|
+
try:
|
|
1639
|
+
df = source.parameter_explicit(cls, "delay")
|
|
1640
|
+
except (KeyError, AttributeError):
|
|
1641
|
+
try:
|
|
1642
|
+
df = source.parameter(cls, "delay")
|
|
1643
|
+
except KeyError:
|
|
1644
|
+
continue
|
|
1645
|
+
if df is None or df.height == 0:
|
|
1646
|
+
continue
|
|
1647
|
+
cols = df.columns
|
|
1648
|
+
if "name" not in cols:
|
|
1649
|
+
continue
|
|
1650
|
+
# 1d_map(td) — the index column may be 'td' or default 'period'/'i'.
|
|
1651
|
+
# Detect by elimination: any column not in {name, value} is the index.
|
|
1652
|
+
idx_cols = [c for c in cols if c not in ("name", "value")]
|
|
1653
|
+
if len(idx_cols) != 1:
|
|
1654
|
+
continue
|
|
1655
|
+
idx = idx_cols[0]
|
|
1656
|
+
lf = (df.lazy()
|
|
1657
|
+
.rename({"name": "p", idx: "td"})
|
|
1658
|
+
.filter(pl.col("value").is_not_null())
|
|
1659
|
+
.filter(pl.col("value") != 0.0))
|
|
1660
|
+
parts.append(lf.select("p", "td"))
|
|
1661
|
+
if not parts:
|
|
1662
|
+
return None
|
|
1663
|
+
out = pl.concat(parts).unique().collect()
|
|
1664
|
+
if out.height == 0:
|
|
1665
|
+
return None
|
|
1666
|
+
return out.sort("p", "td")
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
# ---------------------------------------------------------------------------
|
|
1670
|
+
# Penalty / availability scalars (§1.2 / §1.7) — schema-default broadcast.
|
|
1671
|
+
#
|
|
1672
|
+
# These differ from the "explicit only" pattern: schema sentinel default
|
|
1673
|
+
# (e.g. 10000.0 for penalty_up) means the CSV path receives one row per
|
|
1674
|
+
# entity with the broadcast default. ``SpineDbReader.parameter()``
|
|
1675
|
+
# already broadcasts when ``default is not None``; we just rename.
|
|
1676
|
+
|
|
1677
|
+
def _entity_scalar_with_default(source: "InputSource", entity_class: str,
|
|
1678
|
+
parameter_name: str,
|
|
1679
|
+
dim: str) -> Param | None:
|
|
1680
|
+
"""Variant of :func:`_entity_scalar_explicit` that uses
|
|
1681
|
+
:meth:`parameter` (default-broadcast) instead of ``parameter_explicit``.
|
|
1682
|
+
"""
|
|
1683
|
+
try:
|
|
1684
|
+
df = source.parameter(entity_class, parameter_name)
|
|
1685
|
+
except KeyError:
|
|
1686
|
+
return None
|
|
1687
|
+
if df is None or df.height == 0:
|
|
1688
|
+
return None
|
|
1689
|
+
return Param((dim,),
|
|
1690
|
+
df.lazy().rename({"name": dim}).select(dim, "value"))
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
# ---------------------------------------------------------------------------
|
|
1694
|
+
# §1.7, §1.3 — additional Map(period→time) scalars on object classes.
|
|
1695
|
+
|
|
1696
|
+
def p_process_availability_from_source(source: "InputSource",
|
|
1697
|
+
period_filter: pl.DataFrame | None = None,
|
|
1698
|
+
) -> Param | None:
|
|
1699
|
+
"""``unit/connection.availability`` → Param keyed on ``"p"`` plus
|
|
1700
|
+
whichever of ``"d"`` / ``"t"`` the union of unit + connection
|
|
1701
|
+
authoring requires.
|
|
1702
|
+
|
|
1703
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape` for each of
|
|
1704
|
+
``unit.availability`` and ``connection.availability`` independently
|
|
1705
|
+
(Spine carries them on separate classes; the LP consumes the union).
|
|
1706
|
+
Allowed shapes per class: scalar / 1d_map[period] / 1d_map[time] /
|
|
1707
|
+
2d_map[period,time]. CSV path slices ``pdtProcess.csv``
|
|
1708
|
+
(param='availability') and emits explicit rows only.
|
|
1709
|
+
|
|
1710
|
+
Phase E.1 — each per-class Param keeps its authored dims under the
|
|
1711
|
+
lazified broadcast helpers. When the two classes carry different
|
|
1712
|
+
shapes (e.g. one SCALAR, one MAP_TIME) we promote each part to the
|
|
1713
|
+
union of their dims via lazy joins on ``period_filter`` before the
|
|
1714
|
+
concat, so the output Param has a single consistent dim shape but
|
|
1715
|
+
the upstream LazyFrames remain unmaterialised.
|
|
1716
|
+
"""
|
|
1717
|
+
parts: list[Param] = []
|
|
1718
|
+
for cls in ("unit", "connection"):
|
|
1719
|
+
resolved = resolve_param_shape(
|
|
1720
|
+
source, cls, "availability", period_filter=period_filter)
|
|
1721
|
+
param = broadcast_to_period_time(resolved, "p", period_filter)
|
|
1722
|
+
if param is None:
|
|
1723
|
+
continue
|
|
1724
|
+
parts.append(param)
|
|
1725
|
+
if not parts:
|
|
1726
|
+
return None
|
|
1727
|
+
# Union of all per-part dims preserving order: always starts with
|
|
1728
|
+
# "p"; "d" / "t" appear only when at least one part authored them.
|
|
1729
|
+
union_dims: tuple[str, ...] = ("p",)
|
|
1730
|
+
if any("d" in pt.dims for pt in parts):
|
|
1731
|
+
union_dims = union_dims + ("d",)
|
|
1732
|
+
if any("t" in pt.dims for pt in parts):
|
|
1733
|
+
union_dims = union_dims + ("t",)
|
|
1734
|
+
# Promote each part to the union dims via lazy joins on
|
|
1735
|
+
# period_filter. No-op when the part already carries union_dims.
|
|
1736
|
+
pf_lf = (period_filter.lazy() if period_filter is not None
|
|
1737
|
+
and period_filter.height > 0 else None)
|
|
1738
|
+
part_lfs: list[pl.LazyFrame] = []
|
|
1739
|
+
for pt in parts:
|
|
1740
|
+
lf = pt.lazy
|
|
1741
|
+
missing = [d for d in union_dims if d not in pt.dims]
|
|
1742
|
+
if missing:
|
|
1743
|
+
if pf_lf is None:
|
|
1744
|
+
# Cannot promote without a period_filter to source the
|
|
1745
|
+
# missing axis values — defensive guard, the helper's
|
|
1746
|
+
# contract requires a non-empty filter for any broadcast.
|
|
1747
|
+
return None
|
|
1748
|
+
# Build the (missing-dim) universe from period_filter.
|
|
1749
|
+
uni = pf_lf.select(missing).unique()
|
|
1750
|
+
lf = lf.join(uni, how="cross")
|
|
1751
|
+
lf = lf.select(*union_dims, "value")
|
|
1752
|
+
part_lfs.append(lf)
|
|
1753
|
+
out_lf = pl.concat(part_lfs).sort(*union_dims)
|
|
1754
|
+
return Param(union_dims, out_lf)
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def p_commodity_price_from_source(source: "InputSource",
|
|
1758
|
+
period_filter: pl.DataFrame | None = None,
|
|
1759
|
+
) -> Param | None:
|
|
1760
|
+
"""``commodity.price`` → ``Param(("c", "d", "t"))``.
|
|
1761
|
+
|
|
1762
|
+
Δ.17c — uses :func:`._param_shapes.resolve_param_shape`. Allowed
|
|
1763
|
+
shapes: scalar / 1d_map[period] / 1d_map[time] (no 2d_map per
|
|
1764
|
+
flextool's ``write_pdtCommodity`` cascade pt → pd → p → 0). None
|
|
1765
|
+
default on the schema; CSV path slices ``pdtCommodity.csv``
|
|
1766
|
+
(param='price') and emits explicit rows only.
|
|
1767
|
+
"""
|
|
1768
|
+
resolved = resolve_param_shape(
|
|
1769
|
+
source, "commodity", "price", period_filter=period_filter)
|
|
1770
|
+
return broadcast_to_period_time(resolved, "c", period_filter)
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
# ---------------------------------------------------------------------------
|
|
1774
|
+
# §1.19 — Commodity ladder split (price / quantity sub-frames).
|
|
1775
|
+
#
|
|
1776
|
+
# Spine schema: ``commodity.price_ladder_annual`` is a 3-level Map
|
|
1777
|
+
# ``(period → tier → {"price","quantity"} → f64)`` which the
|
|
1778
|
+
# SpineDbReader unrolls to ``[name, period, tier, x, value]`` where
|
|
1779
|
+
# ``x ∈ {"price", "quantity"}``. We split it into two Params keyed
|
|
1780
|
+
# (c, i, d) with ``value`` being the price (resp. quantity) only.
|
|
1781
|
+
# ``commodity.price_ladder_cumulative`` mirrors but without period.
|
|
1782
|
+
|
|
1783
|
+
def _ladder_split(source: "InputSource", parameter_name: str,
|
|
1784
|
+
*, with_period: bool) -> tuple[Param | None, Param | None]:
|
|
1785
|
+
"""Return ``(price_param, quantity_param)`` from the named ladder
|
|
1786
|
+
parameter. ``with_period=True`` means the unroll produces a
|
|
1787
|
+
``period`` column (annual ladder); ``False`` means tier-only
|
|
1788
|
+
(cumulative ladder).
|
|
1789
|
+
"""
|
|
1790
|
+
try:
|
|
1791
|
+
df = source.parameter_explicit("commodity", parameter_name)
|
|
1792
|
+
except (KeyError, AttributeError):
|
|
1793
|
+
try:
|
|
1794
|
+
df = source.parameter("commodity", parameter_name)
|
|
1795
|
+
except KeyError:
|
|
1796
|
+
return None, None
|
|
1797
|
+
if df is None or df.height == 0:
|
|
1798
|
+
return None, None
|
|
1799
|
+
cols = df.columns
|
|
1800
|
+
needed = {"name", "tier", "value"}
|
|
1801
|
+
if with_period:
|
|
1802
|
+
needed.add("period")
|
|
1803
|
+
if not needed.issubset(cols):
|
|
1804
|
+
return None, None
|
|
1805
|
+
# Detect the leaf-discriminator column ("x" or whatever name carries
|
|
1806
|
+
# the "price"/"quantity" tag). All non-(name, period, tier, value)
|
|
1807
|
+
# columns are candidates; pick the first.
|
|
1808
|
+
keep_cols = ({"name", "period", "tier", "value"} if with_period
|
|
1809
|
+
else {"name", "tier", "value"})
|
|
1810
|
+
leaf_cols = [c for c in cols if c not in keep_cols]
|
|
1811
|
+
if len(leaf_cols) != 1:
|
|
1812
|
+
return None, None
|
|
1813
|
+
leaf = leaf_cols[0]
|
|
1814
|
+
rename = {"name": "c", "tier": "i"}
|
|
1815
|
+
if with_period:
|
|
1816
|
+
rename["period"] = "d"
|
|
1817
|
+
base = (df.lazy()
|
|
1818
|
+
.rename(rename)
|
|
1819
|
+
.filter(pl.col("value").is_not_null())
|
|
1820
|
+
.with_columns(pl.col("i").cast(pl.Utf8)))
|
|
1821
|
+
out_dims = ("c", "i", "d") if with_period else ("c", "i")
|
|
1822
|
+
price_lf = base.filter(pl.col(leaf) == "price").select(*out_dims, "value")
|
|
1823
|
+
qty_lf = base.filter(pl.col(leaf) == "quantity").select(*out_dims, "value")
|
|
1824
|
+
price_df = price_lf.collect()
|
|
1825
|
+
qty_df = qty_lf.collect()
|
|
1826
|
+
p_price = Param(out_dims, price_df) if price_df.height > 0 else None
|
|
1827
|
+
p_qty = Param(out_dims, qty_df) if qty_df.height > 0 else None
|
|
1828
|
+
return p_price, p_qty
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def p_ladder_ann_price_from_source(source: "InputSource") -> Param | None:
|
|
1832
|
+
return _ladder_split(source, "price_ladder_annual", with_period=True)[0]
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
def p_ladder_ann_quantity_from_source(source: "InputSource") -> Param | None:
|
|
1836
|
+
return _ladder_split(source, "price_ladder_annual", with_period=True)[1]
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
def p_ladder_cum_price_from_source(source: "InputSource") -> Param | None:
|
|
1840
|
+
return _ladder_split(source, "price_ladder_cumulative", with_period=False)[0]
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def p_ladder_cum_quantity_from_source(source: "InputSource") -> Param | None:
|
|
1844
|
+
return _ladder_split(source, "price_ladder_cumulative", with_period=False)[1]
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
def apply_direct_params_a(source: "InputSource",
|
|
1848
|
+
flex_data: object) -> None:
|
|
1849
|
+
"""Pass 1a of Direct Params — the dt-independent assignments.
|
|
1850
|
+
|
|
1851
|
+
Δ.28 — split from the legacy single-pass ``apply_direct_params``
|
|
1852
|
+
so the dispatcher can run :func:`apply_derived_a` (which populates
|
|
1853
|
+
``flex_data.dt``) BEFORE the broadcast-needing assignments in
|
|
1854
|
+
:func:`apply_direct_params_b` execute. See module docstring on the
|
|
1855
|
+
bug Δ.28 fixes (fast path's ``flex_data.dt`` is empty when
|
|
1856
|
+
``apply_direct_params`` runs, so scalar / 1d_map[period] /
|
|
1857
|
+
1d_map[time] values authored on the source can't broadcast across
|
|
1858
|
+
the active solve's ``(d, t)`` axis and the Param ends up empty —
|
|
1859
|
+
e.g. ``p_commodity_price`` on ``work_lh2_three_region`` was empty
|
|
1860
|
+
on the fast path even though Spine carries a scalar 30 for coal).
|
|
1861
|
+
|
|
1862
|
+
The slow path still calls these passes in sequence and the legacy
|
|
1863
|
+
behaviour is preserved exactly — slow-path ``flex_data.dt`` is
|
|
1864
|
+
already populated from CSVs by ``_load_*`` before
|
|
1865
|
+
``_apply_db_overrides`` runs, so pass-1a / pass-1b ordering is a
|
|
1866
|
+
no-op there.
|
|
1867
|
+
|
|
1868
|
+
This pass writes:
|
|
1869
|
+
* §5.2.1 scalar Params (``p_co2_content`` etc.).
|
|
1870
|
+
* §5.2.3 relationship 1d_map (constraint coefficients).
|
|
1871
|
+
* §5.2.1 invest/divest total caps.
|
|
1872
|
+
* §1.16 second-wave scalars.
|
|
1873
|
+
|
|
1874
|
+
All of these consume ``source`` only — no ``flex_data.dt``
|
|
1875
|
+
dependency.
|
|
1876
|
+
|
|
1877
|
+
Δ.12b — assignment is unconditional for the scalar / 1d_map and
|
|
1878
|
+
relationship-1d_map helpers (these read the source unchanged and
|
|
1879
|
+
return None only when no upstream row exists, which is a
|
|
1880
|
+
legitimate "feature inactive" outcome).
|
|
1881
|
+
"""
|
|
1882
|
+
# ─── §5.2.1 scalar Params with FlexData fields — Δ.12b unconditional
|
|
1883
|
+
flex_data.p_co2_content = p_co2_content_from_source(source)
|
|
1884
|
+
flex_data.p_constraint_constant = p_constraint_constant_from_source(source)
|
|
1885
|
+
|
|
1886
|
+
# ─── §5.2.3 relationship 1d_map (constraint coefficients) ───────────
|
|
1887
|
+
flex_data.p_node_constraint_invested_capacity_coeff = (
|
|
1888
|
+
_node_constraint_coef(source, "constraint_invested_capacity_coeff"))
|
|
1889
|
+
flex_data.p_process_constraint_invested_capacity_coeff = (
|
|
1890
|
+
_process_constraint_coef(source, "constraint_invested_capacity_coeff"))
|
|
1891
|
+
flex_data.p_node_constraint_state_coeff = (
|
|
1892
|
+
_node_constraint_coef(source, "constraint_state_coeff"))
|
|
1893
|
+
flex_data.p_node_constraint_prebuilt_capacity_coeff = (
|
|
1894
|
+
_node_constraint_coef(source,
|
|
1895
|
+
"constraint_cumulative_pre_built_capacity_coeff"))
|
|
1896
|
+
flex_data.p_process_constraint_prebuilt_capacity_coeff = (
|
|
1897
|
+
_process_constraint_coef(source,
|
|
1898
|
+
"constraint_cumulative_pre_built_capacity_coeff"))
|
|
1899
|
+
|
|
1900
|
+
# ─── §5.2.1 invest/divest total caps (entity-unioned) ───────────────
|
|
1901
|
+
# Max variants: keyed on entityInvest (resp. entityDivest), one row
|
|
1902
|
+
# per entity with value defaulting to 0 — mirrors
|
|
1903
|
+
# ``entity_total_caps.py:_compute_entity_total`` and
|
|
1904
|
+
# ``input.py::_e_total_param``.
|
|
1905
|
+
flex_data.e_invest_max_total = _e_total_param(
|
|
1906
|
+
source, "invest_max_total", kind="invest")
|
|
1907
|
+
flex_data.e_divest_max_total = _e_total_param(
|
|
1908
|
+
source, "retire_max_total", kind="divest")
|
|
1909
|
+
# Min variants: CSV path filters out zero rows (input.py::_read_e_param)
|
|
1910
|
+
# — None when no entity has an explicit non-zero min cap.
|
|
1911
|
+
flex_data.e_invest_min_total = _e_total_param(
|
|
1912
|
+
source, "invest_min_total", kind="invest", filter_zero=True)
|
|
1913
|
+
flex_data.e_divest_min_total = _e_total_param(
|
|
1914
|
+
source, "retire_min_total", kind="divest", filter_zero=True)
|
|
1915
|
+
|
|
1916
|
+
# ─── Δ.4 second wave — node scalars (storage feature) ───────────────
|
|
1917
|
+
flex_data.p_state_self_discharge = p_state_self_discharge_from_source(source)
|
|
1918
|
+
flex_data.p_state_start = p_state_start_from_source(source)
|
|
1919
|
+
|
|
1920
|
+
# ─── Δ.4 second wave — process scalars (online / UC feature) ────────
|
|
1921
|
+
flex_data.p_min_load = p_min_load_from_source(source)
|
|
1922
|
+
# Per-output-arc min_capacity_coefficient — scales the minFlow_minload
|
|
1923
|
+
# floor. Assigned in pass 1a so it is present on the synthetic / rolling
|
|
1924
|
+
# sub-solve path too (passes 3-10 are skipped there).
|
|
1925
|
+
flex_data.p_process_sink_min_capacity_coef = (
|
|
1926
|
+
p_process_sink_min_capacity_coef_from_source(source))
|
|
1927
|
+
|
|
1928
|
+
# ─── Δ.4 second wave — connection scalars (DC power flow feature) ───
|
|
1929
|
+
# Δ.16 — preserve the CSV-loaded value when the source has no rows.
|
|
1930
|
+
# Some fixtures (e.g. ``work_dc_power_flow`` / ``case14.sqlite``)
|
|
1931
|
+
# ship a pre-computed CSV but don't carry the parameter on the
|
|
1932
|
+
# ``connection`` class in the DB.
|
|
1933
|
+
_pcs_src = p_connection_susceptance_from_source(source)
|
|
1934
|
+
if _pcs_src is not None:
|
|
1935
|
+
flex_data.p_connection_susceptance = _pcs_src
|
|
1936
|
+
|
|
1937
|
+
# ─── Δ.4 second wave — commodity scalars (price ladder feature) ─────
|
|
1938
|
+
flex_data.p_commodity_unitsize = p_commodity_unitsize_from_source(source)
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
def apply_direct_params_b(source: "InputSource",
|
|
1942
|
+
flex_data: object) -> None:
|
|
1943
|
+
"""Pass 1b of Direct Params — the dt-dependent assignments.
|
|
1944
|
+
|
|
1945
|
+
Δ.28 — runs AFTER :func:`apply_derived_a` populates
|
|
1946
|
+
``flex_data.dt`` for the active solve. Every helper here is a
|
|
1947
|
+
broadcast-needing Direct Param (scalar / 1d_map[period] /
|
|
1948
|
+
1d_map[time] values authored on the source need ``dt`` to fan out
|
|
1949
|
+
across the per-(d, t) axis).
|
|
1950
|
+
|
|
1951
|
+
Helpers in this pass either:
|
|
1952
|
+
* consume ``period_filter=dt`` directly (scalar broadcast inside
|
|
1953
|
+
``broadcast_to_period_time`` / ``broadcast_to_period`` /
|
|
1954
|
+
``_entity_period_scalar`` / ``_entity_period_time_param``), or
|
|
1955
|
+
* pass ``dt`` to ``_filter_param_by_periods`` to restrict
|
|
1956
|
+
authored Map(period→…) values to the active periods.
|
|
1957
|
+
|
|
1958
|
+
On the fast path ``apply_direct_params_a`` runs first (with empty
|
|
1959
|
+
``dt``), then ``apply_derived_a`` populates ``dt`` from the source's
|
|
1960
|
+
timeset / period structure, then this pass runs — every broadcast
|
|
1961
|
+
helper sees a non-empty ``dt`` and produces the per-(d, t) frame.
|
|
1962
|
+
|
|
1963
|
+
On the slow path ``flex_data.dt`` is already populated by ``_load_*``
|
|
1964
|
+
before ``_apply_db_overrides`` runs, so pass-A then pass-B is
|
|
1965
|
+
behaviourally equivalent to the legacy single pass.
|
|
1966
|
+
"""
|
|
1967
|
+
# ─── Δ.4b — period filter (mirrors flextool's per-solve preprocessing) ─
|
|
1968
|
+
# Spine Map(period→…) parameters cover ALL declared periods, but the
|
|
1969
|
+
# CSV path's pd_group.csv / pdtNode.csv etc. is pre-filtered to the
|
|
1970
|
+
# active solve's periods via flextool's preprocessing. We mirror by
|
|
1971
|
+
# restricting period-keyed Params to the active dt's periods.
|
|
1972
|
+
dt = getattr(flex_data, "dt", None)
|
|
1973
|
+
|
|
1974
|
+
# ─── Δ.4b — group 1d_map(period) (capacity_margin / inertia / nonSync) ─
|
|
1975
|
+
# Δ.12b: unconditional — _entity_period_scalar handles 1d_map(period)
|
|
1976
|
+
# natively (this is the only Spine shape for these parameters).
|
|
1977
|
+
# Δ.12c-fix gap #3: pass ``dt`` as ``period_filter`` so scalar values
|
|
1978
|
+
# (single ``[name, value]`` row) broadcast across the active solve's
|
|
1979
|
+
# periods, matching flextool's pdGroup.csv preprocessing.
|
|
1980
|
+
for fn, field in (
|
|
1981
|
+
(pdGroup_capacity_margin_from_source, "pdGroup_capacity_margin"),
|
|
1982
|
+
(pdGroup_penalty_capacity_margin_from_source,
|
|
1983
|
+
"pdGroup_penalty_capacity_margin"),
|
|
1984
|
+
(pdGroup_inertia_limit_from_source, "pdGroup_inertia_limit"),
|
|
1985
|
+
(pdGroup_penalty_inertia_from_source, "pdGroup_penalty_inertia"),
|
|
1986
|
+
(pdGroup_non_synchronous_limit_from_source,
|
|
1987
|
+
"pdGroup_non_synchronous_limit"),
|
|
1988
|
+
(pdGroup_penalty_non_synchronous_from_source,
|
|
1989
|
+
"pdGroup_penalty_non_synchronous"),
|
|
1990
|
+
):
|
|
1991
|
+
setattr(flex_data, field,
|
|
1992
|
+
_filter_param_by_periods(fn(source, period_filter=dt), dt))
|
|
1993
|
+
|
|
1994
|
+
# ─── Δ.4b — group 1d_map(period) (invest/divest/cumulative) ──────────
|
|
1995
|
+
# Δ.17c-Tier1 — helpers now accept ``period_filter`` (consumed by
|
|
1996
|
+
# ``resolve_param_shape`` for silent-default disambiguation and by
|
|
1997
|
+
# ``broadcast_to_period`` for the active-solve period restriction).
|
|
1998
|
+
for fn, field in (
|
|
1999
|
+
(p_group_invest_max_period_from_source, "p_group_invest_max_period"),
|
|
2000
|
+
(p_group_invest_min_period_from_source, "p_group_invest_min_period"),
|
|
2001
|
+
(p_group_retire_max_period_from_source, "p_group_retire_max_period"),
|
|
2002
|
+
(p_group_retire_min_period_from_source, "p_group_retire_min_period"),
|
|
2003
|
+
(pd_max_cumulative_flow_from_source, "pd_max_cumulative_flow"),
|
|
2004
|
+
(pd_min_cumulative_flow_from_source, "pd_min_cumulative_flow"),
|
|
2005
|
+
):
|
|
2006
|
+
setattr(flex_data, field,
|
|
2007
|
+
_filter_param_by_periods(fn(source, period_filter=dt), dt))
|
|
2008
|
+
|
|
2009
|
+
# ─── Δ.4b — group scalar (no period) ─────────────────────────────────
|
|
2010
|
+
for fn, field in (
|
|
2011
|
+
(p_group_invest_max_total_from_source, "p_group_invest_max_total"),
|
|
2012
|
+
(p_group_invest_min_total_from_source, "p_group_invest_min_total"),
|
|
2013
|
+
(p_group_retire_max_total_from_source, "p_group_retire_max_total"),
|
|
2014
|
+
(p_group_retire_min_total_from_source, "p_group_retire_min_total"),
|
|
2015
|
+
(p_group_invest_max_cumulative_from_source,
|
|
2016
|
+
"p_group_invest_max_cumulative"),
|
|
2017
|
+
(p_group_invest_min_cumulative_from_source,
|
|
2018
|
+
"p_group_invest_min_cumulative"),
|
|
2019
|
+
(p_group_max_cumulative_flow_from_source, "p_group_max_cumulative_flow"),
|
|
2020
|
+
(p_group_min_cumulative_flow_from_source, "p_group_min_cumulative_flow"),
|
|
2021
|
+
):
|
|
2022
|
+
setattr(flex_data, field, fn(source))
|
|
2023
|
+
|
|
2024
|
+
# ─── Δ.4b — group Map(period→time) instant flow caps ─────────────────
|
|
2025
|
+
# Δ.12c-fix gap #1: helpers now broadcast scalar / 1d_map(time) /
|
|
2026
|
+
# 1d_map(period) shapes across the active solve's (d, t) axis when
|
|
2027
|
+
# ``period_filter=dt`` is supplied. Conditional assignment retained
|
|
2028
|
+
# for fixtures with no Spine rows at all (helper returns None) where
|
|
2029
|
+
# the seed-side preprocessing produces an empty/zero-broadcast Param.
|
|
2030
|
+
v = _filter_param_by_periods(
|
|
2031
|
+
pdt_max_instant_flow_from_source(source, period_filter=dt), dt)
|
|
2032
|
+
if v is not None:
|
|
2033
|
+
flex_data.pdt_max_instant_flow = v
|
|
2034
|
+
v = _filter_param_by_periods(
|
|
2035
|
+
pdt_min_instant_flow_from_source(source, period_filter=dt), dt)
|
|
2036
|
+
if v is not None:
|
|
2037
|
+
flex_data.pdt_min_instant_flow = v
|
|
2038
|
+
|
|
2039
|
+
# ─── Δ.4b — multi-class union 1d_map(period): invest / divest ────────
|
|
2040
|
+
# Δ.17c-Tier2 — helpers now accept ``period_filter`` (consumed by
|
|
2041
|
+
# ``resolve_param_shape`` for silent-default disambiguation and by
|
|
2042
|
+
# ``broadcast_to_period`` for the active-solve period restriction and
|
|
2043
|
+
# scalar-to-(e,d) cross-join).
|
|
2044
|
+
for fn, field in (
|
|
2045
|
+
(ed_invest_max_period_from_source, "ed_invest_max_period"),
|
|
2046
|
+
(ed_divest_max_period_from_source, "ed_divest_max_period"),
|
|
2047
|
+
(ed_invest_min_period_from_source, "ed_invest_min_period"),
|
|
2048
|
+
(ed_divest_min_period_from_source, "ed_divest_min_period"),
|
|
2049
|
+
(ed_cumulative_max_capacity_from_source, "ed_cumulative_max_capacity"),
|
|
2050
|
+
(ed_cumulative_min_capacity_from_source, "ed_cumulative_min_capacity"),
|
|
2051
|
+
):
|
|
2052
|
+
setattr(flex_data, field,
|
|
2053
|
+
_filter_param_by_periods(fn(source, period_filter=dt), dt))
|
|
2054
|
+
|
|
2055
|
+
# ─── Δ.4b — relationship scalars (ramp + inertia, sink/source) ───────
|
|
2056
|
+
for fn, field in (
|
|
2057
|
+
(p_ramp_speed_up_sink_from_source, "p_ramp_speed_up_sink"),
|
|
2058
|
+
(p_ramp_speed_down_sink_from_source, "p_ramp_speed_down_sink"),
|
|
2059
|
+
(p_ramp_speed_up_source_from_source, "p_ramp_speed_up_source"),
|
|
2060
|
+
(p_ramp_speed_down_source_from_source, "p_ramp_speed_down_source"),
|
|
2061
|
+
(p_process_sink_inertia_constant_from_source,
|
|
2062
|
+
"p_process_sink_inertia_constant"),
|
|
2063
|
+
(p_process_source_inertia_constant_from_source,
|
|
2064
|
+
"p_process_source_inertia_constant"),
|
|
2065
|
+
):
|
|
2066
|
+
setattr(flex_data, field, fn(source))
|
|
2067
|
+
|
|
2068
|
+
# ─── Δ.4b — UC: startup_cost (1d_map period) ─────────────────────────
|
|
2069
|
+
# Δ.12c-fix gap #3: ``_entity_period_scalar`` handles 1d_map(period)
|
|
2070
|
+
# AND scalar shapes when ``period_filter=dt`` is supplied.
|
|
2071
|
+
v = _filter_param_by_periods(
|
|
2072
|
+
p_startup_cost_from_source(source, period_filter=dt), dt)
|
|
2073
|
+
if v is not None:
|
|
2074
|
+
flex_data.p_startup_cost = v
|
|
2075
|
+
|
|
2076
|
+
# ─── Δ.4b — storage Map(period→time) ─────────────────────────────────
|
|
2077
|
+
# Δ.12c-fix gap #1: helpers broadcast non-Map shapes via period_filter.
|
|
2078
|
+
v = _filter_param_by_periods(
|
|
2079
|
+
p_node_availability_from_source(source, period_filter=dt), dt)
|
|
2080
|
+
if v is not None:
|
|
2081
|
+
flex_data.p_node_availability = v
|
|
2082
|
+
v = _filter_param_by_periods(
|
|
2083
|
+
p_storage_state_reference_value_from_source(source, period_filter=dt),
|
|
2084
|
+
dt)
|
|
2085
|
+
if v is not None:
|
|
2086
|
+
flex_data.p_storage_state_reference_value = v
|
|
2087
|
+
|
|
2088
|
+
# ─── Δ.4b — CO2 (price + cap) ────────────────────────────────────────
|
|
2089
|
+
v = _filter_param_by_periods(
|
|
2090
|
+
p_co2_price_from_source(source, period_filter=dt), dt)
|
|
2091
|
+
if v is not None:
|
|
2092
|
+
flex_data.p_co2_price = v
|
|
2093
|
+
v = _filter_param_by_periods(
|
|
2094
|
+
p_co2_max_period_from_source(source, period_filter=dt), dt)
|
|
2095
|
+
if v is not None:
|
|
2096
|
+
flex_data.p_co2_max_period = v
|
|
2097
|
+
|
|
2098
|
+
# ─── Δ.4b — variable cost (Map period→time) ──────────────────────────
|
|
2099
|
+
# Δ.17c-Tier3 — the four helpers below route through the resolver
|
|
2100
|
+
# cascade (resolve_param_shape + broadcast_to_period_time). Each
|
|
2101
|
+
# accepts the active solve's (d, t) frame as ``period_filter`` so
|
|
2102
|
+
# SCALAR / MAP_PERIOD / MAP_TIME shapes broadcast up to (p, d, t).
|
|
2103
|
+
v = _filter_param_by_periods(
|
|
2104
|
+
p_pdt_varCost_source_from_source(source, period_filter=dt), dt)
|
|
2105
|
+
if v is not None:
|
|
2106
|
+
flex_data.p_pdt_varCost_source = v
|
|
2107
|
+
v = _filter_param_by_periods(
|
|
2108
|
+
p_pdt_varCost_sink_from_source(source, period_filter=dt), dt)
|
|
2109
|
+
if v is not None:
|
|
2110
|
+
flex_data.p_pdt_varCost_sink = v
|
|
2111
|
+
v = _filter_param_by_periods(
|
|
2112
|
+
p_pdt_varCost_process_from_source(source, period_filter=dt), dt)
|
|
2113
|
+
if v is not None:
|
|
2114
|
+
flex_data.p_pdt_varCost_process = v
|
|
2115
|
+
|
|
2116
|
+
# ─── Δ.4b — reserves ─────────────────────────────────────────────────
|
|
2117
|
+
# Δ.17c-Tier3 — pdtReserve_upDown_group_reservation routes through
|
|
2118
|
+
# the resolver cascade with a per-source-column rename dict (3-dim
|
|
2119
|
+
# relationship class). The relationship-scalar reserves below
|
|
2120
|
+
# (penalty / reliability / max_share / failure-ratio /
|
|
2121
|
+
# increase-reserve-ratio) stay direct — they're SCALAR-only.
|
|
2122
|
+
v = _filter_param_by_periods(
|
|
2123
|
+
pdtReserve_upDown_group_reservation_from_source(
|
|
2124
|
+
source, period_filter=dt), dt)
|
|
2125
|
+
if v is not None:
|
|
2126
|
+
flex_data.pdtReserve_upDown_group_reservation = v
|
|
2127
|
+
flex_data.p_reserve_upDown_group_penalty_reserve = (
|
|
2128
|
+
p_reserve_upDown_group_penalty_reserve_from_source(source))
|
|
2129
|
+
flex_data.p_process_reserve_upDown_node_reliability = (
|
|
2130
|
+
p_process_reserve_upDown_node_reliability_from_source(source))
|
|
2131
|
+
flex_data.p_process_reserve_upDown_node_max_share = (
|
|
2132
|
+
p_process_reserve_upDown_node_max_share_from_source(source))
|
|
2133
|
+
flex_data.p_process_reserve_upDown_node_large_failure_ratio_value = (
|
|
2134
|
+
p_process_reserve_upDown_node_large_failure_ratio_value_from_source(source))
|
|
2135
|
+
flex_data.p_process_reserve_upDown_node_increase_reserve_ratio_value = (
|
|
2136
|
+
p_process_reserve_upDown_node_increase_reserve_ratio_value_from_source(source))
|
|
2137
|
+
|
|
2138
|
+
# ─── Δ.4b — delayed processes (set, not Param) ───────────────────────
|
|
2139
|
+
flex_data.process_delayed__duration = process_delayed__duration_from_source(source)
|
|
2140
|
+
|
|
2141
|
+
# ─── Δ.4b — additional Map(period→time) on object classes ───────────
|
|
2142
|
+
# Δ.12c-fix gap #1: helpers broadcast non-Map shapes via period_filter.
|
|
2143
|
+
v = _filter_param_by_periods(
|
|
2144
|
+
p_process_availability_from_source(source, period_filter=dt), dt)
|
|
2145
|
+
if v is not None:
|
|
2146
|
+
flex_data.p_process_availability = v
|
|
2147
|
+
v = _filter_param_by_periods(
|
|
2148
|
+
p_commodity_price_from_source(source, period_filter=dt), dt)
|
|
2149
|
+
if v is not None:
|
|
2150
|
+
flex_data.p_commodity_price = v
|
|
2151
|
+
|
|
2152
|
+
# ─── Δ.4b — commodity ladder split (price / quantity) ────────────────
|
|
2153
|
+
# _ladder_split returns (price, quantity) — both can be None.
|
|
2154
|
+
p_ann_price, p_ann_qty = _ladder_split(source, "price_ladder_annual",
|
|
2155
|
+
with_period=True)
|
|
2156
|
+
if p_ann_price is not None:
|
|
2157
|
+
flex_data.p_ladder_ann_price = _filter_param_by_periods(p_ann_price, dt)
|
|
2158
|
+
if p_ann_qty is not None:
|
|
2159
|
+
flex_data.p_ladder_ann_quantity = _filter_param_by_periods(p_ann_qty, dt)
|
|
2160
|
+
p_cum_price, p_cum_qty = _ladder_split(source, "price_ladder_cumulative",
|
|
2161
|
+
with_period=False)
|
|
2162
|
+
if p_cum_price is not None:
|
|
2163
|
+
flex_data.p_ladder_cum_price = p_cum_price
|
|
2164
|
+
if p_cum_qty is not None:
|
|
2165
|
+
flex_data.p_ladder_cum_quantity = p_cum_qty
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
def apply_direct_params(source: "InputSource",
|
|
2169
|
+
flex_data: object) -> None:
|
|
2170
|
+
"""Apply the full Direct Param wave in two phases.
|
|
2171
|
+
|
|
2172
|
+
Δ.28 — kept as a single back-compat entry point. The dispatcher in
|
|
2173
|
+
:func:`flextool.engine_polars.input._apply_db_overrides` does NOT
|
|
2174
|
+
call this function any more; it calls
|
|
2175
|
+
:func:`apply_direct_params_a` / :func:`apply_direct_params_b`
|
|
2176
|
+
around :func:`apply_derived_a` so the dt-dependent broadcasts in
|
|
2177
|
+
pass 1b see a populated ``flex_data.dt``.
|
|
2178
|
+
|
|
2179
|
+
External callers that want the legacy single-pass behaviour (with
|
|
2180
|
+
pre-populated ``flex_data.dt``) keep this entry point. In the slow
|
|
2181
|
+
path ``flex_data.dt`` is loaded by ``_load_*`` before this runs;
|
|
2182
|
+
in the fast path the dispatcher's two-phase ordering is the only
|
|
2183
|
+
correct sequence.
|
|
2184
|
+
"""
|
|
2185
|
+
apply_direct_params_a(source, flex_data)
|
|
2186
|
+
apply_direct_params_b(source, flex_data)
|