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,195 @@
|
|
|
1
|
+
"""Append an alternative onto a scenario's alternative stack.
|
|
2
|
+
|
|
3
|
+
A FlexTool scenario is defined by an *ordered* list of alternatives via the
|
|
4
|
+
``scenario_alternative`` relationship: each link carries a ``rank`` (integer,
|
|
5
|
+
higher = applied later, so a higher-rank alternative overrides the values of
|
|
6
|
+
lower-rank ones). The representative-periods pre-processor writes its results
|
|
7
|
+
into a fresh RP alternative, but that alternative only influences a solve once
|
|
8
|
+
it is part of the scenario's alternative stack.
|
|
9
|
+
|
|
10
|
+
:func:`add_alternative_to_scenario` appends the RP alternative at the TOP of a
|
|
11
|
+
scenario's stack (the highest rank, so it wins over the baseline
|
|
12
|
+
``period_timeset``), computing the next rank from the scenario's existing max.
|
|
13
|
+
It mirrors the ``DatabaseMapping`` open/commit style of the RP writer
|
|
14
|
+
(``preprocess.py::_write_results_to_db``) and is idempotent — an alternative
|
|
15
|
+
already in the stack is left untouched.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from spinedb_api import DatabaseMapping
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def existing_alternative_names(db_url: str) -> set[str]:
|
|
24
|
+
"""Return the set of alternative names already present in the database.
|
|
25
|
+
|
|
26
|
+
A read-only helper the GUI uses to de-duplicate a freshly derived RP
|
|
27
|
+
alternative name (append ``_2``, ``_3``, … when the base is taken) so a
|
|
28
|
+
repeated build never silently overwrites an earlier one.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
db_url: Spine database URL (e.g. ``'sqlite:///path.sqlite'``).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The names of every alternative in the (unfiltered) database.
|
|
35
|
+
"""
|
|
36
|
+
with DatabaseMapping(db_url) as db:
|
|
37
|
+
return {alt["name"] for alt in db.get_alternative_items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dedup_alternative_name(base: str, taken: set[str]) -> str:
|
|
41
|
+
"""Return *base*, or the first free ``base_2`` / ``base_3`` / … suffix.
|
|
42
|
+
|
|
43
|
+
Pure helper (no DB access) so the GUI can compute the de-duplicated name
|
|
44
|
+
against a cached name set both for the live CLI preview and for the launch,
|
|
45
|
+
keeping the two in lock-step.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
base: The desired alternative name.
|
|
49
|
+
taken: Names already in use (case-sensitive).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
*base* if free, else ``f"{base}_{n}"`` for the smallest ``n >= 2`` that
|
|
53
|
+
is not in *taken*.
|
|
54
|
+
"""
|
|
55
|
+
if base not in taken:
|
|
56
|
+
return base
|
|
57
|
+
n = 2
|
|
58
|
+
while f"{base}_{n}" in taken:
|
|
59
|
+
n += 1
|
|
60
|
+
return f"{base}_{n}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def add_alternative_to_scenario(
|
|
64
|
+
db_url: str,
|
|
65
|
+
scenario_name: str,
|
|
66
|
+
alternative_name: str,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Append *alternative_name* to *scenario_name*'s stack at the top rank.
|
|
69
|
+
|
|
70
|
+
The alternative is added with ``rank = max(existing ranks) + 1`` so it is
|
|
71
|
+
applied last and overrides the alternatives already in the scenario.
|
|
72
|
+
|
|
73
|
+
Idempotent: if *alternative_name* is already in the scenario's stack, the
|
|
74
|
+
function does nothing (no duplicate link, no rank churn, no error).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
db_url: Spine database URL (e.g. ``'sqlite:///path.sqlite'``).
|
|
78
|
+
scenario_name: Name of the scenario whose stack to extend. Must exist.
|
|
79
|
+
alternative_name: Name of the alternative to append. Must exist.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the scenario or the alternative does not exist.
|
|
83
|
+
"""
|
|
84
|
+
with DatabaseMapping(db_url) as db:
|
|
85
|
+
# ``get_*_item`` returns an empty dict (falsy) when nothing matches.
|
|
86
|
+
if not db.get_scenario_item(name=scenario_name):
|
|
87
|
+
raise ValueError(f"Scenario '{scenario_name}' does not exist in the database.")
|
|
88
|
+
if not db.get_alternative_item(name=alternative_name):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Alternative '{alternative_name}' does not exist in the database."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
existing = db.get_scenario_alternative_items(scenario_name=scenario_name)
|
|
94
|
+
# Idempotent: already in the stack (at any rank) → leave it untouched.
|
|
95
|
+
if any(sa["alternative_name"] == alternative_name for sa in existing):
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
next_rank = max((sa["rank"] for sa in existing), default=0) + 1
|
|
99
|
+
# ``add_scenario_alternative`` returns the added item and raises
|
|
100
|
+
# ``SpineDBAPIError`` on failure — let that surface rather than swallow.
|
|
101
|
+
db.add_scenario_alternative(
|
|
102
|
+
scenario_name=scenario_name,
|
|
103
|
+
alternative_name=alternative_name,
|
|
104
|
+
rank=next_rank,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
db.commit_session(
|
|
108
|
+
f"Append alternative '{alternative_name}' to scenario '{scenario_name}'"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_scenario_with_alternative(
|
|
113
|
+
db_url: str,
|
|
114
|
+
base_scenario_name: str,
|
|
115
|
+
new_scenario_name: str,
|
|
116
|
+
alternative_name: str,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Clone *base_scenario_name* into *new_scenario_name* + append *alternative_name*.
|
|
119
|
+
|
|
120
|
+
Creates *new_scenario_name* as a copy of *base_scenario_name*'s ordered
|
|
121
|
+
alternative stack (same alternatives, same ranks) and then appends
|
|
122
|
+
*alternative_name* at ``max(rank) + 1`` so the RP alternative overrides the
|
|
123
|
+
baseline ``period_timeset``. The base scenario is left completely
|
|
124
|
+
untouched, so the original and the representative-period runs can be
|
|
125
|
+
compared side by side.
|
|
126
|
+
|
|
127
|
+
Idempotent: if *new_scenario_name* already exists it is not recreated — the
|
|
128
|
+
function only ensures *alternative_name* is present at the top of its stack
|
|
129
|
+
(mirroring :func:`add_alternative_to_scenario`), so a repeated launch is a
|
|
130
|
+
no-op rather than an error.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
db_url: Spine database URL (e.g. ``'sqlite:///path.sqlite'``).
|
|
134
|
+
base_scenario_name: Scenario to clone. Must exist.
|
|
135
|
+
new_scenario_name: Name of the scenario to create / extend.
|
|
136
|
+
alternative_name: Alternative to append on top. Must exist.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If the base scenario or the alternative does not exist.
|
|
140
|
+
"""
|
|
141
|
+
with DatabaseMapping(db_url) as db:
|
|
142
|
+
if not db.get_scenario_item(name=base_scenario_name):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Scenario '{base_scenario_name}' does not exist in the database."
|
|
145
|
+
)
|
|
146
|
+
if not db.get_alternative_item(name=alternative_name):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Alternative '{alternative_name}' does not exist in the database."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Create the new scenario the first time, cloning the base stack. On a
|
|
152
|
+
# repeat launch the scenario already exists, so only the top-rank append
|
|
153
|
+
# below runs (idempotent).
|
|
154
|
+
changed = False
|
|
155
|
+
if not db.get_scenario_item(name=new_scenario_name):
|
|
156
|
+
db.add_scenario(name=new_scenario_name)
|
|
157
|
+
for sa in db.get_scenario_alternative_items(
|
|
158
|
+
scenario_name=base_scenario_name
|
|
159
|
+
):
|
|
160
|
+
db.add_scenario_alternative(
|
|
161
|
+
scenario_name=new_scenario_name,
|
|
162
|
+
alternative_name=sa["alternative_name"],
|
|
163
|
+
rank=sa["rank"],
|
|
164
|
+
)
|
|
165
|
+
changed = True
|
|
166
|
+
|
|
167
|
+
existing = db.get_scenario_alternative_items(
|
|
168
|
+
scenario_name=new_scenario_name
|
|
169
|
+
)
|
|
170
|
+
if not any(
|
|
171
|
+
sa["alternative_name"] == alternative_name for sa in existing
|
|
172
|
+
):
|
|
173
|
+
next_rank = max((sa["rank"] for sa in existing), default=0) + 1
|
|
174
|
+
db.add_scenario_alternative(
|
|
175
|
+
scenario_name=new_scenario_name,
|
|
176
|
+
alternative_name=alternative_name,
|
|
177
|
+
rank=next_rank,
|
|
178
|
+
)
|
|
179
|
+
changed = True
|
|
180
|
+
|
|
181
|
+
# Nothing to persist on a fully idempotent repeat call — committing an
|
|
182
|
+
# unchanged session raises ``NothingToCommit``.
|
|
183
|
+
if changed:
|
|
184
|
+
db.commit_session(
|
|
185
|
+
f"Create scenario '{new_scenario_name}' from "
|
|
186
|
+
f"'{base_scenario_name}' with alternative '{alternative_name}'"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
__all__ = [
|
|
191
|
+
"add_alternative_to_scenario",
|
|
192
|
+
"create_scenario_with_alternative",
|
|
193
|
+
"dedup_alternative_name",
|
|
194
|
+
"existing_alternative_names",
|
|
195
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Convex weight fitting and simplex projection.
|
|
2
|
+
|
|
3
|
+
Implements the weight computation from the hull clustering paper:
|
|
4
|
+
for each base period, find the convex combination of representative periods
|
|
5
|
+
that best approximates it (minimum L2 error).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def project_onto_simplex(v: np.ndarray) -> np.ndarray:
|
|
12
|
+
"""Project vector v onto the probability simplex {w >= 0, sum(w) = 1}.
|
|
13
|
+
|
|
14
|
+
Uses Condat's algorithm (O(n) average case).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
v: Input vector of length n.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Projected vector on the simplex.
|
|
21
|
+
"""
|
|
22
|
+
n = len(v)
|
|
23
|
+
if n == 0:
|
|
24
|
+
return v
|
|
25
|
+
if n == 1:
|
|
26
|
+
return np.array([1.0])
|
|
27
|
+
|
|
28
|
+
u = np.sort(v)[::-1] # sort descending
|
|
29
|
+
cumsum = np.cumsum(u)
|
|
30
|
+
k_candidates = u + (1.0 - cumsum) / np.arange(1, n + 1)
|
|
31
|
+
K = np.max(np.where(k_candidates > 0)[0]) + 1 if np.any(k_candidates > 0) else 1
|
|
32
|
+
tau = (cumsum[K - 1] - 1.0) / K
|
|
33
|
+
return np.maximum(v - tau, 0.0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fit_convex_weights(
|
|
37
|
+
R: np.ndarray,
|
|
38
|
+
c: np.ndarray,
|
|
39
|
+
max_iter: int = 200,
|
|
40
|
+
tol: float = 1e-8,
|
|
41
|
+
alpha: float | None = None,
|
|
42
|
+
) -> np.ndarray:
|
|
43
|
+
"""Find convex weights w that minimize ||R @ w - c||^2 s.t. w in simplex.
|
|
44
|
+
|
|
45
|
+
Uses projected gradient descent with simplex projection.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
R: Matrix of representative period feature vectors, shape (n_features, n_rp).
|
|
49
|
+
c: Feature vector of the base period to approximate, shape (n_features,).
|
|
50
|
+
max_iter: Maximum PGD iterations.
|
|
51
|
+
tol: Convergence tolerance.
|
|
52
|
+
alpha: Learning rate. If None, computed from Lipschitz constant.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Weight vector w of length n_rp, with w >= 0 and sum(w) = 1.
|
|
56
|
+
"""
|
|
57
|
+
n_rp = R.shape[1]
|
|
58
|
+
if n_rp == 1:
|
|
59
|
+
return np.array([1.0])
|
|
60
|
+
|
|
61
|
+
# Precompute R^T R and R^T c for gradient computation
|
|
62
|
+
RtR = R.T @ R
|
|
63
|
+
Rtc = R.T @ c
|
|
64
|
+
|
|
65
|
+
# Learning rate from Lipschitz constant of gradient
|
|
66
|
+
if alpha is None:
|
|
67
|
+
L = np.linalg.norm(RtR, ord=2)
|
|
68
|
+
alpha = 1.0 / max(L, 1e-10)
|
|
69
|
+
|
|
70
|
+
# Initial guess via pseudoinverse
|
|
71
|
+
R_pinv = np.linalg.pinv(R)
|
|
72
|
+
w = project_onto_simplex(R_pinv @ c)
|
|
73
|
+
|
|
74
|
+
for _ in range(max_iter):
|
|
75
|
+
# Gradient: R^T (R w - c) = RtR w - Rtc
|
|
76
|
+
grad = RtR @ w - Rtc
|
|
77
|
+
w_prev = w.copy()
|
|
78
|
+
w = project_onto_simplex(w - alpha * grad)
|
|
79
|
+
|
|
80
|
+
if np.max(np.abs(w - w_prev)) < tol:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return w
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def distance_to_hull(R: np.ndarray, c: np.ndarray, **kwargs) -> tuple[float, np.ndarray]:
|
|
87
|
+
"""Compute the L2 distance from point c to the convex hull of columns of R.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
R: Matrix of representative period feature vectors, shape (n_features, n_rp).
|
|
91
|
+
c: Feature vector to measure distance from, shape (n_features,).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(distance, weights): the L2 distance and the optimal weight vector.
|
|
95
|
+
"""
|
|
96
|
+
w = fit_convex_weights(R, c, **kwargs)
|
|
97
|
+
residual = R @ w - c
|
|
98
|
+
dist = np.linalg.norm(residual)
|
|
99
|
+
return dist, w
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def compute_weight_matrix(
|
|
103
|
+
C: np.ndarray, rep_indices: list[int]
|
|
104
|
+
) -> np.ndarray:
|
|
105
|
+
"""Compute the full weight matrix W for all base periods.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
C: Clustering matrix, shape (n_features, n_base_periods).
|
|
109
|
+
rep_indices: Indices of selected representative periods.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Weight matrix W of shape (n_base_periods, n_rp),
|
|
113
|
+
where W[d, r] is the weight of representative period r for base period d.
|
|
114
|
+
Each row sums to 1 and all entries >= 0.
|
|
115
|
+
"""
|
|
116
|
+
n_base = C.shape[1]
|
|
117
|
+
n_rp = len(rep_indices)
|
|
118
|
+
R = C[:, rep_indices]
|
|
119
|
+
W = np.zeros((n_base, n_rp))
|
|
120
|
+
|
|
121
|
+
for d in range(n_base):
|
|
122
|
+
W[d, :] = fit_convex_weights(R, C[:, d])
|
|
123
|
+
|
|
124
|
+
return W
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Cross-scenario analysis module.
|
|
2
|
+
|
|
3
|
+
Navigation:
|
|
4
|
+
- data_models.py : TimeSeriesResults, DispatchMappings — start here to understand data shapes
|
|
5
|
+
- db_reader.py : Load parquet files from scenario folders → TimeSeriesResults
|
|
6
|
+
- dispatch_mappings.py: Load dispatch mapping parquet files → DispatchMappings
|
|
7
|
+
- config_builder.py : Discover dispatch entity/scenario names + assign palette colors
|
|
8
|
+
- dispatch_data.py : Prepare per-scenario dispatch DataFrames for plotting
|
|
9
|
+
- dispatch_plots.py : Render stacked area dispatch plots
|
|
10
|
+
- orchestrator.py : Top-level run() function tying all pieces together
|
|
11
|
+
"""
|
|
12
|
+
from flextool.scenario_comparison.db_reader import get_scenario_results
|
|
13
|
+
__all__ = ['get_scenario_results']
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Discover dispatch entity / scenario names and assign palette colors.
|
|
2
|
+
|
|
3
|
+
Reads dispatch mappings to enumerate the group / unit / connection / scenario
|
|
4
|
+
names present in a run, and assigns default palette colors. These feed the
|
|
5
|
+
additive seeding of the project ``plot_settings.yaml`` (the durable colors
|
|
6
|
+
file the renderers read); the legacy ``config.yaml`` system has been removed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import matplotlib
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from flextool.scenario_comparison.constants import DEFAULT_SPECIAL_COLORS
|
|
14
|
+
from flextool.scenario_comparison.data_models import DispatchMappings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover_dispatch_entities(
|
|
18
|
+
mappings: DispatchMappings,
|
|
19
|
+
scenarios: list[str],
|
|
20
|
+
) -> dict[str, list[str]]:
|
|
21
|
+
"""Discover and classify dispatch entity / scenario names from mappings.
|
|
22
|
+
|
|
23
|
+
Reads the dispatch-mapping fields and classifies each discovered name
|
|
24
|
+
into the entity class the project ``plot_settings.yaml`` expects:
|
|
25
|
+
|
|
26
|
+
* ``nodeGroup`` — the dispatch ``group`` name(s) being plotted (the
|
|
27
|
+
group__node collection that is the plot subject);
|
|
28
|
+
* ``flowGroup`` — processGroup / unitGroup / connectionGroup aggregate
|
|
29
|
+
names (``group_aggregate`` of ``processGroup_Unit_to_group`` /
|
|
30
|
+
``processGroup_Group_to_unit`` / ``processGroup_Connection``) that
|
|
31
|
+
stack as items in the dispatch plot;
|
|
32
|
+
* ``unit`` — individual (not-aggregated) unit names, taken as the
|
|
33
|
+
bare ``unit`` / ``process`` entity of the unit member and
|
|
34
|
+
``not_in_aggregate`` unit fields;
|
|
35
|
+
* ``connection`` — individual (not-aggregated) connection names, taken
|
|
36
|
+
as the bare ``process`` / ``connection`` entity of the connection
|
|
37
|
+
member and ``not_in_aggregate`` connection fields;
|
|
38
|
+
* ``scenarios`` — the run's scenario names.
|
|
39
|
+
|
|
40
|
+
The *bare* entity name is recorded (the unit / connection name), matching
|
|
41
|
+
what the dispatch color resolver looks up — never the ``(process, node)``
|
|
42
|
+
composite string. Nodes are deliberately not discovered (node colors are
|
|
43
|
+
dataset-coupled and are not seeded into the portable settings file).
|
|
44
|
+
|
|
45
|
+
Returns a mapping ``{class -> sorted list of names}`` with the keys
|
|
46
|
+
``nodeGroup``, ``flowGroup``, ``unit``, ``connection`` and ``scenarios``;
|
|
47
|
+
empty lists when a class has no members.
|
|
48
|
+
"""
|
|
49
|
+
node_groups: set[str] = set()
|
|
50
|
+
flow_groups: set[str] = set()
|
|
51
|
+
units: set[str] = set()
|
|
52
|
+
connections: set[str] = set()
|
|
53
|
+
|
|
54
|
+
# --- nodeGroups: the dispatch group(s) being plotted (group__node). ---
|
|
55
|
+
dispatch_groups_df = mappings.dispatch_groups
|
|
56
|
+
if dispatch_groups_df is not None and not dispatch_groups_df.empty:
|
|
57
|
+
if 'group' in dispatch_groups_df.columns:
|
|
58
|
+
node_groups.update(str(g) for g in dispatch_groups_df['group'].unique())
|
|
59
|
+
|
|
60
|
+
# --- flowGroups: the processGroup aggregate names (unit/connection
|
|
61
|
+
# aggregates that stack as items in the dispatch plot). ---
|
|
62
|
+
for pg_attr in (
|
|
63
|
+
'processGroup_Unit_to_group',
|
|
64
|
+
'processGroup_Group_to_unit',
|
|
65
|
+
'processGroup_Connection',
|
|
66
|
+
):
|
|
67
|
+
pg_df = getattr(mappings, pg_attr, None)
|
|
68
|
+
if pg_df is not None and not pg_df.empty and 'group_aggregate' in pg_df.columns:
|
|
69
|
+
flow_groups.update(str(g) for g in pg_df['group_aggregate'].unique())
|
|
70
|
+
|
|
71
|
+
# --- Individual unit names (bare entity, not the composite) ---
|
|
72
|
+
# processGroup_*_members carry the units/connections that DO aggregate but
|
|
73
|
+
# are still useful per-entity colors when shown individually; the
|
|
74
|
+
# not_in_aggregate_* fields carry the units/connections shown un-aggregated.
|
|
75
|
+
for unit_attr in (
|
|
76
|
+
'processGroup_unit_to_node_members',
|
|
77
|
+
'processGroup_node_to_unit_members',
|
|
78
|
+
'not_in_aggregate_unit_to_node',
|
|
79
|
+
'not_in_aggregate_node_to_unit',
|
|
80
|
+
):
|
|
81
|
+
u_df = getattr(mappings, unit_attr, None)
|
|
82
|
+
if u_df is None or u_df.empty:
|
|
83
|
+
continue
|
|
84
|
+
col = 'unit' if 'unit' in u_df.columns else (
|
|
85
|
+
'process' if 'process' in u_df.columns else None
|
|
86
|
+
)
|
|
87
|
+
if col is not None:
|
|
88
|
+
units.update(str(u) for u in u_df[col].unique())
|
|
89
|
+
|
|
90
|
+
# --- Individual connection names (bare entity) ---
|
|
91
|
+
for conn_attr in (
|
|
92
|
+
'processGroup_connection_to_node_members',
|
|
93
|
+
'processGroup_node_to_connection_members',
|
|
94
|
+
'not_in_aggregate_connection_to_node',
|
|
95
|
+
'not_in_aggregate_node_to_connection',
|
|
96
|
+
):
|
|
97
|
+
c_df = getattr(mappings, conn_attr, None)
|
|
98
|
+
if c_df is None or c_df.empty:
|
|
99
|
+
continue
|
|
100
|
+
col = 'connection' if 'connection' in c_df.columns else (
|
|
101
|
+
'process' if 'process' in c_df.columns else None
|
|
102
|
+
)
|
|
103
|
+
if col is not None:
|
|
104
|
+
connections.update(str(c) for c in c_df[col].unique())
|
|
105
|
+
|
|
106
|
+
na_conn_df = mappings.not_in_aggregate_connection
|
|
107
|
+
if na_conn_df is not None and not na_conn_df.empty:
|
|
108
|
+
col = 'connection' if 'connection' in na_conn_df.columns else (
|
|
109
|
+
'process' if 'process' in na_conn_df.columns else None
|
|
110
|
+
)
|
|
111
|
+
if col is not None:
|
|
112
|
+
connections.update(str(c) for c in na_conn_df[col].unique())
|
|
113
|
+
|
|
114
|
+
# A name discovered as both a group aggregate and an individual entity is
|
|
115
|
+
# a group (the aggregate is the durable, project-portable color).
|
|
116
|
+
all_groups = node_groups | flow_groups
|
|
117
|
+
units -= all_groups
|
|
118
|
+
connections -= all_groups
|
|
119
|
+
# Guard against a name landing in both individual classes.
|
|
120
|
+
connections -= units
|
|
121
|
+
|
|
122
|
+
scenario_names = [str(s) for s in scenarios if s]
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
'nodeGroup': sorted(node_groups),
|
|
126
|
+
'flowGroup': sorted(flow_groups),
|
|
127
|
+
'unit': sorted(units),
|
|
128
|
+
'connection': sorted(connections),
|
|
129
|
+
'scenarios': sorted(dict.fromkeys(scenario_names)),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def assign_palette_colors(
|
|
134
|
+
names: list[str],
|
|
135
|
+
start_index: int = 0,
|
|
136
|
+
use_tab10: bool = False,
|
|
137
|
+
) -> dict[str, str]:
|
|
138
|
+
"""Assign default palette colors to *names*, in order.
|
|
139
|
+
|
|
140
|
+
Special-token names get their fixed ``DEFAULT_SPECIAL_COLORS`` value;
|
|
141
|
+
everything else cycles the tab20 (or tab10 for scenarios) matplotlib
|
|
142
|
+
palette starting at *start_index* so the assigned colors are stable and
|
|
143
|
+
visually sensible.
|
|
144
|
+
|
|
145
|
+
Returns an ordered ``{name -> '#RRGGBB'}`` mapping for *names*.
|
|
146
|
+
"""
|
|
147
|
+
palette = plt.cm.tab10(np.linspace(0, 1, 10)) if use_tab10 else \
|
|
148
|
+
plt.cm.tab20(np.linspace(0, 1, 20))
|
|
149
|
+
span = 10 if use_tab10 else 20
|
|
150
|
+
out: dict[str, str] = {}
|
|
151
|
+
idx = start_index
|
|
152
|
+
for name in names:
|
|
153
|
+
if name in DEFAULT_SPECIAL_COLORS:
|
|
154
|
+
out[name] = DEFAULT_SPECIAL_COLORS[name]
|
|
155
|
+
continue
|
|
156
|
+
out[name] = matplotlib.colors.rgb2hex(palette[idx % span])
|
|
157
|
+
idx += 1
|
|
158
|
+
return out
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Color and column-name constants for dispatch and summary plots."""
|
|
2
|
+
|
|
3
|
+
# Default color mapping for special columns
|
|
4
|
+
DEFAULT_SPECIAL_COLORS = {
|
|
5
|
+
# Positive special columns (at top of legend/plot)
|
|
6
|
+
'LossOfLoad': 'crimson',
|
|
7
|
+
'Discharge': 'aqua',
|
|
8
|
+
'Import': 'indigo',
|
|
9
|
+
# Negative special columns (at bottom of legend/plot)
|
|
10
|
+
'Charge': 'lime',
|
|
11
|
+
'Export': 'purple',
|
|
12
|
+
'internal_losses': 'darkgray',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Special columns that should be POSITIVE (at top of stacked plot, top of legend)
|
|
16
|
+
POSITIVE_SPECIAL = ['LossOfLoad', 'Discharge', 'Import']
|
|
17
|
+
# Special columns that should be NEGATIVE (at bottom of stacked plot, bottom of legend)
|
|
18
|
+
NEGATIVE_SPECIAL = ['Charge', 'Export', 'internal_losses']
|
|
19
|
+
# Columns plotted as lines, not stacked areas
|
|
20
|
+
LINE_COLUMNS = ['Curtailed', 'Demand']
|