diff --git a/pvmismatch/pvmismatch_lib/pvcell.py b/pvmismatch/pvmismatch_lib/pvcell.py index 5f6442b..081ce69 100644 --- a/pvmismatch/pvmismatch_lib/pvcell.py +++ b/pvmismatch/pvmismatch_lib/pvcell.py @@ -11,6 +11,8 @@ import numpy as np from matplotlib import pyplot as plt from scipy.optimize import newton +import functools +import copy # Defaults RS = 0.004267236774264931 # [ohm] series resistance @@ -27,6 +29,27 @@ ALPHA_ISC = 0.0003551 # [1/K] short circuit current temperature coefficient EPS = np.finfo(np.float64).eps + +def cached(f): + """ + Memoize an object's method using the _cache dictionary on the object. + """ + @functools.wraps(f) + def wrapper(self): + # 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 = 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 + + class PVcell(object): """ Class for PV cells. @@ -54,6 +77,8 @@ 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 @@ -74,6 +99,7 @@ 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 + super(PVcell, self).__setattr__('_calc_now', True) self._calc_now = True # overwrites the class attribute def __str__(self): @@ -91,10 +117,19 @@ 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) + 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 + def update(self, **kwargs): """ Update user-defined constants. @@ -108,6 +143,7 @@ def update(self, **kwargs): self._calc_now = True # recalculate @property + @cached def Vt(self): """ Thermal voltage in volts. @@ -115,10 +151,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 +172,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 +185,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 +198,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 +207,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 +238,7 @@ def _VocSTC(self): ) @property + @cached def Igen(self): """ Photovoltaic generated light current (AKA IL or Iph) 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 6ada80c..b2dd6f4 100644 --- a/pvmismatch/tests/test_pvcell.py +++ b/pvmismatch/tests/test_pvcell.py @@ -107,6 +107,43 @@ 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() + + +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()) + assert pvc1.pvconst is pvc3.pvconst + + 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 = [