Mb/sources#75
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for treating PSY.Source components as part of “generation” results processing, enabling sources to be included in key selection, fixed-parameter promotion, and fuel/category aggregation.
Changes:
- Add Source-specific variable/parameter key filtering and a
sourcestoggle inget_generation_data. - Update categorization logic to handle split-power time-series parameters (
ActivePower(In|Out)TimeSeriesParameter) in addition to variables. - Extend fuel categorization + YAML mapping to include
Sourcecomponents.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/get_data.jl |
Adds Source key collection, improves fixed-parameter handling for multiple Source params, and extends categorization for split-power time-series parameters. |
src/fuel_results.jl |
Includes PSY.Source components in make_fuel_dictionary so they participate in fuel/category aggregation. |
src/definitions.jl |
Defines supported Source variable/parameter entry types. |
deps/generator_mapping.yaml |
Adds a Source category mapping and marks it as a non-generator in metadata. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function get_generation_data( | ||
| results::R; | ||
| # aggregation::Union{ | ||
| # Type{PSY.StaticInjection}, | ||
| # Type{PSY.ACBus}, | ||
| # Type{PSY.System}, | ||
| # Type{<:PSY.AggregationTopology}, | ||
| # } = PSY.StaticInjection, | ||
| filter_func::Union{Function, Nothing} = nothing, | ||
| kwargs..., | ||
| ) where {R <: IS.Results} | ||
| initial_time = get(kwargs, :initial_time, get(kwargs, :start_time, nothing)) | ||
| len = get(kwargs, :horizon, get(kwargs, :len, nothing)) | ||
| variable_keys = get(kwargs, :variable_keys, PSI.list_variable_keys(results)) | ||
| parameter_keys = get(kwargs, :parameter_keys, PSI.list_parameter_keys(results)) | ||
| aux_variable_keys = get(kwargs, :aux_variable_keys, PSI.list_aux_variable_keys(results)) | ||
| curtailment = get(kwargs, :curtailment, true) | ||
| storage = get(kwargs, :storage, true) | ||
| sources = get(kwargs, :sources, true) | ||
|
|
||
| if curtailment && (haskey(kwargs, :variable_keys) || haskey(kwargs, :parameter_keys)) | ||
| @warn "Cannot guarantee curtailment calculations with specified keys" | ||
| end | ||
|
|
||
| injection_keys = get_generation_variable_keys(results; variable_keys = variable_keys) | ||
| if storage | ||
| injection_keys = vcat( | ||
| injection_keys, | ||
| get_storage_variable_keys(results; variable_keys = variable_keys), | ||
| ) | ||
| end | ||
| if sources | ||
| injection_keys = vcat( | ||
| injection_keys, | ||
| get_source_variable_keys(results; variable_keys = variable_keys), | ||
| ) | ||
| end |
| var_types = Dict{String, Symbol}() | ||
| for k in keys(data) | ||
| keystring = string(k) | ||
| device_type_string = last(split(keystring, "__")) | ||
| if occursin("ActivePowerInVariable", keystring) || | ||
| occursin("ActivePowerOutVariable", keystring) | ||
| occursin("ActivePowerOutVariable", keystring) || | ||
| occursin("ActivePowerInTimeSeriesParameter", keystring) || | ||
| occursin("ActivePowerOutTimeSeriesParameter", keystring) | ||
| push!(split_power_component_types, device_type_string) | ||
| continue | ||
| end | ||
| var_types[device_type_string] = k | ||
| end | ||
|
|
||
| # Categories that contain a split-power component type (e.g. storage, which | ||
| # reports separate ActivePowerIn/OutVariable) are emitted as "<category> In" | ||
| # and "<category> Out" instead of a single combined category. Keep the | ||
| # original key type so `aggregation[category]` works even if `aggregation` | ||
| # is keyed by something other than `String`; we stringify only at write time. | ||
| split_categories = Set{keytype(aggregation)}() | ||
| for (category, list) in aggregation | ||
| if any( | ||
| component_type in split_power_component_types for (component_type, _) in list | ||
| ) | ||
| push!(split_categories, category) | ||
| end | ||
| end | ||
|
|
||
| # Non-split components: one column per component under the original category. | ||
| # Split-power components are skipped here and handled by the In/Out pass below. | ||
| for (category, list) in aggregation | ||
| category_df = DataFrames.DataFrame() | ||
| for (component_type, component_name) in list | ||
| component_type in split_power_component_types && continue | ||
| haskey(var_types, component_type) || continue | ||
| category_data = data[var_types[component_type]] | ||
| colname = | ||
| if typeof(names(category_data)[1]) == String | ||
| "$component_name" | ||
| else | ||
| component_name | ||
| end | ||
| DataFrames.insertcols!( | ||
| category_df, | ||
| (colname => category_data[:, colname]); | ||
| makeunique = true, | ||
| ) | ||
| end | ||
| if !isempty(category_df) | ||
| category_dataframes[string(category)] = category_df | ||
| end | ||
| end | ||
|
|
||
| # Split pass: discharging (ActivePowerOutVariable) is generation (+), | ||
| # charging (ActivePowerInVariable) is load (-). | ||
| # ActivePowerInTimeSeriesParameter is already negative (its multiplier is | ||
| # active_power_limits.min, which is negative), so no sign flip is needed for it. | ||
| for category in split_categories | ||
| list = aggregation[category] | ||
| for (suffix, variable_prefix, sign) in ( | ||
| ("Out", "ActivePowerOutVariable", 1.0), | ||
| ("In", "ActivePowerInVariable", -1.0), | ||
| for (suffix, variable_prefix_sign_pairs) in ( | ||
| ( | ||
| "Out", | ||
| [ | ||
| ("ActivePowerOutVariable", 1.0), | ||
| ("ActivePowerOutTimeSeriesParameter", 1.0), | ||
| ], | ||
| ), | ||
| ( | ||
| "In", | ||
| [ | ||
| ("ActivePowerInVariable", -1.0), | ||
| ("ActivePowerInTimeSeriesParameter", 1.0), | ||
| ], | ||
| ), |
| for source in PSY.get_components(PSY.Source, sys) | ||
| if !filter_func2(source) | ||
| continue | ||
| end | ||
|
|
||
| ext = get(PSY.get_ext(source), "ext_category", nothing) | ||
| category = something( | ||
| get_generator_category(typeof(source), nothing, nothing, ext, mapping), | ||
| UNMAPPED_GENERATOR_CATEGORY, | ||
| ) | ||
| push!( | ||
| get!(gen_categories, "$category", Tuple{String, String}[]), | ||
| (string(nameof(typeof(source))), PSY.get_name(source)), | ||
| ) | ||
| end |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #75 +/- ##
==========================================
- Coverage 88.55% 87.94% -0.62%
==========================================
Files 7 7
Lines 734 763 +29
==========================================
+ Hits 650 671 +21
- Misses 84 92 +8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Adds capability for processing sources as part of the generation data.
I've tested this with EI sims. We need to eventually add more comprehensive testing to PowerAnalytics to capture more realistic sets of results.