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,189 @@
|
|
|
1
|
+
"""Output functions for flow group results.
|
|
2
|
+
|
|
3
|
+
Groups that have ``output_flowGroup_indicators = yes`` appear in
|
|
4
|
+
``s.flowGroupIndicators``. Their member flows are listed in
|
|
5
|
+
``s.group_process_node`` — each tuple ``(group, process, node)`` identifies
|
|
6
|
+
one unit-flow or connection-flow leg that should be summed into the group.
|
|
7
|
+
|
|
8
|
+
The metrics emitted here are intentionally minimal (a stub); richer
|
|
9
|
+
direction-aware or technology-aware metrics are future work.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _member_legs(s, g: str) -> pd.MultiIndex:
|
|
17
|
+
"""Return the ``(process, node)`` legs belonging to group ``g``.
|
|
18
|
+
|
|
19
|
+
``s.group_process_node`` is a ``(group, process, node)`` MultiIndex;
|
|
20
|
+
filtering by the ``group`` level gives the flows aggregated into this
|
|
21
|
+
group. The returned index drops the ``group`` level so callers can
|
|
22
|
+
match against ``r.flow_dt``/``r.from_conn``/``r.to_conn`` columns
|
|
23
|
+
(which are ``(process, node)`` after de-duplication).
|
|
24
|
+
"""
|
|
25
|
+
gpn = s.group_process_node
|
|
26
|
+
mask = gpn.get_level_values('group') == g
|
|
27
|
+
return gpn[mask].droplevel('group').unique()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def flowGroup_indicators(par, s, v, r, debug):
|
|
31
|
+
"""Flow-group indicator results by period and a signed timewise series.
|
|
32
|
+
|
|
33
|
+
For each group in ``s.flowGroupIndicators`` two quantities are built per
|
|
34
|
+
timestep:
|
|
35
|
+
|
|
36
|
+
* ``magnitude_dt`` — the UNSIGNED sum of all member unit- and
|
|
37
|
+
connection-flow magnitudes (``.abs()``). This is weighted by step
|
|
38
|
+
duration to get energy per period and divided by period hours for the
|
|
39
|
+
average power. This path is unchanged (byte-identical).
|
|
40
|
+
* ``net_dt`` — the SIGNED net flow into the group's nodes (into-node
|
|
41
|
+
``+``, out-of-node ``−``; matches ``calc_group_flows``/
|
|
42
|
+
``calc_connections``). A unit leg whose ``node`` is the flow's *sink*
|
|
43
|
+
counts ``+`` (delivery into the node); whose ``node`` is the *source*
|
|
44
|
+
counts ``−`` (withdrawal). Connection ``from_conn`` (into node) counts
|
|
45
|
+
``+`` and ``to_conn`` (out of node) counts ``−``. No ``.abs()`` and no
|
|
46
|
+
step-duration weighting — it is MW per timestep.
|
|
47
|
+
|
|
48
|
+
Returns a list of two ``(frame, name)`` tuples:
|
|
49
|
+
|
|
50
|
+
* ``flowGroup_gd_p`` — keyed ``(group, period)`` with columns
|
|
51
|
+
``cumulative_flow`` [MWh] and ``average_flow`` [MW]. A group with no
|
|
52
|
+
matched member flows gets zeros rather than being dropped.
|
|
53
|
+
* ``flowGroup_gd_t`` — keyed ``(group, period, time)`` with the single
|
|
54
|
+
column ``net_flow`` [MW], the signed per-timestep net flow.
|
|
55
|
+
"""
|
|
56
|
+
results: list[tuple[pd.DataFrame, str]] = []
|
|
57
|
+
|
|
58
|
+
if not list(s.flowGroupIndicators) or s.dt_realize_dispatch.empty:
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
step_dur = par.step_duration # Series indexed by (period, time), MW→MWh weight
|
|
62
|
+
dt_index = s.dt_realize_dispatch
|
|
63
|
+
|
|
64
|
+
# Pre-compute lookups of member columns. ``r.flow_dt`` is keyed by
|
|
65
|
+
# (process, source, sink) — a unit flow touches a group's (process,
|
|
66
|
+
# node) membership whenever the node is the source *or* the sink.
|
|
67
|
+
# ``r.from_conn``/``r.to_conn`` are already keyed by (process, node),
|
|
68
|
+
# so membership is a direct column pick.
|
|
69
|
+
flow_cols = r.flow_dt.columns
|
|
70
|
+
flow_sink_pairs = None
|
|
71
|
+
flow_source_pairs = None
|
|
72
|
+
if not r.flow_dt.empty:
|
|
73
|
+
flow_sink_pairs = pd.MultiIndex.from_arrays(
|
|
74
|
+
[flow_cols.get_level_values('process'), flow_cols.get_level_values('sink')],
|
|
75
|
+
names=['process', 'node'],
|
|
76
|
+
)
|
|
77
|
+
flow_source_pairs = pd.MultiIndex.from_arrays(
|
|
78
|
+
[flow_cols.get_level_values('process'), flow_cols.get_level_values('source')],
|
|
79
|
+
names=['process', 'node'],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
rows: list[dict] = []
|
|
83
|
+
net_rows: list[dict] = []
|
|
84
|
+
# Period hours = share of year * 8760 (same convention used by
|
|
85
|
+
# out_flows.unit_online_and_startup for period-hour normalization).
|
|
86
|
+
period_hours = par.complete_period_share_of_year.mul(8760.0)
|
|
87
|
+
|
|
88
|
+
for g in s.flowGroupIndicators:
|
|
89
|
+
legs = _member_legs(s, g) # MultiIndex[(process, node)]
|
|
90
|
+
|
|
91
|
+
# Accumulate |flow| (MW) at each timestep (UNSIGNED magnitude path).
|
|
92
|
+
magnitude_dt = pd.Series(0.0, index=dt_index)
|
|
93
|
+
# Accumulate signed net flow (MW) into the group's nodes at each
|
|
94
|
+
# timestep (into-node +, out-of-node −).
|
|
95
|
+
net_dt = pd.Series(0.0, index=dt_index)
|
|
96
|
+
|
|
97
|
+
if len(legs) and flow_sink_pairs is not None:
|
|
98
|
+
# Unit flows — pick every (process, source, sink) column whose
|
|
99
|
+
# (process, sink) or (process, source) is a member leg.
|
|
100
|
+
sink_hits = flow_sink_pairs.isin(legs)
|
|
101
|
+
source_hits = flow_source_pairs.isin(legs)
|
|
102
|
+
is_unit = flow_cols.get_level_values('process').isin(s.process_unit)
|
|
103
|
+
unit_mask = is_unit & (sink_hits | source_hits)
|
|
104
|
+
if unit_mask.any():
|
|
105
|
+
magnitude_dt = magnitude_dt.add(
|
|
106
|
+
r.flow_dt.loc[:, unit_mask].abs().sum(axis=1), fill_value=0.0,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Signed: node==sink (unit delivers into the node) counts +,
|
|
110
|
+
# node==source (unit withdraws from the node) counts −.
|
|
111
|
+
unit_into = is_unit & sink_hits
|
|
112
|
+
unit_out = is_unit & source_hits
|
|
113
|
+
if unit_into.any():
|
|
114
|
+
net_dt = net_dt.add(
|
|
115
|
+
r.flow_dt.loc[:, unit_into].sum(axis=1), fill_value=0.0,
|
|
116
|
+
)
|
|
117
|
+
if unit_out.any():
|
|
118
|
+
net_dt = net_dt.sub(
|
|
119
|
+
r.flow_dt.loc[:, unit_out].sum(axis=1), fill_value=0.0,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Connection flows — from_conn and to_conn are already keyed by
|
|
123
|
+
# (process, node). Pick every column whose key is a member leg.
|
|
124
|
+
if not r.from_conn.empty:
|
|
125
|
+
conn_legs_from = r.from_conn.columns.intersection(legs)
|
|
126
|
+
if len(conn_legs_from):
|
|
127
|
+
magnitude_dt = magnitude_dt.add(
|
|
128
|
+
r.from_conn[conn_legs_from].abs().sum(axis=1), fill_value=0.0,
|
|
129
|
+
)
|
|
130
|
+
# from_conn flows into the node → +.
|
|
131
|
+
net_dt = net_dt.add(
|
|
132
|
+
r.from_conn[conn_legs_from].sum(axis=1), fill_value=0.0,
|
|
133
|
+
)
|
|
134
|
+
if not r.to_conn.empty:
|
|
135
|
+
conn_legs_to = r.to_conn.columns.intersection(legs)
|
|
136
|
+
if len(conn_legs_to):
|
|
137
|
+
magnitude_dt = magnitude_dt.add(
|
|
138
|
+
r.to_conn[conn_legs_to].abs().sum(axis=1), fill_value=0.0,
|
|
139
|
+
)
|
|
140
|
+
# to_conn flows out of the node → −.
|
|
141
|
+
net_dt = net_dt.sub(
|
|
142
|
+
r.to_conn[conn_legs_to].sum(axis=1), fill_value=0.0,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Weight MW → MWh per step, then sum to period level.
|
|
146
|
+
energy_dt = magnitude_dt.mul(step_dur, fill_value=0.0)
|
|
147
|
+
cumulative_d = energy_dt.groupby(level='period').sum()
|
|
148
|
+
# Reindex to the full realized-period set so groups with no matched
|
|
149
|
+
# flows still produce a row per realized period.
|
|
150
|
+
cumulative_d = cumulative_d.reindex(s.d_realized_period, fill_value=0.0)
|
|
151
|
+
|
|
152
|
+
hours_d = period_hours.reindex(cumulative_d.index)
|
|
153
|
+
average_d = cumulative_d.div(hours_d.where(hours_d != 0, pd.NA))
|
|
154
|
+
average_d = average_d.fillna(0.0)
|
|
155
|
+
|
|
156
|
+
for period in cumulative_d.index:
|
|
157
|
+
rows.append({
|
|
158
|
+
'group': g,
|
|
159
|
+
'period': period,
|
|
160
|
+
'cumulative_flow': float(cumulative_d.loc[period]),
|
|
161
|
+
'average_flow': float(average_d.loc[period]),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# Signed per-timestep net flow (MW) — one row per realized (period,
|
|
165
|
+
# time). No step-duration weighting; this is instantaneous power.
|
|
166
|
+
for (period, time), value in net_dt.items():
|
|
167
|
+
net_rows.append({
|
|
168
|
+
'group': g,
|
|
169
|
+
'period': period,
|
|
170
|
+
'time': time,
|
|
171
|
+
'net_flow': float(value),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
if not rows:
|
|
175
|
+
return results
|
|
176
|
+
|
|
177
|
+
frame = pd.DataFrame(rows).set_index(['group', 'period'])[
|
|
178
|
+
['cumulative_flow', 'average_flow']
|
|
179
|
+
]
|
|
180
|
+
frame.columns.name = 'parameter'
|
|
181
|
+
results.append((frame, 'flowGroup_gd_p'))
|
|
182
|
+
|
|
183
|
+
if net_rows:
|
|
184
|
+
net_frame = pd.DataFrame(net_rows).set_index(
|
|
185
|
+
['group', 'period', 'time'])[['net_flow']]
|
|
186
|
+
net_frame.columns.name = 'parameter'
|
|
187
|
+
results.append((net_frame, 'flowGroup_gd_t'))
|
|
188
|
+
|
|
189
|
+
return results
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from flextool.process_outputs._annualize import annualize_dt_to_d
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Threshold below which LP residuals on flow variables are treated as exact
|
|
7
|
+
# zero. HiGHS's primal feasibility tolerance is 1e-8 (see HiGHS options),
|
|
8
|
+
# so any output below 1e-9 is conclusively below solver precision and
|
|
9
|
+
# represents zero. Without this clip the wide-format flow CSVs round-trip
|
|
10
|
+
# through ``pd.read_csv`` as ``float64`` while integer-zero goldens load
|
|
11
|
+
# as ``int64``, causing ``pd.testing.assert_frame_equal`` to fail on dtype
|
|
12
|
+
# even when values match under ``round_for_comparison``'s 4-decimal
|
|
13
|
+
# threshold.
|
|
14
|
+
#
|
|
15
|
+
# Apply ONLY to the wide output frames destined for CSV write — never to
|
|
16
|
+
# ``r.flow_dt`` itself, because curtailment derives ``potentialVRE - flow``
|
|
17
|
+
# and small physically-meaningful curtailment values could otherwise be
|
|
18
|
+
# masked.
|
|
19
|
+
_FLOW_RESIDUAL_TOLERANCE: float = 1e-9
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _clip_flow_residuals(df: pd.DataFrame) -> pd.DataFrame:
|
|
23
|
+
"""Snap sub-femto LP residuals to exactly zero.
|
|
24
|
+
|
|
25
|
+
Returns a frame where every cell with ``|x| < 1e-9`` is replaced with
|
|
26
|
+
``0.0``. Empty / all-NaN frames are returned unchanged.
|
|
27
|
+
"""
|
|
28
|
+
if df.empty:
|
|
29
|
+
return df
|
|
30
|
+
return df.mask(df.abs() < _FLOW_RESIDUAL_TOLERANCE, 0.0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _indirect_units(s) -> set:
|
|
34
|
+
"""Return set of units classified as indirect (2-arc conversion form).
|
|
35
|
+
|
|
36
|
+
An indirect unit has BOTH a row where ``source == p`` and a row where
|
|
37
|
+
``sink == p`` in ``process_source_sink`` — i.e. the topology was split
|
|
38
|
+
into separate input-side and output-side arcs. Direct-method and
|
|
39
|
+
profile/noConversion units have at most one of those, never both.
|
|
40
|
+
|
|
41
|
+
This mirrors v3.32.0's ordering convention for the wide-pivot output
|
|
42
|
+
CSVs (``unit__outputNode__dt.csv`` etc.): direct-method units listed
|
|
43
|
+
first (in alphabetical order), then indirect units. v3.32.0 inherited
|
|
44
|
+
the partition from the legacy ``write_process_arc_unions`` union order
|
|
45
|
+
(process_arc_unions.py L248-263) which placed ``*_direct.csv`` sets
|
|
46
|
+
ahead of the indirect/noConversion ones.
|
|
47
|
+
"""
|
|
48
|
+
if s.process_source_sink is None or len(s.process_source_sink) == 0:
|
|
49
|
+
return set()
|
|
50
|
+
procs = s.process_source_sink.get_level_values('process')
|
|
51
|
+
srcs = s.process_source_sink.get_level_values('source')
|
|
52
|
+
snks = s.process_source_sink.get_level_values('sink')
|
|
53
|
+
as_source = {p for p, src in zip(procs, srcs) if src == p}
|
|
54
|
+
as_sink = {p for p, snk in zip(procs, snks) if snk == p}
|
|
55
|
+
return as_source & as_sink
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _sort_unit_node_columns(df: pd.DataFrame, indirect: set) -> pd.DataFrame:
|
|
59
|
+
"""Reorder a (unit, node) MultiIndex column frame to match v3.32.0.
|
|
60
|
+
|
|
61
|
+
Partition: direct units first, indirect units last. Alphabetical by
|
|
62
|
+
(unit, node) within each partition.
|
|
63
|
+
"""
|
|
64
|
+
if df.empty or df.columns.empty:
|
|
65
|
+
return df
|
|
66
|
+
sorted_cols = sorted(
|
|
67
|
+
df.columns,
|
|
68
|
+
key=lambda c: (c[0] in indirect, c[0], c[1]),
|
|
69
|
+
)
|
|
70
|
+
return df.reindex(columns=pd.MultiIndex.from_tuples(
|
|
71
|
+
sorted_cols, names=df.columns.names,
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def unit_outputNode(par, s, v, r, debug):
|
|
76
|
+
"""Unit output node flow for periods and time"""
|
|
77
|
+
|
|
78
|
+
results = []
|
|
79
|
+
|
|
80
|
+
if r.flow_dt.empty:
|
|
81
|
+
return results
|
|
82
|
+
|
|
83
|
+
# Calculate timestep-level results first
|
|
84
|
+
result_multi_dt = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['unit', 'node']))
|
|
85
|
+
|
|
86
|
+
# Filter columns: unit processes that have sinks
|
|
87
|
+
unit_sink_cols = r.flow_dt.columns[
|
|
88
|
+
r.flow_dt.columns.get_level_values(0).isin(s.process_unit) &
|
|
89
|
+
r.flow_dt.columns.to_series().apply(lambda col: (col[0], col[2]) in s.process_sink)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
for col in unit_sink_cols:
|
|
93
|
+
u, source, sink = col
|
|
94
|
+
result_multi_dt[(u, sink)] = r.flow_dt[col]
|
|
95
|
+
|
|
96
|
+
# Reorder columns to match v3.32.0's direct-then-indirect / alphabetical
|
|
97
|
+
# ordering — replaces the upstream ``flow_dt.columns`` order which is
|
|
98
|
+
# non-deterministic at HEAD (polars ``unique()`` after sort).
|
|
99
|
+
indirect = _indirect_units(s)
|
|
100
|
+
result_multi_dt = _sort_unit_node_columns(result_multi_dt, indirect)
|
|
101
|
+
|
|
102
|
+
# Snap sub-femto HiGHS LP residuals to exact zero before emitting so the
|
|
103
|
+
# CSV round-trip preserves the int dtype of zero-valued columns.
|
|
104
|
+
result_multi_dt = _clip_flow_residuals(result_multi_dt)
|
|
105
|
+
|
|
106
|
+
# Return timestep results
|
|
107
|
+
results.append((result_multi_dt, 'unit_outputNode_dt_ee'))
|
|
108
|
+
|
|
109
|
+
# Aggregate to period level. v_flow is in MW (MWh/h) — multiply by
|
|
110
|
+
# step_duration to get MWh per step, weight each (d, t) by
|
|
111
|
+
# par.timestep_weight (=1.0 with no/uniform timeset_weights →
|
|
112
|
+
# byte-identical), then sum and annualize.
|
|
113
|
+
result_multi_d = annualize_dt_to_d(
|
|
114
|
+
result_multi_dt, par.timestep_weight,
|
|
115
|
+
par.complete_period_share_of_year, par.step_duration,
|
|
116
|
+
)
|
|
117
|
+
result_multi_d = _clip_flow_residuals(result_multi_d)
|
|
118
|
+
|
|
119
|
+
# Return period results
|
|
120
|
+
results.append((result_multi_d, 'unit_outputNode_d_ee'))
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def unit_inputNode(par, s, v, r, debug):
|
|
126
|
+
"""Unit input node flow for periods and time"""
|
|
127
|
+
|
|
128
|
+
results = []
|
|
129
|
+
|
|
130
|
+
if r.flow_dt.empty:
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
# Calculate timestep-level results first
|
|
134
|
+
result_multi_dt = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['unit', 'node']))
|
|
135
|
+
|
|
136
|
+
# Filter columns: unit processes that have sources
|
|
137
|
+
unit_source_cols = r.flow_dt.columns[
|
|
138
|
+
r.flow_dt.columns.get_level_values(0).isin(s.process_unit) &
|
|
139
|
+
r.flow_dt.columns.to_series().apply(lambda col: (col[0], col[1]) in s.process_source)
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
for col in unit_source_cols:
|
|
143
|
+
u, source, sink = col
|
|
144
|
+
result_multi_dt[(u, source)] = -r.flow_dt[col]
|
|
145
|
+
|
|
146
|
+
# Reorder columns to match v3.32.0's direct-then-indirect / alphabetical
|
|
147
|
+
# ordering (see ``unit_outputNode`` for rationale).
|
|
148
|
+
indirect = _indirect_units(s)
|
|
149
|
+
result_multi_dt = _sort_unit_node_columns(result_multi_dt, indirect)
|
|
150
|
+
|
|
151
|
+
# Snap sub-femto HiGHS LP residuals to exact zero (see ``unit_outputNode``).
|
|
152
|
+
result_multi_dt = _clip_flow_residuals(result_multi_dt)
|
|
153
|
+
|
|
154
|
+
# Return timestep results
|
|
155
|
+
results.append((result_multi_dt, 'unit_inputNode_dt_ee'))
|
|
156
|
+
|
|
157
|
+
# Aggregate to period level. v_flow is MW — multiply by step_duration
|
|
158
|
+
# to get MWh per step, weight each (d, t) by par.timestep_weight (=1.0
|
|
159
|
+
# with no/uniform timeset_weights → byte-identical), then sum/annualize.
|
|
160
|
+
result_multi_d = annualize_dt_to_d(
|
|
161
|
+
result_multi_dt, par.timestep_weight,
|
|
162
|
+
par.complete_period_share_of_year, par.step_duration,
|
|
163
|
+
)
|
|
164
|
+
result_multi_d = _clip_flow_residuals(result_multi_d)
|
|
165
|
+
|
|
166
|
+
# Return period results
|
|
167
|
+
results.append((result_multi_d, 'unit_inputNode_d_ee'))
|
|
168
|
+
|
|
169
|
+
return results
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def unit_cf_outputNode(par, s, v, r, debug):
|
|
173
|
+
"""Unit capacity factors by output node for periods"""
|
|
174
|
+
complete_hours = par.complete_period_share_of_year * 8760
|
|
175
|
+
unit_cols = r.process_sink_flow_d.columns[r.process_sink_flow_d.columns.get_level_values(0).isin(s.process_unit)]
|
|
176
|
+
unit_capacity = r.entity_all_capacity[unit_cols.droplevel(1).unique()].rename_axis('process', axis=1)
|
|
177
|
+
unit_capacity.columns = unit_capacity.columns.get_level_values(0)
|
|
178
|
+
results = r.process_sink_flow_d[unit_cols].div(unit_capacity, level=0).div(complete_hours, axis=0)
|
|
179
|
+
# Capacity factor is undefined for units with no capacity (e.g. an un-built
|
|
180
|
+
# investment candidate, capacity == 0). Dividing a tiny LP residual flow by
|
|
181
|
+
# zero capacity yields +/-inf; collapse those to NaN so downstream reductions
|
|
182
|
+
# (plot sums/means) skip them instead of summing +inf and -inf into a
|
|
183
|
+
# spurious NaN (RuntimeWarning: invalid value encountered in reduce).
|
|
184
|
+
results = results.replace([float('inf'), float('-inf')], float('nan'))
|
|
185
|
+
results.columns.names = ['unit', 'sink']
|
|
186
|
+
return results, 'unit_outputs_cf_d_ee'
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def unit_cf_inputNode(par, s, v, r, debug):
|
|
190
|
+
"""Unit capacity factors by input node for periods"""
|
|
191
|
+
# !!! This should account for efficiency losses in direct conversion units (but it does not)
|
|
192
|
+
complete_hours = par.complete_period_share_of_year * 8760
|
|
193
|
+
unit_source = r.process_source_flow_d.columns[r.process_source_flow_d.columns.get_level_values(0).isin(s.process_unit)]
|
|
194
|
+
unit_capacity = r.entity_all_capacity[unit_source.droplevel(1).unique()].rename_axis('process', axis=1)
|
|
195
|
+
unit_capacity.columns = unit_capacity.columns.get_level_values(0)
|
|
196
|
+
results = r.process_source_flow_d[unit_source].div(unit_capacity, level=0).div(complete_hours, axis=0)
|
|
197
|
+
# CF undefined for zero-capacity units -> NaN (see unit_cf_outputNode).
|
|
198
|
+
results = results.replace([float('inf'), float('-inf')], float('nan'))
|
|
199
|
+
results.columns.names = ['unit', 'source']
|
|
200
|
+
return results, 'unit_inputs_cf_d_ee'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def unit_VRE_curtailment_and_potential(par, s, v, r, debug):
|
|
204
|
+
"""Unit VRE curtailment and potential for both periods and timesteps"""
|
|
205
|
+
|
|
206
|
+
results = []
|
|
207
|
+
vre_processes = s.process_VRE.unique()
|
|
208
|
+
|
|
209
|
+
# Timestep-level curtailment (absolute values) - calculate first
|
|
210
|
+
if not r.flow_dt.empty and not r.potentialVREgen_dt.empty:
|
|
211
|
+
curtail_dt = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['unit', 'sink']))
|
|
212
|
+
potential_dt = pd.DataFrame(index=s.dt_realize_dispatch, columns=pd.MultiIndex.from_tuples([], names=['unit', 'sink']))
|
|
213
|
+
|
|
214
|
+
for col in r.flow_dt.columns:
|
|
215
|
+
u, source, sink = col
|
|
216
|
+
if u in vre_processes and (u, sink) in s.process_sink and (u, sink) in r.potentialVREgen_dt.columns:
|
|
217
|
+
curtail_dt[u, sink] = r.potentialVREgen_dt[(u, sink)] - r.flow_dt[col]
|
|
218
|
+
potential_dt[u, sink] = r.potentialVREgen_dt[(u, sink)]
|
|
219
|
+
|
|
220
|
+
results.append((curtail_dt, 'unit_curtailment_outputNode_dt_ee'))
|
|
221
|
+
results.append((potential_dt, 'unit_VRE_potential_outputNode_dt_ee'))
|
|
222
|
+
|
|
223
|
+
# Calculate curtailment share at timestep level
|
|
224
|
+
curtail_share_dt = (curtail_dt / potential_dt).where(potential_dt != 0, 0)
|
|
225
|
+
results.append((curtail_share_dt, 'unit_curtailment_share_outputNode_dt_ee'))
|
|
226
|
+
|
|
227
|
+
# Aggregate to period level. curtail_dt / potential_dt are MW —
|
|
228
|
+
# multiply by step_duration to get MWh per step, then weight each
|
|
229
|
+
# (d, t) by par.timestep_weight (=1.0 with no/uniform timeset_weights
|
|
230
|
+
# → byte-identical) before summing. Both numerator and denominator
|
|
231
|
+
# carry the same weight, so the curtailment-share ratio below is
|
|
232
|
+
# unchanged; only the absolute MWh outputs move.
|
|
233
|
+
curtail_period = (
|
|
234
|
+
curtail_dt.mul(par.step_duration, axis=0)
|
|
235
|
+
.mul(par.timestep_weight, axis=0)
|
|
236
|
+
.groupby(level='period').sum()
|
|
237
|
+
)
|
|
238
|
+
potential_period = (
|
|
239
|
+
potential_dt.mul(par.step_duration, axis=0)
|
|
240
|
+
.mul(par.timestep_weight, axis=0)
|
|
241
|
+
.groupby(level='period').sum()
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Calculate curtailment share at period level (ratio — step_duration cancels)
|
|
245
|
+
curtail_share_period = (curtail_period / potential_period).where(potential_period != 0, 0)
|
|
246
|
+
|
|
247
|
+
results.append((curtail_period, 'unit_curtailment_outputNode_d_ee'))
|
|
248
|
+
results.append((curtail_share_period, 'unit_curtailment_share_outputNode_d_ee'))
|
|
249
|
+
results.append((potential_period, 'unit_VRE_potential_outputNode_d_ee'))
|
|
250
|
+
|
|
251
|
+
return results
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def unit_ramps(par, s, v, r, debug):
|
|
255
|
+
"""Unit ramps by input and output nodes for timesteps"""
|
|
256
|
+
results = []
|
|
257
|
+
if r.ramp_dtt.empty:
|
|
258
|
+
return results
|
|
259
|
+
|
|
260
|
+
# Output node ramps
|
|
261
|
+
process_sink_ramp_output = s.process_sink[s.process_sink.get_level_values(0).isin(s.process_unit)]
|
|
262
|
+
pss_ramp_output = r.ramp_dtt.columns[r.ramp_dtt.columns.droplevel(1).isin(process_sink_ramp_output)]
|
|
263
|
+
ramp_output = r.ramp_dtt[pss_ramp_output].droplevel('t_previous')
|
|
264
|
+
ramp_output.columns = ramp_output.columns.droplevel(1) # Remove 'source' from (process, source, sink) to get (unit, source)
|
|
265
|
+
ramp_output.columns.names = ['unit', 'sink']
|
|
266
|
+
results.append((ramp_output, 'unit_ramp_outputs_dt_ee'))
|
|
267
|
+
|
|
268
|
+
# Input node ramps
|
|
269
|
+
process_source_ramp_input = s.process_source[s.process_source.get_level_values(0).isin(s.process_unit)]
|
|
270
|
+
pss_ramp_input = r.ramp_dtt.columns[r.ramp_dtt.columns.droplevel(2).isin(process_source_ramp_input)]
|
|
271
|
+
ramp_input = r.ramp_dtt[pss_ramp_input].droplevel('t_previous')
|
|
272
|
+
ramp_input.columns = ramp_input.columns.droplevel(2) # Remove 'sink' from (process, source, sink) to get (unit, source)
|
|
273
|
+
ramp_input.columns.names = ['unit', 'source']
|
|
274
|
+
results.append((ramp_input, 'unit_ramp_inputs_dt_ee'))
|
|
275
|
+
|
|
276
|
+
return results
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def unit_online_and_startup(par, s, v, r, debug):
|
|
280
|
+
"""Unit online status and startups for timesteps and periods"""
|
|
281
|
+
results = []
|
|
282
|
+
|
|
283
|
+
# 1. Online units dt
|
|
284
|
+
online_units_dt = r.process_online_dt[s.process_unit.intersection(s.process_online)]
|
|
285
|
+
results.append((online_units_dt, 'unit_online_dt_e'))
|
|
286
|
+
|
|
287
|
+
# 2. Average online status at period level (weighted by step_duration)
|
|
288
|
+
complete_hours = par.complete_period_share_of_year * 8760
|
|
289
|
+
online_units_d = online_units_dt.mul(par.step_duration, axis=0).groupby('period').sum().div(complete_hours, axis=0)
|
|
290
|
+
results.append((online_units_d, 'unit_online_average_d_e'))
|
|
291
|
+
|
|
292
|
+
# 3. Startups annualized to period level. Raw startups only cover modeled
|
|
293
|
+
# timesteps; weight each by p_timestep_weight and divide by the period share
|
|
294
|
+
# of a year so the value is an annual count (same convention as flows).
|
|
295
|
+
units_online = s.process_unit.intersection(s.process_online)
|
|
296
|
+
startup_units_d = annualize_dt_to_d(
|
|
297
|
+
r.process_startup_dt[units_online], par.timestep_weight,
|
|
298
|
+
par.complete_period_share_of_year)
|
|
299
|
+
results.append((startup_units_d, 'unit_startup_d_e'))
|
|
300
|
+
|
|
301
|
+
return results
|