From dfd316b945234b0b81e7a6d74d4a8df4cd682ef2 Mon Sep 17 00:00:00 2001
From: Normann Rion <normann@dynare.org>
Date: Fri, 20 Jun 2025 15:25:49 +0200
Subject: [PATCH] Unfold complementarity conditions in heterogeneous models
 using Lagrange multipliers

---
 src/HeterogeneousModel.cc | 66 +++++++++++++++++++++++++++++++++++++++
 src/HeterogeneousModel.hh |  2 ++
 src/ModFile.cc            |  4 +++
 src/SymbolTable.cc        | 38 ++++++++++++++++++++++
 src/SymbolTable.hh        | 35 ++++++++++++++++++++-
 5 files changed, 144 insertions(+), 1 deletion(-)

diff --git a/src/HeterogeneousModel.cc b/src/HeterogeneousModel.cc
index d3cc1967..8a967e36 100644
--- a/src/HeterogeneousModel.cc
+++ b/src/HeterogeneousModel.cc
@@ -97,6 +97,72 @@ HeterogeneousModel::computeDerivIDs()
     }
 }
 
+/*
+ * Unfold complementarity conditions: (i) declare the multipliers associated
+ * with each bound constraint μ_l and μ_u ; (ii) add or substract the
+ * multiplier into the associated condition; (iii) add the the complementarity
+ * slackness conditions into the set of equations. For example,
+ * households choose {cₜ, aₜ₊₁} to maximize expected lifetime utility:
+ *     max 𝐸ₜ [∑ₛ₌₀^∞ βˢ · u(cₜ₊ₛ)]
+ *
+ * Subject to:
+ *   1. Budget constraint:      cₜ + aₜ₊₁ = yₜ + (1 + rₜ) · aₜ
+ *   2. Borrowing constraint:   aₜ₊₁ ≥ aₘᵢₙ
+ *
+ * Let u'(cₜ) denote the marginal utility of consumption.
+ * Let μₜ ≥ 0 be the Lagrange multiplier on the borrowing constraint.
+ *
+ * Then, the Euler equation becomes:
+ *     u′(cₜ) = β · (1 + rₜ₊₁) · u′(cₜ₊₁) − μₜ
+ *
+ * Together with:
+ *     aₜ₊₁ ≥ aₘᵢₙ                 [primal feasibility]
+ *     μₜ ≥ 0                      [dual feasibility]
+ *     μₜ · (aₜ₊₁ − aₘᵢₙ) = 0      [complementarity slackness]
+ * Note that the primal feasibility and dual feasibility constraints are not
+ * introduced here, but Bhandari et al. (2023) show in Appendix B.1 that they
+ * are redundant.
+ */
+void
+HeterogeneousModel::transformPass()
+{
+  for (int i = 0; i < static_cast<int>(equations.size()); ++i)
+    {
+      if (!complementarity_conditions[i])
+        continue;
+
+      /*
+       * `const auto& [symb_id, lb, ub] = *complementarity_conditions[i];` was not used here because
+       * the call to `addEquation` may eventually lead to a resize of the
+       * `complementarity_conditions` vector, which may invalidate the reference to its element. We
+       * take a copy instead for safety.
+       */
+      auto [symb_id, lb, ub] = *complementarity_conditions[i];
+
+      VariableNode* var = getVariable(symb_id);
+      if (lb)
+        {
+          int mu_id = symbol_table.addHeterogeneousMultiplierAuxiliaryVar(
+              heterogeneity_dimension, i, "MULT_L_" + symbol_table.getName(symb_id));
+          expr_t mu_L = AddVariable(mu_id);
+          auto substeq = AddEqual(AddPlus(equations[i]->arg1, mu_L), equations[i]->arg2);
+          assert(substeq);
+          equations[i] = substeq;
+          addEquation(AddEqual(AddTimes(mu_L, AddMinus(var, lb)), Zero), nullopt);
+        }
+      if (ub)
+        {
+          int mu_id = symbol_table.addHeterogeneousMultiplierAuxiliaryVar(
+              heterogeneity_dimension, i, "MULT_U_" + symbol_table.getName(symb_id));
+          auto mu_U = AddVariable(mu_id);
+          auto substeq = AddEqual(AddMinus(equations[i]->arg1, mu_U), equations[i]->arg2);
+          assert(substeq);
+          equations[i] = substeq;
+          addEquation(AddEqual(AddTimes(mu_U, AddMinus(ub, var)), Zero), nullopt);
+        }
+    }
+}
+
 void
 HeterogeneousModel::computingPass(int derivsOrder, bool no_tmp_terms, bool use_dll)
 {
diff --git a/src/HeterogeneousModel.hh b/src/HeterogeneousModel.hh
index ed8986ec..15169de7 100644
--- a/src/HeterogeneousModel.hh
+++ b/src/HeterogeneousModel.hh
@@ -38,6 +38,8 @@ public:
   HeterogeneousModel(const HeterogeneousModel& m) = default;
   HeterogeneousModel& operator=(const HeterogeneousModel& m);
 
+  void transformPass();
+
   void computingPass(int derivsOrder, bool no_tmp_terms, bool use_dll);
 
   void writeModelFiles(const string& basename, bool julia) const;
diff --git a/src/ModFile.cc b/src/ModFile.cc
index eb2de0b8..fe88bd1a 100644
--- a/src/ModFile.cc
+++ b/src/ModFile.cc
@@ -731,6 +731,10 @@ ModFile::transformPass(bool nostrict, bool stochastic, bool compute_xrefs, bool
 
   dynamic_model.reorderAuxiliaryEquations();
 
+  symbol_table.resizeHetAuxVars();
+  for (auto& hm : heterogeneous_models)
+    hm.transformPass();
+
   // Freeze the symbol table
   symbol_table.freeze();
 
diff --git a/src/SymbolTable.cc b/src/SymbolTable.cc
index 304b4faf..191d1c3c 100644
--- a/src/SymbolTable.cc
+++ b/src/SymbolTable.cc
@@ -398,6 +398,7 @@ SymbolTable::writeOutput(ostream& output) const noexcept(false)
           case AuxVarType::pacExpectation:
           case AuxVarType::pacTargetNonstationary:
           case AuxVarType::aggregationOp:
+          case AuxVarType::heterogeneousMultiplier:
             break;
           case AuxVarType::endoLag:
           case AuxVarType::exoLag:
@@ -496,11 +497,26 @@ SymbolTable::writeOutput(ostream& output) const noexcept(false)
       output << basefield << "endo_nbr = " << het_endo_nbr(het_dim) << ";" << endl;
       print_symb_names(basefield + "endo_names", het_endo_ids.at(het_dim));
 
+      output << basefield << "orig_endo_nbr = " << het_orig_endo_nbr(het_dim) << ";" << endl;
+
       output << basefield << "exo_nbr = " << het_exo_nbr(het_dim) << ";" << endl;
       print_symb_names(basefield + "exo_names", het_exo_ids.at(het_dim));
 
       output << basefield << "param_nbr = " << het_param_nbr(het_dim) << ";" << endl;
       print_symb_names(basefield + "param_names", het_param_ids.at(het_dim));
+
+      const vector<AuxVarInfo>& hav = het_aux_vars[het_dim];
+      if (hav.size() == 0)
+        output << basefield << "aux_vars = [];";
+      else
+        for (int i = 0; i < static_cast<int>(hav.size()); i++)
+          {
+            output << basefield << "aux_vars(" << i + 1
+                   << ").endo_index = " << getTypeSpecificID(hav[i].symb_id) + 1 << ";" << endl
+                   << basefield << "aux_vars(" << i + 1 << ").type = " << hav[i].get_type_id()
+                   << ";" << basefield << "aux_vars(" << i + 1
+                   << ").eq_nbr = " << hav[i].equation_number_for_multiplier + 1 << ";" << endl;
+          }
     }
 }
 
