How to Create a New Element
This page explains how to add a new CPBD element family in the current wrapper/atom architecture.
It assumes that you already know the general structure described in Element Architecture. Here we focus on the practical implementation steps.
Two Rules To Keep In Mind
supported_tms is a wrapper contract
supported_tms lists the active tracking methods that the public wrapper allows users or lattices to select.
It does not mean:
- every hook family the atom happens to implement
- every internal first-order path available for optics
For example, Cavity keeps a TransferMap-based first-order optics path in first_order_tms, but its active tracking contract is still only:
supported_tms = {CavityTM}
Edge-aware families must always support first-order optics hooks
For every OpticElement, first_order_tms is built with TransferMap.
That means:
has_edge = Falsefamilies must provide a first-orderMAINhookhas_edge = Truefamilies must provide first-orderENTRANCE,MAIN, andEXIThooks
This remains true even if active tracking uses a custom transformation such as CavityTM.
1. Decide the Public Wrapper Contract
Before writing code, decide:
- is this a normal
OpticElementfamily - is it edge-aware (
has_edge=True) or not - what is the default active tracking method
- which active tracking methods should be user-selectable through the wrapper
In current CPBD terms:
default_tmis the family fallback active tracking methodsupported_tmsis the set of wrapper-selectable active tracking methodsfirst_order_tmsis separate and is always theTransferMapoptics path
Typical choices:
- simple linear element:
default_tm = TransferMap - magnetic family with nonlinear tracking options:
supported_tms = {TransferMap, SecondTM, KickTM} - family-specific active tracking only:
supported_tms = {default_tm}
2. Choose the Atom Base Class
The atom is where physics state and hook methods live.
Use Element when:
- the family is not naturally magnet-like
- you only need a custom first-order map or a fully custom hook family
Use Magnet when:
- the family is magnet-like
- you want inherited
k1,k2, generic first-order, second-order, or kick hooks
That inheritance choice matters because it determines what hook surface already exists before you override anything.
3. Implement the Atom
At minimum, a new atom must store its physics state and implement the hook families required by the wrapper contract.
For a no-edge first-order family, the minimum useful hook is:
create_first_order_main_params(self, energy, delta_length=None)
For an edge-aware family, the first-order minimum is:
create_first_order_entrance_params(...)create_first_order_main_params(...)create_first_order_exit_params(...)
If the family changes reference energy, also implement:
create_delta_e(self, total_length, delta_length=None)
Minimal no-edge atom skeleton
import numpy as np
from ocelot.cpbd.elements.element import Element
from ocelot.cpbd.tm_params.first_order_params import FirstOrderParams
class MyElementAtom(Element):
def __init__(self, l=0.0, strength=0.0, eid=None, **kwargs):
super().__init__(eid=eid, has_edge=False, **kwargs)
self.l = l
self.strength = strength
def create_first_order_main_params(self, energy, delta_length=None):
z = self.l if delta_length is None else delta_length
R = np.eye(6)
R[1, 0] = -self.strength * z
B = self._default_B(R)
return FirstOrderParams(R=R, B=B, tilt=self.tilt)
Edge-aware atom skeleton
class MyEdgeElementAtom(Element):
def __init__(self, l=0.0, edge_strength=0.0, eid=None, **kwargs):
super().__init__(eid=eid, has_edge=True, **kwargs)
self.l = l
self.edge_strength = edge_strength
def create_first_order_entrance_params(self, energy, delta_length=None):
...
def create_first_order_main_params(self, energy, delta_length=None):
...
def create_first_order_exit_params(self, energy, delta_length=None):
...
4. Implement the Public Wrapper
The wrapper is usually very small, but it is still important because it declares the public tracking contract.
Typical wrapper structure:
from ocelot.cpbd.elements.optic_element import OpticElement
from ocelot.cpbd.elements.my_element_atom import MyElementAtom
from ocelot.cpbd.transformations.transfer_map import TransferMap
from ocelot.cpbd.transformations.second_order import SecondTM
class MyElement(OpticElement):
default_tm = TransferMap
supported_tms = {TransferMap, SecondTM}
def __init__(self, l=0.0, strength=0.0, eid=None, tm=None, **kwargs):
super().__init__(MyElementAtom(l=l, strength=strength, eid=eid, **kwargs), tm=tm)
Important:
- public physics attributes should live on the atom, not be duplicated on the wrapper
- users should interact with
elem.k1,elem.v, and similar wrapper-level attributes through forwarding - do not bypass wrapper cache invalidation by writing directly into
elem.__dict__
5. Verify the Family Contract
After implementation, check these behaviors:
- construction without
tm=usesdefault_tm R()works and uses the first-order optics pathset_tm(...)accepts declared active TMs- explicit undeclared
set_tm(...)requests raise - global lattice requests may warn and fall back to
default_tm get_section_tms(...)behaves correctly for slices- edge-aware families produce
ENTRANCE -> MAIN -> EXIT
For edge-aware custom-TM families, also verify that the first-order optics path still builds successfully.
Practical Checklist
- choose
default_tmandsupported_tms - decide whether the atom should inherit from
ElementorMagnet - implement the first-order optics hooks
- implement
create_delta_e(...)if the family changes reference energy - verify edge behavior and slice behavior
- verify explicit versus global TM selection