Skip to content

Commit c4011f3

Browse files
authored
Merge pull request #56 from DiamondLightSource/toggle-sim-behaviour
Allow toggling of sim behaviour: -Selecting linopt function -Enabling/disabling emittance, chromaticity and radiation
2 parents eed5656 + eec4a53 commit c4011f3

File tree

8 files changed

+212
-62
lines changed

8 files changed

+212
-62
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ on:
55
pull_request:
66

77
jobs:
8-
98
lint:
109
uses: ./.github/workflows/_tox.yml
1110
with:
@@ -15,11 +14,11 @@ jobs:
1514
strategy:
1615
matrix:
1716
runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest
18-
python-version: ["3.11", "3.12", "3.13"]
17+
python-version: ["3.11"]
1918
include:
2019
# Include one that runs in the dev environment
2120
- runs-on: "ubuntu-latest"
22-
python-version: "dev"
21+
python-version: "3.11"
2322
fail-fast: false
2423
uses: ./.github/workflows/_test.yml
2524
with:
@@ -41,17 +40,16 @@ jobs:
4140
docs:
4241
uses: ./.github/workflows/_docs.yml
4342

44-
4543
dist:
4644
uses: ./.github/workflows/_dist.yml
47-
45+
4846
pypi:
4947
needs: [dist, test]
5048
if: github.ref_type == 'tag'
5149
uses: ./.github/workflows/_pypi.yml
5250
permissions:
5351
id-token: write
54-
52+
5553
release:
5654
needs: [dist, test, docs]
5755
if: github.ref_type == 'tag'

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ classifiers = [
88
"Development Status :: 3 - Alpha",
99
"License :: OSI Approved :: Apache Software License",
1010
"Programming Language :: Python :: 3.11",
11-
"Programming Language :: Python :: 3.12",
12-
"Programming Language :: Python :: 3.13",
1311
]
1412
description = "Accelerator Toolbox Interface for Pytac"
1513
dependencies = [
@@ -103,7 +101,7 @@ commands =
103101
pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs}
104102
type-checking: mypy src tests {posargs}
105103
tests: pytest --cov=atip --cov-report term --cov-report xml:cov.xml {posargs}
106-
docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html
104+
docs: sphinx-{posargs:build -E --keep-going} -T docs build/html
107105
"""
108106
# Add -W flag to sphinx-build if you want to fail on warnings
109107

src/atip/load_sim.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313

1414

1515
def load_from_filepath(
16-
pytac_lattice, at_lattice_filepath, callback=None, disable_emittance=False
16+
pytac_lattice,
17+
at_lattice_filepath,
18+
sim_params=None,
19+
callback=None,
1720
):
1821
"""Load simulator data sources onto the lattice and its elements.
1922
2023
Args:
2124
pytac_lattice (pytac.lattice.Lattice): An instance of a Pytac lattice.
2225
at_lattice_filepath (str): The path to a .mat file from which the
2326
Accelerator Toolbox lattice can be loaded.
27+
sim_params (SimParams | None): An optional dataclass containing the pyAT
28+
simulation parameters to use.
2429
callback (typing.Callable): To be called after completion of each round of
25-
physics calculations.
26-
disable_emittance (bool): Whether the emittance should be calculated.
30+
physics calculations.
2731
2832
Returns:
2933
pytac.lattice.Lattice: The same Pytac lattice object, but now with a
@@ -34,19 +38,29 @@ def load_from_filepath(
3438
name=pytac_lattice.name,
3539
energy=pytac_lattice.get_value("energy", units=pytac.PHYS),
3640
)
37-
return load(pytac_lattice, at_lattice, callback, disable_emittance)
41+
return load(
42+
pytac_lattice,
43+
at_lattice,
44+
sim_params,
45+
callback,
46+
)
3847

3948

