#pragma once

#include <queue>
#include <chrono>

#include "storm/solver/Z3SmtSolver.h"

#include "storm-counterexamples/counterexamples/HighLevelCounterexample.h"

#include "storm/storage/prism/Program.h"
#include "storm/storage/expressions/Expression.h"
#include "storm/storage/sparse/PrismChoiceOrigins.h"
#include "storm/modelchecker/propositional/SparsePropositionalModelChecker.h"
#include "storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h"
#include "storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h"
#include "storm/modelchecker/results/ExplicitQuantitativeCheckResult.h"
#include "storm/settings/SettingsManager.h"
#include "storm/settings/modules/CoreSettings.h"

#include "storm/utility/macros.h"
#include "storm/exceptions/NotSupportedException.h"

#include "storm/utility/counterexamples.h"
#include "storm/utility/cli.h"

namespace storm {
    
    class Environment;
    
    namespace counterexamples {
        
        /*!
         * This class provides functionality to generate a minimal counterexample to a probabilistic reachability
         * property in terms of used labels.
         */
        template <class T>
        class SMTMinimalLabelSetGenerator {
#ifdef STORM_HAVE_Z3
        private:
            struct RelevancyInformation {
                // The set of relevant states in the model.
                storm::storage::BitVector relevantStates;

                // The set of relevant labels.
                boost::container::flat_set<uint_fast64_t> relevantLabels;
                
                // The set of relevant label sets.
                boost::container::flat_set<boost::container::flat_set<uint_fast64_t>> relevantLabelSets;
                
                // The set of labels that matter in terms of minimality.
                boost::container::flat_set<uint_fast64_t> minimalityLabels;

                // A set of labels that is definitely known to be taken in the final solution.
                boost::container::flat_set<uint_fast64_t> knownLabels;

                boost::container::flat_set<uint64_t> dontCareLabels;
                
                // A list of relevant choices for each relevant state.
                std::map<uint_fast64_t, std::list<uint_fast64_t>> relevantChoicesForRelevantStates;
            };

            struct VariableInformation {
                // The manager responsible for the constraints we are building.
                std::shared_ptr<storm::expressions::ExpressionManager> manager;
                
                // The variables associated with the relevant labels.
                std::vector<storm::expressions::Variable> labelVariables;
                
                // The variables associated with the labels that matter in terms of minimality.
                std::vector<storm::expressions::Variable> minimalityLabelVariables;
                
                // A mapping from relevant labels to their indices in the variable vector.
                std::map<uint_fast64_t, uint_fast64_t> labelToIndexMap;

                // A set of original auxiliary variables needed for the Fu-Malik procedure.
                std::vector<storm::expressions::Variable> originalAuxiliaryVariables;
                
                // A set of auxiliary variables that may be modified by the MaxSAT procedure.
                std::vector<storm::expressions::Variable> auxiliaryVariables;
                
                // A vector of variables that can be used to constrain the number of variables that are set to true.
                std::vector<storm::expressions::Variable> adderVariables;
                
                // A flag whether or not there are variables reserved for encoding reachability of a target state.
                bool hasReachabilityVariables;
                
                // Starting from here, all structures hold information only if hasReachabilityVariables is true;
                
                // A mapping from each pair of adjacent relevant states to their index in the corresponding variable vector.
                std::map<std::pair<uint_fast64_t, uint_fast64_t>, uint_fast64_t> statePairToIndexMap;
                
                // A vector of variables associated with each pair of relevant states (s, s') such that s' is
                // a successor of s.
                std::vector<storm::expressions::Variable> statePairVariables;
                
                // A mapping from relevant states to the index with the corresponding order variable in the state order variable vector.
                std::map<uint_fast64_t, uint_fast64_t> relevantStatesToOrderVariableIndexMap;
                
                // A vector of variables that holds all state order variables.
                std::vector<storm::expressions::Variable> stateOrderVariables;
            };
            
            /*!
             * Computes the set of relevant labels in the model. Relevant labels are choice labels such that there exists
             * a scheduler that satisfies phi until psi with a nonzero probability.
             *
             * @param model The model to search for relevant labels.
             * @param phiStates A bit vector representing all states that satisfy phi.
             * @param psiStates A bit vector representing all states that satisfy psi.
             * @param dontCareLabels A set of labels that are "don't care" labels wrt. minimality.
             * @return A structure containing the relevant labels as well as states.
             */
            static RelevancyInformation determineRelevantStatesAndLabels(storm::models::sparse::Model<T> const& model, std::vector<boost::container::flat_set<uint_fast64_t>> const& labelSets, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, boost::container::flat_set<uint_fast64_t> const& dontCareLabels) {
                // Create result.
                RelevancyInformation relevancyInformation;
                
                // Compute all relevant states, i.e. states for which there exists a scheduler that has a non-zero
                // probabilitiy of satisfying phi until psi.
                storm::storage::SparseMatrix<T> backwardTransitions = model.getBackwardTransitions();
                relevancyInformation.relevantStates = storm::utility::graph::performProbGreater0E(backwardTransitions, phiStates, psiStates);
                relevancyInformation.relevantStates &= ~psiStates;

                STORM_LOG_DEBUG("Found " << relevancyInformation.relevantStates.getNumberOfSetBits() << " relevant states.");

                // Retrieve some references for convenient access.
                storm::storage::SparseMatrix<T> const& transitionMatrix = model.getTransitionMatrix();
                std::vector<uint_fast64_t> const& nondeterministicChoiceIndices = transitionMatrix.getRowGroupIndices();

                // Now traverse all choices of all relevant states and check whether there is a successor target state.
                // If so, the associated labels become relevant. Also, if a choice of a relevant state has at least one
                // relevant successor, the choice becomes relevant.
                for (auto state : relevancyInformation.relevantStates) {
                    relevancyInformation.relevantChoicesForRelevantStates.emplace(state, std::list<uint_fast64_t>());
                    
                    for (uint_fast64_t row = nondeterministicChoiceIndices[state]; row < nondeterministicChoiceIndices[state + 1]; ++row) {
                        for (auto const& entry : transitionMatrix.getRow(row)) {
                            // If there is a relevant successor, we need to add the labels of the current choice.
                            if (relevancyInformation.relevantStates.get(entry.getColumn()) || psiStates.get(entry.getColumn())) {
                                relevancyInformation.relevantChoicesForRelevantStates[state].push_back(row);

                                for (auto const& label : labelSets[row]) {
                                    relevancyInformation.relevantLabels.insert(label);
                                }
                                
                                relevancyInformation.relevantLabelSets.insert(labelSets[row]);
                                break;
                            }
                        }
                    }
                }
                
                // Compute the set of labels that are known to be taken in any case.
                relevancyInformation.knownLabels = storm::utility::counterexamples::getGuaranteedLabelSet(model, labelSets, psiStates, relevancyInformation.relevantLabels);
                if (!relevancyInformation.knownLabels.empty()) {
                    boost::container::flat_set<uint_fast64_t> remainingLabels;
                    std::set_difference(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(remainingLabels, remainingLabels.end()));
                    relevancyInformation.relevantLabels = remainingLabels;
                }

                relevancyInformation.dontCareLabels = dontCareLabels;
                std::set_difference(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end(), dontCareLabels.begin(), dontCareLabels.end(), std::inserter(relevancyInformation.minimalityLabels, relevancyInformation.minimalityLabels.begin()));
                
                STORM_LOG_DEBUG("Found " << relevancyInformation.relevantLabels.size() << " relevant and " << relevancyInformation.knownLabels.size() << " known labels.");
                STORM_LOG_DEBUG("Found " << relevancyInformation.minimalityLabels.size() << " labels to minize over.");
                return relevancyInformation;
            }
            
            /*!
             * Creates all necessary variables.
             *
             * @param manager The manager in which to create the variables.
             * @param relevantCommands A set of relevant labels for which to create the expressions.
             * @return A mapping from relevant labels to their corresponding expressions.
             */
            static VariableInformation createVariables(std::shared_ptr<storm::expressions::ExpressionManager> const& manager, storm::models::sparse::Model<T> const& model, storm::storage::BitVector const& psiStates, RelevancyInformation const& relevancyInformation, bool createReachabilityVariables) {
                VariableInformation variableInformation;
                variableInformation.manager = manager;
                
                // Create stringstream to build expression names.
                std::stringstream variableName;
                
                // Create the variables for the relevant labels.
                for (auto label : relevancyInformation.relevantLabels) {
                    variableInformation.labelToIndexMap[label] = variableInformation.labelVariables.size();
                    
                    // Clear contents of the stream to construct new expression name.
                    variableName.clear();
                    variableName.str("");
                    variableName << "c" << label;
                    
                    variableInformation.labelVariables.push_back(manager->declareBooleanVariable(variableName.str()));
                    
                    // Record if the label is among the ones that matter for minimality.
                    if (relevancyInformation.minimalityLabels.find(label) != relevancyInformation.minimalityLabels.end()) {
                        variableInformation.minimalityLabelVariables.push_back(variableInformation.labelVariables.back());
                    }
                    
                    // Clear contents of the stream to construct new expression name.
                    variableName.clear();
                    variableName.str("");
                    variableName << "h" << label;
                    
                    variableInformation.originalAuxiliaryVariables.push_back(manager->declareBooleanVariable(variableName.str()));
                }
                
                // A mapping from each pair of adjacent relevant states to their index in the corresponding variable vector.
                std::map<std::pair<uint_fast64_t, uint_fast64_t>, uint_fast64_t> statePairToIndexMap;
                
                // A vector of variables associated with each pair of relevant states (s, s') such that s' is
                // a successor of s.
                std::vector<storm::expressions::Variable> statePairVariables;
                
                // Create variables needed for encoding reachability of a target state if requested.
                if (createReachabilityVariables) {
                    variableInformation.hasReachabilityVariables = true;
                    
                    storm::storage::SparseMatrix<T> const& transitionMatrix = model.getTransitionMatrix();
                    
                    for (auto state : relevancyInformation.relevantStates) {
                        variableInformation.relevantStatesToOrderVariableIndexMap[state] = variableInformation.stateOrderVariables.size();
                        
                        // Clear contents of the stream to construct new expression name.
                        variableName.clear();
                        variableName.str("");
                        variableName << "o" << state;
                        
                        variableInformation.stateOrderVariables.push_back(manager->declareRationalVariable(variableName.str()));
                        
                        for (auto const& relevantChoice : relevancyInformation.relevantChoicesForRelevantStates.at(state)) {
                            for (auto const& entry : transitionMatrix.getRow(relevantChoice)) {
                                
                                // If the successor state is neither the state itself nor an irrelevant state, we need to add a variable for the transition.
                                if (state != entry.getColumn() && (relevancyInformation.relevantStates.get(entry.getColumn()) || psiStates.get(entry.getColumn()))) {
                                    
                                    // Make sure that there is not already one variable for the state pair. This may happen because of several nondeterministic choices
                                    // targeting the same state.
                                    if (variableInformation.statePairToIndexMap.find(std::make_pair(state, entry.getColumn())) != variableInformation.statePairToIndexMap.end()) {
                                        continue;
                                    }
                                    
                                    // At this point we know that the state-pair does not have an associated variable.
                                    variableInformation.statePairToIndexMap[std::make_pair(state, entry.getColumn())] = variableInformation.statePairVariables.size();
                                    
                                    // Clear contents of the stream to construct new expression name.
                                    variableName.clear();
                                    variableName.str("");
                                    variableName << "t" << state << "_" << entry.getColumn();
                                    
                                    variableInformation.statePairVariables.push_back(manager->declareBooleanVariable(variableName.str()));
                                }
                            }
                        }
                    }
                    
                    for (auto psiState : psiStates) {
                        variableInformation.relevantStatesToOrderVariableIndexMap[psiState] = variableInformation.stateOrderVariables.size();
                        
                        // Clear contents of the stream to construct new expression name.
                        variableName.clear();
                        variableName.str("");
                        variableName << "o" << psiState;
                        
                        variableInformation.stateOrderVariables.push_back(manager->declareRationalVariable(variableName.str()));
                    }
                }
                
                return variableInformation;
            }

            static storm::expressions::Expression getOtherSynchronizationEnabledFormula(boost::container::flat_set<uint_fast64_t> const& labelSet, std::map<uint_fast64_t, std::set<boost::container::flat_set<uint_fast64_t>>> const& synchronizingLabels, boost::container::flat_map<boost::container::flat_set<uint_fast64_t>, storm::expressions::Expression> const& labelSetToFormula, VariableInformation const& variableInformation, RelevancyInformation const& relevancyInformation) {
                // Taking all commands of a combination does not necessarily mean that a following label set needs to be taken.
                // This is because it could be that the commands are taken to enable other synchronizations. Therefore, we need
                // to add an additional clause that says that the right-hand side of the implication is also true if all commands
                // of the current choice have enabled synchronization options.
                if (labelSet.size() > 1) {
                    storm::expressions::Expression result = variableInformation.manager->boolean(true);
                    for (auto label : labelSet) {
                        storm::expressions::Expression alternativeExpressionForLabel = variableInformation.manager->boolean(false);
                        std::set<boost::container::flat_set<uint_fast64_t>> const& synchsForCommand = synchronizingLabels.at(label);
                        
                        for (auto const& synchSet : synchsForCommand) {
                            storm::expressions::Expression alternativeExpression = variableInformation.manager->boolean(true);
                            
                            // If the current synchSet is the same as left-hand side of the implication, we can to skip it.
                            if (synchSet == labelSet) continue;

                            // Build labels that are missing for this synchronization option.
                            std::set<uint_fast64_t> unknownSynchSetLabels;
                            std::set_difference(synchSet.begin(), synchSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownSynchSetLabels, unknownSynchSetLabels.end()));

                            for (auto label : unknownSynchSetLabels) {
                                alternativeExpression = alternativeExpression && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                            
                            alternativeExpressionForLabel = alternativeExpressionForLabel || (alternativeExpression && labelSetToFormula.at(synchSet));
                        }
                        
                        result = result && alternativeExpressionForLabel;
                    }
                    
                    return result;
                } else {
                    return variableInformation.manager->boolean(false);
                }
            }
            
