Skip to content

Mount gallery examples #1266

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

Merged
merged 8 commits into from
Aug 7, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/examples/plot_discontinuous_tracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
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.

import pvlib
from pvlib import tracking, pvsystem, location, modelchain, iotools
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
import matplotlib.pyplot as plt
import pathlib


# %%
# 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:

# a simple weather dataset for illustration -- one day of 1-minute weather
DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data'
Copy link
Member

Choose a reason for hiding this comment

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

Is the downward spike due to DNI momentarily dropping to 0 in the weather data file? While it might be nice to highlight the read_bsrn function here, the spike is the most noticeable thing in the results. My concern is that the people that would most benefit from this example are also the people most likely to be distracted by that spike.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't like that spike either, but that was the only clear-sky day in the file. Would it be better to keep it simple and do a quick clear-sky simulation instead of using the BSRN data?

Copy link
Member

Choose a reason for hiding this comment

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

I'd say yes but it's not a clear win and I certainly don't insist on it.

weather = iotools.read_bsrn(DATA_DIR / 'bsrn-lr0100-pay0616.dat')
weather = weather.fillna(0)
weather = weather.loc['2016-06-23'] # an example clear-sky day
loc = location.Location(46.8150, 6.9440) # BSRN Payerne station

mount = DiscontinuousTrackerMount(axis_azimuth=180, gcr=0.4)
solpos = loc.get_solarposition(weather.index)
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')

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.
44 changes: 44 additions & 0 deletions docs/examples/plot_dual_axis_tracking.py
Original file line number Diff line number Diff line change
@@ -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()
49 changes: 49 additions & 0 deletions docs/examples/plot_mixed_orientation.py
Original file line number Diff line number Diff line change
@@ -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()
98 changes: 98 additions & 0 deletions docs/examples/plot_seasonal_tilt.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

Is the underscore to suppress output? I'm not sure about sphinx-gallery, but the ipython directive will suppress output if you end the line in ;. No big deal to keep it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, to suppress output. Semicolon doesn't suppress in sphinx-gallery, at least in my local build:

image


# %%
# 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()
1 change: 1 addition & 0 deletions docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~
Expand Down