Skip to content

Commit be53c25

Browse files
committed
Merge branch 'main' into remove-deprecated-atmosphere-first-solar-spectral-factor
2 parents f823ca8 + 40a213b commit be53c25

File tree

14 files changed

+451
-154
lines changed

14 files changed

+451
-154
lines changed

docs/sphinx/source/reference/modelchain.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ on the information in the associated :py:class:`~pvsystem.PVSystem` object.
112112
modelchain.ModelChain.infer_dc_model
113113
modelchain.ModelChain.infer_ac_model
114114
modelchain.ModelChain.infer_aoi_model
115-
modelchain.ModelChain.infer_spectral_model
116115
modelchain.ModelChain.infer_temperature_model
117116
modelchain.ModelChain.infer_losses_model
118117

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@
44
v0.11.3 (Anticipated March, 2025)
55
---------------------------------
66

7+
Breaking Changes
8+
~~~~~~~~~~~~~~~~
9+
* The pvlib.location.Location.pytz attribute is now read only. The
10+
pytz attribute is now set internally to be consistent with the
11+
pvlib.location.Location.tz attribute. (:issue:`2340`, :pull:`2341`)
12+
* Users must now provide ModelChain.spectral_model, or the 'no_loss' spectral
13+
model is assumed. pvlib.modelchain.ModelChain no longer attempts to infer
14+
the spectral model from PVSystem attributes. (:issue:`2017`, :pull:`2253`)
15+
16+
Bug fixes
17+
~~~~~~~~~
18+
* Add a check to :py:func:`~pvlib.snow.fully_covered_nrel` and
19+
:py:func:`~pvlib.snow.coverage_nrel`. The check uses snow depth on the ground
20+
to improve modeling for systems with shallow tilt angles. The check
21+
adds a new, optional parameter snow_depth. (:issue:`1171`, :pull:`2292`)
22+
* Fix a bug in :py:func:`pvlib.bifacial.get_irradiance_poa` which may have yielded non-zero
23+
ground irradiance when the sun was below the horizon. (:issue:`2245`, :pull:`2359`)
24+
* Fix a bug where :py:func:`pvlib.transformer.simple_efficiency` could only be imported
25+
using the `from pvlib.transformer` syntax (:pull:`2388`)
26+
* :py:class:`~pvlib.modelchain.ModelChain` now requires only a minimal set of
27+
parameters to run the SAPM electrical model. (:issue:`2369`, :pull:`2393`)
28+
* Correct keys for First Solar modules in `~pvlib.spectrum.spectral_factor_pvspec` (:issue:`2398`, :pull:`2400`)
29+
* Ensure proper tz and pytz types in pvlib.location.Location. To ensure that
30+
the time zone in pvlib.location.Location remains internally consistent
31+
if/when the time zone is updated, the tz attribute is now the single source
32+
of time-zone truth, is the single time-zone setter interface, and its getter
33+
returns an IANA string. (:issue:`2340`, :pull:`2341`)
34+
* :py:func:`~pvlib.iotools.get_pvgis_tmy` with ``outputformat='csv'`` now
35+
works with the updated data format returned by PVGIS. (:issue:`2344`, :pull:`2395`)
36+
737
Deprecations
838
~~~~~~~~~~~~
939

@@ -12,28 +42,27 @@ Enhancements
1242
~~~~~~~~~~~~
1343
* :py:func:`~pvlib.irradiance.gti_dirint` now raises an informative message
1444
when input data don't include values with AOI<90 (:issue:`1342`, :pull:`2347`)
15-
* Fix a bug in :py:func:`pvlib.bifacial.get_irradiance_poa` which may have yielded non-zero
16-
ground irradiance when the sun was below the horizon. (:issue:`2245`, :pull:`2359`)
17-
* Fix a bug where :py:func:`pvlib.transformer.simple_efficiency` could only be imported
18-
using the `from pvlib.transformer` syntax (:pull:`2388`)
1945
* Reduced space requirements by excluding tests and test files from wheel.
2046
Zipped wheel is now 66% of the previous size, and installed size is 50% of
2147
the previous size.
2248
(:issue:`2271`, :pull:`2277`)
23-
* Correct keys for First Solar modules in `~pvlib.spectrum.spectral_factor_pvspec` (:issue:`2398`, :pull:`2400`)
2449

