Skip to content

Commit 3f61e7c

Browse files
committed
Merge remote-tracking branch 'origin/master' into poetry
2 parents 692fa89 + 6bb3a38 commit 3f61e7c

File tree

18 files changed

+446
-189
lines changed

18 files changed

+446
-189
lines changed

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,36 @@ has a human readable syntax and is available in all common programming language.
55
appropriate choice to characterize the magnetic lattice of a particle accelerator.
66

77
## Specification
8+
89
This repository contains the
910
[Specification of LatticeJSON](https://github.com/andreasfelix/latticejson/blob/master/latticejson/schema.json)
1011
in form of a [JSON Schema](https://json-schema.org).
1112

12-
1313
## Example
1414

1515
A LatticeJSON file for a FODO lattice:
16+
1617
```json
1718
{
18-
"name": "FODO_RING",
19-
"description": "This is the simplest possible strong focusing lattice.",
19+
"version": "2.0",
20+
"title": "FODO Lattice",
21+
"info": "This is the simplest possible strong focusing lattice.",
22+
"root": "RING",
2023
"elements": {
2124
"D1": ["Drift", {"length": 0.55}],
2225
"Q1": ["Quadrupole", {"length": 0.2, "k1": 1.2}],
2326
"Q2": ["Quadrupole", {"length": 0.4, "k1": -1.2}],
2427
"B1": ["Dipole", {"length": 1.5, "angle": 0.392701, "e1": 0.1963505, "e2": 0.1963505}]
2528
},
26-
"sub_lattices": {
27-
"FODO": ["Q1", "D1", "B1", "D1", "Q2", "D1", "B1", "D1", "Q1"]
28-
},
29-
"lattice": ["FODO", "FODO", "FODO", "FODO", "FODO", "FODO", "FODO", "FODO"]
29+
"lattices": {
30+
"CELL": ["Q1", "D1", "B1", "D1", "Q2", "D1", "B1", "D1", "Q1"],
31+
"RING": ["CELL", "CELL", "CELL", "CELL", "CELL", "CELL", "CELL", "CELL"]
32+
}
3033
}
31-
3234
```
3335

36+
## LatticeJSON CLI
3437

35-
# LatticeJSON CLI
3638
[![Python Version](https://img.shields.io/pypi/pyversions/latticejson)](https://pypi.org/project/latticejson/)
3739
[![PyPI](https://img.shields.io/pypi/v/latticejson.svg)](https://pypi.org/project/latticejson/)
3840
[![CI](https://github.com/andreasfelix/latticejson/workflows/CI/badge.svg)](https://github.com/andreasfelix/latticejson/actions?query=workflow%3ACI)
@@ -41,34 +43,41 @@ This repository also contains a Python based command-line tool which is able val
4143
and convert LatticeJSON files into other common lattice file formats and vice versa.
4244

4345
You can install and update it using pip or pipenv:
46+
4447
```sh
4548
pip install -U latticejson
4649
```
4750

4851
Validate a LatticeJSON file:
52+
4953
```sh
5054
latticejson validate /path/to/lattice.json
5155
```
5256

5357
Convert an elegant lattice file to the LatticeJSON format:
58+
5459
```sh
5560
latticejson convert --to json /path/to/lattice.lte
5661
```
5762

5863
Autoformat one or more LatticeJSON files:
64+
5965
```sh
6066
latticejson autoformat /path/to/lattice.json ...
6167
```
6268

6369
To activate Bash completion add
64-
```
70+
71+
```sh
6572
eval "$(_LATTICEJSON_COMPLETE=source latticejson)"
6673
```
6774

6875
to your `.bashrc`. Or, create an activation script with:
69-
```
76+
77+
```sh
7078
_LATTICEJSON_COMPLETE=source latticejson > latticejson-complete.sh
7179
```
7280

7381
## License
82+
7483
[GNU General Public License v3.0](https://github.com/andreasfelix/latticejson/blob/master/LICENSE)

latticejson/cli.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import click
1+
import itertools
22
import json
33
from pathlib import Path
4-
import itertools
54

6-
from . import __version__
7-
from .validate import validate_file
8-
from . import io
5+
import click
6+
7+
from . import __version__, io, parse
98
from .format import format_json
9+
from .migrate import MAX_VERSION
1010
from .migrate import migrate as _migrate
11-
from . import parse
12-
11+
from .validate import parse_version, schema, validate_file
1312

1413
FORMATS = "json", "lte", "madx"
1514

1615

17-
@click.group()
18-
@click.version_option(__version__)
16+
@click.group(context_settings=dict(max_content_width=120))
17+
@click.version_option(
18+
message=(f"LatticeJSON CLI, version {__version__}\n{schema['title']}")
19+
)
1920
def cli():
2021
pass
2122

@@ -35,9 +36,12 @@ def cli():
3536
type=click.Choice(FORMATS, case_sensitive=False),
3637
help="Destination format",
3738
)
38-
def convert(file, from_, to):
39+
@click.option(
40+
"--validate/--no-validate", default=True, help="Whether to validate the input file."
41+
)
42+
def convert(file, from_, to, validate):
3943
"""Convert FILE (path or url) to another lattice file format."""
40-
click.echo(io.save_string(io.load(file, from_), to))
44+
click.echo(io.save_string(io.load(file, from_, validate), to))
4145

4246

4347
@cli.command()
@@ -70,16 +74,68 @@ def autoformat(files, dry_run):
7074

7175

7276
@cli.command()
73-
@click.argument("file", type=click.Path(exists=True))
74-
@click.option("--from", "from_", required=True, help="Initial version")
75-
@click.option("--to", required=True, help="Final version")
76-
def migrate(file, from_, to):
77+
@click.argument("files", nargs=-1, type=click.Path(exists=True))
78+
@click.option(
79+
"--final", type=int, default=MAX_VERSION, show_default=True, help="Final version."
80+
)
81+
@click.option(
82+
"--dry-run",
83+
"-d",
84+
is_flag=True,
85+
help="Don't write the files back, just output the formatted files.",
86+
)
87+
def migrate(files, final, dry_run):
7788
"""Migrate old LatticeJSON files to newer versions."""
78-
text = Path(file).read_text()
79-
initial_version = from_.split(".")
80-
final_version = to.split(".")
81-
latticejson = _migrate(json.loads(text), initial_version, final_version)
82-
click.echo(format_json(latticejson))
89+
for path in itertools.chain.from_iterable(
90+
path.rglob("*.json") if path.is_dir() else (path,) for path in map(Path, files)
91+
):
92+
data = json.loads(path.read_text())
93+
initial = parse_version(data["version"]).major
94+
latticejson = _migrate(data, initial, final)
95+
formatted = format_json(latticejson)
96+
click.secho(f"Migrated {path} from version {initial} to {final}", bold=True)
97+
if dry_run:
98+
click.echo(formatted)
99+
else:
100+
path.write_text(formatted)
101+
102+
103+
@cli.group()
104+
def utils():
105+
"""Some useful utilities."""
106+
pass
107+
108+
109+
@utils.command()
110+
@click.argument("file", type=click.Path(exists=True))
111+
@click.option("--lattice", "-l", type=str, help="Root lattice of tree.")
112+
@click.option(
113+
"--format",
114+
"format_",
115+
type=click.Choice(FORMATS, case_sensitive=False),
116+
help="Source format [optional, default: use file extension]",
117+
)
118+
def tree(file, lattice, format_):
119+
"""Print tree of elements for a given LatticeJSON file."""
120+
from .utils import tree
121+
122+
data = io.load(file, format_, validate)
123+
click.echo(tree(data, lattice))
124+
125+
126+
@utils.command()
127+
@click.argument("file", type=click.Path(exists=True))
128+
@click.option("--lattice", "-l", type=str, help="New root lattice [Default: current]")
129+
@click.option("--warn-unused", "-w", is_flag=True, help="Log removed lattices.")
130+
@click.option(
131+
"--validate/--no-validate", default=True, help="Whether to validate the input file."
132+
)
133+
def remove_unused(file, lattice, warn_unused, validate):
134+
"""Remove unused objects from a LatticeJSON file."""
135+
from .utils import remove_unused
136+
137+
data = remove_unused(io.load(file, validate=validate), lattice, warn_unused)
138+
click.echo(format_json(data))
83139

84140

85141
@cli.group()

latticejson/convert.py

Lines changed: 36 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
from typing import List, Dict
2-
from pathlib import Path
31
import json
2+
from pathlib import Path
3+
from typing import Dict, List
44
from warnings import warn
5+
6+
from .exceptions import UnknownAttributeWarning, UnknownElementTypeWarning
57
from .parse import parse_elegant, parse_madx
6-
from .exceptions import UnknownElementWarning, UnknownAttributeWarning
8+
from .utils import sort_lattices
9+
from .validate import schema_version
710

811
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]}
12+
TO_ELEGANT = {x: y[0][0] for x, *y in NAME_MAP}
13+
FROM_ELEGANT = {y: x for x, *tup in NAME_MAP for y in tup[0]}
14+
TO_MADX = {x: y[1][0] for x, *y in NAME_MAP}
15+
FROM_MADX = {y: x for x, *tup in NAME_MAP for y in tup[1]}
1316

1417

1518
def from_elegant(string):
@@ -22,7 +25,7 @@ def from_elegant(string):
2225
:type str, optional
2326
:return: dict in LatticeJSON format
2427
"""
25-
return _map_names_from_elegant(parse_elegant(string))
28+
return _map_names(parse_elegant(string), FROM_ELEGANT)
2629

2730

2831
def from_madx(string):
@@ -35,15 +38,7 @@ def from_madx(string):
3538
:type str, optional
3639
:return: dict in LatticeJSON format
3740
"""
38-
return _map_names_from_madx(parse_madx(string))
39-
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)
41+
return _map_names(parse_madx(string), FROM_MADX)
4742

4843

4944
def _map_names(lattice_data: dict, name_map: dict):
@@ -52,7 +47,7 @@ def _map_names(lattice_data: dict, name_map: dict):
5247
latticejson_type = name_map.get(other_type)
5348
if latticejson_type is None:
5449
elements[name] = ["Drift", {"length": other_attributes.get("L", 0)}]
55-
warn(UnknownElementWarning(name, other_type), stacklevel=2)
50+
warn(UnknownElementTypeWarning(name, other_type))
5651
continue
5752

5853
attributes = {}
@@ -62,15 +57,17 @@ def _map_names(lattice_data: dict, name_map: dict):
6257
if latticejson_key is not None:
6358
attributes[latticejson_key] = value
6459
else:
65-
warn(UnknownAttributeWarning(other_key, name), stacklevel=2)
60+
warn(UnknownAttributeWarning(other_key, name))
6661

6762
lattices = lattice_data["lattices"]
68-
lattice_name, main_lattice = lattices.popitem() # use last lattice as main_lattice
63+
root = lattice_data.get("root", tuple(lattices.keys())[-1])
64+
title = lattice_data.get("title", "")
6965
return dict(
70-
name=lattice_name,
71-
lattice=main_lattice,
72-
sub_lattices=lattices,
66+
version=str(schema_version),
67+
title=title,
68+
root=root,
7369
elements=elements,
70+
lattices=lattices,
7471
)
7572

7673

@@ -81,21 +78,21 @@ def to_elegant(latticejson: dict) -> str:
8178
:return: string with in elegant lattice file format
8279
"""
8380
elements = latticejson["elements"]
84-
sub_lattices = latticejson["sub_lattices"]
81+
lattices = latticejson["lattices"]
8582

86-
strings = [f"! TITLE: {latticejson['name']}"]
83+
strings = [f"! TITLE: {latticejson['title']}"]
8784
element_template = "{}: {}, {}".format
85+
# TODO: check if equivalent type exists in elegant
8886
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_]
87+
attrs = ", ".join(f"{TO_ELEGANT[k]}={v}" for k, v in attributes.items())
88+
elegant_type = TO_ELEGANT[type_]
9189
strings.append(element_template(name, elegant_type, attrs))
9290

9391
lattice_template = "{}: LINE=({})".format
94-
for name in sort_lattices(sub_lattices):
95-
strings.append(lattice_template(name, ", ".join(sub_lattices[name])))
92+
for name, children in sort_lattices(latticejson).items():
93+
strings.append(lattice_template(name, ", ".join(children)))
9694

97-
strings.append(lattice_template("__MAIN__", ", ".join(latticejson["lattice"])))
98-
strings.append("USE, __MAIN__\n")
95+
strings.append(f"USE, {latticejson['root']}\n")
9996
return "\n".join(strings)
10097

10198

@@ -106,39 +103,19 @@ def to_madx(latticejson: dict) -> str:
106103
:return: string with in elegant lattice file format
107104
"""
108105
elements = latticejson["elements"]
109-
sub_lattices = latticejson["sub_lattices"]
106+
lattices = latticejson["lattices"]
110107

111-
strings = [f"TITLE, \"{latticejson['name']}\""]
108+
strings = [f"TITLE, \"{latticejson['title']}\";"]
112109
element_template = "{}: {}, {};".format
110+
# TODO: check if equivalent type exists in madx
113111
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_]
112+
attrs = ", ".join(f"{TO_MADX[k]}={v}" for k, v in attributes.items())
113+
elegant_type = TO_MADX[type_]
116114
strings.append(element_template(name, elegant_type, attrs))
117115

118116
lattice_template = "{}: LINE=({});".format
119-
for name in sort_lattices(sub_lattices):
120-
strings.append(lattice_template(name, ", ".join(sub_lattices[name])))
117+
for name, children in sort_lattices(latticejson).items():
118+
strings.append(lattice_template(name, ", ".join(children)))
121119

122-
strings.append(lattice_template("__MAIN__", ", ".join(latticejson["lattice"])))
123-
strings.append("USE, __MAIN__;\n")
120+
strings.append(f"USE, SEQUENCE={latticejson['root']};\n")
124121
return "\n".join(strings)
125-
126-
127-
def sort_lattices(lattices: Dict[str, List[str]]) -> List[str]:
128-
"""Returns a sorted list of lattice names for a given dict of lattices."""
129-
130-
lattices_set = set(lattices)
131-
lattice_names = []
132-
133-
def _sort_lattices(name):
134-
for child_name in lattices[name]:
135-
if child_name in lattices_set:
136-
lattices_set.remove(child_name)
137-
_sort_lattices(child_name)
138-
139-
lattice_names.append(name)
140-
141-
while len(lattices_set) > 0:
142-
_sort_lattices(lattices_set.pop())
143-
144-
return lattice_names

latticejson/elegant.lark

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
%ignore /!.*/ // ingore comments
1111
%ignore /[ \t\f]/+ // ingore whitespace
1212
%ignore /&[ \t\f]*\r?\n/ // line continuation
13-
%import common (SIGNED_INT, SIGNED_FLOAT, NUMBER, ESCAPED_STRING, CNAME)
13+
%import common (SIGNED_INT, SIGNED_FLOAT, SIGNED_NUMBER, ESCAPED_STRING, CNAME)
1414

1515
int : SIGNED_INT
1616
float : SIGNED_FLOAT
@@ -38,7 +38,7 @@ command : name ["," word]
3838
// identified as string.
3939

4040
assignment : expr "sto" CNAME
41-
?expr : NUMBER -> number
41+
?expr : SIGNED_NUMBER -> number
4242
| CNAME -> variable
4343
| function
4444
| binary

0 commit comments

Comments
 (0)