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,451 @@
|
|
|
1
|
+
"""Parse MATPOWER .m case files and convert to FlexTool Spine DB.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Dataclasses for MATPOWER bus, generator, branch and case data
|
|
5
|
+
- ``read_matpower(filepath)`` — parse a .m file into a ``MatpowerCase``
|
|
6
|
+
- ``create_flextool_db_from_matpower(case, db_path)`` — build a ready-to-solve
|
|
7
|
+
FlexTool Spine DB from parsed MATPOWER data
|
|
8
|
+
|
|
9
|
+
The converter creates a DC-OPF-ready FlexTool database with:
|
|
10
|
+
- One commodity node per generator (fuel cost = linear cost coefficient)
|
|
11
|
+
- One unit per generator (constant_efficiency, efficiency=1.0)
|
|
12
|
+
- One node per bus (demand as negative inflow)
|
|
13
|
+
- One connection per branch (with reactance for DC power flow)
|
|
14
|
+
- A single AC network group with dc_power_flow_with_angles
|
|
15
|
+
- A minimal single-timestep timeline for dispatch
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from spinedb_api import Array, DatabaseMapping, Map, import_data
|
|
25
|
+
|
|
26
|
+
from flextool.update_flextool import FLEXTOOL_DB_VERSION
|
|
27
|
+
from flextool.update_flextool.db_migration import migrate_database
|
|
28
|
+
from flextool.update_flextool.initialize_database import initialize_database
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# MATPOWER data structures
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class MatpowerBus:
|
|
39
|
+
"""Single bus from mpc.bus matrix."""
|
|
40
|
+
bus_id: int
|
|
41
|
+
bus_type: int # 1=PQ, 2=PV, 3=ref (slack)
|
|
42
|
+
pd: float # real power demand (MW)
|
|
43
|
+
qd: float # reactive power demand (MVAr) — unused for DC
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class MatpowerGen:
|
|
48
|
+
"""Single generator from mpc.gen + mpc.gencost matrices."""
|
|
49
|
+
bus: int
|
|
50
|
+
pmax: float # MW
|
|
51
|
+
pmin: float # MW
|
|
52
|
+
status: int # 1=online, 0=offline
|
|
53
|
+
cost_coeffs: list[float] = field(default_factory=list)
|
|
54
|
+
# Polynomial cost coefficients [c2, c1, c0] where cost = c2*P^2 + c1*P + c0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class MatpowerBranch:
|
|
59
|
+
"""Single branch from mpc.branch matrix."""
|
|
60
|
+
fbus: int
|
|
61
|
+
tbus: int
|
|
62
|
+
r: float # resistance (p.u.)
|
|
63
|
+
x: float # reactance (p.u.)
|
|
64
|
+
rate_a: float # thermal limit (MVA), 0 = unlimited
|
|
65
|
+
ratio: float # transformer tap ratio (0 = line, nonzero = transformer)
|
|
66
|
+
status: int # 1=online
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class MatpowerCase:
|
|
71
|
+
"""Complete parsed MATPOWER case."""
|
|
72
|
+
name: str
|
|
73
|
+
base_mva: float
|
|
74
|
+
buses: list[MatpowerBus]
|
|
75
|
+
generators: list[MatpowerGen]
|
|
76
|
+
branches: list[MatpowerBranch]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# MATPOWER parser
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def _strip_comments(text: str) -> str:
|
|
84
|
+
"""Remove MATLAB line comments (% ...) and inline comments."""
|
|
85
|
+
lines = []
|
|
86
|
+
for line in text.splitlines():
|
|
87
|
+
# Remove everything after the first % that's not inside a string
|
|
88
|
+
idx = line.find('%')
|
|
89
|
+
if idx >= 0:
|
|
90
|
+
line = line[:idx]
|
|
91
|
+
lines.append(line)
|
|
92
|
+
return '\n'.join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_matrix(text: str, name: str) -> list[list[float]]:
|
|
96
|
+
"""Extract a matrix block ``mpc.<name> = [ ... ];`` and parse rows.
|
|
97
|
+
|
|
98
|
+
Each row is semicolon-terminated within the brackets.
|
|
99
|
+
"""
|
|
100
|
+
# Match mpc.<name> = [ ... ];
|
|
101
|
+
pattern = rf'mpc\.{name}\s*=\s*\[(.*?)\];'
|
|
102
|
+
match = re.search(pattern, text, re.DOTALL)
|
|
103
|
+
if not match:
|
|
104
|
+
return []
|
|
105
|
+
block = match.group(1)
|
|
106
|
+
rows: list[list[float]] = []
|
|
107
|
+
for row_str in block.split(';'):
|
|
108
|
+
row_str = row_str.strip()
|
|
109
|
+
if not row_str:
|
|
110
|
+
continue
|
|
111
|
+
values = row_str.split()
|
|
112
|
+
rows.append([float(v) for v in values])
|
|
113
|
+
return rows
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _extract_scalar(text: str, name: str) -> float | None:
|
|
117
|
+
"""Extract ``mpc.<name> = <value>;``."""
|
|
118
|
+
pattern = rf'mpc\.{name}\s*=\s*([\d.eE+-]+)\s*;'
|
|
119
|
+
match = re.search(pattern, text)
|
|
120
|
+
if match:
|
|
121
|
+
return float(match.group(1))
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_function_name(text: str) -> str:
|
|
126
|
+
"""Extract the function name from ``function mpc = <name>``."""
|
|
127
|
+
match = re.search(r'function\s+mpc\s*=\s*(\w+)', text)
|
|
128
|
+
if match:
|
|
129
|
+
return match.group(1)
|
|
130
|
+
return "unknown_case"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_matpower(filepath: str | Path) -> MatpowerCase:
|
|
134
|
+
"""Parse a MATPOWER .m file and return a ``MatpowerCase``.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
filepath : str or Path
|
|
139
|
+
Path to the .m file.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
MatpowerCase
|
|
144
|
+
Parsed case with buses, generators, branches and cost data.
|
|
145
|
+
"""
|
|
146
|
+
filepath = Path(filepath)
|
|
147
|
+
raw_text = filepath.read_text(encoding='utf-8')
|
|
148
|
+
text = _strip_comments(raw_text)
|
|
149
|
+
|
|
150
|
+
case_name = _extract_function_name(text)
|
|
151
|
+
base_mva = _extract_scalar(text, 'baseMVA')
|
|
152
|
+
if base_mva is None:
|
|
153
|
+
base_mva = 100.0
|
|
154
|
+
|
|
155
|
+
# Parse bus data
|
|
156
|
+
# Columns: bus_i, type, Pd, Qd, Gs, Bs, area, Vm, Va, baseKV, zone, Vmax, Vmin
|
|
157
|
+
bus_rows = _extract_matrix(text, 'bus')
|
|
158
|
+
buses = [
|
|
159
|
+
MatpowerBus(
|
|
160
|
+
bus_id=int(row[0]),
|
|
161
|
+
bus_type=int(row[1]),
|
|
162
|
+
pd=row[2],
|
|
163
|
+
qd=row[3],
|
|
164
|
+
)
|
|
165
|
+
for row in bus_rows
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
# Parse generator data
|
|
169
|
+
# Columns: bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin
|
|
170
|
+
gen_rows = _extract_matrix(text, 'gen')
|
|
171
|
+
generators = [
|
|
172
|
+
MatpowerGen(
|
|
173
|
+
bus=int(row[0]),
|
|
174
|
+
pmax=row[8], # Pmax is column 9 (0-indexed: 8)
|
|
175
|
+
pmin=row[9], # Pmin is column 10 (0-indexed: 9)
|
|
176
|
+
status=int(row[7]),
|
|
177
|
+
)
|
|
178
|
+
for row in gen_rows
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
# Parse generator cost data
|
|
182
|
+
# Format type 2 (polynomial): type, startup, shutdown, n, c(n-1), ..., c0
|
|
183
|
+
gencost_rows = _extract_matrix(text, 'gencost')
|
|
184
|
+
for i, row in enumerate(gencost_rows):
|
|
185
|
+
if i < len(generators):
|
|
186
|
+
cost_type = int(row[0])
|
|
187
|
+
if cost_type == 2:
|
|
188
|
+
n = int(row[3])
|
|
189
|
+
coeffs = row[4:4 + n] # c(n-1), c(n-2), ..., c0
|
|
190
|
+
generators[i].cost_coeffs = coeffs
|
|
191
|
+
else:
|
|
192
|
+
logger.warning("Generator %d has unsupported cost type %d", i, cost_type)
|
|
193
|
+
|
|
194
|
+
# Parse branch data
|
|
195
|
+
# Columns: fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax
|
|
196
|
+
branch_rows = _extract_matrix(text, 'branch')
|
|
197
|
+
branches = [
|
|
198
|
+
MatpowerBranch(
|
|
199
|
+
fbus=int(row[0]),
|
|
200
|
+
tbus=int(row[1]),
|
|
201
|
+
r=row[2],
|
|
202
|
+
x=row[3],
|
|
203
|
+
rate_a=row[5], # rateA is column 6 (0-indexed: 5)
|
|
204
|
+
ratio=row[8], # ratio is column 9 (0-indexed: 8)
|
|
205
|
+
status=int(row[10]),
|
|
206
|
+
)
|
|
207
|
+
for row in branch_rows
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
return MatpowerCase(
|
|
211
|
+
name=case_name,
|
|
212
|
+
base_mva=base_mva,
|
|
213
|
+
buses=buses,
|
|
214
|
+
generators=generators,
|
|
215
|
+
branches=branches,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# FlexTool DB creation from MATPOWER data
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
def create_flextool_db_from_matpower(
|
|
224
|
+
case: MatpowerCase,
|
|
225
|
+
db_path: str,
|
|
226
|
+
*,
|
|
227
|
+
scenario_name: str = "dc_opf_test",
|
|
228
|
+
alternative_name: str = "base",
|
|
229
|
+
template_json: str | None = None,
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Create a FlexTool Spine DB from parsed MATPOWER data.
|
|
232
|
+
|
|
233
|
+
The DB is initialized from the FlexTool master template and then
|
|
234
|
+
populated with entities/parameters for a single-timestep DC-OPF
|
|
235
|
+
dispatch problem.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
case : MatpowerCase
|
|
240
|
+
Parsed MATPOWER case.
|
|
241
|
+
db_path : str
|
|
242
|
+
File path for the output .sqlite DB.
|
|
243
|
+
scenario_name : str
|
|
244
|
+
Name for the scenario (default ``dc_opf_test``).
|
|
245
|
+
alternative_name : str
|
|
246
|
+
Name for the alternative (default ``base``).
|
|
247
|
+
template_json : str or None
|
|
248
|
+
Path to the FlexTool template JSON. If None, uses the default
|
|
249
|
+
``schemas/spinedb_schema.json`` relative to the project root.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
str
|
|
254
|
+
The ``sqlite:///`` URL for the created database.
|
|
255
|
+
"""
|
|
256
|
+
if template_json is None:
|
|
257
|
+
from flextool._resources import package_data_path
|
|
258
|
+
template_json = str(package_data_path("schemas/spinedb_schema.json"))
|
|
259
|
+
|
|
260
|
+
# Step 1: Initialize from template and migrate to latest schema
|
|
261
|
+
initialize_database(template_json, db_path)
|
|
262
|
+
migrate_database(db_path)
|
|
263
|
+
|
|
264
|
+
url = f"sqlite:///{db_path}"
|
|
265
|
+
|
|
266
|
+
# Step 2: Add MATPOWER data
|
|
267
|
+
with DatabaseMapping(url) as db:
|
|
268
|
+
_add_matpower_data(db, case, alternative_name)
|
|
269
|
+
db.commit_session("Add MATPOWER case data")
|
|
270
|
+
|
|
271
|
+
# Step 3: Add scenario
|
|
272
|
+
with DatabaseMapping(url) as db:
|
|
273
|
+
# The template already creates a 'base' alternative.
|
|
274
|
+
# Add scenario and link it.
|
|
275
|
+
_count, errors = import_data(
|
|
276
|
+
db,
|
|
277
|
+
scenarios=[(scenario_name, True)],
|
|
278
|
+
scenario_alternatives=[(scenario_name, alternative_name)],
|
|
279
|
+
)
|
|
280
|
+
if errors:
|
|
281
|
+
logger.warning("Scenario import errors: %s", errors)
|
|
282
|
+
db.commit_session("Add scenario")
|
|
283
|
+
|
|
284
|
+
return url
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _add_matpower_data(
|
|
288
|
+
db: DatabaseMapping,
|
|
289
|
+
case: MatpowerCase,
|
|
290
|
+
alternative: str,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Populate a FlexTool DB with MATPOWER case entities and parameters."""
|
|
293
|
+
entities: list[tuple] = []
|
|
294
|
+
entity_alternatives: list[tuple] = []
|
|
295
|
+
parameter_values: list[tuple] = []
|
|
296
|
+
|
|
297
|
+
def add_entity(class_name: str, name: str | tuple) -> None:
|
|
298
|
+
"""Add an entity and its entity_alternative in one call."""
|
|
299
|
+
entities.append((class_name, name))
|
|
300
|
+
entity_alternatives.append((class_name, name, alternative, True))
|
|
301
|
+
|
|
302
|
+
# Find the reference (slack) bus
|
|
303
|
+
ref_bus_id: int | None = None
|
|
304
|
+
for bus in case.buses:
|
|
305
|
+
if bus.bus_type == 3:
|
|
306
|
+
ref_bus_id = bus.bus_id
|
|
307
|
+
break
|
|
308
|
+
if ref_bus_id is None:
|
|
309
|
+
# Fallback: use bus 1
|
|
310
|
+
ref_bus_id = case.buses[0].bus_id if case.buses else 1
|
|
311
|
+
|
|
312
|
+
# --- Bus nodes ---
|
|
313
|
+
for bus in case.buses:
|
|
314
|
+
node_name = f"bus_{bus.bus_id}"
|
|
315
|
+
add_entity("node", node_name)
|
|
316
|
+
|
|
317
|
+
# Enable node balance constraint
|
|
318
|
+
parameter_values.append(("node", node_name, "node_type", "balance", alternative))
|
|
319
|
+
|
|
320
|
+
if bus.pd != 0.0:
|
|
321
|
+
# Negative inflow = demand in FlexTool
|
|
322
|
+
parameter_values.append(("node", node_name, "inflow", -bus.pd, alternative))
|
|
323
|
+
|
|
324
|
+
# Large penalties to avoid slack usage
|
|
325
|
+
parameter_values.append(("node", node_name, "penalty_up", 100000.0, alternative))
|
|
326
|
+
parameter_values.append(("node", node_name, "penalty_down", 100000.0, alternative))
|
|
327
|
+
|
|
328
|
+
# --- Generators (only those with Pmax > 0 and status=1) ---
|
|
329
|
+
gen_index = 0
|
|
330
|
+
for gen in case.generators:
|
|
331
|
+
gen_index += 1
|
|
332
|
+
if gen.pmax <= 0 or gen.status == 0:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
gen_name = f"gen_{gen_index}"
|
|
336
|
+
commodity_node_name = f"commodity_gen{gen_index}"
|
|
337
|
+
commodity_name = f"fuel_gen{gen_index}"
|
|
338
|
+
bus_node_name = f"bus_{gen.bus}"
|
|
339
|
+
|
|
340
|
+
# Linear cost coefficient (c1) — $/MWh
|
|
341
|
+
c1 = 0.0
|
|
342
|
+
if len(gen.cost_coeffs) >= 2:
|
|
343
|
+
# For n=3: coeffs = [c2, c1, c0]
|
|
344
|
+
c1 = gen.cost_coeffs[-2] # c1 is the second-to-last
|
|
345
|
+
|
|
346
|
+
# Commodity and commodity node. The node must declare
|
|
347
|
+
# ``node_type = "commodity"`` — otherwise FlexTool treats it as a
|
|
348
|
+
# default ``none`` balance node with no source of inflow, and the
|
|
349
|
+
# unit's input flow ends up served by ``vq_state_up`` slack at
|
|
350
|
+
# the bus's ``penalty_up``. See LATENT BUG B2.
|
|
351
|
+
add_entity("commodity", commodity_name)
|
|
352
|
+
add_entity("node", commodity_node_name)
|
|
353
|
+
add_entity("commodity__node", (commodity_name, commodity_node_name))
|
|
354
|
+
|
|
355
|
+
parameter_values.append((
|
|
356
|
+
"node", commodity_node_name, "node_type", "commodity", alternative,
|
|
357
|
+
))
|
|
358
|
+
|
|
359
|
+
# Set commodity price = marginal cost
|
|
360
|
+
parameter_values.append(("commodity", commodity_name, "price", c1, alternative))
|
|
361
|
+
|
|
362
|
+
# Unit
|
|
363
|
+
add_entity("unit", gen_name)
|
|
364
|
+
add_entity("unit__inputNode", (gen_name, commodity_node_name))
|
|
365
|
+
add_entity("unit__outputNode", (gen_name, bus_node_name))
|
|
366
|
+
|
|
367
|
+
# Unit parameters
|
|
368
|
+
parameter_values.append(("unit", gen_name, "existing", gen.pmax, alternative))
|
|
369
|
+
parameter_values.append(("unit", gen_name, "conversion_method", "constant_efficiency", alternative))
|
|
370
|
+
parameter_values.append(("unit", gen_name, "efficiency", 1.0, alternative))
|
|
371
|
+
|
|
372
|
+
# --- Branches / connections ---
|
|
373
|
+
for branch in case.branches:
|
|
374
|
+
if branch.status != 1:
|
|
375
|
+
continue
|
|
376
|
+
if branch.x == 0.0:
|
|
377
|
+
# Zero reactance means infinite susceptance — skip
|
|
378
|
+
logger.warning("Skipping branch %d-%d with zero reactance", branch.fbus, branch.tbus)
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
conn_name = f"line_{branch.fbus}_{branch.tbus}"
|
|
382
|
+
from_node = f"bus_{branch.fbus}"
|
|
383
|
+
to_node = f"bus_{branch.tbus}"
|
|
384
|
+
|
|
385
|
+
add_entity("connection", conn_name)
|
|
386
|
+
add_entity("connection__node__node", (conn_name, from_node, to_node))
|
|
387
|
+
|
|
388
|
+
parameter_values.append(("connection", conn_name, "existing", branch.rate_a, alternative))
|
|
389
|
+
parameter_values.append(("connection", conn_name, "reactance", branch.x, alternative))
|
|
390
|
+
parameter_values.append(("connection", conn_name, "transfer_method", "regular", alternative))
|
|
391
|
+
|
|
392
|
+
# --- DC power flow group ---
|
|
393
|
+
group_name = "ac_network"
|
|
394
|
+
add_entity("group", group_name)
|
|
395
|
+
|
|
396
|
+
for bus in case.buses:
|
|
397
|
+
add_entity("group__node", (group_name, f"bus_{bus.bus_id}"))
|
|
398
|
+
|
|
399
|
+
parameter_values.append(("group", group_name, "transfer_method", "dc_power_flow_with_angles", alternative))
|
|
400
|
+
parameter_values.append(("group", group_name, "base_MVA", case.base_mva, alternative))
|
|
401
|
+
parameter_values.append(("group", group_name, "reference_node", f"bus_{ref_bus_id}", alternative))
|
|
402
|
+
|
|
403
|
+
# --- Timeline / timeset / solve / model ---
|
|
404
|
+
timeline_name = "tl_dispatch"
|
|
405
|
+
timeset_name = "ts_dispatch"
|
|
406
|
+
solve_name = "dispatch"
|
|
407
|
+
model_name = "flexTool"
|
|
408
|
+
period_name = "p1"
|
|
409
|
+
|
|
410
|
+
# Timeline entity with a single 1-hour timestep
|
|
411
|
+
add_entity("timeline", timeline_name)
|
|
412
|
+
timestep_duration_map = Map(["t0001"], [1.0])
|
|
413
|
+
parameter_values.append(("timeline", timeline_name, "timestep_duration", timestep_duration_map, alternative))
|
|
414
|
+
|
|
415
|
+
# Timeset entity linking to the timeline
|
|
416
|
+
add_entity("timeset", timeset_name)
|
|
417
|
+
parameter_values.append(("timeset", timeset_name, "timeline", timeline_name, alternative))
|
|
418
|
+
# timeset_duration: map from first timestep to count of steps
|
|
419
|
+
timeset_duration_map = Map(["t0001"], [1.0])
|
|
420
|
+
parameter_values.append(("timeset", timeset_name, "timeset_duration", timeset_duration_map, alternative))
|
|
421
|
+
|
|
422
|
+
# Solve entity
|
|
423
|
+
add_entity("solve", solve_name)
|
|
424
|
+
# period_timeset: map period -> timeset
|
|
425
|
+
period_timeset_map = Map([period_name], [timeset_name])
|
|
426
|
+
parameter_values.append(("solve", solve_name, "period_timeset", period_timeset_map, alternative))
|
|
427
|
+
parameter_values.append(("solve", solve_name, "solve_mode", "single_solve", alternative))
|
|
428
|
+
# realized_periods
|
|
429
|
+
realized_periods = Array([period_name])
|
|
430
|
+
parameter_values.append(("solve", solve_name, "realized_periods", realized_periods, alternative))
|
|
431
|
+
|
|
432
|
+
# Model entity
|
|
433
|
+
add_entity("model", model_name)
|
|
434
|
+
parameter_values.append(("model", model_name, "version", FLEXTOOL_DB_VERSION, alternative))
|
|
435
|
+
parameter_values.append(("model", model_name, "inflation_rate", 0.0, alternative))
|
|
436
|
+
# model.solves
|
|
437
|
+
solves_array = Array([solve_name])
|
|
438
|
+
parameter_values.append(("model", model_name, "solves", solves_array, alternative))
|
|
439
|
+
|
|
440
|
+
# --- Import everything ---
|
|
441
|
+
count, errors = import_data(
|
|
442
|
+
db,
|
|
443
|
+
entities=entities,
|
|
444
|
+
entity_alternatives=entity_alternatives,
|
|
445
|
+
parameter_values=parameter_values,
|
|
446
|
+
)
|
|
447
|
+
if errors:
|
|
448
|
+
for err in errors:
|
|
449
|
+
logger.error("import_data error: %s", err)
|
|
450
|
+
raise RuntimeError(f"Failed to import MATPOWER data: {len(errors)} errors")
|
|
451
|
+
logger.info("Imported %d items into FlexTool DB", count)
|