diff --git a/docs/source/usage/parameters.rst b/docs/source/usage/parameters.rst
index ec87c7a29..8da703ac0 100644
--- a/docs/source/usage/parameters.rst
+++ b/docs/source/usage/parameters.rst
@@ -132,6 +132,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``drift`` for a free drift. This requires these additional parameters:
@@ -140,6 +142,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``drift_chromatic`` for a free drift, with chromatic effects included.
@@ -150,6 +154,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``drift_exact`` for a free drift, using the exact nonlinear map. This requires these additional parameters:
@@ -158,6 +164,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``quad`` for a quadrupole. This requires these additional parameters:
@@ -172,6 +180,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``quad_chromatic`` for A Quadrupole magnet, with chromatic effects included.
@@ -191,6 +201,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``quadrupole_softedge`` for a soft-edge quadrupole. This requires these additional parameters:
@@ -204,6 +216,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.mapsteps`` (``integer``) number of integration steps per slice used for map and reference particle push in applied fields
                (default: ``1``)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
@@ -222,6 +236,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``sbend`` for a bending magnet. This requires these additional parameters:
@@ -231,6 +247,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``sbend_exact`` for a bending magnet using the exact nonlinear map for the bend body. The map corresponds to the map described in:
@@ -244,6 +262,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``solenoid`` for an ideal hard-edge solenoid magnet. This requires these additional parameters:
@@ -254,6 +274,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``solenoid_softedge`` for a soft-edge solenoid. This requires these additional parameters:
@@ -272,6 +294,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.mapsteps`` (``integer``) number of integration steps per slice used for map and reference particle push in applied fields (default: ``1``)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
@@ -294,6 +318,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``rfcavity`` a radiofrequency cavity.
@@ -309,6 +335,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.mapsteps`` (``integer``) number of integration steps per slice used for map and reference particle push in applied fields (default: ``1``)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
@@ -352,6 +380,8 @@ Lattice Elements
             * ``<element_name>.dx`` (``float``, in meters) horizontal translation error
             * ``<element_name>.dy`` (``float``, in meters) vertical translation error
             * ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical)
             * ``<element_name>.nslice`` (``integer``) number of slices used for the application of space charge (default: ``1``)
 
         * ``linear_map`` for a custom, linear transport matrix.
@@ -430,8 +460,8 @@ Lattice Elements
         * ``aperture`` for a thin collimator element applying a transverse aperture boundary.
           This requires these additional parameters:
 
-            * ``<element_name>.xmax`` (``float``, in meters) maximum value of the horizontal coordinate
-            * ``<element_name>.ymax`` (``float``, in meters) maximum value of the vertical coordinate
+            * ``<element_name>.aperture_x`` (``float``, in meters) horizontal half-aperture (elliptical or rectangular)
+            * ``<element_name>.aperture_y`` (``float``, in meters) vertical half-aperture (elliptical or rectangular)
             * ``<element_name>.repeat_x`` (``float``, in meters) horizontal period for repeated aperture masking (inactive by default)
             * ``<element_name>.repeat_y`` (``float``, in meters) vertical period for repeated aperture masking (inactive by default)
             * ``<element_name>.shape`` (``string``) shape of the aperture boundary: ``rectangular`` (default) or ``elliptical``
diff --git a/docs/source/usage/python.rst b/docs/source/usage/python.rst
index 889aca74d..4c8c03b81 100644
--- a/docs/source/usage/python.rst
+++ b/docs/source/usage/python.rst
@@ -507,7 +507,7 @@ This module provides elements for the accelerator lattice.
       :param madx_file: file name to MAD-X file with beamline elements
       :param nslice: number of slices used for the application of space charge
 
