diff --git a/CMake/FindFFTW3.cmake b/CMake/FindFFTW3.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..8b5b4c27e1671695253a0e90c480ee3effb53a53
--- /dev/null
+++ b/CMake/FindFFTW3.cmake
@@ -0,0 +1,95 @@
+# - Try to find FFTW3.
+# Usage: find_package(FFTW3 [COMPONENTS [single double long-double threads]])
+#
+# Variables used by this module:
+#  FFTW3_ROOT_DIR             - FFTW3 root directory
+# Variables defined by this module:
+#  FFTW3_FOUND                - system has FFTW3
+#  FFTW3_INCLUDE_DIR          - the FFTW3 include directory (cached)
+#  FFTW3_INCLUDE_DIRS         - the FFTW3 include directories
+#                               (identical to FFTW3_INCLUDE_DIR)
+#  FFTW3[FL]?_LIBRARY         - the FFTW3 library - double, single(F), 
+#                               long-double(L) precision (cached)
+#  FFTW3[FL]?_THREADS_LIBRARY - the threaded FFTW3 library - double, single(F), 
+#                               long-double(L) precision (cached)
+#  FFTW3_LIBRARIES            - list of all FFTW3 libraries found
+
+# Copyright (C) 2009-2010
+# ASTRON (Netherlands Institute for Radio Astronomy)
+# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
+#
+# This file is part of the LOFAR software suite.
+# The LOFAR software suite is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# The LOFAR software suite is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: FindFFTW3.cmake 15918 2010-06-25 11:12:42Z loose $
+
+# Use double precision by default.
+if(FFTW3_FIND_COMPONENTS MATCHES "^$")
+  set(_components double)
+else()
+  set(_components ${FFTW3_FIND_COMPONENTS})
+endif()
+
+# Loop over each component.
+set(_libraries)
+foreach(_comp ${_components})
+  if(_comp STREQUAL "single")
+    list(APPEND _libraries fftw3f)
+  elseif(_comp STREQUAL "double")
+    list(APPEND _libraries fftw3)
+  elseif(_comp STREQUAL "long-double")
+    list(APPEND _libraries fftw3l)
+  elseif(_comp STREQUAL "threads")
+    set(_use_threads ON)
+  else(_comp STREQUAL "single")
+    message(FATAL_ERROR "FindFFTW3: unknown component `${_comp}' specified. "
+      "Valid components are `single', `double', `long-double', and `threads'.")
+  endif(_comp STREQUAL "single")
+endforeach(_comp ${_components})
+
+# If using threads, we need to link against threaded libraries as well.
+if(_use_threads)
+  set(_thread_libs)
+  foreach(_lib ${_libraries})
+    list(APPEND _thread_libs ${_lib}_threads)
+  endforeach(_lib ${_libraries})
+  set(_libraries ${_thread_libs} ${_libraries})
+endif(_use_threads)
+
+# Keep a list of variable names that we need to pass on to
+# find_package_handle_standard_args().
+set(_check_list)
+
+# Search for all requested libraries.
+foreach(_lib ${_libraries})
+  string(TOUPPER ${_lib} _LIB)
+  find_library(${_LIB}_LIBRARY ${_lib}
+    HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES lib)
+  mark_as_advanced(${_LIB}_LIBRARY)
+  list(APPEND FFTW3_LIBRARIES ${${_LIB}_LIBRARY})
+  list(APPEND _check_list ${_LIB}_LIBRARY)
+endforeach(_lib ${_libraries})
+
+# Search for the header file.
+find_path(FFTW3_INCLUDE_DIR fftw3.h 
+  HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES include)
+mark_as_advanced(FFTW3_INCLUDE_DIR)
+list(APPEND _check_list FFTW3_INCLUDE_DIR)
+
+set(FFTW3_INCLUDE_DIRS ${FFTW3_INCLUDE_DIR})
+
+# Handle the QUIETLY and REQUIRED arguments and set FFTW_FOUND to TRUE if
+# all listed variables are TRUE
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(FFTW3 DEFAULT_MSG ${_check_list})
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 903af813301dac1b83ffc2fc9b39bfa49799b422..996273fdc7302c7498c9e762ab9b3bf958d0e203 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,6 +53,9 @@ find_package(OpenMP REQUIRED)
 find_package(Boost REQUIRED)
 include_directories(${Boost_INCLUDE_DIR})
 
+# Find and include FFTW3 float libraries
+find_package(FFTW3 COMPONENTS single REQUIRED)
+
 #------------------------------------------------------------------------------
 # Set CMake and compiler options
 if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
@@ -89,10 +92,6 @@ if("${isSystemDir}" STREQUAL "-1")
    set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
 endif("${isSystemDir}" STREQUAL "-1")
 
