Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Total PowerAnalytics Redesign: ComponentSelectors and Metrics #24

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8caef0d
Implement ComponentEntity
GabrielKS Nov 14, 2023
73297b4
Implement ListEntitySet
GabrielKS Nov 20, 2023
207194a
Implement SubtypeEntitySet
GabrielKS Nov 20, 2023
62a4a28
Implement TopologyEntitySet
GabrielKS Nov 20, 2023
4b3422e
Implement FilterEntitySet
GabrielKS Nov 21, 2023
8f57c88
Make test simulations more interesting
GabrielKS Nov 22, 2023
40b81fb
Implement the very basics of Metrics
GabrielKS Nov 28, 2023
e8e0c8d
Refactor test setup into its own file
GabrielKS Nov 28, 2023
d601fbc
Implement some input convenience functions
GabrielKS Nov 29, 2023
ffb3a83
Minor create_problem_results_dict bugfix
GabrielKS Nov 30, 2023
846b24b
Add metrics wrapper functions and compute_all
GabrielKS Nov 30, 2023
135b895
Make entities only return available components
GabrielKS Dec 1, 2023
c605790
More gracefully handle missing data (draft)
GabrielKS Dec 2, 2023
bfae18a
Add some Metric creation infrastructure and built-in Metrics
GabrielKS Dec 2, 2023
269b885
Add metadata columns feature
GabrielKS Dec 22, 2023
ebfca89
Add aggregate_time function
GabrielKS Dec 23, 2023
803e303
Add hcat_timed and calc_discharge_cycles
GabrielKS Dec 27, 2023
51ffbb9
Add SystemTimedMetrics
GabrielKS Dec 28, 2023
aa992aa
Add some more built-in metrics
GabrielKS Dec 28, 2023
7fe1039
Add ResultsTimelessMetrics
GabrielKS Dec 29, 2023
262562a
Add CustomTimedMetric (untested), more built-in metrics
GabrielKS Jan 4, 2024
515ea8d
Implement compose_metrics
GabrielKS Jan 5, 2024
984cee8
Give metrics control over aggregation functions
GabrielKS Jan 5, 2024
e39f0ed
Add more built-in metrics!
GabrielKS Jan 5, 2024
a36df4b
Add another way to call compute_all
GabrielKS Jan 6, 2024
51c04d3
Add aggregation metadata, etc. (see details)
GabrielKS Jan 17, 2024
df4757e
Fix calc_capacity_factor
GabrielKS Jan 17, 2024
62358e7
Add author
GabrielKS Jan 18, 2024
75de83b
Implement compute_set
GabrielKS Jan 23, 2024
a60408b
Resolve domain knowledge issues in builtin_metrics.jl
GabrielKS Jan 24, 2024
feeacaf
Add documentation for input, metrics
GabrielKS Jan 24, 2024
abdf5b9
Fix metrics documentation formatting
GabrielKS Jan 31, 2024
76619aa
Fix compute_all kwargs bug
GabrielKS Jan 31, 2024
cfc6df4
Replace get_populated_decision_problem_results
tengis-nrl Feb 24, 2024
666ae25
run the formatter
tengis-nrl Feb 24, 2024
b04adae
Add built-in entities for load, storage, fuel types
GabrielKS Feb 27, 2024
af83f76
Rename `Entity` to `ComponentSelector`, handle the ensuing carnage
GabrielKS Feb 28, 2024
3d233c5
Make use of caching in `read_component_result` for massive speedup
GabrielKS Mar 6, 2024
3da6164
Accommodate `psy4` cost function refactor
GabrielKS Jun 25, 2024
3e737db
Rename Entity-related files to refer to ComponentSelector instead
GabrielKS Mar 18, 2024
78a986f
Remove ComponentSelector files for move to IS and PSY
GabrielKS Mar 18, 2024
3547749
make PA tests pass
tengis-nrl Mar 24, 2024
8f1e5bb
run formatter
tengis-nrl Mar 25, 2024
5823217
Update storage naming for `psy4`
GabrielKS Jun 11, 2024
5272ea2
Reevaluate `ComponentSelector`-related imports and exports
GabrielKS Jun 11, 2024
40a886c
Calculate metrics only on components marked available
GabrielKS Jun 26, 2024
00e72e5
Only `compute` on `available` subselectors, use PSY4 storage interface
GabrielKS Jul 17, 2024
15f6fa0
Prototype `ComponentSelector` grouping (tests not updated)
GabrielKS Sep 20, 2024
7ebc456
Prototype `Metric` grouping and callability (tests not updated)
GabrielKS Sep 20, 2024
eab6286
Fix bug in calling `repeat`
GabrielKS Sep 24, 2024
43d3b2d
Rename `select_components` to `make_selector`
GabrielKS Sep 24, 2024
c5e1b48
Add ability to disable test files like in other Sienna packages
GabrielKS Sep 26, 2024
e09fc86
Accommodate upstream refactors
GabrielKS Sep 26, 2024
ec352cd
Wrap built-in selectors in their own submodule
GabrielKS Sep 26, 2024
3de0edb
Rename `input.jl` to `input_utils.jl`
GabrielKS Sep 27, 2024
e11dde2
Cleanup: move helper functions to `output_utils.jl`, use `@kwdef`
GabrielKS Sep 27, 2024
5db41ca
Use `kwargs...` instead of `start_time, len`
GabrielKS Sep 27, 2024
ca4faf3
Remove `Metric` `description` field in favor of docstrings
GabrielKS Sep 27, 2024
d80988c
Cleanup: move more code to `utils` files, run formatter
GabrielKS Sep 27, 2024
b0d78ad
Make minor adjustments, finish `compute` interface change
GabrielKS Sep 30, 2024
5cc6047
Prototype `Metrics` submodule
GabrielKS Sep 30, 2024
c387507
Replace `with_component_agg_fn`, etc. with `rebuild_metric`
GabrielKS Oct 1, 2024
d96c3a7
Make tests pass, add necessary imports, adjust documentation
GabrielKS Oct 1, 2024
85241c1
Add tests for built-in metrics; fix bugs thereby uncovered
GabrielKS Oct 1, 2024
1cae840
Accommodate `filterby` -> `scope_limiter` rename
GabrielKS Oct 4, 2024
c509cb7
Bugfix: add fallback for when `get_available` is not defined
GabrielKS Oct 8, 2024
a005d09
Minor edits from PR comments 1
GabrielKS Oct 9, 2024
57dd698
Accommodate `ComponentSelector` API changes
GabrielKS Oct 30, 2024
4d45fdc
Refactor `generator_mapping.yaml`, prototype new built-in selectors
GabrielKS Oct 30, 2024
abd4467
Clean up `generator_mapping.yaml` selectors implementation, test
GabrielKS Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!, time_df, time_vec, data_cols, data_df,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of these function names are ripe for collision with users' local variables. Perhaps get_time_df instead of time_df?

