Skip to content

Commit eed5656

Browse files
authored
Merge pull request #54 from DiamondLightSource/diamond2-conversion
Diamond2 conversion: -Make atip compatible with Diamond 2 lattice. -Support multipoles in atip -Make create_lattice_matfile.m compatible with Diamond 2 lattice and improve code readibility -Regenerate all lattice files using this new script. -Add Diamond 2 lattice to tests -Made a few improvements to the tests
2 parents e37bbde + a689bff commit eed5656

File tree

8 files changed

+116
-84
lines changed

8 files changed

+116
-84
lines changed

src/atip/load_sim.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from atip.simulator import ATSimulator
1010

1111
# List of all the element fields that can be currently simulated.
12-
SIMULATED_FIELDS = {"a1", "b0", "b1", "b2", "x", "y", "f", "x_kick", "y_kick"}
12+
SIMULATED_FIELDS = {"a1", "b0", "b1", "b2", "b3", "x", "y", "f", "x_kick", "y_kick"}
1313

1414

1515
def load_from_filepath(

src/atip/rings/48.mat

242 KB
Binary file not shown.

src/atip/rings/DIAD.mat

-43 Bytes
Binary file not shown.

src/atip/rings/I04.mat

-12 Bytes
Binary file not shown.
Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,75 @@
11
function create_lattice_matfile(filename)
22
% Creates a .mat file AT lattice compatible with ATIP.
33
% If a filename is given that file will be updated to ATIP standard. Otherwise
4-
% the ring is taken from either of the 'RING' or 'THERING' global variables,
5-
% with 'RING' taking priority. If a filename is not passed the updated lattice
6-
% will be stored in 'lattice.mat'.
4+
% ATIP_RING is initially taken from 'THERING' global variable and save as
5+
% 'lattice.mat'.
76
if ~(nargin == 0)
8-
load(filename, 'RING');
7+
load(filename, 'ATIP_RING');
98
end
10-
if ~exist('RING', 'var')
11-
global RING;
12-
if isempty(RING)
13-
global THERING;
14-
RING = THERING;
9+
10+
if ~exist('ATIP_RING', 'var')
11+
global THERING;
12+
if isempty(THERING)
13+
disp('THERING global variable is empty, try running storageringinit(Ringmode). Exiting with error.');
14+
exit(1)
15+
else
16+
ATIP_RING=THERING
17+
disp('Using global THERING and saving it to global ATIP_RING.');
1518
end
19+
else
20+
disp('Using loaded ATIP_RING from file.');
1621
end
17-
if isempty(RING)
18-
disp('Unable to load a ring from file or global variables.');
19-
return;
20-
end
22+
23+
fprintf('Initial lattice has dimensions: %s\n', mat2str(size(ATIP_RING)))
2124
% Correct dimension order if necessary.
22-
if size(RING, 1) == 1
23-
RING = permute(RING, [2 1]);
25+
if size(ATIP_RING, 1) == 1
26+
ATIP_RING = permute(ATIP_RING, [2 1]);
2427
end
28+
2529
% Correct classes and pass methods.
26-
for x = 1:length(RING)
27-
if strcmp(RING{x, 1}.FamName, 'BPM10')
30+
for x = 1:length(ATIP_RING)
31+
if strcmp(ATIP_RING{x, 1}.FamName, 'BPM10')
2832
% Wouldn't be correctly classed by class guessing otherwise.
29-
RING{x, 1}.Class = 'Monitor';
30-
elseif (strcmp(RING{x, 1}.FamName, 'HSTR') || strcmp(RING{x, 1}.FamName, 'VSTR'))
31-
RING{x, 1}.Class = 'Corrector';
32-
elseif (strcmp(RING{x, 1}.FamName, 'HTRIM') || strcmp(RING{x, 1}.FamName, 'VTRIM'))
33-
RING{x, 1}.Class = 'Corrector';
33+
ATIP_RING{x, 1}.Class = 'Monitor';
34+
elseif (strcmp(ATIP_RING{x, 1}.FamName, 'HSTR') || strcmp(ATIP_RING{x, 1}.FamName, 'VSTR'))
35+
ATIP_RING{x, 1}.Class = 'Corrector';
36+
elseif (strcmp(ATIP_RING{x, 1}.FamName, 'HTRIM') || strcmp(ATIP_RING{x, 1}.FamName, 'VTRIM'))
37+
ATIP_RING{x, 1}.Class = 'Corrector';
3438
end
35-
if isfield(RING{x, 1}, 'Class')
36-
if strcmp(RING{x, 1}.Class, 'SEXT')
37-
RING{x, 1}.Class = 'Sextupole';
39+
40+
if isfield(ATIP_RING{x, 1}, 'Class')
41+
if strcmp(ATIP_RING{x, 1}.Class, 'SEXT')
42+
ATIP_RING{x, 1}.Class = 'Sextupole';
3843
end
3944
end
40-
if strcmp(RING{x, 1}.PassMethod, 'ThinCorrectorPass')
41-
% ThinCorrectorPass no longer exists in AT.
42-
RING{x, 1}.PassMethod = 'CorrectorPass';
43-
elseif strcmp(RING{x, 1}.PassMethod, 'GWigSymplecticPass')
44-
RING{x, 1}.Class = 'Wiggler';
45+
46+
if strcmp(ATIP_RING{x, 1}.PassMethod, 'GWigSymplecticPass')
47+
ATIP_RING{x, 1}.Class = 'Wiggler';
4548
end
4649
end
4750

48-
% Remove elements. Done this way because the size of RING changes during
49-
% the loop.
51+
% Remove elements. Done this way because the size of ATIP_RING changes
52+
% during the loop.
5053
y = 1;
51-
while y < length(RING)
52-
% I should probably transfer the attributes of the deleted corrector
53-
% elements to the sextupole but cba.
54-
if (strcmp(RING{y, 1}.FamName, 'HSTR') && strcmp(RING{y-1, 1}.Class, 'Sextupole'))
55-
RING(y, :) = []; % Delete hstrs that are preceded by a sextupole.
56-
elseif (strcmp(RING{y, 1}.FamName, 'VSTR') && strcmp(RING{y-1, 1}.Class, 'Sextupole'))
57-
RING(y, :) = []; % Delete vstrs that are preceded by a sextupole.
54+
while y < length(ATIP_RING)
55+
% The data within the deleted elements is not needed
56+
if strcmp(ATIP_RING{y, 1}.FamName, 'HSTR') && ATIP_RING{y, 1}.Length == 0 && (strcmp(ATIP_RING{y-1, 1}.Class, 'Sextupole') || strcmp(ATIP_RING{y-1, 1}.Class, 'Multipole'))
57+
ATIP_RING(y, :) = []; % Delete hstrs that are preceded by a sextupole or multipole.
58+
elseif strcmp(ATIP_RING{y, 1}.FamName, 'VSTR') && ATIP_RING{y, 1}.Length == 0 && (strcmp(ATIP_RING{y-1, 1}.Class, 'Sextupole') || strcmp(ATIP_RING{y-1, 1}.Class, 'Multipole'))
59+
ATIP_RING(y, :) = []; % Delete vstrs that are preceded by a sextupole or multipole.
5860
else
5961
y = y + 1;
6062
end
6163
end
62-
if isfield(RING{1, 1}, 'TwissData')
63-
RING{1, 1} = rmfield(RING{1, 1}, 'TwissData');
64+
65+
if isfield(ATIP_RING{1, 1}, 'TwissData')
66+
ATIP_RING{1, 1} = rmfield(ATIP_RING{1, 1}, 'TwissData');
6467
end
68+
69+
fprintf('Converted ATIP_RING has dimensions: %s\n', mat2str(size(ATIP_RING)))
6570
if nargin == 0
66-
save('lattice.mat', 'RING');
71+
save('lattice.mat', 'ATIP_RING');
6772
else
68-
save(filename, 'RING');
73+
save(filename, 'ATIP_RING');
6974
end
7075
end

src/atip/sim_data_sources.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(self, at_element, index, atsim, fields=None):
7272
"a1": partial(self._get_PolynomA, 1),
7373
"b1": partial(self._get_PolynomB, 1),
7474
"b2": partial(self._get_PolynomB, 2),
75+
"b3": partial(self._get_PolynomB, 3),
7576
"b0": self._get_BendingAngle,
7677
"f": self._get_Frequency,
7778
}
@@ -81,6 +82,7 @@ def __init__(self, at_element, index, atsim, fields=None):
8182
"a1": partial(self._set_PolynomA, 1),
8283
"b1": partial(self._set_PolynomB, 1),
8384
"b2": partial(self._set_PolynomB, 2),
85+
"b3": partial(self._set_PolynomB, 3),
8486
"b0": self._set_BendingAngle,
8587
"f": self._set_Frequency,
8688
}
@@ -200,21 +202,21 @@ def _get_KickAngle(self, cell):
200202
"""A data handling function used to get the value of a specific cell
201203
of the KickAngle attribute of the AT element.
202204
203-
.. Note:: If the Corrector is attached to a Sextupole then KickAngle
204-
needs to be returned from cell 0 of the applicable Polynom(A/B)
205-
attribute and so a conversion must take place. For independent
206-
Correctors KickAngle can be returned directly from the element's
207-
KickAngle attribute without any conversion. This is because
208-
independent Correctors have a KickAngle attribute in our AT lattice,
209-
but those attached to Sextupoles do not.
205+
.. Note:: If the Corrector is attached to a Sextupole or Octupole then
206+
KickAngle needs to be returned from cell 0 of the applicable Polynom(A/B)
207+
attribute and so a conversion must take place. For independent
208+
Correctors KickAngle can be returned directly from the element's
209+
KickAngle attribute without any conversion. This is because
210+
independent Correctors have a KickAngle attribute in our AT lattice,
211+
but those attached to Sextupoles do not.
210212
211213
Args:
212214
cell (int): Which cell of KickAngle to get.
213215
214216
Returns:
215217
float: The kick angle of the specified cell.
216218
"""
217-
if isinstance(self._at_element, at.elements.Sextupole):
219+
if isinstance(self._at_element, (at.elements.Sextupole, at.elements.Multipole)):
218220
length = self._at_element.Length
219221
if cell == 0:
220222
return -(self._at_element.PolynomB[0] * length)
@@ -227,19 +229,19 @@ def _set_KickAngle(self, cell, value):
227229
"""A data handling function used to set the value of a specific cell
228230
of the KickAngle attribute of the AT element.
229231
230-
.. Note:: If the Corrector is attached to a Sextupole then KickAngle
231-
needs to be assigned to cell 0 of the applicable Polynom(A/B)
232-
attribute and so a conversion must take place. For independent
233-
Correctors KickAngle can be assigned directly to the element's
234-
KickAngle attribute without any conversion. This is because
235-
independent Correctors have a KickAngle attribute in our AT lattice,
236-
but those attached to Sextupoles do not.
232+
.. Note:: If the Corrector is attached to a Sextupole or Octupole then
233+
KickAngle needs to be assigned to cell 0 of the applicable Polynom(A/B)
234+
attribute and so a conversion must take place. For independent
235+
Correctors KickAngle can be assigned directly to the element's
236+
KickAngle attribute without any conversion. This is because
237+
independent Correctors have a KickAngle attribute in our AT lattice,
238+
but those attached to Sextupoles do not.
237239
238240
Args:
239241
cell (int): Which cell of KickAngle to set.
240242
value (float): The angle to be set.
241243
"""
242-
if isinstance(self._at_element, at.elements.Sextupole):
244+
if isinstance(self._at_element, (at.elements.Sextupole, at.elements.Multipole)):
243245
length = self._at_element.Length
244246
if cell == 0:
245247
self._at_element.PolynomB[0] = -(value / length)

