Xsuite environment

Overview

An xt.Environment is the shared container that keeps together:

  • Variables in env.vars: scalar knobs and deferred expressions used to drive elements and lines. Their current value can be retrieved with env['name'].

  • Elements in env.elements: magnets, markers, RF cavities, etc. They can be reused across multiple lines.

  • Lines in env.lines: ordered sequences of elements assembled from the environment.

  • Additional data including reference particles and user-defined functions.

Accessing env['name'] returns the value of a variable, an element object, or a line. Accessing env.ref['name'] returns a reference object that keeps track of expressions; use references whenever you want to build expressions or inspect dependencies. The contents of all these containers can be inspected with the .get_table() method, and a list of all names in each container is available with the .keys() method.

A simple example illustrating the creation of an environment with variables, elements, and lines is shown below:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Define variables
env['t_turn_s'] = 0.0
env['l_q'] = 1.0
env['kq'] = 0.12
env['kq.trim'] = '0.05 * kq' # deferred expression (is updated when kq changes)
env['kq.total'] = 'kq + kq.trim' # deferred expression

# Create elements reusing the variables
env.new('qf', xt.Quadrupole, length='l_q', k1='kq.total')
env.new('qd', xt.Quadrupole, length='l_q', k1='-kq.total')
env.new('dr', xt.Drift, length=6.0)

# Define two lines from the same elements
env.new_line(name='fodo', components=['qf', 'dr', 'qd', 'dr'])
env.new_line(name='half_fodo', components=['qf', 'dr'])

# Inspect containers
env.vars.get_table()
# VarsTable: 5 rows, 3 cols
# name             value expr
# t_turn_s             0 None
# l_q                  1 None
# kq                0.12 None
# kq.trim          0.006 (0.05 * kq)
# kq.total         0.126 (kq + kq.trim)
env.elements.get_table()
# Table: 3 rows, 7 cols
# name element_type isthick isreplica parent_name iscollective ...
# dr   Drift           True     False None               False
# qd   Quadrupole      True     False None               False
# qf   Quadrupole      True     False None               False
env.lines.get_table()
# Table: 2 rows, 3 cols
# name      num_elements mode
# fodo                 4 normal
# half_fodo            2 normal

# Quick name listings
env.vars.keys()      # is: ['t_turn_s', 'l_q', 'kq', 'kq.trim', 'kq.total']
env.elements.keys()  # is: ['dr', 'qd', 'qf']
env.lines.keys()     # is: ['fodo', 'half_fodo']

# Inspect a deferred expression with references
env.ref['qf'].k1 # is a ref object
env.ref['qf'].k1.xdeps # gives basic information on the ref.
# It prints:
# Ref(element_refs['qf'].k1, expr=vars['kq.total'], value=0.126)
# Additional information can be obtained from the `info()` method
env.ref['qf'].k1.xdeps.info()
# prints:
# Info for element_refs['qf'].k1
#
# value: 0.126
#
# controlled by expr:
#   element_refs['qf'].k1 = vars['kq.total']
#
# expr_dependencies:
#   vars['kq.total'] = 0.126
#
# controlled_targets: None
env.ref['kq.total'].xdeps.info()
# Info for vars['kq.total']
#
# value: 0.126
#
# controlled by expr:
#   vars['kq.total'] = (vars['kq'] + vars['kq.trim'])
#
# expr_dependencies:
#   vars['kq'] = 0.12
#   vars['kq.trim'] = 0.006
#
# controlled_targets:
#    element_refs['qd'].k1
#    element_refs['qf'].k1

Variables

Defining variables and deferred expressions

Variables can be provided by direct item assignment. Strings are parsed as expressions and can mix Python, NumPy, and other variables in the environment.

import numpy as np
import xtrack as xt

env = xt.Environment()
env['l_q'] = 1.2
env['kq'] = 0.08
env['phi'] = np.pi / 8
env['kq.trim'] = '1.1 * kq'           # deferred expression
env['kq.total'] = 'kq + kq.trim'

env['kq.total']                      # -> 0.168

Expressions provided as strings stay deferred: changing an upstream variable updates all dependents automatically.

env['kq'] = 0.2
env['kq.trim']                      # -> 0.22 (recomputed)

Inspecting variables

env.vars.get_table() summarizes current values and expressions:

env.vars.get_table()
# name      value    expr
# l_q       1.2      None
# kq        0.08     None
# kq.trim   0.088    (1.1 * kq)
# kq.total  0.168    (kq + kq.trim)

To understand dependencies, fetch the reference and ask for its xdeps.info:

env.ref['kq.total'].xdeps.info()
# prints:
# Info for vars['kq.total']
#
# value: 0.168
#
# controlled by expr:
#   vars['kq.total'] = (vars['kq'] + vars['kq.trim'])
#
# expr_dependencies:
#   vars['kq'] = 0.08
#   vars['kq.trim'] = 0.08800000000000001
#
# controlled_targets: None

env.ref['kq'].xdeps.info()
# Info for vars['kq']
#
# value: 0.08
#
# Not controlled by other entities.
#
# controlled_targets:
#    vars['kq.trim']
#    vars['kq.total']

Renaming variables

Variables can renamed as follows

env.vars.rename('kq', 'kq.main') # updates expressions automatically

Elements

Creating elements

Elements can be added directly to env.elements or created with env.new(...). The most direct way is to instantiate an element and store it:

import xtrack as xt

env = xt.Environment()
env['kq'] = 0.08
env['kq.trim'] = '1.1 * kq'
env['kq.total'] = 'kq + kq.trim'

env.elements['mq0'] = xt.Quadrupole(length=3.0, k1=0.1)
env['mq0'].k1 = '0.5 * kq.total'   # attach a deferred expression

Elements can also be created using Environment.new. Deferred expressions can be specified directly in the constructor:

env['l_q'] = 1.2
env['kq'] = 0.08
env['kq.total'] = '2 * kq'

env.new('mq1', xt.Quadrupole, length='l_q', k1='kq.total')
env.new('ms1', xt.Sextupole, length=0.3, k2='-0.5*kq.total')

env.new can also clone an existing element; the parent becomes the prototype for the new one:

env.new('mq2', xt.Quadrupole, length='l_q', k1='kq.total')
env.new('mq2.d', 'mq2', k1='-kq.total')     # clone 'mq2' and override k1

env['mq2.d'].length      # -> expression 'l_q', inherited from 'mq2'
env['mq2.d'].k1          # -> expression '-kq.total' (override)

Inspecting deferred attributes

You can inspect deferred attributes the same way as variables:

env.ref['mq0'].k1.xdeps.info()
# prints:
# Info for element_refs['mq0'].k1
#
# value: 0.08
#
# controlled by expr:
#   element_refs['mq0'].k1 = (0.5 * vars['kq.total'])
#
# expr_dependencies:
#   vars['kq.total'] = 0.16
#
# controlled_targets: None

Listing elements

To get a table with all the elements stored in the environment (including attributes), use env.elements.get_table(attr=True):

env = xt.Environment()
env['l_q'] = 1.2
env['kq'] = 0.08
env['kq.total'] = '2 * kq'
env.elements['mq0'] = xt.Quadrupole(length=3.0, k1=0.1)
env.elements['dr0'] = xt.Drift(length=0.4)
env.new('mq1', xt.Quadrupole, length='l_q', k1='kq.total')
env.new('ms1', xt.Sextupole, length=0.3, k2='-0.5*kq.total')
env.new('dr1', xt.Drift, length=0.2)
env.new('mq2', xt.Quadrupole, length='l_q', k1='kq.total')
env.new('mq2.d', 'mq2', k1='-kq.total')

tt = env.elements.get_table(attr=True)
tt.cols['name element_type length k1l']
# Table: 7 rows, 4 cols
# name  element_type        length           k1l
# dr0   Drift                  0.4             0
# dr1   Drift                  0.2             0
# mq0   Quadrupole               3           0.3
# mq1   Quadrupole             1.2         0.192
# mq2   Quadrupole             1.2         0.192
# mq2.d Quadrupole             1.2        -0.192
# ms1   Sextupole              0.3             0

Tables support convenient filtering and regex matching:

tt.rows['mq.*']
# Table: 4 rows, 124 cols
# name  element_type isthick isreplica parent_name ...
# mq0   Quadrupole      True     False None
# mq1   Quadrupole      True     False None
# mq2   Quadrupole      True     False None
# mq2.d Quadrupole      True     False None

tt.rows.match(element_type='Dr.*|Sext.*')
# Table: 3 rows, 124 cols
# name element_type isthick isreplica parent_name ...
# dr0  Drift           True     False None
# dr1  Drift           True     False None
# ms1  Sextupole       True     False None

tt.rows.match_not(element_type='Dr.*')
# Table: 5 rows, 124 cols
# name  element_type isthick isreplica parent_name ...
# mq0   Quadrupole      True     False None
# mq1   Quadrupole      True     False None
# mq2   Quadrupole      True     False None
# mq2.d Quadrupole      True     False None
# ms1   Sextupole       True     False None

Setting element properties

env.set assigns numbers or expressions to one or many elements. When a string is passed, it is treated as a deferred expression.

env.set('mq2', k1='1.05 * kq.total')
env.set(['mq2', 'mq2.d'], k1='0.5 * kq.total')     # broadcast over a list

You can also target groups via table filters:

tt = env.elements.get_table(attr=True)
tt_quad = tt.rows.match(element_type='Quadrupole')
tt_quad
# Table: 4 rows, 124 cols
# name  element_type isthick isreplica parent_name ...
# mq0   Quadrupole      True     False None
# mq1   Quadrupole      True     False None
# mq2   Quadrupole      True     False None
# mq2.d Quadrupole      True     False None

env.set(tt_quad, integrator='yoshida4')  # applies to all quads above

Remove elements

Elements can be deleted from the environment when they are not used in any line:

del env.elements['mq2.d']        # or env.elements.remove('mq2.d')

Lines

Create line as list of existing elements

The simplest way to create a line is to define it as a sequence of existing elements:

env = xt.Environment()
env.new('qf', xt.Quadrupole, length=1.0, k1=0.1)
env.new('qd', xt.Quadrupole, length=1.0, k1=-0.1)
env.new('dr', xt.Drift, length=5.0)

myline = env.new_line(components=['qf', 'dr', 'qd', 'dr'])

In this case, the line is created but not stored in env.lines. Storing it ensures it is saved with the environment:

env.lines['fodo'] = myline

Alternatively, pass a name to store it automatically:

env.new_line(name='fodo', components=['qf', 'dr', 'qd', 'dr'])

Once stored, it is accessible as env['fodo'] or env.fodo.

Line inspection

You can inspect a line using line.get_table():

env['fodo'].get_table()
# Table: 5 rows, 11 cols
# name                   s element_type isthick isreplica ...
# qf                     0 Quadrupole      True     False
# dr::0                  1 Drift           True     False
# qd                     6 Quadrupole      True     False
# dr::1                  7 Drift           True     False
# _end_point            12                False     False

Create line by placing elements

Lines can also be defined by specifying element positions, either absolute or relative to other elements:

