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

Conversation

GabrielKS
Copy link
Collaborator

@GabrielKS GabrielKS commented Sep 30, 2024

GabrielKS added 30 commits July 17, 2024 13:25
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.
@GabrielKS GabrielKS marked this pull request as ready for review October 1, 2024 04:03
Comment on lines +113 to +120
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

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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

Copy link
Collaborator Author

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

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 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.

Copy link
Contributor

@daniel-thom daniel-thom Oct 8, 2024

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.

Copy link
Collaborator Author

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 Components — @jd-lara does that work for you?

Copy link
Collaborator Author

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
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.

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

# SUBMODULES
using .Selectors
using .Metrics

greet() = print("Hello World!")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we delete this?

Copy link
Collaborator Author

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

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


# 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?)
Copy link
Contributor

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.

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

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.

decision_problem_names = ("UC", "ED")
my_results_sets = get_decision_problem_results.(Ref(sim_results), decision_problem_names)

# TODO is there a better way?
Copy link
Contributor

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

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.

Copy link
Collaborator Author

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

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?

Copy link
Collaborator Author

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

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?

Copy link
Collaborator Author

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

Copy link
Contributor

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?

@@ -0,0 +1,121 @@
# For now we are mostly just testing that all the metrics can be called without error,
Copy link
Contributor

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?

Copy link
Collaborator Author

@GabrielKS GabrielKS Oct 9, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants