diff --git a/src/DynamicModel.cc b/src/DynamicModel.cc
index 45a406f1d6ecb197030b786f48435ef2db9729cb..40b3d30cb13e1e1791dcddeb45a25b782cc75ecc 100644
--- a/src/DynamicModel.cc
+++ b/src/DynamicModel.cc
@@ -1461,6 +1461,8 @@ DynamicModel::writeBlockDriverOutput(ostream &output, const string &basename,
              << "M_.block_structure.block(" << blk+1 << ").NNZDerivatives = " << blocks_derivatives[blk].size() << ';' << endl;
     }
 
+  writeBlockDriverSparseIndicesHelper<true>(output);
+
   output << "M_.block_structure.variable_reordered = [";
   for (int i = 0; i < symbol_table.endo_nbr(); i++)
     output << " " << endo_idx_block2orig[i]+1;
@@ -3101,10 +3103,14 @@ DynamicModel::computeChainRuleJacobian()
   size_t nb_blocks { blocks.size() };
 
   blocks_derivatives.resize(nb_blocks);
+  blocks_jacobian_sparse_column_major_order.resize(nb_blocks);
+  blocks_jacobian_sparse_colptr.resize(nb_blocks);
 
   for (int blk {0}; blk < static_cast<int>(nb_blocks); blk++)
     {
       int nb_recursives = blocks[blk].getRecursiveSize();
+      int mfs_size { blocks[blk].mfs_size };
+      BlockSimulationType simulation_type { blocks[blk].simulation_type };
 
       // Create a map from recursive vars to their defining (normalized) equation
       map<int, BinaryOpNode *> recursive_vars;
@@ -3144,6 +3150,25 @@ DynamicModel::computeChainRuleJacobian()
           if (d != Zero)
             blocks_derivatives[blk][{ eq, var, lag }] = d;
         }
+
+      // Compute the sparse representation of the Jacobian
+      if (simulation_type != BlockSimulationType::evaluateForward
+          && simulation_type != BlockSimulationType::evaluateBackward)
+        {
+          const bool one_boundary {simulation_type == BlockSimulationType::solveBackwardSimple
+                                   || simulation_type == BlockSimulationType::solveForwardSimple
+                                   || simulation_type == BlockSimulationType::solveBackwardComplete
+                                   || simulation_type == BlockSimulationType::solveForwardComplete};
+          for (const auto &[indices, d1] : blocks_derivatives[blk])
+            {
+              auto &[eq, var, lag] { indices };
+              assert(lag >= -1 && lag <= 1);
+              if (eq >= nb_recursives && var >= nb_recursives
+                  && !(one_boundary && lag != 0))
+                blocks_jacobian_sparse_column_major_order[blk].emplace(pair{eq-nb_recursives, var-nb_recursives+static_cast<int>(!one_boundary)*(lag+1)*mfs_size}, d1);
+            }
+          blocks_jacobian_sparse_colptr[blk] = computeCSCColPtr(blocks_jacobian_sparse_column_major_order[blk], (one_boundary ? 1 : 3)*mfs_size);
+        }
     }
 
   /* Also store information and derivatives w.r.t. other types of variables
diff --git a/src/DynamicModel.hh b/src/DynamicModel.hh
index 8bdca37458efc0fedd187f4df1f09fcf37e9a03e..2936fdbd84a66cc5abf4e0fb45fed085b0c23ade 100644
--- a/src/DynamicModel.hh
+++ b/src/DynamicModel.hh
@@ -128,7 +128,7 @@ private:
   /* Computes the number of nonzero elements in deterministic Jacobian of
      block-decomposed model */
   int nzeDeterministicJacobianForBlock(int blk) const;
-  //! Helper for writing the per-block dynamic files of block decomposed models
+  // Helper for writing the per-block dynamic files of block decomposed models (legacy representation)
   template<ExprNodeOutputType output_type>
   void writeDynamicPerBlockHelper(int blk, ostream &output, temporary_terms_t &temporary_terms, int nze_stochastic, int nze_deterministic, int nze_exo, int nze_exo_det, int nze_other_endo) const;
   //! Writes the per-block dynamic files of block decomposed model (MATLAB version)