            /*!
             * Asserts cuts that are derived from the explicit representation of the model and rule out a lot of
             * suboptimal solutions.
             *
             * @param symbolicModel The symbolic model description used to build the model.
             * @param model The labeled model for which to compute the cuts.
             * @param context The Z3 context in which to build the expressions.
             * @param solver The solver to use for the satisfiability evaluation.
             */
            static void assertCuts(storm::storage::SymbolicModelDescription const& symbolicModel, storm::models::sparse::Model<T> const& model, std::vector<boost::container::flat_set<uint_fast64_t>> const& labelSets, storm::storage::BitVector const& psiStates, VariableInformation const& variableInformation, RelevancyInformation const& relevancyInformation, storm::solver::SmtSolver& solver) {
                // Walk through the model and
                // * identify labels enabled in initial states
                // * identify labels that can directly precede a given action
                // * identify labels that directly reach a target state
                // * identify labels that can directly follow a given action

                boost::container::flat_set<uint_fast64_t> initialLabels;
                std::set<boost::container::flat_set<uint_fast64_t>> initialCombinations;
                boost::container::flat_set<uint_fast64_t> targetLabels;
                boost::container::flat_set<boost::container::flat_set<uint_fast64_t>> targetCombinations;
                std::map<boost::container::flat_set<uint_fast64_t>, std::set<boost::container::flat_set<uint_fast64_t>>> precedingLabels;
                std::map<boost::container::flat_set<uint_fast64_t>, std::set<boost::container::flat_set<uint_fast64_t>>> followingLabels;
                std::map<uint_fast64_t, std::set<boost::container::flat_set<uint_fast64_t>>> synchronizingLabels;
                
                // Get some data from the model for convenient access.
                storm::storage::SparseMatrix<T> const& transitionMatrix = model.getTransitionMatrix();
                storm::storage::SparseMatrix<T> backwardTransitions = model.getBackwardTransitions();
                storm::storage::BitVector const& initialStates = model.getInitialStates();
                
                for (auto currentState : relevancyInformation.relevantStates) {
                    for (auto currentChoice : relevancyInformation.relevantChoicesForRelevantStates.at(currentState)) {
                        
                        // If the choice is a synchronization choice, we need to record it.
                        if (labelSets[currentChoice].size() > 1) {
                            for (auto label : labelSets[currentChoice]) {
                                synchronizingLabels[label].emplace(labelSets[currentChoice]);
                            }
                        }
                        
                        // If the state is initial, we need to add all the choice labels to the initial label set.
                        if (initialStates.get(currentState)) {
                            initialLabels.insert(labelSets[currentChoice].begin(), labelSets[currentChoice].end());
                            initialCombinations.insert(labelSets[currentChoice]);
                        }
                        
                        // Iterate over successors and add relevant choices of relevant successors to the following label set.
                        bool canReachTargetState = false;
                        for (auto const& entry : transitionMatrix.getRow(currentChoice)) {
                            if (relevancyInformation.relevantStates.get(entry.getColumn())) {
                                for (auto relevantChoice : relevancyInformation.relevantChoicesForRelevantStates.at(entry.getColumn())) {
                                    if (labelSets[currentChoice] == labelSets[relevantChoice]) continue;
                                    
                                    followingLabels[labelSets[currentChoice]].insert(labelSets[relevantChoice]);
                                }
                            } else if (psiStates.get(entry.getColumn())) {
                                canReachTargetState = true;
                            }
                        }
                        
                        // If the choice can reach a target state directly, we add all the labels to the target label set.
                        if (canReachTargetState) {
                            targetLabels.insert(labelSets[currentChoice].begin(), labelSets[currentChoice].end());
                            targetCombinations.insert(labelSets[currentChoice]);
                        }

                        // Iterate over predecessors and add all choices that target the current state to the preceding
                        // label set of all labels of all relevant choices of the current state.
                        for (auto const& predecessorEntry : backwardTransitions.getRow(currentState)) {
                            if (relevancyInformation.relevantStates.get(predecessorEntry.getColumn())) {
                                for (auto predecessorChoice : relevancyInformation.relevantChoicesForRelevantStates.at(predecessorEntry.getColumn())) {
                                    bool choiceTargetsCurrentState = false;
                                    for (auto const& successorEntry : transitionMatrix.getRow(predecessorChoice)) {
                                        if (successorEntry.getColumn() == currentState) {
                                            choiceTargetsCurrentState = true;
                                        }
                                    }
                                    
                                    if (choiceTargetsCurrentState) {
                                        precedingLabels[labelSets.at(currentChoice)].insert(labelSets.at(predecessorChoice));
                                    }
                                }
                            }
                        }
                    }
                }
                
                // Store the found implications in a container similar to the preceding label sets.
                std::map<boost::container::flat_set<uint_fast64_t>, std::set<boost::container::flat_set<uint_fast64_t>>> backwardImplications;

                if (!symbolicModel.isJaniModel() || !symbolicModel.asJaniModel().usesAssignmentLevels()) {
                    // Create a new solver over the same variables as the given symbolic model description to use it for
                    // determining the symbolic cuts.
                    std::unique_ptr<storm::solver::SmtSolver> localSolver;
                    if (symbolicModel.isPrismProgram()) {
                        storm::prism::Program const& program = symbolicModel.asPrismProgram();
                        localSolver = std::make_unique<storm::solver::Z3SmtSolver>(program.getManager());
                        
                        // Then add the constraints for bounds of the integer variables.
                        for (auto const& integerVariable : program.getGlobalIntegerVariables()) {
                            localSolver->add(integerVariable.getExpressionVariable() >= integerVariable.getLowerBoundExpression());
                            localSolver->add(integerVariable.getExpressionVariable() <= integerVariable.getUpperBoundExpression());
                        }
                        for (auto const& module : program.getModules()) {
                            for (auto const& integerVariable : module.getIntegerVariables()) {
                                localSolver->add(integerVariable.getExpressionVariable() >= integerVariable.getLowerBoundExpression());
                                localSolver->add(integerVariable.getExpressionVariable() <= integerVariable.getUpperBoundExpression());
                            }
                        }
                    } else {
                        storm::jani::Model const& janiModel = symbolicModel.asJaniModel();
                        localSolver = std::make_unique<storm::solver::Z3SmtSolver>(janiModel.getManager());
                        
                        for (auto const& integerVariable : janiModel.getGlobalVariables().getBoundedIntegerVariables()) {
                            if (integerVariable.isTransient()) {
                                continue;
                            }
                            
                            localSolver->add(integerVariable.getExpressionVariable() >= integerVariable.getLowerBound());
                            localSolver->add(integerVariable.getExpressionVariable() <= integerVariable.getUpperBound());
                        }
                        for (auto const& automaton : janiModel.getAutomata()) {
                            for (auto const& integerVariable : automaton.getVariables().getBoundedIntegerVariables()) {
                                if (integerVariable.isTransient()) {
                                    continue;
                                }
                                
                                localSolver->add(integerVariable.getExpressionVariable() >= integerVariable.getLowerBound());
                                localSolver->add(integerVariable.getExpressionVariable() <= integerVariable.getUpperBound());
                            }
                        }
                    }
                    
                    
                    // Construct an expression that exactly characterizes the initial state.
                    storm::expressions::Expression initialStateExpression;
                    if (symbolicModel.isPrismProgram()) {
                       initialStateExpression = symbolicModel.asPrismProgram().getInitialStatesExpression();
                    } else {
                        initialStateExpression = symbolicModel.asJaniModel().getInitialStatesExpression();
                    }
                    
                    // Now check for possible backward cuts.
                    for (auto const& labelSetAndPrecedingLabelSetsPair : precedingLabels) {
                        bool backwardImplicationAdded = false;
//                        std::cout << "labelSetAndPrecedingLabelSetsPair.first ";
//                        for (auto const& e : labelSetAndPrecedingLabelSetsPair.first) {
//                            std::cout << e << ", ";
//                        }
//                        std::cout << std::endl;
                        // Find out the commands for the currently considered label set.
                        storm::expressions::Expression guardConjunction;
                        
                        if (symbolicModel.isPrismProgram()) {
                            storm::prism::Program const& program = symbolicModel.asPrismProgram();
                            
                            for (uint_fast64_t moduleIndex = 0; moduleIndex < program.getNumberOfModules(); ++moduleIndex) {
                                storm::prism::Module const& module = program.getModule(moduleIndex);
                                
                                for (uint_fast64_t commandIndex = 0; commandIndex < module.getNumberOfCommands(); ++commandIndex) {
                                    storm::prism::Command const& command = module.getCommand(commandIndex);
                                    
                                    // If the current command is one of the commands we need to consider, add its guard.
                                    if (labelSetAndPrecedingLabelSetsPair.first.find(command.getGlobalIndex()) != labelSetAndPrecedingLabelSetsPair.first.end()) {
                                        guardConjunction = guardConjunction && command.getGuardExpression();
                                    }
                                }
                            }
                        } else {
                            storm::jani::Model const& janiModel = symbolicModel.asJaniModel();

                            for (uint_fast64_t automatonIndex = 0; automatonIndex < janiModel.getNumberOfAutomata(); ++automatonIndex) {
                                storm::jani::Automaton const& automaton = janiModel.getAutomaton(automatonIndex);
                                
                                for (uint_fast64_t edgeIndex = 0; edgeIndex < automaton.getNumberOfEdges(); ++edgeIndex) {
                                    // If the current edge is one of the edges we need to consider, add its guard.
                                    if (labelSetAndPrecedingLabelSetsPair.first.find(janiModel.encodeAutomatonAndEdgeIndices(automatonIndex, edgeIndex)) != labelSetAndPrecedingLabelSetsPair.first.end()) {
                                        storm::jani::Edge const& edge = automaton.getEdge(edgeIndex);
                                        
                                        guardConjunction = guardConjunction && edge.getGuard();
                                    }
                                }
                            }
                        }
                        
                        // Save the state of the solver so we can easily backtrack.
                        localSolver->push();

                        // Push initial state expression.
                        STORM_LOG_DEBUG("About to assert that combination is not enabled in the current state.");
                        localSolver->add(initialStateExpression);

                        // Check if the label set is enabled in the initial state.
                        localSolver->add(guardConjunction);
                        
                        storm::solver::SmtSolver::CheckResult checkResult = localSolver->check();
                        localSolver->pop();
                        localSolver->push();
                        
                        // If the solver reports unsat, then we know that the current selection is not enabled in the initial state.
                        if (checkResult == storm::solver::SmtSolver::CheckResult::Unsat) {
                            STORM_LOG_DEBUG("Selection not enabled in initial state.");

                            //std::cout << "not gc: " << !guardConjunction << std::endl;
                            localSolver->add(!guardConjunction);
                            STORM_LOG_DEBUG("Asserted disjunction of negated guards.");
                            
                            
                            // Now check the possible preceding label sets for the essential ones.
                            for (auto const& precedingLabelSet : labelSetAndPrecedingLabelSetsPair.second) {

                                if (labelSetAndPrecedingLabelSetsPair.first == precedingLabelSet) continue;

                                //std::cout << "push" << std::endl;
                                // Create a restore point so we can easily pop-off all weakest precondition expressions.
                                localSolver->push();
                                
                                // Find out the commands for the currently considered preceding label set.
                                std::vector<std::vector<boost::container::flat_map<storm::expressions::Variable, storm::expressions::Expression>>> currentPreceedingVariableUpdates;
                                storm::expressions::Expression preceedingGuardConjunction;
                                if (symbolicModel.isPrismProgram()) {
                                    storm::prism::Program const& program = symbolicModel.asPrismProgram();
                                    for (uint_fast64_t moduleIndex = 0; moduleIndex < program.getNumberOfModules(); ++moduleIndex) {
                                        storm::prism::Module const& module = program.getModule(moduleIndex);
                                        
                                        for (uint_fast64_t commandIndex = 0; commandIndex < module.getNumberOfCommands(); ++commandIndex) {
                                            storm::prism::Command const& command = module.getCommand(commandIndex);
                                            
                                            // If the current command is one of the commands we need to consider, store a reference to it in the container.
                                            if (precedingLabelSet.find(command.getGlobalIndex()) != precedingLabelSet.end()) {
                                                preceedingGuardConjunction = preceedingGuardConjunction && command.getGuardExpression();
                                                
                                                currentPreceedingVariableUpdates.emplace_back();
                                                
                                                for (uint64_t updateIndex = 0; updateIndex < command.getNumberOfUpdates(); ++updateIndex) {
                                                    storm::prism::Update const& update = command.getUpdate(updateIndex);
                                                    boost::container::flat_map<storm::expressions::Variable, storm::expressions::Expression> variableUpdates;
                                                    for (auto const& assignment : update.getAssignments()) {
                                                        variableUpdates.emplace(assignment.getVariable(), assignment.getExpression());
                                                    }
                                                    currentPreceedingVariableUpdates.back().emplace_back(std::move(variableUpdates));
                                                }
                                            }
                                        }
                                    }
                                } else {
                                    storm::jani::Model const& janiModel = symbolicModel.asJaniModel();
                                    for (uint_fast64_t automatonIndex = 0; automatonIndex < janiModel.getNumberOfAutomata(); ++automatonIndex) {
                                        storm::jani::Automaton const& automaton = janiModel.getAutomaton(automatonIndex);
                                        
                                        for (uint_fast64_t edgeIndex = 0; edgeIndex < automaton.getNumberOfEdges(); ++edgeIndex) {
                                            // If the current command is one of the commands we need to consider, store a reference to it in the container.
                                            if (precedingLabelSet.find(janiModel.encodeAutomatonAndEdgeIndices(automatonIndex, edgeIndex)) != precedingLabelSet.end()) {
                                                storm::jani::Edge const& edge = automaton.getEdge(edgeIndex);

                                                preceedingGuardConjunction = preceedingGuardConjunction && edge.getGuard();

                                                currentPreceedingVariableUpdates.emplace_back();
                                                
                                                for (uint64_t destinationIndex = 0; destinationIndex < edge.getNumberOfDestinations(); ++destinationIndex) {
                                                    storm::jani::EdgeDestination const& destination = edge.getDestination(destinationIndex);
                                                    boost::container::flat_map<storm::expressions::Variable, storm::expressions::Expression> variableUpdates;
                                                    for (auto const& assignment : destination.getOrderedAssignments().getNonTransientAssignments()) {
                                                        variableUpdates.emplace(assignment.getVariable().getExpressionVariable(), assignment.getAssignedExpression());
                                                    }
                                                    currentPreceedingVariableUpdates.back().emplace_back(std::move(variableUpdates));
                                                }
                                            }
                                        }
                                    }
                                }

                                //std::cout << "pgc: " << preceedingGuardConjunction << std::endl;

                                // Assert all the guards of the preceding command set.
                                localSolver->add(preceedingGuardConjunction);
                                
                                std::vector<std::vector<boost::container::flat_map<storm::expressions::Variable, storm::expressions::Expression>>::const_iterator> iteratorVector;
                                for (auto const& variableUpdates : currentPreceedingVariableUpdates) {
                                    iteratorVector.push_back(variableUpdates.begin());
                                }
                                
                                // Iterate over all possible combinations of updates of the preceding command set.
                                std::vector<storm::expressions::Expression> formulae;
                                bool done = false;
                                while (!done) {
                                    std::map<storm::expressions::Variable, storm::expressions::Expression> currentVariableUpdateCombinationMap;
                                    for (auto const& updateIterator : iteratorVector) {
                                        for (auto const& variableUpdatePair : *updateIterator) {
                                            currentVariableUpdateCombinationMap.emplace(variableUpdatePair.first, variableUpdatePair.second);
                                        }
                                    }
                                    
                                    STORM_LOG_DEBUG("About to assert a weakest precondition.");
                                    storm::expressions::Expression wp = guardConjunction.substitute(currentVariableUpdateCombinationMap);
                                    formulae.push_back(wp);
                                    STORM_LOG_DEBUG("Asserted weakest precondition.");
                                    
                                    // Now try to move iterators to the next position if possible. If we could properly
                                    // move it, we can directly move on to the next combination of updates. If we have
                                    // to reset it to the start, we do so unless there is no more iterator to reset.
                                    uint_fast64_t k = iteratorVector.size();
                                    for (; k > 0; --k) {
                                        ++iteratorVector[k - 1];
                                        if (iteratorVector[k - 1] == currentPreceedingVariableUpdates[k - 1].end()) {
                                            iteratorVector[k - 1] = currentPreceedingVariableUpdates[k - 1].begin();
                                        } else {
                                            break;
                                        }
                                    }
                                    
                                    // If we had to reset all iterator to the start, we are done.
                                    if (k == 0) {
                                        done = true;
                                    }
                                }
                                
                                // Now assert the disjunction of all weakest preconditions of all considered update combinations.
                                assertDisjunction(*localSolver, formulae, symbolicModel.isPrismProgram() ? symbolicModel.asPrismProgram().getManager() : symbolicModel.asJaniModel().getManager());
                                
                                STORM_LOG_DEBUG("Asserted disjunction of all weakest preconditions.");
                                storm::solver::SmtSolver::CheckResult result = localSolver->check();

                                if (result == storm::solver::SmtSolver::CheckResult::Sat) {
                                    backwardImplications[labelSetAndPrecedingLabelSetsPair.first].insert(precedingLabelSet);
                                    backwardImplicationAdded = true;
                                } else if (result == storm::solver::SmtSolver::CheckResult::Unknown) {
                                    STORM_LOG_ERROR("The SMT solver does not come to a conclusive answer. Does your model contain integer division?");
                                }

                                localSolver->pop();
                            }
                            
                            // Popping the disjunction of negated guards from the solver stack.
                            localSolver->pop();
                            STORM_LOG_ERROR_COND(backwardImplicationAdded, "Error in adding cuts for counterexample generation (backward implication misses a label set).");
                        } else {
                            STORM_LOG_DEBUG("Selection is enabled in initial state.");
                        }

                    }
                } else if (symbolicModel.isJaniModel()) {
                    STORM_LOG_WARN("Model uses assignment levels, did not assert backward implications.");
                }
                
                STORM_LOG_DEBUG("Successfully gathered data for cuts.");

                // Compute the sets of labels such that the transitions labeled with this set possess at least one known label.
                boost::container::flat_set<boost::container::flat_set<uint_fast64_t>> hasKnownSuccessor;
                for (auto const& labelSetFollowingSetsPair : followingLabels) {
                    for (auto const& set : labelSetFollowingSetsPair.second) {
                        if (std::includes(relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), set.begin(), set.end())) {
                            hasKnownSuccessor.insert(set);
                            break;
                        }
                    }
                }