tests/conftest.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,31 @@ def atlds():
6262
return atip.sim_data_sources.ATLatticeDataSource(mock.Mock())
6363

6464

65-
@pytest.fixture()
66-
def at_lattice():
67-
return atip.utils.load_at_lattice("I04")
65+
@pytest.fixture(scope="function", params=["I04"])
66+
def at_and_pytac_lattices(request):
67+
lattices = []
68+
lattices.append(load_csv.load(request.param, cs.ControlSystem()))
69+
lattices.append(atip.utils.load_at_lattice(request.param))
70+
return lattices
6871

6972

70-
@pytest.fixture(scope="session")
71-
def pytac_lattice():
72-
return load_csv.load("DIAD", cs.ControlSystem())
73+
@pytest.fixture(scope="function", params=["I04"])
74+
def pytac_lattice(request):
75+
return load_csv.load(request.param, cs.ControlSystem())
7376

7477

75-
@pytest.fixture(scope="session")
76-
def mat_filepath():
77-
here = os.path.dirname(__file__)
78-
return os.path.realpath(os.path.join(here, "../src/atip/rings/DIAD.mat"))
78+
@pytest.fixture(scope="function", params=["I04"])
79+
def at_lattice(request):
80+
return atip.utils.load_at_lattice(request.param)
7981

