From e8dbc331ce929062c6360e5ff718d2d720759817 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 31 Mar 2020 20:42:33 -0600 Subject: [PATCH 1/9] test out PVcell caching mechanism --- pvmismatch/pvmismatch_lib/pvcell.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index 5f6442b..c5398ac 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -27,6 +27,18 @@ ALPHA_ISC = 0.0003551 # [1/K] short circuit current temperature coefficient EPS = np.finfo(np.float64).eps + +def cached(f): + def wrapper(self): + key = f.__name__ + if key in self._cache: + return self._cache[key] + value = f(self) + self._cache[key] = value + return value + return wrapper + + class PVcell(object): """ Class for PV cells. @@ -49,6 +61,7 @@ class PVcell(object): """ _calc_now = False #: if True ``calcCells()`` is called in ``__setattr__`` + _cache = {} def __init__(self, Rs=RS, Rsh=RSH, Isat1_T0=ISAT1_T0, Isat2_T0=ISAT2_T0, Isc0_T0=ISC0_T0, aRBD=ARBD, bRBD=BRBD, VRBD=VRBD_, @@ -91,6 +104,7 @@ def __setattr__(self, key, value): pass # fail silently if not float, eg: pvconst or _calc_now super(PVcell, self).__setattr__(key, value) # recalculate IV curve + self._cache.clear() if self._calc_now: Icell, Vcell, Pcell = self.calcCell() self.__dict__.update(Icell=Icell, Vcell=Vcell, Pcell=Pcell) @@ -108,6 +122,7 @@ def update(self, **kwargs): self._calc_now = True # recalculate @property + @cached def Vt(self): """ Thermal voltage in volts. @@ -115,10 +130,12 @@ def Vt(self): return self.pvconst.k * self.Tcell / self.pvconst.q @property + @cached def Isc(self): return self.Ee * self.Isc0 @property + @cached def Aph(self): """ Photogenerated current coefficient, non-dimensional. @@ -134,6 +151,7 @@ def Aph(self): return 1. + (Idiode1_sc + Idiode2_sc + Ishunt_sc) / self.Isc @property + @cached def Isat1(self): """ Diode one saturation current at Tcell in amps. @@ -146,6 +164,7 @@ def Isat1(self): return self.Isat1_T0 * _Tstar * _expTstar # [A] Isat1(Tcell) @property + @cached def Isat2(self): """ Diode two saturation current at Tcell in amps. @@ -158,6 +177,7 @@ def Isat2(self): return self.Isat2_T0 * _Tstar * _expTstar # [A] Isat2(Tcell) @property + @cached def Isc0(self): """ Short circuit current at Tcell in amps. @@ -166,6 +186,7 @@ def Isc0(self): return self.Isc0_T0 * (1. + self.alpha_Isc * _delta_T) # [A] Isc0 @property + @cached def Voc(self): """ Estimate open circuit voltage of cells. @@ -196,6 +217,7 @@ def _VocSTC(self): ) @property + @cached def Igen(self): """ Photovoltaic generated light current (AKA IL or Iph) From e09db294403f66f868f8c21bbe39b1dc13fbd144 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 28 Apr 2020 20:54:23 -0600 Subject: [PATCH 2/9] make the cache dict an instance attribute instead of class attribute, oops --- pvmismatch/pvmismatch_lib/pvcell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index c5398ac..85865b2 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -61,12 +61,13 @@ class PVcell(object): """ _calc_now = False #: if True ``calcCells()`` is called in ``__setattr__`` - _cache = {} def __init__(self, Rs=RS, Rsh=RSH, Isat1_T0=ISAT1_T0, Isat2_T0=ISAT2_T0, Isc0_T0=ISC0_T0, aRBD=ARBD, bRBD=BRBD, VRBD=VRBD_, nRBD=NRBD, Eg=EG, alpha_Isc=ALPHA_ISC, Tcell=TCELL, Ee=1., pvconst=PVconstants()): + # set up property cache + self._cache = {} # user inputs self.Rs = Rs #: [ohm] series resistance self.Rsh = Rsh #: [ohm] shunt resistance From 4cb83f91ab341821d0097be4cbcce645c685dd33 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 28 Apr 2020 20:56:29 -0600 Subject: [PATCH 3/9] add cache test; update cached decorator to be mock friendly --- pvmismatch/pvmismatch_lib/pvcell.py | 14 ++++++++++++-- pvmismatch/tests/test_pvcell.py | 21 +++++++++++++++++++++ setup.py | 3 ++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index 85865b2..19587e3 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -11,6 +11,7 @@ import numpy as np from matplotlib import pyplot as plt from scipy.optimize import newton +import functools # Defaults RS = 0.004267236774264931 # [ohm] series resistance @@ -29,13 +30,22 @@ def cached(f): + """ + Memoize an object's method using the _cache dictionary on the object. + """ + @functools.wraps(f) def wrapper(self): - key = f.__name__ + # note: we use self.__wrapped__ instead of just using f directly + # so that we can spy on the original function in the test suite. + key = wrapper.__wrapped__.__name__ if key in self._cache: return self._cache[key] - value = f(self) + value = wrapper.__wrapped__(self) self._cache[key] = value return value + # store the original function to be accessible by the test suite. + # functools.wraps already sets this in python 3.2+, but for older versions: + wrapper.__wrapped__ = f return wrapper diff --git a/pvmismatch/tests/test_pvcell.py b/pvmismatch/tests/test_pvcell.py index 6ada80c..2626b65 100644 --- a/pvmismatch/tests/test_pvcell.py +++ b/pvmismatch/tests/test_pvcell.py @@ -107,6 +107,27 @@ def test_update(): assert pvc._calc_now +def test_cache(mocker): + """ + Test that the cache mechanism actually works and doesn't calculate the same + thing more than once. + """ + pvc = PVcell() + attrs = ['Vt', 'Isc', 'Aph', 'Isat1', 'Isat2', 'Isc0', 'Voc', 'Igen'] + # it's a little tricky to get ahold of the underlying function for the + # properties -- by accessing the property through the class rather than + # the instance, we get access to the `fget` object, which is the wrapper + # function. And the @cached decorator stores the underlying function + # in the __wrapped__ attribute on the wrapper. + spies = [ + mocker.spy(getattr(PVcell, attr).fget, "__wrapped__") + for attr in attrs + ] + pvc.Ee = 0.5 + for spy in spies: + spy.assert_called_once() + + if __name__ == "__main__": i, v = test_calc_series() iv_calc = np.concatenate([[i], [v]], axis=0).T diff --git a/setup.py b/setup.py index 35fb9ee..8686714 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ ] TESTS_REQUIRES = [ - 'nose>=1.3.7', 'pytest>=3.2.1', 'sympy>=1.1.1', 'pvlib>=0.5.1' + 'nose>=1.3.7', 'pytest>=3.2.1', 'sympy>=1.1.1', 'pvlib>=0.5.1', + 'pytest-mock' ] CLASSIFIERS = [ From 29f17ed9f74a288734c354ef46866abe71491f4b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 17:01:14 -0600 Subject: [PATCH 4/9] add PVcell.clone() method and test --- pvmismatch/pvmismatch_lib/pvcell.py | 9 ++++++++- pvmismatch/pvmismatch_lib/pvmodule.py | 16 ++++++++-------- pvmismatch/tests/test_pvcell.py | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index 19587e3..f6f6093 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -12,6 +12,7 @@ from matplotlib import pyplot as plt from scipy.optimize import newton import functools +import copy # Defaults RS = 0.004267236774264931 # [ohm] series resistance @@ -98,7 +99,8 @@ def __init__(self, Rs=RS, Rsh=RSH, Isat1_T0=ISAT1_T0, Isat2_T0=ISAT2_T0, self.Pcell = None #: cell power on IV curve [W] self.VocSTC = self._VocSTC() #: estimated Voc at STC [V] # set calculation flag - self._calc_now = True # overwrites the class attribute + super(PVcell, self).__setattr__('_calc_now', True) + #self._calc_now = True # overwrites the class attribute def __str__(self): fmt = '' @@ -120,6 +122,11 @@ def __setattr__(self, key, value): Icell, Vcell, Pcell = self.calcCell() self.__dict__.update(Icell=Icell, Vcell=Vcell, Pcell=Pcell) + def clone(self): + cloned = copy.copy(self) + super(PVcell, cloned).__setattr__('_cache', self._cache.copy()) + return cloned + def update(self, **kwargs): """ Update user-defined constants. diff --git a/pvmismatch/pvmismatch_lib/pvmodule.py b/pvmismatch/pvmismatch_lib/pvmodule.py index 67d47a0..324f1c3 100644 --- a/pvmismatch/pvmismatch_lib/pvmodule.py +++ b/pvmismatch/pvmismatch_lib/pvmodule.py @@ -303,7 +303,7 @@ def setSuns(self, Ee, cells=None): old_pvcells = dict.fromkeys(self.pvcells) # same as set(pvcells) for cell_id, pvcell in enumerate(self.pvcells): if old_pvcells[pvcell] is None: - new_pvcells[cell_id] = copy(pvcell) + new_pvcells[cell_id] = pvcell.clone() old_pvcells[pvcell] = new_pvcells[cell_id] else: new_pvcells[cell_id] = old_pvcells[pvcell] @@ -314,7 +314,7 @@ def setSuns(self, Ee, cells=None): elif np.size(Ee) == self.numberCells: self.pvcells = copy(self.pvcells) # copy list first for cell_idx, Ee_idx in enumerate(Ee): - self.pvcells[cell_idx] = copy(self.pvcells[cell_idx]) + self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Ee = Ee_idx else: raise Exception("Input irradiance value (Ee) for each cell!") @@ -326,7 +326,7 @@ def setSuns(self, Ee, cells=None): old_pvcells = dict.fromkeys(cells_to_update) for cell_id, pvcell in zip(cells, cells_to_update): if old_pvcells[pvcell] is None: - self.pvcells[cell_id] = copy(pvcell) + self.pvcells[cell_id] = pvcell.clone() self.pvcells[cell_id].Ee = Ee old_pvcells[pvcell] = self.pvcells[cell_id] else: @@ -344,7 +344,7 @@ def setSuns(self, Ee, cells=None): old_pvcells = dict.fromkeys(cells_to_update) for cell_id, pvcell in zip(cells_subset, cells_to_update): if old_pvcells[pvcell] is None: - self.pvcells[cell_id] = copy(pvcell) + self.pvcells[cell_id] = pvcell.clone() self.pvcells[cell_id].Ee = a_Ee old_pvcells[pvcell] = self.pvcells[cell_id] else: @@ -375,7 +375,7 @@ def setTemps(self, Tc, cells=None): old_pvcells = dict.fromkeys(self.pvcells) # same as set(pvcells) for cell_id, pvcell in enumerate(self.pvcells): if old_pvcells[pvcell] is None: - new_pvcells[cell_id] = copy(pvcell) + new_pvcells[cell_id] = pvcell.clone() old_pvcells[pvcell] = new_pvcells[cell_id] else: new_pvcells[cell_id] = old_pvcells[pvcell] @@ -386,7 +386,7 @@ def setTemps(self, Tc, cells=None): elif np.size(Tc) == self.numberCells: self.pvcells = copy(self.pvcells) # copy list first for cell_idx, Tc_idx in enumerate(Tc): - self.pvcells[cell_idx] = copy(self.pvcells[cell_idx]) + self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Tcell = Tc_idx else: raise Exception("Input temperature value (Tc) for each cell!") @@ -398,7 +398,7 @@ def setTemps(self, Tc, cells=None): old_pvcells = dict.fromkeys(cells_to_update) for cell_id, pvcell in zip(cells, cells_to_update): if old_pvcells[pvcell] is None: - self.pvcells[cell_id] = copy(pvcell) + self.pvcells[cell_id] = pvcell.clone() self.pvcells[cell_id].Tcell = Tc old_pvcells[pvcell] = self.pvcells[cell_id] else: @@ -416,7 +416,7 @@ def setTemps(self, Tc, cells=None): old_pvcells = dict.fromkeys(cells_to_update) for cell_id, pvcell in zip(cells_subset, cells_to_update): if old_pvcells[pvcell] is None: - self.pvcells[cell_id] = copy(pvcell) + self.pvcells[cell_id] = pvcell.clone() self.pvcells[cell_id].Tcell = a_Tc old_pvcells[pvcell] = self.pvcells[cell_id] else: diff --git a/pvmismatch/tests/test_pvcell.py b/pvmismatch/tests/test_pvcell.py index 2626b65..5da300a 100644 --- a/pvmismatch/tests/test_pvcell.py +++ b/pvmismatch/tests/test_pvcell.py @@ -128,6 +128,21 @@ def test_cache(mocker): spy.assert_called_once() +def test_clone(): + """ + Test that the clone method returns an independent object. + """ + # test independence + pvc1 = PVcell() + pvc2 = pvc1.clone() + pvc1.Ee = 0.5 + assert pvc2.Ee == 1.0 + + # test returns identical + pvc3 = pvc1.clone() + assert np.allclose(pvc1.calcCell(), pvc3.calcCell()) + + if __name__ == "__main__": i, v = test_calc_series() iv_calc = np.concatenate([[i], [v]], axis=0).T From c20497f6d1afb32d5ffc0dfe0d9dac85bb7aaba3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 17:11:29 -0600 Subject: [PATCH 5/9] missed some copies --- pvmismatch/pvmismatch_lib/pvmodule.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvmismatch/pvmismatch_lib/pvmodule.py b/pvmismatch/pvmismatch_lib/pvmodule.py index 324f1c3..613323b 100644 --- a/pvmismatch/pvmismatch_lib/pvmodule.py +++ b/pvmismatch/pvmismatch_lib/pvmodule.py @@ -8,7 +8,6 @@ from builtins import zip from six import itervalues import numpy as np -from copy import copy from matplotlib import pyplot as plt # use absolute imports instead of relative, so modules are portable from pvmismatch.pvmismatch_lib.pvconstants import PVconstants, get_series_cells @@ -312,7 +311,7 @@ def setSuns(self, Ee, cells=None): for pvc in pvcell_set: pvc.Ee = Ee elif np.size(Ee) == self.numberCells: - self.pvcells = copy(self.pvcells) # copy list first + self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first for cell_idx, Ee_idx in enumerate(Ee): self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Ee = Ee_idx @@ -320,7 +319,7 @@ def setSuns(self, Ee, cells=None): raise Exception("Input irradiance value (Ee) for each cell!") else: Ncells = np.size(cells) - self.pvcells = copy(self.pvcells) # copy list first + self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first if np.isscalar(Ee): cells_to_update = [self.pvcells[i] for i in cells] old_pvcells = dict.fromkeys(cells_to_update) @@ -384,7 +383,7 @@ def setTemps(self, Tc, cells=None): for pvc in pvcell_set: pvc.Tcell = Tc elif np.size(Tc) == self.numberCells: - self.pvcells = copy(self.pvcells) # copy list first + self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first for cell_idx, Tc_idx in enumerate(Tc): self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Tcell = Tc_idx @@ -392,7 +391,7 @@ def setTemps(self, Tc, cells=None): raise Exception("Input temperature value (Tc) for each cell!") else: Ncells = np.size(cells) - self.pvcells = copy(self.pvcells) # copy list first + self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first if np.isscalar(Tc): cells_to_update = [self.pvcells[i] for i in cells] old_pvcells = dict.fromkeys(cells_to_update) From f41fb4bb2f3ee7f4be75ff6b105e51a3edfb72fb Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 18:04:16 -0600 Subject: [PATCH 6/9] test clone keeps same pvconst --- pvmismatch/tests/test_pvcell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvmismatch/tests/test_pvcell.py b/pvmismatch/tests/test_pvcell.py index 5da300a..b2dd6f4 100644 --- a/pvmismatch/tests/test_pvcell.py +++ b/pvmismatch/tests/test_pvcell.py @@ -141,6 +141,7 @@ def test_clone(): # test returns identical pvc3 = pvc1.clone() assert np.allclose(pvc1.calcCell(), pvc3.calcCell()) + assert pvc1.pvconst is pvc3.pvconst if __name__ == "__main__": From 5029514d0a1e2e8b0f008642dd8c760676c6a7b5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 18:05:29 -0600 Subject: [PATCH 7/9] remove testing code, oops --- pvmismatch/pvmismatch_lib/pvcell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index f6f6093..91dcb48 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -100,7 +100,7 @@ def __init__(self, Rs=RS, Rsh=RSH, Isat1_T0=ISAT1_T0, Isat2_T0=ISAT2_T0, self.VocSTC = self._VocSTC() #: estimated Voc at STC [V] # set calculation flag super(PVcell, self).__setattr__('_calc_now', True) - #self._calc_now = True # overwrites the class attribute + self._calc_now = True # overwrites the class attribute def __str__(self): fmt = '' From 30199068673ae8424a14f9383d9f0662f03e6cae Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 18:05:48 -0600 Subject: [PATCH 8/9] Revert "missed some copies" This reverts commit c20497f6d1afb32d5ffc0dfe0d9dac85bb7aaba3. --- pvmismatch/pvmismatch_lib/pvmodule.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvmismatch/pvmismatch_lib/pvmodule.py b/pvmismatch/pvmismatch_lib/pvmodule.py index 613323b..324f1c3 100644 --- a/pvmismatch/pvmismatch_lib/pvmodule.py +++ b/pvmismatch/pvmismatch_lib/pvmodule.py @@ -8,6 +8,7 @@ from builtins import zip from six import itervalues import numpy as np +from copy import copy from matplotlib import pyplot as plt # use absolute imports instead of relative, so modules are portable from pvmismatch.pvmismatch_lib.pvconstants import PVconstants, get_series_cells @@ -311,7 +312,7 @@ def setSuns(self, Ee, cells=None): for pvc in pvcell_set: pvc.Ee = Ee elif np.size(Ee) == self.numberCells: - self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first + self.pvcells = copy(self.pvcells) # copy list first for cell_idx, Ee_idx in enumerate(Ee): self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Ee = Ee_idx @@ -319,7 +320,7 @@ def setSuns(self, Ee, cells=None): raise Exception("Input irradiance value (Ee) for each cell!") else: Ncells = np.size(cells) - self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first + self.pvcells = copy(self.pvcells) # copy list first if np.isscalar(Ee): cells_to_update = [self.pvcells[i] for i in cells] old_pvcells = dict.fromkeys(cells_to_update) @@ -383,7 +384,7 @@ def setTemps(self, Tc, cells=None): for pvc in pvcell_set: pvc.Tcell = Tc elif np.size(Tc) == self.numberCells: - self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first + self.pvcells = copy(self.pvcells) # copy list first for cell_idx, Tc_idx in enumerate(Tc): self.pvcells[cell_idx] = self.pvcells[cell_idx].clone() self.pvcells[cell_idx].Tcell = Tc_idx @@ -391,7 +392,7 @@ def setTemps(self, Tc, cells=None): raise Exception("Input temperature value (Tc) for each cell!") else: Ncells = np.size(cells) - self.pvcells = [cell.clone() for cell in self.pvcells] # copy list first + self.pvcells = copy(self.pvcells) # copy list first if np.isscalar(Tc): cells_to_update = [self.pvcells[i] for i in cells] old_pvcells = dict.fromkeys(cells_to_update) From 19b434576ecf5241a7e307d2ece9b5238bc6bcc0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 30 May 2020 18:08:56 -0600 Subject: [PATCH 9/9] clone docstring --- pvmismatch/pvmismatch_lib/pvcell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index 91dcb48..081ce69 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -123,6 +123,9 @@ def __setattr__(self, key, value): self.__dict__.update(Icell=Icell, Vcell=Vcell, Pcell=Pcell) def clone(self): + """ + Return a copy of this object with the same pvconst. + """ cloned = copy.copy(self) super(PVcell, cloned).__setattr__('_cache', self._cache.copy()) return cloned