env = xt.Environment()
env['s.q1'] = 3.0
env['s.q2'] = 5.0
env['ds.q4'] = 5.0
env['kquad'] = 0.1
env['line_length'] = 12.0
env.new('q1', xt.Quadrupole, length=1.0, k1='kquad')
env.new('q2', xt.Quadrupole, length=1.0, k1='-kquad')
env.new('q3', xt.Quadrupole, length=1.0, k1='kquad')
env.new('q4', xt.Quadrupole, length=1.0, k1='-kquad')
env.new('s4', xt.Sextupole, length=0.1)

myline = env.new_line(name='myline', length='line_length', components=[
    env.place('q1', at='s.q1'),                       # center at s=3.0
    env.place('q2', anchor='start', at=5.0),          # start at s=5.0
    env.place('q3', anchor='start', at='q2@end'),     # start at end of q2
    env.place('q4', anchor='center', at='ds.q4',
              from_='q3@start'),                      # center 5 m from q3 start
    env.place('s4'),                                  # right after previous
])

tt = myline.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# name             s_start      s_center         s_end
# ||drift_1              0          1.25           2.5
# q1                   2.5             3           3.5
# ||drift_2            3.5          4.25             5
# q2                     5           5.5             6
# q3                     6           6.5             7
# ||drift_3              7          8.75          10.5
# q4                  10.5            11          11.5
# s4                  11.5         11.55          11.6
# ||drift_4           11.6          11.8            12
# _end_point            12            12            12

Elements can also be created and placed with a single instruction:

myline2 = env.new_line(name='myline2', length='line_length', components=[
    env.new('q10', xt.Quadrupole, length=1.0, k1='kquad', at='s.q1'),
    env.new('q20', xt.Quadrupole, length=1.0, k1='-kquad',
            anchor='start', at=5.0),
    env.new('q30', xt.Quadrupole, length=1.0, k1='kquad',
            anchor='start', at='q20@end'),
    env.new('q40', xt.Quadrupole, length=1.0, k1='-kquad',
            anchor='center', at='ds.q4', from_='q30@start'),
    env.new('s40', xt.Sextupole, length=0.1),  # placed after previous
])

tt = myline2.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# name             s_start      s_center         s_end
# ||drift_4              0          1.25           2.5
# q10                  2.5             3           3.5
# ||drift_5            3.5          4.25             5
# q20                    5           5.5             6
# q30                    6           6.5             7
# ||drift_6              7          8.75          10.5
# q40                 10.5            11          11.5
# s40                 11.5         11.55          11.6
# ||drift_7           11.6          11.8            12
# _end_point            12            12            12

“compose” mode

When the line contains many element, it is inconvenient to have to specify all components in a single Python statement. In this case, it is possible to use the “compose” mode, where each component is added by a separate instruction, as illustrated in the following example:

env = xt.Environment()
env['s.q1'] = 3.0
env['s.q2'] = 5.0
env['ds.q4'] = 5.0
env['kquad'] = 0.1
env['line_length'] = 12.0
env.new('q1', xt.Quadrupole, length=1.0, k1='kquad')
env.new('q2', xt.Quadrupole, length=1.0, k1='-kquad')
env.new('q3', xt.Quadrupole, length=1.0, k1='kquad')
env.new('q4', xt.Quadrupole, length=1.0, k1='-kquad')
env.new('s4', xt.Sextupole, length=0.1)

myline = env.new_line(name='myline', length='line_length', compose=True)

myline.place('q1', at='s.q1')                                     # center at s=3.0
myline.place('q2', anchor='start', at=5.0)                        # start at s=5.0
myline.place('q3', anchor='start', at='q2@end')                   # start at end of q2
myline.place('q4', anchor='center', at='ds.q4', from_='q3@start') # center 5 m from q3 start
myline.place('s4')                                                # right after previous

myline.end_compose()

tt = myline.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# name             s_start      s_center         s_end
# ||drift_1              0          1.25           2.5
# q1                   2.5             3           3.5
# ||drift_2            3.5          4.25             5
# q2                     5           5.5             6
# q3                     6           6.5             7
# ||drift_3              7          8.75          10.5
# q4                  10.5            11          11.5
# s4                  11.5         11.55          11.6
# ||drift_4           11.6          11.8            12
# _end_point            12            12            12

Also in compose mode, elements can be created inline while placing them:

myline2 = env.new_line(name='myline2', length='line_length', compose=True)

myline2.new('q10', xt.Quadrupole, length=1.0, k1='kquad', at='s.q1'),
myline2.new('q20', xt.Quadrupole, length=1.0, k1='-kquad', anchor='start', at=5.0),
myline2.new('q30', xt.Quadrupole, length=1.0, k1='kquad', anchor='start', at='q20@end'),
myline2.new('q40', xt.Quadrupole, length=1.0, k1='-kquad',
            anchor='center', at='ds.q4', from_='q30@start'),
myline2.new('s40', xt.Sextupole, length=0.1),  # placed after previous

myline2.end_compose()

tt = myline2.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# name             s_start      s_center         s_end
# ||drift_4              0          1.25           2.5
# q10                  2.5             3           3.5
# ||drift_5            3.5          4.25             5
# q20                    5           5.5             6
# q30                    6           6.5             7
# ||drift_6              7          8.75          10.5
# q40                 10.5            11          11.5
# s40                 11.5         11.55          11.6
# ||drift_7           11.6          11.8            12
# _end_point            12            12            12

Placing sub-lines at given s positions

As for normal elements, it is possible to place sublines at given s positions within a longer line. This is illustrated in the following example:

import xtrack as xt
import numpy as np

env = xt.Environment()
env.particle_ref = xt.Particles(p0c=2e9, mass0=xt.PROTON_MASS_EV)

sub_line = env.new_line(components=[
    env.new('mid_subline', xt.Marker, at=1.0),
    env.new('mq1_subline', xt.Quadrupole, length=0.3, at=-0.5, from_='mid_subline'),
    env.new('mq2_subline', xt.Quadrupole, length=0.3, at=0.5, from_='mid_subline')])

