# Measurements module
"""
.. inheritance-diagram:: pyopus.evaluator.measure
:parts: 1
**Performance measure extraction module**
All functions in this module do one of the following things:
* return an object of a Python numeric type (int, float, or complex)
* return an n-dimensional array (where n can be 0) of int, float or
complex type
* return None to indicate a failure
* raise an exception to indicate a more severe failure
All signals x(t) are defined in tabular form which means that a signal is
fully defined with two 1-dimensional arrays of values of the same
size. One array is the **scale** which represents the values of the scale (t)
while the other column represents the values of the **signal** (x) corresponding
to the scale points.
"""
from .. import PyOpusError
from ..misc.debug import DbgMsgOut
from numpy import array, floor, ceil, where, hstack, unique, abs, arctan, pi, NaN, log10
from scipy import angle, unwrap
# import matplotlib.pyplot as plt
__all__ = [
'debug',
'Deg2Rad', 'Rad2Deg', 'dB2gain', 'gain2dB', 'XatI', 'IatXval', 'filterI', 'XatIrange',
'dYdI', 'dYdX', 'integYdX', 'DCgain', 'DCswingAtGain', 'ACcircle', 'ACtf', 'ACmag',
'ACphase', 'ACgain' ,'ACbandwidth', 'ACugbw', 'ACphaseMargin', 'ACgainMargin',
'Tdelay', 'Tshoot', 'Tovershoot', 'Tundershoot', 'TedgeTime', 'TriseTime', 'TfallTime',
'TslewRate', 'TsettlingTime', 'Poverdrive'
]
#------------------------------------------------------------------------------
# Debugging
[docs]def debug(msg):
"""
Prints a debug message in the log.
"""
DbgMsgOut("MEAS", msg)
#------------------------------------------------------------------------------
# Conversions
[docs]def Deg2Rad(degrees):
"""
Converts degrees to radians.
"""
return degrees/180.0*pi
[docs]def Rad2Deg(radians):
"""
Converts radians to degrees.
"""
return radians*180.0/pi
[docs]def dB2gain(db, unit='db20'):
"""
Converts gain magnitude in decibels to gain as a factor.
*unit* is the type of decibels to convert from:
* ``db`` and ``db20`` are equivalent and should be used when the conversion
of voltage/current gain decibels is required (20dB = gain factor of 10.0).
* ``db10`` should be used for power gain decibels conversion
(10dB = gain factor of 10.0)
"""
if unit=='db':
return 10.0**(db/20.0)
elif unit=='db20':
return 10.0**(db/20.0)
elif unit=='db10':
return 10.0**(db/10.0)
else:
raise PyOpusError("Bad magnitude unit.")
[docs]def gain2dB(x, unit='db20'):
"""
Converts gain as a factor to magnitude in decibels.
*unit* is the type of decibels to convert to:
* ``db`` and ``db20`` are equivalent and should be used when the conversion
to voltage/current gain decibels is required (gain factor of 10.0 = 20dB).
* ``db10`` should be used for conversion to power gain decibels conversion
(gain factor of 10.0 = 10dB)
"""
if unit=='db':
return 20.0*log10(abs(x))
elif unit=='db20':
return 20.0*log10(abs(x))
elif unit=='db10':
return 10.0*log10(abs(x))
else:
raise PyOpusError("Bad magnitude unit.")
#------------------------------------------------------------------------------
# Fractional indexing and cursors
[docs]def XatI(x, i):
"""
Returns the value in 1-dimensional array *x* corresponding to fractional
index *i*. This operation is equivalent to a table lookup with linear
interpolation where the first column of the table represents the index (*i)
and the second column holds the components of *x*.
If *i* is a 1-dimensional array the return value is an array of the same
shape holding the results of table lookups corresponding to fractional
indices in array *i*.
*i* must satisfy 0 <= *i* <= x.size-1.
"""
xLen=x.size
xa=array(x)
ia=array(i)
if ia.size==0:
return array([])
if array(i<0).any() or array(i>xLen-1).any():
raise PyOpusError("Index out of range.")
# Interpolate
i1=floor(ia).astype(int)
i2=ceil(ia).astype(int)
frac=ia-i1
xa1=xa[i1]
xa2=xa[i2]
return xa1+(xa2-xa1)*frac
[docs]def IatXval(x, val, slope='any'):
"""
Returns a 1-dimensional array of fractional indices corresponding places in
vector *x* where the linearly interpolated value is equal to *val*. These
are the crossings of function f(i)=x(i) with g(i)=val. *slope* specifies
what kind of crossings are returned:
* ``any`` - return all crossings
* ``rising`` - return only crossings where the slope of f(i) is
positive or zero
* ``falling`` - return only crossings where the slope of f(i) is
negative or zero
This function corresponds to a reverse table lookup with linear
interpolation where the first folumn of the table contains the index and
the second column contains the corresponding values of *x*. The reverse
lookup finds the fractional indices coresponding to *val* in the second
column of the table.
There can be no crossings (empty return array) or more than one crossing
(return array size>1).
The fractional indices are returned in an increasing sequence.
"""
xa=array(x)
val=array(val)
if val.size!=1:
raise PyOpusError("Value must be a scalar.")
if (val<xa.min()) or (val>xa.max()):
return array([], float)
# Detect level corssing
belowOrEqual=(xa<=val)
aboveOrEqual=(xa>=val)
# Detect edges
risingEdge=belowOrEqual[:-1] & aboveOrEqual[1:]
fallingEdge=aboveOrEqual[:-1] & belowOrEqual[1:]
anyEdge=risingEdge | fallingEdge
if slope=='rising':
edge=risingEdge
elif slope=='falling':
edge=fallingEdge
elif slope=='any':
edge=anyEdge
else:
raise PyOpusError("Bad edge type.")
# Get candidate intervals
candidates=where(edge)[0]
# Prepare interval edges and deltas
i1=candidates
i2=candidates+array(1)
x1=xa[i1]
x2=xa[i2]
dx=x2-x1
# Zero delta interval indices
zeroDeltaI=where(dx==0)[0]
nonzeroDeltaI=where(dx!=0)[0]
# Handle zero delta intervals
ii=hstack((i1[zeroDeltaI], i2[zeroDeltaI]))
# Handle nonzero delta intervals
ii=hstack((ii, 1.0/dx[nonzeroDeltaI]*(val-x1[nonzeroDeltaI])+i1[nonzeroDeltaI]))
return unique(ii)
[docs]def filterI(i, direction='right', start=None, includeStart=False):
"""
Returns a 1-dimensional array of fractional indices obtained by processing
fractional indices given in *i*.
If *direction* is ``right`` *i* is traversed from lower to higher indices.
Only the indices from *i* that are greater than *start* are included in the
return value.
If *direction* is ``left`` *i* is traversed from higher to lower indices.
Only the indices from *i* that are less than *start* are included in the
return value.
The filtered indices are returned in the same order as they appear in *i*.
If *includeStart* is ``True`` the *greater than* and *less than* comparison
operators are replaced by *greater than or equal to* and *less than or
equal to*. This includes indices which are equal to *start* in the return
value.
If *start* is not given, it defaults to ``i[0]`` if *direction* is
``right`` and ``i[-1]`` if *direction* is ``left``.
"""
if start is None:
if direction=='right':
if i.size>0:
start=i[0]
else:
start=0
elif direction=='left':
if i.size>0:
start=i[-1]
else:
start=0
else:
raise PyOpusError("Bad direction.")
if direction=='right':
if includeStart:
selector=i>=start
else:
selector=i>start
elif direction=='left':
if includeStart:
selector=i<=start
else:
selector=i<start
else:
raise PyOpusError("Bad direction.")
return i[where(selector)[0]]
[docs]def XatIrange(x, i1, i2=None):
"""
Returns a subvector (1-dimensional array) of a vector given by
1-dimensional array *x*. The endpoints of the subvector correspond to
fractional indices *i1* and *i2*.
If *i2* is not given the return value is the same as the return value of
``XatI(x, i1)``.
*i1* and *i2* must satisfy
* 0 <= *i1* <= x.size-1
* 0 <= *i2* <= x.size-1
* *i1* <= *i2*
If the endpoints do not correspond to integer indices the subvector
endpoints are obtained with linear interpolation (see :func:`XatI`
function).
"""
xLen=x.size
if (i1<0) or (i1>xLen-1):
raise PyOpusError("Bad fractional index (i1).")
if i2 is None:
return XatI(x, i1)
if (i2<i1) or (i2<0) or (i2>xLen-1):
raise PyOpusError("Bad fractional index range.")
if i1==i2:
return XatI(x, i1)
# Get integer subrange
ilo=ceil(i1).astype(int)
ihi=floor(i2).astype(int)
# Get core vector for the integer subrange
if ilo<=ihi:
coreVector=x[ilo:(ihi+1)]
else:
coreVector=[]
# Construct result
if i1!=ilo:
retval=XatI(x, i1)
else:
retval=[]
retval=hstack((retval, coreVector))
if i2!=ihi:
retval=hstack((retval, XatI(x, i2)))
# Done
return retval
#------------------------------------------------------------------------------
# Calculus
# Derivative wrt integer index
[docs]def dYdI(y):
"""
Returns the derivative of 1-dimensional vector *y* with respect to its
index.
Uses 2nd order polynomial interpolation before the actual derivative is
calculated.
"""
# Interpolating polynomial of 2nd order
# indices used for
# 0, 1, 2 0 and 1
# 1, 2, 3 2
# 2, 3, 4 3
# ... ...
# n-4, n-3, n-2 n-3
# n-3, n-2, n-1 n-2 and n-1
# y=a x^2 + b x + c
# dy/dx = 2 a x + b
# There are n-2 interpolating polynomials, get their coefficients
yminus=array(y[:-2])
y0=array(y[1:-1])
yplus=array(y[2:])
c=y0
b=(yplus-yminus)/2.0
a=(yplus+yminus-2.0*y0)/2.0
# Now differentiate polynomial
c=b
b=2.0*a
a=0
# Generate edge points
# For i=0 (effective index x=-1)
dylo=-b[0]+c[0]
# For i=n-1 (effective index x=1)
dyhi=b[-1]+c[-1]
# For everything else (effective index x=0)
coreVector=c
# Done
return hstack((dylo, coreVector, dyhi))
[docs]def dYdX(y, x):
"""
Derivative of a 1-dimensional vector *y* with respect to the 1-dimensional
vector *x*. The arrays representing *y* and *x* must be of the same size.
"""
n=array(y).shape[0]
if n>2:
yminus=array(y[:-2])
y0=array(y[1:-1])
yplus=array(y[2:])
xminus=array(x[:-2])
x0=array(x[1:-1])
xplus=array(x[2:])
# Differences
dyminus=yminus-y0
dyplus=yplus-y0
dxminus=xminus-x0
dxplus=xplus-x0
# Interpolating polynomial
a=(dyplus/dxplus-dyminus/dxminus)/(dxplus-dxminus)
b=dyminus/dxminus-a*dxminus
# c=0
# Derivative polynomial
c=b
b=2*a
a=0
# Generate edge points
# For i=0 (effective index x=-1)
# dlo=-b[0]+c[0] # interpolated value at edge
dlo=dyminus[0]/dxminus[0] # Simple difference
# For i=n-1 (effective index x=1)
# dhi=b[-1]+c[-1] # interpolated value at edge
dhi=dyplus[-1]/dxplus[-1] # Simple difference
# For everything else (effective index x=0)
coreVector=c
# Done
return hstack((dlo, coreVector, dhi))
elif n>1:
y=array(y)
x=array(x)
dy=y[-1]-y[0]
dx=x[-1]-x[0]
d=dy/dx
return hstack((d, d))
else:
raise PyOpusError("Vector too short for computing the derivative.")
# Integrate vector wrt scale
# Returns an array of values. Each value is an integral from the beginning to belonging x component.
[docs]def integYdX(y, x):
"""
Integral of a 1-dimensional vector *x* with respect to its scale given by a
1-dimensional vector *x*. The arrays representing *y* and *x* must be of
the same size.
Uses 2nd order polynomial interpolation before the actual integral is
calculated.
The lower limit for integration is ``x[0]`` while the pints in *x* define
the upper limits. This means that the first point of the result (the one
corresponding to ``x[0]``) is 0.
"""
# Interpolating polynomial of 2nd order
# indices used for
# 0, 1, 2 0 and 1
# 1, 2, 3 2
# 2, 3, 4 3
# ... ...
# n-4, n-3, n-2 n-3
# n-3, n-2, n-1 n-2 and n-1
# y=a x^2 + b x + c
# dy/dx = 2 a x + b
# There are n-2 interpolating polynomials, get their coefficients
hminus=array(x[:-2]-x[1:-1])
hplus=array(x[2:]-x[1:-1])
yminus=array(y[:-2])
y0=array(y[1:-1])
yplus=array(y[2:])
c=y0
a=(yminus*hplus-yplus*hminus-c*(hplus-hminus))/(hplus*hminus*(hminus-hplus))
b=(yminus-c-a*hminus*hminus)/hminus
# Integrate polynomial (resulting in a x^3 + b x^2 + c x + C)
# Constant C is ignored.
a=a/3.0
b=b/2.0
# Calculate integral for last interval based on last interpolation
# (corresponding to 0..hplus)
ydxLast=a[-1]*(hplus[-1]**3)+b[-1]*(hplus[-1]**2)+c[-1]*hplus[-1]
# Calculate integral for first interval based on first interpolation
# (corresponding to hminus..0)
ydxFirst=-(a[0]*(hminus[0]**3)+b[0]*(hminus[0]**2)+c[0]*hminus[0])
# Calculate core integral - leading part
# values of integral for i..i+1 (corresponding to hminus..0)
coreVectorLeading=-(a*(hminus**3)+b*(hminus**2)+c*hminus)
# Calculate core integral - trailing part
# values of integral for i..i+1 (corresponding to 0..hplus)
coreVectorTrailing=a*(hplus**3)+b*(hplus**2)+c*hplus
# With zero, leading core vector, and ydxLast do a cumulative sum
integLeading=hstack((array(0.0), coreVectorLeading, ydxLast)).cumsum()
# With zero, ydxFirst, and trailing core vector do a cumulative sum
integTrailing=hstack((array(0.0), ydxFirst, coreVectorTrailing)).cumsum()
# Done
return (integLeading+integTrailing)/2.0
#------------------------------------------------------------------------------
# DC measurements
[docs]def DCgain(output, input):
"""
Returns the maximal gain (slope) of a nonlinear transfer function
*output(input*).
*output* and *input* are 1-dimensional arrays of the same size.
"""
# Get gain
A=abs(dYdX(output, input))
# Return maximum
return A.max()
[docs]def DCswingAtGain(output, input, relLevel, type='out'):
"""
Returns the *input* or *output* interval corresponding to the range where
the gain (slope) of *output(input)* is above *relLevel* times maximal
slope. Only *rellevel* < 1 makes sense in this measurement.
*type* specifies what to return
* ``out`` - return the *output* values interval
* ``in`` - return the *input* values interval
*relLevel* must satisfy 0 <= *relLevel* <= 1.
"""
# Check
if (relLevel<=0) or (relLevel>=1):
raise PyOpusError("Bad relative level.")
# Get gain (absolute)
A=abs(dYdX(output, input))
# Get maximum and level
Amax=A.max()
Alev=Amax*relLevel
# Find maximum
maxI=IatXval(A, Amax)
# Find crossings
crossI=IatXval(A, Alev)
if crossI.size<=0:
raise PyOpusError("No crossings with specified level found.")
# Extract crossings to left and to right
Ileft=filterI(crossI, 'left', maxI.min())
Iright=filterI(crossI, 'right', maxI.max())
if Ileft.size<=0:
raise PyOpusError("No crossing to the left from the maximum found.")
if Iright.size<=0:
raise PyOpusError("No crossing to the right from the maximum found.")
# max(), min() will raise an exception if no crossing is found
i1=Ileft.max()
i2=Iright.min()
# Get corresponding range
if type=='out':
vec=output
elif type=='in':
vec=input
else:
raise PyOpusError("Bad output type.")
return abs(XatI(vec, i2)-XatI(vec, i1))
#------------------------------------------------------------------------------
# AC measurements
[docs]def ACcircle(unit='deg'):
"""
Returns the full circle in units specified by *unit*
* ``deg`` - return 360
* ``rad`` - return 2*``pi``
"""
if unit=='deg':
return 360
elif unit=='rad':
return 2*pi
else:
raise PyOpusError("Bad angle unit.")
[docs]def ACtf(output, input):
"""
Return the transfer function *output/input* where *output* and *input* are
complex vectors of the same size representing the systems response at
various frequencies.
"""
return array(output)/array(input)
[docs]def ACmag(tf, unit='db'):
"""
Return the magnitude in desired *unit* of a small signal tranfer function
*tf*.
* ``db`` and ``db20`` stand for voltage/current gain decibels where
20dB = gain factor of 10.0
* ``db10`` stands for voltage/current gain decibels where
10dB = gain factor of 10.0
* ``abs`` stands for gain factor
"""
mag=abs(array(tf))
if (unit=='db') or (unit=='db20'):
return 20*log10(mag)
elif unit=='db10':
return 10*log10(mag)
elif unit=='abs':
return mag
else:
raise PyOpusError("Bad magnitude unit.")
[docs]def ACphase(tf, unit='deg', unwrapTol=0.5):
"""
Return the phase in desired *unit* of a transfer function *tf*
* ``deg`` stands for degrees
* ``rad`` stands for radians
The phase is unwrapped (discontinuities are stiched together to make it
continuous). The tolerance of the unwrapping (in radians) is
*unwrapTol* times ``pi``.
"""
# Get argument
ph=angle(tf)
# Unwrap if requested
if (unwrapTol>0) and (unwrapTol<1):
ph=unwrap(ph, unwrapTol*pi)
# Convert to requested unit
if unit=='deg':
return ph/pi*180.0
elif unit=='rad':
return ph
else:
raise PyOpusError("Bad phase unit.")
[docs]def ACgain(tf, unit='db'):
"""
Returns the maximal gain magnitude of a transfer function in units given
by *unit*
* ``db`` and ``db20`` stand for voltage/current gain decibels where
20dB = gain factor of 10.0
* ``db10`` stands for power gain decibels where
10dB = gain factor of 10.0
* ``abs`` stands for gain factor
"""
mag=ACmag(tf, unit)
return mag.max()
[docs]def ACbandwidth(tf, scl, filter='lp', levelType='db', level=-3.0):
"""
Return the bandwidth of a transfer function *tf* on frequency scale *scl*.
*tf* and *scl* must be 1-dimensional arrays of the same size.
The type of the transfer function is given by *filter* where
* ``lp`` stands for low-pass (return frequency at *level*)
* ``hp`` stands for high-pass (return frequency at *level*)
* ``bp`` stands for band-pass (return bandwidth at *level*)
*levelType* gives the units for the *level* argument. Allowed values for
*levelType* are
* ``db`` and ``db20`` stand for voltage/current gain decibels where
20dB = gain factor of 10.0
* ``db10`` stands for power gain decibels where
10dB = gain factor of 10.0
* ``abs`` stands for gain factor
*level* specifies the level at which the bandwidth should be measured. For
``db``, ``db10``, and ``db20`` *levelType* the level is relative to the
maximal gain and is added to the maximal gain. For ``abs`` *levelType* the
level is a factor with which the maximal gain factor must be multiplied to
obtain the gain factor level at which the bandwidth should be measured.
"""
# Magnitude
mag=ACmag(tf, levelType)
# Reference level
ref=mag.max()
# Crossing level
if levelType=='abs':
cross=ref*level
else:
cross=ref+level
# Find crossings
crossI=IatXval(mag, cross)
# Find reference position
crossMaxI=IatXval(mag, ref).min()
if crossI.size<=0:
raise PyOpusError("No crossings with specified level found.")
# Make scale real
scl=abs(scl)
# Handle filter type
if filter=='lp':
# Get first crossing to the right of the reference position
# min() will raise an exception if no crossing is found
bwI=filterI(crossI, 'right', crossMaxI)
if bwI.size<=0:
raise PyOpusError("No crossing to the right from the maximum found.")
bwI=bwI.min()
bw=XatI(scl, bwI)
elif filter=='hp':
# Get first crossing to the left of the reference position
# max() will raise an exception if no crossing is found
bwI=filterI(crossI, 'left', crossMaxI).max()
if bwI.size<=0:
raise PyOpusError("No crossing to the left from the maximum found.")
bwI=bwI.max()
bw=XatI(scl, bwI)
elif filter=='bp':
# Get first crossing to the left and the right of the reference position
# max(), min() will raise an exception if no crossing is found
bwI1=filterI(crossI, 'left', crossMaxI).max()
bwI2=filterI(crossI, 'right', crossMaxI).min()
if bwI1.size<=0:
raise PyOpusError("No crossing to the left from the maximum found.")
if bwI2.size<=0:
raise PyOpusError("No crossing to the right from the maximum found.")
bwI1=bwI1
bwI2=bwI2
bw=XatI(scl, bwI2)-XatI(scl, bwI1)
else:
raise PyOpusError("Bad filter type.")
return bw
[docs]def ACugbw(tf, scl):
"""
Returns the uniti-gain bandwidth of a transfer function *tf* on frequency
scale *scl*. 1-dimensional arrays *tf* and *scl* must be of the same size.
The return value is the frequency at which the transfer function
reaches 1.0 (0dB).
"""
# Magnitude
mag=ACmag(tf, 'db')
# Make scale real
scl=abs(scl)
# Find 0dB magnitude
# min() will raise an exception if no crossing is found
crossI=IatXval(mag, 0.0)
if crossI.size<=0:
raise PyOpusError("No crossing with 0dB level found.")
crossI=crossI.min()
# Calculate ugbw
ugbw=XatI(scl, crossI)
return ugbw
[docs]def ACphaseMargin(tf, unit='deg', unwrapTol=0.5):
"""
Returns the phase margin of a transfer function given by 1-dimensional
array *tf*. Uses *unwrapTol* as the unwrap tolerance for phase
(see :func:`ACphase`). The phase margin is returned in units given by
*unit* where
* ``deg`` stands for degrees
* ``rad`` stands for radians
The phase margin (in degrees) is the amount the phase at the point where
the transfer function magnitude reaches 0dB should be decreased to become
equal to -180.
For stable systems the phase margin is >0.
"""
# Magnitude
mag=ACmag(tf, 'db')
# Phase
ph=ACphase(tf, unit, unwrapTol)
# Find 0dB magnitude
crossI=IatXval(mag, 0.0)
if crossI.size<=0:
raise PyOpusError("No crossing with 0dB level found.")
crossI=crossI.min()
# Calculate phase at 0dB
ph0=XatI(ph, crossI)
# Return phase margin
pm=ph0+ACcircle(unit)/2
return pm
# Gain margin of a tf
[docs]def ACgainMargin(tf, unit='db', unwrapTol=0.5):
"""
Returns the gain margin of a transfer function given by 1-dimensional array
*tf*. Uses *unwrapTol* as the unwrap tolerance for phase
(see :func:`ACphase`). The gain margin is returned in units given by *unit*
where
* ``db`` and ``db20`` stand for voltage/current gain decibels where
20dB = gain factor of 10.0
* ``db10`` stands for power gain decibels where
10dB = gain factor of 10.0
* ``abs`` stands for gain factor
The phase margin (in voltage/current gain decibels) is the amount the gain
at the point where phase reaches -180 degrees should be increased to become
equal to 0.
For stable systems the gain margin is >0.
"""
# Magnitude
mag=ACmag(tf, 'abs')
# Phase
ph=ACphase(tf, 'deg', unwrapTol)
# Find -180deg in phase
crossI=IatXval(ph, -180.0)
if crossI.size<=0:
raise PyOpusError("No crossing with -180 degrees level found.")
crossI=crossI.min()
# Get gain at -180 degrees
mag180=XatI(mag, crossI)
# Gain margin in absolute units
gm=1.0/mag180
# Return selected units
return ACmag(gm, unit)
#------------------------------------------------------------------------------
# Transient measurements
def _refLevel(sig, scl, t1=None, t2=None):
"""
In signal *sig* with scale *scl* looks up the points where scale is equal
to *t1* and *t2*. The default values of *t1* and *t2* are the first and the
last value in *scl.
Returns a tuple (i1, s1, i2, s2) where i1 and i2 represent the fractional
indices of the two points in signal (or scale) while s1 and s2 represent
the values of the signal at those two points.
"""
# Get interesting part in terms of indices
if t1 is None:
i1=0
else:
i1=IatXval(scl, t1, 'rising')
if i1.size<=0:
raise PyOpusError("Start point not found.")
i1=i1[0]
if t2 is None:
i2=scl.size-1
else:
i2=IatXval(scl, t2, 'rising')
if i2.size<=0:
raise PyOpusError("End point not found.")
i2=i2[0]
if i1>=i2:
raise PyOpusError("Start point after end point.")
# Get reference levels
s1=XatI(sig, i1)
s2=XatI(sig, i2)
return (i1, s1, i2, s2)
[docs]def Tdelay(sig1, sig2, scl,
lev1type='rel', lev1=0.5, edge1='any', skip1=0,
lev2type='rel', lev2=0.5, edge2='any', skip2=0,
t1=None, t2=None):
"""
Calculates the delay of signal *sig2* with respect to signal *sig1*. Both
signals share a common scale *scl*. The delay is the difference in scale
between the point where *sig2* reaches level *lev2*. *edge2* defines the
type of crossing between *sig2* and *lev2*
* ``rising`` - the slope of *sig2* is positive or zero at the crossing
* ``falling`` - the slope of *sig2* is negative or zero at the crossing
* ``any`` - the slope of *sig2* does not matter
*skip2* specifies how many crossings since the beginning of *sig2* are
skipped before the crossing that is used as the point in *sig2* is reached.
0 means that the first crossing is used as the point in *sig2*.
Similarly the point in *sig1* is defined with *lev1*, *edge1*, and *skip1*.
*t1* and *t2* are the points on the scale defining the beginning and the
end of the part of *sig1* and *sig2* which is used in the calculation of
the delay. *skip1* and *skip2* are counted from point *t1* on the scale.
The default values of *t1* and *t2* are the first and the last value in
*scl*.
If *lev1type* is ``abs`` *lev1* specifies the value of the signal at the
crossing. If *lev1type* is ``rel`` *lev1* specifies the relative value of
the signal (between 0.0 and 1.0) where the 0.0 level is defined as the
*sig1* level at point *t1* on the scale while the 1.0 level is defined as
the *sig1* level at point *t2* on the scale. If *t1* and *t2* are not given
the 0.0 and 1.0 relative levels are taken at the beginning and the end of
*sig2*.
Similarly *lev2type* defines the meaning of *lev2* with respect to *sig2*,
*t1*, and *t2*.
"""
# Get reference levels of sig1
(i1, s11, i2, s12)=_refLevel(sig1, scl, t1, t2)
# Get reference levels of sig2
s21=XatI(sig2, i1)
s22=XatI(sig2, i2)
# Extract interesting part
partsig1=XatIrange(sig1, i1, i2)
partsig2=XatIrange(sig2, i1, i2)
partscl=XatIrange(scl, i1, i2)
# Get level crossing for signal 1
if lev1type=='abs':
crossI1=IatXval(partsig1, lev1, edge1)
elif lev1type=='rel':
crossI1=IatXval(partsig1, s11+(s12-s11)*lev1, edge1)
else:
raise PyOpusError("Bad level type for first signal.")
if skip1>=crossI1.size:
raise PyOpusError("No such crossing for first signal.")
# Get level crossing for signal 2
if lev2type=='abs':
crossI2=IatXval(partsig2, lev2, edge2)
elif lev2type=='rel':
crossI2=IatXval(partsig2, s21+(s22-s21)*lev2, edge2)
else:
raise PyOpusError("Bad level type for first signal.")
if skip2>=crossI2.size:
raise PyOpusError("No such crossing for second signal.")
crossI1=crossI1[skip1]
crossI2=crossI2[skip2]
delay=XatI(partscl, crossI2)-XatI(partscl, crossI1)
return delay
[docs]def Tshoot(measureType, sig, scl,
t1=None, t2=None, outputType='rel'):
"""
Gets the overshoot or the undershoot of signal *sig* with scale *scl*. The
over/undershoot is measured on the scale interval between *t1* and *t2*. If
*t1* and *t2* are not given the whole signal *sig1* is used in the
measurement.
The 0.0 and 1.0 relative levels in the signal are defined as the values of
*sig* at points *t1* and *t2* on the scale. The default values of *t1* and
*t2* are the first and the last value in *scl*.
Overshoot is the amount the signal rises above the 1.0 relative level on
the observed scale interval defined by *t1* and *t2*. Undershoot is the
amount the signal falls below the 0.0 relative level on the observed scale
interval.
If *measureType* is set to ``over``, overshoot is measured and the function
expects the signal level at *t1* to be lower than the signal level at *t2*.
If *measureType* is ``under`` the opposite must hold.
Over/undershoot can be measured as relative (when *outputType* is ``rel``)
or absolute (when *outputType* is ``abs``). Abolute values reflect actual
signal values while relative values are measured with respect to the 0.0
and 1.0 relative signal level.
"""
# Get reference levels
(i1, s1, i2, s2)=_refLevel(sig, scl, t1, t2)
# Extract interesting part
partsig=XatIrange(sig, i1, i2)
partscl=XatIrange(scl, i1, i2)
if measureType=='over':
# Overshoot
if s1<=s2:
delta=partsig.max()-s2
else:
delta=0
elif measureType=='under':
# Undershoot
if s1>=s2:
delta=s2-partsig.min()
else:
delta=0
else:
raise PyOpusError("Bad measurement type.")
if outputType=='abs':
return delta
elif outputType=='rel':
span=abs(s2-s1)
if span==0.0:
raise PyOpusError("Can't get relative value on flat signal.")
return delta/span
else:
raise PyOpusError("Bad output type.")
[docs]def Tovershoot(sig, scl,
t1=None, t2=None, outputType='rel'):
"""
An alias for :func:`Tshoot` with *measureType* set to ``over``.
"""
return Tshoot('over', sig, scl, t1, t2, outputType);
[docs]def Tundershoot(sig, scl,
t1=None, t2=None, outputType='rel'):
"""
An alias for :func:`Tshoot` with *measureType* set to ``under``.
"""
return Tshoot('under', sig, scl, t1, t2, outputType);
[docs]def TedgeTime(edgeType, sig, scl,
lev1type='rel', lev1=0.1,
lev2type='rel', lev2=0.9,
t1=None, t2=None):
"""
Measures rise or fall time (scale interval) of signal *sig* on scale *scl*.
The value of the *edgeType* parameter determines the type of the
measurement
* ``rising`` - measures rise time
* ``falling`` - measures fall time
*t1* and *t2* specify the scale interval on which the measurement takes
place. Their default values correspond to the first and the last value in
*scl. The values of the signal at *t1* and *t2* define the 0.0 and the 1.0
relative signal value.
*lev1type* and *lev* specify the point at which the signal rise/fall
begins. If *lev1type* is ``abs`` the level specified by *lev1* is the
actual signal value. If *lev1type* is ``rel`` the value given by *lev1* is
a relative signal value.
Similarly *lev2type* and *lev2* apply to the point at which the signal
rise/fall ends.
*lev1type*, *lev1*, *lev2type*, and *lev2* are by default set to measure
the 10%..90% rise/fall time.
"""
# Get reference levels
(i1, s1, i2, s2)=_refLevel(sig, scl, t1, t2)
# Extract interesting part
partsig=XatIrange(sig, i1, i2)
partscl=XatIrange(scl, i1, i2)
# Get crossing levels
if lev1type=='abs':
sc1=lev1
elif lev1type=='rel':
sc1=s1+(s2-s1)*lev1
else:
raise PyOpusError("Bad level type for first point.")
if lev2type=='abs':
sc2=lev2
elif lev1type=='rel':
sc2=s1+(s2-s1)*lev2
else:
raise PyOpusError("Bad level type for second point.")
# Get level crossings
crossI1=IatXval(partsig, sc1, edgeType)
if crossI1.size<=0:
raise PyOpusError("First point not found.")
# Use first crossing
crossI1=crossI1.min()
crossI2=IatXval(partsig, sc2, edgeType)
# Expect second point after first point
crossI2=filterI(crossI2, 'right', crossI1, includeStart=True)
if crossI2.size<=0:
raise PyOpusError("Second point not found.")
# Use first crossing that remains unfiltered
crossI2=crossI2.min()
# Get crossing times
delta=XatI(partscl, crossI2)-XatI(partscl, crossI1)
return delta
[docs]def TriseTime(sig, scl,
lev1type='rel', lev1=0.1,
lev2type='rel', lev2=0.9,
t1=None, t2=None):
"""
An alias for :func:`TedgeTime` with *edgeType* set to ``rising``.
"""
return TedgeTime('rising', sig, scl, lev1type, lev1, lev2type, lev2, t1, t2)
[docs]def TfallTime(sig, scl,
lev1type='rel', lev1=0.1,
lev2type='rel', lev2=0.9,
t1=None, t2=None):
"""
An alias for :func:`TedgeTime` with *edgeType* set to ``falling``.
"""
return TedgeTime('falling', sig, scl, lev1type, lev1, lev2type, lev2, t1, t2)
[docs]def TslewRate(edgeType, sig, scl,
lev1type='rel', lev1=0.1,
lev2type='rel', lev2=0.9,
t1=None, t2=None):
"""
Measures the slew rate of a signal. The slew rate is defined as the
quotient dx/dt where dx denotes the signal difference between the beginning
and the end of signal's rise/fall, while dt denotes the rise/fall time.
Slew rate is always positive.
See :func:`TedgeTime` for the explanation of the function's parameters.
"""
# Get reference levels
(i1, s1, i2, s2)=_refLevel(sig, scl, t1, t2)
if lev1type=='abs':
sl1=lev1
else:
sl1=s1+(s2-s1)*lev1
if lev2type=='abs':
sl2=lev2
else:
sl2=s1+(s2-s1)*lev2
sigDelta=sl2-sl1
# Get edge time
dt=TedgeTime(edgeType, sig, scl, lev1type, lev1, lev2type, lev2, t1, t2)
if dt==0:
raise PyOpusError("Can't evaluate slew rate if edge time is zero.")
# Get slew rate
return abs(sigDelta)/dt
[docs]def TsettlingTime(sig, scl,
tolType='rel', tol=0.05,
t1=None, t2=None):
"""
Measures the time (scale interval on scale *scl*) in which signal *sig*
settles within some prescribed tolerance of its final value.
*t1* and *t2* define the scale interval within which the settling time is
measured. The default values of *t1* and *t2* are the first and the last
value of *scl*. The final signal value if the value of the signal
corresponding to point *t2* on the scale.
The 0.0 and the 1.0 relative signal levels are defined as signal levels at
points *t1* and *t2* on the scale.
If *tolType* is ``abs`` the settling time is measured from *t1* to the
point at which the signal remains within *tol* of its final value at *t2*.
If *tolType* is ``rel`` the settling tolerance is defined as *tol* times
the difference between the signal levels corresponding to the 0.0 and 1.0
relative signal level.
"""
# Get reference levels
(i1, s1, i2, s2)=_refLevel(sig, scl, t1, t2)
sigDelta=s2-s1
# Extract interesting part
partsig=XatIrange(sig, i1, i2)
partscl=XatIrange(scl, i1, i2)
# Get tolerance level
if tolType=='abs':
tolInt=tol
elif tolType=='rel':
tolInt=tol*abs(sigDelta)
else:
raise PyOpusError("Bad tolerance type.")
# Get absolute deviation from settled value
sigdev=abs(partsig-s2)
# Find crossing of absolute deviation with tolerance level
crossI=IatXval(sigdev, tolInt, 'any')
if crossI.size<=0:
raise PyOpusError("No crossing with tolerance level found.")
# Take last crossing
cross=crossI[-1]
# Get settling time
delta=XatI(partscl, cross)-partscl[0]
if delta<0:
raise PyOpusError("This is weird. Negative settling time.")
return delta
#------------------------------------------------------------------------------
# Overdrive calculation (generic)
# e.g. Vgs-Vth
[docs]class Poverdrive:
"""
Calculates the difference between the values obtained from two driver
functions.
Objects of this class are callable. The calling convention is
``object(name)``. When called it returns the difference between the values
returned by a call to *driver1* with arguments (name, p1) and the value
returned by a call to *driver2* with arguments (name, p2). The difference
is returned as an array. If the size of the array is 1, it is returned as a
scalar (0-dimensional array).
:class:`Poverdrive` can be used for calculating the Vgs-Vth difference of
one or more MOS transistors by defining the measurement script in the
following way::
obj=m.Poverdrive(p, 'vgs', p, 'vth')
retval=list(map(obj, ['mn2', 'mn3', 'mn9']))
__result=np.array(retval)
The :func:`map` Python builtin function calls the :class:`Poverdrive`
object ``obj`` 3 times, once for every member of the list
``['mn2', 'mn3', 'mn9']`` and collects the return values in
a list which is then returned by ``map`` and stored in ``retval``.
A call to :class:`Poverdrive` object ``obj`` with argument ``mn2`` returns
the result of::
p('mn2', 'vgs')-p('mn2', 'vth')
which is actually the difference between the Vgs and the threshold voltage
of MOS transistor ``mn1``. So ``retval`` is a list holding the values of
the difference between Vgs and the threshold voltage of transistors listed
in ``['mn2', 'mn3', 'mn9']``.
Finally the list is converted to an array because the
:class:`~pyopus.evaluator.performance.PerformanceEvaluator` object can's
handle lists.
The previous measurement script could also be written as a measurement
expression::
np.array(list(map(m.Poverdrive(p, 'vgs', p, 'vth'), ['mn2', 'mn3', 'mn9'])))
Note that during measurement evaluation
(when a :class:`~pyopus.evaluator.performance.PerformanceEvaluator` object
is called) the function :func:`p` accesses device properties calculated by
the simulator while the :mod:`pyopus.evaluator.measure` and :mod:`numpy`
modules are available as :mod:`m` and :mod:`np`.
"""
def __init__(self, driver1, p1, driver2, p2):
self.driver1=driver1
self.p1=p1
self.driver2=driver2
self.p2=p2
def __call__(self, instance):
if type(self.p1) is str:
v1=self.driver1(instance, self.p1)
else:
v1=self.driver1(instance, *self.p1)
if type(self.p2) is str:
v2=self.driver2(instance, self.p2)
else:
v2=self.driver2(instance, *self.p2)
diff=array(v1-v2)
# Scalarize if diff is 1 long
if diff.size==1:
diff=diff[0]
return diff
# if __name__=="__main__":
# import numpy as np
#
# x=np.arange(0, 10, 0.1)
#
# print("d(x**2/2):", dYdX(x**2/2,x))
# print("d(sin(x)):", dYdX(np.sin(x),x))
#
# # x=np.array([10, 11])
# # y=np.array([8, 9])
# # rint("d(lin):", dYdX(y,x))
#
# # print("d(lin):", dYdX([1], [2]))