2550
Documentation
2651
~~~~~~~~~~~~~
2752
* Fix Procedural and Object Oriented simulation examples having slightly different results, in :ref:`introtutorial`. (:issue:`2366`, :pull:`2367`)
2853
* Restructure the user guide with subsections (:issue:`2302`, :pull:`2310`)
2954
* Add references for :py:func:`pvlib.snow.loss_townsend`. (:issue:`2383`, :pull:`2384`)
30-
* Add :term:`ghi_clear` to the :ref:`nomenclature` page (:issue:`2272`, :pull`2397`)
55+
* Add :term:`ghi_clear` to the :ref:`nomenclature` page (:issue:`2272`, :pull:`2397`)
56+
* Add output variable naming clarifaction to :py:func:`pvlib.pvsystem.calcparams_desoto` and :py:func:`pvlib.pvsystem.calcparams_pvsyst` (:issue:`716`, :pull:`2405`)
3157

3258
Testing
3359
~~~~~~~
3460
* Moved tests folder to `/tests` and data exclusively used for testing to `/tests/data`.
3561
(:issue:`2271`, :pull:`2277`)
3662
* Added Python 3.13 to test suite. (:pull:`2258`)
63+
* Add tests for all input types for the pvlib.location.Location.tz attribute.
64+
(:issue:`2340`, :pull:`2341`)
65+
* Add tests for time-conversion functions in pvlib.tools. (:issue:`2340`, :pull:`2341`)
3766

3867

3968
Requirements
@@ -51,10 +80,12 @@ Maintenance
5180
Contributors
5281
~~~~~~~~~~~~
5382
* Rajiv Daxini (:ghuser:`RDaxini`)
54-
* Mark Campanelli (:ghuser:`markcampanelli`)
5583
* Cliff Hansen (:ghuser:`cwhanse`)
5684
* Jason Lun Leung (:ghuser:`jason-rpkt`)
5785
* Manoj K S (:ghuser:`manojks1999`)
5886
* Kurt Rhee (:ghuser:`kurt-rhee`)
5987
* Ayush jariyal (:ghuser:`ayushjariyal`)
88+
* Kevin Anderson (:ghuser:`kandersolar`)
6089
* Echedey Luis (:ghuser:`echedey-ls`)
90+
* Mark Campanelli (:ghuser:`markcampanelli`)
91+
* Max Jackson (:ghuser:`MaxJackson`)