@@ -695,6 +695,8 @@ DynamicModel::writeDynamicPerBlockHelper(int blk, ostream &output, temporary_ter
                                          int nze_stochastic, int nze_deterministic, int nze_exo,
                                          int nze_exo_det, int nze_other_endo) const
 {
+  static_assert(!isSparseModelOutput(output_type));
+
   BlockSimulationType simulation_type { blocks[blk].simulation_type };
   int block_mfs_size { blocks[blk].mfs_size };
   int block_recursive_size { blocks[blk].getRecursiveSize() };
diff --git a/src/ModelTree.cc b/src/ModelTree.cc
index b31cac943a228c52838065543baa6e0a4c2075f6..ef94feaabb12ee9496c83241b3c7f1ad7df33d7b 100644
--- a/src/ModelTree.cc
+++ b/src/ModelTree.cc
@@ -125,6 +125,14 @@ ModelTree::copyHelper(const ModelTree &m)
     blocks_temporary_terms.push_back(convert_vector_tt(it));
   for (const auto &it : m.blocks_temporary_terms_idxs)
     blocks_temporary_terms_idxs.emplace(f(it.first), it.second);
+
+  for (const auto &it : m.blocks_jacobian_sparse_column_major_order)
+    {
+      map<pair<int, int>, expr_t, columnMajorOrderLess> v;
+      for (const auto &it2 : it)
+        v.emplace(it2.first, f(it2.second));
+      blocks_jacobian_sparse_column_major_order.push_back(v);
+    }
 }
 
 ModelTree::ModelTree(SymbolTable &symbol_table_arg,
@@ -160,6 +168,7 @@ ModelTree::ModelTree(const ModelTree &m) :
   blocks{m.blocks},
   endo2block{m.endo2block},
   eq2block{m.eq2block},
+  blocks_jacobian_sparse_colptr{m.blocks_jacobian_sparse_colptr},
   endo2eq{m.endo2eq},
   cutoff{m.cutoff},
   mfs{m.mfs}
@@ -204,6 +213,8 @@ ModelTree::operator=(const ModelTree &m)
   eq2block = m.eq2block;
   blocks_temporary_terms.clear();
   blocks_temporary_terms_idxs.clear();
+  blocks_jacobian_sparse_column_major_order.clear();
+  blocks_jacobian_sparse_colptr = m.blocks_jacobian_sparse_colptr;
   endo2eq = m.endo2eq;
   cutoff = m.cutoff;
   mfs = m.mfs;
diff --git a/src/ModelTree.hh b/src/ModelTree.hh
index 8947d052239273956ad69fec9bc3185b92620e8f..226b0e8d6e2e37f3758e67881db392bb1cc2d912 100644
--- a/src/ModelTree.hh
+++ b/src/ModelTree.hh
@@ -238,6 +238,18 @@ protected:
      the vector of all temporary terms */
   temporary_terms_idxs_t blocks_temporary_terms_idxs;
 
+  /* The nonzero values of the sparse block Jacobians in column-major order
+     (which is the natural order for Compressed Sparse Column (CSC) storage).
+     The pair of indices is (row, column). Nothing is stored for blocks of
+     type “evaluate”, since their Jacobian is not used in the sparse
+     representation (since the latter does not support the stochastic mode,
+     which only exists in the legacy representation). */
+  vector<SparseColumnMajorOrderMatrix> blocks_jacobian_sparse_column_major_order;
+  /* Column indices for the sparse block Jacobian in Compressed Sparse Column
+     (CSC) storage (corresponds to the “jc” vector in MATLAB terminology).
+     Same remark as above regarding blocks of type “evaluate”. */
+  vector<vector<int>> blocks_jacobian_sparse_colptr;
+
   //! Computes derivatives
   /*! \param order the derivation order
       \param vars the derivation IDs w.r.t. which compute the derivatives */
@@ -296,9 +308,15 @@ protected:
   void writeModelCFile(const string &basename, const string &mexext, const filesystem::path &matlabroot, const filesystem::path &dynareroot) const;
 
   // Writes per-block residuals and temporary terms (incl. for derivatives)
+  // This part is common to the legacy and sparse representations
   template<ExprNodeOutputType output_type>
   void writePerBlockHelper(int blk, ostream &output, temporary_terms_t &temporary_terms) const;
 
+  // Writes per-block Jacobian (sparse representation)
+  // Assumes temporary terms for derivatives are already set.
+  template<ExprNodeOutputType output_type>
+  void writeSparsePerBlockJacobianHelper(int blk, ostream &output, temporary_terms_t &temporary_terms) const;
+
   /* Helper for writing derivatives w.r.t. parameters.
      Returns { tt, rp, gp, rpp, gpp, hp, g3p }.
      g3p is empty if requesting a static output type. */
@@ -329,6 +347,10 @@ protected:
   template<bool dynamic>
   void writeJsonSparseIndicesHelper(ostream &output) const;
 
+  // Helper for writing sparse block derivatives indices in MATLAB/Octave driver file
+  template<bool dynamic>
+  void writeBlockDriverSparseIndicesHelper(ostream &output) const;
+
   /* Helper for writing JSON output for residuals and derivatives.
      Returns mlv and derivatives output at each derivation order. */
   template<bool dynamic>
@@ -2254,6 +2276,56 @@ ModelTree::writeJsonSparseIndicesHelper(ostream &output) const
     }
 }
 
