Skip to content

Commit

Permalink
Merge pull request #24 from GabrielKS/gks/entity-metric-redesign-psy4
Browse files Browse the repository at this point in the history
Total PowerAnalytics Redesign: `ComponentSelector`s and `Metric`s
  • Loading branch information
jd-lara authored Feb 4, 2025
2 parents 8105669 + 63486ac commit 838c8e1
Show file tree
Hide file tree
Showing 18 changed files with 2,613 additions and 78 deletions.
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "PowerAnalytics"
uuid = "56ce1300-00bc-47e4-ba8c-b166ccc19f51"
authors = ["cbarrows <[email protected]>"]
authors = ["Gabriel Konar-Steenberg <[email protected]>", "cbarrows <[email protected]>"]
version = "0.8.1"

[deps]
Expand All @@ -11,13 +11,14 @@ InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"

[compat]
Dates = "1"
DataFrames = "1"
DataStructures = "0.18"
Dates = "1"
InfrastructureSystems = "2"
InteractiveUtils = "1"
PowerSimulations = "^0.29"
Expand Down
12 changes: 7 additions & 5 deletions deps/generator_mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ NG-CC:
- {gentype: Any, primemover: CC, fuel: NATURAL_GAS}
NG-Steam:
- {gentype: Any, primemover: ST, fuel: NATURAL_GAS}
Natural gas:
- {gentype: Any, primemover: null, fuel: NATURAL_GAS}
- {gentype: Any, primemover: null, fuel: OTHER_GAS}
Hydropower:
- {gentype: Any, primemover: HY, fuel: null}
Coal:
Expand All @@ -36,6 +33,11 @@ Biopower:
- {gentype: Any, primemover: null, fuel: WOOD_WASTE}
CSP:
- {gentype: Any, primemover: CP, fuel: null}
Load:
- {gentype: ElectricLoad, primemover: null, fuel: null}
Other:
- {gentype: Any, primemover: OT, fuel: OTHER}
- {gentype: Any, primemover: null, fuel: null}
- {gentype: Generator, primemover: OT, fuel: OTHER}
__META:
non_generators:
- Storage
- Load
50 changes: 47 additions & 3 deletions src/PowerAnalytics.jl
Original file line number Diff line number Diff line change
@@ -1,31 +1,75 @@
module PowerAnalytics

# EXPORTS
export make_fuel_dictionary
export get_generation_data
export get_load_data
export get_service_data
export categorize_data
export no_datetime

#I/O Imports
export ComponentSelector, SingularComponentSelector, PluralComponentSelector
export make_selector, get_name, get_subselectors
export Metric, TimedMetric, TimelessMetric, ComponentSelectorTimedMetric,
ComponentTimedMetric,
SystemTimedMetric, ResultsTimelessMetric, CustomTimedMetric
export DATETIME_COL, META_COL_KEY, SYSTEM_COL, RESULTS_COL, AGG_META_KEY
export is_col_meta, set_col_meta, set_col_meta!, get_time_df, get_time_vec, get_data_cols,
get_data_df, get_data_vec, get_data_mat, get_description, get_component_agg_fn,
get_time_agg_fn, with_component_agg_fn, with_time_agg_fn, metric_selector_to_string,
get_agg_meta, set_agg_meta!, rebuild_metric
export compute, compute_all, hcat_timed_dfs, aggregate_time, compose_metrics
export create_problem_results_dict
export parse_generator_mapping_file, parse_injector_categories, parse_generator_categories
export mean, weighted_mean, unweighted_sum

# IMPORTS
import Base: @kwdef
import Dates
import Dates: DateTime
import TimeSeries
import Statistics
import Statistics: mean
import DataFrames
import DataFrames: DataFrame, metadata, metadata!, colmetadata, colmetadata!
import YAML
import DataStructures: OrderedDict, SortedDict
import DataStructures: SortedDict
import PowerSystems
import PowerSystems:
Component,
ComponentSelector,
make_selector, get_name, get_groups,
get_component, get_components,
get_available,
COMPONENT_NAME_DELIMITER,
rebuild_selector

