diff --git a/src/ComputingTasks.cc b/src/ComputingTasks.cc
index c1cd8d9e7cefad8bbfa80a3a62e2c2d9e2dcab40..a106ae19f3135b277452c5004eeacb643f709772 100644
--- a/src/ComputingTasks.cc
+++ b/src/ComputingTasks.cc
@@ -2453,7 +2453,7 @@ ModelComparisonStatement::writeJsonOutput(ostream &output) const
   output << "}";
 }
 
-PlannerObjectiveStatement::PlannerObjectiveStatement(const StaticModel &model_tree_arg) :
+PlannerObjectiveStatement::PlannerObjectiveStatement(const PlannerObjective &model_tree_arg) :
   model_tree{model_tree_arg}
 {
 }
@@ -2473,7 +2473,7 @@ PlannerObjectiveStatement::checkPass(ModFileStructure &mod_file_struct,
   mod_file_struct.planner_objective_present = true;
 }
 
-const StaticModel &
+const PlannerObjective &
 PlannerObjectiveStatement::getPlannerObjective() const
 {
   return model_tree;
diff --git a/src/ComputingTasks.hh b/src/ComputingTasks.hh
index 9888fbc114f0e62d1dcbb52af4063e48b5c3e41e..1cb72657dabe589a52489f65509b0e2a74b07d18 100644
--- a/src/ComputingTasks.hh
+++ b/src/ComputingTasks.hh
@@ -580,10 +580,10 @@ public:
 class PlannerObjectiveStatement : public Statement
 {
 private:
-  StaticModel model_tree;
+  PlannerObjective model_tree;
   bool computing_pass_called{false};
 public:
-  explicit PlannerObjectiveStatement(const StaticModel &model_tree_arg);
+  explicit PlannerObjectiveStatement(const PlannerObjective &model_tree_arg);
   /*! \todo check there are only endogenous variables at the current period in the objective
     (no exogenous, no lead/lag) */
   void checkPass(ModFileStructure &mod_file_struct, WarningConsolidation &warnings) override;
@@ -592,7 +592,7 @@ public:
   void writeOutput(ostream &output, const string &basename, bool minimal_workspace) const override;
   void writeJsonOutput(ostream &output) const override;
   //! Return a reference the Planner Objective model tree
-  const StaticModel &getPlannerObjective() const;
+  const PlannerObjective &getPlannerObjective() const;
 };
 
 class BVARDensityStatement : public Statement
diff --git a/src/DynamicModel.cc b/src/DynamicModel.cc
index 2d6a75358afa9bfc6fee031caef41601e4a4537f..85037d4dcce09c630db097418a98f47e8582b8f5 100644
--- a/src/DynamicModel.cc
+++ b/src/DynamicModel.cc
@@ -575,13 +575,17 @@ DynamicModel::writeDynamicBytecode(const string &basename) const
     };
   int jacobian_ncols_exo {symbol_table.exo_nbr()};
   int jacobian_ncols_exo_det {symbol_table.exo_det_nbr()};
