Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Add cache/memoization to PVcell properties (#121) #123

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
43 changes: 43 additions & 0 deletions pvmismatch/pvmismatch_lib/pvcell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -108,17 +143,20 @@ def update(self, **kwargs):
self._calc_now = True # recalculate

@property
@cached
def Vt(self):
"""
Thermal voltage in volts.
"""
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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -196,6 +238,7 @@ def _VocSTC(self):
)

@property
@cached
def Igen(self):
"""
Photovoltaic generated light current (AKA IL or Iph)
Expand Down
16 changes: 8 additions & 8 deletions pvmismatch/pvmismatch_lib/pvmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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!")
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand All @@ -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!")
Expand All @@ -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:
Expand All @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions pvmismatch/tests/test_pvcell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down