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,223 @@
|
|
|
1
|
+
"""Pure post-solve readers for the adequacy calibrator.
|
|
2
|
+
|
|
3
|
+
Each function takes the directory that holds a solve's result parquets
|
|
4
|
+
(``<out_root>/output_parquet/<scenario>/``) and returns plain Python
|
|
5
|
+
dicts. Nothing here solves, mutates the DB, or touches the network — they
|
|
6
|
+
are pure readers, so the loop can call them once per iteration and the
|
|
7
|
+
tests can point them straight at real parquets.
|
|
8
|
+
|
|
9
|
+
Why :func:`flextool.lean_parquet.read_lean_parquet` and not
|
|
10
|
+
``pd.read_parquet``
|
|
11
|
+
-------------------------------------------------------------------
|
|
12
|
+
FlexTool writes these tables with a compact custom footer that records the
|
|
13
|
+
MultiIndex level names; a raw ``pd.read_parquet`` returns flat
|
|
14
|
+
string-tuple column names and loses the level structure the group-bys here
|
|
15
|
+
rely on. :func:`read_lean_parquet` reconstructs the real row/column
|
|
16
|
+
MultiIndex, so ``groupby(level=...)`` works.
|
|
17
|
+
|
|
18
|
+
Signals
|
|
19
|
+
-------
|
|
20
|
+
* ``node_slack_up_d_e`` — per-node unserved-energy up-slack (annual MWh,
|
|
21
|
+
≥0). Dense and always present on a valid solve → the robust adequacy
|
|
22
|
+
signal the loop steers on.
|
|
23
|
+
* ``unit_curtailment_outputNode_d_ee`` — per-(unit, sink) curtailment
|
|
24
|
+
(MWh); OPTIONAL (absent when nothing curtails). Provided now for the
|
|
25
|
+
over-build guard (C1c) to consume.
|
|
26
|
+
* ``cost_node_discounted_d_ec`` — discounted per-(node, category) node
|
|
27
|
+
cost (M€); its ``"upward slack penalty"`` category is the monetised
|
|
28
|
+
slack. This table is legitimately ABSENT on a zero-slack solve
|
|
29
|
+
(out_costs.py skips an empty category) → treated as zero penalty, never
|
|
30
|
+
an error.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from flextool.lean_parquet import read_lean_parquet
|
|
38
|
+
|
|
39
|
+
# Registry keys of the parquet tables read here. Resolved to on-disk
|
|
40
|
+
# basenames through P2's registry helper so a schema rename breaks loudly
|
|
41
|
+
# here rather than silently missing a file.
|
|
42
|
+
_SLACK_KEY = "node_slack_up_d_e"
|
|
43
|
+
_SLACK_KEY_DT = "node_slack_up_dt_e"
|
|
44
|
+
_CURTAILMENT_KEY = "unit_curtailment_outputNode_d_ee"
|
|
45
|
+
_COST_KEY = "cost_node_discounted_d_ec"
|
|
46
|
+
_UNIT_CAPACITY_KEY = "unit_capacity_ed_p"
|
|
47
|
+
_TOTAL_COST_KEY = "costs_discounted_p_"
|
|
48
|
+
|
|
49
|
+
_SLACK_PENALTY_CATEGORY = "upward slack penalty"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_filename(key: str) -> str:
|
|
53
|
+
"""Resolve an output *key* to its ``<key>.parquet`` basename.
|
|
54
|
+
|
|
55
|
+
Prefers P2's registry helper
|
|
56
|
+
(``flextool.calibrate._solve_status._registry_filename``) so an
|
|
57
|
+
OUTPUT_TRANSFORM rename surfaces as a clear error; falls back to the
|
|
58
|
+
``<key>.parquet`` basename (which matches the OUTPUT_TRANSFORM keys) if
|
|
59
|
+
that private helper is ever removed.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
from flextool.calibrate._solve_status import _registry_filename
|
|
63
|
+
|
|
64
|
+
return _registry_filename(key)
|
|
65
|
+
except (ImportError, AttributeError):
|
|
66
|
+
return f"{key}.parquet"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_residual_unserved(assess_dir: Path) -> dict[str, float]:
|
|
70
|
+
"""Return ``{node: total_unserved_MWh}`` from ``node_slack_up_d_e``.
|
|
71
|
+
|
|
72
|
+
Sums each node's up-slack over all periods and drops the ``scenario``
|
|
73
|
+
column level, so the result is keyed by node. This is the robust,
|
|
74
|
+
always-present adequacy signal the loop converges on.
|
|
75
|
+
"""
|
|
76
|
+
path = Path(assess_dir) / _resolve_filename(_SLACK_KEY)
|
|
77
|
+
df = read_lean_parquet(path)
|
|
78
|
+
# columns: (scenario, node); index: period. Sum over periods, then
|
|
79
|
+
# collapse any scenario level so the key is the node name.
|
|
80
|
+
totals = df.sum(axis=0).groupby(level="node").sum()
|
|
81
|
+
return {str(node): float(val) for node, val in totals.items()}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_residual_unserved_dt(assess_dir: Path) -> dict[str, "object"]:
|
|
85
|
+
"""Return ``{node: DataFrame[period, time, value]}`` from ``node_slack_up_dt_e``.
|
|
86
|
+
|
|
87
|
+
The per-``(period, time)`` companion to :func:`read_residual_unserved`:
|
|
88
|
+
where that reader collapses the up-slack to one annual MWh per node, this
|
|
89
|
+
one keeps the FULL per-timestep profile UNFOLDED onto the base timeline —
|
|
90
|
+
the stress SHAPE the ``timed`` sizer redistributes the additive margin
|
|
91
|
+
over. ``node_slack_up_dt_e`` has row index ``(period, time)`` and column
|
|
92
|
+
levels ``(scenario, node)``; per node the scenario level is collapsed
|
|
93
|
+
(summed) and the result returned as a tidy pandas frame with columns
|
|
94
|
+
``period``, ``time``, ``value`` (one row per non-null base cell).
|
|
95
|
+
|
|
96
|
+
Read with :func:`read_lean_parquet` (never ``pd.read_parquet``) so the
|
|
97
|
+
``(period, time)`` / ``(scenario, node)`` MultiIndex is reconstructed.
|
|
98
|
+
A node absent from the table (no slack rows) simply has no key.
|
|
99
|
+
"""
|
|
100
|
+
path = Path(assess_dir) / _resolve_filename(_SLACK_KEY_DT)
|
|
101
|
+
df = read_lean_parquet(path)
|
|
102
|
+
out: dict[str, object] = {}
|
|
103
|
+
node_level = df.columns.get_level_values("node")
|
|
104
|
+
for node in dict.fromkeys(node_level): # ordered-unique node names
|
|
105
|
+
sub = df.loc[:, node_level == node]
|
|
106
|
+
# Collapse any scenario level → one value per (period, time) row.
|
|
107
|
+
series = sub.sum(axis=1)
|
|
108
|
+
frame = series.reset_index()
|
|
109
|
+
frame.columns = ["period", "time", "value"]
|
|
110
|
+
out[str(node)] = frame
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_curtailment_by_sink(assess_dir: Path) -> dict[str, float]:
|
|
115
|
+
"""Return ``{sink_node: total_curtailment_MWh}`` or ``{}`` if absent.
|
|
116
|
+
|
|
117
|
+
Reads ``unit_curtailment_outputNode_d_ee`` (columns
|
|
118
|
+
``(scenario, unit, sink)``), sums each column over periods and groups
|
|
119
|
+
the totals by ``sink`` node. The file is OPTIONAL (no curtailment ⇒ no
|
|
120
|
+
table); a missing file yields ``{}``. Consumed by the over-build guard
|
|
121
|
+
(C1c).
|
|
122
|
+
"""
|
|
123
|
+
path = Path(assess_dir) / _resolve_filename(_CURTAILMENT_KEY)
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
return {}
|
|
126
|
+
df = read_lean_parquet(path)
|
|
127
|
+
totals = df.sum(axis=0).groupby(level="sink").sum()
|
|
128
|
+
return {str(sink): float(val) for sink, val in totals.items()}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_slack_penalty(assess_dir: Path) -> tuple[float, dict[str, float]]:
|
|
132
|
+
"""Return ``(total_Meur, {node: penalty_Meur})`` from the node-cost table.
|
|
133
|
+
|
|
134
|
+
Reads ``cost_node_discounted_d_ec`` (row index ``(period, node)``,
|
|
135
|
+
columns ``(scenario, category)``), selects the
|
|
136
|
+
``"upward slack penalty"`` category and sums it per node (over periods
|
|
137
|
+
and scenarios). This table is legitimately ABSENT on a zero-slack
|
|
138
|
+
solve (out_costs.py skips an empty cost category) → returns
|
|
139
|
+
``(0.0, {})`` rather than raising.
|
|
140
|
+
"""
|
|
141
|
+
path = Path(assess_dir) / _resolve_filename(_COST_KEY)
|
|
142
|
+
if not path.is_file():
|
|
143
|
+
return 0.0, {}
|
|
144
|
+
df = read_lean_parquet(path)
|
|
145
|
+
is_penalty = df.columns.get_level_values("category") == _SLACK_PENALTY_CATEGORY
|
|
146
|
+
penalty = df.loc[:, is_penalty]
|
|
147
|
+
if penalty.shape[1] == 0:
|
|
148
|
+
return 0.0, {}
|
|
149
|
+
# One value per (period, node) row across the selected penalty
|
|
150
|
+
# column(s); collapse periods (and any scenario level) per node.
|
|
151
|
+
per_row = penalty.sum(axis=1)
|
|
152
|
+
per_node = per_row.groupby(level="node").sum()
|
|
153
|
+
by_node = {str(node): float(val) for node, val in per_node.items()}
|
|
154
|
+
total = float(per_row.sum())
|
|
155
|
+
return total, by_node
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_unit_capacity_total(assess_dir: Path) -> dict[str, float]:
|
|
159
|
+
"""Return ``{unit: total_capacity_MW}`` from ``unit_capacity_ed_p``.
|
|
160
|
+
|
|
161
|
+
Reads the post-invest fleet the solve actually committed to: the ``total``
|
|
162
|
+
column of ``unit_capacity_ed_p`` (row index ``(unit, period)``, column
|
|
163
|
+
levels ``(scenario, parameter)`` with ``parameter`` in
|
|
164
|
+
``{existing, invested, divested, total}`` — see
|
|
165
|
+
``flextool.process_outputs.out_capacity.unit_capacity``). ``total`` is the
|
|
166
|
+
CUMULATIVE ``entity_all_capacity`` (existing + carried-forward invest), so
|
|
167
|
+
it is non-decreasing across periods.
|
|
168
|
+
|
|
169
|
+
Per unit the capacity is taken as the MAX over periods — the *mature*
|
|
170
|
+
(fully-built) fleet the net-load signal must be sized against; for a
|
|
171
|
+
single-invest-period solve this is simply that period's total, and for a
|
|
172
|
+
multi-period invest solve it is the final built-out capacity. Any
|
|
173
|
+
``scenario`` column level is collapsed (summed; a single-scenario solve
|
|
174
|
+
dir has exactly one) before the per-unit reduction. Units whose ``total``
|
|
175
|
+
is entirely null (never realized) are dropped, so a unit absent from the
|
|
176
|
+
result cleanly falls back to its existing cap in
|
|
177
|
+
:func:`flextool.representative_periods.netload.build_group_capacities`.
|
|
178
|
+
Sorted by unit name for determinism.
|
|
179
|
+
|
|
180
|
+
This is a plain reader (no solving / DB / network), used by the net-load
|
|
181
|
+
solve-iteration driver to feed each iteration's invested caps back into the
|
|
182
|
+
representative-period selection.
|
|
183
|
+
"""
|
|
184
|
+
path = Path(assess_dir) / _resolve_filename(_UNIT_CAPACITY_KEY)
|
|
185
|
+
df = read_lean_parquet(path)
|
|
186
|
+
# Select the ``total`` parameter column, collapsing any scenario level so
|
|
187
|
+
# the result is a Series indexed by (unit, period).
|
|
188
|
+
total = df.xs("total", level="parameter", axis=1).sum(axis=1)
|
|
189
|
+
per_unit = total.groupby(level="unit").max().dropna()
|
|
190
|
+
return {str(unit): float(val) for unit, val in sorted(per_unit.items())}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def read_total_system_cost(assess_dir: Path) -> float:
|
|
194
|
+
"""Return the discounted total system cost (M€) from ``costs_discounted_p_``.
|
|
195
|
+
|
|
196
|
+
``costs_discounted_p_`` is the authoritative full-horizon cost summary
|
|
197
|
+
(``flextool.process_outputs.out_costs.cost_summaries``): every investment
|
|
198
|
+
AND dispatch cost category summed over all realized periods, discounted and
|
|
199
|
+
years-represented-weighted exactly as the LP objective is. It INCLUDES the
|
|
200
|
+
``upward slack penalty`` / ``downward slack penalty`` categories (the
|
|
201
|
+
monetised unserved-energy penalties) and the negative ``commodity_sales``
|
|
202
|
+
revenue term, so summing every category cell yields the same signed total
|
|
203
|
+
the objective minimises — the correct comparability metric for the
|
|
204
|
+
keep-best selection (same full timeline + same penalty prices each
|
|
205
|
+
iteration).
|
|
206
|
+
|
|
207
|
+
On disk the table is the per-scenario cost Series tagged with a single
|
|
208
|
+
``scenario`` column level (row index ``category``); every numeric cell is
|
|
209
|
+
summed to the scalar total.
|
|
210
|
+
"""
|
|
211
|
+
path = Path(assess_dir) / _resolve_filename(_TOTAL_COST_KEY)
|
|
212
|
+
df = read_lean_parquet(path)
|
|
213
|
+
return float(df.to_numpy().sum())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"read_curtailment_by_sink",
|
|
218
|
+
"read_residual_unserved",
|
|
219
|
+
"read_residual_unserved_dt",
|
|
220
|
+
"read_slack_penalty",
|
|
221
|
+
"read_total_system_cost",
|
|
222
|
+
"read_unit_capacity_total",
|
|
223
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Pure rendering of a calibration run's outcome (C2).
|
|
2
|
+
|
|
3
|
+
This module turns an in-memory :class:`~flextool.calibrate._loop.CalibResult`
|
|
4
|
+
into (a) machine-readable CSV artifacts and (b) a compact human-readable
|
|
5
|
+
stdout summary. It does **no** solving and re-reads **no** parquet — it
|
|
6
|
+
operates purely on the trajectory already carried by ``CalibResult`` — so it
|
|
7
|
+
is cheap, deterministic, and safe to call anywhere.
|
|
8
|
+
|
|
9
|
+
Two CSVs are written by :func:`write_report`:
|
|
10
|
+
|
|
11
|
+
* a per-``(iteration, node)`` **long** table — one row per node observed in
|
|
12
|
+
each iteration, carrying that iteration's residual unserved energy, the
|
|
13
|
+
adder snapshot that was solved, the per-sink curtailment, the monetised
|
|
14
|
+
per-node slack, and whether the node ended the run flagged
|
|
15
|
+
resource-capped;
|
|
16
|
+
* a per-iteration **summary** table — total unserved energy, total slack
|
|
17
|
+
penalty, the count of shedding nodes, the run's flagged count, and the
|
|
18
|
+
convergence flag.
|
|
19
|
+
|
|
20
|
+
Both are emitted in a fully deterministic order: iterations ascending,
|
|
21
|
+
nodes sorted lexicographically.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import csv
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from flextool.calibrate._loop import CalibResult, IterRecord
|
|
30
|
+
|
|
31
|
+
# CSV column orders — pinned here so both the writer and any downstream
|
|
32
|
+
# consumer (and the tests) agree on the exact schema.
|
|
33
|
+
LONG_COLUMNS = [
|
|
34
|
+
"iteration",
|
|
35
|
+
"node",
|
|
36
|
+
"residual_mwh",
|
|
37
|
+
"adder_mwh",
|
|
38
|
+
"curtailment_mwh",
|
|
39
|
+
"penalty_meur",
|
|
40
|
+
"flagged",
|
|
41
|
+
]
|
|
42
|
+
SUMMARY_COLUMNS = [
|
|
43
|
+
"iteration",
|
|
44
|
+
"total_unserved_mwh",
|
|
45
|
+
"total_penalty_meur",
|
|
46
|
+
"n_shedding",
|
|
47
|
+
"n_flagged",
|
|
48
|
+
"solve_seconds",
|
|
49
|
+
"converged",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
LONG_FILENAME = "calibration_by_iteration_node.csv"
|
|
53
|
+
SUMMARY_FILENAME = "calibration_summary.csv"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _record_nodes(record: IterRecord) -> list[str]:
|
|
57
|
+
"""Sorted union of every node observed in one iteration's signals."""
|
|
58
|
+
nodes: set[str] = set()
|
|
59
|
+
nodes.update(record.residual)
|
|
60
|
+
nodes.update(record.adders)
|
|
61
|
+
nodes.update(record.curtailment)
|
|
62
|
+
nodes.update(record.penalty_by_node)
|
|
63
|
+
return sorted(nodes)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _n_shedding(record: IterRecord) -> int:
|
|
67
|
+
"""Count of nodes with strictly-positive residual unserved energy."""
|
|
68
|
+
return sum(1 for v in record.residual.values() if v > 0.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _adder_scalar(adder: "float | dict") -> float:
|
|
72
|
+
"""Collapse a node's adder to one scalar MWh for reporting.
|
|
73
|
+
|
|
74
|
+
A ``uniform`` adder is already the scalar per-timestep value; a ``timed``
|
|
75
|
+
adder is a ``{(period, time): float}`` map — reported as the SUM of its
|
|
76
|
+
per-cell values (its total per-timestep adder mass), so the CSV/summary
|
|
77
|
+
stay single-valued and sortable across both modes.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(adder, dict):
|
|
80
|
+
return float(sum(adder.values()))
|
|
81
|
+
return float(adder)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def write_report(result: CalibResult, *, out_dir: Path) -> list[Path]:
|
|
85
|
+
"""Write the two machine-readable CSV artifacts for *result*.
|
|
86
|
+
|
|
87
|
+
Emits :data:`LONG_FILENAME` (per-``(iteration, node)``) and
|
|
88
|
+
:data:`SUMMARY_FILENAME` (per-iteration) into *out_dir*, creating it if
|
|
89
|
+
needed. Rows are ordered iterations ascending then nodes sorted, so the
|
|
90
|
+
output is byte-deterministic for a given result.
|
|
91
|
+
|
|
92
|
+
Returns the two written paths in ``[long, summary]`` order.
|
|
93
|
+
"""
|
|
94
|
+
out_dir = Path(out_dir)
|
|
95
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
flagged = set(result.guard_flagged_nodes)
|
|
97
|
+
|
|
98
|
+
long_path = out_dir / LONG_FILENAME
|
|
99
|
+
with long_path.open("w", newline="", encoding="utf-8") as fh:
|
|
100
|
+
writer = csv.writer(fh)
|
|
101
|
+
writer.writerow(LONG_COLUMNS)
|
|
102
|
+
for record in sorted(result.trajectory, key=lambda r: r.iteration):
|
|
103
|
+
for node in _record_nodes(record):
|
|
104
|
+
writer.writerow(
|
|
105
|
+
[
|
|
106
|
+
record.iteration,
|
|
107
|
+
node,
|
|
108
|
+
record.residual.get(node, 0.0),
|
|
109
|
+
_adder_scalar(record.adders.get(node, 0.0)),
|
|
110
|
+
record.curtailment.get(node, 0.0),
|
|
111
|
+
record.penalty_by_node.get(node, 0.0),
|
|
112
|
+
node in flagged,
|
|
113
|
+
]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
summary_path = out_dir / SUMMARY_FILENAME
|
|
117
|
+
with summary_path.open("w", newline="", encoding="utf-8") as fh:
|
|
118
|
+
writer = csv.writer(fh)
|
|
119
|
+
writer.writerow(SUMMARY_COLUMNS)
|
|
120
|
+
ordered = sorted(result.trajectory, key=lambda r: r.iteration)
|
|
121
|
+
last_iteration = ordered[-1].iteration if ordered else None
|
|
122
|
+
for record in ordered:
|
|
123
|
+
# ``converged`` is a run-level outcome; it is true only on the
|
|
124
|
+
# final iteration of a converged run (the one that met the
|
|
125
|
+
# threshold), false on every earlier row.
|
|
126
|
+
row_converged = result.converged and record.iteration == last_iteration
|
|
127
|
+
writer.writerow(
|
|
128
|
+
[
|
|
129
|
+
record.iteration,
|
|
130
|
+
record.total_unserved,
|
|
131
|
+
record.penalty_total,
|
|
132
|
+
_n_shedding(record),
|
|
133
|
+
len(result.guard_flagged_nodes),
|
|
134
|
+
record.solve_seconds,
|
|
135
|
+
row_converged,
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return [long_path, summary_path]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def format_summary(result: CalibResult) -> str:
|
|
143
|
+
"""Render a compact human-readable summary of *result* for stdout.
|
|
144
|
+
|
|
145
|
+
Covers: convergence status and iterations run; initial→final total
|
|
146
|
+
unserved energy (and slack penalty, when non-zero); the final per-node
|
|
147
|
+
adders sorted descending with each node's final residual; and a clearly
|
|
148
|
+
marked resource-capped section listing the guard-flagged nodes (the ones
|
|
149
|
+
that need firm capacity / imports / storage rather than more demand
|
|
150
|
+
margin). Pure — reads only the in-memory *result*.
|
|
151
|
+
"""
|
|
152
|
+
lines: list[str] = []
|
|
153
|
+
lines.append("=" * 60)
|
|
154
|
+
lines.append("Adequacy-margin calibration summary")
|
|
155
|
+
lines.append("=" * 60)
|
|
156
|
+
|
|
157
|
+
n_adjust = result.iterations_run - 1
|
|
158
|
+
if result.stop_reason == "converged":
|
|
159
|
+
lines.append(
|
|
160
|
+
f"Status: CONVERGED after {result.iterations_run} solve(s) "
|
|
161
|
+
f"({n_adjust} adjustment iteration(s))."
|
|
162
|
+
)
|
|
163
|
+
elif result.stop_reason == "stalled":
|
|
164
|
+
lines.append(
|
|
165
|
+
f"Status: CONVERGED MODULO RESOURCE-CAPPED NODES after "
|
|
166
|
+
f"{result.iterations_run} solve(s) ({n_adjust} adjustment "
|
|
167
|
+
f"iteration(s))."
|
|
168
|
+
)
|
|
169
|
+
lines.append(
|
|
170
|
+
"The energy-margin lever is exhausted: every remaining shedding "
|
|
171
|
+
"node is resource-capped, so no further demand margin can be "
|
|
172
|
+
"added and another solve would only reproduce this one."
|
|
173
|
+
)
|
|
174
|
+
else: # "budget_exhausted"
|
|
175
|
+
lines.append(
|
|
176
|
+
f"Status: NOT CONVERGED — ran the full budget of "
|
|
177
|
+
f"{result.iterations_run} solve(s) without reaching the slack "
|
|
178
|
+
f"threshold."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
trajectory = sorted(result.trajectory, key=lambda r: r.iteration)
|
|
182
|
+
if trajectory:
|
|
183
|
+
first = trajectory[0]
|
|
184
|
+
last = trajectory[-1]
|
|
185
|
+
lines.append("")
|
|
186
|
+
lines.append(
|
|
187
|
+
f"Total unserved energy: {first.total_unserved:,.1f} MWh "
|
|
188
|
+
f"(baseline) -> {last.total_unserved:,.1f} MWh (final)."
|
|
189
|
+
)
|
|
190
|
+
if first.penalty_total or last.penalty_total:
|
|
191
|
+
lines.append(
|
|
192
|
+
f"Slack penalty: {first.penalty_total:,.3f} M-EUR "
|
|
193
|
+
f"(baseline) -> {last.penalty_total:,.3f} M-EUR (final)."
|
|
194
|
+
)
|
|
195
|
+
if any(r.solve_seconds for r in trajectory):
|
|
196
|
+
per_iter = ", ".join(
|
|
197
|
+
f"#{r.iteration}:{r.solve_seconds:,.1f}s" for r in trajectory
|
|
198
|
+
)
|
|
199
|
+
total_s = sum(r.solve_seconds for r in trajectory)
|
|
200
|
+
lines.append(
|
|
201
|
+
f"Solve time: {total_s:,.1f} s total "
|
|
202
|
+
f"({per_iter})."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Final per-node adders, largest first; pair each with its final residual.
|
|
206
|
+
final_residual = trajectory[-1].residual if trajectory else {}
|
|
207
|
+
lines.append("")
|
|
208
|
+
if result.final_adders:
|
|
209
|
+
lines.append("Final energy-margin adders (MWh/timestep), largest first:")
|
|
210
|
+
for node, adder in sorted(
|
|
211
|
+
result.final_adders.items(),
|
|
212
|
+
key=lambda kv: (-_adder_scalar(kv[1]), kv[0]),
|
|
213
|
+
):
|
|
214
|
+
resid = final_residual.get(node, 0.0)
|
|
215
|
+
lines.append(
|
|
216
|
+
f" {node:<24} adder={_adder_scalar(adder):>14,.4f} "
|
|
217
|
+
f"final residual={resid:>14,.1f} MWh"
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
lines.append("Final energy-margin adders: none (no node was bumped).")
|
|
221
|
+
|
|
222
|
+
# Resource-capped section — nodes the over-build guard froze.
|
|
223
|
+
lines.append("")
|
|
224
|
+
lines.append(
|
|
225
|
+
"Resource-capped nodes (need firm capacity / imports / storage, "
|
|
226
|
+
"not more demand margin):"
|
|
227
|
+
)
|
|
228
|
+
if result.guard_flagged_nodes:
|
|
229
|
+
for node in result.guard_flagged_nodes:
|
|
230
|
+
resid = final_residual.get(node, 0.0)
|
|
231
|
+
lines.append(f" {node:<24} final residual={resid:>14,.1f} MWh")
|
|
232
|
+
else:
|
|
233
|
+
lines.append(" none")
|
|
234
|
+
|
|
235
|
+
if result.stop_reason == "stalled" and trajectory:
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append(
|
|
238
|
+
f"Residual unserved energy still {trajectory[-1].total_unserved:,.1f} "
|
|
239
|
+
f"MWh at exit, but the calibration is CONVERGED MODULO the "
|
|
240
|
+
f"resource-capped nodes above: that residual sits on the "
|
|
241
|
+
f"guard-flagged nodes, which need firm capacity / imports / "
|
|
242
|
+
f"storage — not more demand margin. Raising --iterations will not "
|
|
243
|
+
f"help; the demand-margin lever is exhausted."
|
|
244
|
+
)
|
|
245
|
+
elif result.stop_reason == "budget_exhausted" and trajectory:
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append(
|
|
248
|
+
f"Residual unserved energy still {trajectory[-1].total_unserved:,.1f} "
|
|
249
|
+
f"MWh at exit — raise --iterations or investigate the flagged nodes."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
lines.append("=" * 60)
|
|
253
|
+
return "\n".join(lines)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
__all__ = [
|
|
257
|
+
"LONG_COLUMNS",
|
|
258
|
+
"LONG_FILENAME",
|
|
259
|
+
"SUMMARY_COLUMNS",
|
|
260
|
+
"SUMMARY_FILENAME",
|
|
261
|
+
"format_summary",
|
|
262
|
+
"write_report",
|
|
263
|
+
]
|