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

Matching Python types for return values of pvsystem.calcparams_* #1700

Merged
merged 23 commits into from Jun 23, 2023

Conversation

reepoi
Copy link
Contributor

@reepoi reepoi commented Mar 19, 2023

  • Partially addresses Behavior of calcparams and singlediode function could be improved #1626 (first two checkboxes)
  • I am familiar with the contributing guidelines
  • Tests added
  • [ ] Updates entries in docs/sphinx/source/reference for API changes.
  • Adds description and name entries in the appropriate "what's new" file in 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`).
  • New code is fully documented. Includes numpydoc compliant docstrings, examples, and comments where necessary.
  • Pull request is nearly complete and ready for detailed review.
  • Maintainer: Appropriate GitHub Labels (including remote-data) and Milestone are assigned to the Pull Request and linked Issue.

This PR addresses the observations about pvsystems.calcparams_* in #1626. These are that pd.Series return values can inherit the name of the pd.Series passed to the functions, and that the numeric return values (either scalar, np.ndarray, pd.Series) are not always the same Python type.

A helper function is added to pvlib/tools.py to get the first pandas index from a list of variables.

@reepoi reepoi changed the title Helper functions for consistent Python types of numeric return values. Consistent return types for pvsystem.calcparams_* May 24, 2023
@reepoi reepoi changed the title Consistent return types for pvsystem.calcparams_* Matching Python types for return values of pvsystem.calcparams_* May 24, 2023
@reepoi reepoi marked this pull request as ready for review May 24, 2023 17:36
@cwhanse cwhanse added this to the 0.10.0 milestone May 31, 2023
Copy link
Member

@cwhanse cwhanse left a comment

Choose a reason for hiding this comment

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

I think we need to check the all-float case.

How does test_PVSystem_calcparams_desoto pass? My guess is that calcparams_desoto returning all arrays as expected, and numpy's assert_allclose is doing some work for us, e.g.,

assert_allclose(Rs, 0.1, atol=0.1)

docs/sphinx/source/whatsnew/v0.10.0.rst Outdated Show resolved Hide resolved
Copy link
Member

@wholmgren wholmgren left a comment

Choose a reason for hiding this comment

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

This PR has changed a lot! I like the simplicity of the latest approach.

numeric_args = (effective_irradiance, temp_cell)
out = (IL, I0, Rs, Rsh, nNsVth)

if all(map(np.isscalar, numeric_args)):
Copy link
Member

Choose a reason for hiding this comment

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

np.isscalar docs suggest that we should instead test if np.ndim(x) == 0. The most likely point of failure is example table row 2: np.isscalar(np.array(3.1)). numpy scalar arrays seem to pop up all over the place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the goal of calcparams_* methods' return values having the same Python type, I think we want the behavior of np.isscalar instead of np.ndim(x) == 0. If calcparams_* is given a mix of floats and 0d-arrays (e.g. effective_irradiance is a Python float and temp_cell is a 0d-array), using np.ndim(x) == 0 will cause the calcparams_* methods' return values to be a mix of floats and 0d-arrays. If we use np.isscalar, all return values will become 0d-arrays. This is because np.broadcast_arrays(1.0, np.array(2.0)) equals [np.array(1.0), np.array(2.0)].

Comment on lines 2049 to 2053
index = tools.get_pandas_index(*numeric_args)
if index is not None:
return tuple(pd.Series(a, index=index).rename(None) for a in out)

return np.broadcast_arrays(*out)
Copy link
Member

Choose a reason for hiding this comment

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

two alternatives here.

  1. get_pandas_index raises exception (TypeError?) instead of returning None, this code uses try/except/else
  2. return None, but swap the clauses, so if index is none: return np.broadcast_arrays; else: return tuple(...)

option 1 feels more pythonic, but not sure that it's actually better. If keeping None, then I think option 2 is marginally cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went with your second alternative. get_pandas_index can also be used here in pvsystem.singlediode, but using it there would become awkward if get_pandas_index raised an exception. The calcparams_ functions change their behavior depending on whether get_pandas_index returns None so a try/except pattern makes sense. However, in the pvsystem.singlediode use case I linked, I want to use the None value when creating the pd.DataFrame.

def test_get_pandas_index(args, item_idx):
pd.testing.assert_index_equal(
args[item_idx].index, tools.get_pandas_index(*args)
)
Copy link
Member

Choose a reason for hiding this comment

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

should be tested with all scalars and/or arrays too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added test cases of all scalars and all arrays where get_pandas_index is expected to return None.

Comment on lines +26 to +27
:py:func:`pvlib.pvsystem.calcparams_pvsyst` are all numeric types and have
the same Python type as the `effective_irradiance` and `temp_cell` parameters. (:issue:`1626`, :pull:`1700`)
Copy link
Member

Choose a reason for hiding this comment

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

"the same Python type" - this suggests to me that type(output) == type(input) in every case. This isn't actually tested and I'd be slightly surprised if there are no scalar-ish numpy gotchas.

Copy link
Contributor Author

@reepoi reepoi Jun 8, 2023

Choose a reason for hiding this comment

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

The sentence in the whatsnew is not exactly correct. In this PR, the calcparams_* functions decide the Python type of the return values checking these conditions in order:

  • If arguments passed to calcparams_* are scalars (as determined by np.isscalar), then return values are scalars. Since effective_irradiance and temp_cell are the only numeric-types, and the other arguments are float, this condition only depends on the types of effective_irradiance and temp_cell.
  • If either effective_irradiance or temp_cell is a pd.Series, then return values have type pd.Series.
  • If either effective_irradiance or temp_cell is a nd.ndarray, then return values have type np.ndarray. This includes 0d-arrays.

I added tests to check the return values' Python type here.

pvlib/tests/test_pvsystem.py Outdated Show resolved Hide resolved
pvlib/tests/test_pvsystem.py Show resolved Hide resolved
@kandersolar kandersolar merged commit 0bc5a53 into pvlib:main Jun 23, 2023
28 of 29 checks passed
@kandersolar
Copy link
Member

Thanks @reepoi!

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

Successfully merging this pull request may close these issues.

None yet

4 participants