Skip to content

Commit 0e6fea6

Browse files
authored
Mount gallery examples (#1266)
* create discontinuous tracking example * create dual-axis example * create mixed-orientation example * create seasonal tilt example * stickler * whatsnew * bsrn -> clear-sky sim * stickler, d'oh
1 parent b73d4a2 commit 0e6fea6

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
Discontinuous Tracking
3+
======================
4+
5+
Example of a custom Mount class.
6+
"""
7+
8+
# %%
9+
# Many real-world tracking arrays adjust their position in discrete steps
10+
# rather than through continuous movement. This example shows how to model
11+
# this discontinuous tracking by implementing a custom Mount class.
12+
13+
from pvlib import tracking, pvsystem, location, modelchain
14+
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
15+
import matplotlib.pyplot as plt
16+
import pandas as pd
17+
18+
19+
# %%
20+
# We'll define our custom Mount by extending
21+
# :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount` for convenience.
22+
# Another approach would be to extend ``AbstractMount`` directly; see
23+
# the source code of :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount`
24+
# and :py:class:`~pvlib.pvsystem.FixedMount` for how that is done.
25+
26+
27+
class DiscontinuousTrackerMount(pvsystem.SingleAxisTrackerMount):
28+
# inherit from SingleAxisTrackerMount so that we get the
29+
# constructor and tracking attributes (axis_tilt etc) automatically
30+
31+
def get_orientation(self, solar_zenith, solar_azimuth):
32+
# Different trackers update at different rates; in this example we'll
33+
# assume a relatively slow update interval of 15 minutes to make the
34+
# effect more visually apparent.
35+
zenith_subset = solar_zenith.resample('15min').first()
36+
azimuth_subset = solar_azimuth.resample('15min').first()
37+
38+
tracking_data_15min = tracking.singleaxis(
39+
zenith_subset, azimuth_subset,
40+
self.axis_tilt, self.axis_azimuth,
41+
self.max_angle, self.backtrack,
42+
self.gcr, self.cross_axis_tilt
43+
)
44+
# propagate the 15-minute positions to 1-minute stair-stepped values:
45+
tracking_data_1min = tracking_data_15min.reindex(solar_zenith.index,
46+
method='ffill')
47+
return tracking_data_1min
48+
49+
50+
# %%
51+
# Let's take a look at the tracker rotation curve it produces:
52+
53+
times = pd.date_range('2019-06-01', '2019-06-02', freq='1min', tz='US/Eastern')
54+
loc = location.Location(40, -80)
55+
solpos = loc.get_solarposition(times)
56+
mount = DiscontinuousTrackerMount(axis_azimuth=180, gcr=0.4)
57+
tracker_data = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth)
58+
tracker_data['tracker_theta'].plot()
59+
plt.ylabel('Tracker Rotation [degree]')
60+
plt.show()
61+
62+
# %%
63+
# With our custom tracking logic defined, we can create the corresponding
64+
# Array and PVSystem, and then run a ModelChain as usual:
65+
66+
module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05}
67+
temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer']
68+
array = pvsystem.Array(mount=mount, module_parameters=module_parameters,
69+
temperature_model_parameters=temp_params)
70+
system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1})
71+
mc = modelchain.ModelChain(system, loc, spectral_model='no_loss')
72+
73+
# simple simulated weather, just to show the effect of discrete tracking
74+
weather = loc.get_clearsky(times)
75+
weather['temp_air'] = 25
76+
weather['wind_speed'] = 1
77+
mc.run_model(weather)
78+
79+
fig, axes = plt.subplots(2, 1, sharex=True)
80+
mc.results.effective_irradiance.plot(ax=axes[0])
81+
axes[0].set_ylabel('Effective Irradiance [W/m^2]')
82+
mc.results.ac.plot(ax=axes[1])
83+
axes[1].set_ylabel('AC Power')
84+
fig.show()
85+
86+
# %%
87+
# The effect of discontinuous tracking creates a "jagged" effect in the
88+
# simulated plane-of-array irradiance, which then propagates through to
89+
# the AC power output.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Dual-Axis Tracking
3+
==================
4+
5+
Example of a custom Mount class.
6+
"""
7+
8+
# %%
9+
# Dual-axis trackers can track the sun in two dimensions across the sky dome
10+
# instead of just one like single-axis trackers. This example shows how to
11+
# model a simple dual-axis tracking system using ModelChain with a custom
12+
# Mount class.
13+
14+
from pvlib import pvsystem, location, modelchain
15+
import pandas as pd
16+
import matplotlib.pyplot as plt
17+
18+
# %%
19+
# New Mount classes should extend ``pvlib.pvsystem.AbstractMount``
20+
# and must implement a ``get_orientation(solar_zenith, solar_azimuth)`` method:
21+
22+
23+
class DualAxisTrackerMount(pvsystem.AbstractMount):
24+
def get_orientation(self, solar_zenith, solar_azimuth):
25+
# no rotation limits, no backtracking
26+
return {'surface_tilt': solar_zenith, 'surface_azimuth': solar_azimuth}
27+
28+
29+
loc = location.Location(40, -80)
30+
array = pvsystem.Array(
31+
mount=DualAxisTrackerMount(),
32+
module_parameters=dict(pdc0=1, gamma_pdc=-0.004, b=0.05),
33+
temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3))
34+
system = pvsystem.PVSystem(arrays=[array], inverter_parameters=dict(pdc0=3))
35+
mc = modelchain.ModelChain(system, loc, spectral_model='no_loss')
36+
37+
times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min',
38+
tz='Etc/GMT+5')
39+
weather = loc.get_clearsky(times)
40+
mc.run_model(weather)
41+
42+
mc.results.ac.plot()
43+
plt.ylabel('Output Power')
44+
plt.show()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Mixed Orientation
3+
=================
4+
5+
Using multiple Arrays in a single PVSystem.
6+
"""
7+
8+
# %%
9+
# Residential and Commercial systems often have fixed-tilt arrays
10+
# installed at different azimuths. This can be modeled by using
11+
# multiple :py:class:`~pvlib.pvsystem.Array` objects (one for each
12+
# orientation) with a single :py:class:`~pvlib.pvsystem.PVSystem` object.
13+
#
14+
# This particular example has one east-facing array (azimuth=90) and one
15+
# west-facing array (azimuth=270), which aside from orientation are identical.
16+
17+
18+
from pvlib import pvsystem, modelchain, location
19+
import pandas as pd
20+
import matplotlib.pyplot as plt
21+
22+
array_kwargs = dict(
23+
module_parameters=dict(pdc0=1, gamma_pdc=-0.004),
24+
temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3)
25+
)
26+
27+
arrays = [
28+
pvsystem.Array(pvsystem.FixedMount(30, 270), name='West-Facing Array',
29+
**array_kwargs),
30+
pvsystem.Array(pvsystem.FixedMount(30, 90), name='East-Facing Array',
31+
**array_kwargs),
32+
]
33+
loc = location.Location(40, -80)
34+
system = pvsystem.PVSystem(arrays=arrays, inverter_parameters=dict(pdc0=3))
35+
mc = modelchain.ModelChain(system, loc, aoi_model='physical',
36+
spectral_model='no_loss')
37+
38+
times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min',
39+
tz='Etc/GMT+5')
40+
weather = loc.get_clearsky(times)
41+
mc.run_model(weather)
42+
43+
fig, ax = plt.subplots()
44+
for array, pdc in zip(system.arrays, mc.results.dc):
45+
pdc.plot(label=f'{array.name}')
46+
mc.results.ac.plot(label='Inverter')
47+
plt.ylabel('System Output')
48+
plt.legend()
49+
plt.show()

docs/examples/plot_seasonal_tilt.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Seasonal Tilt
3+
=============
4+
5+
Example of a custom Mount class.
6+
"""
7+
8+
# %%
9+
# Some PV systems are built with the option to adjust the module
10+
# tilt to follow seasonal changes in solar position. For example,
11+
# SAM calls this strategy "Seasonal Tilt". This example shows how
12+
# to use a custom Mount class to use the Seasonal Tilt strategy
13+
# with :py:class:`~pvlib.modelchain.ModelChain`.
14+
15+
import pvlib
16+
from pvlib import pvsystem, location, modelchain, iotools
17+
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
18+
import pandas as pd
19+
import pathlib
20+
import matplotlib.pyplot as plt
21+
from dataclasses import dataclass
22+
23+
24+
# %%
25+
# New Mount classes should extend ``pvlib.pvsystem.AbstractMount``
26+
# and must implement a ``get_orientation(solar_zenith, solar_azimuth)`` method:
27+
28+
29+
@dataclass
30+
class SeasonalTiltMount(pvsystem.AbstractMount):
31+
monthly_tilts: list # length 12, one tilt per calendar month
32+
surface_azimuth: float = 180.0
33+
34+
def get_orientation(self, solar_zenith, solar_azimuth):
35+
tilts = [self.monthly_tilts[m-1] for m in solar_zenith.index.month]
36+
return pd.DataFrame({
37+
'surface_tilt': tilts,
38+
'surface_azimuth': self.surface_azimuth,
39+
}, index=solar_zenith.index)
40+
41+
42+
# %%
43+
# First let's grab some weather data and make sure our mount produces tilts
44+
# like we expect:
45+
46+
DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data'
47+
tmy, metadata = iotools.read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990)
48+
# shift from TMY3 right-labeled index to left-labeled index:
49+
tmy.index = tmy.index - pd.Timedelta(hours=1)
50+
weather = pd.DataFrame({
51+
'ghi': tmy['GHI'], 'dhi': tmy['DHI'], 'dni': tmy['DNI'],
52+
'temp_air': tmy['DryBulb'], 'wind_speed': tmy['Wspd'],
53+
})
54+
loc = location.Location.from_tmy(metadata)
55+
solpos = loc.get_solarposition(weather.index)
56+
# same default monthly tilts as SAM:
57+
tilts = [40, 40, 40, 20, 20, 20, 20, 20, 20, 40, 40, 40]
58+
mount = SeasonalTiltMount(monthly_tilts=tilts)
59+
orientation = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth)
60+
orientation['surface_tilt'].plot()
61+
plt.ylabel('Surface Tilt [degrees]')
62+
plt.show()
63+
64+
# %%
65+
# With our custom tilt strategy defined, we can create the corresponding
66+
# Array and PVSystem, and then run a ModelChain as usual:
67+
68+
module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05}
69+
temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer']
70+
array = pvsystem.Array(mount=mount, module_parameters=module_parameters,
71+
temperature_model_parameters=temp_params)
72+
system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1})
73+
mc = modelchain.ModelChain(system, loc, spectral_model='no_loss')
74+
75+
_ = mc.run_model(weather)
76+
77+
# %%
78+
# Now let's re-run the simulation assuming tilt=30 for the entire year:
79+
80+
array2 = pvsystem.Array(mount=pvsystem.FixedMount(30, 180),
81+
module_parameters=module_parameters,
82+
temperature_model_parameters=temp_params)
83+
system2 = pvsystem.PVSystem(arrays=[array2], inverter_parameters={'pdc0': 1})
84+
mc2 = modelchain.ModelChain(system2, loc, spectral_model='no_loss')
85+
_ = mc2.run_model(weather)
86+
87+
# %%
88+
# And finally, compare simulated monthly generation between the two tilt
89+
# strategies:
90+
91+
# sphinx_gallery_thumbnail_number = 2
92+
results = pd.DataFrame({
93+
'Seasonal 20/40 Production': mc.results.ac,
94+
'Fixed 30 Production': mc2.results.ac,
95+
})
96+
results.resample('m').sum().plot()
97+
plt.ylabel('Monthly Production')
98+
plt.show()

docs/sphinx/source/whatsnew/v0.9.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ Documentation
206206
* Use ``Mount`` classes in ``introtutorial`` and ``pvsystem`` docs pages (:pull:`1267`)
207207
* Clarified how statistics are calculated for :py:func:`pvlib.clearsky.detect_clearsky`
208208
(:issue:`1070`, :pull:`1243`)
209+
* Add gallery examples using the new ``Mount`` classes (:pull:`1266`)
209210

210211
Requirements
211212
~~~~~~~~~~~~

0 commit comments

Comments
 (0)