Skip to content

Commit f7f9dbb

Browse files
authored
Add noct_sam cell temperature model to PVSystem, ModelChain (#1195)
* add ModelChain, PVSystem methods * first cut at pvsystem tests * fix iterable, called_once_with in test * derp * handling of optional argument, misspelling * mimic ModelChain tests to noct_sam * add required params to test fixture * fix key value * complete test spec * complete test coverage, whatsnew * docstring work * additions to api * docstring reverts, raise if missing arg * add types to method test * edits from review
1 parent c17f728 commit f7f9dbb

File tree

7 files changed

+194
-19
lines changed

7 files changed

+194
-19
lines changed

docs/sphinx/source/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ PV temperature models
242242
pvsystem.PVSystem.sapm_celltemp
243243
pvsystem.PVSystem.pvsyst_celltemp
244244
pvsystem.PVSystem.faiman_celltemp
245+
pvsystem.PVSystem.fuentes_celltemp
246+
pvsystem.PVSystem.noct_sam_celltemp
245247

246248
Temperature Model Parameters
247249
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Enhancements
102102
from DC power. Use parameter ``model`` to specify which inverter model to use.
103103
(:pull:`1147`, :issue:`998`, :pull:`1150`)
104104
* Added :py:func:`~pvlib.temperature.noct_sam`, a cell temperature model
105-
implemented in SAM (:pull:`1177`)
105+
implemented in SAM (:pull:`1177`, :pull:`1195`)
106106

107107
Bug fixes
108108
~~~~~~~~~

pvlib/modelchain.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ class ModelChain:
352352
as the first argument to a user-defined function.
353353
354354
temperature_model: None, str or function, default None
355-
Valid strings are 'sapm', 'pvsyst', 'faiman', and 'fuentes'.
355+
Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'.
356356
The ModelChain instance will be passed as the first argument to a
357357
user-defined function.
358358
@@ -935,6 +935,8 @@ def temperature_model(self, model):
935935
self._temperature_model = self.faiman_temp
936936
elif model == 'fuentes':
937937
self._temperature_model = self.fuentes_temp
938+
elif model == 'noct_sam':
939+
self._temperature_model = self.noct_sam_temp
938940
else:
939941
raise ValueError(model + ' is not a valid temperature model')
940942
# check system.temperature_model_parameters for consistency
@@ -965,6 +967,8 @@ def infer_temperature_model(self):
965967
return self.faiman_temp
966968
elif {'noct_installed'} <= params:
967969
return self.fuentes_temp
970+
elif {'noct', 'eta_m_ref'} <= params:
971+
return self.noct_sam_temp
968972
else:
969973
raise ValueError(f'could not infer temperature model from '
970974
f'system.temperature_model_parameters. Check '
@@ -994,7 +998,11 @@ def _set_celltemp(self, model):
994998
self.results.effective_irradiance)
995999
temp_air = _tuple_from_dfs(self.weather, 'temp_air')
9961000
wind_speed = _tuple_from_dfs(self.weather, 'wind_speed')
997-
self.results.cell_temperature = model(poa, temp_air, wind_speed)
1001+
arg_list = [poa, temp_air, wind_speed]
1002+
kwargs = {}
1003+
if model == self.system.noct_sam_celltemp:
1004+
kwargs['effective_irradiance'] = self.results.effective_irradiance
1005+
self.results.cell_temperature = model(*tuple(arg_list))
9981006
return self
9991007

10001008
def sapm_temp(self):
@@ -1009,6 +1017,9 @@ def faiman_temp(self):
10091017
def fuentes_temp(self):
10101018
return self._set_celltemp(self.system.fuentes_celltemp)
10111019

1020+
def noct_sam_temp(self):
1021+
return self._set_celltemp(self.system.noct_sam_celltemp)
1022+
10121023
@property
10131024
def losses_model(self):
10141025
return self._losses_model

pvlib/pvsystem.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ class PVSystem:
143143
Module parameters as defined by the SAPM, CEC, or other.
144144
145145
temperature_model_parameters : None, dict or Series, default None.
146-
Temperature model parameters as defined by the SAPM, Pvsyst, or other.
146+
Temperature model parameters as required by one of the models in
147+
pvlib.temperature (excluding poa_global, temp_air and wind_speed).
147148
148149
modules_per_string: int or float, default 1
149150
See system topology discussion above.
@@ -750,8 +751,6 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed):
750751
if you want to match the PVWatts behavior, you can override it by
751752
including a ``surface_tilt`` value in ``temperature_model_parameters``.
752753
753-
Notes
754-
-----
755754
The `temp_air` and `wind_speed` parameters may be passed as tuples
756755
to provide different values for each Array in the system. If not
757756
passed as a tuple then the same value is used for input to each Array.
@@ -781,6 +780,82 @@ def _build_kwargs_fuentes(array):
781780
)
782781
)
783782