@@ -713,6 +729,27 @@ SymbolTable::addUnaryOpAuxiliaryVar(int index, expr_t expr_arg, string unary_op,
   return symb_id;
 }
 
+int
+SymbolTable::addHeterogeneousMultiplierAuxiliaryVar(int het_dim, int index,
+                                                    const string& varname) noexcept(false)
+{
+  int symb_id;
+  try
+    {
+      symb_id = addSymbol(varname, SymbolType::heterogeneousEndogenous, "", {}, het_dim);
+    }
+  catch (AlreadyDeclaredException& e)
+    {
+      cerr << "ERROR: you should rename your variable called " << varname
+           << ", this name is internally used by Dynare" << endl;
+      exit(EXIT_FAILURE);
+    }
+
+  het_aux_vars[het_dim].emplace_back(symb_id, AuxVarType::heterogeneousMultiplier, 0, 0, index, 0,
+                                     nullptr, "");
+  return symb_id;
+}
+
 int
 SymbolTable::addMultiplierAuxiliaryVar(int index) noexcept(false)
 {
@@ -1100,6 +1137,7 @@ SymbolTable::writeJsonOutput(ostream& output) const
             case AuxVarType::pacExpectation:
             case AuxVarType::pacTargetNonstationary:
             case AuxVarType::aggregationOp:
+            case AuxVarType::heterogeneousMultiplier:
               break;
             case AuxVarType::endoLag:
             case AuxVarType::exoLag:
diff --git a/src/SymbolTable.hh b/src/SymbolTable.hh
index 14eb6ca5..e9b9d98a 100644
--- a/src/SymbolTable.hh
+++ b/src/SymbolTable.hh
@@ -56,7 +56,9 @@ enum class AuxVarType
   pacTargetNonstationary
   = 13, //!< Variable created for the substitution of the pac_target_nonstationary operator
   aggregationOp
-  = 14 // Substitute for an aggregation operator in a heterogeneous setup, such as SUM()
+  = 14, // Substitute for an aggregation operator in a heterogeneous setup, such as SUM()
+  heterogeneousMultiplier = 15 /* Multiplier for bound conditions of complementarity conditions in
+                                  the heterogeneous equations */
 };
 
 //! Information on some auxiliary variables