+  vector<int> eq_idx(equations.size());
+  iota(eq_idx.begin(), eq_idx.end(), 0);
+  vector<int> endo_idx(symbol_table.endo_nbr());
+  iota(endo_idx.begin(), endo_idx.end(), 0);
 
   code_file << FBEGINBLOCK_{symbol_table.endo_nbr(),
                             simulation_type,
                             0,
                             symbol_table.endo_nbr(),
-                            endo_idx_block2orig,
-                            eq_idx_block2orig,
+                            endo_idx,
+                            eq_idx,
                             false,
                             symbol_table.endo_nbr(),
                             max_endo_lag,
@@ -3285,47 +3289,41 @@ DynamicModel::computingPass(bool jacobianExo, int derivsOrder, int paramsDerivsO
       computeParamsDerivatives(paramsDerivsOrder);
     }
 
+  computeTemporaryTerms(!use_dll, no_tmp_terms);
 
-  if (block)
-    {
-      auto contemporaneous_jacobian = evaluateAndReduceJacobian(eval_context);
-
-      computeNonSingularNormalization(contemporaneous_jacobian, true);
-
-      auto [prologue, epilogue] = computePrologueAndEpilogue();
-
-      auto first_order_endo_derivatives = collectFirstOrderDerivativesEndogenous();
-
-      equationTypeDetermination(first_order_endo_derivatives, mfs);
-
-      cout << "Finding the optimal block decomposition of the model ..." << endl;
-
-      computeBlockDecomposition(prologue, epilogue);
-
-      reduceBlockDecomposition();
-
-      printBlockDecomposition();
-
-      computeChainRuleJacobian();
+  /* Must be called after computeTemporaryTerms(), because it depends on
+     temporary_terms_mlv to be filled */
+  if (paramsDerivsOrder > 0 && !no_tmp_terms)
+    computeParamsDerivativesTemporaryTerms();
 
-      determineLinearBlocks();
-
-      computeBlockDynJacobianCols();
-
-      if (!no_tmp_terms)
-        computeBlockTemporaryTerms();
-    }
-  else
+  if (!computingPassBlock(eval_context, no_tmp_terms) && block)
     {
-      computeTemporaryTerms(!use_dll, no_tmp_terms);
-
-      /* Must be called after computeTemporaryTerms(), because it depends on
-         temporary_terms_mlv to be filled */
-      if (paramsDerivsOrder > 0 && !no_tmp_terms)
-        computeParamsDerivativesTemporaryTerms();
+      cerr << "ERROR: Block decomposition requested but failed." << endl;
+      exit(EXIT_FAILURE);
     }
 }
 
+bool
+DynamicModel::computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms)
+{
+  auto contemporaneous_jacobian = evaluateAndReduceJacobian(eval_context);
+  if (!computeNonSingularNormalization(contemporaneous_jacobian, true))
+    return false;
+  auto [prologue, epilogue] = computePrologueAndEpilogue();
+  auto first_order_endo_derivatives = collectFirstOrderDerivativesEndogenous();
+  equationTypeDetermination(first_order_endo_derivatives, mfs);
+  cout << "Finding the optimal block decomposition of the dynamic model..." << endl;
+  computeBlockDecomposition(prologue, epilogue);
+  reduceBlockDecomposition();
+  printBlockDecomposition();
+  computeChainRuleJacobian();
+  determineLinearBlocks();
+  computeBlockDynJacobianCols();
+  if (!no_tmp_terms)
+    computeBlockTemporaryTerms();
+  return true;
+}
+
 void
 DynamicModel::computeXrefs()
 {
diff --git a/src/DynamicModel.hh b/src/DynamicModel.hh
index 8232d22a5a69253f51c05ab77a19a2cf00c50f02..37051525e6b6b90bcd2854b076476a8b7070ddc8 100644
--- a/src/DynamicModel.hh
+++ b/src/DynamicModel.hh
@@ -297,6 +297,12 @@ private:
     return blocks_jacob_cols_endo[blk].at({ var, lag });
   }
 
