-
Notifications
You must be signed in to change notification settings - Fork 1
Shadowband correction factor function and test #30
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
Open
Emil997e
wants to merge
8
commits into
AssessingSolar:main
Choose a base branch
from
Emil997e:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
af26a66
Create shadowband.py
Emil997e c9f860c
Add function to init and docs
Emil997e fa53793
Add tests
Emil997e f70f348
Update test_instrument.py
Emil997e d6d4395
Wrote tests for shadowband correction factor
Emil997e e8d5298
Finished test of shadowband correction factor (test_instrument.py)
Emil997e 4f4ff5f
Finalized tests of shadowband corrrection factor
Emil997e a690bf4
Merge branch 'main' into main
AdamRJensen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,5 +11,6 @@ | |
| quality, | ||
| horizon, | ||
| example, | ||
| instrument, | ||
| iotools, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from solarpy.instrument.shadowband import shadowband_correction_factor # noqa: F401 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| """ | ||
| shadowband_correction.py | ||
| ------------------------ | ||
| Shadowband diffuse-irradiance correction factor for the Kipp & Zonen CM 121 | ||
| shadow-ring pyranometer accessory. | ||
|
|
||
| Reference | ||
| --------- | ||
| Kipp & Zonen *CM 121 B/C Shadow Ring Instruction Manual*, Appendix §6.1. | ||
| https://www.kippzonen.com/Download/46/CM121-B-C-Shadow-Ring-Manual | ||
| """ | ||
|
|
||
| import numpy as np | ||
| import pvlib as pv | ||
| import pandas as pd | ||
| import datetime | ||
|
|
||
|
|
||
| def shadowband_correction_factor( | ||
| input_date, | ||
| latitude: float, | ||
| V: float = 0.185, | ||
| ): | ||
| """Calculate the daily shadowband correction factor for a Kipp & Zonen CM 121. | ||
|
|
||
| A shadow ring blocks a strip of the sky dome to prevent direct beam | ||
| radiation from reaching the pyranometer, so that only diffuse radiation | ||
| is measured. Because the ring also blocks some of the diffuse sky, a | ||
| correction factor C > 1 must be applied to recover the true diffuse | ||
| irradiance:: | ||
|
|
||
| G_d_true = C * G_d_measured | ||
|
|
||
| The factor depends on the ring geometry (angular half-width *V*), the | ||
| site latitude, and the solar declination on the measurement day. It is | ||
| derived analytically by integrating the fraction of the isotropic sky | ||
| hemisphere obscured by the ring over the daily arc of the sun. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| input_date : scalar or array-like | ||
| Date(s) for which to compute the factor. Accepts anything that | ||
| ``pandas.to_datetime`` understands: a ``pd.Timestamp``, a | ||
| ``datetime.date`` / ``datetime.datetime``, an ISO-8601 string, a | ||
| list/array of any of the above, or a ``pd.DatetimeIndex``. | ||
| Sub-daily time information is ignored; only the calendar date matters. | ||
| latitude : float | ||
| Observer latitude in decimal degrees. Positive = North, negative = South. | ||
| Valid range: −90 to +90. | ||
| V : float, optional | ||
| Angular width of the shadow band in radians. This is arc subtended by | ||
| the ring as seen from the sensor. Default is 0.185 rad (~10.6°), | ||
| which matches the standard Kipp & Zonen CM 121 ring geometry. | ||
|
|
||
| Returns | ||
| ------- | ||
| float or array-like | ||
| Dimensionless correction factor C ≥ 1. | ||
|
|
||
| Notes | ||
| ----- | ||
| The correction factor is computed as: | ||
|
|
||
| .. math:: | ||
|
|
||
| S = \\frac{2V \\cos\\delta}{\\pi} | ||
| \\bigl(U_0 \\sin\\phi\\sin\\delta | ||
| + \\sin U_0 \\cos\\phi\\cos\\delta\\bigr) | ||
|
|
||
| C = \\frac{1}{1 - S} | ||
|
|
||
| where | ||
|
|
||
| * :math:`\\delta` – solar declination (Spencer 1971 approximation) | ||
| * :math:`\\phi` – site latitude | ||
| * :math:`U_0` – sunrise/sunset hour angle, | ||
| :math:`\\arccos(-\\tan\\phi\\,\\tan\\delta)`, clamped to [−1, 1] to | ||
| handle polar day/night conditions | ||
| """ | ||
| # ------------------------------------------------------------------ | ||
| # 1. Normalise input to pandas datetime so scalar and array paths | ||
| # share a single code route below. | ||
| # ------------------------------------------------------------------ | ||
| dates = pd.to_datetime(input_date) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 2. Solar declination δ for each day of year. | ||
| # Spencer (1971) approximation, returned in radians by pvlib. | ||
| # ------------------------------------------------------------------ | ||
| day_of_year = dates.dayofyear | ||
| delta = pv.solarposition.declination_spencer71(day_of_year) # radians | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 3. Site latitude φ in radians. | ||
| # ------------------------------------------------------------------ | ||
| phi = np.deg2rad(latitude) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 4. Sunrise/sunset hour angle U₀. | ||
| # cos U₀ = −tan φ · tan δ. | ||
| # Clamped to [−1, 1] so arccos is valid at the poles (midnight sun | ||
| # / polar night), where the tangent product can exceed ±1. | ||
| # ------------------------------------------------------------------ | ||
| cos_U0 = np.clip(-np.tan(phi) * np.tan(delta), -1.0, 1.0) | ||
| U0 = np.arccos(cos_U0) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 5. Fraction S of the isotropic sky dome obscured by the ring. | ||
| # Derived by integrating the ring's shadow across the daily solar arc | ||
| # (Kipp & Zonen manual, Appendix §6.1). | ||
| # ------------------------------------------------------------------ | ||
| S = (2 * V * np.cos(delta) / np.pi) * ( | ||
| U0 * np.sin(phi) * np.sin(delta) | ||
| + np.sin(U0) * np.cos(phi) * np.cos(delta) | ||
| ) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 6. Correction factor C = 1 / (1 − S). | ||
| # S is always < 1 for physically reasonable inputs, so C > 1. | ||
| # ------------------------------------------------------------------ | ||
| C = 1.0 / (1.0 - S) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # 7. Return a plain float for scalar input, Series for array input. | ||
| # ------------------------------------------------------------------ | ||
| if np.isscalar(input_date) or isinstance(input_date, (pd.Timestamp, datetime.date, datetime.datetime)): | ||
| return float(C) | ||
| return pd.Series(C, index=dates) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import solarpy | ||
| import pandas as pd | ||
| import datetime | ||
| import pytest | ||
|
|
||
|
|
||
| def test_shadowband_correction(): | ||
| # Northern hemisphere, spring (near-zero declination) | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-03-17", latitude=55.29007) == 1.0668908588370676 | ||
| # Northern hemisphere, spring - pd.Timestamp | ||
| assert solarpy.instrument.shadowband_correction_factor(pd.Timestamp("2026-03-17"), latitude=55.29007) == 1.0668908588370676 | ||
| # pd.Timestamp with time component - sub-daily info must be ignored | ||
| assert solarpy.instrument.shadowband_correction_factor(pd.Timestamp("2026-03-17 14:30:01"), latitude=55.29007) == pytest.approx(1.0668908588370676) | ||
| # datetime.date scalar | ||
| assert solarpy.instrument.shadowband_correction_factor(datetime.date(2026, 3, 17), latitude=55.29007) == pytest.approx(1.0668908588370676) | ||
| # datetime.datetime scalar | ||
| assert solarpy.instrument.shadowband_correction_factor(datetime.datetime(2026, 3, 17, 14, 30), latitude=55.29007) == pytest.approx(1.0668908588370676) | ||
|
|
||
| # Northern hemisphere, summer | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-06-21", latitude=55.29007) == pytest.approx(1.1408347415382885) | ||
| # Northern hemisphere, autumn | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-10-21", latitude=55.29007) == pytest.approx(1.0418060380205798) | ||
| # Northern hemisphere, winter | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-12-21", latitude=55.29007) == pytest.approx(1.0126112472695903) | ||
|
|
||
| # Southern hemisphere | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-10-21", latitude=-33.87) == pytest.approx(1.1282175055807726) | ||
|
|
||
| def test_shadowband_correction_series(): | ||
| # Single value series | ||
| index = pd.to_datetime(["2026-03-17"]) | ||
| pd.testing.assert_series_equal( | ||
| solarpy.instrument.shadowband_correction_factor(index, latitude=55.29007), | ||
| pd.Series(1.0668908588370676, index=index)) | ||
|
|
||
| # Multiple value series | ||
| index = pd.DatetimeIndex(["2026-03-17", "2026-10-21"]) | ||
| pd.testing.assert_series_equal( | ||
| solarpy.instrument.shadowband_correction_factor(index, latitude=55.29007), | ||
| pd.Series([1.0668908588370676, 1.0418060380205798], index=index)) | ||
|
|
||
| def test_shadowband_correction_always_above_one(): | ||
| # Test if shadowband correction factor C > 1 | ||
| dates = pd.DatetimeIndex(["2026-03-20", "2026-06-21", "2026-09-22", "2026-12-21"]) | ||
| result = solarpy.instrument.shadowband_correction_factor(dates, latitude=55.29007) | ||
| assert (result > 1.0).all() | ||
|
|
||
|
|
||
| def test_shadowband_correction_v_zero(): | ||
| # With no ring there is nothing to correct for | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-06-21", latitude=45.0, V=0.0) == pytest.approx(1.0) | ||
|
|
||
| def test_shadowband_correction_polar(): | ||
| # Test that the clip works and it follows the table provided by Kipp & Zonen | ||
| # At the pole in winter the sun never rises — ring blocks nothing, C = 1 | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-01-01", latitude=90.0) == pytest.approx(1.0) | ||
| # At the pole in summer the sun skims the horizon — C rises above 1 | ||
| assert solarpy.instrument.shadowband_correction_factor("2026-06-21", latitude=90.0) > 1.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.