Skip to content

Commit 969ffe5

Browse files
Add MADX parser (#39)
1 parent fcdfac3 commit 969ffe5

File tree

18 files changed

+490
-278
lines changed

18 files changed

+490
-278
lines changed

latticejson/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
from .__about__ import __version__
2-
from .io import load, convert
3-
from .validate import validate
4-
from .convert import elegant_to_latticejson, latticejson_to_elegant
5-
from .migrate import migrate
6-
from .exceptions import UndefinedObjectError, UndefinedRPNVariableError
2+
from .io import load, save

latticejson/cli.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55

66
from . import __version__
77
from .validate import validate_file
8-
from .io import convert as _convert
9-
from .parse import parse_elegant as _parse_elegant
10-
from .format import CompactJSONEncoder
8+
from . import io
9+
from .format import format_json
1110
from .migrate import migrate as _migrate
11+
from . import parse
1212

1313

14-
FORMATS = "json", "lte"
15-
dump_latticejson = lambda obj: json.dumps(obj, cls=CompactJSONEncoder, indent=4)
14+
FORMATS = "json", "lte", "madx"
1615

1716

1817
@click.group()
@@ -38,7 +37,7 @@ def cli():
3837
)
3938
def convert(file, from_, to):
4039
"""Convert FILE (path or url) to another lattice file format."""
41-
click.echo(_convert(file, from_, to))
40+
click.echo(io.save_string(io.load(file, from_), to))
4241

4342

4443
@cli.command()
@@ -48,14 +47,6 @@ def validate(file):
4847
validate_file(file)
4948

5049

51-
@cli.command()
52-
@click.argument("file", type=click.Path(exists=True))
53-
def parse_elegant(file):
54-
"""Parse elegant file but do not convert to LatticeJSON."""
55-
text = Path(file).read_text()
56-
click.echo(dump_latticejson(_parse_elegant(text)))
57-
58-
5950
@cli.command()
6051
@click.argument("files", nargs=-1, type=click.Path(exists=True))
6152
@click.option(
@@ -70,7 +61,7 @@ def autoformat(files, dry_run):
7061
path.rglob("*.json") if path.is_dir() else (path,) for path in map(Path, files)
7162
):
7263
latticejson = json.loads(path.read_text())
73-
formatted = dump_latticejson(latticejson)
64+
formatted = format_json(latticejson)
7465
click.secho(f"reformatted {path}", bold=True)
7566
if dry_run:
7667
click.echo(formatted)
@@ -88,4 +79,34 @@ def migrate(file, from_, to):
8879
initial_version = from_.split(".")
8980
final_version = to.split(".")
9081
latticejson = _migrate(json.loads(text), initial_version, final_version)
91-
click.echo(dump_latticejson(latticejson))
82+
click.echo(format_json(latticejson))
83+
84+
85+
@cli.group()
86+
def debug():
87+
"""Some useful commands for debugging/development."""
88+
pass
89+
90+
91+
@debug.command()
92+
@click.argument("file", type=click.Path(exists=True))
93+
@click.option("--transform", "-t", is_flag=True, help="Print transformed tree.")
94+
def parse_elegant(file, transform):
95+
"""Print parse tree of elegant lattice file."""
96+
text = Path(file).read_text()
97+
if transform:
98+
click.echo(format_json(parse.parse_elegant(text)))
99+
else:
100+
click.echo(parse.ELEGANT_PARSER.parse(text).pretty())
101+
102+
103+
@debug.command()
104+
@click.argument("file", type=click.Path(exists=True))
105+
@click.option("--transform", "-t", is_flag=True, help="Print transformed tree.")
106+
def parse_madx(file, transform):
107+
"""Print parse tree of madx lattice file."""
108+
text = Path(file).read_text()
109+
if transform:
110+
click.echo(format_json(parse.parse_madx(text)))
111+
else:
112+
click.echo(parse.MADX_PARSER.parse(text).pretty())

latticejson/convert.py

