-
Notifications
You must be signed in to change notification settings - Fork 1
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: ComponentSelector
s and Metric
s
#24
base: main
Are you sure you want to change the base?
Total PowerAnalytics Redesign: ComponentSelector
s and Metric
s
#24
Conversation
This is the first commit in a major PowerAnalytics redesign.
Previously, the test simulations only contained one RenewableDispatch component and did not dispatch any thermal generation. Now there is a solar RenewableDispatch component, and nonzero thermal generation for 2/5 generators. The old PowerAnalytics test still work.
Additional changes in this commit: - Generalize compute_all to take Components - Make create_problem_results_dict return SortedDict
Rather than setting `agg_fn`/`reduce_fn` when calling `compute` or `aggregate_time`, metrics now specify how they should be aggregated across Entities and across time.
- Add and test the ability to have `entity_agg_fn` and `time_agg_fn` use column element-wise "aggregation metadata" for things like weighted averages - Add options for how to aggregate the aggregation metadata itself (insufficiently tested) - Add option to specify what the result of `compute` looks like when there are no components (insufficiently tested) - Fix built-in curtailment fraction and integration calculations using weighted_average
I'm adding myself as the first author to reflect the fact that this redesigned version of the package is meant to be the main one in use going forward.
is_available_for_analytics(comp::Component) = | ||
try | ||
PSY.get_available(comp) | ||
catch e # A bit hacky but the alternative is hardcoding which component types don't have a `get_available` (or using `hasmethod`, which is probably worse) | ||
(e isa MethodError) && return true | ||
rethrow(e) | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_available_for_analytics(comp::Component) = | |
try | |
PSY.get_available(comp) | |
catch e # A bit hacky but the alternative is hardcoding which component types don't have a `get_available` (or using `hasmethod`, which is probably worse) | |
(e isa MethodError) && return true | |
rethrow(e) | |
end | |
function is_available_for_analytics(comp::T) | |
if hasmethod(PSY.get_available, Tuple{T}) | |
return PSY.get_available(comp) | |
else | |
return true | |
end | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My impression was that hasmethod
isn't really supposed to be used in production code like this (see e.g. here) but happy to switch if the consensus is it's less bad than the try/catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an exported function with proper docstrings! I'm not sure about that comment. Anyway, we could implement get_available(x::Component) = true
. I'll defer to @jd-lara if that is a reasonable default. It has the same outcome here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While hasmethod certainly isn't ideal, it is faster than try/catch: https://discourse.julialang.org/t/performance-of-hasmethod-vs-try-catch-on-methoderror/99827.
I agree with what that commenter would likely say...this is ill-formed dynamism. A default get_available is obviously still better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that the best option for this use case is to define get_available
on all Component
s — @jd-lara does that work for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without objection, moving forward with defining
get_available(::Component) = true
in PSY to eliminate this.
export mean, weighted_mean, unweighted_sum | ||
|
||
# IMPORTS | ||
import Base: @kwdef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's already public.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
const PSY = PowerSystems | ||
const IS = InfrastructureSystems | ||
const PSI = PowerSimulations | ||
|
||
# INCLUDES | ||
# Old PowerAnalytics |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Release new PA, keep old PA as is
- Develop a user base on new PA, fix some bugs
- Deprecate old PA (i.e., throw warnings) but keep it so PG can use it
- Revamp PG to only depend on new PA (I would be interested in supervising an intern to do this)
- Remove old PA
but not committed to that, open to discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> #28
src/PowerAnalytics.jl
Outdated
# SUBMODULES | ||
using .Selectors | ||
using .Metrics | ||
|
||
greet() = print("Hello World!") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we delete this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, will do. Surprised that this is the only package of ours that still has it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/builtin_component_selectors.jl
Outdated
|
||
# Parse the strings in generator_mapping.yaml into types and enum items | ||
function parse_fuel_category(category_spec::Dict) | ||
# Use reflection to look up the type corresponding the generator type string (this isn't a security risk, is it?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine. Comment not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
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)), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
decision_problem_names = ("UC", "ED") | ||
my_results_sets = get_decision_problem_results.(Ref(sim_results), decision_problem_names) | ||
|
||
# TODO is there a better way? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compare_values
?
end | ||
end | ||
|
||
# Reimplements Base.Filesystem.cptree since that isn't exported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems crazy that Julia does not provide a way to do this. Since this is just a test, I would have just called the internal function, but am not asking for a change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, this is the basic filesystem function that Julia doesn't provide that I was trying to think of in a recent discussion of ours
@assert all( | ||
in.("ActivePowerVariable__ThermalStandard", list_variable_names.(values(resultses))), | ||
) "Expected all results to contain ActivePowerVariable__ThermalStandard" | ||
comp_results = Dict() # Will be populated later |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this introduce risks of tests colliding with each other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does seem like a kind of bad use of global state, I'll look into changing this
const BASE_DIR = dirname(dirname(pathof(PowerAnalytics))) | ||
const TEST_DIR = joinpath(BASE_DIR, "test") | ||
const TEST_OUTPUTS = joinpath(BASE_DIR, "test", "test_results") | ||
!isdir(TEST_OUTPUTS) && mkdir(TEST_OUTPUTS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use a tmpdir for all outputs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is roughly what PA had before and it's been useful to cache these results so we don't have to regenerate them every time we run the tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's for caching. Can you add a comment?
test/test_builtin_metrics.jl
Outdated
@@ -0,0 +1,121 @@ | |||
# For now we are mostly just testing that all the metrics can be called without error, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still correct? I see plenty of places where you are checking the values returned by functions like aggregate_time. Are there any important functions where you aren't validating the returned values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "infrastructure," including everything in metrics.jl
, output_utils.jl
, etc., gets tested pretty thoroughly; this comment only applies to the built-in metrics in PowerAnalytics.Metrics
— like, we aren't testing that calc_active_power
is actually calculating the active power
See https://github.nrel.gov/gkonars/PowerAnalytics-demos/blob/main/new_pa_pr_demo.ipynb for a full demo. Depends on NREL-Sienna/InfrastructureSystems.jl#342 and NREL-Sienna/PowerSystems.jl#1197.