data_vec, 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_set, compute_all, hcat_timed, 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already public.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in older minor versions we commit to supporting (see NREL-Sienna/InfrastructureSystems.jl#390)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. @jd-lara Is it time to force users to upgrade? 1.11 is out. If people haven't upgraded to at least 1.9, they probably should.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does there need to be an old and new?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to maintain a strict separation between the two interfaces until we decide what to do with the old one (e.g., should it be deprecated once PowerGraphics no longer depends on it)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a 1.0 vs 2.0 kinda thing? Basically, do we need to maintain support for the old methods in the new version if those users can continue to use the old version?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue is that the current latest version of PowerGraphics depends on old PowerAnalytics and I don't want to keep PowerGraphics users from using new PowerAnalytics. I'm thinking something like this:

  1. Release new PA, keep old PA as is
  2. Develop a user base on new PA, fix some bugs
  3. Deprecate old PA (i.e., throw warnings) but keep it so PG can use it
  4. Revamp PG to only depend on new PA (I would be interested in supervising an intern to do this)
  5. Remove old PA

but not committed to that, open to discussion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> #28

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
159 changes: 159 additions & 0 deletions src/builtin_component_selectors.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const FUEL_TYPES_DATA_FILE =
joinpath(dirname(dirname(pathof(PowerAnalytics))), "deps", "generator_mapping.yaml")
const FUEL_TYPES_META_KEY = "__META"

# 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,
)
# TODO support types beyond PowerSystems
gen_type = getproperty(PowerSystems, Symbol(get(category_spec, "gentype", Component)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't Sourabh say that he could have custom types from another package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I should make this more flexible somehow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on adding this as a minor update later?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that I don't see how it could ever be solved. I'm guessing that the PSY table data parser does something like this, and that's OK because it's only parsing types that it knows about. Would this function ever because in a situation with custom types? I'm ok with moving forward if we can bound the problem.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely do something like this to load serialized systems, I think by checking the Main namespace — I get errors loading certain Sourabh systems if I haven't done using StorageSystemsSimulations — so I'm envisioning something like that. This only gets called on types listed in generator_mapping.yaml, but you can have a custom generator_mapping.yaml and I can envision Sourabh putting his custom types there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then that file will have to include the name of the package so that you know what to pass to getproperty. That is a change that could/should be made now.

(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 = join(ifelse.(isnothing.(parse_results), "", string.(parse_results)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if prime_mover and fuel_category are both nothing, the name will be "ThermalStandard____"? That will look like a bug to the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secondly, calling string(gen_type) will produce different results depending on whether you use import or using to load the package. Someone could decide to change the method in this package and mysteriously cause these names to change.

COMPONENT_NAME_DELIMITER)

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 && length(subselectors) == 0 && 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"
all_loads::ComponentSelector = make_selector(PSY.ElectricLoad)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const for all these?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


"A ComponentSelector representing all the storage in a System"
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`
"""
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)
"""
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`
"""
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`
"""
categorized_generators::Union{ComponentSelector, Nothing} =
if isnothing(generator_categories)
nothing
else
make_selector(values(generator_categories)...)
end
end
Loading
Loading