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,125 @@
|
|
|
1
|
+
"""energy_margin_multiplier — invest-only, negative-inflow demand margin emitter.
|
|
2
|
+
|
|
3
|
+
Runs per-solve from ``_emit_solve_time.run`` immediately AFTER
|
|
4
|
+
``emit_pdtNodeInflow`` (batch 54) and BEFORE the positive/negative inflow
|
|
5
|
+
split (batch 58), so that split inherits the margin.
|
|
6
|
+
|
|
7
|
+
FlexTool sign convention: DEMAND is NEGATIVE inflow (the exogenous
|
|
8
|
+
outflow / ``p_negative_inflow`` term); SUPPLY / generation is POSITIVE
|
|
9
|
+
inflow. The energy_margin feature inflates demand in the invest solve, so
|
|
10
|
+
it scales the negative (demand) rows.
|
|
11
|
+
|
|
12
|
+
What it does
|
|
13
|
+
------------
|
|
14
|
+
For a node with ``energy_margin_method == "inflow_multiplier"`` and an effective
|
|
15
|
+
``energy_margin_multiplier`` factor ``!= 1.0``, multiply that node's ``pdtNodeInflow``
|
|
16
|
+
rows by the factor — but ONLY:
|
|
17
|
+
|
|
18
|
+
* in the solve that carries investment periods
|
|
19
|
+
(``bool(state.solve.invest_periods.get(solve_name))``); and
|
|
20
|
+
* on NEGATIVE (demand) inflow rows (``value < 0``) — a ``> 1`` factor makes
|
|
21
|
+
the demand row MORE negative (larger demand) and must never scale a
|
|
22
|
+
positive/zero (supply / net-zero) row.
|
|
23
|
+
|
|
24
|
+
The dispatch solves see the true (un-margined) demand.
|
|
25
|
+
|
|
26
|
+
Early-return byte-parity contract
|
|
27
|
+
----------------------------------
|
|
28
|
+
If the solve is NOT an invest solve, OR no node has
|
|
29
|
+
``energy_margin_method == "inflow_multiplier"`` with an effective
|
|
30
|
+
``energy_margin_multiplier`` factor ``!= 1.0``,
|
|
31
|
+
the emitter returns WITHOUT touching the provider — the batch-54
|
|
32
|
+
``pdtNodeInflow`` frame stands untouched, so the default (nobody sets a
|
|
33
|
+
margin) is byte-identical to today.
|
|
34
|
+
|
|
35
|
+
Invariant #5 (byte-parity)
|
|
36
|
+
--------------------------
|
|
37
|
+
The re-emitted ``value`` column is rendered with the ``repr()``-based
|
|
38
|
+
:func:`_render_value_column`, NEVER ``.cast(Utf8)`` — a cast diverges from
|
|
39
|
+
``repr`` on sci-notation padding and ``NaN``. Only the scaled rows are
|
|
40
|
+
re-rendered; every other row keeps its original ``value`` string verbatim,
|
|
41
|
+
so unchanged rows are byte-identical by construction.
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import polars as pl
|
|
46
|
+
|
|
47
|
+
from flextool.engine_polars._emit_inflow_scaling import (
|
|
48
|
+
_read_keyed_float,
|
|
49
|
+
_read_pairs,
|
|
50
|
+
)
|
|
51
|
+
from flextool.engine_polars._emit_provider_io import (
|
|
52
|
+
_emit,
|
|
53
|
+
_provider_key,
|
|
54
|
+
)
|
|
55
|
+
from flextool.engine_polars._vectorize import _render_value_column
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def emit_energy_margin_inflow(
|
|
59
|
+
state,
|
|
60
|
+
solve_name,
|
|
61
|
+
input_dir,
|
|
62
|
+
solve_data_dir,
|
|
63
|
+
*,
|
|
64
|
+
provider,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Apply the invest-only, demand-only energy_margin_multiplier to pdtNodeInflow.
|
|
67
|
+
|
|
68
|
+
See the module docstring for the full contract. A missing/empty
|
|
69
|
+
method or value frame yields no manual nodes ⇒ early return.
|
|
70
|
+
"""
|
|
71
|
+
# 1. node → method and node → margin factor.
|
|
72
|
+
method_pairs = _read_pairs(
|
|
73
|
+
input_dir / "node__energy_margin_method.csv", provider=provider,
|
|
74
|
+
)
|
|
75
|
+
method_for_node = {n: m for (n, m) in method_pairs}
|
|
76
|
+
margin_for_node = _read_keyed_float(
|
|
77
|
+
input_dir / "energy_margin_multiplier.csv", provider=provider,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# 2. manual = {node: margin}; margin = value if present else 1.0; drop
|
|
81
|
+
# entries whose margin == 1.0 (a no-op).
|
|
82
|
+
manual: dict[str, float] = {}
|
|
83
|
+
for node, method in method_for_node.items():
|
|
84
|
+
if method != "inflow_multiplier":
|
|
85
|
+
continue
|
|
86
|
+
margin = margin_for_node.get(node, 1.0)
|
|
87
|
+
if margin == 1.0:
|
|
88
|
+
continue
|
|
89
|
+
manual[node] = margin
|
|
90
|
+
|
|
91
|
+
# 3. Invest-only gate + no-manual-node gate → early return (byte-parity).
|
|
92
|
+
is_invest = bool(state.solve.invest_periods.get(solve_name))
|
|
93
|
+
if not is_invest or not manual:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# 4. Read the pdtNodeInflow frame from the SAME provider key
|
|
97
|
+
# emit_pdtNodeInflow wrote it under. Columns: node, period, time,
|
|
98
|
+
# value — value is Utf8 (repr-rendered floats).
|
|
99
|
+
key = _provider_key(solve_data_dir / "pdtNodeInflow.csv")
|
|
100
|
+
df = provider.get(key)
|
|
101
|
+
if df is None or df.height == 0:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# 5. Multiply matching nodes' NEGATIVE-value (demand) rows by their
|
|
105
|
+
# margin; leave every other row (other nodes, and positive/zero supply
|
|
106
|
+
# rows) verbatim. Parse value → float, join the per-node margin (1.0
|
|
107
|
+
# for non-manual), render the scaled float via repr, and select it only
|
|
108
|
+
# where the row is a manual node AND value < 0.
|
|
109
|
+
work = df.with_columns(
|
|
110
|
+
pl.col("value").cast(pl.Float64).alias("__vf"),
|
|
111
|
+
pl.col("node")
|
|
112
|
+
.replace_strict(manual, default=1.0, return_dtype=pl.Float64)
|
|
113
|
+
.alias("__margin"),
|
|
114
|
+
)
|
|
115
|
+
scaled_str = _render_value_column(work["__vf"] * work["__margin"])
|
|
116
|
+
work = work.with_columns(scaled_str.alias("__scaled"))
|
|
117
|
+
new_frame = work.with_columns(
|
|
118
|
+
pl.when((pl.col("__margin") != 1.0) & (pl.col("__vf") < 0.0))
|
|
119
|
+
.then(pl.col("__scaled"))
|
|
120
|
+
.otherwise(pl.col("value"))
|
|
121
|
+
.alias("value"),
|
|
122
|
+
).select(df.columns)
|
|
123
|
+
|
|
124
|
+
# 6. Re-emit under the SAME key (repr-rendered value → invariant #5).
|
|
125
|
+
_emit(provider, "solve_data/pdtNodeInflow.csv", new_frame)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""energy_margin_adder — invest-only, additive negative-inflow demand margin.
|
|
2
|
+
|
|
3
|
+
Runs per-solve from ``_emit_solve_time.run`` immediately AFTER the
|
|
4
|
+
multiplier (``emit_energy_margin_inflow``, batch 54) and BEFORE the
|
|
5
|
+
positive/negative inflow split (batch 58), so that split inherits the
|
|
6
|
+
added demand. Ordering is multiply-then-add.
|
|
7
|
+
|
|
8
|
+
FlexTool sign convention: DEMAND is NEGATIVE inflow (the exogenous
|
|
9
|
+
outflow / ``p_negative_inflow`` term); SUPPLY / generation is POSITIVE
|
|
10
|
+
inflow. This feature adds ``energy_margin_adder`` MWh of EXTRA DEMAND at
|
|
11
|
+
a node in the invest solve, which — because demand is negative inflow —
|
|
12
|
+
means the node's inflow value becomes MORE NEGATIVE:
|
|
13
|
+
|
|
14
|
+
value_new = value_old - adder (adder > 0 ⇒ deeper demand)
|
|
15
|
+
|
|
16
|
+
Adding a positive number would REDUCE demand — the inverted-sign no-op.
|
|
17
|
+
The subtraction above is the ONLY correct direction.
|
|
18
|
+
|
|
19
|
+
What it does vs. the multiplier
|
|
20
|
+
-------------------------------
|
|
21
|
+
The multiplier (``_emit_energy_margin.py``) SCALES a node's existing
|
|
22
|
+
NEGATIVE inflow rows — a no-op on a node with zero native inflow. The
|
|
23
|
+
adder targets exactly those zero-inflow slack nodes: it must CREATE
|
|
24
|
+
negative demand rows for a node that has NO ``pdtNodeInflow`` row at all
|
|
25
|
+
(value ``-adder`` at every invest ``(d, t)``), as well as deepen existing
|
|
26
|
+
rows.
|
|
27
|
+
|
|
28
|
+
For a node with ``energy_margin_method == "inflow_adder"`` and an
|
|
29
|
+
effective ``energy_margin_adder`` value ``!= 0.0``, over the invest
|
|
30
|
+
solve's ``(d, t)`` grid (``steps_in_use.csv``):
|
|
31
|
+
|
|
32
|
+
* an EXISTING ``(node, d, t)`` inflow row → ``value - adder``;
|
|
33
|
+
* a MISSING ``(node, d, t)`` row → a new row with value ``-adder``.
|
|
34
|
+
|
|
35
|
+
Applied ONLY:
|
|
36
|
+
|
|
37
|
+
* in the solve that carries investment periods
|
|
38
|
+
(``bool(state.solve.invest_periods.get(solve_name))``) — dispatch
|
|
39
|
+
solves see the true, un-margined demand; and
|
|
40
|
+
* to nodes whose method is ``inflow_adder`` with a non-zero adder.
|
|
41
|
+
|
|
42
|
+
Shape support & the (d, t) broadcast
|
|
43
|
+
------------------------------------
|
|
44
|
+
A SCALAR adder is a constant per-timestep addition broadcast across every
|
|
45
|
+
invest ``(d, t)`` (the "spread uniformly" the calibrator relies on); a
|
|
46
|
+
period (or period,time) Map places per-``(d, t)`` values. Both are
|
|
47
|
+
supported: the authored shape is read from the ingested frame's explicit
|
|
48
|
+
index columns (``period`` / ``time``) — safe here because the frame has
|
|
49
|
+
already been normalised by ingestion — and broadcast over the invest grid
|
|
50
|
+
through :func:`._param_shapes.promote_param_to_dt` (NEVER by a hand-rolled
|
|
51
|
+
column-name cross-join; invariant #2).
|
|
52
|
+
|
|
53
|
+
Early-return byte-parity contract
|
|
54
|
+
----------------------------------
|
|
55
|
+
If the solve is NOT an invest solve, OR no node has
|
|
56
|
+
``energy_margin_method == "inflow_adder"`` with an effective adder
|
|
57
|
+
``!= 0.0``, the emitter returns WITHOUT touching the provider — the
|
|
58
|
+
batch-54 ``pdtNodeInflow`` frame stands untouched, so the default (nobody
|
|
59
|
+
sets an adder) is byte-identical to today.
|
|
60
|
+
|
|
61
|
+
RHS-only (protects warm-start)
|
|
62
|
+
------------------------------
|
|
63
|
+
This emitter only mutates the ``pdtNodeInflow`` provider frame — a
|
|
64
|
+
nodeBalance RHS constant. It introduces NO new variable, constraint,
|
|
65
|
+
column or row into the LP: the node already carries its balance rows;
|
|
66
|
+
creating an inflow row for an existing node only changes the inflow
|
|
67
|
+
constant, never adds an LP row.
|
|
68
|
+
|
|
69
|
+
Invariant #5 (byte-parity)
|
|
70
|
+
--------------------------
|
|
71
|
+
Every touched (deepened) or created row's ``value`` is rendered with the
|
|
72
|
+
``repr()``-based :func:`_render_value_column`, NEVER ``.cast(Utf8)``.
|
|
73
|
+
Untouched rows (other nodes, non-invest (d,t) — none here since the grid
|
|
74
|
+
IS the invest grid — and any row of a node that isn't an ``inflow_adder``
|
|
75
|
+
node) keep their original ``value`` string verbatim.
|
|
76
|
+
|
|
77
|
+
Known limitation — ``no_inflow`` nodes and group accounting
|
|
78
|
+
-----------------------------------------------------------
|
|
79
|
+
The core ``nodeBalance_eq`` reads ``p_inflow`` DIRECTLY from
|
|
80
|
+
``pdtNodeInflow.csv`` (``model.py`` ~L1640), so the added demand IS
|
|
81
|
+
served for every node type, including one set ``inflow_method="no_inflow"``.
|
|
82
|
+
BUT the ``p_positive_inflow`` / ``p_negative_inflow`` split
|
|
83
|
+
(``_emit_period_params._derive_positive_negative_inflow``) forces
|
|
84
|
+
``no_inflow`` nodes to ``0.0`` — and those two frames feed the group
|
|
85
|
+
energy-slack / capacity-margin / reserve auxiliary constraints
|
|
86
|
+
(``_group_slack.py``). So an adder on a node that is BOTH explicitly
|
|
87
|
+
``no_inflow`` AND a member of such a group is served by the balance yet
|
|
88
|
+
invisible to that group's accounting. This does NOT affect the
|
|
89
|
+
calibrator (its targets are default ``use_original`` nodes, which flow
|
|
90
|
+
through both channels consistently); fix it when ``capacity_margin``
|
|
91
|
+
integration lands, where the split can be made adder-aware with full
|
|
92
|
+
context.
|
|
93
|
+
"""
|
|
94
|
+
from __future__ import annotations
|
|
95
|
+
|
|
96
|
+
import polars as pl
|
|
97
|
+
|
|
98
|
+
from polar_high import Param
|
|
99
|
+
|
|
100
|
+
from flextool.engine_polars._emit_inflow_scaling import (
|
|
101
|
+
_read_pairs,
|
|
102
|
+
)
|
|
103
|
+
from flextool.engine_polars._emit_provider_io import (
|
|
104
|
+
_emit,
|
|
105
|
+
_provider_key,
|
|
106
|
+
)
|
|
107
|
+
from flextool.engine_polars._param_shapes import promote_param_to_dt
|
|
108
|
+
from flextool.engine_polars._vectorize import _render_value_column
|
|
109
|
+
|
|
110
|
+
# The pdtNodeInflow frame is all-Utf8: node, period, time, value.
|
|
111
|
+
_PDT_COLS = ("node", "period", "time", "value")
|
|
112
|
+
|
|
113
|
+
# The authored adder ingests through three specs (see _specs.py): a
|
|
114
|
+
# scalar float, a period Map (1d), and a period-time Map (2d). Each lands
|
|
115
|
+
# in its own CSV / Provider key; the emitter reads all three and unions
|
|
116
|
+
# the broadcast frames. A node has a scalar OR a map adder, never both.
|
|
117
|
+
_ADDER_FILES = (
|
|
118
|
+
"energy_margin_adder.csv", # scalar float → shape (node,)
|
|
119
|
+
"pd_energy_margin_adder.csv", # 1d Map → shape (node, d)
|
|
120
|
+
"pdt_energy_margin_adder.csv", # 2d Map → shape (node, d, t)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _broadcast_authored_adder(adder_df, manual_nodes, dt_grid):
|
|
125
|
+
"""Broadcast one authored adder frame over the invest (d, t) grid.
|
|
126
|
+
|
|
127
|
+
*adder_df* is an all-Utf8 ingested frame whose FIRST column is the
|
|
128
|
+
node and whose LAST column is the value; any middle columns are the
|
|
129
|
+
authored Map index axes (``period`` and/or ``time``). Returns a frame
|
|
130
|
+
with columns ``node, period, time, __adder`` (Float64 adder) restricted
|
|
131
|
+
to *manual_nodes*, or ``None`` when there is nothing to apply (empty
|
|
132
|
+
frame, an unrecognised index axis, or all-zero / null adders).
|
|
133
|
+
"""
|
|
134
|
+
if adder_df is None or adder_df.height == 0:
|
|
135
|
+
return None
|
|
136
|
+
cols = adder_df.columns
|
|
137
|
+
if len(cols) < 2:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Resolve the authored shape from the frame's explicit index columns
|
|
141
|
+
# and build the rename → canonical (node, d, t) Param dims. Any index
|
|
142
|
+
# column that is neither ``period`` nor ``time`` is an unrecognised
|
|
143
|
+
# axis for this parameter — skip this frame rather than guessing a
|
|
144
|
+
# broadcast (byte-parity).
|
|
145
|
+
node_col, value_col = cols[0], cols[-1]
|
|
146
|
+
rename: dict[str, str] = {node_col: "node"}
|
|
147
|
+
dims: list[str] = ["node"]
|
|
148
|
+
for c in cols[1:-1]:
|
|
149
|
+
cl = c.strip().lower()
|
|
150
|
+
if cl == "period":
|
|
151
|
+
rename[c] = "d"
|
|
152
|
+
dims.append("d")
|
|
153
|
+
elif cl in ("time", "t"):
|
|
154
|
+
rename[c] = "t"
|
|
155
|
+
dims.append("t")
|
|
156
|
+
else:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Restrict to the manual (inflow_adder) nodes, cast the value to
|
|
160
|
+
# Float64, and drop null / zero adders (a zero adder is a no-op).
|
|
161
|
+
work = (
|
|
162
|
+
adder_df.rename(rename)
|
|
163
|
+
.with_columns(
|
|
164
|
+
pl.col(value_col).cast(pl.Float64, strict=False).alias("value"),
|
|
165
|
+
)
|
|
166
|
+
.filter(pl.col("node").is_in(list(manual_nodes)))
|
|
167
|
+
.filter(pl.col("value").is_not_null() & (pl.col("value") != 0.0))
|
|
168
|
+
.select([*dims, "value"])
|
|
169
|
+
)
|
|
170
|
+
if work.height == 0:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# Broadcast the authored shape over the invest solve's (d, t) grid.
|
|
174
|
+
# Route through _param_shapes' promote_param_to_dt (invariant #2 —
|
|
175
|
+
# never a hand-rolled column-name cross-join): a SCALAR (node,) Param
|
|
176
|
+
# cross-joins the whole grid; a (node, d) / (node, d, t) Param joins on
|
|
177
|
+
# its authored axis.
|
|
178
|
+
adder_dt = (
|
|
179
|
+
promote_param_to_dt(Param(tuple(dims), work), dt_grid)
|
|
180
|
+
.select("node", "d", "t", "value")
|
|
181
|
+
.collect()
|
|
182
|
+
.rename({"d": "period", "t": "time", "value": "__adder"})
|
|
183
|
+
)
|
|
184
|
+
if adder_dt.height == 0:
|
|
185
|
+
return None
|
|
186
|
+
return adder_dt
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def emit_energy_margin_adder(
|
|
190
|
+
state,
|
|
191
|
+
solve_name,
|
|
192
|
+
input_dir,
|
|
193
|
+
solve_data_dir,
|
|
194
|
+
*,
|
|
195
|
+
provider,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Apply the invest-only additive energy_margin_adder to pdtNodeInflow.
|
|
198
|
+
|
|
199
|
+
See the module docstring for the full contract. A non-invest solve,
|
|
200
|
+
no ``inflow_adder`` node, or an all-zero / missing adder frame yields
|
|
201
|
+
an early return (byte-parity).
|
|
202
|
+
"""
|
|
203
|
+
# 1. node → method; manual = the inflow_adder nodes.
|
|
204
|
+
method_pairs = _read_pairs(
|
|
205
|
+
input_dir / "node__energy_margin_method.csv", provider=provider,
|
|
206
|
+
)
|
|
207
|
+
manual_nodes = {n for (n, m) in method_pairs if m == "inflow_adder"}
|
|
208
|
+
|
|
209
|
+
# 2. Invest-only gate + no-manual-node gate → early return (byte-parity).
|
|
210
|
+
is_invest = bool(state.solve.invest_periods.get(solve_name))
|
|
211
|
+
if not is_invest or not manual_nodes:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# 3. Build the invest solve's (d, t) grid once. ``steps_in_use.csv``
|
|
215
|
+
# is the current (invest) solve's active (period, time) grid — the
|
|
216
|
+
# same grid every pdtX emitter expands over.
|
|
217
|
+
dt_pairs = _read_pairs(
|
|
218
|
+
solve_data_dir / "steps_in_use.csv", provider=provider,
|
|
219
|
+
)
|
|
220
|
+
if not dt_pairs:
|
|
221
|
+
return
|
|
222
|
+
dt_grid = pl.DataFrame(
|
|
223
|
+
{"d": [d for d, _t in dt_pairs], "t": [t for _d, t in dt_pairs]},
|
|
224
|
+
schema={"d": pl.Utf8, "t": pl.Utf8},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# 4. Read every authored adder file (scalar float, period Map, and
|
|
228
|
+
# period-time Map — each in its own Provider key) and broadcast each
|
|
229
|
+
# over the (d, t) grid via _broadcast_authored_adder. A node carries
|
|
230
|
+
# a scalar OR a map adder, never both, so the per-file frames are
|
|
231
|
+
# disjoint by node; union them into one (node, period, time, __adder)
|
|
232
|
+
# frame. No authored value anywhere → early return (byte-parity).
|
|
233
|
+
parts = []
|
|
234
|
+
for fname in _ADDER_FILES:
|
|
235
|
+
adder_df = provider.get(_provider_key(input_dir / fname))
|
|
236
|
+
part = _broadcast_authored_adder(adder_df, manual_nodes, dt_grid)
|
|
237
|
+
if part is not None:
|
|
238
|
+
parts.append(part)
|
|
239
|
+
if not parts:
|
|
240
|
+
return
|
|
241
|
+
adder_dt = pl.concat(parts) if len(parts) > 1 else parts[0]
|
|
242
|
+
if adder_dt.height == 0:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# 7. Merge into pdtNodeInflow. Existing rows of an inflow_adder node
|
|
246
|
+
# are deepened (value - adder); grid cells with no existing row are
|
|
247
|
+
# CREATED with value -adder. Every other row is verbatim.
|
|
248
|
+
df = provider.get(_provider_key(solve_data_dir / "pdtNodeInflow.csv"))
|
|
249
|
+
if df is None:
|
|
250
|
+
df = pl.DataFrame(schema={c: pl.Utf8 for c in _PDT_COLS})
|
|
251
|
+
out_cols = df.columns
|
|
252
|
+
|
|
253
|
+
# 7a. Deepen existing rows. Left-join the per-(node, d, t) adder; where
|
|
254
|
+
# present, re-render (value - adder) via repr, else keep the original
|
|
255
|
+
# value string verbatim (byte-parity for untouched rows).
|
|
256
|
+
existing = df.with_columns(
|
|
257
|
+
pl.col("value").cast(pl.Float64).alias("__vf"),
|
|
258
|
+
).join(adder_dt, on=["node", "period", "time"], how="left")
|
|
259
|
+
deepened = _render_value_column(
|
|
260
|
+
existing["__vf"] - existing["__adder"].fill_null(0.0),
|
|
261
|
+
)
|
|
262
|
+
df_mod = (
|
|
263
|
+
existing.with_columns(deepened.alias("__deep"))
|
|
264
|
+
.with_columns(
|
|
265
|
+
pl.when(pl.col("__adder").is_not_null())
|
|
266
|
+
.then(pl.col("__deep"))
|
|
267
|
+
.otherwise(pl.col("value"))
|
|
268
|
+
.alias("value"),
|
|
269
|
+
)
|
|
270
|
+
.select(out_cols)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# 7b. Create rows for grid cells the node had no inflow for. value =
|
|
274
|
+
# -adder (adding demand ⇒ more negative). Deterministic order.
|
|
275
|
+
created = adder_dt.join(
|
|
276
|
+
df.select("node", "period", "time"),
|
|
277
|
+
on=["node", "period", "time"],
|
|
278
|
+
how="anti",
|
|
279
|
+
).sort(["node", "period", "time"])
|
|
280
|
+
created_frame = (
|
|
281
|
+
created.with_columns(
|
|
282
|
+
_render_value_column(-created["__adder"]).alias("value"),
|
|
283
|
+
)
|
|
284
|
+
.select(out_cols)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
new_frame = pl.concat([df_mod, created_frame])
|
|
288
|
+
|
|
289
|
+
# 8. Re-emit under the SAME key (repr-rendered value → invariant #5).
|
|
290
|
+
_emit(provider, "solve_data/pdtNodeInflow.csv", new_frame)
|