Lines changed: 84 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,32 @@
1-
from typing import List, Tuple, Dict
1+
from typing import List, Dict
22
from pathlib import Path
33
import json
4-
import warnings
5-
6-
from .validate import validate
7-
from .parse import parse_elegant
8-
9-
10-
LATTICEJSON_ELEGANT_MAP: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
11-
("Drift", ("DRIF", "DRIFT")),
12-
("Dipole", ("CSBEND", "SBEND", "BEND")),
13-
("Quadrupole", ("KQUAD", "QUAD", "QUADRUPOLE")),
14-
("Sextupole", ("KSEXT", "SEXT", "SEXTUPOLE")),
15-
("Lattice", ("LINE",)),
16-
("length", ("L",)),
17-
("angle", ("ANGLE",)),
18-
("e1", ("E1",)),
19-
("e2", ("E2",)),
20-
("k1", ("K1",)),
21-
("k2", ("K2",)),
22-
)
23-
JSON_TO_ELE: Dict[str, str] = {x: y[0] for x, y in LATTICEJSON_ELEGANT_MAP}
24-
ELE_TO_JSON: Dict[str, str] = {y: x for x, tup in LATTICEJSON_ELEGANT_MAP for y in tup}
25-
26-
27-
def latticejson_to_elegant(lattice_dict) -> str:
28-
"""Convert LatticeJSON dict to elegant lattice file format.
29-
:param dict: dict in LatticeJSON format
30-
:return: string with in elegant lattice file format
31-
"""
32-
elements = lattice_dict["elements"]
33-
sub_lattices = lattice_dict["sub_lattices"]
4+
from warnings import warn
5+
from .parse import parse_elegant, parse_madx
6+
from .exceptions import UnknownElementWarning, UnknownAttributeWarning
347

35-
strings = []
36-
element_template = "{}: {}, {}".format
37-
for name, (type_, attributes) in elements.items():
38-
attrs = ", ".join(f"{JSON_TO_ELE[k]}={v}" for k, v in attributes.items())
39-
elegant_type = JSON_TO_ELE[type_]
40-
strings.append(element_template(name, elegant_type, attrs))
8+
NAME_MAP = json.loads((Path(__file__).parent / "map.json").read_text())["map"]
9+
JSON_TO_ELE = {x: y[0][0] for x, *y in NAME_MAP}
10+
ELE_TO_JSON = {y: x for x, *tup in NAME_MAP for y in tup[0]}
11+
JSON_TO_MADX = {x: y[1][0] for x, *y in NAME_MAP}
12+
MADX_TO_JSON = {y: x for x, *tup in NAME_MAP for y in tup[1]}
4113

42-
lattice_template = "{}: LINE=({})".format
43-
for name in sort_lattices(sub_lattices):
44-
strings.append(lattice_template(name, ", ".join(sub_lattices[name])))
4514

46-
name = lattice_dict["name"]
47-
strings.append(lattice_template(name, ", ".join(lattice_dict["lattice"])))
48-
strings.append("\n")
49-
return "\n".join(strings)
15+
def from_elegant(string):
16+
"""Convert an elegant lattice file to a LatticeJSON dict.
5017
18+
:param str string: input lattice file as string
19+
:param lattice_name: name of the lattice
20+
:type str, optional
21+
:param description: description of the lattice
22+
:type str, optional
23+
:return: dict in LatticeJSON format
24+
"""
25+
return _map_names_from_elegant(parse_elegant(string))
5126

