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,881 @@
|
|
|
1
|
+
"""Cluster C — profile cascade (Δ.7).
|
|
2
|
+
|
|
3
|
+
Lazy-polars port of flextool's
|
|
4
|
+
``preprocessing/entity_period_calc_params.py::write_pdtProfile``
|
|
5
|
+
algorithm — the 5-branch fallback that resolves
|
|
6
|
+
``profile.profile`` parameter values to per-(profile, d, t) rows.
|
|
7
|
+
|
|
8
|
+
Cluster C field (per
|
|
9
|
+
``audit/native_data_path_design_derived_clusters.md``):
|
|
10
|
+
|
|
11
|
+
* ``p_profile_value`` — per-(f, d, t) profile value, resolved across a
|
|
12
|
+
multi-tier cascade.
|
|
13
|
+
|
|
14
|
+
Algorithm (mirror of ``write_pdtProfile`` at
|
|
15
|
+
``entity_period_calc_params.py:767-947``)
|
|
16
|
+
-----------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
For each (profile p, dispatch period d, timestep t) ∈ profile × dt the
|
|
19
|
+
output value is picked by the first matching branch:
|
|
20
|
+
|
|
21
|
+
1. **Stochastic UNION fold** — when ``p`` is referenced by a
|
|
22
|
+
stochastic relationship class (``unit__node__profile``,
|
|
23
|
+
``connection__profile``, ``node__profile`` whose entity is in a
|
|
24
|
+
``include_stochastics`` group), sum
|
|
25
|
+
``Σ_{tb,ts} pbt_profile[p, tb, ts, t]`` over
|
|
26
|
+
``(tb, ts) ∈ tb_for_d[d] × ts_for_d[d]``. Skip if no hit.
|
|
27
|
+
|
|
28
|
+
2. **Parent-period fold** — for each parent ``pe`` of ``d``
|
|
29
|
+
(``period__branch[d_anchor=pe, branch=d]``), sum
|
|
30
|
+
``Σ_{tb,ts} pbt_profile[p, tb, ts, t]`` over
|
|
31
|
+
``(tb, ts) ∈ tb_for_d[pe] × ts_for_d[d]``. Skip if no hit.
|
|
32
|
+
|
|
33
|
+
3. **Time series** — ``pt_profile[(p, t)]`` (``profile.profile`` of
|
|
34
|
+
runtime type ``Map(time → float)`` or ``time_series``).
|
|
35
|
+
|
|
36
|
+
4. **Scalar** — ``p_profile[p]`` (``profile.profile`` of runtime type
|
|
37
|
+
``float`` / ``str`` / ``bool``).
|
|
38
|
+
|
|
39
|
+
5. **Zero** — fallback.
|
|
40
|
+
|
|
41
|
+
Branches 1 and 2 require the per-solve scaffolding ``period__branch``,
|
|
42
|
+
``solve_branch__time_branch``, ``first_timesteps``, and
|
|
43
|
+
``groupIncludeStochastics`` / membership tables. Δ.7 keeps these in the
|
|
44
|
+
workdir-CSV layer for the stochastic path; the chain-runner inputs are
|
|
45
|
+
preserved. The deterministic path (branches 3-5) is fully native.
|
|
46
|
+
|
|
47
|
+
Architectural notes
|
|
48
|
+
-------------------
|
|
49
|
+
|
|
50
|
+
* **Drop the ``_check_canonical_keys`` predicate.** The Δ.6 close
|
|
51
|
+
stanza identified ``_derived_params.py:p_profile_value_from_source``
|
|
52
|
+
as guarding against non-canonical Map keys (``x``/``i`` from
|
|
53
|
+
zero-``index_name`` Spine values) by returning ``None`` and falling
|
|
54
|
+
back to the CSV path. Cluster C drops the predicate: the stochastic
|
|
55
|
+
3d_map case is now handled explicitly via the per-solve CSV, the
|
|
56
|
+
deterministic cases are handled via per-row dispatch on the raw
|
|
57
|
+
parameter value's runtime type rather than column-name sniffing.
|
|
58
|
+
* **Lazy polars throughout.** Every helper returns a
|
|
59
|
+
:class:`pl.LazyFrame`; the public ``apply_profile_cascade`` boundary
|
|
60
|
+
collects once.
|
|
61
|
+
* **Per-solve workdir CSVs for tiers 1 and 2.** Mirroring Cluster B's
|
|
62
|
+
``edd_history`` / ``ppec_handoff`` boundary (Δ.6), the stochastic
|
|
63
|
+
scaffolding lives in ``solve_data/*.csv`` until the chain runner
|
|
64
|
+
surfaces it in-memory (Δ.8+).
|
|
65
|
+
"""
|
|
66
|
+
from __future__ import annotations
|
|
67
|
+
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
from typing import TYPE_CHECKING
|
|
70
|
+
|
|
71
|
+
import polars as pl
|
|
72
|
+
|
|
73
|
+
from polar_high import Param
|
|
74
|
+
|
|
75
|
+
from ._emit_provider_io import _provider_key
|
|
76
|
+
from ._axis_enums import (
|
|
77
|
+
alias_to_axis,
|
|
78
|
+
schema_dtype,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Substrate handle for the cascade-wide axis enum vocabulary.
|
|
83
|
+
# Bare ``None`` here; ``cast_dim`` / ``schema_dtype`` in
|
|
84
|
+
# ``_axis_enums`` fall back to ``_LIVE_AXIS_ENUMS_CTX`` (the live
|
|
85
|
+
# ContextVar) when this is ``None``, so substrate sites pick up
|
|
86
|
+
# activation set by ``load_flextool`` automatically.
|
|
87
|
+
_enums: "dict | None" = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cell_str(value: "object | None") -> str:
|
|
91
|
+
"""Reproduce a ``csv.reader`` cell string for a native frame value.
|
|
92
|
+
|
|
93
|
+
``DataFrame.write_csv`` renders ``null`` as the empty string and every
|
|
94
|
+
other scalar as its textual form; ``csv.reader`` then reads those
|
|
95
|
+
strings back. Mirror that here so dict keys/values stay byte-identical
|
|
96
|
+
to the legacy CSV round-trip.
|
|
97
|
+
"""
|
|
98
|
+
return "" if value is None else str(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _provider_has_key(provider, path: "Path") -> bool:
|
|
102
|
+
"""Provider-only existence check used by the profile cascade.
|
|
103
|
+
Returns ``False`` when *provider* is ``None`` or the key is
|
|
104
|
+
missing. The disk-fallback arm was removed in Step 2.5.
|
|
105
|
+
"""
|
|
106
|
+
if provider is None:
|
|
107
|
+
return False
|
|
108
|
+
return provider.has(_provider_key(path))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _provider_read(provider, path: "Path") -> "pl.DataFrame":
|
|
112
|
+
"""Provider-only read used by the profile cascade. Guard with
|
|
113
|
+
:func:`_provider_has_key` first; calling without a provider
|
|
114
|
+
raises :class:`ValueError`.
|
|
115
|
+
"""
|
|
116
|
+
if provider is None:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"_provider_read requires a FlexDataProvider; guard with "
|
|
119
|
+
"_provider_has_key first."
|
|
120
|
+
)
|
|
121
|
+
return provider.get(_provider_key(path))
|
|
122
|
+
|
|
123
|
+
if TYPE_CHECKING:
|
|
124
|
+
from flextool.engine_polars._input_source import InputSource
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Per-entity profile classification: scalar / period / time / branch+t
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _classify_profile_rows(source: "InputSource") -> dict[str, str]:
|
|
133
|
+
"""Return ``{profile_name: tier}`` where tier ∈ {``scalar``,
|
|
134
|
+
``period``, ``time``, ``stochastic``}.
|
|
135
|
+
|
|
136
|
+
Reads ``profile.profile`` raw values from the source and
|
|
137
|
+
dispatches on Python type:
|
|
138
|
+
|
|
139
|
+
* ``float`` / ``str`` / ``bool`` → tier ``scalar`` (``p_profile.csv``).
|
|
140
|
+
* Map with ``index_name=='time'`` → tier ``time`` (``pt_profile.csv``).
|
|
141
|
+
* Map with ``index_name=='period'`` → tier ``period``
|
|
142
|
+
(``pd_profile.csv`` — note: not consumed by ``write_pdtProfile``).
|
|
143
|
+
* 3-deep Map → tier ``stochastic`` (``pbt_profile.csv``).
|
|
144
|
+
* Other Map shapes (deterministic 1d_map, 2d_map(period, t)) →
|
|
145
|
+
tier ``period`` or ``time`` per outermost ``index_name``.
|
|
146
|
+
|
|
147
|
+
Profiles absent from the parameter table are not in the returned
|
|
148
|
+
dict. The caller treats them as tier 5 (zero fallback).
|
|
149
|
+
|
|
150
|
+
Implementation strategy: peek at the SpineDbReader's per-row raw
|
|
151
|
+
value via ``_param_rows`` when available; fall back to column-name
|
|
152
|
+
heuristics on the CSV-shaped frame for sources that don't expose
|
|
153
|
+
raw parameter values (``InMemoryReader``).
|
|
154
|
+
"""
|
|
155
|
+
# Try to access the typed raw values via SpineDbReader's internals.
|
|
156
|
+
rows_by_name: dict[str, object] = {}
|
|
157
|
+
if hasattr(source, "_class_name_to_id") and hasattr(source, "_param_rows"):
|
|
158
|
+
cls_id = source._class_name_to_id.get("profile") # type: ignore[attr-defined]
|
|
159
|
+
if cls_id is not None:
|
|
160
|
+
pdef = source._pdef_by_class_name.get((cls_id, "profile")) # type: ignore[attr-defined]
|
|
161
|
+
if pdef is not None:
|
|
162
|
+
raw_rows = source._param_rows.get((cls_id, pdef["id"]), []) # type: ignore[attr-defined]
|
|
163
|
+
for eid, v in raw_rows:
|
|
164
|
+
name = source._entity_by_id[eid][1] # type: ignore[attr-defined]
|
|
165
|
+
rows_by_name[name] = v
|
|
166
|
+
if rows_by_name:
|
|
167
|
+
return {n: _tier_for_value(v) for n, v in rows_by_name.items()}
|
|
168
|
+
# Column-shape fallback for InMemoryReader (used in unit tests).
|
|
169
|
+
try:
|
|
170
|
+
df = source.parameter("profile", "profile")
|
|
171
|
+
except KeyError:
|
|
172
|
+
return {}
|
|
173
|
+
if df.height == 0:
|
|
174
|
+
return {}
|
|
175
|
+
cols = set(df.columns)
|
|
176
|
+
out: dict[str, str] = {}
|
|
177
|
+
for name in df["name"].unique().to_list():
|
|
178
|
+
if cols == {"name", "value"}:
|
|
179
|
+
out[name] = "scalar"
|
|
180
|
+
elif cols == {"name", "period", "value"}:
|
|
181
|
+
out[name] = "period"
|
|
182
|
+
elif cols == {"name", "t", "value"}:
|
|
183
|
+
out[name] = "time"
|
|
184
|
+
elif cols == {"name", "period", "t", "value"}:
|
|
185
|
+
# 2d_map(period, t) — fold as time-axis per period. Treat
|
|
186
|
+
# as a multi-row variant of "time" here; the resolver knows.
|
|
187
|
+
out[name] = "period_time"
|
|
188
|
+
else:
|
|
189
|
+
# Generic / stochastic 3d_map — defer to workdir-CSV path.
|
|
190
|
+
out[name] = "stochastic"
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _tier_for_value(v: object) -> str:
|
|
195
|
+
"""Classify a single raw Spine parameter value.
|
|
196
|
+
|
|
197
|
+
Mirror of the input-writer dispatch logic.
|
|
198
|
+
"""
|
|
199
|
+
from spinedb_api.parameter_value import (
|
|
200
|
+
Map, TimeSeries, Array,
|
|
201
|
+
)
|
|
202
|
+
if not isinstance(v, (Map, TimeSeries, Array)):
|
|
203
|
+
return "scalar"
|
|
204
|
+
if isinstance(v, TimeSeries):
|
|
205
|
+
return "time"
|
|
206
|
+
if isinstance(v, Map):
|
|
207
|
+
# Walk to find depth + outer index name.
|
|
208
|
+
depth = 0
|
|
209
|
+
cur: object = v
|
|
210
|
+
while isinstance(cur, Map):
|
|
211
|
+
depth += 1
|
|
212
|
+
if not cur.values:
|
|
213
|
+
break
|
|
214
|
+
cur = cur.values[0]
|
|
215
|
+
outer_idx = (v.index_name or "").lower()
|
|
216
|
+
if depth == 1:
|
|
217
|
+
if outer_idx == "period":
|
|
218
|
+
return "period"
|
|
219
|
+
if outer_idx == "time":
|
|
220
|
+
return "time"
|
|
221
|
+
# Empty or generic index — assume time-axis (the typical
|
|
222
|
+
# flextool 1d_map default).
|
|
223
|
+
return "time"
|
|
224
|
+
if depth == 2:
|
|
225
|
+
if outer_idx == "period":
|
|
226
|
+
return "period_time"
|
|
227
|
+
# 2d_map without "period" as the outer key — treat as
|
|
228
|
+
# stochastic (defer to CSV) for safety.
|
|
229
|
+
return "stochastic"
|
|
230
|
+
# Deeper than 2 → 3d_map (branch × time_start × t).
|
|
231
|
+
return "stochastic"
|
|
232
|
+
# Array — uncommon for profiles; treat as time-axis.
|
|
233
|
+
return "time"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Tier-3 / tier-4 / tier-5 builders (deterministic)
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _profile_time_lf(source: "InputSource",
|
|
242
|
+
dt_lf: pl.LazyFrame,
|
|
243
|
+
time_profiles: list[str],
|
|
244
|
+
workdir: Path | None = None,
|
|
245
|
+
*,
|
|
246
|
+
provider: "object | None" = None,
|
|
247
|
+
) -> pl.LazyFrame:
|
|
248
|
+
"""Lazy ``[f, d, t, value]`` for time-axis profiles.
|
|
249
|
+
|
|
250
|
+
Tier 3 of the cascade: profile values keyed by ``t`` only,
|
|
251
|
+
broadcast over the dispatch periods of dt.
|
|
252
|
+
|
|
253
|
+
Prefers ``workdir/solve_data/pt_profile.csv`` (per-solve averaged
|
|
254
|
+
values written by ``TimelineConfig.create_averaged_timeseries``)
|
|
255
|
+
when present — those values are aggregated to the solve's
|
|
256
|
+
``new_stepduration``, matching the dispatch timeline. Falls back
|
|
257
|
+
to the raw Spine ``profile.profile`` parameter when the CSV is
|
|
258
|
+
absent (e.g. tests that bypass the runner).
|
|
259
|
+
|
|
260
|
+
The Spine fallback path: the source column may be ``t`` (when
|
|
261
|
+
``index_name='time'``) or ``i`` (default index name). Either way
|
|
262
|
+
the column maps 1:1 to t. Mirrors how ``pdtNodeInflow`` already
|
|
263
|
+
sources averaged values from ``solve_data/pt_node_inflow.csv``.
|
|
264
|
+
"""
|
|
265
|
+
empty_schema = {
|
|
266
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
267
|
+
}
|
|
268
|
+
if not time_profiles:
|
|
269
|
+
return pl.LazyFrame(schema=empty_schema)
|
|
270
|
+
|
|
271
|
+
# Prefer per-solve averaged CSV when the runner has produced it.
|
|
272
|
+
if workdir is not None:
|
|
273
|
+
csv_path = Path(workdir) / "solve_data" / "pt_profile.csv"
|
|
274
|
+
if _provider_has_key(provider, csv_path):
|
|
275
|
+
try:
|
|
276
|
+
csv_df = _provider_read(provider, csv_path)
|
|
277
|
+
csv_lf = (csv_df.lazy()
|
|
278
|
+
.filter(pl.col("profile").is_in(time_profiles))
|
|
279
|
+
.select(
|
|
280
|
+
alias_to_axis(
|
|
281
|
+
pl.col("profile").cast(pl.Utf8), "f"),
|
|
282
|
+
alias_to_axis(pl.col("time").cast(pl.Utf8), "t"),
|
|
283
|
+
pl.col("pt_profile")
|
|
284
|
+
.cast(pl.Float64, strict=False)
|
|
285
|
+
.alias("value"),
|
|
286
|
+
))
|
|
287
|
+
return (csv_lf
|
|
288
|
+
.join(dt_lf, on="t", how="inner")
|
|
289
|
+
.select("f", "d", "t", "value"))
|
|
290
|
+
except Exception:
|
|
291
|
+
# Malformed CSV — fall through to Spine source.
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
raw = source.parameter("profile", "profile")
|
|
296
|
+
except KeyError:
|
|
297
|
+
return pl.LazyFrame(schema=empty_schema)
|
|
298
|
+
if raw.height == 0:
|
|
299
|
+
return pl.LazyFrame(schema=empty_schema)
|
|
300
|
+
cols = raw.columns
|
|
301
|
+
# Find the time-axis column. Prefer 't' (canonical) over 'i'
|
|
302
|
+
# (generic Map fallback) when both are present.
|
|
303
|
+
if "t" in cols:
|
|
304
|
+
t_col = "t"
|
|
305
|
+
elif "i" in cols:
|
|
306
|
+
t_col = "i"
|
|
307
|
+
elif "time" in cols:
|
|
308
|
+
t_col = "time"
|
|
309
|
+
else:
|
|
310
|
+
return pl.LazyFrame(schema=empty_schema)
|
|
311
|
+
raw_lf = (raw.lazy()
|
|
312
|
+
.filter(pl.col("name").is_in(time_profiles))
|
|
313
|
+
.select(alias_to_axis("name", "f"),
|
|
314
|
+
alias_to_axis(pl.col(t_col).cast(pl.Utf8), "t"),
|
|
315
|
+
pl.col("value").cast(pl.Float64, strict=False)))
|
|
316
|
+
return (raw_lf
|
|
317
|
+
.join(dt_lf, on="t", how="inner")
|
|
318
|
+
.select("f", "d", "t", "value"))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _profile_period_time_lf(source: "InputSource",
|
|
322
|
+
dt_lf: pl.LazyFrame,
|
|
323
|
+
pt_profiles: list[str],
|
|
324
|
+
) -> pl.LazyFrame:
|
|
325
|
+
"""Lazy ``[f, d, t, value]`` for 2d_map(period, t) profiles.
|
|
326
|
+
|
|
327
|
+
The (d, t) keys are direct; only inner-join with dt_lf for
|
|
328
|
+
membership.
|
|
329
|
+
"""
|
|
330
|
+
if not pt_profiles:
|
|
331
|
+
return pl.LazyFrame(schema={
|
|
332
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
333
|
+
})
|
|
334
|
+
try:
|
|
335
|
+
raw = source.parameter("profile", "profile")
|
|
336
|
+
except KeyError:
|
|
337
|
+
return pl.LazyFrame(schema={
|
|
338
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
339
|
+
})
|
|
340
|
+
if raw.height == 0:
|
|
341
|
+
return pl.LazyFrame(schema={
|
|
342
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
343
|
+
})
|
|
344
|
+
cols = raw.columns
|
|
345
|
+
if "period" not in cols:
|
|
346
|
+
return pl.LazyFrame(schema={
|
|
347
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
348
|
+
})
|
|
349
|
+
if "t" in cols:
|
|
350
|
+
t_col = "t"
|
|
351
|
+
elif "i" in cols:
|
|
352
|
+
t_col = "i"
|
|
353
|
+
else:
|
|
354
|
+
return pl.LazyFrame(schema={
|
|
355
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
356
|
+
})
|
|
357
|
+
raw_lf = (raw.lazy()
|
|
358
|
+
.filter(pl.col("name").is_in(pt_profiles))
|
|
359
|
+
.select(alias_to_axis("name", "f"),
|
|
360
|
+
alias_to_axis(pl.col("period").cast(pl.Utf8), "d"),
|
|
361
|
+
alias_to_axis(pl.col(t_col).cast(pl.Utf8), "t"),
|
|
362
|
+
pl.col("value").cast(pl.Float64, strict=False)))
|
|
363
|
+
return raw_lf.join(dt_lf, on=["d", "t"], how="inner")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _profile_period_only_lf(source: "InputSource",
|
|
367
|
+
dt_lf: pl.LazyFrame,
|
|
368
|
+
period_profiles: list[str],
|
|
369
|
+
) -> pl.LazyFrame:
|
|
370
|
+
"""Lazy ``[f, d, t, value]`` for 1d_map(period) profiles.
|
|
371
|
+
|
|
372
|
+
Note: flextool's ``write_pdtProfile`` does **not** consult
|
|
373
|
+
``pd_profile.csv``; the only deterministic period-keyed branch is
|
|
374
|
+
via the parent-period fold of ``pbt_profile`` (tier 2). This
|
|
375
|
+
helper exists for completeness and is wired in only when the
|
|
376
|
+
fixture explicitly populates a 1d_map_period profile (rare /
|
|
377
|
+
historical). Mirror behaviour: broadcast across t of the matching
|
|
378
|
+
period.
|
|
379
|
+
"""
|
|
380
|
+
if not period_profiles:
|
|
381
|
+
return pl.LazyFrame(schema={
|
|
382
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
383
|
+
})
|
|
384
|
+
try:
|
|
385
|
+
raw = source.parameter("profile", "profile")
|
|
386
|
+
except KeyError:
|
|
387
|
+
return pl.LazyFrame(schema={
|
|
388
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
389
|
+
})
|
|
390
|
+
if raw.height == 0:
|
|
391
|
+
return pl.LazyFrame(schema={
|
|
392
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
393
|
+
})
|
|
394
|
+
cols = raw.columns
|
|
395
|
+
if "period" not in cols:
|
|
396
|
+
return pl.LazyFrame(schema={
|
|
397
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
398
|
+
})
|
|
399
|
+
raw_lf = (raw.lazy()
|
|
400
|
+
.filter(pl.col("name").is_in(period_profiles))
|
|
401
|
+
.select(alias_to_axis("name", "f"),
|
|
402
|
+
alias_to_axis(pl.col("period").cast(pl.Utf8), "d"),
|
|
403
|
+
pl.col("value").cast(pl.Float64, strict=False)))
|
|
404
|
+
return (raw_lf
|
|
405
|
+
.join(dt_lf, on="d", how="inner")
|
|
406
|
+
.select("f", "d", "t", "value"))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _profile_scalar_lf(source: "InputSource",
|
|
410
|
+
dt_lf: pl.LazyFrame,
|
|
411
|
+
scalar_profiles: list[str],
|
|
412
|
+
) -> pl.LazyFrame:
|
|
413
|
+
"""Lazy ``[f, d, t, value]`` for scalar profiles.
|
|
414
|
+
|
|
415
|
+
Tier 4 of the cascade: scalar value broadcast over the full dt grid.
|
|
416
|
+
"""
|
|
417
|
+
if not scalar_profiles:
|
|
418
|
+
return pl.LazyFrame(schema={
|
|
419
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
420
|
+
})
|
|
421
|
+
try:
|
|
422
|
+
raw = source.parameter("profile", "profile")
|
|
423
|
+
except KeyError:
|
|
424
|
+
return pl.LazyFrame(schema={
|
|
425
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
426
|
+
})
|
|
427
|
+
if raw.height == 0:
|
|
428
|
+
return pl.LazyFrame(schema={
|
|
429
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
430
|
+
})
|
|
431
|
+
raw_lf = (raw.lazy()
|
|
432
|
+
.filter(pl.col("name").is_in(scalar_profiles))
|
|
433
|
+
.select(alias_to_axis("name", "f"),
|
|
434
|
+
pl.col("value").cast(pl.Float64, strict=False))
|
|
435
|
+
.unique(subset=["f"]))
|
|
436
|
+
return (raw_lf
|
|
437
|
+
.join(dt_lf, how="cross")
|
|
438
|
+
.select("f", "d", "t", "value"))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
# Tier-1 / tier-2 builders (stochastic — workdir-CSV-fed)
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _profile_stochastic_lf(workdir: Path | None,
|
|
447
|
+
stoch_profiles: list[str],
|
|
448
|
+
*,
|
|
449
|
+
provider: "object | None" = None,
|
|
450
|
+
) -> pl.LazyFrame:
|
|
451
|
+
"""Lazy ``[f, d, t, value]`` for stochastic 3d_map profiles.
|
|
452
|
+
|
|
453
|
+
Branches 1 and 2 of flextool's ``write_pdtProfile`` cascade.
|
|
454
|
+
|
|
455
|
+
Per-solve scaffolding consumed (workdir/solve_data/):
|
|
456
|
+
|
|
457
|
+
* ``period__branch.csv`` — (anchor, branch) tuples; defines parents.
|
|
458
|
+
* ``solve_branch__time_branch.csv`` — (anchor, time_branch); the
|
|
459
|
+
stochastic-fold's outer-time-branch axis.
|
|
460
|
+
* ``first_timesteps.csv`` — (anchor, time_start); the
|
|
461
|
+
stochastic-fold's outer-time-start axis.
|
|
462
|
+
* ``input/pbt_profile.csv`` — (profile, branch, time_start, time,
|
|
463
|
+
value) — the 3d_map values themselves.
|
|
464
|
+
* ``input/groupIncludeStochastics.csv`` + relationship CSVs — gate
|
|
465
|
+
Branch 1 (per-profile is_stoch flag).
|
|
466
|
+
|
|
467
|
+
Output: lazy frame ``[f, d, t, value]`` covering only the
|
|
468
|
+
(profile, d, t) tuples that actually hit Branch 1 or Branch 2.
|
|
469
|
+
The caller layers tiers 3-5 over the gaps.
|
|
470
|
+
"""
|
|
471
|
+
if workdir is None or not stoch_profiles:
|
|
472
|
+
return pl.LazyFrame(schema={
|
|
473
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
474
|
+
})
|
|
475
|
+
workdir = Path(workdir)
|
|
476
|
+
inp = workdir / "input"
|
|
477
|
+
sd = workdir / "solve_data"
|
|
478
|
+
|
|
479
|
+
# Read pbt_profile.csv: (profile, branch, time_start, time, pbt_profile).
|
|
480
|
+
pbt_path = inp / "pbt_profile.csv"
|
|
481
|
+
if not _provider_has_key(provider, pbt_path):
|
|
482
|
+
return pl.LazyFrame(schema={
|
|
483
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
484
|
+
})
|
|
485
|
+
try:
|
|
486
|
+
pbt = _provider_read(provider, pbt_path)
|
|
487
|
+
except Exception:
|
|
488
|
+
return pl.LazyFrame(schema={
|
|
489
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
490
|
+
})
|
|
491
|
+
if pbt.height == 0:
|
|
492
|
+
return pl.LazyFrame(schema={
|
|
493
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
494
|
+
})
|
|
495
|
+
pbt = pbt.rename({"pbt_profile": "value"})
|
|
496
|
+
pbt = pbt.filter(pl.col("profile").is_in(stoch_profiles))
|
|
497
|
+
pbt_lf = pbt.lazy().select(
|
|
498
|
+
alias_to_axis(pl.col("profile").cast(pl.Utf8), "f"),
|
|
499
|
+
pl.col("branch").cast(pl.Utf8).alias("tb"),
|
|
500
|
+
pl.col("time_start").cast(pl.Utf8).alias("ts"),
|
|
501
|
+
alias_to_axis(pl.col("time").cast(pl.Utf8), "t"),
|
|
502
|
+
pl.col("value").cast(pl.Float64, strict=False),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Read per-solve scaffolding.
|
|
506
|
+
pe_for_d = _read_pairs(sd / "period__branch.csv", "branch", "period",
|
|
507
|
+
provider=provider)
|
|
508
|
+
tb_for_d = _read_pairs(sd / "solve_branch__time_branch.csv",
|
|
509
|
+
"period", "branch", provider=provider)
|
|
510
|
+
ts_for_d = _read_pairs(sd / "first_timesteps.csv", "period", "step",
|
|
511
|
+
provider=provider)
|
|
512
|
+
|
|
513
|
+
# Stochastic UNION gate per profile.
|
|
514
|
+
stoch_set = _read_stoch_profiles(inp, provider=provider)
|
|
515
|
+
stoch_active = [p for p in stoch_profiles if p in stoch_set]
|
|
516
|
+
|
|
517
|
+
# Branch 1: stochastic UNION fold.
|
|
518
|
+
# For each (p, d, t): sum pbt[p, tb, ts, t] over (tb, ts) ∈
|
|
519
|
+
# tb_for_d[d] × ts_for_d[d], iff p ∈ stoch_active.
|
|
520
|
+
parts: list[pl.LazyFrame] = []
|
|
521
|
+
if stoch_active and tb_for_d and ts_for_d:
|
|
522
|
+
# Build a join frame: (d, tb, ts) tuples.
|
|
523
|
+
d_tb_ts = []
|
|
524
|
+
for d, tbs in tb_for_d.items():
|
|
525
|
+
tss = ts_for_d.get(d, [])
|
|
526
|
+
for tb in tbs:
|
|
527
|
+
for ts in tss:
|
|
528
|
+
d_tb_ts.append((d, tb, ts))
|
|
529
|
+
if d_tb_ts:
|
|
530
|
+
d_tb_ts_lf = (pl.LazyFrame(
|
|
531
|
+
{"d": [r[0] for r in d_tb_ts],
|
|
532
|
+
"tb": [r[1] for r in d_tb_ts],
|
|
533
|
+
"ts": [r[2] for r in d_tb_ts]})
|
|
534
|
+
.with_columns(alias_to_axis("d", "d")))
|
|
535
|
+
stoch_lf = (pbt_lf
|
|
536
|
+
.filter(pl.col("f").is_in(stoch_active))
|
|
537
|
+
.join(d_tb_ts_lf, on=["tb", "ts"], how="inner")
|
|
538
|
+
.group_by(["f", "d", "t"])
|
|
539
|
+
.agg(pl.col("value").sum())
|
|
540
|
+
.with_columns(branch_priority=pl.lit(1, dtype=pl.Int8))
|
|
541
|
+
)
|
|
542
|
+
parts.append(stoch_lf)
|
|
543
|
+
|
|
544
|
+
# Branch 2: parent-period fold.
|
|
545
|
+
# For each (p, d, t): sum pbt[p, tb, ts, t] over (tb, ts) ∈
|
|
546
|
+
# tb_for_d[pe] × ts_for_d[d] for each pe ∈ pe_for_d[d].
|
|
547
|
+
if pe_for_d and tb_for_d and ts_for_d:
|
|
548
|
+
# Build (d, tb, ts) where tb is from each parent's tb_for_d.
|
|
549
|
+
d_tb_ts_pp = []
|
|
550
|
+
for d, parents in pe_for_d.items():
|
|
551
|
+
tss = ts_for_d.get(d, [])
|
|
552
|
+
for pe in parents:
|
|
553
|
+
tbs = tb_for_d.get(pe, [])
|
|
554
|
+
for tb in tbs:
|
|
555
|
+
for ts in tss:
|
|
556
|
+
d_tb_ts_pp.append((d, tb, ts))
|
|
557
|
+
if d_tb_ts_pp:
|
|
558
|
+
d_tb_ts_pp_lf = (pl.LazyFrame(
|
|
559
|
+
{"d": [r[0] for r in d_tb_ts_pp],
|
|
560
|
+
"tb": [r[1] for r in d_tb_ts_pp],
|
|
561
|
+
"ts": [r[2] for r in d_tb_ts_pp]})
|
|
562
|
+
.with_columns(alias_to_axis("d", "d")))
|
|
563
|
+
pp_lf = (pbt_lf
|
|
564
|
+
.join(d_tb_ts_pp_lf, on=["tb", "ts"], how="inner")
|
|
565
|
+
.group_by(["f", "d", "t"])
|
|
566
|
+
.agg(pl.col("value").sum())
|
|
567
|
+
.with_columns(branch_priority=pl.lit(2, dtype=pl.Int8))
|
|
568
|
+
)
|
|
569
|
+
parts.append(pp_lf)
|
|
570
|
+
|
|
571
|
+
if not parts:
|
|
572
|
+
return pl.LazyFrame(schema={
|
|
573
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
574
|
+
})
|
|
575
|
+
# Combine and resolve priority: lower priority wins.
|
|
576
|
+
union = pl.concat(parts, how="vertical")
|
|
577
|
+
return (union
|
|
578
|
+
.sort(["f", "d", "t", "branch_priority"])
|
|
579
|
+
.group_by(["f", "d", "t"])
|
|
580
|
+
.agg(pl.col("value").first())
|
|
581
|
+
.select("f", "d", "t", "value"))
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _read_pairs(path: Path, key_col: str, val_col: str,
|
|
585
|
+
*, provider: "object | None" = None,
|
|
586
|
+
) -> dict[str, list[str]]:
|
|
587
|
+
"""Read a 2-col CSV into ``{key: [val, …]}``. Empty / missing →
|
|
588
|
+
``{}``.
|
|
589
|
+
"""
|
|
590
|
+
if not _provider_has_key(provider, path):
|
|
591
|
+
return {}
|
|
592
|
+
out: dict[str, list[str]] = {}
|
|
593
|
+
try:
|
|
594
|
+
df = _provider_read(provider, path)
|
|
595
|
+
# The frame carries the header names as ``df.columns``; map by
|
|
596
|
+
# name exactly as the legacy ``header.index(...)`` lookup did.
|
|
597
|
+
if key_col not in df.columns or val_col not in df.columns:
|
|
598
|
+
return out
|
|
599
|
+
for k_cell, v_cell in df.select(key_col, val_col).iter_rows():
|
|
600
|
+
k = _cell_str(k_cell)
|
|
601
|
+
v = _cell_str(v_cell)
|
|
602
|
+
if k and v:
|
|
603
|
+
out.setdefault(k, []).append(v)
|
|
604
|
+
except Exception:
|
|
605
|
+
return {}
|
|
606
|
+
return out
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _read_stoch_profiles(inp: Path,
|
|
610
|
+
*,
|
|
611
|
+
provider: "object | None" = None) -> set[str]:
|
|
612
|
+
"""Return the set of profile names referenced by stochastic
|
|
613
|
+
relationships. Mirrors
|
|
614
|
+
``entity_period_calc_params.py:852-899``.
|
|
615
|
+
"""
|
|
616
|
+
groups_stoch: set[str] = set()
|
|
617
|
+
p = inp / "groupIncludeStochastics.csv"
|
|
618
|
+
if _provider_has_key(provider, p):
|
|
619
|
+
try:
|
|
620
|
+
df = _provider_read(provider, p)
|
|
621
|
+
col = df.columns[0] if df.columns else None
|
|
622
|
+
if col is not None and df.height > 0:
|
|
623
|
+
groups_stoch = set(df[col].cast(pl.Utf8).to_list())
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
if not groups_stoch:
|
|
627
|
+
return set()
|
|
628
|
+
|
|
629
|
+
stoch_processes: set[str] = set()
|
|
630
|
+
p = inp / "group__process.csv"
|
|
631
|
+
if _provider_has_key(provider, p):
|
|
632
|
+
try:
|
|
633
|
+
df = _provider_read(provider, p)
|
|
634
|
+
for row in df.iter_rows(named=True):
|
|
635
|
+
if row.get(df.columns[0]) in groups_stoch and row.get(df.columns[1]):
|
|
636
|
+
stoch_processes.add(row[df.columns[1]])
|
|
637
|
+
except Exception:
|
|
638
|
+
pass
|
|
639
|
+
stoch_nodes: set[str] = set()
|
|
640
|
+
p = inp / "group__node.csv"
|
|
641
|
+
if _provider_has_key(provider, p):
|
|
642
|
+
try:
|
|
643
|
+
df = _provider_read(provider, p)
|
|
644
|
+
for row in df.iter_rows(named=True):
|
|
645
|
+
if row.get(df.columns[0]) in groups_stoch and row.get(df.columns[1]):
|
|
646
|
+
stoch_nodes.add(row[df.columns[1]])
|
|
647
|
+
except Exception:
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
stoch_profile: set[str] = set()
|
|
651
|
+
p = inp / "process__profile__profile_method.csv"
|
|
652
|
+
if _provider_has_key(provider, p):
|
|
653
|
+
try:
|
|
654
|
+
df = _provider_read(provider, p)
|
|
655
|
+
for row in df.iter_rows(named=True):
|
|
656
|
+
process = row.get("process")
|
|
657
|
+
profile = row.get("profile")
|
|
658
|
+
if process in stoch_processes and profile:
|
|
659
|
+
stoch_profile.add(profile)
|
|
660
|
+
except Exception:
|
|
661
|
+
pass
|
|
662
|
+
p = inp / "node__profile__profile_method.csv"
|
|
663
|
+
if _provider_has_key(provider, p):
|
|
664
|
+
try:
|
|
665
|
+
df = _provider_read(provider, p)
|
|
666
|
+
for row in df.iter_rows(named=True):
|
|
667
|
+
node = row.get("node")
|
|
668
|
+
profile = row.get("profile")
|
|
669
|
+
if node in stoch_nodes and profile:
|
|
670
|
+
stoch_profile.add(profile)
|
|
671
|
+
except Exception:
|
|
672
|
+
pass
|
|
673
|
+
p = inp / "process__node__profile__profile_method.csv"
|
|
674
|
+
if _provider_has_key(provider, p):
|
|
675
|
+
try:
|
|
676
|
+
df = _provider_read(provider, p)
|
|
677
|
+
for row in df.iter_rows(named=True):
|
|
678
|
+
process = row.get("process")
|
|
679
|
+
profile = row.get("profile")
|
|
680
|
+
if process in stoch_processes and profile:
|
|
681
|
+
stoch_profile.add(profile)
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
return stoch_profile
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# ---------------------------------------------------------------------------
|
|
688
|
+
# Public entry: p_profile_value_lf + apply_profile_cascade
|
|
689
|
+
# ---------------------------------------------------------------------------
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def p_profile_value_lf(source: "InputSource",
|
|
693
|
+
dt: pl.DataFrame,
|
|
694
|
+
workdir: Path | None = None,
|
|
695
|
+
*,
|
|
696
|
+
provider: "object | None" = None,
|
|
697
|
+
) -> pl.LazyFrame:
|
|
698
|
+
"""Lazy ``[f, d, t, value]`` for the canonical ``p_profile_value``.
|
|
699
|
+
|
|
700
|
+
Entry point for the cluster C cascade. Composes the 5-branch
|
|
701
|
+
fallback from per-tier lazy frames:
|
|
702
|
+
|
|
703
|
+
1. Stochastic / parent-period folds (tiers 1, 2) — when ``workdir``
|
|
704
|
+
is provided and stochastic profiles are present.
|
|
705
|
+
2. Time-axis (tier 3) — for profiles with ``index_name='time'`` /
|
|
706
|
+
generic 1d_map values.
|
|
707
|
+
3. Period-time (2d_map) — for profiles with ``index_name='period'``
|
|
708
|
+
and a child Map / TimeSeries.
|
|
709
|
+
4. Period-only (1d_map(period)) — for profiles with
|
|
710
|
+
``index_name='period'`` and scalar children. Note: flextool's
|
|
711
|
+
``write_pdtProfile`` doesn't actually consult this tier — kept
|
|
712
|
+
for completeness when fixtures populate it.
|
|
713
|
+
5. Scalar (tier 4) — broadcast across (d, t).
|
|
714
|
+
6. Tier 5 (zero) — implicit; the LP-build site treats absence as 0.
|
|
715
|
+
|
|
716
|
+
The output schema is ``[f, d, t, value]``. Profiles absent from the
|
|
717
|
+
parameter table contribute no rows — the LP-build site's own gating
|
|
718
|
+
suppresses them where the schema requires.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
source
|
|
723
|
+
:class:`InputSource` for the spine data.
|
|
724
|
+
dt
|
|
725
|
+
Eager ``[d, t, ...]`` dispatch frame from
|
|
726
|
+
``flex_data.dt`` / ``dt_and_step_duration_from_source``.
|
|
727
|
+
workdir
|
|
728
|
+
Optional ``Path`` for the per-solve workdir. Required for
|
|
729
|
+
stochastic profile resolution (tiers 1-2) which read
|
|
730
|
+
per-solve scaffolding from ``solve_data/``. When omitted, the
|
|
731
|
+
stochastic tiers are skipped (tiers 3-5 still emit).
|
|
732
|
+
"""
|
|
733
|
+
if dt is None:
|
|
734
|
+
return pl.LazyFrame(schema={
|
|
735
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
736
|
+
})
|
|
737
|
+
# Defensive re-cast: callers occasionally pass dt with Utf8 d/t (e.g. stochastic
|
|
738
|
+
# fixtures where a CSV path bypasses upstream casting). Re-cast here so every
|
|
739
|
+
# tier helper composes on a consistent dtype.
|
|
740
|
+
dt_lf = (dt.lazy()
|
|
741
|
+
.select(alias_to_axis("d", "d"), alias_to_axis("t", "t"))
|
|
742
|
+
.unique())
|
|
743
|
+
|
|
744
|
+
# Provider-less workdir callers (e.g. cluster C parity tests) get an
|
|
745
|
+
# auto-built ephemeral Provider for the per-solve scaffolding the
|
|
746
|
+
# stochastic / averaged-time tiers consult. The cascade modules
|
|
747
|
+
# themselves remain disk-read-free (meta-test
|
|
748
|
+
# ``test_meta_provider_invariants.py``); the disk reads happen in
|
|
749
|
+
# ``_emit_provider_io.workdir_provider_for_paths`` which is on the
|
|
750
|
+
# PROVIDER_IMPL_ALLOWLIST.
|
|
751
|
+
if workdir is not None and provider is None:
|
|
752
|
+
from ._emit_provider_io import workdir_provider_for_paths
|
|
753
|
+
provider = workdir_provider_for_paths(workdir, [
|
|
754
|
+
"solve_data/period__branch.csv",
|
|
755
|
+
"solve_data/solve_branch__time_branch.csv",
|
|
756
|
+
"solve_data/first_timesteps.csv",
|
|
757
|
+
"solve_data/pt_profile.csv",
|
|
758
|
+
"input/pbt_profile.csv",
|
|
759
|
+
"input/groupIncludeStochastics.csv",
|
|
760
|
+
"input/group__process.csv",
|
|
761
|
+
"input/group__node.csv",
|
|
762
|
+
"input/process__profile__profile_method.csv",
|
|
763
|
+
"input/node__profile__profile_method.csv",
|
|
764
|
+
"input/process__node__profile__profile_method.csv",
|
|
765
|
+
])
|
|
766
|
+
|
|
767
|
+
tiers = _classify_profile_rows(source)
|
|
768
|
+
scalar_profiles = sorted(p for p, t in tiers.items() if t == "scalar")
|
|
769
|
+
period_profiles = sorted(p for p, t in tiers.items() if t == "period")
|
|
770
|
+
time_profiles = sorted(p for p, t in tiers.items() if t == "time")
|
|
771
|
+
pt_profiles = sorted(p for p, t in tiers.items() if t == "period_time")
|
|
772
|
+
stoch_profiles = sorted(p for p, t in tiers.items() if t == "stochastic")
|
|
773
|
+
|
|
774
|
+
# Build per-tier frames with priority annotation; lower wins.
|
|
775
|
+
parts: list[pl.LazyFrame] = []
|
|
776
|
+
|
|
777
|
+
if stoch_profiles and workdir is not None:
|
|
778
|
+
st = _profile_stochastic_lf(workdir, stoch_profiles, provider=provider)
|
|
779
|
+
parts.append(st.with_columns(prio=pl.lit(1, dtype=pl.Int8)))
|
|
780
|
+
|
|
781
|
+
if pt_profiles:
|
|
782
|
+
pt = _profile_period_time_lf(source, dt_lf, pt_profiles)
|
|
783
|
+
parts.append(pt.with_columns(prio=pl.lit(2, dtype=pl.Int8)))
|
|
784
|
+
|
|
785
|
+
if time_profiles:
|
|
786
|
+
tt = _profile_time_lf(source, dt_lf, time_profiles, workdir=workdir,
|
|
787
|
+
provider=provider)
|
|
788
|
+
parts.append(tt.with_columns(prio=pl.lit(3, dtype=pl.Int8)))
|
|
789
|
+
|
|
790
|
+
if period_profiles:
|
|
791
|
+
pp = _profile_period_only_lf(source, dt_lf, period_profiles)
|
|
792
|
+
parts.append(pp.with_columns(prio=pl.lit(4, dtype=pl.Int8)))
|
|
793
|
+
|
|
794
|
+
if scalar_profiles:
|
|
795
|
+
sc = _profile_scalar_lf(source, dt_lf, scalar_profiles)
|
|
796
|
+
parts.append(sc.with_columns(prio=pl.lit(5, dtype=pl.Int8)))
|
|
797
|
+
|
|
798
|
+
# Tier 5 — zero fallback for every profile entity × dt cell.
|
|
799
|
+
# Mirrors flextool's ``write_pdtProfile`` final ``else: 0.0`` branch
|
|
800
|
+
# (entity_period_calc_params.py:946-947). Every profile listed in
|
|
801
|
+
# ``profile.csv`` (the entity table) gets a row for each (d, t) of
|
|
802
|
+
# the dispatch frame; tiers 1-4 then overlay the actual values.
|
|
803
|
+
try:
|
|
804
|
+
ents = source.entities("profile")
|
|
805
|
+
if ents.height > 0:
|
|
806
|
+
ent_lf = (ents.lazy()
|
|
807
|
+
.select(alias_to_axis("name", "f"))
|
|
808
|
+
.unique())
|
|
809
|
+
zero_lf = (ent_lf
|
|
810
|
+
.join(dt_lf, how="cross")
|
|
811
|
+
.with_columns(value=pl.lit(0.0, dtype=pl.Float64))
|
|
812
|
+
.with_columns(prio=pl.lit(99, dtype=pl.Int8)))
|
|
813
|
+
parts.append(zero_lf)
|
|
814
|
+
except KeyError:
|
|
815
|
+
pass
|
|
816
|
+
|
|
817
|
+
if not parts:
|
|
818
|
+
return pl.LazyFrame(schema={
|
|
819
|
+
"f": schema_dtype(_enums, "f"), "d": schema_dtype(_enums, "d"), "t": schema_dtype(_enums, "t"), "value": pl.Float64,
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
union = pl.concat(parts, how="vertical")
|
|
823
|
+
return (union
|
|
824
|
+
.sort(["f", "d", "t", "prio"])
|
|
825
|
+
.group_by(["f", "d", "t"])
|
|
826
|
+
.agg(pl.col("value").first())
|
|
827
|
+
.select("f", "d", "t", "value")
|
|
828
|
+
.sort(["f", "d", "t"]))
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def p_profile_value_from_source_v2(source: "InputSource",
|
|
832
|
+
dt: pl.DataFrame,
|
|
833
|
+
workdir: Path | None = None,
|
|
834
|
+
*,
|
|
835
|
+
provider: "object | None" = None,
|
|
836
|
+
) -> Param | None:
|
|
837
|
+
"""Δ.7 lazy port replacement for
|
|
838
|
+
``_derived_params.p_profile_value_from_source``.
|
|
839
|
+
|
|
840
|
+
Returns ``Param(("f", "d", "t"), frame)`` or ``None`` when the
|
|
841
|
+
cascade produces no rows (no profiles in the source, or all
|
|
842
|
+
profiles unresolved).
|
|
843
|
+
"""
|
|
844
|
+
if dt is None:
|
|
845
|
+
return None
|
|
846
|
+
lf = p_profile_value_lf(source, dt, workdir=workdir, provider=provider)
|
|
847
|
+
out = lf.collect()
|
|
848
|
+
if out.height == 0:
|
|
849
|
+
return None
|
|
850
|
+
return Param(("f", "d", "t"), out)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def apply_profile_cascade(flex_data: object,
|
|
854
|
+
source: "InputSource",
|
|
855
|
+
workdir: Path | None,
|
|
856
|
+
*,
|
|
857
|
+
provider: "object | None" = None) -> None:
|
|
858
|
+
"""Public entry: write ``flex_data.p_profile_value`` from cluster C.
|
|
859
|
+
|
|
860
|
+
Invoked after :func:`apply_derived_a` so ``flex_data.dt`` is
|
|
861
|
+
populated. Matches flextool's per-solve cascade: emits a Param
|
|
862
|
+
when at least one profile resolves; the helper returns ``None``
|
|
863
|
+
when no profile is declared (tier-5 / zero is implicit at the
|
|
864
|
+
LP-build site).
|
|
865
|
+
|
|
866
|
+
Δ.12b — assignment is unconditional; ``None`` is the explicit
|
|
867
|
+
"no profile data" signal (no silent fall-through to a CSV-loaded
|
|
868
|
+
seed value).
|
|
869
|
+
"""
|
|
870
|
+
dt = getattr(flex_data, "dt", None)
|
|
871
|
+
if dt is None:
|
|
872
|
+
return
|
|
873
|
+
flex_data.p_profile_value = p_profile_value_from_source_v2(
|
|
874
|
+
source, dt, workdir=workdir, provider=provider)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
__all__ = [
|
|
878
|
+
"p_profile_value_lf",
|
|
879
|
+
"p_profile_value_from_source_v2",
|
|
880
|
+
"apply_profile_cascade",
|
|
881
|
+
]
|