From 662e1dc78468723158d3838cef90960c6b3d36ba Mon Sep 17 00:00:00 2001 From: DresdenGman Date: Tue, 3 Nov 2026 10:41:33 +0000 Subject: [PATCH] Fix First Solar CEC modules inferred as CIGS instead of CdTe The CEC database lists First Solar modules as "Thin Film", which Array._infer_cell_type maps to CIGS, so ModelChains using the First Solar spectral correction model pick the wrong parameter set for them. Infer "Thin Film" modules as CdTe when the module name identifies them as First Solar modules. First Solar's crystalline silicon (TetraSun) modules are unaffected: their "Mono-c-Si" technology still maps to monosi. --- docs/sphinx/source/whatsnew/v0.16.2.rst | 4 ++ pvlib/pvsystem.py | 34 ++++++++++++++- tests/test_pvsystem.py | 55 +++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) 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