A Bmad lattice parser, formatter, and analysis toolkit for particle accelerator
lattice files. It parses, formats, lints, diffs, inspects, and templates Bmad
lattices, and also understands Tao tao.init / Fortran-namelist files.
pip install latformFrom source / with a conda environment
conda env create -n latform python=3.12 pip
conda activate latform
git clone https://github.com/ken-lauer/latform
cd latform
python -m pip install .Optional extras — .[test] (test suite), .[doc] (docs), .[lsp] (language
server), or combine them: .[test,doc].
# Format a lattice and print to stdout
latform my_lattice.bmad
# Format in place
latform -i my_lattice.bmad
# Preview what the formatter would change (no writes)
latform --diff my_lattice.bmad
# Format from a pipe
cat my_lattice.bmad | latform -
# Format every file in a directory, in place
latform -i *.bmad
# Lint without reformatting (exits non-zero on findings — good for CI)
latform-lint my_lattice.bmadPoint latform at a Tao tao.init and it formats the tao.init and every
Bmad lattice it references:
latform -i tao.init # rewrite the init and its lattices in place
latform --diff tao.init # preview all of the changeslatform installs several focused commands:
| Command | What it does |
|---|---|
latform |
Parse and format Bmad lattice files (and tao.init namelists) |
latform-lint |
Lint lattice files without reformatting (non-zero exit on findings) |
latform-apply |
Apply value overrides / renames to a single template file |
latform-template |
Expand a template set across many instances |
latform-dump |
Extract parameters and element information |
latform-diff |
Compare two lattice files structurally |
latform-gitdiff |
Compare a lattice file across git revisions |
latform-graph |
Visualize the file dependency (call) tree |
Run any command with -h for its full option list, or see the
CLI reference. A ninth command,
latform-lsp, provides editor integration — see
Editor integration.
Format in place and lint as two steps:
latform -i lat/*.bmad # reformat
latform-lint -r lat/main.bmad # lint recursively; exits 1 on any findingSet your project's rules once in a latform.toml (see
Configuration) so every invocation and every contributor gets
the same result. latform --lint reports lints alongside the formatted output;
latform-lint is the CI-friendly, exit-code-driven counterpart.
# Inline old,new pairs (repeatable)
latform -R 'Q1,QF' -R 'Q2,QD' my_lattice.bmad
# From a CSV of old,new pairs
latform --rename-file renames.csv my_lattice.bmadlatform -r my_lattice.bmad # parse recursively, following `call`s
latform --flatten my_lattice.bmad # inline all called files into one outputlatform-dump my_lattice.bmad # parameters, used, and unused elements
latform-dump -u my_lattice.bmad # only unused elements
latform-dump -m 'Q*' my_lattice.bmad # filter by glob (or -r for regex)
latform-dump -d ',' my_lattice.bmad # CSV-style outputlatform-diff old.bmad new.bmad # structural diff of params & elements
latform-gitdiff my_lattice.bmad HEAD~1 # compare a file against an earlier commitlatform-graph my_lattice.bmad # text tree of `call`ed files
latform-graph -f mermaid -o deps.mmd main.bmad # Mermaid diagram to a fileA Bmad template is itself valid Bmad; a YAML sidecar supplies per-element values.
# values.yaml: Q1: { k1: 1.523 }
latform-apply quad.bmad --values values.yaml
latform-apply quad.bmad --rename 'Q(\d+)' 'ARC_Q\1' # regex renameThe same command edits tao.init / namelist files, keyed by namelist group:
latform-apply tao.init --set tao_params global%n_opti_cycles 50 -iDescribe the template files, per-instance values, and renames in an
instances.yaml, then generate every instance at once:
latform-template instances.yaml -d build/ # write instances to build/
latform-template instances.yaml -d build/ --dry-run # list files without writingSee Templating for the instances.yaml schema
(prefix / suffix / regex / parts renames, shared context: files, and per-instance
tao_init: generation).
latform and latform-lint read project-wide settings from a latform.toml
(bare tables) or a [tool.latform] section in pyproject.toml. Discovery walks
upward from the current directory: the first latform.toml wins, otherwise the
first pyproject.toml that contains a [tool.latform] table. --config PATH
forces a specific file; --no-config skips config entirely.
Precedence is CLI flag → config → built-in default. Only latform and
latform-lint read this file (latform-apply / latform-template take their
namelist options as flags). The block below lists every recognized key with its
default; unknown keys in [format] warn and are ignored.
# latform.toml — all options shown with their defaults.
# ── Entry points ────────────────────────────────────────────────────────────
# Used when latform / latform-lint run with NO file arguments. Paths are
# relative to this config file. Providing either implies recursive parsing.
top-level = ["lat/main.bmad"] # list of lattice files
tao-init = "tao.init" # a path, or a list of paths; its
# design_lattice files become the top level
# ── Formatting ──────────────────────────────────────────────────────────────
# Same names as the CLI flags, minus the leading `--`. An explicit CLI flag
# always overrides the value here.
[format]
line-length = 100 # int
max-line-length = 130 # int; default = 130% of line-length
compact = false # bool; no blank lines between statement types
name-case = "upper" # upper | lower | same (element names)
attribute-case = "lower" # upper | lower | same (attribute names)
kind-case = "lower" # upper | lower | same (element types/keywords)
builtin-case = "lower" # upper | lower | same (builtin functions)
controller-variable-case = "same" # upper | lower | same (overlay/group/ramper vars)
section-break-character = "-" # str
section-break-width = 100 # int; default = line-length
strip-comments = false # bool
# Namelist (tao.init / *.nml) formatting — also under [format]:
format-namelist = true # bool; false = preserve layout verbatim
namelist-indent = 2 # int >= 0; field indent width
namelist-field-case = "lower" # upper | lower | same (field names)
namelist-align-equals = true # bool; align `=` into a column
namelist-align-comments = true # bool; align trailing `!` comments
namelist-logicals = ["T", "F"] # [true, false] tokens, or `false` to
# leave logical values untouched
# ── Linting ─────────────────────────────────────────────────────────────────
[lint]
ignore = ["LF002"] # list of codes to suppress everywhere
min-name-length = 1 # int >= 1; LF009 flags names shorter than this
# (at 1, only the confusable i/l/o are flagged)
builtin-constant-rtol = 1e-4 # positive float; LF010 match tolerance vs.
# built-in physical constants
# Per-file lint suppression. Keys are globs matched against the file's name,
# full path, and path relative to this config; values are codes to ignore there.
[lint.per-file-ignores]
"legacy/*.bmad" = ["LF004", "LF006"]
"vendor/**/*.bmad" = ["LF004"]Lint ignores are cumulative: --ignore on the CLI, the global [lint] ignore
list, and any matching [lint.per-file-ignores] entries all apply.
The equivalent in pyproject.toml nests everything one level deeper under
[tool.latform]:
[tool.latform]
top-level = ["lat/main.bmad"]
[tool.latform.format]
line-length = 100
name-case = "upper"
kind-case = "lower"
[tool.latform.lint]
ignore = ["LF002"]
[tool.latform.lint.per-file-ignores]
"legacy/*.bmad" = ["LF004", "LF006"]With no config at all, the tools fall back to a tao.init found in the current
directory or a parent, so they work in a bare Tao project directory. See the
Lint Codes reference
for what each LF0xx means.
latform ships a Language Server Protocol implementation, latform-lsp, for Bmad
lattice files. It provides go-to-definition, find-references, hover, completion,
rename, formatting (whole-file and range), document/workspace symbols, semantic
highlighting, code actions, and live diagnostics from the same lint rules as
latform-lint. It reads your project's latform.toml / pyproject.toml, so
formatting and diagnostics match the CLI.
The server is behind the optional lsp extra (it needs pygls):
pip install "latform[lsp]"
latform-lsp --help # runs over stdio; normally launched by your editorReady-made clients that discover latform-lsp and wire up the Bmad filetype for
you:
| Editor | Plugin |
|---|---|
| VS Code | ken-lauer/latform-vscode |
| Vim | ken-lauer/vim-latform |
| Neovim | ken-lauer/latform-lsp.nvim |
Each still needs latform-lsp on PATH (pip install "latform[lsp]"); see the
plugin's README for install and configuration.
Manual LSP Configuration
Any LSP client works — point it at the latform-lsp command over stdio,
associated with your Bmad files (*.bmad, *.lat, tao.init). Without one of
the plugins above:
Neovim — built-in LSP client (0.11+):
vim.filetype.add({
extension = { bmad = "bmad", lat = "bmad" },
filename = { ["tao.init"] = "bmad" },
})
vim.lsp.config("latform", {
cmd = { "latform-lsp" },
filetypes = { "bmad" },
root_markers = { "latform.toml", "pyproject.toml", "tao.init", ".git" },
})
vim.lsp.enable("latform")Vim — with vim-lsp:
augroup latform_ft
autocmd!
autocmd BufRead,BufNewFile *.bmad,*.lat,tao.init setfiletype bmad
augroup END
if executable('latform-lsp')
autocmd User lsp_setup call lsp#register_server({
\ 'name': 'latform',
\ 'cmd': {server_info->['latform-lsp']},
\ 'allowlist': ['bmad'],
\ })
endifThe log level and log file can be passed through any client (--log-level,
--log-file, or the LATFORM_LSP_LOG_LEVEL / LATFORM_LSP_LOG_FILE environment
variables). For other editors, see the LSP project's
list of client implementations.
from latform import parse, parse_file, parse_file_recursive
result = parse_file("my_lattice.bmad")See the Python API docs for details.
- CLI reference — full usage for every command
- Configuration —
latform.toml/pyproject.toml - Style guide — the formatting rules latform applies
- Python API — using latform programmatically