Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save and load lattices in JSON format #766

Merged
merged 22 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions atmat/lattice/atdivelem.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
line=atsetfieldvalues(line,'ExitAngle',0.0);
end
if isfield(elem,'KickAngle')
line=atsetfieldvalues(line,'KickAngle',{1,1},el.KickAngle(1,1)*frac(:)/sum(frac));
line=atsetfieldvalues(line,'KickAngle',{1,2},el.KickAngle(1,2)*frac(:)/sum(frac));
line=atsetfieldvalues(line,'KickAngle',{1},el.KickAngle(1)*frac(:)/sum(frac));
line=atsetfieldvalues(line,'KickAngle',{2},el.KickAngle(2)*frac(:)/sum(frac));
end

line{1}=mvfield(line{1},entrancef); % Set back entrance fields
Expand Down
30 changes: 29 additions & 1 deletion atmat/lattice/atloadlattice.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
% the variable name must be specified using the 'matkey' keyword.
%
% .m Matlab function. The function must output a valid AT structure.
% .json JSON file
%
%see also atwritem, atwritejson

persistent link_table

if isempty(link_table)
link_table.mat=@load_mat;
link_table.m=@load_m;
link_table.json=@load_json;
end

[~,~,fext]=fileparts(fspec);
Expand Down Expand Up @@ -57,7 +61,7 @@
dt=load(fpath);
vnames=fieldnames(dt);
key='RING';
if length(vnames) == 1
if isscalar(vnames)
key=vnames{1};
else
for v={'ring','lattice'}
Expand All @@ -75,7 +79,31 @@
error('AT:load','Cannot find variable %s\nmatkey must be in: %s',...
key, strjoin(vnames,', '));
end
end

function [lattice, opts]=load_json(fpath, opts)
data=jsondecode(fileread(fpath));
% File signature for later use
try
atjson=data.atjson;
catch
atjson=1;
end
props=data.properties;
name=props.name;
energy=props.energy;
periodicity=props.periodicity;
particle=atparticle.loadobj(props.particle);
harmnumber=props.harmonic_number;
props=rmfield(props,{'name','energy','periodicity','particle','harmonic_number'});
args=[fieldnames(props) struct2cell(props)]';
lattice=atSetRingProperties(data.elements,...
'FamName', name,...
'Energy', energy,...
'Periodicity', periodicity,...
'Particle', particle,...
'HarmNumber', harmnumber, ...
args{:});
end

end
68 changes: 68 additions & 0 deletions atmat/lattice/atwritejson.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
function varargout=atwritejson(ring, varargin)
%ATWRITEJSON Create a JSON file to store an AT lattice
%
%JS=ATWRITEJSON(RING)
% Return the JSON representation of RING as a character array
%
%ATWRITEJSON(RING, FILENAME)
% Write the JSON representation of RING to the file FILENAME
%
%ATWRITEJSON(RING, ..., 'compact', true)
% If compact is true, write a compact JSON file (no linefeeds)
%
%see also atloadlattice

[compact, varargs]=getoption(varargin, 'compact', false);
[filename, ~]=getargs(varargs,[]);

if ~isempty(filename)
%get filename information
[pname,fname,ext]=fileparts(filename);

%Check file extension
if isempty(ext), ext='.json'; end

% Open file to be written
[fid,mess]=fopen(fullfile(pname,[fname ext]),'wt');

if fid==-1
error('AT:FileErr','Cannot Create file %s\n%s',fn,mess);
else
fprintf(fid, sjson(ring));
fclose(fid);
end
varargout={};
else
varargout={sjson(ring)};
end

function jsondata=sjson(ring)
ok=~atgetcells(ring, 'Class', 'RingParam');
data.atjson= 1;
data.elements=ring(ok);
data.properties=get_params(ring);
jsondata=jsonencode(data, 'PrettyPrint', ~compact);
end

function prms=get_params(ring)
% Get "standard" properties
[name, energy, part, periodicity, harmonic_number]=...
atGetRingProperties(ring,'FamName', 'Energy', 'Particle',...
'Periodicity', 'HarmNumber');
prms=struct('name', name, 'energy', energy, 'periodicity', periodicity,...
'particle', saveobj(part), 'harmonic_number', harmonic_number);
% Add user-defined properties
idx=atlocateparam(ring);
if ~isempty(idx)
flist={'FamName','PassMethod','Length','Class',...
'Energy', 'Particle','Periodicity','cell_harmnumber'};
present=isfield(ring{idx}, flist);
p2=rmfield(ring{idx},flist(present));
for nm=fieldnames(p2)'
na=nm{1};
prms.(na)=p2.(na);
end
end
end

