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,432 @@
|
|
|
1
|
+
"""Cluster F — scalar Param-on-Param arithmetic (Δ.10).
|
|
2
|
+
|
|
3
|
+
Lazy-polars helpers for the "scalar arithmetic" cluster identified in
|
|
4
|
+
``audit/native_data_path_design_derived_clusters.md`` §F. The cluster
|
|
5
|
+
groups Params whose source-driven build is a thin composition of the
|
|
6
|
+
already-cached primitives (``_entity_unitsize_lf`` /
|
|
7
|
+
``_node_unitsize_lf``, ``pss``, ``nodeBalance``, the ``dt`` (d, t)
|
|
8
|
+
grid). They share the same shape: pull a scalar / per-entity value
|
|
9
|
+
out of Spine, broadcast over a structural set, optionally apply the
|
|
10
|
+
unitsize cascade.
|
|
11
|
+
|
|
12
|
+
Δ.10 lifts six helpers off ``input.py``'s CSV preprocessing:
|
|
13
|
+
|
|
14
|
+
* :func:`p_unitsize_from_source` — ``(p,)`` per-process
|
|
15
|
+
unitsize; cascade ``virtual_unitsize OR existing OR 1000`` filtered
|
|
16
|
+
to processes appearing in ``pss``.
|
|
17
|
+
* :func:`p_state_unitsize_from_source` — ``(n,)`` per-node
|
|
18
|
+
unitsize; same cascade filtered to nodes in ``nodeState``.
|
|
19
|
+
* :func:`p_penalty_up_from_source` /
|
|
20
|
+
:func:`p_penalty_down_from_source` — ``(n, d, t)`` sentinel-
|
|
21
|
+
default scalar broadcast over the node × dt grid.
|
|
22
|
+
* :func:`p_process_source_conversion_flow_coeff_from_source` /
|
|
23
|
+
:func:`p_process_sink_conversion_flow_coeff_from_source` —
|
|
24
|
+
``(p, source)`` / ``(p, sink)`` conversion-flow coefficients with
|
|
25
|
+
zero-drop semantics on the caller's input/output sets.
|
|
26
|
+
|
|
27
|
+
Architecture invariants (per the Δ.10 hand-off):
|
|
28
|
+
|
|
29
|
+
1. **Lazy polars throughout.** Every helper returns a lazy chain
|
|
30
|
+
collected once at the rim.
|
|
31
|
+
2. **None-default skip.** When the parameter has no rows on the
|
|
32
|
+
source side and no scalar default, the helper returns ``None``;
|
|
33
|
+
the caller leaves the field untouched. *Exception:* the two
|
|
34
|
+
unitsize helpers (``p_unitsize`` / ``p_state_unitsize``) return
|
|
35
|
+
``None`` ONLY for an empty structural set — for a non-empty set
|
|
36
|
+
they are complete-by-construction (see invariant 4), because the
|
|
37
|
+
model multiplies every flow/state variable by them.
|
|
38
|
+
3. **No defensive gating.** Helpers fail loudly if the cascade
|
|
39
|
+
primitives drift; the parity sweep is the oracle.
|
|
40
|
+
4. **Unitsize completeness.** ``p_unitsize`` / ``p_state_unitsize``
|
|
41
|
+
are structural LP coefficients, so they must COVER every member of
|
|
42
|
+
their input set. :func:`_unitsize_complete` left-joins the set
|
|
43
|
+
against the cascade and coalesces uncovered members to the cascade's
|
|
44
|
+
own canonical default (:data:`UNITSIZE_DEFAULT`), warning on each so
|
|
45
|
+
a real set/vocabulary divergence stays visible. This is the
|
|
46
|
+
build-side analogue of the output-side completeness carrier
|
|
47
|
+
(``p_all_entity_unitsize``); fixing it here, at the producer, rather
|
|
48
|
+
than guarding ``None`` at each consumer, is the upstream fix.
|
|
49
|
+
|
|
50
|
+
The cluster F existing helpers (``p_slope``, ``p_section``,
|
|
51
|
+
``p_flow_upper_existing``, ``p_state_upper``, ``p_process_existing_count``)
|
|
52
|
+
remain in :mod:`._derived_params`; this module hosts only the new Δ.10
|
|
53
|
+
helpers — verification of the existing helpers is a code-review pass,
|
|
54
|
+
not a re-port.
|
|
55
|
+
"""
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import logging
|
|
59
|
+
from typing import TYPE_CHECKING
|
|
60
|
+
|
|
61
|
+
import polars as pl
|
|
62
|
+
|
|
63
|
+
from polar_high import Param
|
|
64
|
+
|
|
65
|
+
from flextool.engine_polars._axis_enums import (
|
|
66
|
+
alias_to_axis,
|
|
67
|
+
cast_frame_axes,
|
|
68
|
+
get_global_axis_enums,
|
|
69
|
+
rename_to_axis,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
from ._derived_params import (
|
|
73
|
+
UNITSIZE_DEFAULT,
|
|
74
|
+
_entity_unitsize_lf,
|
|
75
|
+
_node_unitsize_lf,
|
|
76
|
+
_try_param,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if TYPE_CHECKING:
|
|
80
|
+
from flextool.engine_polars._input_source import InputSource
|
|
81
|
+
|
|
82
|
+
logger = logging.getLogger(__name__)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _unitsize_complete(keys_lf: "pl.LazyFrame",
|
|
86
|
+
us_lf: "pl.LazyFrame",
|
|
87
|
+
axis: str,
|
|
88
|
+
*,
|
|
89
|
+
what: str) -> pl.DataFrame:
|
|
90
|
+
"""Project the unitsize cascade onto a structural ``axis`` set so the
|
|
91
|
+
result COVERS every member of that set — never a subset, never empty
|
|
92
|
+
for a non-empty input.
|
|
93
|
+
|
|
94
|
+
``p_unitsize`` / ``p_state_unitsize`` are *structural* LP coefficients:
|
|
95
|
+
``build_flextool`` multiplies every ``v_flow`` (resp. ``v_state`` /
|
|
96
|
+
``v_invest``) by them. A member of ``keys_lf`` (the pss processes /
|
|
97
|
+
nodeState nodes the model creates variables for) that is missing from
|
|
98
|
+
the cascade must therefore NOT be dropped — an incomplete projection
|
|
99
|
+
silently drops constraint terms, and an *empty* one yields ``None``
|
|
100
|
+
and crashes the build at ``model.py`` with
|
|
101
|
+
``TypeError: ... 'Expr' and 'NoneType'``.
|
|
102
|
+
|
|
103
|
+
We left-join the set against the cascade and coalesce any uncovered
|
|
104
|
+
member to :data:`UNITSIZE_DEFAULT` — the same fallback the cascade
|
|
105
|
+
itself applies to an entity with no explicit unitsize — so the
|
|
106
|
+
invariant "every variable's unitsize is defined" holds by
|
|
107
|
+
construction. The join key is compared as ``Utf8`` so value-equal
|
|
108
|
+
members match regardless of any Enum-dtype edge between the structural
|
|
109
|
+
set's axis and the entity-union cascade axis. A coalesced member is a
|
|
110
|
+
genuine set/vocabulary divergence upstream, so we ``warning`` the
|
|
111
|
+
offending tokens rather than absorb them silently.
|
|
112
|
+
|
|
113
|
+
For a healthy model (every member covered) this is byte-identical to
|
|
114
|
+
the prior inner-join projection: the left-join matches all rows and no
|
|
115
|
+
coalesce fires.
|
|
116
|
+
"""
|
|
117
|
+
keys = (keys_lf.select(pl.col(axis)).unique()
|
|
118
|
+
.with_columns(pl.col(axis).cast(pl.Utf8).alias("_k")))
|
|
119
|
+
rhs = us_lf.select(pl.col(axis).cast(pl.Utf8).alias("_k"), pl.col("us"))
|
|
120
|
+
joined = keys.join(rhs, on="_k", how="left").collect()
|
|
121
|
+
missing = joined.filter(pl.col("us").is_null())
|
|
122
|
+
if missing.height:
|
|
123
|
+
names = missing.get_column(axis).cast(pl.Utf8).to_list()
|
|
124
|
+
shown = ", ".join(names[:20]) + (" …" if missing.height > 20 else "")
|
|
125
|
+
logger.warning(
|
|
126
|
+
"%s: %d %r member(s) absent from the entity-unitsize cascade; "
|
|
127
|
+
"applying the canonical %.0f default. This signals a structural "
|
|
128
|
+
"set / entity-vocabulary divergence upstream: %s",
|
|
129
|
+
what, missing.height, axis, UNITSIZE_DEFAULT, shown,
|
|
130
|
+
)
|
|
131
|
+
return (joined
|
|
132
|
+
.select(pl.col(axis),
|
|
133
|
+
pl.col("us").fill_null(UNITSIZE_DEFAULT).alias("value"))
|
|
134
|
+
.sort(axis))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# §F.1 — p_unitsize (per-process unitsize, filtered to pss)
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def p_unitsize_from_source(source: "InputSource",
|
|
143
|
+
pss: pl.DataFrame | None,
|
|
144
|
+
) -> Param | None:
|
|
145
|
+
"""``p_unitsize[p]`` — per-process unitsize cascade restricted to
|
|
146
|
+
processes appearing in *pss*.
|
|
147
|
+
|
|
148
|
+
Mirrors ``flextool/engine_polars/input.py:800-825``::
|
|
149
|
+
|
|
150
|
+
unitsize_long = _read_unitsize(p_entity_unitsize.csv)
|
|
151
|
+
unitsize_p = unitsize_long.filter(p ∈ pss["p"].unique())
|
|
152
|
+
|
|
153
|
+
The Spine source for ``p_entity_unitsize`` is the cascade::
|
|
154
|
+
|
|
155
|
+
unitsize = virtual_unitsize (if explicitly set non-zero)
|
|
156
|
+
OR existing (if explicitly set non-zero)
|
|
157
|
+
OR 1000.0
|
|
158
|
+
|
|
159
|
+
Already lazified in :func:`._derived_params._entity_unitsize_lf`
|
|
160
|
+
(cluster B/C primitive); we project the cascade onto the processes in
|
|
161
|
+
*pss*. Returns ``None`` only when *pss* is empty — for a non-empty
|
|
162
|
+
*pss* the result is COMPLETE (every process covered, uncovered ones
|
|
163
|
+
defaulted to :data:`UNITSIZE_DEFAULT`), because the model treats
|
|
164
|
+
``p_unitsize`` as a structural coefficient on every ``v_flow``. See
|
|
165
|
+
:func:`_unitsize_complete`.
|
|
166
|
+
"""
|
|
167
|
+
if pss is None or pss.height == 0:
|
|
168
|
+
return None
|
|
169
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame param.
|
|
170
|
+
_enums = get_global_axis_enums()
|
|
171
|
+
if _enums is not None:
|
|
172
|
+
pss = cast_frame_axes(pss, _enums)
|
|
173
|
+
us_lf = _entity_unitsize_lf(source).pipe(rename_to_axis, {"e": "p"})
|
|
174
|
+
df = _unitsize_complete(pss.lazy(), us_lf, "p", what="p_unitsize")
|
|
175
|
+
if df.height == 0:
|
|
176
|
+
return None
|
|
177
|
+
return Param(("p",), df)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# §F.2 — p_state_unitsize (per-node unitsize, filtered to nodeState)
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def p_state_unitsize_from_source(source: "InputSource",
|
|
186
|
+
nodeState_df: pl.DataFrame | None,
|
|
187
|
+
) -> Param | None:
|
|
188
|
+
"""``p_state_unitsize[n]`` — per-node unitsize restricted to nodes
|
|
189
|
+
in *nodeState_df*.
|
|
190
|
+
|
|
191
|
+
Mirrors ``input.py:1780-1789``::
|
|
192
|
+
|
|
193
|
+
state_us_long = unitsize_long.filter(n ∈ nodeState["n"])
|
|
194
|
+
|
|
195
|
+
Uses the canonical :func:`._derived_params._node_unitsize_lf`
|
|
196
|
+
cascade. Returns ``None`` only when *nodeState_df* is empty — for a
|
|
197
|
+
non-empty *nodeState_df* the result is COMPLETE (every node covered,
|
|
198
|
+
uncovered ones defaulted to :data:`UNITSIZE_DEFAULT`), because the
|
|
199
|
+
model treats ``p_state_unitsize`` as a structural coefficient on every
|
|
200
|
+
``v_state`` / ``v_invest`` term. See :func:`_unitsize_complete`.
|
|
201
|
+
"""
|
|
202
|
+
if nodeState_df is None or nodeState_df.height == 0:
|
|
203
|
+
return None
|
|
204
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame param.
|
|
205
|
+
_enums = get_global_axis_enums()
|
|
206
|
+
if _enums is not None:
|
|
207
|
+
nodeState_df = cast_frame_axes(nodeState_df, _enums)
|
|
208
|
+
us_lf = _node_unitsize_lf(source)
|
|
209
|
+
df = _unitsize_complete(nodeState_df.lazy(), us_lf, "n",
|
|
210
|
+
what="p_state_unitsize")
|
|
211
|
+
if df.height == 0:
|
|
212
|
+
return None
|
|
213
|
+
return Param(("n",), df)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# §F.3 — p_penalty_up / p_penalty_down (sentinel-default scalar broadcast)
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _penalty_param_from_source(source: "InputSource",
|
|
222
|
+
parameter_name: str,
|
|
223
|
+
nodeBalance_df: pl.DataFrame | None,
|
|
224
|
+
dt: pl.DataFrame | None,
|
|
225
|
+
) -> Param | None:
|
|
226
|
+
"""Build ``p_penalty_<dir>[n, d, t]`` by broadcasting the per-node
|
|
227
|
+
scalar / Map (period→time) over the (n, d, t) grid restricted to
|
|
228
|
+
nodes in *nodeBalance_df*.
|
|
229
|
+
|
|
230
|
+
``node.penalty_up`` / ``node.penalty_down`` carry a sentinel
|
|
231
|
+
default (e.g. 10000.0) on the schema; the source plugin returns one
|
|
232
|
+
row per node with the scalar (default-broadcast). The CSV path
|
|
233
|
+
slices ``pdtNode.csv`` already broadcast to (n, d, t); we mirror by
|
|
234
|
+
cross-joining the per-node value against the active-solve dt grid.
|
|
235
|
+
|
|
236
|
+
Map-shaped inputs (per-period or per-(period, time) overrides) are
|
|
237
|
+
passed through their own indices and joined on the matching subset
|
|
238
|
+
of (d, t). Scalar inputs broadcast to every (d, t).
|
|
239
|
+
"""
|
|
240
|
+
if (nodeBalance_df is None or nodeBalance_df.height == 0
|
|
241
|
+
or dt is None or dt.height == 0):
|
|
242
|
+
return None
|
|
243
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame params.
|
|
244
|
+
_enums = get_global_axis_enums()
|
|
245
|
+
if _enums is not None:
|
|
246
|
+
nodeBalance_df = cast_frame_axes(nodeBalance_df, _enums)
|
|
247
|
+
dt = cast_frame_axes(dt, _enums)
|
|
248
|
+
df = _try_param(source, "node", parameter_name)
|
|
249
|
+
if df is None or df.height == 0:
|
|
250
|
+
return None
|
|
251
|
+
nb_lf = nodeBalance_df.lazy().select(pl.col("n")).unique()
|
|
252
|
+
# Defensive re-cast: re-cast d/t to canonical Enum so joins below
|
|
253
|
+
# against ``base`` (which alias_to_axis-casts already) match dtype
|
|
254
|
+
# even when ``dt`` arrives with Utf8 d/t.
|
|
255
|
+
dt_lf = (dt.lazy()
|
|
256
|
+
.select(alias_to_axis("d", "d"), alias_to_axis("t", "t"))
|
|
257
|
+
.unique())
|
|
258
|
+
cols = df.columns
|
|
259
|
+
period_col = next((c for c in ("period", "d", "x") if c in cols), None)
|
|
260
|
+
time_col = next((c for c in ("t", "time", "step") if c in cols), None)
|
|
261
|
+
base = df.lazy().select(
|
|
262
|
+
alias_to_axis("name", "n"),
|
|
263
|
+
*([alias_to_axis(period_col, "d")] if period_col else []),
|
|
264
|
+
*([alias_to_axis(time_col, "t")] if time_col else []),
|
|
265
|
+
pl.col("value").cast(pl.Float64),
|
|
266
|
+
)
|
|
267
|
+
if period_col and time_col:
|
|
268
|
+
out_lf = (nb_lf
|
|
269
|
+
.join(base, on="n", how="inner")
|
|
270
|
+
.join(dt_lf, on=["d", "t"], how="inner"))
|
|
271
|
+
elif period_col:
|
|
272
|
+
out_lf = (nb_lf
|
|
273
|
+
.join(base, on="n", how="inner")
|
|
274
|
+
.join(dt_lf, on="d", how="inner"))
|
|
275
|
+
elif time_col:
|
|
276
|
+
out_lf = (nb_lf
|
|
277
|
+
.join(base, on="n", how="inner")
|
|
278
|
+
.join(dt_lf, on="t", how="inner"))
|
|
279
|
+
else:
|
|
280
|
+
out_lf = (nb_lf
|
|
281
|
+
.join(base, on="n", how="inner")
|
|
282
|
+
.join(dt_lf, how="cross"))
|
|
283
|
+
out = (out_lf
|
|
284
|
+
.select("n", "d", "t", "value")
|
|
285
|
+
.sort("n", "d", "t")
|
|
286
|
+
.collect())
|
|
287
|
+
if out.height == 0:
|
|
288
|
+
return None
|
|
289
|
+
return Param(("n", "d", "t"), out)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def p_penalty_up_from_source(source: "InputSource",
|
|
293
|
+
nodeBalance_df: pl.DataFrame | None,
|
|
294
|
+
dt: pl.DataFrame | None,
|
|
295
|
+
) -> Param | None:
|
|
296
|
+
"""``p_penalty_up[n, d, t]`` — broadcast ``node.penalty_up`` over
|
|
297
|
+
nodeBalance × dt. See :func:`_penalty_param_from_source`.
|
|
298
|
+
"""
|
|
299
|
+
return _penalty_param_from_source(source, "penalty_up",
|
|
300
|
+
nodeBalance_df, dt)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def p_penalty_down_from_source(source: "InputSource",
|
|
304
|
+
nodeBalance_df: pl.DataFrame | None,
|
|
305
|
+
dt: pl.DataFrame | None,
|
|
306
|
+
) -> Param | None:
|
|
307
|
+
"""``p_penalty_down[n, d, t]`` — broadcast ``node.penalty_down`` over
|
|
308
|
+
nodeBalance × dt. See :func:`_penalty_param_from_source`.
|
|
309
|
+
"""
|
|
310
|
+
return _penalty_param_from_source(source, "penalty_down",
|
|
311
|
+
nodeBalance_df, dt)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# §F.4 — p_process_source_conversion_flow_coeff /
|
|
316
|
+
# p_process_sink_conversion_flow_coeff
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
#
|
|
319
|
+
# Mirrors input.py:950-1002. The CSV path reads
|
|
320
|
+
# p_process_source_conversion_flow_coeff.csv /
|
|
321
|
+
# p_process_sink_conversion_flow_coeff.csv (always emitted by flextool's
|
|
322
|
+
# input writer for indirect units), then:
|
|
323
|
+
#
|
|
324
|
+
# 1. Anti-joins zero-coef rows out of the inputs / outputs sets.
|
|
325
|
+
# 2. If any non-default (≠ 1.0) non-zero coef remains on a surviving
|
|
326
|
+
# (p, source) / (p, sink) pair, builds a Param keyed on the *full*
|
|
327
|
+
# surviving set (default-fill 1.0 where not listed). The default-
|
|
328
|
+
# fill is structural: ``v_flow * Param`` would inner-join and drop
|
|
329
|
+
# rows otherwise.
|
|
330
|
+
# 3. If every coef is 1.0 (the trivial CHP-base case), the Param is
|
|
331
|
+
# ``None`` — model.py's gate falls through to the no-coef path.
|
|
332
|
+
#
|
|
333
|
+
# Spine source: ``unit__inputNode.conversion_flow_coeff`` /
|
|
334
|
+
# ``unit__outputNode.conversion_flow_coeff``. The default value on
|
|
335
|
+
# the schema is 1.0; values are stored on the relationship class.
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _flow_coef_from_source(source: "InputSource",
|
|
339
|
+
relationship_class: str,
|
|
340
|
+
node_role: str,
|
|
341
|
+
indirect_pairs: pl.DataFrame | None,
|
|
342
|
+
) -> tuple[pl.DataFrame | None,
|
|
343
|
+
Param | None]:
|
|
344
|
+
"""Build (zero_pairs, coef_param) for a relationship's flow
|
|
345
|
+
coefficient.
|
|
346
|
+
|
|
347
|
+
*relationship_class* — ``"unit__inputNode"`` or ``"unit__outputNode"``.
|
|
348
|
+
*node_role* — ``"source"`` or ``"sink"`` (the column name to alias
|
|
349
|
+
the node dim to in the output).
|
|
350
|
+
*indirect_pairs* — the caller's surviving (p, source) / (p, sink)
|
|
351
|
+
set (post upstream filters), used to gate Param generation and
|
|
352
|
+
default-fill.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
* ``zero_pairs`` — ``(p, <node_role>)`` rows where coef == 0; the
|
|
356
|
+
caller anti-joins these out of its own inputs / outputs set.
|
|
357
|
+
* ``coef_param`` — ``Param((p, <node_role>), value)`` covering
|
|
358
|
+
*every* surviving pair (default-fill 1.0) iff any non-default,
|
|
359
|
+
non-zero coef is present; otherwise ``None``.
|
|
360
|
+
"""
|
|
361
|
+
df = _try_param(source, relationship_class, "conversion_flow_coeff")
|
|
362
|
+
if df is None or df.height == 0:
|
|
363
|
+
return None, None
|
|
364
|
+
cols = df.columns
|
|
365
|
+
# The relationship has two dims: "unit" + "node". source.parameter
|
|
366
|
+
# returns them as columns of the same names.
|
|
367
|
+
unit_col = "unit" if "unit" in cols else cols[0]
|
|
368
|
+
node_col = "node" if "node" in cols else cols[1]
|
|
369
|
+
base = df.lazy().select(
|
|
370
|
+
alias_to_axis(unit_col, "p"),
|
|
371
|
+
alias_to_axis(node_col, node_role),
|
|
372
|
+
pl.col("value").cast(pl.Float64).alias("coef"),
|
|
373
|
+
)
|
|
374
|
+
zero = (base.filter(pl.col("coef") == 0.0)
|
|
375
|
+
.select("p", node_role)
|
|
376
|
+
.collect())
|
|
377
|
+
if zero.height == 0:
|
|
378
|
+
zero = None
|
|
379
|
+
if indirect_pairs is None or indirect_pairs.height == 0:
|
|
380
|
+
return zero, None
|
|
381
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame param.
|
|
382
|
+
_enums = get_global_axis_enums()
|
|
383
|
+
if _enums is not None:
|
|
384
|
+
indirect_pairs = cast_frame_axes(indirect_pairs, _enums)
|
|
385
|
+
nondef = base.filter(
|
|
386
|
+
(pl.col("coef") != 0.0) & (pl.col("coef") != 1.0))
|
|
387
|
+
if nondef.collect().height == 0:
|
|
388
|
+
return zero, None
|
|
389
|
+
pair_lf = (indirect_pairs.lazy()
|
|
390
|
+
.select("p", node_role).unique())
|
|
391
|
+
merged = (pair_lf
|
|
392
|
+
.join(base, on=["p", node_role], how="left")
|
|
393
|
+
.with_columns(pl.col("coef").fill_null(1.0))
|
|
394
|
+
.select("p", node_role, pl.col("coef").alias("value"))
|
|
395
|
+
.sort("p", node_role)
|
|
396
|
+
.collect())
|
|
397
|
+
if merged.height == 0:
|
|
398
|
+
return zero, None
|
|
399
|
+
return zero, Param(("p", node_role), merged)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def p_process_source_conversion_flow_coeff_from_source(
|
|
403
|
+
source: "InputSource",
|
|
404
|
+
process_input_flows: pl.DataFrame | None,
|
|
405
|
+
) -> tuple[pl.DataFrame | None, Param | None]:
|
|
406
|
+
"""``p_process_source_conversion_flow_coeff`` for indirect units.
|
|
407
|
+
|
|
408
|
+
Returns ``(zero_pairs, coef_param)`` mirroring
|
|
409
|
+
``input.py:_load_indirect``'s contract (lines 950-978). The
|
|
410
|
+
caller anti-joins ``zero_pairs`` from its inputs set and assigns
|
|
411
|
+
``coef_param`` to ``flex_data.p_process_source_conversion_flow_coeff``.
|
|
412
|
+
|
|
413
|
+
*process_input_flows* — the caller's surviving (p, source) set
|
|
414
|
+
(post upstream zero-drop / classifier filter). Empty / None →
|
|
415
|
+
return ``(None, None)``.
|
|
416
|
+
"""
|
|
417
|
+
return _flow_coef_from_source(
|
|
418
|
+
source, "unit__inputNode", "source", process_input_flows)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def p_process_sink_conversion_flow_coeff_from_source(
|
|
422
|
+
source: "InputSource",
|
|
423
|
+
process_output_flows: pl.DataFrame | None,
|
|
424
|
+
) -> tuple[pl.DataFrame | None, Param | None]:
|
|
425
|
+
"""``p_process_sink_conversion_flow_coeff`` for indirect units.
|
|
426
|
+
|
|
427
|
+
Symmetric counterpart to
|
|
428
|
+
:func:`p_process_source_conversion_flow_coeff_from_source` — see
|
|
429
|
+
that helper's docstring for the contract.
|
|
430
|
+
"""
|
|
431
|
+
return _flow_coef_from_source(
|
|
432
|
+
source, "unit__outputNode", "sink", process_output_flows)
|