783+
@_unwrap_single_value
784+
def noct_sam_celltemp(self, poa_global, temp_air, wind_speed,
785+
effective_irradiance=None):
786+
"""
787+
Use :py:func:`temperature.noct_sam` to calculate cell temperature.
788+
789+
Parameters
790+
----------
791+
poa_global : numeric or tuple of numeric
792+
Total incident irradiance in W/m^2.
793+
794+
temp_air : numeric or tuple of numeric
795+
Ambient dry bulb temperature in degrees C.
796+
797+
wind_speed : numeric or tuple of numeric
798+
Wind speed in m/s at a height of 10 meters.
799+
800+
effective_irradiance : numeric, tuple of numeric, or None.
801+
The irradiance that is converted to photocurrent. If None,
802+
assumed equal to ``poa_global``. [W/m^2]
803+
804+
Returns
805+
-------
806+
temperature_cell : numeric or tuple of numeric
807+
The modeled cell temperature [C]
808+
809+
Notes
810+
-----
811+
The `temp_air` and `wind_speed` parameters may be passed as tuples
812+
to provide different values for each Array in the system. If not
813+
passed as a tuple then the same value is used for input to each Array.
814+
If passed as a tuple the length must be the same as the number of
815+
Arrays.
816+
"""
817+
# default to using the Array attribute, but allow user to
818+
# override with a custom surface_tilt value
819+
poa_global = self._validate_per_array(poa_global)
820+
temp_air = self._validate_per_array(temp_air, system_wide=True)
821+
wind_speed = self._validate_per_array(wind_speed, system_wide=True)
822+
823+
# need effective_irradiance to be an iterable
824+
if effective_irradiance is None:
825+
effective_irradiance = tuple([None] * self.num_arrays)
826+
else:
827+
effective_irradiance = self._validate_per_array(
828+
effective_irradiance)
829+
830+
def _build_kwargs_noct_sam(array):
831+
temp_model_kwargs = _build_kwargs([
832+
'transmittance_absorptance',
833+
'array_height', 'mount_standoff'],
834+
array.temperature_model_parameters)
835+
try:
836+
# noct_sam required args
837+
# bundled with kwargs for simplicity
838+
temp_model_kwargs['noct'] = \
839+
array.temperature_model_parameters['noct']
840+
temp_model_kwargs['eta_m_ref'] = \
841+
array.temperature_model_parameters['eta_m_ref']
842+
except KeyError:
843+
msg = ('Parameters noct and eta_m_ref are required.'
844+
' Found {} in temperature_model_parameters.'
845+
.format(array.temperature_model_parameters))
846+
raise KeyError(msg)
847+
return temp_model_kwargs
848+
return tuple(
849+
temperature.noct_sam(
850+
poa_global, temp_air, wind_speed,
851+
effective_irradiance=eff_irrad,
852+
**_build_kwargs_noct_sam(array))
853+
for array, poa_global, temp_air, wind_speed, eff_irrad in zip(
854+
self.arrays, poa_global, temp_air, wind_speed,
855+
effective_irradiance
856+
)
857+
)
858+
784859
@_unwrap_single_value
785860
def first_solar_spectral_loss(self, pw, airmass_absolute):
786861