line = env.new_line(components=[
    env.new('bb', xt.Bend, length=1.0, at=2),
    env.place(sub_line, at=5.0),
    env.place(sub_line, at=8.0),
    ])

line.get_table()
# is:
# Table: 17 rows, 11 cols
# name                       s element_type isthick isreplica parent_name ...
# ||drift_3                  0 Drift           True     False None
# bb                       1.5 Bend            True     False None
# ||drift_4                2.5 Drift           True     False None
# ||drift_1::0           4.175 Drift           True     False None
# mq1_subline::0         4.525 Quadrupole      True     False None
# ||drift_2::0           4.825 Drift           True     False None
# mid_subline::0         5.175 Marker         False     False None
# ||drift_2::1           5.175 Drift           True     False None
# mq2_subline::0         5.525 Quadrupole      True     False None
# ||drift_5              5.825 Drift           True     False None
# ||drift_1::1           7.175 Drift           True     False None
# mq1_subline::1         7.525 Quadrupole      True     False None
# ||drift_2::2           7.825 Drift           True     False None
# mid_subline::1         8.175 Marker         False     False None
# ||drift_2::3           8.175 Drift           True     False None
# mq2_subline::1         8.525 Quadrupole      True     False None
# _end_point             8.825                False     False None

# Complete source: xtrack/examples/lattice_design/008c_place_line_at_s.py

Line mirroring and composition

Lines can be mirrored with the unary minus operator and combined with addition and multiplication:

env = xt.Environment()
env.particle_ref = xt.Particles(p0c=2e9, mass0=xt.PROTON_MASS_EV)
env['pi'] = np.pi
env['l_bend'] = 3.5
env['l_quad'] = 1.0
env['l_cell'] = 20.0
env['n_bends'] = 24
env['angle_bend'] = 'pi / n_bends'

env.new('mq', xt.Quadrupole, length='l_quad')
env.new('mb', xt.Bend, length='l_bend', angle='angle_bend')
env.new('mqf', 'mq', k1=0.1)
env.new('mqd', 'mq', k1=-0.1)

arc_half_cell = env.new_line(components=[
    env.place('mqf'),
    env.place('mb', at='l_cell/4 - (l_bend/2 + 0.2)', from_='mqf'),
    env.place('mb', at='l_cell/4 + (l_bend/2 + 0.2)', from_='mqf'),
    env.place('mqd', at='l_cell/2 - l_quad/2', from_='mqf'),
])

mirror_arc_half_cell = -arc_half_cell
mirror_arc_half_cell.get_table()
# Table: 8 rows, 11 cols
# name                   s element_type isthick isreplica parent_name ...
# mqd                    0 Quadrupole      True     False None
# ||drift_3              1 Drift           True     False None
# mb::0                1.3 Bend            True     False None
# ||drift_2            4.8 Drift           True     False None
# mb::1                5.2 Bend            True     False None
# ||drift_1            8.7 Drift           True     False None
# mqf                  9.5 Quadrupole      True     False None
# _end_point          10.5                False     False None

arc_cell = -arc_half_cell + arc_half_cell   # mirror then concatenate
arc_cell.get_table()
# Table: 15 rows, 11 cols
# name                     s element_type isthick isreplica parent_name ...
# mqd::0                   0 Quadrupole      True     False None
# ||drift_3::0             1 Drift           True     False None
# mb::0                  1.3 Bend            True     False None
# ||drift_2::0           4.8 Drift           True     False None
# mb::1                  5.2 Bend            True     False None
# ||drift_1::0           8.7 Drift           True     False None
# mqf::0                 9.5 Quadrupole      True     False None
# mqf::1                10.5 Quadrupole      True     False None
# ||drift_1::1          11.5 Drift           True     False None
# mb::2                 12.3 Bend            True     False None
# ||drift_2::1          15.8 Drift           True     False None
# mb::3                 16.2 Bend            True     False None
# ||drift_3::1          19.7 Drift           True     False None
# mqd::1                  20 Quadrupole      True     False None
# _end_point              21                False     False None

arc = 2 * arc_cell
arc.get_table()
# Table: 29 rows, 11 cols
# name                     s element_type isthick isreplica parent_name ...
# mqd.l::0                 0 Quadrupole      True      True mqd
# ||drift_3::0             1 Drift           True     False None
# mb2.l::0               1.3 Bend            True      True mb2
# ||drift_2::0           4.8 Drift           True     False None
# mb1.l::0               5.2 Bend            True      True mb1
# ||drift_1::0           8.7 Drift           True     False None
# mqf.l::0               9.5 Quadrupole      True      True mqf
# mqf.r::0              10.5 Quadrupole      True      True mqf
# ||drift_1::1          11.5 Drift           True     False None
# mb1.r::0              12.3 Bend            True      True mb1
# ||drift_2::1          15.8 Drift           True     False None
# mb2.r::0              16.2 Bend            True      True mb2
# ||drift_3::1          19.7 Drift           True     False None
# mqd.r::0                20 Quadrupole      True      True mqd
# mqd.l::1                21 Quadrupole      True      True mqd
# ||drift_3::2            22 Drift           True     False None
# mb2.l::1              22.3 Bend            True      True mb2
# ||drift_2::2          25.8 Drift           True     False None
# mb1.l::1              26.2 Bend            True      True mb1
# ||drift_1::2          29.7 Drift           True     False None
# mqf.l::1              30.5 Quadrupole      True      True mqf
# mqf.r::1              31.5 Quadrupole      True      True mqf
# ||drift_1::3          32.5 Drift           True     False None
# mb1.r::1              33.3 Bend            True      True mb1
# ||drift_2::3          36.8 Drift           True     False None
# mb2.r::1              37.2 Bend            True      True mb2
# ||drift_3::3          40.7 Drift           True     False None
# mqd.r::1                41 Quadrupole      True      True mqd
# _end_point              42                False     False None

