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,375 @@
|
|
|
1
|
+
"""Commodity ladder input-derivation — cumulative + annual tabular emission.
|
|
2
|
+
|
|
3
|
+
Two ladder writers:
|
|
4
|
+
|
|
5
|
+
* :func:`derive_commodity_ladder_cumulative`
|
|
6
|
+
* :func:`derive_commodity_ladder_annual`
|
|
7
|
+
|
|
8
|
+
Both emit one canonical frame to the :class:`FlexDataProvider`:
|
|
9
|
+
|
|
10
|
+
* ``input/commodity_ladder_cumulative`` ``(commodity, tier, price, quantity)``
|
|
11
|
+
* ``input/commodity_ladder_annual`` ``(commodity, period, tier, price, quantity)``
|
|
12
|
+
|
|
13
|
+
The annual derivation auto-detects 2d vs 3d Spine maps and expands 2d
|
|
14
|
+
maps across the model period list (sourced from the Provider's
|
|
15
|
+
``input/periods_available`` key — populated by the
|
|
16
|
+
``_PARAMETER_SPECS`` loop — with DB fallbacks for setups that supply
|
|
17
|
+
periods only through ``solve.period_timeset``).
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
import polars as pl
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _tier_sort_key(t: str) -> tuple[int, str]:
|
|
32
|
+
"""Stable sort by integer tier when possible, else by string."""
|
|
33
|
+
try:
|
|
34
|
+
return (0, f"{int(t):020d}")
|
|
35
|
+
except ValueError:
|
|
36
|
+
return (1, t)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _quantity_sentinel(quantity: str) -> str:
|
|
40
|
+
"""Convert user-facing infinite quantities into the 1e30 sentinel
|
|
41
|
+
interpreted as the unbounded tail tier
|
|
42
|
+
(see ``ladder_tier_cap_infinite_cum`` / ``_ann``).
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
q_float = float(quantity)
|
|
46
|
+
except ValueError:
|
|
47
|
+
q_float = float("inf")
|
|
48
|
+
if q_float == float("inf") or q_float >= 1e30:
|
|
49
|
+
return "1e30"
|
|
50
|
+
return quantity
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _iter_flat_ladder_rows(api, value, commodity: str,
|
|
54
|
+
logger: logging.Logger) -> list[list]:
|
|
55
|
+
"""Flatten a Spine map ladder value. Returns the raw list-of-lists
|
|
56
|
+
from ``convert_map_to_table`` or an empty list on failure.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
return api.convert_map_to_table(value)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.warning(
|
|
62
|
+
"Could not flatten ladder for commodity '%s': %s",
|
|
63
|
+
commodity, exc,
|
|
64
|
+
)
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _collect_periods(backend, provider, api) -> list[str]:
|
|
69
|
+
"""Return the model's period list (for 1d-map → per-period expansion
|
|
70
|
+
of ``price_ladder_annual``).
|
|
71
|
+
|
|
72
|
+
Step 2.5-F Phase E
|
|
73
|
+
------------------
|
|
74
|
+
|
|
75
|
+
Provider-first: the ``input/periods_available`` key is populated by
|
|
76
|
+
:meth:`SpineDBBackend.parameter_values` during the
|
|
77
|
+
``_PARAMETER_SPECS`` pass for ``model.periods_available``. DB
|
|
78
|
+
fallbacks (in order) cover the case where the parameter is set as a
|
|
79
|
+
raw map or where periods come exclusively from
|
|
80
|
+
``solve.period_timeset``.
|
|
81
|
+
"""
|
|
82
|
+
periods: list[str] = []
|
|
83
|
+
seen: set[str] = set()
|
|
84
|
+
|
|
85
|
+
# Provider arm — frame layout: "model,period_from_model" (the legacy
|
|
86
|
+
# input/periods_available.csv schema). Take the rightmost column.
|
|
87
|
+
if provider is not None and provider.has("input/periods_available"):
|
|
88
|
+
df = provider.get("input/periods_available")
|
|
89
|
+
if df is not None and df.height > 0:
|
|
90
|
+
col = df.columns[-1]
|
|
91
|
+
for value in df.get_column(col).to_list():
|
|
92
|
+
v = str(value).strip()
|
|
93
|
+
if v and v not in seen:
|
|
94
|
+
periods.append(v)
|
|
95
|
+
seen.add(v)
|
|
96
|
+
if periods:
|
|
97
|
+
return periods
|
|
98
|
+
|
|
99
|
+
# Fallback 1: raw map on model.periods_available.
|
|
100
|
+
for pv in backend.find_parameter_values(
|
|
101
|
+
entity_class_name="model",
|
|
102
|
+
parameter_definition_name="periods_available",
|
|
103
|
+
):
|
|
104
|
+
if pv["type"] is None:
|
|
105
|
+
continue
|
|
106
|
+
val = pv["parsed_value"]
|
|
107
|
+
try:
|
|
108
|
+
flat = api.convert_map_to_table(val)
|
|
109
|
+
except Exception:
|
|
110
|
+
flat = []
|
|
111
|
+
for entry in flat:
|
|
112
|
+
for c in (str(x) for x in entry):
|
|
113
|
+
if c and c not in seen:
|
|
114
|
+
periods.append(c)
|
|
115
|
+
seen.add(c)
|
|
116
|
+
if periods:
|
|
117
|
+
return periods
|
|
118
|
+
|
|
119
|
+
# Fallback 2: scan solve.period_timeset map indexes.
|
|
120
|
+
for pv in backend.find_parameter_values(
|
|
121
|
+
entity_class_name="solve",
|
|
122
|
+
parameter_definition_name="period_timeset",
|
|
123
|
+
):
|
|
124
|
+
if pv["type"] is None:
|
|
125
|
+
continue
|
|
126
|
+
val = pv["parsed_value"]
|
|
127
|
+
try:
|
|
128
|
+
flat = api.convert_map_to_table(val)
|
|
129
|
+
except Exception:
|
|
130
|
+
flat = []
|
|
131
|
+
for entry in flat:
|
|
132
|
+
if len(entry) >= 1:
|
|
133
|
+
p = str(entry[0])
|
|
134
|
+
if p and p not in seen:
|
|
135
|
+
periods.append(p)
|
|
136
|
+
seen.add(p)
|
|
137
|
+
return periods
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Phase D — commodity_ladder_cumulative
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def derive_commodity_ladder_cumulative(
|
|
146
|
+
backend, provider, logger: logging.Logger,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Run the cumulative-ladder derivation.
|
|
149
|
+
|
|
150
|
+
Emits the Provider frame ``input/commodity_ladder_cumulative`` with
|
|
151
|
+
columns ``(commodity, tier, price, quantity)`` — one row per
|
|
152
|
+
(commodity, tier).
|
|
153
|
+
|
|
154
|
+
Only the ``commodity.price_ladder_cumulative`` parameter is
|
|
155
|
+
consulted (always a 2d map: ``Map(tier -> {price, quantity})`` —
|
|
156
|
+
2d in Spine's counting because ``{price, quantity}`` is a second
|
|
157
|
+
index layer). The ``price_method`` filter happens mod-side via
|
|
158
|
+
the ``commodity_with_ladder_cumulative`` set.
|
|
159
|
+
"""
|
|
160
|
+
api = backend.api
|
|
161
|
+
rows: list[tuple[str, int, str, str]] = []
|
|
162
|
+
|
|
163
|
+
for pv in backend.find_parameter_values(
|
|
164
|
+
entity_class_name="commodity",
|
|
165
|
+
parameter_definition_name="price_ladder_cumulative",
|
|
166
|
+
):
|
|
167
|
+
if pv["type"] is None:
|
|
168
|
+
continue
|
|
169
|
+
if pv["type"] != "map":
|
|
170
|
+
logger.warning(
|
|
171
|
+
"commodity.price_ladder_cumulative on '%s' has type %s "
|
|
172
|
+
"(expected nested 1d map); skipping.",
|
|
173
|
+
pv["entity_byname"][0], pv["type"],
|
|
174
|
+
)
|
|
175
|
+
continue
|
|
176
|
+
commodity = pv["entity_byname"][0]
|
|
177
|
+
flat = _iter_flat_ladder_rows(api, pv["parsed_value"], commodity, logger)
|
|
178
|
+
per_tier: dict[str, dict[str, str]] = {}
|
|
179
|
+
for entry in flat:
|
|
180
|
+
# Expected layout: [tier_idx, facet, value] (length 3).
|
|
181
|
+
if len(entry) < 3:
|
|
182
|
+
continue
|
|
183
|
+
tier_str = str(entry[0])
|
|
184
|
+
facet = str(entry[1])
|
|
185
|
+
val = entry[-1]
|
|
186
|
+
per_tier.setdefault(tier_str, {})[facet] = str(val)
|
|
187
|
+
|
|
188
|
+
for tier_str in sorted(per_tier.keys(), key=_tier_sort_key):
|
|
189
|
+
facets = per_tier[tier_str]
|
|
190
|
+
price = facets.get("price", "0")
|
|
191
|
+
quantity = _quantity_sentinel(facets.get("quantity", "inf"))
|
|
192
|
+
try:
|
|
193
|
+
tier_int = int(tier_str)
|
|
194
|
+
except ValueError:
|
|
195
|
+
logger.warning(
|
|
196
|
+
"commodity.price_ladder_cumulative tier on '%s' is not "
|
|
197
|
+
"an integer ('%s'); skipping tier.",
|
|
198
|
+
commodity, tier_str,
|
|
199
|
+
)
|
|
200
|
+
continue
|
|
201
|
+
rows.append((commodity, tier_int, price, quantity))
|
|
202
|
+
|
|
203
|
+
# All-Utf8 frame to match the legacy CSV's textual schema (consumers
|
|
204
|
+
# read these through the writer-port ``_read_csv`` helper which
|
|
205
|
+
# expects string columns).
|
|
206
|
+
frame = pl.DataFrame(
|
|
207
|
+
{
|
|
208
|
+
"commodity": [r[0] for r in rows],
|
|
209
|
+
"tier": [str(r[1]) for r in rows],
|
|
210
|
+
"price": [r[2] for r in rows],
|
|
211
|
+
"quantity": [r[3] for r in rows],
|
|
212
|
+
},
|
|
213
|
+
schema={
|
|
214
|
+
"commodity": pl.Utf8,
|
|
215
|
+
"tier": pl.Utf8,
|
|
216
|
+
"price": pl.Utf8,
|
|
217
|
+
"quantity": pl.Utf8,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
provider.put("input/commodity_ladder_cumulative", frame)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Phase E — commodity_ladder_annual (2d / 3d auto-detect + period expand)
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def derive_commodity_ladder_annual(
|
|
229
|
+
backend, provider, logger: logging.Logger,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Run the annual-ladder derivation.
|
|
232
|
+
|
|
233
|
+
Emits the Provider frame ``input/commodity_ladder_annual`` with
|
|
234
|
+
columns ``(commodity, period, tier, price, quantity)`` — one row
|
|
235
|
+
per (commodity, period, tier).
|
|
236
|
+
|
|
237
|
+
Reads ``commodity.price_ladder_annual``. Auto-detects the map depth:
|
|
238
|
+
|
|
239
|
+
* 2d form (Spine 2d_map): ``Map(tier -> {price, quantity})`` — the
|
|
240
|
+
same (price, quantity) is expanded across every model period.
|
|
241
|
+
* 3d form (Spine 3d_map): ``Map(period -> Map(tier -> {price,
|
|
242
|
+
quantity}))`` — per-period rows are kept as-is.
|
|
243
|
+
"""
|
|
244
|
+
api = backend.api
|
|
245
|
+
rows: list[tuple[str, str, int, str, str]] = []
|
|
246
|
+
periods_cache: list[str] | None = None
|
|
247
|
+
|
|
248
|
+
for pv in backend.find_parameter_values(
|
|
249
|
+
entity_class_name="commodity",
|
|
250
|
+
parameter_definition_name="price_ladder_annual",
|
|
251
|
+
):
|
|
252
|
+
if pv["type"] is None:
|
|
253
|
+
continue
|
|
254
|
+
if pv["type"] != "map":
|
|
255
|
+
logger.warning(
|
|
256
|
+
"commodity.price_ladder_annual on '%s' has type %s "
|
|
257
|
+
"(expected nested map); skipping.",
|
|
258
|
+
pv["entity_byname"][0], pv["type"],
|
|
259
|
+
)
|
|
260
|
+
continue
|
|
261
|
+
commodity = pv["entity_byname"][0]
|
|
262
|
+
flat = _iter_flat_ladder_rows(api, pv["parsed_value"], commodity, logger)
|
|
263
|
+
if not flat:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Depth detection via the flat table row length. Spine's Map
|
|
267
|
+
# dimension count matches the flat row length: 2d_map yields
|
|
268
|
+
# length-3 rows, 3d_map yields length-4 rows.
|
|
269
|
+
# 2d_map: [tier, facet, value] → len 3
|
|
270
|
+
# 3d_map: [period, tier, facet, value] → len 4
|
|
271
|
+
max_len = max((len(row) for row in flat), default=0)
|
|
272
|
+
if max_len == 3:
|
|
273
|
+
# 2d_map → expand across all model periods.
|
|
274
|
+
per_tier: dict[str, dict[str, str]] = {}
|
|
275
|
+
for entry in flat:
|
|
276
|
+
if len(entry) < 3:
|
|
277
|
+
continue
|
|
278
|
+
tier_str = str(entry[0])
|
|
279
|
+
facet = str(entry[1])
|
|
280
|
+
val = entry[-1]
|
|
281
|
+
per_tier.setdefault(tier_str, {})[facet] = str(val)
|
|
282
|
+
if periods_cache is None:
|
|
283
|
+
periods_cache = _collect_periods(backend, provider, api)
|
|
284
|
+
if not periods_cache:
|
|
285
|
+
logger.warning(
|
|
286
|
+
"commodity.price_ladder_annual on '%s' is 2d_map but "
|
|
287
|
+
"no model periods were available for expansion; "
|
|
288
|
+
"skipping.", commodity,
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
for period in periods_cache:
|
|
292
|
+
for tier_str in sorted(per_tier.keys(), key=_tier_sort_key):
|
|
293
|
+
facets = per_tier[tier_str]
|
|
294
|
+
price = facets.get("price", "0")
|
|
295
|
+
quantity = _quantity_sentinel(facets.get("quantity", "inf"))
|
|
296
|
+
try:
|
|
297
|
+
tier_int = int(tier_str)
|
|
298
|
+
except ValueError:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"commodity.price_ladder_annual tier on '%s' is "
|
|
301
|
+
"not an integer ('%s'); skipping tier.",
|
|
302
|
+
commodity, tier_str,
|
|
303
|
+
)
|
|
304
|
+
continue
|
|
305
|
+
rows.append(
|
|
306
|
+
(commodity, period, tier_int, price, quantity)
|
|
307
|
+
)
|
|
308
|
+
elif max_len >= 4:
|
|
309
|
+
# 3d_map → per-period. Flat row layout [period, tier,
|
|
310
|
+
# facet, value] — Spine nests Map(period -> Map(tier ->
|
|
311
|
+
# {price, quantity})).
|
|
312
|
+
per_period_tier: dict[tuple[str, str], dict[str, str]] = {}
|
|
313
|
+
for entry in flat:
|
|
314
|
+
if len(entry) < 4:
|
|
315
|
+
continue
|
|
316
|
+
period = str(entry[0])
|
|
317
|
+
tier_str = str(entry[1])
|
|
318
|
+
facet = str(entry[2])
|
|
319
|
+
val = entry[-1]
|
|
320
|
+
per_period_tier.setdefault(
|
|
321
|
+
(period, tier_str), {}
|
|
322
|
+
)[facet] = str(val)
|
|
323
|
+
|
|
324
|
+
def _sort_key(k: tuple[str, str]) -> tuple:
|
|
325
|
+
return (k[0], _tier_sort_key(k[1]))
|
|
326
|
+
|
|
327
|
+
for (period, tier_str) in sorted(
|
|
328
|
+
per_period_tier.keys(), key=_sort_key,
|
|
329
|
+
):
|
|
330
|
+
facets = per_period_tier[(period, tier_str)]
|
|
331
|
+
price = facets.get("price", "0")
|
|
332
|
+
quantity = _quantity_sentinel(facets.get("quantity", "inf"))
|
|
333
|
+
try:
|
|
334
|
+
tier_int = int(tier_str)
|
|
335
|
+
except ValueError:
|
|
336
|
+
logger.warning(
|
|
337
|
+
"commodity.price_ladder_annual tier on '%s' is not "
|
|
338
|
+
"an integer ('%s'); skipping tier.",
|
|
339
|
+
commodity, tier_str,
|
|
340
|
+
)
|
|
341
|
+
continue
|
|
342
|
+
rows.append(
|
|
343
|
+
(commodity, period, tier_int, price, quantity)
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
logger.warning(
|
|
347
|
+
"commodity.price_ladder_annual on '%s' has unexpected "
|
|
348
|
+
"flattened shape (max row length %d); skipping.",
|
|
349
|
+
commodity, max_len,
|
|
350
|
+
)
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
frame = pl.DataFrame(
|
|
354
|
+
{
|
|
355
|
+
"commodity": [r[0] for r in rows],
|
|
356
|
+
"period": [r[1] for r in rows],
|
|
357
|
+
"tier": [str(r[2]) for r in rows],
|
|
358
|
+
"price": [r[3] for r in rows],
|
|
359
|
+
"quantity": [r[4] for r in rows],
|
|
360
|
+
},
|
|
361
|
+
schema={
|
|
362
|
+
"commodity": pl.Utf8,
|
|
363
|
+
"period": pl.Utf8,
|
|
364
|
+
"tier": pl.Utf8,
|
|
365
|
+
"price": pl.Utf8,
|
|
366
|
+
"quantity": pl.Utf8,
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
provider.put("input/commodity_ladder_annual", frame)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = [
|
|
373
|
+
"derive_commodity_ladder_cumulative",
|
|
374
|
+
"derive_commodity_ladder_annual",
|
|
375
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Commodity-ladder filtered subsets of ``commodity``.
|
|
2
|
+
|
|
3
|
+
Three single-dimension sets selected by ``p_commodity_price_method``:
|
|
4
|
+
|
|
5
|
+
set commodity_with_ladder := {c : price_method[c] != 'price'};
|
|
6
|
+
set commodity_with_ladder_annual := {c : price_method[c] == 'price_ladder_annual'};
|
|
7
|
+
set commodity_with_ladder_cumulative := {c : price_method[c] == 'price_ladder_cumulative'};
|
|
8
|
+
|
|
9
|
+
``p_commodity_price_method`` has ``default 'price'``, so commodities
|
|
10
|
+
not present in the price-method CSV are treated as ``'price'`` and
|
|
11
|
+
therefore excluded from all three sets.
|
|
12
|
+
|
|
13
|
+
Output frames (canonical CSV column = ``commodity``):
|
|
14
|
+
|
|
15
|
+
* ``solve_data/commodity_with_ladder``
|
|
16
|
+
* ``solve_data/commodity_with_ladder_annual``
|
|
17
|
+
* ``solve_data/commodity_with_ladder_cumulative``
|
|
18
|
+
|
|
19
|
+
The frames land under the ``solve_data/<name>`` Provider key, matching
|
|
20
|
+
the ``solve_data/<file>.csv`` paths that consumers
|
|
21
|
+
(``_emit_mid_sets``, ``_emit_per_solve``, ``model.py``, the
|
|
22
|
+
``_commodity_ladder`` cascade loader) resolve through the
|
|
23
|
+
Provider-aware ``_read_csv`` helper.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import polars as pl
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def derive_commodity_ladder_sets(backend, provider) -> None:
|
|
31
|
+
"""Run the commodity-ladder-sets derivation.
|
|
32
|
+
|
|
33
|
+
Reads ``commodity.price_method`` via the
|
|
34
|
+
:class:`SpineDBBackend` and emits three Provider frames.
|
|
35
|
+
"""
|
|
36
|
+
# Read price_method per commodity directly from the Backend.
|
|
37
|
+
price_methods: dict[str, str] = {}
|
|
38
|
+
for pv in backend.find_parameter_values(
|
|
39
|
+
entity_class_name="commodity",
|
|
40
|
+
parameter_definition_name="price_method",
|
|
41
|
+
):
|
|
42
|
+
if pv["type"] is None:
|
|
43
|
+
continue
|
|
44
|
+
price_methods[pv["entity_byname"][0]] = str(pv["parsed_value"])
|
|
45
|
+
|
|
46
|
+
with_ladder = list(dict.fromkeys(
|
|
47
|
+
c for c, m in price_methods.items() if m != "price"
|
|
48
|
+
))
|
|
49
|
+
with_annual = list(dict.fromkeys(
|
|
50
|
+
c for c, m in price_methods.items() if m == "price_ladder_annual"
|
|
51
|
+
))
|
|
52
|
+
with_cum = list(dict.fromkeys(
|
|
53
|
+
c for c, m in price_methods.items() if m == "price_ladder_cumulative"
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
for key, rows in (
|
|
57
|
+
("solve_data/commodity_with_ladder", with_ladder),
|
|
58
|
+
("solve_data/commodity_with_ladder_annual", with_annual),
|
|
59
|
+
("solve_data/commodity_with_ladder_cumulative", with_cum),
|
|
60
|
+
):
|
|
61
|
+
provider.put(
|
|
62
|
+
key,
|
|
63
|
+
pl.DataFrame(
|
|
64
|
+
{"commodity": rows},
|
|
65
|
+
schema={"commodity": pl.Utf8},
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["derive_commodity_ladder_sets"]
|