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,335 @@
|
|
|
1
|
+
"""Per-solve sets derived natively from InputSource.
|
|
2
|
+
|
|
3
|
+
Produces — directly from an :class:`InputSource` (and an optional
|
|
4
|
+
active solve name) — the per-solve aggregates that preprocessing
|
|
5
|
+
emits to ``solve_data/*.csv``. Specifically:
|
|
6
|
+
|
|
7
|
+
* ``period_in_use`` — the ``[d]`` set of periods active in the current
|
|
8
|
+
solve (``setof d from dt`` projected from ``steps_in_use.csv``).
|
|
9
|
+
* ``dt_complete`` — the ``[d, t]`` set of complete-time-in-use pairs,
|
|
10
|
+
matching ``steps_complete_solve.csv``.
|
|
11
|
+
* ``period__timeline`` — the ``[d, timeline]`` mapping (timeset →
|
|
12
|
+
timeline lookup).
|
|
13
|
+
* ``p_timeline_duration_in_years`` — the ``[timeline, value]`` per-
|
|
14
|
+
timeline year fraction (``sum_t step_duration / 8760``).
|
|
15
|
+
* ``complete_period_share_of_year`` — the ``[d, value]`` per-period
|
|
16
|
+
year fraction, restricted to dt_complete.
|
|
17
|
+
|
|
18
|
+
The authoritative caller is :func:`derive_per_solve_aggregates`
|
|
19
|
+
which returns a typed :class:`PerSolveAggregates` dataclass. A
|
|
20
|
+
downstream consumer benefits:
|
|
21
|
+
|
|
22
|
+
* :mod:`flextool.engine_polars._derived_params` —
|
|
23
|
+
``_dt_period_active_steps_from_workdir`` can fall through to native
|
|
24
|
+
derivation when the workdir CSVs are absent.
|
|
25
|
+
|
|
26
|
+
When a fixture has a ``solve.period_timeset`` filter for the active
|
|
27
|
+
solve in the source DB, the helpers no longer need ``solve_data/``
|
|
28
|
+
CSV output to compute their domain.
|
|
29
|
+
|
|
30
|
+
Architecture invariants
|
|
31
|
+
-----------------------
|
|
32
|
+
|
|
33
|
+
* **Lazy throughout** — every internal frame is a
|
|
34
|
+
:class:`polars.LazyFrame`; the public dataclass holds eager
|
|
35
|
+
:class:`polars.DataFrame` for caller convenience but every
|
|
36
|
+
computation funnels through one ``.collect()`` at the rim.
|
|
37
|
+
* **None default → skip entirely** — the public function returns
|
|
38
|
+
``None`` when the source lacks ``solve.period_timeset`` /
|
|
39
|
+
``timeline.timestep_duration``; callers fall through to the workdir
|
|
40
|
+
path when present, or the existing legacy CSV fallback.
|
|
41
|
+
* **No defensive gating** — helpers fail loudly when frames have
|
|
42
|
+
unexpected schemas.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from typing import TYPE_CHECKING
|
|
48
|
+
|
|
49
|
+
import polars as pl
|
|
50
|
+
|
|
51
|
+
from ._axis_enums import alias_to_axis
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from flextool.engine_polars._input_source import InputSource
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"PerSolveAggregates",
|
|
59
|
+
"derive_per_solve_aggregates",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Public dataclass
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class PerSolveAggregates:
|
|
70
|
+
"""Native per-solve aggregates derived from an :class:`InputSource`.
|
|
71
|
+
|
|
72
|
+
All frames are eagerly materialised
|
|
73
|
+
(:class:`polars.DataFrame`) for caller convenience; internally the
|
|
74
|
+
derivation is lazy with one ``.collect()`` per field at the rim.
|
|
75
|
+
|
|
76
|
+
Attributes
|
|
77
|
+
----------
|
|
78
|
+
period_in_use : pl.DataFrame
|
|
79
|
+
``[d]`` distinct periods active in the current solve.
|
|
80
|
+
dt_complete : pl.DataFrame
|
|
81
|
+
``[d, t]`` complete-time-in-use pairs (every (d, t) over the
|
|
82
|
+
period's full timeline range, not just dt).
|
|
83
|
+
period_timeline : pl.DataFrame
|
|
84
|
+
``[d, timeline]`` mapping — one row per (period, timeline_name).
|
|
85
|
+
p_timeline_duration_in_years : pl.DataFrame
|
|
86
|
+
``[timeline, value]`` — ``sum_t step_duration[tl, t] / 8760``.
|
|
87
|
+
complete_period_share_of_year : pl.DataFrame
|
|
88
|
+
``[d, value]`` — period's full-timeline coverage as a year
|
|
89
|
+
fraction (``sum_t step_duration / 8760`` over dt_complete).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
period_in_use: pl.DataFrame
|
|
93
|
+
dt_complete: pl.DataFrame
|
|
94
|
+
period_timeline: pl.DataFrame
|
|
95
|
+
p_timeline_duration_in_years: pl.DataFrame
|
|
96
|
+
complete_period_share_of_year: pl.DataFrame
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Internal source helpers
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _try_param(source: "InputSource", entity_class: str,
|
|
105
|
+
parameter_name: str) -> pl.DataFrame | None:
|
|
106
|
+
try:
|
|
107
|
+
df = source.parameter(entity_class, parameter_name)
|
|
108
|
+
except KeyError:
|
|
109
|
+
return None
|
|
110
|
+
if df.height == 0:
|
|
111
|
+
return None
|
|
112
|
+
return df
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _timeline_step_duration_lf(source: "InputSource"
|
|
116
|
+
) -> pl.LazyFrame | None:
|
|
117
|
+
"""Lazy ``[timeline, t, step_duration]`` from
|
|
118
|
+
``timeline.timestep_duration``.
|
|
119
|
+
|
|
120
|
+
Returns ``None`` when the parameter is absent or has an unexpected
|
|
121
|
+
schema. The Spine source emits the index column under
|
|
122
|
+
``t`` / ``step`` / ``timestep`` / ``x`` depending on the underlying
|
|
123
|
+
Map index_name; the helper auto-discovers the column.
|
|
124
|
+
"""
|
|
125
|
+
tl_dur = _try_param(source, "timeline", "timestep_duration")
|
|
126
|
+
if tl_dur is None:
|
|
127
|
+
return None
|
|
128
|
+
cols = tl_dur.columns
|
|
129
|
+
step_col = next(
|
|
130
|
+
(c for c in ("t", "step", "timestep", "x")
|
|
131
|
+
if c in cols and c not in ("name", "value")),
|
|
132
|
+
None,
|
|
133
|
+
)
|
|
134
|
+
if step_col is None:
|
|
135
|
+
return None
|
|
136
|
+
return (tl_dur.lazy().select(
|
|
137
|
+
pl.col("name").alias("timeline"),
|
|
138
|
+
alias_to_axis(step_col, "t"),
|
|
139
|
+
pl.col("value").cast(pl.Float64).alias("step_duration"),
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _period_timeset_lf(source: "InputSource", active_solve: str
|
|
144
|
+
) -> pl.LazyFrame | None:
|
|
145
|
+
"""Lazy ``[d, ts]`` for the active solve from ``solve.period_timeset``.
|
|
146
|
+
|
|
147
|
+
Returns ``None`` when the parameter is absent OR the filter for the
|
|
148
|
+
active solve is empty (synthetic rolling-horizon / nested-invest
|
|
149
|
+
sub-solves whose names aren't in Spine — caller handles None).
|
|
150
|
+
"""
|
|
151
|
+
p_ts = _try_param(source, "solve", "period_timeset")
|
|
152
|
+
if p_ts is None:
|
|
153
|
+
return None
|
|
154
|
+
period_col = next((c for c in ("period", "x") if c in p_ts.columns),
|
|
155
|
+
None)
|
|
156
|
+
if period_col is None:
|
|
157
|
+
return None
|
|
158
|
+
lf = (p_ts.lazy()
|
|
159
|
+
.filter(pl.col("name") == active_solve)
|
|
160
|
+
.select(alias_to_axis(period_col, "d"),
|
|
161
|
+
pl.col("value").alias("ts")))
|
|
162
|
+
if lf.collect().height == 0:
|
|
163
|
+
return None
|
|
164
|
+
return lf
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _timeset_duration_lf(source: "InputSource") -> pl.LazyFrame | None:
|
|
168
|
+
"""Lazy ``[ts, start_step, count]`` from ``timeset.timeset_duration``.
|
|
169
|
+
|
|
170
|
+
Mirrors the (start, count) Map semantics: each ts has zero or more
|
|
171
|
+
contiguous timestep blocks, each starting at ``start_step`` and
|
|
172
|
+
spanning ``count`` consecutive timesteps in the timeline's lex order.
|
|
173
|
+
"""
|
|
174
|
+
ts_dur = _try_param(source, "timeset", "timeset_duration")
|
|
175
|
+
if ts_dur is None:
|
|
176
|
+
return None
|
|
177
|
+
cols = ts_dur.columns
|
|
178
|
+
step_col = next(
|
|
179
|
+
(c for c in ("t", "x", "step", "timestep")
|
|
180
|
+
if c in cols and c not in ("name", "value")),
|
|
181
|
+
None,
|
|
182
|
+
)
|
|
183
|
+
if step_col is None:
|
|
184
|
+
return None
|
|
185
|
+
return (ts_dur.lazy()
|
|
186
|
+
.select(pl.col("name").alias("ts"),
|
|
187
|
+
pl.col(step_col).alias("start_step"),
|
|
188
|
+
pl.col("value").cast(pl.Float64).alias("count")))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _timeset_timeline_lf(source: "InputSource") -> pl.LazyFrame | None:
|
|
192
|
+
"""Lazy ``[ts, timeline]`` from ``timeset.timeline``."""
|
|
193
|
+
ts_tl = _try_param(source, "timeset", "timeline")
|
|
194
|
+
if ts_tl is None:
|
|
195
|
+
return None
|
|
196
|
+
return (ts_tl.lazy()
|
|
197
|
+
.select(pl.col("name").alias("ts"),
|
|
198
|
+
pl.col("value").alias("timeline")))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Per-period active-step expansion (mirrors flextool's get_active_time)
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _expand_active_steps_lf(
|
|
207
|
+
pt_lf: pl.LazyFrame, # [d, ts]
|
|
208
|
+
ts_tl_lf: pl.LazyFrame, # [ts, timeline]
|
|
209
|
+
ts_dur_lf: pl.LazyFrame, # [ts, start_step, count]
|
|
210
|
+
tl_lf: pl.LazyFrame, # [timeline, t, step_duration]
|
|
211
|
+
) -> pl.LazyFrame:
|
|
212
|
+
"""Expand (d, ts) → (d, t, step_duration) by walking the timeline
|
|
213
|
+
starting at each block's ``start_step`` for ``count`` steps.
|
|
214
|
+
|
|
215
|
+
Mirrors :func:`flextool.engine_polars._timeline.get_active_time`'s
|
|
216
|
+
procedural expansion: for each timeset block, find the rank of
|
|
217
|
+
``start_step`` within the timeline's lex-ordered timestep list,
|
|
218
|
+
then emit timesteps with ranks in ``[start_rank, start_rank+count)``.
|
|
219
|
+
"""
|
|
220
|
+
# Rank timeline timesteps in lex order (matches t0001 < t0002 < ...).
|
|
221
|
+
tl_ranked = (tl_lf.sort("timeline", "t")
|
|
222
|
+
.with_columns(rank=pl.col("t").cum_count()
|
|
223
|
+
.over("timeline")
|
|
224
|
+
.cast(pl.Int64)))
|
|
225
|
+
# Resolve start_rank by joining timeline ranks on (timeline, t=start_step).
|
|
226
|
+
blocks = (pt_lf
|
|
227
|
+
.join(ts_tl_lf, on="ts", how="inner")
|
|
228
|
+
.join(ts_dur_lf, on="ts", how="inner")
|
|
229
|
+
.join(tl_ranked.select(
|
|
230
|
+
pl.col("timeline"),
|
|
231
|
+
pl.col("t").alias("start_step"),
|
|
232
|
+
pl.col("rank").alias("start_rank")),
|
|
233
|
+
on=["timeline", "start_step"], how="left")
|
|
234
|
+
.filter(pl.col("start_rank").is_not_null()))
|
|
235
|
+
# Cross-join with timeline ranks, then filter rank-in-window.
|
|
236
|
+
expanded = (blocks
|
|
237
|
+
.join(tl_ranked, on="timeline", how="inner")
|
|
238
|
+
.filter((pl.col("rank") >= pl.col("start_rank"))
|
|
239
|
+
& (pl.col("rank") < pl.col("start_rank")
|
|
240
|
+
+ pl.col("count").cast(pl.Int64))))
|
|
241
|
+
return expanded.select("d", "timeline", "t", "step_duration").unique()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Public driver
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def derive_per_solve_aggregates(
|
|
250
|
+
source: "InputSource",
|
|
251
|
+
active_solve: str,
|
|
252
|
+
) -> PerSolveAggregates | None:
|
|
253
|
+
"""Build per-solve aggregates natively from *source*.
|
|
254
|
+
|
|
255
|
+
Returns ``None`` when:
|
|
256
|
+
* ``timeline.timestep_duration`` is absent;
|
|
257
|
+
* ``solve.period_timeset`` for *active_solve* is empty (synthetic
|
|
258
|
+
rolling/nested sub-solves whose names aren't in Spine — caller
|
|
259
|
+
falls through to the workdir path);
|
|
260
|
+
* ``timeset.timeset_duration`` or ``timeset.timeline`` is absent.
|
|
261
|
+
|
|
262
|
+
Otherwise returns a fully-populated :class:`PerSolveAggregates`.
|
|
263
|
+
|
|
264
|
+
The derivation is lazy until each field's eager ``.collect()`` —
|
|
265
|
+
five collects total per call. Caller is expected to materialise
|
|
266
|
+
once per per-solve iteration and reuse across the override chain
|
|
267
|
+
(``_dt_period_active_steps``).
|
|
268
|
+
"""
|
|
269
|
+
pt_lf = _period_timeset_lf(source, active_solve)
|
|
270
|
+
if pt_lf is None:
|
|
271
|
+
return None
|
|
272
|
+
ts_dur_lf = _timeset_duration_lf(source)
|
|
273
|
+
if ts_dur_lf is None:
|
|
274
|
+
return None
|
|
275
|
+
ts_tl_lf = _timeset_timeline_lf(source)
|
|
276
|
+
if ts_tl_lf is None:
|
|
277
|
+
return None
|
|
278
|
+
tl_lf = _timeline_step_duration_lf(source)
|
|
279
|
+
if tl_lf is None:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
# Expand to (d, t, timeline, step_duration) at full lazy.
|
|
283
|
+
expanded_lf = _expand_active_steps_lf(pt_lf, ts_tl_lf, ts_dur_lf, tl_lf)
|
|
284
|
+
|
|
285
|
+
# ── period_in_use[d] ─────────────────────────────────────────────
|
|
286
|
+
period_in_use = (expanded_lf.select("d").unique().sort("d").collect())
|
|
287
|
+
if period_in_use.height == 0:
|
|
288
|
+
# No active periods for this solve — caller falls through.
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
# ── dt_complete[d, t] ────────────────────────────────────────────
|
|
292
|
+
# ``complete_active_time_lists = get_active_time(complete_solve_name)``
|
|
293
|
+
# — the ACTIVE expansion of the period's timeset blocks (NOT the
|
|
294
|
+
# full timeline). For non-rolling solves this equals dt; for
|
|
295
|
+
# rolling solves it's the parent solve's full-year expansion. Both
|
|
296
|
+
# are produced by the same expand-blocks-to-timesteps walk.
|
|
297
|
+
period_tl_lf = expanded_lf.select("d", "timeline").unique()
|
|
298
|
+
dt_complete = (expanded_lf.select("d", "t").unique()
|
|
299
|
+
.sort("d", "t").collect())
|
|
300
|
+
|
|
301
|
+
# ── period_timeline[d, timeline] ─────────────────────────────────
|
|
302
|
+
period_timeline = (period_tl_lf.sort("d", "timeline").collect())
|
|
303
|
+
|
|
304
|
+
# ── p_timeline_duration_in_years[timeline] ───────────────────────
|
|
305
|
+
# = sum_t step_duration[timeline, t] / 8760
|
|
306
|
+
p_tdy = (tl_lf.group_by("timeline")
|
|
307
|
+
.agg((pl.col("step_duration").sum() / 8760.0)
|
|
308
|
+
.alias("value"))
|
|
309
|
+
.sort("timeline")
|
|
310
|
+
.collect())
|
|
311
|
+
|
|
312
|
+
# ── complete_period_share_of_year[d] ─────────────────────────────
|
|
313
|
+
# Mirrors ``period_calculated_params.py`` lines 186-204:
|
|
314
|
+
# complete_hours_in_period[d] = sum_{(d2, t) ∈ dt_complete,
|
|
315
|
+
# (d2, d) ∈ period__branch}
|
|
316
|
+
# complete_step_duration[d2, t]
|
|
317
|
+
# For deterministic (non-stochastic) solves the period__branch is
|
|
318
|
+
# the diagonal — (d, d) for every d — so the sum collapses to
|
|
319
|
+
# ``sum_t complete_step_duration[d, t]``. Multi-branch stochastic
|
|
320
|
+
# extension is out of scope for the current dispatch; the
|
|
321
|
+
# PerSolveAggregates dataclass already exposes the per-(d, d) frame
|
|
322
|
+
# which downstream branch-aware helpers can fold themselves.
|
|
323
|
+
cpsoy = (expanded_lf
|
|
324
|
+
.group_by("d").agg((pl.col("step_duration").sum() / 8760.0)
|
|
325
|
+
.alias("value"))
|
|
326
|
+
.sort("d")
|
|
327
|
+
.collect())
|
|
328
|
+
|
|
329
|
+
return PerSolveAggregates(
|
|
330
|
+
period_in_use=period_in_use,
|
|
331
|
+
dt_complete=dt_complete,
|
|
332
|
+
period_timeline=period_timeline,
|
|
333
|
+
p_timeline_duration_in_years=p_tdy,
|
|
334
|
+
complete_period_share_of_year=cpsoy,
|
|
335
|
+
)
|