-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Support list of weather input for ModelChain with a single-array PVSystem #1157
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
Conversation
Covers passing a length-one list of weather DataFrames to each of - `ModelChain.run_model()` - `ModelChain.run_model_from_poa()` - `ModelChain.run_model_from_effective_irradiance()` When the PVSystem being modeled has only one Array. In this case the output stored in each field of `ModelChain.results` should be a length-one tuple.
Allow p_dc and v_dc to be length-1 tuples even when the system has only one Array. This helps keep the API consistent so that if the dc power methods are called with `unwrap=False` their output can be passed directly to `PVSystem.get_ac()`.
This method is responsible for ensuring that per-array results match the type of ModelChain.weather. This is an issue when ModelChain.system has only one Array, but ModelChain.weather is a tuple of length 1. In this case we want the results attributes to also be length-1 tuples for the sake of consistency and to ensure that calculations down-stream can proceed without needing to worry about mixed types (e.g. total_irrad is a Series, but weather is a tuple).
Accidentally under-indented a whole test: test_run_model_from_poa_singleton_weather_single_array()
At the risk of making things more complicated... what do you think about using a setter instead of explicitly adding the private function call to many of the assignment lines? I'm undecided. |
if multiple_arrays: | ||
p_dc = self._validate_per_array(p_dc) |
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.
why move the _validate_per_array
outside if multiple_arrays
? IIRC, that doesn't work when p_dc
and v_dc
are floats.
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.
If p_dc
and v_dc
are floats they will get wrapped up as (p_dc,)
and (v_dc,)
. Doing this regardless of the number of arrays lets us always subscript when passing to the single array inverter models. If we didn't wrap them up we would need extra logic to handle unwrapping if the input was initially a singleton tuple.
The goal is to be able to do both of these and have them act the same:
system = PVSystem(arrays=[Array()], ...)
system.get_ac(p_dc=50, model='pvwatts')
system.get_ac(p_dc=(50,), model='pvwatts')
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.
As written, get_ac
references the inverter
functions directly, not through a PVSystem
method for each inverter function. The inverter
functions don't know what to do with a tuple of values; they assume numeric
.
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 tuples are always subscripted before being passed to the inverter functions when multiple_arrays
is False
. For example:
if model == 'sandia':
p_dc = self._validate_per_array(p_dc)
v_dc = self._validate_per_array(v_dc)
if multiple_arrays:
return inverter.sandia_multi(
v_dc, p_dc, self.inverter_parameters)
return inverter.sandia(v_dc[0], p_dc[0], self.inverter_parameters)
When multiple_arrays
is True the tuples are passed directly to inverter.sandia_multi
, but p_dc[0]
and v_dc[0]
is used for inverter.sandia
.
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 think we can now move the _validate_per_array
calls above the if model ==...
block and save a few lines:
p_dc = self._validate_per_array(p_dc)
if v_dc is not None:
v_dc = self._validate_per_array(v_dc)
The problem with a setter is that it is setting fields on |
Related note: should we infer the type of a |
It needs to be based on weather to handle the |
|
|
Oops... forgot to address a couple |
Here is an interesting use case. If I want to set constant spectral loss as in this test, but I am calling def constant_spectral_loss(mc):
mc.results.spectral_modifier = 0.9
@pytest.mark.parametrize('spectral_model', [
'sapm', 'first_solar', 'no_loss', constant_spectral_loss
])
def test_spectral_models(sapm_dc_snl_ac_system, location, spectral_model,
weather):
# add pw to weather dataframe
weather['precipitable_water'] = [0.3, 0.5]
mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm',
aoi_model='no_loss', spectral_model=spectral_model)
spectral_modifier = mc.run_model(weather).results.spectral_modifier
assert isinstance(spectral_modifier, (pd.Series, float, int)) Since execution of Things might get weird when there are multiple arrays though; since there currently is not mechanism to turn the constant in to a tuple as we do for |
Add tests for spectral_model != 'no_loss' and aoi_model != 'no_loss' when calling ModelChain.run_model([weather]) on a system with 1 Array. Handles the type of ModelChain.weather correctly in ModelChain.first_solar_spectral_loss() by using _tuple_from_dfs() to get 'precipitable_water'. Still needs support for constant spectral loss
Extends `PVSystem.first_solar_spectral_loss()` to treat `pw` as a per-Array or system wide parameter.
A bit cleaner than using a generator expression.
Add a configuration step in ModelChain._assign_weather() that sets a flag on the ModelChain.results to indicate whether non-tuples assigned to per-array fields should be wrapped in length-1 tuples. This is accomplished by a custom __setattr__ methon on ModelChainResult. The setter checks whether the attribute is a per-array attribute and coerces it if the _singleton_tuples flag is set.
Covers the case where the aoi_model is 'no_loss'.
@wholmgren I have refactored to use a setter-based approach. Take a look and let me know what you think. I believe it is an improvement. |
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.
@wfvining I like the approach. A couple of comments below - I've not looked carefully at the pvsystem changes or tests.
return value | ||
|
||
def __setattr__(self, key, value): | ||
if key in ModelChainResult._per_array_fields: |
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.
could we use typing.get_type_hints
and delete the private set?
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.
That would be nice, but I can't seem to figure out how to compare the concrete types returned from get_type_hints
with the generic Optional[PerArray[_T]]
type.
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.
After further research, my idea seems like a misuse of the typing module at this point in time, so I'm good with your solution.
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 a little bummed... I was excited to actually put the types to use.
@@ -1271,6 +1291,17 @@ def _verify(data, index=None): | |||
for (i, array_data) in enumerate(data): | |||
_verify(array_data, i) | |||
|
|||
def _configure_results(self): |
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.
an alternative to this method might be to instantiate ModelChain
with self.results = None
(or not at all), then put this line at the start of each run_model
workflow:
self.results = ModelChainResults(singleton_tuples=(self.system.num_arrays == 1 and isinstance(self.weather, tuple))
I guess maybe you'd still want to extract that logic into a method like this one. Then the difference comes down to setting the right value vs instantiating the object with the right value. 🤷
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 would basically be a transformation from _configure_results
to _prepare_results
. I kind of like that better, but I don't think it makes a big difference. It would also take a little bit of work, since some of the tests (and ModelChain
methods) make the (reasonable) assumption that all the results fields exist before one of the run_model
workflows is triggered (e.g. test__assign_total_irrad
and ModelChain.complete_irradiance
).
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.
Should _configure_results()
(or some ModelChainResult
method) iterate over per-array fields to and wrap them in tuple if _singleton_tuples
is changed from False to True?
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 viewing ModelChainResult as a single use object so I don't think we need to watch for that kind of state change.
if multiple_arrays: | ||
p_dc = self._validate_per_array(p_dc) |
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 think we can now move the _validate_per_array
calls above the if model ==...
block and save a few lines:
p_dc = self._validate_per_array(p_dc)
if v_dc is not None:
v_dc = self._validate_per_array(v_dc)
arrays=[pvsystem.Array()], | ||
inverter_parameters=inverter_parameters[model] | ||
) | ||
ac = system.get_ac(p_dc=(pdcs[model],), v_dc=(vdcs[model],), model=model) |
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.
Do we actually want to support v_dc=(None,)
for model='pvwatts'
? If we do support that, then my suggestion to move the _validate_per_array
calls won't work.
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 think I started with these calls above the control flow, but ran in to this. I don't really like them inside the control flow, but I don't see a better way.
Re-order fields and comments. Add docstring to _result_type() method.
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.
Updates entries todocs/sphinx/source/api.rst
for API changes.docs/sphinx/source/whatsnew
for all changes. Includes link to the GitHub Issue with:issue:`num`
or this Pull Request with:pull:`num`
. Includes contributor name and/or GitHub username (link with:ghuser:`user`
).Adds a layer of indirection for assigning attributes of
ModelChain.results
that ensures the types of the results are consistent with the type of the input (i.e.ModelChain.weather
).