-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Mount gallery examples #1266
Changes from 6 commits
4556a6a
b4bc3ac
dbad4ca
76555b7
6e72f25
bfefa72
dbf0602
e6866d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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' | ||
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. |
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() |
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() |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
# %% | ||
# 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() |
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.
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.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.
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?
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.
I'd say yes but it's not a clear win and I certainly don't insist on it.