diff --git a/bilby/core/utils/calculus.py b/bilby/core/utils/calculus.py
index d6dbee9cd64b7d6a75142cf54c5f6ce178ac0833..eb2d26a80d5fb6b75c4cbe9dca2f4d841c678cd7 100644
--- a/bilby/core/utils/calculus.py
+++ b/bilby/core/utils/calculus.py
@@ -1,3 +1,4 @@
+import math
 from numbers import Number
 import numpy as np
 from scipy.interpolate import interp2d
@@ -250,3 +251,18 @@ class UnsortedInterp2d(interp2d):
         elif not isinstance(x, np.ndarray) and isinstance(y, np.ndarray):
             x = x * np.ones_like(y)
         return x, y
+
+
+def round_up_to_power_of_two(x):
+    """Round up to the next power of two
+
+    Parameters
+    ----------
+    x: float
+
+    Returns
+    -------
+    float: next power of two
+
+    """
+    return 2**math.ceil(np.log2(x))
diff --git a/bilby/gw/likelihood.py b/bilby/gw/likelihood.py
index b3bd6bed6008dc3ef6808d28e61b8090e05c1628..476f585ac4c4987327b095f85fefd0511f9950de 100644
--- a/bilby/gw/likelihood.py
+++ b/bilby/gw/likelihood.py
@@ -2,6 +2,7 @@
 import os
 import json
 import copy
+import math
 
 import numpy as np
 import pandas as pd
@@ -11,7 +12,8 @@ from ..core.likelihood import Likelihood
 from ..core.utils import BilbyJsonEncoder, decode_bilby_json
 from ..core.utils import (
     logger, UnsortedInterp2d, create_frequency_series, create_time_series,
-    speed_of_light, radius_of_earth)
+    speed_of_light, solar_mass, radius_of_earth, gravitational_constant,
+    round_up_to_power_of_two)
 from ..core.prior import Interped, Prior, Uniform, PriorDict, DeltaFunction
 from .detector import InterferometerList, get_empty_interferometer, calibration
 from .prior import BBHPriorDict, CBCPriorDict, Cosmological
@@ -1711,3 +1713,575 @@ def get_binary_black_hole_likelihood(interferometers):
 
 class BilbyROQParamsRangeError(Exception):
     pass