+  /* Compute block decomposition, its derivatives and temporary terms.
+     Meant to be overriden in derived classes which don’t support block
+     decomposition (currently Epilogue). Returns a boolean indicating success
+     (failure can happen in normalization). */
+  virtual bool computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms);
+
 public:
   DynamicModel(SymbolTable &symbol_table_arg,
                NumericalConstants &num_constants_arg,
@@ -313,7 +319,7 @@ public:
   //! Write cross references
   void writeXrefs(ostream &output) const;
 
-  //! Execute computations (variable sorting + derivation)
+  //! Execute computations (variable sorting + derivation + block decomposition)
   /*!
     \param jacobianExo whether derivatives w.r. to exo and exo_det should be in the Jacobian (derivatives w.r. to endo are always computed)
     \param derivsOrder order of derivatives w.r. to exo, exo_det and endo should be computed (implies jacobianExo = true when order >= 2)
diff --git a/src/ModFile.cc b/src/ModFile.cc
index a0ddb4d8d75321bafa42d4a3743a2180e57c571d..bb0b8ca8c82de104a167dd93f762c0982fea3dee 100644
--- a/src/ModFile.cc
+++ b/src/ModFile.cc
@@ -496,7 +496,7 @@ ModFile::transformPass(bool nostrict, bool stochastic, bool compute_xrefs, bool
           else
             pos = pos2;
       assert(pos);
-      const StaticModel &planner_objective = pos->getPlannerObjective();
+      const PlannerObjective &planner_objective = pos->getPlannerObjective();
 
       /*
         clone the model then clone the new equations back to the original because
diff --git a/src/ModelEquationBlock.cc b/src/ModelEquationBlock.cc
index 9b0551f46b7677308ca8f0789f6fbc3da28230c7..fe0d649146333b3842a4c4d7ef06b82ada493b87 100644
--- a/src/ModelEquationBlock.cc
+++ b/src/ModelEquationBlock.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2010-2021 Dynare Team
+ * Copyright © 2010-2022 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -23,6 +23,21 @@
 
 #include "ModelEquationBlock.hh"
 
+PlannerObjective::PlannerObjective(SymbolTable &symbol_table_arg,
+                                   NumericalConstants &num_constants_arg,
+                                   ExternalFunctionsTable &external_functions_table_arg) :
+  StaticModel {symbol_table_arg, num_constants_arg, external_functions_table_arg}
+{
+}
+
+bool
+PlannerObjective::computingPassBlock([[maybe_unused]] const eval_context_t &eval_context,
+                                     [[maybe_unused]] bool no_tmp_terms)
+{
+  // Disable block decomposition on planner objective
+  return false;
+}
+
 SteadyStateModel::SteadyStateModel(SymbolTable &symbol_table_arg,
                                    NumericalConstants &num_constants_arg,
                                    ExternalFunctionsTable &external_functions_table_arg,
@@ -488,3 +503,11 @@ Epilogue::writeOutput(ostream &output) const
     symbol_list.push_back(symbol_table.getName(symb_id));
   SymbolList{move(symbol_list)}.writeOutput("M_.epilogue_var_list_", output);
 }
+
+bool
+Epilogue::computingPassBlock([[maybe_unused]] const eval_context_t &eval_context,
+                             [[maybe_unused]] bool no_tmp_terms)
+{
+  // Disable block decomposition on epilogue blocks
+  return false;
+}
diff --git a/src/ModelEquationBlock.hh b/src/ModelEquationBlock.hh
index d2382cdb715ae985007042c7146149019eb8b47c..eb255e03ef61e6e53152fcb2fb9babb5e4cea6db 100644
--- a/src/ModelEquationBlock.hh
+++ b/src/ModelEquationBlock.hh
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2010-2019 Dynare Team
+ * Copyright © 2010-2022 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -26,6 +26,16 @@
 #include "DynamicModel.hh"
 #include "WarningConsolidation.hh"
 
+class PlannerObjective : public StaticModel
+{
+public:
+  PlannerObjective(SymbolTable &symbol_table_arg,
+                   NumericalConstants &num_constants_arg,
+                   ExternalFunctionsTable &external_functions_table_arg);
+private:
+  bool computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms) override;
+};
+
 class SteadyStateModel : public DataTree
 {
 private:
@@ -98,6 +108,7 @@ private:
   //! Helper for public writeEpilogueFile
   void writeStaticEpilogueFile(const string &basename) const;
   void writeDynamicEpilogueFile(const string &basename) const;
+  bool computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms) override;
 };
 
 #endif
diff --git a/src/ModelTree.cc b/src/ModelTree.cc
index d69b605ba525996dc88b9dbb10968908a4c127ef..8da7b1ad8a2ebd67ca8e79e9b3dc299aadc2acf8 100644
--- a/src/ModelTree.cc
+++ b/src/ModelTree.cc
@@ -242,7 +242,7 @@ ModelTree::computeNormalization(const jacob_map_t &contemporaneous_jacobian, boo
       it != mate_map.begin() + n)
     {
       if (verbose)
-        cerr << "ERROR: Could not normalize the " << (dynamic ? "dynamic" : "static") << " model. Variable "
+        cerr << "Could not normalize the " << (dynamic ? "dynamic" : "static") << " model. Variable "
              << symbol_table.getName(symbol_table.getID(SymbolType::endogenous, it - mate_map.begin()))
              << " is not in the maximum cardinality matching." << endl;
       check = false;
@@ -250,13 +250,21 @@ ModelTree::computeNormalization(const jacob_map_t &contemporaneous_jacobian, boo
   return check;
 }
 
-void
+bool
 ModelTree::computeNonSingularNormalization(const jacob_map_t &contemporaneous_jacobian, bool dynamic)
 {
-  cout << "Normalizing the " << (dynamic ? "dynamic" : "static") << " model..." << endl;
-
   int n = equations.size();
 
+  /* Optimal policy models (discretionary, or Ramsey before computing FOCs) do
+     not have as many equations as variables. */
+  if (n != symbol_table.endo_nbr())
+    {
+      cout << "The " << (dynamic ? "dynamic" : "static") << " model cannot be normalized, since it does not have as many equations as variables." << endl;
+      return false;
+    }
+
+  cout << "Normalizing the " << (dynamic ? "dynamic" : "static") << " model..." << endl;
+
   // Compute the maximum value of each row of the contemporaneous Jacobian matrix
   vector max_val(n, 0.0);
   for (const auto &[eq_and_endo, val] : contemporaneous_jacobian)
@@ -306,18 +314,9 @@ ModelTree::computeNonSingularNormalization(const jacob_map_t &contemporaneous_ja
       found_normalization = computeNormalization(symbolic_jacobian, dynamic, true);
     }
 
-  if (!found_normalization)
-    {
-      /* Some models don’t have a steady state, and this can cause the
-         normalization to fail (e.g. if some variable only appears in a diff(),
-         it will disappear from the static model). Suggest the “no_static”
-         option as a possible solution. */
-      if (!dynamic)
-        cerr << "If your model does not have a steady state, you may want to try the 'no_static' option of the 'model' block." << endl;
-      /* The last call to computeNormalization(), which was verbose, already
-         printed an error message, so we can immediately exit. */
-      exit(EXIT_FAILURE);
-    }
+  /* NB: If normalization failed, an explanatory message has been printed by the last call
+     to computeNormalization(), which has verbose=true */
+  return found_normalization;
 }
 
 ModelTree::jacob_map_t