-.. py:class:: impactx.elements.CFbend(ds, rc, k, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.CFbend(ds, rc, k, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A combined function bending magnet.  This is an ideal Sbend with a normal quadrupole field component.
 
@@ -520,10 +520,12 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.ConstF(ds, kx, ky, kt, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ConstF(ds, kx, ky, kt, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A linear Constant Focusing element.
 
@@ -534,6 +536,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -570,15 +574,20 @@ This module provides elements for the accelerator lattice.
    :param rotation: rotation error in the transverse plane [degrees]
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.Drift(ds, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.Drift(ds, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A drift.
 
    :param ds: Segment length in m
+   :param dx: horizontal translation error in m
+   :param dy: vertical translation error in m
+   :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.ChrDrift(ds, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ChrDrift(ds, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A drift with chromatic effects included.  The Hamiltonian is expanded
    through second order in the transverse variables (x,px,y,py), with the exact pt
@@ -588,10 +597,12 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.ExactDrift(ds, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ExactDrift(ds, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A drift using the exact nonlinear transfer map.
 
@@ -599,6 +610,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -748,7 +761,7 @@ This module provides elements for the accelerator lattice.
          This function is called for the reference particle as it passes through the element.
          The reference particle is updated *before* the beam particles are pushed.
 
-.. py:class:: impactx.elements.Quad(ds, k, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.Quad(ds, k, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A Quadrupole magnet.
 
@@ -760,10 +773,12 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.ChrQuad(ds, k, unit=0, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ChrQuad(ds, k, unit=0, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    A Quadrupole magnet, with chromatic effects included.  The Hamiltonian is expanded
    through second order in the transverse variables (x,px,y,py), with the exact pt
@@ -779,6 +794,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -790,7 +807,7 @@ This module provides elements for the accelerator lattice.
 
       unit specification for quad strength
 
-.. py:class:: impactx.elements.ChrPlasmaLens(ds, k, unit=0, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ChrPlasmaLens(ds, k, unit=0, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    An active cylindrically symmetric plasma lens, with chromatic effects included.
    The Hamiltonian is expanded through second order in the transverse variables
@@ -804,6 +821,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -815,7 +834,7 @@ This module provides elements for the accelerator lattice.
 
       unit specification for plasma lens focusing strength
 
-.. py:class:: impactx.elements.ChrAcc(ds, ez, bz, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ChrAcc(ds, ez, bz, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    Acceleration in a uniform field Ez, with a uniform solenoidal field Bz.
 
@@ -830,6 +849,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -841,7 +862,7 @@ This module provides elements for the accelerator lattice.
 
       magnetic field strength in 1/m
 
-.. py:class:: impactx.elements.RFCavity(ds, escale, freq, phase, cos_coefficients, sin_coefficients, dx=0, dy=0, rotation=0, mapsteps=1, nslice=1, name=None)
+.. py:class:: impactx.elements.RFCavity(ds, escale, freq, phase, cos_coefficients, sin_coefficients, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, mapsteps=1, nslice=1, name=None)
 
    A radiofrequency cavity.
 
@@ -856,11 +877,13 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param mapsteps: number of integration steps per slice used for map and reference particle push in applied fields
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.Sbend(ds, rc, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.Sbend(ds, rc, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    An ideal sector bend.
 
@@ -869,10 +892,12 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.ExactSbend(ds, phi, B=0.0, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.ExactSbend(ds, phi, B=0.0, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    An ideal sector bend using the exact nonlinear map.  The model consists of a uniform bending field B_y with a hard edge.  Pole faces are
    normal to the entry and exit velocity of the reference particle.
@@ -888,6 +913,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -913,7 +940,7 @@ This module provides elements for the accelerator lattice.
    :param rotation: rotation error in the transverse plane [degrees]
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.SoftSolenoid(ds, bscale, cos_coefficients, sin_coefficients, unit=0, dx=0, dy=0, rotation=0, mapsteps=1, nslice=1, name=None)
+.. py:class:: impactx.elements.SoftSolenoid(ds, bscale, cos_coefficients, sin_coefficients, unit=0, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, mapsteps=1, nslice=1, name=None)
 
    A soft-edge solenoid.
 
@@ -929,11 +956,13 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param mapsteps: number of integration steps per slice used for map and reference particle push in applied fields
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.Sol(ds, ks, dx=0, dy=0, rotation=0, nslice=1, name=None)
+.. py:class:: impactx.elements.Sol(ds, ks, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, nslice=1, name=None)
 
    An ideal hard-edge Solenoid magnet.
 
@@ -942,6 +971,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
 
@@ -963,12 +994,12 @@ This module provides elements for the accelerator lattice.
    :param rotation: rotation error in the transverse plane [degrees]
    :param name: an optional name for the element
 
-.. py:class:: impactx.elements.Aperture(xmax, ymax, shape="rectangular", dx=0, dy=0, rotation=0, name=None)
+.. py:class:: impactx.elements.Aperture(aperture_x, aperture_y, shape="rectangular", dx=0, dy=0, rotation=0, name=None)
 
    A thin collimator element, applying a transverse aperture boundary.
 
-   :param xmax: maximum allowed value of the horizontal coordinate (meter)
-   :param ymax: maximum allowed value of the vertical coordinate (meter)
+   :param aperture_x: horizontal half-aperture (rectangular or elliptical) in m
+   :param aperture_y: vertical half-aperture (rectangular or elliptical) in m
    :param repeat_x: horizontal period for repeated aperture masking (inactive by default) (meter)
    :param repeat_y: vertical period for repeated aperture masking (inactive by default) (meter)
    :param shape: aperture boundary shape: ``"rectangular"`` (default) or ``"elliptical"``
@@ -994,7 +1025,7 @@ This module provides elements for the accelerator lattice.
 
       maximum vertical coordinate
 
-.. py:class:: impactx.elements.SoftQuadrupole(ds, gscale, cos_coefficients, sin_coefficients, dx=0, dy=0, rotation=0, mapsteps=1, nslice=1, name=None)
+.. py:class:: impactx.elements.SoftQuadrupole(ds, gscale, cos_coefficients, sin_coefficients, dx=0, dy=0, rotation=0, aperture_x=0, aperture_y=0, mapsteps=1, nslice=1, name=None)
 
    A soft-edge quadrupole.
 
@@ -1007,6 +1038,8 @@ This module provides elements for the accelerator lattice.
    :param dx: horizontal translation error in m
    :param dy: vertical translation error in m
    :param rotation: rotation error in the transverse plane [degrees]
+   :param aperture_x: horizontal half-aperture (elliptical) in m
+   :param aperture_y: vertical half-aperture (elliptical) in m
    :param mapsteps: number of integration steps per slice used for map and reference particle push in applied fields
    :param nslice: number of slices used for the application of space charge
    :param name: an optional name for the element
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 93b429087..4e6137b3a 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -1110,6 +1110,21 @@ add_impactx_test(linac-segment.py
     OFF  # no plot script yet
 )
 
+# Drift with transverse aperture ################################################
+#
+# w/o space charge
+add_impactx_test(aperture-thick
+    examples/aperture/input_aperture_thick.in
+    ON   # ImpactX MPI-parallel
+    examples/aperture/analysis_aperture_thick.py
+    OFF  # no plot script yet
+)
+add_impactx_test(aperture-thick.py
+    examples/aperture/run_aperture_thick.py
+    OFF  # ImpactX MPI-parallel
+    examples/aperture/analysis_aperture_thick.py
+    OFF  # no plot script yet
+)
 
 # Iteration of a linear one-turn map #########################################
 #
diff --git a/examples/aperture/README.rst b/examples/aperture/README.rst
index edb4247c5..d57da2b1c 100644
--- a/examples/aperture/README.rst
+++ b/examples/aperture/README.rst
@@ -154,3 +154,56 @@ We run the following script to analyze correctness:
    .. literalinclude:: analysis_absorber.py
       :language: python3
       :caption: You can copy this file from ``examples/aperture/analysis_absorber.py``.
+
+
+.. _examples-aperture-thick:
+
+Aperture Collimation for a Thick Element
+=========================================
+
+Proton beam in a drift, undergoing collimation by a rectangular boundary aperture.
+
+We use a 250 MeV proton beam with a horizontal rms beam size of 1.56 mm and a vertical rms beam size of 2.21 mm.
+
+The beam is scraped by a 1 mm x 1.5 mm rectangular aperture.  For this test, the parameter nslice = 1, so application at the aperture boundary is equivalent to using a thin aperture element located at the exit of the drift.
+
+In this test, the initial values of :math:`\sigma_x`, :math:`\sigma_y`, :math:`\sigma_t`, :math:`\epsilon_x`, :math:`\epsilon_y`, and :math:`\epsilon_t` must>
+The test fails if:
+
+* any of the final coordinates for the valid (not lost) particles lie outside the aperture boundary or
+* any of the lost particles are inside the aperture boundary or
+* if the sum of lost and kept particles is not equal to the initial particles or
+* if the recorded position :math:`s` for the lost particles does not coincide with the drift distance.
+
+
+Run
+---
+
+This example can be run as a Python script (``python3 run_aperture_thick.py``) or with an app with an input file (``impactx input_aperture_thick.in``).
+Each can also be prefixed with an `MPI executor <https://www.mpi-forum.org>`__, such as ``mpiexec -n 4 ...`` or ``srun -n 4 ...``, depending on the system.
+
+.. tab-set::
+
+   .. tab-item:: Python Script
+
+       .. literalinclude:: run_aperture_thick.py
+          :language: python3
+          :caption: You can copy this file from ``examples/aperture/run_aperture_thick.py``.
+
+   .. tab-item:: App Input File
+
+       .. literalinclude:: input_aperture_thick.in
+          :language: ini
+          :caption: You can copy this file from ``examples/aperture/input_aperture_thick.in``.
+
+
+Analyze
+-------
+
+We run the following script to analyze correctness:
+
+.. dropdown:: Script ``analysis_aperture_thick.py``
+
+   .. literalinclude:: analysis_aperture_thick.py
+      :language: python3
+      :caption: You can copy this file from ``examples/aperture/analysis_aperture_thick.py``.
diff --git a/examples/aperture/analysis_aperture_thick.py b/examples/aperture/analysis_aperture_thick.py
new file mode 100755
index 000000000..702cb6bc9
--- /dev/null
+++ b/examples/aperture/analysis_aperture_thick.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+#
+# Copyright 2022-2023 ImpactX contributors
+# Authors: Axel Huebl, Chad Mitchell
+# License: BSD-3-Clause-LBNL
+#
+
+import numpy as np
+import openpmd_api as io
+from scipy.stats import moment
+
+
+def get_moments(beam):
+    """Calculate standard deviations of beam position & momenta
+    and emittance values
+
+    Returns
+    -------
+    sigx, sigy, sigt, emittance_x, emittance_y, emittance_t
+    """
+    sigx = moment(beam["position_x"], moment=2) ** 0.5  # variance -> std dev.
+    sigpx = moment(beam["momentum_x"], moment=2) ** 0.5
+    sigy = moment(beam["position_y"], moment=2) ** 0.5
+    sigpy = moment(beam["momentum_y"], moment=2) ** 0.5
+    sigt = moment(beam["position_t"], moment=2) ** 0.5
+    sigpt = moment(beam["momentum_t"], moment=2) ** 0.5
+
+    epstrms = beam.cov(ddof=0)
+    emittance_x = (sigx**2 * sigpx**2 - epstrms["position_x"]["momentum_x"] ** 2) ** 0.5
+    emittance_y = (sigy**2 * sigpy**2 - epstrms["position_y"]["momentum_y"] ** 2) ** 0.5
+    emittance_t = (sigt**2 * sigpt**2 - epstrms["position_t"]["momentum_t"] ** 2) ** 0.5
+
+    return (sigx, sigy, sigt, emittance_x, emittance_y, emittance_t)
+
+
+# initial/final beam
+series = io.Series("diags/openPMD/monitor.h5", io.Access.read_only)
+last_step = list(series.iterations)[-1]
+initial = series.iterations[1].particles["beam"].to_df()
+final = series.iterations[last_step].particles["beam"].to_df()
+
+series_lost = io.Series("diags/openPMD/particles_lost.h5", io.Access.read_only)
+particles_lost = series_lost.iterations[0].particles["beam"].to_df()
+
+# compare number of particles
+num_particles = 10000
+assert num_particles == len(initial)
+# we lost particles in apertures
+assert num_particles > len(final)
+assert num_particles == len(particles_lost) + len(final)
+
+print("Initial Beam:")
+sigx, sigy, sigt, emittance_x, emittance_y, emittance_t = get_moments(initial)
+print(f"  sigx={sigx:e} sigy={sigy:e} sigt={sigt:e}")
+print(
+    f"  emittance_x={emittance_x:e} emittance_y={emittance_y:e} emittance_t={emittance_t:e}"
+)
+
+atol = 0.0  # ignored
+rtol = 1.8 * num_particles**-0.5  # from random sampling of a smooth distribution
+print(f"  rtol={rtol} (ignored: atol~={atol})")
+
+assert np.allclose(
+    [sigx, sigy, sigt, emittance_x, emittance_y, emittance_t],
+    [
+        1.559531175539e-3,
+        2.205510139392e-3,
+        1.0e-3,
+        1.0e-6,
+        2.0e-6,
+        1.0e-6,
+    ],
+    rtol=rtol,
+    atol=atol,
+)
+
+# particle-wise comparison against the rectangular aperture boundary
+xmax = 1.0e-3
+ymax = 1.5e-3
+
+# kept particles
+dx = abs(final["position_x"]) - xmax
+dy = abs(final["position_y"]) - ymax
+
+print()
+print(f"  x_max={final['position_x'].max()}")
+print(f"  x_min={final['position_x'].min()}")
+assert np.less_equal(dx.max(), 0.0)
+
+print(f"  y_max={final['position_y'].max()}")
+print(f"  y_min={final['position_y'].min()}")
+assert np.less_equal(dy.max(), 0.0)
+
+# lost particles
+dx = abs(particles_lost["position_x"]) - xmax
+dy = abs(particles_lost["position_y"]) - ymax
+
+print()
+print(f"  x_max={particles_lost['position_x'].max()}")
+print(f"  x_min={particles_lost['position_x'].min()}")
+assert np.greater_equal(dx.max(), 0.0)
+
+print(f"  y_max={particles_lost['position_y'].max()}")
+print(f"  y_min={particles_lost['position_y'].min()}")
+assert np.greater_equal(dy.max(), 0.0)
+
+# check that s is set correctly
+lost_at_s = particles_lost["s_lost"]
+drift_s = np.ones_like(lost_at_s) * 0.123
+assert np.allclose(lost_at_s, drift_s)
diff --git a/examples/aperture/input_absorber.in b/examples/aperture/input_absorber.in
index 129229983..a564fd6ba 100644
--- a/examples/aperture/input_absorber.in
+++ b/examples/aperture/input_absorber.in
@@ -32,8 +32,8 @@ drift.ds = 0.123
 
 collimator.type = aperture
 collimator.shape = rectangular
-collimator.xmax = 1.0e-3
-collimator.ymax = 1.5e-3
+collimator.aperture_x = 1.0e-3
+collimator.aperture_y = 1.5e-3
 collimator.action = absorb
 
 ###############################################################################
diff --git a/examples/aperture/input_aperture.in b/examples/aperture/input_aperture.in
index 4a1c32a4d..41218fc79 100644
--- a/examples/aperture/input_aperture.in
+++ b/examples/aperture/input_aperture.in
@@ -32,8 +32,8 @@ drift.ds = 0.123
 
 collimator.type = aperture
 collimator.shape = rectangular
-collimator.xmax = 1.0e-3
-collimator.ymax = 1.5e-3
+collimator.aperture_x = 1.0e-3
+collimator.aperture_y = 1.5e-3
 
 
 ###############################################################################
diff --git a/examples/aperture/input_aperture_periodic.in b/examples/aperture/input_aperture_periodic.in
index eaec8f1ed..c1c707924 100644
--- a/examples/aperture/input_aperture_periodic.in
+++ b/examples/aperture/input_aperture_periodic.in
@@ -32,8 +32,8 @@ drift.ds = 0.123
 
 pepperpot.type = aperture
 pepperpot.shape = rectangular
-pepperpot.xmax = 1.5e-4
-pepperpot.ymax = 1.0e-4
+pepperpot.aperture_x = 1.5e-4
+pepperpot.aperture_y = 1.0e-4
 pepperpot.repeat_x = 1.0e-3
 pepperpot.repeat_y = 1.0e-3
 
diff --git a/examples/aperture/input_aperture_thick.in b/examples/aperture/input_aperture_thick.in
new file mode 100644
index 000000000..0921240b0
--- /dev/null
+++ b/examples/aperture/input_aperture_thick.in
@@ -0,0 +1,47 @@
+###############################################################################
+# Particle Beam(s)
+###############################################################################
+beam.npart = 10000
+beam.units = static
+beam.kin_energy = 250.0
+beam.charge = 1.0e-9
+beam.particle = proton
+beam.distribution = waterbag
+beam.lambdaX = 1.559531175539e-3
+beam.lambdaY = 2.205510139392e-3
+beam.lambdaT = 1.0e-3
+beam.lambdaPx = 6.41218345413e-4
+beam.lambdaPy = 9.06819680526e-4
+beam.lambdaPt = 1.0e-3
+beam.muxpx = 0.0
+beam.muypy = 0.0
+beam.mutpt = 0.0
+
+
+###############################################################################
+# Beamline: lattice elements and segments
+###############################################################################
+lattice.elements = monitor drift monitor
+lattice.nslice = 1
+
+monitor.type = beam_monitor
+monitor.backend = h5
+
+drift.type = drift
+drift.ds = 0.123
+drift.aperture_x = 1.0e-3
+drift.aperture_y = 1.5e-3
+
+
+###############################################################################
+# Algorithms
+###############################################################################
+algo.particle_shape = 2
+algo.space_charge = false
+
+
+###############################################################################
+# Diagnostics
+###############################################################################
+diag.slice_step_diagnostics = true
+diag.backend = h5
diff --git a/examples/aperture/run_absorber.py b/examples/aperture/run_absorber.py
index e93c46026..12f7a2c6c 100755
--- a/examples/aperture/run_absorber.py
+++ b/examples/aperture/run_absorber.py
@@ -52,8 +52,8 @@
         elements.Drift(name="drift", ds=0.123),
         elements.Aperture(
             name="collimator",
-            xmax=1.0e-3,
-            ymax=1.5e-3,
+            aperture_x=1.0e-3,
+            aperture_y=1.5e-3,
             shape="rectangular",
             action="absorb",
         ),
diff --git a/examples/aperture/run_aperture.py b/examples/aperture/run_aperture.py
index 1d186db5e..b80922639 100755
--- a/examples/aperture/run_aperture.py
+++ b/examples/aperture/run_aperture.py
@@ -51,7 +51,7 @@
         monitor,
         elements.Drift(name="drift", ds=0.123),
         elements.Aperture(
-            name="collimator", xmax=1.0e-3, ymax=1.5e-3, shape="rectangular"
+            name="collimator", aperture_x=1.0e-3, aperture_y=1.5e-3, shape="rectangular"
         ),
         monitor,
     ]
diff --git a/examples/aperture/run_aperture_periodic.py b/examples/aperture/run_aperture_periodic.py
index c0568718d..6e7cc03bf 100755
--- a/examples/aperture/run_aperture_periodic.py
+++ b/examples/aperture/run_aperture_periodic.py
@@ -52,8 +52,8 @@
         elements.Drift(name="drift", ds=0.123),
         elements.Aperture(
             name="pepperpot",
-            xmax=1.5e-4,
-            ymax=1.0e-4,
+            aperture_x=1.5e-4,
+            aperture_y=1.0e-4,
             repeat_x=1.0e-3,
             repeat_y=1.0e-3,
             shape="rectangular",
diff --git a/examples/aperture/run_aperture_thick.py b/examples/aperture/run_aperture_thick.py
new file mode 100755
index 000000000..6184c7946
--- /dev/null
+++ b/examples/aperture/run_aperture_thick.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Copyright 2022-2023 ImpactX contributors
+# Authors: Axel Huebl, Chad Mitchell
+# License: BSD-3-Clause-LBNL
+#
+# -*- coding: utf-8 -*-
+
+from impactx import ImpactX, distribution, elements
+
+sim = ImpactX()
+
+# set numerical parameters and IO control
+sim.particle_shape = 2  # B-spline order
+sim.space_charge = False
+# sim.diagnostics = False  # benchmarking
+sim.slice_step_diagnostics = True
+sim.particle_lost_diagnostics_backend = "h5"
+
+# domain decomposition & space charge mesh
+sim.init_grids()
+
+# load a 250 MeV proton beam with an initial
+# horizontal rms emittance of 1 um and an
+# initial vertical rms emittance of 2 um
+kin_energy_MeV = 250.0  # reference energy
+bunch_charge_C = 1.0e-9  # used with space charge
+npart = 10000  # number of macro particles
+
+#   reference particle
+ref = sim.particle_container().ref_particle()
+ref.set_charge_qe(1.0).set_mass_MeV(938.27208816).set_kin_energy_MeV(kin_energy_MeV)
+
+#   particle bunch
+distr = distribution.Waterbag(
+    lambdaX=1.559531175539e-3,
+    lambdaY=2.205510139392e-3,
+    lambdaT=1.0e-3,
+    lambdaPx=6.41218345413e-4,
+    lambdaPy=9.06819680526e-4,
+    lambdaPt=1.0e-3,
+)
+sim.add_particles(bunch_charge_C, distr, npart)
+
+# add beam diagnostics
+monitor = elements.BeamMonitor("monitor", backend="h5")
+
+# design the accelerator lattice
+sim.lattice.extend(
+    [
+        monitor,
+        elements.Drift(name="drift", ds=0.123, aperture_x=1.0e-3, aperture_y=1.5e-3),
+        monitor,
+    ]
+)
+
+# run simulation
+sim.track_particles()
+
+# clean shutdown
+sim.finalize()
diff --git a/src/initialization/InitElement.cpp b/src/initialization/InitElement.cpp
index da4c24c35..0d19969f3 100644
--- a/src/initialization/InitElement.cpp
+++ b/src/initialization/InitElement.cpp
@@ -96,6 +96,28 @@ namespace detail
 
         return values;
     }
+
+    /** Read the Aperture parameters aperture_x and aperture_y from inputs
+     *
+     * @param pp_element the element being read
+     * @return key-value pairs for aperture_x and aperture_y
+     */
+    std::map<std::string, amrex::ParticleReal>
+    query_aperture (amrex::ParmParse& pp_element)
+    {
+        amrex::ParticleReal aperture_x = 0;
+        amrex::ParticleReal aperture_y = 0;
+        pp_element.query("aperture_x", aperture_x);
+        pp_element.query("aperture_y", aperture_y);
+
+        std::map<std::string, amrex::ParticleReal> values = {
+                {"aperture_x", aperture_x},
+                {"aperture_y", aperture_y}
+        };
+
+        return values;
+    }
+
 } // namespace detail
 
     /** Read a lattice element
@@ -122,36 +144,40 @@ namespace detail
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal k;
             pp_element.getWithParser("k", k);
 
-            m_lattice.emplace_back( Quad(ds, k, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( Quad(ds, k, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "drift")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
-            m_lattice.emplace_back( Drift(ds, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( Drift(ds, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "sbend")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal rc;
             pp_element.getWithParser("rc", rc);
 
-            m_lattice.emplace_back( Sbend(ds, rc, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( Sbend(ds, rc, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "cfbend")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal rc, k;
             pp_element.getWithParser("rc", rc);
             pp_element.getWithParser("k", k);
 
-            m_lattice.emplace_back( CFbend(ds, rc, k, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( CFbend(ds, rc, k, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "dipedge")
         {
             auto a = detail::query_alignment(pp_element);
@@ -167,13 +193,14 @@ namespace detail
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::Real kx, ky, kt;
             pp_element.getWithParser("kx", kx);
             pp_element.getWithParser("ky", ky);
             pp_element.getWithParser("kt", kt);
 
-            m_lattice.emplace_back( ConstF(ds, kx, ky, kt, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ConstF(ds, kx, ky, kt, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "buncher")
         {
             auto a = detail::query_alignment(pp_element);
@@ -218,6 +245,7 @@ namespace detail
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal escale, freq, phase;
             int mapsteps = mapsteps_default;
@@ -231,16 +259,17 @@ namespace detail
             detail::queryAddResize(pp_element, "cos_coefficients", cos_coef);
             detail::queryAddResize(pp_element, "sin_coefficients", sin_coef);
 
-            m_lattice.emplace_back( RFCavity(ds, escale, freq, phase, cos_coef, sin_coef, a["dx"], a["dy"], a["rotation_degree"], mapsteps, nslice, element_name) );
+            m_lattice.emplace_back( RFCavity(ds, escale, freq, phase, cos_coef, sin_coef, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], mapsteps, nslice, element_name) );
         } else if (element_type == "solenoid")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal ks;
             pp_element.getWithParser("ks", ks);
 
-            m_lattice.emplace_back( Sol(ds, ks, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( Sol(ds, ks, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "prot")
         {
             amrex::ParticleReal phi_in, phi_out;
@@ -260,6 +289,7 @@ namespace detail
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal bscale;
             int mapsteps = mapsteps_default;
@@ -273,11 +303,12 @@ namespace detail
             detail::queryAddResize(pp_element, "cos_coefficients", cos_coef);
             detail::queryAddResize(pp_element, "sin_coefficients", sin_coef);
 
-            m_lattice.emplace_back( SoftSolenoid(ds, bscale, cos_coef, sin_coef, units, a["dx"], a["dy"], a["rotation_degree"], mapsteps, nslice, element_name) );
+            m_lattice.emplace_back( SoftSolenoid(ds, bscale, cos_coef, sin_coef, units, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], mapsteps, nslice, element_name) );
         } else if (element_type == "quadrupole_softedge")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal gscale;
             int mapsteps = mapsteps_default;
@@ -289,16 +320,18 @@ namespace detail
             detail::queryAddResize(pp_element, "cos_coefficients", cos_coef);
             detail::queryAddResize(pp_element, "sin_coefficients", sin_coef);
 
-            m_lattice.emplace_back( SoftQuadrupole(ds, gscale, cos_coef, sin_coef, a["dx"], a["dy"], a["rotation_degree"], mapsteps, nslice, element_name) );
+            m_lattice.emplace_back( SoftQuadrupole(ds, gscale, cos_coef, sin_coef, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], mapsteps, nslice, element_name) );
         } else if (element_type == "drift_chromatic")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
-            m_lattice.emplace_back( ChrDrift(ds, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ChrDrift(ds, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "quad_chromatic")
         {
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
 
             amrex::ParticleReal k;
@@ -306,10 +339,11 @@ namespace detail
             pp_element.getWithParser("k", k);
             pp_element.queryAddWithParser("units", units);
 
-            m_lattice.emplace_back( ChrQuad(ds, k, units, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ChrQuad(ds, k, units, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "plasma_lens_chromatic")
         {
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
 
             amrex::ParticleReal k;
@@ -317,7 +351,7 @@ namespace detail
             pp_element.getWithParser("k", k);
             pp_element.queryAddWithParser("units", units);
 
-            m_lattice.emplace_back( ChrPlasmaLens(ds, k, units, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ChrPlasmaLens(ds, k, units, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "tapered_plasma_lens")
         {
             auto a = detail::query_alignment(pp_element);
@@ -334,29 +368,32 @@ namespace detail
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
-            m_lattice.emplace_back( ExactDrift(ds, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ExactDrift(ds, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "sbend_exact")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal phi;
             amrex::ParticleReal B = 0.0;
             pp_element.getWithParser("phi", phi);
             pp_element.queryAddWithParser("B", B);
 
-            m_lattice.emplace_back( ExactSbend(ds, phi, B, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ExactSbend(ds, phi, B, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "uniform_acc_chromatic")
         {
             auto const [ds, nslice] = detail::query_ds(pp_element, nslice_default);
             auto a = detail::query_alignment(pp_element);
+            auto b = detail::query_aperture(pp_element);
 
             amrex::ParticleReal ez, bz;
             pp_element.getWithParser("ez", ez);
             pp_element.getWithParser("bz", bz);
 
-            m_lattice.emplace_back( ChrAcc(ds, ez, bz, a["dx"], a["dy"], a["rotation_degree"], nslice, element_name) );
+            m_lattice.emplace_back( ChrAcc(ds, ez, bz, a["dx"], a["dy"], a["rotation_degree"], b["aperture_x"], b["aperture_y"], nslice, element_name) );
         } else if (element_type == "thin_dipole")
         {
             auto a = detail::query_alignment(pp_element);
@@ -386,13 +423,40 @@ namespace detail
         {
             auto a = detail::query_alignment(pp_element);
 
-            amrex::Real xmax, ymax;
+            amrex::Real aperture_x, aperture_y;
             amrex::ParticleReal repeat_x = 0.0;
             amrex::ParticleReal repeat_y = 0.0;
             std::string shape_str = "rectangular";
             std::string action_str = "transmit";
-            pp_element.getWithParser("xmax", xmax);
-            pp_element.getWithParser("ymax", ymax);
+
+            // In the future, just use this:
+            // pp_element.getWithParser("aperture_x", aperture_x);
+            // pp_element.getWithParser("aperture_y", aperture_y);
+            // Backwards compatibility to ImpactX <= 25.01
+            bool const has_old_xmax = pp_element.queryAddWithParser("xmax", aperture_x);
+            bool const has_old_ymax = pp_element.queryAddWithParser("ymax", aperture_y);
+            if (has_old_xmax) {
+                pp_element.queryAddWithParser("aperture_x", aperture_x);
+                ablastr::warn_manager::WMRecordWarning(
+                    "ImpactX::read_element",
+                    element_name + ".xmax is deprecated. Use " + element_name + ".aperture_x instead.",
+                    ablastr::warn_manager::WarnPriority::high
+                );
+            } else {
+                pp_element.getWithParser("aperture_x", aperture_x);
+            }
+            if (has_old_ymax) {
+                pp_element.queryAddWithParser("aperture_y", aperture_y);
+                ablastr::warn_manager::WMRecordWarning(
+                    "ImpactX::read_element",
+                    element_name + ".ymax is deprecated. Use " + element_name + ".aperture_y instead.",
+                    ablastr::warn_manager::WarnPriority::high
+                );
+            } else {
+                pp_element.getWithParser("aperture_y", aperture_y);
+            }
+
+            pp_element.queryAddWithParser("repeat_y", repeat_y);
             pp_element.queryAddWithParser("repeat_x", repeat_x);
             pp_element.queryAddWithParser("repeat_y", repeat_y);
             pp_element.queryAdd("shape", shape_str);
@@ -408,7 +472,7 @@ namespace detail
                                         Aperture::Action::transmit :
                                         Aperture::Action::absorb;
 
-            m_lattice.emplace_back( Aperture(xmax, ymax, repeat_x, repeat_y, shape, action, a["dx"], a["dy"], a["rotation_degree"], element_name) );
+            m_lattice.emplace_back( Aperture(aperture_x, aperture_y, repeat_x, repeat_y, shape, action, a["dx"], a["dy"], a["rotation_degree"], element_name) );
         } else if (element_type == "beam_monitor")
         {
             std::string openpmd_name = element_name;
@@ -465,7 +529,7 @@ namespace detail
             amrex::ParticleReal ds = 0.0;
             pp_element.queryAdd("ds", ds);
 
-            elements::LinearTransport::Map6x6 transport_map = elements::LinearTransport::Map6x6::Identity();
+            elements::mixin::LinearTransport::Map6x6 transport_map = elements::mixin::LinearTransport::Map6x6::Identity();
 
             // safe to ParmParse inputs for reproducibility
             for (int i=1; i<=6; ++i) {
diff --git a/src/particles/elements/Aperture.H b/src/particles/elements/Aperture.H
index bf5621793..d9bea0602 100644
--- a/src/particles/elements/Aperture.H
+++ b/src/particles/elements/Aperture.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct Aperture
-    : public elements::Named,
-      public elements::BeamOptic<Aperture>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Aperture>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Aperture";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -58,8 +58,8 @@ namespace impactx
          *
          * @param shape aperture shape
          * @param action specify action of domain (transmit/absorb)
-         * @param xmax maximum value of horizontal coordinate (m)
-         * @param ymax maximum value of vertical coordinate (m)
+         * @param aperture_x horizontal half-aperture (m)
+         * @param aperture_y vertical half-aperture (m)
          * @param repeat_x horizontal period for repeated masking, optional (m)
          * @param repeat_y vertical period for repeated masking, optional (m)
          * @param dx horizontal translation error in m
@@ -68,8 +68,8 @@ namespace impactx
          * @param name a user defined and not necessarily unique name of the element
          */
         Aperture (
-            amrex::ParticleReal xmax,
-            amrex::ParticleReal ymax,
+            amrex::ParticleReal aperture_x,
+            amrex::ParticleReal aperture_y,
             amrex::ParticleReal repeat_x,
             amrex::ParticleReal repeat_y,
             Shape shape,
@@ -81,7 +81,7 @@ namespace impactx
         )
         : Named(std::move(name)),
           Alignment(dx, dy, rotation_degree),
-          m_shape(shape), m_action(action), m_xmax(xmax), m_ymax(ymax), m_repeat_x(repeat_x), m_repeat_y(repeat_y)
+          m_shape(shape), m_action(action), m_aperture_x(aperture_x), m_aperture_y(aperture_y), m_repeat_x(repeat_x), m_repeat_y(repeat_y)
         {
         }
 
@@ -128,8 +128,8 @@ namespace impactx
             v = (m_repeat_y==0.0) ? y : (std::fmod(std::abs(y)+dy,m_repeat_y)-dy);
 
             // scale horizontal and vertical coordinates
-            u = u / m_xmax;
-            v = v / m_ymax;
+            u = u / m_aperture_x;
+            v = v / m_aperture_y;
 
             // compare against the aperture boundary
             switch (m_action)
@@ -181,8 +181,8 @@ namespace impactx
 
         Shape m_shape; //! aperture type (rectangular, elliptical)
         Action m_action; //! action type (transmit, absorb)
-        amrex::ParticleReal m_xmax; //! maximum horizontal coordinate
-        amrex::ParticleReal m_ymax; //! maximum vertical coordinate
+        amrex::ParticleReal m_aperture_x; //! maximum horizontal coordinate
+        amrex::ParticleReal m_aperture_y; //! maximum vertical coordinate
         amrex::ParticleReal m_repeat_x; //! horizontal period for repeated masking
         amrex::ParticleReal m_repeat_y; //! vertical period for repeated masking
 
diff --git a/src/particles/elements/Buncher.H b/src/particles/elements/Buncher.H
index 0ca7efb1e..3079875ee 100644
--- a/src/particles/elements/Buncher.H
+++ b/src/particles/elements/Buncher.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct Buncher
-    : public elements::Named,
-      public elements::BeamOptic<Buncher>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Buncher>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Buncher";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/CFbend.H b/src/particles/elements/CFbend.H
index 409f70146..6fb71e18b 100644
--- a/src/particles/elements/CFbend.H
+++ b/src/particles/elements/CFbend.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -27,11 +28,12 @@
 namespace impactx
 {
     struct CFbend
-    : public elements::Named,
-      public elements::BeamOptic<CFbend>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<CFbend>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "CFbend";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -48,6 +50,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -58,12 +62,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_rc(rc), m_k(k)
         {
         }
@@ -79,7 +86,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -90,7 +97,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -177,6 +184,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ChrDrift.H b/src/particles/elements/ChrDrift.H
index 6b59d8b06..b5a063241 100644
--- a/src/particles/elements/ChrDrift.H
+++ b/src/particles/elements/ChrDrift.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct ChrDrift
-    : public elements::Named,
-      public elements::BeamOptic<ChrDrift>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ChrDrift>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ChrDrift";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -44,6 +46,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -52,12 +56,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
-          Alignment(dx, dy, rotation_degree)
+          Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y)
         {
         }
 
@@ -72,7 +79,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -83,7 +90,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -133,6 +140,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ChrPlasmaLens.H b/src/particles/elements/ChrPlasmaLens.H
index bfe0fe78c..e588ccf8c 100644
--- a/src/particles/elements/ChrPlasmaLens.H
+++ b/src/particles/elements/ChrPlasmaLens.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct ChrPlasmaLens
-    : public elements::Named,
-      public elements::BeamOptic<ChrPlasmaLens>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ChrPlasmaLens>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ChrPlasmaLens";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -50,6 +52,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -60,12 +64,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_k(k), m_unit(unit)
         {
         }
@@ -81,7 +88,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -92,7 +99,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -171,6 +178,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ChrQuad.H b/src/particles/elements/ChrQuad.H
index 5e246341b..a24a07579 100644
--- a/src/particles/elements/ChrQuad.H
+++ b/src/particles/elements/ChrQuad.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -27,11 +28,12 @@
 namespace impactx
 {
     struct ChrQuad
-    : public elements::Named,
-      public elements::BeamOptic<ChrQuad>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ChrQuad>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ChrQuad";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -53,6 +55,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -63,12 +67,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_k(k), m_unit(unit)
         {
         }
@@ -84,7 +91,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -95,7 +102,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -210,6 +217,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ChrUniformAcc.H b/src/particles/elements/ChrUniformAcc.H
index 45d7fd056..2503c56a7 100644
--- a/src/particles/elements/ChrUniformAcc.H
+++ b/src/particles/elements/ChrUniformAcc.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct ChrAcc
-    : public elements::Named,
-      public elements::BeamOptic<ChrAcc>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ChrAcc>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ChrAcc";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -48,6 +50,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -58,12 +62,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_ez(ez), m_bz(bz)
         {
         }
@@ -79,7 +86,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -90,7 +97,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -173,6 +180,9 @@ namespace impactx
             py = py/bgf;
             pt = pt/bgf;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ConstF.H b/src/particles/elements/ConstF.H
index 10ab1077c..3f7dc557f 100644
--- a/src/particles/elements/ConstF.H
+++ b/src/particles/elements/ConstF.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct ConstF
-    : public elements::Named,
-      public elements::BeamOptic<ConstF>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ConstF>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ConstF";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -44,6 +46,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -55,12 +59,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_kx(kx), m_ky(ky), m_kt(kt)
         {
         }
@@ -76,7 +83,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -87,7 +94,7 @@ namespace impactx
                 amrex::ParticleReal & AMREX_RESTRICT px,
                 amrex::ParticleReal & AMREX_RESTRICT py,
                 amrex::ParticleReal & AMREX_RESTRICT pt,
-                [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+                uint64_t & AMREX_RESTRICT idcpu,
                 RefPart const & refpart) const {
 
             using namespace amrex::literals; // for _rt and _prt
@@ -128,6 +135,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/DipEdge.H b/src/particles/elements/DipEdge.H
index 12b528e5f..825bd0112 100644
--- a/src/particles/elements/DipEdge.H
+++ b/src/particles/elements/DipEdge.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct DipEdge
-    : public elements::Named,
-      public elements::BeamOptic<DipEdge>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<DipEdge>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "DipEdge";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/Drift.H b/src/particles/elements/Drift.H
index cdc4b8e1f..001bd2583 100644
--- a/src/particles/elements/Drift.H
+++ b/src/particles/elements/Drift.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -29,11 +30,12 @@
 namespace impactx
 {
     struct Drift
-    : public elements::Named,
-      public elements::BeamOptic<Drift>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Drift>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Drift";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -44,6 +46,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -52,12 +56,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
-          Alignment(dx, dy, rotation_degree)
+          Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y)
         {
         }
 
@@ -72,7 +79,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -83,7 +90,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -123,6 +130,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
@@ -168,7 +178,7 @@ namespace impactx
          * @returns 6x6 transport matrix
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
-        elements::LinearTransport::Map6x6
+        elements::mixin::LinearTransport::Map6x6
         transport_map (RefPart & AMREX_RESTRICT refpart) const
         {
             using namespace amrex::literals; // for _rt and _prt
@@ -181,7 +191,7 @@ namespace impactx
             amrex::ParticleReal const betgam2 = std::pow(pt_ref, 2) - 1.0_prt;
 
             // assign linear map matrix elements
-            elements::LinearTransport::Map6x6 R = elements::LinearTransport::Map6x6::Identity();
+            elements::mixin::LinearTransport::Map6x6 R = elements::mixin::LinearTransport::Map6x6::Identity();
             R(1,2) = slice_ds;
             R(3,4) = slice_ds;
             R(5,6) = slice_ds / betgam2;
diff --git a/src/particles/elements/Empty.H b/src/particles/elements/Empty.H
index 9ce3eece8..30b29d3ac 100644
--- a/src/particles/elements/Empty.H
+++ b/src/particles/elements/Empty.H
@@ -22,8 +22,8 @@
 namespace impactx
 {
     struct Empty
-    : public elements::Thin,
-      public elements::NoFinalize
+    : public elements::mixin::Thin,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "None";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/ExactDrift.H b/src/particles/elements/ExactDrift.H
index 6f3a4060f..b182e1705 100644
--- a/src/particles/elements/ExactDrift.H
+++ b/src/particles/elements/ExactDrift.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct ExactDrift
-    : public elements::Named,
-      public elements::BeamOptic<ExactDrift>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ExactDrift>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ExactDrift";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -41,6 +43,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -49,12 +53,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
-          Alignment(dx, dy, rotation_degree)
+          Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y)
         {
         }
 
@@ -70,7 +77,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -81,7 +88,7 @@ namespace impactx
                 amrex::ParticleReal & AMREX_RESTRICT px,
                 amrex::ParticleReal & AMREX_RESTRICT py,
                 amrex::ParticleReal & AMREX_RESTRICT pt,
-                [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+                uint64_t & AMREX_RESTRICT idcpu,
                 RefPart const & refpart
         ) const
         {
@@ -124,6 +131,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ExactSbend.H b/src/particles/elements/ExactSbend.H
index 083ae0c58..058c41647 100644
--- a/src/particles/elements/ExactSbend.H
+++ b/src/particles/elements/ExactSbend.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -28,11 +29,12 @@
 namespace impactx
 {
     struct ExactSbend
-    : public elements::Named,
-      public elements::BeamOptic<ExactSbend>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ExactSbend>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ExactSbend";
         static constexpr amrex::ParticleReal degree2rad = ablastr::constant::math::pi / 180.0;
@@ -56,6 +58,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -66,12 +70,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_phi(phi * degree2rad), m_B(B)
         {
         }
@@ -96,7 +103,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -107,7 +114,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -160,6 +167,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/Kicker.H b/src/particles/elements/Kicker.H
index 7bd3c17c0..cd3556ed1 100644
--- a/src/particles/elements/Kicker.H
+++ b/src/particles/elements/Kicker.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct Kicker
-    : public elements::Named,
-      public elements::BeamOptic<Kicker>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Kicker>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Kicker";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/LinearMap.H b/src/particles/elements/LinearMap.H
index 472ae12be..4d66c9131 100644
--- a/src/particles/elements/LinearMap.H
+++ b/src/particles/elements/LinearMap.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct LinearMap
-    : public elements::Named,
-      public elements::BeamOptic<LinearMap>,
-      public elements::Alignment,
-      public elements::LinearTransport,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<LinearMap>,
+      public elements::mixin::Alignment,
+      public elements::mixin::LinearTransport,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "LinearMap";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/Marker.H b/src/particles/elements/Marker.H
index 3291aff79..5fd90ab88 100644
--- a/src/particles/elements/Marker.H
+++ b/src/particles/elements/Marker.H
@@ -22,9 +22,9 @@
 namespace impactx
 {
     struct Marker
-    : public elements::Named,
-      public elements::Thin,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::Thin,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Marker";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/Multipole.H b/src/particles/elements/Multipole.H
index f2db02d58..121c2665b 100644
--- a/src/particles/elements/Multipole.H
+++ b/src/particles/elements/Multipole.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct Multipole
-    : public elements::Named,
-      public elements::BeamOptic<Multipole>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Multipole>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Multipole";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/NonlinearLens.H b/src/particles/elements/NonlinearLens.H
index 8413d000e..d268e0366 100644
--- a/src/particles/elements/NonlinearLens.H
+++ b/src/particles/elements/NonlinearLens.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct NonlinearLens
-    : public elements::Named,
-      public elements::BeamOptic<NonlinearLens>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<NonlinearLens>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "NonlinearLens";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/PRot.H b/src/particles/elements/PRot.H
index 242a82534..5683c1e6f 100644
--- a/src/particles/elements/PRot.H
+++ b/src/particles/elements/PRot.H
@@ -28,10 +28,10 @@
 namespace impactx
 {
     struct PRot
-    : public elements::Named,
-      public elements::BeamOptic<PRot>,
-      public elements::Thin,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<PRot>,
+      public elements::mixin::Thin,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "PRot";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/PlaneXYRot.H b/src/particles/elements/PlaneXYRot.H
index eeb56bfca..250deb4d6 100644
--- a/src/particles/elements/PlaneXYRot.H
+++ b/src/particles/elements/PlaneXYRot.H
@@ -29,11 +29,11 @@
 namespace impactx
 {
     struct PlaneXYRot
-    : public elements::Named,
-      public elements::BeamOptic<PlaneXYRot>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<PlaneXYRot>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "PlaneXYRot";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/Programmable.H b/src/particles/elements/Programmable.H
index 2227fd64b..582612424 100644
--- a/src/particles/elements/Programmable.H
+++ b/src/particles/elements/Programmable.H
@@ -23,7 +23,7 @@
 namespace impactx
 {
     struct Programmable
-    : public elements::Named
+    : public elements::mixin::Named
     {
         static constexpr auto type = "Programmable";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/Quad.H b/src/particles/elements/Quad.H
index be0464ea8..e41af5293 100644
--- a/src/particles/elements/Quad.H
+++ b/src/particles/elements/Quad.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -26,11 +27,12 @@
 namespace impactx
 {
     struct Quad
-    : public elements::Named,
-      public elements::BeamOptic<Quad>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Quad>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Quad";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -45,6 +47,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -54,12 +58,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_k(k)
         {
         }
@@ -75,7 +82,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -86,7 +93,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -152,6 +159,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/RFCavity.H b/src/particles/elements/RFCavity.H
index c9fc02d57..98685a0b5 100644
--- a/src/particles/elements/RFCavity.H
+++ b/src/particles/elements/RFCavity.H
@@ -13,6 +13,7 @@
 #include "particles/ImpactXParticleContainer.H"
 #include "particles/integrators/Integrators.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/named.H"
 #include "mixin/thick.H"
@@ -103,10 +104,11 @@ namespace RFCavityData
 } // namespace RFCavityData
 
     struct RFCavity
-    : public elements::Named,
-      public elements::BeamOptic<RFCavity>,
-      public elements::Thick,
-      public elements::Alignment
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<RFCavity>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture
     {
         static constexpr auto type = "RFCavity";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -122,6 +124,8 @@ namespace RFCavityData
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param mapsteps number of integration steps per slice used for
          *        map and reference particle push in applied fields
          * @param nslice number of slices used for the application of space charge
@@ -137,6 +141,8 @@ namespace RFCavityData
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int mapsteps = 1,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
@@ -144,6 +150,7 @@ namespace RFCavityData
           : Named(std::move(name)),
             Thick(ds, nslice),
             Alignment(dx, dy, rotation_degree),
+            PipeAperture(aperture_x, aperture_y),
             m_escale(escale), m_freq(freq), m_phase(phase), m_mapsteps(mapsteps), m_id(RFCavityData::next_id)
         {
             // next created RF cavity has another id for its data
@@ -188,7 +195,7 @@ namespace RFCavityData
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -199,7 +206,7 @@ namespace RFCavityData
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             [[maybe_unused]] RefPart const & refpart
         ) const
         {
@@ -229,6 +236,9 @@ namespace RFCavityData
             t  = out[5];
             pt = out[6];
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/Sbend.H b/src/particles/elements/Sbend.H
index c6f67d9ad..f896d1fa4 100644
--- a/src/particles/elements/Sbend.H
+++ b/src/particles/elements/Sbend.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -27,11 +28,12 @@
 namespace impactx
 {
     struct Sbend
-    : public elements::Named,
-      public elements::BeamOptic<Sbend>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Sbend>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Sbend";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -43,6 +45,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -52,12 +56,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_rc(rc)
         {
         }
@@ -73,7 +80,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -84,7 +91,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -138,6 +145,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/ShortRF.H b/src/particles/elements/ShortRF.H
index 64546503a..024e007fb 100644
--- a/src/particles/elements/ShortRF.H
+++ b/src/particles/elements/ShortRF.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct ShortRF
-    : public elements::Named,
-      public elements::BeamOptic<ShortRF>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ShortRF>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ShortRF";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/SoftQuad.H b/src/particles/elements/SoftQuad.H
index 187f55803..06beccab1 100644
--- a/src/particles/elements/SoftQuad.H
+++ b/src/particles/elements/SoftQuad.H
@@ -13,6 +13,7 @@
 #include "particles/ImpactXParticleContainer.H"
 #include "particles/integrators/Integrators.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/named.H"
 #include "mixin/thick.H"
@@ -112,10 +113,11 @@ namespace SoftQuadrupoleData
 } // namespace SoftQuadrupoleData
 
     struct SoftQuadrupole
-    : public elements::Named,
-      public elements::BeamOptic<SoftQuadrupole>,
-      public elements::Thick,
-      public elements::Alignment
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<SoftQuadrupole>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture
     {
         static constexpr auto type = "SoftQuadrupole";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -129,6 +131,8 @@ namespace SoftQuadrupoleData
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param mapsteps number of integration steps per slice used for
          *        map and reference particle push in applied fields
          * @param nslice number of slices used for the application of space charge
@@ -142,6 +146,8 @@ namespace SoftQuadrupoleData
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int mapsteps = 1,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
@@ -149,6 +155,7 @@ namespace SoftQuadrupoleData
           : Named(std::move(name)),
             Thick(ds, nslice),
             Alignment(dx, dy, rotation_degree),
+            PipeAperture(aperture_x, aperture_y),
             m_gscale(gscale), m_mapsteps(mapsteps), m_id(SoftQuadrupoleData::next_id)
         {
             // next created soft quad has another id for its data
@@ -193,7 +200,7 @@ namespace SoftQuadrupoleData
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -204,7 +211,7 @@ namespace SoftQuadrupoleData
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             [[maybe_unused]] RefPart const & refpart
         ) const
         {
@@ -234,6 +241,9 @@ namespace SoftQuadrupoleData
             t  = out[5];
             pt = out[6];
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/SoftSol.H b/src/particles/elements/SoftSol.H
index 5152f604c..db0516304 100644
--- a/src/particles/elements/SoftSol.H
+++ b/src/particles/elements/SoftSol.H
@@ -13,6 +13,7 @@
 #include "particles/ImpactXParticleContainer.H"
 #include "particles/integrators/Integrators.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/named.H"
 #include "mixin/thick.H"
@@ -117,10 +118,11 @@ namespace SoftSolenoidData
 } // namespace SoftSolenoidData
 
     struct SoftSolenoid
-    : public elements::Named,
-      public elements::BeamOptic<SoftSolenoid>,
-      public elements::Thick,
-      public elements::Alignment
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<SoftSolenoid>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture
     {
         static constexpr auto type = "SoftSolenoid";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -139,6 +141,8 @@ namespace SoftSolenoidData
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param mapsteps number of integration steps per slice used for
          *        map and reference particle push in applied fields
          * @param nslice number of slices used for the application of space charge
@@ -153,6 +157,8 @@ namespace SoftSolenoidData
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int mapsteps = 1,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
@@ -160,6 +166,7 @@ namespace SoftSolenoidData
           : Named(std::move(name)),
             Thick(ds, nslice),
             Alignment(dx, dy, rotation_degree),
+            PipeAperture(aperture_x, aperture_y),
             m_bscale(bscale), m_unit(unit), m_mapsteps(mapsteps), m_id(SoftSolenoidData::next_id)
        {
            // next created soft solenoid has another id for its data
@@ -204,7 +211,7 @@ namespace SoftSolenoidData
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -215,7 +222,7 @@ namespace SoftSolenoidData
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             [[maybe_unused]] RefPart const & refpart
         ) const
         {
@@ -245,6 +252,9 @@ namespace SoftSolenoidData
             t  = out[5];
             pt = out[6];
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/Sol.H b/src/particles/elements/Sol.H
index 8573648a0..265b651c3 100644
--- a/src/particles/elements/Sol.H
+++ b/src/particles/elements/Sol.H
@@ -12,6 +12,7 @@
 
 #include "particles/ImpactXParticleContainer.H"
 #include "mixin/alignment.H"
+#include "mixin/pipeaperture.H"
 #include "mixin/beamoptic.H"
 #include "mixin/thick.H"
 #include "mixin/named.H"
@@ -27,11 +28,12 @@
 namespace impactx
 {
     struct Sol
-    : public elements::Named,
-      public elements::BeamOptic<Sol>,
-      public elements::Thick,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<Sol>,
+      public elements::mixin::Thick,
+      public elements::mixin::Alignment,
+      public elements::mixin::PipeAperture,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "Sol";
         using PType = ImpactXParticleContainer::ParticleType;
@@ -44,6 +46,8 @@ namespace impactx
          * @param dx horizontal translation error in m
          * @param dy vertical translation error in m
          * @param rotation_degree rotation error in the transverse plane [degrees]
+         * @param aperture_x horizontal half-aperture in m
+         * @param aperture_y vertical half-aperture in m
          * @param nslice number of slices used for the application of space charge
          * @param name a user defined and not necessarily unique name of the element
          */
@@ -53,12 +57,15 @@ namespace impactx
             amrex::ParticleReal dx = 0,
             amrex::ParticleReal dy = 0,
             amrex::ParticleReal rotation_degree = 0,
+            amrex::ParticleReal aperture_x = 0,
+            amrex::ParticleReal aperture_y = 0,
             int nslice = 1,
             std::optional<std::string> name = std::nullopt
         )
         : Named(std::move(name)),
           Thick(ds, nslice),
           Alignment(dx, dy, rotation_degree),
+          PipeAperture(aperture_x, aperture_y),
           m_ks(ks)
         {
         }
@@ -74,7 +81,7 @@ namespace impactx
          * @param px particle momentum in x
          * @param py particle momentum in y
          * @param pt particle momentum in t
-         * @param idcpu particle global index (unused)
+         * @param idcpu particle global index
          * @param refpart reference particle
          */
         AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
@@ -85,7 +92,7 @@ namespace impactx
             amrex::ParticleReal & AMREX_RESTRICT px,
             amrex::ParticleReal & AMREX_RESTRICT py,
             amrex::ParticleReal & AMREX_RESTRICT pt,
-            [[maybe_unused]] uint64_t & AMREX_RESTRICT idcpu,
+            uint64_t & AMREX_RESTRICT idcpu,
             RefPart const & refpart
         ) const
         {
@@ -145,6 +152,9 @@ namespace impactx
             py = pyout;
             pt = ptout;
 
+            // apply transverse aperture
+            apply_aperture(x, y, idcpu);
+
             // undo shift due to alignment errors of the element
             shift_out(x, y, px, py);
         }
diff --git a/src/particles/elements/TaperedPL.H b/src/particles/elements/TaperedPL.H
index 2f7dabad4..8039e2a97 100644
--- a/src/particles/elements/TaperedPL.H
+++ b/src/particles/elements/TaperedPL.H
@@ -26,11 +26,11 @@
 namespace impactx
 {
     struct TaperedPL
-    : public elements::Named,
-      public elements::BeamOptic<TaperedPL>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<TaperedPL>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "TaperedPL";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/ThinDipole.H b/src/particles/elements/ThinDipole.H
index 097cb85c4..14ed4f062 100644
--- a/src/particles/elements/ThinDipole.H
+++ b/src/particles/elements/ThinDipole.H
@@ -24,11 +24,11 @@
 namespace impactx
 {
     struct ThinDipole
-    : public elements::Named,
-      public elements::BeamOptic<ThinDipole>,
-      public elements::Thin,
-      public elements::Alignment,
-      public elements::NoFinalize
+    : public elements::mixin::Named,
+      public elements::mixin::BeamOptic<ThinDipole>,
+      public elements::mixin::Thin,
+      public elements::mixin::Alignment,
+      public elements::mixin::NoFinalize
     {
         static constexpr auto type = "ThinDipole";
         using PType = ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/diagnostics/openPMD.H b/src/particles/elements/diagnostics/openPMD.H
index f5ee0d4a8..6d47ecee6 100644
--- a/src/particles/elements/diagnostics/openPMD.H
+++ b/src/particles/elements/diagnostics/openPMD.H
@@ -63,7 +63,7 @@ namespace detail
      * same series name as an existing instance.
      */
     struct BeamMonitor
-    : public elements::Thin
+    : public elements::mixin::Thin
     {
         static constexpr auto type = "BeamMonitor";
         using PType = typename ImpactXParticleContainer::ParticleType;
diff --git a/src/particles/elements/mixin/alignment.H b/src/particles/elements/mixin/alignment.H
index d108c4e1b..283e85147 100644
--- a/src/particles/elements/mixin/alignment.H
+++ b/src/particles/elements/mixin/alignment.H
@@ -19,7 +19,7 @@
 #include <AMReX_REAL.H>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements with horizontal/vertical alignment errors
      */
@@ -147,6 +147,6 @@ namespace impactx::elements
         amrex::ParticleReal m_rotation = 0; //! rotation error in the transverse plane [rad]
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_ALIGNMENT_H
diff --git a/src/particles/elements/mixin/beamoptic.H b/src/particles/elements/mixin/beamoptic.H
index 0616b5374..bd1d5f314 100644
--- a/src/particles/elements/mixin/beamoptic.H
+++ b/src/particles/elements/mixin/beamoptic.H
@@ -19,7 +19,7 @@
 #include <type_traits>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
 namespace detail
 {
@@ -190,6 +190,6 @@ namespace detail
          }
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_BEAMOPTIC_H
diff --git a/src/particles/elements/mixin/lineartransport.H b/src/particles/elements/mixin/lineartransport.H
index 9fce44ff6..add40df61 100644
--- a/src/particles/elements/mixin/lineartransport.H
+++ b/src/particles/elements/mixin/lineartransport.H
@@ -20,7 +20,7 @@
 #include <AMReX_SmallMatrix.H>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements that can be expressed as linear transport maps.
      */
@@ -47,6 +47,6 @@ namespace impactx::elements
         // Map6x6 m_transport_map; ///< linearized map
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_LINEAR_TRANSPORT_H
diff --git a/src/particles/elements/mixin/named.H b/src/particles/elements/mixin/named.H
index f06a6d86c..1ebaea4fc 100644
--- a/src/particles/elements/mixin/named.H
+++ b/src/particles/elements/mixin/named.H
@@ -21,7 +21,7 @@
 #include <string>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements with a user-defined name
      */
@@ -148,6 +148,6 @@ namespace impactx::elements
         char * m_name = nullptr; //! a user defined and not necessarily unique name of the element
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_NAMED_H
diff --git a/src/particles/elements/mixin/nofinalize.H b/src/particles/elements/mixin/nofinalize.H
index 3ab5a9607..7ea6e4da0 100644
--- a/src/particles/elements/mixin/nofinalize.H
+++ b/src/particles/elements/mixin/nofinalize.H
@@ -11,7 +11,7 @@
 #define IMPACTX_ELEMENTS_MIXIN_NOFINALIZE_H
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements that need no finalize function.
      *
@@ -29,6 +29,6 @@ namespace impactx::elements
         }
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_NOFINALIZE_H
diff --git a/src/particles/elements/mixin/pipeaperture.H b/src/particles/elements/mixin/pipeaperture.H
new file mode 100644
index 000000000..be336bc08
--- /dev/null
+++ b/src/particles/elements/mixin/pipeaperture.H
@@ -0,0 +1,103 @@
+/* Copyright 2022-2025 The Regents of the University of California, through Lawrence
+ *           Berkeley National Laboratory (subject to receipt of any required
+ *           approvals from the U.S. Dept. of Energy). All rights reserved.
+ *
+ * This file is part of ImpactX.
+ *
+ * Authors: Axel Huebl, Chad Mitchell
+ * License: BSD-3-Clause-LBNL
+ */
+#ifndef IMPACTX_ELEMENTS_MIXIN_PIPE_APERTURE_H
+#define IMPACTX_ELEMENTS_MIXIN_PIPE_APERTURE_H
+
+#include <AMReX_Extension.H>
+#include <AMReX_GpuQualifiers.H>
+#include <AMReX_Math.H>
+#include <AMReX_Particle.H>
+#include <AMReX_REAL.H>
+
+
+namespace impactx::elements::mixin
+{
+    /** This is a helper class for applying a transverse aperture restriction to thick lattice elements
+     */
+    struct PipeAperture
+    {
+
+        /** A finite-length element with a constant elliptical aperture over s.
+         *
+         * @param aperture_x horizontal half-aperture size in m
+         * @param aperture_y vertical half-aperture size in m
+         */
+        PipeAperture (
+            amrex::ParticleReal aperture_x,
+            amrex::ParticleReal aperture_y
+        )
+        : m_aperture_x(aperture_x), m_aperture_y(aperture_y)
+        {
+        }
+
+        PipeAperture () = default;
+        PipeAperture (PipeAperture const &) = default;
+        PipeAperture& operator= (PipeAperture const &) = default;
+        PipeAperture (PipeAperture&&) = default;
+        PipeAperture& operator= (PipeAperture&& rhs) = default;
+
+        ~PipeAperture () = default;
+
+        /** Apply the transverse aperture
+         *
+         * @param[inout] x horizontal position relative to reference particle
+         * @param[inout] y vertical position relative to reference particle
+         * @param idcpu particle global index
+         */
+        AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
+        void apply_aperture (
+            amrex::ParticleReal & AMREX_RESTRICT x,
+            amrex::ParticleReal & AMREX_RESTRICT y,
+            uint64_t & AMREX_RESTRICT idcpu
+        ) const {
+            using namespace amrex::literals; // for _rt and _prt
+
+            // skip aperture application if aperture_x <= 0 or aperture_y <= 0
+            if (m_aperture_x > 0 && m_aperture_y > 0) {
+
+               // scale horizontal and vertical coordinates
+               amrex::ParticleReal const u = x / m_aperture_x;
+               amrex::ParticleReal const v = y / m_aperture_y;
+
+               // compare against the aperture boundary
+               if (std::pow(u,2) + std::pow(v,2) > 1_prt) {
+                  amrex::ParticleIDWrapper{idcpu}.make_invalid();
+               }
+
+            }
+        }
+
+        /** Horizontal aperture size
+         *
+         * @return horizontal aperture size in m
+         */
+        AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
+        amrex::ParticleReal aperture_x () const
+        {
+            return m_aperture_x;
+        }
+
+        /** Vertical aperture size
+         *
+         * @return vertical aperture size in m
+         */
+        AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE
+        amrex::ParticleReal aperture_y () const
+        {
+            return m_aperture_y;
+        }
+
+        amrex::ParticleReal m_aperture_x = 0; //! horizontal aperture size [m]
+        amrex::ParticleReal m_aperture_y = 0; //! vertical aperture size [m]
+    };
+
+} // namespace impactx::elements::mixin
+
+#endif // IMPACTX_ELEMENTS_MIXIN_PIPE_APERTURE_H
diff --git a/src/particles/elements/mixin/thick.H b/src/particles/elements/mixin/thick.H
index 4572d2a67..85a9917d5 100644
--- a/src/particles/elements/mixin/thick.H
+++ b/src/particles/elements/mixin/thick.H
@@ -16,7 +16,7 @@
 #include <AMReX_REAL.H>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements with finite length
      */
@@ -59,6 +59,6 @@ namespace impactx::elements
         int m_nslice; //! number of slices used for the application of space charge
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_THICK_H
diff --git a/src/particles/elements/mixin/thin.H b/src/particles/elements/mixin/thin.H
index b9c165e64..87138a95c 100644
--- a/src/particles/elements/mixin/thin.H
+++ b/src/particles/elements/mixin/thin.H
@@ -16,7 +16,7 @@
 #include <AMReX_REAL.H>
 
 
-namespace impactx::elements
+namespace impactx::elements::mixin
 {
     /** This is a helper class for lattice elements with zero length
      */
@@ -54,6 +54,6 @@ namespace impactx::elements
         }
     };
 
-} // namespace impactx::elements
+} // namespace impactx::elements::mixin
 
 #endif // IMPACTX_ELEMENTS_MIXIN_THIN_H
diff --git a/src/python/elements.cpp b/src/python/elements.cpp
index aeb0e06d9..3e601c580 100644
--- a/src/python/elements.cpp
+++ b/src/python/elements.cpp
@@ -154,93 +154,87 @@ void init_elements(py::module& m)
         "Mixin classes for accelerator lattice elements in ImpactX"
     );
 
-    py::class_<elements::Named>(mx, "Named")
+    py::class_<elements::mixin::Named>(mx, "Named")
         .def_property("name",
-            [](elements::Named & nm) { return nm.name(); },
-            [](elements::Named & nm, std::string new_name) { nm.set_name(new_name); },
+            [](elements::mixin::Named & nm) { return nm.name(); },
+            [](elements::mixin::Named & nm, std::string new_name) { nm.set_name(new_name); },
             "segment length in m"
         )
-        .def_property_readonly("has_name", &elements::Named::has_name)
+        .def_property_readonly("has_name", &elements::mixin::Named::has_name)
     ;
 
-    py::class_<elements::Thick>(mx, "Thick")
-        .def(py::init<
-                 amrex::ParticleReal,
-                 amrex::ParticleReal
-             >(),
-             py::arg("ds"),
-             py::arg("nslice") = 1,
-             "Mixin class for lattice elements with finite length."
-        )
+    py::class_<elements::mixin::Thick>(mx, "Thick")
         .def_property("ds",
-            [](elements::Thick & th) { return th.m_ds; },
-            [](elements::Thick & th, amrex::ParticleReal ds) { th.m_ds = ds; },
+            [](elements::mixin::Thick & th) { return th.m_ds; },
+            [](elements::mixin::Thick & th, amrex::ParticleReal ds) { th.m_ds = ds; },
             "segment length in m"
         )
         .def_property("nslice",
-            [](elements::Thick & th) { return th.m_nslice; },
-            [](elements::Thick & th, int nslice) { th.m_nslice = nslice; },
+            [](elements::mixin::Thick & th) { return th.m_nslice; },
+            [](elements::mixin::Thick & th, int nslice) { th.m_nslice = nslice; },
             "number of slices used for the application of space charge"
         )
     ;
 
-    py::class_<elements::Thin>(mx, "Thin")
-        .def(py::init<>(),
-             "Mixin class for lattice elements with zero length."
-        )
+    py::class_<elements::mixin::Thin>(mx, "Thin")
         .def_property_readonly("ds",
-            &elements::Thin::ds,
+            &elements::mixin::Thin::ds,
             "segment length in m"
         )
         .def_property_readonly("nslice",
-            &elements::Thin::nslice,
+            &elements::mixin::Thin::nslice,
             "number of slices used for the application of space charge"
         )
     ;
 
-    py::class_<elements::Alignment>(mx, "Alignment")
-        .def(py::init<>(),
-             "Mixin class for lattice elements with horizontal/vertical alignment errors."
-        )
+    py::class_<elements::mixin::Alignment>(mx, "Alignment")
         .def_property("dx",
-            [](elements::Alignment & a) { return a.dx(); },
-            [](elements::Alignment & a, amrex::ParticleReal dx) { a.m_dx = dx; },
+            [](elements::mixin::Alignment & a) { return a.dx(); },
+            [](elements::mixin::Alignment & a, amrex::ParticleReal dx) { a.m_dx = dx; },
             "horizontal translation error in m"
         )
         .def_property("dy",
-            [](elements::Alignment & a) { return a.dy(); },
-            [](elements::Alignment & a, amrex::ParticleReal dy) { a.m_dy = dy; },
+            [](elements::mixin::Alignment & a) { return a.dy(); },
+            [](elements::mixin::Alignment & a, amrex::ParticleReal dy) { a.m_dy = dy; },
             "vertical translation error in m"
         )
         .def_property("rotation",
-            [](elements::Alignment & a) { return a.rotation(); },
-            [](elements::Alignment & a, amrex::ParticleReal rotation_degree)
+            [](elements::mixin::Alignment & a) { return a.rotation(); },
+            [](elements::mixin::Alignment & a, amrex::ParticleReal rotation_degree)
             {
-                a.m_rotation = rotation_degree * elements::Alignment::degree2rad;
+                a.m_rotation = rotation_degree * elements::mixin::Alignment::degree2rad;
             },
             "rotation error in the transverse plane in degree"
         )
     ;
 
-    py::class_<elements::LinearTransport>(mx, "LinearTransport")
-        .def(py::init<>(),
-             "Mixin class for linear transport approximation via matrices."
+    py::class_<elements::mixin::PipeAperture>(mx, "PipeAperture")
+        .def_property_readonly("aperture_x",
+            &elements::mixin::PipeAperture::aperture_x,
+            "horizontal aperture in m"
         )
+        .def_property_readonly("aperture_y",
+            &elements::mixin::PipeAperture::aperture_y,
+            "vertical aperture in m"
+        )
+    ;
+
+    py::class_<elements::mixin::LinearTransport>(mx, "LinearTransport")
         // type of map
         .def_property_readonly_static("Map6x6",
-              [](py::object /* lt */){ return py::type::of<elements::LinearTransport::Map6x6>(); },
+              [](py::object /* lt */){ return py::type::of<elements::mixin::LinearTransport::Map6x6>(); },
               "1-indexed, Fortran-ordered, 6x6 linear transport map type"
         )
         // values of the map
         //.def_property_readonly("R",
-        //      [](elements::LinearTransport const & lt) { return lt.m_transport_map; },
+        //      [](elements::mixin::LinearTransport const & lt) { return lt.m_transport_map; },
         //      "1-indexed, Fortran-ordered, 6x6 linear transport map values"
         //)
     ;
 
     // diagnostics
 
-    py::class_<diagnostics::BeamMonitor, elements::Thin> py_BeamMonitor(me, "BeamMonitor");
+    py::class_<diagnostics::BeamMonitor, elements::mixin::Thin> py_BeamMonitor(me, "BeamMonitor");
     py_BeamMonitor
         .def(py::init<std::string, std::string, std::string, int>(),
              py::arg("name"),
@@ -301,7 +295,7 @@ void init_elements(py::module& m)
 
     // beam optics
 
-    py::class_<Aperture, elements::Named, elements::Thin, elements::Alignment> py_Aperture(me, "Aperture");
+    py::class_<Aperture, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_Aperture(me, "Aperture");
     py_Aperture
         .def("__repr__",
              [](Aperture const & ap) {
@@ -313,8 +307,8 @@ void init_elements(py::module& m)
              }
         )
         .def(py::init([](
-                 amrex::ParticleReal xmax,
-                 amrex::ParticleReal ymax,
+                 amrex::ParticleReal aperture_x,
+                 amrex::ParticleReal aperture_y,
                  amrex::ParticleReal repeat_x,
                  amrex::ParticleReal repeat_y,
                  std::string const & shape,
@@ -337,10 +331,10 @@ void init_elements(py::module& m)
                  Aperture::Action const a = action == "transmit" ?
                      Aperture::Action::transmit :
                      Aperture::Action::absorb;
-                 return new Aperture(xmax, ymax, repeat_x, repeat_y, s, a, dx, dy, rotation_degree, name);
+                 return new Aperture(aperture_x, aperture_y, repeat_x, repeat_y, s, a, dx, dy, rotation_degree, name);
              }),
-             py::arg("xmax"),
-             py::arg("ymax"),
+             py::arg("aperture_x"),
+             py::arg("aperture_y"),
              py::arg("repeat_x") = 0,
              py::arg("repeat_y") = 0,
              py::arg("shape") = "rectangular",
@@ -383,14 +377,14 @@ void init_elements(py::module& m)
             },
             "action type (transmit, absorb)"
         )
-        .def_property("xmax",
-            [](Aperture & ap) { return ap.m_xmax; },
-            [](Aperture & ap, amrex::ParticleReal xmax) { ap.m_xmax = xmax; },
+        .def_property("aperture_x",
+            [](Aperture & ap) { return ap.m_aperture_x; },
+            [](Aperture & ap, amrex::ParticleReal aperture_x) { ap.m_aperture_x = aperture_x; },
             "maximum horizontal coordinate"
         )
-        .def_property("ymax",
-            [](Aperture & ap) { return ap.m_ymax; },
-            [](Aperture & ap, amrex::ParticleReal ymax) { ap.m_ymax = ymax; },
+        .def_property("aperture_y",
+            [](Aperture & ap) { return ap.m_aperture_y; },
+            [](Aperture & ap, amrex::ParticleReal aperture_y) { ap.m_aperture_y = aperture_y; },
             "maximum vertical coordinate"
         )
         .def_property("repeat_x",
@@ -406,7 +400,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Aperture);
 
-    py::class_<ChrDrift, elements::Named, elements::Thick, elements::Alignment> py_ChrDrift(me, "ChrDrift");
+    py::class_<ChrDrift, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ChrDrift(me, "ChrDrift");
     py_ChrDrift
         .def("__repr__",
              [](ChrDrift const & chr_drift) {
@@ -421,6 +415,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -428,6 +424,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A Drift with chromatic effects included."
@@ -435,7 +433,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ChrDrift);
 
-    py::class_<ChrQuad, elements::Named, elements::Thick, elements::Alignment> py_ChrQuad(me, "ChrQuad");
+    py::class_<ChrQuad, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ChrQuad(me, "ChrQuad");
     py_ChrQuad
         .def("__repr__",
              [](ChrQuad const & chr_quad) {
@@ -453,6 +451,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -462,6 +462,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A Quadrupole magnet with chromatic effects included."
@@ -479,7 +481,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ChrQuad);
 
-    py::class_<ChrPlasmaLens, elements::Named, elements::Thick, elements::Alignment> py_ChrPlasmaLens(me, "ChrPlasmaLens");
+    py::class_<ChrPlasmaLens, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ChrPlasmaLens(me, "ChrPlasmaLens");
     py_ChrPlasmaLens
         .def("__repr__",
              [](ChrPlasmaLens const & chr_pl_lens) {
@@ -497,6 +499,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -506,6 +510,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "An active Plasma Lens with chromatic effects included."
@@ -523,7 +529,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ChrPlasmaLens);
 
-    py::class_<ChrAcc, elements::Named, elements::Thick, elements::Alignment> py_ChrAcc(me, "ChrAcc");
+    py::class_<ChrAcc, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment> py_ChrAcc(me, "ChrAcc");
     py_ChrAcc
         .def("__repr__",
              [](ChrAcc const & chr_acc) {
@@ -542,6 +548,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -551,6 +559,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A region of Uniform Acceleration, with chromatic effects included."
@@ -568,7 +578,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ChrAcc);
 
-    py::class_<ConstF, elements::Named, elements::Thick, elements::Alignment> py_ConstF(me, "ConstF");
+    py::class_<ConstF, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ConstF(me, "ConstF");
     py_ConstF
         .def("__repr__",
              [](ConstF const & constf) {
@@ -589,6 +599,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -599,6 +611,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A linear Constant Focusing element."
@@ -621,7 +635,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ConstF);
 
-    py::class_<DipEdge, elements::Named, elements::Thin, elements::Alignment> py_DipEdge(me, "DipEdge");
+    py::class_<DipEdge, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_DipEdge(me, "DipEdge");
     py_DipEdge
         .def("__repr__",
              [](DipEdge const & dip_edge) {
@@ -677,7 +691,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_DipEdge);
 
-    py::class_<Drift, elements::Named, elements::Thick, elements::Alignment> py_Drift(me, "Drift");
+    py::class_<Drift, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_Drift(me, "Drift");
     py_Drift
         .def("__repr__",
              [](Drift const & drift) {
@@ -692,6 +706,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -699,6 +715,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A drift."
@@ -706,7 +724,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Drift);
 
-    py::class_<ExactDrift, elements::Named, elements::Thick, elements::Alignment> py_ExactDrift(me, "ExactDrift");
+    py::class_<ExactDrift, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ExactDrift(me, "ExactDrift");
     py_ExactDrift
         .def("__repr__",
              [](ExactDrift const & exact_drift) {
@@ -721,6 +739,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -728,6 +748,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A Drift using the exact nonlinear map."
@@ -735,7 +757,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ExactDrift);
 
-    py::class_<ExactSbend, elements::Named, elements::Thick, elements::Alignment> py_ExactSbend(me, "ExactSbend");
+    py::class_<ExactSbend, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_ExactSbend(me, "ExactSbend");
     py_ExactSbend
         .def("__repr__",
              [](ExactSbend const & exact_sbend) {
@@ -754,6 +776,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -763,6 +787,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "An ideal sector bend using the exact nonlinear map.  When B = 0, the reference bending radius is defined by r0 = length / (angle in rad), corresponding to a magnetic field of B = rigidity / r0; otherwise the reference bending radius is defined by r0 = rigidity / B."
@@ -780,7 +806,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ExactSbend);
 
-    py::class_<Kicker, elements::Named, elements::Thin, elements::Alignment> py_Kicker(me, "Kicker");
+    py::class_<Kicker, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_Kicker(me, "Kicker");
     py_Kicker
         .def("__repr__",
              [](Kicker const & kicker) {
@@ -832,7 +858,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Kicker);
 
-    py::class_<Multipole, elements::Named, elements::Thin, elements::Alignment> py_Multipole(me, "Multipole");
+    py::class_<Multipole, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_Multipole(me, "Multipole");
     py_Multipole
         .def("__repr__",
              [](Multipole const & multipole) {
@@ -881,7 +907,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Multipole);
 
-    py::class_<Empty, elements::Thin> py_Empty(me, "Empty");
+    py::class_<Empty, elements::mixin::Thin> py_Empty(me, "Empty");
     py_Empty
         .def("__repr__",
              [](Empty const & /* empty */) {
@@ -894,7 +920,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Empty);
 
-    py::class_<Marker, elements::Named, elements::Thin> py_Marker(me, "Marker");
+    py::class_<Marker, elements::mixin::Named, elements::mixin::Thin> py_Marker(me, "Marker");
     py_Marker
             .def("__repr__",
                  [](Marker const & marker) {
@@ -907,7 +933,7 @@ void init_elements(py::module& m)
             ;
     register_beamoptics_push(py_Marker);
 
-    py::class_<NonlinearLens, elements::Named, elements::Thin, elements::Alignment> py_NonlinearLens(me, "NonlinearLens");
+    py::class_<NonlinearLens, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_NonlinearLens(me, "NonlinearLens");
     py_NonlinearLens
         .def("__repr__",
              [](NonlinearLens const & nl) {
@@ -947,7 +973,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_NonlinearLens);
 
-    py::class_<PlaneXYRot, elements::Named, elements::Thin, elements::Alignment> py_PlaneXYRot(me, "PlaneXYRot");
+    py::class_<PlaneXYRot, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_PlaneXYRot(me, "PlaneXYRot");
     py_PlaneXYRot
         .def("__repr__",
              [](PlaneXYRot const & plane_xyrot) {
@@ -979,7 +1005,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_PlaneXYRot);
 
-    py::class_<Programmable, elements::Named>(me, "Programmable", py::dynamic_attr())
+    py::class_<Programmable, elements::mixin::Named>(me, "Programmable", py::dynamic_attr())
         .def("__repr__",
              [](Programmable const & prg) {
                  return element_name(
@@ -1034,7 +1060,7 @@ void init_elements(py::module& m)
         )
     ;
 
-    py::class_<Quad, elements::Named, elements::Thick, elements::Alignment> py_Quad(me, "Quad");
+    py::class_<Quad, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_Quad(me, "Quad");
     py_Quad
         .def("__repr__",
              [](Quad const & quad) {
@@ -1051,6 +1077,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -1059,6 +1087,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "A Quadrupole magnet."
@@ -1071,7 +1101,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Quad);
 
-    py::class_<RFCavity, elements::Named, elements::Thick, elements::Alignment> py_RFCavity(me, "RFCavity");
+    py::class_<RFCavity, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_RFCavity(me, "RFCavity");
     py_RFCavity
         .def("__repr__",
              [](RFCavity const & rfc) {
@@ -1094,6 +1124,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 int,
                 std::optional<std::string>
@@ -1107,6 +1139,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("mapsteps") = 1,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
@@ -1137,7 +1171,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_RFCavity);
 
-    py::class_<Sbend, elements::Named, elements::Thick, elements::Alignment> py_Sbend(me, "Sbend");
+    py::class_<Sbend, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_Sbend(me, "Sbend");
     py_Sbend
         .def("__repr__",
              [](Sbend const & sbend) {
@@ -1154,6 +1188,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -1162,6 +1198,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "An ideal sector bend."
@@ -1174,7 +1212,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Sbend);
 
-    py::class_<CFbend, elements::Named, elements::Thick, elements::Alignment> py_CFbend(me, "CFbend");
+    py::class_<CFbend, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_CFbend(me, "CFbend");
     py_CFbend
         .def("__repr__",
              [](CFbend const & cfbend) {
@@ -1193,6 +1231,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -1202,6 +1242,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "An ideal combined function bend (sector bend with quadrupole component)."
@@ -1219,7 +1261,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_CFbend);
 
-    py::class_<Buncher, elements::Named, elements::Thin, elements::Alignment> py_Buncher(me, "Buncher");
+    py::class_<Buncher, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_Buncher(me, "Buncher");
     py_Buncher
         .def("__repr__",
              [](Buncher const & buncher) {
@@ -1259,7 +1301,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Buncher);
 
-    py::class_<ShortRF, elements::Named, elements::Thin, elements::Alignment> py_ShortRF(me, "ShortRF");
+    py::class_<ShortRF, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_ShortRF(me, "ShortRF");
     py_ShortRF
         .def("__repr__",
              [](ShortRF const & short_rf) {
@@ -1307,7 +1349,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ShortRF);
 
-    py::class_<SoftSolenoid, elements::Named, elements::Thick, elements::Alignment> py_SoftSolenoid(me, "SoftSolenoid");
+    py::class_<SoftSolenoid, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_SoftSolenoid(me, "SoftSolenoid");
     py_SoftSolenoid
         .def("__repr__",
              [](SoftSolenoid const & soft_sol) {
@@ -1327,6 +1369,8 @@ void init_elements(py::module& m)
                  amrex::ParticleReal,
                  amrex::ParticleReal,
                  amrex::ParticleReal,
+                 amrex::ParticleReal,
+                 amrex::ParticleReal,
                  int,
                  int,
                  std::optional<std::string>
@@ -1339,6 +1383,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("mapsteps") = 1,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
@@ -1364,7 +1410,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_SoftSolenoid);
 
-    py::class_<Sol, elements::Named, elements::Thick, elements::Alignment> py_Sol(me, "Sol");
+    py::class_<Sol, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_Sol(me, "Sol");
     py_Sol
         .def("__repr__",
              [](Sol const & sol) {
@@ -1381,6 +1427,8 @@ void init_elements(py::module& m)
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
+                amrex::ParticleReal,
+                amrex::ParticleReal,
                 int,
                 std::optional<std::string>
              >(),
@@ -1389,6 +1437,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
              "An ideal hard-edge Solenoid magnet."
@@ -1401,7 +1451,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_Sol);
 
-    py::class_<PRot, elements::Named, elements::Thin> py_PRot(me, "PRot");
+    py::class_<PRot, elements::mixin::Named, elements::mixin::Thin> py_PRot(me, "PRot");
     py_PRot
         .def("__repr__",
              [](PRot const & prot) {
@@ -1435,7 +1485,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_PRot);
 
-    py::class_<SoftQuadrupole, elements::Named, elements::Thick, elements::Alignment> py_SoftQuadrupole(me, "SoftQuadrupole");
+    py::class_<SoftQuadrupole, elements::mixin::Named, elements::mixin::Thick, elements::mixin::Alignment, elements::mixin::PipeAperture> py_SoftQuadrupole(me, "SoftQuadrupole");
     py_SoftQuadrupole
         .def("__repr__",
              [](SoftQuadrupole const & soft_quad) {
@@ -1454,6 +1504,8 @@ void init_elements(py::module& m)
                  amrex::ParticleReal,
                  amrex::ParticleReal,
                  amrex::ParticleReal,
+                 amrex::ParticleReal,
+                 amrex::ParticleReal,
                  int,
                  int,
                  std::optional<std::string>
@@ -1465,6 +1517,8 @@ void init_elements(py::module& m)
              py::arg("dx") = 0,
              py::arg("dy") = 0,
              py::arg("rotation") = 0,
+             py::arg("aperture_x") = 0,
+             py::arg("aperture_y") = 0,
              py::arg("mapsteps") = 1,
              py::arg("nslice") = 1,
              py::arg("name") = py::none(),
@@ -1485,7 +1539,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_SoftQuadrupole);
 
-    py::class_<ThinDipole, elements::Named, elements::Thin, elements::Alignment> py_ThinDipole(me, "ThinDipole");
+    py::class_<ThinDipole, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_ThinDipole(me, "ThinDipole");
     py_ThinDipole
         .def("__repr__",
              [](ThinDipole const & thin_dp) {
@@ -1525,7 +1579,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_ThinDipole);
 
-    py::class_<TaperedPL, elements::Named, elements::Thin, elements::Alignment> py_TaperedPL(me, "TaperedPL");
+    py::class_<TaperedPL, elements::mixin::Named, elements::mixin::Thin, elements::mixin::Alignment> py_TaperedPL(me, "TaperedPL");
     py_TaperedPL
         .def("__repr__",
              [](TaperedPL const & taperedpl) {
@@ -1579,7 +1633,7 @@ void init_elements(py::module& m)
     ;
     register_beamoptics_push(py_TaperedPL);
 
-    py::class_<LinearMap, elements::Named, elements::Alignment, elements::LinearTransport> py_LinearMap(me, "LinearMap");
+    py::class_<LinearMap, elements::mixin::Named, elements::mixin::Alignment, elements::mixin::LinearTransport> py_LinearMap(me, "LinearMap");
     py_LinearMap
         .def("__repr__",
              [](LinearMap const & linearmap) {
@@ -1589,7 +1643,7 @@ void init_elements(py::module& m)
              }
         )
         .def(py::init<
-                elements::LinearTransport::Map6x6,
+                elements::mixin::LinearTransport::Map6x6,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
                 amrex::ParticleReal,
@@ -1606,7 +1660,7 @@ void init_elements(py::module& m)
         )
         .def_property("R",
             [](LinearMap & linearmap) { return linearmap.m_transport_map; },
-            [](LinearMap & linearmap, elements::LinearTransport::Map6x6 R) { linearmap.m_transport_map = R; },
+            [](LinearMap & linearmap, elements::mixin::LinearTransport::Map6x6 R) { linearmap.m_transport_map = R; },
             "linear map as a 6x6 transport matrix"
         )
         .def_property("ds",