import InfrastructureSystems
import PowerSimulations
import PowerSimulations:
get_system
import InteractiveUtils

# ALIASES
const PSY = PowerSystems
const IS = InfrastructureSystems
const PSI = PowerSimulations

# INCLUDES
# Old PowerAnalytics
include("definitions.jl")
include("get_data.jl")
include("fuel_results.jl")

greet() = print("Hello World!")
# New PowerAnalytics
include("input_utils.jl")
include("output_utils.jl")
include("metrics.jl")
include("builtin_component_selectors.jl")
include("builtin_metrics.jl")

# SUBMODULES
using .Selectors
using .Metrics

end # module PowerAnalytics
182 changes: 182 additions & 0 deletions src/builtin_component_selectors.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
const FUEL_TYPES_DATA_FILE =
joinpath(dirname(dirname(pathof(PowerAnalytics))), "deps", "generator_mapping.yaml")
const FUEL_TYPES_META_KEY = "__META"

"""
Parse the `gentype` to a type. This is done by first checking whether gentype is qualified
(`ModuleName.TypeName`). If so, the module is fetched from the `Main` scope and the type
name is fetched from the module. If not, we default to fetching from `PowerSystems` for
convenience.
"""
function lookup_gentype(gentype::AbstractString)
if occursin(".", gentype)
splitted = split(gentype, ".")
(length(splitted) == 2) || throw(ArgumentError("Cannot parse gentype '$gentype'"))
mod, typename = splitted
return getproperty(getproperty(Main, Symbol(mod)), Symbol(typename))
end
return getproperty(PowerSystems, Symbol(gentype))
end

# Parse the strings in generator_mapping.yaml into types and enum items
function parse_fuel_category(
category_spec::Dict;
root_type::Type{<:Component} = PSY.StaticInjection,
)
gen_type = lookup_gentype(get(category_spec, "gentype", "Component"))
(gen_type === Any) && (gen_type = root_type)
# Constrain gen_type such that gen_type <: root_type
gen_type = typeintersect(gen_type, root_type)

pm = get(category_spec, "primemover", nothing)
isnothing(pm) || (pm = PSY.parse_enum_mapping(PSY.PrimeMovers, pm))

fc = get(category_spec, "fuel", nothing)
isnothing(fc) || (fc = PSY.parse_enum_mapping(PSY.ThermalFuels, fc))

return gen_type, pm, fc
end

function make_fuel_component_selector(
category_spec::Dict;
root_type::Type{<:Component} = PSY.StaticInjection,
)
parse_results = parse_fuel_category(category_spec; root_type = root_type)
(gen_type, prime_mover, fuel_category) = parse_results
# If gen_type is the bottom type, it means it doesn't fit in root_type and we shouldn't include the selector at all
(gen_type <: Union{}) && return nothing

function filter_closure(comp::Component)
comp_sig = Tuple{typeof(comp)}
if !isnothing(prime_mover)
hasmethod(PowerSystems.get_prime_mover_type, comp_sig) || return false
(PowerSystems.get_prime_mover_type(comp) == prime_mover) || return false
end
if !isnothing(fuel_category)
hasmethod(PowerSystems.get_fuel, comp_sig) || return false
(PowerSystems.get_fuel(comp) == fuel_category) || return false
end
return true
end

# Create a nice name that is guaranteed to never collide with fully-qualified component names
selector_name = string(nameof(parse_results[1]))
if !all(isnothing.(parse_results[2:end]))
selector_name *=
COMPONENT_NAME_DELIMITER * join(
ifelse.(
isnothing.(parse_results[2:end]),
"Any",
string.(parse_results[2:end])),
COMPONENT_NAME_DELIMITER)
end

return make_selector(filter_closure, gen_type; name = selector_name)
end

