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

Mandd/pareto front PP prestruct #1479

Merged
merged 10 commits into from
Mar 18, 2021
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
35 changes: 11 additions & 24 deletions doc/user_manual/postprocessor.tex
Original file line number Diff line number Diff line change
Expand Up @@ -1863,50 +1863,37 @@ \subsubsection{RavenOutput}

\subsubsection{ParetoFrontier}
\label{ParetoFrontierPP}
The \textbf{ParetoFrontier} post-processor is designed to identify the points lying on the Pareto Frontier in a cost-value space.
This post-processor receives as input a \textbf{DataObject} (a PointSet only) which contains all data points in the cost-value space and it
The \textbf{ParetoFrontier} post-processor is designed to identify the points lying on the Pareto Frontier in a multi-dimensional trade-space.
This post-processor receives as input a \textbf{DataObject} (a PointSet only) which contains all data points in the trade-space space and it
returns the subset of points lying in the Pareto Frontier as a PointSet.

It is here assumed that each data point of the input PointSet is a realization of the system under consideration for a
specific configuration to which corresponds a cost and a value.
specific configuration to which corresponds several objective variables (e.g., cost and value).

%
\ppType{ParetoFrontier}{ParetoFrontier}
%
\begin{itemize}
\item \xmlNode{costID},\xmlDesc{string, required parameter}, ID of the input PointSet variable that is considered the cost variable.
\item \xmlNode{objective},\xmlDesc{string, required parameter}, ID of the objective variable that represents a dimension of the trade-space space.
The \xmlNode{costID} requires one identifying attribute:
\begin{itemize}
\item \xmlAttr{inv}, \xmlDesc{boolean, required field}, False if costID represents a cost, True if costID represents a saving
\end{itemize}
\item \xmlNode{valueID},\xmlDesc{string, required parameter}, ID of the input PointSet variable that is considered the value variable
The \xmlNode{valueID} requires one identifying attribute:
\begin{itemize}
\item \xmlAttr{inv}, \xmlDesc{boolean, required field}, False if valueID represents a value, True if valueID represents a loss of value
\end{itemize}
\item \xmlNode{costLimit},\xmlDesc{float, optional parameter}, maximum value of the cost variable to be considered in the pareto frontier
The \xmlNode{costLimit} requires one identifying attribute:
\begin{itemize}
\item \xmlAttr{type}, \xmlDesc{string, required field}, this attribute can be either ``lower" or ``upper'', it indicates if the limit is an upper or lower limit
\end{itemize}
\item \xmlNode{valueLimit},\xmlDesc{float, optional parameter}, minimum value of the value variable to be considered in the pareto frontier
The \xmlNode{valueLimit} requires one identifying attribute:
\begin{itemize}
\item \xmlAttr{type}, \xmlDesc{string, required field}, this attribute can be either ``lower" or ``upper'', it indicates if the limit is an upper or lower limit
\item \xmlAttr{goal}, \xmlDesc{string, required field}, Goal of the objective variable characteristic: minimzation (min) or maximization (max)
\item \xmlAttr{upperLimit}, \xmlDesc{string, optional field}, Desired upper limit of the objective variable for the points in the Pareto frontier
\item \xmlAttr{lowerLimit}, \xmlDesc{string, optional field}, Desired lower limit of the objective variable for the points in the Pareto frontier
\end{itemize}
\end{itemize}

The following is an example where a set of realizations (the ``candidates'' PointSet) has been generated by changing two parameters
(var1 and var2) which produced two output variables (cost and value).
(var1 and var2) which produced two output variables: cost (which it is desired to be minimized) and value (which it is desired to be maximized).
The \textbf{ParetoFrontier} post-processor takes the ``candidates'' PointSet and populates a Point similar in structure
(the ``paretoPoints'' PointSet).

