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,1001 @@
|
|
|
1
|
+
"""Solve-level configuration loaded from a SpineDB scenario.
|
|
2
|
+
|
|
3
|
+
Architecture notes
|
|
4
|
+
------------------
|
|
5
|
+
|
|
6
|
+
* Every downstream module reads ``state.solve.<dict>`` keys produced
|
|
7
|
+
here; the keys must match the canonical names (including the
|
|
8
|
+
``np.str_`` leak through Spine ``Map.indexes``, the stringified-
|
|
9
|
+
float ``rolling_times`` entries, and the lockstep mutation in
|
|
10
|
+
:meth:`SolveConfig.duplicate_solve`).
|
|
11
|
+
* The factory uses :class:`spinedb_api.DatabaseMapping` directly.
|
|
12
|
+
* Reads ``solve``, ``model``, ``unit`` parameter classes only.
|
|
13
|
+
Loading order matters (``make_roll_counter`` →
|
|
14
|
+
``get_period_timesets`` → 4× ``periods_to_tuples``) because each
|
|
15
|
+
may call :meth:`duplicate_solve`, which mutates 19 sibling
|
|
16
|
+
defaultdicts in lockstep.
|
|
17
|
+
* The DB schema is assumed to be v50+. v50 moved
|
|
18
|
+
``new_stepduration`` from ``timeset`` to ``solve``;
|
|
19
|
+
``update_flextool/db_migration.py`` handles upgrades for older DBs
|
|
20
|
+
before this loader runs.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
import spinedb_api as api
|
|
31
|
+
|
|
32
|
+
from flextool.engine_polars._solve_state import FlexToolConfigError
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from spinedb_api import DatabaseMapping
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# DB-reader helpers used by the solve-config factory.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DictMode(Enum):
|
|
44
|
+
"""Output container shape for :func:`params_to_dict`."""
|
|
45
|
+
|
|
46
|
+
DICT = "dict"
|
|
47
|
+
DEFAULTDICT = "defaultdict"
|
|
48
|
+
LIST = "list"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_single_entities(db: "DatabaseMapping", entity_class_name: str) -> list[str]:
|
|
52
|
+
"""Return entity names for a single-dimension entity class."""
|
|
53
|
+
return [
|
|
54
|
+
entity["entity_byname"][0]
|
|
55
|
+
for entity in db.find_entities(entity_class_name=entity_class_name)
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def params_to_dict(
|
|
60
|
+
db: "DatabaseMapping",
|
|
61
|
+
cl: str,
|
|
62
|
+
par: str,
|
|
63
|
+
mode: DictMode,
|
|
64
|
+
str_to_list: bool = False,
|
|
65
|
+
) -> dict | defaultdict | list:
|
|
66
|
+
"""Read parameter values of *par* on entity class *cl*.
|
|
67
|
+
|
|
68
|
+
Value-type dispatch:
|
|
69
|
+
|
|
70
|
+
* Map of float → list of (index, float) tuples.
|
|
71
|
+
* Map of str → list of (index, str) tuples.
|
|
72
|
+
* Map of Map → :func:`spinedb_api.convert_map_to_table` flattening.
|
|
73
|
+
* Array → ``param_value.values`` (the raw numpy-string-typed list).
|
|
74
|
+
* Float scalar → :class:`str` of the float (preserved verbatim).
|
|
75
|
+
* String scalar → either the bare string or ``[string]`` when
|
|
76
|
+
*str_to_list* is set.
|
|
77
|
+
"""
|
|
78
|
+
all_params = db.find_parameter_values(
|
|
79
|
+
entity_class_name=cl, parameter_definition_name=par
|
|
80
|
+
)
|
|
81
|
+
result: dict | defaultdict | list
|
|
82
|
+
if mode == DictMode.DEFAULTDICT:
|
|
83
|
+
result = defaultdict(list)
|
|
84
|
+
elif mode == DictMode.DICT:
|
|
85
|
+
result = dict()
|
|
86
|
+
elif mode == DictMode.LIST:
|
|
87
|
+
result = []
|
|
88
|
+
else: # pragma: no cover — exhaustive enum
|
|
89
|
+
raise ValueError(f"Unknown DictMode: {mode!r}")
|
|
90
|
+
for param in all_params:
|
|
91
|
+
param_value = api.from_database(param["value"], param["type"])
|
|
92
|
+
if mode in (DictMode.DEFAULTDICT, DictMode.DICT):
|
|
93
|
+
if isinstance(param_value, api.Map):
|
|
94
|
+
if isinstance(param_value.values[0], float):
|
|
95
|
+
result[param["entity_name"]] = list(
|
|
96
|
+
zip(
|
|
97
|
+
list(param_value.indexes),
|
|
98
|
+
list(map(float, param_value.values)),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
elif isinstance(param_value.values[0], str):
|
|
102
|
+
result[param["entity_name"]] = list(
|
|
103
|
+
zip(list(param_value.indexes), param_value.values)
|
|
104
|
+
)
|
|
105
|
+
elif isinstance(param_value.values[0], api.Map):
|
|
106
|
+
result[param["entity_name"]] = api.convert_map_to_table(
|
|
107
|
+
param_value
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
raise TypeError(
|
|
111
|
+
"params_to_dict function does not handle other "
|
|
112
|
+
"values than floats and strings"
|
|
113
|
+
)
|
|
114
|
+
elif isinstance(param_value, api.Array):
|
|
115
|
+
result[param["entity_name"]] = param_value.values
|
|
116
|
+
elif isinstance(param_value, float):
|
|
117
|
+
result[param["entity_name"]] = str(param_value)
|
|
118
|
+
elif isinstance(param_value, str):
|
|
119
|
+
if str_to_list:
|
|
120
|
+
result[param["entity_name"]] = [param_value]
|
|
121
|
+
else:
|
|
122
|
+
result[param["entity_name"]] = param_value
|
|
123
|
+
elif mode == DictMode.LIST:
|
|
124
|
+
if isinstance(param_value, (float, str)):
|
|
125
|
+
result.append([param["entity_name"], param_value]) # type: ignore[union-attr]
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Solver-config dataclasses
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class HiGHSConfig:
|
|
136
|
+
"""HiGHS solver option overrides — solve-level dispatch.
|
|
137
|
+
|
|
138
|
+
Each field is keyed by solve name; values are the raw strings stored
|
|
139
|
+
in Spine (e.g. ``"on"`` / ``"off"`` / ``"choose"``). ``HiGHSProblem``
|
|
140
|
+
converts them at solve time.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
presolve: dict[str, str]
|
|
144
|
+
method: dict[str, str]
|
|
145
|
+
parallel: dict[str, str]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class SolverSettings:
|
|
150
|
+
"""Solver selection + invocation settings, keyed by solve name.
|
|
151
|
+
|
|
152
|
+
*arguments* is the per-solve HiGHS option overrides Map authored
|
|
153
|
+
on the ``solver_arguments`` parameter (1d-map of HiGHS option name
|
|
154
|
+
→ value). Read into the effective-options resolver via
|
|
155
|
+
:func:`flextool.engine_polars._solver_dispatch._resolve_effective_highs_options`
|
|
156
|
+
where it is overlaid on top of ``solver_config/highs.opt`` and
|
|
157
|
+
below the CLI overrides. Empty when no solve authored an entry.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
solvers: dict[str, str]
|
|
161
|
+
precommand: dict[str, str]
|
|
162
|
+
arguments: dict[str, dict[str, str]]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class SolverConfig:
|
|
167
|
+
"""Per-solve multi-solver dispatch configuration (v52 schema).
|
|
168
|
+
|
|
169
|
+
Holds the seven solver-selection parameters introduced by the
|
|
170
|
+
v52 migration (see ``specs/flextool-multi-solver-handoff.md``
|
|
171
|
+
Step 1). Defaults match the v52 parameter definition defaults so
|
|
172
|
+
a solve that does not author any ``solver_*`` parameter still
|
|
173
|
+
constructs a meaningful :class:`SolverConfig` (HiGHS via the
|
|
174
|
+
direct in-process API, no convenience knobs set, "normal" log).
|
|
175
|
+
|
|
176
|
+
Fields
|
|
177
|
+
------
|
|
178
|
+
name
|
|
179
|
+
Solver name; one of polar-high's ``available_solvers``
|
|
180
|
+
("highs", "gurobi", "cplex", "xpress", "copt"). Default
|
|
181
|
+
``"highs"``.
|
|
182
|
+
io_api
|
|
183
|
+
"direct" (in-process binding, fastest), "mps" or "lp" (file
|
|
184
|
+
fallback). Default ``"direct"``.
|
|
185
|
+
options
|
|
186
|
+
Free-form key→value dict forwarded raw to the solver. Empty
|
|
187
|
+
by default; user populates via the ``solver_arguments``
|
|
188
|
+
1d-map parameter (Batch C.2: the legacy ``solver_options``
|
|
189
|
+
Map was folded into ``solver_arguments`` and removed).
|
|
190
|
+
time_limit
|
|
191
|
+
Wall-clock seconds; ``None`` means no limit. Translated to
|
|
192
|
+
each solver's native parameter name by
|
|
193
|
+
:func:`flextool.engine_polars._solver_dispatch.build_solver_options`.
|
|
194
|
+
mip_gap
|
|
195
|
+
Relative MIP gap; ``None`` means solver default.
|
|
196
|
+
threads
|
|
197
|
+
Worker thread cap; ``None`` means solver default.
|
|
198
|
+
log_level
|
|
199
|
+
``"silent"`` / ``"normal"`` / ``"verbose"``. Default
|
|
200
|
+
``"normal"``.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
name: str = "highs"
|
|
204
|
+
io_api: str = "direct"
|
|
205
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
206
|
+
time_limit: float | None = None
|
|
207
|
+
mip_gap: float | None = None
|
|
208
|
+
threads: int | None = None
|
|
209
|
+
log_level: str = "normal"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
# SolveConfig — main container
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class SolveConfig:
|
|
218
|
+
"""All solve-level parameters and mutable tracking state.
|
|
219
|
+
|
|
220
|
+
See ``audit/solve_orchestration_plan.md §1.3`` for the per-field
|
|
221
|
+
contract; downstream modules read these dicts directly so the
|
|
222
|
+
container shape (``defaultdict(list)`` vs plain ``dict``), the
|
|
223
|
+
keys, and the value types must match flextool exactly.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
model: list,
|
|
229
|
+
model_solve: defaultdict,
|
|
230
|
+
solve_modes: dict,
|
|
231
|
+
rolling_times: defaultdict,
|
|
232
|
+
highs: HiGHSConfig,
|
|
233
|
+
solver_settings: SolverSettings,
|
|
234
|
+
solve_period_years_represented: defaultdict,
|
|
235
|
+
hole_multipliers: defaultdict,
|
|
236
|
+
contains_solves: defaultdict,
|
|
237
|
+
stochastic_branches: defaultdict,
|
|
238
|
+
periods_available: dict,
|
|
239
|
+
delay_durations: dict,
|
|
240
|
+
logger: logging.Logger,
|
|
241
|
+
use_row_scaling: dict | None = None,
|
|
242
|
+
scale_the_objective: dict | None = None,
|
|
243
|
+
user_bound_scale: dict | None = None,
|
|
244
|
+
solver_configs: dict[str, "SolverConfig"] | None = None,
|
|
245
|
+
decomposition: dict | None = None,
|
|
246
|
+
benders_max_iter: dict | None = None,
|
|
247
|
+
benders_tolerance: dict | None = None,
|
|
248
|
+
benders_in_out_weight: dict | None = None,
|
|
249
|
+
scaling: dict | None = None,
|
|
250
|
+
) -> None:
|
|
251
|
+
# Base fields (read directly from DB in load_from_db).
|
|
252
|
+
self.model = model
|
|
253
|
+
self.model_solve = model_solve
|
|
254
|
+
self.solve_modes = solve_modes
|
|
255
|
+
self.rolling_times = rolling_times
|
|
256
|
+
self.highs = highs
|
|
257
|
+
self.solver_settings = solver_settings
|
|
258
|
+
self.solve_period_years_represented = solve_period_years_represented
|
|
259
|
+
self.hole_multipliers = hole_multipliers
|
|
260
|
+
self.contains_solves = contains_solves
|
|
261
|
+
self.stochastic_branches = stochastic_branches
|
|
262
|
+
self.periods_available = periods_available
|
|
263
|
+
self.delay_durations = delay_durations
|
|
264
|
+
self.logger = logger
|
|
265
|
+
# solve-name → "yes"/"no" string from the DB (default off everywhere).
|
|
266
|
+
self.use_row_scaling: dict = (
|
|
267
|
+
use_row_scaling if use_row_scaling is not None else {}
|
|
268
|
+
)
|
|
269
|
+
self.scale_the_objective: dict = (
|
|
270
|
+
scale_the_objective if scale_the_objective is not None else {}
|
|
271
|
+
)
|
|
272
|
+
# solve-name → integer user_bound_scale override. When set, overrides
|
|
273
|
+
# polar-high's stream-time auto-pick
|
|
274
|
+
# (``polar_high.engine._recommend_user_bound_scale``). Pass the
|
|
275
|
+
# value HiGHS recommends in its "user-scaled problem has some
|
|
276
|
+
# excessively large row bounds — Consider setting the user_bound_scale
|
|
277
|
+
# option to <N>" warning for the most reliable result.
|
|
278
|
+
self.user_bound_scale: dict = (
|
|
279
|
+
user_bound_scale if user_bound_scale is not None else {}
|
|
280
|
+
)
|
|
281
|
+
# v52 multi-solver dispatch — solve-name → :class:`SolverConfig`.
|
|
282
|
+
# Empty when no ``solver_*`` parameters are authored on any solve;
|
|
283
|
+
# callers fall back to ``SolverConfig()`` defaults (HiGHS/direct).
|
|
284
|
+
self.solver_configs: dict[str, SolverConfig] = (
|
|
285
|
+
solver_configs if solver_configs is not None else {}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# v60/v62 per-solve decomposition. ``decomposition`` maps
|
|
289
|
+
# solve-name → "none"/"benders"; absent means the schema
|
|
290
|
+
# default "none" (monolithic). The two ``benders_*`` dicts carry
|
|
291
|
+
# the per-solve Benders knobs (str(float) values, only present
|
|
292
|
+
# when authored); absence falls back to the schema defaults via
|
|
293
|
+
# :meth:`benders_config_for`.
|
|
294
|
+
self.decomposition: dict = (
|
|
295
|
+
decomposition if decomposition is not None else {}
|
|
296
|
+
)
|
|
297
|
+
self.benders_max_iter: dict = (
|
|
298
|
+
benders_max_iter if benders_max_iter is not None else {}
|
|
299
|
+
)
|
|
300
|
+
self.benders_tolerance: dict = (
|
|
301
|
+
benders_tolerance if benders_tolerance is not None else {}
|
|
302
|
+
)
|
|
303
|
+
self.benders_in_out_weight: dict = (
|
|
304
|
+
benders_in_out_weight if benders_in_out_weight is not None else {}
|
|
305
|
+
)
|
|
306
|
+
# v64 per-solve autoscaler mode (solve-name -> "off"/"solver_only"/
|
|
307
|
+
# "basic"/"full"); only authored solves appear. Resolved at access
|
|
308
|
+
# time via :meth:`scaling_for` (absent -> None -> CLI/env/default).
|
|
309
|
+
self.scaling: dict = (
|
|
310
|
+
scaling if scaling is not None else {}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Computed fields — populated by load_from_db after construction.
|
|
314
|
+
self.roll_counter: dict[str, int] = {}
|
|
315
|
+
self.timesets_used_by_solves: defaultdict = defaultdict(list)
|
|
316
|
+
self.invest_periods: defaultdict = defaultdict(list)
|
|
317
|
+
self.realized_periods: defaultdict = defaultdict(list)
|
|
318
|
+
self.realized_invest_periods: defaultdict = defaultdict(list)
|
|
319
|
+
self.fix_storage_periods: defaultdict = defaultdict(list)
|
|
320
|
+
|
|
321
|
+
# Mutable tracking — populated during the recursive solve loop.
|
|
322
|
+
self.real_solves: list[str] = []
|
|
323
|
+
self.first_of_complete_solve: list[str] = []
|
|
324
|
+
self.last_of_solve: list[str] = []
|
|
325
|
+
|
|
326
|
+
# ------------------------------------------------------------------
|
|
327
|
+
# Factories
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def load_from_db(
|
|
332
|
+
cls, db: "DatabaseMapping", logger: logging.Logger
|
|
333
|
+
) -> "SolveConfig":
|
|
334
|
+
"""Read all solve-level parameters from *db* into a SolveConfig.
|
|
335
|
+
|
|
336
|
+
Loading order is preserved exactly — see the four-step block at
|
|
337
|
+
the bottom of this method.
|
|
338
|
+
|
|
339
|
+
1. Basic params (model, solvers, rolling_times, …)
|
|
340
|
+
2. ``make_roll_counter`` (needs ``solve_modes``).
|
|
341
|
+
3. ``get_period_timesets`` (needs ``model_solve`` +
|
|
342
|
+
``contains_solves``; may call ``duplicate_solve``).
|
|
343
|
+
4. Four ``periods_to_tuples`` calls
|
|
344
|
+
(``invest_periods``, ``realized_periods``,
|
|
345
|
+
``realized_invest_periods``, ``fix_storage_periods``;
|
|
346
|
+
may call ``duplicate_solve`` for 2D-Map values).
|
|
347
|
+
"""
|
|
348
|
+
model = get_single_entities(db=db, entity_class_name="model")
|
|
349
|
+
model_solve: defaultdict = params_to_dict(
|
|
350
|
+
db=db, cl="model", par="solves", mode=DictMode.DEFAULTDICT
|
|
351
|
+
)
|
|
352
|
+
# Auto-wire when no model:solves defined and only one solve exists.
|
|
353
|
+
solves_temp = get_single_entities(db=db, entity_class_name="solve")
|
|
354
|
+
if len(model_solve) == 0 and len(solves_temp) == 1:
|
|
355
|
+
model_solve["flextool"] = [solves_temp[0]]
|
|
356
|
+
|
|
357
|
+
solve_modes: dict = params_to_dict(
|
|
358
|
+
db=db, cl="solve", par="solve_mode", mode=DictMode.DICT
|
|
359
|
+
)
|
|
360
|
+
# Batch C.5 — ``highs_presolve`` shortcut removed; override
|
|
361
|
+
# is now keyed as ``presolve`` inside ``solver_arguments``.
|
|
362
|
+
highs_presolve: dict = {}
|
|
363
|
+
# Batch C.3 — ``highs_method`` shortcut removed; the equivalent
|
|
364
|
+
# override is now keyed as ``solver`` inside
|
|
365
|
+
# ``solver_arguments``.
|
|
366
|
+
highs_method: dict = {}
|
|
367
|
+
# Batch C.4 — ``highs_parallel`` shortcut removed; override
|
|
368
|
+
# is now keyed as ``parallel`` inside ``solver_arguments``.
|
|
369
|
+
highs_parallel: dict = {}
|
|
370
|
+
solve_period_years_represented: defaultdict = params_to_dict(
|
|
371
|
+
db=db,
|
|
372
|
+
cl="solve",
|
|
373
|
+
par="years_represented",
|
|
374
|
+
mode=DictMode.DEFAULTDICT,
|
|
375
|
+
)
|
|
376
|
+
solvers: dict = params_to_dict(
|
|
377
|
+
db=db, cl="solve", par="solver", mode=DictMode.DICT
|
|
378
|
+
)
|
|
379
|
+
solver_precommand: dict = params_to_dict(
|
|
380
|
+
db=db, cl="solve", par="solver_precommand", mode=DictMode.DICT
|
|
381
|
+
)
|
|
382
|
+
# Batch C.1 — ``solver_arguments`` retyped from array to 1d-map
|
|
383
|
+
# at v56 schema; read directly via the Spine API the same way
|
|
384
|
+
# ``solver_options`` is read below so we get a dict-of-dicts
|
|
385
|
+
# (solve name → option-key → value) ready for the
|
|
386
|
+
# effective-options resolver in ``_solver_dispatch``.
|
|
387
|
+
solver_arguments: dict[str, dict[str, str]] = {}
|
|
388
|
+
for param in db.find_parameter_values(
|
|
389
|
+
entity_class_name="solve", parameter_definition_name="solver_arguments"
|
|
390
|
+
):
|
|
391
|
+
pv = api.from_database(param["value"], param["type"])
|
|
392
|
+
if isinstance(pv, api.Map):
|
|
393
|
+
solver_arguments[param["entity_name"]] = {
|
|
394
|
+
str(k): str(v)
|
|
395
|
+
for k, v in zip(list(pv.indexes), list(pv.values))
|
|
396
|
+
}
|
|
397
|
+
elif pv is None:
|
|
398
|
+
continue
|
|
399
|
+
else:
|
|
400
|
+
logger.warning(
|
|
401
|
+
"solve.%s.solver_arguments is not a 1d-map (%r) — "
|
|
402
|
+
"ignoring; expected a Map of HiGHS option name -> value",
|
|
403
|
+
param["entity_name"],
|
|
404
|
+
type(pv).__name__,
|
|
405
|
+
)
|
|
406
|
+
# v52 multi-solver dispatch params. Each per-solve value is
|
|
407
|
+
# rolled up into one :class:`SolverConfig` keyed by solve name
|
|
408
|
+
# (see ``specs/flextool-multi-solver-handoff.md`` Steps 1-3).
|
|
409
|
+
# The seven params are read individually here and aggregated
|
|
410
|
+
# into ``solver_configs`` below. ``solver_options`` is a Map of
|
|
411
|
+
# str→Any forwarded raw to the chosen solver; everything else is
|
|
412
|
+
# a scalar default-None convenience knob (None means "no override").
|
|
413
|
+
#
|
|
414
|
+
# NOTE: ``solver`` is also read via the older ``solvers``
|
|
415
|
+
# variable above for the legacy :class:`SolverSettings` dataclass
|
|
416
|
+
# (which downstream callers still consume); we reuse that dict
|
|
417
|
+
# here rather than re-querying.
|
|
418
|
+
# Batch C.9 — ``solver_io_api`` DB axis removed. Replaced by
|
|
419
|
+
# the ``--matrix-file-format`` CLI flag (mps | lp),
|
|
420
|
+
# env-var-plumbed via ``FLEXTOOL_MATRIX_FILE_FORMAT``. The
|
|
421
|
+
# in-process vs. file dispatch is implicit:
|
|
422
|
+
# * HiGHS + no --save-memory: ``SolverConfig.io_api`` defaults
|
|
423
|
+
# to ``"direct"`` (in-process binding).
|
|
424
|
+
# * HiGHS + --save-memory: ``Problem.solve(save_memory=True)``
|
|
425
|
+
# round-trips through MPS internally; ``io_api`` is ignored
|
|
426
|
+
# on that path.
|
|
427
|
+
# * Commercial solver: ``polar_high.solvers.solve`` consults
|
|
428
|
+
# ``io_api``. When the CLI flag is set its value
|
|
429
|
+
# (``"mps"`` or ``"lp"``) applies uniformly to every solve.
|
|
430
|
+
# ``matrix_file_format`` stays an empty dict (no per-solve
|
|
431
|
+
# author override since the DB axis is gone); the default is
|
|
432
|
+
# resolved below using the env var when present, else
|
|
433
|
+
# ``"direct"``.
|
|
434
|
+
import os as _os_c9
|
|
435
|
+
_cli_io_api = _os_c9.environ.get("FLEXTOOL_MATRIX_FILE_FORMAT")
|
|
436
|
+
matrix_file_format: dict = {}
|
|
437
|
+
# Batch C.7 — ``solver_log_level`` shortcut removed; use the
|
|
438
|
+
# --solver-log-level CLI flag (silent / normal / verbose →
|
|
439
|
+
# HiGHS output_flag + log_dev_level).
|
|
440
|
+
solver_log_level: dict = {}
|
|
441
|
+
# solver_mip_gap comes back as the stringified float that
|
|
442
|
+
# ``params_to_dict`` produces for scalar floats (see line
|
|
443
|
+
# ~141). Batches C.6 and C.8 dropped the ``solver_threads``
|
|
444
|
+
# and ``solver_time_limit`` DB axes (use --highs-threads and
|
|
445
|
+
# --solver-time-limit CLI flags instead).
|
|
446
|
+
solver_time_limit_raw: dict = {}
|
|
447
|
+
solver_mip_gap_raw: dict = params_to_dict(
|
|
448
|
+
db=db, cl="solve", par="solver_mip_gap", mode=DictMode.DICT
|
|
449
|
+
)
|
|
450
|
+
solver_threads_raw: dict = {}
|
|
451
|
+
# Batch C.2 — ``solver_options`` was folded into
|
|
452
|
+
# ``solver_arguments`` (now the canonical 1d-map) and the
|
|
453
|
+
# parameter_definition was removed. ``SolverConfig.options``
|
|
454
|
+
# is sourced from ``solver_arguments`` below.
|
|
455
|
+
stochastic_branches: defaultdict = params_to_dict(
|
|
456
|
+
db=db,
|
|
457
|
+
cl="solve",
|
|
458
|
+
par="stochastic_branches",
|
|
459
|
+
mode=DictMode.DEFAULTDICT,
|
|
460
|
+
)
|
|
461
|
+
contains_solves: defaultdict = params_to_dict(
|
|
462
|
+
db=db,
|
|
463
|
+
cl="solve",
|
|
464
|
+
par="contains_solves",
|
|
465
|
+
mode=DictMode.DEFAULTDICT,
|
|
466
|
+
str_to_list=True,
|
|
467
|
+
)
|
|
468
|
+
hole_multipliers: defaultdict = params_to_dict(
|
|
469
|
+
db=db,
|
|
470
|
+
cl="solve",
|
|
471
|
+
par="timeline_hole_multiplier",
|
|
472
|
+
mode=DictMode.DEFAULTDICT,
|
|
473
|
+
)
|
|
474
|
+
delay_durations: dict = params_to_dict(
|
|
475
|
+
db=db, cl="unit", par="delay", mode=DictMode.DICT
|
|
476
|
+
)
|
|
477
|
+
periods_available: dict = params_to_dict(
|
|
478
|
+
db=db, cl="model", par="periods_available", mode=DictMode.DICT
|
|
479
|
+
)
|
|
480
|
+
# Per-solve opt-in for automatic LP-row scaling. Default
|
|
481
|
+
# absent / "no" leaves AMPL behaviour as pre-Agent-5; the
|
|
482
|
+
# native engine consumes this flag during preprocessing the
|
|
483
|
+
# same way.
|
|
484
|
+
# Batch C.10 — DB-level ``use_row_scaling`` removed; use the
|
|
485
|
+
# --scaling CLI flag (autoscale; off/solver_only/basic/full).
|
|
486
|
+
# The per-solve dict is hard-wired to {} so every solve
|
|
487
|
+
# emits p_use_row_scaling=0 (the
|
|
488
|
+
# ``use_row_scaling.get(solve, "no")`` default branch in
|
|
489
|
+
# _emit_solve_writers.derive_p_use_row_scaling), preserving
|
|
490
|
+
# the Mode A pre-scaling behaviour for the row-scaling
|
|
491
|
+
# capacity-proxy emitter. The autoscaler's Layer 2 + Layer
|
|
492
|
+
# 3 (driven by --scaling) are unaffected.
|
|
493
|
+
use_row_scaling: dict = {}
|
|
494
|
+
scale_the_objective: dict = params_to_dict(
|
|
495
|
+
db=db, cl="solve", par="scale_the_objective", mode=DictMode.DICT
|
|
496
|
+
)
|
|
497
|
+
user_bound_scale: dict = params_to_dict(
|
|
498
|
+
db=db, cl="solve", par="user_bound_scale", mode=DictMode.DICT
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# v60/v62 per-solve decomposition scheme + Benders knobs. Only
|
|
502
|
+
# solves that explicitly author the parameter appear in each
|
|
503
|
+
# dict; absent solves fall back to the schema defaults
|
|
504
|
+
# (decomposition "none"; max_iter 50 / tol 1e-3) at access time
|
|
505
|
+
# via ``decomposition_for`` / ``benders_config_for``.
|
|
506
|
+
decomposition: dict = params_to_dict(
|
|
507
|
+
db=db, cl="solve", par="decomposition", mode=DictMode.DICT
|
|
508
|
+
)
|
|
509
|
+
benders_max_iter: dict = params_to_dict(
|
|
510
|
+
db=db, cl="solve", par="benders_max_iter", mode=DictMode.DICT
|
|
511
|
+
)
|
|
512
|
+
benders_tolerance: dict = params_to_dict(
|
|
513
|
+
db=db, cl="solve", par="benders_tolerance", mode=DictMode.DICT
|
|
514
|
+
)
|
|
515
|
+
benders_in_out_weight: dict = params_to_dict(
|
|
516
|
+
db=db, cl="solve", par="benders_in_out_weight", mode=DictMode.DICT
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# v64 per-solve autoscaler mode. Only solves that explicitly
|
|
520
|
+
# author solve.scaling appear; absent solves resolve to None via
|
|
521
|
+
# ``scaling_for`` (caller falls back to CLI/env/default "full").
|
|
522
|
+
scaling: dict = params_to_dict(
|
|
523
|
+
db=db, cl="solve", par="scaling", mode=DictMode.DICT
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# rolling_times: assemble per-solve [jump, horizon, duration].
|
|
527
|
+
rolling_duration: dict = params_to_dict(
|
|
528
|
+
db=db, cl="solve", par="rolling_duration", mode=DictMode.DICT
|
|
529
|
+
)
|
|
530
|
+
rolling_solve_horizon: dict = params_to_dict(
|
|
531
|
+
db=db, cl="solve", par="rolling_solve_horizon", mode=DictMode.DICT
|
|
532
|
+
)
|
|
533
|
+
rolling_solve_jump: dict = params_to_dict(
|
|
534
|
+
db=db, cl="solve", par="rolling_solve_jump", mode=DictMode.DICT
|
|
535
|
+
)
|
|
536
|
+
all_keys = (
|
|
537
|
+
set(rolling_duration)
|
|
538
|
+
| set(rolling_solve_horizon)
|
|
539
|
+
| set(rolling_solve_jump)
|
|
540
|
+
)
|
|
541
|
+
rolling_times: defaultdict = defaultdict(
|
|
542
|
+
list,
|
|
543
|
+
{
|
|
544
|
+
key: [
|
|
545
|
+
rolling_solve_jump.get(key, 0),
|
|
546
|
+
rolling_solve_horizon.get(key, 0),
|
|
547
|
+
rolling_duration.get(key, -1),
|
|
548
|
+
]
|
|
549
|
+
for key in all_keys
|
|
550
|
+
},
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
highs = HiGHSConfig(
|
|
554
|
+
presolve=highs_presolve,
|
|
555
|
+
method=highs_method,
|
|
556
|
+
parallel=highs_parallel,
|
|
557
|
+
)
|
|
558
|
+
solver_settings = SolverSettings(
|
|
559
|
+
solvers=solvers,
|
|
560
|
+
precommand=solver_precommand,
|
|
561
|
+
arguments=solver_arguments,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Roll the seven v52 per-solve param dicts into a single
|
|
565
|
+
# ``solver_configs[solve_name] -> SolverConfig`` mapping. Keys
|
|
566
|
+
# are the union of solve names appearing across any of the
|
|
567
|
+
# seven dicts — a solve that authors *any* solver_* param gets
|
|
568
|
+
# an explicit entry; solves with no override remain absent and
|
|
569
|
+
# callers fall back to ``SolverConfig()`` defaults.
|
|
570
|
+
# Batch C.2 — ``solver_options`` removed; the per-solve free-form
|
|
571
|
+
# option dict for the commercial-solver path is sourced from
|
|
572
|
+
# ``solver_arguments`` (the same 1d-map the HiGHS-side resolver
|
|
573
|
+
# consumes).
|
|
574
|
+
solver_config_keys = (
|
|
575
|
+
set(solvers)
|
|
576
|
+
| set(matrix_file_format)
|
|
577
|
+
| set(solver_arguments)
|
|
578
|
+
| set(solver_time_limit_raw)
|
|
579
|
+
| set(solver_mip_gap_raw)
|
|
580
|
+
| set(solver_threads_raw)
|
|
581
|
+
| set(solver_log_level)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def _opt_float(raw: dict, key: str) -> float | None:
|
|
585
|
+
v = raw.get(key)
|
|
586
|
+
return float(v) if v is not None else None
|
|
587
|
+
|
|
588
|
+
def _opt_int(raw: dict, key: str) -> int | None:
|
|
589
|
+
v = raw.get(key)
|
|
590
|
+
return int(float(v)) if v is not None else None
|
|
591
|
+
|
|
592
|
+
# Batch C.9 — the CLI ``--matrix-file-format`` env var override
|
|
593
|
+
# (when set, ``"mps"`` or ``"lp"``) becomes the default
|
|
594
|
+
# ``SolverConfig.io_api`` for every solve. Otherwise the
|
|
595
|
+
# default is ``"direct"`` (HiGHS in-process binding; commercial
|
|
596
|
+
# solvers' in-process Python API).
|
|
597
|
+
_io_api_default = _cli_io_api if _cli_io_api in ("mps", "lp") else "direct"
|
|
598
|
+
solver_configs: dict[str, SolverConfig] = {}
|
|
599
|
+
for key in solver_config_keys:
|
|
600
|
+
solver_configs[key] = SolverConfig(
|
|
601
|
+
name=solvers.get(key, "highs"),
|
|
602
|
+
io_api=matrix_file_format.get(key, _io_api_default),
|
|
603
|
+
options=dict(solver_arguments.get(key, {})),
|
|
604
|
+
time_limit=_opt_float(solver_time_limit_raw, key),
|
|
605
|
+
mip_gap=_opt_float(solver_mip_gap_raw, key),
|
|
606
|
+
threads=_opt_int(solver_threads_raw, key),
|
|
607
|
+
log_level=solver_log_level.get(key, "normal"),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
obj = cls(
|
|
611
|
+
model=model,
|
|
612
|
+
model_solve=model_solve,
|
|
613
|
+
solve_modes=solve_modes,
|
|
614
|
+
rolling_times=rolling_times,
|
|
615
|
+
highs=highs,
|
|
616
|
+
solver_settings=solver_settings,
|
|
617
|
+
solve_period_years_represented=solve_period_years_represented,
|
|
618
|
+
hole_multipliers=hole_multipliers,
|
|
619
|
+
contains_solves=contains_solves,
|
|
620
|
+
stochastic_branches=stochastic_branches,
|
|
621
|
+
periods_available=periods_available,
|
|
622
|
+
delay_durations=delay_durations,
|
|
623
|
+
logger=logger,
|
|
624
|
+
use_row_scaling=use_row_scaling,
|
|
625
|
+
scale_the_objective=scale_the_objective,
|
|
626
|
+
user_bound_scale=user_bound_scale,
|
|
627
|
+
solver_configs=solver_configs,
|
|
628
|
+
decomposition=decomposition,
|
|
629
|
+
benders_max_iter=benders_max_iter,
|
|
630
|
+
benders_tolerance=benders_tolerance,
|
|
631
|
+
benders_in_out_weight=benders_in_out_weight,
|
|
632
|
+
scaling=scaling,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Computed fields — loading order MUST be preserved exactly.
|
|
636
|
+
# ``duplicate_solve`` mutates 19 sibling dicts in lockstep, so
|
|
637
|
+
# any reordering desyncs them and downstream reads silently
|
|
638
|
+
# produce empty/zero results.
|
|
639
|
+
obj.roll_counter = obj.make_roll_counter()
|
|
640
|
+
obj.timesets_used_by_solves = obj.get_period_timesets(db=db)
|
|
641
|
+
obj.invest_periods = obj.periods_to_tuples(
|
|
642
|
+
db=db, cl="solve", par="invest_periods"
|
|
643
|
+
)
|
|
644
|
+
obj.realized_periods = obj.periods_to_tuples(
|
|
645
|
+
db=db, cl="solve", par="realized_periods"
|
|
646
|
+
)
|
|
647
|
+
obj.realized_invest_periods = obj.periods_to_tuples(
|
|
648
|
+
db=db, cl="solve", par="realized_invest_periods"
|
|
649
|
+
)
|
|
650
|
+
obj.fix_storage_periods = obj.periods_to_tuples(
|
|
651
|
+
db=db, cl="solve", par="fix_storage_periods"
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return obj
|
|
655
|
+
|
|
656
|
+
@classmethod
|
|
657
|
+
def load_from_db_url(
|
|
658
|
+
cls,
|
|
659
|
+
db_url: str,
|
|
660
|
+
scenario: str,
|
|
661
|
+
logger: logging.Logger | None = None,
|
|
662
|
+
) -> "SolveConfig":
|
|
663
|
+
"""Convenience factory: open *db_url*, apply *scenario*, load.
|
|
664
|
+
|
|
665
|
+
Builds a short-lived :class:`spinedb_api.DatabaseMapping` with the
|
|
666
|
+
scenario filter applied, calls :meth:`load_from_db`, and closes
|
|
667
|
+
the DB. Useful for tests / CLI entry points that don't already
|
|
668
|
+
own a session.
|
|
669
|
+
"""
|
|
670
|
+
from spinedb_api import DatabaseMapping
|
|
671
|
+
from spinedb_api.filters.scenario_filter import (
|
|
672
|
+
apply_scenario_filter_to_subqueries,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
if logger is None:
|
|
676
|
+
logger = logging.getLogger(f"flextool.engine_polars.solve_config[{scenario}]")
|
|
677
|
+
url = str(db_url)
|
|
678
|
+
if "://" not in url:
|
|
679
|
+
url = f"sqlite:///{url}"
|
|
680
|
+
with DatabaseMapping(url) as db:
|
|
681
|
+
apply_scenario_filter_to_subqueries(db, scenario)
|
|
682
|
+
# Pre-warm the entity + parameter_value caches so the
|
|
683
|
+
# ``find_entities`` / ``find_parameter_values`` calls inside
|
|
684
|
+
# ``load_from_db`` (get_period_timesets, get_single_param, …)
|
|
685
|
+
# hit memory rather than running a SQL round-trip each.
|
|
686
|
+
# Pre-fetch at construction and share the DB context across
|
|
687
|
+
# the whole pipeline. Measured 5.5-6.4× speedup on large
|
|
688
|
+
# customer DBs (H2_trade ≈ 13 MB, 2.5 s → 0.35 s).
|
|
689
|
+
db.fetch_all("entity")
|
|
690
|
+
db.fetch_all("parameter_value")
|
|
691
|
+
return cls.load_from_db(db, logger)
|
|
692
|
+
|
|
693
|
+
@classmethod
|
|
694
|
+
def load_from_source(
|
|
695
|
+
cls,
|
|
696
|
+
source: object,
|
|
697
|
+
logger: logging.Logger | None = None,
|
|
698
|
+
) -> "SolveConfig":
|
|
699
|
+
"""Load via the :class:`InputSource` Protocol.
|
|
700
|
+
|
|
701
|
+
Currently only :class:`flextool.engine_polars._spinedb_reader.SpineDbReader`
|
|
702
|
+
sources are supported — they expose the underlying ``db_url`` and
|
|
703
|
+
``scenario`` so the canonical :meth:`load_from_db_url` path can
|
|
704
|
+
be reused. In-memory and CSV-backed sources for solve-class
|
|
705
|
+
parameters are out of Γ.8.A scope; Γ.8.D wires those once the
|
|
706
|
+
chain.run_chain integration needs them.
|
|
707
|
+
"""
|
|
708
|
+
# Late import: SpineDbReader brings spinedb_api into the import
|
|
709
|
+
# graph, but we only need it for the isinstance check.
|
|
710
|
+
from flextool.engine_polars._spinedb_reader import SpineDbReader
|
|
711
|
+
|
|
712
|
+
if isinstance(source, SpineDbReader):
|
|
713
|
+
return cls.load_from_db_url(
|
|
714
|
+
source.db_url, source.scenario, logger=logger
|
|
715
|
+
)
|
|
716
|
+
raise NotImplementedError(
|
|
717
|
+
f"SolveConfig.load_from_source does not yet support "
|
|
718
|
+
f"{type(source).__name__!r} sources. Use load_from_db / "
|
|
719
|
+
f"load_from_db_url with a Spine DB for now; the in-memory and "
|
|
720
|
+
f"CSV adapters land in Gamma.8.D when chain.run_chain is rewired."
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# ------------------------------------------------------------------
|
|
724
|
+
# Methods
|
|
725
|
+
# ------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
def make_roll_counter(self) -> dict[str, int]:
|
|
728
|
+
"""Return a roll counter initialised to 0 for every rolling-window
|
|
729
|
+
solve. Single-solve mode entries are intentionally absent from
|
|
730
|
+
the returned dict (NOT zero) so callers can distinguish ``not in``
|
|
731
|
+
from ``== 0`` semantics.
|
|
732
|
+
"""
|
|
733
|
+
roll_counter_map: dict[str, int] = {}
|
|
734
|
+
for key, mode in list(self.solve_modes.items()):
|
|
735
|
+
if mode == "rolling_window":
|
|
736
|
+
roll_counter_map[key] = 0
|
|
737
|
+
return roll_counter_map
|
|
738
|
+
|
|
739
|
+
def get_period_timesets(self, db: "DatabaseMapping") -> defaultdict:
|
|
740
|
+
"""Read ``period_timeset`` parameters for every active solve.
|
|
741
|
+
|
|
742
|
+
May call :meth:`duplicate_solve` when a solve carries a
|
|
743
|
+
2D-Map-shaped ``period_timeset`` parameter (one input solve fans
|
|
744
|
+
out into one solve per outer-Map key).
|
|
745
|
+
"""
|
|
746
|
+
entities = db.find_entities(entity_class_name="solve")
|
|
747
|
+
params = db.find_parameter_values(
|
|
748
|
+
entity_class_name="solve",
|
|
749
|
+
parameter_definition_name="period_timeset",
|
|
750
|
+
)
|
|
751
|
+
timesets_used_by_solves: defaultdict = defaultdict(list)
|
|
752
|
+
solves_in_model = [
|
|
753
|
+
item
|
|
754
|
+
for sublist in (
|
|
755
|
+
list(self.model_solve.values())
|
|
756
|
+
+ list(self.contains_solves.values())
|
|
757
|
+
)
|
|
758
|
+
for item in sublist
|
|
759
|
+
]
|
|
760
|
+
for entity in entities:
|
|
761
|
+
if entity["name"] not in solves_in_model:
|
|
762
|
+
continue
|
|
763
|
+
for param in params:
|
|
764
|
+
if param["entity_name"] != entity["name"]:
|
|
765
|
+
continue
|
|
766
|
+
param_value = api.from_database(param["value"], param["type"])
|
|
767
|
+
for i, _row in enumerate(param_value.indexes):
|
|
768
|
+
if isinstance(param_value.values[i], api.Map):
|
|
769
|
+
new_name = (
|
|
770
|
+
param["entity_name"] + "_" + param_value.indexes[i]
|
|
771
|
+
)
|
|
772
|
+
self.duplicate_solve(param["entity_name"], new_name)
|
|
773
|
+
timesets_used_by_solves[new_name].append(
|
|
774
|
+
(
|
|
775
|
+
param_value.values[i].indexes[i],
|
|
776
|
+
param_value.values[i].values[i],
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
else:
|
|
780
|
+
timesets_used_by_solves[param["entity_name"]].append(
|
|
781
|
+
(
|
|
782
|
+
param_value.indexes[i],
|
|
783
|
+
param_value.values[i],
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
return timesets_used_by_solves
|
|
787
|
+
|
|
788
|
+
def duplicate_solve(
|
|
789
|
+
self,
|
|
790
|
+
old_solve: str,
|
|
791
|
+
new_name: str,
|
|
792
|
+
update_model_solves: bool = True,
|
|
793
|
+
) -> None:
|
|
794
|
+
"""Duplicate every solve-level dict entry from *old_solve* under
|
|
795
|
+
*new_name*.
|
|
796
|
+
|
|
797
|
+
Mutates 19 sibling defaultdicts (and ``model_solve`` when
|
|
798
|
+
*update_model_solves* is set) so downstream readers can address
|
|
799
|
+
the duplicated solve transparently.
|
|
800
|
+
|
|
801
|
+
``update_model_solves=False`` is used by the rolling builder
|
|
802
|
+
(Γ.8.C) where roll-named sub-solves should NOT replace their
|
|
803
|
+
parent in ``model_solve``.
|
|
804
|
+
"""
|
|
805
|
+
if (
|
|
806
|
+
new_name not in self.model_solve.values()
|
|
807
|
+
and new_name not in self.contains_solves.values()
|
|
808
|
+
):
|
|
809
|
+
dup_map_list = [
|
|
810
|
+
self.solve_modes,
|
|
811
|
+
self.roll_counter,
|
|
812
|
+
self.highs.presolve,
|
|
813
|
+
self.highs.method,
|
|
814
|
+
self.highs.parallel,
|
|
815
|
+
self.solve_period_years_represented,
|
|
816
|
+
self.solver_settings.solvers,
|
|
817
|
+
self.solver_settings.precommand,
|
|
818
|
+
self.solver_settings.arguments,
|
|
819
|
+
self.contains_solves,
|
|
820
|
+
self.rolling_times,
|
|
821
|
+
self.realized_periods,
|
|
822
|
+
self.realized_invest_periods,
|
|
823
|
+
self.invest_periods,
|
|
824
|
+
self.fix_storage_periods,
|
|
825
|
+
self.decomposition,
|
|
826
|
+
self.benders_max_iter,
|
|
827
|
+
self.benders_tolerance,
|
|
828
|
+
self.benders_in_out_weight,
|
|
829
|
+
self.scaling,
|
|
830
|
+
]
|
|
831
|
+
for dup_map in dup_map_list:
|
|
832
|
+
if old_solve in dup_map.keys():
|
|
833
|
+
dup_map[new_name] = dup_map[old_solve]
|
|
834
|
+
if update_model_solves:
|
|
835
|
+
for model, solves in list(self.model_solve.items()):
|
|
836
|
+
if old_solve in solves:
|
|
837
|
+
solves.remove(old_solve)
|
|
838
|
+
if new_name not in solves:
|
|
839
|
+
solves.append(new_name)
|
|
840
|
+
self.model_solve[model] = solves
|
|
841
|
+
|
|
842
|
+
# ------------------------------------------------------------------
|
|
843
|
+
# v60/v62 per-solve decomposition accessors
|
|
844
|
+
# ------------------------------------------------------------------
|
|
845
|
+
|
|
846
|
+
def decomposition_for(self, solve_name: str) -> str:
|
|
847
|
+
"""Resolve the decomposition scheme for *solve_name*.
|
|
848
|
+
|
|
849
|
+
Returns ``"benders"`` only when the solve explicitly authors
|
|
850
|
+
``solve.decomposition = benders``; everything else (unset, the
|
|
851
|
+
schema default ``"none"``, blank, or any unrecognised value)
|
|
852
|
+
resolves to ``"none"`` (monolithic). Recognised values are
|
|
853
|
+
normalised to lower-case so authoring case does not matter.
|
|
854
|
+
"""
|
|
855
|
+
raw = self.decomposition.get(solve_name)
|
|
856
|
+
if raw is None:
|
|
857
|
+
return "none"
|
|
858
|
+
value = str(raw).strip().lower()
|
|
859
|
+
return "benders" if value == "benders" else "none"
|
|
860
|
+
|
|
861
|
+
def scaling_for(self, solve_name: str) -> str | None:
|
|
862
|
+
"""Resolve the autoscaler scaling mode authored on *solve_name*.
|
|
863
|
+
|
|
864
|
+
Returns the normalised lower-case mode string (one of
|
|
865
|
+
``off`` / ``solver_only`` / ``basic`` / ``full``) when the solve
|
|
866
|
+
explicitly authors ``solve.scaling``; returns ``None`` when the
|
|
867
|
+
parameter is absent, blank, or unrecognised so the caller falls
|
|
868
|
+
back to the run-time ``--scaling`` / ``FLEXTOOL_SCALING``
|
|
869
|
+
override or the engine default (``full``). Authoring case does
|
|
870
|
+
not matter.
|
|
871
|
+
"""
|
|
872
|
+
raw = self.scaling.get(solve_name)
|
|
873
|
+
if raw is None:
|
|
874
|
+
return None
|
|
875
|
+
value = str(raw).strip().lower()
|
|
876
|
+
return value if value in (
|
|
877
|
+
"off", "solver_only", "basic", "full"
|
|
878
|
+
) else None
|
|
879
|
+
|
|
880
|
+
def benders_config_for(self, solve_name: str) -> tuple[int, float, float]:
|
|
881
|
+
"""Resolve ``(max_iter, tol, in_out_weight)`` for *solve_name*.
|
|
882
|
+
|
|
883
|
+
Falls back to the schema defaults (50 / 1e-3 / 0.0) for any knob
|
|
884
|
+
the solve does not author. ``params_to_dict`` stores scalar
|
|
885
|
+
floats as ``str(float)``, so values are coerced through ``float``
|
|
886
|
+
here; ``max_iter`` is additionally rounded to ``int``. The
|
|
887
|
+
``in_out_weight`` (default 0.0 = OFF = exact Benders) is the DB
|
|
888
|
+
value; the machine-local ``FLEXTOOL_BENDERS_IN_OUT_WEIGHT`` env
|
|
889
|
+
overrides it downstream in
|
|
890
|
+
:func:`flextool.engine_polars._benders._resolve_benders_in_out_weight`.
|
|
891
|
+
"""
|
|
892
|
+
max_iter_raw = self.benders_max_iter.get(solve_name)
|
|
893
|
+
tol_raw = self.benders_tolerance.get(solve_name)
|
|
894
|
+
weight_raw = self.benders_in_out_weight.get(solve_name)
|
|
895
|
+
max_iter = int(float(max_iter_raw)) if max_iter_raw is not None else 50
|
|
896
|
+
tol = float(tol_raw) if tol_raw is not None else 1e-3
|
|
897
|
+
in_out_weight = float(weight_raw) if weight_raw is not None else 0.0
|
|
898
|
+
return max_iter, tol, in_out_weight
|
|
899
|
+
|
|
900
|
+
def periods_to_tuples(
|
|
901
|
+
self,
|
|
902
|
+
db: "DatabaseMapping",
|
|
903
|
+
cl: str,
|
|
904
|
+
par: str,
|
|
905
|
+
) -> defaultdict:
|
|
906
|
+
"""Read period-shaped solve parameters as ``[(p_from, p_in), …]``.
|
|
907
|
+
|
|
908
|
+
For 1D Array values (e.g. ``realized_periods=["p2020","p2025"]``)
|
|
909
|
+
each element ``p`` becomes ``(p, p)``.
|
|
910
|
+
|
|
911
|
+
For 2D Map values (e.g. ``invest_periods`` under the
|
|
912
|
+
``invest_twoYears4Times_5weeks`` scenario where one solve ladders
|
|
913
|
+
an "invest in p2020 covers p2020+p2025" pattern) the outer Map
|
|
914
|
+
triggers :meth:`duplicate_solve` and per-(outer, inner) tuples
|
|
915
|
+
flow into the new solve's entry. Inner shape is required to be
|
|
916
|
+
Map-of-scalars; mixed 1D/2D with the same name raises.
|
|
917
|
+
"""
|
|
918
|
+
entities = db.find_entities(entity_class_name=cl)
|
|
919
|
+
params = db.find_parameter_values(
|
|
920
|
+
entity_class_name=cl,
|
|
921
|
+
parameter_definition_name=par,
|
|
922
|
+
)
|
|
923
|
+
result_dict: defaultdict = defaultdict(list)
|
|
924
|
+
for entity in entities:
|
|
925
|
+
for param in params:
|
|
926
|
+
if param["entity_name"] != entity["name"]:
|
|
927
|
+
continue
|
|
928
|
+
param_value = api.from_database(param["value"], param["type"])
|
|
929
|
+
for i, row in enumerate(param_value.values):
|
|
930
|
+
if isinstance(param_value.values[i], api.Map):
|
|
931
|
+
# 2D Map: outer index → inner Map of (p, "yes")-ish.
|
|
932
|
+
for j, _row2 in enumerate(row.values):
|
|
933
|
+
if isinstance(param_value.values[j], api.Map):
|
|
934
|
+
new_name = (
|
|
935
|
+
param["entity_name"]
|
|
936
|
+
+ "_"
|
|
937
|
+
+ param_value.indexes[i]
|
|
938
|
+
)
|
|
939
|
+
self.duplicate_solve(
|
|
940
|
+
param["entity_name"], new_name
|
|
941
|
+
)
|
|
942
|
+
result_dict[new_name].append(
|
|
943
|
+
(
|
|
944
|
+
param_value.indexes[i],
|
|
945
|
+
param_value.values[i].indexes[j],
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
# Re-shape ``timesets_used_by_solves`` for
|
|
949
|
+
# the duplicated solve: keep only entries
|
|
950
|
+
# whose period matches the inner index.
|
|
951
|
+
new_period_timeset_list = []
|
|
952
|
+
for solve, period__timeset_list in list(
|
|
953
|
+
self.timesets_used_by_solves.items()
|
|
954
|
+
):
|
|
955
|
+
if solve != param["entity_name"]:
|
|
956
|
+
continue
|
|
957
|
+
for period__timeset in period__timeset_list:
|
|
958
|
+
if (
|
|
959
|
+
period__timeset[0]
|
|
960
|
+
== param_value.values[i].indexes[j]
|
|
961
|
+
):
|
|
962
|
+
new_period_timeset_list.append(
|
|
963
|
+
period__timeset
|
|
964
|
+
)
|
|
965
|
+
if (
|
|
966
|
+
new_name
|
|
967
|
+
not in self.timesets_used_by_solves.keys()
|
|
968
|
+
):
|
|
969
|
+
self.timesets_used_by_solves[new_name] = (
|
|
970
|
+
new_period_timeset_list
|
|
971
|
+
)
|
|
972
|
+
else:
|
|
973
|
+
for item in new_period_timeset_list:
|
|
974
|
+
if (
|
|
975
|
+
item
|
|
976
|
+
not in self.timesets_used_by_solves[
|
|
977
|
+
new_name
|
|
978
|
+
]
|
|
979
|
+
):
|
|
980
|
+
self.timesets_used_by_solves[
|
|
981
|
+
new_name
|
|
982
|
+
].append(item)
|
|
983
|
+
else:
|
|
984
|
+
raise FlexToolConfigError(
|
|
985
|
+
"periods_to_tuple function handles only "
|
|
986
|
+
f"arrays or 2d maps: {entity}, {param}"
|
|
987
|
+
)
|
|
988
|
+
else:
|
|
989
|
+
result_dict[param["entity_name"]].append((row, row))
|
|
990
|
+
return result_dict
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
__all__ = [
|
|
994
|
+
"DictMode",
|
|
995
|
+
"HiGHSConfig",
|
|
996
|
+
"SolverConfig",
|
|
997
|
+
"SolverSettings",
|
|
998
|
+
"SolveConfig",
|
|
999
|
+
"get_single_entities",
|
|
1000
|
+
"params_to_dict",
|
|
1001
|
+
]
|