# Based on old PowerAnalytics' get_generator_mapping
"""
Parse a `generator_mapping.yaml` file into a dictionary of `ComponentSelector`s and a
dictionary of metadata if present
"""
function parse_generator_mapping_file(
filename;
root_type::Type{<:Component} = PSY.StaticInjection,
)
# NOTE the YAML library does not support ordered loading
in_data = open(YAML.load, filename)
mappings = Dict{String, ComponentSelector}()
for top_level in keys(in_data)
(top_level == FUEL_TYPES_META_KEY) && continue
subselectors =
make_fuel_component_selector.(
in_data[top_level]; root_type = PSY.StaticInjection)
# A subselector will be nothing if it doesn't fit under root_type
subselectors = filter(!isnothing, subselectors)
# Omit the category entirely if root_type causes elimination of all subselectors
(length(in_data[top_level]) > 0 && isempty(subselectors)) && continue
mappings[top_level] = make_selector(subselectors...; name = top_level)
end
return mappings, get(in_data, FUEL_TYPES_META_KEY, nothing)
end

"""
Use [`parse_generator_mapping_file`](@ref) to parse a `generator_mapping.yaml` file into a
dictionary of all `ComponentSelector`s
"""
parse_injector_categories(filename; root_type::Type{<:Component} = PSY.StaticInjection) =
first(parse_generator_mapping_file(filename; root_type = root_type))

"""
Use [`parse_generator_mapping_file`](@ref) to parse a `generator_mapping.yaml` file into a
dictionary of `ComponentSelector`, excluding categories in the 'non_generators' list in
metadata
"""
function parse_generator_categories(filename;
root_type::Type{<:Component} = PSY.StaticInjection)
categories, meta = parse_generator_mapping_file(filename; root_type = root_type)
(isnothing(meta) || !haskey(meta, "non_generators")) && return nothing
return filter(pair -> !(first(pair) in meta["non_generators"]), categories)
end

# SELECTORS MODULE
"`PowerAnalytics` built-in `ComponentSelector`s. Use `names` to list what is available."
module Selectors
import
..make_selector,
..PSY,
..parse_generator_mapping_file,
..parse_injector_categories,
..parse_generator_categories,
..ComponentSelector,
..FUEL_TYPES_DATA_FILE
export
all_loads,
all_storage,
injector_categories,
generator_categories,
categorized_injectors,
categorized_generators

"A ComponentSelector representing all the electric load in a System"
const all_loads::ComponentSelector = make_selector(PSY.ElectricLoad)

"A ComponentSelector representing all the storage in a System"
const all_storage::ComponentSelector = make_selector(PSY.Storage)

"""
A dictionary of `ComponentSelector`s, each of which corresponds to one of the static
injector categories in `generator_mapping.yaml`
"""
const injector_categories::AbstractDict{String, ComponentSelector} =
parse_injector_categories(FUEL_TYPES_DATA_FILE)

"""
A dictionary of `ComponentSelector`s, each of which corresponds to one of the categories in
`generator_mapping.yaml`, only considering the components and categories that represent
generators (no storage or load)
"""
const generator_categories::Union{AbstractDict{String, ComponentSelector}, Nothing} = let
result = parse_generator_categories(FUEL_TYPES_DATA_FILE)
isnothing(result) && @warn "Could not construct generator categories"
result
end

"""
A single `ComponentSelector` representing the static injectors in a `System` grouped by the
categories in `generator_mapping.yaml`
"""
const categorized_injectors::ComponentSelector =
make_selector(values(injector_categories)...)

"""
A single `ComponentSelector` representing the generators in a `System` (no storage or load)
grouped by the categories in `generator_mapping.yaml`
"""
const categorized_generators::Union{ComponentSelector, Nothing} =
if isnothing(generator_categories)
nothing
else
make_selector(values(generator_categories)...)
end
end
Loading

0 comments on commit 838c8e1

Please sign in to comment.