                // Compute the sets of labels such that the transitions labeled with this set possess at least one known predecessor.
                boost::container::flat_set<boost::container::flat_set<uint_fast64_t>> hasKnownPredecessor;
                for (auto const& labelSetImplicationsPair : backwardImplications) {
                    for (auto const& set : labelSetImplicationsPair.second) {
                        if (std::includes(relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), set.begin(), set.end())) {
                            hasKnownPredecessor.insert(labelSetImplicationsPair.first);
                            break;
                        }
                    }
                }
                
                STORM_LOG_DEBUG("Asserting initial combination is taken.");
                {
                    std::vector<storm::expressions::Expression> formulae;
                    
                    // Start by asserting that we take at least one initial label. We may do so only if there is no initial
                    // combination that is already known. Otherwise this condition would be too strong.
                    bool initialCombinationKnown = false;
                    for (auto const& combination : initialCombinations) {
                        boost::container::flat_set<uint_fast64_t> tmpSet;
                        std::set_difference(combination.begin(), combination.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(tmpSet, tmpSet.end()));
                        if (tmpSet.size() == 0) {
                            initialCombinationKnown = true;
                            break;
                        } else {
                            storm::expressions::Expression conj = variableInformation.manager->boolean(true);
                            for (auto label : tmpSet) {
                                conj = conj && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                            formulae.push_back(conj);
                        }
                    }
                    if (!initialCombinationKnown) {
                        assertDisjunction(solver, formulae, *variableInformation.manager);
                    }
                }
                
                STORM_LOG_DEBUG("Asserting target combination is taken.");
                {
                    std::vector<storm::expressions::Expression> formulae;

                    // Likewise, if no target combination is known, we may assert that there is at least one.
                    bool targetCombinationKnown = false;
                    for (auto const& combination : targetCombinations) {
                        boost::container::flat_set<uint_fast64_t> tmpSet;
                        std::set_difference(combination.begin(), combination.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(tmpSet, tmpSet.end()));
                        if (tmpSet.size() == 0) {
                            targetCombinationKnown = true;
                            break;
                        } else {
                            storm::expressions::Expression conj = variableInformation.manager->boolean(true);
                            for (auto label : tmpSet) {
                                conj = conj && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                            formulae.push_back(conj);
                        }
                    }
                    if (!targetCombinationKnown) {
                        assertDisjunction(solver, formulae, *variableInformation.manager);
                    }
                }
                
                STORM_LOG_DEBUG("Asserting taken labels are followed and preceeded by another label if they are not a target label or an initial label, respectively.");
                boost::container::flat_map<boost::container::flat_set<uint_fast64_t>, storm::expressions::Expression> labelSetToFormula;
                for (auto const& labelSet : relevancyInformation.relevantLabelSets) {
                    storm::expressions::Expression labelSetFormula = variableInformation.manager->boolean(false);

                    // Compute the set of unknown labels on the left-hand side of the implication.
                    boost::container::flat_set<uint_fast64_t> unknownLhsLabels;
                    std::set_difference(labelSet.begin(), labelSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownLhsLabels, unknownLhsLabels.end()));
                    for (auto label : unknownLhsLabels) {
                        labelSetFormula = labelSetFormula || !variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                    }
                    
                    // Only build a constraint if the combination does not lead to a target state and
                    // no successor set is already known.
                    storm::expressions::Expression successorExpression;
                    if (targetCombinations.find(labelSet) == targetCombinations.end() && hasKnownSuccessor.find(labelSet) == hasKnownSuccessor.end()) {
                        successorExpression = variableInformation.manager->boolean(false);

                        auto const& followingLabelSets = followingLabels.at(labelSet);

                        for (auto const& followingSet : followingLabelSets) {
                            boost::container::flat_set<uint_fast64_t> tmpSet;
                            
                            // Check which labels of the current following set are not known. This set must be non-empty, because
                            // otherwise a successor combination would already be known and control cannot reach this point.
                            std::set_difference(followingSet.begin(), followingSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(tmpSet, tmpSet.end()));
                            
                            // Construct an expression that enables all unknown labels of the current following set.
                            storm::expressions::Expression conj = variableInformation.manager->boolean(true);
                            for (auto label : tmpSet) {
                                conj = conj && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                            successorExpression = successorExpression || conj;
                        }
                    } else {
                        successorExpression = variableInformation.manager->boolean(true);
                    }
                    // Constructed following cuts at this point.
                    
                    // Only build a constraint if the combination is no initial combination and no
                    // predecessor set is already known.
                    storm::expressions::Expression predecessorExpression;
                    if (initialCombinations.find(labelSet) == initialCombinations.end() && hasKnownPredecessor.find(labelSet) == hasKnownPredecessor.end()) {
                        predecessorExpression = variableInformation.manager->boolean(false);
                        
//                        std::cout << "labelSet" << std::endl;
//                        for (auto const& e : labelSet) {
//                            std::cout << e << ", ";
//                        }
//                        std::cout << std::endl;

                        auto const& preceedingLabelSets = backwardImplications.at(labelSet);

                        for (auto const& preceedingSet : preceedingLabelSets) {
                            boost::container::flat_set<uint_fast64_t> tmpSet;
                            
                            // Check which labels of the current following set are not known. This set must be non-empty, because
                            // otherwise a predecessor combination would already be known and control cannot reach this point.
                            std::set_difference(preceedingSet.begin(), preceedingSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(tmpSet, tmpSet.end()));
                            
                            // Construct an expression that enables all unknown labels of the current following set.
                            storm::expressions::Expression conj = variableInformation.manager->boolean(true);
                            for (auto label : tmpSet) {
                                conj = conj && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                            predecessorExpression = predecessorExpression || conj;
                        }
                    } else {
                        predecessorExpression = variableInformation.manager->boolean(true);
                    }
                    
                    labelSetFormula = labelSetFormula || (successorExpression && predecessorExpression);

                    labelSetToFormula[labelSet] = labelSetFormula;
                }
                
                for (auto const& labelSetFormula : labelSetToFormula) {
                    solver.add(labelSetFormula.second || getOtherSynchronizationEnabledFormula(labelSetFormula.first, synchronizingLabels, labelSetToFormula, variableInformation, relevancyInformation));
                }

                STORM_LOG_DEBUG("Asserting synchronization cuts.");
                // Finally, assert that if we take one of the synchronizing labels, we also take one of the combinations
                // the label appears in.
                for (auto const& labelSynchronizingSetsPair : synchronizingLabels) {
                    std::vector<storm::expressions::Expression> formulae;

                    if (relevancyInformation.knownLabels.find(labelSynchronizingSetsPair.first) == relevancyInformation.knownLabels.end()) {
                        formulae.push_back(!variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(labelSynchronizingSetsPair.first)));
                    }

                    // We need to be careful, because there may be one synchronisation set out of which all labels are
                    // known, which means we must not assert anything.
                    bool allImplicantsKnownForOneSet = false;
                    for (auto const& synchronizingSet : labelSynchronizingSetsPair.second) {
                        storm::expressions::Expression currentCombination = variableInformation.manager->boolean(true);
                        bool allImplicantsKnownForCurrentSet = true;
                        for (auto label : synchronizingSet) {
                            if (relevancyInformation.knownLabels.find(label) == relevancyInformation.knownLabels.end() && label != labelSynchronizingSetsPair.first) {
                                currentCombination = currentCombination && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label));
                            }
                        }
                        formulae.push_back(currentCombination);

                        // If all implicants of the current set are known, we do not need to further build the constraint.
                        if (allImplicantsKnownForCurrentSet) {
                            allImplicantsKnownForOneSet = true;
                            break;
                        }
                    }

                    if (!allImplicantsKnownForOneSet) {
                        assertDisjunction(solver, formulae, *variableInformation.manager);
                    }
                }
            }
            