\textbf{Example:}
\begin{lstlisting}[style=XML,morekeywords={anAttribute},caption=ParetoFrontier input example (no expand)., label=lst:ParetoFrontier_PP_InputExample]
<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID>cost</costID>
<valueID>value</valueID>
<objective goal='min' upperLimit='0.5'>cost</objective>
<objective goal='max' lowerLimit='0.5'>value</objective>
</PostProcessor>
</Models>

Expand All @@ -1930,7 +1917,7 @@ \subsubsection{ParetoFrontier}
</DataObjects>
\end{lstlisting}

\nb it is possible to specify upper and lower limits for the cost and value variable respectively.
\nb it is possible to specify both upper and lower limits for each objective variable.
When one or both of these limits are specified, then the pareto frontier is filtered such that all pareto frontier points that
satisfy those limits are preserved.

Expand Down
92 changes: 31 additions & 61 deletions framework/Models/PostProcessors/ParetoFrontierPostProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .PostProcessor import PostProcessor
from utils import utils
from utils import InputData, InputTypes
from utils import frontUtils
import Runners
#Internal Modules End-----------------------------------------------------------

Expand All @@ -47,6 +48,7 @@ def __init__(self, runInfoDict):
self.validDataType = ['PointSet'] # The list of accepted types of DataObject
self.outputMultipleRealizations = True # True indicate multiple realizations are returned


@classmethod
def getInputSpecification(cls):
"""
Expand All @@ -58,48 +60,31 @@ class cls.
"""
inputSpecification = super(ParetoFrontier, cls).getInputSpecification()

costIDInput = InputData.parameterInputFactory("costID", contentType=InputTypes.StringType)
costIDInput.addParam("inv", InputTypes.BoolType, True)
inputSpecification.addSub(costIDInput)

valueIDInput = InputData.parameterInputFactory("valueID", contentType=InputTypes.StringType)
valueIDInput.addParam("inv", InputTypes.BoolType, True)
inputSpecification.addSub(valueIDInput)

costLimitInput = InputData.parameterInputFactory("costLimit", contentType=InputTypes.FloatType)
costLimitInput.addParam("type", InputTypes.StringType, True)
inputSpecification.addSub(costLimitInput)
objDataType = InputTypes.makeEnumType("objective", "objectiveType", ['min','max'])

valueLimitInput = InputData.parameterInputFactory("valueLimit", contentType=InputTypes.FloatType)
valueLimitInput.addParam("type", InputTypes.StringType, True)
inputSpecification.addSub(valueLimitInput)
objective = InputData.parameterInputFactory('objective', contentType=InputTypes.StringType)
objective.addParam('goal', param_type=objDataType, required=True)
objective.addParam('upperLimit', param_type=InputTypes.FloatType, required=False)
objective.addParam('lowerLimit', param_type=InputTypes.FloatType, required=False)
inputSpecification.addSub(objective)

return inputSpecification


def _handleInput(self, paramInput):
"""
Function to handle the parsed paramInput for this class.
@ In, paramInput, ParameterInput, the already-parsed input.
@ Out, None
"""
PostProcessor._handleInput(self, paramInput)
costID = paramInput.findFirst('costID')
self.costID = costID.value
self.invCost = costID.parameterValues['inv']

valueID = paramInput.findFirst('valueID')
self.valueID = valueID.value
self.invValue = valueID.parameterValues['inv']
self.objectives = {}

costLimit = paramInput.findFirst('costLimit')
if costLimit is not None:
self.costLimit = costLimit.value
self.costLimitType = costLimit.parameterValues['type']

valueLimit = paramInput.findFirst('valueLimit')
if valueLimit is not None:
self.valueLimit = valueLimit.value
self.valueLimitType = valueLimit.parameterValues['type']
for child in paramInput.subparts:
if child.getName() == 'objective':
self.objectives[child.value]={}
self.objectives[child.value]['goal'] = child.parameterValues['goal']
self.objectives[child.value]['upperLimit'] = child.parameterValues.get('upperLimit')
self.objectives[child.value]['lowerLimit'] = child.parameterValues.get('lowerLimit')


