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,990 @@
|
|
|
1
|
+
"""Cluster E — block-layout consumers (Δ.9).
|
|
2
|
+
|
|
3
|
+
Lazy-polars port of flextool's block-aware derived helpers. Cluster E is
|
|
4
|
+
the fifth of six derived-helper port phases per
|
|
5
|
+
``audit/native_data_path_design_derived_clusters.md``.
|
|
6
|
+
|
|
7
|
+
Cluster E fields (per the schematic):
|
|
8
|
+
|
|
9
|
+
* ``nodeStateBlock`` — set: nodes pulling daily-aggregation balance.
|
|
10
|
+
* ``period_block`` / ``period_block_succ`` / ``period_block_time``
|
|
11
|
+
— multi-resolution block decomposition for storage state.
|
|
12
|
+
* ``arc_sink_block_dt`` / ``arc_source_block_dt`` — per-arc daily-block
|
|
13
|
+
aggregation index ``(p, source, sink, d, b_first, t, weight)``.
|
|
14
|
+
* ``p_arc_sink_weight`` / ``p_arc_source_weight`` —
|
|
15
|
+
``Param[(p, source, sink, d, t), weight]`` projected from the above.
|
|
16
|
+
* ``dtttdt_block_interior`` — interior-of-block dtttdt rows.
|
|
17
|
+
* ``nodeState_last_dt`` — ``(n, d, t)`` last fine-step of last block.
|
|
18
|
+
* ``flow_to_n`` / ``flow_from_n`` — block-compatibility filtered.
|
|
19
|
+
* ``flow_from_nodeBalance_eff`` / ``flow_from_nodeBalance_noEff`` —
|
|
20
|
+
block-compatibility filtered source-side nodeBalance arcs.
|
|
21
|
+
|
|
22
|
+
All consumers read ``BlockLayout``'s in-memory frames; no helper
|
|
23
|
+
re-reads ``solve_data/{entity_block,process_side_block,
|
|
24
|
+
block_step_duration,overlap_set,block_period_time_*}.csv``.
|
|
25
|
+
|
|
26
|
+
The single ``BlockLayout`` is built once per solve via
|
|
27
|
+
``BlockLayout.load_from_solve_data`` (or, post-Γ.8, natively from
|
|
28
|
+
``BlockLayout.build``). ``BlockBundle`` wraps the layout with cached
|
|
29
|
+
derived frames (``block_compat_frame``, ``process_side_block_lf`` etc.)
|
|
30
|
+
that the cluster E helpers join against.
|
|
31
|
+
|
|
32
|
+
Design decisions:
|
|
33
|
+
|
|
34
|
+
* **One BlockLayout per solve.** Repeated CSV reads were the Δ.2 carry-
|
|
35
|
+
over; cluster E folds every consumer onto one shared layout.
|
|
36
|
+
* **Cache ``block_compat`` and the rename'd join helpers.** Lazy frames
|
|
37
|
+
share the cached materialisation; downstream `.collect()` once at
|
|
38
|
+
the rim.
|
|
39
|
+
* **Identity-trivial fast path.** Single-block fixtures (``work_base``
|
|
40
|
+
etc.) collapse the filter joins to no-ops because
|
|
41
|
+
``block_compat`` carries only ``(default, default)``.
|
|
42
|
+
* **Closes the Δ.3 `flow_to_n` / `flow_from_n` gap.** The block-aware
|
|
43
|
+
filter previously lived only in the CSV path
|
|
44
|
+
(``input.py::_load_process_topology``); the helper here mirrors it
|
|
45
|
+
on the source-driven path so multi-block fixtures' `db_direct_parity`
|
|
46
|
+
test stops being a known gap.
|
|
47
|
+
|
|
48
|
+
Reference: ``flextool/engine_polars/input.py::_load_process_topology``
|
|
49
|
+
(lines 703-783) and ``input.py::_load_storage`` (lines 1647-1699,
|
|
50
|
+
2233-2253).
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from dataclasses import dataclass, field
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import TYPE_CHECKING
|
|
57
|
+
|
|
58
|
+
import polars as pl
|
|
59
|
+
|
|
60
|
+
from polar_high import Param
|
|
61
|
+
|
|
62
|
+
from flextool.engine_polars._axis_enums import (
|
|
63
|
+
alias_to_axis,
|
|
64
|
+
cast_dim,
|
|
65
|
+
cast_frame_axes,
|
|
66
|
+
get_global_axis_enums,
|
|
67
|
+
lit_axis,
|
|
68
|
+
rename_to_axis,
|
|
69
|
+
schema_dtype,
|
|
70
|
+
)
|
|
71
|
+
from flextool.engine_polars._block_layout import (
|
|
72
|
+
DEFAULT_BLOCK,
|
|
73
|
+
BlockLayout,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Substrate handle for the cascade-wide axis enum vocabulary.
|
|
78
|
+
# Bare ``None`` here; ``cast_dim`` / ``schema_dtype`` in
|
|
79
|
+
# ``_axis_enums`` fall back to ``_LIVE_AXIS_ENUMS_CTX`` (the live
|
|
80
|
+
# ContextVar) when this is ``None``, so substrate sites pick up
|
|
81
|
+
# activation set by ``load_flextool`` automatically.
|
|
82
|
+
_enums: "dict | None" = None
|
|
83
|
+
|
|
84
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# BlockBundle — BlockLayout + cached lazy join frames
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class BlockBundle:
|
|
95
|
+
"""Cached lazy-frame surface over a :class:`BlockLayout`.
|
|
96
|
+
|
|
97
|
+
The bundle wraps a single per-solve :class:`BlockLayout` and exposes
|
|
98
|
+
pre-renamed lazy frames keyed for cluster E consumers' joins. The
|
|
99
|
+
rename + cache pattern matches the schematic's "cache ``overlap_set``
|
|
100
|
+
per (b_coarse, b_fine) pair" recommendation — every helper that
|
|
101
|
+
joins on ``block_compat`` shares the same materialised frame.
|
|
102
|
+
|
|
103
|
+
Attributes
|
|
104
|
+
----------
|
|
105
|
+
layout : BlockLayout
|
|
106
|
+
The underlying per-solve block layout.
|
|
107
|
+
process_side_block_lf : pl.LazyFrame
|
|
108
|
+
Renamed ``(p, side, b_f)`` lazy frame for arc-side block lookups.
|
|
109
|
+
entity_block_lf : pl.LazyFrame
|
|
110
|
+
Renamed ``(n, b)`` lazy frame for node-side block lookups.
|
|
111
|
+
block_compat_frame : pl.DataFrame
|
|
112
|
+
Cached compatibility set ``(b, b_f)`` derived from
|
|
113
|
+
``overlap_set`` — populated lazily on first access.
|
|
114
|
+
block_step_duration_arc_lf : pl.LazyFrame
|
|
115
|
+
Renamed ``(b_f, d, t, weight)`` lazy frame for per-arc weight
|
|
116
|
+
materialisation.
|
|
117
|
+
block_period_time_first_lf : pl.LazyFrame
|
|
118
|
+
Renamed ``(b, d, t)`` lazy frame for first-step boundaries.
|
|
119
|
+
block_period_time_last_lf : pl.LazyFrame
|
|
120
|
+
Renamed ``(b, d, t)`` lazy frame for last-step boundaries.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
layout: BlockLayout
|
|
124
|
+
_block_compat_cached: pl.DataFrame | None = field(default=None, repr=False)
|
|
125
|
+
_coarse_blocks_cached: list[str] | None = field(default=None, repr=False)
|
|
126
|
+
_is_multi_block_cached: bool | None = field(default=None, repr=False)
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
# Lazy-frame surface (joins consume these)
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def process_side_block_lf(self) -> pl.LazyFrame:
|
|
134
|
+
"""Lazy ``(p, side, b_f)`` frame, or empty when no block data."""
|
|
135
|
+
f = self.layout.process_side_block_frame
|
|
136
|
+
if f.height == 0:
|
|
137
|
+
return pl.LazyFrame(schema={
|
|
138
|
+
"p": schema_dtype(_enums, "p"),
|
|
139
|
+
"side": pl.Utf8,
|
|
140
|
+
"b_f": schema_dtype(_enums, "b_f"),
|
|
141
|
+
})
|
|
142
|
+
return f.lazy().pipe(rename_to_axis, {"process": "p", "block": "b_f"})
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def entity_block_lf(self) -> pl.LazyFrame:
|
|
146
|
+
"""Lazy ``(n, bk)`` frame, or empty when no block data.
|
|
147
|
+
|
|
148
|
+
The block axis column is named ``bk`` (not ``b``) to disambiguate
|
|
149
|
+
from the branch axis — see the b_collision review note in
|
|
150
|
+
``schemas/flextool_axis_contract.json``.
|
|
151
|
+
"""
|
|
152
|
+
f = self.layout.entity_block_frame
|
|
153
|
+
if f.height == 0:
|
|
154
|
+
return pl.LazyFrame(schema={
|
|
155
|
+
"n": schema_dtype(_enums, "n"),
|
|
156
|
+
"bk": schema_dtype(_enums, "bk"),
|
|
157
|
+
})
|
|
158
|
+
return f.lazy().pipe(rename_to_axis, {"entity": "n", "block": "bk"})
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def block_step_duration_arc_lf(self) -> pl.LazyFrame:
|
|
162
|
+
"""Lazy ``(b_f, d, t, weight)`` arc-keyed step duration."""
|
|
163
|
+
f = self.layout.block_step_duration_frame
|
|
164
|
+
if f.height == 0:
|
|
165
|
+
return pl.LazyFrame(schema={
|
|
166
|
+
"b_f": pl.Utf8,
|
|
167
|
+
"d": schema_dtype(_enums, "d"),
|
|
168
|
+
"t": schema_dtype(_enums, "t"),
|
|
169
|
+
"weight": pl.Float64,
|
|
170
|
+
})
|
|
171
|
+
return f.lazy().pipe(rename_to_axis, {
|
|
172
|
+
"block": "b_f", "period": "d",
|
|
173
|
+
"step": "t", "step_duration": "weight",
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def block_period_time_first_lf(self) -> pl.LazyFrame:
|
|
178
|
+
f = self.layout.block_period_time_first_frame
|
|
179
|
+
if f.height == 0:
|
|
180
|
+
return pl.LazyFrame(schema={
|
|
181
|
+
"bk": schema_dtype(_enums, "bk"),
|
|
182
|
+
"d": schema_dtype(_enums, "d"),
|
|
183
|
+
"t": schema_dtype(_enums, "t"),
|
|
184
|
+
})
|
|
185
|
+
return f.lazy().pipe(rename_to_axis, {"block": "bk", "period": "d", "step": "t"})
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def block_period_time_last_lf(self) -> pl.LazyFrame:
|
|
189
|
+
f = self.layout.block_period_time_last_frame
|
|
190
|
+
if f.height == 0:
|
|
191
|
+
return pl.LazyFrame(schema={
|
|
192
|
+
"bk": schema_dtype(_enums, "bk"),
|
|
193
|
+
"d": schema_dtype(_enums, "d"),
|
|
194
|
+
"t": schema_dtype(_enums, "t"),
|
|
195
|
+
})
|
|
196
|
+
return f.lazy().pipe(rename_to_axis, {"block": "bk", "period": "d", "step": "t"})
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def block_compat_frame(self) -> pl.DataFrame:
|
|
200
|
+
"""Cached ``(b, b_f)`` overlap-derived compatibility set.
|
|
201
|
+
|
|
202
|
+
Computed on first access from ``layout.overlap_set_frame``;
|
|
203
|
+
materialised once per :class:`BlockBundle` instance.
|
|
204
|
+
"""
|
|
205
|
+
if self._block_compat_cached is None:
|
|
206
|
+
self._block_compat_cached = self.layout.block_compat()
|
|
207
|
+
return self._block_compat_cached
|
|
208
|
+
|
|
209
|
+
def is_multi_block(self) -> bool:
|
|
210
|
+
"""Return ``True`` when the layout exercises >1 distinct block.
|
|
211
|
+
|
|
212
|
+
Cached: layout is immutable for the bundle's lifetime.
|
|
213
|
+
"""
|
|
214
|
+
if self._is_multi_block_cached is None:
|
|
215
|
+
bsd = self.layout.block_step_duration_frame
|
|
216
|
+
if bsd.height == 0:
|
|
217
|
+
self._is_multi_block_cached = False
|
|
218
|
+
else:
|
|
219
|
+
self._is_multi_block_cached = bsd["block"].n_unique() >= 2
|
|
220
|
+
return self._is_multi_block_cached
|
|
221
|
+
|
|
222
|
+
def coarse_blocks(self, threshold: float = 1.0) -> list[str]:
|
|
223
|
+
"""Return blocks with at least one row of ``step_duration >
|
|
224
|
+
threshold``.
|
|
225
|
+
|
|
226
|
+
Cached for the canonical ``threshold=1.0`` (the schematic's
|
|
227
|
+
rule for "coarse" blocks). Other thresholds bypass the cache.
|
|
228
|
+
"""
|
|
229
|
+
if threshold == 1.0:
|
|
230
|
+
if self._coarse_blocks_cached is None:
|
|
231
|
+
self._coarse_blocks_cached = self.layout.coarse_blocks(
|
|
232
|
+
threshold=1.0,
|
|
233
|
+
)
|
|
234
|
+
return self._coarse_blocks_cached
|
|
235
|
+
return self.layout.coarse_blocks(threshold=threshold)
|
|
236
|
+
|
|
237
|
+
def has_block_data(self) -> bool:
|
|
238
|
+
"""Return ``True`` when any block frame has data."""
|
|
239
|
+
return (
|
|
240
|
+
self.layout.process_side_block_frame.height > 0
|
|
241
|
+
or self.layout.entity_block_frame.height > 0
|
|
242
|
+
or self.layout.block_step_duration_frame.height > 0
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Workdir bridge — load BlockBundle from solve_data/ CSVs
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def load_block_bundle(
|
|
252
|
+
workdir: Path | None,
|
|
253
|
+
*,
|
|
254
|
+
block_layout: "BlockLayout | None" = None,
|
|
255
|
+
provider: "object | None" = None,
|
|
256
|
+
) -> BlockBundle | None:
|
|
257
|
+
"""Load a :class:`BlockBundle` from a Provider (preferred) or an
|
|
258
|
+
in-memory layout.
|
|
259
|
+
|
|
260
|
+
When ``block_layout`` is supplied (Phase 2 multi-block fast-path),
|
|
261
|
+
wrap that layout directly. Otherwise route through
|
|
262
|
+
:meth:`BlockLayout.load_from_solve_data` against *provider*.
|
|
263
|
+
|
|
264
|
+
The legacy ``workdir``-only signature is honoured by seeding an
|
|
265
|
+
ephemeral Provider from ``workdir/solve_data`` via
|
|
266
|
+
:func:`_input_source.seed_provider_from_dir`. This keeps off-
|
|
267
|
+
cascade tests (and the few callers that still pass a workdir
|
|
268
|
+
without a Provider) working while the cascade itself never
|
|
269
|
+
touches disk.
|
|
270
|
+
|
|
271
|
+
Returns ``None`` if neither input yields a non-empty layout.
|
|
272
|
+
"""
|
|
273
|
+
if block_layout is not None:
|
|
274
|
+
if block_layout.is_empty():
|
|
275
|
+
return None
|
|
276
|
+
return BlockBundle(layout=block_layout)
|
|
277
|
+
if provider is None and workdir is None:
|
|
278
|
+
return None
|
|
279
|
+
if provider is None and workdir is not None:
|
|
280
|
+
sd = Path(workdir) / "solve_data"
|
|
281
|
+
if not sd.exists():
|
|
282
|
+
return None
|
|
283
|
+
sd = (
|
|
284
|
+
Path(workdir) / "solve_data" if workdir is not None
|
|
285
|
+
else Path(".")
|
|
286
|
+
)
|
|
287
|
+
layout = BlockLayout.load_from_solve_data(sd, provider=provider)
|
|
288
|
+
if layout.is_empty():
|
|
289
|
+
return None
|
|
290
|
+
return BlockBundle(layout=layout)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# §3.3.1 — flow_to_n / flow_from_n block-aware filter (Δ.3 gap closure)
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def filter_flow_n_by_block(
|
|
299
|
+
flow_n: pl.DataFrame,
|
|
300
|
+
bundle: BlockBundle | None,
|
|
301
|
+
*,
|
|
302
|
+
side: str,
|
|
303
|
+
) -> pl.DataFrame:
|
|
304
|
+
"""Apply block-compatibility filter to ``flow_to_n`` / ``flow_from_n``.
|
|
305
|
+
|
|
306
|
+
Mirror of ``input.py::_load_process_topology`` lines 728-782. An
|
|
307
|
+
arc ``(p, source, sink)`` contributes to node ``n``'s nodeBalance
|
|
308
|
+
iff ``(b_n, b_f)`` exists in :py:attr:`BlockBundle.block_compat_frame`,
|
|
309
|
+
where:
|
|
310
|
+
|
|
311
|
+
* ``b_n`` is the entity-block of the destination node ``n``
|
|
312
|
+
(default = ``DEFAULT_BLOCK`` when missing).
|
|
313
|
+
* ``b_f`` is the process-side block on *side* (default =
|
|
314
|
+
``DEFAULT_BLOCK`` when missing).
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
flow_n : pl.DataFrame
|
|
319
|
+
Schema ``[p, source, sink, n]``.
|
|
320
|
+
bundle : BlockBundle or None
|
|
321
|
+
When ``None`` or empty / single-block, filter is a no-op
|
|
322
|
+
(returns ``flow_n`` unchanged).
|
|
323
|
+
side : str
|
|
324
|
+
``"sink"`` (for ``flow_to_n``) or ``"source"``
|
|
325
|
+
(for ``flow_from_n``).
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
pl.DataFrame
|
|
330
|
+
The filtered frame. Mirrors the reference's "only replace if
|
|
331
|
+
the filter actually drops rows" guard so empty-overlap fixtures
|
|
332
|
+
keep their pre-filter shape.
|
|
333
|
+
"""
|
|
334
|
+
if flow_n is None or flow_n.height == 0:
|
|
335
|
+
return flow_n
|
|
336
|
+
if bundle is None:
|
|
337
|
+
return flow_n
|
|
338
|
+
psb_f = bundle.layout.process_side_block_frame
|
|
339
|
+
eb_f = bundle.layout.entity_block_frame
|
|
340
|
+
compat = bundle.block_compat_frame
|
|
341
|
+
if psb_f.height == 0 or eb_f.height == 0 or compat.height == 0:
|
|
342
|
+
return flow_n
|
|
343
|
+
|
|
344
|
+
psb_side = (
|
|
345
|
+
bundle.process_side_block_lf
|
|
346
|
+
.filter(pl.col("side") == side)
|
|
347
|
+
.select("p", "b_f")
|
|
348
|
+
)
|
|
349
|
+
eb_lf = bundle.entity_block_lf
|
|
350
|
+
|
|
351
|
+
# Align ``n`` dtype across both sides. Under activation
|
|
352
|
+
# ``flow_n.n`` carries e-vocabulary (node + process tokens for
|
|
353
|
+
# indirect units' arcs); entity_block_lf.n carries n-vocabulary
|
|
354
|
+
# (nodes only). Per contract n ⊂ e, so up-cast entity_block_lf.n
|
|
355
|
+
# to e-Enum and the join composes natively in Enum. Process
|
|
356
|
+
# tokens in flow_n.n won't match any entity_block_lf row (left
|
|
357
|
+
# join produces null block info) and the subsequent inner join
|
|
358
|
+
# with ``compat`` drops them — same semantics as the prior
|
|
359
|
+
# Utf8-roundtrip but without the materialisation.
|
|
360
|
+
#
|
|
361
|
+
# Block join keys (bk, b_f) use the cast_dim fill: substrate
|
|
362
|
+
# produces them as block-Enum under activation, Utf8 otherwise.
|
|
363
|
+
# The ``DEFAULT_BLOCK`` fill must use lit_axis so the literal
|
|
364
|
+
# matches the column dtype.
|
|
365
|
+
eb_lf_e = eb_lf.with_columns(cast_dim(pl.col("n"), None, "e"))
|
|
366
|
+
flow_n_e = flow_n.with_columns(cast_dim(pl.col("n"), None, "e"))
|
|
367
|
+
with_blocks = (
|
|
368
|
+
flow_n_e.lazy()
|
|
369
|
+
.join(psb_side, on="p", how="left")
|
|
370
|
+
.join(eb_lf_e, on="n", how="left")
|
|
371
|
+
.with_columns(
|
|
372
|
+
b_f=pl.col("b_f").fill_null(lit_axis(DEFAULT_BLOCK, "block")),
|
|
373
|
+
bk=pl.col("bk").fill_null(lit_axis(DEFAULT_BLOCK, "block")),
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
filtered = (
|
|
377
|
+
with_blocks
|
|
378
|
+
.join(compat.lazy(), on=["bk", "b_f"], how="inner")
|
|
379
|
+
.select("p", "source", "sink", "n")
|
|
380
|
+
.unique()
|
|
381
|
+
.collect()
|
|
382
|
+
)
|
|
383
|
+
# Match reference: only replace if filter dropped rows. The
|
|
384
|
+
# ``filtered.height > 0`` guard preserves the pre-filter frame for
|
|
385
|
+
# fixtures whose overlap_set is degenerate-but-non-empty.
|
|
386
|
+
if 0 < filtered.height < flow_n.height:
|
|
387
|
+
return filtered
|
|
388
|
+
return flow_n
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def flow_to_n_block_filtered(
|
|
392
|
+
pss: pl.DataFrame,
|
|
393
|
+
bundle: BlockBundle | None,
|
|
394
|
+
) -> pl.DataFrame:
|
|
395
|
+
"""Build ``flow_to_n`` (``n = sink``) and apply the block-aware filter.
|
|
396
|
+
|
|
397
|
+
Schema: ``[p, source, sink, n]``. The block-aware filter is the
|
|
398
|
+
Δ.3 gap closure — flextool's CSV path filters in
|
|
399
|
+
``_load_process_topology``; the source-driven path now mirrors the
|
|
400
|
+
same filter.
|
|
401
|
+
"""
|
|
402
|
+
if pss is None or pss.height == 0:
|
|
403
|
+
# Empty-fallback dtype: ``n`` carries e-vocabulary (sink/source
|
|
404
|
+
# values are e-typed under activation; the column legitimately
|
|
405
|
+
# mixes node + process tokens for indirect units' arcs).
|
|
406
|
+
# Declare it ``e`` so empty and populated branches agree.
|
|
407
|
+
return pl.DataFrame(schema={
|
|
408
|
+
"p": schema_dtype(_enums, "p"),
|
|
409
|
+
"source": schema_dtype(_enums, "source"),
|
|
410
|
+
"sink": schema_dtype(_enums, "sink"),
|
|
411
|
+
"n": schema_dtype(_enums, "e"),
|
|
412
|
+
})
|
|
413
|
+
# Cross-axis projection: ``sink`` carries e-axis tokens (mix of
|
|
414
|
+
# node + process names). ``alias_to_axis("sink", "e")`` casts
|
|
415
|
+
# to e-Enum under activation, preserving every token. The
|
|
416
|
+
# downstream block-filter join in ``filter_flow_n_by_block``
|
|
417
|
+
# also up-casts entity_block_lf.n to e-Enum (n ⊂ e), so the
|
|
418
|
+
# join composes natively without Utf8 materialisation.
|
|
419
|
+
base = (
|
|
420
|
+
pss.lazy()
|
|
421
|
+
.with_columns(alias_to_axis(pl.col("sink"), "e").alias("n"))
|
|
422
|
+
.select("p", "source", "sink", "n")
|
|
423
|
+
.sort("p", "source", "sink", "n")
|
|
424
|
+
.collect()
|
|
425
|
+
)
|
|
426
|
+
return filter_flow_n_by_block(base, bundle, side="sink")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def flow_from_n_block_filtered(
|
|
430
|
+
pss: pl.DataFrame,
|
|
431
|
+
bundle: BlockBundle | None,
|
|
432
|
+
) -> pl.DataFrame:
|
|
433
|
+
"""Build ``flow_from_n`` (``n = source``) with block-aware filter."""
|
|
434
|
+
if pss is None or pss.height == 0:
|
|
435
|
+
return pl.DataFrame(schema={
|
|
436
|
+
"p": schema_dtype(_enums, "p"),
|
|
437
|
+
"source": schema_dtype(_enums, "source"),
|
|
438
|
+
"sink": schema_dtype(_enums, "sink"),
|
|
439
|
+
"n": schema_dtype(_enums, "e"),
|
|
440
|
+
})
|
|
441
|
+
base = (
|
|
442
|
+
pss.lazy()
|
|
443
|
+
# Cross-axis projection: ``source`` is e-axis (node + process
|
|
444
|
+
# union). Cast to e-Enum (preserves every token) so the
|
|
445
|
+
# downstream block-filter join on ``n`` composes natively in
|
|
446
|
+
# Enum after entity_block_lf.n is up-cast to e as well.
|
|
447
|
+
.with_columns(alias_to_axis(pl.col("source"), "e").alias("n"))
|
|
448
|
+
.select("p", "source", "sink", "n")
|
|
449
|
+
.sort("p", "source", "sink", "n")
|
|
450
|
+
.collect()
|
|
451
|
+
)
|
|
452
|
+
return filter_flow_n_by_block(base, bundle, side="source")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# §3.9 — flow_from_nodeBalance block filter
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def flow_from_nodeBalance_block_filtered(
|
|
461
|
+
flow_from_nb: pl.DataFrame | None,
|
|
462
|
+
bundle: BlockBundle | None,
|
|
463
|
+
) -> pl.DataFrame | None:
|
|
464
|
+
"""Apply the block-compatibility filter to source-side nodeBalance arcs.
|
|
465
|
+
|
|
466
|
+
Mirror of ``input.py::_load_storage`` lines 1664-1699. The
|
|
467
|
+
``flow_from_nodeBalance_eff`` / ``flow_from_nodeBalance_noEff``
|
|
468
|
+
frames carry ``(p, source, sink, n=source)``; the filter drops arcs
|
|
469
|
+
whose source-block doesn't overlap the destination node's block.
|
|
470
|
+
|
|
471
|
+
Returns the filtered frame (possibly identical to input when no
|
|
472
|
+
filter applies). ``None`` in → ``None`` out.
|
|
473
|
+
"""
|
|
474
|
+
if flow_from_nb is None or flow_from_nb.height == 0:
|
|
475
|
+
return flow_from_nb
|
|
476
|
+
if bundle is None:
|
|
477
|
+
return flow_from_nb
|
|
478
|
+
return filter_flow_n_by_block(flow_from_nb, bundle, side="source")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ---------------------------------------------------------------------------
|
|
482
|
+
# Δ.27 — flow_from_nodeBalance_{eff,noEff} source-driven seed
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def flow_from_nodeBalance_seed(
|
|
487
|
+
pss_partition: pl.DataFrame | None,
|
|
488
|
+
nodeBalance: pl.DataFrame | None,
|
|
489
|
+
bundle: BlockBundle | None = None,
|
|
490
|
+
) -> pl.DataFrame | None:
|
|
491
|
+
"""Source-driven seed for ``flow_from_nodeBalance_eff`` /
|
|
492
|
+
``flow_from_nodeBalance_noEff``.
|
|
493
|
+
|
|
494
|
+
Mirror of the inline derivation in
|
|
495
|
+
``input.py::_load_storage`` lines 1658-1705 (the slow path's
|
|
496
|
+
storage loader). Per the dispatch:
|
|
497
|
+
|
|
498
|
+
::
|
|
499
|
+
|
|
500
|
+
flow_from_nb_<part> = pss_<part>
|
|
501
|
+
.filter(source ∈ nodeBalance.n)
|
|
502
|
+
.with_columns(n=source)
|
|
503
|
+
.select(p, source, sink, n)
|
|
504
|
+
|
|
505
|
+
Then the block-compatibility filter (when *bundle* carries an
|
|
506
|
+
``overlap_set``) drops arc rows whose source-block doesn't overlap
|
|
507
|
+
the destination node's block.
|
|
508
|
+
|
|
509
|
+
The two partitions (``eff`` / ``noEff``) share this logic; the
|
|
510
|
+
caller passes the appropriate ``process_source_sink_eff`` /
|
|
511
|
+
``_noEff`` partition to produce the matching field.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
pss_partition : pl.DataFrame or None
|
|
516
|
+
``process_source_sink_eff`` or ``_noEff`` — schema
|
|
517
|
+
``[p, source, sink]``. ``None`` → returns ``None``.
|
|
518
|
+
nodeBalance : pl.DataFrame or None
|
|
519
|
+
``[n]`` set frame. Empty / ``None`` → returns ``None`` (no
|
|
520
|
+
nodeBalance nodes means there are no source-side flows to
|
|
521
|
+
gather).
|
|
522
|
+
bundle : BlockBundle or None
|
|
523
|
+
When supplied, applies the source-side block-compat filter
|
|
524
|
+
(mirrors :func:`flow_from_nodeBalance_block_filtered`).
|
|
525
|
+
``None`` keeps the unfiltered seed.
|
|
526
|
+
|
|
527
|
+
Returns
|
|
528
|
+
-------
|
|
529
|
+
pl.DataFrame or None
|
|
530
|
+
``[p, source, sink, n]`` with ``n = source`` and the filter
|
|
531
|
+
applied. ``None`` when the inputs cannot produce a non-empty
|
|
532
|
+
frame.
|
|
533
|
+
"""
|
|
534
|
+
if pss_partition is None or pss_partition.height == 0:
|
|
535
|
+
return None
|
|
536
|
+
if nodeBalance is None or nodeBalance.height == 0:
|
|
537
|
+
return None
|
|
538
|
+
if "n" not in nodeBalance.columns:
|
|
539
|
+
return None
|
|
540
|
+
# Cross-axis is_in: nodeBalance.n is n-Enum (n ⊂ e), pss.source is
|
|
541
|
+
# e-Enum. Up-cast nodeBalance.n to e-Enum (Pattern 2) so the
|
|
542
|
+
# membership filter composes natively in Enum. Project ``n`` as
|
|
543
|
+
# e-Enum via alias_to_axis — matches the downstream block-filter
|
|
544
|
+
# join in :func:`filter_flow_n_by_block`, which also up-casts
|
|
545
|
+
# entity_block_lf.n to e-Enum.
|
|
546
|
+
nb_nodes_e = nodeBalance.lazy().select(cast_dim(pl.col("n"), None, "e"))
|
|
547
|
+
seed = (
|
|
548
|
+
pss_partition.lazy()
|
|
549
|
+
.filter(pl.col("source").is_in(nb_nodes_e.collect()["n"]))
|
|
550
|
+
.with_columns(alias_to_axis(pl.col("source"), "e").alias("n"))
|
|
551
|
+
.select("p", "source", "sink", "n")
|
|
552
|
+
.unique()
|
|
553
|
+
.sort("p", "source", "sink", "n")
|
|
554
|
+
.collect()
|
|
555
|
+
)
|
|
556
|
+
if seed.height == 0:
|
|
557
|
+
return None
|
|
558
|
+
if bundle is not None:
|
|
559
|
+
return filter_flow_n_by_block(seed, bundle, side="source")
|
|
560
|
+
return seed
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
# §3.9.2 — nodeStateBlock multi-resolution synthesis
|
|
565
|
+
# ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def nodeStateBlock_lf(
|
|
569
|
+
bundle: BlockBundle | None,
|
|
570
|
+
explicit_intraperiod: pl.LazyFrame | None,
|
|
571
|
+
node_set: set[str] | None = None,
|
|
572
|
+
*,
|
|
573
|
+
coarse_threshold: float = 1.0,
|
|
574
|
+
) -> pl.LazyFrame:
|
|
575
|
+
"""Synthesise ``nodeStateBlock`` per audit §3.9.2.
|
|
576
|
+
|
|
577
|
+
Two contributing branches:
|
|
578
|
+
|
|
579
|
+
1. **Explicit method**: nodes whose
|
|
580
|
+
``storage_binding_method == 'bind_intraperiod_blocks'`` enter
|
|
581
|
+
the set verbatim.
|
|
582
|
+
2. **Multi-resolution synthesis**: when ``bundle`` carries >=2
|
|
583
|
+
distinct blocks AND a node entity is assigned a coarse block
|
|
584
|
+
(``step_duration > coarse_threshold``), that node is folded
|
|
585
|
+
into the set so the daily-aggregation balance fires.
|
|
586
|
+
|
|
587
|
+
Returns a lazy ``[n]`` frame, possibly empty.
|
|
588
|
+
"""
|
|
589
|
+
parts: list[pl.LazyFrame] = []
|
|
590
|
+
if explicit_intraperiod is not None:
|
|
591
|
+
parts.append(
|
|
592
|
+
explicit_intraperiod.select("n").unique()
|
|
593
|
+
)
|
|
594
|
+
if bundle is not None and bundle.is_multi_block():
|
|
595
|
+
coarse = bundle.coarse_blocks(threshold=coarse_threshold)
|
|
596
|
+
if coarse:
|
|
597
|
+
eb_lf = bundle.entity_block_lf
|
|
598
|
+
picked = (
|
|
599
|
+
eb_lf
|
|
600
|
+
.filter(pl.col("bk").is_in(coarse))
|
|
601
|
+
.select(pl.col("n"))
|
|
602
|
+
)
|
|
603
|
+
if node_set is not None:
|
|
604
|
+
picked = picked.filter(
|
|
605
|
+
pl.col("n").is_in(list(node_set)))
|
|
606
|
+
parts.append(picked.unique())
|
|
607
|
+
if not parts:
|
|
608
|
+
return pl.LazyFrame(schema={"n": schema_dtype(_enums, "n")})
|
|
609
|
+
out = pl.concat(parts).unique().sort("n")
|
|
610
|
+
return out
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------------------------------------
|
|
614
|
+
# §3.9.3 — period_block / period_block_succ / period_block_time
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def period_block_multi_resolution_lf(
|
|
619
|
+
bundle: BlockBundle | None,
|
|
620
|
+
*,
|
|
621
|
+
coarse_threshold: float = 1.0,
|
|
622
|
+
) -> dict[str, pl.LazyFrame] | None:
|
|
623
|
+
"""Build the multi-resolution synthesis branch of period_block_*.
|
|
624
|
+
|
|
625
|
+
Returns a dict ``{"period_block": LF, "period_block_succ": LF,
|
|
626
|
+
"period_block_time": LF}`` when *bundle* exercises multiple blocks
|
|
627
|
+
AND at least one block is coarse (per *coarse_threshold*). Returns
|
|
628
|
+
``None`` when the synthesis branch doesn't fire (caller falls back
|
|
629
|
+
to the timeset-based default branch).
|
|
630
|
+
|
|
631
|
+
Mirror of ``input.py:1985-2126`` and the multi-resolution path of
|
|
632
|
+
``period_block_family_from_source`` in ``_derived_params.py``.
|
|
633
|
+
"""
|
|
634
|
+
if bundle is None or not bundle.is_multi_block():
|
|
635
|
+
return None
|
|
636
|
+
coarse = bundle.coarse_blocks(threshold=coarse_threshold)
|
|
637
|
+
if not coarse:
|
|
638
|
+
return None
|
|
639
|
+
eb = bundle.layout.entity_block_frame
|
|
640
|
+
coarse_use_set = set(
|
|
641
|
+
eb.filter(pl.col("block").is_in(coarse))["block"]
|
|
642
|
+
.unique().to_list()
|
|
643
|
+
)
|
|
644
|
+
if not coarse_use_set:
|
|
645
|
+
return None
|
|
646
|
+
coarse_use = sorted(coarse_use_set)
|
|
647
|
+
|
|
648
|
+
bsd = bundle.layout.block_step_duration_frame
|
|
649
|
+
bsd_c = bsd.filter(pl.col("block").is_in(coarse_use))
|
|
650
|
+
if bsd_c.height == 0:
|
|
651
|
+
return None
|
|
652
|
+
|
|
653
|
+
# period_block: (d, b_first) — coarse block step list.
|
|
654
|
+
new_pb = (
|
|
655
|
+
bsd_c
|
|
656
|
+
.pipe(rename_to_axis, {"period": "d", "step": "b_first"})
|
|
657
|
+
.select("d", "b_first")
|
|
658
|
+
.unique()
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# period_block_succ: cyclic per (block, period).
|
|
662
|
+
#
|
|
663
|
+
# WARNING — legacy mirror, NOT on any live path (this function is
|
|
664
|
+
# exported in ``__all__`` but has no caller; the live producer is
|
|
665
|
+
# ``_derived_params.period_block_family_from_source``). The live
|
|
666
|
+
# producer segments the cyclic loop PER representative period so a
|
|
667
|
+
# coarse node cannot chain storage across months-apart representative
|
|
668
|
+
# days (the coarse-storage relaxation bug). This mirror still closes
|
|
669
|
+
# ONE loop over the whole period. If this function is ever revived it
|
|
670
|
+
# MUST replicate that seam segmentation (see ``_repday_segment_map``);
|
|
671
|
+
# doing it here needs the fine-timeline ranks, which this ``bundle``-
|
|
672
|
+
# only signature does not carry.
|
|
673
|
+
succ_rows: list[tuple[str, str, str]] = []
|
|
674
|
+
bsd_sorted = (
|
|
675
|
+
bsd_c
|
|
676
|
+
.pipe(rename_to_axis, {"period": "d", "step": "b_first"})
|
|
677
|
+
.sort("block", "d", "b_first")
|
|
678
|
+
)
|
|
679
|
+
for (_blk, dval), grp in bsd_sorted.group_by(
|
|
680
|
+
["block", "d"], maintain_order=True
|
|
681
|
+
):
|
|
682
|
+
bfs = grp["b_first"].to_list()
|
|
683
|
+
n = len(bfs)
|
|
684
|
+
for i in range(n):
|
|
685
|
+
succ_rows.append((dval, bfs[i], bfs[(i + 1) % n]))
|
|
686
|
+
if succ_rows:
|
|
687
|
+
new_pbs = pl.DataFrame(
|
|
688
|
+
succ_rows,
|
|
689
|
+
schema=["d", "b_first", "b_next"],
|
|
690
|
+
orient="row",
|
|
691
|
+
).with_columns(
|
|
692
|
+
alias_to_axis("d", "d"),
|
|
693
|
+
alias_to_axis("b_first", "b_first"),
|
|
694
|
+
alias_to_axis("b_next", "b_next"),
|
|
695
|
+
)
|
|
696
|
+
else:
|
|
697
|
+
new_pbs = pl.DataFrame(schema={
|
|
698
|
+
"d": schema_dtype(_enums, "d"),
|
|
699
|
+
"b_first": schema_dtype(_enums, "b_first"),
|
|
700
|
+
"b_next": schema_dtype(_enums, "b_next"),
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
# period_block_time: (d, b_first, t) — overlap_set rows where
|
|
704
|
+
# b_coarse=coarse, b_fine=default.
|
|
705
|
+
ov = bundle.layout.overlap_set_frame
|
|
706
|
+
if ov.height == 0:
|
|
707
|
+
return None
|
|
708
|
+
ov_renamed = ov.pipe(rename_to_axis, {
|
|
709
|
+
"period": "d",
|
|
710
|
+
"block_coarse": "bk",
|
|
711
|
+
"step_coarse": "b_first",
|
|
712
|
+
"block_fine": "b_fine",
|
|
713
|
+
"step_fine": "t",
|
|
714
|
+
})
|
|
715
|
+
ov_keep = ov_renamed.filter(
|
|
716
|
+
pl.col("bk").is_in(coarse_use)
|
|
717
|
+
& (pl.col("b_fine") == DEFAULT_BLOCK)
|
|
718
|
+
)
|
|
719
|
+
if ov_keep.height == 0:
|
|
720
|
+
new_pbt = pl.DataFrame(schema={
|
|
721
|
+
"d": schema_dtype(_enums, "d"),
|
|
722
|
+
"b_first": schema_dtype(_enums, "b_first"),
|
|
723
|
+
"t": schema_dtype(_enums, "t"),
|
|
724
|
+
})
|
|
725
|
+
else:
|
|
726
|
+
new_pbt = ov_keep.select("d", "b_first", "t").unique()
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
"period_block": new_pb.lazy(),
|
|
730
|
+
"period_block_succ": new_pbs.lazy(),
|
|
731
|
+
"period_block_time": new_pbt.lazy(),
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# ---------------------------------------------------------------------------
|
|
736
|
+
# §3.9.4 — arc_sink_block_dt / arc_source_block_dt + weights
|
|
737
|
+
# ---------------------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@dataclass
|
|
741
|
+
class ArcBlockFrames:
|
|
742
|
+
"""Container for the cluster E arc-block aggregation frames."""
|
|
743
|
+
|
|
744
|
+
arc_sink_block_dt: pl.DataFrame | None = None
|
|
745
|
+
arc_source_block_dt: pl.DataFrame | None = None
|
|
746
|
+
p_arc_sink_weight: Param | None = None
|
|
747
|
+
p_arc_source_weight: Param | None = None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def arc_block_dt(
|
|
751
|
+
pss: pl.DataFrame | None,
|
|
752
|
+
nodeStateBlock: pl.DataFrame | None,
|
|
753
|
+
period_block_time: pl.DataFrame | None,
|
|
754
|
+
bundle: BlockBundle | None,
|
|
755
|
+
) -> ArcBlockFrames:
|
|
756
|
+
"""Build per-arc daily-block aggregation frames.
|
|
757
|
+
|
|
758
|
+
For each arc ``(p, source, sink)`` whose nodeStateBlock side participates
|
|
759
|
+
in ``nodeStateBlock``, project to ``(p, source, sink, d, b_first, t,
|
|
760
|
+
weight)`` with ``weight = block_step_duration`` of the arc-side block
|
|
761
|
+
at fine ``(d, t)``.
|
|
762
|
+
|
|
763
|
+
Returns an :class:`ArcBlockFrames` with up to four populated fields;
|
|
764
|
+
any may be ``None`` when the corresponding side has no rows.
|
|
765
|
+
"""
|
|
766
|
+
out = ArcBlockFrames()
|
|
767
|
+
if (bundle is None
|
|
768
|
+
or not bundle.has_block_data()
|
|
769
|
+
or pss is None or pss.height == 0
|
|
770
|
+
or nodeStateBlock is None or nodeStateBlock.height == 0
|
|
771
|
+
or period_block_time is None or period_block_time.height == 0):
|
|
772
|
+
return out
|
|
773
|
+
|
|
774
|
+
psb_f = bundle.layout.process_side_block_frame
|
|
775
|
+
bsd_f = bundle.layout.block_step_duration_frame
|
|
776
|
+
if psb_f.height == 0 or bsd_f.height == 0:
|
|
777
|
+
return out
|
|
778
|
+
|
|
779
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame params.
|
|
780
|
+
# ``pss`` joins on ``["d", "t"]`` against pbt and on entity-axis cols
|
|
781
|
+
# via ``is_in`` below; ``nodeStateBlock`` provides the n→entity set.
|
|
782
|
+
_enums = get_global_axis_enums()
|
|
783
|
+
if _enums is not None:
|
|
784
|
+
pss = cast_frame_axes(pss, _enums)
|
|
785
|
+
nodeStateBlock = cast_frame_axes(nodeStateBlock, _enums)
|
|
786
|
+
period_block_time = cast_frame_axes(period_block_time, _enums)
|
|
787
|
+
|
|
788
|
+
psb = bundle.process_side_block_lf
|
|
789
|
+
bsd_arc = bundle.block_step_duration_arc_lf
|
|
790
|
+
# Phase 4.8f: ``is_in`` against ``pss['sink']`` / ``pss['source']``
|
|
791
|
+
# whose canonical axis is the ``entity`` union Enum vocabulary —
|
|
792
|
+
# cannot pass a node-Enum Series. Use a 1-column frame + semi join.
|
|
793
|
+
nsb_set_e_df = (nodeStateBlock.select(pl.col("n").alias("e")).unique())
|
|
794
|
+
if _enums is not None:
|
|
795
|
+
nsb_set_e_df = cast_frame_axes(nsb_set_e_df, _enums)
|
|
796
|
+
pbt = period_block_time
|
|
797
|
+
|
|
798
|
+
psb_sink = psb.filter(pl.col("side") == "sink").select("p", "b_f")
|
|
799
|
+
sink_arcs = (
|
|
800
|
+
pss.lazy()
|
|
801
|
+
.join(nsb_set_e_df.lazy().rename({"e": "sink"}), on="sink", how="semi")
|
|
802
|
+
.join(psb_sink, on="p", how="inner")
|
|
803
|
+
)
|
|
804
|
+
sink_ab = (
|
|
805
|
+
sink_arcs
|
|
806
|
+
.join(bsd_arc, on="b_f", how="inner")
|
|
807
|
+
.join(pbt.lazy(), on=["d", "t"], how="inner")
|
|
808
|
+
.select("p", "source", "sink", "d", "b_first", "t", "weight")
|
|
809
|
+
.unique()
|
|
810
|
+
.collect()
|
|
811
|
+
)
|
|
812
|
+
if sink_ab.height > 0:
|
|
813
|
+
out.arc_sink_block_dt = sink_ab
|
|
814
|
+
wf = (
|
|
815
|
+
sink_ab.select("p", "source", "sink", "d", "t", "weight")
|
|
816
|
+
.unique()
|
|
817
|
+
.rename({"weight": "value"})
|
|
818
|
+
)
|
|
819
|
+
out.p_arc_sink_weight = Param(
|
|
820
|
+
("p", "source", "sink", "d", "t"), wf,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
psb_src = psb.filter(pl.col("side") == "source").select("p", "b_f")
|
|
824
|
+
src_arcs = (
|
|
825
|
+
pss.lazy()
|
|
826
|
+
.join(nsb_set_e_df.lazy().rename({"e": "source"}), on="source", how="semi")
|
|
827
|
+
.join(psb_src, on="p", how="inner")
|
|
828
|
+
)
|
|
829
|
+
src_ab = (
|
|
830
|
+
src_arcs
|
|
831
|
+
.join(bsd_arc, on="b_f", how="inner")
|
|
832
|
+
.join(pbt.lazy(), on=["d", "t"], how="inner")
|
|
833
|
+
.select("p", "source", "sink", "d", "b_first", "t", "weight")
|
|
834
|
+
.unique()
|
|
835
|
+
.collect()
|
|
836
|
+
)
|
|
837
|
+
if src_ab.height > 0:
|
|
838
|
+
out.arc_source_block_dt = src_ab
|
|
839
|
+
wf = (
|
|
840
|
+
src_ab.select("p", "source", "sink", "d", "t", "weight")
|
|
841
|
+
.unique()
|
|
842
|
+
.rename({"weight": "value"})
|
|
843
|
+
)
|
|
844
|
+
out.p_arc_source_weight = Param(
|
|
845
|
+
("p", "source", "sink", "d", "t"), wf,
|
|
846
|
+
)
|
|
847
|
+
return out
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# ---------------------------------------------------------------------------
|
|
851
|
+
# §3.9 — nodeState_last_dt
|
|
852
|
+
# ---------------------------------------------------------------------------
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def nodeState_last_dt_lf(
|
|
856
|
+
nodeState: pl.DataFrame | None,
|
|
857
|
+
bundle: BlockBundle | None,
|
|
858
|
+
) -> pl.LazyFrame:
|
|
859
|
+
"""Build ``nodeState_last_dt`` ``(n, d, t)``.
|
|
860
|
+
|
|
861
|
+
Last-fine-step-of-last-block per node. Built from
|
|
862
|
+
``block_period_time_last`` × ``entity_block`` × ``nodeState``.
|
|
863
|
+
Mirror of ``input.py:2233-2253``.
|
|
864
|
+
"""
|
|
865
|
+
empty = pl.LazyFrame(schema={
|
|
866
|
+
"n": schema_dtype(_enums, "n"),
|
|
867
|
+
"d": schema_dtype(_enums, "d"),
|
|
868
|
+
"t": schema_dtype(_enums, "t"),
|
|
869
|
+
})
|
|
870
|
+
if nodeState is None or nodeState.height == 0:
|
|
871
|
+
return empty
|
|
872
|
+
if bundle is None:
|
|
873
|
+
return empty
|
|
874
|
+
bptl_f = bundle.layout.block_period_time_last_frame
|
|
875
|
+
eb_f = bundle.layout.entity_block_frame
|
|
876
|
+
if bptl_f.height == 0 or eb_f.height == 0:
|
|
877
|
+
return empty
|
|
878
|
+
# Phase 4.8f: defend axis-aware join keys on incoming frame param.
|
|
879
|
+
_live = get_global_axis_enums()
|
|
880
|
+
if _live is not None:
|
|
881
|
+
nodeState = cast_frame_axes(nodeState, _live)
|
|
882
|
+
return (
|
|
883
|
+
nodeState.lazy().select("n")
|
|
884
|
+
.join(bundle.entity_block_lf, on="n", how="inner")
|
|
885
|
+
.join(bundle.block_period_time_last_lf, on="bk", how="inner")
|
|
886
|
+
.select("n", "d", "t")
|
|
887
|
+
.unique()
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
# ---------------------------------------------------------------------------
|
|
892
|
+
# §3.9 — dtttdt_block_interior
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def dtttdt_block_interior_lf(
|
|
897
|
+
dtttdt: pl.DataFrame | None,
|
|
898
|
+
period_block_time: pl.DataFrame | None,
|
|
899
|
+
) -> pl.LazyFrame:
|
|
900
|
+
"""Interior-of-block dtttdt rows.
|
|
901
|
+
|
|
902
|
+
Two paths matching ``input.py``'s branching:
|
|
903
|
+
|
|
904
|
+
1. **Default** (timeset-block decomposition): keep dtttdt rows where
|
|
905
|
+
``t_previous_within_timeset == t_previous`` (jump=1 interior).
|
|
906
|
+
2. **Synthesised (multi-resolution)**: when *period_block_time* has
|
|
907
|
+
multiple ``b_first`` per period, rebuild interior pairs from the
|
|
908
|
+
coarse block decomposition: per (d, b_first), consecutive sorted
|
|
909
|
+
fine t's give intra-day predecessor pairs.
|
|
910
|
+
|
|
911
|
+
The caller distinguishes via *period_block_time*'s shape; this
|
|
912
|
+
helper detects automatically.
|
|
913
|
+
"""
|
|
914
|
+
empty = pl.LazyFrame(schema={
|
|
915
|
+
"d": schema_dtype(_enums, "d"),
|
|
916
|
+
"t": schema_dtype(_enums, "t"),
|
|
917
|
+
"t_previous": schema_dtype(_enums, "t_previous"),
|
|
918
|
+
})
|
|
919
|
+
if dtttdt is None or dtttdt.height == 0:
|
|
920
|
+
return empty
|
|
921
|
+
multi_res = False
|
|
922
|
+
if period_block_time is not None and period_block_time.height > 0:
|
|
923
|
+
nb = (
|
|
924
|
+
period_block_time
|
|
925
|
+
.group_by("d")
|
|
926
|
+
.agg(pl.col("b_first").n_unique().alias("nb"))
|
|
927
|
+
["nb"].max()
|
|
928
|
+
)
|
|
929
|
+
if nb is not None and nb > 1:
|
|
930
|
+
multi_res = True
|
|
931
|
+
if multi_res and period_block_time is not None:
|
|
932
|
+
rows: list[tuple[str, str, str]] = []
|
|
933
|
+
pbt_sorted = period_block_time.sort("d", "b_first", "t")
|
|
934
|
+
for (dval, _bf), grp in pbt_sorted.group_by(
|
|
935
|
+
["d", "b_first"], maintain_order=True
|
|
936
|
+
):
|
|
937
|
+
ts = grp["t"].to_list()
|
|
938
|
+
for i in range(1, len(ts)):
|
|
939
|
+
rows.append((dval, ts[i], ts[i - 1]))
|
|
940
|
+
if not rows:
|
|
941
|
+
return empty
|
|
942
|
+
return (
|
|
943
|
+
pl.DataFrame(
|
|
944
|
+
rows,
|
|
945
|
+
schema=["d", "t", "t_previous"],
|
|
946
|
+
orient="row",
|
|
947
|
+
)
|
|
948
|
+
.with_columns(
|
|
949
|
+
alias_to_axis("d", "d"),
|
|
950
|
+
alias_to_axis("t", "t"),
|
|
951
|
+
alias_to_axis("t_previous", "t_previous"),
|
|
952
|
+
)
|
|
953
|
+
.unique()
|
|
954
|
+
.lazy()
|
|
955
|
+
)
|
|
956
|
+
if "t_previous_within_timeset" not in dtttdt.columns:
|
|
957
|
+
return empty
|
|
958
|
+
# Defensive re-cast: align d / t / t_previous to canonical Enum so
|
|
959
|
+
# the returned schema matches the empty-fallback and multi_res
|
|
960
|
+
# branches above even when ``dtttdt`` arrives with Utf8 axis columns.
|
|
961
|
+
return (
|
|
962
|
+
dtttdt.lazy()
|
|
963
|
+
.filter(pl.col("t_previous_within_timeset")
|
|
964
|
+
== pl.col("t_previous"))
|
|
965
|
+
.select(alias_to_axis("d", "d"),
|
|
966
|
+
alias_to_axis("t", "t"),
|
|
967
|
+
alias_to_axis("t_previous", "t_previous"))
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
# ---------------------------------------------------------------------------
|
|
972
|
+
# Public — apply_block_cluster: single-pass entry for apply_derived_e.
|
|
973
|
+
# ---------------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
__all__ = [
|
|
977
|
+
"BlockBundle",
|
|
978
|
+
"load_block_bundle",
|
|
979
|
+
"filter_flow_n_by_block",
|
|
980
|
+
"flow_to_n_block_filtered",
|
|
981
|
+
"flow_from_n_block_filtered",
|
|
982
|
+
"flow_from_nodeBalance_block_filtered",
|
|
983
|
+
"flow_from_nodeBalance_seed",
|
|
984
|
+
"nodeStateBlock_lf",
|
|
985
|
+
"period_block_multi_resolution_lf",
|
|
986
|
+
"arc_block_dt",
|
|
987
|
+
"ArcBlockFrames",
|
|
988
|
+
"nodeState_last_dt_lf",
|
|
989
|
+
"dtttdt_block_interior_lf",
|
|
990
|
+
]
|