"""
.. inheritance-diagram:: pyopus.design.yt
:parts: 1
**Corners-based design (PyOPUS subsystem name: YT)**
Finds the circuit's design parameters for which the worst-case performances
(within *beta* of the statistical parameters origin) satisfy the design
requirements. *beta* specifies the target yield through the following
equation.
Y = 0.5 *beta* ( 1 + erf( *beta* / sqrt(2) ) )
"""
from ..optimizer import optimizerClass
from ..optimizer.base import Reporter, Annotator
from ..misc.debug import DbgMsgOut, DbgMsg
from ..evaluator.performance import updateAnalysisCount, PerformanceEvaluator
from ..evaluator.aggregate import formatParameters
from .cbd import CornerBasedDesign
from .wc import WorstCase
import numpy as np
import itertools
from copy import deepcopy
from .. import PyOpusError
from pprint import pprint
__all__ = [ 'YieldTargeting' ]
[docs]class YieldTargeting(object):
"""
*paramSpec* is the design parameter specification dictionary
with ``lo`` and ``hi`` members specifying the lower and the
upper bound.
*statParamSpec* is the statistical parameter specification
dictionary with ``lo`` and ``hi`` members specifying the lower
and the upper bound.
*opParamSpec* is the operating parameter specification
dictionary with ``lo`` and ``hi`` members specifying the lower
and the upper bound. The nominal value is specified by the
``init`` member.
See :class:`PerformanceEvaluator` for details on *heads*,
*analyses*, *measures*, *corners*, and *variables*.
*corners* are prototype corners and do not specify any operating
or statistical parameters. They are used by the worst-case
analysis for computing worst case performance corners.
*fixedCorners* are predefined, fully specified corners (i.e.
not prototype corners) where performance measures for which
worst-case corners are not computed will be evaluated.
If not specified, only the corners generated by the worst-case
analysis will be used for these measures.
Fixed parameters are given by *fixedParams* - a dictionary
with parameter name for key and parameter value for value.
Alternatively the value can be a dictionary in which case the
``init`` member specifies the parameter value.
If *fixedParams* is a list the members of this list must be
dictionaries describing parameters. The set of fixed parameters
is obtained by merging the information from these dictionaries.
*beta* is the spehere radius within which the worst case is
sought. It defines the target yield.
*sigmaBox* is the box constraint on normalized statistical
parameters (i.e. normalized to N(0,1)). If not specified it is
set to 10 or 2x *beta*, whichever is greater.
*wcSpecs* is the list of worst cases to compute in the form
of a list of (name, type) pairs where name is the performance
measure name an type is either ``upper`` or ``lower``. If a
specification is just a string it represents the performance
measure name. In that case the type of the specification is
obtained from the *measures* structure (the ``lower`` and the
``upper`` member of a performance measure description dictionary).
If *wcSpecs* is not specified the complete list of all
performance measures is used and the presence of the ``lower``
and the ``upper`` member in the performance measure
description dictionary specifies the type of the worst case
that is considered in the process of yield targetting.
*initial* is a dictionary of initial design parameter values.
If not specified the mean of the lower and the upper bound are
used.
If *initialNominalDesign* is ``True`` an initial design in the
nominal corner is performed using :class:`CornerBasedDesign`
class and the resulting design parameter values are used as
the initial point for yield targeting. *initial* is used as
the strating point for the nominal design.
If *forwardSolution* is ``True`` the solution of previous
pass is used as the initial point for the next pass.
*initialCbdOptions* is a dictionary of options passed to the
:class:`~pyopus.design.cbd.CornerBasedDesign` object at its
creation in initial nominal design. These options define the
behavior of the sizing across corners. If not given *cbdOptions*
are used.
*firstPassCbdOptions* is a dictionary of options passed to the
:class:`~pyopus.design.cbd.CornerBasedDesign` object at its
creation in the first pass. These options define the behavior
of the sizing across corners in the first pass. If not specified
*cbdOptions* are used.
*cbdOptions* is a dictionary of options passed to the
:class:`~pyopus.design.cbd.CornerBasedDesign` object at its
creation. These options define the behavior of the sizing across
corners.
*wcOptions* is a dictionary of options passed to the
:class:`~pyopus.design.wc.WorstCase` object. These options define
the behavior of the worst case analysis.
*cornerTol* is the relative tolerance for determining if two
corners are almost equal. The absolute tolerance for the norm of
the vector of statistical parameters is obtained by multiplying
this value with *beta*. The absolute tolerance for operating
parameters is obtained by multiplying the low-high range with
*cornerTol*.
*angleTol* is the angular tolerance in degrees for determining
if two sets of statistical parameters are almost equal.
*debug* turns on debugging.
Setting *spawnerLevel* to a value not greater than 1 distributes
the evaluations across available computing nodes. This argument
is forwarded to the :class:`~pyopus.design.cbd.CornerBasedDesign`
and the :class:`~pyopus.design.wc.WorstCase` objects.
This is a callable object with no arguments. The return value
is a tuple comprising a dictionary with the final values of the
design parameters, the :class:`~pyopus.evaluator.aggregate.Aggregator`
object used for evaluating the final result across all relevant
corners, the :class:`~pyopus.design.wc.WorstCase` object used for
computingthe final worst case performance, and the total dictionary
holding the number of all analyses performed. If no optimization
step is performed (i.e. if the initial design satisfies the design
requirements in worst case corners) the returned aggregator is
``None``.
Objects of this type store the number of analyses performed
during the last call to the object in the :attr:`analysisCount`
member. The last :class:`~pyopus.evaluator.aggregate.Aggregator`
object and the last :class:`~pyopus.design.wc.WorstCase` object
are stored in the :attr:`aggregator` and the :attr:`wc` member.
The resulting set of design parameters is stored in the
:attr:`atParam` member.
"""
def __init__(
self, paramSpec, statParamSpec, opParamSpec,
heads, analyses, measures, corners, fixedCorners=None,
fixedParams={}, variables={},
beta=3.0, sigmaBox=None, wcSpecs=None,
initial=None, initialNominalDesign=True,
forwardSolution=True,
initialCbdOptions=None,
firstPassCbdOptions=None,
cbdOptions={},
wcOptions={}, cornerTol=0.01, angleTol=10,
debug=0, spawnerLevel=1
):
self.heads=heads
self.analyses=analyses
self.measures=measures
self.corners=corners
self.fixedCorners=fixedCorners if fixedCorners is not None else {}
self.variables=variables
self.paramSpec=paramSpec
self.statParamSpec=statParamSpec
self.opParamSpec=opParamSpec
self.initial=initial
self.initialNominalDesign=initialNominalDesign
self.forwardSolution=forwardSolution
self.debug=debug
self.initialCbdOptions=initialCbdOptions
self.firstPassCbdOptions=firstPassCbdOptions
self.cbdOptions=cbdOptions
self.wcOptions=wcOptions
self.beta=beta
self.sigmaBox=sigmaBox
if wcSpecs is not None:
self.wcSpecs=wcSpecs
else:
self.wcSpecs=list(self.measures.keys())
self.spawnerLevel=spawnerLevel
self.cornerTol=cornerTol
self.angleTol=angleTol
# Process fixed parameters
self.fixedParams={}
if fixedParams is not None:
if type(fixedParams) is list or type(fixedParams) is tuple:
lst=fixedParams
else:
lst=[fixedParams]
for entry in lst:
nameList=list(entry.keys())
if len(nameList)>0 and type(entry[nameList[0]]) is dict:
# Extract init
self.fixedParams.update(
{ name: entry[name]['init'] for name in nameList }
)
else:
self.fixedParams.update(entry)
# Parameter names and counts
self.paramNames=list(self.paramSpec.keys())
self.paramNames.sort()
self.nParam=len(self.paramNames)
self.opNames=list(self.opParamSpec.keys())
self.opNames.sort()
self.nOp=len(self.opNames)
self.statNames=list(self.statParamSpec.keys())
self.statNames.sort()
self.nStat=len(self.statNames)
self.opLo=np.array([ self.opParamSpec[name]['lo'] for name in self.opNames])
self.opHi=np.array([ self.opParamSpec[name]['hi'] for name in self.opNames])
# Nominal op values
self.opNominal=np.array([self.opParamSpec[name]['init'] for name in self.opNames])
def cornersClose(self, c1, c2, dbgPfx=""):
c1s=np.array([c1['params'][name] for name in self.statNames])
c2s=np.array([c2['params'][name] for name in self.statNames])
c1o=np.array([c1['params'][name] for name in self.opNames])
c2o=np.array([c2['params'][name] for name in self.opNames])
# Distance
ds1=(c1s**2).sum()**0.5
ds2=(c2s**2).sum()**0.5
statdif=np.abs(ds1-ds2)
# Angle
if ds1==0.0 or ds2==0.0:
angle=0.0
else:
angle=np.arccos((c1s*c2s).sum()/(ds1*ds2))/np.pi*180
# Op difference (relative)
opdif=np.abs(c1o-c2o)/(self.opHi-self.opLo)
if self.debug>1:
DbgMsgOut("YT", " %sstatdif=%e angle=%e opdif_max=%e" % (dbgPfx, statdif, angle, opdif.max()))
if statdif<=self.beta*self.cornerTol and angle<=self.angleTol and (opdif<=self.cornerTol).all():
return True
else:
return False
def __call__(self):
# Measures that have corners
# subject to wc - add new corners
# not subject to wc - do not add new corners
# Measures without corners
# subject to wc - add new corners
# not subject to wc - use all corners
self.atParam=None
self.wcresult=None
self.analysisCount=None
self.aggregator=None
analysisCount={}
# Prepare wc specs if none specified
if self.wcSpecs is None:
wcSpecs=list(self.measures.keys())
# Get heads used by performance measures
m2head={}
head2m={}
for name, mdef in self.measures.items():
anName=mdef['analysis']
hName=self.analyses[anName]['head']
m2head[name]=hName
if hName not in head2m:
head2m[hName]=set()
head2m[hName].add(name)
# Check specifications, skip measures without specs, fill nominal corner lists
wcNames=[]
for wcSpec in self.wcSpecs:
if type(wcSpec) is tuple:
wcName, wcType = wcSpec
if wcType not in measures[wcName]:
raise PyOpusError(DbgMsg("YT", "Measure %s has no %s specification." % (wcName, wcSpec[1])))
else:
wcName=wcSpec
if (
'lower' not in self.measures[wcName] and
'upper' not in self.measures[wcName]
):
continue
wcNames.append(wcName)
# Other measures (for which worst-case corners are not computed) have no corners field
# They use all corners that apply to them
# Assume we have no aggregator
aggregator=None
# Initial nominal design
if self.initialNominalDesign:
if self.debug:
DbgMsgOut("YT", "Initial nominal design")
# Corner-based design
cbdOptions={}
if self.initialCbdOptions is not None:
cbdOptions.update(self.initialCbdOptions)
else:
cbdOptions.update(self.cbdOptions)
# Generate nominal corners from prototypes
# Their keys are identical to prototype corner keys
nominalCorners={}
for key, cdef in self.corners.items():
d=deepcopy(cdef)
# Nominal statistical parameters
d['params'].update({
name: 0.0 for name in self.statNames
})
# Nominal design parameters
d['params'].update({
name: pdef['init'] for name, pdef in self.opParamSpec.items()
})
if type(key) is tuple:
key=(key[0]+"_nominal", key[1])
else:
key=key+"_nominal"
nominalCorners[key]=d
# Build nominal corner lists for heads
cornersHC, cornersCH, definedModulesSets = PerformanceEvaluator.buildHCLists(
self.heads, nominalCorners
)
# Add corners field to measures for which worst-case corners will be computed
# Add the corresponding nominal corner to the list
measures=deepcopy(self.measures)
for name in wcNames:
hName=m2head[name]
# Take first (and only) corner name
measures[name]['corners']=[next(iter(cornersHC[hName].keys()))]
# Build corners list
corners={}
corners.update(self.fixedCorners)
corners.update(nominalCorners)
cbd=CornerBasedDesign(
self.paramSpec, self.heads, self.analyses, measures, corners,
self.fixedParams, self.variables, initial=self.initial,
spawnerLevel=self.spawnerLevel,
**cbdOptions
)
initialDesignParams, aggregator, anCount = cbd()
updateAnalysisCount(analysisCount, anCount)
if self.debug:
DbgMsgOut("YT", aggregator.formatResults())
DbgMsgOut("YT", "Analysis count: %s" % str(anCount))
DbgMsgOut("YT", "Result:")
DbgMsgOut("YT", formatParameters(initialDesignParams))
else:
# Initial values
if self.initial is None:
# No initial nominal design, use mean of hi and lo
initialDesignParams={
name: (pdef['lo']+pdef['hi'])/2 for name, pdef in self.paramSpec.items()
}
else:
# Use explicitly given initial design params
initialDesignParams=self.initial
designParams=initialDesignParams
cornerHistory=[] # list of dicts with (name, type) for key holding corner definitions
atPass=1
while True:
# Compute worst case
if self.debug:
DbgMsgOut("YT", "Computing worst case, pass %d" % (atPass))
# DbgMsgOut("YT", formatParameters(designParams))
wc=WorstCase(
self.heads, self.analyses, self.measures, self.corners,
self.statParamSpec, self.opParamSpec, variables=self.variables,
fixedParams=designParams,
beta=self.beta, sigmaBox=self.sigmaBox,
spawnerLevel=self.spawnerLevel,
**self.wcOptions
)
wcresults, anCount = wc(self.wcSpecs)
updateAnalysisCount(analysisCount, anCount)
if self.debug:
DbgMsgOut("YT", wc.formatResults())
DbgMsgOut("YT", "Analysis count: %s" % str(anCount))
# Collect new corners
# Find worst corner for every key (wcName, wcType)
# This finds the worst corner across all components of a vector measure
worstCorner={}
worstValue={}
keys=set()
for res in wcresults:
wcName=res['name']
wcComp=res['component']
wcType=res['type']
wcVal=res['wc']
key=(wcName, wcType)
keys.add(key)
# Construct corner definition
params={}
params.update(res['op'])
params.update(res['stat'])
corner={
'params': params,
'modules': res['modules'],
'head': res['head'],
'name': 'wc_%d_%s_%s' % (atPass, wcName, wcType)
}
# Add to list
if key not in worstValue:
# First corner
worstValue[key]=wcVal
worstCorner[key]=corner
else:
# Check if this corner is worse, replace previous corner
if (
(wcType=='lower' and wcVal<worstValue[key]) or
(wcType=='upper' and wcVal>worstValue[key])
):
worstValue[key]=wcVal
worstCorner[key]=corner
if self.debug:
DbgMsgOut("YT", "Checking worst corners")
# Keep only corners where design requirements are violated
keptWorstCorners={}
haveNew=False
for key, corner in worstCorner.items():
wcName, wcType = key
wcVal=worstValue[key]
if (
(wcType=='lower' and wcVal<self.measures[wcName]['lower']) or
(wcType=='upper' and wcVal>self.measures[wcName]['upper'])
):
keptWorstCorners[key]=corner
haveNew=True
if self.debug:
DbgMsgOut("YT", " Adding new worst corner for (%s, %s)" % (wcName, wcType))
# Stop if no new corners found
if not haveNew:
if self.debug:
DbgMsgOut("YT", "Design requierements satisfied in all worst-case corners. Stopping.")
break
# Add kept worst corners to corner history
cornerHistory.append(keptWorstCorners)
if self.debug:
DbgMsgOut("YT", "Looking for redundant corners")
# Go through all keys, collect corners, fill blanks with None
# This is a dict with (name, type) for key holding corner history for a requirement
filteredCornerHistory={}
significantNewCornersFound=False
for key in keys:
wcName, wcType = key
# Collect history across passes
hist=[]
# Go through all passes
for histEnt in cornerHistory:
if key in histEnt:
hist.append(histEnt[key])
else:
hist.append(None)
eliminated=0
# Start at corner index (last corner)
startIndex=len(hist)-1
first=True
# Iterate
while True:
# Start at latest corner, eliminate similar past corners
refCorner=None
for ii in range(startIndex,-1,-1):
atCorner=hist[ii]
# Look for first reference corner
if refCorner is None:
refCorner=atCorner
refIndex=ii
startIndex=ii-1
continue
# No corner in this pass
if atCorner is None:
continue
# If we have current corner, compare it to reference
dbgPfx="(%s, %s): comparing pass %d to pass %d " % (wcName, wcType, ii+1, refIndex+1)
if self.cornersClose(refCorner, atCorner, dbgPfx):
# Too similar, eliminate older corner
hist[ii]=None
eliminated+=1
if self.debug:
DbgMsgOut("YT", " (%s, %s): eliminatig pass %d corner because it is close to pass %d corner" % (wcName, wcType, ii+1, refIndex+1))
if first and eliminated==0:
significantNewCornersFound=True
first=False
# Stop when no reference corner is found
if refCorner is None:
break
filteredCornerHistory[key]=hist
# Check if we have any new corners left from worst-case analysis
haveNew=False
for key, hist in filteredCornerHistory.items():
if hist[-1] is not None:
haveNew=True
break
# Stop if no new corners found (only similar corners)
if not significantNewCornersFound:
if self.debug:
DbgMsgOut("YT", "No significantly different new corners found. Stopping.")
break
# Update corner sets with candidate corners
if self.debug:
DbgMsgOut("YT", "Building corner sets for optimization")
# Add fixed corners to set of corners
corners={}
corners.update(self.fixedCorners)
# Add worst corners
measures=deepcopy(self.measures)
for key, hist in filteredCornerHistory.items():
wcName, wcType = key
# Empty corners entry
if 'corners' not in measures[wcName]:
measures[wcName]['corners']=[]
# Scan passes
cNameList=[]
for ii in range(len(hist)):
corner=hist[ii]
if corner is None:
continue
# Make a copy
corner=deepcopy(corner)
# Build name
cName='wc_%d_%s_%s' % (ii+1, wcName, wcType)
cNameList.append(cName)
# Extract head name, delete head entry
head=corner['head']
del corner['head']
del corner['name']
# Add corner to list of corners
corners[(cName, (head,))]=corner
# Add to list of measure's corners
measures[wcName]['corners'].append(cName)
# Add to lists of corners of dependencies
if 'depends' in measures[wcName]:
for dep in measures[wcName]['depends']:
if 'corners' not in measures[dep]:
measures[dep]['corners']=[]
measures[dep]['corners'].append(cName)
if self.debug:
if len(cNameList)>0:
DbgMsgOut("YT", "(%s, %s) corners" % (wcName, wcType))
for cName in cNameList:
DbgMsgOut("YT", " %s" % (cName))
else:
DbgMsgOut("YT", "(%s, %s) is not evaluated in any corner" % (wcName, wcType))
# Remove duplicates
for name, mDef in measures.items():
if 'corners' not in mDef:
continue
mDef['corners']=list(set(mDef['corners']))
# Corner-based design
if self.debug:
DbgMsgOut("YT", "Sizing across corners, pass %d" % (atPass))
# CBD options
cbdOptions={}
if self.firstPassCbdOptions is not None and atPass==1:
cbdOptions.update(self.firstPassCbdOptions)
else:
cbdOptions.update(self.cbdOptions)
# Copy the solution from last pass or use the initial solution
if self.forwardSolution:
cbdOptions['initial']=designParams
else:
cbdOptions['initial']=initialDesignParams
cbd=CornerBasedDesign(
self.paramSpec, self.heads, self.analyses, measures, corners,
self.fixedParams, self.variables,
spawnerLevel=self.spawnerLevel, **cbdOptions
)
designParams, aggregator, anCount = cbd()
updateAnalysisCount(analysisCount, anCount)
if self.debug:
DbgMsgOut("YT", aggregator.formatResults())
DbgMsgOut("YT", "Analysis count: %s" % str(anCount))
DbgMsgOut("YT", "Result:")
DbgMsgOut("YT", formatParameters(designParams))
atPass+=1
# Convert parameters to dictionary
self.atParam=designParams
self.wc=wc
self.analysisCount=analysisCount
self.aggregator=aggregator
self.passes=atPass-1
if self.debug:
DbgMsgOut("YT", "Total analysis count: %s" % str(self.analysisCount))
DbgMsgOut("YT", "Passes: %d" % self.passes)
return (self.atParam, self.aggregator, self.wc, self.analysisCount)