435 lines
27 KiB

#include "storm/storage/bisimulation/NondeterministicModelBisimulationDecomposition.h"
#include "storm/models/sparse/Mdp.h"
#include "storm/models/sparse/StandardRewardModel.h"
#include "storm/utility/graph.h"
#include "storm/utility/macros.h"
#include "storm/exceptions/IllegalFunctionCallException.h"
#include "storm/adapters/RationalFunctionAdapter.h"
namespace storm {
namespace storage {
using namespace bisimulation;
template<typename ModelType>
NondeterministicModelBisimulationDecomposition<ModelType>::NondeterministicModelBisimulationDecomposition(ModelType const& model, typename BisimulationDecomposition<ModelType, NondeterministicModelBisimulationDecomposition::BlockDataType>::Options const& options) : BisimulationDecomposition<ModelType, NondeterministicModelBisimulationDecomposition::BlockDataType>(model, model.getTransitionMatrix().transpose(false), options), choiceToStateMapping(model.getNumberOfChoices()), quotientDistributions(model.getNumberOfChoices()), orderedQuotientDistributions(model.getNumberOfChoices()) {
STORM_LOG_THROW(options.getType() == BisimulationType::Strong, storm::exceptions::IllegalFunctionCallException, "Weak bisimulation is currently not supported for nondeterministic models.");
}
template<typename ModelType>
std::pair<storm::storage::BitVector, storm::storage::BitVector> NondeterministicModelBisimulationDecomposition<ModelType>::getStatesWithProbability01() {
STORM_LOG_THROW(this->options.isOptimizationDirectionSet(), storm::exceptions::IllegalFunctionCallException, "Can only compute states with probability 0/1 with an optimization direction (min/max).");
if (this->options.getOptimizationDirection() == OptimizationDirection::Minimize) {
return storm::utility::graph::performProb01Min(this->model.getTransitionMatrix(), this->model.getTransitionMatrix().getRowGroupIndices(), this->model.getBackwardTransitions(), this->options.phiStates.get(), this->options.psiStates.get());
} else {
return storm::utility::graph::performProb01Max(this->model.getTransitionMatrix(), this->model.getTransitionMatrix().getRowGroupIndices(), this->model.getBackwardTransitions(), this->options.phiStates.get(), this->options.psiStates.get());
}
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::initialize() {
this->createChoiceToStateMapping();
this->initializeQuotientDistributions();
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::createChoiceToStateMapping() {
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
for (storm::storage::sparse::state_type state = 0; state < this->model.getNumberOfStates(); ++state) {
for (uint_fast64_t choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
choiceToStateMapping[choice] = state;
}
}
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::initializeQuotientDistributions() {
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
for (auto const& block : this->partition.getBlocks()) {
if (block->data().absorbing()) {
// If the block is marked as absorbing, we need to create the corresponding distributions.
for (auto stateIt = this->partition.begin(*block), stateIte = this->partition.end(*block); stateIt != stateIte; ++stateIt) {
for (uint_fast64_t choice = nondeterministicChoiceIndices[*stateIt]; choice < nondeterministicChoiceIndices[*stateIt + 1]; ++choice) {
this->quotientDistributions[choice].addProbability(block->getId(), storm::utility::one<ValueType>());
orderedQuotientDistributions[choice] = &this->quotientDistributions[choice];
}
}
} else {
// Otherwise, we compute the probabilities from the transition matrix.
for (auto stateIt = this->partition.begin(*block), stateIte = this->partition.end(*block); stateIt != stateIte; ++stateIt) {
for (uint_fast64_t choice = nondeterministicChoiceIndices[*stateIt]; choice < nondeterministicChoiceIndices[*stateIt + 1]; ++choice) {
if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
auto const& rewardModel = this->model.getUniqueRewardModel();
if (rewardModel.hasStateActionRewards()) {
this->quotientDistributions[choice].setReward(rewardModel.getStateActionReward(choice));
}
}
for (auto entry : this->model.getTransitionMatrix().getRow(choice)) {
if (!this->comparator.isZero(entry.getValue())) {
this->quotientDistributions[choice].addProbability(this->partition.getBlock(entry.getColumn()).getId(), entry.getValue());
}
}
orderedQuotientDistributions[choice] = &this->quotientDistributions[choice];
}
}
}
}
for (decltype(this->model.getNumberOfStates()) state = 0; state < this->model.getNumberOfStates(); ++state) {
updateOrderedQuotientDistributions(state);
}
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::updateOrderedQuotientDistributions(storm::storage::sparse::state_type state) {
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
std::sort(this->orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state], this->orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state + 1],
[this] (storm::storage::Distribution<ValueType> const* dist1, storm::storage::Distribution<ValueType> const* dist2) {
return dist1->less(*dist2, this->comparator);
});
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::buildQuotient() {
// In order to create the quotient model, we need to construct
// (a) the new transition matrix,
// (b) the new labeling,
// (c) the new reward structures.
// Prepare a matrix builder for (a).
storm::storage::SparseMatrixBuilder<ValueType> builder(0, this->size(), 0, false, true, this->size());
// Prepare the new state labeling for (b).
storm::models::sparse::StateLabeling newLabeling(this->size());
std::set<std::string> atomicPropositionsSet = this->options.respectedAtomicPropositions.get();
atomicPropositionsSet.insert("init");
std::vector<std::string> atomicPropositions = std::vector<std::string>(atomicPropositionsSet.begin(), atomicPropositionsSet.end());
for (auto const& ap : atomicPropositions) {
newLabeling.addLabel(ap);
}
// If the model had state (action) rewards, we need to build the state rewards for the quotient as well.
boost::optional<std::vector<ValueType>> stateRewards;
boost::optional<std::vector<ValueType>> stateActionRewards;
boost::optional<storm::models::sparse::StandardRewardModel<ValueType> const&> rewardModel;
if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
rewardModel = this->model.getUniqueRewardModel();
if (rewardModel.get().hasStateRewards()) {
stateRewards = std::vector<ValueType>(this->blocks.size());
}
if (rewardModel.get().hasStateActionRewards()) {
stateActionRewards = std::vector<ValueType>();
}
}
// Now build (a) and (b) by traversing all blocks.
uint_fast64_t currentRow = 0;
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
for (uint_fast64_t blockIndex = 0; blockIndex < this->blocks.size(); ++blockIndex) {
auto const& block = this->blocks[blockIndex];
// Open new row group for the new meta state.
builder.newRowGroup(currentRow);
// Pick one representative state. For strong bisimulation it doesn't matter which state it is, because
// they all behave equally.
storm::storage::sparse::state_type representativeState = *block.begin();
Block<BlockDataType> const& oldBlock = this->partition.getBlock(representativeState);
// If the block is absorbing, we simply add a self-loop.
if (oldBlock.data().absorbing()) {
builder.addNextValue(currentRow, blockIndex, storm::utility::one<ValueType>());
++currentRow;
// If the block has a special representative state, we retrieve it now.
if (oldBlock.data().hasRepresentativeState()) {
representativeState = oldBlock.data().representativeState();
}
// Give the choice a reward of zero as we artificially introduced that the block is absorbing.
if (this->options.getKeepRewards() && rewardModel && rewardModel.get().hasStateActionRewards()) {
stateActionRewards.get().push_back(storm::utility::zero<ValueType>());
}
// Add all of the selected atomic propositions that hold in the representative state to the state
// representing the block.
for (auto const& ap : atomicPropositions) {
if (this->model.getStateLabeling().getStateHasLabel(ap, representativeState)) {
newLabeling.addLabelToState(ap, blockIndex);
}
}
} else {
// Add the outgoing choices of the block.
for (uint_fast64_t choice = nondeterministicChoiceIndices[representativeState]; choice < nondeterministicChoiceIndices[representativeState + 1]; ++choice) {
// If the choice is the same as the last one, we do not need to add it.
if (choice > nondeterministicChoiceIndices[representativeState] && quotientDistributions[choice - 1].equals(quotientDistributions[choice], this->comparator)) {
continue;
}
for (auto entry : quotientDistributions[choice]) {
builder.addNextValue(currentRow, entry.first, entry.second);
}
if (this->options.getKeepRewards() && rewardModel && rewardModel.get().hasStateActionRewards()) {
stateActionRewards.get().push_back(quotientDistributions[choice].getReward());
}
++currentRow;
}
// Otherwise add all atomic propositions to the equivalence class that the representative state
// satisfies.
for (auto const& ap : atomicPropositions) {
if (this->model.getStateLabeling().getStateHasLabel(ap, representativeState)) {
newLabeling.addLabelToState(ap, blockIndex);
}
}
}
// If the model has state rewards, we simply copy the state reward of the representative state, because
// all states in a block are guaranteed to have the same state reward.
if (this->options.getKeepRewards() && rewardModel && rewardModel.get().hasStateRewards()) {
stateRewards.get()[blockIndex] = rewardModel.get().getStateRewardVector()[representativeState];
}
}
// Now check which of the blocks of the partition contain at least one initial state.
for (auto initialState : this->model.getInitialStates()) {
Block<BlockDataType> const& initialBlock = this->partition.getBlock(initialState);
newLabeling.addLabelToState("init", initialBlock.getId());
}
// Construct the reward model mapping.
std::unordered_map<std::string, typename ModelType::RewardModelType> rewardModels;
if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
STORM_LOG_THROW(this->model.hasUniqueRewardModel(), storm::exceptions::IllegalFunctionCallException, "Cannot preserve more than one reward model.");
typename std::unordered_map<std::string, typename ModelType::RewardModelType>::const_iterator nameRewardModelPair = this->model.getRewardModels().begin();
rewardModels.insert(std::make_pair(nameRewardModelPair->first, typename ModelType::RewardModelType(stateRewards, stateActionRewards)));
}
// Finally construct the quotient model.
this->quotient = std::make_shared<ModelType>(builder.build(), std::move(newLabeling), std::move(rewardModels));
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::possiblyNeedsRefinement(bisimulation::Block<BlockDataType> const& block) const {
return block.getNumberOfStates() > 1 && !block.data().absorbing();
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::updateQuotientDistributionsOfPredecessors(Block<BlockDataType> const& newBlock, Block<BlockDataType> const& oldBlock, std::vector<Block<BlockDataType>*>& splitterQueue) {
uint_fast64_t lastState = 0;
bool lastStateInitialized = false;
for (auto stateIt = this->partition.begin(newBlock), stateIte = this->partition.end(newBlock); stateIt != stateIte; ++stateIt) {
for (auto predecessorEntry : this->backwardTransitions.getRow(*stateIt)) {
if (this->comparator.isZero(predecessorEntry.getValue())) {
continue;
}
storm::storage::sparse::state_type predecessorChoice = predecessorEntry.getColumn();
storm::storage::sparse::state_type predecessorState = choiceToStateMapping[predecessorChoice];
Block<BlockDataType>& predecessorBlock = this->partition.getBlock(predecessorState);
// If the predecessor block is marked as absorbing, we do not need to update anything.
if (predecessorBlock.data().absorbing()) {
continue;
}
// If the predecessor block is not marked as to-be-refined, we do so now.
if (!predecessorBlock.data().splitter()) {
predecessorBlock.data().setSplitter();
splitterQueue.push_back(&predecessorBlock);
}
if (lastStateInitialized) {
// If we have skipped to the choices of the next state, we need to repair the order of the
// distributions for the last state.
if (lastState != predecessorState) {
updateOrderedQuotientDistributions(lastState);
lastState = predecessorState;
}
} else {
lastStateInitialized = true;
lastState = choiceToStateMapping[predecessorChoice];
}
// Now shift the probability from this transition from the old block to the new one.
this->quotientDistributions[predecessorChoice].shiftProbability(oldBlock.getId(), newBlock.getId(), predecessorEntry.getValue());
}
}
if (lastStateInitialized) {
updateOrderedQuotientDistributions(lastState);
}
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::checkQuotientDistributions() const {
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
for (decltype(this->model.getNumberOfStates()) state = 0; state < this->model.getNumberOfStates(); ++state) {
for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
storm::storage::DistributionWithReward<ValueType> distribution;
if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
auto const& rewardModel = this->model.getUniqueRewardModel();
if (rewardModel.hasStateActionRewards()) {
distribution.setReward(rewardModel.getStateActionReward(choice));
}
}
for (auto const& element : this->model.getTransitionMatrix().getRow(choice)) {
distribution.addProbability(this->partition.getBlock(element.getColumn()).getId(), element.getValue());
}
if (!distribution.equals(quotientDistributions[choice])) {
std::cout << "the distributions for choice " << choice << " of state " << state << " do not match." << std::endl;
std::cout << "is: " << quotientDistributions[choice] << " but should be " << distribution << std::endl;
exit(-1);
}
bool less1 = distribution.less(quotientDistributions[choice], this->comparator);
bool less2 = quotientDistributions[choice].less(distribution, this->comparator);
if (distribution.equals(quotientDistributions[choice]) && (less1 || less2)) {
std::cout << "mismatch of equality and less for " << std::endl;
std::cout << quotientDistributions[choice] << " vs " << distribution << std::endl;
exit(-1);
}
}
for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1] - 1; ++choice) {
if (orderedQuotientDistributions[choice + 1]->less(*orderedQuotientDistributions[choice], this->comparator)) {
std::cout << "choice " << (choice+1) << " is less than predecessor" << std::endl;
std::cout << *orderedQuotientDistributions[choice] << " should be less than " << *orderedQuotientDistributions[choice + 1] << std::endl;
exit(-1);
}
}
}
return true;
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::printDistributions(uint_fast64_t state) const {
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
std::cout << quotientDistributions[choice] << std::endl;
}
return true;
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::checkBlockStable(bisimulation::Block<BlockDataType> const& newBlock) const {
std::cout << "checking stability of new block " << newBlock.getId() << " of size " << newBlock.getNumberOfStates() << std::endl;
for (auto stateIt1 = this->partition.begin(newBlock), stateIte1 = this->partition.end(newBlock); stateIt1 != stateIte1; ++stateIt1) {
for (auto stateIt2 = this->partition.begin(newBlock), stateIte2 = this->partition.end(newBlock); stateIt2 != stateIte2; ++stateIt2) {
bool less1 = quotientDistributionsLess(*stateIt1, *stateIt2);
bool less2 = quotientDistributionsLess(*stateIt2, *stateIt1);
if (less1 || less2) {
std::cout << "the partition is not stable for the states " << *stateIt1 << " and " << *stateIt2 << std::endl;
std::cout << "less1 " << less1 << " and less2 " << less2 << std::endl;
std::cout << "distributions of state " << *stateIt1 << std::endl;
this->printDistributions(*stateIt1);
std::cout << "distributions of state " << *stateIt2 << std::endl;
this->printDistributions(*stateIt2);
exit(-1);
}
}
}
return true;
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::checkDistributionsDifferent(bisimulation::Block<BlockDataType> const& block, storm::storage::sparse::state_type end) const {
for (auto stateIt1 = this->partition.begin(block), stateIte1 = this->partition.end(block); stateIt1 != stateIte1; ++stateIt1) {
for (auto stateIt2 = this->partition.begin() + block.getEndIndex(), stateIte2 = this->partition.begin() + end; stateIt2 != stateIte2; ++stateIt2) {
if (!quotientDistributionsLess(*stateIt1, *stateIt2)) {
std::cout << "distributions are not less, even though they should be!" << std::endl;
exit(-3);
} else {
std::cout << "less:" << std::endl;
this->printDistributions(*stateIt1);
std::cout << "and" << std::endl;
this->printDistributions(*stateIt2);
}
}
}
return true;
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::splitBlockAccordingToCurrentQuotientDistributions(Block<BlockDataType>& block, std::vector<Block<BlockDataType>*>& splitterQueue) {
std::list<Block<BlockDataType>*> newBlocks;
bool split = this->partition.splitBlock(block,
[this] (storm::storage::sparse::state_type state1, storm::storage::sparse::state_type state2) {
bool result = quotientDistributionsLess(state1, state2);
return result;
},
[&newBlocks] (Block<BlockDataType>& newBlock) {
newBlocks.push_back(&newBlock);
});
// Defer updating the quotient distributions until *after* all splits, because
// it otherwise influences the subsequent splits!
for (auto el : newBlocks) {
this->updateQuotientDistributionsOfPredecessors(*el, block, splitterQueue);
}
return split;
}
template<typename ModelType>
bool NondeterministicModelBisimulationDecomposition<ModelType>::quotientDistributionsLess(storm::storage::sparse::state_type state1, storm::storage::sparse::state_type state2) const {
STORM_LOG_TRACE("Comparing the quotient distributions of state " << state1 << " and " << state2 << ".");
std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
auto firstIt = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state1];
auto firstIte = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state1 + 1];
auto secondIt = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state2];
auto secondIte = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state2 + 1];
for (; firstIt != firstIte && secondIt != secondIte; ++firstIt, ++secondIt) {
// If the current distributions are in a less-than relationship, we can return a result.
if ((*firstIt)->less(**secondIt, this->comparator)) {
return true;
} else if ((*secondIt)->less(**firstIt, this->comparator)) {
return false;
}
// If the distributions matched, we need to advance both distribution iterators to the next distribution
// that is larger.
while (firstIt != firstIte && std::next(firstIt) != firstIte && !(*firstIt)->less(**std::next(firstIt), this->comparator)) {
++firstIt;
}
while (secondIt != secondIte && std::next(secondIt) != secondIte && !(*secondIt)->less(**std::next(secondIt), this->comparator)) {
++secondIt;
}
}
if (firstIt == firstIte && secondIt != secondIte) {
return true;
}
return false;
}
template<typename ModelType>
void NondeterministicModelBisimulationDecomposition<ModelType>::refinePartitionBasedOnSplitter(bisimulation::Block<BlockDataType>& splitter, std::vector<bisimulation::Block<BlockDataType>*>& splitterQueue) {
if (!possiblyNeedsRefinement(splitter)) {
return;
}
STORM_LOG_TRACE("Refining block " << splitter.getId());
splitBlockAccordingToCurrentQuotientDistributions(splitter, splitterQueue);
}
template class NondeterministicModelBisimulationDecomposition<storm::models::sparse::Mdp<double>>;
#ifdef STORM_HAVE_CARL
template class NondeterministicModelBisimulationDecomposition<storm::models::sparse::Mdp<storm::RationalNumber>>;
template class NondeterministicModelBisimulationDecomposition<storm::models::sparse::Mdp<storm::RationalFunction>>;
#endif
}
}