pvlib/iotools/pvgis.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,9 +559,17 @@ def _parse_pvgis_tmy_csv(src):
559559
inputs['longitude'] = float(src.readline().split(b':')[1])
560560
# Elevation (m): 1389.0\r\n
561561
inputs['elevation'] = float(src.readline().split(b':')[1])
562+
563+
# TMY has an extra line here: Irradiance Time Offset (h): 0.1761\r\n
564+
line = src.readline()
565+
if line.startswith(b'Irradiance Time Offset'):
566+
inputs['irradiance time offset'] = float(line.split(b':')[1])
567+
src.readline() # skip over the "month,year\r\n"
568+
else:
569+
# `line` is already the "month,year\r\n" line, so nothing to do
570+
pass
562571
# then there's a 13 row comma separated table with two columns: month, year
563-
# which contains the year used for that month in the
564-
src.readline() # get "month,year\r\n"
572+
# which contains the year used for that month in the TMY
565573
months_selected = []
566574
for month in range(12):
567575
months_selected.append(

pvlib/location.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pathlib
88
import datetime
9+
import zoneinfo
910

1011
import pandas as pd
1112
import pytz
@@ -18,13 +19,16 @@
1819
class Location:
1920
"""
2021
Location objects are convenient containers for latitude, longitude,
21-
timezone, and altitude data associated with a particular
22-
geographic location. You can also assign a name to a location object.
22+
time zone, and altitude data associated with a particular geographic
23+
location. You can also assign a name to a location object.
2324
24-
Location objects have two timezone attributes:
25+
Location objects have two time-zone attributes:
2526
26-
* ``tz`` is a IANA timezone string.
27-
* ``pytz`` is a pytz timezone object.
27+
* ``tz`` is an IANA time-zone string.
28+
* ``pytz`` is a pytz-based time-zone object (read only).
29+
30+
The read-only ``pytz`` attribute will stay in sync with any changes made
31+
using ``tz``.
2832
2933
Location objects support the print method.
3034
@@ -38,12 +42,16 @@ class Location:
3842
Positive is east of the prime meridian.
3943
Use decimal degrees notation.
4044
41-
tz : str, int, float, or pytz.timezone, default 'UTC'.
42-
See
43-
http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
44-
for a list of valid time zones.
45-
pytz.timezone objects will be converted to strings.
46-
ints and floats must be in hours from UTC.
45+
tz : time zone as str, int, float, or datetime.tzinfo, default 'UTC'.
46+
See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a
47+
list of valid name strings. An `int` or `float` must be a whole-number
48+
hour offsets from UTC that can be converted to the IANA-supported
49+
'Etc/GMT-N' format. (Note the limited range of the offset N and its
50+
sign-change convention.) Time zones from the pytz and zoneinfo packages
51+
may also be passed here, as they are subclasses of datetime.tzinfo.
52+
53+
The `tz` attribute is represented as a valid IANA time zone name
54+
string.
4755
4856
altitude : float, optional
4957
Altitude from sea level in meters.
@@ -54,43 +62,75 @@ class Location:
5462
name : string, optional
5563
Sets the name attribute of the Location object.
5664
65+
Raises
66+
------
67+
ValueError
68+
when the time zone ``tz`` cannot be converted.
69+
70+
zoneinfo.ZoneInfoNotFoundError
71+
when the time zone ``tz`` is not recognizable as an IANA time zone by
72+
the ``zoneinfo.ZoneInfo`` initializer used for internal time-zone
73+
representation.
74+
5775
See also
5876
--------
5977
pvlib.pvsystem.PVSystem
6078
"""
6179

62-
def __init__(self, latitude, longitude, tz='UTC', altitude=None,
63-
name=None):
64-
80+
def __init__(
81+
self, latitude, longitude, tz='UTC', altitude=None, name=None
82+
):
6583
self.latitude = latitude
6684
self.longitude = longitude
67-
68-
if isinstance(tz, str):
69-
self.tz = tz
70-
self.pytz = pytz.timezone(tz)
71-
elif isinstance(tz, datetime.timezone):
72-
self.tz = 'UTC'
73-
self.pytz = pytz.UTC
74-
elif isinstance(tz, datetime.tzinfo):
75-
self.tz = tz.zone
76-
self.pytz = tz
77-
elif isinstance(tz, (int, float)):
78-
self.tz = tz
79-
self.pytz = pytz.FixedOffset(tz*60)
80-
else:
81-
raise TypeError('Invalid tz specification')
85+
self.tz = tz
8286

8387
if altitude is None:
8488
altitude = lookup_altitude(latitude, longitude)
8589

8690
self.altitude = altitude
87-
8891
self.name = name
8992

9093
def __repr__(self):
9194
attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz']
95+
# Use None as getattr default in case __repr__ is called during
96+
# initialization before all attributes have been assigned.
9297
return ('Location: \n ' + '\n '.join(
93-
f'{attr}: {getattr(self, attr)}' for attr in attrs))
98+
f'{attr}: {getattr(self, attr, None)}' for attr in attrs))
99+
100+
@property
101+
def tz(self):
102+
"""The location's IANA time-zone string."""
103+
return str(self._zoneinfo)
104+
105+
@tz.setter
106+
def tz(self, tz_):
107+
# self._zoneinfo holds single source of time-zone truth as IANA name.
108+
if isinstance(tz_, str):
109+
self._zoneinfo = zoneinfo.ZoneInfo(tz_)
110+
elif isinstance(tz_, int):
111+
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-tz_:+d}")
112+
elif isinstance(tz_, float):
113+
if tz_ % 1 != 0:
114+
raise TypeError(
115+
"Floating-point tz has non-zero fractional part: "
116+
f"{tz_}. Only whole-number offsets are supported."
117+
)
118+
119+
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-int(tz_):+d}")
120+
elif isinstance(tz_, datetime.tzinfo):
121+
# Includes time zones generated by pytz and zoneinfo packages.
122+
self._zoneinfo = zoneinfo.ZoneInfo(str(tz_))
123+
else:
124+
raise TypeError(
125+
f"invalid tz specification: {tz_}, must be an IANA time zone "
126+
"string, a whole-number int/float UTC offset, or a "
127+
"datetime.tzinfo object (including subclasses)"
128+
)
129+
130+
@property
131+
def pytz(self):
132+
"""The location's pytz time zone (read only)."""
133+
return pytz.timezone(str(self._zoneinfo))
94134

95135
@classmethod
96136
def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs):

pvlib/modelchain.py

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
# Optional keys to communicate temperature data. If provided,
3131
# 'cell_temperature' overrides ModelChain.temperature_model and sets
32-
# ModelChain.cell_temperature to the data. If 'module_temperature' is provdied,
32+
# ModelChain.cell_temperature to the data. If 'module_temperature' is provided,
3333
# overrides ModelChain.temperature_model with
3434
# pvlib.temperature.sapm_celL_from_module
3535
TEMPERATURE_KEYS = ('module_temperature', 'cell_temperature')
@@ -253,7 +253,7 @@ def __repr__(self):
253253
def _head(obj):
254254
try:
255255
return obj[:3]
256-
except:
256+
except Exception:
257257
return obj
258258

