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

Non-multilevel Optimizers reworked #507

Merged
merged 2 commits into from
Jan 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions developer_tools/XSDSchemas/Optimizers.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<xsd:element name="initialSeed" type="xsd:integer" minOccurs="0"/>
<xsd:element name="type" type="xsd:string" minOccurs="0"/>
<xsd:element name="thresholdTrajRemoval" type="xsd:float" minOccurs="0"/>
<xsd:element name="writeSteps" type="xsd:string" minOccurs="0"/>
</xsd:all>
</xsd:complexType>

Expand Down
14 changes: 10 additions & 4 deletions doc/user_manual/optimizer.tex
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ \subsubsection{Simultaneous Perturbation Stochastic Approximation (SPSA)}
of the second. Note that this value is
calculated as Euclidean distance in a normalized 0 to 1 cubic domain, not the original input space domain.
\default{0.05}
\item \xmlNode{writeSteps}, \xmlDesc{string, optional field}, specifies how often the current optimal
point should be stored to the solution export. Options are \xmlString{every}, in which case each new
optimal point will be stored in the solution export; or \xmlString{final}, in which case only the most
optimal point found during the simulation will be stored in the solution export.
\default{every};

\end{itemize}
\end{itemize}
\begin{itemize}
Expand Down Expand Up @@ -429,8 +435,8 @@ \subsubsection{Simultaneous Perturbation Stochastic Approximation (SPSA)}
\subsubsection{Finite Difference Gradient Optimizer (FiniteDifferenceGradientOptimizer)}
\label{subsubsubsec:FiniteDifferenceGradientOptimizer}
The \textbf{FiniteDifferenceGradientOptimizer} optimization approach is the simplest Gradient based approach since it is based on the
first order evaluation of the Gradient. A minimal number of $n variable$
model evaluations are required in order to get a first order approximation of the gradient.
first order evaluation of the Gradient. A minimal number of $n variable$
model evaluations are required in order to get a first order approximation of the gradient.