pvlib/tests/test_modelchain.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,18 @@ def pvwatts_dc_pvwatts_ac_fuentes_temp_system():
223223
return system
224224

225225

226+
@pytest.fixture(scope="function")
227+
def pvwatts_dc_pvwatts_ac_noct_sam_temp_system():
228+
module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
229+
temp_model_params = {'noct': 45, 'eta_m_ref': 0.2}
230+
inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95}
231+
system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
232+
module_parameters=module_parameters,
233+
temperature_model_parameters=temp_model_params,
234+
inverter_parameters=inverter_parameters)
235+
return system
236+
237+
226238
@pytest.fixture(scope="function")
227239
def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m,
228240
cec_inverter_parameters):
@@ -693,6 +705,23 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location,
693705
assert not mc.results.ac.empty
694706

695707

708+
def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location,
709+
weather, mocker):
710+
weather['wind_speed'] = 5
711+
weather['temp_air'] = 10
712+
sapm_dc_snl_ac_system.temperature_model_parameters = {
713+
'noct': 45, 'eta_m_ref': 0.2
714+
}
715+
mc = ModelChain(sapm_dc_snl_ac_system, location)
716+
mc.temperature_model = 'noct_sam'
717+
m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'noct_sam_celltemp')
718+
mc.run_model(weather)
719+
assert m_noct_sam.call_count == 1
720+
assert_series_equal(m_noct_sam.call_args[0][1], weather['temp_air'])
721+
assert_series_equal(m_noct_sam.call_args[0][2], weather['wind_speed'])
722+
assert not mc.results.ac.empty
723+
724+
696725
def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker):
697726
system = SingleAxisTracker(
698727
module_parameters=sapm_dc_snl_ac_system.module_parameters,
@@ -907,7 +936,9 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays,
907936
({'u0': 25.0, 'u1': 6.84},
908937
ModelChain.faiman_temp),
909938
({'noct_installed': 45},
910-
ModelChain.fuentes_temp)])
939+
ModelChain.fuentes_temp),
940+
({'noct': 45, 'eta_m_ref': 0.2},
941+
ModelChain.noct_sam_temp)])
911942
def test_temperature_models_arrays_multi_weather(
912943
temp_params, temp_model,
913944
sapm_dc_snl_ac_system_same_arrays,
@@ -1256,16 +1287,19 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system,
12561287

12571288

12581289
@pytest.mark.parametrize('temp_model', [
1259-
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp'])
1290+
'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp',
1291+
'noct_sam_temp'])
12601292
def test_infer_temp_model(location, sapm_dc_snl_ac_system,
12611293
pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
12621294
pvwatts_dc_pvwatts_ac_faiman_temp_system,
12631295
pvwatts_dc_pvwatts_ac_fuentes_temp_system,
1296+
pvwatts_dc_pvwatts_ac_noct_sam_temp_system,
12641297
temp_model):
12651298
dc_systems = {'sapm_temp': sapm_dc_snl_ac_system,
12661299
'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
12671300
'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system,
1268-
'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system}
1301+
'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system,
1302+
'noct_sam_temp': pvwatts_dc_pvwatts_ac_noct_sam_temp_system}
12691303
system = dc_systems[temp_model]
12701304
mc = ModelChain(system, location, aoi_model='physical',
12711305
spectral_model='no_loss')

pvlib/tests/test_pvsystem.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,11 @@ def two_array_system(pvsyst_module_params, cec_module_params):
388388
# Need u_v to be non-zero so wind-speed changes cell temperature
389389
# under the pvsyst model.
390390
temperature_model['u_v'] = 1.0
391+
# parameter for fuentes temperature model
391392
temperature_model['noct_installed'] = 45
393+
# parameters for noct_sam temperature model
394+
temperature_model['noct'] = 45.
395+
temperature_model['eta_m_ref'] = 0.2
392396
module_params = {**pvsyst_module_params, **cec_module_params}
393397
return pvsystem.PVSystem(
394398
arrays=[
@@ -495,11 +499,53 @@ def test_PVSystem_faiman_celltemp(mocker):
495499
assert_allclose(out, 56.4, atol=1)
496500

497501

502+
def test_PVSystem_noct_celltemp(mocker):
503+
poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
504+
0.2)
505+
expected = 55.230790492
506+
temp_model_params = {'noct': noct, 'eta_m_ref': eta_m_ref}
507+
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
508+
mocker.spy(temperature, 'noct_sam')
509+
out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed)
510+
temperature.noct_sam.assert_called_once_with(
511+
poa_global, temp_air, wind_speed, effective_irradiance=None, noct=noct,
512+
eta_m_ref=eta_m_ref)
513+
assert_allclose(out, expected)
514+
# dufferent types
515+
out = system.noct_sam_celltemp(np.array(poa_global), np.array(temp_air),
516+
np.array(wind_speed))
517+
assert_allclose(out, expected)
518+
dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00',
519+
freq='1H')
520+
out = system.noct_sam_celltemp(pd.Series(index=dr, data=poa_global),
521+
pd.Series(index=dr, data=temp_air),
522+
pd.Series(index=dr, data=wind_speed))
523+
assert_series_equal(out, pd.Series(index=dr, data=expected))
524+
# now use optional arguments
525+
temp_model_params.update({'transmittance_absorptance': 0.8,
526+
'array_height': 2,
527+
'mount_standoff': 2.0})
528+
expected = 60.477703576
529+
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
530+
out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed,
531+
effective_irradiance=1100.)
532+
assert_allclose(out, expected)
533+
534+
535+
def test_PVSystem_noct_celltemp_error():
536+
poa_global, temp_air, wind_speed, eta_m_ref = (1000., 25., 1., 0.2)
537+
temp_model_params = {'eta_m_ref': eta_m_ref}
538+
system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
539+
with pytest.raises(KeyError):
540+
system.noct_sam_celltemp(poa_global, temp_air, wind_speed)
541+
542+
498543
@pytest.mark.parametrize("celltemp",
499544
[pvsystem.PVSystem.faiman_celltemp,
500545
pvsystem.PVSystem.pvsyst_celltemp,
501546
pvsystem.PVSystem.sapm_celltemp,
502-
pvsystem.PVSystem.fuentes_celltemp])
547+
pvsystem.PVSystem.fuentes_celltemp,
548+
pvsystem.PVSystem.noct_sam_celltemp])
503549
def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
504550
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
505551
irrad_one = pd.Series(1000, index=times)
@@ -515,7 +561,8 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
515561
[pvsystem.PVSystem.faiman_celltemp,
516562
pvsystem.PVSystem.pvsyst_celltemp,
517563
pvsystem.PVSystem.sapm_celltemp,
518-
pvsystem.PVSystem.fuentes_celltemp])
564+
pvsystem.PVSystem.fuentes_celltemp,
565+
pvsystem.PVSystem.noct_sam_celltemp])
519566
def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
520567
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
521568
irrad = pd.Series(1000, index=times)
@@ -543,7 +590,8 @@ def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
543590
[pvsystem.PVSystem.faiman_celltemp,
544591
pvsystem.PVSystem.pvsyst_celltemp,
545592
pvsystem.PVSystem.sapm_celltemp,
546-
pvsystem.PVSystem.fuentes_celltemp])
593+
pvsystem.PVSystem.fuentes_celltemp,
594+
pvsystem.PVSystem.noct_sam_celltemp])
547595
def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
548596
times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
549597
irrad = pd.Series(1000, index=times)
@@ -571,7 +619,8 @@ def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
571619
[pvsystem.PVSystem.faiman_celltemp,
572620
pvsystem.PVSystem.pvsyst_celltemp,
573621
pvsystem.PVSystem.sapm_celltemp,
574-
pvsystem.PVSystem.fuentes_celltemp])
622+
pvsystem.PVSystem.fuentes_celltemp,
623+
pvsystem.PVSystem.noct_sam_celltemp])
575624
def test_PVSystem_multi_array_celltemp_temp_too_short(
576625
celltemp, two_array_system):
577626
with pytest.raises(ValueError,
@@ -583,7 +632,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_short(
583632
[pvsystem.PVSystem.faiman_celltemp,
584633
pvsystem.PVSystem.pvsyst_celltemp,
585634
pvsystem.PVSystem.sapm_celltemp,
586-
pvsystem.PVSystem.fuentes_celltemp])
635+
pvsystem.PVSystem.fuentes_celltemp,
636+
pvsystem.PVSystem.noct_sam_celltemp])
587637
def test_PVSystem_multi_array_celltemp_temp_too_long(
588638
celltemp, two_array_system):
589639
with pytest.raises(ValueError,
@@ -595,7 +645,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_long(
595645
[pvsystem.PVSystem.faiman_celltemp,
596646
pvsystem.PVSystem.pvsyst_celltemp,
597647
pvsystem.PVSystem.sapm_celltemp,
598-
pvsystem.PVSystem.fuentes_celltemp])
648+
pvsystem.PVSystem.fuentes_celltemp,
649+
pvsystem.PVSystem.noct_sam_celltemp])
599650
def test_PVSystem_multi_array_celltemp_wind_too_short(
600651
celltemp, two_array_system):
601652
with pytest.raises(ValueError,
@@ -607,7 +658,8 @@ def test_PVSystem_multi_array_celltemp_wind_too_short(
607658
[pvsystem.PVSystem.faiman_celltemp,
608659
pvsystem.PVSystem.pvsyst_celltemp,
609660
pvsystem.PVSystem.sapm_celltemp,
610-
pvsystem.PVSystem.fuentes_celltemp])
661+
pvsystem.PVSystem.fuentes_celltemp,
662+
pvsystem.PVSystem.noct_sam_celltemp])
611663
def test_PVSystem_multi_array_celltemp_wind_too_long(
612664
celltemp, two_array_system):
613665
with pytest.raises(ValueError,
@@ -618,8 +670,9 @@ def test_PVSystem_multi_array_celltemp_wind_too_long(
618670
@pytest.mark.parametrize("celltemp",
619671
[pvsystem.PVSystem.faiman_celltemp,
620672
pvsystem.PVSystem.pvsyst_celltemp,
673+
pvsystem.PVSystem.sapm_celltemp,
621674
pvsystem.PVSystem.fuentes_celltemp,
622-
pvsystem.PVSystem.sapm_celltemp])
675+
pvsystem.PVSystem.noct_sam_celltemp])
623676
def test_PVSystem_multi_array_celltemp_poa_length_mismatch(
624677
celltemp, two_array_system):
625678
with pytest.raises(ValueError,

pvlib/tests/test_temperature.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,12 @@ def test_noct_sam_options():
271271
poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
272272
0.2)
273273
effective_irradiance = 1100.
274-
transmittance_absorbtance = 0.8
274+
transmittance_absorptance = 0.8
275275
array_height = 2
276276
mount_standoff = 2.0
277277
result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
278278
eta_m_ref, effective_irradiance,
279-
transmittance_absorbtance, array_height,
279+
transmittance_absorptance, array_height,
280280
mount_standoff)
281281
expected = 60.477703576
282282
assert_allclose(result, expected)

0 commit comments

Comments
 (0)