Insert elements

It is possible to insert elements in a line also after its creation. The position of the new elements can be specified as absolute s position or as relative to an existing element. This is illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Create a set of new elements to be placed
env.new('s1', xt.Sextupole, length=0.1, k2=0.2)
env.new('s2', xt.Sextupole, length=0.1, k2=-0.2)
env.new('m1', xt.Marker)
env.new('m2', xt.Marker)
env.new('m3', xt.Marker)

# Insert the new elements in the line
line.insert([
    env.place('s1', at=5.),
    env.place('s2', anchor='end', at=-5., from_='q1@start'),
    env.place(['m1', 'm2'], at='m0@start'),
    env.place('m3', at='m0@end'),
    ])

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# ||drift_4              0         2.475          4.95
# s1                  4.95             5          5.05
# ||drift_6           5.05         7.025             9
# q0                     9            10            11
# ||drift_7             11         12.45          13.9
# s2                  13.9         13.95            14
# ||drift_8             14          16.5            19
# q1                    19            20            21
# ||drift_3             21          30.5            40
# m1                    40            40            40
# m2                    40            40            40
# m0                    40            40            40
# m3                    40            40            40
# _end_point            40            40            40

# Complete source: xtrack/examples/lattice_design/007b_insert_elements.py

Insert custom elements and elements instantiated by the user

It is possible to insert elements that are created by the user using the class directly instead of using the Environment.new method. This can be done in a single step or alternatively by first adding the element to the environment and then inserting it in the line. This is illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Instantiate elements using the class directly
mysext =  xt.Sextupole(length=0.1, k2=0.2)
myaperture =  xt.LimitEllipse(a=0.01, b=0.02)

# Insert the element in the line and, contextually, define its name:
line.insert('s1', mysext, at=5., from_='q1')

# Alternatively, add the element to the environment and then do the insertion:
env.elements['ap1'] = myaperture
line.insert('ap1', at='q0@start')

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# ap1                    9             9             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3..0            21        22.975         24.95
# s1                 24.95            25         25.05
# drift_3..2         25.05        32.525            40
# m0                    40            40            40
# _end_point            40            40            40

# The above is convenient when adding elements of types defined by the user.
# For example:

class MyElement:
    def __init__(self, param=1.0):
        self.param = param

    def track(self, particles):
        # Custom tracking logic
        self.px += self.param * particles.x

# Create multiple instances of MyElement and store them in the environment
env.elements['my_element1'] = MyElement(param=2.0)
env.elements['my_element2'] = MyElement(param=3.0)
env.elements['my_element3'] = MyElement(param=4.0)

# Insert in the lines
line.insert([
    env.place('my_element1', at=12.),
    env.place('my_element2', at=1, from_='my_element1'),
    env.place('my_element3', at=2, from_='my_element2')
]
)

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])

# Complete source: xtrack/examples/lattice_design/007h_insert_element_instantiated_by_user.py

Insert a line into another line

It is also possible to insert entire lines, as illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=50),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          35.5            50
# m0                    50            50            50

# Create a set of new elements to be placed
env.new('s1', xt.Sextupole, length=2., k2=0.2)
env.new('s2', xt.Sextupole, length=2., k2=-0.2)
env.new('m1', xt.Marker)
env.new('m2', xt.Marker)
env.new('m3', xt.Marker)

subline = env.new_line(components=[
    env.place('s1', at=1.0),
    env.place('s2', at=5.0),
    env.place('m1', at='s1@start'),
    env.place('m2', at='s2@start')])

# Insert the new elements in the line
line.insert([
    env.place(subline, anchor='start', at=1., from_='q1@end'),
    env.place('m3', at=30.)])

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3..0            21          21.5            22
# m1                    22            22            22
# s1                    22            23            24
# drift_4               24            25            26
# m2                    26            26            26
# s2                    26            27            28
# drift_3..4            28            29            30
# m3                    30            30            30
# drift_3..5            30            40            50
# m0                    50            50            50
# _end_point            50            50            50

# Complete source: xtrack/examples/lattice_design/007d_insert_line.py

Simplified syntax for single insertion

A compact syntax is available to perform a single insertion in a line. Note that when multiple insertions need to be made, it is significantly faster to install all the elements at once, as shown in the previous example. The compact syntax for single insertion is illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Create a set of new elements to be placed
env.new('s1', xt.Sextupole, length=0.1, k2=0.2)
env.new('s2', xt.Sextupole, length=0.1, k2=-0.2)
env.new('m1', xt.Marker)
env.new('m2', xt.Marker)
env.new('m3', xt.Marker)

# Insert the new elements in the line
line.insert('s1', at=5.)
line.insert('s2', anchor='end', at=-5., from_='q1@start')
line.insert(['m1', 'm2'], at='m0@start')
line.insert('m3', at='m0@end')

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1..0             0         2.475          4.95
# s1                  4.95             5          5.05
# drift_1..2          5.05         7.025             9
# q0                     9            10            11
# drift_2..0            11         12.45          13.9
# s2                  13.9         13.95            14
# drift_2..2            14          16.5            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m1                    40            40            40
# m0                    40            40            40
# m3                    40            40            40

# Complete source: xtrack/examples/lattice_design/007c_insert_individual.py

Append elements to a line

New elements can also be installed at the end of a line, as illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Create a set of new elements to be placed
env.new('s1', xt.Sextupole, length=0.1, k2=0.2)
env.new('s2', xt.Sextupole, length=0.1, k2=-0.2)
env.new('m1', xt.Marker)
env.new('m2', xt.Marker)
env.new('m3', xt.Marker)