-#------------------------------------------------------------------------------
-# Find/load top level dependencies
-# add_subdirectory(external)
-
 #------------------------------------------------------------------------------
 # Add source
 add_subdirectory(cpp)
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 182a1ef51fd877de6619cb890c8fff7649e6f04a..9505ade91e2d98ce999e5e7e240e34c97d804ad8 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -16,6 +16,7 @@ add_library(everybeam SHARED
   beamformeridenticalantennas.cc
   element.cc
   load.cc
+  common/fftresampler.cc
   coords/itrfconverter.cc
   coords/itrfdirection.cc
   lofarreadutils.cc
@@ -25,6 +26,7 @@ add_library(everybeam SHARED
   telescope/dish.cc
   telescope/mwa.cc
   telescope/oskar.cc
+  griddedresponse/griddedresponse.cc
   griddedresponse/lofargrid.cc
   griddedresponse/dishgrid.cc
   griddedresponse/mwagrid.cc
@@ -43,7 +45,7 @@ target_include_directories(everybeam PUBLIC
 
 target_include_directories(everybeam PUBLIC ${CASACORE_INCLUDE_DIR})
 target_link_libraries(everybeam PUBLIC hamaker lobes oskar)
-target_link_libraries(everybeam PUBLIC ${CASACORE_LIBRARIES} ${HDF5_LIBRARIES})
+target_link_libraries(everybeam PUBLIC ${CASACORE_LIBRARIES} ${HDF5_LIBRARIES} ${FFTW3F_LIBRARY})
 
 install (
   TARGETS everybeam
diff --git a/cpp/common/CMakeLists.txt b/cpp/common/CMakeLists.txt
index 6ba321b5db43d6568326f2d8b4ce5509502ee4a0..d61c842af59936672cf332818564afffd5893bbd 100644
--- a/cpp/common/CMakeLists.txt
+++ b/cpp/common/CMakeLists.txt
@@ -4,4 +4,5 @@ install (FILES
   mathutils.h
   mutable_ptr.h
   types.h
+  fftresampler.h
 DESTINATION "include/${CMAKE_PROJECT_NAME}/common")
diff --git a/cpp/common/fftresampler.cc b/cpp/common/fftresampler.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c6b8a82f91cd270d1d876b5201f8a8bab536dad4
--- /dev/null
+++ b/cpp/common/fftresampler.cc
@@ -0,0 +1,171 @@
+#include "fftresampler.h"
+
+#include <complex>
+#include <iostream>
+
+using aocommon::WindowFunction;
+using everybeam::common::FFTResampler;
+
+FFTResampler::FFTResampler(size_t width_in, size_t height_in, size_t width_out,
+                           size_t height_out)
+    : width_in_(width_in),
+      height_in_(height_in),
+      width_out_(width_out),
+      height_out_(height_out),
+      fft_width_(std::max(width_in, width_out)),
+      fft_height_(std::max(height_in, height_out)),
+      window_function_(WindowFunction::Rectangular),
+      tukey_inset_size_(0.0),
+      correct_window_(false) {
+  float* input_data = reinterpret_cast<float*>(
+      fftwf_malloc(fft_width_ * fft_height_ * sizeof(float)));
+  fftwf_complex* fft_data = reinterpret_cast<fftwf_complex*>(
+      fftwf_malloc(fft_width_ * fft_height_ * sizeof(fftwf_complex)));
+  in_to_f_plan_ = fftwf_plan_dft_r2c_2d(height_in_, width_in_, input_data,
+                                        fft_data, FFTW_ESTIMATE);
+  f_to_out_plan_ = fftwf_plan_dft_c2r_2d(height_out_, width_out_, fft_data,
+                                         input_data, FFTW_ESTIMATE);
+  fftwf_free(fft_data);
+  fftwf_free(input_data);
+}
+
+FFTResampler::~FFTResampler() {
+  fftwf_destroy_plan(in_to_f_plan_);
+  fftwf_destroy_plan(f_to_out_plan_);
+}
+
+void FFTResampler::RunSingle(const Task& task, bool skip_window) const {
+  float* ptr_end = task.input + width_in_ * height_in_;
+  for (float* i = task.input; i != ptr_end; ++i) {
+    if (!std::isfinite(*i)) *i = 0.0;
+  }
+
+  if (window_function_ != WindowFunction::Rectangular && !skip_window)
+    ApplyWindow(task.input);
+
+  size_t fft_in_width = width_in_ / 2 + 1;
+  std::complex<float>* fft_data = reinterpret_cast<std::complex<float>*>(
+      fftwf_malloc(fft_in_width * height_in_ * sizeof(std::complex<float>)));
+  fftwf_execute_dft_r2c(in_to_f_plan_, task.input,
+                        reinterpret_cast<fftwf_complex*>(fft_data));
+
+  size_t fft_out_width = width_out_ / 2 + 1;
+  // TODO this can be done without allocating more mem!
+  std::complex<float>* fft_data_new = reinterpret_cast<std::complex<float>*>(
+      fftwf_malloc(fft_out_width * height_out_ * sizeof(std::complex<float>)));
+  std::uninitialized_fill_n(fft_data_new, fft_out_width * height_out_,
+                            std::complex<float>(0));
+
+  size_t old_mid_x = width_in_ / 2;
+  size_t new_mid_x = width_out_ / 2;
+
+  size_t min_width = std::min(width_in_, width_out_);
+  size_t min_height = std::min(height_in_, height_out_);
+
+  size_t min_mid_x = min_width / 2;
+  size_t min_mid_y = min_height / 2;
+
+  float factor = 1.0 / (min_width * min_height);
+
+  for (size_t y = 0; y != min_height; ++y) {
+    size_t y_old = y - min_mid_y + height_in_;
+    size_t y_new = y - min_mid_y + height_out_;
+    if (y_old >= height_in_) y_old -= height_in_;
+    if (y_new >= height_out_) y_new -= height_out_;
+
+    // The last dimension is stored half
+    for (size_t x = 0; x != min_mid_x; ++x) {
+      size_t index_old = x + y_old * (old_mid_x + 1);
+      size_t index_new = x + y_new * (new_mid_x + 1);
+
+      fft_data_new[index_new] = fft_data[index_old] * factor;
+    }
+    if (width_in_ >= width_out_) {
+      size_t index_old = width_in_ / 2 + y_old * (old_mid_x + 1);
+      size_t index_new = width_out_ / 2 + y_new * (new_mid_x + 1);
+      fft_data_new[index_new] = fft_data[index_old] * factor;
+    }
+  }
+
+  fftwf_free(fft_data);
+
+  fftwf_execute_dft_c2r(f_to_out_plan_,
+                        reinterpret_cast<fftwf_complex*>(fft_data_new),
+                        task.output);
+
+  fftwf_free(fft_data_new);
+
+  if (correct_window_ && window_function_ != WindowFunction::Rectangular &&
+      !skip_window)
+    UnapplyWindow(task.output);
+}
+
+void FFTResampler::MakeWindow(aocommon::UVector<float>& data,
+                              size_t width) const {
+  if (window_function_ == WindowFunction::Tukey)
+    MakeTukeyWindow(data, width);
+  else {
+    data.resize(width);
+    for (size_t x = 0; x != width; ++x)
+      data[x] = WindowFunction::Evaluate(window_function_, width, x) + 1e-5;
+  }
+}
+
+void FFTResampler::MakeTukeyWindow(aocommon::UVector<float>& data,
+                                   size_t width) const {
+  // Make a Tukey window, which consists of
+  // left: a cosine going from 0 to 1
+  // mid: all 1
+  // right: a cosine going from 1 to 0
+  data.resize(width);
+  for (size_t x = 0; x != width; ++x) {
+    // left part of Tukey window
+    double x_sh = (0.5 + x) * 2;
+    if (x_sh < width - tukey_inset_size_) {
+      double pos = x_sh / (width - tukey_inset_size_);
+      data[x] = (std::cos((pos + 1.0) * M_PI) + 1.0) * 0.5;
+    } else if (x_sh < width + tukey_inset_size_) {
+      data[x] = 1.0;
+    } else {
+      double pos =
+          (x_sh - (width + tukey_inset_size_)) / (width - tukey_inset_size_);
+      data[x] = (std::cos(pos * M_PI) + 1.0) * 0.5;
+    }
+  }
+}
+
+void FFTResampler::ApplyWindow(float* data) const {
+  if (window_row_in_.empty()) {
+    MakeWindow(window_row_in_, width_in_);
+    MakeWindow(window_col_in_, height_in_);
+    if (correct_window_) {
+      aocommon::UVector<float> window_img_in(width_in_ * height_in_);
+      float* ptr_in = window_img_in.data();
+      for (size_t y = 0; y != height_in_; ++y) {
+        for (size_t x = 0; x != width_in_; ++x) {
+          *ptr_in = window_row_in_[x] * window_col_in_[y];
+          ++ptr_in;
+        }
+      }
+
+      window_out_.resize(width_out_ * height_out_);
+      Task task;
+      task.input = window_img_in.data();
+      task.output = window_out_.data();
+      RunSingle(task, true);
+    }
+  }
+  for (size_t y = 0; y != height_in_; ++y) {
+    for (size_t x = 0; x != width_in_; ++x) {
+      *data *= window_row_in_[x] * window_col_in_[y];
+      ++data;
+    }
+  }
+}
+
+void FFTResampler::UnapplyWindow(float* data) const {
+  size_t n = width_out_ * height_out_;
+  for (size_t i = 0; i != n; ++i) {
+    data[i] /= window_out_[i];
+  }
+}
diff --git a/cpp/common/fftresampler.h b/cpp/common/fftresampler.h
new file mode 100644
index 0000000000000000000000000000000000000000..f6627e7000b8949956d2e5ea771d58619b805f3e
--- /dev/null
+++ b/cpp/common/fftresampler.h
@@ -0,0 +1,95 @@
+#ifndef EVERYBEAM_COMMON_FFT_RESAMPLE_H_
+#define EVERYBEAM_COMMON_FFT_RESAMPLE_H_
+
+#include <aocommon/windowfunction.h>
+#include <aocommon/uvector.h>
+
+#include <vector>
+#include <thread>
+
+#include <fftw3.h>
+
+namespace everybeam {
+namespace common {
+
+/**
+ * @brief FFT resampling from (coarse) input grid to (high resolution) output
+ * grid
+ *
+ */
+class FFTResampler {
+ private:
+  struct Task {
+    float *input, *output;
+  };
+
+ public:
+  /**
+   * @brief Construct a new FFTResampler object
+   *
+   * @param width_in input image width (int)
+   * @param height_in input image height (int)
+   * @param width_out output image width (int)
+   * @param height_out output image height (int)
+   */
+  FFTResampler(size_t width_in, size_t height_in, size_t width_out,
+               size_t height_out);
+
+  ~FFTResampler();
+
+  /**
+   * @brief Do the FFT resampling
+   *
+   * @param input Input image buffer
+   * @param output Output image buffer
+   */
+  void Resample(float* input, float* output) {
+    Task task;
+    task.input = input;
+    task.output = output;
+    RunSingle(task, false);
+  }
+
+  /**
+   * Only to be used with SingleFT (it makes resampling thread unsafe!)
+   */
+  void SetTukeyWindow(double inset_size, bool correct_window) {
+    window_function_ = aocommon::WindowFunction::Tukey;
+    tukey_inset_size_ = inset_size;
+    correct_window_ = correct_window;
+    window_row_in_.clear();
+    window_col_in_.clear();
+    window_out_.clear();
+  }
+
+  void SetWindowFunction(aocommon::WindowFunction::Type window,
+                         bool correct_window) {
+    window_function_ = window;
+    correct_window_ = correct_window;
+    window_row_in_.clear();
+    window_col_in_.clear();
+    window_out_.clear();
+  }
+
+ private:
+  void RunSingle(const Task& task, bool skip_window) const;
+  void ApplyWindow(float* data) const;
+  void UnapplyWindow(float* data) const;
+  void MakeWindow(aocommon::UVector<float>& data, size_t width) const;
+  void MakeTukeyWindow(aocommon::UVector<float>& data, size_t width) const;
+
+  size_t width_in_, height_in_;
+  size_t width_out_, height_out_;
+  size_t fft_width_, fft_height_;
+  aocommon::WindowFunction::Type window_function_;
+  double tukey_inset_size_;
+  mutable aocommon::UVector<float> window_row_in_;
+  mutable aocommon::UVector<float> window_col_in_;
+  mutable aocommon::UVector<float> window_out_;
+  bool correct_window_;
+
+  fftwf_plan in_to_f_plan_, f_to_out_plan_;
+};
+}  // namespace common
+}  // namespace everybeam
+#endif  // EVERYBEAM_COMMON_FFT_RESAMPLE_H_
diff --git a/cpp/griddedresponse/dishgrid.cc b/cpp/griddedresponse/dishgrid.cc
index c5bd5a74ff2deb964c0ab67e6e57ebf9e54e114f..ce0f81291bbd04fe79ec3a33d9b9367a137997c5 100644
--- a/cpp/griddedresponse/dishgrid.cc
+++ b/cpp/griddedresponse/dishgrid.cc
@@ -4,9 +4,15 @@
 #include "../circularsymmetric/vlabeam.h"
 
 #include <aocommon/uvector.h>
-
+#include <aocommon/matrix2x2.h>
+#include <aocommon/matrix4x4.h>
+#include <aocommon/hmatrix4x4.h>
 #include <algorithm>
 
+using aocommon::HMC4x4;
+using aocommon::MC2x2;
+using aocommon::MC4x4;
+using aocommon::UVector;
 using everybeam::griddedresponse::DishGrid;
 
 void DishGrid::CalculateStation(std::complex<float>* buffer, double,
@@ -35,4 +41,56 @@ void DishGrid::CalculateAllStations(std::complex<float>* buffer, double,
     std::copy_n(buffer, width_ * height_ * 4,
                 buffer + i * width_ * height_ * 4);
   }
-}
\ No newline at end of file
+}
+
+void DishGrid::CalculateIntegratedResponse(double* buffer, double,
+                                           double frequency, size_t field_id,
+                                           size_t undersampling_factor,
+                                           const std::vector<double>&) {
+  // Copy coordinate members
+  size_t width_original = width_, height_original = height_;
+  double dl_original = dl_, dm_original = dm_;
+
+  width_ /= undersampling_factor;
+  height_ /= undersampling_factor;
+  dl_ *= (double(width_original) / double(width_));
+  dm_ *= (double(width_original) / double(width_));
+
+  // Init (Hermitian) Mueller matrix for every pixel in the coarse grid
+  size_t npixels = width_ * height_;
+  std::vector<HMC4x4> matrices(npixels, HMC4x4::Zero());
+  MakeIntegratedSnapshot(matrices, frequency, field_id);
+
+  DoFFTResampling(buffer, width_, height_, width_original, height_original,
+                  matrices);
+
+  // Reset coordinate members to original values
+  width_ = width_original;
+  height_ = height_original;
+  dl_ = dl_original;
+  dm_ = dm_original;
+};
+
+void DishGrid::MakeIntegratedSnapshot(std::vector<aocommon::HMC4x4>& matrices,
+                                      double frequency, size_t field_id) {
+  size_t nstations = telescope_->GetNrStations();
+  UVector<std::complex<float>> buffer_undersampled(
+      GetStationBufferSize(nstations));
+  // Assert that buffer size can accomodate Jones matrix on pixels
+  assert(buffer_undersampled.size() >= width_ * height_ * 4);
+
+  CalculateAllStations(buffer_undersampled.data(), 0., frequency, field_id);
+  // Loop over the pixels just once, and compute auto correlation
+  for (size_t y = 0; y != height_; ++y) {
+    for (size_t x = 0; x != width_; ++x) {
+      size_t offset = (y * width_ + x) * 4;
+      MC2x2 A(buffer_undersampled[offset], buffer_undersampled[offset + 1],
+              buffer_undersampled[offset + 2], buffer_undersampled[offset + 3]);
+      // Compute Mueller matrix and apply vec trick, see
+      // https://en.wikipedia.org/wiki/Kronecker_product#Matrix_equations
+      // No need to add (+) and average (*0.5) for auto-correlation
+      matrices[y * width_ + x] =
+          HMC4x4::KroneckerProduct(A.HermTranspose().Transpose(), A);
+    }
+  }
+};
\ No newline at end of file
diff --git a/cpp/griddedresponse/dishgrid.h b/cpp/griddedresponse/dishgrid.h
index e0a9bf5a681fbf774572c0824218b86c7140f259..7f2234e59e310326320622217eaa0a8ecc1878a1 100644
--- a/cpp/griddedresponse/dishgrid.h
+++ b/cpp/griddedresponse/dishgrid.h
@@ -41,6 +41,33 @@ class DishGrid final : public GriddedResponse {
 
   void CalculateAllStations(std::complex<float>* buffer, double time,
                             double frequency, size_t field_id) override;
+
+  virtual void CalculateIntegratedResponse(
+      double* buffer, double time, double frequency, size_t field_id,
+      size_t undersampling_factor,
+      const std::vector<double>& baseline_weights) override;
+
+  virtual void CalculateIntegratedResponse(
+      double* buffer, const std::vector<double>& time_array, double frequency,
+      size_t field_id, size_t undersampling_factor,
+      const std::vector<double>& baseline_weights) override {
+    // Time does not play a role in the integrated response of a dish telescope,
+    // so call CalculateIntegratedResponse as if it were one time step
+    CalculateIntegratedResponse(buffer, 0., frequency, field_id,
+                                undersampling_factor, std::vector<double>{0});
+  };
+
+ private:
+  /**
+   * @brief Make integrated snapshot, specialized/simplified for dish
+   * telescopes.
+   *
+   * @param matrices Vector of Mueller matrices
+   * @param frequency Frequency (Hz)
+   * @param field_id Field id
+   */
+  void MakeIntegratedSnapshot(std::vector<aocommon::HMC4x4>& matrices,
+                              double frequency, size_t field_id);
 };
 }  // namespace griddedresponse
 }  // namespace everybeam
diff --git a/cpp/griddedresponse/griddedresponse.cc b/cpp/griddedresponse/griddedresponse.cc
new file mode 100644
index 0000000000000000000000000000000000000000..cbe70aba13fde4b7d740eada10cb7a0a40625ade
--- /dev/null
+++ b/cpp/griddedresponse/griddedresponse.cc
@@ -0,0 +1,170 @@
+#include "griddedresponse.h"
+#include "../common/fftresampler.h"
+
+#include "../telescope/telescope.h"
+
+#include <aocommon/matrix2x2.h>
+#include <aocommon/matrix4x4.h>
+#include <vector>
+#include <complex>
+#include <stdexcept>
+
+using aocommon::HMC4x4;
+using aocommon::MC2x2;
+using aocommon::MC4x4;
+using aocommon::UVector;
+using everybeam::common::FFTResampler;
+using everybeam::griddedresponse::GriddedResponse;
+
+void GriddedResponse::CalculateIntegratedResponse(
+    double* buffer, double time, double frequency, size_t field_id,
+    size_t undersampling_factor, const std::vector<double>& baseline_weights) {
+  size_t nstations = telescope_->GetNrStations();
+  size_t nbaselines = nstations * (nstations + 1) / 2;
+  if (baseline_weights.size() != nbaselines) {
+    throw std::runtime_error("baseline_weights vector has incorrect size.");
+  }
+  // Scaling factor
+  double baseline_total_weight =
+      std::accumulate(baseline_weights.begin(), baseline_weights.end(), 0);
+
+  // Copy coordinate members
+  size_t width_original = width_, height_original = height_;
+  double dl_original = dl_, dm_original = dm_;
+
+  width_ /= undersampling_factor;
+  height_ /= undersampling_factor;
+  dl_ *= (double(width_original) / double(width_));
+  dm_ *= (double(width_original) / double(width_));
+
+  // Init (Hermitian) Mueller matrix for every pixel in the coarse grid
+  size_t npixels = width_ * height_;
+  std::vector<HMC4x4> matrices(npixels, HMC4x4::Zero());
+  MakeIntegratedSnapshot(matrices, time, frequency, field_id,
+                         baseline_weights.data());
+
+  for (HMC4x4& matrix : matrices) {
+    matrix /= baseline_total_weight;
+  }
+
+  DoFFTResampling(buffer, width_, height_, width_original, height_original,
+                  matrices);
+
+  // Reset coordinate members to original values
+  width_ = width_original;
+  height_ = height_original;
+  dl_ = dl_original;
+  dm_ = dm_original;
+}
+
+void GriddedResponse::CalculateIntegratedResponse(
+    double* buffer, const std::vector<double>& time_array, double frequency,
+    size_t field_id, size_t undersampling_factor,
+    const std::vector<double>& baseline_weights) {
+  size_t nstations = telescope_->GetNrStations();
+  size_t nbaselines = nstations * (nstations + 1) / 2;
+  if (baseline_weights.size() != time_array.size() * nbaselines) {
+    throw std::runtime_error("baseline_weights vector has incorrect size.");
+  }
+
+  // Scaling factor
+  double baseline_total_weight =
+      std::accumulate(baseline_weights.begin(), baseline_weights.end(), 0);
+
+  // Copy coordinate members
+  size_t width_original = width_, height_original = height_;
+  double dl_original = dl_, dm_original = dm_;
+
+  width_ /= undersampling_factor;
+  height_ /= undersampling_factor;
+  dl_ *= (double(width_original) / double(width_));
+  dm_ *= (double(width_original) / double(width_));
+
+  // Init (Hermitian) Mueller matrix for every pixel in the coarse grid
+  size_t npixels = width_ * height_;
+  std::vector<HMC4x4> matrices(npixels, HMC4x4::Zero());
+
+  for (std::size_t tstep = 0; tstep != time_array.size(); ++tstep) {
+    MakeIntegratedSnapshot(matrices, time_array[tstep], frequency, field_id,
+                           baseline_weights.data() + tstep * nbaselines);
+  }
+
+  for (HMC4x4& matrix : matrices) {
+    matrix /= baseline_total_weight;
+  }
+
+  DoFFTResampling(buffer, width_, height_, width_original, height_original,
+                  matrices);
+
+  // Reset coordinate members to original values
+  width_ = width_original;
+  height_ = height_original;
+  dl_ = dl_original;
+  dm_ = dm_original;
+}
+
+void GriddedResponse::MakeIntegratedSnapshot(
+    std::vector<HMC4x4>& matrices, double time, double frequency,
+    size_t field_id, const double* baseline_weights_interval) {
+  size_t nstations = telescope_->GetNrStations();
+  UVector<std::complex<float>> buffer_undersampled(
+      GetStationBufferSize(nstations));
+  CalculateAllStations(buffer_undersampled.data(), time, frequency, field_id);
+
+  size_t npixels = width_ * height_;
+  for (size_t y = 0; y != height_; ++y) {
+    for (size_t x = 0; x != width_; ++x) {
+      size_t index = 0;
+      HMC4x4 gain = HMC4x4::Zero();
+      for (size_t s1 = 0; s1 != nstations; ++s1) {
+        size_t offset_s1 = (s1 * npixels + y * width_ + x) * 4;
+        MC2x2 A(buffer_undersampled[offset_s1],
+                buffer_undersampled[offset_s1 + 1],
+                buffer_undersampled[offset_s1 + 2],
+                buffer_undersampled[offset_s1 + 3]);
+        for (size_t s2 = s1; s2 != nstations; ++s2) {
+          size_t offset_s2 = offset_s1 + (s2 - s1) * npixels * 4;
+          MC2x2 B(buffer_undersampled[offset_s2],
+                  buffer_undersampled[offset_s2 + 1],
+                  buffer_undersampled[offset_s2 + 2],
+                  buffer_undersampled[offset_s2 + 3]);
+
+          // Compute Mueller matrix and apply vec trick, see
+          // https://en.wikipedia.org/wiki/Kronecker_product#Matrix_equations
+          MC4x4 baseline_mueller =
+              MC4x4::KroneckerProduct(B.HermTranspose().Transpose(), A) +
+              MC4x4::KroneckerProduct(A.HermTranspose().Transpose(), B);
+
+          // Note: keep scalar factor in brackets to avoid a redundant
+          // element-wise multiplication of the HMC4x4 matrix. Factor 0.5 is to
+          // account for Kronecker delta additions.
+          gain += HMC4x4(baseline_mueller) *
+                  (0.5 * baseline_weights_interval[index]);
+          ++index;
+        }
+      }
+      matrices[y * width_ + x] += gain;
+    }
+  }
+}
+
+void GriddedResponse::DoFFTResampling(
+    double* buffer, int width_in, int height_in, int width_out, int height_out,
+    const std::vector<aocommon::HMC4x4>& matrices) {
+  // (FFT) resampling, run multi-threaded?
+  FFTResampler resampler(width_in, height_in, width_out, height_out);
+  UVector<float> lowres_input(width_in * height_in);
+  // Loop over the "double"  representation of the HMC4x4 Hermitian matrix
+  for (size_t p = 0; p != 16; ++p) {
+    for (int i = 0; i != width_in * height_in; ++i) {
+      lowres_input[i] = matrices[i].Data(p);
+    }
+    // Resample and write to the output buffer
+    UVector<float> hires_output(width_out * height_out);
+    resampler.Resample(lowres_input.data(), hires_output.data());
+    // Copy hires_output (float) to buffer (double), conversion is implicit in
+    // std::copy
+    std::copy(hires_output.begin(), hires_output.end(),
+              buffer + p * width_out * height_out);
+  }
+}
\ No newline at end of file
diff --git a/cpp/griddedresponse/griddedresponse.h b/cpp/griddedresponse/griddedresponse.h
index ce070ec3bb57ffdb748817ca265a957fade34ed9..f1dc5068638fbe95dfbcd975f213dfd69b5c6861 100644
--- a/cpp/griddedresponse/griddedresponse.h
+++ b/cpp/griddedresponse/griddedresponse.h
@@ -31,7 +31,11 @@
 #include <memory>
 #include <vector>
 #include <thread>
+
 #include <aocommon/lane.h>
+#include <aocommon/uvector.h>
+#include <aocommon/hmatrix4x4.h>
+
 #include <casacore/measures/Measures/MDirection.h>
 
 namespace everybeam {
@@ -52,7 +56,7 @@ class GriddedResponse {
    * @brief Compute the Beam response for a single station
    *
    * @param buffer Output buffer, compute and set size with
-   * GriddedResponse::GetBufferSize(1)
+   * GriddedResponse::GetStationBufferSize(1)
    * @param station_idx Station index, must be smaller than number of stations
    * in the Telescope
    * @param time Time, modified Julian date, UTC, in seconds (MJD(UTC), s).
@@ -66,15 +70,54 @@ class GriddedResponse {
    * @brief Compute the Beam response for all stations in a Telescope
    *
    * @param buffer Output buffer, compute and set size with
-   * GriddedResponse::GetBufferSize()
+   * GriddedResponse::GetStationBufferSize()
    * @param time Time, modified Julian date, UTC, in seconds (MJD(UTC), s).
    * @param frequency Frequency (Hz)
    */
   virtual void CalculateAllStations(std::complex<float>* buffer, double time,
                                     double frequency, size_t field_id) = 0;
 
-  std::size_t GetBufferSize(std::size_t nstations) const {
-    return std::size_t(nstations * width_ * height_ * 2 * 2);
+  /**
+   * @brief Calculate integrated beam for a single time step.
+   *
+   * @param buffer Buffer for storing the result, should have size width *
+   * height * 16
+   * @param time Time, modified Julian date, UTC, in seconds (MJD(UTC), s).
+   * @param frequency Frequency (Hz)
+   * @param field_id Field id
+   * @param undersampling_factor Undersampling factor
+   * @param baseline_weights Baseline weights, size should equal
+   * Telescope::GetNrStations() *  (Telescope::GetNrStations() + 1)/2
+   */
+  virtual void CalculateIntegratedResponse(
+      double* buffer, double time, double frequency, size_t field_id,
+      size_t undersampling_factor, const std::vector<double>& baseline_weights);
+
+  /**
+   * @brief Calculate integrated beam over multiple time steps.
+   *
+   * @param buffer Buffer for storing the result, should have size width *
+   * height * 16
+   * @param time_array Vector with probing times, modified Julian date, UTC, in
+   * seconds (MJD(UTC), s).
+   * @param frequency Frequency (Hz)
+   * @param field_id Field id
+   * @param undersampling_factor Undersampling factor
+   * @param baseline_weights Baseline weights, size should equal
+   * (Telescope::GetNrStations() *  (Telescope::GetNrStations() + 1)/2) *
+   * time_array.size()
+   */
+  virtual void CalculateIntegratedResponse(
+      double* buffer, const std::vector<double>& time_array, double frequency,
+      size_t field_id, size_t undersampling_factor,
+      const std::vector<double>& baseline_weights);
+
+  std::size_t GetStationBufferSize(std::size_t nstations) const {
+    return std::size_t(nstations * width_ * height_ * 4);
+  }
+
+  std::size_t GetIntegratedBufferSize() const {
+    return std::size_t(width_ * height_ * 16);
   }
 
  protected:
@@ -96,9 +139,18 @@ class GriddedResponse {
         phase_centre_dl_(coordinate_system.phase_centre_dl),
         phase_centre_dm_(coordinate_system.phase_centre_dm){};
 
+  static void DoFFTResampling(double* buffer, int width_in, int height_in,
+                              int width_out, int height_out,
+                              const std::vector<aocommon::HMC4x4>& matrices);
+
   const telescope::Telescope* const telescope_;
   size_t width_, height_;
   double ra_, dec_, dl_, dm_, phase_centre_dl_, phase_centre_dm_;
+
+ private:
+  void MakeIntegratedSnapshot(std::vector<aocommon::HMC4x4>& matrices,
+                              double time, double frequency, size_t field_id,
+                              const double* baseline_weights_interval);
 };
 }  // namespace griddedresponse
 }  // namespace everybeam
diff --git a/cpp/griddedresponse/lofargrid.cc b/cpp/griddedresponse/lofargrid.cc
index 0afee4becd9568d49b7a8c8ca3f9100c44f389e7..27dbd6078bad04dba9a84ca91c00d99bac372b66 100644
--- a/cpp/griddedresponse/lofargrid.cc
+++ b/cpp/griddedresponse/lofargrid.cc
@@ -160,7 +160,7 @@ void LOFARGrid::CalcThread(std::complex<float>* buffer, double time,
       itrf_direction[2] =
           l * l_vector_itrf_[2] + m * m_vector_itrf_[2] + n * n_vector_itrf_[2];
 
-      std::complex<float>* base_buffer = buffer + (x + job.y * height_) * 4;
+      std::complex<float>* base_buffer = buffer + (x + job.y * width_) * 4;
 
       std::complex<float>* ant_buffer_ptr =
           base_buffer + job.buffer_offset * values_per_ant;
diff --git a/cpp/griddedresponse/lofargrid.h b/cpp/griddedresponse/lofargrid.h
index 28ada8044524046b0312a0a885aa4aabecabcd65..40d41de119d8100cc9a11fea7936039e7705d3af 100644
--- a/cpp/griddedresponse/lofargrid.h
+++ b/cpp/griddedresponse/lofargrid.h
@@ -54,7 +54,7 @@ class LOFARGrid final : public GriddedResponse {
    * @brief Compute the Beam response for a single station
    *
    * @param buffer Output buffer, compute and set size with
-   * GriddedResponse::GetBufferSize(1)
+   * GriddedResponse::GetStationBufferSize(1)
    * @param stationIdx Station index, must be smaller than number of stations
    * in the Telescope
    * @param time Time, modified Julian date, UTC, in seconds (MJD(UTC), s).
@@ -68,7 +68,7 @@ class LOFARGrid final : public GriddedResponse {
    * @brief Compute the Beam response for all stations in a Telescope
    *
    * @param buffer Output buffer, compute and set size with
-   * GriddedResponse::GetBufferSize()
+   * GriddedResponse::GetStationBufferSize()
    * @param time Time, modified Julian date, UTC, in seconds (MJD(UTC), s).
    * @param freq Frequency (Hz)
    */
diff --git a/cpp/test/tlofar.cc b/cpp/test/tlofar.cc
index 1f0433eb39659ff463bd05ca25b518743e0f3a88..87f769416f6c07eb8da7208bfc086a7a7a896607 100644
--- a/cpp/test/tlofar.cc
+++ b/cpp/test/tlofar.cc
@@ -9,6 +9,7 @@
 #include "config.h"
 #include <complex>
 #include <cmath>
+#include <iostream>
 
 using everybeam::ElementResponseModel;
 using everybeam::Load;
@@ -62,7 +63,7 @@ BOOST_AUTO_TEST_CASE(load_lofar) {
 
   // Define buffer and get gridded responses
   std::vector<std::complex<float>> antenna_buffer_single(
-      grid_response->GetBufferSize(1));
+      grid_response->GetStationBufferSize(1));
   grid_response->CalculateStation(antenna_buffer_single.data(), time, frequency,
                                   23, 0);
   BOOST_CHECK_EQUAL(antenna_buffer_single.size(),
@@ -94,8 +95,9 @@ BOOST_AUTO_TEST_CASE(load_lofar) {
                 1e-4);
   }
 
+  // All stations
   std::vector<std::complex<float>> antenna_buffer_all(
-      grid_response->GetBufferSize(telescope->GetNrStations()));
+      grid_response->GetStationBufferSize(telescope->GetNrStations()));
   grid_response->CalculateAllStations(antenna_buffer_all.data(), time,
                                       frequency, 0);
   BOOST_CHECK_EQUAL(
@@ -114,7 +116,7 @@ BOOST_AUTO_TEST_CASE(load_lofar) {
       telescope_diff_beam->GetGriddedResponse(coord_system);
 
   std::vector<std::complex<float>> antenna_buffer_diff_beam(
-      grid_response_diff_beam->GetBufferSize(1));
+      grid_response_diff_beam->GetStationBufferSize(1));
   grid_response_diff_beam->CalculateStation(antenna_buffer_diff_beam.data(),
                                             time, frequency, 15, 0);
 
@@ -124,10 +126,87 @@ BOOST_AUTO_TEST_CASE(load_lofar) {
   }
   BOOST_CHECK(std::abs(norm_jones_mat - 2.) < 1e-6);
 
-  // Print to np array
-  const long unsigned leshape[] = {(long unsigned int)width, height, 2, 2};
-  npy::SaveArrayAsNumpy("lofar_station_responses.npy", false, 4, leshape,
-                        antenna_buffer_single);
+  // Primary beam tests
+  //
+  // Just check whether CalculateIntegratedResponse does run and reproduces
+  // results One time interval
+  std::vector<double> antenna_buffer_integrated(
+      grid_response->GetIntegratedBufferSize());
+  std::vector<double> baseline_weights(
+      telescope->GetNrStations() * (telescope->GetNrStations() + 1) / 2, 1.);
+  grid_response->CalculateIntegratedResponse(antenna_buffer_integrated.data(),
+                                             time, frequency, 0, 2,
+                                             baseline_weights);
+
+  // Just check whether some (rather arbitrary) numbers are reproduced
+  BOOST_CHECK(std::abs(antenna_buffer_integrated[10] - 0.0262708) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_integrated[20] - 0.127972) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_integrated.back() - 0.00847742) < 1e-6);
+
+  // Two time intervals, should give same output as single time interval
+  std::fill(antenna_buffer_integrated.begin(), antenna_buffer_integrated.end(),
+            0);
+  std::vector<double> tarray = {time, time};
+  baseline_weights.resize(baseline_weights.size() * tarray.size());
+  std::fill(baseline_weights.begin(), baseline_weights.end(), 1.);
+  grid_response->CalculateIntegratedResponse(antenna_buffer_integrated.data(),
+                                             tarray, frequency, 0, 2,
+                                             baseline_weights);
+
+  BOOST_CHECK(std::abs(antenna_buffer_integrated[10] - 0.0262708) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_integrated[20] - 0.127972) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_integrated.back() - 0.00847742) < 1e-6);
+
+  // Primary beam response on 40 x 40 grid.
+  // Validated results were obtained with the following wsclean command
+  //
+  // wsclean -size 40 40  -scale 900asec -apply-primary-beam  LOFAR_MOCK.ms
+  //
+  // where LOFAR_MOCK.ms the MS available from
+  // https://www.astron.nl/citt/EveryBeam/L258627-one-timestep.tar.bz2
+  //
+  // PLEASE NOTE: for the sake of testing, the baseline weights were set to 1
+  // in wsclean::lbeamimagemaker, i.e.
+  //
+  // --- a/lofar/lbeamimagemaker.cpp
+  // +++ b/lofar/lbeamimagemaker.cpp
+  // @@ -380,7 +380,7 @@ void LBeamImageMaker::makeBeamSnapshot(
+  //                                 MC4x4::KroneckerProduct(
+  //                                     stationGains[a1].HermTranspose().Transpose(),
+  //                                     stationGains[a2]);
+  // -          double w = weights.Value(a1, a2);
+  // +          double w = 1.;
+  //
+  std::size_t width_pb = 40, height_pb = 40;
+  CoordinateSystem coord_system_pb = {.width = width_pb,
+                                      .height = height_pb,
+                                      .ra = ra,
+                                      .dec = dec,
+                                      // 900asec
+                                      .dl = (0.25 * M_PI / 180.),
+                                      .dm = (0.25 * M_PI / 180.),
+                                      .phase_centre_dl = shift_l,
+                                      .phase_centre_dm = shift_m};
+  std::unique_ptr<GriddedResponse> grid_response_pb =
+      telescope->GetGriddedResponse(coord_system_pb);
+
+  std::vector<double> antenna_buffer_pb(
+      grid_response_pb->GetIntegratedBufferSize());
+  std::vector<double> baseline_weights_pb(
+      telescope->GetNrStations() * (telescope->GetNrStations() + 1) / 2, 1.);
+  grid_response_pb->CalculateIntegratedResponse(
+      antenna_buffer_pb.data(), time, frequency, 0, 8, baseline_weights_pb);
+  // Check diagonal and off-diagonal term in component 0 and 5 of HMC4x4
+  // representation of Mueller matrix
+  std::size_t offset_01616 = 16 * width_pb + 16,
+              offset_02310 = 23 * width_pb + 10,
+              offset_52020 = 5 * width_pb * height_pb + 20 * width_pb + 20,
+              offset_51825 = 5 * width_pb * height_pb + 18 * width_pb + 25;
+
+  BOOST_CHECK(std::abs(antenna_buffer_pb[offset_01616] - 0.020324793) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_pb[offset_02310] - 0.0059926948) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_pb[offset_52020] - 0.00018088287) < 1e-6);
+  BOOST_CHECK(std::abs(antenna_buffer_pb[offset_51825] - 0.00013052078) < 1e-6);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/cpp/test/tmwa.cc b/cpp/test/tmwa.cc
index 82840e1798d2a66ef921df83c43e87209700e70d..5a8fa5ce2b1edc25858a72dd2f06aa0ff9d73256 100644
--- a/cpp/test/tmwa.cc
+++ b/cpp/test/tmwa.cc
@@ -54,7 +54,7 @@ BOOST_AUTO_TEST_CASE(load_mwa) {
   BOOST_CHECK(nullptr != dynamic_cast<MWAGrid*>(grid_response.get()));
 
   std::vector<std::complex<float>> antenna_buffer(
-      grid_response->GetBufferSize(telescope->GetNrStations()));
+      grid_response->GetStationBufferSize(telescope->GetNrStations()));
 
   grid_response->CalculateAllStations(antenna_buffer.data(), time, frequency,
                                       0);
