diff --git a/docs/examples/plot_discontinuous_tracking.py b/docs/examples/plot_discontinuous_tracking.py new file mode 100644 index 0000000000..f385675d57 --- /dev/null +++ b/docs/examples/plot_discontinuous_tracking.py @@ -0,0 +1,89 @@ +""" +Discontinuous Tracking +====================== + +Example of a custom Mount class. +""" + +# %% +# Many real-world tracking arrays adjust their position in discrete steps +# rather than through continuous movement. This example shows how to model +# this discontinuous tracking by implementing a custom Mount class. + +from pvlib import tracking, pvsystem, location, modelchain +from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS +import matplotlib.pyplot as plt +import pandas as pd + + +# %% +# We'll define our custom Mount by extending +# :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount` for convenience. +# Another approach would be to extend ``AbstractMount`` directly; see +# the source code of :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount` +# and :py:class:`~pvlib.pvsystem.FixedMount` for how that is done. + + +class DiscontinuousTrackerMount(pvsystem.SingleAxisTrackerMount): + # inherit from SingleAxisTrackerMount so that we get the + # constructor and tracking attributes (axis_tilt etc) automatically + + def get_orientation(self, solar_zenith, solar_azimuth): + # Different trackers update at different rates; in this example we'll + # assume a relatively slow update interval of 15 minutes to make the + # effect more visually apparent. + zenith_subset = solar_zenith.resample('15min').first() + azimuth_subset = solar_azimuth.resample('15min').first() + + tracking_data_15min = tracking.singleaxis( + zenith_subset, azimuth_subset, + self.axis_tilt, self.axis_azimuth, + self.max_angle, self.backtrack, + self.gcr, self.cross_axis_tilt + ) + # propagate the 15-minute positions to 1-minute stair-stepped values: + tracking_data_1min = tracking_data_15min.reindex(solar_zenith.index, + method='ffill') + return tracking_data_1min + + +# %% +# Let's take a look at the tracker rotation curve it produces: + +times = pd.date_range('2019-06-01', '2019-06-02', freq='1min', tz='US/Eastern') +loc = location.Location(40, -80) +solpos = loc.get_solarposition(times) +mount = DiscontinuousTrackerMount(axis_azimuth=180, gcr=0.4) +tracker_data = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth) +tracker_data['tracker_theta'].plot() +plt.ylabel('Tracker Rotation [degree]') +plt.show() + +# %% +# With our custom tracking logic defined, we can create the corresponding +# Array and PVSystem, and then run a ModelChain as usual: + +module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05} +temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'] +array = pvsystem.Array(mount=mount, module_parameters=module_parameters, + temperature_model_parameters=temp_params) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1}) +mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') + +# simple simulated weather, just to show the effect of discrete tracking +weather = loc.get_clearsky(times) +weather['temp_air'] = 25 +weather['wind_speed'] = 1 +mc.run_model(weather) + +fig, axes = plt.subplots(2, 1, sharex=True) +mc.results.effective_irradiance.plot(ax=axes[0]) +axes[0].set_ylabel('Effective Irradiance [W/m^2]') +mc.results.ac.plot(ax=axes[1]) +axes[1].set_ylabel('AC Power') +fig.show() + +# %% +# The effect of discontinuous tracking creates a "jagged" effect in the +# simulated plane-of-array irradiance, which then propagates through to +# the AC power output. diff --git a/docs/examples/plot_dual_axis_tracking.py b/docs/examples/plot_dual_axis_tracking.py new file mode 100644 index 0000000000..8f51512726 --- /dev/null +++ b/docs/examples/plot_dual_axis_tracking.py @@ -0,0 +1,44 @@ +""" +Dual-Axis Tracking +================== + +Example of a custom Mount class. +""" + +# %% +# Dual-axis trackers can track the sun in two dimensions across the sky dome +# instead of just one like single-axis trackers. This example shows how to +# model a simple dual-axis tracking system using ModelChain with a custom +# Mount class. + +from pvlib import pvsystem, location, modelchain +import pandas as pd +import matplotlib.pyplot as plt + +# %% +# New Mount classes should extend ``pvlib.pvsystem.AbstractMount`` +# and must implement a ``get_orientation(solar_zenith, solar_azimuth)`` method: + + +class DualAxisTrackerMount(pvsystem.AbstractMount): + def get_orientation(self, solar_zenith, solar_azimuth): + # no rotation limits, no backtracking + return {'surface_tilt': solar_zenith, 'surface_azimuth': solar_azimuth} + + +loc = location.Location(40, -80) +array = pvsystem.Array( + mount=DualAxisTrackerMount(), + module_parameters=dict(pdc0=1, gamma_pdc=-0.004, b=0.05), + temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3)) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters=dict(pdc0=3)) +mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') + +times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min', + tz='Etc/GMT+5') +weather = loc.get_clearsky(times) +mc.run_model(weather) + +mc.results.ac.plot() +plt.ylabel('Output Power') +plt.show() diff --git a/docs/examples/plot_mixed_orientation.py b/docs/examples/plot_mixed_orientation.py new file mode 100644 index 0000000000..987b4b4994 --- /dev/null +++ b/docs/examples/plot_mixed_orientation.py @@ -0,0 +1,49 @@ +""" +Mixed Orientation +================= + +Using multiple Arrays in a single PVSystem. +""" + +# %% +# Residential and Commercial systems often have fixed-tilt arrays +# installed at different azimuths. This can be modeled by using +# multiple :py:class:`~pvlib.pvsystem.Array` objects (one for each +# orientation) with a single :py:class:`~pvlib.pvsystem.PVSystem` object. +# +# This particular example has one east-facing array (azimuth=90) and one +# west-facing array (azimuth=270), which aside from orientation are identical. + + +from pvlib import pvsystem, modelchain, location +import pandas as pd +import matplotlib.pyplot as plt + +array_kwargs = dict( + module_parameters=dict(pdc0=1, gamma_pdc=-0.004), + temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3) +) + +arrays = [ + pvsystem.Array(pvsystem.FixedMount(30, 270), name='West-Facing Array', + **array_kwargs), + pvsystem.Array(pvsystem.FixedMount(30, 90), name='East-Facing Array', + **array_kwargs), +] +loc = location.Location(40, -80) +system = pvsystem.PVSystem(arrays=arrays, inverter_parameters=dict(pdc0=3)) +mc = modelchain.ModelChain(system, loc, aoi_model='physical', + spectral_model='no_loss') + +times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min', + tz='Etc/GMT+5') +weather = loc.get_clearsky(times) +mc.run_model(weather) + +fig, ax = plt.subplots() +for array, pdc in zip(system.arrays, mc.results.dc): + pdc.plot(label=f'{array.name}') +mc.results.ac.plot(label='Inverter') +plt.ylabel('System Output') +plt.legend() +plt.show() diff --git a/docs/examples/plot_seasonal_tilt.py b/docs/examples/plot_seasonal_tilt.py new file mode 100644 index 0000000000..999aaca96a --- /dev/null +++ b/docs/examples/plot_seasonal_tilt.py @@ -0,0 +1,98 @@ +""" +Seasonal Tilt +============= + +Example of a custom Mount class. +""" + +# %% +# Some PV systems are built with the option to adjust the module +# tilt to follow seasonal changes in solar position. For example, +# SAM calls this strategy "Seasonal Tilt". This example shows how +# to use a custom Mount class to use the Seasonal Tilt strategy +# with :py:class:`~pvlib.modelchain.ModelChain`. + +import pvlib +from pvlib import pvsystem, location, modelchain, iotools +from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS +import pandas as pd +import pathlib +import matplotlib.pyplot as plt +from dataclasses import dataclass + + +# %% +# New Mount classes should extend ``pvlib.pvsystem.AbstractMount`` +# and must implement a ``get_orientation(solar_zenith, solar_azimuth)`` method: + + +@dataclass +class SeasonalTiltMount(pvsystem.AbstractMount): + monthly_tilts: list # length 12, one tilt per calendar month + surface_azimuth: float = 180.0 + + def get_orientation(self, solar_zenith, solar_azimuth): + tilts = [self.monthly_tilts[m-1] for m in solar_zenith.index.month] + return pd.DataFrame({ + 'surface_tilt': tilts, + 'surface_azimuth': self.surface_azimuth, + }, index=solar_zenith.index) + + +# %% +# First let's grab some weather data and make sure our mount produces tilts +# like we expect: + +DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' +tmy, metadata = iotools.read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990) +# shift from TMY3 right-labeled index to left-labeled index: +tmy.index = tmy.index - pd.Timedelta(hours=1) +weather = pd.DataFrame({ + 'ghi': tmy['GHI'], 'dhi': tmy['DHI'], 'dni': tmy['DNI'], + 'temp_air': tmy['DryBulb'], 'wind_speed': tmy['Wspd'], +}) +loc = location.Location.from_tmy(metadata) +solpos = loc.get_solarposition(weather.index) +# same default monthly tilts as SAM: +tilts = [40, 40, 40, 20, 20, 20, 20, 20, 20, 40, 40, 40] +mount = SeasonalTiltMount(monthly_tilts=tilts) +orientation = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth) +orientation['surface_tilt'].plot() +plt.ylabel('Surface Tilt [degrees]') +plt.show() + +# %% +# With our custom tilt strategy defined, we can create the corresponding +# Array and PVSystem, and then run a ModelChain as usual: + +module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05} +temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'] +array = pvsystem.Array(mount=mount, module_parameters=module_parameters, + temperature_model_parameters=temp_params) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1}) +mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') + +_ = mc.run_model(weather) + +# %% +# Now let's re-run the simulation assuming tilt=30 for the entire year: + +array2 = pvsystem.Array(mount=pvsystem.FixedMount(30, 180), + module_parameters=module_parameters, + temperature_model_parameters=temp_params) +system2 = pvsystem.PVSystem(arrays=[array2], inverter_parameters={'pdc0': 1}) +mc2 = modelchain.ModelChain(system2, loc, spectral_model='no_loss') +_ = mc2.run_model(weather) + +# %% +# And finally, compare simulated monthly generation between the two tilt +# strategies: + +# sphinx_gallery_thumbnail_number = 2 +results = pd.DataFrame({ + 'Seasonal 20/40 Production': mc.results.ac, + 'Fixed 30 Production': mc2.results.ac, +}) +results.resample('m').sum().plot() +plt.ylabel('Monthly Production') +plt.show() diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 9a17b2d875..70cb971f39 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -204,6 +204,7 @@ Documentation * Update documentation links in :py:func:`pvlib.iotools.get_psm3` * Clarified how statistics are calculated for :py:func:`pvlib.clearsky.detect_clearsky` (:issue:`1070`, :pull:`1243`) +* Add gallery examples using the new ``Mount`` classes (:pull:`1266`) Requirements ~~~~~~~~~~~~