diff --git a/include/libgmshdg/param_loader.h b/include/libgmshdg/param_loader.h
index 7ca629455d77b287ecebedd1cc1b031cdb6dc507..4390e96258208fdac076bb74cc2b9bc6ad4b2c49 100644
--- a/include/libgmshdg/param_loader.h
+++ b/include/libgmshdg/param_loader.h
@@ -66,6 +66,8 @@ public:
 
     void            call_timestep_callback(size_t);
     void            call_initialization_callback();
+    void            call_finalization_callback();
+
 
     sol::state&     lua_state(void) { return lua; }
 };
diff --git a/include/maxwell/maxwell_postpro.h b/include/maxwell/maxwell_postpro.h
index fe345ade591e8aedd4380882c93e0db6768a4020..943bf50aedc0dc1c22e84ec334858d54653e0fea 100644
--- a/include/maxwell/maxwell_postpro.h
+++ b/include/maxwell/maxwell_postpro.h
@@ -8,5 +8,7 @@ field_values eval_field(const field&, size_t, size_t, const vecxd&);
 field_values compute_error(const model&, const solver_state&, const parameter_loader&);
 field_values compute_energy(const model&, const solver_state&, const parameter_loader&);
 
+void compare_at_gauss_points(const model&, const solver_state&, const parameter_loader&);
+
 } // namespace maxwell
 
diff --git a/share/validation/params_resonator_gmshfem_comparison.lua b/share/validation/params_resonator_gmshfem_comparison.lua
new file mode 100644
index 0000000000000000000000000000000000000000..9721a1f6ca9c80a88561a3d9d6dc69da33d7d4c9
--- /dev/null
+++ b/share/validation/params_resonator_gmshfem_comparison.lua
@@ -0,0 +1,97 @@
+-- Resonant cavity test. See Pozar, section 6.3 for theory.
+
+-----------------------
+--[[ Problem setup ]]--
+sim.name = "test_maxwell_resonator"             -- simulation name
+sim.dt = 1e-11                                  -- timestep size
+sim.timesteps = 11                           -- num of iterations
+sim.gmsh_model = "resonator.geo"                -- gmsh model filename
+sim.use_gpu = 0                                 -- 0: cpu, 1: gpu
+sim.approx_order = 2                            -- approximation order
+sim.time_integrator = "leapfrog"
+postpro.silo_output_rate = 100
+postpro.cycle_print_rate = 10                  -- console print rate
+
+postpro["E"].silo_mode = "zonal"
+postpro["H"].silo_mode = "none"
+postpro["J"].silo_mode = "none"
+
+local epsr = 1
+local mur = 1
+
+materials[1] = {}
+materials[1].epsilon = epsr
+materials[1].mu = mur
+materials[1].sigma = 0
+
+function electric_initial_condition(x, y, z)
+    local Ex = 0
+    local Ey = math.sin(math.pi*x) * math.sin(math.pi*z)
+    local Ez = 0
+    return Ex, Ey, Ez
+end
+
+--------------------------
+--[[ Validation stuff ]]--
+debug = {}
+
+-- determine if we should do I/O
+local do_IO = (not parallel) or (parallel and parallel.comm_rank == 0)
+
+local c0 = 1/math.sqrt(const.eps0*const.mu0)
+-- Mode
+local m = 1     -- along x
+local n = 0     -- along y
+local l = 1     -- along z
+-- Cavity dimensions (must match sim.gmsh_model)
+local a = 1     -- along x
+local b = 0.1   -- along y
+local d = 1     -- along z
+
+local u = m*math.pi/a
+local v = n*math.pi/b
+local w = l*math.pi/d
+-- Compute resonant frequency
+local omega = c0*math.sqrt(u*u + v*v + w*w)/math.sqrt(epsr*mur)
+local resonance_f = omega/(2*math.pi)
+local resonance_MHz = resonance_f/1e6
+local cycle_timesteps = 1/(resonance_f*sim.dt)
+-- Compute impedance
+local eps = materials[1].epsilon * const.eps0
+local mu = materials[1].mu * const.mu0
+local k_sq = (omega*mu)*(omega*eps)
+local kc_sq = u*u + v*v
+local beta = math.sqrt(k_sq - kc_sq)
+local Zte = omega*mu/beta 
+local We = eps*a*b*d/16
+
+if ( do_IO ) then
+    print("\x1b[1mANALYTICAL SOLUTION DATA:")
+    print("  Resonance frequency: " .. resonance_MHz .. " Mhz")
+    print("  Cavity impedance: " .. Zte .. " Ohm")
+    print("  Timesteps for 1 cycle: " .. cycle_timesteps)
+    print("  Expected energy " .. 2*We .. " J \x1b[0m")
+end
+
+function ansol(tag, x, y, z, t)
+    local Ex = 0.0
+    local Ey = math.cos(omega*t)*math.sin(math.pi*x)*math.sin(math.pi*z)
+    local Ez = 0.0
+    local Hx = math.sin(omega*t)*math.sin(math.pi*x)*math.cos(math.pi*z)/Zte
+    local Hy = 0.0
+    local Hz = -math.sin(omega*t)*math.cos(math.pi*x)*math.sin(math.pi*z)/Zte
+
+    return Ex, Ey, Ez, Hx, Hy, Hz
+end
+
+debug.analytical_solution = ansol
+--debug.dump_cell_ranks = true
+
+function on_timestepping_finished()
+    err = compute_error()
+    if ( do_IO ) then
+        print("\x1b[32m*** GMSH-FEM COMPARISON DATA ***\x1b[0m")
+        print("Error on Ey: " .. err.Ey)
+        --compare_at_gauss_points() -- NOT MPI SAFE
+    end
+end
diff --git a/share/validation/resonator.geo b/share/validation/resonator.geo
index 3c3f82936d969c3b7a9a80eeb9c048c2b67cbfac..fc0c04645fca7f0c071c4f07bbab9ca0ad0efd3e 100644
--- a/share/validation/resonator.geo
+++ b/share/validation/resonator.geo
@@ -1,4 +1,7 @@
 SetFactory("OpenCASCADE");