@@ -332,25 +331,20 @@ ModelTree::evaluateAndReduceJacobian(const eval_context_t &eval_context) const
           int eq = indices[0];
           int var { getTypeSpecificIDByDerivID(deriv_id) };
           int lag = getLagByDerivID(deriv_id);
-          double val = 0;
-          try
-            {
-              val = d1->eval(eval_context);
-            }
-          catch (ExprNode::EvalExternalFunctionException &e)
-            {
-              val = 1;
-            }
-          catch (ExprNode::EvalException &e)
-            {
-              cerr << "ERROR: evaluation of Jacobian failed for equation " << eq+1;
-              if (equations_lineno[eq])
-                cerr << " (line " << *equations_lineno[eq] << ")";
-              cerr << " and variable " << getNameByDerivID(deriv_id) << "(" << lag << ") !" << endl;
-              d1->writeOutput(cerr, ExprNodeOutputType::matlabDynamicModel, {}, {});
-              cerr << endl;
-              exit(EXIT_FAILURE);
-            }
+          double val { [&]
+          {
+            try
+              {
+                return d1->eval(eval_context);
+              }
+            catch (ExprNode::EvalExternalFunctionException &e)
+              {
+                return 1.0;
+              }
+            /* Other types of EvalException should not happen (all symbols should
+               have a value; we don’t evaluate an equal sign) */
+          }() };
+
           if ((isnan(val) || fabs(val) >= cutoff) && lag == 0)
             contemporaneous_jacobian[{ eq, var }] = val;
         }
