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,876 @@
|
|
|
1
|
+
"""Build SheetSpec objects describing every sheet to generate in the Excel output.
|
|
2
|
+
|
|
3
|
+
Takes DatabaseContents (from db_reader) and YAML settings to produce an ordered
|
|
4
|
+
list of SheetSpec, each encoding the layout, entity columns, parameter columns,
|
|
5
|
+
and special rules for one Excel sheet.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from flextool.export_to_tabular.db_reader import DatabaseContents
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SheetSpec:
|
|
21
|
+
"""Description of a single Excel sheet to generate."""
|
|
22
|
+
|
|
23
|
+
sheet_name: str
|
|
24
|
+
layout: str # 'constant', 'periodic', 'timeseries', 'link', 'scenario', 'navigate', 'version'
|
|
25
|
+
entity_classes: list[str] # DB entity classes that contribute data
|
|
26
|
+
entity_columns: list[str] # Column header names for entity dimensions
|
|
27
|
+
parameter_names: list[str] = field(default_factory=list)
|
|
28
|
+
direction_column: str | None = None # e.g. 'input_output' for merged unit__inputNode/outputNode
|
|
29
|
+
direction_map: dict[str, str] = field(default_factory=dict) # entity_class -> direction value
|
|
30
|
+
extra_entity_columns: list[str] = field(default_factory=list) # e.g. ['left_node', 'right_node']
|
|
31
|
+
extra_entity_class: str | None = None # class to get extra elements from
|
|
32
|
+
unpack_index_column: str | None = None # e.g. 'constraint' for unpacked Map params
|
|
33
|
+
has_entity_alternative: bool = True # whether to include Entity Alternative column
|
|
34
|
+
pre_ea_params: list[str] = field(default_factory=list) # params before Entity Alternative column
|
|
35
|
+
index_name_default: str | None = None # fallback index column name for this layout
|
|
36
|
+
descriptions: dict[str, str] = field(default_factory=dict) # param_name -> description text
|
|
37
|
+
# Periodic-layout only: params whose SCALAR values are written on a sibling
|
|
38
|
+
# constant sheet (e.g. node.existing lives on both node_c and node_p because
|
|
39
|
+
# it is float for some entities, 1d-map for others). The periodic writer
|
|
40
|
+
# MUST NOT re-emit a scalar-only row for these — doing so duplicates the
|
|
41
|
+
# value and collides on import round-trip. Empty for split-params periodic
|
|
42
|
+
# sheets (e.g. 'solve'), where scalar params have no constant home and the
|
|
43
|
+
# scalar-only row is the only carrier.
|
|
44
|
+
scalar_params_on_constant_sibling: set[str] = field(default_factory=set)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_settings() -> dict:
|
|
48
|
+
"""Load export_settings.yaml from the same directory as this module."""
|
|
49
|
+
settings_path = Path(__file__).parent / "export_settings.yaml"
|
|
50
|
+
with open(settings_path, encoding="utf-8") as f:
|
|
51
|
+
return yaml.safe_load(f)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _has_facet_leaf_shape(entity_class: str, param_name: str) -> bool:
|
|
55
|
+
"""Return True when ``(entity_class, param_name)`` is registered with a
|
|
56
|
+
facet-leaf shape (``LeafKind.FACET_PRICE_QUANTITY``).
|
|
57
|
+
|
|
58
|
+
Consults :data:`PARAM_ALLOWED_SHAPES` lazily to avoid pulling polars on
|
|
59
|
+
the export hot path.
|
|
60
|
+
"""
|
|
61
|
+
if not entity_class or not param_name:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
from flextool.engine_polars._param_shapes import (
|
|
65
|
+
LeafKind,
|
|
66
|
+
PARAM_ALLOWED_SHAPES,
|
|
67
|
+
shape_to_axes,
|
|
68
|
+
)
|
|
69
|
+
except Exception: # pragma: no cover — registry must be importable
|
|
70
|
+
return False
|
|
71
|
+
allowed = PARAM_ALLOWED_SHAPES.get((entity_class, param_name))
|
|
72
|
+
if not allowed:
|
|
73
|
+
return False
|
|
74
|
+
for shape in allowed:
|
|
75
|
+
if shape_to_axes(shape).leaf is LeafKind.FACET_PRICE_QUANTITY:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def classify_param_types(
|
|
81
|
+
param_type_list: tuple | None,
|
|
82
|
+
entity_class: str = "",
|
|
83
|
+
param_name: str = "",
|
|
84
|
+
) -> set[str]:
|
|
85
|
+
"""Given a parameter's type_list tuple, return which sheet layouts it can appear on.
|
|
86
|
+
|
|
87
|
+
Returns a set that may contain ``'constant'``, ``'periodic'``,
|
|
88
|
+
``'timeseries'``, ``'stochastic'``, or ``'ladder'``.
|
|
89
|
+
|
|
90
|
+
When the registry tags ``(entity_class, param_name)`` with a
|
|
91
|
+
``FACET_PRICE_QUANTITY`` leaf shape, the result becomes ``{'ladder'}``
|
|
92
|
+
— the param is NOT a generic stochastic / nested-periodic param and
|
|
93
|
+
must NOT share a sheet with constants. ``stochastic`` is suppressed
|
|
94
|
+
even when the type-list contains ``3d_map`` (the depth-3 variant of
|
|
95
|
+
``commodity.price_ladder_annual``); the third Map level carries the
|
|
96
|
+
``{price, quantity}`` facet, NOT a temporal axis.
|
|
97
|
+
"""
|
|
98
|
+
# Registry override for facet-leaf params (commodity.price_ladder_*):
|
|
99
|
+
# route exclusively to the dedicated ``ladder`` layout regardless of
|
|
100
|
+
# the schema-declared type-list.
|
|
101
|
+
if _has_facet_leaf_shape(entity_class, param_name):
|
|
102
|
+
return {"ladder"}
|
|
103
|
+
|
|
104
|
+
if param_type_list is None:
|
|
105
|
+
return {"constant"}
|
|
106
|
+
|
|
107
|
+
result: set[str] = set()
|
|
108
|
+
types = set(param_type_list)
|
|
109
|
+
|
|
110
|
+
if "float" in types or "str" in types:
|
|
111
|
+
result.add("constant")
|
|
112
|
+
if "array" in types:
|
|
113
|
+
result.add("constant")
|
|
114
|
+
if "2d_map" in types:
|
|
115
|
+
result.add("constant")
|
|
116
|
+
|
|
117
|
+
if "1d_map" in types:
|
|
118
|
+
# If 1d_map coexists with 3d_map but has no float/str constant fallback,
|
|
119
|
+
# the 1d_map variant is a simpler time-series (not period-indexed).
|
|
120
|
+
# Only add 'periodic' when there's also a constant type or no 3d_map.
|
|
121
|
+
if "3d_map" in types and not (types & {"float", "str"}):
|
|
122
|
+
# Pure time-series param (e.g. profile: ('1d_map', '3d_map'))
|
|
123
|
+
result.add("timeseries")
|
|
124
|
+
else:
|
|
125
|
+
result.add("periodic")
|
|
126
|
+
if "3d_map" in types:
|
|
127
|
+
result.add("timeseries")
|
|
128
|
+
|
|
129
|
+
if "3d_map" in types:
|
|
130
|
+
result.add("stochastic")
|
|
131
|
+
result.add("timeseries")
|
|
132
|
+
|
|
133
|
+
if "4d_map" in types:
|
|
134
|
+
result.add("stochastic")
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _has_time_indexed_values(
|
|
140
|
+
db_contents: DatabaseContents,
|
|
141
|
+
entity_class: str,
|
|
142
|
+
param_name: str,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Check whether any actual parameter value for this param is time-indexed.
|
|
145
|
+
|
|
146
|
+
Uses the IndexClassifier from excel_writer if available, falling back to
|
|
147
|
+
a simple index_name=='time' check.
|
|
148
|
+
"""
|
|
149
|
+
from flextool.export_to_tabular.excel_writer import _is_time_indexed_map
|
|
150
|
+
from spinedb_api import Map
|
|
151
|
+
|
|
152
|
+
for (cls, _byname, pname, _alt), value in db_contents.parameter_values.items():
|
|
153
|
+
if cls == entity_class and pname == param_name and isinstance(value, Map):
|
|
154
|
+
if _is_time_indexed_map(value):
|
|
155
|
+
return True
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_string_param(pname: str, entity_class: str, db_contents: DatabaseContents) -> bool:
|
|
160
|
+
"""Check if a parameter is string-valued (type_list contains 'str')."""
|
|
161
|
+
for pdef in db_contents.parameter_definitions.get(entity_class, []):
|
|
162
|
+
if pdef["name"] == pname:
|
|
163
|
+
type_list = pdef.get("parameter_type_list")
|
|
164
|
+
if type_list and "str" in type_list:
|
|
165
|
+
return True
|
|
166
|
+
return False
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def order_parameters(
|
|
171
|
+
param_names: list[str],
|
|
172
|
+
entity_class: str,
|
|
173
|
+
db_contents: DatabaseContents,
|
|
174
|
+
) -> list[str]:
|
|
175
|
+
"""Sort parameters: string-typed first, then numeric, each by group priority and name.
|
|
176
|
+
|
|
177
|
+
This ensures the string/float column-type boundary in the import spec
|
|
178
|
+
lines up correctly for roundtrip xlsx compatibility.
|
|
179
|
+
"""
|
|
180
|
+
{
|
|
181
|
+
p["name"]: p for p in db_contents.parameter_definitions.get(entity_class, [])
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
string_grouped: list[tuple[int, str]] = []
|
|
185
|
+
string_ungrouped: list[str] = []
|
|
186
|
+
float_grouped: list[tuple[int, str]] = []
|
|
187
|
+
float_ungrouped: list[str] = []
|
|
188
|
+
|
|
189
|
+
for pname in param_names:
|
|
190
|
+
is_str = _is_string_param(pname, entity_class, db_contents)
|
|
191
|
+
|
|
192
|
+
group_name = db_contents.param_to_group.get((entity_class, pname))
|
|
193
|
+
if group_name and group_name in db_contents.parameter_groups:
|
|
194
|
+
priority = db_contents.parameter_groups[group_name].get("priority") or 999
|
|
195
|
+
if is_str:
|
|
196
|
+
string_grouped.append((priority, pname))
|
|
197
|
+
else:
|
|
198
|
+
float_grouped.append((priority, pname))
|
|
199
|
+
else:
|
|
200
|
+
if is_str:
|
|
201
|
+
string_ungrouped.append(pname)
|
|
202
|
+
else:
|
|
203
|
+
float_ungrouped.append(pname)
|
|
204
|
+
|
|
205
|
+
string_grouped.sort(key=lambda x: (x[0], x[1]))
|
|
206
|
+
string_ungrouped.sort()
|
|
207
|
+
float_grouped.sort(key=lambda x: (x[0], x[1]))
|
|
208
|
+
float_ungrouped.sort()
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
[p[1] for p in string_grouped] + string_ungrouped
|
|
212
|
+
+ [p[1] for p in float_grouped] + float_ungrouped
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _get_param_defs_dict(
|
|
217
|
+
db_contents: DatabaseContents, entity_class: str
|
|
218
|
+
) -> dict[str, dict]:
|
|
219
|
+
"""Return {param_name: pdef_dict} for a given entity class."""
|
|
220
|
+
return {
|
|
221
|
+
pdef["name"]: pdef
|
|
222
|
+
for pdef in db_contents.parameter_definitions.get(entity_class, [])
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _get_dimension_names(db_contents: DatabaseContents, class_name: str) -> tuple:
|
|
227
|
+
"""Get dimension_name_list for a class."""
|
|
228
|
+
for ec in db_contents.entity_classes:
|
|
229
|
+
if ec["name"] == class_name:
|
|
230
|
+
return ec["dimension_name_list"]
|
|
231
|
+
return ()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _make_base_name(class_name: str, drop_dims: list[str]) -> str:
|
|
235
|
+
"""Convert a DB entity class name to a sheet base name.
|
|
236
|
+
|
|
237
|
+
Replaces ``__`` with ``_`` and drops any dimension names in *drop_dims*.
|
|
238
|
+
"""
|
|
239
|
+
parts = class_name.split("__")
|
|
240
|
+
filtered = [p for p in parts if p not in drop_dims]
|
|
241
|
+
return "_".join(filtered)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _sheet_name_in_order(name: str, sheet_order: list[str]) -> bool:
|
|
245
|
+
"""Check if a sheet name is present in the YAML sheet_order list."""
|
|
246
|
+
return name in sheet_order
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_sheet_specs(
|
|
250
|
+
db_contents: DatabaseContents,
|
|
251
|
+
settings: dict | None = None,
|
|
252
|
+
include_groups: set[str] | None = None,
|
|
253
|
+
) -> list[SheetSpec]:
|
|
254
|
+
"""Build the ordered list of SheetSpec objects for all Excel sheets.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
db_contents: Database contents from :func:`db_reader.read_database`.
|
|
258
|
+
settings: Parsed YAML settings dict, or *None* to load from disk.
|
|
259
|
+
include_groups: When given, restrict output to parameters whose
|
|
260
|
+
``parameter_group_name`` is in the set. Parameters without a
|
|
261
|
+
group are dropped. Sheets whose column set goes empty are
|
|
262
|
+
dropped, unless listed under a selected group in
|
|
263
|
+
``always_include_with_groups``.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Ordered list of :class:`SheetSpec` objects.
|
|
267
|
+
"""
|
|
268
|
+
if settings is None:
|
|
269
|
+
settings = load_settings()
|
|
270
|
+
|
|
271
|
+
# ---- Build lookup structures from settings ----
|
|
272
|
+
merge_rules: dict[str, dict] = settings.get("merge_classes", {})
|
|
273
|
+
split_rules: dict[str, dict] = settings.get("split_params", {})
|
|
274
|
+
unpack_rules: dict[str, dict] = settings.get("unpack_map_params", {})
|
|
275
|
+
element_rules: dict[str, dict] = settings.get("add_entity_elements", {})
|
|
276
|
+
sheet_order: list[str] = settings.get("sheet_order", [])
|
|
277
|
+
drop_dims: list[str] = settings.get("drop_dimensions", [])
|
|
278
|
+
rename_sheets: dict[str, str] = settings.get("rename_sheets", {})
|
|
279
|
+
ea_classes: set[str] = set(settings.get("entity_alternative_classes", []))
|
|
280
|
+
pre_ea_rules: dict[str, list[str]] = settings.get("pre_entity_alternative_params", {})
|
|
281
|
+
index_name_defaults: dict[str, str] = settings.get("index_name_defaults", {})
|
|
282
|
+
|
|
283
|
+
# Classes that are used as extra entity element sources (skip in generic path)
|
|
284
|
+
element_source_classes: set[str] = set()
|
|
285
|
+
for _cls, erule in element_rules.items():
|
|
286
|
+
element_source_classes.add(erule["from_class"])
|
|
287
|
+
|
|
288
|
+
# Sets of classes handled by merge/split/unpack so the generic path skips them
|
|
289
|
+
merged_classes: set[str] = set()
|
|
290
|
+
for _group_name, rule in merge_rules.items():
|
|
291
|
+
for cls_name in rule["classes"]:
|
|
292
|
+
merged_classes.add(cls_name)
|
|
293
|
+
|
|
294
|
+
split_classes: set[str] = set(split_rules.keys())
|
|
295
|
+
|
|
296
|
+
# Map from source class -> list of unpack rules
|
|
297
|
+
unpack_source_classes: dict[str, list[dict[str, Any]]] = {}
|
|
298
|
+
# Track which params are unpacked per class
|
|
299
|
+
unpacked_params_per_class: dict[str, set[str]] = {}
|
|
300
|
+
for group_name, rule in unpack_rules.items():
|
|
301
|
+
for src_cls in rule["source_classes"]:
|
|
302
|
+
unpack_source_classes.setdefault(src_cls, []).append(
|
|
303
|
+
{"group_name": group_name, **rule}
|
|
304
|
+
)
|
|
305
|
+
unpacked_params_per_class.setdefault(src_cls, set()).update(rule["params"])
|
|
306
|
+
|
|
307
|
+
handled_classes: set[str] = set()
|
|
308
|
+
specs: list[SheetSpec] = []
|
|
309
|
+
|
|
310
|
+
# ---- 1. Process merge_classes rules ----
|
|
311
|
+
for group_name, rule in merge_rules.items():
|
|
312
|
+
classes_map: dict[str, str] = rule["classes"] # class_name -> direction_value
|
|
313
|
+
direction_col: str = rule["direction_column"]
|
|
314
|
+
class_names = list(classes_map.keys())
|
|
315
|
+
|
|
316
|
+
# Collect ALL param defs from the merged classes (they share the same params)
|
|
317
|
+
all_param_defs: dict[str, dict] = {}
|
|
318
|
+
for cls_name in class_names:
|
|
319
|
+
for pdef in db_contents.parameter_definitions.get(cls_name, []):
|
|
320
|
+
all_param_defs[pdef["name"]] = pdef
|
|
321
|
+
|
|
322
|
+
# Filter out unpacked params
|
|
323
|
+
unpacked: set[str] = set()
|
|
324
|
+
for cls_name in class_names:
|
|
325
|
+
unpacked.update(unpacked_params_per_class.get(cls_name, set()))
|
|
326
|
+
remaining_params = {
|
|
327
|
+
k: v for k, v in all_param_defs.items() if k not in unpacked
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Classify params by eligible layouts
|
|
331
|
+
layout_params: dict[str, list[str]] = {
|
|
332
|
+
"constant": [],
|
|
333
|
+
"periodic": [],
|
|
334
|
+
"timeseries": [],
|
|
335
|
+
}
|
|
336
|
+
for pname, pdef in remaining_params.items():
|
|
337
|
+
types = classify_param_types(
|
|
338
|
+
pdef.get("parameter_type_list"),
|
|
339
|
+
entity_class=class_names[0],
|
|
340
|
+
param_name=pname,
|
|
341
|
+
)
|
|
342
|
+
if "constant" in types:
|
|
343
|
+
layout_params["constant"].append(pname)
|
|
344
|
+
if "periodic" in types:
|
|
345
|
+
layout_params["periodic"].append(pname)
|
|
346
|
+
if "timeseries" in types:
|
|
347
|
+
layout_params["timeseries"].append(pname)
|
|
348
|
+
# Check for actual time-indexed values across merged classes
|
|
349
|
+
if "periodic" in types and "timeseries" not in types:
|
|
350
|
+
for cls_name in class_names:
|
|
351
|
+
if _has_time_indexed_values(db_contents, cls_name, pname):
|
|
352
|
+
layout_params["timeseries"].append(pname)
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
# Determine entity_columns from first class (all merged should match)
|
|
356
|
+
dims = _get_dimension_names(db_contents, class_names[0])
|
|
357
|
+
entity_cols = list(dims) if dims else [class_names[0]]
|
|
358
|
+
|
|
359
|
+
# Determine suffix rules
|
|
360
|
+
has_layouts = [k for k in ("constant", "periodic", "timeseries") if layout_params[k]]
|
|
361
|
+
use_suffix = len(has_layouts) > 1
|
|
362
|
+
|
|
363
|
+
direction_map = dict(classes_map)
|
|
364
|
+
layout_to_suffix = {"constant": "_c", "periodic": "_p", "timeseries": "_t"}
|
|
365
|
+
|
|
366
|
+
for layout_key in ("constant", "periodic", "timeseries"):
|
|
367
|
+
params = layout_params[layout_key]
|
|
368
|
+
if not params:
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
suffix = layout_to_suffix[layout_key] if use_suffix else ""
|
|
372
|
+
if not use_suffix and layout_key == "constant":
|
|
373
|
+
if _sheet_name_in_order(f"{group_name}_c", sheet_order):
|
|
374
|
+
suffix = "_c"
|
|
375
|
+
|
|
376
|
+
sheet_nm = f"{group_name}{suffix}"
|
|
377
|
+
has_ea = layout_key == "constant" and any(
|
|
378
|
+
c in ea_classes for c in class_names
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
spec = SheetSpec(
|
|
382
|
+
sheet_name=sheet_nm,
|
|
383
|
+
layout=layout_key,
|
|
384
|
+
entity_classes=class_names,
|
|
385
|
+
entity_columns=entity_cols,
|
|
386
|
+
direction_column=direction_col,
|
|
387
|
+
direction_map=direction_map,
|
|
388
|
+
has_entity_alternative=has_ea,
|
|
389
|
+
)
|
|
390
|
+
if layout_key == "periodic":
|
|
391
|
+
spec.scalar_params_on_constant_sibling = (
|
|
392
|
+
set(params) & set(layout_params["constant"])
|
|
393
|
+
)
|
|
394
|
+
spec._raw_params = params # type: ignore[attr-defined]
|
|
395
|
+
spec._primary_class = class_names[0] # type: ignore[attr-defined]
|
|
396
|
+
spec._all_param_defs = all_param_defs # type: ignore[attr-defined]
|
|
397
|
+
specs.append(spec)
|
|
398
|
+
|
|
399
|
+
for cls_name in class_names:
|
|
400
|
+
handled_classes.add(cls_name)
|
|
401
|
+
|
|
402
|
+
# ---- 2. Process split_params rules ----
|
|
403
|
+
for entity_class, sub_groups in split_rules.items():
|
|
404
|
+
class_pdefs = _get_param_defs_dict(db_contents, entity_class)
|
|
405
|
+
dims = _get_dimension_names(db_contents, entity_class)
|
|
406
|
+
entity_cols = list(dims) if dims else [entity_class]
|
|
407
|
+
|
|
408
|
+
for sub_name, sub_rule in sub_groups.items():
|
|
409
|
+
sub_params = sub_rule["params"]
|
|
410
|
+
# An explicit ``layout:`` key in the sub-rule overrides type
|
|
411
|
+
# inference. Used to route a nested-Map param that the schema
|
|
412
|
+
# declares as ``2d_map`` (which type inference would treat as
|
|
413
|
+
# periodic, where the Map value is dropped) onto the multi-index
|
|
414
|
+
# stochastic writer instead — e.g. timeset_s /
|
|
415
|
+
# representative_period_weights.
|
|
416
|
+
explicit_layout = sub_rule.get("layout")
|
|
417
|
+
# Determine layout from param types in the sub-group
|
|
418
|
+
has_periodic = False
|
|
419
|
+
has_stochastic = False
|
|
420
|
+
for pname in sub_params:
|
|
421
|
+
pdef = class_pdefs.get(pname)
|
|
422
|
+
if pdef:
|
|
423
|
+
type_list = pdef.get("parameter_type_list")
|
|
424
|
+
if type_list and ("1d_map" in type_list or "2d_map" in type_list):
|
|
425
|
+
has_periodic = True
|
|
426
|
+
if type_list and ("3d_map" in type_list or "4d_map" in type_list):
|
|
427
|
+
has_stochastic = True
|
|
428
|
+
|
|
429
|
+
if explicit_layout:
|
|
430
|
+
layout = explicit_layout
|
|
431
|
+
elif has_stochastic:
|
|
432
|
+
layout = "stochastic"
|
|
433
|
+
elif has_periodic:
|
|
434
|
+
layout = "periodic"
|
|
435
|
+
else:
|
|
436
|
+
layout = "constant"
|
|
437
|
+
|
|
438
|
+
spec = SheetSpec(
|
|
439
|
+
sheet_name=sub_name,
|
|
440
|
+
layout=layout,
|
|
441
|
+
entity_classes=[entity_class],
|
|
442
|
+
entity_columns=entity_cols,
|
|
443
|
+
has_entity_alternative=(layout == "constant" and entity_class in ea_classes),
|
|
444
|
+
)
|
|
445
|
+
# Only include params that actually exist in the DB schema
|
|
446
|
+
existing_params = [p for p in sub_params if p in class_pdefs]
|
|
447
|
+
spec._raw_params = existing_params # type: ignore[attr-defined]
|
|
448
|
+
spec._primary_class = entity_class # type: ignore[attr-defined]
|
|
449
|
+
spec._all_param_defs = class_pdefs # type: ignore[attr-defined]
|
|
450
|
+
specs.append(spec)
|
|
451
|
+
|
|
452
|
+
handled_classes.add(entity_class)
|
|
453
|
+
|
|
454
|
+
# ---- 2b. Create solve_period_period sheet for nested Map params ----
|
|
455
|
+
# Look for a spec named 'solve_period' and clone it for nested Maps
|
|
456
|
+
for spec in list(specs):
|
|
457
|
+
if spec.sheet_name == "solve_period":
|
|
458
|
+
nested_spec = SheetSpec(
|
|
459
|
+
sheet_name="solve_period_period",
|
|
460
|
+
layout="nested_periodic",
|
|
461
|
+
entity_classes=list(spec.entity_classes),
|
|
462
|
+
entity_columns=list(spec.entity_columns),
|
|
463
|
+
has_entity_alternative=False,
|
|
464
|
+
)
|
|
465
|
+
raw = getattr(spec, "_raw_params", None)
|
|
466
|
+
if raw is not None:
|
|
467
|
+
nested_spec._raw_params = list(raw) # type: ignore[attr-defined]
|
|
468
|
+
pclass = getattr(spec, "_primary_class", None)
|
|
469
|
+
if pclass is not None:
|
|
470
|
+
nested_spec._primary_class = pclass # type: ignore[attr-defined]
|
|
471
|
+
all_pdefs = getattr(spec, "_all_param_defs", None)
|
|
472
|
+
if all_pdefs is not None:
|
|
473
|
+
nested_spec._all_param_defs = dict(all_pdefs) # type: ignore[attr-defined]
|
|
474
|
+
specs.append(nested_spec)
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
# ---- 3. Process unpack_map_params rules ----
|
|
478
|
+
for group_name, rule in unpack_rules.items():
|
|
479
|
+
source_classes: list[str] = rule["source_classes"]
|
|
480
|
+
unpack_params: list[str] = rule["params"]
|
|
481
|
+
index_column: str = rule["index_column"]
|
|
482
|
+
direction_col_name: str | None = rule.get("direction_column")
|
|
483
|
+
|
|
484
|
+
# Get entity_columns from first source class
|
|
485
|
+
dims = _get_dimension_names(db_contents, source_classes[0])
|
|
486
|
+
entity_cols = list(dims) if dims else [source_classes[0]]
|
|
487
|
+
|
|
488
|
+
# Build direction_map if source classes are merged
|
|
489
|
+
dir_map: dict[str, str] = {}
|
|
490
|
+
if direction_col_name:
|
|
491
|
+
for _mg_name, mg_rule in merge_rules.items():
|
|
492
|
+
for cls_name, dir_val in mg_rule["classes"].items():
|
|
493
|
+
if cls_name in source_classes:
|
|
494
|
+
dir_map[cls_name] = dir_val
|
|
495
|
+
|
|
496
|
+
# Collect param defs for descriptions
|
|
497
|
+
all_pdefs: dict[str, dict] = {}
|
|
498
|
+
for src_cls in source_classes:
|
|
499
|
+
for pdef in db_contents.parameter_definitions.get(src_cls, []):
|
|
500
|
+
if pdef["name"] in unpack_params:
|
|
501
|
+
all_pdefs[pdef["name"]] = pdef
|
|
502
|
+
|
|
503
|
+
sheet_nm = f"{group_name}_c"
|
|
504
|
+
spec = SheetSpec(
|
|
505
|
+
sheet_name=sheet_nm,
|
|
506
|
+
layout="constant",
|
|
507
|
+
entity_classes=source_classes,
|
|
508
|
+
entity_columns=entity_cols,
|
|
509
|
+
parameter_names=unpack_params,
|
|
510
|
+
direction_column=direction_col_name,
|
|
511
|
+
direction_map=dir_map,
|
|
512
|
+
unpack_index_column=index_column,
|
|
513
|
+
has_entity_alternative=False,
|
|
514
|
+
)
|
|
515
|
+
spec._all_param_defs = all_pdefs # type: ignore[attr-defined]
|
|
516
|
+
specs.append(spec)
|
|
517
|
+
|
|
518
|
+
# ---- 4. Process remaining entity classes (generic path) ----
|
|
519
|
+
for ec in db_contents.entity_classes:
|
|
520
|
+
cls_name = ec["name"]
|
|
521
|
+
if cls_name in handled_classes:
|
|
522
|
+
continue
|
|
523
|
+
if cls_name in merged_classes:
|
|
524
|
+
continue
|
|
525
|
+
if cls_name in split_classes:
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
dims = ec["dimension_name_list"]
|
|
529
|
+
entity_cols = list(dims) if dims else [cls_name]
|
|
530
|
+
pdefs = db_contents.parameter_definitions.get(cls_name, [])
|
|
531
|
+
|
|
532
|
+
# Filter out params that went to unpack rules
|
|
533
|
+
unpacked = unpacked_params_per_class.get(cls_name, set())
|
|
534
|
+
remaining_pdefs = [p for p in pdefs if p["name"] not in unpacked]
|
|
535
|
+
remaining_param_names = [p["name"] for p in remaining_pdefs]
|
|
536
|
+
remaining_param_defs = {p["name"]: p for p in remaining_pdefs}
|
|
537
|
+
|
|
538
|
+
base_name = _make_base_name(cls_name, drop_dims)
|
|
539
|
+
|
|
540
|
+
if not remaining_param_names:
|
|
541
|
+
# No params remaining: either link sheet or skip
|
|
542
|
+
if not dims:
|
|
543
|
+
# Zero-dim class with no params — skip (upDown, reserve, etc.)
|
|
544
|
+
continue
|
|
545
|
+
# Multi-dim relationship class -> link sheet
|
|
546
|
+
spec = SheetSpec(
|
|
547
|
+
sheet_name=base_name,
|
|
548
|
+
layout="link",
|
|
549
|
+
entity_classes=[cls_name],
|
|
550
|
+
entity_columns=entity_cols,
|
|
551
|
+
has_entity_alternative=False,
|
|
552
|
+
)
|
|
553
|
+
specs.append(spec)
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
# Has parameters: classify by layout
|
|
557
|
+
layout_params: dict[str, list[str]] = {
|
|
558
|
+
"constant": [],
|
|
559
|
+
"periodic": [],
|
|
560
|
+
"timeseries": [],
|
|
561
|
+
"stochastic": [],
|
|
562
|
+
"ladder": [],
|
|
563
|
+
}
|
|
564
|
+
for pdef in remaining_pdefs:
|
|
565
|
+
pname = pdef["name"]
|
|
566
|
+
types = classify_param_types(
|
|
567
|
+
pdef.get("parameter_type_list"),
|
|
568
|
+
entity_class=cls_name,
|
|
569
|
+
param_name=pname,
|
|
570
|
+
)
|
|
571
|
+
if "ladder" in types:
|
|
572
|
+
# Facet-leaf params (price_ladder_*) get their own
|
|
573
|
+
# dedicated sheet per param; never co-occur with
|
|
574
|
+
# constant/periodic/timeseries/stochastic.
|
|
575
|
+
layout_params["ladder"].append(pname)
|
|
576
|
+
continue
|
|
577
|
+
if "constant" in types:
|
|
578
|
+
layout_params["constant"].append(pname)
|
|
579
|
+
if "periodic" in types:
|
|
580
|
+
layout_params["periodic"].append(pname)
|
|
581
|
+
if "timeseries" in types:
|
|
582
|
+
layout_params["timeseries"].append(pname)
|
|
583
|
+
if "stochastic" in types:
|
|
584
|
+
layout_params["stochastic"].append(pname)
|
|
585
|
+
# For params with periodic capability but no 3d_map: check actual DB values
|
|
586
|
+
if "periodic" in types and "timeseries" not in types:
|
|
587
|
+
if _has_time_indexed_values(db_contents, cls_name, pname):
|
|
588
|
+
layout_params["timeseries"].append(pname)
|
|
589
|
+
|
|
590
|
+
has_layouts = [k for k in ("constant", "periodic", "timeseries") if layout_params[k]]
|
|
591
|
+
|
|
592
|
+
if not has_layouts:
|
|
593
|
+
# Fallback: treat as constant
|
|
594
|
+
has_layouts = ["constant"]
|
|
595
|
+
layout_params["constant"] = remaining_param_names
|
|
596
|
+
|
|
597
|
+
# Apply rename_sheets
|
|
598
|
+
display_name = rename_sheets.get(base_name, base_name)
|
|
599
|
+
|
|
600
|
+
use_suffix = len(has_layouts) > 1
|
|
601
|
+
layout_to_suffix = {"constant": "_c", "periodic": "_p", "timeseries": "_t"}
|
|
602
|
+
|
|
603
|
+
for layout_key in ("constant", "periodic", "timeseries"):
|
|
604
|
+
params = layout_params[layout_key]
|
|
605
|
+
if not params:
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
suffix = layout_to_suffix[layout_key] if use_suffix else ""
|
|
609
|
+
# When only one layout, check sheet_order for suffix convention
|
|
610
|
+
if not use_suffix:
|
|
611
|
+
candidate = f"{display_name}{layout_to_suffix[layout_key]}"
|
|
612
|
+
if _sheet_name_in_order(candidate, sheet_order):
|
|
613
|
+
suffix = layout_to_suffix[layout_key]
|
|
614
|
+
|
|
615
|
+
sheet_nm = f"{display_name}{suffix}"
|
|
616
|
+
has_ea = layout_key == "constant" and cls_name in ea_classes
|
|
617
|
+
|
|
618
|
+
spec = SheetSpec(
|
|
619
|
+
sheet_name=sheet_nm,
|
|
620
|
+
layout=layout_key,
|
|
621
|
+
entity_classes=[cls_name],
|
|
622
|
+
entity_columns=entity_cols,
|
|
623
|
+
has_entity_alternative=has_ea,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Apply add_entity_elements rules (only to constant sheets)
|
|
627
|
+
if cls_name in element_rules and layout_key == "constant":
|
|
628
|
+
erule = element_rules[cls_name]
|
|
629
|
+
spec.extra_entity_columns = erule["columns"]
|
|
630
|
+
spec.extra_entity_class = erule["from_class"]
|
|
631
|
+
|
|
632
|
+
# Mark params that also have a constant sibling sheet so the
|
|
633
|
+
# periodic writer skips their scalar-only rows (avoids the
|
|
634
|
+
# node.existing-style round-trip duplication).
|
|
635
|
+
if layout_key == "periodic":
|
|
636
|
+
spec.scalar_params_on_constant_sibling = (
|
|
637
|
+
set(params) & set(layout_params["constant"])
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
spec._raw_params = params # type: ignore[attr-defined]
|
|
641
|
+
spec._primary_class = cls_name # type: ignore[attr-defined]
|
|
642
|
+
spec._all_param_defs = remaining_param_defs # type: ignore[attr-defined]
|
|
643
|
+
specs.append(spec)
|
|
644
|
+
|
|
645
|
+
# Stochastic sheets: one per parameter
|
|
646
|
+
# Use _s when only one stochastic param, _s_paramname when multiple
|
|
647
|
+
n_stoch = len(layout_params["stochastic"])
|
|
648
|
+
for stoch_pname in layout_params["stochastic"]:
|
|
649
|
+
if n_stoch == 1:
|
|
650
|
+
sheet_nm = f"{display_name}_s"
|
|
651
|
+
else:
|
|
652
|
+
sheet_nm = f"{display_name}_s_{stoch_pname}"
|
|
653
|
+
stoch_spec = SheetSpec(
|
|
654
|
+
sheet_name=sheet_nm,
|
|
655
|
+
layout="stochastic",
|
|
656
|
+
entity_classes=[cls_name],
|
|
657
|
+
entity_columns=entity_cols,
|
|
658
|
+
has_entity_alternative=False,
|
|
659
|
+
)
|
|
660
|
+
stoch_spec._raw_params = [stoch_pname] # type: ignore[attr-defined]
|
|
661
|
+
stoch_spec._primary_class = cls_name # type: ignore[attr-defined]
|
|
662
|
+
stoch_spec._all_param_defs = remaining_param_defs # type: ignore[attr-defined]
|
|
663
|
+
specs.append(stoch_spec)
|
|
664
|
+
|
|
665
|
+
# Ladder sheets: one per facet-leaf param. Sheet name is the
|
|
666
|
+
# DB param name verbatim (e.g. ``price_ladder_cumulative``) — the
|
|
667
|
+
# ``ladder`` writer renders the price/quantity facet as two real
|
|
668
|
+
# Excel parameter columns sharing the entity/tier/[period] index
|
|
669
|
+
# columns.
|
|
670
|
+
for ladder_pname in layout_params["ladder"]:
|
|
671
|
+
ladder_spec = SheetSpec(
|
|
672
|
+
sheet_name=ladder_pname,
|
|
673
|
+
layout="ladder",
|
|
674
|
+
entity_classes=[cls_name],
|
|
675
|
+
entity_columns=entity_cols,
|
|
676
|
+
has_entity_alternative=False,
|
|
677
|
+
)
|
|
678
|
+
ladder_spec._raw_params = [ladder_pname] # type: ignore[attr-defined]
|
|
679
|
+
ladder_spec._primary_class = cls_name # type: ignore[attr-defined]
|
|
680
|
+
ladder_spec._all_param_defs = remaining_param_defs # type: ignore[attr-defined]
|
|
681
|
+
specs.append(ladder_spec)
|
|
682
|
+
|
|
683
|
+
# ---- 5. Add special sheets ----
|
|
684
|
+
specs.append(SheetSpec(
|
|
685
|
+
sheet_name="scenario",
|
|
686
|
+
layout="scenario",
|
|
687
|
+
entity_classes=[],
|
|
688
|
+
entity_columns=[],
|
|
689
|
+
has_entity_alternative=False,
|
|
690
|
+
))
|
|
691
|
+
specs.append(SheetSpec(
|
|
692
|
+
sheet_name="navigate",
|
|
693
|
+
layout="navigate",
|
|
694
|
+
entity_classes=[],
|
|
695
|
+
entity_columns=[],
|
|
696
|
+
has_entity_alternative=False,
|
|
697
|
+
))
|
|
698
|
+
specs.append(SheetSpec(
|
|
699
|
+
sheet_name="version",
|
|
700
|
+
layout="version",
|
|
701
|
+
entity_classes=[],
|
|
702
|
+
entity_columns=[],
|
|
703
|
+
has_entity_alternative=False,
|
|
704
|
+
))
|
|
705
|
+
|
|
706
|
+
# ---- 6. Populate parameter_names using order_parameters ----
|
|
707
|
+
for spec in specs:
|
|
708
|
+
raw_params: list[str] | None = getattr(spec, "_raw_params", None)
|
|
709
|
+
if raw_params is None:
|
|
710
|
+
continue
|
|
711
|
+
primary_class: str = getattr(spec, "_primary_class", "")
|
|
712
|
+
spec.parameter_names = order_parameters(raw_params, primary_class, db_contents)
|
|
713
|
+
|
|
714
|
+
# Move pre-EA params out of parameter_names into pre_ea_params
|
|
715
|
+
if spec.has_entity_alternative and spec.layout == "constant":
|
|
716
|
+
for cls in spec.entity_classes:
|
|
717
|
+
pre_ea = pre_ea_rules.get(cls, [])
|
|
718
|
+
if pre_ea:
|
|
719
|
+
spec.pre_ea_params = [p for p in pre_ea if p in spec.parameter_names]
|
|
720
|
+
spec.parameter_names = [
|
|
721
|
+
p for p in spec.parameter_names if p not in spec.pre_ea_params
|
|
722
|
+
]
|
|
723
|
+
break
|
|
724
|
+
|
|
725
|
+
# ---- 7. Populate descriptions ----
|
|
726
|
+
for spec in specs:
|
|
727
|
+
all_pdefs_attr: dict[str, dict] | None = getattr(spec, "_all_param_defs", None)
|
|
728
|
+
if all_pdefs_attr is None:
|
|
729
|
+
continue
|
|
730
|
+
for pname in list(spec.parameter_names) + spec.pre_ea_params:
|
|
731
|
+
pdef = all_pdefs_attr.get(pname)
|
|
732
|
+
if pdef and pdef.get("description"):
|
|
733
|
+
spec.descriptions[pname] = pdef["description"]
|
|
734
|
+
|
|
735
|
+
# ---- 7b. Set index_name_default from layout type ----
|
|
736
|
+
for spec in specs:
|
|
737
|
+
default = index_name_defaults.get(spec.layout)
|
|
738
|
+
if default:
|
|
739
|
+
spec.index_name_default = default
|
|
740
|
+
|
|
741
|
+
# ---- 8. Clean up temporary attributes ----
|
|
742
|
+
for spec in specs:
|
|
743
|
+
for attr in ("_raw_params", "_primary_class", "_all_param_defs"):
|
|
744
|
+
if hasattr(spec, attr):
|
|
745
|
+
delattr(spec, attr)
|
|
746
|
+
|
|
747
|
+
# ---- 9. Sort by sheet_order ----
|
|
748
|
+
order_map = {name: i for i, name in enumerate(sheet_order)}
|
|
749
|
+
max_order = len(sheet_order)
|
|
750
|
+
|
|
751
|
+
def sort_key(spec: SheetSpec) -> tuple[int, str]:
|
|
752
|
+
if spec.sheet_name in order_map:
|
|
753
|
+
return (order_map[spec.sheet_name], spec.sheet_name)
|
|
754
|
+
return (max_order, spec.sheet_name)
|
|
755
|
+
|
|
756
|
+
specs.sort(key=sort_key)
|
|
757
|
+
|
|
758
|
+
# ---- 10. Optional parameter-group filter ----
|
|
759
|
+
if include_groups is not None:
|
|
760
|
+
always_include_cfg: dict[str, list[str]] = settings.get(
|
|
761
|
+
"always_include_with_groups", {}
|
|
762
|
+
) or {}
|
|
763
|
+
specs = _filter_specs_by_groups(
|
|
764
|
+
specs, db_contents, include_groups, always_include_cfg,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
return specs
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _filter_specs_by_groups(
|
|
771
|
+
specs: list[SheetSpec],
|
|
772
|
+
db_contents: DatabaseContents,
|
|
773
|
+
include_groups: set[str],
|
|
774
|
+
always_include_with_groups: dict[str, list[str]],
|
|
775
|
+
) -> list[SheetSpec]:
|
|
776
|
+
"""Filter specs so only parameters belonging to *include_groups* survive.
|
|
777
|
+
|
|
778
|
+
- Drops names from ``parameter_names`` and ``pre_ea_params`` whose
|
|
779
|
+
``(class, name)`` group (per ``db_contents.param_to_group``) is not
|
|
780
|
+
in ``include_groups``. For multi-class specs (merge / unpack), a
|
|
781
|
+
column is kept if ANY class places it in an allowed group.
|
|
782
|
+
- Drops any data-layout spec that ends up with no surviving columns,
|
|
783
|
+
unless its sheet name is listed under a selected group in
|
|
784
|
+
``always_include_with_groups``.
|
|
785
|
+
- Keeps ``navigate``, ``scenario``, ``version`` untouched.
|
|
786
|
+
- Keeps ``link`` sheets only when every dimension class still owns a
|
|
787
|
+
retained data sheet (or the link sheet itself is force-kept).
|
|
788
|
+
"""
|
|
789
|
+
always_include: set[str] = set()
|
|
790
|
+
for g in include_groups:
|
|
791
|
+
for sheet_name in always_include_with_groups.get(g, []):
|
|
792
|
+
always_include.add(sheet_name)
|
|
793
|
+
|
|
794
|
+
def _allowed(class_names: list[str], param_name: str) -> bool:
|
|
795
|
+
for c in class_names:
|
|
796
|
+
g = db_contents.param_to_group.get((c, param_name))
|
|
797
|
+
if g is not None and g in include_groups:
|
|
798
|
+
return True
|
|
799
|
+
return False
|
|
800
|
+
|
|
801
|
+
data_layouts = {
|
|
802
|
+
"constant", "periodic", "timeseries", "stochastic",
|
|
803
|
+
"nested_periodic", "ladder",
|
|
804
|
+
}
|
|
805
|
+
structural_layouts = {"navigate", "scenario", "version"}
|
|
806
|
+
|
|
807
|
+
# Pass 1: filter data-layout specs; defer link sheets to pass 2.
|
|
808
|
+
kept: list[SheetSpec] = []
|
|
809
|
+
deferred_links: list[SheetSpec] = []
|
|
810
|
+
retained_classes: set[str] = set()
|
|
811
|
+
|
|
812
|
+
for spec in specs:
|
|
813
|
+
if spec.layout in structural_layouts:
|
|
814
|
+
kept.append(spec)
|
|
815
|
+
continue
|
|
816
|
+
if spec.layout == "link":
|
|
817
|
+
deferred_links.append(spec)
|
|
818
|
+
continue
|
|
819
|
+
if spec.layout in data_layouts:
|
|
820
|
+
spec.parameter_names = [
|
|
821
|
+
p for p in spec.parameter_names
|
|
822
|
+
if _allowed(spec.entity_classes, p)
|
|
823
|
+
]
|
|
824
|
+
spec.pre_ea_params = [
|
|
825
|
+
p for p in spec.pre_ea_params
|
|
826
|
+
if _allowed(spec.entity_classes, p)
|
|
827
|
+
]
|
|
828
|
+
if (
|
|
829
|
+
spec.parameter_names
|
|
830
|
+
or spec.pre_ea_params
|
|
831
|
+
or spec.sheet_name in always_include
|
|
832
|
+
):
|
|
833
|
+
kept.append(spec)
|
|
834
|
+
retained_classes.update(spec.entity_classes)
|
|
835
|
+
continue
|
|
836
|
+
# Unknown layouts: keep as-is (defensive).
|
|
837
|
+
kept.append(spec)
|
|
838
|
+
|
|
839
|
+
# Pass 2: link sheets are kept when:
|
|
840
|
+
# - the sheet is in always_include, or
|
|
841
|
+
# - the link's own entity class has parameter definitions in the
|
|
842
|
+
# schema AND those produced a retained data sheet (i.e. the
|
|
843
|
+
# class is in retained_classes), or
|
|
844
|
+
# - the link's own entity class has no parameter definitions at
|
|
845
|
+
# all (pure topology) AND every dimension class still has a
|
|
846
|
+
# retained data sheet.
|
|
847
|
+
#
|
|
848
|
+
# The first sub-rule is what drops e.g. ``connection_node`` when its
|
|
849
|
+
# only schema parameters live in a non-selected group such as
|
|
850
|
+
# ``constraint``. The third covers pure relationships like
|
|
851
|
+
# ``commodity_node`` and ``group_node`` — they survive while their
|
|
852
|
+
# endpoint classes own data, and disappear together with the
|
|
853
|
+
# endpoint when the endpoint's params are all filtered out.
|
|
854
|
+
class_dims: dict[str, tuple] = {
|
|
855
|
+
ec["name"]: ec["dimension_name_list"] for ec in db_contents.entity_classes
|
|
856
|
+
}
|
|
857
|
+
for spec in deferred_links:
|
|
858
|
+
if spec.sheet_name in always_include:
|
|
859
|
+
kept.append(spec)
|
|
860
|
+
continue
|
|
861
|
+
cls = spec.entity_classes[0] if spec.entity_classes else None
|
|
862
|
+
if cls is None:
|
|
863
|
+
continue
|
|
864
|
+
has_own_params = bool(db_contents.parameter_definitions.get(cls))
|
|
865
|
+
if has_own_params:
|
|
866
|
+
if cls not in retained_classes:
|
|
867
|
+
continue
|
|
868
|
+
else:
|
|
869
|
+
dims = class_dims.get(cls, ())
|
|
870
|
+
if not dims or not all(d in retained_classes for d in dims):
|
|
871
|
+
continue
|
|
872
|
+
kept.append(spec)
|
|
873
|
+
|
|
874
|
+
# Preserve the prior sort order from build_sheet_specs (specs was already
|
|
875
|
+
# sorted before this filter ran). Re-running the same key keeps it stable.
|
|
876
|
+
return kept
|