-
Notifications
You must be signed in to change notification settings - Fork 1.1k
add marion's adjustment to pvwatts_dc #2569
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
base: main
Are you sure you want to change the base?
Conversation
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 like this better than a separate function.
pvlib/pvsystem.py
Outdated
For example, a 500 W module that produces 95 W at 200 W/m^2 (a 5% relative | ||
reduction in efficiency) would have a value of `k` = 0.01. | ||
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.
Please include Equation from above with k
, I'd place it 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.
See my latest change. Is that what you had in mind?
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, that comment was not clear. I was thinking we should copy the equation for P_DC
here, and show how k
modifies the power. But the equation for the adjustment is not simple (the piecewise part), so I retract that thought. If someone wants to know how the adjustment works, we've provided code and the reference.
Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Cliff Hansen <[email protected]>
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.
Nice work @williamhobbs. Just a few simple comments/suggestions on the docs from me. Only had a quick look at the python implementation, LGTM at a glance but I could have a deeper look later if required
doi sphinx Co-authored-by: RDaxini <[email protected]>
Co-authored-by: RDaxini <[email protected]>
Co-authored-by: RDaxini <[email protected]>
pvlib/pvsystem.py
Outdated
# apply Marion's correction if k is anything but zero | ||
if k is not None: | ||
err_1 = (k * (1 - (1 - effective_irradiance / 200)**4) / | ||
(effective_irradiance / 1000)) | ||
err_2 = (k * (1000 - effective_irradiance) / (1000 - 200)) | ||
|
||
pdc_marion = np.where(effective_irradiance <= 200, | ||
pdc * (1 - err_1), | ||
pdc * (1 - err_2)) | ||
|
||
# "cap" Marion's correction at 1000 W/m^2 | ||
if cap_adjustment is True: | ||
pdc_marion = np.where(effective_irradiance >= 1000, | ||
pdc, | ||
pdc_marion) | ||
|
||
pdc = pdc_marion | ||
|
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.
@williamhobbs have you started / do you intend to write tests? The codecov check fails since this section is not covered by tests. Happy to help with that if you need.
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.
Haven't started, but figured I would need to. Do you have suggestions on tests to add? I have pretty limited experience with the pvlib testing structure/best practices.
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'd hand-calculate half a dozen points, using k
. Test for correct output with input of three types: float, array, Series. Then test with cap_adjustment=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.
Be creative, test whatever comes to mind! Generally speaking, test a range of intended and extreme conditions.
Intended:
For the if
statements, check whether the indented block is executed if the condition is met (for example, if k
is not None, use some example values to check whether the correction is applied).
Whether the block is executed correctly should also be checked--- if k
is not None, then check example irradiance values >200 and <=200 (is the intended behaviour executed correctly in these cases?)
Extreme:
What if the user enters non-physical values, how should these be handled and are they handled in this way? e.g. negative or NaN irradiance values
Someone else might be able to offer a clearer/more succinct explanation 😅
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'd hand-calculate half a dozen points, using
k
. Test for correct output with input of three types: float, array, Series. Then test withcap_adjustment=True
.
Good point, check whether different data types are handled appropriately too
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.
@williamhobbs you do not have to test with k=None
, that is covered by existing tests. Any new tests would be better in their own function, e.g., test_pvwatts_dc_with_k
.
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.
@williamhobbs you do not have to test with
k=None
, that is covered by existing tests.
Correct, my bad. I was trying to make a general point but overlooked that in the example.
See the codecov report
Any new tests would be better in their own function, e.g.,
test_pvwatts_dc_with_k
.
Add to test_pvsystem.py (examples also visible there)
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.
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.
@williamhobbs these look good to me! I see tests covering different dtypes, various combinations of k
and cap_adjustment
, etc. Get a second opinion from @cwhanse in case I am missing something.
One other case to add would be the scalar case when cap_adjustment=True
and effective_irradiance>=1000
That should cover the else
branch highlighted as missing in the codecov report. Something simple like:
def test_pvwatts_dc_cap_adjustment_scalar_above_1000():
irrad = 1200
temp_cell = 25
k = 0.01
cap_adjustment = True
out = pvsystem.pvwatts_dc(irrad, temp_cell, 100, -0.003, 25, k, cap_adjustment)
expected = 120.0
assert_allclose(out, expected)
So in this case, the unadjusted pdc
value is returned. Right?
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.
Thanks @williamhobbs good stuff! Happy to see this added to pvlib, less sure about the API right now.
We've been talking about changing this function name to pvwatts_dc_v5
in #1350. I think these new parameters will look quite out of place if we do go through with that. Which makes me think a separate function might be preferable.
pvlib/pvsystem.py
Outdated
This adjustment increases relative efficiency for irradiance above 1000 | ||
Wm⁻², which may not be desired. An optional input, `capped_adjustment`, | ||
modifies the adjustment from [2]_ to only apply below 1000 Wm⁻². |
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.
any concerns about deviating from a reference here? I'm ok with it but imagine it could be a point of contention. perhaps some more documentation clarity would help
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 also wondered about deviating from the reference. It just gives the option to apply the Marion 2008 correction up to 1000 Wm-2, and stay with standard pvwatts above that, so seems like a minor deviation.
And I can definitely add more documentation. I tend to get to long-winded and was trying to fight that. I’m open to suggestions, but will also work on some additions.
pvlib/pvsystem.py
Outdated
(effective_irradiance / 1000)) | ||
err_2 = (k * (1000 - effective_irradiance) / (1000 - 200)) | ||
|
||
pdc_marion = np.where(effective_irradiance <= 200, |
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.
np.where will break the paradigm of "return the same object type that was input" since it always returns an array. Options:
- keep np.where, cast output to match input
- switch to slicing, assume array input
- switch to slicing, promote scalars to arrays for compatibility
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 what you mean. I tried a few things, but can’t seem to figure out how to get your proposed solutions to work. Any pointers or examples?
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.
@wholmgren, I think this addresses your comment about returning the same object type
9f756bf (Edit: not that one - see the latest commit...)
(1 + gamma_pdc * (temp_cell - temp_ref))) | ||
|
||
# apply Marion's correction if k is anything but zero | ||
if k is not None: |
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.
needing to use the is not None
paradigm is a small reason to prefer a separate function
I'm not dismissing your point. But to reply, PVWatts v8 (or whatever it is called now) is a moving target that does not have much documentation. I think, if we do anything with the newer PVWatts in pvlib (once it stablizes and is documented somehow), we could name that new function "pvwatts_v8" and leave the existing functions names without the version suffix. |
Co-authored-by: Will Holmgren <[email protected]>
For positive `k` values, and `k` is typically positive, this adjustment | ||
increases relative efficiency when irradiance is above 1000 Wm⁻². This may | ||
not be desired, as modules with nonlinear irradiance response often have | ||
peak efficiency near 1000 Wm⁻², and it is either flat or declining at | ||
higher irradiance. An optional parameter, `cap_adjustment`, can address | ||
this by modifying the adjustment from [2]_ to only apply below 1000 Wm⁻². |
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.
@wholmgren, does this help with clarifying the deviation?
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.
Just a few formatting suggestions + a suggestion for the tests to complement my other comment #2569 (comment)
25, k, cap_adjustment)) | ||
assert_allclose(out, expected) | ||
|
||
|
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.
See #2569 (comment); I think this test should help. The rest look good already, nice job!
def test_pvwatts_dc_cap_adjustment_scalar_above_1000(): | |
irrad = 1200 | |
temp_cell = 25 | |
k = 0.01 | |
cap_adjustment = True | |
out = pvsystem.pvwatts_dc(irrad, temp_cell, 100, -0.003, 25, k, cap_adjustment) | |
expected = 120.0 | |
assert_allclose(out, expected) |
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.
See my changes to test_pvwatts_dc_with_k_and_cap_adjustment()
. It now includes irradiance > 1000 and cap_adjustment=True
. I think I made those changes before seeing your comments here.
Does that cover it?
Co-authored-by: RDaxini <[email protected]>
Co-authored-by: RDaxini <[email protected]>
Co-authored-by: RDaxini <[email protected]>
A general comment: after other Will H. pointed out the issue with returning the same object type that was input, my code has gotten a lot harder to follow, with lots of nested ifs, slicing, etc. If anyone has suggestions for cleaning it up, I'm very interested. A few specific thoughts/questions:
Example of reorganizing the ifs. Is this better? (edit: I went ahead and made this change. Looks much better to me.) (2nd edit: implemented new changes suggested by Kevin, not shown in this comment.) click to expand and see code # apply Marion's correction if k is anything but zero
if k is not None:
err_1 = k * (1 - (1 - effective_irradiance / 200)**4)
err_2 = k * (1000 - effective_irradiance) / (1000 - 200)
# if input is Series or array
if hasattr(effective_irradiance, '__len__'):
# precalculate pdc before error adjustments
pdc_marion = (effective_irradiance * 0.001 * pdc0 *
(1 + gamma_pdc * (temp_cell - temp_ref)))
# apply error adjustments
pdc_marion[effective_irradiance <= 200] = (
pdc[effective_irradiance <= 200] -
(pdc0 * err_1[effective_irradiance <= 200]))
pdc_marion[effective_irradiance > 200] = (
pdc[effective_irradiance > 200] -
(pdc0 * err_2[effective_irradiance > 200]))
# "cap" Marion's correction at 1000 W/m^2
if cap_adjustment:
pdc_marion[effective_irradiance >= 1000] = (
pdc[effective_irradiance >= 1000])
# set negative power to zero
pdc_marion[pdc_marion < 0] = 0
# else (input is scalar)
else:
# apply error adjustments
if effective_irradiance <= 200:
pdc_marion = pdc - (pdc0 * err_1)
elif effective_irradiance > 200:
pdc_marion = pdc - (pdc0 * err_2)
# "cap" Marion's correction at 1000 W/m^2
if cap_adjustment:
if effective_irradiance >= 1000:
pdc_marion = pdc
# set negative power to zero
if pdc_marion < 0:
pdc_marion = 0
pdc = pdc_marion |
Having different computation paths for different input types results in quite a lot of code, and duplicating the math also introduces the risk of accidentally performing different computations for different input types. What we do elsewhere in pvlib is to have only one computation path that operates on numpy arrays (the lingua franca) and then fix up the types at the very end. Something like this (please check that the computations are correct; this is just for illustration): def pvwatts_dc(...):
pdc = (effective_irradiance * 0.001 * pdc0 *
(1 + gamma_pdc * (temp_cell - temp_ref)))
# apply Marion's correction if k is anything but zero
if k is not None:
# preserve input types
index = pdc.index if isinstance(pdc, pd.Series) else None
is_scalar = np.isscalar(pdc)
# calculate error adjustments
err_1 = k * (1 - (1 - effective_irradiance / 200)**4)
err_2 = k * (1000 - effective_irradiance) / (1000 - 200)
err = np.where(effective_irradiance <= 200, err_1, err_2)
if cap_adjustment:
err = np.where(effective_irradiance >= 1000, 0, err)
pdc = pdc - pdc0 * err
if index is not None:
pdc = pd.Series(pdc, index=index)
elif is_scalar:
pdc = float(pdc)
return pdc |
Thanks, @kandersolar! This is the exact help I needed. |
[ ] Updates entries indocs/sphinx/source/reference
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`
).remote-data
) and Milestone are assigned to the Pull Request and linked Issue.Still some work to do . Replaces #2568.