def inputToInternal(self, currentInp):
Expand All @@ -119,6 +104,7 @@ def inputToInternal(self, currentInp):
.format(self.name, currentInp.type))
return currentInp


def run(self, inputIn):
"""
This method executes the postprocessor action.
Expand All @@ -128,40 +114,24 @@ def run(self, inputIn):
inData = self.inputToInternal(inputIn)
data = inData.asDataset()

if self.invCost:
data[self.costID] = (-1.) * data[self.costID]
if self.invValue:
data[self.valueID] = (-1.) * data[self.valueID]

sortedData = data.sortby(self.costID)
coordinates = np.zeros(1,dtype=int)
for index,elem in enumerate(sortedData[self.costID].values):
if (index>1) and (sortedData[self.valueID].values[index]>sortedData[self.valueID].values[coordinates[-1]]):
# the point at index is part of the pareto frontier
coordinates = np.append(coordinates,index)

selection = sortedData.isel(RAVEN_sample_ID=coordinates)

if self.invCost:
selection[self.costID] = (-1.) * selection[self.costID]
if self.invValue:
selection[self.valueID] = (-1.) * selection[self.valueID]

if self.valueLimit is not None:
if self.valueLimitType=="upper":
selection = selection.where(selection[self.valueID]<=self.valueLimit)
else:
selection = selection.where(selection[self.valueID]>=self.valueLimit)
if self.costLimit is not None:
if self.costLimitType=="upper":
selection = selection.where(selection[self.costID]<=self.costLimit)
else:
selection = selection.where(selection[self.costID]>=self.costLimit)
dataTemp = data[list(self.objectives.keys())]
for index,obj in enumerate(self.objectives.keys()):
if self.objectives[obj]['goal']=='max':
dataTemp[obj] = (-1.) * dataTemp[obj]

paretoFrontMask = frontUtils.nonDominatedFrontier(np.transpose(dataTemp.to_array().values), returnMask=False)
selection = data.isel(RAVEN_sample_ID=np.array(paretoFrontMask))

for obj in self.objectives.keys():
if self.objectives[obj]['upperLimit']:
selection = selection.where(selection[obj]<=self.objectives[obj]['upperLimit'])
if self.objectives[obj]['lowerLimit']:
selection = selection.where(selection[obj]>=self.objectives[obj]['lowerLimit'])
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

edited as well


filteredParetoFrontier = selection.to_array().values
paretoFrontierData = np.transpose(filteredParetoFrontier)
paretoFrontierDict = {}
for index,varID in enumerate(sortedData.data_vars):
for index,varID in enumerate(data.data_vars):
paretoFrontierDict[varID] = paretoFrontierData[:,index]
paretoFrontierDict = {'data':paretoFrontierDict, 'dims':{}}
return paretoFrontierDict
Expand Down
12 changes: 11 additions & 1 deletion framework/utils/frontUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@



def nonDominatedFrontier(data, returnMask):
def nonDominatedFrontier(data, returnMask, minMask=None):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@wangcj05 I honestly feel like the required structure of returnMask and minMask might not be optimal, if you have comments/suggestions let me know

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, I did not get your point here. Could you clarify it?

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 was not sure if passing the data as numpy array and min mask as a separate numpy array was the best way to pass this info to this method

"""
This method is designed to identify the set of non-dominated points (nEfficientPoints)

Expand All @@ -38,11 +38,21 @@ def nonDominatedFrontier(data, returnMask):

@ In, data, np.array, data matrix (nPoints, nCosts) containing the data points
@ In, returnMask, bool, type of data to be returned: indices (False) or True/False mask (True)
@ Out, minMask, np.array, array (nCosts,1) of boolean values: True (dimension need to be minimized), False (dimension need to be maximized)
@ Out, isEfficientMask , np.array, data matrix (nPoints,1), array of boolean values if returnMask=True
@ Out, isEfficient, np.array, data matrix (nEfficientPoints,1), integer array of indexes if returnMask=False