259259
if type(self.dc) is tuple:
@@ -269,7 +269,7 @@ def _head(obj):
269269
'\n')
270270
lines = []
271271
for attr in mc_attrs:
272-
if not (attr.startswith('_') or attr=='times'):
272+
if not (attr.startswith('_') or attr == 'times'):
273273
lines.append(f' {attr}: ' + _mcr_repr(getattr(self, attr)))
274274
desc4 = '\n'.join(lines)
275275
return (desc1 + desc2 + desc3 + desc4)
@@ -330,12 +330,15 @@ class ModelChain:
330330
'interp' and 'no_loss'. The ModelChain instance will be passed as the
331331
first argument to a user-defined function.
332332
333-
spectral_model : str, or function, optional
334-
If not specified, the model will be inferred from the parameters that
335-
are common to all of system.arrays[i].module_parameters.
336-
Valid strings are 'sapm', 'first_solar', 'no_loss'.
333+
spectral_model : str or function, optional
334+
Valid strings are:
335+
336+
- ``'sapm'``
337+
- ``'first_solar'``
338+
- ``'no_loss'``
339+
337340
The ModelChain instance will be passed as the first argument to
338-
a user-defined function.
341+
a user-defined function. If not specified, ``'no_loss'`` is assumed.
339342
340343
temperature_model : str or function, optional
341344
Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'.
@@ -386,7 +389,6 @@ def __init__(self, system, location,
386389

387390
self.results = ModelChainResult()
388391

389-
390392
@classmethod
391393
def with_pvwatts(cls, system, location,
392394
clearsky_model='ineichen',
@@ -855,9 +857,7 @@ def spectral_model(self):
855857

856858
@spectral_model.setter
857859
def spectral_model(self, model):
858-
if model is None:
859-
self._spectral_model = self.infer_spectral_model()
860-
elif isinstance(model, str):
860+
if isinstance(model, str):
861861
model = model.lower()
862862
if model == 'first_solar':
863863
self._spectral_model = self.first_solar_spectral_loss
@@ -867,30 +867,12 @@ def spectral_model(self, model):
867867
self._spectral_model = self.no_spectral_loss
868868
else:
869869
raise ValueError(model + ' is not a valid spectral loss model')
870-
else:
870+
elif model is None:
871+
# not setting a model is equivalent to setting no_loss
872+
self._spectral_model = self.no_spectral_loss
873+
else: # assume model is callable with 1st argument = the MC instance
871874
self._spectral_model = partial(model, self)
872875

873-
def infer_spectral_model(self):
874-
"""Infer spectral model from system attributes."""
875-
module_parameters = tuple(
876-
array.module_parameters for array in self.system.arrays)
877-
params = _common_keys(module_parameters)
878-
if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params:
879-
return self.sapm_spectral_loss
880-
elif ((('Technology' in params or
881-
'Material' in params) and
882-
(self.system._infer_cell_type() is not None)) or
883-
'first_solar_spectral_coefficients' in params):
884-
return self.first_solar_spectral_loss
885-
else:
886-
raise ValueError('could not infer spectral model from '
887-
'system.arrays[i].module_parameters. Check that '
888-
'the module_parameters for all Arrays in '
889-
'system.arrays contain valid '
890-
'first_solar_spectral_coefficients, a valid '
891-
'Material or Technology value, or set '
892-
'spectral_model="no_loss".')
893-
894876
def first_solar_spectral_loss(self):
895877
self.results.spectral_modifier = self.system.first_solar_spectral_loss(
896878
_tuple_from_dfs(self.results.weather, 'precipitable_water'),
@@ -1570,7 +1552,7 @@ def _prepare_temperature(self, data):
15701552
----------
15711553
data : DataFrame
15721554
May contain columns ``'cell_temperature'`` or
1573-
``'module_temperaure'``.
1555+
``'module_temperature'``.
15741556
15751557
Returns
15761558
-------
@@ -1679,6 +1661,7 @@ def run_model(self, weather):
16791661
self.prepare_inputs(weather)
16801662
self.aoi_model()
16811663
self.spectral_model()
1664+
16821665
self.effective_irradiance_model()
16831666

16841667
self._run_from_effective_irrad(weather)

0 commit comments

Comments
 (0)