end
47 changes: 28 additions & 19 deletions pyat/at/lattice/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,26 +302,17 @@ def __setattr__(self, key, value):
super(Element, self).__setattr__(key, value)

def __str__(self):
first3 = ["FamName", "Length", "PassMethod"]
# Get values and parameter objects
attrs = dict(self.items())
keywords = [f"\t{k} : {attrs.pop(k)!s}" for k in first3]
keywords += [f"\t{k} : {v!s}" for k, v in attrs.items()]
return "\n".join((type(self).__name__ + ":", "\n".join(keywords)))
return "\n".join(
[self.__class__.__name__ + ":"]
+ [f"{k:>14}: {v!s}" for k, v in self.items()]
)

def __repr__(self):
# Get values only, even for parameters
attrs = dict((k, getattr(self, k)) for k, v in self.items())
arguments = [attrs.pop(k) for k in self._BUILD_ATTRIBUTES]
defelem = self.__class__(*arguments)
keywords = [f"{v!r}" for v in arguments]
keywords += [
f"{k}={v!r}"
for k, v in sorted(attrs.items())
if not numpy.array_equal(v, getattr(defelem, k, None))
]
clsname, args, kwargs = self.definition
keywords = [f"{arg!r}" for arg in args]
keywords += [f"{k}={v!r}" for k, v in kwargs.items()]
args = re.sub(r"\n\s*", " ", ", ".join(keywords))
return "{0}({1})".format(self.__class__.__name__, args)
return f"{clsname}({args})"

def equals(self, other) -> bool:
"""Whether an element is equivalent to another.
Expand Down Expand Up @@ -399,10 +390,28 @@ def deepcopy(self) -> Element:
"""Return a deep copy of the element"""
return deepcopy(self)

@property
def definition(self) -> tuple[str, tuple, dict]:
"""tuple (class_name, args, kwargs) defining the element"""
attrs = dict(self.items())
arguments = tuple(attrs.pop(
k, getattr(self, k)) for k in self._BUILD_ATTRIBUTES
)
defelem = self.__class__(*arguments)
keywords = dict(
(k, v)
for k, v in attrs.items()
if not numpy.array_equal(v, getattr(defelem, k, None))
)
return self.__class__.__name__, arguments, keywords

def items(self) -> Generator[tuple[str, Any], None, None]:
"""Iterates through the data members"""
# Properties may be added by overloading this method
yield from vars(self).items()
v = vars(self).copy()
for k in ["FamName", "Length", "PassMethod"]:
yield k, v.pop(k)
for k, v in sorted(v.items()):
yield k, v

def is_compatible(self, other: Element) -> bool:
"""Checks if another :py:class:`Element` can be merged"""
Expand Down
1 change: 1 addition & 0 deletions pyat/at/load/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .reprfile import *
from .tracy import *
from .elegant import *
from .json import *
111 changes: 51 additions & 60 deletions pyat/at/load/allfiles.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
"""Generic function to save and load python AT lattices. The format is
determined by the file extension
"""

from __future__ import annotations

__all__ = ["load_lattice", "save_lattice", "register_format"]

import os.path
from at.lattice import Lattice
from collections.abc import Callable
from typing import Optional

__all__ = ['load_lattice', 'save_lattice', 'register_format']
from at.lattice import Lattice

_load_extension = {}
_save_extension = {}
Expand All @@ -13,41 +19,31 @@
def load_lattice(filepath: str, **kwargs) -> Lattice:
"""Load a Lattice object from a file

The file format is indicated by the filepath extension.

Parameters:
filepath: Name of the file

Keyword Args:
name (str): Name of the lattice.
Default: taken from the file, or ``''``
energy (float): Energy of the lattice
(default: taken from the file)
periodicity (int]): Number of periods
(default: taken from the file, or 1)
*: All other keywords will be set as :py:class:`.Lattice`
attributes

Specific keywords for .mat files

Keyword Args:
mat_key (str): Name of the Matlab variable containing
the lattice. Default: Matlab variable name if there is only one,
otherwise ``'RING'``
check (bool): Run coherence tests. Default: :py:obj:`True`
quiet (bool): Suppress the warning for non-standard classes.
Default: :py:obj:`False`
keep_all (bool): Keep Matlab RingParam elements as Markers.
Default: :py:obj:`False`

Returns:
lattice (Lattice): New :py:class:`.Lattice` object