# Insert the new elements in the line
line.append(['m1', 's1', 'm2', 's2', 'm3'])

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# m1                    40            40            40
# s1                    40         40.05          40.1
# m2                  40.1          40.1          40.1
# s2                  40.1         40.15          40.2
# m3                  40.2          40.2          40.2
# _end_point          40.2          40.2          40.2

# Elements can be appended also when they are created using the class directly.
# The element name is defined contextually:
myoct = xt.Octupole(length=0.1, k3=0.3)
line.append('o1', myoct)

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# m1                    40            40            40
# s1                    40         40.05          40.1
# m2                  40.1          40.1          40.1
# s2                  40.1         40.15          40.2
# m3                  40.2          40.2          40.2
# o1                  40.2         40.25          40.3
# _end_point          40.3          40.3          40.3

# Complete source: xtrack/examples/lattice_design/007e_append_elements.py

Remove elements from a line

Elements can be removed from a line using the Line.remove method. Thick elements are replaced by drift spaces, so that the position of all other elements is preserved. This is illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Remove two elements
line.remove('q1')
line.remove('m0')

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# drift_4               19            20            21
# drift_3               21          30.5            40
# _end_point            40            40            40

# Complete source: xtrack/examples/lattice_design/007f_remove_elements.py

Replace elements in a line

Elements in a line can be replaced with elements having the same length, as illustrated in the following example:

import xtrack as xt

# Create an environment
env = xt.Environment()

# Create a line with two quadrupoles and a marker
line = env.new_line(name='myline', components=[
    env.new('q0', xt.Quadrupole, length=2.0, at=10.),
    env.new('q1', xt.Quadrupole, length=2.0, at=20.),
    env.new('m0', xt.Marker, at=40.),
    ])

tt0 = line.get_table()
tt0.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# q1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Create a new element
env.new('s1', xt.Sextupole, length=2, k2=0.2)

# Replace `q1` with `s1`
line.replace('q1', 's1')

tt = line.get_table()
tt.show(cols=['s_start', 's_center', 's_end'])
# is:
# name             s_start      s_center         s_end
# drift_1                0           4.5             9
# q0                     9            10            11
# drift_2               11            15            19
# s1                    19            20            21
# drift_3               21          30.5            40
# m0                    40            40            40
# _end_point            40            40            40

# Complete source: xtrack/examples/lattice_design/007g_replace_elements.py

Slice elements in a line

It is possible to slice thick element with thin or thick slices, using the Uniform or the Teapot scheme. This is illustrated in the following example:

import numpy as np
import xtrack as xt

# We build a simple ring
pi = np.pi
lbend = 3
lquad = 0.3
env = xt.Environment()
line = env.new_line(components=[
    env.new('mqf.1', xt.Quadrupole, length=lquad, k1=0.1),
    env.new('msf.1', xt.Sextupole, length=0.1, k2=0.02),
    env.new('d1.1',  xt.Drift, length=0.9),
    env.new('mb1.1', xt.Bend, length=lbend, k0=pi / 2 / lbend, h=pi / 2 / lbend),
    env.new('d2.1',  xt.Drift, length=1),

    env.new('mqd.1', xt.Quadrupole, length=lquad, k1=-0.7),
    env.new('d3.1',  xt.Drift, length=1),
    env.new('mb2.1', xt.Bend, length=lbend, k0=pi / 2 / lbend, h=pi / 2 / lbend),
    env.new('d4.1',  xt.Drift, length=1),

    env.new('mqf.2', xt.Quadrupole, length=lquad, k1=0.1),
    env.new('d1.2',  xt.Drift, length=1),
    env.new('mb1.2', xt.Bend, length=lbend, k0=pi / 2 / lbend, h=pi / 2 / lbend),
    env.new('d2.2',  xt.Drift, length=1),

    env.new('mqd.2', xt.Quadrupole, length=lquad, k1=-0.7),
    env.new('d3.2',  xt.Drift, length=1),
    env.new('mb2.2', xt.Bend, length=lbend, k0=pi / 2 / lbend, h=pi / 2 / lbend),
    env.new('d4.2',  xt.Drift, length=1),
])
line.set_particle_ref('proton', p0c=1.2e9)

line_before_slicing = line.copy() # Keep for comparison

# Slice different elements with different strategies (in case multiple strategies
# apply to the same element, the last one takes precedence)
line.slice_thick_elements(
    slicing_strategies=[
        # Slicing with thin elements
        xt.Strategy(slicing=xt.Teapot(1)), # (1) Default applied to all elements
        xt.Strategy(slicing=xt.Uniform(2), element_type=xt.Bend), # (2) Selection by element type
        xt.Strategy(slicing=xt.Teapot(3), element_type=xt.Quadrupole),  # (4) Selection by element type
        xt.Strategy(slicing=xt.Teapot(4), name='mb1.*'), # (5) Selection by name pattern
        # Slicing with thick elements
        xt.Strategy(slicing=xt.Uniform(2, mode='thick'), name='mqf.*'), # (6) Selection by name pattern
        # Do not slice (leave untouched)
        xt.Strategy(slicing=None, name='mqd.1') # (7) Selection by name
    ])

# Ispect the result:

ltable = line.get_table(attr=True).cols['s', 'isthick', 'element_type',
                                        'parent_name', 'k0l', 'k1l', 'k2l']

# The sextupole msf.1 has one thin slice, as default strategy (1) is applied.
ltable.rows['msf.1_entry':'msf.1_exit']
# returns:
#
# Table: 5 rows, 8 cols
# name                s isthick element_type         parent_name k0l k1l   k2l
# msf.1_entry       0.3   False Marker                      None   0   0     0
# drift_msf.1..0    0.3    True DriftSliceSextupole        msf.1   0   0     0
# msf.1..0         0.35   False ThinSliceSextupole         msf.1   0   0 0.002
# drift_msf.1..1   0.35    True DriftSliceSextupole        msf.1   0   0     0
# msf.1_exit        0.4   False Marker                      None   0   0     0