            /*!
             * Asserts constraints necessary to encode the reachability of at least one target state from the initial states.
             */
            static void assertReachabilityCuts(storm::models::sparse::Model<T> const& model, std::vector<boost::container::flat_set<uint_fast64_t>> const& labelSets, storm::storage::BitVector const& psiStates, VariableInformation const& variableInformation, RelevancyInformation const& relevancyInformation, storm::solver::SmtSolver& solver) {
                
                if (!variableInformation.hasReachabilityVariables) {
                    throw storm::exceptions::InvalidStateException() << "Impossible to assert reachability cuts without the necessary variables.";
                }
                
                // Get some data from the model for convenient access.
                storm::storage::SparseMatrix<T> const& transitionMatrix = model.getTransitionMatrix();
                storm::storage::SparseMatrix<T> backwardTransitions = model.getBackwardTransitions();

                // First, we add the formulas that encode
                // (1) if an incoming transition is chosen, an outgoing one is chosen as well (for non-initial states)
                // (2) an outgoing transition out of the initial states is taken.
                storm::expressions::Expression initialStateExpression = variableInformation.manager->boolean(false);
                for (auto relevantState : relevancyInformation.relevantStates) {
                    if (!model.getInitialStates().get(relevantState)) {
                        // Assert the constraints (1).
                        boost::container::flat_set<uint_fast64_t> relevantPredecessors;
                        for (auto const& predecessorEntry : backwardTransitions.getRow(relevantState)) {
                            if (relevantState != predecessorEntry.getColumn() && relevancyInformation.relevantStates.get(predecessorEntry.getColumn())) {
                                relevantPredecessors.insert(predecessorEntry.getColumn());
                            }
                        }
                        
                        boost::container::flat_set<uint_fast64_t> relevantSuccessors;
                        for (auto const& relevantChoice : relevancyInformation.relevantChoicesForRelevantStates.at(relevantState)) {
                            for (auto const& successorEntry : transitionMatrix.getRow(relevantChoice)) {
                                if (relevantState != successorEntry.getColumn() && (relevancyInformation.relevantStates.get(successorEntry.getColumn()) || psiStates.get(successorEntry.getColumn()))) {
                                    relevantSuccessors.insert(successorEntry.getColumn());
                                }
                            }
                        }
                        
                        storm::expressions::Expression expression = variableInformation.manager->boolean(true);
                        for (auto predecessor : relevantPredecessors) {
                            expression = expression && !variableInformation.statePairVariables.at(variableInformation.statePairToIndexMap.at(std::make_pair(predecessor, relevantState)));
                        }
                        for (auto successor : relevantSuccessors) {
                            expression = expression || variableInformation.statePairVariables.at(variableInformation.statePairToIndexMap.at(std::make_pair(relevantState, successor)));
                        }
                        
                        solver.add(expression);
                    } else {
                        // Assert the constraints (2).
                        boost::container::flat_set<uint_fast64_t> relevantSuccessors;
                        for (auto const& relevantChoice : relevancyInformation.relevantChoicesForRelevantStates.at(relevantState)) {
                            for (auto const& successorEntry : transitionMatrix.getRow(relevantChoice)) {
                                if (relevantState != successorEntry.getColumn() && (relevancyInformation.relevantStates.get(successorEntry.getColumn()) || psiStates.get(successorEntry.getColumn()))) {
                                    relevantSuccessors.insert(successorEntry.getColumn());
                                }
                            }
                        }
                        
                        for (auto successor : relevantSuccessors) {
                            initialStateExpression = initialStateExpression || variableInformation.statePairVariables.at(variableInformation.statePairToIndexMap.at(std::make_pair(relevantState, successor)));
                        }
                    }
                }
                solver.add(initialStateExpression);
                
                // Finally, add constraints that
                // (1) if a transition is selected, a valid labeling is selected as well.
                // (2) enforce that if a transition from s to s' is selected, the ordering variables become strictly larger.
                for (auto const& statePairIndexPair : variableInformation.statePairToIndexMap) {
                    uint_fast64_t sourceState = statePairIndexPair.first.first;
                    uint_fast64_t targetState = statePairIndexPair.first.second;
                    
                    // Assert constraint for (1).
                    boost::container::flat_set<uint_fast64_t> choicesForStatePair;
                    for (auto const& relevantChoice : relevancyInformation.relevantChoicesForRelevantStates.at(sourceState)) {
                        for (auto const& successorEntry : transitionMatrix.getRow(relevantChoice)) {
                            if (successorEntry.getColumn() == targetState) {
                                choicesForStatePair.insert(relevantChoice);
                            }
                        }
                    }
                    storm::expressions::Expression labelExpression = !variableInformation.statePairVariables.at(statePairIndexPair.second);
                    for (auto choice : choicesForStatePair) {
                        storm::expressions::Expression choiceExpression = variableInformation.manager->boolean(true);
                        for (auto element : labelSets.at(choice)) {
                            if (relevancyInformation.knownLabels.find(element) == relevancyInformation.knownLabels.end()) {
                                choiceExpression = choiceExpression && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(element));
                            }
                        }
                        labelExpression = labelExpression || choiceExpression;
                    }
                    solver.add(labelExpression);

                    // Assert constraint for (2).
                    storm::expressions::Expression orderExpression = !variableInformation.statePairVariables.at(statePairIndexPair.second) || variableInformation.stateOrderVariables.at(variableInformation.relevantStatesToOrderVariableIndexMap.at(sourceState)).getExpression() < variableInformation.stateOrderVariables.at(variableInformation.relevantStatesToOrderVariableIndexMap.at(targetState)).getExpression();
                    solver.add(orderExpression);
                }
            }
        
            /*!
             * Asserts that the disjunction of the given formulae holds. If the content of the disjunction is empty,
             * this corresponds to asserting false.
             *
             * @param solver The solver to use for the satisfiability evaluation.
             * @param formulaVector A vector of expressions that shall form the disjunction.
             * @param manager The expression manager to use.
             */
            static void assertDisjunction(storm::solver::SmtSolver& solver, std::vector<storm::expressions::Expression> const& formulaVector, storm::expressions::ExpressionManager const& manager) {
                storm::expressions::Expression disjunction = manager.boolean(false);
                for (auto expr : formulaVector) {
                    disjunction = disjunction || expr;
                }
                solver.add(disjunction);
            }

            
            /*!
             * Creates a full-adder for the two inputs and returns the resulting bit as well as the carry bit.
             *
             * @param in1 The first input to the adder.
             * @param in2 The second input to the adder.
             * @param carryIn The carry bit input to the adder.
             * @return A pair whose first component represents the carry bit and whose second component represents the
             * result bit.
             */
            static std::pair<storm::expressions::Expression, storm::expressions::Expression> createFullAdder(storm::expressions::Expression in1, storm::expressions::Expression in2, storm::expressions::Expression carryIn) {
                storm::expressions::Expression resultBit;
                storm::expressions::Expression carryBit;

                if (carryIn.isFalse()) {
                    resultBit = (in1 && !in2) || (!in1 && in2);
                    carryBit = in1 && in2;
                } else {
                    resultBit = (in1 && !in2 && !carryIn) || (!in1 && in2 && !carryIn) || (!in1 && !in2 && carryIn) || (in1 && in2 && carryIn);
                    carryBit = in1 && in2 || in1 && carryIn || in2 && carryIn;
                }
                
                return std::make_pair(carryBit, resultBit);
            }
            
            /*!
             * Creates an adder for the two inputs of equal size. The resulting vector represents the different bits of
             * the sum (and is thus one bit longer than the two inputs).
             *
             * @param variableInformation The variable information structure.
             * @param in1 The first input to the adder.
             * @param in2 The second input to the adder.
             * @return A vector representing the bits of the sum of the two inputs.
             */
            static std::vector<storm::expressions::Expression> createAdder(VariableInformation const& variableInformation, std::vector<storm::expressions::Expression> const& in1, std::vector<storm::expressions::Expression> const& in2) {
                // Sanity check for sizes of input.
                if (in1.size() != in2.size() || in1.size() == 0) {
                    STORM_LOG_ERROR("Illegal input to adder (" << in1.size() << ", " << in2.size() << ").");
                    throw storm::exceptions::InvalidArgumentException() << "Illegal input to adder.";
                }
                
                // Prepare result.
                std::vector<storm::expressions::Expression> result;
                result.reserve(in1.size() + 1);
                
                // Add all bits individually and pass on carry bit appropriately.
                storm::expressions::Expression carryBit = variableInformation.manager->boolean(false);
                for (uint_fast64_t currentBit = 0; currentBit < in1.size(); ++currentBit) {
                    std::pair<storm::expressions::Expression, storm::expressions::Expression> localResult = createFullAdder(in1[currentBit], in2[currentBit], carryBit);
                    
                    result.push_back(localResult.second);
                    carryBit = localResult.first;
                }
                result.push_back(carryBit);
                
                return result;
            }
            
            /*!
             * Given a number of input numbers, creates a number of output numbers that corresponds to the sum of two
             * consecutive numbers of the input. If the number if input numbers is odd, the last number is simply added
             * to the output.
             *
             * @param variableInformation The variable information structure.
             * @param in A vector or binary encoded numbers.
             * @return A vector of numbers that each correspond to the sum of two consecutive elements of the input.
             */
            static std::vector<std::vector<storm::expressions::Expression>> createAdderPairs(VariableInformation const& variableInformation, std::vector<std::vector<storm::expressions::Expression>> const& in) {
                std::vector<std::vector<storm::expressions::Expression>> result;
                
                result.reserve(in.size() / 2 + in.size() % 2);
                
                for (uint_fast64_t index = 0; index < in.size() / 2; ++index) {
                    result.push_back(createAdder(variableInformation, in[2 * index], in[2 * index + 1]));
                }
                
                if (in.size() % 2 != 0) {
                    result.push_back(in.back());
                    result.back().push_back(variableInformation.manager->boolean(false));
                }
                
                return result;
            }
            
            /*!
             * Creates a counter circuit that returns the number of literals out of the given vector that are set to true.
             *
             * @param variableInformation The variable information structure.
             * @param literals The literals for which to create the adder circuit.
             * @return A bit vector representing the number of literals that are set to true.
             */
            static std::vector<storm::expressions::Expression> createCounterCircuit(VariableInformation const& variableInformation, std::vector<storm::expressions::Variable> const& literals) {
                if (literals.empty()) {
                    return std::vector<storm::expressions::Expression>();
                }
                
                // Create the auxiliary vector.
                std::vector<std::vector<storm::expressions::Expression>> aux;
                for (uint_fast64_t index = 0; index < literals.size(); ++index) {
                    aux.emplace_back();
                    aux.back().push_back(literals[index]);
                }
                
                while (aux.size() > 1) {
                    aux = createAdderPairs(variableInformation, aux);
                }

                STORM_LOG_DEBUG("Created counter circuit for " << literals.size() << " literals.");

                return aux.front();
            }
            
            /*!
             * Determines whether the bit at the given index is set in the given value.
             *
             * @param value The value to test.
             * @param index The index of the bit to test.
             * @return True iff the bit at the given index is set in the given value.
             */
            static bool bitIsSet(uint64_t value, uint64_t index) {
                uint64_t mask = 1 << (index & 63);
                return (value & mask) != 0;
            }
            
            /*!
             * Asserts a constraint in the given solver that expresses that the value encoded by the given input variables
             * may at most represent the number k. The constraint is associated with a relaxation variable, that is
             * returned by this function and may be used to deactivate the constraint.
             *
             * @param solver The solver to use for the satisfiability evaluation.
             * @param variableInformation The struct that holds the variable information.
             * @param k The bound for the binary-encoded value.
             * @return The relaxation variable associated with the constraint.
             */
            static storm::expressions::Variable assertLessOrEqualKRelaxed(storm::solver::SmtSolver& solver, VariableInformation const& variableInformation, uint64_t k) {
                STORM_LOG_DEBUG("Asserting solution has size less or equal " << k << ".");
                
                std::vector<storm::expressions::Variable> const& input = variableInformation.adderVariables;
                
                // If there are no input variables, the value is always 0 <= k, so there is nothing to assert.
                if (input.empty()) {
                    std::stringstream variableName;
                    variableName << "relaxed" << k;
                    return variableInformation.manager->declareBooleanVariable(variableName.str());
                }
                
                storm::expressions::Expression result;
                if (bitIsSet(k, 0)) {
                    result = variableInformation.manager->boolean(true);
                } else {
                    result = !input.at(0);
                }
                for (uint_fast64_t index = 1; index < input.size(); ++index) {
                    storm::expressions::Expression i1;
                    storm::expressions::Expression i2;
                    
                    if (bitIsSet(k, index)) {
                        i1 = !input.at(index);
                        i2 = result;
                    } else {
                        i1 = variableInformation.manager->boolean(false);
                        i2 = variableInformation.manager->boolean(false);
                    }
                    result = i1 || i2 || (!input.at(index) && result);
                }
                
                std::stringstream variableName;
                variableName << "relaxed" << k;
                storm::expressions::Variable relaxingVariable = variableInformation.manager->declareBooleanVariable(variableName.str());
                result = result || relaxingVariable;
                
                solver.add(result);
                
                return relaxingVariable;
            }
            

            /*!
             * Determines the set of labels that was chosen by the given model.
             *
             * @param model The model from which to extract the information.
             * @param variableInformation A structure with information about the variables of the solver.
             */
            static boost::container::flat_set<uint_fast64_t> getUsedLabelSet(storm::solver::SmtSolver::ModelReference const& model, VariableInformation const& variableInformation) {
                boost::container::flat_set<uint_fast64_t> result;
                for (auto const& labelIndexPair : variableInformation.labelToIndexMap) {
                    bool commandIncluded = model.getBooleanValue(variableInformation.labelVariables.at(labelIndexPair.second));
                    
                    if (commandIncluded) {
                        result.insert(labelIndexPair.first);
                    }
                }
                return result;
            }
            
            /*!
             * Asserts an adder structure in the given solver that counts the number of variables that are set to true
             * out of the given variables.
             *
             * @param solver The solver for which to add the adder.
             * @param variableInformation A structure with information about the variables of the solver.
             */
            static std::vector<storm::expressions::Variable> assertAdder(storm::solver::SmtSolver& solver, VariableInformation const& variableInformation) {
                auto start = std::chrono::high_resolution_clock::now();
                std::stringstream variableName;
                std::vector<storm::expressions::Variable> result;
                
                std::vector<storm::expressions::Expression> adderVariables = createCounterCircuit(variableInformation, variableInformation.minimalityLabelVariables);
                for (uint_fast64_t i = 0; i < adderVariables.size(); ++i) {
                    variableName.str("");
                    variableName.clear();
                    variableName << "adder" << i;
                    result.push_back(variableInformation.manager->declareBooleanVariable(variableName.str()));
                    solver.add(storm::expressions::implies(adderVariables[i], result.back()));
                    STORM_LOG_TRACE("Added bit " << i << " of adder.");
                }

                auto end = std::chrono::high_resolution_clock::now();
                uint64_t duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
                STORM_LOG_DEBUG("Asserted adder in " << duration << "ms.");

                return result;
            }
            
            /*!
             * Finds the smallest set of labels such that the constraint system of the solver is still satisfiable.
             *
             * @param solver The solver to use for the satisfiability evaluation.
             * @param variableInformation A structure with information about the variables of the solver.
             * @param currentBound The currently known lower bound for the number of labels that need to be enabled
             * in order to satisfy the constraint system.
             * @return The smallest set of labels such that the constraint system of the solver is satisfiable.
             */
            static boost::optional<boost::container::flat_set<uint_fast64_t>> findSmallestCommandSet(storm::solver::SmtSolver& solver, VariableInformation& variableInformation, uint_fast64_t& currentBound) {
                // Check if we can find a solution with the current bound.
                storm::expressions::Expression assumption = !variableInformation.auxiliaryVariables.back();

                // As long as the constraints are unsatisfiable, we need to relax the last at-most-k constraint and
                // try with an increased bound.
                while (solver.checkWithAssumptions({assumption}) == storm::solver::SmtSolver::CheckResult::Unsat) {
                    STORM_LOG_DEBUG("Constraint system is unsatisfiable with at most " << currentBound << " taken commands; increasing bound.");
#ifndef NDEBUG
                    STORM_LOG_DEBUG("Sanity check to see whether constraint system is still satisfiable.");
                    STORM_LOG_ASSERT(solver.check() == storm::solver::SmtSolver::CheckResult::Sat, "Constraint system is not satisfiable anymore.");
#endif
                    solver.add(variableInformation.auxiliaryVariables.back());
                    variableInformation.auxiliaryVariables.push_back(assertLessOrEqualKRelaxed(solver, variableInformation, ++currentBound));
                    assumption = !variableInformation.auxiliaryVariables.back();
                    if (currentBound > variableInformation.minimalityLabelVariables.size()) {
                        STORM_LOG_DEBUG("Constraint system fully explored: Bound exceeds maximum of " <<  variableInformation.minimalityLabelVariables.size());
                        return boost::none;
                    }
                }
                
                // At this point we know that the constraint system was satisfiable, so compute the induced label
                // set and return it.
                return getUsedLabelSet(*solver.getModel(), variableInformation);
            }
            
            static void ruleOutSingleSolution(storm::solver::SmtSolver& solver, boost::container::flat_set<uint_fast64_t> const& labelSet, VariableInformation& variableInformation, RelevancyInformation const& relevancyInformation) {
                std::vector<storm::expressions::Expression> formulae;
                
                boost::container::flat_set<uint_fast64_t> unknownLabels;
                std::set_intersection(labelSet.begin(), labelSet.end(), relevancyInformation.minimalityLabels.begin(), relevancyInformation.minimalityLabels.end(), std::inserter(unknownLabels, unknownLabels.end()));

                //std::set_difference(labelSet.begin(), labelSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownLabels, unknownLabels.end()));
                for (auto const& label : unknownLabels) {
                    formulae.emplace_back(!variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label)));
                }

                boost::container::flat_set<uint_fast64_t> remainingLabels;
                //std::set_difference(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end(), labelSet.begin(), labelSet.end(), std::inserter(remainingLabels, remainingLabels.end()));
                std::set_difference(relevancyInformation.minimalityLabels.begin(), relevancyInformation.minimalityLabels.end(), labelSet.begin(), labelSet.end(), std::inserter(remainingLabels, remainingLabels.end()));

                for (auto const& label : remainingLabels) {
                    formulae.emplace_back(variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label)));
                }
                
                STORM_LOG_DEBUG("Ruling out single solution.");
                assertDisjunction(solver, formulae, *variableInformation.manager);
            }

            static void ruleOutBiggerSolutions(storm::solver::SmtSolver& solver, boost::container::flat_set<uint_fast64_t> const& labelSet, VariableInformation& variableInformation, RelevancyInformation const& relevancyInformation) {
                std::vector<storm::expressions::Expression> formulae;

                boost::container::flat_set<uint_fast64_t> unknownLabels;
                std::set_intersection(labelSet.begin(), labelSet.end(), relevancyInformation.minimalityLabels.begin(), relevancyInformation.minimalityLabels.end(), std::inserter(unknownLabels, unknownLabels.end()));

                //std::set_difference(labelSet.begin(), labelSet.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownLabels, unknownLabels.end()));
                for (auto const& label : unknownLabels) {
                    formulae.emplace_back(!variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label)));
                }


                STORM_LOG_DEBUG("Ruling out set of solutions.");
                assertDisjunction(solver, formulae, *variableInformation.manager);
            }
            
            /*!
             * Analyzes the given sub-model that has a maximal reachability of zero (i.e. no psi states are reachable) and tries to construct assertions that aim to make at least one psi state reachable.
             *
             * @param solver The solver to use for the satisfiability evaluation.
             * @param subModel The sub-model resulting from restricting the original model to the given command set.
             * @param originalModel The original model.
             * @param phiStates A bit vector characterizing all phi states in the model.
             * @param psiState A bit vector characterizing all psi states in the model.
             * @param commandSet The currently chosen set of commands.
             * @param variableInformation A structure with information about the variables of the solver.
             */
            static void analyzeZeroProbabilitySolution(storm::solver::SmtSolver& solver, storm::models::sparse::Model<T> const& subModel, std::vector<boost::container::flat_set<uint_fast64_t>> const& subLabelSets, storm::models::sparse::Model<T> const& originalModel, std::vector<boost::container::flat_set<uint_fast64_t>> const& originalLabelSets, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, boost::container::flat_set<uint_fast64_t> const& commandSet, VariableInformation& variableInformation, RelevancyInformation const& relevancyInformation) {
                storm::storage::BitVector reachableStates(subModel.getNumberOfStates());
                
                STORM_LOG_DEBUG("Analyzing solution with zero probability.");
                
                // Initialize the stack for the DFS.
                bool targetStateIsReachable = false;
                std::vector<uint_fast64_t> stack;
                stack.reserve(subModel.getNumberOfStates());
                for (auto initialState : subModel.getInitialStates()) {
                    stack.push_back(initialState);
                    reachableStates.set(initialState, true);
                }
                
                storm::storage::SparseMatrix<T> const& transitionMatrix = subModel.getTransitionMatrix();
                std::vector<uint_fast64_t> const& nondeterministicChoiceIndices = transitionMatrix.getRowGroupIndices();
                
                // Now determine which states and labels are actually reachable.
                boost::container::flat_set<uint_fast64_t> reachableLabels;
                while (!stack.empty()) {
                    uint_fast64_t currentState = stack.back();
                    stack.pop_back();

                    for (uint_fast64_t currentChoice = nondeterministicChoiceIndices[currentState]; currentChoice < nondeterministicChoiceIndices[currentState + 1]; ++currentChoice) {
                        bool choiceTargetsRelevantState = false;
                        
                        for (auto const& successorEntry : transitionMatrix.getRow(currentChoice)) {
                            if (relevancyInformation.relevantStates.get(successorEntry.getColumn()) && currentState != successorEntry.getColumn()) {
                                choiceTargetsRelevantState = true;
                                if (!reachableStates.get(successorEntry.getColumn())) {
                                    reachableStates.set(successorEntry.getColumn());
                                    stack.push_back(successorEntry.getColumn());
                                }
                            } else if (psiStates.get(successorEntry.getColumn())) {
                                targetStateIsReachable = true;
                            }
                        }
                        
                        if (choiceTargetsRelevantState) {
                            for (auto label : subLabelSets[currentChoice]) {
                                reachableLabels.insert(label);
                            }
                        }
                    }
                }
                
                STORM_LOG_DEBUG("Successfully performed reachability analysis.");
                
                if (targetStateIsReachable) {
                    STORM_LOG_ERROR("Target must be unreachable for this analysis.");
                    throw storm::exceptions::InvalidStateException() << "Target must be unreachable for this analysis.";
                }
                
                storm::storage::BitVector unreachableRelevantStates = ~reachableStates & relevancyInformation.relevantStates;
                storm::storage::BitVector statesThatCanReachTargetStates = storm::utility::graph::performProbGreater0E(subModel.getBackwardTransitions(), phiStates, psiStates);
                
                boost::container::flat_set<uint_fast64_t> locallyRelevantLabels;
                std::set_difference(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end(), commandSet.begin(), commandSet.end(), std::inserter(locallyRelevantLabels, locallyRelevantLabels.begin()));
                
                std::vector<boost::container::flat_set<uint_fast64_t>> guaranteedLabelSets = storm::utility::counterexamples::getGuaranteedLabelSets(originalModel, originalLabelSets, statesThatCanReachTargetStates, locallyRelevantLabels);
                STORM_LOG_DEBUG("Found " << reachableLabels.size() << " reachable labels and " << reachableStates.getNumberOfSetBits() << " reachable states.");
                
                // Search for states on the border of the reachable state space, i.e. states that are still reachable
                // and possess a (disabled) option to leave the reachable part of the state space.
                std::set<boost::container::flat_set<uint_fast64_t>> cutLabels;
                for (auto state : reachableStates) {
                    for (auto currentChoice : relevancyInformation.relevantChoicesForRelevantStates.at(state)) {
                        if (!std::includes(commandSet.begin(), commandSet.end(), originalLabelSets[currentChoice].begin(), originalLabelSets[currentChoice].end())) {
                            bool isBorderChoice = false;

                            // Determine whether the state has the option to leave the reachable state space and go to the unreachable relevant states.
                            for (auto const& successorEntry : originalModel.getTransitionMatrix().getRow(currentChoice)) {
                                if (unreachableRelevantStates.get(successorEntry.getColumn())) {
                                    isBorderChoice = true;
                                }
                            }

                            if (isBorderChoice) {
                                boost::container::flat_set<uint_fast64_t> currentLabelSet;
                                std::set_difference(originalLabelSets[currentChoice].begin(), originalLabelSets[currentChoice].end(), commandSet.begin(), commandSet.end(), std::inserter(currentLabelSet, currentLabelSet.begin()));
                                std::set_difference(guaranteedLabelSets[state].begin(), guaranteedLabelSets[state].end(), commandSet.begin(), commandSet.end(), std::inserter(currentLabelSet, currentLabelSet.end()));

                                cutLabels.insert(currentLabelSet);
                            }
                        }
                    }
                }
                
                // Given the results of the previous analysis, we construct the implications.
                std::vector<storm::expressions::Expression> formulae;
                boost::container::flat_set<uint_fast64_t> unknownReachableLabels;
                std::set_difference(reachableLabels.begin(), reachableLabels.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownReachableLabels, unknownReachableLabels.end()));
                boost::container::flat_set<uint64_t> unknownReachableMinimalityLabels;
                std::set_intersection(unknownReachableLabels.begin(), unknownReachableLabels.end(), relevancyInformation.minimalityLabels.begin(), relevancyInformation.minimalityLabels.end(), std::inserter(unknownReachableMinimalityLabels, unknownReachableMinimalityLabels.end()));

                for (auto label : unknownReachableMinimalityLabels) {
                    formulae.push_back(!variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label)));
                }
                for (auto const& cutLabelSet : cutLabels) {
                    storm::expressions::Expression cube = variableInformation.manager->boolean(true);
                    for (auto cutLabel : cutLabelSet) {
                        cube = cube && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(cutLabel));
                    }
                    
                    formulae.push_back(cube);
                }
                
                STORM_LOG_DEBUG("Asserting reachability implications.");
                assertDisjunction(solver, formulae, *variableInformation.manager);
            }
            
            /*!
             * Analyzes the given sub-model that has a non-zero maximal reachability and tries to construct assertions that aim to guide the solver to solutions
             * with an improved probability value.
             *
             * @param solver The solver to use for the satisfiability evaluation.
             * @param subModel The sub-model resulting from restricting the original model to the given command set.
             * @param originalModel The original model.
             * @param phiStates A bit vector characterizing all phi states in the model.
             * @param psiState A bit vector characterizing all psi states in the model.
             * @param commandSet The currently chosen set of commands.
             * @param variableInformation A structure with information about the variables of the solver.
             */
            static void analyzeInsufficientProbabilitySolution(storm::solver::SmtSolver& solver, storm::models::sparse::Model<T> const& subModel, std::vector<boost::container::flat_set<uint_fast64_t>> const& subLabelSets, storm::models::sparse::Model<T> const& originalModel, std::vector<boost::container::flat_set<uint_fast64_t>> const& originalLabelSets, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, boost::container::flat_set<uint_fast64_t> const& commandSet, VariableInformation& variableInformation, RelevancyInformation const& relevancyInformation) {

                STORM_LOG_DEBUG("Analyzing solution with insufficient probability.");

                storm::storage::BitVector reachableStates(subModel.getNumberOfStates());
                
                // Initialize the stack for the DFS.
                std::vector<uint_fast64_t> stack;
                stack.reserve(subModel.getNumberOfStates());
                for (auto initialState : subModel.getInitialStates()) {
                    stack.push_back(initialState);
                    reachableStates.set(initialState, true);
                }
                
                storm::storage::SparseMatrix<T> const& transitionMatrix = subModel.getTransitionMatrix();
                std::vector<uint_fast64_t> const& nondeterministicChoiceIndices = transitionMatrix.getRowGroupIndices();
                
                // Now determine which states and labels are actually reachable.
                boost::container::flat_set<uint_fast64_t> reachableLabels;
                while (!stack.empty()) {
                    uint_fast64_t currentState = stack.back();
                    stack.pop_back();
                    
                    for (uint_fast64_t currentChoice = nondeterministicChoiceIndices[currentState]; currentChoice < nondeterministicChoiceIndices[currentState + 1]; ++currentChoice) {
                        bool choiceTargetsRelevantState = false;
                        
                        for (auto const& successorEntry : transitionMatrix.getRow(currentChoice)) {
                            if (relevancyInformation.relevantStates.get(successorEntry.getColumn()) && currentState != successorEntry.getColumn()) {
                                choiceTargetsRelevantState = true;
                                if (!reachableStates.get(successorEntry.getColumn())) {
                                    reachableStates.set(successorEntry.getColumn(), true);
                                    stack.push_back(successorEntry.getColumn());
                                }
                            } //else if (psiStates.get(successorEntry.getColumn())) {
                                //targetStateIsReachable = true;
                            //}
                        }
                        
                        if (choiceTargetsRelevantState) {
                            for (auto label : subLabelSets[currentChoice]) {
                                reachableLabels.insert(label);
                            }
                        }
                    }
                }
                STORM_LOG_DEBUG("Successfully determined reachable state space.");
                
                storm::storage::BitVector unreachableRelevantStates = ~reachableStates & relevancyInformation.relevantStates;
                storm::storage::BitVector statesThatCanReachTargetStates = storm::utility::graph::performProbGreater0E(subModel.getBackwardTransitions(), phiStates, psiStates);
                
                boost::container::flat_set<uint_fast64_t> locallyRelevantLabels;
                std::set_difference(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end(), commandSet.begin(), commandSet.end(), std::inserter(locallyRelevantLabels, locallyRelevantLabels.begin()));
                
                std::vector<boost::container::flat_set<uint_fast64_t>> guaranteedLabelSets = storm::utility::counterexamples::getGuaranteedLabelSets(originalModel, originalLabelSets, statesThatCanReachTargetStates, locallyRelevantLabels);
                
                // Search for states for which we could enable another option and possibly improve the reachability probability.
                std::set<boost::container::flat_set<uint_fast64_t>> cutLabels;
                for (auto state : reachableStates) {
                    for (auto currentChoice : relevancyInformation.relevantChoicesForRelevantStates.at(state)) {
                        if (!std::includes(commandSet.begin(), commandSet.end(), originalLabelSets[currentChoice].begin(), originalLabelSets[currentChoice].end())) {
                            boost::container::flat_set<uint_fast64_t> currentLabelSet;
                            std::set_difference(originalLabelSets[currentChoice].begin(), originalLabelSets[currentChoice].end(), commandSet.begin(), commandSet.end(), std::inserter(currentLabelSet, currentLabelSet.begin()));
                            std::set_difference(guaranteedLabelSets[state].begin(), guaranteedLabelSets[state].end(), commandSet.begin(), commandSet.end(), std::inserter(currentLabelSet, currentLabelSet.end()));

                            cutLabels.insert(currentLabelSet);
                        }
                    }
                }
                
                // Given the results of the previous analysis, we construct the implications
                std::vector<storm::expressions::Expression> formulae;
                boost::container::flat_set<uint_fast64_t> unknownReachableLabels;
                std::set_difference(reachableLabels.begin(), reachableLabels.end(), relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end(), std::inserter(unknownReachableLabels, unknownReachableLabels.end()));
                boost::container::flat_set<uint64_t> unknownReachableMinimalityLabels;
                std::set_intersection(unknownReachableLabels.begin(), unknownReachableLabels.end(), relevancyInformation.minimalityLabels.begin(), relevancyInformation.minimalityLabels.end(), std::inserter(unknownReachableMinimalityLabels, unknownReachableMinimalityLabels.end()));

                for (auto label : unknownReachableMinimalityLabels) {
                    formulae.push_back(!variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(label)));
                }
                for (auto const& cutLabelSet : cutLabels) {
                    storm::expressions::Expression cube = variableInformation.manager->boolean(true);
                    for (auto cutLabel : cutLabelSet) {
                        cube = cube && variableInformation.labelVariables.at(variableInformation.labelToIndexMap.at(cutLabel));
                    }
                    
                    formulae.push_back(cube);
                }
                
                STORM_LOG_DEBUG("Asserting reachability implications.");
                assertDisjunction(solver, formulae, *variableInformation.manager);
            }
