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
flextool/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""FlexTool energy system optimization model package."""
|
|
2
|
+
import os as _os
|
|
3
|
+
# jemalloc decay config — set BEFORE the first ``import polars`` (which
|
|
4
|
+
# happens transitively in the imports just below). polars statically
|
|
5
|
+
# links jemalloc; jemalloc reads ``_RJEM_MALLOC_CONF`` exactly once at
|
|
6
|
+
# .so load. Its default ``dirty_decay_ms`` (~10 s) is longer than the
|
|
7
|
+
# gap between rolling-solve rolls, so each roll's freed GB-scale
|
|
8
|
+
# coefficient frames stay resident as dirty pages when the next roll
|
|
9
|
+
# starts allocating — the per-roll ``priv_dirty`` floor ratchets up
|
|
10
|
+
# (~+2 GB/roll on the 9-roll DES run). ``dirty_decay_ms:1000`` purges a
|
|
11
|
+
# roll's freed pages ~1 s after they go idle (well before the next roll,
|
|
12
|
+
# which takes minutes) without eagerly madvise-ing on every transient
|
|
13
|
+
# free inside a hot operation. ``muzzy_decay_ms:0`` forces MADV_DONTNEED
|
|
14
|
+
# (so Private_Dirty actually drops) rather than leaving pages as
|
|
15
|
+
# reclaimable-but-resident MADV_FREE. glibc knobs (MALLOC_ARENA_MAX,
|
|
16
|
+
# malloc_trim) do NOT touch polars memory — this is the lever that does.
|
|
17
|
+
# ``setdefault`` so a shell-provided value still wins (A/B profiling).
|
|
18
|
+
_os.environ.setdefault("_RJEM_MALLOC_CONF", "dirty_decay_ms:1000,muzzy_decay_ms:0")
|
|
19
|
+
import warnings as _warnings
|
|
20
|
+
# Silence requests' RequestsDependencyWarning. The FlexTool venv ships a
|
|
21
|
+
# ``requests`` whose pinned urllib3/charset_normalizer version ranges lag the
|
|
22
|
+
# (working) installed versions, so ``import requests`` warns once at import
|
|
23
|
+
# time. It is harmless but leaks into Spine Toolbox's Tool console for any
|
|
24
|
+
# path that pulls in requests (e.g. an ``http://`` Spine DB-server URL).
|
|
25
|
+
# Match by message — set BEFORE requests is imported transitively below — so
|
|
26
|
+
# we needn't import requests here just to reference its warning class, and the
|
|
27
|
+
# filter stays robust across the venv's exact version numbers.
|
|
28
|
+
_warnings.filterwarnings(
|
|
29
|
+
"ignore",
|
|
30
|
+
message=r"urllib3 .*doesn't match a supported version",
|
|
31
|
+
)
|
|
32
|
+
__all__ = [
|
|
33
|
+
'write_outputs',
|
|
34
|
+
'migrate_database',
|
|
35
|
+
'initialize_database',
|
|
36
|
+
'update_flextool',
|
|
37
|
+
]
|
|
38
|
+
from flextool.process_outputs import write_outputs # noqa: E402 (after env/warning config above)
|
|
39
|
+
from flextool.update_flextool import migrate_database, initialize_database, update_flextool # noqa: E402
|
|
40
|
+
|
|
41
|
+
name = "flextool"
|
flextool/_mem_sampler.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""In-process memory sampler — timestamped, allocator-aware memory trace.
|
|
2
|
+
|
|
3
|
+
RSS is a poor signal for this workload: it conflates shared, clean, and
|
|
4
|
+
allocator-reserved-but-free pages with genuinely-live private data. This
|
|
5
|
+
sampler additionally records, per sample:
|
|
6
|
+
|
|
7
|
+
* ``pss_gb`` / ``priv_dirty_gb`` / ``proc_swap_gb`` — from
|
|
8
|
+
``/proc/self/smaps_rollup``. ``Private_Dirty + Swap`` is the
|
|
9
|
+
allocator-agnostic "what this process truly owns" number (counts dirty
|
|
10
|
+
anon pages whether they live in glibc, mimalloc, or the C++ heap, and
|
|
11
|
+
whether resident or swapped).
|
|
12
|
+
* ``glibc_inuse_gb`` / ``glibc_free_gb`` / ``glibc_mmap_gb`` — from
|
|
13
|
+
glibc ``mallinfo2()`` (``uordblks`` / ``fordblks`` / ``hblkhd``). The
|
|
14
|
+
*live-vs-reserved* discriminator: ``fordblks`` is memory glibc has
|
|
15
|
+
freed but is holding on its arena free-list rather than returning to
|
|
16
|
+
the OS (what ``malloc_trim`` would release). NOTE: only sees the glibc
|
|
17
|
+
allocator — HiGHS/C++ go through glibc, but polars (Rust) may use its
|
|
18
|
+
own allocator, so a large ``priv_dirty`` with small ``glibc_inuse``
|
|
19
|
+
points at the non-glibc (polars) side.
|
|
20
|
+
* ``mem_used_gb`` — system used (``MemTotal - MemAvailable``), the number
|
|
21
|
+
most desktop system monitors plot. ``available_gb`` / ``swap_used_gb``
|
|
22
|
+
kept for back-compat.
|
|
23
|
+
|
|
24
|
+
Gated by ``FLEXTOOL_MEM_SAMPLER``; interval via
|
|
25
|
+
``FLEXTOOL_MEM_SAMPLER_INTERVAL_MS`` (default 100 ms).
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import ctypes
|
|
30
|
+
import datetime
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Mallinfo2(ctypes.Structure):
|
|
38
|
+
# glibc >= 2.33. All fields size_t (the legacy ``mallinfo`` used int
|
|
39
|
+
# and overflows past 2 GB — useless here).
|
|
40
|
+
_fields_ = [
|
|
41
|
+
(n, ctypes.c_size_t)
|
|
42
|
+
for n in (
|
|
43
|
+
"arena", "ordblks", "smblks", "hblks", "hblkhd", "usmblks",
|
|
44
|
+
"fsmblks", "uordblks", "fordblks", "keepcost",
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _make_mallinfo2():
|
|
50
|
+
"""Return a callable giving (inuse, free, mmap) bytes, or None."""
|
|
51
|
+
try:
|
|
52
|
+
libc = ctypes.CDLL("libc.so.6")
|
|
53
|
+
fn = libc.mallinfo2 # AttributeError on glibc < 2.33
|
|
54
|
+
fn.restype = _Mallinfo2
|
|
55
|
+
fn.argtypes = []
|
|
56
|
+
except (OSError, AttributeError):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _read():
|
|
60
|
+
mi = fn()
|
|
61
|
+
return mi.uordblks, mi.fordblks, mi.hblkhd
|
|
62
|
+
|
|
63
|
+
return _read
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def start_mem_sampler() -> None:
|
|
67
|
+
"""Start a daemon thread that samples memory at a fixed interval if FLEXTOOL_MEM_SAMPLER is set."""
|
|
68
|
+
if os.environ.get("FLEXTOOL_MEM_SAMPLER", "").strip().lower() in ("", "0", "false"):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
pid = os.getpid()
|
|
72
|
+
log_path = os.environ.get("FLEXTOOL_MEM_SAMPLER_LOG", f"/tmp/flextool_mem_sampler_{pid}.log")
|
|
73
|
+
try:
|
|
74
|
+
interval_ms = max(20, min(10000, int(os.environ.get("FLEXTOOL_MEM_SAMPLER_INTERVAL_MS", "100"))))
|
|
75
|
+
except ValueError:
|
|
76
|
+
interval_ms = 100
|
|
77
|
+
interval_s = interval_ms / 1000.0
|
|
78
|
+
|
|
79
|
+
if not sys.platform.startswith("linux"):
|
|
80
|
+
with open(log_path, "a", buffering=1) as _f:
|
|
81
|
+
_f.write(
|
|
82
|
+
f"# flextool memory sampler pid={pid} WARNING: platform is not "
|
|
83
|
+
f"linux — sampler disabled\n"
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
_GB = float(2 ** 30)
|
|
88
|
+
_mallinfo2 = _make_mallinfo2()
|
|
89
|
+
|
|
90
|
+
def _read_kv_bytes(path: str, keys: tuple[str, ...]) -> dict[str, int]:
|
|
91
|
+
"""Parse ``Key: N kB`` lines from a /proc file → bytes."""
|
|
92
|
+
out: dict[str, int] = {}
|
|
93
|
+
want = set(keys)
|
|
94
|
+
with open(path) as fh:
|
|
95
|
+
for line in fh:
|
|
96
|
+
k = line.split(":", 1)[0]
|
|
97
|
+
if k in want:
|
|
98
|
+
out[k] = int(line.split()[1]) * 1024
|
|
99
|
+
if len(out) == len(want):
|
|
100
|
+
break
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
def _sample() -> dict[str, float]:
|
|
104
|
+
# Per-process private cost — allocator-agnostic.
|
|
105
|
+
rss = pss = priv_dirty = proc_swap = 0
|
|
106
|
+
try:
|
|
107
|
+
r = _read_kv_bytes(
|
|
108
|
+
"/proc/self/smaps_rollup",
|
|
109
|
+
("Rss", "Pss", "Private_Dirty", "Swap"),
|
|
110
|
+
)
|
|
111
|
+
rss = r.get("Rss", 0)
|
|
112
|
+
pss = r.get("Pss", 0)
|
|
113
|
+
priv_dirty = r.get("Private_Dirty", 0)
|
|
114
|
+
proc_swap = r.get("Swap", 0)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
if rss == 0:
|
|
118
|
+
# smaps_rollup unavailable — fall back to VmRSS.
|
|
119
|
+
try:
|
|
120
|
+
rss = _read_kv_bytes("/proc/self/status", ("VmRSS",)).get("VmRSS", 0)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# System-wide.
|
|
125
|
+
mem_total = mem_avail = swap_total = swap_free = 0
|
|
126
|
+
try:
|
|
127
|
+
m = _read_kv_bytes(
|
|
128
|
+
"/proc/meminfo",
|
|
129
|
+
("MemTotal", "MemAvailable", "SwapTotal", "SwapFree"),
|
|
130
|
+
)
|
|
131
|
+
mem_total = m.get("MemTotal", 0)
|
|
132
|
+
mem_avail = m.get("MemAvailable", 0)
|
|
133
|
+
swap_total = m.get("SwapTotal", 0)
|
|
134
|
+
swap_free = m.get("SwapFree", 0)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
# glibc allocator breakdown — live vs reserved-free.
|
|
139
|
+
g_inuse = g_free = g_mmap = -1
|
|
140
|
+
if _mallinfo2 is not None:
|
|
141
|
+
try:
|
|
142
|
+
g_inuse, g_free, g_mmap = _mallinfo2()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"rss_gb": rss / _GB,
|
|
148
|
+
"pss_gb": pss / _GB,
|
|
149
|
+
"priv_dirty_gb": priv_dirty / _GB,
|
|
150
|
+
"proc_swap_gb": proc_swap / _GB,
|
|
151
|
+
"available_gb": mem_avail / _GB,
|
|
152
|
+
"mem_used_gb": (mem_total - mem_avail) / _GB,
|
|
153
|
+
"swap_used_gb": (swap_total - swap_free) / _GB,
|
|
154
|
+
"glibc_inuse_gb": g_inuse / _GB if g_inuse >= 0 else -1.0,
|
|
155
|
+
"glibc_free_gb": g_free / _GB if g_free >= 0 else -1.0,
|
|
156
|
+
"glibc_mmap_gb": g_mmap / _GB if g_mmap >= 0 else -1.0,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
started_at = datetime.datetime.now(datetime.UTC).isoformat()
|
|
160
|
+
# Stable column order so post-hoc parsing is trivial.
|
|
161
|
+
_cols = (
|
|
162
|
+
"rss_gb", "pss_gb", "priv_dirty_gb", "proc_swap_gb",
|
|
163
|
+
"available_gb", "mem_used_gb", "swap_used_gb",
|
|
164
|
+
"glibc_inuse_gb", "glibc_free_gb", "glibc_mmap_gb",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _loop(log_path: str, interval_s: float) -> None:
|
|
168
|
+
mono_start = time.monotonic()
|
|
169
|
+
with open(log_path, "a", buffering=1) as f:
|
|
170
|
+
f.write(
|
|
171
|
+
f"# flextool memory sampler pid={pid} started_at={started_at} "
|
|
172
|
+
f"interval_ms={interval_ms} mallinfo2={'yes' if _mallinfo2 else 'no'}\n"
|
|
173
|
+
)
|
|
174
|
+
f.write("# priv_dirty_gb+proc_swap_gb = true private cost; "
|
|
175
|
+
"glibc_free_gb = held-free (malloc_trim-able); "
|
|
176
|
+
"mem_used_gb = system used (monitor metric)\n")
|
|
177
|
+
while True:
|
|
178
|
+
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
|
179
|
+
mono_s = time.monotonic() - mono_start
|
|
180
|
+
try:
|
|
181
|
+
s = _sample()
|
|
182
|
+
parts = "\t".join(f"{c}={s[c]:.4f}" for c in _cols)
|
|
183
|
+
line = f"ts={ts}\tmono_s={mono_s:.3f}\tepoch={time.time():.3f}\t{parts}\n"
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
line = f"ts={ts}\tmono_s={mono_s:.3f}\tepoch={time.time():.3f}\terror={repr(exc)}\n"
|
|
186
|
+
f.write(line)
|
|
187
|
+
# Explicit flush so the last samples before SIGKILL reach disk;
|
|
188
|
+
# line-buffered mode alone won't flush until the process exits cleanly.
|
|
189
|
+
f.flush()
|
|
190
|
+
time.sleep(interval_s)
|
|
191
|
+
|
|
192
|
+
t = threading.Thread(target=_loop, args=(log_path, interval_s), name="flextool-mem-sampler", daemon=True)
|
|
193
|
+
t.start()
|
flextool/_resources.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Locate bundled FlexTool package data files.
|
|
2
|
+
|
|
3
|
+
Static FlexTool resources — schema JSONs, YAML/text templates, and
|
|
4
|
+
canonical-database sources (all under ``flextool/schemas/``), plus the HiGHS
|
|
5
|
+
options template (``flextool/bin/``) — must be reached through
|
|
6
|
+
:mod:`importlib.resources` rather than ``Path(__file__).resolve()``
|
|
7
|
+
walks. After ``pip install flextool`` the package is the only thing
|
|
8
|
+
on ``sys.path``; the historical layout where these dirs sat at the
|
|
9
|
+
*repo root* alongside the package no longer exists.
|
|
10
|
+
|
|
11
|
+
Two helpers are exposed:
|
|
12
|
+
|
|
13
|
+
``package_data_path(relative)``
|
|
14
|
+
Returns a :class:`pathlib.Path` to a resource inside the
|
|
15
|
+
``flextool`` package, e.g.
|
|
16
|
+
``package_data_path("schemas/default_plots.yaml")``.
|
|
17
|
+
Editable installs return the real on-disk path; wheel installs
|
|
18
|
+
return the unpacked site-packages path (importlib.resources
|
|
19
|
+
materialises zipped data on demand, but we never ship zipped so
|
|
20
|
+
in practice this is always a stable filesystem path).
|
|
21
|
+
|
|
22
|
+
``package_data_text(relative)``
|
|
23
|
+
Convenience wrapper that returns the file contents as a string —
|
|
24
|
+
handy when the caller doesn't actually need a path (YAML/JSON
|
|
25
|
+
parsing).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from importlib import resources
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import flextool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def package_data_path(relative: str) -> Path:
|
|
37
|
+
"""Path to ``flextool/<relative>`` inside the installed package."""
|
|
38
|
+
return Path(resources.files(flextool).joinpath(relative))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def package_data_text(relative: str) -> str:
|
|
42
|
+
"""Text contents of ``flextool/<relative>``."""
|
|
43
|
+
return resources.files(flextool).joinpath(relative).read_text(encoding="utf-8")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""FlexTool energy-margin calibrator package.
|
|
2
|
+
|
|
3
|
+
The calibrator drives an investment+dispatch solve each iteration by
|
|
4
|
+
shelling out to :mod:`flextool.cli.cmd_run_flextool`, then reads the
|
|
5
|
+
per-node unserved-energy slack from the produced outputs to decide how to
|
|
6
|
+
adjust the energy margin. This package hosts that loop (the ``C1`` CLI,
|
|
7
|
+
``python -m flextool.calibrate``, lands here) and its building blocks.
|
|
8
|
+
|
|
9
|
+
The first building block is the resilient solve-success detector in
|
|
10
|
+
:mod:`flextool.calibrate._solve_status` — it decides "did this solve
|
|
11
|
+
actually succeed?" from the solve's *output directory* plus the (weak)
|
|
12
|
+
subprocess exit code, tolerating a known model-specific post-solve writer
|
|
13
|
+
crash that can bubble to a nonzero exit even when every output is present.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from flextool.calibrate._final_outputs import (
|
|
19
|
+
FINAL_WRITE_METHOD_CHOICES,
|
|
20
|
+
write_final_outputs,
|
|
21
|
+
)
|
|
22
|
+
from flextool.calibrate._loop import (
|
|
23
|
+
CalibConfig,
|
|
24
|
+
CalibError,
|
|
25
|
+
CalibResult,
|
|
26
|
+
IterRecord,
|
|
27
|
+
run_calibration,
|
|
28
|
+
)
|
|
29
|
+
from flextool.calibrate._report import format_summary, write_report
|
|
30
|
+
from flextool.calibrate._solve_status import (
|
|
31
|
+
OutputCheck,
|
|
32
|
+
SolveOutcome,
|
|
33
|
+
assess_solve,
|
|
34
|
+
default_required_outputs,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"FINAL_WRITE_METHOD_CHOICES",
|
|
39
|
+
"CalibConfig",
|
|
40
|
+
"CalibError",
|
|
41
|
+
"CalibResult",
|
|
42
|
+
"IterRecord",
|
|
43
|
+
"OutputCheck",
|
|
44
|
+
"SolveOutcome",
|
|
45
|
+
"assess_solve",
|
|
46
|
+
"default_required_outputs",
|
|
47
|
+
"format_summary",
|
|
48
|
+
"run_calibration",
|
|
49
|
+
"write_final_outputs",
|
|
50
|
+
"write_report",
|
|
51
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""``python -m flextool.calibrate`` entry point.
|
|
2
|
+
|
|
3
|
+
Delegates to :func:`flextool.calibrate._cli.main` so the calibrator can be
|
|
4
|
+
launched as a module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from flextool.calibrate._cli import main
|
|
10
|
+
|
|
11
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Command-line front end for the adequacy-margin calibrator (C2).
|
|
2
|
+
|
|
3
|
+
Wires the argument surface onto :class:`~flextool.calibrate._loop.CalibConfig`,
|
|
4
|
+
runs :func:`~flextool.calibrate._loop.run_calibration`, then renders the
|
|
5
|
+
report CSVs and prints the human-readable summary. Exposed as
|
|
6
|
+
``python -m flextool.calibrate`` via :mod:`flextool.calibrate.__main__`.
|
|
7
|
+
|
|
8
|
+
The calibrator solves ``--iterations + 1`` times (iteration 0 is the
|
|
9
|
+
baseline) and, for every shedding node, sizes an ``energy_margin_adder``
|
|
10
|
+
increment that injects that node's residual unserved energy back as demand,
|
|
11
|
+
damped and guarded against over-build. This module is the operator's entry
|
|
12
|
+
point to that loop.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from flextool.calibrate._loop import (
|
|
22
|
+
CalibConfig,
|
|
23
|
+
CalibError,
|
|
24
|
+
CalibResult,
|
|
25
|
+
run_calibration,
|
|
26
|
+
)
|
|
27
|
+
from flextool.calibrate._final_outputs import (
|
|
28
|
+
FINAL_WRITE_METHOD_CHOICES,
|
|
29
|
+
write_final_outputs,
|
|
30
|
+
)
|
|
31
|
+
from flextool.calibrate._report import format_summary, write_report
|
|
32
|
+
|
|
33
|
+
DEFAULT_WORK_DIR = Path("calib_work")
|
|
34
|
+
DEFAULT_OUT_ROOT = Path("calib_out")
|
|
35
|
+
# Small positive default: convergence when total residual unserved energy is
|
|
36
|
+
# at/below this many MWh (a single-MWh floor absorbs solver noise).
|
|
37
|
+
DEFAULT_SLACK_THRESHOLD_MWH = 1.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
41
|
+
"""Build the argument parser for the calibrator CLI."""
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
prog="flextool.calibrate",
|
|
44
|
+
description=(
|
|
45
|
+
"Calibrate per-node energy-margin adders until every node's "
|
|
46
|
+
"residual unserved energy falls to/under the slack threshold, "
|
|
47
|
+
"freezing resource-capped nodes rather than over-building demand."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"db",
|
|
52
|
+
help="SpineDB URL (or path) of the model to calibrate.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"scenario",
|
|
56
|
+
help="Name of the scenario (model instance) to calibrate.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--iterations",
|
|
60
|
+
type=int,
|
|
61
|
+
required=True,
|
|
62
|
+
help=(
|
|
63
|
+
"Number of ADJUSTMENT iterations after the baseline; the loop "
|
|
64
|
+
"runs iterations+1 solves (iteration 0 is the baseline)."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--slack-threshold",
|
|
69
|
+
type=float,
|
|
70
|
+
default=DEFAULT_SLACK_THRESHOLD_MWH,
|
|
71
|
+
dest="slack_threshold_mwh",
|
|
72
|
+
help=(
|
|
73
|
+
"Convergence threshold in MWh: stop once total residual unserved "
|
|
74
|
+
"energy is at/under this value (default: "
|
|
75
|
+
f"{DEFAULT_SLACK_THRESHOLD_MWH})."
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--stall-fraction",
|
|
80
|
+
type=float,
|
|
81
|
+
default=0.05,
|
|
82
|
+
dest="stall_fraction",
|
|
83
|
+
help=(
|
|
84
|
+
"Over-build guard STALL fraction [0..1]. A shedding node whose "
|
|
85
|
+
"residual unserved energy drops by LESS than this fraction of its "
|
|
86
|
+
"prior gap in response to its own bump is frozen as resource-capped "
|
|
87
|
+
"(margin buys it no adequacy) and reported as needing firm "
|
|
88
|
+
"capacity / imports / storage. HIGHER freezes a stalled node "
|
|
89
|
+
"SOONER; 0.0 disables the guard. Default: 0.05."
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--over-build-tightness",
|
|
94
|
+
type=float,
|
|
95
|
+
default=0.05,
|
|
96
|
+
dest="over_build_tightness",
|
|
97
|
+
help=(
|
|
98
|
+
"DEPRECATED / no-op: retained for compatibility only. The "
|
|
99
|
+
"over-build guard now freezes on residual STALL (see "
|
|
100
|
+
"--stall-fraction), not curtailment efficiency, so this value is "
|
|
101
|
+
"not consulted. Default: 0.05."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--damping-first-iteration",
|
|
106
|
+
type=float,
|
|
107
|
+
default=1.0,
|
|
108
|
+
dest="damping_first",
|
|
109
|
+
help=(
|
|
110
|
+
"Damping factor lambda applied to the FIRST correction "
|
|
111
|
+
"(increment = lambda * residual / W). 1.0 = full undamped step. "
|
|
112
|
+
"Default: 1.0."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--damping-remaining-iterations",
|
|
117
|
+
type=float,
|
|
118
|
+
default=0.5,
|
|
119
|
+
dest="damping_remaining",
|
|
120
|
+
help=(
|
|
121
|
+
"Damping factor lambda applied to every correction AFTER the "
|
|
122
|
+
"first; lower damps oscillation as slack nears zero. Default: 0.5."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--overshoot",
|
|
127
|
+
type=float,
|
|
128
|
+
default=1.0,
|
|
129
|
+
dest="overshoot",
|
|
130
|
+
help=(
|
|
131
|
+
"Planning-margin SAFETY multiplier on the sized margin (default "
|
|
132
|
+
"1.0 = off). A single-year (or single-year representative-period) "
|
|
133
|
+
"model under-estimates true multi-year severity, so a value >1 "
|
|
134
|
+
"deliberately over-provisions: overshoot=1.2 builds ~20%% beyond "
|
|
135
|
+
"the measured slack. Higher builds more headroom for unmodeled "
|
|
136
|
+
"multi-year risk; the right value is MODEL-DEPENDENT."
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--sizing",
|
|
141
|
+
choices=("uniform", "timed"),
|
|
142
|
+
default="uniform",
|
|
143
|
+
dest="sizing",
|
|
144
|
+
help=(
|
|
145
|
+
"Adder placement mode. 'uniform' (default): a constant "
|
|
146
|
+
"per-timestep margin sized lambda*residual/W. 'timed': the SAME "
|
|
147
|
+
"total energy placed per-cell at the low-VRE stress hours, folded "
|
|
148
|
+
"from node_slack_up_dt_e onto the representative timeline."
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--warm-start-cache-dir",
|
|
153
|
+
type=Path,
|
|
154
|
+
default=None,
|
|
155
|
+
dest="warm_start_cache_dir",
|
|
156
|
+
help=(
|
|
157
|
+
"Directory for the warm-start basis cache shared across "
|
|
158
|
+
"iterations (default: <work-dir>/warm_start_cache)."
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--work-dir",
|
|
163
|
+
type=Path,
|
|
164
|
+
default=DEFAULT_WORK_DIR,
|
|
165
|
+
dest="work_dir",
|
|
166
|
+
help=(
|
|
167
|
+
"Subprocess working directory for the per-iteration solves "
|
|
168
|
+
f"(default: {DEFAULT_WORK_DIR}/)."
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--output-location",
|
|
173
|
+
type=Path,
|
|
174
|
+
default=DEFAULT_OUT_ROOT,
|
|
175
|
+
dest="out_root",
|
|
176
|
+
help=(
|
|
177
|
+
"Output-location root; solve outputs land under "
|
|
178
|
+
"<output-location>/output_parquet/<scenario>/ and the report "
|
|
179
|
+
"CSVs under <output-location>/calibration_reports/<scenario>/ "
|
|
180
|
+
f"(default: {DEFAULT_OUT_ROOT}/)."
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--final-write-methods",
|
|
185
|
+
type=str,
|
|
186
|
+
nargs="+",
|
|
187
|
+
default=["csv"],
|
|
188
|
+
choices=list(FINAL_WRITE_METHOD_CHOICES),
|
|
189
|
+
dest="final_write_methods",
|
|
190
|
+
help=(
|
|
191
|
+
"After the loop, regenerate these output formats from the final "
|
|
192
|
+
"surviving parquet (<output-location>/output_parquet/<scenario>/) "
|
|
193
|
+
"WITHOUT re-solving — so the calibrated model's results are "
|
|
194
|
+
"available just like a regular run's. parquet is always present, "
|
|
195
|
+
"so it is not a choice here; the rest derive from it. "
|
|
196
|
+
"Default: csv. Use --skip-final-outputs to leave the results "
|
|
197
|
+
"parquet-only."
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--skip-final-outputs",
|
|
202
|
+
action="store_true",
|
|
203
|
+
dest="skip_final_outputs",
|
|
204
|
+
help=(
|
|
205
|
+
"Do not regenerate any non-parquet outputs after the loop; leave "
|
|
206
|
+
"the results as the parquet tree written by the final solve."
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"--results-db-url",
|
|
211
|
+
type=str,
|
|
212
|
+
default=None,
|
|
213
|
+
dest="results_db_url",
|
|
214
|
+
help=(
|
|
215
|
+
"Target SpineDB URL for the 'spinedb' final-write-method (default: "
|
|
216
|
+
"<output-location>/results.sqlite). Only consulted when 'spinedb' "
|
|
217
|
+
"is in --final-write-methods."
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--debug",
|
|
222
|
+
action="store_true",
|
|
223
|
+
help=(
|
|
224
|
+
"Archive each iteration's outputs to out_iter_<k>/ instead of "
|
|
225
|
+
"clearing them between iterations."
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
return parser
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _config_from_args(args: argparse.Namespace) -> CalibConfig:
|
|
232
|
+
"""Translate parsed args into a :class:`CalibConfig`.
|
|
233
|
+
|
|
234
|
+
Fills the warm-start cache default relative to the work dir when the
|
|
235
|
+
operator did not pin one.
|
|
236
|
+
"""
|
|
237
|
+
warm_start_cache_dir = args.warm_start_cache_dir
|
|
238
|
+
if warm_start_cache_dir is None:
|
|
239
|
+
warm_start_cache_dir = Path(args.work_dir) / "warm_start_cache"
|
|
240
|
+
# --skip-final-outputs wins over any requested formats.
|
|
241
|
+
final_write_methods = (
|
|
242
|
+
() if args.skip_final_outputs else tuple(args.final_write_methods)
|
|
243
|
+
)
|
|
244
|
+
return CalibConfig(
|
|
245
|
+
iterations=args.iterations,
|
|
246
|
+
slack_threshold_mwh=args.slack_threshold_mwh,
|
|
247
|
+
damping_first=args.damping_first,
|
|
248
|
+
damping_remaining=args.damping_remaining,
|
|
249
|
+
over_build_tightness=args.over_build_tightness,
|
|
250
|
+
warm_start_cache_dir=Path(warm_start_cache_dir),
|
|
251
|
+
work_dir=Path(args.work_dir),
|
|
252
|
+
out_root=Path(args.out_root),
|
|
253
|
+
debug=args.debug,
|
|
254
|
+
sizing=args.sizing,
|
|
255
|
+
overshoot=args.overshoot,
|
|
256
|
+
stall_fraction=args.stall_fraction,
|
|
257
|
+
final_write_methods=final_write_methods,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main(argv: list[str] | None = None) -> int:
|
|
262
|
+
"""Run the calibrator from the command line.
|
|
263
|
+
|
|
264
|
+
Parses *argv* (or ``sys.argv``), runs the calibration, writes the report
|
|
265
|
+
CSVs under ``<output-location>/calibration_reports/<scenario>/``, prints the
|
|
266
|
+
summary to stdout, and (unless ``--skip-final-outputs``) regenerates the
|
|
267
|
+
requested non-parquet output formats from the final surviving parquet
|
|
268
|
+
without re-solving. Returns 0 on success; on a fail-closed solve
|
|
269
|
+
(:class:`CalibError`) prints the reason to stderr and returns 1.
|
|
270
|
+
"""
|
|
271
|
+
parser = build_parser()
|
|
272
|
+
args = parser.parse_args(argv)
|
|
273
|
+
config = _config_from_args(args)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
result: CalibResult = run_calibration(args.db, args.scenario, config)
|
|
277
|
+
except CalibError as exc:
|
|
278
|
+
print(f"calibration failed: {exc}", file=sys.stderr)
|
|
279
|
+
return 1
|
|
280
|
+
|
|
281
|
+
# Namespace the report CSVs by scenario (matching output_parquet/<scenario>/)
|
|
282
|
+
# so calibrating several scenarios into one shared output-location root does
|
|
283
|
+
# not overwrite one scenario's report with the next.
|
|
284
|
+
report_dir = Path(config.out_root) / "calibration_reports" / args.scenario
|
|
285
|
+
write_report(result, out_dir=report_dir)
|
|
286
|
+
print(format_summary(result))
|
|
287
|
+
print(f"\nReport CSVs written under {report_dir}/")
|
|
288
|
+
|
|
289
|
+
# Regenerate the requested regular output formats from the final surviving
|
|
290
|
+
# parquet (no re-solve). A failure here does NOT fail the calibration — the
|
|
291
|
+
# solve succeeded and its results are intact as parquet, so this is a
|
|
292
|
+
# convenience step; warn and still return 0.
|
|
293
|
+
if config.final_write_methods:
|
|
294
|
+
try:
|
|
295
|
+
written = write_final_outputs(
|
|
296
|
+
config.out_root,
|
|
297
|
+
args.scenario,
|
|
298
|
+
config.final_write_methods,
|
|
299
|
+
results_db_url=args.results_db_url,
|
|
300
|
+
)
|
|
301
|
+
print(
|
|
302
|
+
f"Final outputs ({', '.join(written)}) regenerated from parquet "
|
|
303
|
+
f"under {config.out_root}/"
|
|
304
|
+
)
|
|
305
|
+
except Exception as exc: # noqa: BLE001 — never fail the run on output regen
|
|
306
|
+
print(
|
|
307
|
+
f"warning: could not regenerate final outputs "
|
|
308
|
+
f"({', '.join(config.final_write_methods)}) from parquet: {exc}. "
|
|
309
|
+
f"The parquet results under {config.out_root}/output_parquet/"
|
|
310
|
+
f"{args.scenario}/ are intact.",
|
|
311
|
+
file=sys.stderr,
|
|
312
|
+
)
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
__all__ = ["build_parser", "main"]
|