optimiser-solver/Solver.hpp
Geir Horn 58b37118fb First complete Solver release
Change-Id: Icd5a77c6400a57db93bdd65e61a022af95f8bacb
2024-04-18 11:15:06 +02:00

395 lines
16 KiB
C++

/*==============================================================================
Solver
The solver is a generic base class for all solvers defining the interface with
the Solution Manager actor. The solver reacts to an Application Execution
Context messagedefined in the class. The application execution context is
defined to be independent metric values that has no, or little, correlation
with the application configuration and that are involved in the utility
expression(s) or in the constraints of the optimisation problem.
Receiving this message triggers the search for an optimial solution to the
given named objective. Once the solution is found, the Solution message should
be returned to the actor making the request. The solution message will contain
the configuration being the feasible assignment to all variables of the
problem, all the objective values in this problem, and the identifier for the
application execution context.
The messages are essentially JSON objects defined using Niels Lohmann's
library [1] and in particular the AMQ extension for the Theron++ Actor
library [2] allowing JSON messages to be transmitted as Qpid Proton AMQ [3]
messages to renote requestors.
References:
[1] https://github.com/nlohmann/json
[2] https://github.com/GeirHo/TheronPlusPlus
[3] https://qpid.apache.org/proton/
Author and Copyright: Geir Horn, University of Oslo
Contact: Geir.Horn@mn.uio.no
License: MPL2.0 (https://www.mozilla.org/en-US/MPL/2.0/)
==============================================================================*/
#ifndef NEBULOUS_SOLVER
#define NEBULOUS_SOLVER
// Standard headers
#include <string_view> // Constant strings
#include <string> // Normal strings
#include <unordered_map> // To store metric-value maps
#include <concepts> // To test template parameters
// Other packages
#include <nlohmann/json.hpp> // JSON object definition
using JSON = nlohmann::json; // Short form name space
// Theron++ headers
#include "Actor.hpp" // Actor base class
#include "Utility/StandardFallbackHandler.hpp" // Exception unhanded messages
#include "Communication/PolymorphicMessage.hpp" // The network message type
#include "Communication/NetworkingActor.hpp" // External communications
// AMQ communication headers
#include "Communication/AMQ/AMQjson.hpp" // For JSON metric messages
#include "Communication/AMQ/AMQEndpoint.hpp" // Enabling AMQ communication
#include "Communication/AMQ/AMQSessionLayer.hpp" // For topic subscriptions
namespace NebulOuS
{
/*==============================================================================
Solver Actor
==============================================================================*/
class Solver
: virtual public Theron::Actor,
virtual public Theron::StandardFallbackHandler,
virtual public Theron::NetworkingActor<
typename Theron::AMQ::Message::PayloadType >
{
public:
// --------------------------------------------------------------------------
// Application Execution Context
// --------------------------------------------------------------------------
//
// To ensure that the execution context is correctly provided by the senders
// The expected metric value structure is defined as a type based on the
// standard unsorted map based on a JSON value object since this can hold
// various value types.
using MetricValueType = std::unordered_map< std::string, JSON >;
// The identification type for the application execution context is defined
// so that other classes may use it, but also so that it can be easily
// changed if needed. It is assumed that the type must have a hash function
// so that the type can be used in ordered data structures.
using ContextIdentifierType = std::string;
// The same goes for the time point type. This is defined as the number of
// microseconds since the POSIX time epoch (1 January 1970) and stored as a
// long integral value.
using TimePointType = unsigned long long;
// The message is a simple JSON object where the various fields of the
// message struct are set by the constructor to ensure that all fields are
// given when the message is constructed. The message is a JSON Topic Message
// received on the topic with the same name as the message identifier.
class ApplicationExecutionContext
: public Theron::AMQ::JSONTopicMessage
{
public:
// First the topic on which these messages will arrive is defined so that
// it can be used when subscribing.
static constexpr std::string_view AMQTopic
= "eu.nebulouscloud.optimiser.solver.context";
// The keys used in the JSON message to send are defined first:
//
// "Timestamp" : This is the field giving the implicit order of the
// different application execution execution contexts waiting for being
// solved when there are more requests than there are solvers available
// to work on the different problems.
// "ObjectFunction" : There is also a definition for the objective function
// label since a multi-objective optimisation problem can have multiple
// objective functions and the solution is found for only one of these
// functions at the time even though all objective function values will
// be returned with the solution, the solution will maximise only the
// objective function whose label is given in the application execution
// context request message. The Application Execution Cntext message may
// contain the name of the objective function to maximise. If so, this
// should be stored under the key name indicated here. However, if the
// objective function name is not given, the default objective function
// is used. The default objective function will be named when defining
// the optimisation problem.
// "ExecutionContext" : Defines all the metric name and value pairs that
// define the actual execution context. Note that there must be at least
// one metric-value pair for the request to be valid.
// "DeploySolution" : The execution context can come from the Metric
// Collector actor as a consequence of an SLO Violation being detected.
// In this case the optimised solution found by the solver should trigger
// a reconfiguration. However, various application execution context can
// also be tried for simulating future events and to investigate which
// configuration would be the best for these situations. In this case the
// optimised solution should not reconfigure the running application. For
// this reason there is a flag in the message indicating whether the
// solution should be deployed, and its default value is 'false' to
// prevent solutions form accidentially being deployed.
struct Keys
{
static constexpr std::string_view
TimeStamp = "Timestamp",
ObjectiveFunctionLabel = "ObjectiveFunction",
ExecutionContext = "ExecutionContext",
DeploymentFlag = "DeploySolution";
};
// The full constructor takes the time point, the objective function to
// solve for, and the application's execution context as the metric map
ApplicationExecutionContext( const TimePointType MicroSecondTimePoint,
const std::string ObjectiveFunctionID,
const MetricValueType & TheContext,
bool DeploySolution = false )
: JSONTopicMessage( std::string( AMQTopic ),
{ { Keys::TimeStamp, MicroSecondTimePoint },
{ Keys::ObjectiveFunctionLabel, ObjectiveFunctionID },
{ Keys::ExecutionContext, TheContext },
{ Keys::DeploymentFlag, DeploySolution }
}) {}
// The constructor omitting the objective function identifier is similar
// but without the objective function string implying that the default
// objective function should be used.
ApplicationExecutionContext( const TimePointType MicroSecondTimePoint,
const MetricValueType & TheContext,
bool DeploySolution = false )
: JSONTopicMessage( std::string( AMQTopic ),
{ { Keys::TimeStamp, MicroSecondTimePoint },
{ Keys::ExecutionContext, TheContext },
{ Keys::DeploymentFlag, DeploySolution }
}) {}
// The copy constructor simply passes the job on to the JSON Topic
// message for copying the message
ApplicationExecutionContext( const ApplicationExecutionContext & Other )
: JSONTopicMessage( Other )
{}
// The default constructor simply stores the message identifier
ApplicationExecutionContext()
: JSONTopicMessage( std::string( AMQTopic ) )
{}
// The default destrucor is used
virtual ~ApplicationExecutionContext() = default;
};
// The handler for this message is virtual as it is where the real action
// will happen and the search for the optimal solution will hopefully lead
// to a feasible soltuion that can be returned to the sender of the applicaton
// context.
protected:
virtual void SolveProblem( const ApplicationExecutionContext & TheContext,
const Address TheRequester ) = 0;
// --------------------------------------------------------------------------
// Solution
// --------------------------------------------------------------------------
//
// When a solution is found to a given problem, the solver should return the
// found optimal value for the given objective function, It should return
// this value together with the values assigned to the feasible variables
// leading to this optimal objective value. Additionally, the message will
// contain the time point for which this solution is valid, and the
// application execution context as the optimal solution is conditioned
// on this solution.
//
// Since the probelm being resolved can be multi-objective, the values of all
// objective values will be returned as a JSON map where the attributes are
// the names of the objective functions in the optimisation problem, and the
// values are the ones assigned by the optimiser. This JSON map object is
// passed under the global attribute "ObjectiveValues"
public:
class Solution
: public Theron::AMQ::JSONTopicMessage
{
public:
// There are some aliases that can be used in other Actors processing this
// message in order to ensure portability
using ObjectiveValuesType = MetricValueType;
using VariableValuesType = MetricValueType;
// The topic for which the message is published is defined first
static constexpr std::string_view AMQTopic
= "eu.nebulouscloud.optimiser.solver.solution";
// Most of the message keys are the same as for the application execution
// context, but there are two new:
//
// "ObjectiveValues" : This holds a map of objective function names and
// their values under the currently found solution which is optimised
// for the given objective function or the default objective function.
// The other objective values is useful if one is searching for the
// Pareto front of the problem.
// "VariableValues" : This key is a map holding the variable names and
// their values found by the solver for the optimal solution. This is
// used to reconfigure the application.
struct Keys : public ApplicationExecutionContext::Keys
{
static constexpr std::string_view
ObjectiveValues = "ObjectiveValues",
VariableValues = "VariableValues";
};
Solution( const TimePointType MicroSecondTimePoint,
const std::string ObjectiveFunctionID,
const ObjectiveValuesType & TheObjectiveValues,
const VariableValuesType & TheVariables,
bool DeploySolution )
: JSONTopicMessage( std::string( AMQTopic ) ,
{ { Keys::TimeStamp, MicroSecondTimePoint },
{ Keys::ObjectiveFunctionLabel, ObjectiveFunctionID },
{ Keys::ObjectiveValues, TheObjectiveValues },
{ Keys::VariableValues, TheVariables },
{ Keys::DeploymentFlag, DeploySolution }
} )
{}
Solution()
: JSONTopicMessage( std::string( AMQTopic ) )
{}
virtual ~Solution() = default;
};
// --------------------------------------------------------------------------
// Optimisation problem definition
// --------------------------------------------------------------------------
//
// There are many ways the optimisation problem can be passed to the solver,
// and it is therefore not possible to give an exact format for the message
// to define or update the optimisation problem. The message is basically
// left as a JSON message and it will be up to the actual solver algorithm
// to implement this in a way appropriate for the algorithm.
class OptimisationProblem
: public Theron::AMQ::JSONTopicMessage
{
public:
static constexpr std::string_view AMQTopic
= "eu.nebulouscloud.optimiser.controller.model";
OptimisationProblem( const JSON & TheProblem )
: JSONTopicMessage( std::string( AMQTopic ), TheProblem )
{}
OptimisationProblem()
: JSONTopicMessage( std::string( AMQTopic ) )
{}
virtual ~OptimisationProblem() = default;
};
// The handler for this message must also be defined by the algorithm that
// implements the solver.
virtual void DefineProblem( const OptimisationProblem & TheProblem,
const Address TheOracle ) = 0;
// --------------------------------------------------------------------------
// Constructor and destructor
// --------------------------------------------------------------------------
//
// The constructor defines the message handlers so that the derived solver
// classes will not need to deal with the Actor specific details, and to
// ensure that the handlers are called when the Actor receives the various
// messages. It should be noted that the problem definition can arrive from
// a remote actor on a topic corresponding to the message indentifier name.
// However, no subscription will be made for application execution contexts
// since these should be sorted and sent in order by the Solution Manager
// actor, and external communication should go throug the Solution Manager.
//
// The constructor requires an actor name as the only parameter, and the
// destructor unsubscribes from the topics previously subscribed to by
// the constuctor.
Solver( const std::string & TheSolverName )
: Actor( TheSolverName ),
StandardFallbackHandler( Actor::GetAddress().AsString() ),
NetworkingActor( Actor::GetAddress().AsString() )
{
RegisterHandler( this, &Solver::SolveProblem );
RegisterHandler( this, &Solver::DefineProblem );
Send( Theron::AMQ::NetworkLayer::TopicSubscription(
Theron::AMQ::NetworkLayer::TopicSubscription::Action::Subscription,
OptimisationProblem::AMQTopic
), GetSessionLayerAddress() );
Send( Theron::AMQ::NetworkLayer::TopicSubscription(
Theron::AMQ::NetworkLayer::TopicSubscription::Action::Subscription,
ApplicationExecutionContext::AMQTopic
), GetSessionLayerAddress() );
}
Solver() = delete;
virtual ~Solver()
{
if( HasNetwork() )
{
Send( Theron::AMQ::NetworkLayer::TopicSubscription(
Theron::AMQ::NetworkLayer::TopicSubscription::Action::CloseSubscription,
OptimisationProblem::AMQTopic
), GetSessionLayerAddress() );
Send( Theron::AMQ::NetworkLayer::TopicSubscription(
Theron::AMQ::NetworkLayer::TopicSubscription::Action::CloseSubscription,
ApplicationExecutionContext::AMQTopic
), GetSessionLayerAddress() );
}
}
};
/*==============================================================================
Solver concept
==============================================================================*/
//
// A concept is defined to validate that solvers used inherits this standard
// base class and that they implement the virtual methods.
template< class TheSolverType >
concept SolverAlgorithm = std::derived_from< TheSolverType, Solver >;
} // namespace NebulOuS
#endif // NEBULOUS_SOLVER