-Box(1) = {0, 0, 0, 1, 0.1, 1};
+Box(1) = {0, 0, 0, 1, 1, 1};
 
-MeshSize{:} = 0.05;
+Physical Volume(1000) = {1};
+Physical Surface(2000) = {1,2,3,4,5,6};
+
+MeshSize{:} = 0.2;
diff --git a/src/libgmshdg/param_loader.cpp b/src/libgmshdg/param_loader.cpp
index 238da20ec851af53dad694de80ccb29394348196..831aec733bbb5265218b0c7b6df0ff5e7f231102 100644
--- a/src/libgmshdg/param_loader.cpp
+++ b/src/libgmshdg/param_loader.cpp
@@ -247,6 +247,21 @@ parameter_loader_base::call_initialization_callback()
     }
 }
 
+void
+parameter_loader_base::call_finalization_callback()
+{
+    if ( lua["on_timestepping_finished"].valid() )
+    {
+        try {
+            lua["on_timestepping_finished"]();
+        }
+        catch (...)
+        {
+            std::cout << "An error was thrown calling on_timestepping_finished()" << std::endl;
+        }
+    }
+}
+
 bool
 parameter_loader_base::source_has_changed_state(void) const
 {
diff --git a/src/libgmshdg/physical_element.cpp b/src/libgmshdg/physical_element.cpp
index c9190fae938339f837f4051c0981179588681dc2..655c49cffdebad422c532a5e23ec22edf2f5b85d 100644
--- a/src/libgmshdg/physical_element.cpp
+++ b/src/libgmshdg/physical_element.cpp
@@ -1,5 +1,6 @@
 #include <iostream>
 #include <cassert>
+#include <fstream>
 
 #include "libgmshdg/physical_element.h"
 #include "libgmshdg/gmsh_io.h"
diff --git a/src/maxwell/maxwell_postpro.cpp b/src/maxwell/maxwell_postpro.cpp
index 2d162f6c88c5893fb7b0b9ecba87a9779bc9a5a7..21db7ea7bd0c5a6e19b6bce267ac0a986ead9c56 100644
--- a/src/maxwell/maxwell_postpro.cpp
+++ b/src/maxwell/maxwell_postpro.cpp
@@ -1,4 +1,7 @@
 
+#include <fstream>
+#include <fcntl.h>
+
 #ifdef USE_MPI
 #include "common/mpi_helpers.h"
 #endif /* USE_MPI */
@@ -37,12 +40,10 @@ compute_error(const model& mod, const solver_state& state, const parameter_loade
             const auto& re = e.cell_refelem(pe);
 
             const auto pqps = pe.integration_points();
-            const auto dets = pe.determinants();
 
             const auto ofs = e.cell_model_dof_offset(iT);
             const auto cbs = re.num_basis_functions();
 
-            assert(pqps.size() == dets.size());
             for (size_t iQp = 0; iQp < pqps.size(); iQp++)
             {
                 const auto& pqp = pqps[iQp];
@@ -78,12 +79,10 @@ compute_energy(const model& mod, const solver_state& state, const parameter_load
             const auto& re = e.cell_refelem(pe);
 
             const auto pqps = pe.integration_points();
-            const auto dets = pe.determinants();
 
             const auto ofs = e.cell_model_dof_offset(iT);
             const auto cbs = re.num_basis_functions();
 
-            assert(pqps.size() == dets.size());
             const auto bar = pe.barycenter();
             auto eps = mpl.epsilon( e.material_tag(), bar );
             auto mu = mpl.mu( e.material_tag(), bar );
@@ -113,5 +112,91 @@ validate(const model&, maxwell::solver_state_gpu&,
 {}
 #endif /* ENABLE_GPU_SOLVER */
 
+
+void
+compare_at_gauss_points(const model& mod, const solver_state& state, const parameter_loader& mpl)
+{
+    struct qpFval { double Fx, Fy, Fz; };
+
+    size_t point_id = 0;
+    std::ofstream pmap_ofs("pointmap.txt");
+
+    double errorsq_dg_vs_gmshfem = 0.0;
+    double errorsq_dg_vs_ana = 0.0;
+    double errorsq_gmshfem_vs_ana = 0.0;
+    double normsq_dg = 0.0;
+    double normsq_gmshfem = 0.0;
+    double normsq_ana = 0.0;
+    for (auto& e : mod)
+    {
+        std::stringstream ss;
+        ss << "e_" << e.gmsh_dim() << "_" << e.gmsh_tag() << ".dat";
+
+        std::cout << "Loading " << ss.str() << std::endl;
+        int fd = open(ss.str().c_str(), O_RDONLY);
+        if (fd < 0)
+        {
+            std::cout << "Can't open '" << ss.str() << "'" << std::endl;
+            return;
+        }
+        size_t num_vals;
+        read(fd, &num_vals, sizeof(size_t));
+        std::cout << "Reading " << num_vals << " values" << std::endl;
+        std::vector<qpFval> qpFvals(num_vals);
+        read(fd, qpFvals.data(), num_vals*sizeof(qpFval));
+        close(fd);
+
+        std::cout << "Num of cells: " << e.num_cells() << std::endl;    
+        for (size_t iT = 0; iT < e.num_cells(); iT++)
+        {
+            const auto& pe = e.cell(iT);
+            const auto& re = e.cell_refelem(pe);
+            const auto ofs = e.cell_model_dof_offset(iT);
+            const auto cbs = re.num_basis_functions();
+            const auto pqps = pe.integration_points();
+            
+            const auto gmsh_pos = e.cell_local_index_by_gmsh(iT);
+            const size_t num_qps = pqps.size();
+            const size_t qps_pos_base = num_qps*gmsh_pos;
+            for (size_t iQp = 0; iQp < num_qps; iQp++)
+            {
+                const auto& pqp = pqps[iQp];
+                const vecxd phi = re.basis_functions(iQp);
+                field_values Fh = eval_field(state.emf_curr, ofs, cbs, phi);
+                auto& qpFval = qpFvals.at(qps_pos_base+iQp);
+                auto Exd = Fh.Ex - qpFval.Fx;
+                auto Eyd = Fh.Ey - qpFval.Fy;
+                auto Ezd = Fh.Ez - qpFval.Fz;
+                errorsq_dg_vs_gmshfem += pqp.weight() * (Exd*Exd + Eyd*Eyd + Ezd*Ezd);
+                normsq_dg += pqp.weight() * (Fh.Ex*Fh.Ex + Fh.Ey*Fh.Ey + Fh.Ez*Fh.Ez);
+                normsq_gmshfem += pqp.weight() * (qpFval.Fy*qpFval.Fy);
+
+                auto p = pqp.point();
+                auto Ey_ana = std::sin(M_PI*p.x())*std::sin(M_PI*p.z())*std::cos(2.*M_PI*211.98528005809e6*state.curr_time);
+                normsq_ana += pqp.weight() * Ey_ana * Ey_ana;
+                errorsq_dg_vs_ana += pqp.weight() * ( (Fh.Ey - Ey_ana)*(Fh.Ey - Ey_ana) );
+                errorsq_gmshfem_vs_ana += pqp.weight() * ( (qpFval.Fy - Ey_ana)*(qpFval.Fy - Ey_ana) );
+                pmap_ofs << pe.element_tag() << " " << qps_pos_base+iQp << " " << p.x() << " " << p.y() << " " << p.z() << " " << point_id++ << std::endl;
+            }
+        }
+    }
+
+    double norm_dg_error = 100.*(std::sqrt(normsq_dg) - std::sqrt(normsq_ana))/std::sqrt(normsq_ana);
+    double norm_gmshfem_error = 100.*(std::sqrt(normsq_gmshfem) - std::sqrt(normsq_ana))/std::sqrt(normsq_ana);
+    std::cout << sgr::Bredfg << "Errors and norms:" << sgr::reset << std::endl;
+    std::cout << " Analytic norm:        " << std::sqrt(normsq_ana) << std::endl;
+    std::cout << " Computed norm (DG):   " << std::sqrt(normsq_dg) << ", off by " << norm_dg_error << " %" << std::endl;
+    std::cout << sgr::BGreenfg;
+    std::cout << " Computed norm (FEM):  " << std::sqrt(normsq_gmshfem) << ", off by " << norm_gmshfem_error << " %" << std::endl;
+    std::cout << sgr::reset;
+
+    std::cout << " DG vs. FEM:           " << std::sqrt(errorsq_dg_vs_gmshfem) << std::endl;
+    std::cout << " DG vs. analytic:      " << std::sqrt(errorsq_dg_vs_ana) << std::endl;
+    std::cout << sgr::BGreenfg;
+    std::cout << " FEM vs. analytic:     " << std::sqrt(errorsq_gmshfem_vs_ana) << std::endl;
+    std::cout << sgr::reset;
+}
+
+
 } // namespace maxwell
 
diff --git a/src/maxwell/maxwell_solver.cpp b/src/maxwell/maxwell_solver.cpp
index 6a3e29a2ff0f5fcbf59fb4086a9d08e086da46ec..3fb6cdf03e4e89711266e2ae2e6d5763e64db79c 100644
--- a/src/maxwell/maxwell_solver.cpp
+++ b/src/maxwell/maxwell_solver.cpp
@@ -191,7 +191,7 @@ void solver_mainloop(const model& mod, State& state, maxwell::parameter_loader&
     if (proc_rank == 0) {
 #endif /* USE_MPI */
     std::cout << "    BEGINNING SIMULATION" << std::endl;
-    std::cout << "I will do " << num_timesteps << " of " << state.delta_t;
+    std::cout << "I will do " << num_timesteps << " timesteps of " << state.delta_t;
     std::cout << "s each." << std::endl;
     std::cout << "Time integrator: " << mpl.sim_timeIntegratorName() << std::endl;
 #ifdef USE_MPI
@@ -220,15 +220,22 @@ void solver_mainloop(const model& mod, State& state, maxwell::parameter_loader&
     sol::state& lua = mpl.lua_state();
     lua["relmat"] = relmat;
 #endif /* ENABLE_EXP_GEOMETRIC_DIODE */
-    timecounter tc;
-    tc.tic();
+
+    double total_iteration_time = 0.;
+
     prepare_sources(mod, state, mpl);
     for(size_t i = 0; i < num_timesteps; i++)
     {
+        timecounter tc;
+        tc.tic();
+
         mpl.call_timestep_callback(i);
         timestep(mod, state, mpl, ti);
         do_sources(mod, state, mpl);
         
+        double time = tc.toc();
+        total_iteration_time += time;
+
         if ( (silo_output_rate != 0) and (i%silo_output_rate == 0) )
             export_fields_to_silo(mod, state, mpl, "");
 
@@ -236,29 +243,44 @@ void solver_mainloop(const model& mod, State& state, maxwell::parameter_loader&
 
         if ( (cycle_print_rate != 0) and (i%cycle_print_rate == 0) )
         {
-            double time = tc.toc();
-            double dofs_s_proc = (mod.num_dofs()*6*cycle_print_rate)/(time);
+            double dofs_s_proc = (mod.num_dofs()*6)/time;
 #ifdef USE_MPI
             double dofs_s_total;
             MPI_Reduce(&dofs_s_proc, &dofs_s_total, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
             if (proc_rank == 0)
             {
-                std::cout << hidecursor << clrline << "Cycle " << i << ": t = ";
+                std::cout << cr << clrline << "Cycle " << i << ": t = ";
                 std::cout << state.curr_time << " s" << ", DOFs/s: ";
-                std::cout << dofs_s_total << cr << std::flush;
+                std::cout << dofs_s_total << std::flush;
             }
 #else /* USE_MPI */
-                std::cout << hidecursor << clrline << "Cycle " << i << ": t = ";
+                std::cout << cr << clrline << "Cycle " << i << ": t = ";
                 std::cout << state.curr_time << " s" << ", DOFs/s: ";
-                std::cout << dofs_s_proc << ", avg time: " << time/cycle_print_rate << " s" << cr << std::flush;
+                std::cout << dofs_s_proc << ", time: " << time << " s" << std::flush;
 #endif /* USE_MPI */
-            tc.tic();
         }
     }
-    #ifdef USE_MPI
+
+#ifdef USE_MPI
     if (proc_rank == 0)
-    #endif /* USE_MPI */
-        std::cout << showcursor << std::endl;
+#endif
+        std::cout << std::endl;
+
+    mpl.call_finalization_callback();
+
+#ifdef USE_MPI
+    if (proc_rank == 0)
+#endif
+    {
+        std::cout << Bon << greenfg;
+        std::cout << "Total iteration time = " << total_iteration_time << " s" << std::endl;
+        std::cout << "Avg iteration time   = " << total_iteration_time/num_timesteps << " s" << std::endl;
+#ifdef USE_MPI
+        std::cout << "Avg dofs/s           = " << num_timesteps*(mod.num_dofs_world()*6)/total_iteration_time << reset << std::endl;
+#else
+        std::cout << "Avg dofs/s           = " << num_timesteps*(mod.num_dofs()*6)/total_iteration_time << reset << std::endl;
+#endif
+    }
 }
 
 /****************************************************************************/
@@ -315,6 +337,11 @@ register_lua_usertypes_bystate(maxwell::parameter_loader& mpl,
     };
     lua["compute_error"] = compute_error_lambda;
 
+    auto compare_at_gauss_points_lambda = [&]() {
+        compare_at_gauss_points(mod, state, mpl);
+    };
+    lua["compare_at_gauss_points"] = compare_at_gauss_points_lambda;
+
 #ifdef ENABLE_EXP_GEOMETRIC_DIODE
     lua["integrate_E"] = integrate_electric_field;
 #endif /*ENABLE_EXP_GEOMETRIC_DIODE */