diff --git a/docs/sphinx/source/whatsnew/v0.16.2.rst b/docs/sphinx/source/whatsnew/v0.16.2.rst index 67e908862a..0d6f8c2bd4 100644 --- a/docs/sphinx/source/whatsnew/v0.16.2.rst +++ b/docs/sphinx/source/whatsnew/v0.16.2.rst @@ -14,6 +14,9 @@ Deprecations Bug fixes ~~~~~~~~~ +* :py:class:`pvlib.pvsystem.Array` now infers First Solar modules listed + as "Thin Film" in the CEC database as CdTe instead of CIGS; "Thin Film" + is otherwise mapped to CIGS. (:issue:`1984`) * Correct the Series name in ``ModelChain.results.dc_ohmic_losses`` from ``i_mp`` to ``p_dc_ohmic_loss`` when using ``dc_ohms_from_percent``. (:issue:`1959`, :pull:`2912`) @@ -75,6 +78,7 @@ Contributors * Rajiv Daxini (:ghuser:`rdaxini`) * jason-writer (:ghuser:`jason-writer`) * :ghuser:`mmporong` +* :ghuser:`DresdenGman` * :ghuser:`bfs2021` * :ghuser:`xiajiahang` * Jakub Krasuski (:ghuser:`Blizzeq`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 79d8a58d11..4d0f3c698a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1109,7 +1109,12 @@ def _infer_cell_type(self): if 'Technology' in self.module_parameters.keys(): # CEC module parameter set - cell_type = _cell_type_dict[self.module_parameters['Technology']] + technology = self.module_parameters['Technology'] + cell_type = _cell_type_dict[technology] + if technology == 'Thin Film' and self._is_first_solar_module(): + # First Solar modules labeled "Thin Film" are CdTe, + # but that technology maps to CIGS above + cell_type = 'cdte' elif 'Material' in self.module_parameters.keys(): # Sandia module parameter set cell_type = _cell_type_dict[self.module_parameters['Material']] @@ -1118,6 +1123,33 @@ def _infer_cell_type(self): return cell_type + def _is_first_solar_module(self): + """ + Whether the module is made by First Solar, based on the module + name. The name is taken from the ``module`` attribute or, if + ``module_parameters`` is a Series, from its name (e.g. the column + name when selected from a database retrieved with + :py:func:`retrieve_sam`). The normalized name must start with + the manufacturer's name, followed by a non-letter boundary, so + that names merely containing "First Solar" do not match. + + Returns + ------- + bool + + """ + names = [self.module, + getattr(self.module_parameters, 'name', None)] + for name in names: + if name is None: + continue + normalized = str(name).lower().replace('_', ' ').strip() + if normalized.startswith('first solar'): + rest = normalized[len('first solar'):] + if not rest or not rest[0].isalpha(): + return True + return False + def get_aoi(self, solar_zenith, solar_azimuth): """ Get the angle of incidence on the array. diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index 71a515ab03..69e6ee3d8f 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -844,6 +844,61 @@ def test_Array__infer_cell_type(): assert array._infer_cell_type() is None +@pytest.mark.parametrize('module, module_parameters, expected', [ + # First Solar modules labeled "Thin Film" are CdTe, but that + # technology is otherwise mapped to CIGS. GH 1984 + (None, pd.Series({'Technology': 'Thin Film'}, + name='First_Solar__Inc__FS_4117_3'), 'cdte'), + ('First Solar, Inc. FS 4117-3', {'Technology': 'Thin Film'}, 'cdte'), + # the name in one channel is enough, even if the other channel + # holds an unrelated name + ('Some_Other_Brand_Module', pd.Series( + {'Technology': 'Thin Film'}, + name='First_Solar__Inc__FS_4117_3'), 'cdte'), + # "Thin Film" modules from other manufacturers are still CIGS, as + # are "Thin Film" modules with no name to identify them by + (None, pd.Series({'Technology': 'Thin Film'}, + name='Auria_Solar_M115000'), 'cigs'), + (None, pd.Series({'Technology': 'Thin Film'}), 'cigs'), + (None, {'Technology': 'Thin Film'}, 'cigs'), + # names merely containing "First Solar" are not First Solar modules + (None, pd.Series({'Technology': 'Thin Film'}, + name='NotFirst_Solar_Model'), 'cigs'), + (None, pd.Series({'Technology': 'Thin Film'}, + name='First_Solaris_Model'), 'cigs'), + (None, pd.Series({'Technology': 'Thin Film'}, + name='OtherBrand_First_Solar_Compatible'), 'cigs'), + # First Solar's crystalline silicon (TetraSun) modules are not CdTe + (None, pd.Series({'Technology': 'Mono-c-Si'}, + name='First_Solar__Inc__TS_295_R11_02'), 'monosi'), + # the pre-existing mappings are unchanged + (None, {'Technology': 'CdTe'}, 'cdte'), + (None, {'Technology': 'Multi-c-Si'}, 'multisi'), + (None, {'Material': 'CdTe'}, 'cdte'), + (None, {'Material': 'mc-Si'}, 'multisi'), +]) +def test_Array__infer_cell_type_mappings(module, module_parameters, + expected): + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module=module, + module_parameters=module_parameters) + assert array._infer_cell_type() == expected + + +def test_Array__infer_cell_type_first_solar_cecmod(): + # GH 1984: First Solar modules labeled "Thin Film" in the CEC + # database are CdTe modules + cec_modules = pvsystem.retrieve_sam('cecmod') + module_parameters = cec_modules['First_Solar__Inc__FS_4117_3'] + # the override only applies to the "Thin Film" rows; assert the + # premise so a future correction of the database entry cannot + # silently bypass this test's coverage + assert module_parameters['Technology'] == 'Thin Film' + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters=module_parameters) + assert array._infer_cell_type() == 'cdte' + + def _calcparams_correct_Python_type_numeric_type_cases(): """ An auxilary function used in the unit tests named