#endif
        
            
            /*!
             * Returns the sub-model obtained from removing all choices that do not originate from the specified filterLabelSet.
             * Also returns the Labelsets of the sub-model.
             */
            static std::pair<std::shared_ptr<storm::models::sparse::Model<T>>, std::vector<boost::container::flat_set<uint_fast64_t>>> restrictModelToLabelSet(storm::models::sparse::Model<T> const& model,  boost::container::flat_set<uint_fast64_t> const& filterLabelSet, boost::optional<std::string> const& rewardName = boost::none, boost::optional<uint64_t> absorbState = boost::none) {

                bool customRowGrouping = model.isOfType(storm::models::ModelType::Mdp);
                
                std::vector<boost::container::flat_set<uint_fast64_t>> resultLabelSet;
                storm::storage::SparseMatrixBuilder<T> transitionMatrixBuilder(0, model.getTransitionMatrix().getColumnCount(), 0, true, customRowGrouping, model.getTransitionMatrix().getRowGroupCount());

                // Check for each choice of each state, whether the choice commands are fully contained in the given command set.
                uint_fast64_t currentRow = 0;
                for(uint_fast64_t state = 0; state < model.getNumberOfStates(); ++state) {
                    bool stateHasValidChoice = false;
                    for (uint_fast64_t choice = model.getTransitionMatrix().getRowGroupIndices()[state]; choice < model.getTransitionMatrix().getRowGroupIndices()[state + 1]; ++choice) {
                        auto const& choiceLabelSet = model.getChoiceOrigins()->isPrismChoiceOrigins() ? model.getChoiceOrigins()->asPrismChoiceOrigins().getCommandSet(choice) : model.getChoiceOrigins()->asJaniChoiceOrigins().getEdgeIndexSet(choice);
                        bool choiceValid = std::includes(filterLabelSet.begin(), filterLabelSet.end(), choiceLabelSet.begin(), choiceLabelSet.end());

                        // If the choice is valid, copy over all its elements.
                        if (choiceValid) {
                            if (!stateHasValidChoice && customRowGrouping) {
                                transitionMatrixBuilder.newRowGroup(currentRow);
                            }
                            stateHasValidChoice = true;
                            for (auto const& entry : model.getTransitionMatrix().getRow(choice)) {
                                transitionMatrixBuilder.addNextValue(currentRow, entry.getColumn(), entry.getValue());
                            }
                            resultLabelSet.push_back(choiceLabelSet);
                            ++currentRow;
                        }
                    }

                    // If no choice of the current state may be taken, we insert a self-loop to the state instead.
                    if (!stateHasValidChoice) {
                        if (customRowGrouping) {
                            transitionMatrixBuilder.newRowGroup(currentRow);
                        }
                        uint64_t targetState = absorbState == boost::none ? state : absorbState.get();
                        transitionMatrixBuilder.addNextValue(currentRow, targetState, storm::utility::one<T>());
                        // Insert an empty label set for this choice
                        resultLabelSet.emplace_back();
                        ++currentRow;
                    }

                }

                std::shared_ptr<storm::models::sparse::Model<T>> resultModel;
                if (model.isOfType(storm::models::ModelType::Dtmc)) {
                    resultModel = std::make_shared<storm::models::sparse::Dtmc<T>>(transitionMatrixBuilder.build(), storm::models::sparse::StateLabeling(model.getStateLabeling()), model.getRewardModels());
                } else {
                    resultModel = std::make_shared<storm::models::sparse::Mdp<T>>(transitionMatrixBuilder.build(), storm::models::sparse::StateLabeling(model.getStateLabeling()), model.getRewardModels());
                }
                
                return std::make_pair(resultModel, std::move(resultLabelSet));
            }

            static T computeMaximalReachabilityProbability(Environment const& env, storm::models::sparse::Model<T> const& model, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, boost::optional<std::string> const& rewardName) {
                T result = storm::utility::zero<T>();
                
                std::vector<T> allStatesResult;
                
                STORM_LOG_DEBUG("Invoking model checker.");
                if (model.isOfType(storm::models::ModelType::Dtmc)) {
                    if (rewardName == boost::none) {
                        allStatesResult = storm::modelchecker::helper::SparseDtmcPrctlHelper<T>::computeUntilProbabilities(env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), phiStates, psiStates, false);
                    } else {
                        allStatesResult = storm::modelchecker::helper::SparseDtmcPrctlHelper<T>::computeReachabilityRewards(env, false, model.getTransitionMatrix(), model.getBackwardTransitions(), model.getRewardModel(rewardName.get()), psiStates, false);
                    }
                } else {
                    if (rewardName == boost::none) {
                        storm::modelchecker::helper::SparseMdpPrctlHelper<T> modelCheckerHelper;
                        allStatesResult = std::move(modelCheckerHelper.computeUntilProbabilities(env, false, model.getTransitionMatrix(),model.getBackwardTransitions(), phiStates,psiStates, false, false).values);
                    } else {
                        STORM_LOG_THROW(rewardName != boost::none, storm::exceptions::NotSupportedException, "Reward property counterexample generation is currently only supported for DTMCs.");
                    }
                }
                for (auto state : model.getInitialStates()) {
                    result = std::max(result, allStatesResult[state]);
                }
                return result;
            }
            
        public:
            struct Options {
                Options(bool checkThresholdFeasible = false) : checkThresholdFeasible(checkThresholdFeasible) {
                    auto const& settings = storm::settings::getModule<storm::settings::modules::CounterexampleGeneratorSettings>();
                    
                    encodeReachability = settings.isEncodeReachabilitySet();
                    useDynamicConstraints = settings.isUseDynamicConstraintsSet();
                }
                
                bool checkThresholdFeasible;
                bool encodeReachability;
                bool useDynamicConstraints;
                bool silent = false;
                uint64_t continueAfterFirstCounterexampleUntil = 0;
                uint64_t maximumCounterexamples = 1;
                uint64_t multipleCounterexampleSizeCap = 100000000;
            };

            struct GeneratorStats {
                std::chrono::milliseconds setupTime;
                std::chrono::milliseconds solverTime;
                std::chrono::milliseconds modelCheckingTime;
                std::chrono::milliseconds analysisTime;
            };
            
            /*!
             * Computes the minimal command set that is needed in the given model to exceed the given probability threshold for satisfying phi until psi.
             *
             * @param symbolicModel The symbolic model description that was used to build the model.
             * @param model The sparse model in which to find the minimal command set.
             * @param phiStates A bit vector characterizing all phi states in the model.
             * @param psiStates A bit vector characterizing all psi states in the model.
             * @param propertyThreshold The threshold that is to be achieved or exceeded.
             * @param rewardName The name of the reward structure to use, or boost::none if probabilities are considerd.
             * @param strictBound Indicates whether the threshold needs to be achieved (true) or exceeded (false).
             * @param options A set of options for customization.
             */
            static std::vector<boost::container::flat_set<uint_fast64_t>> getMinimalLabelSet(Environment const& env, GeneratorStats& stats, storm::storage::SymbolicModelDescription const& symbolicModel, storm::models::sparse::Model<T> const& model, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates, double propertyThreshold, boost::optional<std::string> const& rewardName, bool strictBound, boost::container::flat_set<uint_fast64_t> const& dontCareLabels = boost::container::flat_set<uint_fast64_t>(), Options const& options = Options()) {
#ifdef STORM_HAVE_Z3
                std::vector<boost::container::flat_set<uint_fast64_t>> result;
                // Set up all clocks used for time measurement.
                auto totalClock = std::chrono::high_resolution_clock::now();
                auto timeOfLastMessage = std::chrono::high_resolution_clock::now();
                decltype(std::chrono::high_resolution_clock::now() - totalClock) totalTime(0);
                
                auto setupTimeClock = std::chrono::high_resolution_clock::now();
                decltype(std::chrono::high_resolution_clock::now() - setupTimeClock) totalSetupTime(0);
                
                auto solverClock = std::chrono::high_resolution_clock::now();
                decltype(std::chrono::high_resolution_clock::now() - solverClock) totalSolverTime(0);
                
                auto modelCheckingClock = std::chrono::high_resolution_clock::now();
                decltype(std::chrono::high_resolution_clock::now() - modelCheckingClock) totalModelCheckingTime(0);

                auto analysisClock = std::chrono::high_resolution_clock::now();
                decltype(std::chrono::high_resolution_clock::now() - analysisClock) totalAnalysisTime(0);

                // (0) Obtain the label sets for each choice.
                // The label set of a choice corresponds to the set of prism commands that induce the choice.
                STORM_LOG_THROW(model.hasChoiceOrigins(), storm::exceptions::InvalidArgumentException, "Restriction to minimal command set is impossible for model without choice origins.");
                STORM_LOG_THROW(model.getChoiceOrigins()->isPrismChoiceOrigins() || model.getChoiceOrigins()->isJaniChoiceOrigins(), storm::exceptions::InvalidArgumentException, "Restriction to label set is impossible for model without PRISM or JANI choice origins.");
                
                std::vector<boost::container::flat_set<uint_fast64_t>> labelSets(model.getNumberOfChoices());
                if (model.getChoiceOrigins()->isPrismChoiceOrigins()) {
                    storm::storage::sparse::PrismChoiceOrigins const& choiceOrigins = model.getChoiceOrigins()->asPrismChoiceOrigins();
                    for (uint_fast64_t choice = 0; choice < model.getNumberOfChoices(); ++choice) {
                        labelSets[choice] = choiceOrigins.getCommandSet(choice);
                    }
                } else {
                    storm::storage::sparse::JaniChoiceOrigins const& choiceOrigins = model.getChoiceOrigins()->asJaniChoiceOrigins();
                    for (uint_fast64_t choice = 0; choice < model.getNumberOfChoices(); ++choice) {
                        labelSets[choice] = choiceOrigins.getEdgeIndexSet(choice);
                    }
                }
                assert(labelSets.size() == model.getNumberOfChoices());
                
                // (1) Check whether its possible to exceed the threshold if checkThresholdFeasible is set.
                double maximalReachabilityProbability = 0;
                if (options.checkThresholdFeasible) {
                    maximalReachabilityProbability = computeMaximalReachabilityProbability(env, model, phiStates, psiStates, rewardName);
                    
                    STORM_LOG_THROW((strictBound && maximalReachabilityProbability >= propertyThreshold) || (!strictBound && maximalReachabilityProbability > propertyThreshold), storm::exceptions::InvalidArgumentException, "Given probability threshold " << propertyThreshold << " can not be " << (strictBound ? "achieved" : "exceeded") << " in model with maximal reachability probability of " << maximalReachabilityProbability << ".");
                    std::cout << std::endl << "Maximal property value in model is " << maximalReachabilityProbability << "." << std::endl << std::endl;
                }
                
                // (2) Identify all states and commands that are relevant, because only these need to be considered later.
                RelevancyInformation relevancyInformation = determineRelevantStatesAndLabels(model, labelSets, phiStates, psiStates, dontCareLabels);
                
                // (3) Create a solver.
                std::shared_ptr<storm::expressions::ExpressionManager> manager = std::make_shared<storm::expressions::ExpressionManager>();
                std::unique_ptr<storm::solver::SmtSolver> solver = std::make_unique<storm::solver::Z3SmtSolver>(*manager);
                
                // (4) Create the variables for the relevant commands.
                VariableInformation variableInformation = createVariables(manager, model, psiStates, relevancyInformation, options.encodeReachability);
                STORM_LOG_DEBUG("Created variables.");

                // (5) Now assert an adder whose result variables can later be used to constrain the nummber of label
                // variables that were set to true. Initially, we are looking for a solution that has no label enabled
                // and subsequently relax that.
                variableInformation.adderVariables = assertAdder(*solver, variableInformation);
                variableInformation.auxiliaryVariables.push_back(assertLessOrEqualKRelaxed(*solver, variableInformation, 0));
                
                // (6) Add constraints that cut off a lot of suboptimal solutions.
                STORM_LOG_DEBUG("Asserting cuts.");
                assertCuts(symbolicModel, model, labelSets, psiStates, variableInformation, relevancyInformation, *solver);
                STORM_LOG_DEBUG("Asserted cuts.");
                if (options.encodeReachability) {
                    assertReachabilityCuts(model, labelSets, psiStates, variableInformation, relevancyInformation, *solver);
                    STORM_LOG_DEBUG("Asserted reachability cuts.");
                }
                
                // As we are done with the setup at this point, stop the clock for the setup time.
                totalSetupTime = std::chrono::high_resolution_clock::now() - setupTimeClock;
                
                // (7) Find the smallest set of commands that satisfies all constraints. If the probability of
                // satisfying phi until psi exceeds the given threshold, the set of labels is minimal and can be returned.
                // Otherwise, the current solution has to be ruled out and the next smallest solution is retrieved from
                // the solver.

                boost::container::flat_set<uint_fast64_t> commandSet(relevancyInformation.knownLabels);
                
                // If there are no relevant labels, return directly.
                if (relevancyInformation.relevantLabels.empty()) {
                    return {commandSet};
                } else if (relevancyInformation.minimalityLabels.empty()) {
                    commandSet.insert(relevancyInformation.relevantLabels.begin(), relevancyInformation.relevantLabels.end());
                    return {commandSet};
                }
                
                // Set up some variables for the iterations.
                bool done = false;
                uint_fast64_t lastSize = 0;
                uint_fast64_t iterations = 0;
                uint_fast64_t currentBound = 0;
                double maximalPropertyValue = 0;
                uint_fast64_t zeroProbabilityCount = 0;
                uint64_t smallestCounterexampleSize = model.getNumberOfChoices(); // Definitive u
                uint64_t progressDelay = storm::settings::getModule<storm::settings::modules::GeneralSettings>().getShowProgressDelay();
                do {
                    STORM_LOG_DEBUG("Computing minimal command set.");
                    solverClock = std::chrono::high_resolution_clock::now();
                    boost::optional<boost::container::flat_set<uint_fast64_t>> smallest = findSmallestCommandSet(*solver, variableInformation, currentBound);
                    totalSolverTime += std::chrono::high_resolution_clock::now() - solverClock;
                    if(smallest == boost::none) {
                        STORM_LOG_DEBUG("No further counterexamples.");
                        break;
                    } else {
                        commandSet = smallest.get();
                    }
                    STORM_LOG_DEBUG("Computed minimal command with bound " << currentBound <<  " and set of size " <<  commandSet.size() + relevancyInformation.knownLabels.size() << " (" << commandSet.size() << " + " << relevancyInformation.knownLabels.size() << ") ");
                    
                    // Restrict the given model to the current set of labels and compute the reachability probability.
                    modelCheckingClock = std::chrono::high_resolution_clock::now();
                    commandSet.insert(relevancyInformation.knownLabels.begin(), relevancyInformation.knownLabels.end());
                    commandSet.insert(relevancyInformation.dontCareLabels.begin(), relevancyInformation.dontCareLabels.end());
                    if (commandSet.size() > smallestCounterexampleSize + options.continueAfterFirstCounterexampleUntil || (result.size() > 1 && commandSet.size() > options.multipleCounterexampleSizeCap)  ) {
                        STORM_LOG_DEBUG("No further counterexamples of similar size.");
                        break;
                    }


                    auto subChoiceOrigins = restrictModelToLabelSet(model, commandSet, rewardName, psiStates.getNextSetIndex(0));
                    std::shared_ptr<storm::models::sparse::Model<T>> const& subModel = subChoiceOrigins.first;
                    std::vector<boost::container::flat_set<uint_fast64_t>> const& subLabelSets = subChoiceOrigins.second;
  
                    // Now determine the maximal reachability probability in the sub-model.
                    maximalPropertyValue = computeMaximalReachabilityProbability(env, *subModel, phiStates, psiStates, rewardName);
                    totalModelCheckingTime += std::chrono::high_resolution_clock::now() - modelCheckingClock;
                    
                    // Depending on whether the threshold was successfully achieved or not, we proceed by either analyzing the bad solution or stopping the iteration process.
                    analysisClock = std::chrono::high_resolution_clock::now();
                    if ((strictBound && maximalPropertyValue < propertyThreshold) || (!strictBound && maximalPropertyValue <= propertyThreshold)) {
                        if (maximalPropertyValue == storm::utility::zero<T>()) {
                            ++zeroProbabilityCount;
                        }
                        
                        if (options.useDynamicConstraints) {
                            // Determine which of the two analysis techniques to call by performing a reachability analysis.
                            storm::storage::BitVector reachableStates = storm::utility::graph::getReachableStates(subModel->getTransitionMatrix(), subModel->getInitialStates(), phiStates, psiStates);
                            
                            if (reachableStates.isDisjointFrom(psiStates)) {
                                // If there was no target state reachable, analyze the solution and guide the solver into the right direction.
                                analyzeZeroProbabilitySolution(*solver, *subModel, subLabelSets, model, labelSets, phiStates, psiStates, commandSet, variableInformation, relevancyInformation);
                            } else {
                                // If the reachability probability was greater than zero (i.e. there is a reachable target state), but the probability was insufficient to exceed
                                // the given threshold, we analyze the solution and try to guide the solver into the right direction.
                                analyzeInsufficientProbabilitySolution(*solver, *subModel, subLabelSets, model, labelSets, phiStates, psiStates, commandSet, variableInformation, relevancyInformation);
                            }

                            if (relevancyInformation.dontCareLabels.size() > 0) {
                                ruleOutSingleSolution(*solver, commandSet, variableInformation, relevancyInformation);
                            }
                        } else {
                            // Do not guide solver, just rule out current solution.
                            ruleOutSingleSolution(*solver, commandSet, variableInformation, relevancyInformation);
                        }
                    } else {
                        STORM_LOG_DEBUG("Found a counterexample.");
                        result.push_back(commandSet);
                        if (options.maximumCounterexamples > result.size()) {
                            STORM_LOG_DEBUG("Exclude counterexample for future.");
                            ruleOutBiggerSolutions(*solver, commandSet, variableInformation, relevancyInformation);

                        } else {
                            STORM_LOG_DEBUG("Stop searching for further counterexamples.");
                            done = true;
                        }

                    }
                    totalAnalysisTime += (std::chrono::high_resolution_clock::now() - analysisClock);
                    ++iterations;

                    auto now = std::chrono::high_resolution_clock::now();
                    auto durationSinceLastMessage = std::chrono::duration_cast<std::chrono::seconds>(now - timeOfLastMessage).count();
                    if (static_cast<uint64_t>(durationSinceLastMessage) >= progressDelay || lastSize < commandSet.size()) {
                        auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now - totalClock).count();
                        if (lastSize < commandSet.size()) {
                            std::cout << "Improved lower bound to " << currentBound << " after " << milliseconds << "ms." << std::endl;
                            lastSize = commandSet.size();
                        } else {
                            std::cout << "Lower bound on label set size is " << currentBound << " after " << milliseconds << "ms (checked " << iterations << " models, " << zeroProbabilityCount << " could not reach the target set)." << std::endl;
                            timeOfLastMessage = std::chrono::high_resolution_clock::now();
                        }
                    }
                } while (!done);

                // Compute and emit the time measurements if the corresponding flag was set.
                totalTime = std::chrono::high_resolution_clock::now() - totalClock;

                stats.analysisTime = std::chrono::duration_cast<std::chrono::milliseconds>(totalAnalysisTime);
                stats.setupTime = std::chrono::duration_cast<std::chrono::milliseconds>(totalSetupTime);
                stats.modelCheckingTime = std::chrono::duration_cast<std::chrono::milliseconds>(totalModelCheckingTime);
                stats.solverTime = std::chrono::duration_cast<std::chrono::milliseconds>(totalSolverTime);

                if (storm::settings::getModule<storm::settings::modules::CoreSettings>().isShowStatisticsSet()) {
                    boost::container::flat_set<uint64_t> allLabels;
                    for (auto const& e : labelSets) {
                        allLabels.insert(e.begin(), e.end());
                    }


                    std::cout << "Metrics:" << std::endl;
                    std::cout << "    * all labels: " << allLabels.size() << std::endl;
                    std::cout << "    * known labels: " << relevancyInformation.knownLabels.size() << std::endl;
                    std::cout << "    * relevant labels: " << (relevancyInformation.knownLabels.size() + relevancyInformation.relevantLabels.size()) << std::endl;
                    std::cout << std::endl;
                    std::cout << "Time breakdown:" << std::endl;
                    std::cout << "    * time for setup: " << std::chrono::duration_cast<std::chrono::milliseconds>(totalSetupTime).count() << "ms" << std::endl;
                    std::cout << "    * time for solving: " << std::chrono::duration_cast<std::chrono::milliseconds>(totalSolverTime).count() << "ms" << std::endl;
                    std::cout << "    * time for checking: " << std::chrono::duration_cast<std::chrono::milliseconds>(totalModelCheckingTime).count() << "ms" << std::endl;
                    std::cout << "    * time for analysis: " << std::chrono::duration_cast<std::chrono::milliseconds>(totalAnalysisTime).count() << "ms" << std::endl;
                    std::cout << "------------------------------------------" << std::endl;
                    std::cout << "    * total time: " << std::chrono::duration_cast<std::chrono::milliseconds>(totalTime).count() << "ms" << std::endl;
                    std::cout << std::endl;
                    std::cout << "Other:" << std::endl;
                    std::cout << "    * number of models checked: " << iterations << std::endl;
                    std::cout << "    * number of models that could not reach a target state: " << zeroProbabilityCount << " (" << 100 * static_cast<double>(zeroProbabilityCount)/iterations << "%)" << std::endl << std::endl;
                }

                return result;
