Skip to content

Commit

Permalink
SimulatedAnnealing Discrete Variables (#2312)
Browse files Browse the repository at this point in the history
* moved tests for SimulatedAnnealing to continuos distribution, since a discrete one is coming

* fixed dist

* updated test names for continous

* discrete opt with simulated annealing

* moving

* rand of var

* changing delta as function of variable bounds

* added discrete

* discrete?

* fixed tests

* updated xsd schema

* Apply suggestions from code review

* spelling

* isFloats in categorical

* isFloats is now checked for False entitites

* regolded serpent test since it fails in optional machine

* added denromalization

* Update ravenframework/Distributions.py

* simulated annealing test

* use utils.isClose instead of np.isClose

* Apply suggestions from code review

* Update tests/framework/Optimizers/SimulatedAnnealing/discrete/FunctionallyConstrainedSA/test_funcConstrSimulatedAnnealing.xml
  • Loading branch information
alfoa authored May 20, 2024
1 parent e04e4dd commit 5c29164
Show file tree
Hide file tree
Showing 34 changed files with 1,532 additions and 958 deletions.
2 changes: 2 additions & 0 deletions developer_tools/XSDSchemas/Optimizers.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@
<xsd:complexType name="boltzmanCoolingType">
<xsd:all>
<xsd:element name="d" type="xsd:float"/>
<xsd:element name="learningRate" type="xsd:float"/>
</xsd:all>
</xsd:complexType>

<xsd:complexType name="cauchyCoolingType">
<xsd:all>
<xsd:element name="d" type="xsd:float"/>
<xsd:element name="learningRate" type="xsd:float"/>
</xsd:all>
</xsd:complexType>

Expand Down
1 change: 1 addition & 0 deletions doc/user_manual/ProbabilityDistributions.tex
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ \subsubsection{1-Dimensional Discrete Distributions.}
\vspace{-5mm}
\subnodeIntro
\begin{itemize}
\item \xmlNode{rtol}, \xmlDesc{float, optional parameter}, relative tolerance used to identify close state in case of float or integer states, \default{1E-6}.
\item \xmlNode{state}, \xmlDesc{float, required parameter}, probability for outcome 1
\begin{itemize}
\item \xmlAttr{outcome}, \xmlDesc{float, required parameter}, outcome value.
Expand Down
6 changes: 5 additions & 1 deletion doc/user_manual/generated/optimizer.tex
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ \subsection{SimulatedAnnealing}
T^{k} = \frac{T^0}{log(k + d)}$$ In case of \xmlString{cauchy} is provided,
The cooling process will be governed by: $$ T^{k} = \frac{T^0}{k + d}$$In case of
\xmlString{veryfast} is provided, The cooling process will be governed by: $$ T^{k} = T^0 *
\exp(-ck^{1/D}),$$ where $D$ is the dimentionality of the problem (i.e.,
\exp(-ck^{1/D}),$$ where $D$ is the dimensionality of the problem (i.e.,
number of optimized variables), $k$ is the number of the current iteration
$T^{0} = \max{(0.01,1-\frac{k}{\xmlNode{limit}})}$ is the initial temperature, and $T^{k}$ is
the current temperature according to the specified cooling schedule.
Expand Down Expand Up @@ -758,6 +758,8 @@ \subsection{SimulatedAnnealing}
\begin{itemize}
\item \xmlNode{d}: \xmlDesc{float},
bias, \default{1.0}
\item \xmlNode{learningRate}: \xmlDesc{float},
learning rate: Scale constant for adjusting guesses, \default{0.9}
\end{itemize}

\item \xmlNode{boltzmann}: \xmlDesc{string},
Expand All @@ -767,6 +769,8 @@ \subsection{SimulatedAnnealing}
\begin{itemize}
\item \xmlNode{d}: \xmlDesc{float},
bias, \default{1.0}
\item \xmlNode{learningRate}: \xmlDesc{float},
learning rate: Scale constant for adjusting guesses, \default{0.9}
\end{itemize}
\end{itemize}

Expand Down
110 changes: 68 additions & 42 deletions ravenframework/Distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import copy
import math as math
import bisect
from collections import namedtuple

from .EntityFactoryBase import EntityFactory
from .BaseClasses import BaseEntity, InputDataUser
Expand Down Expand Up @@ -76,6 +77,12 @@ def factorial(x):
'UniformDiscrete' : 'UniformDiscreteDistribution'
}


# Declaring namedtuple(DistributionTypes)
DistributionTypes = namedtuple('DistributionType', ['discrete', 'continuous'])
# Adding values
distType = DistributionTypes('Discrete', 'Continuous')

class DistributionsCollection(InputData.ParameterInput):
"""
Class for reading in a collection of distributions
Expand Down Expand Up @@ -393,7 +400,7 @@ def __init__(self):
"""
super().__init__()
self.dimensionality = 1
self.distType = 'Continuous'
self.distType = distType.continuous

def cdf(self,x):
"""
Expand Down Expand Up @@ -798,7 +805,7 @@ def __init__(self, low=0.0, alpha=0.0, beta=1.0):
self.alpha = alpha
self.beta = beta
self.type = 'Gamma'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('Laguerre')
self.compatibleQuadrature.append('CDF')
Expand Down Expand Up @@ -955,7 +962,7 @@ def __init__(self):
self.alpha = 0.0
self.beta = 0.0
self.type = 'Beta'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('Jacobi')
self.compatibleQuadrature.append('CDF')
Expand Down Expand Up @@ -1129,7 +1136,7 @@ def __init__(self):
self.min = None # domain lower boundary
self.max = None # domain upper boundary
self.type = 'Triangular'
self.distType = 'Continuous'
self.distType = distType.continuous
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
self.preferredPolynomials = 'CDF'
Expand Down Expand Up @@ -1244,7 +1251,7 @@ def __init__(self):
self.mu = 0.0
self.type = 'Poisson'
self.hasInfiniteBound = True
self.distType = 'Discrete'
self.distType = distType.discrete
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
self.preferredPolynomials = 'CDF'
Expand Down Expand Up @@ -1340,7 +1347,7 @@ def __init__(self):
self.p = 0.0
self.type = 'Binomial'
self.hasInfiniteBound = True
self.distType = 'Discrete'
self.distType = distType.discrete
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
self.preferredPolynomials = 'CDF'
Expand Down Expand Up @@ -1439,7 +1446,7 @@ def __init__(self):
super().__init__()
self.p = 0.0
self.type = 'Bernoulli'
self.distType = 'Discrete'
self.distType = distType.discrete
self.lowerBound = 0.0
self.upperBound = 1.0
self.compatibleQuadrature.append('CDF')
Expand Down Expand Up @@ -1532,7 +1539,7 @@ def __init__(self):
super().__init__()
self.p = 0.0
self.type = 'Geometric'
self.distType = 'Discrete'
self.distType = distType.discrete
self.lowerBound = 0.0
self.upperBound = 1.0
self.compatibleQuadrature.append('CDF')
Expand Down Expand Up @@ -1614,6 +1621,11 @@ def getInputSpecification(cls):
StatePartInput = InputData.parameterInputFactory("state", contentType=InputTypes.FloatType)
StatePartInput.addParam("outcome", InputTypes.FloatOrStringType, True)
inputSpecification.addSub(StatePartInput, InputData.Quantity.one_to_infinity)
inputSpecification.addSub(InputData.parameterInputFactory("rtol",
contentType=InputTypes.FloatType,
descr=r"""Relative tolerance used to identify close state in case of"""
r""" float/int states. Not used for string states!""", default=1e-6))


## Because we do not inherit from the base class, we need to manually
## add the name back in.
Expand All @@ -1632,8 +1644,9 @@ def __init__(self):
self.values = set()
self.type = 'Categorical'
self.dimensionality = 1
self.distType = 'Discrete'
self.isFloat = False
self.distType = distType.discrete
self.isFloat = True
self.rtol = 1e-6

def _handleInput(self, paramInput):
"""
Expand All @@ -1642,22 +1655,23 @@ def _handleInput(self, paramInput):
@ Out, None
"""
super()._handleInput(paramInput)
isFloats = []
for child in paramInput.subparts:
if child.getName() == "state":
outcome = child.parameterValues["outcome"]
value = child.value
self.mapping[outcome] = value
try:
float(outcome)
self.isFloat = True
except:
self.isFloat = False
isFloats.append(utils.floatConversion(outcome) is not None)
if outcome in self.values:
self.raiseAnError(IOError,'Categorical distribution has identical outcomes')
else:
self.values.add(float(outcome) if self.isFloat else outcome)
self.values.add(float(outcome) if isFloats[-1] else outcome)
else:
self.raiseAnError(IOError,'Invalid xml node for Categorical distribution; only "state" is allowed')
if False in isFloats:
self.isFloat = False
else:
self.rtol = paramInput.findNodesAndExtractValues(['rtol'])[0]['rtol']
self.initializeDistribution()
self.upperBoundUsed = True
self.lowerBoundUsed = True
Expand Down Expand Up @@ -1690,10 +1704,13 @@ def initializeFromDict(self, inputDict):
@ In, inputDict, dict, dictionary containing the np.arrays for xAxis and pAxis
@ Out, None
"""
isFloats = []
for idx, val in enumerate(inputDict['outcome']):
self.mapping[val] = inputDict['state'][idx]
self.values.add(val)

isFloats.append(utils.floatConversion(val) is not None)
if False in isFloats:
self.isFloat = False
self.checkDistParams()

def initializeDistribution(self):
Expand Down Expand Up @@ -1721,9 +1738,9 @@ def checkDistParams(self):
self.raiseAnError(IOError,'Categorical distribution cannot be initialized with probabilities greater than 1')

localSum = sum(self.mapping.values())
if not mathUtils.compareFloats(localSum,1.0):
self.raiseAnError('Categorical distribution cannot be initialized: sum of probabilities is ',
repr(localSum), ', not 1.0!', 'Please re-normalize it to 1!')
if not mathUtils.compareFloats(localSum,1., self.rtol):
self.raiseAnError(f'Categorical distribution cannot be initialized: sum of probabilities is {localSum},'
' not 1.0! Please re-normalize it to 1!')

# Probability values normalization
for key in self.mapping.keys():
Expand All @@ -1740,10 +1757,19 @@ def pdf(self,x):
else:
if self.isFloat:
vals = sorted(list(self.values))
idx = bisect.bisect(vals, x)
pdfValue = self.mapping[list(vals)[idx]]
idx = [idx for idx in range(len(vals)) if utils.isClose(vals[idx], x, relTolerance=self.rtol)]
if not len(idx):
self.raiseAnError(IOError,f'{self.type} distribution cannot compute pdf for {x} since the closest '
f'state {list(vals)[bisect.bisect(vals, x)]} is outside the acceptance interval given by the provided relative tolerance {self.rtol}!')
idx = idx[0]
val = list(vals)[idx]
pdfValue = self.mapping[val]
if not utils.isClose(val, x, relTolerance=self.rtol):
self.raiseAnError(IOError,f'{self.type} distribution cannot compute pdf for {x} since the closest '
f'state {val} is outside the acceptance interval given by the provided relative tolerance {self.rtol}!')
else:
self.raiseAnError(IOError,'Categorical distribution cannot calculate pdf for ' + str(x))
self.raiseAnError(IOError,f'{self.type} distribution cannot compute pdf for {x} since the states are not floats/integers and the'
f'value {x} is not present in the list of states!!')
return pdfValue

def cdf(self,x):
Expand All @@ -1755,6 +1781,7 @@ def cdf(self,x):
sortedMapping = sorted(self.mapping.items(), key=operator.itemgetter(0))
if x == sortedMapping[-1][0]:
return 1.0

if x in self.values:
cumulative=0.0
for element in sortedMapping:
Expand All @@ -1764,21 +1791,21 @@ def cdf(self,x):
else:
if self.isFloat:
cumulative=0.0
for element in sortedMapping:
cumulative += element[1]
if x >= element[0]:
for idx in range(len(sortedMapping)-1):
cumulative += sortedMapping[idx][1]
if x >= sortedMapping[idx][0] and x <= sortedMapping[idx+1][0]:
return cumulative
# if we reach this point we must error out
self.raiseAnError(IOError,'Categorical distribution cannot calculate cdf for ' + str(x))
self.raiseAnError(IOError,f'{self.type} distribution cannot calculate cdf for ' + str(x))

def ppf(self,x):
"""
Function that calculates the inverse of the cdf given 0 =< x =< 1
@ In, x, float, value to get the ppf at
@ Out, element[0], float/string, requested inverse cdf
"""
if x > 1.0 or x < 0:
self.raiseAnError(IOError,'Categorical distribution cannot calculate ppf for', str(x), '! Valid value should within [0,1]!')
if x > 1. or x < 0.:
self.raiseAnError(IOError,f'{self.type} distribution cannot calculate ppf for', str(x), '! Valid value should within [0,1]!')
sortedMapping = sorted(self.mapping.items(), key=operator.itemgetter(0))
if x == 1.0:
return float(sortedMapping[-1][0]) if self.isFloat else sortedMapping[-1][0]
Expand Down Expand Up @@ -1844,8 +1871,9 @@ def __init__(self):
super().__init__()
self.type = 'UniformDiscrete'
self.dimensionality = 1
self.distType = 'Discrete'
self.distType = distType.discrete
self.memory = True
self.nPoints = None

def _handleInput(self, paramInput):
"""
Expand All @@ -1861,16 +1889,14 @@ def _handleInput(self, paramInput):
self.raiseAnError(IOError,'upperBound value needed for UniformDiscrete distribution')

strategy = paramInput.findFirst('strategy')
if strategy != None:
if strategy is not None:
self.strategy = strategy.value
else:
self.raiseAnError(IOError,'strategy specification needed for UniformDiscrete distribution')

nPoints = paramInput.findFirst('nPoints')
if nPoints != None:
if nPoints is not None:
self.nPoints = nPoints.value
else:
self.nPoints = None

self.initializeDistribution()

Expand Down Expand Up @@ -2036,7 +2062,7 @@ def __init__(self):
"""
super().__init__()
self.dimensionality = 1
self.distType = 'Discrete'
self.distType = distType.discrete
self.type = 'MarkovCategorical'
self.steadyStatePb = None # variable containing the steady state probabilities of the Markov Model
self.transition = None # transition matrix of a continuous time Markov Model
Expand Down Expand Up @@ -2163,7 +2189,7 @@ def __init__(self):
self.location = 0.0
self.scale = 1.0
self.type = 'Logistic'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
Expand Down Expand Up @@ -2273,7 +2299,7 @@ def __init__(self):
self.location = 0.0
self.scale = 1.0
self.type = 'Laplace'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
Expand Down Expand Up @@ -2376,7 +2402,7 @@ def __init__(self):
self.lambdaVar = 1.0
self.low = 0.0
self.type = 'Exponential'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
Expand Down Expand Up @@ -2516,7 +2542,7 @@ def __init__(self):
self.sigma = 1.0
self.low = 0.0
self.type = 'LogNormal'
self.distType = 'Continuous'
self.distType = distType.continuous
self.hasInfiniteBound = True
self.compatibleQuadrature.append('CDF')
self.preferredQuadrature = 'CDF'
Expand Down Expand Up @@ -2632,7 +2658,7 @@ def __init__(self):
self.lambdaVar = 1.0
self.k = 1.0
self.type = 'Weibull'
self.distType = 'Continuous'
self.distType = distType.continuous
self.low = 0.0
self.hasInfiniteBound = True
self.compatibleQuadrature.append('CDF')
Expand Down Expand Up @@ -2754,7 +2780,7 @@ def __init__(self):
self.functionID = None
self.variableID = None
self.dimensionality = 1
self.distType = 'Continuous'
self.distType = distType.continuous
# Scipy.interpolate.UnivariateSpline is used
self.k = 4 # Degree of the smoothing spline, Must be <=5
self.s = 0 # Positive smoothing factor used to choose the number of knots
Expand Down Expand Up @@ -3556,7 +3582,7 @@ def __init__(self):
"""
super().__init__()
self.type = 'MultivariateNormal'
self.distType = 'Continuous'
self.distType = distType.continuous
self.mu = None
self.covariance = None
self.covarianceType = 'abs' # abs: absolute covariance, rel: relative covariance matrix
Expand Down
Loading

0 comments on commit 5c29164

Please sign in to comment.