Repository: atcollab/at (language: python)

Issue:
Introduce an RF frequency shift input to specify off-momentum computation
Wherever there is an input for `dp` or `dct` to specify off-momentum computation, we can now alternatively use `df`, the RF frequency deviation from nominal. This off-momentum specification is closer to the way it is obtained in a real machine. So we can use one of these 3 keywords:

- dp=* (off-momentum)
- dct=* (path lengthening)
- df=* (RF frequency shift)

At this time, the following functions accept these inputs:

- `atsetcavity`
- `findorbit`(4D),  `findorbit4`, `findsyncorbit`
- `findm44`
- `atlinopt2`, `atlinopt4`, `atlinopt6`
- `tunechrom`

Missing functions will be added later (`atx`, `atsummary` and `ringpara` are pending in #500)


Discussion / hints:
Dear Laurent, 
I find this is a really nice addition. 
I will not be able to review the pull request till January 2023. May be @carmignani could review the pull request instead of me?
all the best
Simone

Fix patch:
diff --git a/docs/conf.py b/docs/conf.py
index 5791ec8e6..7e922a91b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -57,6 +57,10 @@
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
 exclude_patterns = ["README.rst", "**/*.so"]
+rst_prolog = """
+.. role:: pycode(code)
+   :language: python
+"""
 autodoc_default_options = {
         # Make sure that any autodoc declarations show the right members
         "members": True,
diff --git a/pyat/at/lattice/lattice_object.py b/pyat/at/lattice/lattice_object.py
index 266cab620..4e8685516 100644
--- a/pyat/at/lattice/lattice_object.py
+++ b/pyat/at/lattice/lattice_object.py
@@ -410,7 +410,7 @@ def attrs_filter(self, params: dict, elem_filter: Filter, *args) \
         Parameters:
             params:         Dictionary of Lattice attributes
             elem_filter:    Element filter
-            *args:          Arguments provided to ``elem_filter``
+            *args:          Arguments provided to *elem_filter*
         """
         for key in self._std_attributes:
             try:
@@ -421,7 +421,8 @@ def attrs_filter(self, params: dict, elem_filter: Filter, *args) \
 
     @property
     def s_range(self) -> Union[None, Tuple[float, float]]:
-        """Range of interest: (s_min, s_max). ``None`` means the full cell."""
+        """Range of interest: (s_min, s_max). :py:obj:`None` means
+          the full cell."""
         try:
             return self._s_range
         except AttributeError:
@@ -471,14 +472,59 @@ def energy(self, energy: float):
         # self._energy = numpy.array(energy, dtype=float)
         self._energy = energy
 
+    @property
+    def cell_length(self) -> float:
+        """Cell length (1 cell) [m]
+
+        See Also:
+
+            :py:meth:`circumference`.
+        """
+        return self.get_s_pos(len(self))[0]
+
+    @property
+    def cell_revolution_frequency(self) -> float:
+        """Revolution frequency for 1 cell [Hz]
+
+        See Also:
+
+            :py:meth:`revolution_frequency`.
+        """
+        beta = self.beta
+        return beta * clight / self.cell_length
+
+    @property
+    def cell_harmnumber(self) -> float:
+        """Harmonic number per cell
+
+        See Also:
+
+            :py:meth:`harmonic_number`.
+        """
+        try:
+            return self._cell_harmnumber
+        except AttributeError:
+            raise AttributeError('harmonic_number undefined: '
+                                 'No cavity found in the lattice') from None
+
     @property
     def circumference(self) -> float:
-        """Ring circumference (full self) [m]"""
+        """Ring circumference (full ring) [m]
+
+        See Also:
+
+            :py:meth:`cell_length`.
+        """
         return self.periodicity * self.get_s_pos(len(self))[0]
 
     @property
     def revolution_frequency(self) -> float:
-        """Revolution frequency (fullring) [Hz]"""
+        """Revolution frequency (full ring) [Hz]
+
+        See Also:
+
+            :py:meth:`cell_revolution_frequency`.
+        """
         beta = self.beta
         return beta * clight / self.circumference
 
@@ -576,7 +622,12 @@ def nbunch(self) -> int:
 
     @property
     def harmonic_number(self) -> int:
-        """Ring harmonic number (full self)"""
+        """Ring harmonic number (full ring)
+
+        See Also:
+
+            :py:meth:`cell_harmnumber`.
+        """
         try:
             return int(self.periodicity * self._cell_harmnumber)
         except AttributeError:
@@ -671,7 +722,7 @@ def modify_elements(self, elem_modify: Callable,
 
         Parameters:
             elem_modify : element selection function.
-              If ``elem_modify(elem)`` returns ``None``, the element is
+              If ``elem_modify(elem)`` returns :py:obj:`None`, the element is
               unchanged. Otherwise, ``elem_modify(elem)`` must return a
               dictionary of attribute name and values, to be set to elem.
             copy:         If True, return a shallow copy of the lattice.
@@ -679,7 +730,8 @@ def modify_elements(self, elem_modify: Callable,
               If False, the modification is done in-place
 
         Returns:
-            newring:    New lattice if copy is True, None if copy is False
+            newring:    New lattice if copy is :py:obj:`True`, :py:obj:`None`
+              if copy is :py:obj:`False`
 
         Keyword Arguments:
         """
@@ -821,7 +873,7 @@ def enable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
 
         Keyword arguments:
             all_pass:                   PassMethod overloading the default
-              values for all elements (``None`` or 'auto')
+              values for all elements (:py:obj:`None` or 'auto')
             cavity_pass='auto':         PassMethod set on cavities
             dipole_pass='auto':         PassMethod set on dipoles
             quadrupole_pass='auto':     PassMethod set on quadrupoles
@@ -834,7 +886,7 @@ def enable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
               elements (:py:class:`.WakeElement`,...)
             diffusion_pass='auto':      PassMethod set on
               :py:class:`.QuantumDiffusion`
-            copy=False: If ``False``, the modification is done in-place,
+            copy=False: If :py:obj:`False`, the modification is done in-place,
               If ``True``, return a shallow copy of the lattice. Only the
               radiating elements are copied with PassMethod modified.
 
@@ -846,7 +898,7 @@ def enable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
 
         For PassMethod names, the convention is:
 
-        * ``None``:        No change
+        * :py:obj:`None`:        No change
         * 'auto':          Use the default conversion (replace \*Pass by
           \*RadPass)
         * anything else:   set as the new PassMethod
@@ -919,7 +971,7 @@ def disable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
 
         Keyword arguments:
             all_pass:                   PassMethod overloading the default
-              values for all elements (``None`` or 'auto')
+              values for all elements (:py:obj:`None` or 'auto')
             cavity_pass='auto':         PassMethod set on cavities
             dipole_pass='auto':         PassMethod set on dipoles
             quadrupole_pass=auto:       PassMethod set on quadrupoles
@@ -932,7 +984,7 @@ def disable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
               elements (:py:class:`.WakeElement`,...)
             diffusion_pass='auto':      PassMethod set on
               :py:class:`.QuantumDiffusion`
-            copy=False: If ``False``, the modification is done in-place,
+            copy=False: If :py:obj:`False`, the modification is done in-place,
               If ``True``, return a shallow copy of the lattice. Only the
               radiating elements are copied with PassMethod modified.
 
@@ -944,7 +996,7 @@ def disable_6d(self, *args, **kwargs) -> Optional["Lattice"]:
 
         For PassMethod names, the convention is:
 
-        * ``None``:        no change
+        * :py:obj:`None`:        no change
         * 'auto':          Use the default conversion (replace \*RadPass by
           \*Pass)
         * anything else:   set as the new PassMethod
diff --git a/pyat/at/lattice/utils.py b/pyat/at/lattice/utils.py
index 438230ca1..97a813891 100644
--- a/pyat/at/lattice/utils.py
+++ b/pyat/at/lattice/utils.py
@@ -97,8 +97,8 @@ def radiation_decorator(func):
         def wrapper(ring, *args, **kwargs):
             ringrad = getattr(ring, 'is_6d', is_6d)
             if ringrad != is_6d:
-                raise AtError('{0} needs radiation {1}'.format(
-                    func.__name__, 'ON' if is_6d else 'OFF'))
+                raise AtError('{0} needs "ring.is_6d" {1}'.format(
+                    func.__name__, is_6d))
             return func(ring, *args, **kwargs)
         return wrapper
     return radiation_decorator
diff --git a/pyat/at/physics/linear.py b/pyat/at/physics/linear.py
index 8dc18f49b..325a2f9cc 100644
--- a/pyat/at/physics/linear.py
+++ b/pyat/at/physics/linear.py
@@ -3,12 +3,12 @@
 """
 import numpy
 from math import sqrt, pi, sin, cos, atan2
-from typing import Optional, Callable
+from typing import Callable
 import warnings
 from scipy.linalg import solve
 from ..constants import clight
 from ..lattice import DConstant, get_s_pos, Refpts
-from ..lattice import AtWarning, Lattice, check_radiation
+from ..lattice import AtWarning, Lattice, check_6d
 from ..tracking import lattice_pass
 from .orbit import Orbit, find_orbit4, find_orbit6
 from .matrix import find_m44, find_m66
@@ -86,7 +86,7 @@ def _closure(m22):
 # noinspection PyShadowingNames,PyPep8Naming
 def _tunes(ring, **kwargs):
     """"""
-    if ring.radiation:
+    if ring.is_6d:
         mt, _ = find_m66(ring, **kwargs)
     else:
         mt, _ = find_m44(ring, **kwargs)
@@ -245,9 +245,10 @@ def propagate6(ri, phi, ai):
 
 
 # noinspection PyShadowingNames,PyPep8Naming
-def _linopt(ring: Lattice, analyze, refpts=None, dp=None, dct=None, orbit=None,
-            twiss_in=None, get_chrom=False, get_w=False, keep_lattice=False,
-            mname='M', add0=(), adds=(), cavpts=None, **kwargs):
+def _linopt(ring: Lattice, analyze, refpts=None, dp=None, dct=None, df=None,
+            orbit=None, twiss_in=None, get_chrom=False, get_w=False,
+            keep_lattice=False, mname='M', add0=(), adds=(), cavpts=None,
+            **kwargs):
     """"""
     def build_sigma(twin, orbit):
         """Build the initial distribution at entrance of the transfer line"""
@@ -313,7 +314,7 @@ def unwrap(mu):
     dp_step = kwargs.get('DPStep', DConstant.DPStep)
     addtype = kwargs.pop('addtype', [])
 
-    if ring.radiation:
+    if ring.is_6d:
         get_matrix = find_m66
         get_orbit = find_orbit6
     else:
@@ -322,7 +323,7 @@ def unwrap(mu):
 
     if twiss_in is None:        # Ring
         if orbit is None:
-            orbit, _ = get_orbit(ring, dp=dp, dct=dct,
+            orbit, _ = get_orbit(ring, dp=dp, dct=dct, df=df,
                                  keep_lattice=keep_lattice, **kwargs)
             keep_lattice = True
         # Get 1-turn transfer matrix
@@ -367,7 +368,7 @@ def unwrap(mu):
         datas = (orbs, ms, spos)
         if get_chrom or get_w:
             f0 = ring.get_rf_frequency(cavpts=cavpts)
-            df = dp_step * ring.radiation_off(copy=True).slip_factor * f0
+            df = dp_step * ring.disable_6d(copy=True).slip_factor * f0
             rgup = ring.set_rf_frequency(f0 + 0.5*df, cavpts=cavpts, copy=True)
             rgdn = ring.set_rf_frequency(f0 - 0.5*df, cavpts=cavpts, copy=True)
             o0up, _ = get_orbit(rgup, guess=orb0, **kwargs)
@@ -435,7 +436,7 @@ def unwrap(mu):
     return elemdata0, beamdata, elemdata
 
 
-@check_radiation(False)
+@check_6d(False)
 def linopt2(ring: Lattice, *args, **kwargs):
     r"""Linear analysis of an uncoupled lattice
 
@@ -453,10 +454,11 @@ def linopt2(ring: Lattice, *args, **kwargs):
           2. an ordered list of such integers without duplicates,
           3. a numpy array of booleans of maximum length len(ring)+1,
              where selected elements are :py:obj:`True`.
-        dp (Optional[float]):   Momentum deviation.
-        dct (Optional[float]):  Path lengthening. If specified, ``dp`` is
-          ignored and the off-momentum is deduced from the path lengthening.
-        orbit (Orbit): Avoids looking for the closed orbit if is
+        dp (float):             Momentum deviation. Defaults to :py:obj:`None`
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation of RF frequency. Defaults to
+          :py:obj:`None`
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
         get_chrom (bool):       Compute chromaticities. Needs computing
           the tune at 2 different momentum deviations around the central one.
@@ -486,7 +488,7 @@ def linopt2(ring: Lattice, *args, **kwargs):
     Returns:
         elemdata0:      Linear optics data at the entrance of the ring
         ringdata:       Lattice properties
-        elemdata:       Linear optics at the points refered to by ``refpts``,
+        elemdata:       Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
 
     **elemdata** is a record array with fields:
@@ -501,19 +503,18 @@ def linopt2(ring: Lattice, *args, **kwargs):
     **alpha**           :math:`\left[ \alpha_x,\alpha_y \right]` vector
     **mu**              :math:`\left[ \mu_x,\mu_y \right]`, betatron phase
                         (modulo :math:`2\pi`)
-    **W**               :math:`\left[ W_x,W_y \right]` only if ``get_w``
+    **W**               :math:`\left[ W_x,W_y \right]` only if *get_w*
                         is :py:obj:`True`: chromatic amplitude function
     ================    ===================================================
 
     All values given at the entrance of each element specified in refpts.
-    Field values can be obtained with either
-    ``lindata['idx']`` or ``lindata.idx``
+    Field values can be obtained with either *lindata['idx']* or *lindata.idx*
 
     **ringdata** is a record array with fields:
 
     =================   ======
     **tune**            Fractional tunes
-    **chromaticity**    Chromaticities, only computed if ``get_chrom`` is
+    **chromaticity**    Chromaticities, only computed if *get_chrom* is
                         :py:obj:`True`
     =================   ======
 
@@ -531,7 +532,7 @@ def linopt2(ring: Lattice, *args, **kwargs):
     return _linopt(ring, _analyze2, *args, **kwargs)
 
 
-@check_radiation(False)
+@check_6d(False)
 def linopt4(ring: Lattice, *args, **kwargs):
     r"""Linear analysis of a H/V coupled lattice
 
@@ -551,10 +552,11 @@ def linopt4(ring: Lattice, *args, **kwargs):
           2. an ordered list of such integers without duplicates,
           3. a numpy array of booleans of maximum length len(ring)+1,
              where selected elements are :py:obj:`True`.
-        dp (Optional[float]):   Momentum deviation.
-        dct (Optional[float]):  Path lengthening. If specified, ``dp`` is
-          ignored and the off-momentum is deduced from the path lengthening.
-        orbit (Orbit): Avoids looking for the closed orbit if is
+        dp (float):             Momentum deviation. Defaults to :py:obj:`None`
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation of RF frequency. Defaults to
+          :py:obj:`None`
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
         get_chrom (bool):       Compute chromaticities. Needs computing
           the tune at 2 different momentum deviations around the central one.
@@ -577,13 +579,6 @@ def linopt4(ring: Lattice, *args, **kwargs):
           dispersion
             Optional (6,) array, default 0
 
-          alpha, beta
-            mandatory (2,) arrays
-          closed_orbit
-            Optional (6,) array, default 0
-          dispersion
-            Optional (6,) array, default 0
-
           If present, the attribute **R** will be used, otherwise the
           attributes **alpha** and **beta** will be used. All other attributes
           are ignored.
@@ -591,7 +586,7 @@ def linopt4(ring: Lattice, *args, **kwargs):
     Returns:
         elemdata0:      Linear optics data at the entrance of the ring
         ringdata:       Lattice properties
-        elemdata:       Linear optics at the points refered to by ``refpts``,
+        elemdata:       Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
 
     **elemdata** is a record array with fields:
@@ -608,19 +603,19 @@ def linopt4(ring: Lattice, *args, **kwargs):
                         (modulo :math:`2\pi`)
     **gamma**           gamma parameter of the transformation to
                         eigenmodes [7]_
-    **W**               :math:`\left[ W_x,W_y \right]` only if ``get_w``
+    **W**               :math:`\left[ W_x,W_y \right]` only if *get_w*
                         is :py:obj:`True`: chromatic amplitude function
     ================    ===================================================
 
     All values given at the entrance of each element specified in refpts.
     Field values can be obtained with either
-    ``lindata['idx']`` or ``lindata.idx``
+    *lindata['idx']* or *lindata.idx*
 
     **ringdata** is a record array with fields:
 
     =================   ======
     **tune**            Fractional tunes
-    **chromaticity**    Chromaticities, only computed if ``get_chrom`` is
+    **chromaticity**    Chromaticities, only computed if *get_chrom* is
                         :py:obj:`True`
     =================   ======
 
@@ -643,8 +638,8 @@ def linopt6(ring: Lattice, *args, **kwargs):
 
     For circular machines, :py:func:`linopt6` analyses
 
-    * the 4x4 1-turn transfer matrix if radiation is OFF, or
-    * the 6x6 1-turn transfer matrix if radiation is ON.
+    * the 4x4 1-turn transfer matrix if *ring* is 4D, or
+    * the 6x6 1-turn transfer matrix if  *ring* is 6D.
 
     For a transfer line, The "twiss_in" intput must contain either:
 
@@ -665,10 +660,11 @@ def linopt6(ring: Lattice, *args, **kwargs):
           2. an ordered list of such integers without duplicates,
           3. a numpy array of booleans of maximum length len(ring)+1,
              where selected elements are :py:obj:`True`.
-        dp (Optional[float]):   Momentum deviation.
-        dct (Optional[float]):  Path lengthening. If specified, ``dp`` is
-          ignored and the off-momentum is deduced from the path lengthening.
-        orbit (Orbit): Avoids looking for the closed orbit if is
+        dp (float):             Momentum deviation. Defaults to :py:obj:`None`
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation of RF frequency. Defaults to
+          :py:obj:`None`
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
         get_chrom (bool):       Compute chromaticities. Needs computing
           the tune at 2 different momentum deviations around the central one.
@@ -694,12 +690,12 @@ def linopt6(ring: Lattice, *args, **kwargs):
           If present, the attribute **R** will be used, otherwise the
           attributes **alpha** and **beta** will be used. All other attributes
           are ignored.
-        cavpts (Optional[Refpts]):  Cavity location for off-momentum tuning
+        cavpts (Refpts):        Cavity location for off-momentum tuning
 
     Returns:
         elemdata0:      Linear optics data at the entrance of the ring
         ringdata:       Lattice properties
-        elemdata:       Linear optics at the points refered to by ``refpts``,
+        elemdata:       Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
 
     **elemdata** is a record array with fields:
@@ -716,19 +712,19 @@ def linopt6(ring: Lattice, *args, **kwargs):
     **alpha**           :math:`\left[ \alpha_x,\alpha_y \right]` vector
     **mu**              :math:`\left[ \mu_x,\mu_y \right]`, betatron phase
                         (modulo :math:`2\pi`)
-    **W**               :math:`\left[ W_x,W_y \right]` only if ``get_w``
+    **W**               :math:`\left[ W_x,W_y \right]` only if *get_w*
                         is :py:obj:`True`: chromatic amplitude function
     ================    ===================================================
 
     All values given at the entrance of each element specified in refpts.
     Field values can be obtained with either
-    ``lindata['idx']`` or ``lindata.idx``
+    *lindata['idx']* or *lindata.idx*
 
     **ringdata** is a record array with fields:
 
     =================   ======
     **tune**            Fractional tunes
-    **chromaticity**    Chromaticities, only computed if ``get_chrom`` is
+    **chromaticity**    Chromaticities, only computed if *get_chrom* is
                         :py:obj:`True`
     **damping_time**    Damping times [s] (only if radiation is ON)
     =================   ======
@@ -747,42 +743,43 @@ def linopt6(ring: Lattice, *args, **kwargs):
 def linopt_auto(ring: Lattice, *args, **kwargs):
     """
     This is a convenience function to automatically switch to the faster
-    :py:func:`linopt2` in case coupled=:py:obj:`False` **and**
-    ring.radiation=:py:obj:`False`. Otherwise the default :py:func:`linopt6`
-    is used
+    :py:func:`linopt2` in case the *coupled* keyword argument is
+    :py:obj:`False` **and** ring.is_6d is :py:obj:`False`.
+    Otherwise the default :py:func:`linopt6` is used
 
     Parameters: Same as :py:func:`.linopt2` or :py:func:`.linopt6`
 
     Keyword Args;
         coupled (bool):     If set to :py:obj:`False`, H/V coupling
-          will be ignored to simplify the calculation, needs radiation OFF
+          will be ignored to simplify the calculation (needs ring.is_6d
+          :py:obj:`False`)
 
 
     Returns:
         elemdata0:      Linear optics data at the entrance of the ring
         ringdata:       Lattice properties
-        elemdata:       Linear optics at the points refered to by ``refpts``,
+        elemdata:       Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
 
     Warning:
         The output varies depending whether :py:func:`.linopt2` or
         :py:func:`.linopt6` is called. To be used with care!
     """
-    if not (kwargs.pop('coupled', True) or ring.radiation):
+    if not (kwargs.pop('coupled', True) or ring.is_6d):
         return linopt2(ring, *args, **kwargs)
     else:
         return linopt6(ring, *args, **kwargs)
 
 
-def get_optics(ring: Lattice, refpts: Optional[Refpts] = None,
-               dp: Optional[float] = None,
+def get_optics(ring: Lattice, refpts: Refpts = None,
+               dp: float = None,
                method: Callable = linopt6,
                **kwargs):
     """Linear analysis of a fully coupled lattice
 
     Parameters:
-        ring:   Lattice description.
-        refpts (Optional[Refpts]): Elements at which data is returned.
+        ring:                   Lattice description.
+        refpts:                 Elements at which data is returned.
           It can be:
 
           1. an integer in the range [-len(ring), len(ring)-1]
@@ -792,8 +789,8 @@ def get_optics(ring: Lattice, refpts: Optional[Refpts] = None,
           2. an ordered list of such integers without duplicates,
           3. a numpy array of booleans of maximum length len(ring)+1,
              where selected elements are :py:obj:`True`.
-        dp (Optional[float]):   Momentum deviation.
-        method (Callable):  Method for linear optics:
+        dp:                     Momentum deviation.
+        method (Callable):      Method for linear optics:
 
           :py:obj:`~.linear.linopt2`: no longitudinal motion, no H/V coupling,
 
@@ -804,9 +801,10 @@ def get_optics(ring: Lattice, refpts: Optional[Refpts] = None,
           motion, normal mode analysis
 
     Keyword Args:
-        dct (Optional[float]):  Path lengthening. If specified, ``dp`` is
-          ignored and the off-momentum is deduced from the path lengthening.
-        orbit (Orbit): Avoids looking for the closed orbit if is
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation of RF frequency. Defaults to
+          :py:obj:`None`
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
         get_chrom (bool):       Compute chromaticities. Needs computing
           the tune at 2 different momentum deviations around the central one.
@@ -836,7 +834,7 @@ def get_optics(ring: Lattice, refpts: Optional[Refpts] = None,
     Returns:
         elemdata0:      Linear optics data at the entrance of the ring
         ringdata:       Lattice properties
-        elemdata:       Linear optics at the points refered to by ``refpts``,
+        elemdata:       Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
 
     Warning:
@@ -847,16 +845,15 @@ def get_optics(ring: Lattice, refpts: Optional[Refpts] = None,
 
 
 # noinspection PyPep8Naming
-@check_radiation(False)
-def linopt(ring: Lattice, dp: Optional[float] = 0.0,
-           refpts: Optional[Refpts] = None,
+@check_6d(False)
+def linopt(ring: Lattice, dp: float = 0.0, refpts: Refpts = None,
            get_chrom: bool = False, **kwargs):
     """Linear analysis of a H/V coupled lattice (deprecated)
 
-    PARAMETERS
-        lattice         lattice description.
-        dp=0.0          momentum deviation.
-        refpts=None     elements at which data is returned. It can be:
+    Parameters:
+        ring:           lattice description.
+        dp:             momentum deviation.
+        refpts:         elements at which data is returned. It can be:
                         1) an integer in the range [-len(ring), len(ring)-1]
                            selecting the element according to python indexing
                            rules. As a special case, len(ring) is allowed and
@@ -865,7 +862,7 @@ def linopt(ring: Lattice, dp: Optional[float] = 0.0,
                         3) a numpy array of booleans of maximum length
                            len(ring)+1, where selected elements are
                            :py:obj:`True`.
-    KEYWORDS
+    Keyword Args:
         orbit           avoids looking for the closed orbit if is already known
                         ((6,) array)
         get_chrom=False compute chromaticities. Needs computing the tune at
@@ -933,9 +930,8 @@ def linopt(ring: Lattice, dp: Optional[float] = 0.0,
 
 
 # noinspection PyPep8Naming
-@check_radiation(False)
-def avlinopt(ring: Lattice, dp: Optional[float] = 0.0,
-             refpts: Optional[Refpts] = None, **kwargs):
+@check_6d(False)
+def avlinopt(ring: Lattice, dp: float = None, refpts: Refpts = None, **kwargs):
     r"""Linear analysis of a lattice with average values
 
     :py:func:`avlinopt` returns average beta, mu, dispersion over the lattice
@@ -956,9 +952,10 @@ def avlinopt(ring: Lattice, dp: Optional[float] = 0.0,
              where selected elements are :py:obj:`True`.
 
     Keyword Args:
-        dct (Optional[float]):  Path lengthening. If specified, ``dp`` is
-          ignored and the off-momentum is deduced from the path lengthening.
-        orbit (Orbit): Avoids looking for the closed orbit if is
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation of RF frequency. Defaults to
+          :py:obj:`None`
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
         get_chrom (bool):       Compute chromaticities. Needs computing
           the tune at 2 different momentum deviations around the central one.
@@ -986,15 +983,15 @@ def avlinopt(ring: Lattice, dp: Optional[float] = 0.0,
           are ignored.
 
     Returns:
-        elemdata:   Linear optics at the points refered to by ``refpts``,
+        elemdata:   Linear optics at the points refered to by *refpts*,
           if refpts is :py:obj:`None` an empty lindata structure is returned.
         avebeta:    Average beta functions [:math:`\hat{\beta_x},\hat{\beta_y}`]
-          at ``refpts``
+          at *refpts*
         avemu:      Average phase advances [:math:`\hat{\mu_x},\hat{\mu_y}`]
-          at ``refpts``
+          at *refpts*
         avedisp:    Average dispersion [:math:`\hat{\eta_x}, \hat{\eta'_x},
-          \hat{\eta_y}, \hat{\eta'_y}`] at ``refpts``
-        avespos:    Average s position at ``refpts``
+          \hat{\eta_y}, \hat{\eta'_y}`] at *refpts*
+        avespos:    Average s position at *refpts*
         tune:       [:math:`\nu_1,\nu_2`], linear tunes for the two normal
           modes of linear motion [1]
         chrom:      [:math:`\xi_1,\xi_2`], chromaticities
@@ -1068,24 +1065,24 @@ def dispfoc(dispp0, dispp1, k2, lg):
     return lindata, avebeta, avemu, avedisp, aves, bd.tune, bd.chromaticity
 
 
-def get_tune(ring: Lattice, method: str = 'linopt',
-             dp: Optional[float] = None, dct: Optional[float] = None,
-             orbit: Optional[Orbit] = None, **kwargs):
+def get_tune(ring: Lattice, *, method: str = 'linopt',
+             dp: float = None, dct: float = None, df: float = None,
+             orbit: Orbit = None, **kwargs):
     r"""Computes the tunes using several available methods
 
     Parameters:
-        ring:       Lattice description
-        method:     ``'linopt'`` returns the tunes from the :py:func:`.linopt6`
-          function,
+        ring:                   Lattice description
+        method:                 ``'linopt'`` returns the tunes from the
+          :py:func:`.linopt6` function,
 
           ``'fft'`` tracks a single particle and computes the tunes with fft,
 
           ``'laskar'`` tracks a single particle and computes the tunes with
           NAFF.
-        dp:         Momentum deviation.
-        dct:        Path lengthening. If specified, ``dp`` is ignored and
-          the off-momentum is deduced from the path lengthening.
-        orbit (Optional[Orbit]): Avoids looking for the closed orbit if is
+        dp (float):             Momentum deviation.
+        dct (float):            Path lengthening.
+        df (float):             Deviation of RF frequency.
+        orbit (Orbit):          Avoids looking for the closed orbit if it is
           already known ((6,) array)
 
     for the ``'fft'`` and ``'laskar'`` methods only:
@@ -1094,7 +1091,7 @@ def get_tune(ring: Lattice, method: str = 'linopt',
         nturns (int):           Number of turns. Default: 512
         amplitude (float):      Amplitude of oscillation.
           Default: 1.E-6
-        remove_dc (bool:        Remove the mean of oscillation data.
+        remove_dc (bool):       Remove the mean of oscillation data.
           Default: :py:obj:`True`
         num_harmonics (int):    Number of harmonic components to
           compute (before mask applied, default: 20)
@@ -1126,58 +1123,59 @@ def gen_centroid(ring, ampl, nturns, remove_dc, ld):
            'Integer tune only accessible with method=linopt'
     if method == 'linopt':
         if get_integer:
-            _, _, c = ring.get_optics(refpts=range(len(ring)+1))
+            _, _, c = get_optics(ring, refpts=range(len(ring)+1),
+                                 dp=dp, dct=dct, df=df, orbit=orbit)
             tunes = c.mu[-1]/(2*numpy.pi)
         else:
-            tunes = _tunes(ring, dp=dp, dct=dct, orbit=orbit)
+            tunes = _tunes(ring, dp=dp, dct=dct, df=df, orbit=orbit)
     else:
         nturns = kwargs.pop('nturns', 512)
         ampl = kwargs.pop('ampl', 1.0e-6)
         remove_dc = kwargs.pop('remove_dc', True)
-        ld, _, _ = linopt6(ring, dp=dp, dct=dct, orbit=orbit)
+        ld, _, _ = linopt6(ring, dp=dp, dct=dct, df=df, orbit=orbit)
         cents = gen_centroid(ring, ampl, nturns, remove_dc, ld)
         tunes = get_tunes_harmonic(cents, method=method, **kwargs)
     return tunes
 
 
-def get_chrom(ring: Lattice, method: str = 'linopt',
-              dp: Optional[float] = 0, dct: Optional[float] = None,
-              cavpts: Optional[Refpts] = None, **kwargs):
+def get_chrom(ring: Lattice, *, method: str = 'linopt',
+              dp: float = None, dct: float = None, df: float = None,
+              cavpts: Refpts = None, **kwargs):
     r"""Computes the chromaticities using several available methods
 
     Parameters:
-        ring:       Lattice description.
-        method:     ``'linopt'`` returns the tunes from the :py:func:`linopt6`
-          function,
+        ring:               Lattice description.
+        method:             ``'linopt'`` returns the tunes from the
+          :py:func:`linopt6` function,
 
           ``'fft'`` tracks a single particle and computes the tunes with
           :py:func:`~scipy.fftpack.fft`,
 
           ``'laskar'`` tracks a single particle and computes the tunes with
           NAFF.
-        dp:         Momentum deviation.
-        dct:        Path lengthening. If specified, ``dp`` is ignored and
-          the off-momentum is deduced from the path lengthening.
+        dp (float):         Momentum deviation.
+        dct (float):        Path lengthening.
+        df (float):         Deviation of RF frequency.
         cavpts:     If :py:obj:`None`, look for ring.cavpts, or
           otherwise take all cavities.
 
     Keyword Args:
-        DPStep (float):       Momentum step for differentiation
+        DPStep (float):     Momentum step for differentiation
           Default: :py:data:`DConstant.DPStep <.DConstant>`
 
     for the ``'fft'`` and ``'laskar'`` methods only:
 
     Keyword Args:
-        nturns (int):           Number of turns. Default: 512
-        amplitude (float):      Amplitude of oscillation.
+        nturns (int):       Number of turns. Default: 512
+        amplitude (float):  Amplitude of oscillation.
           Default: 1.E-6
-        remove_dc (bool):       Remove the mean of oscillation data.
+        remove_dc (bool):   Remove the mean of oscillation data.
           Default: :py:obj:`True`
-        num_harmonics (int):    Number of harmonic components to
+        num_harmonics (int):Number of harmonic components to
           compute (before mask applied, default: 20)
-        fmin (float):           Lower tune bound. Default: 0
-        fmax (float):           Upper tune bound. Default: 1
-        hann (bool):            Turn on Hanning window.
+        fmin (float):       Lower tune bound. Default: 0
+        fmax (float):       Upper tune bound. Default: 1
+        hann (bool):        Turn on Hanning window.
           Default: :py:obj:`False`
 
     Returns:
@@ -1189,9 +1187,9 @@ def get_chrom(ring: Lattice, method: str = 'linopt',
         print('Warning fft method not accurate to get the ' +
               'chromaticity')
 
-    if ring.radiation:
+    if ring.is_6d:
         f0 = ring.get_rf_frequency(cavpts=cavpts)
-        df = dp_step * ring.radiation_off(copy=True).slip_factor * f0
+        df = dp_step * ring.disable_6d(copy=True).slip_factor * f0
         rgup = ring.set_rf_frequency(f0 + 0.5 * df, cavpts=cavpts, copy=True)
         o0up, _ = find_orbit6(rgup, **kwargs)
         tune_up = get_tune(rgup,  method=method, orbit=o0up, **kwargs)
@@ -1200,9 +1198,10 @@ def get_chrom(ring: Lattice, method: str = 'linopt',
         tune_down = get_tune(rgdn,  method=method, orbit=o0dn, **kwargs)
         dp_step = o0up[4] - o0dn[4]
     else:
-        if dct is not None:
-            orbit = find_orbit4(ring, dct=dct)
-            dp = orbit[4]
+        if dct is not None or df is not None:
+            dp = find_orbit4(ring, dct=dct, df=df)[0][4]
+        elif dp is None:
+            dp = 0.0
         tune_up = get_tune(ring, method=method, dp=dp + 0.5*dp_step, **kwargs)
         tune_down = get_tune(ring, method=method,
                              dp=dp - 0.5*dp_step, **kwargs)
@@ -1216,6 +1215,5 @@ def get_chrom(ring: Lattice, method: str = 'linopt',
 Lattice.linopt6 = linopt6
 Lattice.get_optics = get_optics
 Lattice.avlinopt = avlinopt
-Lattice.avlinopt = avlinopt
 Lattice.get_tune = get_tune
 Lattice.get_chrom = get_chrom
diff --git a/pyat/at/physics/matrix.py b/pyat/at/physics/matrix.py
index 3530dce9e..dc3fb2b28 100644
--- a/pyat/at/physics/matrix.py
+++ b/pyat/at/physics/matrix.py
@@ -4,7 +4,6 @@
 A collection of functions to compute 4x4 and 6x6 transfer matrices
 """
 import numpy
-from typing import Optional
 from ..lattice import Lattice, Element, get_refpts, DConstant, Refpts
 from ..lattice.elements import Bend, M66
 from ..tracking import lattice_pass, element_pass
@@ -16,11 +15,9 @@
 _jmt = jmat(2)
 
 
-def find_m44(ring: Lattice, dp: Optional[float] = 0.0,
-             refpts: Optional[Refpts] = None,
-             dct: Optional[float] = None,
-             orbit: Optional[Orbit] = None,
-             keep_lattice: bool = False, **kwargs):
+def find_m44(ring: Lattice, dp: float = None, refpts: Refpts = None,
+             dct: float = None, df: float = None,
+             orbit: Orbit = None, keep_lattice: bool = False, **kwargs):
     """One turn 4x4 transfer matrix
 
     :py:func:`find_m44` finds the 4x4 transfer matrix of an accelerator
@@ -39,10 +36,10 @@ def find_m44(ring: Lattice, dp: Optional[float] = 0.0,
 
     Parameters:
         ring:           Lattice description (radiation must be OFF)
-        dp:             Momentum deviation. Defaults to 0
+        dp:             Momentum deviation.
         refpts:         Observation points
-        dct:            Path lengthening. If specified, ``dp`` is ignored and
-          the off-momentum is deduced from the path lengthening.
+        dct:            Path lengthening.
+        df:             Deviation of RF frequency.
         orbit:          Avoids looking for initial the closed orbit if it is
           already known ((6,) array).
         keep_lattice:   Assume no lattice change since the previous tracking.
@@ -72,8 +69,8 @@ def mrotate(m):
     xy_step = kwargs.pop('XYStep', DConstant.XYStep)
     full = kwargs.pop('full', False)
     if orbit is None:
-        orbit, _ = find_orbit4(ring, dp, dct=dct, keep_lattice=keep_lattice,
-                               XYStep=xy_step)
+        orbit, _ = find_orbit4(ring, dp=dp, dct=dct, df=df,
+                               keep_lattice=keep_lattice, XYStep=xy_step)
         keep_lattice = True
     # Construct matrix of plus and minus deltas
     # scaling = 2*xy_step*numpy.array([1.0, 0.1, 1.0, 0.1])
@@ -103,9 +100,8 @@ def mrotate(m):
     return m44, mstack
 
 
-def find_m66(ring: Lattice, refpts: Optional[Refpts] = None,
-             orbit: Optional[Orbit] = None,
-             keep_lattice: bool = False, **kwargs):
+def find_m66(ring: Lattice, refpts: Refpts = None,
+             orbit: Orbit = None, keep_lattice: bool = False, **kwargs):
     """One-turn 6x6 transfer matrix
 
     :py:func:`find_m66` finds the 6x6 transfer matrix of an accelerator
@@ -174,9 +170,7 @@ def find_m66(ring: Lattice, refpts: Optional[Refpts] = None,
     return m66, mstack
 
 
-def find_elem_m66(elem: Element,
-                  orbit: Optional[Orbit] = None,
-                  **kwargs):
+def find_elem_m66(elem: Element, orbit: Orbit = None, **kwargs):
     """Single element 6x6 transfer matrix
 
     Numerically finds the 6x6 transfer matrix of a single element
diff --git a/pyat/at/physics/orbit.py b/pyat/at/physics/orbit.py
index 7d181e46c..7e37f3509 100644
--- a/pyat/at/physics/orbit.py
+++ b/pyat/at/physics/orbit.py
@@ -2,9 +2,8 @@
 Closed orbit related functions
 """
 import numpy
-from typing import Optional
 from at.constants import clight
-from at.lattice import AtWarning, check_radiation, DConstant
+from at.lattice import AtError, AtWarning, check_6d, DConstant
 from at.lattice import Lattice, get_s_pos, uint32_refpts, Refpts
 from at.tracking import lattice_pass
 from . import ELossMethod, get_timelag_fromU0
@@ -16,8 +15,8 @@
            'find_orbit']
 
 
-@check_radiation(False)
-def _orbit_dp(ring, dp=None, guess=None, **kwargs):
+@check_6d(False)
+def _orbit_dp(ring: Lattice, dp: float = None, guess: Orbit = None, **kwargs):
     """Solver for fixed energy deviation"""
     # We seek
     #  - f(x) = x
@@ -35,6 +34,10 @@ def _orbit_dp(ring, dp=None, guess=None, **kwargs):
     convergence = kwargs.pop('convergence', DConstant.OrbConvergence)
     max_iterations = kwargs.pop('max_iterations', DConstant.OrbMaxIter)
     xy_step = kwargs.pop('XYStep', DConstant.XYStep)
+    rem = kwargs.keys()
+    if len(rem) > 0:
+        raise AtError(f'Unexpected keywords for orbit_dp: {", ".join(rem)}')
+
     ref_in = numpy.zeros((6,)) if guess is None else numpy.copy(guess)
     ref_in[4] = 0.0 if dp is None else dp
 
@@ -69,13 +72,17 @@ def _orbit_dp(ring, dp=None, guess=None, **kwargs):
     return ref_in
 
 
-@check_radiation(False)
-def _orbit_dct(ring, dct=None, guess=None, **kwargs):
+@check_6d(False)
+def _orbit_dct(ring: Lattice, dct: float = None, guess: Orbit = None, **kwargs):
     """Solver for fixed path lengthening"""
     keep_lattice = kwargs.pop('keep_lattice', False)
     convergence = kwargs.pop('convergence', DConstant.OrbConvergence)
     max_iterations = kwargs.pop('max_iterations', DConstant.OrbMaxIter)
     xy_step = kwargs.pop('XYStep', DConstant.XYStep)
+    rem = kwargs.keys()
+    if len(rem) > 0:
+        raise AtError(f'Unexpected keywords for orbit_dct: {", ".join(rem)}')
+
     ref_in = numpy.zeros((6,)) if guess is None else numpy.copy(guess)
 
     scaling = xy_step * numpy.array([1.0, 1.0, 1.0, 1.0, 1.0])
@@ -114,10 +121,10 @@ def _orbit_dct(ring, dct=None, guess=None, **kwargs):
     return ref_in
 
 
-def find_orbit4(ring: Lattice, dp: Optional[float] = 0.0,
-                refpts: Optional[Refpts] = None,
-                dct: Optional[float] = None,
-                orbit: Optional[Orbit] = None,
+def find_orbit4(ring: Lattice, dp: float = None, refpts: Refpts = None, *,
+                dct: float = None,
+                df: float = None,
+                orbit: Orbit = None,
                 keep_lattice: bool = False, **kwargs):
     r"""Gets the 4D closed orbit for a given dp
 
@@ -133,7 +140,7 @@ def find_orbit4(ring: Lattice, dp: Optional[float] = 0.0,
     constraint on the 6-th coordinate :math:`c\tau`
 
     Important:
-        :py:func:`find_orbit4` imposes a constraint on ``dp`` and relaxes the
+        :py:func:`find_orbit4` imposes a constraint on *dp* and relaxes the
         constraint on the revolution frequency. A physical storage ring does
         exactly the opposite: the momentum deviation of a particle on the
         closed orbit settles at the value such that the revolution is
@@ -142,19 +149,22 @@ def find_orbit4(ring: Lattice, dp: Optional[float] = 0.0,
     To impose this artificial constraint in :py:func:`find_orbit4`,
     the ``PassMethod`` used for any element **SHOULD NOT**:
 
-    1. Change the longitudinal momentum ``dp`` (cavities ,
+    1. Change the longitudinal momentum *dp* (cavities ,
        magnets with radiation)
     2. Have any time dependence (localized impedance, fast kickers etc)
 
     Parameters:
-        ring:           Lattice description (radiation must be OFF)
+        ring:           Lattice description (``is_6d`` must be :py:obj:`False`)
         dp:             Momentum deviation. Defaults to 0
         refpts:         Observation points
-        dct:            Path lengthening. If specified, ``dp`` is ignored and
+        dct:            Path lengthening. If specified, *dp* is ignored and
           the off-momentum is deduced from the path lengthening.
+        df:             Deviation from the nominal RF frequency. If specified,
+          *dp* is ignored and the off-momentum is deduced from the frequency
+          deviation.
         orbit:          Avoids looking for initial the closed orbit if it is
           already known ((6,) array). :py:func:`find_orbit4` propagates it to
-          the specified ``refpts``.
+          the specified *refpts*.
         keep_lattice:   Assume no lattice change since the previous tracking.
           Default: False
 
@@ -172,13 +182,20 @@ def find_orbit4(ring: Lattice, dp: Optional[float] = 0.0,
         orbit0:         (6,) closed orbit vector at the entrance of the
                         1-st element (x,px,y,py,dp,0)
         orbit:          (Nrefs, 6) closed orbit vector at each location
-                        specified in ``refpts``
+                        specified in *refpts*
 
     See also:
         :py:func:`find_sync_orbit`, :py:func:`find_orbit6`
     """
+    if len([v for v in (dp, dct, df) if v is not None]) > 1:
+        raise AtError("For off-momentum specification, only one of "
+                      "dp, dct and df may be specified")
     if orbit is None:
-        if dct is not None:
+        if df is not None:
+            frf = ring.cell_revolution_frequency * ring.cell_harmnumber
+            dct = -ring.cell_length * df / (frf+df)
+            orbit = _orbit_dct(ring, dct, keep_lattice=keep_lattice, **kwargs)
+        elif dct is not None:
             orbit = _orbit_dct(ring, dct, keep_lattice=keep_lattice, **kwargs)
         else:
             orbit = _orbit_dp(ring, dp, keep_lattice=keep_lattice, **kwargs)
@@ -193,10 +210,10 @@ def find_orbit4(ring: Lattice, dp: Optional[float] = 0.0,
     return orbit, all_points
 
 
-def find_sync_orbit(ring: Lattice, dct: Optional[float] = 0.0,
-                    refpts: Optional[Refpts] = None,
-                    dp: Optional[float] = None,
-                    orbit: Optional[Orbit] = None,
+def find_sync_orbit(ring: Lattice, dct: float = None, refpts: Refpts = None, *,
+                    dp: float = None,
+                    df: float = None,
+                    orbit: Orbit = None,
                     keep_lattice: bool = False, **kwargs):
     r"""Gets the 4D closed orbit for a given dct
 
@@ -222,18 +239,21 @@ def find_sync_orbit(ring: Lattice, dct: Optional[float] = 0.0,
     To impose this artificial constraint in :py:func:`find_sync_orbit`,
     the ``PassMethod`` used for any element **SHOULD NOT**:
 
-    1. Change the longitudinal momentum ``dp`` (cavities ,
+    1. Change the longitudinal momentum *dp* (cavities ,
        magnets with radiation)
     2. Have any time dependence (localized impedance, fast kickers etc)
 
     Parameters:
-        ring:           Lattice description (radiation must be OFF)
+        ring:           Lattice description (``is_6d`` must be :py:obj:`False`)
         dct:            Path lengthening.
         refpts:         Observation points
-        dp:             Momentum deviation. Defaults to None
+        dp:             Momentum deviation. Defaults to :py:obj:`None`
+        df:             Deviation from the nominal RF frequency. If specified,
+          *dct* is ignored and the off-momentum is deduced from the frequency
+          deviation.
         orbit:          Avoids looking for initial the closed orbit if it is
           already known ((6,) array). :py:func:`find_sync_orbit` propagates it
-          to the specified ``refpts``.
+          to the specified *refpts*.
         keep_lattice:   Assume no lattice change since the previous tracking.
           Default: False
 
@@ -251,13 +271,20 @@ def find_sync_orbit(ring: Lattice, dct: Optional[float] = 0.0,
         orbit0:         (6,) closed orbit vector at the entrance of the
                         1-st element (x,px,y,py,dp,0)
         orbit:          (Nrefs, 6) closed orbit vector at each location
-                        specified in ``refpts``
+                        specified in *refpts*
 
     See also:
         :py:func:`find_orbit4`, :py:func:`find_orbit6`
     """
+    if len([v for v in (dp, dct, df) if v is not None]) > 1:
+        raise AtError("For off-momentum specification, only one of "
+                      "dp, dct and df may be specified")
     if orbit is None:
-        if dp is not None:
+        if df is not None:
+            frf = ring.cell_revolution_frequency * ring.cell_harmnumber
+            dct = -ring.cell_length * df / (frf+df)
+            orbit = _orbit_dct(ring, dct, keep_lattice=keep_lattice, **kwargs)
+        elif dp is not None:
             orbit = _orbit_dp(ring, dp, keep_lattice=keep_lattice, **kwargs)
         else:
             orbit = _orbit_dct(ring, dct,  keep_lattice=keep_lattice, **kwargs)
@@ -280,6 +307,9 @@ def _orbit6(ring: Lattice, cavpts=None, guess=None, keep_lattice=False,
     xy_step = kwargs.pop('XYStep', DConstant.XYStep)
     dp_step = kwargs.pop('DPStep', DConstant.DPStep)
     method = kwargs.pop('method', ELossMethod.TRACKING)
+    rem = kwargs.keys()
+    if len(rem) > 0:
+        raise AtError(f'Unexpected keywords for orbit6: {", ".join(rem)}')
 
     l0 = get_s_pos(ring, len(ring))[0]
     f_rf = ring.get_rf_frequency()
@@ -332,14 +362,13 @@ def _orbit6(ring: Lattice, cavpts=None, guess=None, keep_lattice=False,
     return ref_in
 
 
-def find_orbit6(ring: Lattice, refpts: Optional[Refpts] = None,
-                orbit: Optional[Orbit] = None,
-                dp: Optional[float] = None,
-                dct: Optional[float] = None,
-                keep_lattice: Optional[bool] = False, **kwargs):
+# noinspection PyIncorrectDocstring
+def find_orbit6(ring: Lattice, refpts: Refpts = None, *,
+                dp: float = None, dct: float = None, df: float = None,
+                orbit: Orbit = None, keep_lattice: bool = False, **kwargs):
     r"""Gets the closed orbit in the full 6-D phase space
 
-    Findsinds the closed orbit in the full 6-D phase space
+    Finds the closed orbit in the full 6-D phase space
     by numerically solving  for a fixed point of the one turn
     map M calculated with :py:func:`.lattice_pass`
 
@@ -377,9 +406,7 @@ def find_orbit6(ring: Lattice, refpts: Optional[Refpts] = None,
         refpts:         Observation points
         orbit:          Avoids looking for initial the closed orbit if it is
           already known ((6,) array). :py:func:`find_sync_orbit` propagates it
-          to the specified ``refpts``.
-        dp:             Momentum deviation. Defaults to None
-        dct:            Path lengthening. Defaults to None
+          to the specified *refpts*.
         keep_lattice:   Assume no lattice change since the previous tracking.
           Default: False
 
@@ -396,21 +423,21 @@ def find_orbit6(ring: Lattice, refpts: Optional[Refpts] = None,
           Default: :py:data:`DConstant.DPStep <.DConstant>`
         method (ELossMethod): Method for energy loss computation.
           See :py:class:`.ELossMethod`.
-        cavpts (Optional[Refpts]):      Cavity location. If None, use all
+        cavpts (Refpts):      Cavity location. If :py:obj:`None`, use all
           cavities. This is used to compute the initial synchronous phase.
 
     Returns:
         orbit0:         (6,) closed orbit vector at the entrance of the
                         1-st element (x,px,y,py,dp,0)
         orbit:          (Nrefs, 6) closed orbit vector at each location
-                        specified in ``refpts``
+                        specified in *refpts*
 
 
     See also:
         :py:func:`find_orbit4`, :py:func:`find_sync_orbit`
     """
-    if not (dp is None and dct is None):
-        warnings.warn(AtWarning('In 6D, "dp" and "dct" are ignored'))
+    if dp is not None or dct is not None or df is not None:
+        raise AtError("orbit6: in 6d, dp, dct and df are not allowed")
     if orbit is None:
         orbit = _orbit6(ring, keep_lattice=keep_lattice, **kwargs)
         keep_lattice = True
@@ -429,30 +456,32 @@ def find_orbit(ring, refpts=None, **kwargs):
 
     Depending on the lattice, :py:func:`find_orbit` will:
 
-    * use :py:func:`find_orbit6` if ring.radiation is ON,
-    * use :py:func:`find_sync_orbit` if ring.radiation is OFF and ``dct``
-      is specified,
-    * use :py:func:`find_orbit4` otherwise
+    * use :py:func:`find_orbit6` if ``ring.is_6d`` is :py:obj:`True`,
+    * use :py:func:`find_sync_orbit` if ``ring.is_6d`` is :py:obj:`False` and
+      *dct* or *df* is specified,
+    * use :py:func:`find_orbit4` otherwise.
 
     Parameters:
         ring:           Lattice description
         refpts:         Observation points
 
     Keyword Args:
-        orbit (Optional[Orbit]):       Avoids looking for initial the closed
+        orbit (Orbit):          Avoids looking for initial the closed
           orbit if it is already known. :py:func:`find_orbit` propagates it
-          to the specified ``refpts``.
-        dp (Optional[float]):           Momentum deviation. Defaults to None
-        dct (Optional[float]):          Path lengthening. Defaults to None
-        guess (Optional[Orbit]):        (6,) initial value for the
-          closed orbit. It may help convergence. Default: (0, 0, 0, 0, 0, 0)
-        convergence (Optional[float]):  Convergence criterion.
+          to the specified *refpts*.
+        dp (float):             Momentum deviation. Defaults to :py:obj:`None`
+        dct (float):            Path lengthening. Defaults to :py:obj:`None`
+        df (float):             Deviation from the nominal RF frequency.
+          Defaults to :py:obj:`None`
+        guess (Orbit):          (6,) initial value for the closed orbit.
+          It may help convergence. Default: (0, 0, 0, 0, 0, 0)
+        convergence (float):    Convergence criterion.
           Default: :py:data:`DConstant.OrbConvergence <.DConstant>`
-        max_iterations (Optional[int]): Maximum number of iterations.
+        max_iterations (int):   Maximum number of iterations.
           Default: :py:data:`DConstant.OrbMaxIter <.DConstant>`
-        XYStep (Optional[float]):       Step size.
+        XYStep (float):         Step size.
           Default: :py:data:`DConstant.XYStep <.DConstant>`
-        DPStep (Optional[float]):       Momentum step size.
+        DPStep (float):         Momentum step size.
           Default: :py:data:`DConstant.DPStep <.DConstant>`
 
     For other keywords, refer to the underlying methods
@@ -461,13 +490,13 @@ def find_orbit(ring, refpts=None, **kwargs):
         orbit0:         (6,) closed orbit vector at the entrance of the
                         1-st element (x,px,y,py,dp,0)
         orbit:          (Nrefs, 6) closed orbit vector at each location
-                        specified in ``refpts``
+                        specified in *refpts*
 
     See also:
         :py:func:`find_orbit4`, :py:func:`find_sync_orbit`,
         :py:func:`find_orbit6`
     """
-    if ring.radiation:
+    if ring.is_6d:
         return find_orbit6(ring, refpts=refpts, **kwargs)
     else:
         return find_orbit4(ring, refpts=refpts, **kwargs)
diff --git a/pyat/at/physics/revolution.py b/pyat/at/physics/revolution.py
index cde7e58f0..a074ba36c 100644
--- a/pyat/at/physics/revolution.py
+++ b/pyat/at/physics/revolution.py
@@ -1,4 +1,4 @@
-from ..lattice import Lattice, get_rf_frequency, check_radiation, get_s_pos
+from ..lattice import Lattice, get_rf_frequency, check_6d, get_s_pos
 from ..lattice import DConstant
 from ..constants import clight
 from ..tracking import lattice_pass
@@ -12,25 +12,27 @@
 
 
 def frequency_control(func):
-    """Function to be used as decorator for ``func(ring, *args, **kwargs)``
+    r""" Function to be used as decorator for :pycode:`func(ring, *args, **kwargs)`
 
-    If ``ring.radiation`` is :py:obj:`True` **and** ``dp``, ``dct`` or ``df``
-    is specified in ``kwargs``, make a copy of ``ring`` with a modified
-    RF frequency, remove ``dp``, ``dct`` or ``df`` from ``kwargs`` and call
-    ``func`` with the modified ``ring``.
+    If :pycode:`ring.is_6d` is :py:obj:`True` **and** *dp*, *dct* or *df*
+    is specified in *kwargs*, make a copy of *ring* with a modified
+    RF frequency, remove *dp*, *dct* or *df* from *kwargs* and call
+    *func* with the modified *ring*.
 
-    If ``ring.radiation`` is :py:obj:`False` **or** no ``dp``, ``dct`` or
-    ``df`` is specified in ``kwargs``, ``func`` is called unchanged.
+    If :pycode:`ring.is_6d` is :py:obj:`False` **or** no *dp*, *dct* or
+    *df* is specified in *kwargs*, *func* is called unchanged.
 
-    Examples::
+    Examples:
 
-        @frequency_control
-        def func(ring, *args, dp=None, dct=None, **kwargs):
-            pass
+        .. code-block:: python
+
+            @frequency_control
+            def func(ring, *args, dp=None, dct=None, **kwargs):
+                pass
     """
     @functools.wraps(func)
     def wrapper(ring, *args, **kwargs):
-        if ring.radiation:
+        if ring.is_6d:
             momargs = {}
             for key in ['dp', 'dct', 'df']:
                 v = kwargs.pop(key, None)
@@ -42,13 +44,14 @@ def wrapper(ring, *args, **kwargs):
     return wrapper
 
 
-@check_radiation(False)
+@check_6d(False)
 def get_mcf(ring: Lattice, dp: Optional[float] = 0.0,
             keep_lattice: bool = False, **kwargs) -> float:
     r"""Compute the momentum compaction factor :math:`\alpha`
 
     Parameters:
-        ring:           Lattice description (radiation must be OFF)
+        ring:           Lattice description (:pycode:`ring.is_6d` must be
+          :py:obj:`False`)
         dp:             Momentum deviation. Defaults to :py:obj:`None`
         keep_lattice:   Assume no lattice change since the previous tracking.
           Default: :py:obj:`False`
@@ -75,11 +78,12 @@ def get_slip_factor(ring: Lattice, **kwargs) -> float:
     r"""Compute the slip factor :math:`\eta`
 
     Parameters:
-        ring:           Lattice description (radiation must be OFF)
+        ring:           Lattice description (:pycode:`ring.is_6d` must be
+          :py:obj:`False`)
 
     Keyword Args:
-        dp (Optional[float]):       Momentum deviation
-        DPStep (Optional[float]):       Momentum step size.
+        dp (float):     Momentum deviation
+        DPStep (float): Momentum step size.
           Default: :py:data:`DConstant.DPStep <.DConstant>`
 
     Returns:
@@ -90,33 +94,39 @@ def get_slip_factor(ring: Lattice, **kwargs) -> float:
     return etac
 
 
-def get_revolution_frequency(ring,
-                             dp: Optional[float] = None,
-                             dct: Optional[float] = None) -> float:
+def get_revolution_frequency(ring: Lattice,
+                             dp: float = None,
+                             dct: float = None,
+                             df: float = None) -> float:
     """Compute the revolution frequency of the full ring [Hz]
 
     Parameters:
         ring:       Lattice description
-        dp:             Momentum deviation. Defaults to :py:obj:`None`
-        dct:            Path lengthening. Defaults to :py:obj:`None`
+        dp:         Momentum deviation. Defaults to :py:obj:`None`
+        dct:        Path lengthening. Defaults to :py:obj:`None`
+        df:         Deviation of RF frequency. Defaults to :py:obj:`None`
 
     Returns:
         frev:       Revolution frequency [Hz]
     """
-    lcell = ring.get_s_pos(len(ring))[0]
-    frev = ring.beta * clight / lcell / ring.periodicity
+    lcell = ring.cell_length
+    cell_frev = ring.beta * clight / lcell
     if dct is not None:
-        frev *= lcell / (lcell + dct)
+        cell_frev *= lcell / (lcell + dct)
     elif dp is not None:
         # Find the path lengthening for dp
-        rnorad = ring.radiation_off(copy=True) if ring.radiation else ring
+        rnorad = ring.disable_6d(copy=True) if ring.is_6d else ring
         orbit = lattice_pass(rnorad, rnorad.find_orbit4(dp=dp)[0])
         dct = numpy.squeeze(orbit)[5]
-        frev *= lcell / (lcell + dct)
-    return frev
+        cell_frev *= lcell / (lcell + dct)
+    elif df is not None:
+        cell_frev += df / ring.cell_harmnumber
+    return cell_frev / ring.periodicity
 
 
-def set_rf_frequency(ring, frequency=None, dp=None, dct=None, **kwargs):
+def set_rf_frequency(ring: Lattice, frequency: float = None,
+                     dp: float = None, dct: float = None, df: float = None,
+                     **kwargs):
     """Set the RF frequency
 
     Parameters:
@@ -124,23 +134,24 @@ def set_rf_frequency(ring, frequency=None, dp=None, dct=None, **kwargs):
         frequency:      RF frequency [Hz]. Default: nominal frequency.
         dp:             Momentum deviation. Defaults to :py:obj:`None`
         dct:            Path lengthening. Defaults to :py:obj:`None`
+        df:             Deviation of RF frequency. Defaults to :py:obj:`None`
 
     Keyword Args:
         cavpts (Optional[Refpts]):  If :py:obj:`None`, look for ring.cavpts, or
           otherwise take all cavities.
-        array (Optional[bool]):     If :py:obj:`False` (default), ``frequency``
+        array (Optional[bool]):     If :py:obj:`False` (default), *frequency*
           is applied to the selected cavities with the lowest frequency. The
           frequency of all the other selected cavities is scaled by the same
           ratio.
 
-          If :py:obj:`True`, directly apply ``frequency`` to the selected
+          If :py:obj:`True`, directly apply *frequency* to the selected
           cavities. The value must be broadcastable to the number of cavities.
         copy (Optional[bool]):     If :py:obj:`True`, returns a shallow copy of
-          ``ring`` with new cavity elements. Otherwise (default), modify
-          ``ring`` in-place
+          *ring* with new cavity elements. Otherwise (default), modify
+          *ring* in-place
     """
     if frequency is None:
-        frequency = ring.get_revolution_frequency(dp=dp, dct=dct) \
+        frequency = ring.get_revolution_frequency(dp=dp, dct=dct, df=df) \
                     * ring.harmonic_number
     return ring.set_cavity(Frequency=frequency, **kwargs)
 


Test patch:
diff --git a/pyat/test_matlab/test_cmp_physics.py b/pyat/test_matlab/test_cmp_physics.py
index d2ce99466..5d2c3f6f6 100644
--- a/pyat/test_matlab/test_cmp_physics.py
+++ b/pyat/test_matlab/test_cmp_physics.py
@@ -43,8 +43,7 @@ def test_linear_analysis(engine, lattices, dp):
               ('gamma', 'gamma')]
     refpts = range(nelems + 1)
     py_data0, py_tune, py_chrom, py_data = physics.linopt(py_lattice, dp,
-                                                          refpts, True,
-                                                          ddp=1.E-6)
+                                                          refpts, True)
     # Matlab call
     ml_data, ml_tune, ml_chrom = engine.pyproxy('atlinopt', ml_lattice, dp,
                                                 _ml_refs(refpts, nelems),