40-
def load(pytac_lattice, at_lattice, callback=None, disable_emittance=False):
49+
def load(
50+
pytac_lattice,
51+
at_lattice,
52+
sim_params=None,
53+
callback=None,
54+
):
4155
"""Load simulator data sources onto the lattice and its elements.
4256
4357
Args:
4458
pytac_lattice (pytac.lattice.Lattice): An instance of a Pytac lattice.
45-
at_lattice (at.lattice_object.Lattice): An instance of an Accelerator
46-
Toolbox lattice object.
59+
at_lattice (at.lattice_object.Lattice): An instance of an AT lattice object.
60+
sim_params (SimParams | None): An optional dataclass containing the pyAT
61+
simulation parameters to use.
4762
callback (typing.Callable): To be called after completion of each round of
48-
physics calculations.
49-
disable_emittance (bool): Whether the emittance should be calculated.
63+
physics calculations.
5064
5165
Returns:
5266
pytac.lattice.Lattice: The same Pytac lattice object, but now with a
@@ -58,7 +72,11 @@ def load(pytac_lattice, at_lattice, callback=None, disable_emittance=False):
5872
f"(AT:{len(at_lattice)} Pytac:{len(pytac_lattice)})."
5973
)
6074
# Initialise an instance of the ATSimulator Object.
61-
atsim = ATSimulator(at_lattice, callback, disable_emittance)
75+
atsim = ATSimulator(
76+
at_lattice,
77+
sim_params,
78+
callback,
79+
)
6280
# Set the simulator data source on the Pytac lattice.
6381
pytac_lattice.set_data_source(ATLatticeDataSource(atsim), pytac.SIM)
6482
# Load the sim onto each element.

src/atip/simulator.py

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from dataclasses import dataclass
5+
from enum import StrEnum, auto
56
from warnings import warn
67

78
import at
@@ -12,6 +13,42 @@
1213
from scipy.constants import speed_of_light
1314

1415

16+
class LinoptType(StrEnum):
17+
LINOPT2 = auto()
18+
LINOPT4 = auto()
19+
LINOPT6 = auto()
20+
21+
22+
@dataclass(frozen=True)
23+
class SimParams:
24+
linopt: LinoptType = LinoptType.LINOPT6
25+
emittance: bool = True
26+
chromaticity: bool = True
27+
radiation: bool = True
28+
29+
def __post_init__(self):
30+
"""Check that we have a valid combination of simulation parameters."""
31+
try:
32+
LinoptType(self.linopt)
33+
except ValueError as e:
34+
raise ValueError(
35+
f"{self.linopt} is not a valid linopt function. Choose from: "
36+
f"{[sp.value for sp in LinoptType]}"
37+
) from e
38+
39+
if self.linopt == LinoptType.LINOPT2 or self.linopt == LinoptType.LINOPT4:
40+
if self.emittance or self.radiation:
41+
raise ValueError(
42+
"Emittance and radiation calculations must be disabled when using "
43+
f"{self.linopt}",
44+
)
45+
elif self.linopt == LinoptType.LINOPT6:
46+
if not self.radiation and self.emittance:
47+
raise ValueError(
48+
"You cannot calculate emittance with radiation disabled",
49+
)
50+
51+
1552
@dataclass
1653
class LatticeData:
1754
twiss: ArrayLike
@@ -22,9 +59,7 @@ class LatticeData:
2259

2360

2461
def calculate_optics(
25-
at_lattice: at.lattice_object.Lattice,
26-
refpts: ArrayLike,
27-
disable_emittance: bool = False,
62+
at_lattice: at.lattice_object.Lattice, refpts: ArrayLike, sp: SimParams
2863
) -> LatticeData:
2964
"""Perform the physics calculations on the lattice.
3065
@@ -36,28 +71,55 @@ def calculate_optics(
3671
Args:
3772
at_lattice (at.lattice_object.Lattice): AT lattice definition.
3873
refpts (numpy.typing.NDArray): A boolean array specifying the points at which
39-
to calculate physics data.
40-
disable_emittance (bool): whether to calculate emittance.
74+
to calculate physics data.
75+
sp (SimParams): An optional dataclass containing the pyAT simulation
76+
parameters to use.
4177
4278
Returns:
4379
LatticeData: The calculated lattice data.
4480
"""
4581
logging.debug("Starting physics calculations.")
82+
logging.debug(
83+
f"Using simulation params: {sp.linopt}, emittance={sp.emittance}, chromaticity="
84+
f"{sp.chromaticity}, radiation={sp.radiation}"
85+
)
4686

47-
orbit0, _ = at_lattice.find_orbit6()
87+
match sp.linopt:
88+
case LinoptType.LINOPT2:
89+
orbit_func = at_lattice.find_orbit
90+
linopt_func = at_lattice.linopt2
91+
case LinoptType.LINOPT4:
92+
orbit_func = at_lattice.find_orbit4
93+
linopt_func = at_lattice.linopt4
94+
case LinoptType.LINOPT6:
95+
orbit_func = at_lattice.find_orbit6
96+
linopt_func = at_lattice.linopt6
97+
98+
# Perform pyAT orbit calculation
99+
orbit0, _ = orbit_func()
48100
logging.debug("Completed orbit calculation.")
49101

50-
_, beamdata, twiss = at_lattice.linopt6(
51-
refpts=refpts, get_chrom=True, orbit=orbit0, keep_lattice=True
102+
# Perform pyAT linear optics calculation
103+
_, beamdata, twiss = linopt_func(
104+
refpts=refpts,
105+
get_chrom=sp.chromaticity,
106+
orbit=orbit0,
107+
keep_lattice=True,
52108
)
53109
logging.debug("Completed linear optics calculation.")
54110

55-
if not disable_emittance:
111+
if sp.emittance:
56112
emitdata = at_lattice.ohmi_envelope(orbit=orbit0, keep_lattice=True)
57113
logging.debug("Completed emittance calculation")
58114
else:
59115
emitdata = ()
60-
radint = at_lattice.get_radiation_integrals(twiss=twiss)
116+
117+
if sp.radiation:
118+
radint = at_lattice.get_radiation_integrals(twiss=twiss)
119+
logging.debug("Completed radiation calculation")
120+
else:
121+
radint = ()
122+
61123
logging.debug("All calculation complete.")
62124
return LatticeData(twiss, beamdata.tune, beamdata.chromaticity, emitdata, radint)
63125

@@ -83,8 +145,6 @@ class ATSimulator:
83145
physics data is calculated.
84146
_rp (numpy.typing.NDArray): A boolean array to be used as refpts for the
85147
physics calculations.
86-
_disable_emittance (bool): Whether or not to perform the beam
87-
envelope based emittance calculations.
88148
_lattice_data (LatticeData): calculated physics data
89149
function linopt (see at.lattice.linear.py).
90150
_queue (cothread.EventQueue): A queue of changes to be applied to
@@ -98,7 +158,12 @@ class ATSimulator:
98158
physics data upon a change.
99159
"""
100160

101-
def __init__(self, at_lattice, callback=None, disable_emittance=False):
161+
def __init__(
162+
self,
163+
at_lattice,
164+
sim_params=None,
165+
callback=None,
166+
):
102167
"""
103168
.. Note:: To avoid errors, the physics data must be initially
104169
calculated here, during creation, otherwise it could be accidentally
@@ -107,12 +172,11 @@ def __init__(self, at_lattice, callback=None, disable_emittance=False):
107172
the thread.
108173
109174
Args:
110-
at_lattice (at.lattice_object.Lattice): An instance of an AT
111-
lattice object.
112-
callback (typing.Callable): Optional, if passed it is called on completion
113-
of each round of physics calculations.
114-
disable_emittance (bool): Whether or not to perform the beam
115-
envelope based emittance calculations.
175+
at_lattice (at.lattice_object.Lattice): An instance of an AT lattice object.
176+
sim_params (SimParams | None): An optional dataclass containing the pyAT
177+
simulation parameters to use.
178+
callback (typing.Callable): To be called after completion of each round of
179+
physics calculations.
116180
117181
**Methods:**
118182
"""
@@ -122,13 +186,16 @@ def __init__(self, at_lattice, callback=None, disable_emittance=False):
122186
)
123187
self._at_lat = at_lattice
124188
self._rp = numpy.ones(len(at_lattice) + 1, dtype=bool)
125-
self._disable_emittance = disable_emittance
126-
self._at_lat.radiation_on()
189+
190+
if sim_params is None:
191+
sim_params = SimParams()
192+
self._sim_params = sim_params
193+
194+
if self._sim_params.linopt == LinoptType.LINOPT6:
195+
self._at_lat.enable_6d()
127196