#else
                throw storm::exceptions::NotImplementedException() << "This functionality is unavailable since storm has been compiled without support for Z3.";
#endif
            }
            
            static void extendLabelSetLowerBound(storm::models::sparse::Model<T> const& model, boost::container::flat_set<uint_fast64_t>& commandSet, storm::storage::BitVector const& phiStates, storm::storage::BitVector const& psiStates,  bool silent = false) {
                auto startTime = std::chrono::high_resolution_clock::now();
                
                // Create sub-model that only contains the choices allowed by the given command set.
                std::shared_ptr<storm::models::sparse::Model<T>> subModel = restrictModelToLabelSet(model, commandSet).first;
                
                // Then determine all prob0E(psi) states that are reachable in the sub-model.
                storm::storage::BitVector reachableProb0EStates = storm::utility::graph::getReachableStates(subModel->getTransitionMatrix(), subModel->getInitialStates(), phiStates, psiStates);
                
                // Create a queue of reachable prob0E(psi) states so we can check which commands need to be added
                // to give them a strategy that avoids psi states.
                std::queue<uint_fast64_t> prob0EWorklist;
                for (auto const& e : reachableProb0EStates) {
                    prob0EWorklist.push(e);
                }
                
                // As long as there are reachable prob0E(psi) states, we add commands so they can stay within
                // prob0E(states).
                while (!prob0EWorklist.empty()) {
                    uint_fast64_t state = prob0EWorklist.front();
                    prob0EWorklist.pop();
                    
                    // Now iterate over the original choices of the prob0E(psi) state and add at least one.
                    bool hasLabeledChoice = false;
                    uint64_t smallestCommandSetSize = 0;
                    uint64_t smallestCommandChoice = model.getTransitionMatrix().getRowGroupIndices()[state];
                    
                    // Determine the choice with the least amount of commands (bad heuristic).
                    for (uint64_t choice = smallestCommandChoice; choice < model.getTransitionMatrix().getRowGroupIndices()[state + 1]; ++choice) {
                        bool onlyProb0ESuccessors = true;
                        for (auto const& successorEntry : model.getTransitionMatrix().getRow(choice)) {
                            if (!psiStates.get(successorEntry.getColumn())) {
                                onlyProb0ESuccessors = false;
                                break;
                            }
                        }
                        
                        if (onlyProb0ESuccessors) {
                            uint64_t labelSetSize = 0;
                            if (model.getChoiceOrigins()->isPrismChoiceOrigins()) {
                                labelSetSize = model.getChoiceOrigins()->asPrismChoiceOrigins().getCommandSet(choice).size();
                            } else {
                                assert(model.getChoiceOrigins()->isJaniChoiceOrigins());
                                labelSetSize = model.getChoiceOrigins()->asJaniChoiceOrigins().getEdgeIndexSet(choice).size();
                            }
                            hasLabeledChoice |= (labelSetSize != 0);
                            
                            if (smallestCommandChoice == 0 || labelSetSize < smallestCommandSetSize) {
                                smallestCommandSetSize = labelSetSize;
                                smallestCommandChoice = choice;
                            }
                        }
                    }
                    
                    if (hasLabeledChoice) {
                        // Take all labels of the selected choice.
                        if (model.getChoiceOrigins()->isPrismChoiceOrigins()) {
                            auto const& labelSet = model.getChoiceOrigins()->asPrismChoiceOrigins().getCommandSet(smallestCommandChoice);
                            commandSet.insert(labelSet.begin(), labelSet.end());
                        } else {
                            assert(model.getChoiceOrigins()->isJaniChoiceOrigins());
                            auto const& labelSet = model.getChoiceOrigins()->asJaniChoiceOrigins().getEdgeIndexSet(smallestCommandChoice);
                            commandSet.insert(labelSet.begin(), labelSet.end());
                        }

                        // Check for which successor states choices need to be added
                        for (auto const& successorEntry : model.getTransitionMatrix().getRow(smallestCommandChoice)) {
                            if (!storm::utility::isZero(successorEntry.getValue())) {
                                if (!reachableProb0EStates.get(successorEntry.getColumn())) {
                                    reachableProb0EStates.set(successorEntry.getColumn());
                                    prob0EWorklist.push(successorEntry.getColumn());
                                }
                            }
                        }
                    }
                }
                
                auto endTime = std::chrono::high_resolution_clock::now();
                if (!silent) {
                    std::cout << std::endl << "Extended command for lower bounded property to size " << commandSet.size() << " in " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() << "ms." << std::endl;
                }

            }

            static std::vector<boost::container::flat_set<uint_fast64_t>> computeCounterexampleLabelSet(Environment const& env, GeneratorStats& stats, storm::storage::SymbolicModelDescription const& symbolicModel, storm::models::sparse::Model<T> const& model, std::shared_ptr<storm::logic::Formula const> const& formula, boost::container::flat_set<uint_fast64_t> const& dontCareLabels = boost::container::flat_set<uint_fast64_t>(),  Options const& options = Options(true)) {
                STORM_LOG_THROW(model.isOfType(storm::models::ModelType::Dtmc) || model.isOfType(storm::models::ModelType::Mdp), storm::exceptions::NotSupportedException, "MaxSAT-based counterexample generation is supported only for discrete-time models.");
                if (!options.silent) {
                    std::cout << std::endl << "Generating minimal label counterexample for formula " << *formula << std::endl;
                }

                storm::logic::ComparisonType comparisonType;
                double threshold;
                boost::optional<std::string> rewardName = boost::none;
                
                STORM_LOG_THROW(formula->isProbabilityOperatorFormula() || formula->isRewardOperatorFormula(), storm::exceptions::InvalidPropertyException, "Counterexample generation does not support this kind of formula. Expecting a probability operator as the outermost formula element.");
                if (formula->isProbabilityOperatorFormula()) {
                    storm::logic::ProbabilityOperatorFormula const& probabilityOperator = formula->asProbabilityOperatorFormula();
                    STORM_LOG_THROW(probabilityOperator.hasBound(), storm::exceptions::InvalidPropertyException, "Counterexample generation only supports bounded formulas.");
                    STORM_LOG_THROW(probabilityOperator.getSubformula().isUntilFormula() || probabilityOperator.getSubformula().isEventuallyFormula(), storm::exceptions::InvalidPropertyException, "Path formula is required to be of the form 'phi U psi' for counterexample generation.");

                    comparisonType = probabilityOperator.getComparisonType();
                    threshold = probabilityOperator.getThresholdAs<T>();
                } else {
                    assert(formula->isRewardOperatorFormula());
                    storm::logic::RewardOperatorFormula const& rewardOperator = formula->asRewardOperatorFormula();
                    STORM_LOG_THROW(rewardOperator.hasBound(), storm::exceptions::InvalidPropertyException, "Counterexample generation only supports bounded formulas.");
                    STORM_LOG_THROW( rewardOperator.getSubformula().isEventuallyFormula(), storm::exceptions::InvalidPropertyException, "Path formula is required to be of the form 'F phi' for counterexample generation.");
                    
                    comparisonType = rewardOperator.getComparisonType();
                    threshold = rewardOperator.getThresholdAs<T>();
                    rewardName = rewardOperator.getRewardModelName();

                    STORM_LOG_THROW(!storm::logic::isLowerBound(comparisonType), storm::exceptions::NotSupportedException, "Lower bounds in counterexamples are only supported for probability formulas.");
                    STORM_LOG_THROW(model.hasRewardModel(rewardName.get()), storm::exceptions::InvalidPropertyException, "Property refers to reward " << rewardName.get() << " but model does not contain such a reward model.");
                    STORM_LOG_THROW(model.getRewardModel(rewardName.get()).hasOnlyStateRewards(), storm::exceptions::NotSupportedException, "We only support state-based rewards at the moment.");
                }
                bool strictBound = comparisonType == storm::logic::ComparisonType::Less;
                storm::logic::Formula const& subformula = formula->asOperatorFormula().getSubformula();
                
                storm::storage::BitVector phiStates;
                storm::storage::BitVector psiStates;
                storm::modelchecker::SparsePropositionalModelChecker<storm::models::sparse::Model<T>> modelchecker(model);

                if (subformula.isUntilFormula()) {
                    STORM_LOG_THROW(!storm::logic::isLowerBound(comparisonType), storm::exceptions::NotSupportedException, "Lower bounds in counterexamples are only supported for eventually formulas.");
                    storm::logic::UntilFormula const& untilFormula = subformula.asUntilFormula();

                    std::unique_ptr<storm::modelchecker::CheckResult> leftResult = modelchecker.check(env, untilFormula.getLeftSubformula());
                    std::unique_ptr<storm::modelchecker::CheckResult> rightResult = modelchecker.check(env, untilFormula.getRightSubformula());

                    storm::modelchecker::ExplicitQualitativeCheckResult const& leftQualitativeResult = leftResult->asExplicitQualitativeCheckResult();
                    storm::modelchecker::ExplicitQualitativeCheckResult const& rightQualitativeResult = rightResult->asExplicitQualitativeCheckResult();

                    phiStates = leftQualitativeResult.getTruthValuesVector();
                    psiStates = rightQualitativeResult.getTruthValuesVector();
                } else if (subformula.isEventuallyFormula()) {
                    storm::logic::EventuallyFormula const& eventuallyFormula = subformula.asEventuallyFormula();

                    std::unique_ptr<storm::modelchecker::CheckResult> subResult = modelchecker.check(env, eventuallyFormula.getSubformula());

                    storm::modelchecker::ExplicitQualitativeCheckResult const& subQualitativeResult = subResult->asExplicitQualitativeCheckResult();

                    phiStates = storm::storage::BitVector(model.getNumberOfStates(), true);
                    psiStates = subQualitativeResult.getTruthValuesVector();
                }

                bool lowerBoundedFormula = false;
                if (storm::logic::isLowerBound(comparisonType)) {
                    // If the formula specifies a lower bound, we need to modify the phi and psi states.
                    // More concretely, we convert P(min)>lambda(F psi) to P(max)<(1-lambda)(G !psi) = P(max)<(1-lambda)(!psi U prob0E(psi))
                    // where prob0E(psi) is the set of states for which there exists a strategy \sigma_0 that avoids
                    // reaching psi states completely.

                    // This means that from all states in prob0E(psi) we need to include labels such that \sigma_0
                    // is actually included in the resulting model. This prevents us from guaranteeing the minimality of
                    // the returned counterexample, so we warn about that.
                    if (!options.silent) {
                        STORM_LOG_WARN("Generating counterexample for lower-bounded property. The resulting command set need not be minimal.");
                    }

                    // Modify bound appropriately.
                    comparisonType = storm::logic::invertPreserveStrictness(comparisonType);
                    threshold = storm::utility::one<T>() - threshold;

                    // Modify the phi and psi states appropriately.
                    storm::storage::BitVector statesWithProbability0E = storm::utility::graph::performProb0E(model.getTransitionMatrix(), model.getTransitionMatrix().getRowGroupIndices(), model.getBackwardTransitions(), phiStates, psiStates);
                    phiStates = ~psiStates;
                    psiStates = std::move(statesWithProbability0E);

                    // Remember our transformation so we can add commands to guarantee that the prob0E(a) states actually
                    // have a strategy that voids a states.
                    lowerBoundedFormula = true;
                }

                // Delegate the actual computation work to the function of equal name.
                auto startTime = std::chrono::high_resolution_clock::now();
                auto labelSets = getMinimalLabelSet(env, stats, symbolicModel, model, phiStates, psiStates, threshold, rewardName, strictBound, dontCareLabels, options);
                auto endTime = std::chrono::high_resolution_clock::now();
                if (!options.silent) {
                    for (auto const& labelSet : labelSets) {
                         std::cout << std::endl << "Computed minimal label set of size " << labelSet.size();

                    }
                    std::cout << std::endl << " in " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() << "ms." << std::endl;

                }

                // Extend the command set properly.
                for (auto& labelSet : labelSets) {
                    if (lowerBoundedFormula) {
                        extendLabelSetLowerBound(model, labelSet, phiStates, psiStates, options.silent);
                    }
                }
                return labelSets;
            }

            static std::shared_ptr<HighLevelCounterexample> computeCounterexample(Environment const& env, storm::storage::SymbolicModelDescription const& symbolicModel, storm::models::sparse::Model<T> const& model, std::shared_ptr<storm::logic::Formula const> const& formula) {
#ifdef STORM_HAVE_Z3
                GeneratorStats stats;
                auto labelSets = computeCounterexampleLabelSet(env, stats, symbolicModel, model, formula);


                if (symbolicModel.isPrismProgram()) {
                    return std::make_shared<HighLevelCounterexample>(symbolicModel.asPrismProgram().restrictCommands(labelSets[0]));
                } else {
                    STORM_LOG_ASSERT(symbolicModel.isJaniModel(), "Unknown symbolic model description type.");
                    return std::make_shared<HighLevelCounterexample>(symbolicModel.asJaniModel().restrictEdges(labelSets[0]));
                }
#else
                throw storm::exceptions::NotImplementedException() << "This functionality is unavailable since storm has been compiled without support for Z3.";
                return nullptr;
#endif
            }
            
        };
        
    } // namespace counterexamples
} // namespace storm