8082

81-
@pytest.fixture(scope="session")
82-
def at_diad_lattice(mat_filepath):
83-
return at.load.load_mat(mat_filepath)
83+
@pytest.fixture(scope="function", params=["DIAD"])
84+
def lattice_filepath(request):
85+
here = os.path.dirname(__file__)
86+
filepath = os.path.realpath(
87+
os.path.join(here, f"../src/atip/rings/{request.param}.mat")
88+
)
89+
return filepath
8490

8591

8692
@pytest.fixture()

tests/test_load.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@
55

66
import atip
77

8+
RINGMODES_TO_TEST = ["I04", "DIAD", "48"]
89

9-
def test_load_pytac_side(pytac_lattice, at_diad_lattice):
10-
lat = atip.load_sim.load(pytac_lattice, at_diad_lattice)
10+
11+
@pytest.mark.parametrize(
12+
"at_lattice",
13+
RINGMODES_TO_TEST,
14+
indirect=True,
15+
)
16+
def test_load_atip_lattice(request, at_lattice):
17+
assert at_lattice.name == request.node.callspec.params["at_lattice"]
18+
19+
20+
@pytest.mark.parametrize("at_and_pytac_lattices", RINGMODES_TO_TEST, indirect=True)
21+
def test_load_pytac_side(at_and_pytac_lattices):
22+
lat = atip.load_sim.load(at_and_pytac_lattices[0], at_and_pytac_lattices[1])
1123
# Check lattice has simulator data source
1224
assert pytac.SIM in lat._data_source_manager._data_sources
1325
# Check all elements have simulator data source
@@ -18,22 +30,29 @@ def test_load_pytac_side(pytac_lattice, at_diad_lattice):
1830
assert isinstance(lat._data_source_manager._uc["mu"], pytac.units.NullUnitConv)
1931