Current implementation of \textbf{FiniteDifferenceGradientOptimizer} can also handles
constrained optimization problem. This paragraph briefly describes how current implementation ensures the input satisfies the
Expand Down Expand Up @@ -645,7 +651,7 @@ \subsubsection{Finite Difference Gradient Optimizer (FiniteDifferenceGradientOpt
\cite{spall1998implementation}. A practical suggestion for $\gamma$ is 0.101 (paired with an
$\alpha$ value of 0.602); however, the asymptotic limit is $\gamma=1/6$ ($\alpha=1$). \default{0.101}
\item \xmlNode{c}, \xmlDesc{float, optional field} Step size coefficient. This term determines the
nominal step size, and increasing it will directly increase the distance between points sampled in evaluating the gradient.
nominal step size, and increasing it will directly increase the distance between points sampled in evaluating the gradient.
It is suggested this parameter be approximately equal to the standard
deviation of the measurement noise in the response for stochastic responses. For regular responses,
it can be a small arbitrary value. \default{0.005}
Expand Down Expand Up @@ -706,4 +712,4 @@ \subsubsection{Finite Difference Gradient Optimizer (FiniteDifferenceGradientOpt
</FiniteDifferenceGradientOptimizer>
...
</Optimizers>
\end{lstlisting}
\end{lstlisting}
141 changes: 90 additions & 51 deletions framework/Optimizers/GradientBasedOptimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def _updateConvergenceVector(self, traj, varsUpdate, currentLossVal):

## first, determine if we want to keep the new point
# obtain the old loss value
oldLossVal = self.counter['recentOptHist'][traj][0]['output']
oldLossVal = self.counter['recentOptHist'][traj][0][self.objVar]
# see if new point is better than old point
newerIsBetter = self.checkIfBetter(currentLossVal,oldLossVal)
# if this was a recommended preconditioning point, we should not be converged.
Expand Down Expand Up @@ -379,7 +379,7 @@ def printProgress(name,boolCheck,test,gold):

#same coordinate check
oldInputSpace = set(self.optVarsHist[traj][varsUpdate].items())
curInputSpace = set(self.counter['recentOptHist'][traj][0]['inputs'].items())
curInputSpace = set(dict((var,self.counter['recentOptHist'][traj][0][var]) for var in self.getOptVars()).items())
sameCoordinateCheck = oldInputSpace == curInputSpace
self.raiseAMessage(printString.format('Same coordinate check',str(minStepSizeCheck)))
converged = converged or sameCoordinateCheck
Expand Down Expand Up @@ -548,65 +548,80 @@ def localFinalizeActualSampling(self,jobObject,model,myInput):
except KeyError:
# this means we don't have an entry for this trajectory yet, so don't copy anything
pass
self.counter['recentOptHist'][traj][0] = {}
self.counter['recentOptHist'][traj][0]['inputs'] = self.optVarsHist[traj][self.counter['varsUpdate'][traj]]
self.counter['recentOptHist'][traj][0]['output'] = currentObjectiveValue
# store realization of most recent developments
rlz = {}
rlz.update(self.optVarsHist[traj][self.counter['varsUpdate'][traj]])
rlz.update(outputs)
self.counter['recentOptHist'][traj][0] = rlz
if traj not in self.counter['prefixHistory']:
self.counter['prefixHistory'][traj] = []
self.counter['prefixHistory'][traj].append(prefix)
# update solution export
#FIXME much of this should move to the base class!
if not failedTraj:
# create realization to add to data object
rlz = {}
badValue = -1 #value to use if we don't have a value # TODO make this accessible to user?
for var in self.solutionExport.getVars():
if var in self.getOptVars():
new = self.denormalizeData(self.counter['recentOptHist'][traj][0]['inputs'])[var]
elif var == self.objVar:
new = self.counter['recentOptHist'][traj][0]['output']
elif var in outputs.keys():
new = outputs[var]
elif var == 'varsUpdate':
new = self.counter['solutionUpdate'][traj]
elif var == 'trajID':
new = traj+1 # +1 is for historical reasons, when histories were indexed on 1 instead of 0
elif var == 'stepSize':
try:
new = self.counter['lastStepSize'][traj]
except KeyError:
new = badValue
elif var.startswith( 'gradient_'):
varName = var[9:]
vec = self.counter['gradientHistory'][traj][0].get(varName,None)
if vec is not None:
new = vec*self.counter['gradNormHistory'][traj][0]
else:
new = badValue
elif var.startswith( 'convergenceAbs'):
try:
new = self.convergenceProgress[traj].get('abs',badValue)
except KeyError:
new = badValue
elif var.startswith( 'convergenceRel'):
try:
new = self.convergenceProgress[traj].get('rel',badValue)
except KeyError:
new = badValue
elif var.startswith( 'convergenceGrad'):
try:
new = self.convergenceProgress[traj].get('grad',badValue)
except KeyError:
new = badValue
else:
self.raiseAnError(IOError,'Unrecognized output request:',var)
# format for realization
rlz[var] = np.atleast_1d(new)
self.solutionExport.addRealization(rlz)
# only write here if we want to write on EVERY optimizer iteration (each new optimal point)
if self.writeSolnExportOn == 'every':
self.writeToSolutionExport(traj)
# whether we wrote to solution export or not, update the counter
self.counter['solutionUpdate'][traj] += 1
else: #not ready to update solutionExport
break

def writeToSolutionExport(self,traj):
"""
Standardizes how the solution export is written to.
Uses data from "recentOptHist" and other counters to fill in values.
@ In, traj, int, the trajectory for which an entry is being written
@ Out, None
"""
# create realization to add to data object
rlz = {}
badValue = -1 #value to use if we don't have a value # TODO make this accessible to user?
recent = self.counter['recentOptHist'][traj][0]
for var in self.solutionExport.getVars():
# inputs, objVar, other outputs
if var in recent.keys():
new = self.denormalizeData(recent)[var]
# custom counters: varsUpdate, trajID, stepSize
elif var == 'varsUpdate':
new = self.counter['solutionUpdate'][traj]
elif var == 'trajID':
new = traj+1 # +1 is for historical reasons, when histories were indexed on 1 instead of 0
elif var == 'stepSize':
try:
new = self.counter['lastStepSize'][traj]
except KeyError:
new = badValue
# variable-dependent information: gradients
elif var.startswith( 'gradient_'):
varName = var[9:]
vec = self.counter['gradientHistory'][traj][0].get(varName,None)
if vec is not None:
new = vec*self.counter['gradNormHistory'][traj][0]
else:
new = badValue
# convergence metrics
elif var.startswith( 'convergenceAbs'):
try:
new = self.convergenceProgress[traj].get('abs',badValue)
except KeyError:
new = badValue
elif var.startswith( 'convergenceRel'):
try:
new = self.convergenceProgress[traj].get('rel',badValue)
except KeyError:
new = badValue
elif var.startswith( 'convergenceGrad'):
try:
new = self.convergenceProgress[traj].get('grad',badValue)
except KeyError:
new = badValue
else:
self.raiseAnError(IOError,'Unrecognized output request:',var)
# format for realization
rlz[var] = np.atleast_1d(new)
self.solutionExport.addRealization(rlz)

def fractionalStepChangeFromGradHistory(self,traj):
"""
Uses the dot product between two successive gradients to determine a fractional multiplier for the step size.
Expand Down Expand Up @@ -687,3 +702,27 @@ def proposeNewPoint(self,traj,point):
Optimizer.proposeNewPoint(self,traj,point)
self.counter['varsUpdate'][traj] += 1 #usually done when evaluating gradient, but we're bypassing that
self.queueUpOptPointRuns(traj,self.recommendedOptPoint[traj])

def finalizeSampler(self,failedRuns):
"""
Method called at the end of the Step when no more samples will be taken. Closes out optimizer.
@ In, failedRuns, list, list of JobHandler.ExternalRunner objects
@ Out, None
"""
Optimizer.handleFailedRuns(self,failedRuns)
# if writing soln export only on final, now is the time to do it
if self.writeSolnExportOn == 'final':
# get the most optimal point among the trajectories
bestValue = None
bestTraj = None
for traj in self.counter['recentOptHist'].keys():
value = self.counter['recentOptHist'][traj][0][self.objVar]
if bestTraj is None:
bestTraj = traj
bestValue = value
continue
if self.checkIfBetter(value,bestValue):
bestTraj = traj
bestValue = value
# now have the best trajectory, so write solution export
self.writeToSolutionExport(bestTraj)
24 changes: 22 additions & 2 deletions framework/Optimizers/Optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(self):
self.optVarsInit['ranges'] = {} # Dict of the ranges (min and max) of each variable's domain
self.initSeed = None # Seed for random number generators
self.optType = None # Either max or min
self.writeSolnExportOn = None # Determines when we write to solution export (every step or final solution)
self.paramDict = {} # Dict containing additional parameters for derived class
self.initializationSampler = None # Sampler that can be used to initialize the optimizer trajectories
self.optVarsInitialized = {} # Dict {var1:<initial> present?,var2:<initial> present?}
Expand Down Expand Up @@ -249,8 +250,15 @@ def _readMoreXMLbase(self,xmlNode):
self.initSeed = int(childChild.text)
elif childChild.tag == 'thresholdTrajRemoval':
self.thresholdTrajRemoval = float(childChild.text)
elif childChild.tag == 'writeSteps':
if childChild.text.strip().lower() == 'every':
self.writeSolnExportOn = 'every'
elif childChild.text.strip().lower() == 'final':
self.writeSolnExportOn = 'final'
else:
self.raiseAnError(IOError,'Unexpected frequency for <writeSteps>: "{}". Expected "every" or "final".')
else:
self.raiseAnError(IOError,'Unknown tag '+childChild.tag+' .Available: limit, type, initialSeed!')
self.raiseAnError(IOError,'Unknown tag: '+childChild.tag)

elif child.tag == "convergence":
for childChild in child:
Expand Down Expand Up @@ -297,6 +305,10 @@ def _readMoreXMLbase(self,xmlNode):
elif subnode.tag == 'sequence':
self.mlSequence = list(x.strip() for x in subnode.text.split(','))

if self.writeSolnExportOn is None:
# default
self.writeSolnExportOn = 'every'
self.raiseAMessage('Writing to solution export on "{}" optimizer iteration.'.format(self.writeSolnExportOn))
if self.optType is None:
self.optType = 'min'
if self.thresholdTrajRemoval is None:
Expand Down Expand Up @@ -660,7 +672,7 @@ def amIreadyToProvideAnInput(self):
# do we have any opt points yet?
if len(self.counter['recentOptHist'][traj][0]) > 0:
# get the latset optimization point (normalized)
latestPoint = self.counter['recentOptHist'][traj][0]['inputs']
latestPoint = dict((var,self.counter['recentOptHist'][traj][0][var]) for var in self.getOptVars())
#some flags for clarity of checking
justStarted = self.mlDepth[traj] is None
inInnermost = self.mlDepth[traj] is not None and self.mlDepth[traj] == len(self.mlSequence)-1
Expand Down Expand Up @@ -996,6 +1008,14 @@ def _getJobsByID(self):
"""
pass

def finalizeSampler(self,failedRuns):
"""
Method called at the end of the Step when no more samples will be taken. Closes out the optimizer for this step.
@ In, failedRuns, list, list of JobHandler.ExternalRunner objects
@ Out, None
"""
self.handleFailedRuns(failedRuns)

def handleFailedRuns(self,failedRuns):
"""
Collects the failed runs from the Step and allows optimizer to handle them individually if need be.
Expand Down
4 changes: 2 additions & 2 deletions framework/Optimizers/SPSA.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def _newOptPointAdd(self, gradient, traj):
"""
ak = self._computeGainSequenceAk(self.paramDict,self.counter['varsUpdate'][traj],traj) # Compute the new ak
self.optVarsHist[traj][self.counter['varsUpdate'][traj]] = {}
varK = copy.deepcopy(self.counter['recentOptHist'][traj][0]['inputs'])
varK = dict((var,self.counter['recentOptHist'][traj][0][var]) for var in self.getOptVars(traj)) #copy.deepcopy(self.counter['recentOptHist'][traj][0]['inputs'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm commented part?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to leave this part there just in case the deepcopy ends up coming up again, so I can find it easily.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

varKPlus,modded = self._generateVarsUpdateConstrained(traj,ak,gradient,varK)
#check for redundant paths
if len(self.optTrajLive) > 1 and self.counter['solutionUpdate'][traj] > 0:
Expand Down Expand Up @@ -330,7 +330,7 @@ def localGenerateInput(self,model,oldInput):
if self.counter['perturbation'][traj] == 1:
# Generate all the perturbations at once, then we can submit them one at a time
ck = self._computeGainSequenceCk(self.paramDict,self.counter['varsUpdate'][traj]+1)
varK = self.counter['recentOptHist'][traj][0]['inputs']
varK = dict((var,self.counter['recentOptHist'][traj][0][var]) for var in self.getOptVars(traj))
#check the submission queue is empty; otherwise something went wrong # TODO this is a sanity check, might be removed for efficiency
#TODO this same check is in GradientBasedOptimizer.queueUpOptPointRuns, they might benefit from abstracting
if len(self.submissionQueue[traj]) > 0:
Expand Down
21 changes: 18 additions & 3 deletions framework/PostProcessorFunctions/HistorySetSnapShot.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,32 @@ def historySnapShot(inputDic, pivotVar, snapShotType, pivotVal=None, tempID = No
@ In, tempID, string, name of the temporal variable (default None)
@ Out, outputDic, dict, it contains the temporal slice of all histories
"""
# place to store data results
outputDic={'data':{}}
outputDic['data']['ProbabilityWeight'] = inputDic['data']['ProbabilityWeight']
outputDic['data']['prefix'] = inputDic['data']['prefix']
# collect metadata, if it exists, to pass through
# TODO collecting by name is problemsome; for instance, Optimizers don't produce "probability weight" information
## ProbabilityWeight
try:
outputDic['data']['ProbabilityWeight'] = inputDic['data']['ProbabilityWeight']
except KeyError:
pass
## prefix
try:
outputDic['data']['prefix'] = inputDic['data']['prefix']
except KeyError:
pass
# place to store dimensionalities
outputDic['dims'] = {key: [] for key in inputDic['dims'].keys()}

for var in inputDic['inpVars']:
outputDic['data'][var] = inputDic['data'][var]

outVars = inputDic['data'].keys()
outVars = [var for var in outVars if 'Probability' not in var]
outVars.remove('prefix')
try:
outVars.remove('prefix')
except ValueError:
pass
vars = [var for var in outVars if var not in inputDic['inpVars']]

for var in vars:
Expand Down
8 changes: 8 additions & 0 deletions framework/Samplers/Sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,14 @@ def _reassignPbWeightToCorrelatedVars(self):
for subVarName in varName.split(","):
self.inputInfo['ProbabilityWeight-' + subVarName.strip()] = self.inputInfo['ProbabilityWeight-' + varName]

def finalizeSampler(self,failedRuns):
"""
Method called at the end of the Step when no more samples will be taken. Closes out sampler for step.
@ In, failedRuns, list, list of JobHandler.ExternalRunner objects
@ Out, None
"""
self.handleFailedRuns(failedRuns)

def handleFailedRuns(self,failedRuns):
"""
Collects the failed runs from the Step and allows samples to handle them individually if need be.
Expand Down
5 changes: 3 additions & 2 deletions framework/Steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,9 @@ def _localTakeAstepRun(self,inDictionary):
self.raiseADebug('Finished with %d runs submitted, %d jobs running, and %d completed jobs waiting to be processed.' % (jobHandler.numSubmitted(),jobHandler.numRunning(),len(jobHandler.getFinishedNoPop())) )
break
time.sleep(self.sleepTime)
# if any new collected runs failed, let the sampler treat them appropriately
sampler.handleFailedRuns(self.failedRuns)
# END while loop that runs the step iterations
# if any collected runs failed, let the sampler treat them appropriately, and any other closing-out actions
sampler.finalizeSampler(self.failedRuns)

def _findANewInputToRun(self, sampler, model, inputs, outputs):
"""
Expand Down
Loading