See Also:
:py:func:`.load_mat`, :py:func:`.load_m`, :py:func:`.load_repr`,
:py:func:`.load_elegant`, :py:func:`.load_tracy`

.. Admonition:: Known extensions are:
The file format is indicated by the filepath extension. The file name is stored in
the *in_file* Lattice attribute. The selected variable, if relevant, is stored
in the *use* Lattice attribute.

Parameters:
filepath: Name of the file

Keyword Args:
use (str): Name of the variable containing the desired lattice.
Default: if there is a single variable, use it, otherwise select ``"RING"``
name (str): Name of the lattice.
Default: taken from the file, or ``""``
energy (float): Energy of the lattice
(default: taken from the file)
periodicity (int): Number of periods
(default: taken from the file, or 1)
*: All other keywords will be set as :py:class:`.Lattice`
attributes

Returns:
lattice (Lattice): New :py:class:`.Lattice` object

Check the format-specific function for specific keyword arguments:

.. Admonition:: Known extensions are:
"""
_, ext = os.path.splitext(filepath)
try:
Expand All @@ -58,25 +54,18 @@ def load_lattice(filepath: str, **kwargs) -> Lattice:
return load_func(filepath, **kwargs)


def save_lattice(ring: Lattice, filepath: str, **kwargs):
def save_lattice(ring: Lattice, filepath: str, **kwargs) -> None:
"""Save a Lattice object

The file format is indicated by the filepath extension.

Parameters:
ring: Lattice description
filepath: Name of the file
The file format is indicated by the filepath extension.

Specific keywords for .mat files

Keyword Args:
mat_key (str): Name of the Matlab variable containing the lattice.
Default: ``'RING'``
Parameters:
ring: Lattice description
filepath: Name of the file

See Also:
:py:func:`.save_mat`, :py:func:`.save_m`, :py:func:`.save_repr`
Check the format-specific function for specific keyword arguments:

.. Admonition:: Known extensions are:
.. Admonition:: Known extensions are:
"""
_, ext = os.path.splitext(filepath)
try:
Expand All @@ -87,24 +76,26 @@ def save_lattice(ring: Lattice, filepath: str, **kwargs):
return save_func(ring, filepath, **kwargs)


def register_format(extension: str, load_func=None, save_func=None,
descr: str = ''):
def register_format(
extension: str,
load_func: Optional[Callable[..., Lattice]] = None,
save_func: Optional[Callable[..., None]] = None,
descr: str = "",
):
"""Register format-specific processing functions

Parameters:
extension: File extension string.
load_func: load function. Default: :py:obj:`None`
save_func: save_lattice function Default: :py:obj:`None`
descr: File type description
load_func: load function.
save_func: save function.
descr: File type description.
"""
if load_func is not None:
_load_extension[extension] = load_func
load_lattice.__doc__ += '\n {0:<10}'\
'\n {1}\n'.format(extension, descr)
load_lattice.__doc__ += f"\n {extension:<10}\n {descr}\n"
if save_func is not None:
_save_extension[extension] = save_func
save_lattice.__doc__ += '\n {0:<10}'\
'\n {1}\n'.format(extension, descr)
save_lattice.__doc__ += f"\n {extension:<10}\n {descr}\n"


Lattice.load = staticmethod(load_lattice)
Expand Down
11 changes: 5 additions & 6 deletions pyat/at/load/elegant.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,10 +337,8 @@ def load_elegant(filename: str, **kwargs) -> Lattice:
name (str): Name of the lattice. Default: taken from
the file.
energy (float): Energy of the lattice [eV]
periodicity(int): Number of periods. Default: taken from the
elements, or 1
*: All other keywords will be set as Lattice
attributes
periodicity(int): Number of periods. Default: taken from the elements, or 1
*: All other keywords will be set as Lattice attributes

Returns:
lattice (Lattice): New :py:class:`.Lattice` object
Expand All @@ -354,7 +352,7 @@ def load_elegant(filename: str, **kwargs) -> Lattice:
harmonic_number = kwargs.pop("harmonic_number")

def elem_iterator(params, elegant_file):
with open(params.setdefault("elegant_file", elegant_file)) as f:
with open(params.setdefault("in_file", elegant_file)) as f:
contents = f.read()
element_lines = expand_elegant(
contents, lattice_key, energy, harmonic_number
Expand All @@ -370,4 +368,5 @@ def elem_iterator(params, elegant_file):
'lattice {}: {}'.format(filename, e))


register_format(".lte", load_elegant, descr="Elegant format")
register_format(
".lte", load_elegant, descr="Elegant format. See :py:func:`.load_elegant`.")