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,438 @@
|
|
|
1
|
+
"""Shared vectorize-per-roll helpers.
|
|
2
|
+
|
|
3
|
+
The heavy preprocessing families (``pdtProcess``, ``pdtNode``,
|
|
4
|
+
``pdtProcess_{source,sink}``, ``pdtCommodity``, ``pdtGroup``, …) each
|
|
5
|
+
derive a dense ``(domain × dt)`` frame by looping over every
|
|
6
|
+
``(entity, period, time)`` cell and calling a scalar cascade
|
|
7
|
+
(``PdtLookup.get`` and friends). That inner loop is recomputed on every
|
|
8
|
+
roll and is the per-roll preprocessing hotspot.
|
|
9
|
+
|
|
10
|
+
This module provides the reusable polars pieces that let a family
|
|
11
|
+
replace ONLY that inner compute with vectorized left-joins +
|
|
12
|
+
``coalesce`` (cascade-priority order) + group-by-sum (the folds), still
|
|
13
|
+
**per roll, over the roll's own window** — no cache, no full-domain
|
|
14
|
+
frame, no slice. See ``specs/vectorize_per_roll.md`` for the full
|
|
15
|
+
design + the adversarial-critique corrections folded into the fold here.
|
|
16
|
+
|
|
17
|
+
Engine / rendering decoupling (design §3)
|
|
18
|
+
-----------------------------------------
|
|
19
|
+
The join/coalesce/group-by graph collects to a **Float64** value column
|
|
20
|
+
under either engine (eager ``df.collect()`` default, or
|
|
21
|
+
``df.collect(engine="streaming")`` as a later per-family speed upgrade).
|
|
22
|
+
Rendering to the legacy ``repr(float)`` string form is ALWAYS the
|
|
23
|
+
post-collect Python loop :func:`_render_value_column` — engine
|
|
24
|
+
independent, so streaming is a one-line flag flip with no rendering
|
|
25
|
+
parity risk. **Never** render with ``.cast(Utf8)`` (it diverges from
|
|
26
|
+
``repr`` on sci-notation exponent padding and ``NaN``/``nan``).
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import polars as pl
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"build_entity_dt_grid",
|
|
34
|
+
"build_entity_period_grid",
|
|
35
|
+
"lift_dict_to_lookup",
|
|
36
|
+
"build_fold_frame",
|
|
37
|
+
"coalesce_value",
|
|
38
|
+
"collect_value_frame",
|
|
39
|
+
"_render_value_column",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Rendering — repr loop, engine-independent (NEVER .cast(Utf8)).
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def _render_value_column(s: pl.Series) -> pl.Series:
|
|
48
|
+
"""Render a Float64 value series to the legacy ``repr(v)`` strings.
|
|
49
|
+
|
|
50
|
+
Mirrors the legacy emitters' ``f",{repr(v)}\\n"`` exactly: a bare
|
|
51
|
+
``repr(x)`` per cell (NOT ``repr(float(x))`` — Tier-B int-vs-float
|
|
52
|
+
cells must survive where a family emits them). This is the ONLY
|
|
53
|
+
supported render path; ``.cast(Utf8)`` diverges from ``repr`` on
|
|
54
|
+
sci-notation exponent padding and ``NaN``/``nan``.
|
|
55
|
+
"""
|
|
56
|
+
return pl.Series("value", [repr(x) for x in s], dtype=pl.Utf8)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Base grid — entity-major ``domain × dt`` with integer order keys.
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def build_entity_dt_grid(
|
|
64
|
+
domain: list[tuple],
|
|
65
|
+
dt: list[tuple[str, str]],
|
|
66
|
+
*,
|
|
67
|
+
key_cols: list[str],
|
|
68
|
+
) -> pl.DataFrame:
|
|
69
|
+
"""Build the entity-major ``domain × dt`` grid.
|
|
70
|
+
|
|
71
|
+
*domain* is the legacy ``_read_pairs`` / ``_read_triples`` **list**
|
|
72
|
+
(order AND duplicates preserved — never ``.unique()`` it: S-M2). Its
|
|
73
|
+
tuples carry the entity-key columns named by *key_cols* (e.g.
|
|
74
|
+
``["process", "param"]`` or ``["process", "side", "param"]``). *dt*
|
|
75
|
+
is the ``(period, time)`` list from ``steps_in_use``.
|
|
76
|
+
|
|
77
|
+
The result carries two integer order keys:
|
|
78
|
+
|
|
79
|
+
* ``__eo`` — the domain row position (entity-major ordering key),
|
|
80
|
+
* ``__to`` — the ``dt`` row position,
|
|
81
|
+
|
|
82
|
+
so the final ``.sort([__eo, __to])`` reproduces the legacy nested
|
|
83
|
+
``for entity: for (d, t):`` emission order even under streaming
|
|
84
|
+
(which may reorder). An empty domain or dt yields a zero-row grid
|
|
85
|
+
with the correct schema (all key cols Utf8 + the two order keys).
|
|
86
|
+
"""
|
|
87
|
+
n_keys = len(key_cols)
|
|
88
|
+
base_data: dict[str, list] = {
|
|
89
|
+
key_cols[i]: [row[i] for row in domain] for i in range(n_keys)
|
|
90
|
+
}
|
|
91
|
+
base_data["__eo"] = list(range(len(domain)))
|
|
92
|
+
base = pl.DataFrame(
|
|
93
|
+
base_data,
|
|
94
|
+
schema={
|
|
95
|
+
**{c: pl.Utf8 for c in key_cols},
|
|
96
|
+
"__eo": pl.Int64,
|
|
97
|
+
},
|
|
98
|
+
).with_columns(pl.lit(1, dtype=pl.Int8).alias("__one"))
|
|
99
|
+
|
|
100
|
+
dt_df = pl.DataFrame(
|
|
101
|
+
{
|
|
102
|
+
"period": [d for (d, _t) in dt],
|
|
103
|
+
"time": [t for (_d, t) in dt],
|
|
104
|
+
"__to": list(range(len(dt))),
|
|
105
|
+
},
|
|
106
|
+
schema={"period": pl.Utf8, "time": pl.Utf8, "__to": pl.Int64},
|
|
107
|
+
).with_columns(pl.lit(1, dtype=pl.Int8).alias("__one"))
|
|
108
|
+
|
|
109
|
+
return base.join(dt_df, on="__one", how="inner").drop("__one")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Base grid — entity-major ``domain × period`` (period-only, no time).
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def build_entity_period_grid(
|
|
117
|
+
domain: list[tuple],
|
|
118
|
+
periods: list[str],
|
|
119
|
+
*,
|
|
120
|
+
key_cols: list[str],
|
|
121
|
+
) -> pl.DataFrame:
|
|
122
|
+
"""Build the entity-major ``domain × period`` grid (no time axis).
|
|
123
|
+
|
|
124
|
+
Mirror of :func:`build_entity_dt_grid` but with a single ``period``
|
|
125
|
+
axis instead of ``(period, time)`` — for the period-only families
|
|
126
|
+
(e.g. ``pdGroup``). *domain* is the legacy entity list (order AND
|
|
127
|
+
duplicates preserved — never ``.unique()`` it: S-M2), its tuples
|
|
128
|
+
carrying the entity-key columns named by *key_cols*. *periods* is the
|
|
129
|
+
``period_in_use`` list (order AND duplicates preserved — never
|
|
130
|
+
``.unique()`` it either).
|
|
131
|
+
|
|
132
|
+
The result carries two integer order keys:
|
|
133
|
+
|
|
134
|
+
* ``__eo`` — the domain row position (entity-major ordering key),
|
|
135
|
+
* ``__po`` — the ``periods`` row position,
|
|
136
|
+
|
|
137
|
+
so the final ``.sort([__eo, __po])`` reproduces the legacy nested
|
|
138
|
+
``for entity: for d:`` emission order even under streaming (which may
|
|
139
|
+
reorder). An empty domain or periods list yields a zero-row grid with
|
|
140
|
+
the correct schema (all key cols Utf8 + period Utf8 + the two order
|
|
141
|
+
keys).
|
|
142
|
+
"""
|
|
143
|
+
n_keys = len(key_cols)
|
|
144
|
+
base_data: dict[str, list] = {
|
|
145
|
+
key_cols[i]: [row[i] for row in domain] for i in range(n_keys)
|
|
146
|
+
}
|
|
147
|
+
base_data["__eo"] = list(range(len(domain)))
|
|
148
|
+
base = pl.DataFrame(
|
|
149
|
+
base_data,
|
|
150
|
+
schema={
|
|
151
|
+
**{c: pl.Utf8 for c in key_cols},
|
|
152
|
+
"__eo": pl.Int64,
|
|
153
|
+
},
|
|
154
|
+
).with_columns(pl.lit(1, dtype=pl.Int8).alias("__one"))
|
|
155
|
+
|
|
156
|
+
period_df = pl.DataFrame(
|
|
157
|
+
{
|
|
158
|
+
"period": list(periods),
|
|
159
|
+
"__po": list(range(len(periods))),
|
|
160
|
+
},
|
|
161
|
+
schema={"period": pl.Utf8, "__po": pl.Int64},
|
|
162
|
+
).with_columns(pl.lit(1, dtype=pl.Int8).alias("__one"))
|
|
163
|
+
|
|
164
|
+
return base.join(period_df, on="__one", how="inner").drop("__one")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Lookup frames — lift a cascade dict to a Float64-valued lookup frame.
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def lift_dict_to_lookup(
|
|
172
|
+
d: dict,
|
|
173
|
+
key_cols: list[str],
|
|
174
|
+
value_col: str,
|
|
175
|
+
) -> pl.DataFrame:
|
|
176
|
+
"""Lift a cascade ``dict[tuple] -> float`` to a polars lookup frame.
|
|
177
|
+
|
|
178
|
+
Lift from the already-built (last-wins-deduped) dict, NOT the raw
|
|
179
|
+
CSV — lifting from CSV would re-introduce duplicate join keys and
|
|
180
|
+
explode the left-join (S-claim4). *key_cols* names the tuple
|
|
181
|
+
positions (all Utf8); *value_col* names the Float64 value column.
|
|
182
|
+
|
|
183
|
+
An empty dict yields an explicit empty-schema frame so the
|
|
184
|
+
downstream join-key dtypes still line up (Utf8 keys + Float64 value).
|
|
185
|
+
"""
|
|
186
|
+
if not d:
|
|
187
|
+
return pl.DataFrame(
|
|
188
|
+
{c: [] for c in [*key_cols, value_col]},
|
|
189
|
+
schema={
|
|
190
|
+
**{c: pl.Utf8 for c in key_cols},
|
|
191
|
+
value_col: pl.Float64,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
keys = list(d.keys())
|
|
195
|
+
n = len(key_cols)
|
|
196
|
+
if n == 1:
|
|
197
|
+
data = {key_cols[0]: [k if not isinstance(k, tuple) else k[0]
|
|
198
|
+
for k in keys]}
|
|
199
|
+
else:
|
|
200
|
+
data = {key_cols[i]: [k[i] for k in keys] for i in range(n)}
|
|
201
|
+
data[value_col] = list(d.values())
|
|
202
|
+
return pl.DataFrame(
|
|
203
|
+
data,
|
|
204
|
+
schema={
|
|
205
|
+
**{c: pl.Utf8 for c in key_cols},
|
|
206
|
+
value_col: pl.Float64,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# The fold — stochastic (branch 1) + parent-period (branch 2).
|
|
213
|
+
# Critique-corrected: multi-parent multiplicity + stoch fall-through.
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def build_fold_frame(
|
|
217
|
+
*,
|
|
218
|
+
pbt: dict,
|
|
219
|
+
pbt_key_cols: list[str],
|
|
220
|
+
out_key_cols: list[str],
|
|
221
|
+
ts_for_d: dict[str, list[str]],
|
|
222
|
+
tb_for_d: dict[str, list[str]],
|
|
223
|
+
pe_for_d: dict[str, list[str]],
|
|
224
|
+
stoch_entities: set[str],
|
|
225
|
+
stoch_filter_cols: list[str],
|
|
226
|
+
periods: list[str],
|
|
227
|
+
) -> pl.DataFrame | None:
|
|
228
|
+
"""Vectorize the stochastic + parent-period fold (branches 1 & 2).
|
|
229
|
+
|
|
230
|
+
Reproduces ``PdtLookup.get`` branches 1-2 exactly (the scalar
|
|
231
|
+
``_pdt_lookup.py:407-463`` cascade):
|
|
232
|
+
|
|
233
|
+
* **Branch 1 (stochastic):** for an entity whose ``stoch_filter_cols``
|
|
234
|
+
key is in *stoch_entities*, sum ``pbt[(e, …, tb, ts, t)]`` over
|
|
235
|
+
``tb ∈ tb_for_d[d] × ts ∈ ts_for_d[d]``.
|
|
236
|
+
* **Branch 2 (parent-period):** sum ``pbt`` over
|
|
237
|
+
``pe ∈ pe_for_d[d], tb ∈ tb_for_d[pe], ts ∈ ts_for_d[d]``.
|
|
238
|
+
|
|
239
|
+
Critique corrections vs the template (design §4):
|
|
240
|
+
|
|
241
|
+
* **Multi-parent multiplicity (S2):** the parent expansion KEEPS
|
|
242
|
+
``pe`` in its key ``(period, pe, tb, ts)`` and is NOT
|
|
243
|
+
``.unique()``-d, so a pbt value shared by two parents is added
|
|
244
|
+
once per parent — then the join output is group-by-summed on the
|
|
245
|
+
OUTPUT key.
|
|
246
|
+
* **Stoch fall-through (S2/§12.6):** the parent fold is NOT filtered
|
|
247
|
+
to non-stoch entities. ``v_stoch`` and ``v_parent`` are computed
|
|
248
|
+
as separate frames and coalesced ``coalesce(v_stoch, v_parent)``
|
|
249
|
+
(stoch-first preserves branch priority), so a stoch entity that
|
|
250
|
+
misses branch 1 can still hit branch 2.
|
|
251
|
+
* **Duplicate-row invariant (S3):** the expansion is NOT
|
|
252
|
+
``.unique()``-d — production (``_read_pairs_to_dict``) does not
|
|
253
|
+
dedup, so it sums once per duplicate ``(tb)``/``(ts)``/``(pe)``
|
|
254
|
+
occurrence; matching that requires preserving duplicates here.
|
|
255
|
+
|
|
256
|
+
*pbt_key_cols* names the pbt entity-key columns (``["process",
|
|
257
|
+
"param"]`` for pdtProcess/pdtNode, ``["process", "side", "param"]``
|
|
258
|
+
for PerSide). *out_key_cols* names the OUTPUT group key (entity key
|
|
259
|
+
cols + ``["period", "time"]``). *stoch_filter_cols* names the
|
|
260
|
+
column(s) the stoch membership test keys on — note the PerSide M1
|
|
261
|
+
quirk: it filters on the ``process`` column ALONE, not the 3-col key.
|
|
262
|
+
|
|
263
|
+
Returns a ``(*out_key_cols, "v_fold")`` frame, or ``None`` when no
|
|
264
|
+
fold rows exist (no pbt / no expansion / empty join).
|
|
265
|
+
"""
|
|
266
|
+
if not pbt:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
# --- pbt frame: entity-key cols + (tb, ts, time) + v_pbt ---------------
|
|
270
|
+
keys = list(pbt.keys())
|
|
271
|
+
n_ent = len(pbt_key_cols)
|
|
272
|
+
pbt_data: dict[str, list] = {
|
|
273
|
+
pbt_key_cols[i]: [k[i] for k in keys] for i in range(n_ent)
|
|
274
|
+
}
|
|
275
|
+
pbt_data["tb"] = [k[n_ent] for k in keys]
|
|
276
|
+
pbt_data["ts"] = [k[n_ent + 1] for k in keys]
|
|
277
|
+
pbt_data["time"] = [k[n_ent + 2] for k in keys]
|
|
278
|
+
pbt_data["v_pbt"] = list(pbt.values())
|
|
279
|
+
pbt_df = pl.DataFrame(
|
|
280
|
+
pbt_data,
|
|
281
|
+
schema={
|
|
282
|
+
**{c: pl.Utf8 for c in pbt_key_cols},
|
|
283
|
+
"tb": pl.Utf8,
|
|
284
|
+
"ts": pl.Utf8,
|
|
285
|
+
"time": pl.Utf8,
|
|
286
|
+
"v_pbt": pl.Float64,
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# --- expansion rows (preserve duplicates — S3) -------------------------
|
|
291
|
+
# Stoch: (period, tb, ts) for ts in ts_for_d[d], tb in tb_for_d[d].
|
|
292
|
+
# Parent: (period, pe, tb, ts) for ts in ts_for_d[d], pe in pe_for_d[d],
|
|
293
|
+
# tb in tb_for_d[pe]. Keep pe (S2 multiplicity).
|
|
294
|
+
#
|
|
295
|
+
# The expansion is a function of the DISTINCT period set: the legacy
|
|
296
|
+
# scalar cascade recomputes the same fold value for every ``(d, t)``
|
|
297
|
+
# cell of a period, so the per-output-key group-by-sum below must see
|
|
298
|
+
# each ``(period, tb, ts)`` / ``(period, pe, tb, ts)`` term exactly
|
|
299
|
+
# once per period — NOT once per ``(d, t)`` cell. *periods* is the
|
|
300
|
+
# caller's ``[d for (d, _t) in dt]`` list, which repeats a period once
|
|
301
|
+
# per timestep; de-duplicate it order-preserving here so a multi-
|
|
302
|
+
# timestep period does not inflate the fold by its timestep count.
|
|
303
|
+
# (This is distinct from the S3 ``tb``/``ts``/``pe`` duplicate-row
|
|
304
|
+
# invariant, which is preserved verbatim within a single period.)
|
|
305
|
+
seen: set[str] = set()
|
|
306
|
+
uniq_periods: list[str] = []
|
|
307
|
+
for d in periods:
|
|
308
|
+
if d not in seen:
|
|
309
|
+
seen.add(d)
|
|
310
|
+
uniq_periods.append(d)
|
|
311
|
+
stoch_period: list[str] = []
|
|
312
|
+
stoch_tb: list[str] = []
|
|
313
|
+
stoch_ts: list[str] = []
|
|
314
|
+
par_period: list[str] = []
|
|
315
|
+
par_pe: list[str] = []
|
|
316
|
+
par_tb: list[str] = []
|
|
317
|
+
par_ts: list[str] = []
|
|
318
|
+
for d in uniq_periods:
|
|
319
|
+
ts_list = ts_for_d.get(d, ())
|
|
320
|
+
tb_list = tb_for_d.get(d, ())
|
|
321
|
+
pe_list = pe_for_d.get(d, ())
|
|
322
|
+
for ts in ts_list:
|
|
323
|
+
for tb in tb_list:
|
|
324
|
+
stoch_period.append(d)
|
|
325
|
+
stoch_tb.append(tb)
|
|
326
|
+
stoch_ts.append(ts)
|
|
327
|
+
for pe in pe_list:
|
|
328
|
+
for tb in tb_for_d.get(pe, ()):
|
|
329
|
+
par_period.append(d)
|
|
330
|
+
par_pe.append(pe)
|
|
331
|
+
par_tb.append(tb)
|
|
332
|
+
par_ts.append(ts)
|
|
333
|
+
|
|
334
|
+
stoch_list = list(stoch_entities)
|
|
335
|
+
|
|
336
|
+
# --- branch 1: stochastic fold ----------------------------------------
|
|
337
|
+
v_stoch = None
|
|
338
|
+
if stoch_period and stoch_list:
|
|
339
|
+
stoch_exp = pl.DataFrame(
|
|
340
|
+
{"period": stoch_period, "tb": stoch_tb, "ts": stoch_ts},
|
|
341
|
+
schema={"period": pl.Utf8, "tb": pl.Utf8, "ts": pl.Utf8},
|
|
342
|
+
)
|
|
343
|
+
j = pbt_df.join(stoch_exp, on=["tb", "ts"], how="inner")
|
|
344
|
+
# Stoch membership keys on stoch_filter_cols (process alone for
|
|
345
|
+
# PerSide; the full entity key for pdtProcess/pdtNode).
|
|
346
|
+
if len(stoch_filter_cols) == 1:
|
|
347
|
+
mask = pl.col(stoch_filter_cols[0]).is_in(stoch_list)
|
|
348
|
+
else:
|
|
349
|
+
# multi-col membership — stoch_entities holds tuples
|
|
350
|
+
stoch_struct = pl.struct(stoch_filter_cols)
|
|
351
|
+
mask = stoch_struct.is_in(stoch_list)
|
|
352
|
+
j = j.filter(mask)
|
|
353
|
+
if j.height > 0:
|
|
354
|
+
v_stoch = (
|
|
355
|
+
j.group_by(out_key_cols)
|
|
356
|
+
.agg(pl.col("v_pbt").sum().alias("v_stoch"))
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# --- branch 2: parent-period fold (NO stoch filter — fall-through) -----
|
|
360
|
+
v_parent = None
|
|
361
|
+
if par_period:
|
|
362
|
+
par_exp = pl.DataFrame(
|
|
363
|
+
{
|
|
364
|
+
"period": par_period,
|
|
365
|
+
"pe": par_pe,
|
|
366
|
+
"tb": par_tb,
|
|
367
|
+
"ts": par_ts,
|
|
368
|
+
},
|
|
369
|
+
schema={
|
|
370
|
+
"period": pl.Utf8,
|
|
371
|
+
"pe": pl.Utf8,
|
|
372
|
+
"tb": pl.Utf8,
|
|
373
|
+
"ts": pl.Utf8,
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
j = pbt_df.join(par_exp, on=["tb", "ts"], how="inner")
|
|
377
|
+
if j.height > 0:
|
|
378
|
+
v_parent = (
|
|
379
|
+
j.group_by(out_key_cols)
|
|
380
|
+
.agg(pl.col("v_pbt").sum().alias("v_parent"))
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# --- coalesce stoch-first ---------------------------------------------
|
|
384
|
+
if v_stoch is not None and v_parent is not None:
|
|
385
|
+
fold = (
|
|
386
|
+
v_stoch.join(
|
|
387
|
+
v_parent, on=out_key_cols, how="full", coalesce=True,
|
|
388
|
+
)
|
|
389
|
+
.with_columns(
|
|
390
|
+
pl.coalesce(pl.col("v_stoch"), pl.col("v_parent"))
|
|
391
|
+
.alias("v_fold")
|
|
392
|
+
)
|
|
393
|
+
.select([*out_key_cols, "v_fold"])
|
|
394
|
+
)
|
|
395
|
+
elif v_stoch is not None:
|
|
396
|
+
fold = v_stoch.rename({"v_stoch": "v_fold"})
|
|
397
|
+
elif v_parent is not None:
|
|
398
|
+
fold = v_parent.rename({"v_parent": "v_fold"})
|
|
399
|
+
else:
|
|
400
|
+
return None
|
|
401
|
+
return fold
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
# Coalesce assembler + collect.
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
def coalesce_value(exprs: list[pl.Expr], alias: str = "value_f") -> pl.Expr:
|
|
409
|
+
"""``pl.coalesce`` over *exprs* in cascade-priority order."""
|
|
410
|
+
return pl.coalesce(exprs).alias(alias)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def collect_value_frame(
|
|
414
|
+
lf: "pl.LazyFrame | pl.DataFrame",
|
|
415
|
+
*,
|
|
416
|
+
key_cols: list[str],
|
|
417
|
+
value_f_col: str = "value_f",
|
|
418
|
+
sort_cols: list[str] | None = None,
|
|
419
|
+
engine: str = "eager",
|
|
420
|
+
) -> pl.DataFrame:
|
|
421
|
+
"""Collect the join/coalesce graph and render the value column.
|
|
422
|
+
|
|
423
|
+
Collects under *engine* (``"eager"`` → ``lf.collect()``; anything
|
|
424
|
+
else → ``lf.collect(engine="streaming")``), sorts by *sort_cols*
|
|
425
|
+
(default ``["__eo", "__to"]`` for entity-major order), renders the
|
|
426
|
+
Float64 *value_f_col* via :func:`_render_value_column` AFTER collect
|
|
427
|
+
(engine-independent), and selects ``[*key_cols, "value"]``.
|
|
428
|
+
"""
|
|
429
|
+
if sort_cols is None:
|
|
430
|
+
sort_cols = ["__eo", "__to"]
|
|
431
|
+
if isinstance(lf, pl.LazyFrame):
|
|
432
|
+
df = lf.collect() if engine == "eager" else lf.collect(
|
|
433
|
+
engine="streaming")
|
|
434
|
+
else:
|
|
435
|
+
df = lf
|
|
436
|
+
df = df.sort(sort_cols)
|
|
437
|
+
value = _render_value_column(df[value_f_col])
|
|
438
|
+
return df.select(key_cols).with_columns(value)
|