# The bend mb2.1 has three thin slices, as strategy (2) is applied.
ltable.rows['mb2.1_entry':'mb2.1_exit']
# returns:
#
# Table: 9 rows, 8 cols
# name               s isthick element_type         parent_name      k0l k1l k2l
# mb2.1_entry      6.6   False Marker                      None        0   0   0
# mb2.1..entry_map 6.6   False ThinSliceBendEntry         mb2.1        0   0   0
# drift_mb2.1..0   6.6    True DriftSliceBend             mb2.1        0   0   0
# mb2.1..0         7.6   False ThinSliceBend              mb2.1 0.785398   0   0
# drift_mb2.1..1   7.6    True DriftSliceBend             mb2.1        0   0   0
# mb2.1..1         8.6   False ThinSliceBend              mb2.1 0.785398   0   0
# drift_mb2.1..2   8.6    True DriftSliceBend             mb2.1        0   0   0
# mb2.1..exit_map  9.6   False ThinSliceBendExit          mb2.1        0   0   0
# mb2.1_exit       9.6   False Marker                      None        0   0   0

# The quadrupole mqd.2 has four thin slices, as strategy (3) is applied.
ltable.rows['mqd.2_entry':'mqd.2_exit']
# returns:
#
# Table: 9 rows, 8 cols
# name                   s isthick element_type         parent_name k0l   k1l ...
# mqd.2_entry         15.9   False Marker                      None   0     0
# drift_mqd.2..0      15.9    True DriftSliceQuadrupole       mqd.2   0     0
# mqd.2..0         15.9375   False ThinSliceQuadrupole        mqd.2   0 -0.07
# drift_mqd.2..1   15.9375    True DriftSliceQuadrupole       mqd.2   0     0
# mqd.2..1           16.05   False ThinSliceQuadrupole        mqd.2   0 -0.07
# drift_mqd.2..2     16.05    True DriftSliceQuadrupole       mqd.2   0     0
# mqd.2..2         16.1625   False ThinSliceQuadrupole        mqd.2   0 -0.07
# drift_mqd.2..3   16.1625    True DriftSliceQuadrupole       mqd.2   0     0
# mqd.2_exit          16.2   False Marker                      None   0     0

# The quadrupole mqf.1 has two thick slices, as strategy (6) is applied.
ltable.rows['mqf.1_entry':'mqf.1_exit']
# returns:
#
# Table: 4 rows, 8 cols
# name                s isthick element_type         parent_name k0l   k1l k2l
# mqf.1_entry         0   False Marker                      None   0     0   0
# mqf.1..0            0    True ThickSliceQuadrupole       mqf.1   0 0.015   0
# mqf.1..1         0.15    True ThickSliceQuadrupole       mqf.1   0 0.015   0
# mqf.1_exit        0.3   False Marker                      None   0     0   0

# The quadrupole mqd.1 is left untouched, as strategy (7) is applied.
ltable.rows['mqd.1']
# returns:
#
# Table: 1 row, 8 cols
# name               s isthick element_type         parent_name k0l   k1l k2l
# mqd.1            5.3    True Quadrupole                  None   0 -0.21   0


########################################
# Change properties of sliced elements #
########################################

# Sliced elements are updated whenever their parent is changed. For example:

# Inspect a quadrupole:
ltable.rows['mqf.1.*']
# returns:
#
# Table: 4 rows, 8 cols
# name                s isthick element_type         parent_name k0l   k1l k2l
# mqf.1_entry         0   False Marker                      None   0     0   0
# mqf.1..0            0    True ThickSliceQuadrupole       mqf.1   0 0.015   0
# mqf.1..1         0.15    True ThickSliceQuadrupole       mqf.1   0 0.015   0
# mqf.1_exit        0.3   False Marker                      None   0     0   0

# Change the the strength of the parent
line['mqf.1'].k1 = 0.2

# Inspect
ltable = line.get_table(attr=True).cols['s', 'isthick', 'element_type',
                                        'parent_name', 'k0l', 'k1l', 'k2l']
ltable.rows['mqf.1.*']
# returns (the strength of the slices has changed):
#
# Table: 4 rows, 8 cols
# name                s isthick element_type         parent_name k0l  k1l k2l
# mqf.1_entry         0   False Marker                      None   0    0   0
# mqf.1..0            0    True ThickSliceQuadrupole       mqf.1   0 0.03   0
# mqf.1..1         0.15    True ThickSliceQuadrupole       mqf.1   0 0.03   0
# mqf.1_exit        0.3   False Marker                      None   0    0   0

# Complete source: xtrack/examples/toy_ring/003_slicing.py

Cut line elements at given s positions

The method xtrack.Line.cut_at_s() allows for cutting the line elements at the specified s positions. In the example before we take the same toy ring introduced in the earlier example and we cut it into 100 equal length slices:

hundred_cuts = np.linspace(0, line.get_length(), num=100)
print(f'Make cuts every {hundred_cuts[1]} metres')  # => 21 centimetres
line.cut_at_s(s=list(hundred_cuts))