2032

21-
def test_load_from_filepath(pytac_lattice, mat_filepath):
22-
atip.load_sim.load_from_filepath(pytac_lattice, mat_filepath)
33+
@pytest.mark.parametrize(
34+
["pytac_lattice", "lattice_filepath"],
35+
[(mode, mode) for mode in RINGMODES_TO_TEST],
36+
indirect=True,
37+
)
38+
def test_load_atip_and_pytac_lattices(pytac_lattice, lattice_filepath):
39+
atip.load_sim.load_from_filepath(pytac_lattice, lattice_filepath)
2340

2441

25-
def test_load_with_non_callable_callback_raises_TypeError(
26-
pytac_lattice, at_diad_lattice
27-
):
42+
@pytest.mark.parametrize("at_and_pytac_lattices", RINGMODES_TO_TEST, indirect=True)
43+
def test_load_with_non_callable_callback_raises_TypeError(at_and_pytac_lattices):
2844
with pytest.raises(TypeError):
29-
atip.load_sim.load(pytac_lattice, at_diad_lattice, "")
45+
atip.load_sim.load(at_and_pytac_lattices[0], at_and_pytac_lattices[1], "")
3046

3147

32-
def test_load_with_callback(pytac_lattice, at_diad_lattice):
48+
@pytest.mark.parametrize("at_and_pytac_lattices", RINGMODES_TO_TEST, indirect=True)
49+
def test_load_with_callback(at_and_pytac_lattices):
3350
callback_func = mock.Mock()
34-
lat = atip.load_sim.load(pytac_lattice, at_diad_lattice, callback_func)
51+
lat = atip.load_sim.load(
52+
at_and_pytac_lattices[0], at_and_pytac_lattices[1], callback_func
53+
)
3554
atsim = lat._data_source_manager._data_sources[pytac.SIM]._atsim
36-
atip.utils.trigger_calc(pytac_lattice)
55+
atip.utils.trigger_calc(at_and_pytac_lattices[0])
3756
atsim.wait_for_calculations()
3857
callback_func.assert_called_once_with()
3958

0 commit comments

Comments
 (0)