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,709 @@
|
|
|
1
|
+
"""Chain-cluster entity-period parameter writers.
|
|
2
|
+
|
|
3
|
+
Three ``write_*`` helpers emitting the per-(entity, period)
|
|
4
|
+
existing/capacity-max chain that the LP build consumes via
|
|
5
|
+
``p_entity_*_existing_*`` and ``p_entity_*_capacity_*`` parameters:
|
|
6
|
+
|
|
7
|
+
* :func:`write_p_entity_pre_existing` (12-branch).
|
|
8
|
+
* :func:`write_p_entity_existing_chain` (3 live emits).
|
|
9
|
+
* :func:`write_p_entity_capacity_max_chain` (2 live emits).
|
|
10
|
+
|
|
11
|
+
The chain-existing / chain-cap-max writers are the most state-dependent
|
|
12
|
+
writers in Phase 1: they straddle the per-solve handoff boundary (prior
|
|
13
|
+
solve's ``realized_existing``/``realized_invest``/``divest_cumulative``
|
|
14
|
+
either as in-memory ``SolveHandoff`` carriers or as CSVs left by the
|
|
15
|
+
parent solve's output writer). We keep the legacy dict-keyed lookup
|
|
16
|
+
shape — it's already optimal for the per-row access pattern and the
|
|
17
|
+
intricate branching is much easier to verify against the mod source
|
|
18
|
+
when written procedurally.
|
|
19
|
+
|
|
20
|
+
Byte-for-byte parity with the legacy emitters is the gate; tests live
|
|
21
|
+
in ``tests/engine_polars/test_writer_port_phase1.py``.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
import polars as pl
|
|
29
|
+
|
|
30
|
+
from flextool.engine_polars._emit_provider_io import (
|
|
31
|
+
_emit,
|
|
32
|
+
_provider_key,
|
|
33
|
+
)
|
|
34
|
+
from flextool.engine_polars import _provider_keys as K
|
|
35
|
+
from flextool.engine_polars._provider_translators import read_handoff_frame
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from ._solve_handoff import SolveHandoff # noqa: F401 — retained for legacy callers
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ed_value_frame(
|
|
42
|
+
rows: list[tuple[str, str, object]],
|
|
43
|
+
) -> pl.DataFrame:
|
|
44
|
+
"""Build a 3-col ``(entity, period, value)`` Utf8 frame whose ``value``
|
|
45
|
+
cells are ``repr(v)`` strings — preserving the *Python type-as-emitted*
|
|
46
|
+
semantics of the legacy ``f"{e},{d},{repr(v)}\\n"`` line builders.
|
|
47
|
+
|
|
48
|
+
Importantly we call ``repr`` directly (NOT ``repr(float(v))``):
|
|
49
|
+
``write_p_entity_divest_cumulative_max`` accumulates a ``period_sum =
|
|
50
|
+
sum(empty) → int(0)``, and the legacy CSV column for those rows is
|
|
51
|
+
``"0"``, not ``"0.0"``. Similarly
|
|
52
|
+
``write_p_entity_existing_integer_count`` emits ``repr(round(cnt))``
|
|
53
|
+
which is an int repr. Coercing to float here would silently break
|
|
54
|
+
byte-parity on either path — see commit notes for the symptom.
|
|
55
|
+
Storing ``value`` as Utf8 preserves the bit-exact precision parity the
|
|
56
|
+
legacy CSV writers achieved through ``repr`` (see
|
|
57
|
+
:mod:`._emit_arc_unions` docstring).
|
|
58
|
+
"""
|
|
59
|
+
return pl.DataFrame(
|
|
60
|
+
{
|
|
61
|
+
"entity": [r[0] for r in rows],
|
|
62
|
+
"period": [r[1] for r in rows],
|
|
63
|
+
"value": [repr(r[2]) for r in rows],
|
|
64
|
+
},
|
|
65
|
+
schema={"entity": pl.Utf8, "period": pl.Utf8, "value": pl.Utf8},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Native-frame readers.
|
|
71
|
+
#
|
|
72
|
+
# These read the in-memory polars frame directly from the Provider via
|
|
73
|
+
# ``provider.get(_provider_key(path))`` instead of round-tripping through
|
|
74
|
+
# CSV text (the legacy ``_provider_open`` + ``csv.reader`` path).
|
|
75
|
+
#
|
|
76
|
+
# Type-fidelity contract — reproduce *exactly* what ``csv.reader`` over a
|
|
77
|
+
# ``DataFrame.write_csv`` serialisation would have yielded:
|
|
78
|
+
#
|
|
79
|
+
# * Key columns (the dict-key / structural string positions) were
|
|
80
|
+
# ``csv.reader`` strings. ``write_csv`` serialises ``null`` → ``""``
|
|
81
|
+
# and any scalar (Enum / Int / Float / Utf8) → its string form. We
|
|
82
|
+
# coerce each key cell with :func:`_cell_str` and apply the original
|
|
83
|
+
# truthiness guard to the *string* form so a null cell is skipped
|
|
84
|
+
# (matching the legacy ``if row[i]`` test) while a literal ``"0"`` is
|
|
85
|
+
# kept.
|
|
86
|
+
# * Value columns were re-coerced with ``float(...)`` / ``int(...)``.
|
|
87
|
+
# We apply the same coercion to the native cell and widen the legacy
|
|
88
|
+
# ``except ValueError`` to ``except (ValueError, TypeError)`` so a
|
|
89
|
+
# null value cell is skipped (matching the legacy CSV behaviour where
|
|
90
|
+
# the empty-string value cell raised ``ValueError``).
|
|
91
|
+
#
|
|
92
|
+
# ``provider.get`` returns data rows only (no header), so there is no
|
|
93
|
+
# header row to skip; an empty / missing frame yields the same empty
|
|
94
|
+
# output the legacy loop produced.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cell_str(value: "object | None") -> str:
|
|
99
|
+
"""Reproduce a ``csv.reader`` cell string for a native frame value.
|
|
100
|
+
|
|
101
|
+
``DataFrame.write_csv`` renders ``null`` as the empty string and every
|
|
102
|
+
other scalar as its textual form; ``csv.reader`` then reads those
|
|
103
|
+
strings back. Mirror that here so dict keys / structural strings stay
|
|
104
|
+
byte-identical to the legacy CSV round-trip.
|
|
105
|
+
"""
|
|
106
|
+
return "" if value is None else str(value)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _read_singles(path: Path,
|
|
110
|
+
*, provider: "object | None" = None) -> list[str]:
|
|
111
|
+
df = provider.get(_provider_key(path))
|
|
112
|
+
if df is None:
|
|
113
|
+
return []
|
|
114
|
+
out: list[str] = []
|
|
115
|
+
for row in df.iter_rows():
|
|
116
|
+
if not row:
|
|
117
|
+
continue
|
|
118
|
+
c0 = _cell_str(row[0])
|
|
119
|
+
if c0:
|
|
120
|
+
out.append(c0)
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_pairs(path: Path,
|
|
125
|
+
*, provider: "object | None" = None) -> list[tuple[str, str]]:
|
|
126
|
+
df = provider.get(_provider_key(path))
|
|
127
|
+
if df is None:
|
|
128
|
+
return []
|
|
129
|
+
out: list[tuple[str, str]] = []
|
|
130
|
+
for row in df.iter_rows():
|
|
131
|
+
if len(row) < 2:
|
|
132
|
+
continue
|
|
133
|
+
c0, c1 = _cell_str(row[0]), _cell_str(row[1])
|
|
134
|
+
if c0 and c1:
|
|
135
|
+
out.append((c0, c1))
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _read_triples(path: Path,
|
|
140
|
+
*, provider: "object | None" = None) -> list[tuple[str, str, str]]:
|
|
141
|
+
df = provider.get(_provider_key(path))
|
|
142
|
+
if df is None:
|
|
143
|
+
return []
|
|
144
|
+
out: list[tuple[str, str, str]] = []
|
|
145
|
+
for row in df.iter_rows():
|
|
146
|
+
if len(row) < 3:
|
|
147
|
+
continue
|
|
148
|
+
c0, c1, c2 = _cell_str(row[0]), _cell_str(row[1]), _cell_str(row[2])
|
|
149
|
+
if c0 and c1 and c2:
|
|
150
|
+
out.append((c0, c1, c2))
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _load_ed_value_csv(path: Path,
|
|
155
|
+
*, provider: "object | None" = None,
|
|
156
|
+
) -> dict[tuple[str, str], float]:
|
|
157
|
+
"""``[entity, period, value]`` (data rows) → ``{(e, d): v}``."""
|
|
158
|
+
out: dict[tuple[str, str], float] = {}
|
|
159
|
+
df = provider.get(_provider_key(path))
|
|
160
|
+
if df is None:
|
|
161
|
+
return out
|
|
162
|
+
for row in df.iter_rows():
|
|
163
|
+
if len(row) < 3:
|
|
164
|
+
continue
|
|
165
|
+
c0, c1 = _cell_str(row[0]), _cell_str(row[1])
|
|
166
|
+
if c0 and c1:
|
|
167
|
+
try:
|
|
168
|
+
out[(c0, c1)] = float(row[2])
|
|
169
|
+
except (ValueError, TypeError):
|
|
170
|
+
continue
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_e_value_csv(path: Path,
|
|
175
|
+
*, provider: "object | None" = None,
|
|
176
|
+
) -> dict[str, float]:
|
|
177
|
+
"""``[entity, value]`` (data rows) → ``{e: v}``."""
|
|
178
|
+
out: dict[str, float] = {}
|
|
179
|
+
df = provider.get(_provider_key(path))
|
|
180
|
+
if df is None:
|
|
181
|
+
return out
|
|
182
|
+
for row in df.iter_rows():
|
|
183
|
+
if len(row) < 2:
|
|
184
|
+
continue
|
|
185
|
+
c0 = _cell_str(row[0])
|
|
186
|
+
if c0:
|
|
187
|
+
try:
|
|
188
|
+
out[c0] = float(row[1])
|
|
189
|
+
except (ValueError, TypeError):
|
|
190
|
+
continue
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _load_param_value_at(
|
|
195
|
+
path: Path, param_col: int, param_value: str,
|
|
196
|
+
key_cols: tuple[int, ...], value_col: int,
|
|
197
|
+
*, provider: "object | None" = None,
|
|
198
|
+
) -> dict[tuple[str, ...], float]:
|
|
199
|
+
"""``[..., param, ..., value]`` filtered to rows where column ``param_col``
|
|
200
|
+
equals ``param_value`` → ``{tuple(row[c] for c in key_cols): value}``."""
|
|
201
|
+
out: dict[tuple[str, ...], float] = {}
|
|
202
|
+
df = provider.get(_provider_key(path))
|
|
203
|
+
if df is None:
|
|
204
|
+
return out
|
|
205
|
+
for row in df.iter_rows():
|
|
206
|
+
if len(row) <= max(param_col, value_col, *key_cols):
|
|
207
|
+
continue
|
|
208
|
+
if _cell_str(row[param_col]) != param_value:
|
|
209
|
+
continue
|
|
210
|
+
key = tuple(_cell_str(row[c]) for c in key_cols)
|
|
211
|
+
if not all(key):
|
|
212
|
+
continue
|
|
213
|
+
try:
|
|
214
|
+
out[key] = float(row[value_col])
|
|
215
|
+
except (ValueError, TypeError):
|
|
216
|
+
continue
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _read_solve_first_flag(solve_data_dir: Path,
|
|
221
|
+
*, provider: "object | None" = None) -> bool:
|
|
222
|
+
"""Read ``solve_data/p_model.csv`` for the ``solveFirst`` flag."""
|
|
223
|
+
pm_path = solve_data_dir / "p_model.csv"
|
|
224
|
+
df = provider.get(_provider_key(pm_path))
|
|
225
|
+
if df is None:
|
|
226
|
+
return False
|
|
227
|
+
for row in df.iter_rows():
|
|
228
|
+
if len(row) >= 2 and _cell_str(row[0]) == "solveFirst":
|
|
229
|
+
try:
|
|
230
|
+
return bool(int(row[1]))
|
|
231
|
+
except (ValueError, TypeError):
|
|
232
|
+
return False
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# write_p_entity_pre_existing — mod L1886-1895 (12-branch lifetime-method
|
|
238
|
+
# gate × entity-kind × virtual_unitsize trichotomy).
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def derive_p_entity_pre_existing(
|
|
243
|
+
input_dir: Path, solve_data_dir: Path,
|
|
244
|
+
*, provider: "object | None" = None,
|
|
245
|
+
) -> pl.DataFrame:
|
|
246
|
+
"""Pre-existing capacity per (entity, period) — 12-branch sum where
|
|
247
|
+
exactly one branch fires per (e, d) given the method/kind/unitsize
|
|
248
|
+
trichotomy.
|
|
249
|
+
|
|
250
|
+
Equivalent simplified form (per legacy docstring):
|
|
251
|
+
|
|
252
|
+
method = entity__lifetime_method[e]
|
|
253
|
+
v_existing = pdProcess[e,'existing',d] if e in process
|
|
254
|
+
else pdNode[e,'existing',d] if e in node
|
|
255
|
+
else 0
|
|
256
|
+
v_unit = p_process[e,'virtual_unitsize'] if e in process
|
|
257
|
+
else p_node[e,'virtual_unitsize'] if e in node
|
|
258
|
+
else 0
|
|
259
|
+
if method not in {reinvest_automatic, reinvest_choice, no_investment}: 0
|
|
260
|
+
elif method ∈ {reinvest_choice, no_investment}
|
|
261
|
+
and not (p_years_d[d] <
|
|
262
|
+
Σ_{d_first ∈ period_first} (p_years_d[d_first]
|
|
263
|
+
+ edEntity_lifetime[e, d_first])):
|
|
264
|
+
0
|
|
265
|
+
else: v_existing * v_unit if v_unit else v_existing
|
|
266
|
+
|
|
267
|
+
Output domain: ``entity × period_in_use``. Reads pdProcess /
|
|
268
|
+
pdNode / edEntity_lifetime CSVs that ``write_entity_period_calc_params``
|
|
269
|
+
has emitted earlier in the same per-solve pass.
|
|
270
|
+
"""
|
|
271
|
+
process_set = frozenset(_read_singles(input_dir / "process.csv",
|
|
272
|
+
provider=provider))
|
|
273
|
+
node_set = frozenset(_read_singles(input_dir / "node.csv",
|
|
274
|
+
provider=provider))
|
|
275
|
+
entities = _read_singles(input_dir / "entity.csv", provider=provider)
|
|
276
|
+
period_in_use = _read_singles(solve_data_dir / "period_in_use_set.csv",
|
|
277
|
+
provider=provider)
|
|
278
|
+
period_first = _read_singles(solve_data_dir / "period_first.csv",
|
|
279
|
+
provider=provider)
|
|
280
|
+
|
|
281
|
+
lifetime_method: dict[str, str] = {
|
|
282
|
+
e_: m for e_, m in _read_pairs(solve_data_dir / "entity__lifetime_method.csv",
|
|
283
|
+
provider=provider)
|
|
284
|
+
}
|
|
285
|
+
p_years_d = _load_e_value_csv(solve_data_dir / "p_years_d.csv",
|
|
286
|
+
provider=provider)
|
|
287
|
+
|
|
288
|
+
ed_lifetime: dict[tuple[str, str], float] = {}
|
|
289
|
+
for e_, d_, v_ in _read_triples(solve_data_dir / "edEntity_lifetime.csv",
|
|
290
|
+
provider=provider):
|
|
291
|
+
try:
|
|
292
|
+
ed_lifetime[(e_, d_)] = float(v_)
|
|
293
|
+
except ValueError:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# pdProcess.csv / pdNode.csv: [entity, param, period, value]; filter to
|
|
297
|
+
# rows where param == 'existing'.
|
|
298
|
+
pd_existing_proc = _load_param_value_at(
|
|
299
|
+
solve_data_dir / "pdProcess.csv",
|
|
300
|
+
param_col=1, param_value="existing",
|
|
301
|
+
key_cols=(0, 2), value_col=3,
|
|
302
|
+
provider=provider,
|
|
303
|
+
)
|
|
304
|
+
pd_existing_node = _load_param_value_at(
|
|
305
|
+
solve_data_dir / "pdNode.csv",
|
|
306
|
+
param_col=1, param_value="existing",
|
|
307
|
+
key_cols=(0, 2), value_col=3,
|
|
308
|
+
provider=provider,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# p_process.csv / p_node.csv: [entity, param, value]; filter to
|
|
312
|
+
# param == 'virtual_unitsize'.
|
|
313
|
+
p_process_vu = _load_param_value_at(
|
|
314
|
+
input_dir / "p_process.csv",
|
|
315
|
+
param_col=1, param_value="virtual_unitsize",
|
|
316
|
+
key_cols=(0,), value_col=2,
|
|
317
|
+
provider=provider,
|
|
318
|
+
)
|
|
319
|
+
p_node_vu = _load_param_value_at(
|
|
320
|
+
input_dir / "p_node.csv",
|
|
321
|
+
param_col=1, param_value="virtual_unitsize",
|
|
322
|
+
key_cols=(0,), value_col=2,
|
|
323
|
+
provider=provider,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Per-entity lifetime gate sum:
|
|
327
|
+
# Σ_{d_first ∈ period_first} (p_years_d[d_first]
|
|
328
|
+
# + edEntity_lifetime[e, d_first])
|
|
329
|
+
def _life_sum(e: str) -> float:
|
|
330
|
+
return sum(
|
|
331
|
+
p_years_d.get(d_first, 0.0) + ed_lifetime.get((e, d_first), 0.0)
|
|
332
|
+
for d_first in period_first
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
rows: list[tuple[str, str, float]] = []
|
|
336
|
+
for e in entities:
|
|
337
|
+
method = lifetime_method.get(e, "")
|
|
338
|
+
is_proc = e in process_set
|
|
339
|
+
is_node = e in node_set
|
|
340
|
+
if is_proc:
|
|
341
|
+
v_unit = p_process_vu.get((e,), 0.0)
|
|
342
|
+
elif is_node:
|
|
343
|
+
v_unit = p_node_vu.get((e,), 0.0)
|
|
344
|
+
else:
|
|
345
|
+
v_unit = 0.0
|
|
346
|
+
gate_sum = (
|
|
347
|
+
_life_sum(e)
|
|
348
|
+
if method in ("reinvest_choice", "no_investment")
|
|
349
|
+
else None
|
|
350
|
+
)
|
|
351
|
+
for d in period_in_use:
|
|
352
|
+
v: float = 0.0
|
|
353
|
+
if method in ("reinvest_automatic", "reinvest_choice", "no_investment"):
|
|
354
|
+
gate_passes = method == "reinvest_automatic" or (
|
|
355
|
+
gate_sum is not None and p_years_d.get(d, 0.0) < gate_sum
|
|
356
|
+
)
|
|
357
|
+
if gate_passes:
|
|
358
|
+
if is_proc:
|
|
359
|
+
pd_e = pd_existing_proc.get((e, d), 0.0)
|
|
360
|
+
elif is_node:
|
|
361
|
+
pd_e = pd_existing_node.get((e, d), 0.0)
|
|
362
|
+
else:
|
|
363
|
+
pd_e = 0.0
|
|
364
|
+
v = pd_e * v_unit if v_unit else pd_e
|
|
365
|
+
rows.append((e, d, v))
|
|
366
|
+
|
|
367
|
+
return _ed_value_frame(rows)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def emit_p_entity_pre_existing(
|
|
371
|
+
input_dir: Path, solve_data_dir: Path,
|
|
372
|
+
*, provider,
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Emit ``p_entity_pre_existing`` to the Provider."""
|
|
375
|
+
_emit(provider, "solve_data/p_entity_pre_existing.csv",
|
|
376
|
+
derive_p_entity_pre_existing(input_dir, solve_data_dir,
|
|
377
|
+
provider=provider))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# write_p_entity_existing_chain — mod L1680-1697 (5 cascading entity-capacity
|
|
382
|
+
# params; straddles the per-solve handoff boundary).
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _load_realized_from_handoff(
|
|
387
|
+
*, provider: "object | None" = None,
|
|
388
|
+
) -> tuple[dict[tuple[str, str], float], dict[tuple[str, str], float]]:
|
|
389
|
+
"""Resolve ``(realized_existing, realized_invest)`` from the
|
|
390
|
+
Provider's ``handoff/realized_*`` carriers, populated by the
|
|
391
|
+
iteration-start translator.
|
|
392
|
+
|
|
393
|
+
Returns ``(ppec, ppic)`` where ``ppec`` maps
|
|
394
|
+
``(entity, period) → realized_existing`` and ``ppic`` maps
|
|
395
|
+
``(entity, period) → realized_invest``. An empty/None handoff
|
|
396
|
+
frame yields an empty mapping — the translator pipeline always
|
|
397
|
+
primes these carriers (header-only frame when no prior handoff
|
|
398
|
+
exists, populated frame otherwise), so this is the sole source of
|
|
399
|
+
truth.
|
|
400
|
+
"""
|
|
401
|
+
ppec: dict[tuple[str, str], float] = {}
|
|
402
|
+
ppic: dict[tuple[str, str], float] = {}
|
|
403
|
+
realized_existing = read_handoff_frame(provider, K.HANDOFF_REALIZED_EXISTING)
|
|
404
|
+
realized_invest = read_handoff_frame(provider, K.HANDOFF_REALIZED_INVEST)
|
|
405
|
+
if realized_existing is not None:
|
|
406
|
+
for r in realized_existing.iter_rows(named=True):
|
|
407
|
+
ppec[(str(r["entity"]), str(r["period"]))] = float(r["value"])
|
|
408
|
+
if realized_invest is not None:
|
|
409
|
+
for r in realized_invest.iter_rows(named=True):
|
|
410
|
+
ppic[(str(r["entity"]), str(r["period"]))] = float(r["value"])
|
|
411
|
+
return ppec, ppic
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _compute_p_entity_existing_chain(
|
|
415
|
+
input_dir: Path, solve_data_dir: Path,
|
|
416
|
+
*, provider: "object | None" = None,
|
|
417
|
+
) -> tuple[
|
|
418
|
+
list[tuple[str, str, float]], # all_existing rows
|
|
419
|
+
list[tuple[str, str, float]], # count rows
|
|
420
|
+
list[tuple[str, str, int]], # integer_count rows (legacy emits int repr)
|
|
421
|
+
list[tuple[str, str, float]], # previously_invested rows
|
|
422
|
+
]:
|
|
423
|
+
"""Compute the four row-streams emitted by
|
|
424
|
+
:func:`emit_p_entity_existing_chain` in their canonical
|
|
425
|
+
(entity-major, period-in-use-minor) order.
|
|
426
|
+
|
|
427
|
+
Kept as a single compute so the orchestrator runs it once and funnels
|
|
428
|
+
each row-stream through :func:`_emit`, so the per-sub-solve
|
|
429
|
+
:mod:`._flex_data_accumulator` captures the resulting frames.
|
|
430
|
+
"""
|
|
431
|
+
solve_first = _read_solve_first_flag(solve_data_dir, provider=provider)
|
|
432
|
+
entities = _read_singles(input_dir / "entity.csv", provider=provider)
|
|
433
|
+
periods_in_use = _read_singles(
|
|
434
|
+
solve_data_dir / "period_in_use_set.csv", provider=provider,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
pre_existing = _load_ed_value_csv(
|
|
438
|
+
solve_data_dir / "p_entity_pre_existing.csv", provider=provider,
|
|
439
|
+
)
|
|
440
|
+
unitsize = _load_e_value_csv(
|
|
441
|
+
solve_data_dir / "p_entity_unitsize.csv", provider=provider,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
edd_by_ed: dict[tuple[str, str], list[str]] = {}
|
|
445
|
+
for (e, d_h, d) in _read_triples(
|
|
446
|
+
solve_data_dir / "edd_history.csv", provider=provider,
|
|
447
|
+
):
|
|
448
|
+
edd_by_ed.setdefault((e, d), []).append(d_h)
|
|
449
|
+
|
|
450
|
+
ppec, ppic = _load_realized_from_handoff(provider=provider)
|
|
451
|
+
|
|
452
|
+
ed_history_realized: set[tuple[str, str]] = set(ppec.keys())
|
|
453
|
+
for e_, d_ in _read_pairs(
|
|
454
|
+
solve_data_dir / "ed_history_realized_first.csv", provider=provider,
|
|
455
|
+
):
|
|
456
|
+
ed_history_realized.add((e_, d_))
|
|
457
|
+
|
|
458
|
+
entity_divest = frozenset(_read_singles(
|
|
459
|
+
solve_data_dir / "entityDivest.csv", provider=provider,
|
|
460
|
+
))
|
|
461
|
+
p_divested: dict[str, float] = {}
|
|
462
|
+
divest_cumulative = read_handoff_frame(provider, K.HANDOFF_DIVEST_CUMULATIVE)
|
|
463
|
+
if divest_cumulative is not None:
|
|
464
|
+
for r in divest_cumulative.iter_rows(named=True):
|
|
465
|
+
p_divested[str(r["entity"])] = float(r["value"])
|
|
466
|
+
|
|
467
|
+
later_existing: dict[tuple[str, str], float] = {}
|
|
468
|
+
later_invested: dict[tuple[str, str], float] = {}
|
|
469
|
+
if not solve_first:
|
|
470
|
+
for e in entities:
|
|
471
|
+
for d in periods_in_use:
|
|
472
|
+
tot_e = 0.0
|
|
473
|
+
tot_i = 0.0
|
|
474
|
+
for d_h in edd_by_ed.get((e, d), ()):
|
|
475
|
+
if (e, d_h) in ed_history_realized:
|
|
476
|
+
tot_e += ppec.get((e, d_h), 0.0)
|
|
477
|
+
tot_i += ppic.get((e, d_h), 0.0)
|
|
478
|
+
later_existing[(e, d)] = tot_e
|
|
479
|
+
later_invested[(e, d)] = tot_i
|
|
480
|
+
|
|
481
|
+
all_rows: list[tuple[str, str, float]] = []
|
|
482
|
+
count_rows: list[tuple[str, str, float]] = []
|
|
483
|
+
int_count_rows: list[tuple[str, str, int]] = []
|
|
484
|
+
prev_inv_rows: list[tuple[str, str, float]] = []
|
|
485
|
+
for e in entities:
|
|
486
|
+
us = unitsize.get(e, 0.0)
|
|
487
|
+
for d in periods_in_use:
|
|
488
|
+
if solve_first:
|
|
489
|
+
v_all = pre_existing.get((e, d), 0.0)
|
|
490
|
+
else:
|
|
491
|
+
v_all = later_existing.get((e, d), 0.0)
|
|
492
|
+
if e in entity_divest:
|
|
493
|
+
v_all -= p_divested.get(e, 0.0)
|
|
494
|
+
all_rows.append((e, d, v_all))
|
|
495
|
+
cnt = v_all / us if us else 0.0
|
|
496
|
+
count_rows.append((e, d, cnt))
|
|
497
|
+
# round() with a single arg returns int in Py3 → legacy emits
|
|
498
|
+
# ``repr(int)`` (e.g. ``"3"``, not ``"3.0"``). Preserve.
|
|
499
|
+
int_count_rows.append((e, d, round(cnt)))
|
|
500
|
+
v_prev = 0.0 if solve_first else later_invested.get((e, d), 0.0)
|
|
501
|
+
prev_inv_rows.append((e, d, v_prev))
|
|
502
|
+
|
|
503
|
+
return all_rows, count_rows, int_count_rows, prev_inv_rows
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def emit_p_entity_existing_chain(
|
|
507
|
+
input_dir: Path, solve_data_dir: Path,
|
|
508
|
+
*, provider,
|
|
509
|
+
) -> None:
|
|
510
|
+
"""Emit ``p_entity_existing_chain`` to the Provider."""
|
|
511
|
+
all_rows, _count_rows, _int_rows, prev_rows = (
|
|
512
|
+
_compute_p_entity_existing_chain(
|
|
513
|
+
input_dir, solve_data_dir, provider=provider,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
_emit(provider, "solve_data/p_entity_all_existing.csv",
|
|
517
|
+
_ed_value_frame(all_rows))
|
|
518
|
+
_emit(provider,
|
|
519
|
+
"solve_data/p_entity_previously_invested_capacity.csv",
|
|
520
|
+
_ed_value_frame(prev_rows))
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
# write_p_entity_capacity_max_chain — mod L1699-1764 (4 cascading entity-
|
|
525
|
+
# capacity ceiling params).
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _compute_p_entity_capacity_max_chain(
|
|
530
|
+
input_dir: Path, solve_data_dir: Path,
|
|
531
|
+
*, provider: "object | None" = None,
|
|
532
|
+
) -> tuple[
|
|
533
|
+
list[tuple[str, str, float]], # max_capacity rows (period_in_use only)
|
|
534
|
+
list[tuple[str, str, float]], # max_units rows (full period_set)
|
|
535
|
+
list[tuple[str, str, float]], # invest_cumulative_max rows
|
|
536
|
+
list[tuple[str, str, float]], # dispatch_capacity_max rows
|
|
537
|
+
]:
|
|
538
|
+
"""Compute the four row-streams emitted by
|
|
539
|
+
:func:`emit_p_entity_capacity_max_chain` in their canonical order.
|
|
540
|
+
|
|
541
|
+
Kept as a single compute so the orchestrator funnels each stream
|
|
542
|
+
through :func:`_emit` for accumulator capture.
|
|
543
|
+
"""
|
|
544
|
+
entities = _read_singles(input_dir / "entity.csv", provider=provider)
|
|
545
|
+
periods = _read_singles(solve_data_dir / "period_set.csv", provider=provider)
|
|
546
|
+
period_in_use = frozenset(
|
|
547
|
+
_read_singles(solve_data_dir / "period_in_use_set.csv", provider=provider)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
all_existing = _load_ed_value_csv(solve_data_dir / "p_entity_all_existing.csv",
|
|
551
|
+
provider=provider)
|
|
552
|
+
cum_max_cap = _load_ed_value_csv(solve_data_dir / "ed_cumulative_max_capacity.csv",
|
|
553
|
+
provider=provider)
|
|
554
|
+
invest_max_period = _load_ed_value_csv(
|
|
555
|
+
solve_data_dir / "ed_invest_max_period.csv",
|
|
556
|
+
provider=provider,
|
|
557
|
+
)
|
|
558
|
+
unitsize = _load_e_value_csv(solve_data_dir / "p_entity_unitsize.csv",
|
|
559
|
+
provider=provider)
|
|
560
|
+
e_invest_max_total = _load_e_value_csv(
|
|
561
|
+
solve_data_dir / "e_invest_max_total.csv",
|
|
562
|
+
provider=provider,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
invest_cumulative = frozenset(
|
|
566
|
+
_read_pairs(solve_data_dir / "ed_invest_cumulative.csv", provider=provider)
|
|
567
|
+
)
|
|
568
|
+
invest_period = frozenset(
|
|
569
|
+
_read_pairs(solve_data_dir / "ed_invest_period.csv", provider=provider)
|
|
570
|
+
)
|
|
571
|
+
invest_total = frozenset(_read_singles(solve_data_dir / "e_invest_total.csv",
|
|
572
|
+
provider=provider))
|
|
573
|
+
invest_forbidden = frozenset(
|
|
574
|
+
_read_pairs(solve_data_dir / "ed_invest_forbidden_no_investment.csv",
|
|
575
|
+
provider=provider)
|
|
576
|
+
)
|
|
577
|
+
entity_invest = frozenset(_read_singles(solve_data_dir / "entityInvest.csv",
|
|
578
|
+
provider=provider))
|
|
579
|
+
|
|
580
|
+
invest_method_pairs = frozenset(
|
|
581
|
+
_read_pairs(input_dir / "entity__invest_method.csv", provider=provider)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
p_unc = 1000000.0
|
|
585
|
+
pmaxf_path = input_dir / "p_max_flow_for_unconstrained_variables.csv"
|
|
586
|
+
pmaxf_df = provider.get(_provider_key(pmaxf_path))
|
|
587
|
+
if pmaxf_df is not None:
|
|
588
|
+
max_v: float | None = None
|
|
589
|
+
for row in pmaxf_df.iter_rows():
|
|
590
|
+
if len(row) >= 2 and _cell_str(row[0]):
|
|
591
|
+
try:
|
|
592
|
+
v = float(row[1])
|
|
593
|
+
except (ValueError, TypeError):
|
|
594
|
+
continue
|
|
595
|
+
if max_v is None or v > max_v:
|
|
596
|
+
max_v = v
|
|
597
|
+
if max_v is not None:
|
|
598
|
+
p_unc = max_v
|
|
599
|
+
|
|
600
|
+
edd_invest_by_ed: dict[tuple[str, str], list[str]] = {}
|
|
601
|
+
for (e, d_inv, d) in _read_triples(solve_data_dir / "edd_invest.csv",
|
|
602
|
+
provider=provider):
|
|
603
|
+
edd_invest_by_ed.setdefault((e, d), []).append(d_inv)
|
|
604
|
+
|
|
605
|
+
max_capacity: dict[tuple[str, str], float] = {}
|
|
606
|
+
mc_rows: list[tuple[str, str, float]] = []
|
|
607
|
+
for e in entities:
|
|
608
|
+
in_total = e in invest_total
|
|
609
|
+
has_no_limit = (e, "invest_no_limit") in invest_method_pairs
|
|
610
|
+
for d in periods:
|
|
611
|
+
if d not in period_in_use:
|
|
612
|
+
max_capacity[(e, d)] = 0.0
|
|
613
|
+
continue
|
|
614
|
+
if (e, d) in invest_cumulative:
|
|
615
|
+
v = cum_max_cap.get((e, d), 0.0)
|
|
616
|
+
else:
|
|
617
|
+
v = all_existing.get((e, d), 0.0)
|
|
618
|
+
in_period = (e, d) in invest_period
|
|
619
|
+
imp = invest_max_period.get((e, d), 0.0)
|
|
620
|
+
eim = e_invest_max_total.get(e, 0.0)
|
|
621
|
+
if in_period and not in_total:
|
|
622
|
+
v += imp
|
|
623
|
+
if in_total and not in_period:
|
|
624
|
+
v += eim
|
|
625
|
+
if in_period and in_total:
|
|
626
|
+
v += max(imp, eim)
|
|
627
|
+
if has_no_limit:
|
|
628
|
+
v += p_unc
|
|
629
|
+
max_capacity[(e, d)] = v
|
|
630
|
+
mc_rows.append((e, d, v))
|
|
631
|
+
|
|
632
|
+
mu_rows: list[tuple[str, str, float]] = []
|
|
633
|
+
for e in entities:
|
|
634
|
+
us = unitsize.get(e, 0.0)
|
|
635
|
+
for d in periods:
|
|
636
|
+
mc = max_capacity[(e, d)]
|
|
637
|
+
v = mc / us if us else 0.0
|
|
638
|
+
mu_rows.append((e, d, v))
|
|
639
|
+
|
|
640
|
+
invest_cum_max: dict[tuple[str, str], float] = {}
|
|
641
|
+
icm_rows: list[tuple[str, str, float]] = []
|
|
642
|
+
for e in entities:
|
|
643
|
+
if e not in entity_invest:
|
|
644
|
+
continue
|
|
645
|
+
in_total = e in invest_total
|
|
646
|
+
has_no_limit_method = (e, "invest_no_limit") in invest_method_pairs
|
|
647
|
+
for d in periods:
|
|
648
|
+
if d not in period_in_use:
|
|
649
|
+
invest_cum_max[(e, d)] = 0.0
|
|
650
|
+
continue
|
|
651
|
+
if (e, d) in invest_cumulative:
|
|
652
|
+
v = max(
|
|
653
|
+
0.0,
|
|
654
|
+
cum_max_cap.get((e, d), 0.0)
|
|
655
|
+
- all_existing.get((e, d), 0.0),
|
|
656
|
+
)
|
|
657
|
+
elif has_no_limit_method:
|
|
658
|
+
v = p_unc
|
|
659
|
+
else:
|
|
660
|
+
v = 0.0
|
|
661
|
+
in_period = (e, d) in invest_period
|
|
662
|
+
if in_period and not in_total:
|
|
663
|
+
per_period_sum = sum(
|
|
664
|
+
invest_max_period.get((e, d_inv), 0.0)
|
|
665
|
+
for d_inv in edd_invest_by_ed.get((e, d), ())
|
|
666
|
+
if (e, d_inv) in invest_period
|
|
667
|
+
and (e, d_inv) not in invest_forbidden
|
|
668
|
+
)
|
|
669
|
+
v += per_period_sum
|
|
670
|
+
if in_total and not in_period:
|
|
671
|
+
v += e_invest_max_total.get(e, 0.0)
|
|
672
|
+
if in_period and in_total:
|
|
673
|
+
per_period_sum = sum(
|
|
674
|
+
invest_max_period.get((e, d_inv), 0.0)
|
|
675
|
+
for d_inv in edd_invest_by_ed.get((e, d), ())
|
|
676
|
+
if (e, d_inv) in invest_period
|
|
677
|
+
and (e, d_inv) not in invest_forbidden
|
|
678
|
+
)
|
|
679
|
+
v += max(
|
|
680
|
+
per_period_sum, e_invest_max_total.get(e, 0.0),
|
|
681
|
+
)
|
|
682
|
+
invest_cum_max[(e, d)] = v
|
|
683
|
+
icm_rows.append((e, d, v))
|
|
684
|
+
|
|
685
|
+
dcm_rows: list[tuple[str, str, float]] = []
|
|
686
|
+
for e in entities:
|
|
687
|
+
for d in periods:
|
|
688
|
+
if d not in period_in_use:
|
|
689
|
+
continue
|
|
690
|
+
v = all_existing.get((e, d), 0.0)
|
|
691
|
+
if e in entity_invest:
|
|
692
|
+
v += invest_cum_max.get((e, d), 0.0)
|
|
693
|
+
dcm_rows.append((e, d, v))
|
|
694
|
+
|
|
695
|
+
return mc_rows, mu_rows, icm_rows, dcm_rows
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def emit_p_entity_capacity_max_chain(
|
|
699
|
+
input_dir: Path, solve_data_dir: Path,
|
|
700
|
+
*, provider,
|
|
701
|
+
) -> None:
|
|
702
|
+
"""Emit ``p_entity_capacity_max_chain`` to the Provider."""
|
|
703
|
+
_mc_rows, mu_rows, _icm_rows, dcm_rows = _compute_p_entity_capacity_max_chain(
|
|
704
|
+
input_dir, solve_data_dir, provider=provider,
|
|
705
|
+
)
|
|
706
|
+
_emit(provider, "solve_data/p_entity_max_units.csv",
|
|
707
|
+
_ed_value_frame(mu_rows))
|
|
708
|
+
_emit(provider, "solve_data/p_entity_dispatch_capacity_max.csv",
|
|
709
|
+
_ed_value_frame(dcm_rows))
|