+template<bool dynamic>
+void
+ModelTree::writeBlockDriverSparseIndicesHelper(ostream &output) const
+{
+  for (int blk {0}; blk < static_cast<int>(blocks.size()); blk++)
+    {
+      const string struct_name { "M_.block_structure"s + (dynamic ? "" : "_stat") + ".block(" + to_string(blk+1) + ")." };
+
+      // Write indices for the sparse Jacobian (both naive and CSC storage)
+      output << struct_name << "g1_sparse_rowval = int32([";
+      for (const auto &[indices, d1] : blocks_jacobian_sparse_column_major_order.at(blk))
+        output << indices.first+1 << ' ';
+      output << "]);" << endl
+             << struct_name << "g1_sparse_colval = int32([";
+      for (const auto &[indices, d1] : blocks_jacobian_sparse_column_major_order.at(blk))
+        output << indices.second+1 << ' ';
+      output << "]);" << endl
+             << struct_name << "g1_sparse_colptr = int32([";
+      for (int it : blocks_jacobian_sparse_colptr.at(blk))
+        output << it+1 << ' ';
+      output << "]);" << endl;
+    }
+}
+
+template<ExprNodeOutputType output_type>
+void
+ModelTree::writeSparsePerBlockJacobianHelper(int blk, ostream &output, temporary_terms_t &temporary_terms) const
+{
+  static_assert(isSparseModelOutput(output_type));
+
+  // NB: stochastic mode is currently unsupported by sparse representation
+  /* See also the comment above the definition of
+     blocks_jacobian_sparse_column_major_order and
+     blocks_jacobian_sparse_column_major_colptr */
+
+  assert(blocks[blk].simulation_type != BlockSimulationType::evaluateForward
+         && blocks[blk].simulation_type != BlockSimulationType::evaluateBackward);
+
+  int k {0};
+  for (const auto &[row_col, d1] : blocks_jacobian_sparse_column_major_order[blk])
+    {
+      output << "g1_v" << LEFT_ARRAY_SUBSCRIPT(output_type)
+             << k + ARRAY_SUBSCRIPT_OFFSET(output_type)
+             << RIGHT_ARRAY_SUBSCRIPT(output_type) << "=";
+      d1->writeOutput(output, output_type, temporary_terms, blocks_temporary_terms_idxs);
+      output << ";" << endl;
+      k++;
+    }
+}
+
 template<bool dynamic>
 void
 ModelTree::writeSparseModelJuliaFiles(const string &basename) const