52-
def elegant_to_latticejson(string):
53-
"""Convert an elegant lattice file to a LatticeJSON dict.
27+
28+
def from_madx(string):
29+
"""Convert a MADX lattice file to a LatticeJSON dict.
5430
5531
:param str string: input lattice file as string
5632
:param lattice_name: name of the lattice
@@ -59,30 +35,36 @@ def elegant_to_latticejson(string):
5935
:type str, optional
6036
:return: dict in LatticeJSON format
6137
"""
62-
elegant_dict = parse_elegant(string)
38+
return _map_names_from_madx(parse_madx(string))
6339

40+
41+
def _map_names_from_madx(madx_dict: dict):
42+
return _map_names(madx_dict, MADX_TO_JSON)
43+
44+
45+
def _map_names_from_elegant(elegant_dict: dict):
46+
return _map_names(elegant_dict, ELE_TO_JSON)
47+
48+
49+
def _map_names(lattice_data: dict, name_map: dict):
6450
elements = {}
65-
for name, (elegant_type, elegant_attributes) in elegant_dict["elements"].items():
66-
latticejson_type = ELE_TO_JSON.get(elegant_type)
51+
for name, (other_type, other_attributes) in lattice_data["elements"].items():
52+
latticejson_type = name_map.get(other_type)
6753
if latticejson_type is None:
68-
elements[name] = ["Drift", {"length": elegant_attributes.get("L", 0)}]
69-
warnings.warn(
70-
f"Replacing element {name} ({elegant_type}) with Drift.", stacklevel=2
71-
)
54+
elements[name] = ["Drift", {"length": other_attributes.get("L", 0)}]
55+
warn(UnknownElementWarning(name, other_type), stacklevel=2)
7256
continue
7357

7458
attributes = {}
7559
elements[name] = [latticejson_type, attributes]
76-
for elegant_key, value in elegant_attributes.items():
77-
latticejson_key = ELE_TO_JSON.get(elegant_key)
60+
for other_key, value in other_attributes.items():
61+
latticejson_key = name_map.get(other_key)
7862
if latticejson_key is not None:
7963
attributes[latticejson_key] = value
8064
else:
81-
warnings.warn(
82-
f"Ignoring attribute {elegant_key} of {name}.", stacklevel=2
83-
)
65+
warn(UnknownAttributeWarning(other_key, name), stacklevel=2)
8466

85-
lattices = elegant_dict["lattices"]
67+
lattices = lattice_data["lattices"]
8668
lattice_name, main_lattice = lattices.popitem() # use last lattice as main_lattice
8769
return dict(
8870
name=lattice_name,
@@ -92,24 +74,54 @@ def elegant_to_latticejson(string):
9274
)
9375

9476

95-
def sort_lattices_old(lattices: Dict[str, List[str]]) -> List[str]:
96-
"""Returns a sorted list of lattice names for a given dict of lattices."""
77+
def to_elegant(latticejson: dict) -> str:
78+
"""Convert a LatticeJSON dict to the elegant lattice file format.
9779
98-
lattices_copy = lattices.copy()
99-
lattice_names = []
80+
:param lattice_dict dict: dict in LatticeJSON format
81+
:return: string with in elegant lattice file format
82+
"""
83+
elements = latticejson["elements"]
84+
sub_lattices = latticejson["sub_lattices"]
10085

101-
def _sort_lattices(name, arrangement: List[str]):
102-
for child_name in arrangement:
103-
if child_name in lattices_copy:
104-
_sort_lattices(child_name, lattices_copy[child_name])
86+
strings = [f"! TITLE: {latticejson['name']}"]
87+
element_template = "{}: {}, {}".format
88+
for name, (type_, attributes) in elements.items():
89+
attrs = ", ".join(f"{JSON_TO_ELE[k]}={v}" for k, v in attributes.items())
90+
elegant_type = JSON_TO_ELE[type_]
91+
strings.append(element_template(name, elegant_type, attrs))
10592

106-
lattice_names.append(name)
107-
lattices_copy.pop(name)
93+
lattice_template = "{}: LINE=({})".format
94+
for name in sort_lattices(sub_lattices):
95+
strings.append(lattice_template(name, ", ".join(sub_lattices[name])))
96+
97+
strings.append(lattice_template("__MAIN__", ", ".join(latticejson["lattice"])))
98+
strings.append("USE, __MAIN__\n")
99+
return "\n".join(strings)
108100

109-
for name, arrangement in lattices.items():
110-
_sort_lattices(name, arrangement)
111101

112-
return lattice_names
102+
def to_madx(latticejson: dict) -> str:
103+
"""Convert a LatticeJSON dict to the MADX lattice file format.
104+
105+
:param lattice_dict dict: dict in LatticeJSON format
106+
:return: string with in elegant lattice file format
107+
"""
108+
elements = latticejson["elements"]
109+
sub_lattices = latticejson["sub_lattices"]
110+
111+
strings = [f"TITLE, \"{latticejson['name']}\""]
112+
element_template = "{}: {}, {};".format
113+
for name, (type_, attributes) in elements.items():
114+
attrs = ", ".join(f"{JSON_TO_MADX[k]}={v}" for k, v in attributes.items())
115+
elegant_type = JSON_TO_MADX[type_]
116+
strings.append(element_template(name, elegant_type, attrs))
117+
118+
lattice_template = "{}: LINE=({});".format
119+
for name in sort_lattices(sub_lattices):
120+
strings.append(lattice_template(name, ", ".join(sub_lattices[name])))
121+
122+
strings.append(lattice_template("__MAIN__", ", ".join(latticejson["lattice"])))
123+
strings.append("USE, __MAIN__;\n")
124+
return "\n".join(strings)
113125

114126

115127
def sort_lattices(lattices: Dict[str, List[str]]) -> List[str]:

latticejson/elegant.lark

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Elegant Grammar
12
// This grammer is not 100% consistent with elegants parser:
23
// - Elegants parser allows tokens to be split by the line continuation character "&".
34
// For example, it parses ANGLE=0.123&\n456 without an error.
@@ -9,26 +10,17 @@
910
%ignore /!.*/ // ingore comments
1011
%ignore /[ \t\f]/+ // ingore whitespace
1112
%ignore /&[ \t\f]*\r?\n/ // line continuation
12-
%import common.ESCAPED_STRING
13-
14-
INT : ["+" | "-"] DIGIT_PART
15-
FLOAT : ["+" | "-"] (EXP_FLOAT | POINT_FLOAT)
16-
EXP_FLOAT : (DIGIT_PART | POINT_FLOAT) EXPONENT
17-
POINT_FLOAT : [DIGIT_PART] FRACTION | DIGIT_PART "."
18-
DIGIT_PART : /\d+/
19-
FRACTION : "." DIGIT_PART
20-
EXPONENT : ("e" | "E") ["+" | "-"] DIGIT_PART
21-
22-
int : INT
23-
u_int : DIGIT_PART
24-
float : FLOAT
13+
%import common (SIGNED_INT, SIGNED_FLOAT, NUMBER, ESCAPED_STRING, CNAME)
14+
15+
int : SIGNED_INT
16+
float : SIGNED_FLOAT
2517
string : ESCAPED_STRING
2618
word : /\w+/
2719
name : /\w+/ | "\"" /[\w:]+/ "\""
2820

29-
file : _NEWLINE* (statement _NEWLINE+)*
21+
start : _NEWLINE* (statement _NEWLINE+)*
3022
_NEWLINE : /[ \t\f]*\r?\n[ \t\f]*/
31-
?statement : element | lattice | command | rpn_store
23+
?statement : element | lattice | command | "%" assignment
3224

3325
element : name ":" [name] ("," attribute)* ","?
3426
attribute : word "=" (int | float | string | word)
@@ -40,4 +32,19 @@ ref_name : [int "*"] [/-/] ["\""] /[\w:]+/ ["\""]
4032

4133
command : name ["," word]
4234

43-
rpn_store : "%" /.+sto.+/
35+
// RPN Expression
36+
// 1. As there is no syntactic distinction between an escaped string and a variable,
37+
// it is possible that a collison can happen. In this case a variable is wrongly
38+
// identified as string.
39+
40+
assignment : expr "sto" CNAME
41+
?expr : NUMBER -> number
42+
| CNAME -> variable
43+
| function
44+
| binary
45+
!function : expr ("exp" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan")
46+
?binary : expr expr "+" -> add
47+
| expr expr "-" -> sub
48+
| expr expr "*" -> mul
49+
| expr expr "/" -> div
50+
?start_rpn : assignment | expr // used to tested the rpn parser

latticejson/exceptions.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,24 @@ def __init__(self, object_name, lattice_name):
1212
)
1313

1414

15-
class UndefinedRPNVariableError(Exception):
15+
class UnknownElementWarning(UserWarning):
16+
"""Raised if there is no equivalent LatticeJSON element."""
17+
18+
def __init__(self, name, type_, *args, **kwargs):
19+
message = f"Replacing element {name} ({type_}) with Drift."
20+
super().__init__(message, *args, **kwargs)
21+
22+
23+
class UnknownAttributeWarning(UserWarning):
24+
"""Raised if there is no equivalent LatticeJSON attribute."""
25+
26+
def __init__(self, attribute, element, *args, **kwargs):
27+
message = f"Ignoring attribute {attribute} of {element}."
28+
super().__init__(message, *args, **kwargs)
29+
30+
31+
class UndefinedVariableError(Exception):
1632
"""Raised if a rpn variable is not defined."""
1733

1834
def __init__(self, name, *args, **kwargs):
19-
super().__init__(f"RPN variable {name} is not defined!", *args, **kwargs)
35+
super().__init__(f"The variable '{name}' is not defined!", *args, **kwargs)

latticejson/format.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import json
22

33

4+
def format_json(obj):
5+
"""Compact version of json.dumps."""
6+
return json.dumps(obj, cls=CompactJSONEncoder, indent=4)
7+
8+
49
class CompactJSONEncoder(json.JSONEncoder):
510
"""A JSON Encoder which only indents the first two levels."""
611

0 commit comments

Comments
 (0)