+
+
+class MBGravitationalWaveTransient(GravitationalWaveTransient):
+    """A multi-banded likelihood object
+
+    This uses the method described in S. Morisaki, 2021, arXiv: 2104.07813.
+
+    Parameters
+    ----------
+    interferometers: list, bilby.gw.detector.InterferometerList
+        A list of `bilby.detector.Interferometer` instances - contains the detector data and power spectral densities
+    waveform_generator: `bilby.waveform_generator.WaveformGenerator`
+        An object which computes the frequency-domain strain of the signal, given some set of parameters
+    reference_chirp_mass: float
+        A reference chirp mass for determining the frequency banding
+    highest_mode: int, optional
+        The maximum magnetic number of gravitational-wave moments. Default is 2
+    linear_interpolation: bool, optional
+        If True, the linear-interpolation method is used for the computation of (h, h). If False, the IFFT-FFT method
+        is used. Default is True.
+    accuracy_factor: float, optional
+        A parameter to determine the accuracy of multi-banding. The larger this factor is, the more accurate the
+        approximation is. This corresponds to L in the paper. Default is 5.
+    time_offset: float, optional
+        (end time of data) - (maximum arrival time). If None, it is inferred from the prior of geocent time.
+    delta_f_end: float, optional
+        The frequency scale with which waveforms at the high-frequency end are smoothed. If None, it is determined from
+        the prior of geocent time.
+    maximum_banding_frequency: float, optional
+        A maximum frequency for multi-banding. If specified, the low-frequency limit of a band does not exceed it.
+    minimum_banding_duration: float, optional
+        A minimum duration for multi-banding. If specified, the duration of a band is not smaller than it.
+    distance_marginalization: bool, optional
+        If true, marginalize over distance in the likelihood. This uses a look up table calculated at run time. The
+        distance prior is set to be a delta function at the minimum distance allowed in the prior being marginalised
+        over.
+    phase_marginalization: bool, optional
+        If true, marginalize over phase in the likelihood. This is done analytically using a Bessel function. The phase
+        prior is set to be a delta function at phase=0.
+    priors: dict, bilby.prior.PriorDict
+        A dictionary of priors containing at least the geocent_time prior
+    distance_marginalization_lookup_table: (dict, str), optional
+        If a dict, dictionary containing the lookup_table, distance_array, (distance) prior_array, and
+        reference_distance used to construct the table. If a string the name of a file containing these quantities. The
+        lookup table is stored after construction in either the provided string or a default location:
+        '.distance_marginalization_lookup_dmin{}_dmax{}_n{}.npz'
+    reference_frame: (str, bilby.gw.detector.InterferometerList, list), optional
+        Definition of the reference frame for the sky location.
+        - "sky": sample in RA/dec, this is the default
+        - e.g., "H1L1", ["H1", "L1"], InterferometerList(["H1", "L1"]):
+          sample in azimuth and zenith, `azimuth` and `zenith` defined in the frame where the z-axis is aligned the the
+          vector connecting H1 and L1.
+    time_reference: str, optional
+        Name of the reference for the sampled time parameter.
+        - "geocent"/"geocenter": sample in the time at the Earth's center, this is the default
+        - e.g., "H1": sample in the time of arrival at H1
+
+    Returns
+    -------
+    Likelihood: `bilby.core.likelihood.Likelihood`
+        A likelihood object, able to compute the likelihood of the data given some model parameters
+
+    """
+    def __init__(
+        self, interferometers, waveform_generator, reference_chirp_mass, highest_mode=2, linear_interpolation=True,
+        accuracy_factor=5, time_offset=None, delta_f_end=None, maximum_banding_frequency=None,
+        minimum_banding_duration=0., distance_marginalization=False, phase_marginalization=False, priors=None,
+        distance_marginalization_lookup_table=None, reference_frame="sky", time_reference="geocenter"
+    ):
+        super(MBGravitationalWaveTransient, self).__init__(
+            interferometers=interferometers, waveform_generator=waveform_generator, priors=priors,
+            distance_marginalization=distance_marginalization, phase_marginalization=phase_marginalization,
+            time_marginalization=False, distance_marginalization_lookup_table=distance_marginalization_lookup_table,
+            jitter_time=False, reference_frame=reference_frame, time_reference=time_reference
+        )
+        self.reference_chirp_mass = reference_chirp_mass
+        self.highest_mode = highest_mode
+        self.linear_interpolation = linear_interpolation
+        self.accuracy_factor = accuracy_factor
+        self.time_offset = time_offset
+        self.delta_f_end = delta_f_end
+        self.minimum_frequency = np.min([i.minimum_frequency for i in self.interferometers])
+        self.maximum_frequency = np.max([i.maximum_frequency for i in self.interferometers])
+        self.maximum_banding_frequency = maximum_banding_frequency
+        self.minimum_banding_duration = minimum_banding_duration
+        self.setup_multibanding()
+
+    @property
+    def reference_chirp_mass(self):
+        return self._reference_chirp_mass
+
+    @property
+    def reference_chirp_mass_in_second(self):
+        return gravitational_constant * self._reference_chirp_mass * solar_mass / speed_of_light**3.
+
+    @reference_chirp_mass.setter
+    def reference_chirp_mass(self, reference_chirp_mass):
+        if isinstance(reference_chirp_mass, int) or isinstance(reference_chirp_mass, float):
+            self._reference_chirp_mass = reference_chirp_mass
+        else:
+            raise TypeError("reference_chirp_mass must be a number")
+
+    @property
+    def highest_mode(self):
+        return self._highest_mode
+
+    @highest_mode.setter
+    def highest_mode(self, highest_mode):
+        if isinstance(highest_mode, int) or isinstance(highest_mode, float):
+            self._highest_mode = highest_mode
+        else:
+            raise TypeError("highest_mode must be a number")
+
+    @property
+    def linear_interpolation(self):
+        return self._linear_interpolation
+
+    @linear_interpolation.setter
+    def linear_interpolation(self, linear_interpolation):
+        if isinstance(linear_interpolation, bool):
+            self._linear_interpolation = linear_interpolation
+        else:
+            raise TypeError("linear_interpolation must be a bool")
+
+    @property
+    def accuracy_factor(self):
+        return self._accuracy_factor
+
+    @accuracy_factor.setter
+    def accuracy_factor(self, accuracy_factor):
+        if isinstance(accuracy_factor, int) or isinstance(accuracy_factor, float):
+            self._accuracy_factor = accuracy_factor
+        else:
+            raise TypeError("accuracy_factor must be a number")
+
+    @property
+    def time_offset(self):
+        return self._time_offset
+
+    @time_offset.setter
+    def time_offset(self, time_offset):
+        """
+        This sets the time offset assumed when frequency bands are constructed. The default value is (the
+        maximum offset of geocent time in the prior range) +  (light-traveling time of the Earth). If the
+        prior does not contain 'geocent_time', 2.12 seconds is used. It is calculated assuming that the
+        maximum offset of geocent time is 2.1 seconds, which is the value for the standard prior used by
+        LIGO-Virgo-KAGRA.
+        """
+        if time_offset is not None:
+            if isinstance(time_offset, int) or isinstance(time_offset, float):
+                self._time_offset = time_offset
+            else:
+                raise TypeError("time_offset must be a number")
+        elif self.priors is not None and 'geocent_time' in self.priors:
+            self._time_offset = self.interferometers.start_time + self.interferometers.duration \
+                - self.priors['geocent_time'].minimum + radius_of_earth / speed_of_light
+        else:
+            self._time_offset = 2.12
+            logger.warning("time offset can not be inferred. Use the standard time offset of {} seconds.".format(
+                self._time_offset))
+
+    @property
+    def delta_f_end(self):
+        return self._delta_f_end
+
+    @delta_f_end.setter
+    def delta_f_end(self, delta_f_end):
+        """
+        This sets the frequency scale of tapering the high-frequency end of waveform, to avoid the issues of
+        abrupt termination of waveform described in Sec. 2. F of arXiv: 2104.07813. This needs to be much
+        larger than the inverse of the minimum time offset, and the default value is 100 times of that. If
+        the prior does not contain 'geocent_time' and the minimum time offset can not be computed, 53Hz is
+        used. It is computed assuming that the minimum offset of geocent time is 1.9 seconds, which is the
+        value for the standard prior used by LIGO-Virgo-KAGRA.
+        """
+        if delta_f_end is not None:
+            if isinstance(delta_f_end, int) or isinstance(delta_f_end, float):
+                self._delta_f_end = delta_f_end
+            else:
+                raise TypeError("delta_f_end must be a number")
+        elif self.priors is not None and 'geocent_time' in self.priors:
+            self._delta_f_end = 100. / (self.interferometers.start_time + self.interferometers.duration
+                                        - self.priors['geocent_time'].maximum - radius_of_earth / speed_of_light)
+        else:
+            self._delta_f_end = 53.
+            logger.warning("delta_f_end can not be inferred. Use the standard delta_f_end of {} Hz.".format(
+                self._delta_f_end))
+
+    @property
+    def maximum_banding_frequency(self):
+        return self._maximum_banding_frequency
+
+    @maximum_banding_frequency.setter
+    def maximum_banding_frequency(self, maximum_banding_frequency):
+        """
+        This sets the upper limit on a starting frequency of a band. The default value is the frequency at
+        which f - 1 / \sqrt(- d\tau / df) starts to decrease, because the bisection search of the starting
+        frequency does not work from that frequency. The stationary phase approximation is not valid at such
+        a high frequency, which can break down the approximation. It is calculated from the 0PN formula of
+        time-to-merger \tau(f). The user-specified frequency is used if it is lower than that frequency.
+        """
+        fmax_tmp = (15. / 968.)**(3. / 5.) * (self.highest_mode / (2. * np.pi))**(8. / 5.) \
+            / self.reference_chirp_mass_in_second
+        if maximum_banding_frequency is not None:
+            if isinstance(maximum_banding_frequency, int) or isinstance(maximum_banding_frequency, float):
+                if maximum_banding_frequency < fmax_tmp:
+                    fmax_tmp = maximum_banding_frequency
+                else:
+                    logger.warning("The input maximum_banding_frequency is too large."
+                                   "It is set to be {} Hz.".format(fmax_tmp))
+            else:
+                raise TypeError("maximum_banding_frequency must be a number")
+        self._maximum_banding_frequency = fmax_tmp
+
+    @property
+    def minimum_banding_duration(self):
+        return self._minimum_banding_duration
+
+    @minimum_banding_duration.setter
+    def minimum_banding_duration(self, minimum_banding_duration):
+        if isinstance(minimum_banding_duration, int) or isinstance(minimum_banding_duration, float):
+            self._minimum_banding_duration = minimum_banding_duration
+        else:
+            raise TypeError("minimum_banding_duration must be a number")
+
+    def setup_multibanding(self):
+        """Set up frequency bands and coefficients needed for likelihood evaluations"""
+        self._setup_frequency_bands()
+        self._setup_integers()
+        self._setup_waveform_frequency_points()
+        self._setup_linear_coefficients()
+        if self.linear_interpolation:
+            self._setup_quadratic_coefficients_linear_interp()
+        else:
+            self._setup_quadratic_coefficients_ifft_fft()
+
+    def _tau(self, f):
+        """Compute time-to-merger from the input frequency. This uses the 0PN formula.
+
+        Parameters
+        ----------
+        f: float
+            input frequency
+
+        Returns
+        -------
+        tau: float
+            time-to-merger
+
+        """
+        f_22 = 2. * f / self.highest_mode
+        return 5. / 256. * self.reference_chirp_mass_in_second * \
+            (np.pi * self.reference_chirp_mass_in_second * f_22)**(-8. / 3.)
+
+    def _dtaudf(self, f):
+        """Compute the derivative of time-to-merger with respect to a starting frequency. This uses the 0PN formula.
+
+        Parameters
+        ----------
+        f: float
+            input frequency
+
+        Returns
+        -------
+        dtaudf: float
+            derivative of time-to-merger
+
+        """
+        f_22 = 2. * f / self.highest_mode
+        return -5. / 96. * self.reference_chirp_mass_in_second * \
+            (np.pi * self.reference_chirp_mass_in_second * f_22)**(-8. / 3.) / f
+
+    def _find_starting_frequency(self, duration, fnow):
+        """Find the starting frequency of the next band satisfying (10) and
+        (51) of arXiv: 2104.07813.
+
+        Parameters
+        ----------
+        duration: float
+            duration of the next band
+        fnow: float
+            starting frequency of the current band
+
+        Returns
+        -------
+        fnext: float or None
+            starting frequency of the next band. None if a frequency satisfying the conditions does not exist.
+        dfnext: float or None
+            frequency scale with which waveforms are smoothed. None if a frequency satisfying the conditions does not
+            exist.
+
+        """
+        def _is_above_fnext(f):
+            "This function returns True if f > fnext"
+            cond1 = duration - self.time_offset - self._tau(f) - \
+                self.accuracy_factor * np.sqrt(-self._dtaudf(f)) > 0.
+            cond2 = f - 1. / np.sqrt(-self._dtaudf(f)) - fnow > 0.
+            return cond1 and cond2
+        # Bisection search for fnext
+        fmin, fmax = fnow, self.maximum_banding_frequency
+        if not _is_above_fnext(fmax):
+            return None, None
+        while fmax - fmin > 1e-2 / duration:
+            f = (fmin + fmax) / 2.
+            if _is_above_fnext(f):
+                fmax = f
+            else:
+                fmin = f
+        return f, 1. / np.sqrt(-self._dtaudf(f))
+
+    def _setup_frequency_bands(self):
+        """Set up frequency bands. The durations of bands geometrically decrease T, T/2. T/4, ..., where T is the
+        original duration. This sets the following instance variables.
+
+        durations: durations of bands (T^(b) in the paper)
+        fb_dfb: the list of tuples, which contain starting frequencies (f^(b) in the paper) and frequency scales for
+        smoothing waveforms (\Delta f^(b) in the paper) of bands
+
+        """
+        self.durations = [self.interferometers.duration]
+        self.fb_dfb = [(self.minimum_frequency, 0.)]
+        dnext = self.interferometers.duration / 2
+        while dnext > max(self.time_offset, self.minimum_banding_duration):
+            fnow, _ = self.fb_dfb[-1]
+            fnext, dfnext = self._find_starting_frequency(dnext, fnow)
+            if fnext is not None and fnext < min(self.maximum_frequency, self.maximum_banding_frequency):
+                self.durations.append(dnext)
+                self.fb_dfb.append((fnext, dfnext))
+                dnext /= 2
+            else:
+                break
+        self.fb_dfb.append((self.maximum_frequency + self.delta_f_end, self.delta_f_end))
+        logger.info("The total frequency range is divided into {} bands with frequency intervals of {}.".format(
+            len(self.durations), ", ".join(["1/{} Hz".format(d) for d in self.durations])))
+
+    def _setup_integers(self):
+        """Set up integers needed for likelihood evaluations. This sets the following instance variables.
+
+        Nbs: the numbers of samples of downsampled data (N^(b) in the paper)
+        Mbs: the numbers of samples of shortened data (M^(b) in the paper)
+        Ks_Ke: start and end frequency indices of bands (K^(b)_s and K^(b)_e in the paper)
+
+        """
+        self.Nbs = []
+        self.Mbs = []
+        self.Ks_Ke = []
+        for b in range(len(self.durations)):
+            dnow = self.durations[b]
+            fnow, dfnow = self.fb_dfb[b]
+            fnext, _ = self.fb_dfb[b + 1]
+            Nb = max(round_up_to_power_of_two(2. * (fnext * self.interferometers.duration + 1.)), 2**b)
+            self.Nbs.append(Nb)
+            self.Mbs.append(Nb // 2**b)
+            self.Ks_Ke.append((math.ceil((fnow - dfnow) * dnow), math.floor(fnext * dnow)))
+
+    def _setup_waveform_frequency_points(self):
+        """Set up frequency points where waveforms are evaluated. Frequency points are reordered because some waveform
+        models raise an error if the input frequencies are not increasing. This adds frequency_points into the
+        waveform_arguments of waveform_generator. This sets the following instance variables.
+
+        banded_frequency_points: ndarray of total banded frequency points
+        start_end_idxs: list of tuples containing start and end indices of each band
+        unique_to_original_frequencies: indices converting unique frequency
+        points into the original duplicated banded frequencies
+
+        """
+        self.banded_frequency_points = np.array([])
+        self.start_end_idxs = []
+        start_idx = 0
+        for i in range(len(self.fb_dfb) - 1):
+            d = self.durations[i]
+            Ks, Ke = self.Ks_Ke[i]
+            self.banded_frequency_points = np.append(self.banded_frequency_points, np.arange(Ks, Ke + 1) / d)
+            end_idx = start_idx + Ke - Ks
+            self.start_end_idxs.append((start_idx, end_idx))
+            start_idx = end_idx + 1
+        unique_frequencies, idxs = np.unique(self.banded_frequency_points, return_inverse=True)
+        self.waveform_generator.waveform_arguments['frequencies'] = unique_frequencies
+        self.unique_to_original_frequencies = idxs
+        logger.info("The number of frequency points where waveforms are evaluated is {}.".format(
+            len(unique_frequencies)))
+        logger.info("The speed-up gain of multi-banding is {}.".format(
+            (self.maximum_frequency - self.minimum_frequency) * self.interferometers.duration /
+            len(unique_frequencies)))
+
+    def _window(self, f, b):
+        """Compute window function in the b-th band
+
+        Parameters
+        ----------
+        f: float or ndarray
+            frequency at which the window function is computed
+        b: int
+
+        Returns
+        -------
+        window: float
+            window function at f
+        """
+        fnow, dfnow = self.fb_dfb[b]
+        fnext, dfnext = self.fb_dfb[b + 1]
+
+        @np.vectorize
+        def _vectorized_window(f):
+            if fnow - dfnow < f < fnow:
+                return (1. + np.cos(np.pi * (f - fnow) / dfnow)) / 2.
+            elif fnow <= f <= fnext - dfnext:
+                return 1.
+            elif fnext - dfnext < f < fnext:
+                return (1. - np.cos(np.pi * (f - fnext) / dfnext)) / 2.
+            else:
+                return 0.
+
+        return _vectorized_window(f)
+
+    def _setup_linear_coefficients(self):
+        """Set up coefficients by which waveforms are multiplied to compute (d, h)"""
+        self.linear_coeffs = dict((ifo.name, np.array([])) for ifo in self.interferometers)
+        N = self.Nbs[-1]
+        for ifo in self.interferometers:
+            logger.info("Pre-computing linear coefficients for {}".format(ifo.name))
+            fddata = np.zeros(N // 2 + 1, dtype=complex)
+            fddata[:len(ifo.frequency_domain_strain)][ifo.frequency_mask] += \
+                ifo.frequency_domain_strain[ifo.frequency_mask] / ifo.power_spectral_density_array[ifo.frequency_mask]
+            for b in range(len(self.fb_dfb) - 1):
+                start_idx, end_idx = self.start_end_idxs[b]
+                windows = self._window(self.banded_frequency_points[start_idx:end_idx + 1], b)
+                fddata_in_ith_band = np.copy(fddata[:int(self.Nbs[b] / 2 + 1)])
+                fddata_in_ith_band[-1] = 0.  # zeroing data at the Nyquist frequency
+                tddata = np.fft.irfft(fddata_in_ith_band)[-self.Mbs[b]:]
+                Ks, Ke = self.Ks_Ke[b]
+                fddata_in_ith_band = np.fft.rfft(tddata)[Ks:Ke + 1]
+                self.linear_coeffs[ifo.name] = np.append(
+                    self.linear_coeffs[ifo.name], (4. / self.durations[b]) * windows * np.conj(fddata_in_ith_band))
+
+    def _setup_quadratic_coefficients_linear_interp(self):
+        """Set up coefficients by which the squares of waveforms are multiplied to compute (h, h) for the
+        linear-interpolation algorithm"""
+        logger.info("Linear-interpolation algorithm is used for (h, h).")
+        self.quadratic_coeffs = dict((ifo.name, np.array([])) for ifo in self.interferometers)
+        N = self.Nbs[-1]
+        for ifo in self.interferometers:
+            logger.info("Pre-computing quadratic coefficients for {}".format(ifo.name))
+            full_frequencies = np.arange(N // 2 + 1) / ifo.duration
+            full_inv_psds = np.zeros(N // 2 + 1)
+            full_inv_psds[:len(ifo.power_spectral_density_array)][ifo.frequency_mask] = \
+                1. / ifo.power_spectral_density_array[ifo.frequency_mask]
+            for i in range(len(self.fb_dfb) - 1):
+                start_idx, end_idx = self.start_end_idxs[i]
+                banded_frequencies = self.banded_frequency_points[start_idx:end_idx + 1]
+                coeffs = np.zeros(len(banded_frequencies))
+                for k in range(len(coeffs) - 1):
+                    if k == 0:
+                        start_idx_in_sum = 0
+                    else:
+                        start_idx_in_sum = math.ceil(ifo.duration * banded_frequencies[k])
+                    if k == len(coeffs) - 2:
+                        end_idx_in_sum = len(full_frequencies) - 1
+                    else:
+                        end_idx_in_sum = math.ceil(ifo.duration * banded_frequencies[k + 1]) - 1
+                    window_over_psd = full_inv_psds[start_idx_in_sum:end_idx_in_sum + 1] \
+                        * self._window(full_frequencies[start_idx_in_sum:end_idx_in_sum + 1], i)
+                    frequencies_in_sum = full_frequencies[start_idx_in_sum:end_idx_in_sum + 1]
+                    coeffs[k] += 4. * self.durations[i] / ifo.duration * np.sum(
+                        (banded_frequencies[k + 1] - frequencies_in_sum) * window_over_psd)
+                    coeffs[k + 1] += 4. * self.durations[i] / ifo.duration \
+                        * np.sum((frequencies_in_sum - banded_frequencies[k]) * window_over_psd)
+                self.quadratic_coeffs[ifo.name] = np.append(self.quadratic_coeffs[ifo.name], coeffs)
+
+    def _setup_quadratic_coefficients_ifft_fft(self):
+        """Set up coefficients needed for the IFFT-FFT algorithm to compute (h, h)"""
+        logger.info("IFFT-FFT algorithm is used for (h, h).")
+        N = self.Nbs[-1]
+        # variables defined below correspond to \hat{N}^(b), \hat{T}^(b), \tilde{I}^(b)_{c, k}, h^(b)_{c, m} and
+        # \sqrt{w^(b)(f^(b)_k)} \tilde{h}(f^(b)_k) in the paper
+        Nhatbs = [min(2 * Mb, Nb) for Mb, Nb in zip(self.Mbs, self.Nbs)]
+        self.Tbhats = [self.interferometers.duration * Nbhat / Nb for Nb, Nbhat in zip(self.Nbs, Nhatbs)]
+        self.Ibcs = dict((ifo.name, []) for ifo in self.interferometers)
+        self.hbcs = dict((ifo.name, []) for ifo in self.interferometers)
+        self.wths = dict((ifo.name, []) for ifo in self.interferometers)
+        for ifo in self.interferometers:
+            logger.info("Pre-computing quadratic coefficients for {}".format(ifo.name))
+            full_inv_psds = np.zeros(N // 2 + 1)
+            full_inv_psds[:len(ifo.power_spectral_density_array)][ifo.frequency_mask] = 1. / \
+                ifo.power_spectral_density_array[ifo.frequency_mask]
+            for b in range(len(self.fb_dfb) - 1):
+                Imb = np.fft.irfft(full_inv_psds[:self.Nbs[b] // 2 + 1])
+                half_length = Nhatbs[b] // 2
+                Imbc = np.append(Imb[:half_length + 1], Imb[-(Nhatbs[b] - half_length - 1):])
+                self.Ibcs[ifo.name].append(np.fft.rfft(Imbc))
+                # Allocate arrays for IFFT-FFT operations
+                self.hbcs[ifo.name].append(np.zeros(Nhatbs[b]))
+                self.wths[ifo.name].append(np.zeros(self.Mbs[b] // 2 + 1, dtype=complex))
+        # precompute windows and their squares
+        self.windows = np.array([])
+        self.square_root_windows = np.array([])
+        for b in range(len(self.fb_dfb) - 1):
+            start, end = self.start_end_idxs[b]
+            ws = self._window(self.banded_frequency_points[start:end + 1], b)
+            self.windows = np.append(self.windows, ws)
+            self.square_root_windows = np.append(self.square_root_windows, np.sqrt(ws))
+
+    def calculate_snrs(self, waveform_polarizations, interferometer):
+        """
+        Compute the snrs for multi-banding
+
+        Parameters
+        ----------
+        waveform_polarizations: waveform
+        interferometer: bilby.gw.detector.Interferometer
+
+        Returns
+        -------
+        snrs: named tuple of snrs
+
+        """
+        f_plus = interferometer.antenna_response(
+            self.parameters['ra'], self.parameters['dec'],
+            self.parameters['geocent_time'], self.parameters['psi'], 'plus')
+        f_cross = interferometer.antenna_response(
+            self.parameters['ra'], self.parameters['dec'],
+            self.parameters['geocent_time'], self.parameters['psi'], 'cross')
+
+        dt = interferometer.time_delay_from_geocenter(
+            self.parameters['ra'], self.parameters['dec'],
+            self.parameters['geocent_time'])
+        dt_geocent = self.parameters['geocent_time'] - interferometer.strain_data.start_time
+        ifo_time = dt_geocent + dt
+
+        calib_factor = interferometer.calibration_model.get_calibration_factor(
+            self.banded_frequency_points, prefix='recalib_{}_'.format(interferometer.name), **self.parameters)
+
+        h = f_plus * waveform_polarizations['plus'][self.unique_to_original_frequencies] \
+            + f_cross * waveform_polarizations['cross'][self.unique_to_original_frequencies]
+        h *= np.exp(-1j * 2. * np.pi * self.banded_frequency_points * ifo_time)
+        h *= np.conjugate(calib_factor)
+
+        d_inner_h = np.dot(h, self.linear_coeffs[interferometer.name])
+
+        if self.linear_interpolation:
+            optimal_snr_squared = np.vdot(np.real(h * np.conjugate(h)), self.quadratic_coeffs[interferometer.name])
+        else:
+            optimal_snr_squared = 0.
+            for b in range(len(self.fb_dfb) - 1):
+                Ks, Ke = self.Ks_Ke[b]
+                start_idx, end_idx = self.start_end_idxs[b]
+                Mb = self.Mbs[b]
+                if b == 0:
+                    optimal_snr_squared += (4. / self.interferometers.duration) * np.vdot(
+                        np.real(h[start_idx:end_idx + 1] * np.conjugate(h[start_idx:end_idx + 1])),
+                        interferometer.frequency_mask[Ks:Ke + 1] * self.windows[start_idx:end_idx + 1]
+                        / interferometer.power_spectral_density_array[Ks:Ke + 1])
+                else:
+                    self.wths[interferometer.name][b][Ks:Ke + 1] = self.square_root_windows[start_idx:end_idx + 1] \
+                        * h[start_idx:end_idx + 1]
+                    self.hbcs[interferometer.name][b][-Mb:] = np.fft.irfft(self.wths[interferometer.name][b])
+                    thbc = np.fft.rfft(self.hbcs[interferometer.name][b])
+                    optimal_snr_squared += (4. / self.Tbhats[b]) * np.vdot(
+                        np.real(thbc * np.conjugate(thbc)), self.Ibcs[interferometer.name][b])
+
+        complex_matched_filter_snr = d_inner_h / (optimal_snr_squared**0.5)
+
+        return self._CalculatedSNRs(
+            d_inner_h=d_inner_h, optimal_snr_squared=optimal_snr_squared,
+            complex_matched_filter_snr=complex_matched_filter_snr,
+            d_inner_h_squared_tc_array=None,
+            d_inner_h_array=None,
+            optimal_snr_squared_array=None)
+
+    def _rescale_signal(self, signal, new_distance):
+        for mode in signal:
+            signal[mode] *= self._ref_dist / new_distance
diff --git a/bilby/gw/source.py b/bilby/gw/source.py
index fade7acfb245830b17ba8cee50a92500d56b8983..f2aea7409211bc605a7a16a1bab709d15db9b8e3 100644
--- a/bilby/gw/source.py
+++ b/bilby/gw/source.py
@@ -542,6 +542,291 @@ def _base_roq_waveform(
     return waveform_polarizations
 
 
+def binary_black_hole_frequency_sequence(
+        frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
+        phi_12, a_2, tilt_2, phi_jl, theta_jn, phase, **kwargs):
+    """ A Binary Black Hole waveform model using lalsimulation. This generates
+    a waveform only on specified frequency points. This is useful for
+    likelihood requiring waveform values at a subset of all the frequency
+    samples. For example, this is used for MBGravitationalWaveTransient.
+
+    Parameters
+    ==========
+    frequency_array: array_like
+        The input is ignored.
+    mass_1: float
+        The mass of the heavier object in solar masses
+    mass_2: float
+        The mass of the lighter object in solar masses
+    luminosity_distance: float
+        The luminosity distance in megaparsec
+    a_1: float
+        Dimensionless primary spin magnitude
+    tilt_1: float
+        Primary tilt angle
+    phi_12: float
+        Azimuthal angle between the two component spins
+    a_2: float
+        Dimensionless secondary spin magnitude
+    tilt_2: float
+        Secondary tilt angle
+    phi_jl: float
+        Azimuthal angle between the total binary angular momentum and the
+        orbital angular momentum
+    theta_jn: float
+        Angle between the total binary angular momentum and the line of sight
+    phase: float
+        The phase at coalescence
+    kwargs: dict
+        Required keyword arguments
+        - frequencies:
+            ndarray of frequencies at which waveforms are evaluated
+
+        Optional keyword arguments
+        - waveform_approximant
+        - reference_frequency
+        - catch_waveform_errors
+        - pn_spin_order
+        - pn_tidal_order
+        - pn_phase_order
+        - pn_amplitude_order
+        - mode_array:
+          Activate a specific mode array and evaluate the model using those
+          modes only.  e.g. waveform_arguments =
+          dict(waveform_approximant='IMRPhenomHM', mode_array=[[2,2],[2,-2])
+          returns the 22 and 2-2 modes only of IMRPhenomHM.  You can only
+          specify modes that are included in that particular model.  e.g.
+          waveform_arguments = dict(waveform_approximant='IMRPhenomHM',
+          mode_array=[[2,2],[2,-2],[5,5],[5,-5]]) is not allowed because the
+          55 modes are not included in this model.  Be aware that some models
+          only take positive modes and return the positive and the negative
+          mode together, while others need to call both.  e.g.
+          waveform_arguments = dict(waveform_approximant='IMRPhenomHM',
+          mode_array=[[2,2],[4,-4]]) returns the 22 and 2-2 of IMRPhenomHM.
+          However, waveform_arguments =
+          dict(waveform_approximant='IMRPhenomXHM', mode_array=[[2,2],[4,-4]])
+          returns the 22 and 4-4 of IMRPhenomXHM.
+
+    Returns
+    =======
+    dict: A dictionary with the plus and cross polarisation strain modes
+    """
+    waveform_kwargs = dict(
+        waveform_approximant='IMRPhenomPv2', reference_frequency=50.0,
+        catch_waveform_errors=False, pn_spin_order=-1, pn_tidal_order=-1,
+        pn_phase_order=-1, pn_amplitude_order=0)
+    waveform_kwargs.update(kwargs)
+    return _base_waveform_frequency_sequence(
+        frequency_array=frequency_array, mass_1=mass_1, mass_2=mass_2,
+        luminosity_distance=luminosity_distance, theta_jn=theta_jn, phase=phase,
+        a_1=a_1, a_2=a_2, tilt_1=tilt_1, tilt_2=tilt_2, phi_jl=phi_jl,
+        phi_12=phi_12, lambda_1=0.0, lambda_2=0.0, **waveform_kwargs)
+
+
+def binary_neutron_star_frequency_sequence(
+        frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
+        phi_12, a_2, tilt_2, phi_jl, lambda_1, lambda_2, theta_jn, phase,
+        **kwargs):
+    """ A Binary Neutron Star waveform model using lalsimulation. This generates
+    a waveform only on specified frequency points. This is useful for
+    likelihood requiring waveform values at a subset of all the frequency
+    samples. For example, this is used for MBGravitationalWaveTransient.
+
+    Parameters
+    ==========
+    frequency_array: array_like
+        The input is ignored.
+    mass_1: float
+        The mass of the heavier object in solar masses
+    mass_2: float
+        The mass of the lighter object in solar masses
+    luminosity_distance: float
+        The luminosity distance in megaparsec
+    a_1: float
+        Dimensionless primary spin magnitude
+    tilt_1: float
+        Primary tilt angle
+    phi_12: float
+        Azimuthal angle between the two component spins
+    a_2: float
+        Dimensionless secondary spin magnitude
+    tilt_2: float
+        Secondary tilt angle
+    phi_jl: float
+        Azimuthal angle between the total binary angular momentum and the
+        orbital angular momentum
+    lambda_1: float
+        Dimensionless tidal deformability of mass_1
+    lambda_2: float
+        Dimensionless tidal deformability of mass_2
+    theta_jn: float
+        Angle between the total binary angular momentum and the line of sight
+    phase: float
+        The phase at coalescence
+    kwargs: dict
+        Required keyword arguments
+        - frequencies:
+            ndarray of frequencies at which waveforms are evaluated
+
+        Optional keyword arguments
+        - waveform_approximant
+        - reference_frequency
+        - catch_waveform_errors
+        - pn_spin_order
+        - pn_tidal_order
+        - pn_phase_order
+        - pn_amplitude_order
+        - mode_array:
+          Activate a specific mode array and evaluate the model using those
+          modes only.  e.g. waveform_arguments =
+          dict(waveform_approximant='IMRPhenomHM', mode_array=[[2,2],[2,-2])
+          returns the 22 and 2-2 modes only of IMRPhenomHM.  You can only
+          specify modes that are included in that particular model.  e.g.
+          waveform_arguments = dict(waveform_approximant='IMRPhenomHM',
+          mode_array=[[2,2],[2,-2],[5,5],[5,-5]]) is not allowed because the
+          55 modes are not included in this model.  Be aware that some models
+          only take positive modes and return the positive and the negative
+          mode together, while others need to call both.  e.g.
+          waveform_arguments = dict(waveform_approximant='IMRPhenomHM',
+          mode_array=[[2,2],[4,-4]]) returns the 22 and 2-2 of IMRPhenomHM.
+          However, waveform_arguments =
+          dict(waveform_approximant='IMRPhenomXHM', mode_array=[[2,2],[4,-4]])
+          returns the 22 and 4-4 of IMRPhenomXHM.
+
+    Returns
+    =======
+    dict: A dictionary with the plus and cross polarisation strain modes
+    """
+    waveform_kwargs = dict(
+        waveform_approximant='IMRPhenomPv2_NRTidal', reference_frequency=50.0,
+        catch_waveform_errors=False, pn_spin_order=-1, pn_tidal_order=-1,
+        pn_phase_order=-1, pn_amplitude_order=0)
+    waveform_kwargs.update(kwargs)
+    return _base_waveform_frequency_sequence(
+        frequency_array=frequency_array, mass_1=mass_1, mass_2=mass_2,
+        luminosity_distance=luminosity_distance, theta_jn=theta_jn, phase=phase,
+        a_1=a_1, a_2=a_2, tilt_1=tilt_1, tilt_2=tilt_2, phi_jl=phi_jl,
+        phi_12=phi_12, lambda_1=lambda_1, lambda_2=lambda_2, **waveform_kwargs)
+
+
+def _base_waveform_frequency_sequence(
+        frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
+        phi_12, a_2, tilt_2, lambda_1, lambda_2, phi_jl, theta_jn, phase,
+        **waveform_kwargs):
+    """ Generate a cbc waveform model on specified frequency samples
+
+    Parameters
+    ----------
+    frequency_array: np.array
+        This input is ignored
+    mass_1: float
+        The mass of the heavier object in solar masses
+    mass_2: float
+        The mass of the lighter object in solar masses
+    luminosity_distance: float
+        The luminosity distance in megaparsec
+    a_1: float
+        Dimensionless primary spin magnitude
+    tilt_1: float
+        Primary tilt angle
+    phi_12: float
+    a_2: float
+        Dimensionless secondary spin magnitude
+    tilt_2: float
+        Secondary tilt angle
+    phi_jl: float
+    theta_jn: float
+        Orbital inclination
+    phase: float
+        The phase at coalescence
+    waveform_kwargs: dict
+        Optional keyword arguments
+
+    Returns
+    -------
+    waveform_polarizations: dict
+        Dict containing plus and cross modes evaluated at the linear and
+        quadratic frequency nodes.
+    """
+    from lal import CreateDict
+    import lalsimulation as lalsim
+
+    frequencies = waveform_kwargs['frequencies']
+    reference_frequency = waveform_kwargs['reference_frequency']
+    approximant = lalsim_GetApproximantFromString(waveform_kwargs['waveform_approximant'])
+    catch_waveform_errors = waveform_kwargs['catch_waveform_errors']
+    pn_spin_order = waveform_kwargs['pn_spin_order']
+    pn_tidal_order = waveform_kwargs['pn_tidal_order']
+    pn_phase_order = waveform_kwargs['pn_phase_order']
+    pn_amplitude_order = waveform_kwargs['pn_amplitude_order']
+    waveform_dictionary = waveform_kwargs.get(
+        'lal_waveform_dictionary', CreateDict()
+    )
+
+    lalsim.SimInspiralWaveformParamsInsertPNSpinOrder(
+        waveform_dictionary, int(pn_spin_order))
+    lalsim.SimInspiralWaveformParamsInsertPNTidalOrder(
+        waveform_dictionary, int(pn_tidal_order))
+    lalsim.SimInspiralWaveformParamsInsertPNPhaseOrder(
+        waveform_dictionary, int(pn_phase_order))
+    lalsim.SimInspiralWaveformParamsInsertPNAmplitudeOrder(
+        waveform_dictionary, int(pn_amplitude_order))
+    lalsim_SimInspiralWaveformParamsInsertTidalLambda1(
+        waveform_dictionary, lambda_1)
+    lalsim_SimInspiralWaveformParamsInsertTidalLambda2(
+        waveform_dictionary, lambda_2)
+
+    for key, value in waveform_kwargs.items():
+        func = getattr(lalsim, "SimInspiralWaveformParamsInsert" + key, None)
+        if func is not None:
+            func(waveform_dictionary, value)
+
+    if waveform_kwargs.get('numerical_relativity_file', None) is not None:
+        lalsim.SimInspiralWaveformParamsInsertNumRelData(
+            waveform_dictionary, waveform_kwargs['numerical_relativity_file'])
+
+    if ('mode_array' in waveform_kwargs) and waveform_kwargs['mode_array'] is not None:
+        mode_array = waveform_kwargs['mode_array']
+        mode_array_lal = lalsim.SimInspiralCreateModeArray()
+        for mode in mode_array:
+            lalsim.SimInspiralModeArrayActivateMode(mode_array_lal, mode[0], mode[1])
+        lalsim.SimInspiralWaveformParamsInsertModeArray(waveform_dictionary, mode_array_lal)
+
+    luminosity_distance = luminosity_distance * 1e6 * utils.parsec
+    mass_1 = mass_1 * utils.solar_mass
+    mass_2 = mass_2 * utils.solar_mass
+
+    iota, spin_1x, spin_1y, spin_1z, spin_2x, spin_2y, spin_2z = bilby_to_lalsimulation_spins(
+        theta_jn=theta_jn, phi_jl=phi_jl, tilt_1=tilt_1, tilt_2=tilt_2,
+        phi_12=phi_12, a_1=a_1, a_2=a_2, mass_1=mass_1, mass_2=mass_2,
+        reference_frequency=reference_frequency, phase=phase)
+
+    try:
+        h_plus, h_cross = lalsim_SimInspiralChooseFDWaveformSequence(
+            phase, mass_1, mass_2, spin_1x, spin_1y, spin_1z, spin_2x, spin_2y,
+            spin_2z, reference_frequency, luminosity_distance, iota,
+            waveform_dictionary, approximant, frequencies)
+    except Exception as e:
+        if not catch_waveform_errors:
+            raise
+        else:
+            EDOM = (e.args[0] == 'Internal function call failed: Input domain error')
+            if EDOM:
+                failed_parameters = dict(mass_1=mass_1, mass_2=mass_2,
+                                         spin_1=(spin_1x, spin_2y, spin_1z),
+                                         spin_2=(spin_2x, spin_2y, spin_2z),
+                                         luminosity_distance=luminosity_distance,
+                                         iota=iota, phase=phase)
+                logger.warning("Evaluating the waveform failed with error: {}\n".format(e) +
+                               "The parameters were {}\n".format(failed_parameters) +
+                               "Likelihood will be set to -inf.")
+                return None
+            else:
+                raise
+
+    return dict(plus=h_plus.data.data, cross=h_cross.data.data)
+
+
 def sinegaussian(frequency_array, hrss, Q, frequency, **kwargs):
     tau = Q / (np.sqrt(2.0) * np.pi * frequency)
     temp = Q / (4.0 * np.sqrt(np.pi) * frequency)
diff --git a/examples/gw_examples/injection_examples/multiband_example.py b/examples/gw_examples/injection_examples/multiband_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce6c9f436319a96695d0c9337b3e69701d454bec
--- /dev/null
+++ b/examples/gw_examples/injection_examples/multiband_example.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+"""
+Example of how to use the multi-banding method (see Morisaki, (2021) arXiv:
+2104.07813) for a binary neutron star simulated signal in Gaussian noise.
+"""
+
+import numpy as np
+
+import bilby
+
+outdir = 'outdir'
+label = 'multibanding'
+
+np.random.seed(170808)
+
+minimum_frequency = 20.
+reference_frequency = 100.
+duration = 256.
+sampling_frequency = 4096.
+approximant = 'IMRPhenomD'
+injection_parameters = dict(
+    chirp_mass=1.2, mass_ratio=0.8, chi_1=0., chi_2=0.,
+    ra=3.44616, dec=-0.408084, luminosity_distance=200.,
+    theta_jn=0.4, psi=0.659, phase=1.3, geocent_time=1187008882)
+
+# inject signal
+waveform_generator = bilby.gw.WaveformGenerator(
+    duration=duration, sampling_frequency=sampling_frequency,
+    frequency_domain_source_model=bilby.gw.source.lal_binary_black_hole,
+    waveform_arguments=dict(waveform_approximant=approximant,
+                            reference_frequency=reference_frequency),
+    parameter_conversion=bilby.gw.conversion.convert_to_lal_binary_black_hole_parameters)
+ifos = bilby.gw.detector.InterferometerList(['H1', 'L1', 'V1'])
+ifos.set_strain_data_from_power_spectral_densities(
+    sampling_frequency=sampling_frequency, duration=duration,
+    start_time=injection_parameters['geocent_time'] - duration + 2.)
+ifos.inject_signal(waveform_generator=waveform_generator,
+                   parameters=injection_parameters)
+for ifo in ifos:
+    ifo.minimum_frequency = minimum_frequency
+
+# make waveform generator for likelihood evaluations
+search_waveform_generator = bilby.gw.waveform_generator.WaveformGenerator(
+    duration=duration, sampling_frequency=sampling_frequency,
+    frequency_domain_source_model=bilby.gw.source.binary_black_hole_frequency_sequence,
+    waveform_arguments=dict(waveform_approximant=approximant,
+                            reference_frequency=reference_frequency),
+    parameter_conversion=bilby.gw.conversion.convert_to_lal_binary_black_hole_parameters)
+
+# make prior
+priors = bilby.gw.prior.BNSPriorDict()
+priors['chi_1'] = 0.
+priors['chi_2'] = 0.
+priors.pop('lambda_1')
+priors.pop('lambda_2')
+priors['chirp_mass'] = bilby.core.prior.Uniform(name='chirp_mass', minimum=1.15, maximum=1.25)
+priors['geocent_time'] = bilby.core.prior.Uniform(
+    injection_parameters['geocent_time'] - 0.1,
+    injection_parameters['geocent_time'] + 0.1, latex_label='$t_c$', unit='s')
+
+# make multi-banded likelihood
+likelihood = bilby.gw.likelihood.MBGravitationalWaveTransient(
+    interferometers=ifos, waveform_generator=search_waveform_generator, priors=priors,
+    reference_chirp_mass=priors['chirp_mass'].minimum,
+    distance_marginalization=True, phase_marginalization=True
+)
+
+# sampling
+result = bilby.run_sampler(
+    likelihood=likelihood, priors=priors, sampler='dynesty',
+    nlive=500, walks=100, maxmcmc=5000, nact=5,
+    injection_parameters=injection_parameters, outdir=outdir, label=label)
+
+# Make a corner plot.
+result.plot_corner()
diff --git a/test/gw/likelihood_test.py b/test/gw/likelihood_test.py
index b4862d79b5f2aee778ed970ed44f4df867a991bf..f2c6e7ec52782a882fc3de13e732310cb5d107b5 100644
--- a/test/gw/likelihood_test.py
+++ b/test/gw/likelihood_test.py
@@ -1,4 +1,5 @@
 import unittest
+from copy import deepcopy
 import os
 
 import numpy as np
@@ -1006,5 +1007,164 @@ class TestBBHLikelihoodSetUp(unittest.TestCase):
         self.like = bilby.gw.likelihood.get_binary_black_hole_likelihood(self.ifos)
 
 
+class TestMBLikelihood(unittest.TestCase):
+    def setUp(self):
+        duration = 16
+        fmin = 20.
+        sampling_frequency = 2048.
+        self.test_parameters = dict(
+            chirp_mass=6.0,
+            mass_ratio=0.5,
+            a_1=0.0,
+            a_2=0.0,
+            tilt_1=0.0,
+            tilt_2=0.0,
+            phi_12=0.0,
+            phi_jl=0.0,
+            luminosity_distance=200.0,
+            theta_jn=0.4,
+            psi=0.659,
+            phase=1.3,
+            geocent_time=1187008882,
+            ra=1.3,
+            dec=-1.2
+        )  # Network SNR is ~50
+
+        ifos = bilby.gw.detector.InterferometerList(["H1", "L1", "V1"])
+        np.random.seed(170817)
+        ifos.set_strain_data_from_power_spectral_densities(
+            sampling_frequency=sampling_frequency, duration=duration,
+            start_time=self.test_parameters['geocent_time'] - duration + 2.
+        )
+        for ifo in ifos:
+            ifo.minimum_frequency = fmin
+
+        priors = bilby.gw.prior.BBHPriorDict()
+        priors.pop("mass_1")
+        priors.pop("mass_2")
+        priors["chirp_mass"] = bilby.core.prior.Uniform(5.5, 6.5)
+        priors["mass_ratio"] = bilby.core.prior.Uniform(0.125, 1)
+        priors["geocent_time"] = bilby.core.prior.Uniform(
+            self.test_parameters['geocent_time'] - 0.1,
+            self.test_parameters['geocent_time'] + 0.1)
+
+        approximant_22 = "IMRPhenomD"
+        approximant_homs = "IMRPhenomHM"
+        non_mb_wfg_22 = bilby.gw.WaveformGenerator(
+            duration=duration, sampling_frequency=sampling_frequency,
+            frequency_domain_source_model=bilby.gw.source.lal_binary_black_hole,
+            waveform_arguments=dict(
+                reference_frequency=fmin, minimum_frequency=fmin, approximant=approximant_22)
+        )
+        mb_wfg_22 = bilby.gw.waveform_generator.WaveformGenerator(
+            duration=duration, sampling_frequency=sampling_frequency,
+            frequency_domain_source_model=bilby.gw.source.binary_black_hole_frequency_sequence,
+            waveform_arguments=dict(
+                reference_frequency=fmin, approximant=approximant_22)
+        )
+        non_mb_wfg_homs = bilby.gw.WaveformGenerator(
+            duration=duration, sampling_frequency=sampling_frequency,
+            frequency_domain_source_model=bilby.gw.source.lal_binary_black_hole,
+            waveform_arguments=dict(
+                reference_frequency=fmin, minimum_frequency=fmin, approximant=approximant_homs)
+        )
+        mb_wfg_homs = bilby.gw.waveform_generator.WaveformGenerator(
+            duration=duration, sampling_frequency=sampling_frequency,
+            frequency_domain_source_model=bilby.gw.source.binary_black_hole_frequency_sequence,
+            waveform_arguments=dict(
+                reference_frequency=fmin, approximant=approximant_homs)
+        )
+
+        ifos_22 = deepcopy(ifos)
+        ifos_22.inject_signal(
+            parameters=self.test_parameters, waveform_generator=non_mb_wfg_22
+        )
+        ifos_homs = deepcopy(ifos)
+        ifos_homs.inject_signal(
+            parameters=self.test_parameters, waveform_generator=non_mb_wfg_homs
+        )
+
+        self.non_mb_22 = bilby.gw.likelihood.GravitationalWaveTransient(
+            interferometers=ifos_22, waveform_generator=non_mb_wfg_22
+        )
+        self.non_mb_homs = bilby.gw.likelihood.GravitationalWaveTransient(
+            interferometers=ifos_homs, waveform_generator=non_mb_wfg_homs
+        )
+
+        self.mb_22 = bilby.gw.likelihood.MBGravitationalWaveTransient(
+            interferometers=ifos_22, waveform_generator=deepcopy(mb_wfg_22),
+            reference_chirp_mass=self.test_parameters['chirp_mass'],
+            priors=priors.copy()
+        )
+        self.mb_ifftfft_22 = bilby.gw.likelihood.MBGravitationalWaveTransient(
+            interferometers=ifos_22, waveform_generator=deepcopy(mb_wfg_22),
+            reference_chirp_mass=self.test_parameters['chirp_mass'],
+            priors=priors.copy(), linear_interpolation=False
+        )
+        self.mb_homs = bilby.gw.likelihood.MBGravitationalWaveTransient(
+            interferometers=ifos_homs, waveform_generator=deepcopy(mb_wfg_homs),
+            reference_chirp_mass=self.test_parameters['chirp_mass'],
+            priors=priors.copy(), linear_interpolation=False, highest_mode=4
+        )
+        self.mb_more_accurate = bilby.gw.likelihood.MBGravitationalWaveTransient(
+            interferometers=ifos_22, waveform_generator=deepcopy(mb_wfg_22),
+            reference_chirp_mass=self.test_parameters['chirp_mass'],
+            priors=priors.copy(), accuracy_factor=50
+        )
+
+    def tearDown(self):
+        del (
+            self.non_mb_22,
+            self.non_mb_homs,
+            self.mb_22,
+            self.mb_ifftfft_22,
+            self.mb_homs,
+            self.mb_more_accurate
+        )
+
+    def test_matches_non_mb(self):
+        self.non_mb_22.parameters.update(self.test_parameters)
+        self.mb_22.parameters.update(self.test_parameters)
+        self.assertLess(
+            abs(self.non_mb_22.log_likelihood_ratio() - self.mb_22.log_likelihood_ratio()),
+            1e-2
+        )
+
+    def test_ifft_fft(self):
+        """
+        Check if multi-banding likelihood with (h, h) computed with the
+        IFFT-FFT algorithm matches the original likelihood.
+        """
+        self.non_mb_22.parameters.update(self.test_parameters)
+        self.mb_ifftfft_22.parameters.update(self.test_parameters)
+        self.assertLess(
+            abs(self.non_mb_22.log_likelihood_ratio() - self.mb_ifftfft_22.log_likelihood_ratio()),
+            5e-3
+        )
+
+    def test_homs(self):
+        """
+        Check if multi-banding likelihood matches the original likelihood for higher-order moments.
+        """
+        self.non_mb_homs.parameters.update(self.test_parameters)
+        self.mb_homs.parameters.update(self.test_parameters)
+        self.assertLess(
+            abs(self.non_mb_homs.log_likelihood_ratio() - self.mb_homs.log_likelihood_ratio()),
+            1e-3
+        )
+
+    def test_large_accuracy_factor(self):
+        """
+        Check if larger accuracy factor increases the accuracy.
+        """
+        self.non_mb_22.parameters.update(self.test_parameters)
+        self.mb_22.parameters.update(self.test_parameters)
+        self.mb_more_accurate.parameters.update(self.test_parameters)
+        self.assertLess(
+            abs(self.non_mb_22.log_likelihood_ratio() - self.mb_more_accurate.log_likelihood_ratio()),
+            abs(self.non_mb_22.log_likelihood_ratio() - self.mb_22.log_likelihood_ratio()) / 2
+        )
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/gw/source_test.py b/test/gw/source_test.py
index 24c7346d663ca1de19dd616ea10dc73f52747b8e..627278a337df155d09a82a342e3e80bc4bf6608f 100644
--- a/test/gw/source_test.py
+++ b/test/gw/source_test.py
@@ -4,6 +4,8 @@ import numpy as np
 from copy import copy
 
 import bilby
+import lal
+import lalsimulation
 
 
 class TestLalBBH(unittest.TestCase):
@@ -247,5 +249,179 @@ class TestROQBBH(unittest.TestCase):
             bilby.gw.source.binary_black_hole_roq(self.frequency_array, **self.parameters)
 
 
+class TestBBHfreqseq(unittest.TestCase):
+    def setUp(self):
+        self.parameters = dict(
+            mass_1=30.0,
+            mass_2=30.0,
+            luminosity_distance=400.0,
+            a_1=0.4,
+            tilt_1=0.,
+            phi_12=0.,
+            a_2=0.8,
+            tilt_2=0.,
+            phi_jl=0.,
+            theta_jn=0.3,
+            phase=0.0
+        )
+        minimum_frequency = 20.0
+        self.frequency_array = bilby.core.utils.create_frequency_series(2048, 8)
+        self.full_frequencies_to_sequence = self.frequency_array >= minimum_frequency
+        self.waveform_kwargs = dict(
+            waveform_approximant="IMRPhenomHM",
+            reference_frequency=50.0,
+            minimum_frequency=minimum_frequency,
+            catch_waveform_errors=True,
+            frequencies=self.frequency_array[self.full_frequencies_to_sequence]
+        )
+        self.bad_parameters = copy(self.parameters)
+        self.bad_parameters["mass_1"] = -30.0
+
+    def tearDown(self):
+        del self.parameters
+        del self.waveform_kwargs
+        del self.frequency_array
+        del self.bad_parameters
+
+    def test_valid_parameters(self):
+        self.parameters.update(self.waveform_kwargs)
+        self.assertIsInstance(
+            bilby.gw.source.binary_black_hole_frequency_sequence(
+                self.frequency_array, **self.parameters
+            ),
+            dict
+        )
+
+    def test_waveform_error_catching(self):
+        self.bad_parameters.update(self.waveform_kwargs)
+        self.assertIsNone(
+            bilby.gw.source.binary_black_hole_frequency_sequence(
+                self.frequency_array, **self.bad_parameters
+            )
+        )
+
+    def test_waveform_error_raising(self):
+        raise_error_parameters = copy(self.bad_parameters)
+        raise_error_parameters.update(self.waveform_kwargs)
+        raise_error_parameters["catch_waveform_errors"] = False
+        with self.assertRaises(Exception):
+            bilby.gw.source.binary_black_hole_frequency_sequence(
+                self.frequency_array, **raise_error_parameters
+            )
+
+    def test_match_LalBBH(self):
+        self.parameters.update(self.waveform_kwargs)
+        freqseq = bilby.gw.source.binary_black_hole_frequency_sequence(
+            self.frequency_array, **self.parameters
+        )
+        lalbbh = bilby.gw.source.lal_binary_black_hole(
+            self.frequency_array, **self.parameters
+        )
+        self.assertEqual(freqseq.keys(), lalbbh.keys())
+        for mode in freqseq:
+            diff = np.sum(np.abs(freqseq[mode] - lalbbh[mode][self.full_frequencies_to_sequence])**2.)
+            norm = np.sum(np.abs(freqseq[mode])**2.)
+            self.assertLess(diff / norm, 1e-15)
+
+    def test_match_LalBBH_specify_modes(self):
+        parameters = copy(self.parameters)
+        parameters.update(self.waveform_kwargs)
+        parameters['mode_array'] = [[2, 2]]
+        freqseq = bilby.gw.source.binary_black_hole_frequency_sequence(
+            self.frequency_array, **parameters
+        )
+        lalbbh = bilby.gw.source.lal_binary_black_hole(
+            self.frequency_array, **parameters
+        )
+        self.assertEqual(freqseq.keys(), lalbbh.keys())
+        for mode in freqseq:
+            diff = np.sum(np.abs(freqseq[mode] - lalbbh[mode][self.full_frequencies_to_sequence])**2.)
+            norm = np.sum(np.abs(freqseq[mode])**2.)
+            self.assertLess(diff / norm, 1e-15)
+
+    def test_match_LalBBH_nonGR(self):
+        parameters = copy(self.parameters)
+        parameters.update(self.waveform_kwargs)
+        wf_dict = lal.CreateDict()
+        lalsimulation.SimInspiralWaveformParamsInsertNonGRDChi0(wf_dict, 1.)
+        parameters['lal_waveform_dictionary'] = wf_dict
+        freqseq = bilby.gw.source.binary_black_hole_frequency_sequence(
+            self.frequency_array, **parameters
+        )
+        lalbbh = bilby.gw.source.lal_binary_black_hole(
+            self.frequency_array, **parameters
+        )
+        self.assertEqual(freqseq.keys(), lalbbh.keys())
+        for mode in freqseq:
+            diff = np.sum(np.abs(freqseq[mode] - lalbbh[mode][self.full_frequencies_to_sequence])**2.)
+            norm = np.sum(np.abs(freqseq[mode])**2.)
+            self.assertLess(diff / norm, 1e-15)
+
+
+class TestBNSfreqseq(unittest.TestCase):
+    def setUp(self):
+        self.parameters = dict(
+            mass_1=1.4,
+            mass_2=1.4,
+            luminosity_distance=400.0,
+            a_1=0.4,
+            a_2=0.3,
+            tilt_1=0.2,
+            tilt_2=1.7,
+            phi_jl=0.2,
+            phi_12=0.9,
+            theta_jn=1.7,
+            phase=0.0,
+            lambda_1=1000.0,
+            lambda_2=1000.0
+        )
+        minimum_frequency = 50.0
+        self.frequency_array = bilby.core.utils.create_frequency_series(2048, 16)
+        self.full_frequencies_to_sequence = self.frequency_array >= minimum_frequency
+        self.waveform_kwargs = dict(
+            waveform_approximant="IMRPhenomPv2_NRTidal",
+            reference_frequency=50.0,
+            minimum_frequency=minimum_frequency,
+            frequencies=self.frequency_array[self.full_frequencies_to_sequence]
+        )
+
+    def tearDown(self):
+        del self.parameters
+        del self.waveform_kwargs
+        del self.frequency_array
+
+    def test_with_valid_parameters(self):
+        self.parameters.update(self.waveform_kwargs)
+        self.assertIsInstance(
+            bilby.gw.source.binary_neutron_star_frequency_sequence(
+                self.frequency_array, **self.parameters
+            ),
+            dict
+        )
+
+    def test_fails_without_tidal_parameters(self):
+        self.parameters.pop("lambda_1")
+        self.parameters.pop("lambda_2")
+        self.parameters.update(self.waveform_kwargs)
+        with self.assertRaises(TypeError):
+            bilby.gw.source.binary_neutron_star_frequency_sequence(
+                self.frequency_array, **self.parameters
+            )
+
+    def test_match_LalBNS(self):
+        self.parameters.update(self.waveform_kwargs)
+        freqseq = bilby.gw.source.binary_neutron_star_frequency_sequence(
+            self.frequency_array, **self.parameters
+        )
+        lalbns = bilby.gw.source.lal_binary_neutron_star(
+            self.frequency_array, **self.parameters
+        )
+        self.assertEqual(freqseq.keys(), lalbns.keys())
+        for mode in freqseq:
+            diff = np.sum(np.abs(freqseq[mode] - lalbns[mode][self.full_frequencies_to_sequence])**2.)
+            norm = np.sum(np.abs(freqseq[mode])**2.)
+            self.assertLess(diff / norm, 1e-5)
+
+
 if __name__ == "__main__":
     unittest.main()