diff --git a/src/StaticModel.cc b/src/StaticModel.cc
index c005f124cd1b29b15e86f69713a1e816bf564f79..acb299d3ec7f4f41205b4291a96ef5d86c482b75 100644
--- a/src/StaticModel.cc
+++ b/src/StaticModel.cc
@@ -846,6 +846,8 @@ StaticModel::writeBlockDriverOutput(ostream &output) const
   output << "];" << endl
          << "M_.block_structure_stat.tmp_nbr = " << blocks_temporary_terms_idxs.size()
          << ";" << endl;
+
+  writeBlockDriverSparseIndicesHelper<false>(output);
 }
 
 SymbolType
@@ -911,10 +913,15 @@ void
 StaticModel::computeChainRuleJacobian()
 {
   int nb_blocks = blocks.size();
+
   blocks_derivatives.resize(nb_blocks);
+  blocks_jacobian_sparse_column_major_order.resize(nb_blocks);
+  blocks_jacobian_sparse_colptr.resize(nb_blocks);
+
   for (int blk = 0; blk < nb_blocks; blk++)
     {
       int nb_recursives = blocks[blk].getRecursiveSize();
+      BlockSimulationType simulation_type { blocks[blk].simulation_type };
 
       map<int, BinaryOpNode *> recursive_vars;
       for (int i = 0; i < nb_recursives; i++)
@@ -926,8 +933,8 @@ StaticModel::computeChainRuleJacobian()
             recursive_vars[deriv_id] = getBlockEquationExpr(blk, i);
         }
 
-      assert(blocks[blk].simulation_type != BlockSimulationType::solveTwoBoundariesSimple
-             && blocks[blk].simulation_type != BlockSimulationType::solveTwoBoundariesComplete);
+      assert(simulation_type != BlockSimulationType::solveTwoBoundariesSimple
+             && simulation_type != BlockSimulationType::solveTwoBoundariesComplete);
 
       int size = blocks[blk].size;
 
@@ -942,6 +949,19 @@ StaticModel::computeChainRuleJacobian()
                 blocks_derivatives[blk][{ eq, var, 0 }] = d1;
             }
         }
+
+      // Compute the sparse representation of the Jacobian
+      if (simulation_type != BlockSimulationType::evaluateForward
+          && simulation_type != BlockSimulationType::evaluateBackward)
+        {
+          for (const auto &[indices, d1] : blocks_derivatives[blk])
+            {
+              auto &[eq, var, lag] { indices };
+              assert(lag == 0);
+              blocks_jacobian_sparse_column_major_order[blk].emplace(pair{eq, var}, d1);
+            }
+          blocks_jacobian_sparse_colptr[blk] = computeCSCColPtr(blocks_jacobian_sparse_column_major_order[blk], size);
+        }
     }
 }
 
diff --git a/src/StaticModel.hh b/src/StaticModel.hh
index e50dc69610b11a6e0425baefc17f8a7d2f1b64a6..ae893c7aa9e1c7f0f893c97062043fa8eef663aa 100644
--- a/src/StaticModel.hh
+++ b/src/StaticModel.hh
@@ -44,7 +44,7 @@ private:
      then compiles it with the per-block functions into a single MEX */
   void writeStaticBlockCFile(const string &basename, vector<filesystem::path> per_block_object_files, const string &mexext, const filesystem::path &matlabroot, const filesystem::path &dynareroot) const;
 
-  //! Helper for writing a per-block static file of block decomposed model
+  // Helper for writing a per-block static file of block decomposed model (legacy representation)
   template<ExprNodeOutputType output_type>
   void writeStaticPerBlockHelper(int blk, ostream &output, temporary_terms_t &temporary_terms) const;
 
@@ -178,6 +178,8 @@ template<ExprNodeOutputType output_type>
 void
 StaticModel::writeStaticPerBlockHelper(int blk, ostream &output, temporary_terms_t &temporary_terms) const
 {
+  static_assert(!isSparseModelOutput(output_type));
+
   BlockSimulationType simulation_type { blocks[blk].simulation_type };
   int block_recursive_size { blocks[blk].getRecursiveSize() };