128197
# Initial phys data calculation.
129-
self._lattice_data = calculate_optics(
130-
self._at_lat, self._rp, self._disable_emittance
131-
)
198+
self._lattice_data = calculate_optics(self._at_lat, self._rp, self._sim_params)
132199

133200
# Threading stuff initialisation.
134201
self._queue = cothread.EventQueue()
@@ -196,7 +263,7 @@ def _recalculate_phys_data(self, callback):
196263
if bool(self._paused) is False:
197264
try:
198265
self._lattice_data = calculate_optics(
199-
self._at_lat, self._rp, self._disable_emittance
266+
self._at_lat, self._rp, self._sim_params
200267
)
201268
except Exception as e:
202269
warn(at.AtWarning(e), stacklevel=1)
@@ -493,7 +560,7 @@ def get_emittance(self, field=None):
493560
Raises:
494561
pytac.FieldException: if the specified field is not valid for emittance.
495562
"""
496-
if not self._disable_emittance:
563+
if self._sim_params.emittance:
497564
if field is None:
498565
return self._lattice_data.emittance[0]["emitXY"]
499566
elif field == "x":

src/atip/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ def load_at_lattice(mode="I04", **kwargs):
2929
return at_lattice
3030

3131

32-
def loader(mode="I04", callback=None, disable_emittance=False):
32+
def loader(
33+
mode="I04",
34+
sim_params=None,
35+
callback=None,
36+
):
3337
"""Load a unified lattice of the specifed mode.
3438
3539
.. Note:: A unified lattice is a Pytac lattice where the corresponding AT
@@ -38,9 +42,10 @@ def loader(mode="I04", callback=None, disable_emittance=False):
3842
3943
Args:
4044
mode (str): The lattice operation mode.
45+
sim_params (SimParams | None): An optional dataclass containing the pyAT
46+
simulation parameters to use.
4147
callback (typing.Callable): Callable to be called after completion of each
4248
round of physics calculations in ATSimulator.
43-
disable_emittance (bool): Whether the emittance should be calculated.
4449
4550
Returns:
4651
pytac.lattice.Lattice: A Pytac lattice object with the simulator data
@@ -52,7 +57,12 @@ def loader(mode="I04", callback=None, disable_emittance=False):
5257
periodicity=1,
5358
energy=pytac_lattice.get_value("energy", units=pytac.PHYS),
5459
)
55-
lattice = atip.load_sim.load(pytac_lattice, at_lattice, callback, disable_emittance)
60+
lattice = atip.load_sim.load(
61+
pytac_lattice,
62+
at_lattice,
63+
sim_params,
64+
callback,
65+
)
5666
return lattice
5767

5868

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def at_lattice(request):
8080
return atip.utils.load_at_lattice(request.param)
8181

8282

83-
@pytest.fixture(scope="function", params=["DIAD"])
83+
@pytest.fixture(scope="function", params=["I04"])
8484
def lattice_filepath(request):
8585
here = os.path.dirname(__file__)
8686
filepath = os.path.realpath(

0 commit comments

Comments
 (0)