AMPL Solver supports setting Constant values to variable values for solutions to be deployed
Change-Id: If34b9cfc38e032f043435ac9b8fdeabbca3ee7ba
This commit is contained in:
parent
3ef5b1c32b
commit
d77292357f
160
AMPLSolver.cpp
160
AMPLSolver.cpp
@ -23,9 +23,13 @@ namespace NebulOuS
|
||||
{
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Utility function
|
||||
// Utility functions
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// There are two situations when it is necessary to store a file from a message:
|
||||
// Firstly when the AMPL model is defined, and second every time a data file
|
||||
// is received updating AMPL model parameters. Hence the common file creation
|
||||
// is taken care of by a dedicated function.
|
||||
|
||||
std::string AMPLSolver::SaveFile( const JSON & TheMessage,
|
||||
const std::source_location & Location )
|
||||
@ -77,17 +81,77 @@ std::string AMPLSolver::SaveFile( const JSON & TheMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// Setting named AMPL parameters from JSON objects requires that the JSON object
|
||||
// is converted to the same type as the AMPL parameter. This conversion
|
||||
// requires that the type of the parameter is tested, and there is a shared
|
||||
// function to set a named parameter from the JSON object.
|
||||
|
||||
void AMPLSolver::SetAMPLParameter( const std::string & ParameterName,
|
||||
const JSON & ParameterValue )
|
||||
{
|
||||
ampl::Parameter
|
||||
TheParameter = ProblemDefinition.getParameter( ParameterName );
|
||||
|
||||
switch ( ParameterValue.type() )
|
||||
{
|
||||
case JSON::value_t::number_integer :
|
||||
case JSON::value_t::number_unsigned :
|
||||
case JSON::value_t::boolean :
|
||||
TheParameter.set( ParameterValue.get< long >() );
|
||||
break;
|
||||
case JSON::value_t::number_float :
|
||||
TheParameter.set( ParameterValue.get< double >() );
|
||||
break;
|
||||
case JSON::value_t::string :
|
||||
TheParameter.set( ParameterValue.get< std::string >() );
|
||||
break;
|
||||
default:
|
||||
{
|
||||
std::source_location Location = std::source_location::current();
|
||||
std::ostringstream ErrorMessage;
|
||||
|
||||
ErrorMessage << "[" << Location.file_name() << " at line "
|
||||
<< Location.line()
|
||||
<< "in function " << Location.function_name() <<"] "
|
||||
<< "The JSON value " << ParameterValue
|
||||
<< " has JSON type "
|
||||
<< static_cast< int >( ParameterValue.type() )
|
||||
<< " which is not supported"
|
||||
<< std::endl;
|
||||
|
||||
throw std::invalid_argument( ErrorMessage.str() );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Optimisation
|
||||
// Problem definition
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// The first step in solving an optimisation problem is to define the problme
|
||||
// involving the decision variables, the parameters, and the constraints over
|
||||
// these entities. The problem is received as an AMQ JSON message where where
|
||||
// the only key is the file name and the value is the AMPL model file. This file
|
||||
// is first saved, and if there is no exception thrown form the save file
|
||||
// function, the filename will be returned and read back into the problem
|
||||
// definition.
|
||||
// these entities. The AMPL Domoain Specific Language (DSL) defining the
|
||||
// problem is received as a JSON message where the File Name and the File
|
||||
// Content is managed by the file reader utility function.
|
||||
//
|
||||
// After reading the file the name of the default objective function is taken
|
||||
// from the message. Not that this is a mandatory field and the solver will
|
||||
// throw an exception if the field does not exist.
|
||||
//
|
||||
// Finally, the optimisation happens relative to the current configuration as
|
||||
// baseline aiming to improve the variable values. However, this may need that
|
||||
// candidate variable values are compared with the current values of the same
|
||||
// variables. Hence, the current variable values are defined to be "constants"
|
||||
// of the optimisation problem. These constants must be set by the solver for
|
||||
// a found solution that will be deployed, and this requires a mapping between
|
||||
// the name of a constant and the name of the variable used to initialise the
|
||||
// constant. This map is initialised from the message, if it is provided, and
|
||||
// the initial values are set for the corresponding "constant" parameters in
|
||||
// the problem definition. The constant field holds a JSON map where the keys
|
||||
// are the names of the constants defined as parameters in the problem
|
||||
// definition, and the value is again a map with two fields: The variable name
|
||||
// and the variable's intial value.
|
||||
|
||||
void AMPLSolver::DefineProblem(const Solver::OptimisationProblem & TheProblem,
|
||||
const Address TheOracle)
|
||||
@ -97,7 +161,13 @@ void AMPLSolver::DefineProblem(const Solver::OptimisationProblem & TheProblem,
|
||||
<< TheProblem.dump(2)
|
||||
<< std::endl;
|
||||
|
||||
//ProblemDefinition.read( SaveFile( TheProblem ) );
|
||||
// First storing the AMPL problem file from its definition in the message
|
||||
// and read the file back to the AMPL interpreter.
|
||||
|
||||
ProblemDefinition.read( SaveFile( TheProblem ) );
|
||||
|
||||
// The next is to read the label of the default objective function and
|
||||
// store this. An invalid argument exception is thrown if the field is missing
|
||||
|
||||
if( TheProblem.contains( Solver::ObjectiveFunctionLabel ) )
|
||||
DefaultObjectiveFunction = TheProblem.at( Solver::ObjectiveFunctionLabel );
|
||||
@ -117,9 +187,27 @@ void AMPLSolver::DefineProblem(const Solver::OptimisationProblem & TheProblem,
|
||||
throw std::invalid_argument( ErrorMessage.str() );
|
||||
}
|
||||
|
||||
// After all the manatory fields have been processed, the set of constants
|
||||
// will be processed storing the mapping from variable value to constant.
|
||||
|
||||
if( TheProblem.contains( ConstantsLabel ) &&
|
||||
TheProblem.at( ConstantsLabel ).is_object() )
|
||||
for( const auto & [ ConstantName, ConstantRecord ] :
|
||||
TheProblem.at( ConstantsLabel ).items() )
|
||||
{
|
||||
VariablesToConstants.emplace( ConstantRecord.at( VariableName ),
|
||||
ConstantName );
|
||||
SetAMPLParameter( ConstantName,
|
||||
ConstantRecord.at( InitialConstantValue ) );
|
||||
}
|
||||
|
||||
Output << "Problem loaded!" << std::endl;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Optimimsation parameter values
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// The data file(s) corresponding to the current optimisation problem will be
|
||||
// sent in the same way and separately file by file. The logic is the same as
|
||||
// the Define Problem message handler: The save file is used to store the
|
||||
@ -131,6 +219,10 @@ void AMPLSolver::DataFileUpdate( const DataFileMessage & TheDataFile,
|
||||
ProblemDefinition.readData( SaveFile( TheDataFile ) );
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Solving
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// The solver function is more involved as must set the metric values received
|
||||
// in the application execution context message as parameter values for the
|
||||
// optimisation problem, then solve for the optimal objective value, and finally
|
||||
@ -146,41 +238,7 @@ void AMPLSolver::SolveProblem(
|
||||
|
||||
for( const auto & [ TheName, MetricValue ] :
|
||||
Solver::MetricValueType( TheContext.at( Solver::ExecutionContext ) ) )
|
||||
{
|
||||
ampl::Parameter TheParameter = ProblemDefinition.getParameter( TheName );
|
||||
|
||||
switch ( MetricValue.type() )
|
||||
{
|
||||
case JSON::value_t::number_integer :
|
||||
case JSON::value_t::number_unsigned :
|
||||
case JSON::value_t::boolean :
|
||||
TheParameter.set( MetricValue.get< long >() );
|
||||
break;
|
||||
case JSON::value_t::number_float :
|
||||
TheParameter.set( MetricValue.get< double >() );
|
||||
break;
|
||||
case JSON::value_t::string :
|
||||
TheParameter.set( MetricValue.get< std::string >() );
|
||||
break;
|
||||
default:
|
||||
{
|
||||
std::source_location Location = std::source_location::current();
|
||||
std::ostringstream ErrorMessage;
|
||||
|
||||
ErrorMessage << "[" << Location.file_name() << " at line "
|
||||
<< Location.line()
|
||||
<< "in function " << Location.function_name() <<"] "
|
||||
<< "The JSON value " << MetricValue
|
||||
<< " has JSON type "
|
||||
<< static_cast< int >( MetricValue.type() )
|
||||
<< " which is not supported"
|
||||
<< std::endl;
|
||||
|
||||
throw std::invalid_argument( ErrorMessage.str() );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
SetAMPLParameter( TheName, MetricValue );
|
||||
|
||||
// Setting the given objective as the active objective and all other
|
||||
// objective functions as 'dropped'. Note that this is experimental code
|
||||
@ -253,19 +311,31 @@ void AMPLSolver::SolveProblem(
|
||||
for( auto TheObjective : ProblemDefinition.getObjectives() )
|
||||
ObjectiveValues.emplace( TheObjective.name(), TheObjective.value() );
|
||||
|
||||
// The variable values are obtained in the same way
|
||||
// The variable values are obtained in the same way, but each variable
|
||||
// is checked to see if there is a constant that has to be initialised
|
||||
// with the variable value. The AMPL parameter whose name corresponds
|
||||
// with the constant name mapped from the variable name, will then
|
||||
// be initialised. The constant values are only to be updated if the
|
||||
// application execution context has the deployment flag set.
|
||||
|
||||
Solver::Solution::VariableValuesType VariableValues;
|
||||
bool DeploymentFlagSet = TheContext.at( DeploymentFlag ).get<bool>();
|
||||
|
||||
for( auto Variable : ProblemDefinition.getVariables() )
|
||||
{
|
||||
VariableValues.emplace( Variable.name(), Variable.value() );
|
||||
|
||||
if( DeploymentFlagSet && VariablesToConstants.contains( Variable.name() ) )
|
||||
SetAMPLParameter( VariablesToConstants.at( Variable.name() ),
|
||||
JSON( Variable.value() ) );
|
||||
}
|
||||
|
||||
// The found solution can then be returned to the requesting actor or topic
|
||||
|
||||
Send( Solver::Solution(
|
||||
TheContext.at( Solver::TimeStamp ).get< Solver::TimePointType >(),
|
||||
OptimisationGoal, ObjectiveValues, VariableValues,
|
||||
TheContext.at( DeploymentFlag ).get<bool>()
|
||||
DeploymentFlagSet
|
||||
), TheRequester );
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ License: MPL2.0 (https://www.mozilla.org/en-US/MPL/2.0/)
|
||||
#include <list> // To store names
|
||||
#include <filesystem> // For problem files
|
||||
#include <source_location> // For better errors
|
||||
#include <map> // Storing key-value pairs
|
||||
|
||||
// Other packages
|
||||
|
||||
@ -101,6 +102,12 @@ private:
|
||||
const std::source_location & Location
|
||||
= std::source_location::current() );
|
||||
|
||||
// There is also a utility function to look up a named AMPL parameter and
|
||||
// sets it value based on a JSON scalar value.
|
||||
|
||||
void SetAMPLParameter( const std::string & ParameterName,
|
||||
const JSON & ParameterValue );
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// The optimisation problem
|
||||
// --------------------------------------------------------------------------
|
||||
@ -129,13 +136,22 @@ protected:
|
||||
static constexpr std::string_view AMPLProblemTopic
|
||||
= "eu.nebulouscloud.optimiser.solver.model";
|
||||
|
||||
// The JSON message received on this topic is supposed to contain three keys
|
||||
// The JSON message received on this topic is supposed to contain several
|
||||
// keys in the JSON message
|
||||
// 1) The filename of the problem file
|
||||
// 2) The file content as a single string
|
||||
// 3) The default objective function (defined in the Solver class)
|
||||
// 4) An optional constants section containing constant names as keys
|
||||
// and the values will be another map containing the variable
|
||||
// whose value should be passed to the constant, and the initial
|
||||
// value of the constant.
|
||||
|
||||
static constexpr std::string_view FileName = "FileName",
|
||||
FileContent = "FileContent";
|
||||
static constexpr std::string_view
|
||||
FileName = "FileName",
|
||||
FileContent = "FileContent",
|
||||
ConstantsLabel = "Constants",
|
||||
VariableName = "Variable",
|
||||
InitialConstantValue = "Value";
|
||||
|
||||
// The AMPL problem file can contain many objective functions, but can be
|
||||
// solved only for one objective function at the time. The name of the
|
||||
@ -148,6 +164,12 @@ private:
|
||||
|
||||
std::string DefaultObjectiveFunction;
|
||||
|
||||
// To set the constant values to the right variable values, the mapping
|
||||
// between the variable name and the constant name must be stored in
|
||||
// a map.
|
||||
|
||||
std::map< std::string, std::string > VariablesToConstants;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Data file updates
|
||||
// --------------------------------------------------------------------------
|
||||
|
@ -284,8 +284,8 @@ public:
|
||||
{
|
||||
public:
|
||||
|
||||
static constexpr std::string_view MessageIdentifier
|
||||
= "eu.nebulouscloud.optimiser.solver.model";
|
||||
static constexpr std::string_view
|
||||
MessageIdentifier = "eu.nebulouscloud.optimiser.solver.model";
|
||||
|
||||
OptimisationProblem( const JSON & TheProblem )
|
||||
: JSONTopicMessage( std::string( MessageIdentifier ), TheProblem )
|
||||
|
Loading…
x
Reference in New Issue
Block a user