From fe0162480fe6d8c4c7c3a3ab42243ea1f8e9a0b4 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:24:28 +0200 Subject: [PATCH 01/22] add new parameters, modify tests and warnings Added parameters to enable user to set min/max airmass Changed handling of values exceeding max pw to default to max pw rather than np.nan Replaced subjective phrasing such as "exceptionally high" with "high" and removed references to "divergence" in the warnings in order to align with the fact that the user can now determine their own limits, which may or may not be "exceptionally" high/low or cause model divergence Added tests for new AMa limit parameters, modified warnings and expected values in existing tests --- pvlib/spectrum/mismatch.py | 50 +++++++++++++++++++++--------------- pvlib/tests/test_spectrum.py | 19 +++++++++----- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 7c70a35360..fcd316bf5d 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -253,14 +253,16 @@ def integrate(e): def spectral_factor_firstsolar(precipitable_water, airmass_absolute, module_type=None, coefficients=None, min_precipitable_water=0.1, - max_precipitable_water=8): + max_precipitable_water=8, + min_airmass_absolute=0.58, + max_airmass_absolute=10): r""" Spectral mismatch modifier based on precipitable water and absolute (pressure-adjusted) air mass. Estimates a spectral mismatch modifier :math:`M` representing the effect on module short circuit current of variation in the spectral - irradiance. :math:`M` is estimated from absolute (pressure currected) air + irradiance. :math:`M` is estimated from absolute (pressure corrected) air mass, :math:`AM_a`, and precipitable water, :math:`Pw`, using the following function: @@ -325,12 +327,20 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, min_precipitable_water : float, default 0.1 minimum atmospheric precipitable water. Any ``precipitable_water`` value lower than ``min_precipitable_water`` - is set to ``min_precipitable_water`` to avoid model divergence. [cm] + is set to ``min_precipitable_water``. [cm] max_precipitable_water : float, default 8 maximum atmospheric precipitable water. Any ``precipitable_water`` value greater than ``max_precipitable_water`` - is set to ``np.nan`` to avoid model divergence. [cm] + is set to ``np.nan``. [cm] + + min_airmass_absolute : float, default 0.58 + minimum absolute airmass. Any ``airmass_absolute`` value lower than + ``min_airmass_absolute`` is set to ``min_airmass_absolute``. + + max_airmass_absolute : float, default 10 + minimum absolute airmass. Any ``airmass_absolute`` value greater than + ``max_airmass_absolute`` is set to ``max_airmass_absolute``. Returns ------- @@ -360,31 +370,31 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, # --- Screen Input Data --- # *** Pw *** - # Replace Pw Values below 0.1 cm with 0.1 cm to prevent model from - # diverging" + # Replace Pw Values below min_pw with min_pw" pw = np.atleast_1d(precipitable_water) pw = pw.astype('float64') if np.min(pw) < min_precipitable_water: pw = np.maximum(pw, min_precipitable_water) - warn('Exceptionally low pw values replaced with ' - f'{min_precipitable_water} cm to prevent model divergence') + warn('Low pw values replaced with 'f'{min_precipitable_water} cm in ' + 'the calculation of spectral mismatch') - # Warn user about Pw data that is exceptionally high if np.max(pw) > max_precipitable_water: - pw[pw > max_precipitable_water] = np.nan - warn('Exceptionally high pw values replaced by np.nan: ' - 'check input data.') + pw = np.minimum(pw, max_precipitable_water) + warn('High pw values replaced with 'f'{max_precipitable_water} cm in ' + 'the calculation of spectral mismatch') # *** AMa *** - # Replace Extremely High AM with AM 10 to prevent model divergence + # Replace Extremely High AM with max_am # AM > 10 will only occur very close to sunset - if np.max(airmass_absolute) > 10: - airmass_absolute = np.minimum(airmass_absolute, 10) - - # Warn user about AMa data that is exceptionally low - if np.min(airmass_absolute) < 0.58: - warn('Exceptionally low air mass: ' + - 'model not intended for extra-terrestrial use') + if np.max(airmass_absolute) > max_airmass_absolute: + airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) + warn('High AMa values replaced with 'f'{max_airmass_absolute} in the' + ' calculation of spectral mismatch') + + if np.min(airmass_absolute) < min_airmass_absolute: + airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) + warn('Low AMa values replaced with 'f'{min_airmass_absolute} in the' + ' calculation of spectral mismatch') # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000. diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 6b1dcd4506..ffbc60e7b6 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -225,35 +225,40 @@ def test_spectral_factor_firstsolar_ambiguous_both(): def test_spectral_factor_firstsolar_large_airmass(): - # test that airmass > 10 is treated same as airmass==10 + # test that airmass > 10 is treated same as airmass=10 m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, 'monosi') m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') assert_allclose(m_eq10, m_gt10) + with pytest.warns(UserWarning, match='High AMa values replaced with'): + _ = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') def test_spectral_factor_firstsolar_low_airmass(): - with pytest.warns(UserWarning, match='Exceptionally low air mass'): + m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, 'monosi') + m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') + assert_allclose(m_eq58, m_lt58) + with pytest.warns(UserWarning, match='Low AMa values replaced with'): _ = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') def test_spectral_factor_firstsolar_range(): - with pytest.warns(UserWarning, match='Exceptionally high pw values'): + with pytest.warns(UserWarning, match='High pw values replaced with'): out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), np.array([1, 3, 5]), module_type='monosi') - expected = np.array([0.96080878, 1.03055092, np.nan]) + expected = np.array([0.96080878, 1.03055092, 1.04932727]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Exceptionally high pw values'): + with pytest.warns(UserWarning, match='High pw values replaced with'): out = spectrum.spectral_factor_firstsolar(6, 1.5, max_precipitable_water=5, module_type='monosi') - with pytest.warns(UserWarning, match='Exceptionally low pw values'): + with pytest.warns(UserWarning, match='Low pw values replaced with'): out = spectrum.spectral_factor_firstsolar(np.array([0, 3, 8]), np.array([1, 3, 5]), module_type='monosi') expected = np.array([0.96080878, 1.03055092, 1.04932727]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Exceptionally low pw values'): + with pytest.warns(UserWarning, match='Low pw values replaced with'): out = spectrum.spectral_factor_firstsolar(0.2, 1.5, min_precipitable_water=1, module_type='monosi') From be7a79863e3f4b60b2cd09b6f3d4bf44cdaf2fee Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:37:02 +0100 Subject: [PATCH 02/22] out of limit input handling --- pvlib/spectrum/mismatch.py | 44 +++++++++++++++++++++--------------- pvlib/tests/test_spectrum.py | 2 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index fcd316bf5d..f41a4f4759 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -260,16 +260,10 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, Spectral mismatch modifier based on precipitable water and absolute (pressure-adjusted) air mass. - Estimates a spectral mismatch modifier :math:`M` representing the effect on - module short circuit current of variation in the spectral - irradiance. :math:`M` is estimated from absolute (pressure corrected) air - mass, :math:`AM_a`, and precipitable water, :math:`Pw`, using the following - function: - - .. math:: - - M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5} - + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}} + Estimates the spectral mismatch modifier, :math:`M`, representing the effect + of variation in the spectral irradiance on the module short circuit current + :math:`M` is estimated from absolute (pressure corrected) air mass, + :math:`AM_a`, and precipitable water, :math:`Pw` Default coefficients are determined for several cell types with known quantum efficiency curves, by using the Simple Model of the @@ -283,7 +277,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * spectrum simulated on a plane normal to the sun * All other parameters fixed at G173 standard - From these simulated spectra, M is calculated using the known + From these simulated spectra, :math:`M` is calculated using the known quantum efficiency curves. Multiple linear regression is then applied to fit Eq. 1 to determine the coefficients for each module. @@ -350,6 +344,20 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, effective irradiance, i.e., the irradiance that is converted to electrical current. + Notes + ---- + The ``spectral_factor_firstsolar`` model takes the following form: + + .. math:: + + M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5} + + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}}. + + The default values for the limits applied to :math::`AM_a` and :math::`Pw` + via the ``min_precipitable_water``, ``max_precipitable_water``, + ``min_airmass_absolute``, and ``max_airmass_absolute`` are set to prevent + divergence of the model presented above. + References ---------- .. [1] Gueymard, Christian. SMARTS2: a simple model of the atmospheric @@ -376,12 +384,12 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, if np.min(pw) < min_precipitable_water: pw = np.maximum(pw, min_precipitable_water) warn('Low pw values replaced with 'f'{min_precipitable_water} cm in ' - 'the calculation of spectral mismatch') + 'the calculation of spectral mismatch.') if np.max(pw) > max_precipitable_water: - pw = np.minimum(pw, max_precipitable_water) - warn('High pw values replaced with 'f'{max_precipitable_water} cm in ' - 'the calculation of spectral mismatch') + pw[pw > max_precipitable_water] = np.nan + warn('High pw values replaced with np.nan in ' + 'the calculation of spectral mismatch.') # *** AMa *** # Replace Extremely High AM with max_am @@ -389,12 +397,12 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, if np.max(airmass_absolute) > max_airmass_absolute: airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) warn('High AMa values replaced with 'f'{max_airmass_absolute} in the' - ' calculation of spectral mismatch') + ' calculation of spectral mismatch.') if np.min(airmass_absolute) < min_airmass_absolute: airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) warn('Low AMa values replaced with 'f'{min_airmass_absolute} in the' - ' calculation of spectral mismatch') + ' calculation of spectral mismatch.') # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000. @@ -486,7 +494,7 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, available here via the ``module_type`` parameter were determined by fitting the model equations to spectral factors calculated from global tilted spectral irradiance measurements taken in the city of - Jaén, Spain. See [1]_ for details. + Jaén, Spain. See [1]_ for details. Parameters ---------- diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index ffbc60e7b6..365e4ac7ee 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -246,7 +246,7 @@ def test_spectral_factor_firstsolar_range(): out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), np.array([1, 3, 5]), module_type='monosi') - expected = np.array([0.96080878, 1.03055092, 1.04932727]) + expected = np.array([0.96080878, 1.03055092, np.nan]) assert_allclose(out, expected, atol=1e-3) with pytest.warns(UserWarning, match='High pw values replaced with'): out = spectrum.spectral_factor_firstsolar(6, 1.5, From d6ef34b4fc40db4bf25d0ca9a2ea1ba8fa305291 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:38:14 +0100 Subject: [PATCH 03/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index f41a4f4759..af5a527e0a 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -260,10 +260,10 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, Spectral mismatch modifier based on precipitable water and absolute (pressure-adjusted) air mass. - Estimates the spectral mismatch modifier, :math:`M`, representing the effect - of variation in the spectral irradiance on the module short circuit current - :math:`M` is estimated from absolute (pressure corrected) air mass, - :math:`AM_a`, and precipitable water, :math:`Pw` + Estimates the spectral mismatch modifier, :math:`M`, representing the + effect of variation in the spectral irradiance on the module short circuit + current :math:`M` is estimated from absolute (pressure corrected) air + mass, :math:`AM_a`, and precipitable water, :math:`Pw` Default coefficients are determined for several cell types with known quantum efficiency curves, by using the Simple Model of the From 6b5f1b6a3141e1e34f0272461c5e992652b5c4c6 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:36:05 +0100 Subject: [PATCH 04/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index af5a527e0a..121ef19007 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -374,11 +374,6 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, MMF Approach, TUV Rheinland Energy GmbH report 21237296.003, January 2017 """ - - # --- Screen Input Data --- - - # *** Pw *** - # Replace Pw Values below min_pw with min_pw" pw = np.atleast_1d(precipitable_water) pw = pw.astype('float64') if np.min(pw) < min_precipitable_water: @@ -391,14 +386,6 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, warn('High pw values replaced with np.nan in ' 'the calculation of spectral mismatch.') - # *** AMa *** - # Replace Extremely High AM with max_am - # AM > 10 will only occur very close to sunset - if np.max(airmass_absolute) > max_airmass_absolute: - airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) - warn('High AMa values replaced with 'f'{max_airmass_absolute} in the' - ' calculation of spectral mismatch.') - if np.min(airmass_absolute) < min_airmass_absolute: airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) warn('Low AMa values replaced with 'f'{min_airmass_absolute} in the' @@ -432,7 +419,6 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, raise TypeError('Cannot resolve input, must supply only one of ' + 'module_type and coefficients') - # Evaluate Spectral Shift coeff = coefficients ama = airmass_absolute modifier = ( From 70c832a8a7ea3aa1dcafb1a20ba682d2108c59f7 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:44:18 +0100 Subject: [PATCH 05/22] Update test_spectrum.py --- pvlib/tests/test_spectrum.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 365e4ac7ee..b2f1a0de86 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -224,15 +224,6 @@ def test_spectral_factor_firstsolar_ambiguous_both(): spectrum.spectral_factor_firstsolar(1, 1, 'cdte', coefficients=coeffs) -def test_spectral_factor_firstsolar_large_airmass(): - # test that airmass > 10 is treated same as airmass=10 - m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, 'monosi') - m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') - assert_allclose(m_eq10, m_gt10) - with pytest.warns(UserWarning, match='High AMa values replaced with'): - _ = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') - - def test_spectral_factor_firstsolar_low_airmass(): m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, 'monosi') m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') @@ -242,10 +233,9 @@ def test_spectral_factor_firstsolar_low_airmass(): def test_spectral_factor_firstsolar_range(): - with pytest.warns(UserWarning, match='High pw values replaced with'): - out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), - np.array([1, 3, 5]), - module_type='monosi') + out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), + np.array([1, 3, 5]), + module_type='monosi') expected = np.array([0.96080878, 1.03055092, np.nan]) assert_allclose(out, expected, atol=1e-3) with pytest.warns(UserWarning, match='High pw values replaced with'): From ed4a9730bbdc2dd9c8b9eb7eec44563e0c2d9e5a Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:09:08 +0100 Subject: [PATCH 06/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 121ef19007..0990d402e5 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -386,6 +386,9 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, warn('High pw values replaced with np.nan in ' 'the calculation of spectral mismatch.') + if np.max(airmass_absolute) > max_airmass_absolute: + airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) + if np.min(airmass_absolute) < min_airmass_absolute: airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) warn('Low AMa values replaced with 'f'{min_airmass_absolute} in the' From 4157f7f75004419f8a7b380e87131afc4fdf18b6 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:43:09 +0100 Subject: [PATCH 07/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 0990d402e5..35e91fe025 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -262,8 +262,8 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, Estimates the spectral mismatch modifier, :math:`M`, representing the effect of variation in the spectral irradiance on the module short circuit - current :math:`M` is estimated from absolute (pressure corrected) air - mass, :math:`AM_a`, and precipitable water, :math:`Pw` + current :math:`M` is estimated from absolute (pressure-corrected) air + mass, :math:`AM_a`, and precipitable water, :math:`Pw`. Default coefficients are determined for several cell types with known quantum efficiency curves, by using the Simple Model of the @@ -279,10 +279,8 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, From these simulated spectra, :math:`M` is calculated using the known quantum efficiency curves. Multiple linear regression is then - applied to fit Eq. 1 to determine the coefficients for each module. - - Based on the PVLIB Matlab function ``pvl_FSspeccorr`` by Mitchell - Lee and Alex Panchula of First Solar, 2016 [2]_. + applied to fit Eq. 1 to determine the coefficients for each module. More + details on the model can be found in [2]_. Parameters ---------- @@ -290,7 +288,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, atmospheric precipitable water. [cm] airmass_absolute : numeric - absolute (pressure-adjusted) air mass. [unitless] + absolute (pressure-corrected) air mass. [unitless] module_type : str, optional a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi', From 16d266fed840ff6fd28513d17af3080929fb9459 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:06:15 +0100 Subject: [PATCH 08/22] Create v0.11.1.rst --- docs/sphinx/source/whatsnew/v0.11.1.rst | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/sphinx/source/whatsnew/v0.11.1.rst diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst new file mode 100644 index 0000000000..a3a5054172 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -0,0 +1,38 @@ +.. _whatsnew_01110: + + +v0.11.1 (Anticipated Sep, 2024) +------------------------------- + +Deprecations +~~~~~~~~~~~~ + + +Enhancements +~~~~~~~~~~~~ +* Add new losses function that accounts for non-uniform irradiance on bifacial + modules, :py:func:`pvlib.bifacial.power_mismatch_deline`. + (:issue:`2045`, :pull:`2046`) +* Add new parameters for min/max absolute air mass to + :py:func:`pvlib.spectrum.spectral_factor_firstsolar`. + +Bug fixes +~~~~~~~~~ + + +Testing +~~~~~~~ + + +Documentation +~~~~~~~~~~~~~ + + +Requirements +~~~~~~~~~~~~ + + +Contributors +~~~~~~~~~~~~ +* Echedey Luis (:ghuser:`echedey-ls`) +* Rajiv Daxini (:ghuser:`RDaxini`) From e373a70c22e79a6b49c2aea35b8c8883f247fb79 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:12:40 +0100 Subject: [PATCH 09/22] Update v0.11.1.rst --- docs/sphinx/source/whatsnew/v0.11.1.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 9460146fb4..519d1edfd1 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -15,6 +15,7 @@ Enhancements (:issue:`2045`, :pull:`2046`) * Add new parameters for min/max absolute air mass to :py:func:`pvlib.spectrum.spectral_factor_firstsolar`. + (:issue:`2086`, :pull:`2100`) Bug fixes From 3cb6c759ec95897d42a6609024c83fab6ac9432a Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 5 Jul 2024 21:28:25 +0100 Subject: [PATCH 10/22] Apply suggestions from code review adriesse + AdamRJensen review Co-authored-by: Anton Driesse Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/spectrum/mismatch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 1077059d6d..58d20ebb46 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -359,7 +359,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, module_type=None, coefficients=None, min_precipitable_water=0.1, max_precipitable_water=8, - min_airmass_absolute=0.58, + min_airmass_absolute=1.0, max_airmass_absolute=10): r""" Spectral mismatch modifier based on precipitable water and absolute @@ -393,7 +393,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, atmospheric precipitable water. [cm] airmass_absolute : numeric - absolute (pressure-corrected) air mass. [unitless] + absolute (pressure-adjusted) air mass. [unitless] module_type : str, optional a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi', @@ -433,11 +433,11 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, min_airmass_absolute : float, default 0.58 minimum absolute airmass. Any ``airmass_absolute`` value lower than - ``min_airmass_absolute`` is set to ``min_airmass_absolute``. + ``min_airmass_absolute`` is set to ``min_airmass_absolute``. [unitless] max_airmass_absolute : float, default 10 minimum absolute airmass. Any ``airmass_absolute`` value greater than - ``max_airmass_absolute`` is set to ``max_airmass_absolute``. + ``max_airmass_absolute`` is set to ``max_airmass_absolute``. [unitless] Returns ------- @@ -456,7 +456,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5} + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}}. - The default values for the limits applied to :math::`AM_a` and :math::`Pw` + The default values for the limits applied to :math:`AM_a` and :math:`Pw` via the ``min_precipitable_water``, ``max_precipitable_water``, ``min_airmass_absolute``, and ``max_airmass_absolute`` are set to prevent divergence of the model presented above. From ca6ddf6dacccda5cf47a5de16ca74343697dbcfb Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Sun, 7 Jul 2024 12:07:25 +0100 Subject: [PATCH 11/22] Update pvlib/spectrum/mismatch.py Co-authored-by: Anton Driesse --- pvlib/spectrum/mismatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 58d20ebb46..00496d18b4 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -379,7 +379,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * :math:`0.5 \textrm{cm} <= Pw <= 5 \textrm{cm}` * :math:`1.0 <= AM_a <= 5.0` * Spectral range is limited to that of CMP11 (280 nm to 2800 nm) - * spectrum simulated on a plane normal to the sun + * spectrum simulated on an equatorial facing surface with 37° tilt * All other parameters fixed at G173 standard From these simulated spectra, :math:`M` is calculated using the known From 755eb3d5ab2a5407fb294fc5cc1927e613624162 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:14:12 +0100 Subject: [PATCH 12/22] create --- pvlib/tests/test_spectrum.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 0ff496a880..1db38044ad 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -264,6 +264,22 @@ def test_spectral_factor_firstsolar_supplied(): assert_allclose(out, expected, atol=1e-3) +def test_spectral_factor_firstsolar_large_airmass_supplied_max(): + # test airmass > user-defined maximum is treated same as airmass=maximum + m_eq10 = spectrum.spectral_factor_firstsolar(1, 11, 'monosi', + max_airmass_absolute=11) + m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi', + max_airmass_absolute=11) + assert_allclose(m_eq10, m_gt10) + + +def test_spectral_factor_firstsolar_large_airmass(): + # test that airmass > 10 is treated same as airmass=10 + m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, 'monosi') + m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') + assert_allclose(m_eq10, m_gt10) + + def test_spectral_factor_firstsolar_ambiguous(): with pytest.raises(TypeError): spectrum.spectral_factor_firstsolar(1, 1) From 24ec55b6f5e349894470f36aa394d88e5148ddc9 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:23:04 +0100 Subject: [PATCH 13/22] Update test_spectrum.py --- pvlib/tests/test_spectrum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 1db38044ad..05edab7e9e 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -266,11 +266,11 @@ def test_spectral_factor_firstsolar_supplied(): def test_spectral_factor_firstsolar_large_airmass_supplied_max(): # test airmass > user-defined maximum is treated same as airmass=maximum - m_eq10 = spectrum.spectral_factor_firstsolar(1, 11, 'monosi', + m_eq11 = spectrum.spectral_factor_firstsolar(1, 11, 'monosi', max_airmass_absolute=11) - m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi', + m_gt11 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi', max_airmass_absolute=11) - assert_allclose(m_eq10, m_gt10) + assert_allclose(m_eq11, m_gt11) def test_spectral_factor_firstsolar_large_airmass(): From fc3e784d0551a21d7c1642fda18b5e17a845c9fb Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:19:27 +0100 Subject: [PATCH 14/22] module_type typesetting plus removed a few commas, capitalised a letter --- pvlib/spectrum/mismatch.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index eee1f911c2..2409f1b9b0 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -379,7 +379,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * :math:`0.5 \textrm{cm} <= Pw <= 5 \textrm{cm}` * :math:`1.0 <= AM_a <= 5.0` * Spectral range is limited to that of CMP11 (280 nm to 2800 nm) - * spectrum simulated on an equatorial facing surface with 37° tilt + * Spectrum simulated on an equatorial facing surface with 37° tilt * All other parameters fixed at G173 standard From these simulated spectra, :math:`M` is calculated using the known @@ -400,11 +400,11 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, 'multisi', and 'polysi' (can be lower or upper case). If provided, module_type selects default coefficients for the following modules: - * 'cdte' - First Solar Series 4-2 CdTe module. - * 'monosi', 'xsi' - First Solar TetraSun module. - * 'multisi', 'polysi' - anonymous multi-crystalline silicon module. - * 'cigs' - anonymous copper indium gallium selenide module. - * 'asi' - anonymous amorphous silicon module. + * ``'cdte'`` - First Solar Series 4-2 CdTe module. + * ``'monosi'``, 'xsi' - First Solar TetraSun module. + * ``'multisi'``, 'polysi' - anonymous multi-crystalline silicon module. + * ``'cigs'`` - anonymous copper indium gallium selenide module. + * ``'asi'`` - anonymous amorphous silicon module. The module used to calculate the spectral correction coefficients corresponds to the Multi-crystalline silicon @@ -645,8 +645,8 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, One of the following PV technology strings from [1]_: * ``'cdte'`` - anonymous CdTe module. - * ``'monosi'``, - anonymous sc-si module. - * ``'multisi'``, - anonymous mc-si- module. + * ``'monosi'`` - anonymous sc-si module. + * ``'multisi'`` - anonymous mc-si- module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. * ``'perovskite'`` - anonymous pervoskite module. @@ -760,8 +760,8 @@ def spectral_factor_pvspec(airmass_absolute, clearsky_index, * ``'fs4-1'`` - First Solar series 4-1 and earlier CdTe module. * ``'fs4-2'`` - First Solar 4-2 and later CdTe module. - * ``'monosi'``, - anonymous monocrystalline Si module. - * ``'multisi'``, - anonymous multicrystalline Si module. + * ``'monosi'`` - anonymous monocrystalline Si module. + * ``'multisi'`` - anonymous multicrystalline Si module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. From f39042911bda47ea0d646f305bb558c7d95182b3 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:10:22 +0100 Subject: [PATCH 15/22] Update pvlib/spectrum/mismatch.py Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/spectrum/mismatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 2409f1b9b0..2928368827 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -359,7 +359,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, module_type=None, coefficients=None, min_precipitable_water=0.1, max_precipitable_water=8, - min_airmass_absolute=1.0, + min_airmass_absolute=0.58, max_airmass_absolute=10): r""" Spectral mismatch modifier based on precipitable water and absolute From 51b31bc88c38daf62f9b16736fa55a23b051e915 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:39:18 +0100 Subject: [PATCH 16/22] Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/spectrum/mismatch.py | 7 +++---- pvlib/tests/test_spectrum.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 69b1f1ef6f..92f290dd14 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -489,13 +489,12 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, warn('High pw values replaced with np.nan in ' 'the calculation of spectral mismatch.') - if np.max(airmass_absolute) > max_airmass_absolute: - airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) + airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) if np.min(airmass_absolute) < min_airmass_absolute: airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) - warn('Low AMa values replaced with 'f'{min_airmass_absolute} in the' - ' calculation of spectral mismatch.') + warn('Low airmass values replaced with 'f'{min_airmass_absolute} in ' + 'the calculation of spectral mismatch.') # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000. diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 05edab7e9e..fd565b2298 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -296,7 +296,7 @@ def test_spectral_factor_firstsolar_low_airmass(): m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, 'monosi') m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') assert_allclose(m_eq58, m_lt58) - with pytest.warns(UserWarning, match='Low AMa values replaced with'): + with pytest.warns(UserWarning, match='Low airmass values replaced'): _ = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') From 341aafa80b1f51bda880439992467b572f77ae99 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:55:53 +0100 Subject: [PATCH 17/22] Update notes --- pvlib/spectrum/mismatch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 92f290dd14..e6717db183 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -459,7 +459,9 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, The default values for the limits applied to :math:`AM_a` and :math:`Pw` via the ``min_precipitable_water``, ``max_precipitable_water``, ``min_airmass_absolute``, and ``max_airmass_absolute`` are set to prevent - divergence of the model presented above. + divergence of the model presented above. These default values were + determined by the publication authors in the original pvlib-python + implementation (:pull:`208`). References ---------- @@ -481,12 +483,13 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, pw = pw.astype('float64') if np.min(pw) < min_precipitable_water: pw = np.maximum(pw, min_precipitable_water) - warn('Low pw values replaced with 'f'{min_precipitable_water} cm in ' - 'the calculation of spectral mismatch.') + warn('Low precipitable water values replaced with ' + f'{min_precipitable_water} cm in the calculation of spectral ' + 'mismatch.') if np.max(pw) > max_precipitable_water: pw[pw > max_precipitable_water] = np.nan - warn('High pw values replaced with np.nan in ' + warn('High preciptable water values replaced with np.nan in ' 'the calculation of spectral mismatch.') airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) From 5ee902ad6d5f0db8cfdd83283b9bb6e6fb0e1212 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:35:39 +0100 Subject: [PATCH 18/22] review comments pw -> precipitable water --- pvlib/spectrum/mismatch.py | 2 +- pvlib/tests/test_spectrum.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index e6717db183..e7a6cbcb81 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -489,7 +489,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, if np.max(pw) > max_precipitable_water: pw[pw > max_precipitable_water] = np.nan - warn('High preciptable water values replaced with np.nan in ' + warn('High precipitable water values replaced with np.nan in ' 'the calculation of spectral mismatch.') airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index fd565b2298..09b24866ea 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -306,17 +306,20 @@ def test_spectral_factor_firstsolar_range(): module_type='monosi') expected = np.array([0.96080878, 1.03055092, np.nan]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='High pw values replaced with'): + with pytest.warns(UserWarning, match='High precipitable water values ' + 'replaced'): out = spectrum.spectral_factor_firstsolar(6, 1.5, max_precipitable_water=5, module_type='monosi') - with pytest.warns(UserWarning, match='Low pw values replaced with'): + with pytest.warns(UserWarning, match='Low precipitable water values ' + 'replaced'): out = spectrum.spectral_factor_firstsolar(np.array([0, 3, 8]), np.array([1, 3, 5]), module_type='monosi') expected = np.array([0.96080878, 1.03055092, 1.04932727]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Low pw values replaced with'): + with pytest.warns(UserWarning, match='Low precipitable water values ' + 'replaced'): out = spectrum.spectral_factor_firstsolar(0.2, 1.5, min_precipitable_water=1, module_type='monosi') From 046c67dfd444de6a052b3e9cd2cc6fabcccd077b Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:21:52 +0100 Subject: [PATCH 19/22] Update pvlib/spectrum/mismatch.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/spectrum/mismatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index e7a6cbcb81..5a28cf2d5b 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -401,8 +401,8 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, module_type selects default coefficients for the following modules: * ``'cdte'`` - First Solar Series 4-2 CdTe module. - * ``'monosi'``, 'xsi' - First Solar TetraSun module. - * ``'multisi'``, 'polysi' - anonymous multi-crystalline silicon module. + * ``'monosi'``, ``'xsi'`` - First Solar TetraSun module. + * ``'multisi'``, ``'polysi'`` - anonymous multi-crystalline silicon module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. From 80fe9e82fe859cf14bce1c9797f68c2bfe695f7a Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:30:49 +0100 Subject: [PATCH 20/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 5a28cf2d5b..d355e208f3 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -402,7 +402,8 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * ``'cdte'`` - First Solar Series 4-2 CdTe module. * ``'monosi'``, ``'xsi'`` - First Solar TetraSun module. - * ``'multisi'``, ``'polysi'`` - anonymous multi-crystalline silicon module. + * ``'multisi'``, ``'polysi'`` - anonymous multi-crystalline silicon + module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. From 397d1dcf01c81f51d5a8c2c3fc0cac42e028a4e7 Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:34:35 +0100 Subject: [PATCH 21/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index d355e208f3..936b1d97b7 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -403,7 +403,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * ``'cdte'`` - First Solar Series 4-2 CdTe module. * ``'monosi'``, ``'xsi'`` - First Solar TetraSun module. * ``'multisi'``, ``'polysi'`` - anonymous multi-crystalline silicon - module. + module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. From a1807932d80a8fe3c2ec9db1edaa988e1b31202e Mon Sep 17 00:00:00 2001 From: RDaxini <143435106+RDaxini@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:45:16 +0100 Subject: [PATCH 22/22] Update mismatch.py --- pvlib/spectrum/mismatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 936b1d97b7..cab6084cac 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -499,7 +499,8 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) warn('Low airmass values replaced with 'f'{min_airmass_absolute} in ' 'the calculation of spectral mismatch.') - # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of + # pvlib.atmosphere.get_absolute_airmass(1, + # pvlib.atmosphere.alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000.