diff --git a/src/ModelTree.hh b/src/ModelTree.hh
index a03e96597f35b7d0dbdb7ad8b789d5591d423a6f..ecb49614c135e0dcb464a81039c0e50759b443b7 100644
--- a/src/ModelTree.hh
+++ b/src/ModelTree.hh
@@ -352,8 +352,9 @@ protected:
     If no matching is found using a strictly positive cutoff, then a zero cutoff is applied (i.e. use a symbolic normalization); in that case, the method adds zeros in the jacobian matrices to reflect all the edges in the symbolic incidence matrix.
     If no matching is found with a zero cutoff, an error message is printed.
     The resulting normalization is stored in endo2eq.
+    Returns a boolean indicating success.
   */
-  void computeNonSingularNormalization(const jacob_map_t &contemporaneous_jacobian, bool dynamic);
+  bool computeNonSingularNormalization(const jacob_map_t &contemporaneous_jacobian, bool dynamic);
   //! Evaluate the jacobian (w.r.t. endogenous) and suppress all the elements below the cutoff
   /*! Returns the contemporaneous_jacobian.
       Elements below the cutoff are discarded. External functions are evaluated to 1. */
diff --git a/src/ParsingDriver.cc b/src/ParsingDriver.cc
index 9d6e27da116a48c23772c1126d410cc007f6786a..39110bfb6f9cc1f51576e8d03866318c2145526e 100644
--- a/src/ParsingDriver.cc
+++ b/src/ParsingDriver.cc
@@ -2154,9 +2154,9 @@ ParsingDriver::run_model_comparison()
 void
 ParsingDriver::begin_planner_objective()
 {
-  planner_objective = make_unique<StaticModel>(mod_file->symbol_table,
-                                               mod_file->num_constants,
-                                               mod_file->external_functions_table);
+  planner_objective = make_unique<PlannerObjective>(mod_file->symbol_table,
+                                                    mod_file->num_constants,
+                                                    mod_file->external_functions_table);
   set_current_data_tree(planner_objective.get());
 }
 
diff --git a/src/ParsingDriver.hh b/src/ParsingDriver.hh
index 6cafa8daf321bc46f81524b00e570fb606ee8238..b91aeabf5c58d8f77264ba428b0fec022f8d1bd3 100644
--- a/src/ParsingDriver.hh
+++ b/src/ParsingDriver.hh
@@ -111,7 +111,7 @@ private:
   int declare_symbol(const string &name, SymbolType type, const string &tex_name, const vector<pair<string, string>> &partition_value);
 
   //! Temporary store for the planner objective
-  unique_ptr<StaticModel> planner_objective;
+  unique_ptr<PlannerObjective> planner_objective;
 
   //! Temporary store for the occbin_constraints statement
   unique_ptr<DataTree> occbin_constraints_tree;
diff --git a/src/StaticModel.cc b/src/StaticModel.cc
index 45d8723cb0eaf73a5ecbd2f6c59b3c78443a16a6..8636bd76407a9f908c53d98ddd44c6c84b205a85 100644
--- a/src/StaticModel.cc
+++ b/src/StaticModel.cc
@@ -23,6 +23,7 @@
 #include <cassert>
 #include <algorithm>
 #include <sstream>
+#include <numeric>
 
 #include "StaticModel.hh"
 #include "DynamicModel.hh"
@@ -245,6 +246,10 @@ StaticModel::writeStaticBytecode(const string &basename) const
   int u_count_int { writeBytecodeBinFile(basename + "/model/bytecode/static.bin", false) };
 
   BytecodeWriter code_file {basename + "/model/bytecode/static.cod"};
