Working with tables

Several Xsuite methods return information in the form of Table objects. Examples of such methods are xtrack.Line.get_table(), xtrack.Line.twiss(), xtrack.Line.survey(), xtrack.Line.vars.get_table(), xtrack.Environment.vars.get_table().

See Table class in the Reference guide for the base table API.

Table objects offer several capabilities to access data, for example by selecting rows and columns in various ways. This is illustrated in the following example:

import numpy as np
import xtrack as xt

pi = np.pi
lbend = 3

# Build a simple ring
env = xt.Environment()
line = env.new_line(components=[
    env.new('mqf.1', xt.Quadrupole, length=0.3, k1=0.1),
    env.new('d1.1',  xt.Drift, length=1),
    env.new('mb1.1', xt.Bend, length=lbend, angle=pi / 2),
    env.new('d2.1',  xt.Drift, length=1),

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

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

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

# Get the twiss table of the line with the strengths
tab = line.twiss4d(strengths=True)

# The table can be printed
tab.show()
# prints:
#
# name       element_type             s          betx          bety ...
# mqf.1      Quadrupole               0       1.27738       4.79104
# d1.1       Drift                  0.3       1.27738       4.79104
# mb1.1      Bend                   1.3       2.26749       5.20838
# d2.1       Drift                  4.3       2.88391       8.99176
# mqd.1      Quadrupole             5.3       1.72481       11.0967
# d3.1       Drift                  5.6       1.72481       11.0967
# mb2.1      Bend                   6.6       2.88391       8.99176
# d4.1       Drift                  9.6       2.26749       5.20838
# mqf.2      Quadrupole            10.6       1.27738       4.79104
# d1.2       Drift                 10.9       1.27738       4.79104
# mb1.2      Bend                  11.9       2.26749       5.20838
# d2.2       Drift                 14.9       2.88391       8.99176
# mqd.2      Quadrupole            15.9       1.72481       11.0967
# d3.2       Drift                 16.2       1.72481       11.0967
# mb2.2      Bend                  17.2       2.88391       8.99176
# d4.2       Drift                 20.2       2.26749       5.20838
# _end_point                       21.2       1.27738       4.79104

# Single values can be accessed using the column name and the row name. For example:
tab['s', 'mb2.1'] # is 6.6

# Entire columns can be accessed using the column name. For example:
tab['s'] # is [0.0, 0.3, 1.3, 4.3, 5.3, 5.6, 6.6, 9.6, 10.6, 10.9, 11.9, ...

# The `.cols` attribute can be used to access multiple columns (the output is 
# a Table object). For example:
tab.cols['betx bety alfx alfy']
# returns:
#
# TwissTable: 17 rows, 5 cols
# name                betx          bety          alfx          alfy
# mqf.1            1.27738       4.79104     0.0997349      0.103198
# d1.1             1.27738       4.79104    -0.0997349     -0.103198
# mb1.1            2.26749       5.20838     -0.890376     -0.314144
# d2.1             2.88391       8.99176      0.890376     -0.946981
# mqd.1            1.72481       11.0967      0.268732      -1.15793
# d3.1             1.72481       11.0967     -0.268732       1.15793
# mb2.1            2.88391       8.99176     -0.890376      0.946981
# d4.1             2.26749       5.20838      0.890376      0.314144
# mqf.2            1.27738       4.79104     0.0997349      0.103198
# d1.2             1.27738       4.79104    -0.0997349     -0.103198
# mb1.2            2.26749       5.20838     -0.890376     -0.314144
# d2.2             2.88391       8.99176      0.890376     -0.946981
# mqd.2            1.72481       11.0967      0.268732      -1.15793
# d3.2             1.72481       11.0967     -0.268732       1.15793
# mb2.2            2.88391       8.99176     -0.890376      0.946981
# d4.2             2.26749       5.20838      0.890376      0.314144
# _end_point       1.27738       4.79104     0.0997349      0.103198

# Simple expressions can be used in the cols
tab.cols['betx dx dx/sqrt(betx)']
# returns:
#
# TwissTable: 17 rows, 4 cols
# name                betx            dx dx/sqrt(betx)
# mqf.1            1.27738       2.27721       2.01486
# d1.1             1.27738       2.27721       2.01486
# mb1.1            2.26749       2.24303       1.48958
# d2.1             2.88391       1.84457       1.08619
# mqd.1            1.72481       1.67012       1.27168
# d3.1             1.72481       1.67012       1.27168
# mb2.1            2.88391       1.84457       1.08619
# d4.1             2.26749       2.24303       1.48958
# mqf.2            1.27738       2.27721       2.01486
# d1.2             1.27738       2.27721       2.01486
# mb1.2            2.26749       2.24303       1.48958
# d2.2             2.88391       1.84457       1.08619
# mqd.2            1.72481       1.67012       1.27168
# d3.2             1.72481       1.67012       1.27168
# mb2.2            2.88391       1.84457       1.08619
# d4.2             2.26749       2.24303       1.48958
# _end_point       1.27738       2.27721       2.01486

# The .rows attribute can be used access selected rows
tab.rows[['mqf.1', 'mqd.1']]
# returns:
#
# TwissTable: 2 rows, 98 cols
# TwissTable: 2 rows, 98 cols
# name  element_type             s          betx          bety ...
# mqf.1 Quadrupole               0       1.27738       4.79104
# mqd.1 Quadrupole             5.3       1.72481       11.0967

# Regular expressions can be used to select rows
tab.rows['mb.*']
# returns:
#
# TwissTable: 4 rows, 98 cols
# name  element_type             s          betx          bety ...
# mb1.1 Bend                   1.3       2.26749       5.20838
# mb2.1 Bend                   6.6       2.88391       8.99176
# mb1.2 Bend                  11.9       2.26749       5.20838
# mb2.2 Bend                  17.2       2.88391       8.99176

# Elements can be selected by type using the match search (applicable to any column)
tab.rows.match(element_type='Quadrupole')
# returns:
#
# TwissTable: 4 rows, 98 cols
# name  element_type             s          betx          bety ...
# mqf.1 Quadrupole               0       1.27738       4.79104
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# mqf.2 Quadrupole            10.6       1.27738       4.79104
# mqd.2 Quadrupole            15.9       1.72481       11.0967

# rows.match supports regular expressions
tab.rows.match(element_type='Quad.*|Be.*')
# returns:
# TwissTable: 8 rows, 98 cols
# name  element_type             s          betx          bety ...
# mqf.1 Quadrupole               0       1.27738       4.79104
# mb1.1 Bend                   1.3       2.26749       5.20838
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# mb2.1 Bend                   6.6       2.88391       8.99176
# mqf.2 Quadrupole            10.6       1.27738       4.79104
# mb1.2 Bend                  11.9       2.26749       5.20838
# mqd.2 Quadrupole            15.9       1.72481       11.0967
# mb2.2 Bend                  17.2       2.88391       8.99176

# rows.match_not can be used to select rows not matching a given condition.
# For example to select all elements that are not drifts:
tab.rows.match_not(element_type='Drift')
#
# TwissTable: 9 rows, 98 cols
# name  element_type             s          betx          bety ...
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# d3.1  Drift                  5.6       1.72481       11.0967
# mb2.1 Bend                   6.6       2.88391       8.99176
# d4.1  Drift                  9.6       2.26749       5.20838
# mqf.2 Quadrupole            10.6       1.27738       4.79104
# d1.2  Drift                 10.9       1.27738       4.79104
# mb1.2 Bend                  11.9       2.26749       5.20838
# d2.2  Drift                 14.9       2.88391       8.99176
# mqd.2 Quadrupole            15.9       1.72481       11.0967

# A section of the table can be selected using names
tab.rows['mqd.1':'mqd.2']
# returns:
#
# TwissTable: 9 rows, 98 cols
# name  element_type             s          betx          bety ...
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# d3.1  Drift                  5.6       1.72481       11.0967
# mb2.1 Bend                   6.6       2.88391       8.99176
# d4.1  Drift                  9.6       2.26749       5.20838
# mqf.2 Quadrupole            10.6       1.27738       4.79104
# d1.2  Drift                 10.9       1.27738       4.79104
# mb1.2 Bend                  11.9       2.26749       5.20838
# d2.2  Drift                 14.9       2.88391       8.99176
# mqd.2 Quadrupole            15.9       1.72481       11.0967

# A section of the table can be selected using any given column. For example,
# to select all elements between s=3 and s=7:
tab.rows[3.0:7.0:'s']
# returns:
#
# TwissTable: 4 rows, 98 cols
# name  element_type             s          betx          bety ...
# d2.1  Drift                  4.3       2.88391       8.99176
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# d3.1  Drift                  5.6       1.72481       11.0967
# mb2.1 Bend                   6.6       2.88391       8.99176

# A section of the table can be selected using indexes relative one element
# (e.g. to get from three elements upstream of 'mqd.1' to two elements
# downstream of 'mb2.1')
tab.rows['mqd.1<<3':'mb2.1>>2']
# returns:
#
# TwissTable: 8 rows, 98 cols
# name  element_type             s          betx          bety ...
# d1.1  Drift                  0.3       1.27738       4.79104
# mb1.1 Bend                   1.3       2.26749       5.20838
# d2.1  Drift                  4.3       2.88391       8.99176
# mqd.1 Quadrupole             5.3       1.72481       11.0967
# d3.1  Drift                  5.6       1.72481       11.0967
# mb2.1 Bend                   6.6       2.88391       8.99176
# d4.1  Drift                  9.6       2.26749       5.20838
# mqf.2 Quadrupole            10.6       1.27738       4.79104

# As rows and column selectors return Table objects they can be chained for example
# in the following we select rows in the range 'd1.1'-'d2.2', which are not of type Drift,
# and that match the regular expression 'mb.*' and we select the columns `betx` and `bety`:
tab.rows['d1.1':'d2.2'].rows.match_not(element_type='Drift').rows.match(name='mb.*').cols['betx bety']
# returns:
#
# TwissTable: 3 rows, 3 cols
# name           betx          bety
# mb1.1       2.26749       5.20838
# mb2.1       2.88391       8.99176
# mb1.2       2.26749       5.20838

# Complete source: xtrack/examples/toy_ring/004b_work_with_table.py

Writing tables in TFS format

Tables can be exported to TFS with xtrack.Table.to_tfs(), including the Twiss tables returned by line.twiss(). The optional format_spec argument controls which columns and scalar headers are written, their order and names, and the numerical formatting. It accepts a Python dictionary, a JSON or TOML file path, or a built-in preset. Calls without format_spec retain the default TFS export behavior.

For the same selected values, types and row order, a specification defines the same output text across Xtrack releases. Additional table fields do not appear unless explicitly selected.

Selecting and formatting fields

A dictionary gives full control over the output. columns and scalars are required ordered lists; only the listed fields are written. Scalars become TFS @ headers. An empty list selects no fields in that category.

# A dictionary is the complete output contract. Only listed fields are emitted.
# name is mandatory; source_name defaults to name. Names are looked up before
# case conversion. The input table's rows and row order are preserved.
spec = {
    'version': '1',
    'case': 'upper',
    'float_format': '.16e',
    'columns': [
        {'name': 'name', 'type': 'string'},
        {'name': 's', 'type': 'float', 'format': '.9f'},
        {'name': 'betx', 'type': 'float'},
        {'name': 'bety', 'type': 'float'},
    ],
    'scalars': [
        {'name': 'TYPE', 'value': 'TWISS'},
        {'name': 'Q1', 'source_name': 'qx', 'type': 'float', 'format': '.12f'},
        {'name': 'Q2', 'source_name': 'qy', 'type': 'float', 'format': '.12f'},
    ],
}
tw.to_tfs('custom.tfs', format_spec=spec)

Each field requires name, which specifies the output name. source_name is optional and defaults to name. For example, the entry for Q1 reads tw.qx and writes a header named Q1. A scalar entry can instead provide a constant value, as with TYPE above; value and source_name cannot be combined.

The following options control formatting:

  • float_format sets the default for floating-point columns and scalars. A field’s format overrides it. Python notation such as .9f (nine decimal places), .16e (scientific notation with sixteen decimal places) and .12g (twelve significant digits) is supported. The default is .16e.

  • case defaults to "preserve"; "upper" and "lower" convert output field names and string values, including element names. Source lookup remains case-sensitive and happens before conversion. Numeric notation is unaffected.

  • type optionally fixes a field’s TFS type: string, float, int, bool or complex. Explicit types are useful for long-lived specifications and empty object arrays. Otherwise the type is inferred from the data.

  • A column’s width sets its minimum character width. The top-level default_column_width and numeric_column_width set minimum widths for all columns and numeric columns respectively (defaults: 0 and 16). Widths expand to fit the output; values are never truncated.

Rows retain their order in the input table. To select or reorder rows, use the table’s row selection methods before exporting. No additional class, version, timestamp or other metadata is added automatically. Missing fields, duplicate output names, unknown keys and invalid formats raise errors before the output file is opened. A specification cannot be combined with the legacy selection or formatting arguments such as include, exclude or float_precision.

Saving a specification in a file

A specification can be saved as JSON and passed directly to to_tfs:

# JSON is convenient for saving a specification made in Python.
Path('custom_format.json').write_text(json.dumps(spec, indent=2), encoding='utf-8')
tw.to_tfs('custom_from_json.tfs', format_spec='custom_format.json')

TOML is convenient for hand-maintained files because it supports comments. The same specification can be saved as tfs_format_spec.toml:

# The same specification as the dictionary in 004_tfs_format_spec.py.
version = "1"
case = "upper"
float_format = ".16e"

columns = [
    { name = "name", type = "string" },
    { name = "s", type = "float", format = ".9f" },
    { name = "betx", type = "float" },
    { name = "bety", type = "float" },
]

scalars = [
    { name = "TYPE", value = "TWISS" },
    { name = "Q1", source_name = "qx", type = "float", format = ".12f" },
    { name = "Q2", source_name = "qy", type = "float", format = ".12f" },
]

The file can then be used as follows:

# TOML is convenient for hand-maintained specifications with comments.
toml_spec = Path('tfs_format_spec.toml')
tw.to_tfs('custom_from_toml.tfs', format_spec=toml_spec)

Built-in format specifications

xt.format_spec provides ready-to-use specifications: optics_orbit_v1 for optics and closed orbit, optics_orbit_strengths_v1 to also include integrated strengths, and madx_twiss_v1 for a MAD-X-style layout. For a periodic Twiss table computed with strengths=True:

tw.to_tfs('optics_orbit.tfs', format_spec=xt.format_spec.optics_orbit_v1)
tw.to_tfs('optics_orbit_strengths.tfs',
          format_spec=xt.format_spec.optics_orbit_strengths_v1)
tw.to_tfs('madx_layout.tfs', format_spec=xt.format_spec.madx_twiss_v1)