@@ -155,6 +157,9 @@ private:
   //! Information about auxiliary variables
   vector<AuxVarInfo> aux_vars;
 
+  //! Information about heterogeneous auxiliary variables
+  vector<vector<AuxVarInfo>> het_aux_vars;
+
   //! Stores the predetermined variables (by symbol IDs)
   set<int> predetermined_variables;
 
@@ -282,6 +287,14 @@ public:
     \return the symbol ID of the new symbol
   */
   int addExpectationAuxiliaryVar(int information_set, int index, expr_t arg) noexcept(false);
+  /* Adds an auxiliary variable for the bound-specific multiplier of the MCPs and returns its
+     symbolID.
+     – het_dim is the heterogeneity dimension of the model
+     – index is the equation's index
+     – varname is the multiplier name
+  */
+  int addHeterogeneousMultiplierAuxiliaryVar(int het_dim, int index,
+                                             const string& varname) noexcept(false);
   //! Adds an auxiliary variable for the multiplier for the FOCs of the Ramsey Problem
   /*!
     \param[in] index Used to construct the variable name
@@ -347,6 +360,8 @@ public:
   int addPacTargetNonstationaryAuxiliaryVar(const string& name, expr_t expr_arg);
   // An auxiliary variable for an aggregation operator (e.g. SUM(yh) where yh is heterogeneous)
   int addAggregationOpAuxiliaryVar(const string& name, expr_t expr_arg);
+  //! Set the size of het_aux_vars
+  inline void resizeHetAuxVars();
   //! Returns the number of auxiliary variables
   [[nodiscard]] int
   AuxVarsSize() const
@@ -398,6 +413,9 @@ public:
   [[nodiscard]] inline int param_nbr() const noexcept(false);
   //! Get number of heterogeneous endogenous variables along a given dimension
   [[nodiscard]] inline int het_endo_nbr(int het_dim) const noexcept(false);
+  //! Get number of user-declared heterogeneous endogenous variables (without
+  //! the auxiliary variables)
+  [[nodiscard]] inline int het_orig_endo_nbr(int het_dim) const noexcept(false);
   //! Get number of heterogeneous exogenous variables along a given dimension
   [[nodiscard]] inline int het_exo_nbr(int het_dim) const noexcept(false);
   //! Get number of heterogeneous parameters along a given dimension
@@ -477,6 +495,12 @@ SymbolTable::validateSymbID(int symb_id) const noexcept(false)
     throw UnknownSymbolIDException {symb_id};
 }
 
+inline void
+SymbolTable::resizeHetAuxVars()
+{
+  het_aux_vars.resize(heterogeneity_table.size());
+}
+
 inline bool
 SymbolTable::exists(const string& name) const
 {
@@ -591,6 +615,15 @@ SymbolTable::het_endo_nbr(int het_dim) const noexcept(false)
   return het_endo_ids.at(het_dim).size();
 }
 
+inline int
+SymbolTable::het_orig_endo_nbr(int het_dim) const noexcept(false)
+{
+  if (!frozen)
+    throw NotYetFrozenException();
+
+  return het_endo_ids.at(het_dim).size() - het_aux_vars.at(het_dim).size();
+}
+
 inline int
 SymbolTable::het_exo_nbr(int het_dim) const noexcept(false)
 {
-- 
GitLab