diff --git a/cpp/test/tvla.cc b/cpp/test/tvla.cc
index 76364a394d16161a7f795f9c0f935f3d796a9ae1..903c6374f14e9a80c0e146d9616b752bda13cc37 100644
--- a/cpp/test/tvla.cc
+++ b/cpp/test/tvla.cc
@@ -53,7 +53,7 @@ BOOST_AUTO_TEST_CASE(load_vla) {
   BOOST_CHECK(nullptr != dynamic_cast<DishGrid*>(grid_response.get()));
 
   std::vector<std::complex<float>> antenna_buffer(
-      grid_response->GetBufferSize(telescope->GetNrStations()));
+      grid_response->GetStationBufferSize(telescope->GetNrStations()));
   grid_response->CalculateAllStations(antenna_buffer.data(), time, frequency,
                                       0);
 
diff --git a/external/README.md b/external/README.md
index 18b169d710a50d55c412eccf60083686ccf4f981..bedab49b789c7059603473b5704cf4f1905f3287 100644
--- a/external/README.md
+++ b/external/README.md
@@ -3,7 +3,7 @@ External dependencies are included via git submodules, see .gitmodules in the ro
 Please note that the dependencies are fixed to a specific commit, to make sure tests do not break as a 
 result of silently updating the submodules. Dependencies are checked out on the following commits:
 
-- `aocommon`: 05917d37e32a1bcd3fc985bc775b0b7b13d12532 (https://gitlab.com/aroffringa/aocommon)
+- `aocommon`: dd3722b9c3c3b8f0eb14f75db4b0bfb80cc91a68 (https://gitlab.com/aroffringa/aocommon)
 - `eigen`: `3.3.7` (https://gitlab.com/libeigen/eigen/-/tags/3.3.7)
 - `pybind11`: `v2.5.0` (https://github.com/pybind/pybind11/releases/tag/v2.5.0)
 
diff --git a/external/aocommon b/external/aocommon
index 05917d37e32a1bcd3fc985bc775b0b7b13d12532..100ae51f7a13ea4dfc453513970eaa91ab44b987 160000
--- a/external/aocommon
+++ b/external/aocommon
@@ -1 +1 @@
-Subproject commit 05917d37e32a1bcd3fc985bc775b0b7b13d12532
+Subproject commit 100ae51f7a13ea4dfc453513970eaa91ab44b987