Source code for pyopus.design.yt

"""
.. 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)