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

Projected zenith convenience function #1904

Merged

Conversation

echedey-ls
Copy link
Contributor

@echedey-ls echedey-ls commented Nov 6, 2023

  • Closes make projected solar zenith a convenience function #1734
  • 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.

Adds a function that calculates the projected solar zenith.

Pending tasks:

  • Add paper references
  • [ ] Add diagram?
  • Generate test data from singleaxis, change it's implementation to use projected_solar_zenith_angle and change tests accordingly.
Test data generation script for the PSZ implementation port from `singleaxis` to `projected_solar_zenith_angle`

# %%
import pvlib
import pandas as pd

from datetime import timedelta, timezone

axis_tilt_angle = 12.224
axis_azimuth_angle = 187.2

times = pd.date_range(
    # these limits avoid NaN results in singleaxis
    "2024-01-25 08:40",
    "2024-01-25 18:20",
    freq="20min", tz=timezone(timedelta(hours=1))
)
solar_pos = pvlib.solarposition.get_solarposition(times, 40.4165, -3.70256, 700)

tracker_data = pvlib.tracking.singleaxis(
    solar_pos["apparent_zenith"],
    solar_pos["azimuth"],
    axis_tilt=axis_tilt_angle,
    axis_azimuth=axis_azimuth_angle,
    backtrack=False,
)

psz_angle = pvlib.shading.projected_solar_zenith_angle(
    axis_tilt=axis_tilt_angle,
    axis_azimuth=axis_azimuth_angle,
    solar_zenith=solar_pos["apparent_zenith"],
    solar_azimuth=solar_pos["azimuth"],
)

# %%
tracker_thetas = tracker_data["tracker_theta"]

# %%
# optional, just to remove singleaxis nans
psz_angle = psz_angle[tracker_thetas.notna()]
tracker_thetas = tracker_thetas[tracker_thetas.notna()]

# %%
psz_angle == tracker_thetas

# %%
tracker_thetas.to_numpy()

Documentation links

Link to function.

@kandersolar
Copy link
Member

about the diagram, should I replicate it?

Such a diagram might be helpful, but no need in this PR IMHO. I'm not sure where it would live anyway. I don't think we currently have any diagrams in function-level documentation. I guess it could be in a gallery example (or a new User's Guide page) but unless it improves on the references, I'd say let's not bother :)

may be better to write pure NumPy python so anyone can numba-decorate it

Is there a reason this function needs JIT more than the rest of pvlib does? We use sind et al for code readability, so unless this function is special somehow, I think we should stick with that convention. (as an aside, I'd be surprised if numba couldn't handle pvlib.tools.sind, but I don't know for sure either)

About the tests, do you have any data handy for that

Slope-Aware Backtracking has a small validation dataset in a table at the end. It's not enough on its own for this PR, but it's at least a start, and can be easily extended in at least one way (subtract 180 from axis_azimuth and negate the expected PSZAs). I'm not aware of any other existing datasets to use. The tracker_theta output of pvlib.tracking.singleaxis with backtrack=False and max_angle=180 can be used to calculate expected values for additional test cases.

@kandersolar kandersolar added this to the v0.10.3 milestone Nov 6, 2023
@echedey-ls
Copy link
Contributor Author

subtract 180 from axis_azimuth and negate the expected PSZAs

I can't make this work. Could you double check if that's supposed to happen?

    # test by changing array azimuth
    psz = psz_func(
        array_tilt,
        array_azimuth-180,
        timedata["Apparent Elevation"],
        timedata["Solar Azimuth"],
    )
    assert_allclose(psz, -timedata["True-Tracking"], atol=1e-3)

@kandersolar
Copy link
Member

subtract 180 from axis_azimuth and negate the expected PSZAs

I can't make this work

My mistake, I forgot those test cases used a nonzero axis_tilt. Negate the axis tilt as well and I think that little trick should work.

@echedey-ls echedey-ls marked this pull request as ready for review November 9, 2023 21:08
@echedey-ls
Copy link
Contributor Author

I've left out whether the result is valid like pvlib.tracking.singleaxis does.

echedey-ls and others added 2 commits November 9, 2023 22:19
Reported at: pvlib#1725 (comment)

Confirmed via "Slope-Aware Backtracking for Single-Axis Trackers", paragraph after Eq. 1

Co-Authored-By: Mark Mikofski <bwana.marko@yahoo.com>
Copy link
Member

@kandersolar kandersolar left a comment

Choose a reason for hiding this comment

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

A few initial notes

pvlib/shading.py Outdated Show resolved Hide resolved
pvlib/shading.py Outdated Show resolved Hide resolved
pvlib/tests/test_shading.py Outdated Show resolved Hide resolved
echedey-ls and others added 3 commits November 9, 2023 22:50
Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com>
Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com>
Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com>
echedey-ls and others added 5 commits February 9, 2024 12:18
Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com>
@echedey-ls
Copy link
Contributor Author

echedey-ls commented Feb 9, 2024

@kandersolar , GitHub does not allow me to reply to your last review message, so here it goes:

we might as well choose axis_ for consistency with the reference

I agree it's a good point. It's difficult to understand exactly the vectors and the projection plane without reading it. In fact, such a recommendation would be a nice addition to the notes section.

we're going to have to include some kind of explanation/clarification

I also agree with that. But regarding the suggested explanations, I think it would be better to describe the use cases with real examples in general. If I didn't already know about the internals and purposes, I wouldn't understand why the math is also applicable to fixed-tilt arrays since the paper only talks about SATs and the optimal rotation angle. I would ask myself what would be the rotation angle in a fixed-tilt array. My suggestion, feel free to edit as much as yall want:

    Notes
    -----
    This projection has a variety of applications in PV. For example:

    - Given a single-axis tracker, a plane whose normal vector's
      ``axis_azimuth`` equals the torque tube direction and its ``axis_tilt``
      is the zero rotation angle, this function yields the optimal angle to
      point to in order to maximize direct irradiance capture (this is called
      :ref:`true-tracking
      <sphx_glr_examples_plot_single_axis_tracking.py:True-tracking>`).

    - Given rows of PV arrays (or other sunlight obstacles), a projection plane
      that contains the module's normal vector (i.e., its ``axis_azimuth`` is
      the module's normal vector's azimuth shifted ``-90º``) and its
      ``axis_rotation`` is zero, the result is the smallest angle between the
      shadow plane and a horizontal plane. 
      See :py:func:`pvlib.shading.tracker_shaded_fraction`.

I wouldn't change the current arguments definitions (almost due to the 1:1 relation with the paper). Maybe move your suggested clarifications to the notes section, although I'm not sure of how to integrate them.

I'd like to clear up that I don't have a problem applying your suggestions straight forward if you want to, specially due to my lack of experience in PV and the language barrier. And that I hope this comment helps improving the quality of the PR and doesn't add noise to the conversation.

@echedey-ls
Copy link
Contributor Author

I'd like to move forward this PR another bit, specially the docstring. Would you like me to push @kandersolar suggestions, or give another suggestions? Feel free to request anything from my side.

@kandersolar
Copy link
Member

Thanks @echedey-ls for keeping this going :)