line.get_table()
# returns:
#
# Table: 139 rows, 7 cols
# name                    s element_type         isthick isreplica parent_name ...
# mqf.1_entry             0 Marker                 False     False        None
# mqf.1..0                0 ThickSliceQuadrupole    True     False       mqf.1
# mqf.1..1         0.214141 ThickSliceQuadrupole    True     False       mqf.1
# mqf.1_exit            0.3 Marker                 False     False        None
# d1.1..0               0.3 DriftSlice              True     False        d1.1
# d1.1..1          0.428283 DriftSlice              True     False        d1.1
# d1.1..2          0.642424 DriftSlice              True     False        d1.1
# d1.1..3          0.856566 DriftSlice              True     False        d1.1
# d1.1..4           1.07071 DriftSlice              True     False        d1.1
# d1.1..5           1.28485 DriftSlice              True     False        d1.1
# mb1.1_entry           1.3 Marker                 False     False        None
# mb1.1..entry_map      1.3 ThinSliceBendEntry     False     False       mb1.1
# mb1.1..0              1.3 ThickSliceBend          True     False       mb1.1
# mb1.1..1          1.49899 ThickSliceBend          True     False       mb1.1
# mb1.1..2          1.71313 ThickSliceBend          True     False       mb1.1
# mb1.1..3          1.92727 ThickSliceBend          True     False       mb1.1
# mb1.1..4          2.14141 ThickSliceBend          True     False       mb1.1
# etc...

# Complete source: xtrack/examples/toy_ring/007_cut_at_s.py

Remove lines

Lines stored in the environment can be removed with del:

del env.lines['fodo']

Reference particles

Reference particles define the beam mass, energy, and charge used by optics tools such as Line.twiss. A reference particle must be set before running Twiss.

Set directly on a line

Assign a built-in species with the desired energy (total energy in eV):

import xtrack as xt

env = xt.Environment()

env['l_quad'] = 1.0
env['k_quad'] = 0.1
env.new('qf', xt.Quadrupole, length='l_quad', k1='k_quad')
env.new('qd', xt.Quadrupole, length='l_quad', k1='-k_quad')
env.new('dr', xt.Drift, length=5.0)

line = env.new_line(components=['qf', 'dr', 'qd', 'dr'])

line.set_particle_ref('proton', energy0=2e9)  # 2 GeV total energy

# alternative set the momentum, kinetic energy, or gamma
# line.set_particle_ref('proton', p0c=2e9)
# line.set_particle_ref('proton', kinetic_energy0=200e6)
# line.set_particle_ref('proton', gamma0=10.5)

# set mass0, charge0 instead of particle type
# line.set_particle_ref(mass0=xt.PROTON_MASS_EV, charge0=1, p0c=2e9)

tw = line.twiss4d()  # uses the reference particle above

Reuse particles stored in the environment

You can define reusable particles (optionally with deferred expressions) and attach them as the line reference. Particles can live in the environment and be driven by variables.

import xtrack as xt

env = xt.Environment()
env['energy_gev'] = 5.0

env.new_particle(name='my_ref_part',
                 mass0=xt.PROTON_MASS_EV,
                 q0=1,
                 energy0='energy_gev * 1e9')  # deferred expression

# Inspect stored particles in the environment
env.particles.get_table()
# is:
# Table: 1 row, 7 cols
# name                mass0       charge0       energy0           p0c ...
# my_ref_part   9.38272e+08             1      5.25e+09   5.16548e+09

# Any line built from this environment can reuse the same particle definition
line = env.new_line(name='ring', components=[
      env.new('qf', xt.Quadrupole, length=1.0, k1=0.1),
      env.new('dr', xt.Drift, length=5.0),
      env.new('qd', xt.Quadrupole, length=1.0, k1=-0.1),
      env.new('dr2', xt.Drift, length=5.0),
   ])

# Set reference particle for the line
line.particle_ref = 'my_ref_part'

env['energy_gev'] = 5.25            # updates reference energy automatically
tw = line.twiss4d()                 # Twiss now uses the updated reference

# Inspect the deferred expression on the particle
env.ref['my_ref_part'].energy0.xdeps.info()
# Info for particles['my_ref_part'].energy0
#
# value: [5.25e+09]
#
# controlled by expr:
#   particles['my_ref_part'].energy0 = (vars['energy_gev'] * 1000000000.0)
#
# expr_dependencies:
#   vars['energy_gev'] = 5.25
#
# controlled_targets: None

Remove reference particles

Particles stored in the environment can be removed with del:

# Remove a stored particle
del env.particles['my_ref_part']

Saving and loading environment or individual lines

Environments (including all elements and lines) can be serialized to JSON and loaded back, and individual lines can be saved and loaded separately.

import xtrack as xt

env = xt.Environment()
env['k1'] = 0.1
env.new('qf', xt.Quadrupole, length=1.0, k1='k1')
env.new('qd', xt.Quadrupole, length=1.0, k1='-k1')
env.new_line(name='line_a', components=['qf', 'qd'])
env.new_line(name='line_b', components=['qd', 'qf'])

# Save whole environment (variables, elements, lines)
env.to_json('env.json')

# Reload environment
env2 = xt.load('env.json')

# Save a single line (variables and elements are included automatically)
env['line_a'].to_json('line_a.json')

# Reload the line
line_loaded = xt.load('line_a.json')
env3 = line_loaded.env # get the environment from the line

Loading MAD-X lattices

Import via cpymad (legacy)

You can also import a MAD-X sequence using cpymad by first running MAD-X and then converting the in-memory sequence to an Xsuite line. This route depends on MAD-X being available and will be discontinued alongside MAD-X end-of-support plans.

from cpymad.madx import Madx
import xtrack as xt

mad = Madx()
# example files from xtrack/test_data/ps_sftpro/
mad.call('ps_sftpro/ps.seq')
mad.call('ps_sftpro/ps_hs_sftpro.str')
mad.use(sequence='ps')

line = xt.Line.from_madx_sequence(mad.sequence.ps,
                                  deferred_expressions=True)

Choose this approach only when you explicitly need to run MAD-X calculations before importing; otherwise prefer the native xt.load parser above.