Python API
latform can also be used as a Python library for parsing, formatting, and
analyzing Bmad lattice files programmatically.
Parsing
parse
Parse a Bmad lattice string into a list of statements.
import latform
statements = latform.parse("""
parameter[particle] = electron
Q1: quadrupole, L=0.5, k1=1.2
D1: drift, L=2.0
FODO: line = (Q1, D1)
use, FODO
""")
for st in statements:
print(type(st).__name__, st)
parse_file
Parse a single .bmad file from disk.
statements = latform.parse_file("my_lattice.bmad")
parse_file_recursive
Parse a lattice file and all files it references via call statements.
Returns a Files object containing parsed statements organized by filename.
files = latform.parse_file_recursive("my_lattice.bmad")
# Iterate over all files and their statements
for filename, statements in files.by_filename.items():
print(f"{filename}: {len(statements)} statements")
# Access the call graph
for caller, callees in files.filename_calls.items():
for callee in callees:
print(f"{caller} -> {callee}")
Format parsed statements back to a Bmad string using FormatOptions.
from latform.output import format_statements
from latform.types import FormatOptions
statements = latform.parse_file("my_lattice.bmad")
options = FormatOptions(
line_length=100,
name_case="upper",
kind_case="lower",
)
formatted = format_statements(statements, options)
print(formatted)
A convenience function that parses and formats a file in one step.
from latform.output import format_file
from latform.types import FormatOptions
formatted = format_file("my_lattice.bmad", FormatOptions())
All formatting behavior is controlled through the FormatOptions dataclass.
The defaults match the latform CLI defaults:
from latform.types import FormatOptions, NamelistFormatOptions
options = FormatOptions(
line_length=100, # target line length
max_line_length=130, # force multiline above this (default: 130% of line_length)
compact=False, # if True, no blank lines between statement types
indent_size=2, # spaces per indent level
indent_char=" ", # indentation character
comment_col=40, # column for inline comment alignment
name_case="upper", # element names: "upper", "lower", "same"
attribute_case="lower", # attribute names: "upper", "lower", "same"
kind_case="lower", # element types/keywords: "upper", "lower", "same"
builtin_case="lower", # builtin functions: "upper", "lower", "same"
section_break_character="-", # character for section break lines
section_break_width=None, # width of section breaks (None = line_length)
trailing_comma=False, # trailing comma in multiline blocks
renames={}, # element rename mapping {"old": "new"}
flatten_call=False, # inline call statements
flatten_inline=False, # inline call:: arguments
strip_comments=False, # remove all comments
newline_at_eof=True, # ensure trailing newline
namelist=NamelistFormatOptions(), # tao.init / namelist formatting (see below)
)
Formatting of Fortran-namelist files (*.init / *.nml, e.g. a Tao tao.init)
is controlled by the nested FormatOptions.namelist dataclass. It applies only
to the field section between a &name opener and its / terminator and changes
only layout and field-name case — a bare render() never modifies values.
(Value normalization is a separate layer; see
Namelist value normalization below.)
from latform.types import NamelistFormatOptions
namelist = NamelistFormatOptions(
indent_size=2, # spaces per field indent
indent_char=" ", # indentation character
blank_line_after_group=True, # one blank line after each group's "/"
field_case="lower", # field names: "upper", "lower", "same"
align_equals=True, # line up "=" within a run of fields
align_comments=True, # line up trailing "!" comments
)
Alignment is scoped to contiguous runs of fields (it resets at blank lines).
Rendering with these options is opt-in: Namelist.render() /
NamelistFile.render() reproduce the source verbatim when passed None, and
apply this formatting when given a NamelistFormatOptions.
!!! note
`align_equals` and `align_comments` both default to `True`. On the CLI these
map to `--no-namelist-align-equals` / `--no-namelist-align-comments`, and in
a `latform.toml` `[format]` table to `namelist-align-equals` /
`namelist-align-comments` (see [Configuration](configuration.md#namelist-formatting-settings)).
Namelist value normalization
latform.tao.format_tao_namelist renders a Tao namelist (a Namelist or a
whole NamelistFile) and, unlike a bare render(), first normalizes its
values against a schema of the standard Tao namelist groups bundled with
latform: unquoted character values are quoted, enum integer indices become names
(colors, line patterns, symbol types, fill patterns), and logicals are
canonicalized to a configurable (true, false) pair (default ("T", "F")).
fix_tao_namelist applies the same edits in place, without rendering.
from nmlform import NamelistFile
from latform.tao import format_tao_namelist, fix_tao_namelist
nf = NamelistFile.parse(
"&tao_params\n"
" global%prompt_color = 2\n"
" bmad_com%radiation_damping_on = .true.\n"
"/\n"
)
print(format_tao_namelist(nf))
# &tao_params
# global%prompt_color = 'red'
# bmad_com%radiation_damping_on = T
# /
fix_tao_namelist(nf, logicals=None) # normalize in place, but leave logicals alone
Only groups and fields present in the schema are touched; values in the
positional/anonymous field form (datum(4) = 'a' '' ...) are not normalized
yet. Pass fix_types=False to format_tao_namelist to render without any
normalization, or logicals=None to keep logical values as written. The
underlying validators are available too — latform.tao.schema.check_value and
latform.tao.schema.logical_value.
Statement Types
Parsed statements are instances of these classes from latform.statements:
| Class |
Description |
Example |
Element |
Element definition |
Q1: quadrupole, L=0.5 |
Line |
Beamline definition |
FODO: line = (Q1, D1) |
Constant |
Constant assignment |
K1_VAL = 1.5 |
Parameter |
Bracketed parameter |
parameter[particle] = electron |
Simple |
Keyword statement |
use, FODO or call, file=sub.bmad |
Assignment |
General assignment |
Q1[k1] = 0.5 |
Empty |
Empty/whitespace-only |
|
Working with Files
The Files class manages recursive parsing. MemoryFiles is a subclass
that starts from a string rather than a file on disk.
from latform.parser import Files, MemoryFiles
# From disk
files = Files(main=pathlib.Path("my_lattice.bmad"))
files.parse(recurse=True)
files.annotate()
# From a string
files = MemoryFiles.from_contents(
contents="Q1: quadrupole, L=0.5\n",
root_path="/path/to/lattice_dir/virtual.bmad",
)
files.parse()
files.annotate()
Diffing
Compare two parsed lattice files programmatically.
from latform.parser import Files
from latform.diff import calculate_diff
files1 = Files(main=pathlib.Path("old_lattice.bmad"))
files1.parse()
files1.annotate()
files2 = Files(main=pathlib.Path("new_lattice.bmad"))
files2.parse()
files2.annotate()
diff = calculate_diff(files1, files2)
for p in diff.params_added:
print(f"Added parameter: {p.name} = {p.new_value}")
for name, details in diff.eles_changed.items():
print(f"Changed element: {name}")
for attr, old, new in details.attrs_changed:
print(f" {attr}: {old} -> {new}")
API Reference
Bases: NamelistFile
A tao.init namelist file, with lattice/data/variable conveniences.
| Attributes: |
-
sources
(dict[str, NamelistFile])
–
Auxiliary namelist files from &tao_start (e.g. "data_file").
|
latform.TaoInit.beam_init
property
The &tao_beam_init groups, from the beam_file source.
latform.TaoInit.building_wall_sections
property
The &building_wall_section groups, from the building_wall_file source.
latform.TaoInit.d1_data
property
All &tao_d1_data groups, from the data_file source, in order.
latform.TaoInit.data_file
property
&tao_start data_file (where &tao_d1_data lives), if named.
latform.TaoInit.design_lattice
property
The &tao_design_lattice group (the first, if repeated).
latform.TaoInit.lattice_file_with_use_line
property
writable
lattice_file_with_use_line
Ordered, unquoted design_lattice(i)%file values with use_line names.
When set, rewrites the entries from (filename, use_line_names)
tuples, joining the names back into the @ suffix form.
latform.TaoInit.lattice_files
property
writable
Ordered design_lattice(i)%file values (unquoted), by index.
When set, rewrites the design_lattice(i)%file entries to files (1-based).
Existing entries are updated in place. Non-matching additional entries
are removed, and new entries are appended.
latform.TaoInit.plot_page
property
The &tao_plot_page groups, from the plot_file source.
latform.TaoInit.tao_start
property
The &tao_start group (the first, if repeated).
latform.TaoInit.var_file
property
&tao_start var_file (where &tao_var lives), if named.
latform.TaoInit.variables
property
All &tao_var groups, from the var_file source, in order.
latform.TaoInit.load_sources
load_sources(base=None, reader=None)
Resolve and load the auxiliary files named in &tao_start.
| Parameters: |
-
base
(Path, default:
None
)
–
Directory relative names resolve against. Defaults to the tao.init
directory.
-
reader
(callable, default:
None
)
–
path -> text | None hook used to read each resolved file
(None = "missing"). Defaults to reading from disk.
|
Source code in latform/tao/file.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384 | def load_sources(
self,
base: pathlib.Path | None = None,
reader: Callable[[pathlib.Path], str | None] | None = None,
) -> None:
"""
Resolve and load the auxiliary files named in ``&tao_start``.
Parameters
----------
base : pathlib.Path, optional
Directory relative names resolve against. Defaults to the tao.init
directory.
reader : callable, optional
``path -> text | None`` hook used to read each resolved file
(``None`` = "missing"). Defaults to reading from disk.
"""
if base is None:
base = self.filename.parent if self.filename is not None else pathlib.Path()
if reader is None:
reader = _read_if_exists
for key in SOURCE_FILE_KEYS:
name = self._start_value(key)
if not name:
continue
path = pathlib.Path(os.path.expandvars(name.strip()))
if not path.is_absolute():
path = base / path
path = path.resolve()
text = reader(path)
if text is not None:
self.sources[key] = NamelistFile.parse(text, filename=path)
|
latform.TaoInit.namelists_for
namelists_for(namelist_name)
All namelist_name groups from their resolved source, in file order.
Source code in latform/tao/file.py
| def namelists_for(self, namelist_name: str) -> list[Namelist]:
"""All ``namelist_name`` groups from their resolved source, in file order."""
source = self._source_for(namelist_name)
return source.namelists_by_name.get(namelist_name.lower(), [])
|
format_statements(statements, options=default_options)
Format a statement and return the code string
Source code in latform/output.py
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678 | def format_statements(
statements: Sequence[Statement] | Statement,
options: FormatOptions = default_options,
) -> str:
"""Format a statement and return the code string"""
if isinstance(statements, Statement):
statements = [statements]
res: list[OutputLine] = []
def maybe_add_blank_line():
if res and not res[-1].parts:
return
res.append(OutputLine())
last_statement = None
for statement in statements:
if options.newline_before_new_type:
if last_statement is not None:
if (
options.newline_between_lines
and isinstance(statement, Line)
and isinstance(last_statement, Line)
):
maybe_add_blank_line()
elif not isinstance(statement, type(last_statement)):
maybe_add_blank_line()
elif (
isinstance(statement, Simple)
and statement.statement != last_statement.statement
):
maybe_add_blank_line()
for line in format_nodes(statement, options=options):
if not line.parts and not line.comment:
maybe_add_blank_line()
else:
res.append(line)
last_statement = statement
while res and not res[0].parts and not res[0].comment:
res = res[1:]
text = "\n".join(line.render(options) for line in res)
if options.newline_at_eof and text:
return text + "\n"
return text
|
Block(opener=None, closer=None, items=list())
latform.types.Block.to_token
to_token(include_opener=True)
Convert this Block to a single Token with merged location information.
Source code in latform/types.py
| def to_token(self, include_opener: bool = True) -> Token:
"""
Convert this Block to a single Token with merged location information.
"""
return Token.join(self.flatten(include_opener=include_opener))
|
Bases: str, Enum
Stable identifiers for each lint, usable to opt out via the CLI.
OutputLine(indent=0, parts=list(), comment=None)
A single line of output with indentation and an optional comment.
Seq(opener=None, closer=None, items=list(), delimiter=SPACE)
Ordered sequence of mixed items:
* Attribute (a named value, i.e., a name=value pair)
* Expression (may be a single token)
* Seq (a nested sequence)
latform.types.Seq.to_call_name
Convert Seq to a single Token.
Source code in latform/types.py
204
205
206
207
208
209
210
211 | def to_call_name(self) -> CallName:
"""Convert Seq to a single Token."""
match self.items:
case Token() as name, Seq(opener="(") as args:
return CallName(name=name, args=args)
raise UnexpectedCallName(
f"Expected function call pattern not matched: {self} at {self.loc}"
)
|
latform.types.Seq.to_text
Convert Seq to its full output representation.
Source code in latform/types.py
232
233
234
235
236
237
238
239 | def to_text(self, opts: FormatOptions | None = None) -> str:
"""Convert Seq to its full output representation."""
from .output import FormatOptions, format_nodes
if opts is None:
opts = FormatOptions()
lines = format_nodes([self], options=opts)
return "\n".join(line.render(options=opts) for line in lines)
|
latform.types.Seq.to_token
to_token(include_opener=True)
Convert Seq to a single Token.
Source code in latform/types.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262 | def to_token(self, include_opener: bool = True) -> Token:
"""Convert Seq to a single Token."""
from .output import FormatOptions, format_nodes
if not include_opener:
nodes = Seq(items=self.items, delimiter=self.delimiter).to_output_nodes()
else:
nodes = self.to_output_nodes()
def check_can_tokenize(seq: Seq):
for item in seq.items:
if isinstance(item, Attribute):
raise ValueError("Unable to tokenize Attributes")
elif isinstance(item, Seq):
check_can_tokenize(item)
check_can_tokenize(self)
opts = FormatOptions()
(line,) = format_nodes(list(nodes), options=opts)
line.comment = None
return Token(line.render(options=opts), loc=self.loc)
|
Files(top_files=list(), stack=list(), by_filename=dict(), blocks_by_filename=dict(), local_file_to_source_filename=dict(), filename_calls=dict(), tao_init=None)
Represents a collection of parsed files starting from one or more
top-level entry points.
latform.parser.Files.call_graph_edges
property
Return a list of (caller, callee) string edges for visualization.
latform.parser.Files.main
property
The first top-level file; convenient for single-entry cases.
latform.parser.Files.annotate
Resolve named items across all parsed files.
Source code in latform/parser.py
861
862
863
864
865
866
867
868 | def annotate(self):
"""
Resolve named items across all parsed files.
"""
named = self.get_named_items()
for fn in self.by_filename:
self._annotate_file(fn, named)
self._resolve_element_types_in_order()
|
latform.parser.Files.flatten_all
flatten_all(call, inline)
Flatten each top-level file independently, keyed by its path.
Source code in latform/parser.py
| def flatten_all(self, call: bool, inline: bool) -> dict[pathlib.Path, list[Statement]]:
"""Flatten each top-level file independently, keyed by its path."""
return {top: self.flatten(call=call, inline=inline, top=top) for top in self.top_files}
|
latform.parser.Files.from_tao_init
classmethod
Build a :class:Files over the lattices listed in a tao.init.
The design_lattice(i)%file entries of the &tao_design_lattice
group become the top-level files, resolved (with environment-variable
expansion) relative to the tao.init file's directory.
Source code in latform/parser.py
684
685
686
687
688
689
690
691
692
693
694
695 | @classmethod
def from_tao_init(cls, path: pathlib.Path | str) -> "Files":
"""Build a :class:`Files` over the lattices listed in a ``tao.init``.
The ``design_lattice(i)%file`` entries of the ``&tao_design_lattice``
group become the top-level files, resolved (with environment-variable
expansion) relative to the ``tao.init`` file's directory.
"""
path = pathlib.Path(path)
tao_init = TaoInit.from_file(path)
top_files = _resolve_lattice_paths(tao_init.lattice_files, path.parent)
return cls(top_files=top_files, tao_init=tao_init)
|
latform.parser.Files.get_named_items
Aggregate named items from all files.
Source code in latform/parser.py
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925 | def get_named_items(self) -> dict[Token, Statement]:
"""
Aggregate named items from all files.
"""
named_items = {}
for statements in self.by_filename.values():
new_items = get_named_items(statements)
# TODO: potential for linting with redef
named_items.update(new_items)
if "BEGINNING" not in named_items:
named_items["BEGINNING"] = Element(
name=Token("BEGINNING", loc=implicit_location, role=Role.name_),
keyword=Token(
"BEGINNING_ELE",
loc=implicit_location,
role=Role.kind,
),
)
if "END" not in named_items:
named_items["END"] = Element(
name=Token("END", loc=implicit_location, role=Role.name_),
keyword=Token("MARKER", loc=implicit_location, role=Role.kind),
)
if "PARAMETER" not in named_items:
named_items["PARAMETER"] = Element(
name=Token("PARAMETER", loc=implicit_location, role=Role.name_),
keyword=Token("!PARAMETER", loc=implicit_location, role=Role.kind),
)
if "PARTICLE_START" not in named_items:
named_items["PARTICLE_START"] = Element(
name=Token("PARTICLE_START", loc=implicit_location, role=Role.name_),
keyword=Token("!PARTICLE_START", loc=implicit_location, role=Role.kind),
)
if "PTC_COM" not in named_items:
named_items["PTC_COM"] = Element(
name=Token("PTC_COM", loc=implicit_location, role=Role.name_),
keyword=Token("!PTC_COM", loc=implicit_location, role=Role.kind),
)
return named_items
|
latform.parser.Files.get_statements_in_order
get_statements_in_order(*, repeat_called_files=True, top_files=None)
Get all statements in order, as evaluated by Bmad.
top_files restricts the traversal to the call trees of the given
top-level files (defaulting to all of them).
Source code in latform/parser.py
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984 | def get_statements_in_order(
self,
*,
repeat_called_files: bool = True,
top_files: Sequence[pathlib.Path] | None = None,
) -> list[Statement]:
"""
Get all statements in order, as evaluated by Bmad.
``top_files`` restricts the traversal to the call trees of the given
top-level files (defaulting to all of them).
"""
handled = set()
active = set()
def _flatten(fn):
if fn in active:
# Guard against call cycles
return
if fn in handled and not repeat_called_files:
return
handled.add(fn)
active.add(fn)
try:
for st in self.by_filename.get(fn, []):
yield st
if is_call_statement(st):
called = st.metadata.get("local_path")
if called is not None:
yield from _flatten(called)
finally:
active.discard(fn)
res = []
for fn in self.top_files if top_files is None else top_files:
res.extend(_flatten(fn))
return res
|
latform.parser.Files.match_elements
Element definitions across all loaded files matching an element selector.
See match_element_selector for the supported syntax; returns None for
selector syntax that is not supported yet.
Source code in latform/parser.py
870
871
872
873
874
875
876
877
878
879
880 | def match_elements(self, pattern: str) -> list[Element] | None:
"""
Element definitions across all loaded files matching an element selector.
See `match_element_selector` for the supported syntax; returns None for
selector syntax that is not supported yet.
"""
return match_element_selector(
(st for statements in self.by_filename.values() for st in statements),
pattern,
)
|
latform.parser.Files.parse
parse(recurse=True, raise_if_missing=False, keep_blocks=False)
Parse the top-level file(s) and optionally their dependencies recursively.
| Parameters: |
-
recurse
(bool, default:
True
)
–
Recurse into called lattice files. Defaults to True.
-
raise_if_missing
(bool, default:
False
)
–
For lattice files included by way of call statements,
this flag will control whether FileNotFoundError is raised.
If a top-level file is missing, FileNotFoundError is always raised.
-
keep_blocks
(bool, default:
False
)
–
Store the intermediate Block objects in
self.blocks_by_filename so callers (e.g. verbose debug output)
don't have to re-tokenize.
|
Source code in latform/parser.py
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844 | def parse(
self,
recurse: bool = True,
raise_if_missing: bool = False,
keep_blocks: bool = False,
):
"""
Parse the top-level file(s) and optionally their dependencies recursively.
Parameters
----------
recurse : bool, optional
Recurse into called lattice files. Defaults to True.
raise_if_missing : bool, optional
For lattice files included by way of ``call`` statements,
this flag will control whether `FileNotFoundError` is raised.
If a top-level file is missing, `FileNotFoundError` is always raised.
keep_blocks : bool, optional
Store the intermediate `Block` objects in
``self.blocks_by_filename`` so callers (e.g. verbose debug output)
don't have to re-tokenize.
"""
if not self.top_files:
raise ValueError("Files requires at least one top-level file in top_files")
self.top_files = [p.resolve() for p in self.top_files]
top_set = set(self.top_files)
if not self.stack:
# Seed the stack so the first top file is popped first.
for top in reversed(self.top_files):
self.stack.append((pathlib.Path(top.name), top.parent))
self.local_file_to_source_filename.setdefault(top, top.name)
# We need to track processed files to avoid infinite loops in circular refs
processed = set(self.by_filename.keys())
while self.stack:
filename_part, parent_dir = self.stack.pop()
# Resolve the full path based on the parent context
# (Note: filename_part might already be absolute if it's the main entry from disk)
if filename_part.is_absolute():
full_path = filename_part
else:
full_path = parent_dir / filename_part
# Optimization: skip if already parsed
if full_path in processed:
continue
logger.debug("Parsing %s", full_path)
processed.add(full_path)
try:
contents = self._get_file_contents(full_path)
except FileNotFoundError:
logger.error(
f"Could not find file: {full_path} (parent={parent_dir} file={filename_part})"
)
# Top-level files must exist. Otherwise, missing included files
# are optionally an error.
if full_path in top_set or raise_if_missing:
raise FileNotFoundError(
f"Could not find file: {full_path} (parent={parent_dir} file={filename_part})"
) from None
continue
if is_init_file(full_path) or looks_like_namelist(contents):
logger.debug("Skipping non-lattice (namelist) file: %s", full_path)
self.by_filename[full_path] = []
continue
try:
if keep_blocks:
blocks = tokenize(contents=contents, filename=full_path)
self.blocks_by_filename[full_path] = blocks
statements: list[Statement] = [b.parse() for b in blocks]
else:
# We don't annotate individually here, we do it in bulk later
statements = self._parse_file(contents, full_path)
except Exception as ex:
if hasattr(ex, "add_note"): # py 3.11+
ex.add_note(f"Exception ocurred while parsing {full_path}")
raise
self.by_filename[full_path] = statements
for st in statements:
if is_call_statement(st):
# assert isinstance(st, Simple)
st.metadata["local_path"] = self._add_file_by_statement(
statement_filename=full_path, st=st
)
if not recurse:
# Without recursion, still process remaining top-level files,
# but drop anything pulled in via `call` from this file.
self.stack = [item for item in self.stack if (item[1] / item[0]) in top_set]
if not self.stack:
break
return self.by_filename
|
latform.parser.Files.reformat
Reformat all files in the collection.
Source code in latform/parser.py
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025 | def reformat(self, options: FormatOptions) -> None:
"""
Reformat all files in the collection.
"""
from .output import format_statements
if options.flatten_call:
for top, statements in self.flatten_all(
call=options.flatten_call, inline=options.flatten_inline
).items():
formatted = format_statements(statements, options)
self._write_reformatted(top, formatted)
return
for fn, statements in self.by_filename.items():
formatted = format_statements(statements, options)
self._write_reformatted(fn, formatted)
|
MemoryFiles(top_files=list(), stack=list(), by_filename=dict(), blocks_by_filename=dict(), local_file_to_source_filename=dict(), filename_calls=dict(), tao_init=None, initial_contents=dict(), _formatted_contents=dict())
Bases: Files
Files alternative that starts parsing from a string in memory rather than a
file on disk. Recursion will look to the filesystem relative to
root_path.
latform.parser.MemoryFiles.formatted_contents
property
Get the formatted result for a single in-memory top file.
latform.parser.MemoryFiles.formatted_contents_by_path
property
formatted_contents_by_path
All formatted in-memory entries.
latform.parser.MemoryFiles.from_contents
classmethod
from_contents(contents, root_path)
Create a MemoryFiles instance from a single string.
| Parameters: |
-
contents
(str)
–
-
root_path
(Path | str)
–
A "virtual" path where this file supposedly lives, used for resolving
relative calls to other files.
|
| Returns: |
-
MemoryFiles
–
The initialized object (call .parse() on it next).
|
Source code in latform/parser.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078 | @classmethod
def from_contents(cls, contents: str, root_path: pathlib.Path | str) -> MemoryFiles:
"""
Create a MemoryFiles instance from a single string.
Parameters
----------
contents : str
The source code content.
root_path : pathlib.Path | str
A "virtual" path where this file supposedly lives, used for resolving
relative calls to other files.
Returns
-------
MemoryFiles
The initialized object (call .parse() on it next).
"""
path = pathlib.Path(root_path).resolve()
return cls(top_files=[path], initial_contents={path: contents})
|
latform.parser.MemoryFiles.from_mapping
classmethod
Create a MemoryFiles instance from multiple in-memory files.
Keys are treated as top-level files in iteration order.
Source code in latform/parser.py
1122
1123
1124
1125
1126
1127
1128
1129
1130 | @classmethod
def from_mapping(cls, contents: dict[pathlib.Path | str, str]) -> MemoryFiles:
"""
Create a MemoryFiles instance from multiple in-memory files.
Keys are treated as top-level files in iteration order.
"""
resolved = {pathlib.Path(path).resolve(): cts for path, cts in contents.items()}
return cls(top_files=list(resolved.keys()), initial_contents=resolved)
|
latform.parser.MemoryFiles.from_tao_init_contents
classmethod
from_tao_init_contents(contents, root_path, lattice_contents=None)
Create a MemoryFiles from in-memory tao.init contents.
| Parameters: |
-
contents
(str)
–
The tao.init file contents.
-
root_path
(Path | str)
–
The "virtual" path of the tao.init file; lattice entries resolve
relative to its parent directory.
-
lattice_contents
(dict, default:
None
)
–
In-memory contents for referenced lattice files, keyed by path or
name (resolved the same way as the design_lattice entries). Any
lattice not provided here is read from disk when parsed.
|
| Returns: |
-
MemoryFiles
–
The initialized object (call .parse() on it next).
|
Source code in latform/parser.py
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120 | @classmethod
def from_tao_init_contents(
cls,
contents: str,
root_path: pathlib.Path | str,
lattice_contents: dict[pathlib.Path | str, str] | None = None,
) -> MemoryFiles:
"""
Create a MemoryFiles from in-memory ``tao.init`` contents.
Parameters
----------
contents : str
The ``tao.init`` file contents.
root_path : pathlib.Path | str
The "virtual" path of the ``tao.init`` file; lattice entries resolve
relative to its parent directory.
lattice_contents : dict, optional
In-memory contents for referenced lattice files, keyed by path or
name (resolved the same way as the ``design_lattice`` entries). Any
lattice not provided here is read from disk when parsed.
Returns
-------
MemoryFiles
The initialized object (call .parse() on it next).
"""
tao_path = pathlib.Path(root_path).resolve()
base = tao_path.parent
tao_init = TaoInit.parse(contents, filename=tao_path)
top_files = _resolve_lattice_paths(tao_init.lattice_files, base)
initial = {
_resolve_lattice_paths([key], base)[0]: text
for key, text in (lattice_contents or {}).items()
}
tao_init.load_sources(base=base, reader=initial.get)
return cls(
top_files=top_files,
initial_contents=initial,
tao_init=tao_init,
)
|
build_files(filenames, *, combine=False, root_path=None, input_format=None)
Construct one or more Files objects from CLI-style filename arguments.
| Parameters: |
-
filenames
(list of str or Path)
–
Filenames to load. "-" reads from stdin.
-
combine
(bool, default:
False
)
–
If True, all filenames are combined into a single Files
(or MemoryFiles if any entry is stdin). If False (default),
each filename becomes its own Files, preserving the per-file
semantics of the legacy CLI loop.
-
root_path
(Path, default:
None
)
–
Directory used to resolve the synthetic stdin path. Defaults to Path.cwd().
-
input_format
(('bmad', 'namelist'), default:
"bmad"
)
–
Force how inputs are interpreted. None (default) auto-detects: an
input is treated as a Tao tao.init namelist when it is named
*.init or its contents look like a namelist (see
looks_like_namelist); otherwise it is treated as Bmad. "namelist"
forces namelist handling for files that would not otherwise be detected;
"bmad" forces lattice handling even for a *.init file.
|
| Returns: |
-
list of Files
–
One element if combine is True, otherwise one per input filename.
|
Source code in latform/parser.py
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270 | def build_files(
filenames: Sequence[str | pathlib.Path],
*,
combine: bool = False,
root_path: pathlib.Path | None = None,
input_format: str | None = None,
) -> list[Files]:
"""
Construct one or more `Files` objects from CLI-style filename arguments.
Parameters
----------
filenames : list of str or Path
Filenames to load. ``"-"`` reads from stdin.
combine : bool, optional
If True, all filenames are combined into a single `Files`
(or `MemoryFiles` if any entry is stdin). If False (default),
each filename becomes its own `Files`, preserving the per-file
semantics of the legacy CLI loop.
root_path : pathlib.Path, optional
Directory used to resolve the synthetic stdin path. Defaults to ``Path.cwd()``.
input_format : {"bmad", "namelist"}, optional
Force how inputs are interpreted. ``None`` (default) auto-detects: an
input is treated as a Tao ``tao.init`` namelist when it is named
``*.init`` or its contents look like a namelist (see
`looks_like_namelist`); otherwise it is treated as Bmad. ``"namelist"``
forces namelist handling for files that would not otherwise be detected;
``"bmad"`` forces lattice handling even for a ``*.init`` file.
Returns
-------
list of Files
One element if ``combine`` is True, otherwise one per input filename.
"""
if not filenames:
return []
if root_path is None:
root_path = pathlib.Path.cwd()
def _is_stdin(fn) -> bool:
return str(fn) == STDIN_TOKEN
def _is_namelist(fn, contents: str | None = None) -> bool:
"""Decide whether ``fn`` should be handled as a Tao namelist file."""
if input_format is not None:
return input_format == "namelist"
if is_init_file(fn):
return True
if contents is None:
# Peek at the file to auto-detect a misnamed namelist.
try:
contents = pathlib.Path(fn).read_text()
except OSError:
return False
return looks_like_namelist(contents)
def _make_one(fn: str | pathlib.Path) -> Files:
if _is_stdin(fn):
fake_name = (root_path / STDIN_FAKE_NAME).resolve()
contents = sys.stdin.read()
if _is_namelist(fn, contents):
files: Files = MemoryFiles.from_tao_init_contents(contents, fake_name)
else:
files = MemoryFiles(top_files=[fake_name], initial_contents={fake_name: contents})
files.local_file_to_source_filename[fake_name] = STDIN_LABEL
return files
if _is_namelist(fn):
return Files.from_tao_init(fn)
return Files(top_files=[pathlib.Path(fn)])
if not combine:
return [_make_one(fn) for fn in filenames]
# Combined mode: a single Files (or MemoryFiles if any stdin entry).
stdin_path: pathlib.Path | None = None
top_files: list[pathlib.Path] = []
initial_contents: dict[pathlib.Path, str] = {}
for fn in filenames:
if _is_stdin(fn):
if stdin_path is not None:
raise ValueError("stdin ('-') can only be used once when combining inputs")
stdin_path = (root_path / STDIN_FAKE_NAME).resolve()
contents = sys.stdin.read()
if _is_namelist(fn, contents):
stdin_files = MemoryFiles.from_tao_init_contents(contents, stdin_path)
top_files.extend(stdin_files.top_files)
initial_contents.update(stdin_files.initial_contents)
else:
top_files.append(stdin_path)
initial_contents[stdin_path] = contents
elif _is_namelist(fn):
top_files.extend(Files.from_tao_init(fn).top_files)
else:
top_files.append(pathlib.Path(fn))
if initial_contents:
files = MemoryFiles(top_files=top_files, initial_contents=initial_contents)
if stdin_path is not None:
files.local_file_to_source_filename[stdin_path] = STDIN_LABEL
return [files]
return [Files(top_files=top_files)]
|
match_element_selector(statements, selector)
Elements matched by a Bmad element selector, in definition order.
Supported: * and % wildcards in the element name, and an optional
class::pattern prefix whose class part (which may also use wildcards)
is matched against each element's resolved type. A plain name matches
exactly. All matching is case-insensitive.
Returns None for selector syntax that is not supported yet:
ranges (q1:q5), branch qualifiers (lat>>q1), instance counts
(q1##2), and s-position selectors.
Source code in latform/parser.py
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544 | def match_element_selector(statements: Iterable[Statement], selector: str) -> list[Element] | None:
"""
Elements matched by a Bmad element selector, in definition order.
Supported: ``*`` and ``%`` wildcards in the element name, and an optional
``class::pattern`` prefix whose class part (which may also use wildcards)
is matched against each element's resolved type. A plain name matches
exactly. All matching is case-insensitive.
Returns None for selector syntax that is not supported yet:
ranges (``q1:q5``), branch qualifiers (``lat>>q1``), instance counts
(``q1##2``), and s-position selectors.
"""
# TODO: branch qualifiers ("lat>>q1") and instance counts ("q1##2")
if ">>" in selector or "##" in selector:
return None
class_pattern: str | None
match selector.split("::"):
case [name_pattern]:
class_pattern = None
case [class_pattern, name_pattern]:
pass
case _:
return None
# TODO: ranges ("q1:q5") and s-position selectors
if ":" in (class_pattern or "") or ":" in name_pattern:
return None
matched = []
for st in statements:
if not isinstance(st, Element):
continue
if class_pattern is not None and (
st.element_type is None or not _selector_part_matches(class_pattern, st.element_type)
):
continue
if _selector_part_matches(name_pattern, str(st.name)):
matched.append(st)
return matched
|
latform.parser.target_selector_text
target_selector_text(target)
Reconstruct the text of an attribute-set statement's target (case-normalized).
Source code in latform/parser.py
| def target_selector_text(target: Token | Seq) -> str:
"""Reconstruct the text of an attribute-set statement's target (case-normalized)."""
if isinstance(target, Token):
return str(target)
return str(target.to_token())
|
latform-diff - compare two lattice files.
ElementDiffDetails(type_change=None, attrs_added=list(), attrs_removed=list(), attrs_changed=list())
Holds specific differences for a single element.
| Attributes: |
-
type_change
(tuple[str, str] | None)
–
(old_type, new_type) if changed, else None.
-
attrs_added
(list[tuple[str, str]])
–
List of (attr_name, value).
-
attrs_removed
(list[tuple[str, str]])
–
List of (attr_name, value).
-
attrs_changed
(list[tuple[str, str, str]])
–
List of (attr_name, old_value, new_value).
|
LatticeDiff(params_added=list(), params_removed=list(), params_changed=list(), eles_added=list(), eles_removed=list(), eles_changed=dict(), eles_renamed=list())
Aggregates all differences between two lattice definitions.
ParameterChange(target, name, old_value, new_value)
Represents a change in a single parameter (target, name).
calculate_diff(files1, files2)
Compute differences between two file sets and return a dataclass.
Source code in latform/diff.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333 | def calculate_diff(files1: Files, files2: Files) -> LatticeDiff:
"""
Compute differences between two file sets and return a dataclass.
Parameters
----------
files1 : Files
Left-hand file set.
files2 : Files
Right-hand file set.
Returns
-------
LatticeDiff
The structured differences.
"""
diff = LatticeDiff()
params1 = _collect_parameters(files1)
params2 = _collect_parameters(files2)
p_keys1 = set(params1.keys())
p_keys2 = set(params2.keys())
# Added
for key in sorted(p_keys2 - p_keys1):
diff.params_added.append(
ParameterChange(key[0], key[1], old_value=None, new_value=params2[key])
)
# Removed
for key in sorted(p_keys1 - p_keys2):
diff.params_removed.append(
ParameterChange(key[0], key[1], old_value=params1[key], new_value=None)
)
# Changed
for key in sorted(p_keys1 & p_keys2):
if params1[key] != params2[key]:
diff.params_changed.append(
ParameterChange(key[0], key[1], old_value=params1[key], new_value=params2[key])
)
elements1 = _collect_elements(files1)
elements2 = _collect_elements(files2)
e_keys1 = set(elements1.keys())
e_keys2 = set(elements2.keys())
diff.eles_added = sorted(e_keys2 - e_keys1)
diff.eles_removed = sorted(e_keys1 - e_keys2)
common_eles = e_keys1 & e_keys2
def is_same_ele(ele1, ele2):
e1 = elements1[ele1]
e2 = elements2[ele2]
return e1["type"] == e2["type"] and e1["attributes"] == e2["attributes"]
diff.eles_renamed = [
(ele1, ele2)
for ele1 in diff.eles_removed
for ele2 in diff.eles_added
if is_same_ele(ele1, ele2)
]
# Could detect multiple renames; A -> B1, B2
# Not technically valid as far as a rename goes;
# Make this instead a remove/add? Hmm
for ele1, ele2 in diff.eles_renamed:
try:
diff.eles_removed.remove(ele1)
except ValueError:
pass
try:
diff.eles_added.remove(ele2)
except ValueError:
pass
for name in common_eles:
e1 = elements1[name]
e2 = elements2[name]
details = ElementDiffDetails()
if e1["type"] != e2["type"]:
details = dataclasses.replace(details, type_change=(e1["type"], e2["type"]))
attrs1 = e1["attributes"]
attrs2 = e2["attributes"]
a_keys1 = set(attrs1.keys())
a_keys2 = set(attrs2.keys())
for a in sorted(a_keys2 - a_keys1):
details.attrs_added.append((a, attrs2[a]))
for a in sorted(a_keys1 - a_keys2):
details.attrs_removed.append((a, attrs1[a]))
for a in sorted(a_keys1 & a_keys2):
if attrs1[a] != attrs2[a]:
details.attrs_changed.append((a, attrs1[a], attrs2[a]))
if details.has_changes:
diff.eles_changed[name] = details
return diff
|
print_diff(diff, console)
Render the diff using Rich tables.
Source code in latform/diff.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398 | def print_diff(diff: LatticeDiff, console: Console) -> None:
"""
Render the diff using Rich tables.
"""
if diff.has_param_diffs:
console.rule("[bold]Parameters[/bold]")
table = Table(show_header=True, header_style="bold magenta")
table.add_column("State")
table.add_column("Target")
table.add_column("Name")
table.add_column("Value (Left)", style="red")
table.add_column("Value (Right)", style="green")
for p in diff.params_added:
table.add_row("Added", p.target, p.name, "", p.value_new_str, style="green")
for p in diff.params_removed:
table.add_row("Removed", p.target, p.name, p.value_old_str, "", style="red")
for p in diff.params_changed:
table.add_row(
"Changed", p.target, p.name, p.value_old_str, p.value_new_str, style="yellow"
)
console.print(table)
console.print()
if diff.has_ele_diffs:
console.rule("[bold]Elements[/bold]")
table = Table(show_header=True, header_style="bold cyan")
table.add_column("State")
table.add_column("Element")
table.add_column("Property/Attribute")
table.add_column("Value (Left)", style="red")
table.add_column("Value (Right)", style="green")
for name in diff.eles_added:
table.add_row("Added", name, "Element", "", "Exist", style="green")
for name in diff.eles_removed:
table.add_row("Removed", name, "Element", "Exist", "", style="red")
for from_, to in diff.eles_renamed:
table.add_row("Renamed", from_, "Element", from_, to, style="red")
for name in sorted(diff.eles_changed.keys()):
details = diff.eles_changed[name]
if details.type_change:
old_t, new_t = details.type_change
table.add_row("Changed", name, "Type", old_t, new_t, style="magenta")
for attr, val in details.attrs_added:
table.add_row("Changed", name, f"Attr: {attr}", "", val, style="green")
for attr, val in details.attrs_removed:
table.add_row("Changed", name, f"Attr: {attr}", val, "", style="red")
for attr, old_v, new_v in details.attrs_changed:
table.add_row("Changed", name, f"Attr: {attr}", old_v, new_v, style="yellow")
console.print(table)
|
Constant(name, value, redef=False, *, comments=Comments(), metadata=dict())
Bases: Statement
There are five types of parameters in Bmad: reals, integers, switches, logicals (booleans), and strings.
Element(name, keyword, ele_list=None, attributes=list(), base_element=None, element_type=None, *, comments=Comments(), metadata=dict())
Bases: Statement
latform.statements.Element.is_controller
property
Whether this is an overlay/group/ramper.
latform.statements.Element.get_superposition_settings
get_superposition_settings(deferred=None)
Resolve an element's effective superimpose/ref settings.
Inline attributes are read first; deferred element[attr] = value
statements (as collected by get_deferred_element_attributes) override
them.
Source code in latform/statements.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 | def get_superposition_settings(
self,
deferred: dict[Token, dict[str, Token | Seq | None]] | None = None,
) -> SuperpositionSettings:
"""
Resolve an element's effective ``superimpose``/``ref`` settings.
Inline attributes are read first; deferred ``element[attr] = value``
statements (as collected by `get_deferred_element_attributes`) override
them.
"""
def as_logical(value: Token | Seq | None) -> bool:
text = _attribute_value_text(value)
return text is None or text.upper() not in FALSE_LOGICALS_UPPER
enabled = False
ref_value: Token | Seq | None = None
have_ref = False
for attr in self.attributes:
if not isinstance(attr.name, Token):
continue
uname = attr.name._upper
if uname == "SUPERIMPOSE":
enabled = as_logical(attr.value)
elif uname == "REF":
ref_value = attr.value
have_ref = True
overrides = (deferred or {}).get(self.name.upper(), {})
if "SUPERIMPOSE" in overrides:
enabled = as_logical(overrides["SUPERIMPOSE"])
if "REF" in overrides:
ref_value = overrides["REF"]
have_ref = True
ref = _attribute_value_text(ref_value) if have_ref else None
ref_token = ref_value if isinstance(ref_value, Token) else None
return SuperpositionSettings(enabled=enabled, ref=ref, ref_token=ref_token)
|
SuperpositionSettings(enabled, ref=None, ref_token=None)
Effective superposition state of an element definition.
annotate_controller_variables(element)
Annotate a controller's var={...} names and their usages.
The declared variables are in var={...}.
They are then annotated in the control expressions, and as default-value
attributes.
Source code in latform/statements.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561 | def annotate_controller_variables(element: Element) -> None:
"""
Annotate a controller's ``var={...}`` names and their usages.
The declared variables are in ``var={...}``.
They are then annotated in the control expressions, and as default-value
attributes.
"""
from .walk import iter_tokens
var_names = {var._upper for var in get_controller_variables(element)}
if not var_names:
return
var_attr = element.get_named_attribute("var", partial_match=False)
def get_tokens():
yield from iter_tokens(element.ele_list)
for attr in element.attributes:
yield from iter_tokens(attr)
for tok in get_tokens():
if (tok.role is None or tok.role == Role.name_) and tok._upper in var_names:
tok.role = Role.controller_variable
for attr in element.attributes:
if attr is var_attr:
continue
if isinstance(attr.name, Token) and attr.name._upper in var_names:
attr.name.role = Role.controller_variable
|
get_controller_variables(element)
Get variable names declared in an element's var={...}.
Source code in latform/statements.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528 | def get_controller_variables(element: Element) -> set[Token]:
"""
Get variable names declared in an element's ``var={...}``.
"""
from .walk import iter_tokens
try:
var_attr = element.get_named_attribute("var", partial_match=False)
except KeyError:
return set()
if not isinstance(var_attr.value, Seq):
return set()
return {tok for tok in iter_tokens(var_attr.value)}
|
get_deferred_element_attributes(statements, names=DEFERRABLE_ELEMENT_ATTRIBUTES_UPPER)
Collect element[attr] = value statements that adjust element definitions.
| Parameters: |
-
statements
(Sequence[Statement])
–
Statements to scan, in file order.
-
names
(frozenset[str], default:
DEFERRABLE_ELEMENT_ATTRIBUTES_UPPER
)
–
Uppercased attribute names to collect.
|
| Returns: |
-
dict[Token, dict[str, Token | Seq | None]]
–
Mapping of uppercased element name to {ATTRIBUTE: value}. Later
statements override earlier ones.
|
Source code in latform/statements.py
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491 | def get_deferred_element_attributes(
statements: Sequence[Statement],
names: frozenset[str] = DEFERRABLE_ELEMENT_ATTRIBUTES_UPPER,
) -> dict[Token, dict[str, Token | Seq | None]]:
"""
Collect ``element[attr] = value`` statements that adjust element definitions.
Parameters
----------
statements : Sequence[Statement]
Statements to scan, in file order.
names : frozenset[str], optional
Uppercased attribute names to collect.
Returns
-------
dict[Token, dict[str, Token | Seq | None]]
Mapping of uppercased element name to ``{ATTRIBUTE: value}``. Later
statements override earlier ones.
"""
deferred: dict[Token, dict[str, Token | Seq | None]] = {}
for st in statements:
if isinstance(st, Parameter) and isinstance(st.target, Token):
name = st.name._upper
if name in names:
deferred.setdefault(st.target.upper(), {})[name] = st.value
return deferred
|
normalize_element_selector(selector)
Strip branch qualifiers (branch>>name) and instance counts (name##2).
Source code in latform/statements.py
503
504
505
506
507
508
509
510
511 | def normalize_element_selector(selector: str) -> str:
"""
Strip branch qualifiers (``branch>>name``) and instance counts (``name##2``).
"""
if ">>" in selector:
selector = selector.rsplit(">>", 1)[1]
if "##" in selector:
selector = selector.split("##", 1)[0]
return selector
|