+  vector<int> eq_idx(equations.size());
+  iota(eq_idx.begin(), eq_idx.end(), 0);
+  vector<int> endo_idx(symbol_table.endo_nbr());
+  iota(endo_idx.begin(), endo_idx.end(), 0);
 
   // Declare temporary terms and the (single) block
   code_file << FDIMST_{static_cast<int>(temporary_terms_derivatives[0].size()
@@ -253,8 +258,8 @@ StaticModel::writeStaticBytecode(const string &basename) const
                             BlockSimulationType::solveForwardComplete,
                             0,
                             symbol_table.endo_nbr(),
-                            endo_idx_block2orig,
-                            eq_idx_block2orig,
+                            endo_idx,
+                            eq_idx,
                             false,
                             symbol_table.endo_nbr(),
                             0,
@@ -362,44 +367,40 @@ StaticModel::computingPass(int derivsOrder, int paramsDerivsOrder, const eval_co
       computeParamsDerivatives(paramsDerivsOrder);
     }
 
-  if (block)
-    {
-      auto contemporaneous_jacobian = evaluateAndReduceJacobian(eval_context);
-
-      computeNonSingularNormalization(contemporaneous_jacobian, false);
-
-      auto [prologue, epilogue] = computePrologueAndEpilogue();
-
-      auto first_order_endo_derivatives = collectFirstOrderDerivativesEndogenous();
-
-      equationTypeDetermination(first_order_endo_derivatives, mfs);
-
-      cout << "Finding the optimal block decomposition of the model ..." << endl;
-
-      computeBlockDecomposition(prologue, epilogue);
-
-      reduceBlockDecomposition();
-
-      printBlockDecomposition();
+  computeTemporaryTerms(true, no_tmp_terms);
 
-      computeChainRuleJacobian();
+  /* Must be called after computeTemporaryTerms(), because it depends on
+     temporary_terms_mlv to be filled */
+  if (paramsDerivsOrder > 0 && !no_tmp_terms)
+    computeParamsDerivativesTemporaryTerms();
 
-      determineLinearBlocks();
-
-      if (!no_tmp_terms)
-        computeBlockTemporaryTerms();
-    }
-  else
+  if (!computingPassBlock(eval_context, no_tmp_terms) && block)
     {
-      computeTemporaryTerms(true, no_tmp_terms);
-
-      /* Must be called after computeTemporaryTerms(), because it depends on
-         temporary_terms_mlv to be filled */
-      if (paramsDerivsOrder > 0 && !no_tmp_terms)
-        computeParamsDerivativesTemporaryTerms();
+      cerr << "ERROR: Block decomposition requested but failed. If your model does not have a steady state, you may want to try the 'no_static' option of the 'model' block." << endl;
+      exit(EXIT_FAILURE);
     }
 }
 
+bool
+StaticModel::computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms)
+{
+  auto contemporaneous_jacobian = evaluateAndReduceJacobian(eval_context);
+  if (!computeNonSingularNormalization(contemporaneous_jacobian, false))
+    return false;
+  auto [prologue, epilogue] = computePrologueAndEpilogue();
+  auto first_order_endo_derivatives = collectFirstOrderDerivativesEndogenous();
+  equationTypeDetermination(first_order_endo_derivatives, mfs);
+  cout << "Finding the optimal block decomposition of the static model..." << endl;
+  computeBlockDecomposition(prologue, epilogue);
+  reduceBlockDecomposition();
+  printBlockDecomposition();
+  computeChainRuleJacobian();
+  determineLinearBlocks();
+  if (!no_tmp_terms)
+    computeBlockTemporaryTerms();
+  return true;
+}
+
 void
 StaticModel::writeStaticMFile(const string &basename) const
 {
diff --git a/src/StaticModel.hh b/src/StaticModel.hh
index 092ebdc046af0fb1ef5feebe99504963d6f088e6..da65283905cc5a4069e114d64bbf491744138a5f 100644
--- a/src/StaticModel.hh
+++ b/src/StaticModel.hh
@@ -110,6 +110,12 @@ private:
     return var;
   }
 
+  /* Compute block decomposition, its derivatives and temporary terms.
+     Meant to be overriden in derived classes which don’t support block
+     decomposition (currently PlannerObjective). Returns a boolean indicating success
+     (failure can happen in normalization). */
+  virtual bool computingPassBlock(const eval_context_t &eval_context, bool no_tmp_terms);
+
 public:
   StaticModel(SymbolTable &symbol_table_arg,
               NumericalConstants &num_constants,
@@ -124,7 +130,7 @@ public:
   //! Writes information about the static model to the driver file
   void writeDriverOutput(ostream &output, bool block) const;
 
-  //! Execute computations (variable sorting + derivation)
+  //! Execute computations (variable sorting + derivation + block decomposition)
   /*!
     \param eval_context evaluation context for normalization
     \param no_tmp_terms if true, no temporary terms will be computed in the static files