diff --git a/src/DynamicModel.cc b/src/DynamicModel.cc index 1b1e6c313ad3c557691a6d1695d34aac759f9faf..e85ab80056596a28dc7cca121431b313780f98a0 100644 --- a/src/DynamicModel.cc +++ b/src/DynamicModel.cc @@ -3606,6 +3606,8 @@ DynamicModel::addOccbinEquation(expr_t eq, const optional<int>& lineno, map<stri auto beq = dynamic_cast<BinaryOpNode*>(eq); assert(beq && beq->op_code == BinaryOpcode::equal); + occbin_regime_trackers[eq_tags.at("name")].addAlternative(regimes_bind, regimes_relax); + // Construct the term to be added to the corresponding equation expr_t basic_term = AddMinus(beq->arg1, beq->arg2); expr_t term = basic_term; @@ -4021,3 +4023,144 @@ DynamicModel::checkIsLinear() const exit(EXIT_FAILURE); } } + +void +DynamicModel::checkOccbinRegimes() const +{ + for (const auto& [eq_name, tracker] : occbin_regime_trackers) + { + try + { + tracker.checkAllAlternativesPresent(); + } + catch (OccbinRegimeTracker::MissingAlternativeException& e) + { + cerr << "ERROR: for equation '" << eq_name << "', the alternative corresponding to '"; + if (!e.regimes_bind.empty()) + { + cerr << "bind="; + for (bool first_printed {false}; const auto& r : e.regimes_bind) + { + if (exchange(first_printed, true)) + cerr << ","; + cerr << r; + } + } + if (!e.regimes_bind.empty() && !e.regimes_relax.empty()) + cerr << "'and '"; + if (!e.regimes_relax.empty()) + { + cerr << "relax="; + for (bool first_printed {false}; const auto& r : e.regimes_relax) + { + if (exchange(first_printed, true)) + cerr << ","; + cerr << r; + } + } + cerr << "' is not defined" << endl; + exit(EXIT_FAILURE); + } + } +} + +void +DynamicModel::OccbinRegimeTracker::addAlternative(const vector<string>& regimes_bind, + const vector<string>& regimes_relax) +{ + // Check that no regime appears in both bind and relax + set regimes_bind_sorted(regimes_bind.begin(), regimes_bind.end()), + regimes_relax_sorted(regimes_relax.begin(), regimes_relax.end()); + vector<string> regimes_intersect; + ranges::set_intersection(regimes_bind_sorted, regimes_relax_sorted, + back_inserter(regimes_intersect)); + if (!regimes_intersect.empty()) + throw RegimeInBothBindAndRelaxException {regimes_intersect.front()}; + + // If a regime has never been mentioned before, add it to the list and adapt the alternatives + vector<string> regimes_union; + ranges::set_union(regimes_bind_sorted, regimes_relax_sorted, back_inserter(regimes_union)); + for (const auto& r : regimes_union) + if (ranges::find(regimes, r) == regimes.end()) + { + regimes.push_back(r); + auto alt_copy = alternatives_present; + alternatives_present.clear(); + for (const auto& a : alt_copy) + { + auto a0 = a, a1 = a; + a0.push_back(false); + a1.push_back(true); + alternatives_present.insert(a0); + alternatives_present.insert(a1); + } + } + + // Create the bit vector(s) corresponding to the function arguments + vector<bool> new_alt_template(regimes.size(), false); + for (const auto& r : regimes_bind) + { + int i = distance(regimes.begin(), ranges::find(regimes, r)); + new_alt_template[i] = true; + } + set<vector<bool>> new_alts {new_alt_template}; + set all_regimes_sorted(regimes.begin(), regimes.end()); + vector<string> regimes_not_mentioned; + ranges::set_difference(all_regimes_sorted, regimes_union, back_inserter(regimes_not_mentioned)); + for (const auto& r : regimes_not_mentioned) + { + int i = distance(regimes.begin(), ranges::find(regimes, r)); + auto new_alts_copy = new_alts; + for (const auto& a : new_alts_copy) + { + auto a2 = a; + a2[i] = true; + new_alts.insert(move(a2)); + } + } + + // Add the new bit vector(s) + for (const auto& a : new_alts) + { + auto [it, success] = alternatives_present.insert(a); + if (!success) + { + auto [regimes_bind_duplicate, regimes_relax_duplicate] = convertBitVectorToRegimes(a); + throw AlternativeAlreadyPresentException {regimes_bind_duplicate, + regimes_relax_duplicate}; + } + } +} + +void +DynamicModel::OccbinRegimeTracker::checkAllAlternativesPresent() const +{ + vector<bool> a(regimes.size(), false); + do + { + if (!alternatives_present.contains(a)) + { + auto [regimes_bind, regimes_relax] = convertBitVectorToRegimes(a); + throw MissingAlternativeException {regimes_bind, regimes_relax}; + } + auto it = ranges::find(a, false); + if (it == a.end()) + break; + *it = true; + if (it != a.begin()) + fill(a.begin(), prev(it), false); + } + while (true); +} + +pair<vector<string>, vector<string>> +DynamicModel::OccbinRegimeTracker::convertBitVectorToRegimes(const vector<bool>& a) const +{ + vector<string> regimes_bind, regimes_relax; + for (size_t i = 0; i < regimes.size(); i++) + if (a[i]) + regimes_bind.push_back(regimes[i]); + else + regimes_relax.push_back(regimes[i]); + return {regimes_bind, regimes_relax}; +} diff --git a/src/DynamicModel.hh b/src/DynamicModel.hh index 7d77e34419740e70fc147f09d5fdcfbfbd726a0f..fc34f190074d6eb032876d92a476766fd671e33e 100644 --- a/src/DynamicModel.hh +++ b/src/DynamicModel.hh @@ -43,6 +43,39 @@ public: “model” block. The default should not be too small (see dynare#1389). */ double balanced_growth_test_tol {1e-6}; + /* For a given equation, tracks all the regimes and the declared alternatives with combinations of + bind and relax tags */ + class OccbinRegimeTracker + { + private: + // The list of regimes used for this equation + vector<string> regimes; + /* The list of alternatives present for this equation; each alternative is a vector of boolean, + of same length as “regimes”; each boolean represents a regime (in the order of “regimes”): + false for relax, true for bind */ + set<vector<bool>> alternatives_present; + + public: + struct RegimeInBothBindAndRelaxException + { + const string regime; + }; + struct AlternativeAlreadyPresentException + { + const vector<string> regimes_bind, regimes_relax; + }; + void addAlternative(const vector<string>& regimes_bind, + const vector<string>& regimes_relax) noexcept(false); + struct MissingAlternativeException + { + const vector<string> regimes_bind, regimes_relax; + }; + void checkAllAlternativesPresent() const noexcept(false); + + private: + pair<vector<string>, vector<string>> convertBitVectorToRegimes(const vector<bool>& a) const; + }; + private: /* Used in the balanced growth test, for skipping equations where the test cannot be performed (i.e. when LHS=RHS at the initial values). Should not @@ -276,6 +309,9 @@ private: return blocks_jacob_cols_endo[blk].at({var, lag}); } + // Used to check consistency of bind/relax tags; the keys are equation names + map<string, OccbinRegimeTracker> occbin_regime_trackers; + protected: string modelClassName() const override @@ -695,6 +731,9 @@ public: { static_mfs = static_mfs_arg; } + + // Checks that all alternatives are declared for all Occbin regimes in all equations + void checkOccbinRegimes() const; }; template<bool julia> diff --git a/src/ModFile.cc b/src/ModFile.cc index 3201be71aac621bfb12d1da5498da521f8f7aac3..6ab8b2671de260afcf81958dafbf12254ff5904a 100644 --- a/src/ModFile.cc +++ b/src/ModFile.cc @@ -279,6 +279,10 @@ ModFile::checkPass(bool nostrict, bool stochastic) } } + /* This check must come before checking that there are as many static-only as dynamic-only + equations, see #103 */ + dynamic_model.checkOccbinRegimes(); + if (dynamic_model.staticOnlyEquationsNbr() != dynamic_model.dynamicOnlyEquationsNbr()) { cerr << "ERROR: the number of equations marked [static] must be equal to the number of " diff --git a/src/ParsingDriver.cc b/src/ParsingDriver.cc index 94e275454a019658b334cf86e75f0dde620c0ad5..79b68d66df9ba030d151fc79d8c7c07e05f7f30c 100644 --- a/src/ParsingDriver.cc +++ b/src/ParsingDriver.cc @@ -2676,8 +2676,44 @@ ParsingDriver::add_model_equal(expr_t arg1, expr_t arg2, map<string, string> eq_ } eq_tags.erase("bind"); eq_tags.erase("relax"); - dynamic_model->addOccbinEquation(id, location.begin.line, move(eq_tags), regimes_bind, - regimes_relax); + + try + { + dynamic_model->addOccbinEquation(id, location.begin.line, move(eq_tags), regimes_bind, + regimes_relax); + } + catch (DynamicModel::OccbinRegimeTracker::RegimeInBothBindAndRelaxException& e) + { + error("The regime '" + e.regime + "' is both in the 'bind' and 'relax' tags"); + } + catch (DynamicModel::OccbinRegimeTracker::AlternativeAlreadyPresentException& e) + { + stringstream s; + if (!e.regimes_bind.empty()) + { + cerr << "bind="; + for (bool first_printed {false}; const auto& r : e.regimes_bind) + { + if (exchange(first_printed, true)) + cerr << ","; + cerr << r; + } + } + if (!e.regimes_bind.empty() && !e.regimes_relax.empty()) + cerr << "'and '"; + if (!e.regimes_relax.empty()) + { + cerr << "relax="; + for (bool first_printed {false}; const auto& r : e.regimes_relax) + { + if (exchange(first_printed, true)) + cerr << ","; + cerr << r; + } + } + error("The alternative corresponding to '" + s.str() + + "' has already been declared for this equation"); + } } else // General case model_tree->addEquation(id, location.begin.line, move(eq_tags));