You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
36 KiB
478 lines
36 KiB
#include "SparseNondeterministicInfiniteHorizonHelper.h"
|
|
|
|
#include "storm/modelchecker/helper/infinitehorizon/internal/ComponentUtility.h"
|
|
#include "storm/modelchecker/helper/infinitehorizon/internal/LraViHelper.h"
|
|
|
|
#include "storm/storage/SparseMatrix.h"
|
|
#include "storm/storage/MaximalEndComponentDecomposition.h"
|
|
#include "storm/storage/Scheduler.h"
|
|
|
|
#include "storm/solver/MinMaxLinearEquationSolver.h"
|
|
#include "storm/solver/Multiplier.h"
|
|
#include "storm/solver/LpSolver.h"
|
|
|
|
#include "storm/utility/solver.h"
|
|
#include "storm/utility/vector.h"
|
|
|
|
#include "storm/environment/solver/LongRunAverageSolverEnvironment.h"
|
|
#include "storm/environment/solver/MinMaxSolverEnvironment.h"
|
|
|
|
#include "storm/exceptions/UnmetRequirementException.h"
|
|
|
|
namespace storm {
|
|
namespace modelchecker {
|
|
namespace helper {
|
|
|
|
template <typename ValueType>
|
|
SparseNondeterministicInfiniteHorizonHelper<ValueType>::SparseNondeterministicInfiniteHorizonHelper(storm::storage::SparseMatrix<ValueType> const& transitionMatrix) : SparseInfiniteHorizonHelper<ValueType, true>(transitionMatrix) {
|
|
// Intentionally left empty.
|
|
}
|
|
|
|
template <typename ValueType>
|
|
SparseNondeterministicInfiniteHorizonHelper<ValueType>::SparseNondeterministicInfiniteHorizonHelper(storm::storage::SparseMatrix<ValueType> const& transitionMatrix, storm::storage::BitVector const& markovianStates, std::vector<ValueType> const& exitRates) : SparseInfiniteHorizonHelper<ValueType, true>(transitionMatrix, markovianStates, exitRates) {
|
|
// Intentionally left empty.
|
|
}
|
|
|
|
template <typename ValueType>
|
|
std::vector<uint64_t> const& SparseNondeterministicInfiniteHorizonHelper<ValueType>::getProducedOptimalChoices() const {
|
|
STORM_LOG_ASSERT(this->isProduceSchedulerSet(), "Trying to get the produced optimal choices although no scheduler was requested.");
|
|
STORM_LOG_ASSERT(this->_producedOptimalChoices.is_initialized(), "Trying to get the produced optimal choices but none were available. Was there a computation call before?");
|
|
return this->_producedOptimalChoices.get();
|
|
}
|
|
|
|
template <typename ValueType>
|
|
std::vector<uint64_t>& SparseNondeterministicInfiniteHorizonHelper<ValueType>::getProducedOptimalChoices() {
|
|
STORM_LOG_ASSERT(this->isProduceSchedulerSet(), "Trying to get the produced optimal choices although no scheduler was requested.");
|
|
STORM_LOG_ASSERT(this->_producedOptimalChoices.is_initialized(), "Trying to get the produced optimal choices but none were available. Was there a computation call before?");
|
|
return this->_producedOptimalChoices.get();
|
|
}
|
|
|
|
template <typename ValueType>
|
|
storm::storage::Scheduler<ValueType> SparseNondeterministicInfiniteHorizonHelper<ValueType>::extractScheduler() const {
|
|
auto const& optimalChoices = getProducedOptimalChoices();
|
|
storm::storage::Scheduler<ValueType> scheduler(optimalChoices.size());
|
|
for (uint64_t state = 0; state < optimalChoices.size(); ++state) {
|
|
scheduler.setChoice(optimalChoices[state], state);
|
|
}
|
|
return scheduler;
|
|
}
|
|
|
|
template <typename ValueType>
|
|
void SparseNondeterministicInfiniteHorizonHelper<ValueType>::createDecomposition() {
|
|
if (this->_longRunComponentDecomposition == nullptr) {
|
|
// The decomposition has not been provided or computed, yet.
|
|
if (this->_backwardTransitions == nullptr) {
|
|
this->_computedBackwardTransitions = std::make_unique<storm::storage::SparseMatrix<ValueType>>(this->_transitionMatrix.transpose(true));
|
|
this->_backwardTransitions = this->_computedBackwardTransitions.get();
|
|
}
|
|
this->_computedLongRunComponentDecomposition = std::make_unique<storm::storage::MaximalEndComponentDecomposition<ValueType>>(this->_transitionMatrix, *this->_backwardTransitions);
|
|
this->_longRunComponentDecomposition = this->_computedLongRunComponentDecomposition.get();
|
|
}
|
|
}
|
|
|
|
template <typename ValueType>
|
|
ValueType SparseNondeterministicInfiniteHorizonHelper<ValueType>::computeLraForComponent(Environment const& env, ValueGetter const& stateRewardsGetter, ValueGetter const& actionRewardsGetter, storm::storage::MaximalEndComponent const& component) {
|
|
// For models with potential nondeterminisim, we compute the LRA for a maximal end component (MEC)
|
|
|
|
// Allocate memory for the nondeterministic choices.
|
|
if (this->isProduceSchedulerSet()) {
|
|
if (!this->_producedOptimalChoices.is_initialized()) {
|
|
this->_producedOptimalChoices.emplace();
|
|
}
|
|
this->_producedOptimalChoices->resize(this->_transitionMatrix.getRowGroupCount());
|
|
}
|
|
|
|
auto trivialResult = this->computeLraForTrivialMec(env, stateRewardsGetter, actionRewardsGetter, component);
|
|
if (trivialResult.first) {
|
|
return trivialResult.second;
|
|
}
|
|
|
|
// Solve nontrivial MEC with the method specified in the settings
|
|
storm::solver::LraMethod method = env.solver().lra().getNondetLraMethod();
|
|
if ((storm::NumberTraits<ValueType>::IsExact || env.solver().isForceExact()) && env.solver().lra().isNondetLraMethodSetFromDefault() && method != storm::solver::LraMethod::LinearProgramming) {
|
|
STORM_LOG_INFO("Selecting 'LP' as the solution technique for long-run properties to guarantee exact results. If you want to override this, please explicitly specify a different LRA method.");
|
|
method = storm::solver::LraMethod::LinearProgramming;
|
|
} else if (env.solver().isForceSoundness() && env.solver().lra().isNondetLraMethodSetFromDefault() && method != storm::solver::LraMethod::ValueIteration) {
|
|
STORM_LOG_INFO("Selecting 'VI' as the solution technique for long-run properties to guarantee sound results. If you want to override this, please explicitly specify a different LRA method.");
|
|
method = storm::solver::LraMethod::ValueIteration;
|
|
}
|
|
STORM_LOG_ERROR_COND(!this->isProduceSchedulerSet() || method == storm::solver::LraMethod::ValueIteration, "Scheduler generation not supported for the chosen LRA method. Try value-iteration.");
|
|
if (method == storm::solver::LraMethod::LinearProgramming) {
|
|
return computeLraForMecLp(env, stateRewardsGetter, actionRewardsGetter, component);
|
|
} else if (method == storm::solver::LraMethod::ValueIteration) {
|
|
return computeLraForMecVi(env, stateRewardsGetter, actionRewardsGetter, component);
|
|
} else {
|
|
STORM_LOG_THROW(false, storm::exceptions::InvalidSettingsException, "Unsupported technique.");
|
|
}
|
|
}
|
|
|
|
template <typename ValueType>
|
|
std::pair<bool, ValueType> SparseNondeterministicInfiniteHorizonHelper<ValueType>::computeLraForTrivialMec(Environment const& env, ValueGetter const& stateRewardsGetter, ValueGetter const& actionRewardsGetter, storm::storage::MaximalEndComponent const& component) {
|
|
|
|
// If the component only consists of a single state, we compute the LRA value directly
|
|
if (component.size() == 1) {
|
|
auto const& element = *component.begin();
|
|
uint64_t state = internal::getComponentElementState(element);
|
|
auto choiceIt = internal::getComponentElementChoicesBegin(element);
|
|
if (!this->isContinuousTime()) {
|
|
// This is an MDP.
|
|
// Find the choice with the highest/lowest reward
|
|
ValueType bestValue = actionRewardsGetter(*choiceIt);
|
|
uint64_t bestChoice = *choiceIt;
|
|
for (++choiceIt; choiceIt != internal::getComponentElementChoicesEnd(element); ++choiceIt) {
|
|
ValueType currentValue = actionRewardsGetter(*choiceIt);
|
|
if ((this->minimize() && currentValue < bestValue) || (this->maximize() && currentValue > bestValue)) {
|
|
bestValue = std::move(currentValue);
|
|
bestChoice = *choiceIt;
|
|
}
|
|
}
|
|
if (this->isProduceSchedulerSet()) {
|
|
this->_producedOptimalChoices.get()[state] = bestChoice - this->_transitionMatrix.getRowGroupIndices()[state];
|
|
}
|
|
bestValue += stateRewardsGetter(state);
|
|
return {true, bestValue};
|
|
} else {
|
|
// In a Markov Automaton, singleton components have to consist of a Markovian state because of the non-Zenoness assumption. Then, there is just one possible choice.
|
|
STORM_LOG_ASSERT(this->_markovianStates != nullptr, "Nondeterministic continuous time model without Markovian states... Is this a not a Markov Automaton?");
|
|
STORM_LOG_THROW(this->_markovianStates->get(state), storm::exceptions::InvalidOperationException, "Markov Automaton has Zeno behavior. Computation of Long Run Average values not supported.");
|
|
STORM_LOG_ASSERT(internal::getComponentElementChoiceCount(element) == 1, "Markovian state has Nondeterministic behavior.");
|
|
if (this->isProduceSchedulerSet()) {
|
|
this->_producedOptimalChoices.get()[state] = 0;
|
|
}
|
|
ValueType result = stateRewardsGetter(state) + (this->isContinuousTime() ? (*this->_exitRates)[state] * actionRewardsGetter(*choiceIt) : actionRewardsGetter(*choiceIt));
|
|
return {true, result};
|
|
}
|
|
}
|
|
return {false, storm::utility::zero<ValueType>()};
|
|
}
|
|
|
|
|
|
template <typename ValueType>
|
|
ValueType SparseNondeterministicInfiniteHorizonHelper<ValueType>::computeLraForMecVi(Environment const& env, ValueGetter const& stateRewardsGetter, ValueGetter const& actionRewardsGetter, storm::storage::MaximalEndComponent const& mec) {
|
|
|
|
// Collect some parameters of the computation
|
|
ValueType aperiodicFactor = storm::utility::convertNumber<ValueType>(env.solver().lra().getAperiodicFactor());
|
|
std::vector<uint64_t>* optimalChoices = nullptr;
|
|
if (this->isProduceSchedulerSet()) {
|
|
optimalChoices = &this->_producedOptimalChoices.get();
|
|
}
|
|
|
|
// Now create a helper and perform the algorithm
|
|
if (this->isContinuousTime()) {
|
|
// We assume a Markov Automaton (with deterministic timed states and nondeterministic instant states)
|
|
storm::modelchecker::helper::internal::LraViHelper<ValueType, storm::storage::MaximalEndComponent, storm::modelchecker::helper::internal::LraViTransitionsType::DetTsNondetIs> viHelper(mec, this->_transitionMatrix, aperiodicFactor, this->_markovianStates, this->_exitRates);
|
|
return viHelper.performValueIteration(env, stateRewardsGetter, actionRewardsGetter, this->_exitRates, &this->getOptimizationDirection(), optimalChoices);
|
|
} else {
|
|
// We assume an MDP (with nondeterministic timed states and no instant states)
|
|
storm::modelchecker::helper::internal::LraViHelper<ValueType, storm::storage::MaximalEndComponent, storm::modelchecker::helper::internal::LraViTransitionsType::NondetTsNoIs> viHelper(mec, this->_transitionMatrix, aperiodicFactor);
|
|
return viHelper.performValueIteration(env, stateRewardsGetter, actionRewardsGetter, nullptr, &this->getOptimizationDirection(), optimalChoices);
|
|
}
|
|
}
|
|
|
|
template <typename ValueType>
|
|
ValueType SparseNondeterministicInfiniteHorizonHelper<ValueType>::computeLraForMecLp(Environment const& env, ValueGetter const& stateRewardsGetter, ValueGetter const& actionRewardsGetter, storm::storage::MaximalEndComponent const& mec) {
|
|
// Create an LP solver
|
|
auto solver = storm::utility::solver::LpSolverFactory<ValueType>().create("LRA for MEC");
|
|
|
|
// Now build the LP formulation as described in:
|
|
// Guck et al.: Modelling and Analysis of Markov Reward Automata (ATVA'14), https://doi.org/10.1007/978-3-319-11936-6_13
|
|
solver->setOptimizationDirection(invert(this->getOptimizationDirection()));
|
|
|
|
// Create variables
|
|
// TODO: Investigate whether we can easily make the variables bounded
|
|
std::map<uint_fast64_t, storm::expressions::Variable> stateToVariableMap;
|
|
for (auto const& stateChoicesPair : mec) {
|
|
std::string variableName = "x" + std::to_string(stateChoicesPair.first);
|
|
stateToVariableMap[stateChoicesPair.first] = solver->addUnboundedContinuousVariable(variableName);
|
|
}
|
|
storm::expressions::Variable k = solver->addUnboundedContinuousVariable("k", storm::utility::one<ValueType>());
|
|
solver->update();
|
|
|
|
// Add constraints.
|
|
for (auto const& stateChoicesPair : mec) {
|
|
uint_fast64_t state = stateChoicesPair.first;
|
|
bool stateIsMarkovian = this->_markovianStates && this->_markovianStates->get(state);
|
|
|
|
// Now create a suitable constraint for each choice
|
|
// x_s {≤, ≥} -k/rate(s) + sum_s' P(s,act,s') * x_s' + (value(s)/rate(s) + value(s,act))
|
|
for (auto choice : stateChoicesPair.second) {
|
|
std::vector<storm::expressions::Expression> summands;
|
|
auto matrixRow = this->_transitionMatrix.getRow(choice);
|
|
summands.reserve(matrixRow.getNumberOfEntries() + 2);
|
|
// add -k/rate(s) (only if s is either a Markovian state or we have an MDP)
|
|
if (stateIsMarkovian) {
|
|
summands.push_back(-(k / solver->getManager().rational((*this->_exitRates)[state])));
|
|
} else if (!this->isContinuousTime()) {
|
|
summands.push_back(-k);
|
|
}
|
|
// add sum_s' P(s,act,s') * x_s'
|
|
for (auto const& element : matrixRow) {
|
|
summands.push_back(stateToVariableMap.at(element.getColumn()) * solver->getConstant(element.getValue()));
|
|
}
|
|
// add value for state and choice
|
|
ValueType value;
|
|
if (stateIsMarkovian) {
|
|
// divide state reward with exit rate
|
|
value = stateRewardsGetter(state) / (*this->_exitRates)[state] + actionRewardsGetter(choice);
|
|
} else if (!this->isContinuousTime()) {
|
|
// in discrete time models no scaling is needed
|
|
value = stateRewardsGetter(state) + actionRewardsGetter(choice);
|
|
} else {
|
|
// state is a probabilistic state of a Markov automaton. The state reward is not collected
|
|
value = actionRewardsGetter(choice);
|
|
}
|
|
summands.push_back(solver->getConstant(value));
|
|
storm::expressions::Expression constraint;
|
|
if (this->minimize()) {
|
|
constraint = stateToVariableMap.at(state) <= storm::expressions::sum(summands);
|
|
} else {
|
|
constraint = stateToVariableMap.at(state) >= storm::expressions::sum(summands);
|
|
}
|
|
solver->addConstraint("s" + std::to_string(state) + "," + std::to_string(choice), constraint);
|
|
}
|
|
}
|
|
|
|
solver->optimize();
|
|
STORM_LOG_THROW(!this->isProduceSchedulerSet(), storm::exceptions::NotImplementedException, "Scheduler extraction is not yet implemented for LP based LRA method.");
|
|
return solver->getContinuousValue(k);
|
|
}
|
|
|
|
/*!
|
|
* Auxiliary function that adds the entries of the Ssp Matrix for a single choice (i.e., row)
|
|
* Transitions that lead to a Component state will be redirected to a new auxiliary state (there is one aux. state for each component).
|
|
* Transitions that don't lead to a Component state are copied (taking a state index mapping into account).
|
|
*/
|
|
template <typename ValueType>
|
|
void addSspMatrixChoice(uint64_t const& inputMatrixChoice, storm::storage::SparseMatrix<ValueType> const& inputTransitionMatrix, std::vector<uint64_t> const& inputToSspStateMap, uint64_t const& numberOfNonComponentStates, uint64_t const& currentSspChoice, storm::storage::SparseMatrixBuilder<ValueType>& sspMatrixBuilder) {
|
|
|
|
// As there could be multiple transitions to the same MEC, we accumulate them in this map before adding them to the matrix builder.
|
|
std::map<uint64_t, ValueType> auxiliaryStateToProbabilityMap;
|
|
|
|
for (auto const& transition : inputTransitionMatrix.getRow(inputMatrixChoice)) {
|
|
if (!storm::utility::isZero(transition.getValue())) {
|
|
auto const& sspTransitionTarget = inputToSspStateMap[transition.getColumn()];
|
|
// Since the auxiliary Component states are appended at the end of the matrix, we can use this check to
|
|
// decide whether the transition leads to a component state or not
|
|
if (sspTransitionTarget < numberOfNonComponentStates) {
|
|
// If the target state is not contained in a component, we can copy over the entry.
|
|
sspMatrixBuilder.addNextValue(currentSspChoice, sspTransitionTarget, transition.getValue());
|
|
} else {
|
|
// If the target state is contained in component i, we need to add the probability to the corresponding field in the vector
|
|
// so that we are able to write the cumulative probability to the component into the matrix later.
|
|
auto insertionRes = auxiliaryStateToProbabilityMap.emplace(sspTransitionTarget, transition.getValue());
|
|
if (!insertionRes.second) {
|
|
// sspTransitionTarget already existed in the map, i.e., there already was a transition to that component.
|
|
// Hence, we add up the probabilities.
|
|
insertionRes.first->second += transition.getValue();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now insert all (cumulative) probability values that target a component.
|
|
for (auto const& componentToProbEntry : auxiliaryStateToProbabilityMap) {
|
|
sspMatrixBuilder.addNextValue(currentSspChoice, componentToProbEntry.first, componentToProbEntry.second);
|
|
}
|
|
}
|
|
|
|
template <typename ValueType>
|
|
std::pair<storm::storage::SparseMatrix<ValueType>, std::vector<ValueType>> SparseNondeterministicInfiniteHorizonHelper<ValueType>::buildSspMatrixVector(std::vector<ValueType> const& mecLraValues, std::vector<uint64_t> const& inputToSspStateMap, storm::storage::BitVector const& statesNotInComponent, uint64_t numberOfNonComponentStates, std::vector<std::pair<uint64_t, uint64_t>>* sspComponentExitChoicesToOriginalMap) {
|
|
|
|
auto const& choiceIndices = this->_transitionMatrix.getRowGroupIndices();
|
|
|
|
std::vector<ValueType> rhs;
|
|
uint64_t numberOfSspStates = numberOfNonComponentStates + this->_longRunComponentDecomposition->size();
|
|
storm::storage::SparseMatrixBuilder<ValueType> sspMatrixBuilder(0, numberOfSspStates , 0, true, true, numberOfSspStates);
|
|
// If the source state of a transition is not contained in any component, we copy its choices (and perform the necessary modifications).
|
|
uint64_t currentSspChoice = 0;
|
|
for (auto const& nonComponentState : statesNotInComponent) {
|
|
sspMatrixBuilder.newRowGroup(currentSspChoice);
|
|
for (uint64_t choice = choiceIndices[nonComponentState]; choice < choiceIndices[nonComponentState + 1]; ++choice, ++currentSspChoice) {
|
|
rhs.push_back(storm::utility::zero<ValueType>());
|
|
addSspMatrixChoice(choice, this->_transitionMatrix, inputToSspStateMap, numberOfNonComponentStates, currentSspChoice, sspMatrixBuilder);
|
|
}
|
|
}
|
|
// Now we construct the choices for the auxiliary states which reflect former Component states.
|
|
for (uint64_t componentIndex = 0; componentIndex < this->_longRunComponentDecomposition->size(); ++componentIndex) {
|
|
auto const& component = (*this->_longRunComponentDecomposition)[componentIndex];
|
|
sspMatrixBuilder.newRowGroup(currentSspChoice);
|
|
// For nondeterministic models it might still be that we leave the component again. This needs to be reflected in the SSP
|
|
// by adding the "exiting" choices of the MEC to the axiliary states
|
|
for (auto const& element : component) {
|
|
uint64_t componentState = internal::getComponentElementState(element);
|
|
for (uint64_t choice = choiceIndices[componentState]; choice < choiceIndices[componentState + 1]; ++choice) {
|
|
// If the choice is not contained in the component itself, we have to add a similar distribution to the auxiliary state.
|
|
if (!internal::componentElementChoicesContains(element, choice)) {
|
|
rhs.push_back(storm::utility::zero<ValueType>());
|
|
addSspMatrixChoice(choice, this->_transitionMatrix, inputToSspStateMap, numberOfNonComponentStates, currentSspChoice, sspMatrixBuilder);
|
|
if (sspComponentExitChoicesToOriginalMap) {
|
|
// Later we need to be able to map this choice back to the original input model
|
|
sspComponentExitChoicesToOriginalMap->emplace_back(componentState, choice - choiceIndices[componentState]);
|
|
}
|
|
++currentSspChoice;
|
|
}
|
|
}
|
|
}
|
|
// For each auxiliary state, there is the option to achieve the reward value of the LRA associated with the component.
|
|
rhs.push_back(mecLraValues[componentIndex]);
|
|
if (sspComponentExitChoicesToOriginalMap) {
|
|
// Insert some invalid values so we can later detect that this choice is not an exit choice
|
|
sspComponentExitChoicesToOriginalMap->emplace_back(std::numeric_limits<uint_fast64_t>::max(), std::numeric_limits<uint_fast64_t>::max());
|
|
}
|
|
++currentSspChoice;
|
|
}
|
|
return std::make_pair(sspMatrixBuilder.build(currentSspChoice, numberOfSspStates, numberOfSspStates), std::move(rhs));
|
|
}
|
|
|
|
template <typename ValueType>
|
|
void SparseNondeterministicInfiniteHorizonHelper<ValueType>::constructOptimalChoices(std::vector<uint64_t> const& sspChoices, storm::storage::SparseMatrix<ValueType> const& sspMatrix, std::vector<uint64_t> const& inputToSspStateMap, storm::storage::BitVector const& statesNotInComponent, uint64_t numberOfNonComponentStates, std::vector<std::pair<uint64_t, uint64_t>> const& sspComponentExitChoicesToOriginalMap) {
|
|
// We first take care of non-mec states
|
|
storm::utility::vector::setVectorValues(this->_producedOptimalChoices.get(), statesNotInComponent, sspChoices);
|
|
// Secondly, we consider MEC states. There are 3 cases for each MEC state:
|
|
// 1. The SSP choices encode that we want to stay in the MEC
|
|
// 2. The SSP choices encode that we want to leave the MEC and
|
|
// a) we take an exit (non-MEC) choice at the given state
|
|
// b) we have to take a MEC choice at the given state in a way that eventually an exit state of the MEC is reached
|
|
uint64_t exitChoiceOffset = sspMatrix.getRowGroupIndices()[numberOfNonComponentStates];
|
|
for (auto const& mec : *this->_longRunComponentDecomposition) {
|
|
// Get the sspState of this MEC (using one representative mec state)
|
|
auto const& sspState = inputToSspStateMap[mec.begin()->first];
|
|
uint64_t sspChoiceIndex = sspMatrix.getRowGroupIndices()[sspState] + sspChoices[sspState];
|
|
// Obtain the state and choice of the original model to which the selected choice corresponds.
|
|
auto const& originalStateChoice = sspComponentExitChoicesToOriginalMap[sspChoiceIndex - exitChoiceOffset];
|
|
// Check if we are in Case 1 or 2
|
|
if (originalStateChoice.first == std::numeric_limits<uint_fast64_t>::max()) {
|
|
// The optimal choice is to stay in this mec (Case 1)
|
|
// In this case, no further operations are necessary. The scheduler has already been set to the optimal choices during the call of computeLraForMec.
|
|
STORM_LOG_ASSERT(sspMatrix.getRow(sspState, sspChoices[sspState]).getNumberOfEntries() == 0, "Expected empty row at choice that stays in MEC.");
|
|
} else {
|
|
// The best choice is to leave this MEC via the selected state and choice. (Case 2)
|
|
// Set the exit choice (Case 2.a)
|
|
this->_producedOptimalChoices.get()[originalStateChoice.first] = originalStateChoice.second;
|
|
// The remaining states in this MEC need to reach the state with the exit choice with probability 1. (Case 2.b)
|
|
// Perform a backwards search from the exit state, only using MEC choices
|
|
// We start by setting an invalid choice to all remaining mec states (so that we can easily detect them as unprocessed)
|
|
for (auto const& stateActions : mec) {
|
|
if (stateActions.first != originalStateChoice.first) {
|
|
this->_producedOptimalChoices.get()[stateActions.first] = std::numeric_limits<uint64_t>::max();
|
|
}
|
|
}
|
|
// Ensure that backwards transitions are available
|
|
if (this->_backwardTransitions == nullptr) {
|
|
this->_computedBackwardTransitions = std::make_unique<storm::storage::SparseMatrix<ValueType>>(this->_transitionMatrix.transpose(true));
|
|
this->_backwardTransitions = this->_computedBackwardTransitions.get();
|
|
}
|
|
// Now start a backwards DFS
|
|
std::vector<uint64_t> stack = {originalStateChoice.first};
|
|
while (!stack.empty()) {
|
|
uint64_t currentState = stack.back();
|
|
stack.pop_back();
|
|
for (auto const& backwardsTransition : this->_backwardTransitions->getRowGroup(currentState)) {
|
|
uint64_t predecessorState = backwardsTransition.getColumn();
|
|
if (mec.containsState(predecessorState)) {
|
|
auto& selectedPredChoice = this->_producedOptimalChoices.get()[predecessorState];
|
|
if (selectedPredChoice == std::numeric_limits<uint64_t>::max()) {
|
|
// We don't already have a choice for this predecessor.
|
|
// We now need to check whether there is a *MEC* choice leading to currentState
|
|
for (auto const& predChoice : mec.getChoicesForState(predecessorState)) {
|
|
for (auto const& forwardTransition : this->_transitionMatrix.getRow(predChoice)) {
|
|
if (forwardTransition.getColumn() == currentState && !storm::utility::isZero(forwardTransition.getValue())) {
|
|
// Playing this choice (infinitely often) will lead to current state (infinitely often)!
|
|
selectedPredChoice = predChoice - this->_transitionMatrix.getRowGroupIndices()[predecessorState];
|
|
stack.push_back(predecessorState);
|
|
break;
|
|
}
|
|
}
|
|
if (selectedPredChoice != std::numeric_limits<uint64_t>::max()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename ValueType>
|
|
std::vector<ValueType> SparseNondeterministicInfiniteHorizonHelper<ValueType>::buildAndSolveSsp(Environment const& env, std::vector<ValueType> const& componentLraValues) {
|
|
STORM_LOG_ASSERT(this->_longRunComponentDecomposition != nullptr, "Decomposition not computed, yet.");
|
|
|
|
// For fast transition rewriting, we build a mapping from the input state indices to the state indices of a new transition matrix
|
|
// which redirects all transitions leading to a former component state to a new auxiliary state.
|
|
// There will be one auxiliary state for each component. These states will be appended to the end of the matrix.
|
|
|
|
// First gather the states that are part of a component
|
|
// and create a mapping from states that lie in a component to the corresponding component index.
|
|
storm::storage::BitVector statesInComponents(this->_transitionMatrix.getRowGroupCount());
|
|
std::vector<uint64_t> inputToSspStateMap(this->_transitionMatrix.getRowGroupCount(), std::numeric_limits<uint64_t>::max());
|
|
for (uint64_t currentComponentIndex = 0; currentComponentIndex < this->_longRunComponentDecomposition->size(); ++currentComponentIndex) {
|
|
for (auto const& element : (*this->_longRunComponentDecomposition)[currentComponentIndex]) {
|
|
uint64_t state = internal::getComponentElementState(element);
|
|
statesInComponents.set(state);
|
|
inputToSspStateMap[state] = currentComponentIndex;
|
|
}
|
|
}
|
|
// Now take care of the non-component states. Note that the order of these states will be preserved.
|
|
uint64_t numberOfNonComponentStates = 0;
|
|
storm::storage::BitVector statesNotInComponent = ~statesInComponents;
|
|
for (auto const& nonComponentState : statesNotInComponent) {
|
|
inputToSspStateMap[nonComponentState] = numberOfNonComponentStates;
|
|
++numberOfNonComponentStates;
|
|
}
|
|
// Finalize the mapping for the component states which now still assigns component states to to their component index.
|
|
// To make sure that they point to the auxiliary states (located at the end of the SspMatrix), we need to shift them by the
|
|
// number of states that are not in a component.
|
|
for (auto const& mecState : statesInComponents) {
|
|
inputToSspStateMap[mecState] += numberOfNonComponentStates;
|
|
}
|
|
|
|
// For scheduler extraction, we will need to create a mapping between choices at the auxiliary states and the
|
|
// corresponding choices in the original model.
|
|
std::vector<std::pair<uint_fast64_t, uint_fast64_t>> sspComponentExitChoicesToOriginalMap;
|
|
|
|
// The next step is to create the SSP matrix and the right-hand side of the SSP.
|
|
auto sspMatrixVector = buildSspMatrixVector(componentLraValues, inputToSspStateMap, statesNotInComponent, numberOfNonComponentStates, this->isProduceSchedulerSet() ? &sspComponentExitChoicesToOriginalMap : nullptr);
|
|
|
|
// Set-up a solver
|
|
storm::solver::GeneralMinMaxLinearEquationSolverFactory<ValueType> minMaxLinearEquationSolverFactory;
|
|
storm::solver::MinMaxLinearEquationSolverRequirements requirements = minMaxLinearEquationSolverFactory.getRequirements(env, true, true, this->getOptimizationDirection(), false, this->isProduceSchedulerSet());
|
|
requirements.clearBounds();
|
|
STORM_LOG_THROW(!requirements.hasEnabledCriticalRequirement(), storm::exceptions::UnmetRequirementException, "Solver requirements " + requirements.getEnabledRequirementsAsString() + " not checked.");
|
|
std::unique_ptr<storm::solver::MinMaxLinearEquationSolver<ValueType>> solver = minMaxLinearEquationSolverFactory.create(env, sspMatrixVector.first);
|
|
solver->setHasUniqueSolution();
|
|
solver->setHasNoEndComponents();
|
|
solver->setTrackScheduler(this->isProduceSchedulerSet());
|
|
auto lowerUpperBounds = std::minmax_element(componentLraValues.begin(), componentLraValues.end());
|
|
solver->setLowerBound(*lowerUpperBounds.first);
|
|
solver->setUpperBound(*lowerUpperBounds.second);
|
|
solver->setRequirementsChecked();
|
|
|
|
// Solve the equation system
|
|
std::vector<ValueType> x(sspMatrixVector.first.getRowGroupCount());
|
|
solver->solveEquations(env, this->getOptimizationDirection(), x, sspMatrixVector.second);
|
|
|
|
// Prepare scheduler (if requested)
|
|
if (this->isProduceSchedulerSet() && solver->hasScheduler()) {
|
|
// Translate result for ssp matrix to original model
|
|
constructOptimalChoices(solver->getSchedulerChoices(), sspMatrixVector.first, inputToSspStateMap, statesNotInComponent, numberOfNonComponentStates, sspComponentExitChoicesToOriginalMap);
|
|
} else {
|
|
STORM_LOG_ERROR_COND(!this->isProduceSchedulerSet(), "Requested to produce a scheduler, but no scheduler was generated.");
|
|
}
|
|
|
|
// Prepare result vector.
|
|
// For efficiency reasons, we re-use the memory of our rhs for this!
|
|
std::vector<ValueType> result = std::move(sspMatrixVector.second);
|
|
result.resize(this->_transitionMatrix.getRowGroupCount());
|
|
result.shrink_to_fit();
|
|
storm::utility::vector::selectVectorValues(result, inputToSspStateMap, x);
|
|
return result;
|
|
}
|
|
|
|
template class SparseNondeterministicInfiniteHorizonHelper<double>;
|
|
template class SparseNondeterministicInfiniteHorizonHelper<storm::RationalNumber>;
|
|
|
|
}
|
|
}
|
|
}
|