Reference: the following code has been adapted from https://stackoverflow.com/questions/32791911/fast-calculation-of-pareto-front-in-python
"""
if minMask is None:
pass
elif minMask is not None and minMask.shape[0] != data.shape[1]:
raise IOError("nonDominatedFrontier method: Data features do not match minMask dimensions: data has shape " + str(data.shape) + " while minMask has shape " + str(minMask.shape))
else:
for index,elem in np.ndenumerate(minMask):
if not elem:
data[:,index] = -1. * data[:,index]

isEfficient = np.arange(data.shape[0])
nPoints = data.shape[0]
nextPointIndex = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID inv="False" >cost</costID>
<valueID inv="False" >value</valueID>
<objective goal='min'>cost</objective>
<objective goal='max'>value</objective>
</PostProcessor>
</Models>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID inv="True" >out2</costID>
<valueID inv="True" >out1</valueID>
<objective goal='max'>out2</objective>
<objective goal='min'>out1</objective>
</PostProcessor>
</Models>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@

<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID inv="False" >cost</costID>
<valueID inv="False" >value</valueID>
<valueLimit type="lower">0.5</valueLimit>
<costLimit type="upper">0.5</costLimit>
<objective goal='min' upperLimit='0.5'>cost</objective>
<objective goal='max' lowerLimit='0.5'>value</objective>
</PostProcessor>
</Models>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@

<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID inv="False" >cost</costID>
<valueID inv="False" >value</valueID>
<costLimit type="upper">0.5</costLimit>
<objective goal='min' upperLimit='0.5'>cost</objective>
<objective goal='max'>value</objective>
</PostProcessor>
</Models>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@

<Models>
<PostProcessor name="paretoPP" subType="ParetoFrontier">
<costID inv="False" >cost</costID>
<valueID inv="False" >value</valueID>
<valueLimit type="lower">0.5</valueLimit>
<objective goal='min'>cost</objective>
<objective goal='max' lowerLimit='0.5'>value</objective>
</PostProcessor>
</Models>

Expand Down
10 changes: 5 additions & 5 deletions tests/framework/PostProcessors/ParetoFrontierPostProcessor/tests
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
[./ParetoFrontier]
type = 'RavenFramework'
input = 'test_paretoFrontier.xml'
csv = 'ParetoFrontier/PrintPareto.csv'
UnorderedCsv = 'ParetoFrontier/PrintPareto.csv'
[../]
[./ParetoFrontierWithCostLimit]
type = 'RavenFramework'
input = 'test_paretoFrontierWithCostLimit.xml'
csv = 'ParetoFrontier/PrintParetoCostLimit.csv'
UnorderedCsv = 'ParetoFrontier/PrintParetoCostLimit.csv'
[../]
[./ParetoFrontierWithValueLimit]
type = 'RavenFramework'
input = 'test_paretoFrontierWithValueLimit.xml'
csv = 'ParetoFrontier/PrintParetoValueLimit.csv'
UnorderedCsv = 'ParetoFrontier/PrintParetoValueLimit.csv'
[../]
[./ParetoFrontierWithBothLimits]
type = 'RavenFramework'
input = 'test_paretoFrontierWithBothLimits.xml'
csv = 'ParetoFrontier/PrintParetoBothLimits.csv'
UnorderedCsv = 'ParetoFrontier/PrintParetoBothLimits.csv'
[../]
[./ParetoFrontierInverted]
type = 'RavenFramework'
input = 'test_paretoFrontierInverted.xml'
csv = 'ParetoFrontier/PrintParetoInverted.csv'
UnorderedCsv = 'ParetoFrontier/PrintParetoInverted.csv'
[../]
[]

Expand Down
Loading