Skip to content

Evolve

evolve(cellsize, sx_full, sy_full, sz_full, sx2_full, sy2_full, sz2_full, covers_full_box=False, cellsize_zoom=0, sx_full_zoom=None, sy_full_zoom=None, sz_full_zoom=None, sx2_full_zoom=None, sy2_full_zoom=None, sz2_full_zoom=None, offset_zoom=None, bbox_zoom=None, ngrid_x=None, ngrid_y=None, ngrid_z=None, gridcellsize=None, ngrid_x_lpt=None, ngrid_y_lpt=None, ngrid_z_lpt=None, gridcellsize_lpt=None, Om=0.274, Ol=0.726, a_initial=0.06666666666666667, a_final=1.0, n_steps=15, ncola=-2.5, filename_npz=None, verbose=True)

Evolve a set of ICs forward in time using the COLA method.

The COLA method operates in both the spatial and temporal domains.

Parameters

cellsize : float The inter-particle spacing in Lagrangian space.

sx_full, sy_full, sz_full : array_like 3D numpy float32 arrays containing the components of the particle displacements today as calculated in the Zel'dovich Approximation in the full box.

These particles should cover the COLA volume only. If a refined
subvolume is provided, these crude particles which reside inside that
subvolume are discarded and replaced with the fine particles. These
arrays are overwritten.

sx2_full, sy2_full, sz2_full : array_like Same as above but for the second-order displacement field.

!!! covers_full_box "bool, optional" Indicates whether the COLA volume covers the full box. If True, LPT in the COLA volume is not calculated, as it matches the LPT in the full box. Default: False.

cellsize_zoom : float, optional The inter-particle spacing in Lagrangian space for the refined subvolume, if such is provided. If not, cellsize_zoom must be set to zero, as that is used as a check for the presence of a subvolume. Default: 0.

s_full_zoom, s2_full_zoom : array_like, optional Same as above, but for the refined region. Default: None.

offset_zoom : array_like, optional Array (3-vector) of floats, giving the physical coordinates of the origin of the refinement region relative to the the origin of the full box. Default: None.

bbox_zoom : array_like, optional A 3x2 array of integers of the form [[i0,i1],[j0,j1],[k0,k1]]. This gives the bounding box for the refinement region in units of the crude particles Lagrangian index. Thus, the particles with displacements sx_full|sy_full|sz_full[i0:i1,j0:j1,k0:k1] are replaced with fine particles with displacements sx_full_zoom|sy_full_zoom|sz_full_zoom.

ngrid_x, ngrid_y, ngrid_z : int, optional The size of the PM grid, which the algorithm uses to calculate the forces for the kicks. Default: None.

gridcellsize : float, optional The grid spacing of the PM grid, which the algorithm uses to calculate the forces for the kicks. Default: None.

ngrid_x_lpt, ngrid_y_lpt, ngrid_z_lpt : int, optional Same as above, but for calculating the LPT displacements in the COLA volume. These better match their counterparts above for the force calculation, as mismatches often lead to unexpected non-cancellations and artifacts. Default: None.

gridcellsize_lpt : float, optional Same as above, for the LPT displacements. Default: None.

Om, Ol : float, optional Cosmological parameters: The matter density today, :math:\Omega_m, (default: 0.274), and the Cosmological Constant density today, :math:\Omega_\Lambda (default: 1.-0.274).

a_initial : float, optional The initial scale factor from which to start the COLA evolution. This should be near 1/n_steps. Default: 1/15.

a_final : float, optional The final scale factor for the COLA evolution. Default: 1.

n_steps : int, optional The total number of timesteps that the COLA algorithm should make. Default: 15.

ncola : float, optional The spectral index for the time-domain COLA. Reasonable values lie in the range (-4, 3.5). Can't be 0 exactly, but can be near 0. See Section A.3 of [temporalCOLA]_. Default: -2.5.

filename_npz : str, optional Filename for the numpy .npz container file in which to save the snapshot and selected metadata. If None, no file will be saved. Default: None.