Describing some real use cases is a great idea, and I like your proposed text. What do you think about a combination of Notes and Examples like this?

    Notes
    -----
    This projection has a variety of applications in PV. For example:

    - Projecting the sun's position onto the plane perpendicular to
      the axis of a single-axis tracker (i.e. the plane
      whose normal vector coincides with the tracker torque tube)
      yields the tracker rotation angle that maximizes direct irradiance
      capture.  This tracking strategy is called
      :ref:`true-tracking
      <sphx_glr_examples_plot_single_axis_tracking.py:True-tracking>`.

    - Self-shading in large PV arrays is often modeled by assuming
      a simplified 2-D array geometry where the sun's position is
      projected onto the plane perpendicular to the PV rows.
      The projected zenith angle is then used for calculations
      regarding row-to-row shading.

    Examples
    --------

    Calculate the ideal true-tracking angle for a horizontal north-south
    single-axis tracker:

    >>> rotation = projected_solar_zenith_angle(solar_zenith, solar_azimuth,
    >>>                                         axis_tilt=0, axis_azimuth=180)

    Calculate the projected zenith angle in a south-facing fixed tilt array
    (note: the ``axis_azimuth`` of a fixed-tilt row points along the length
    of the row):

    >>> psza = projected_solar_zenith_angle(solar_zenith, solar_azimuth,
    >>>                                     axis_tilt=0, axis_azimuth=90)

echedey-ls and others added 2 commits February 27, 2024 23:49
Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com>
@echedey-ls
Copy link
Contributor Author

I like that pretty much, thanks for the improvements. I will add the still inexistent pvlib.shading.tracker_shaded_fraction to the see also section and the notes section in its PR (#1962). Just in case anyone heard of the PSZA for the calculation of shadows so that s/he finds the upcoming feature.

You can check the docs out here.

@echedey-ls
Copy link
Contributor Author

I had some trouble finding the example's link. I can add a section link too but reading it all seems a better choice.

From my side, this looks like it can be merged!

Copy link
Member

@kandersolar kandersolar left a comment

Choose a reason for hiding this comment

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

One trivial suggestion so that the image doesn't take up so much webpage space, but otherwise LGTM!

pvlib/shading.py Outdated Show resolved Hide resolved
echedey-ls and others added 2 commits March 4, 2024 22:54
@kandersolar kandersolar merged commit e5fa03c into pvlib:main Mar 8, 2024
28 of 29 checks passed
@kandersolar
Copy link
Member

Thanks for this contribution and the patience @echedey-ls :)

@echedey-ls echedey-ls deleted the projected-solar-zenith-angle-issue1734 branch March 8, 2024 15:41
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.

make projected solar zenith a convenience function
5 participants