verbose : bool, optional Whether to print progress messages. Default: True.

Returns

px, py, pz : array_like 3D float32 arrays containing the components of the particle positions inside the COLA volume. Units are :math:\mathrm{Mpc}/h.

vx, vy, vz : array_like 3D float32 arrays containing the components of the particle velocities, :math:\bm{v}. Velocities are in units of :math:\mathrm{Mpc}/h, and are calculated according to:

.. math::
  :nowrap:

  \begin{eqnarray}
      \bm{v}\equiv \frac{1}{a\,H(a)}\frac{d\bm{x}}{d\eta}
  \end{eqnarray}

where :math:`\eta` is conformal time, :math:`a` is the final scale
factor ``a_final``, :math:`H(a)` is the Hubble parameter, and
:math:`\bm{x}` is the comoving position. This definition makes
calculating redshift-space positions trivial: one simply has to
add the line-of-sight velocity to the particle position.
Source code in pycola3/evolve.py
def evolve(
    cellsize,
    sx_full,
    sy_full,
    sz_full,
    sx2_full,
    sy2_full,
    sz2_full,
    covers_full_box=False,
    cellsize_zoom=0,
    sx_full_zoom=None,
    sy_full_zoom=None,
    sz_full_zoom=None,
    sx2_full_zoom=None,
    sy2_full_zoom=None,
    sz2_full_zoom=None,
    offset_zoom=None,
    bbox_zoom=None,
    ngrid_x=None,
    ngrid_y=None,
    ngrid_z=None,
    gridcellsize=None,
    ngrid_x_lpt=None,
    ngrid_y_lpt=None,
    ngrid_z_lpt=None,
    gridcellsize_lpt=None,
    Om=0.274,
    Ol=1.0 - 0.274,
    a_initial=1.0 / 15.0,
    a_final=1.0,
    n_steps=15,
    ncola=-2.5,
    filename_npz=None,
    verbose=True,
):
    r"""Evolve a set of ICs forward in time using the COLA method.

    The COLA method operates in both the spatial and temporal domains.

    Parameters
    ----------
    cellsize : float
        The inter-particle spacing in Lagrangian space.

    sx_full, sy_full, sz_full : array_like
        3D numpy ``float32`` arrays containing the components of the particle
        displacements today as calculated in the Zel'dovich Approximation in
        the full box.

        These particles should cover the COLA volume only. If a refined
        subvolume is provided, these crude particles which reside inside that
        subvolume are discarded and replaced with the fine particles. These
        arrays are overwritten.

    sx2_full, sy2_full, sz2_full : array_like
        Same as above but for the second-order displacement field.

    covers_full_box: bool, optional
        Indicates whether the COLA volume covers the full box. If True, LPT in
        the COLA volume is not calculated, as it matches the LPT in the full
        box. Default: False.

    cellsize_zoom : float, optional
        The inter-particle spacing in Lagrangian space for the refined
        subvolume, if such is provided. If not, ``cellsize_zoom`` must be set
        to zero, as that is used as a check for the presence of a subvolume.
        Default: 0.

    s*_full_zoom, s*2_full_zoom : array_like, optional
        Same as above, but for the refined region. Default: None.

    offset_zoom : array_like, optional
        Array (3-vector) of floats, giving the physical coordinates of the
        origin of the refinement region relative to the the origin of the full
        box. Default: None.

    bbox_zoom : array_like, optional
        A 3x2 array of integers of the form ``[[i0,i1],[j0,j1],[k0,k1]]``. This
        gives the bounding box for the refinement region in units of the crude
        particles Lagrangian index. Thus, the particles with displacements
        ``sx_full|sy_full|sz_full[i0:i1,j0:j1,k0:k1]`` are replaced with fine
        particles with displacements ``sx_full_zoom|sy_full_zoom|sz_full_zoom``.

    ngrid_x, ngrid_y, ngrid_z : int, optional
        The size of the PM grid, which the algorithm uses to calculate the
        forces for the kicks. Default: None.

    gridcellsize : float, optional
        The grid spacing of the PM grid, which the algorithm uses to calculate
        the forces for the kicks. Default: None.

    ngrid_x_lpt, ngrid_y_lpt, ngrid_z_lpt : int, optional
        Same as above, but for calculating the LPT displacements in the COLA
        volume. These better match their counterparts above for the force
        calculation, as mismatches often lead to unexpected non-cancellations
        and artifacts. Default: None.

    gridcellsize_lpt : float, optional
        Same as above, for the LPT displacements. Default: None.

    Om, Ol : float, optional
        Cosmological parameters: The matter density today, :math:`\Omega_m`,
        (default: ``0.274``), and the Cosmological Constant density today,
        :math:`\Omega_\Lambda` (default: ``1.-0.274``).

    a_initial : float, optional
        The initial scale factor from which to start the COLA evolution. This
        should be near ``1/n_steps``. Default: 1/15.

    a_final : float, optional
        The final scale factor for the COLA evolution. Default: 1.

    n_steps : int, optional
        The total number of timesteps that the COLA algorithm should make.
        Default: 15.

    ncola : float, optional
        The spectral index for the time-domain COLA. Reasonable values lie in
        the range ``(-4, 3.5)``. Can't be 0 exactly, but can be near 0. See
        Section A.3 of [temporalCOLA]_. Default: -2.5.

    filename_npz : str, optional
        Filename for the numpy ``.npz`` container file in which to save the
        snapshot and selected metadata. If ``None``, no file will be saved.
        Default: None.

    verbose : bool, optional
        Whether to print progress messages. Default: True.

    Returns
    -------
    px, py, pz : array_like
        3D float32 arrays containing the components of the particle positions
        inside the COLA volume. Units are :math:`\mathrm{Mpc}/h`.

    vx, vy, vz : array_like
        3D float32 arrays containing the components of the particle velocities,
        :math:`\bm{v}`. Velocities are in units of :math:`\mathrm{Mpc}/h`, and
        are calculated according to:

        .. math::
          :nowrap:

          \begin{eqnarray}
              \bm{v}\equiv \frac{1}{a\,H(a)}\frac{d\bm{x}}{d\eta}
          \end{eqnarray}

        where :math:`\eta` is conformal time, :math:`a` is the final scale
        factor ``a_final``, :math:`H(a)` is the Hubble parameter, and
        :math:`\bm{x}` is the comoving position. This definition makes
        calculating redshift-space positions trivial: one simply has to
        add the line-of-sight velocity to the particle position.
    """

    if cellsize_zoom == 0:
        bbox_zoom = np.array([[0, 0], [0, 0], [0, 0]], dtype="int32")
    else:
        offset_zoom = offset_zoom.astype("float32")
    offset = np.array([0.0, 0.0, 0.0], dtype="float32")

    # time-related stuff
    da = (a_final - a_initial) / float(n_steps)

    d = growth_factor_solution(Om, Ol)
    growth = interpolate.interp1d(d[:, 0].tolist(), d[:, 1].tolist(), kind="linear")
    d_growth = interpolate.interp1d(d[:, 0].tolist(), d[:, 2].tolist(), kind="linear")

    initial_growth_factor = growth(a_initial)
    initial_growth2_factor = growth_2lpt(a_initial, initial_growth_factor, Om)
    final_d_growth = d_growth(a_final)
    final_d_growth2 = d_growth2(a_final, final_d_growth, Om, Ol)
    initial_d_growth = d_growth(a_initial)
    initial_d_growth2 = d_growth2(a_initial, initial_d_growth, Om, Ol)
    del d_growth

    #############
    npart_x, npart_y, npart_z = sx_full.shape

    npart_x_zoom = None
    npart_y_zoom = None
    npart_z_zoom = None
    if cellsize_zoom != 0:
        npart_x_zoom, npart_y_zoom, npart_z_zoom = sx_full_zoom.shape

    #############

    start = time.time()

    #####################
    # Do LPT in COLA box
    #####################
    if covers_full_box:
        # if (COLA box)=(full box), then their lpt's match:
        sx = sx_full
        sy = sy_full
        sz = sz_full

        sx2 = sx2_full
        sy2 = sy2_full
        sz2 = sz2_full

        if cellsize_zoom != 0:
            sx_zoom = sx_full_zoom
            sy_zoom = sy_full_zoom
            sz_zoom = sz_full_zoom

            sx2_zoom = sx2_full_zoom
            sy2_zoom = sy2_full_zoom
            sz2_zoom = sz2_full_zoom

    else:
        # if (COLA box) != (full box), then we need the lpt in the COLA box:
        if verbose:
            print("Calculating LPT in the COLA box")
        (
            sx,
            sy,
            sz,
            sx2,
            sy2,
            sz2,
            sx_zoom,
            sy_zoom,
            sz_zoom,
            sx2_zoom,
            sy2_zoom,
            sz2_zoom,
        ) = ic_2lpt_engine(
            sx_full,
            sy_full,
            sz_full,
            cellsize,
            ngrid_x_lpt,
            ngrid_y_lpt,
            ngrid_z_lpt,
            gridcellsize_lpt,
            with_2lpt=True,
            sx2_full=sx2_full,
            sy2_full=sy2_full,
            sz2_full=sz2_full,
            cellsize_zoom=cellsize_zoom,
            bbox_zoom=bbox_zoom,
            sx_full_zoom=sx_full_zoom,
            sy_full_zoom=sy_full_zoom,
            sz_full_zoom=sz_full_zoom,
            sx2_full_zoom=sx2_full_zoom,
            sy2_full_zoom=sy2_full_zoom,
            sz2_full_zoom=sz2_full_zoom,
            offset_zoom=offset_zoom,
        )
        if verbose:
            print("    Done.")

    #######################
    # Some initializations:
    #######################
    px_zoom = py_zoom = pz_zoom = None
    vx_zoom = vy_zoom = vz_zoom = None

    # density:
    density, den_k, den_fft, phi_fft = initialize_density(ngrid_x, ngrid_y, ngrid_z)

    # positions:
    if verbose:
        print("Initializing particle positions")
    px, py, pz = initial_positions(
        sx_full,
        sy_full,
        sz_full,
        sx2_full,
        sy2_full,
        sz2_full,
        cellsize,
        initial_growth_factor,
        initial_growth2_factor,
        ngrid_x,
        ngrid_y,
        ngrid_z,
        gridcellsize,
    )
    if cellsize_zoom != 0:
        px_zoom, py_zoom, pz_zoom = initial_positions(
            sx_full_zoom,
            sy_full_zoom,
            sz_full_zoom,
            sx2_full_zoom,
            sy2_full_zoom,
            sz2_full_zoom,
            cellsize_zoom,
            initial_growth_factor,
            initial_growth2_factor,
            ngrid_x,
            ngrid_y,
            ngrid_z,
            gridcellsize,
            offset=offset_zoom,
        )
    if verbose:
        print("    Done.")

    # velocities:

    # Initial residual velocities are zero in COLA. This corresponds to the L_-
    # operator in 1301.0322. But to avoid short-scale noise, we do the
    # smoothing trick explained in the new paper. However, that smoothing
    # should not affect the IC velocities! So, first add the full vel, then
    # further down subtract the same but smoothed.
    # This smoothing is not really needed if covers_full_box=True. But that
    # case is not very interesting here, so we do it just the same.
    vx = initial_d_growth * (sx_full) + initial_d_growth2 * (sx2_full)
    vy = initial_d_growth * (sy_full) + initial_d_growth2 * (sy2_full)
    vz = initial_d_growth * (sz_full) + initial_d_growth2 * (sz2_full)

    if cellsize_zoom != 0:
        vx_zoom = initial_d_growth * (sx_full_zoom) + initial_d_growth2 * (
            sx2_full_zoom
        )
        vy_zoom = initial_d_growth * (sy_full_zoom) + initial_d_growth2 * (
            sy2_full_zoom
        )
        vz_zoom = initial_d_growth * (sz_full_zoom) + initial_d_growth2 * (
            sz2_full_zoom
        )

    if verbose:
        print("Smoothing arrays for the COLA game")

    tmp = np.zeros(sx_full.shape, dtype="float32")
    box_smooth(sx_full, tmp)
    sx_full[:] = tmp[:]
    box_smooth(sy_full, tmp)
    sy_full[:] = tmp[:]
    box_smooth(sz_full, tmp)
    sz_full[:] = tmp[:]
    box_smooth(sx2_full, tmp)
    sx2_full[:] = tmp[:]
    box_smooth(sy2_full, tmp)
    sy2_full[:] = tmp[:]
    box_smooth(sz2_full, tmp)
    sz2_full[:] = tmp[:]
    #
    box_smooth(sx, tmp)
    sx[:] = tmp[:]
    box_smooth(sy, tmp)
    sy[:] = tmp[:]
    box_smooth(sz, tmp)
    sz[:] = tmp[:]
    box_smooth(sx2, tmp)
    sx2[:] = tmp[:]
    box_smooth(sy2, tmp)
    sy2[:] = tmp[:]
    box_smooth(sz2, tmp)
    sz2[:] = tmp[:]
    del tmp
    if cellsize_zoom != 0:
        tmp = np.zeros(sx_full_zoom.shape, dtype="float32")
        box_smooth(sx_full_zoom, tmp)
        sx_full_zoom[:] = tmp[:]
        box_smooth(sy_full_zoom, tmp)
        sy_full_zoom[:] = tmp[:]
        box_smooth(sz_full_zoom, tmp)
        sz_full_zoom[:] = tmp[:]
        box_smooth(sx2_full_zoom, tmp)
        sx2_full_zoom[:] = tmp[:]
        box_smooth(sy2_full_zoom, tmp)
        sy2_full_zoom[:] = tmp[:]
        box_smooth(sz2_full_zoom, tmp)
        sz2_full_zoom[:] = tmp[:]
        #
        box_smooth(sx_zoom, tmp)
        sx_zoom[:] = tmp[:]
        box_smooth(sy_zoom, tmp)
        sy_zoom[:] = tmp[:]
        box_smooth(sz_zoom, tmp)
        sz_zoom[:] = tmp[:]
        box_smooth(sx2_zoom, tmp)
        sx2_zoom[:] = tmp[:]
        box_smooth(sy2_zoom, tmp)
        sy2_zoom[:] = tmp[:]
        box_smooth(sz2_zoom, tmp)
        sz2_zoom[:] = tmp[:]
        del tmp
    if verbose:
        print("    Done.")

    # All s* arrays are now smoothed!
    # Next subtract smoothed vels as prescribed above.
    vx -= initial_d_growth * (sx_full) + initial_d_growth2 * (sx2_full)
    vy -= initial_d_growth * (sy_full) + initial_d_growth2 * (sy2_full)
    vz -= initial_d_growth * (sz_full) + initial_d_growth2 * (sz2_full)
    if cellsize_zoom != 0:
        vx_zoom -= initial_d_growth * (sx_full_zoom) + initial_d_growth2 * (
            sx2_full_zoom
        )
        vy_zoom -= initial_d_growth * (sy_full_zoom) + initial_d_growth2 * (
            sy2_full_zoom
        )
        vz_zoom -= initial_d_growth * (sz_full_zoom) + initial_d_growth2 * (
            sz2_full_zoom
        )

    # vx = np.zeros(sx.shape,dtype='float32')
    # vy = np.zeros(sx.shape,dtype='float32')
    # vz = np.zeros(sx.shape,dtype='float32')
    # if (cellsize_zoom!=0):
    #    vx_zoom = np.zeros(sx_zoom.shape,dtype='float32')
    #    vy_zoom = np.zeros(sx_zoom.shape,dtype='float32')
    #    vz_zoom = np.zeros(sx_zoom.shape,dtype='float32')

    # scale factors:
    # initialize scale factor
    aiKick = a_initial
    aKick = a_initial
    aiDrift = a_initial
    aDrift = a_initial

    # dummy values, to initialize as global
    afKick = 0
    afDrift = 0

    dummy = 0.0  # yet another dummy

    ####################
    # DO THE TIMESTEPS
    ####################
    if verbose:
        print("Beginning evolution")

    for i in range(n_steps + 1):

        if i == 0 or i == n_steps:
            afKick = aiKick + da / 2.0
        else:
            afKick = aiKick + da

        ################
        # FORCES
        ################

        # Calculate PM density:
        density.fill(0.0)

        CICDeposit_3(
            px,
            py,
            pz,
            px,
            py,
            pz,  # dummies
            density,
            cellsize,
            gridcellsize,
            0,
            dummy,
            dummy,
            bbox_zoom,
            offset,
            1,
        )
        if cellsize_zoom != 0:
            CICDeposit_3(
                px_zoom,
                py_zoom,
                pz_zoom,
                px_zoom,
                py_zoom,
                pz_zoom,  # dummies
                density,
                cellsize_zoom,
                gridcellsize,
                0,
                dummy,
                dummy,
                np.array([[0, 0], [0, 0], [0, 0]], dtype="int32"),
                offset_zoom,
                1,
            )

        density -= 1.0

        # Calculate potential
        get_phi(
            density, den_k, den_fft, phi_fft, ngrid_x, ngrid_y, ngrid_z, gridcellsize
        )
        phi = density  # density now holds phi, so rename it

        ################
        # KICK
        ################
        beta = -1.5 * aDrift * Om * _vel_coef(aiKick, afKick, aDrift, ncola, Om, Ol)
        d = growth(aDrift)
        Om143 = (Om / (Om + (1.0 - Om) * aDrift * aDrift * aDrift)) ** (1.0 / 143.0)

        # Note that grad_phi_engine() will subtract the lpt forces in the COLA volume
        # before doing the kick.
        grad_phi_engine(
            px,
            py,
            pz,
            vx,
            vy,
            vz,
            sx,
            sy,
            sz,
            sx2,
            sy2,
            sz2,
            beta,
            beta,
            npart_x,
            npart_y,
            npart_z,
            phi,
            ngrid_x,
            ngrid_y,
            ngrid_z,
            cellsize,
            gridcellsize,
            d,
            d * d * (1.0 + 7.0 / 3.0 * Om143),
            np.array([0.0, 0.0, 0.0], dtype="float32"),
            0,
        )
        if cellsize_zoom != 0:
            grad_phi_engine(
                px_zoom,
                py_zoom,
                pz_zoom,
                vx_zoom,
                vy_zoom,
                vz_zoom,
                sx_zoom,
                sy_zoom,
                sz_zoom,
                sx2_zoom,
                sy2_zoom,
                sz2_zoom,
                beta,
                beta,
                npart_x_zoom,
                npart_y_zoom,
                npart_z_zoom,
                phi,
                ngrid_x,
                ngrid_y,
                ngrid_z,
                cellsize_zoom,
                gridcellsize,
                d,
                d * d * (1.0 + 7.0 / 3.0 * Om143),
                np.array([0.0, 0.0, 0.0], dtype="float32"),
                0,
            )

        del phi

        aKick = afKick
        aiKick = afKick

        if verbose:
            print("    Kicked to a =  %4.4f" % aKick)

        ################
        # DRIFT
        ################
        if i < n_steps:
            afDrift = aiDrift + da
            alpha = _displ_coef(aiDrift, afDrift, aKick, ncola, Om, Ol)
            gamma = 1.0 * (growth(afDrift) - growth(aiDrift))
            gamma2 = 1.0 * (
                growth_2lpt(afDrift, growth(afDrift), Om)
                - growth_2lpt(aiDrift, growth(aiDrift), Om)
            )

            # Drift, but also add the lpt displacement from the full volume:
            px += (
                vx * alpha
                + sx_full * gamma
                + sx2_full * gamma2
                + float(ngrid_x) * gridcellsize
            )
            py += (
                vy * alpha
                + sy_full * gamma
                + sy2_full * gamma2
                + float(ngrid_y) * gridcellsize
            )
            pz += (
                vz * alpha
                + sz_full * gamma
                + sz2_full * gamma2
                + float(ngrid_z) * gridcellsize
            )

            px %= float(ngrid_x) * gridcellsize
            py %= float(ngrid_y) * gridcellsize
            pz %= float(ngrid_z) * gridcellsize

            if cellsize_zoom != 0:
                px_zoom += (
                    vx_zoom * alpha
                    + sx_full_zoom * gamma
                    + sx2_full_zoom * gamma2
                    + float(ngrid_x) * gridcellsize
                )
                py_zoom += (
                    vy_zoom * alpha
                    + sy_full_zoom * gamma
                    + sy2_full_zoom * gamma2
                    + float(ngrid_y) * gridcellsize
                )
                pz_zoom += (
                    vz_zoom * alpha
                    + sz_full_zoom * gamma
                    + sz2_full_zoom * gamma2
                    + float(ngrid_z) * gridcellsize
                )

                px_zoom %= float(ngrid_x) * gridcellsize
                py_zoom %= float(ngrid_y) * gridcellsize
                pz_zoom %= float(ngrid_z) * gridcellsize

            aiDrift = afDrift
            aDrift = afDrift
            if verbose:
                print("    Drifted to a = %4.4f" % (aDrift))

    del den_k, den_fft, phi_fft, density

    # Add back LPT velocity to velocity residual
    # This corresponds to the L_+ operator in 1301.0322.
    vx += final_d_growth * (sx_full) + final_d_growth2 * (sx2_full)
    vy += final_d_growth * (sy_full) + final_d_growth2 * (sy2_full)
    vz += final_d_growth * (sz_full) + final_d_growth2 * (sz2_full)

    if cellsize_zoom != 0:
        vx_zoom += final_d_growth * (sx_full_zoom) + final_d_growth2 * (sx2_full_zoom)
        vy_zoom += final_d_growth * (sy_full_zoom) + final_d_growth2 * (sy2_full_zoom)
        vz_zoom += final_d_growth * (sz_full_zoom) + final_d_growth2 * (sz2_full_zoom)

    # Now convert velocities to
    # v_{rsd}\equiv (ds/d\eta)/(a H(a)):
    rsd_fac = a_final / _q_factor(a_final, Om, Ol)
    vx *= rsd_fac
    vy *= rsd_fac
    vz *= rsd_fac
    if cellsize_zoom != 0:
        vx_zoom *= rsd_fac
        vy_zoom *= rsd_fac
        vz_zoom *= rsd_fac

    end = time.time()
    if verbose:
        print("Time elapsed on small box (incl. IC): %3.3f sec" % (end - start))

    if filename_npz is not None:

        np.savez(
            filename_npz,
            px_zoom=px_zoom,
            py_zoom=py_zoom,
            pz_zoom=pz_zoom,
            vx_zoom=vx_zoom,
            vy_zoom=vy_zoom,
            vz_zoom=vz_zoom,
            cellsize_zoom=cellsize_zoom,
            px=px,
            py=py,
            pz=pz,
            vx=vx,
            vy=vy,
            vz=vz,
            cellsize=cellsize,
            ngrid_x=ngrid_x,
            ngrid_y=ngrid_y,
            ngrid_z=ngrid_z,
            z_final=1.0 / (aDrift) - 1.0,
            z_init=1.0 / (a_initial) - 1.0,
            n_steps=n_steps,
            Om=Om,
            Ol=Ol,
            ncola=ncola,
            ngrid_x_lpt=ngrid_x_lpt,
            ngrid_y_lpt=ngrid_y_lpt,
            ngrid_z_lpt=ngrid_z_lpt,
            gridcellsize=gridcellsize,
            gridcellsize_lpt=gridcellsize_lpt,
        )
    return px, py, pz, vx, vy, vz, px_zoom, py_zoom, pz_zoom, vx_zoom, vy_zoom, vz_zoom