-
Notifications
You must be signed in to change notification settings - Fork 34
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
VariableMultipole
compatibility between matlab AT and pyat
#839
base: master
Are you sure you want to change the base?
Changes from 15 commits
17219e7
84d3bf8
ce697be
7c3949d
51db2ce
f6dae67
6ced948
7a12dc4
05c0e82
aa71888
032c9da
e19c8f3
ad441ba
9e62915
59aeee3
58d703b
5115496
d899d08
93f6bb5
97854d8
7c405e2
b531d4e
3ffa2f7
79351f2
07fc985
aedf3ab
7c4361a
0f55260
fb889ae
c089301
9d1412c
aa1d905
5630fa5
59adb54
6d88ad6
9d80e37
67fb8dc
c7197dd
4168268
a272b79
be0f0a4
a1045d7
a87c6c7
39c37d6
2d07122
d11fb99
e0ee739
9227f95
6a219b4
d05627a
a4c66f1
287bb37
731b52b
74555bd
9a804f2
680efdb
e1308ea
ae9b0e1
a524815
f84c550
c0de957
4f13031
1cd90d8
13f1cad
94d5837
68ff29e
0a6e2f5
ed6be2c
8f1e7f3
93ba719
40bf18b
aa00a4f
5f704f7
cfb48be
8da79fb
5aa0b0e
c631ae8
7f5f574
3716abf
5fecb70
d0cb8f0
acfb812
1fbfcfb
b47392d
956d32e
8b64d11
e10b71c
eabf92e
19e0560
2b637d3
8a6a266
d4a2e66
a1ecaa0
6573a21
7544283
8605f7b
cd27c42
4b1812a
01b2a66
02ee269
02a7bc0
3642a67
8c0ea73
6de4a39
3d54be8
8cbcd97
0054f6e
bf9b7ef
4084069
02dad6f
e25a0f1
f5a341b
df259e9
f08edc0
368ad62
b97d73d
8181d19
07c1290
f059f00
d03522b
3774656
500b0a9
d0fcb9b
0c94dda
01f09d9
9024245
ec7da17
bcd928b
5c9d75e
cddca24
ef840e5
9b2280e
2f7549d
49e70ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,85 @@ | ||
import numpy | ||
from __future__ import annotations | ||
|
||
from datetime import datetime | ||
from enum import IntEnum | ||
|
||
import numpy as np | ||
|
||
from .elements import Element, _array | ||
from .utils import AtError | ||
from typing import Optional, Union | ||
from enum import IntEnum | ||
from datetime import datetime | ||
|
||
|
||
class ACMode(IntEnum): | ||
"""Class to define the excitation types""" | ||
"""Class to define the excitation types.""" | ||
|
||
SINE = 0 | ||
WHITENOISE = 1 | ||
ARBITRARY = 2 | ||
|
||
|
||
class VariableMultipole(Element): | ||
"""Class to generate an AT variable thin multipole element | ||
""" | ||
"""Class to generate an AT variable thin multipole element.""" | ||
|
||
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES | ||
_conversions = dict(Element._conversions, | ||
Mode=int, | ||
AmplitudeA=_array, AmplitudeB=_array, | ||
FrequencyA=float, FrequencyB=float, | ||
PhaseA=float, PhaseB=float, | ||
Seed=int, NSamplesA=int, NSamplesB=int, | ||
FuncA=_array, FuncB=_array, Ramps=_array, | ||
Periodic=bool) | ||
|
||
def __init__(self, family_name, AmplitudeA=None, AmplitudeB=None, | ||
mode=ACMode.SINE, **kwargs): | ||
_conversions = dict( | ||
Element._conversions, | ||
Mode=int, | ||
AmplitudeA=_array, | ||
AmplitudeB=_array, | ||
FrequencyA=float, | ||
FrequencyB=float, | ||
PhaseA=float, | ||
PhaseB=float, | ||
Seed=int, | ||
NSamplesA=int, | ||
NSamplesB=int, | ||
FuncA=_array, | ||
FuncB=_array, | ||
Ramps=_array, | ||
Periodic=bool, | ||
) | ||
|
||
def __init__(self, family_name: str, **kwargs: dict[str, any]): | ||
# noinspection PyUnresolvedReferences | ||
r""" | ||
Create VariableMultipole. | ||
|
||
Parameters: | ||
family_name(str): Element name | ||
|
||
Keyword Arguments: | ||
AmplitudeA(list,float): Amplitude of the excitation for PolynomA. Default None | ||
AmplitudeB(list,float): Amplitude of the excitation for PolynomB. Default None | ||
AmplitudeA(list,float): Amplitude of the excitation for PolynomA. | ||
Default None | ||
AmplitudeB(list,float): Amplitude of the excitation for PolynomB. | ||
Default None | ||
mode(ACMode): defines the evaluation grid. Default ACMode.SINE | ||
|
||
* :py:attr:`.ACMode.SINE`: sine function | ||
* :py:attr:`.ACMode.WHITENOISE`: gaussian white noise | ||
* :py:attr:`.GridMode.ARBITRARY`: user defined turn-by-turn kick list | ||
* :py:attr:`.ACMode.ARBITRARY`: user defined turn-by-turn kick list | ||
FrequencyA(float): Frequency of the sine excitation for PolynomA | ||
FrequencyB(float): Frequency of the sine excitation for PolynomB | ||
PhaseA(float): Phase of the sine excitation for PolynomA. Default 0 | ||
PhaseB(float): Phase of the sine excitation for PolynomB. Default 0 | ||
MaxOrder(int): Order of the multipole for scalar amplitude. Default 0 | ||
It is overwritten with the length of Amplitude(A,B) | ||
Seed(int): Seed of the random number generator for white | ||
noise excitation. Default datetime.now() | ||
FuncA(list): User defined tbt kick list for PolynomA | ||
FuncB(list): User defined tbt kick list for PolynomB | ||
Periodic(bool): If True (default) the user defined kick is repeated | ||
Ramps(list): Vector (t0, t1, t2, t3) in turn number to define the ramping | ||
of the excitation | ||
Ramps(list): Vector (t0, t1, t2, t3) in turn number to define | ||
the ramping of the excitation | ||
|
||
* ``t<t0``: excitation amplitude is zero | ||
* ``t0<t<t1``: exciation amplitude is linearly ramped up | ||
* ``t1<t<t2``: exciation amplitude is constant | ||
* ``t2<t<t3``: exciation amplitude is linearly ramped down | ||
* ``t3<t``: exciation amplitude is zero | ||
* ``t0<t<t1``: excitation amplitude is linearly ramped up | ||
* ``t1<t<t2``: excitation amplitude is constant | ||
* ``t2<t<t3``: excitation amplitude is linearly ramped down | ||
* ``t3<t``: excitation amplitude is zero | ||
Raises: | ||
AtError if none of AmplitudeA or AmplitudeB is passed. | ||
AtError if ramp is not vector of length 4 when using Ramps | ||
AtError when Frequency is not defined if using Mode ``ACMode.SINE`` | ||
AtError when Funct is not defined if using Mode ``ACMode.ARBITRARY`` | ||
|
||
|
||
Examples: | ||
|
||
|
@@ -68,74 +89,103 @@ def __init__(self, family_name, AmplitudeA=None, AmplitudeB=None, | |
|
||
.. note:: | ||
|
||
* For all excitation modes all least one amplitude has to be provided. | ||
* If no parameters are given it will be initialized as IdentityPass. | ||
* For all excitation modes at least one amplitudes (A or B) has | ||
to be provided. | ||
The default excitation is ``ACMode.SINE`` | ||
* For ``mode=ACMode.SINE`` the ``Frequency(A,B)`` corresponding to the | ||
``Amplitude(A,B)`` has to be provided | ||
``Amplitude(A,B)`` has to be provided. | ||
* For ``mode=ACMode.ARBITRARY`` the ``Func(A,B)`` corresponding to the | ||
``Amplitude(A,B)`` has to be provided | ||
``Amplitude(A,B)`` has to be provided. | ||
""" | ||
kwargs.setdefault('PassMethod', 'VariableThinMPolePass') | ||
self.MaxOrder = kwargs.pop('MaxOrder', 0) | ||
self.Periodic = kwargs.pop('Periodic', True) | ||
self.Mode = int(mode) | ||
if AmplitudeA is None and AmplitudeB is None: | ||
raise AtError('Please provide at least one amplitude for A or B') | ||
AmplitudeB = self._set_params(AmplitudeB, mode, 'B', **kwargs) | ||
AmplitudeA = self._set_params(AmplitudeA, mode, 'A', **kwargs) | ||
self._setmaxorder(AmplitudeA, AmplitudeB) | ||
if mode == ACMode.WHITENOISE: | ||
self.Seed = kwargs.pop('Seed', datetime.now().timestamp()) | ||
self.PolynomA = numpy.zeros(self.MaxOrder+1) | ||
self.PolynomB = numpy.zeros(self.MaxOrder+1) | ||
ramps = kwargs.pop('Ramps', None) | ||
if ramps is not None: | ||
assert len(ramps)==4, \ | ||
'Ramps has to be a vector with 4 elements' | ||
self.Ramps = ramps | ||
super(VariableMultipole, self).__init__(family_name, **kwargs) | ||
|
||
def _setmaxorder(self, ampa, ampb): | ||
if len(kwargs) > 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was needed in order to separate the case when the element is called inside pyat, therefore the object already exists and there are no kwargs, from the case when the element is instantiated using a dictionary (the dictionary comming from reading a .mat, or from user settings). Sometimes the object is read, for example when printing in a terminal, the element is passed again (I guess this happens because it inherits |
||
self.FamName = family_name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. family_name = kwargs.pop('FamName', family_name) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I explained before, but, when the element comes from AT matlab, a dictionary is created but all BUILD_ATTRIBUTES are stripped. 'FamName' is a a BUILD_ATTRIBUTE of the class element and therefore it is not in kwargs. One has to take it from args. |
||
if "AmplitudeA" not in kwargs and "AmplitudeB" not in kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AmplitudeA/B présence in kwargs also needs to be checked There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry @swhite2401 , I don't understand. What you say is implemented in line 103 above. |
||
raise AtError("Please provide at least one amplitude for A or B") | ||
# start setting up Amplitudes and modes | ||
# fist modes are called differently | ||
modepyatinput = kwargs.pop("mode", ACMode.SINE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could replace all these lines by: Self.Mode = kwargs.pop('Mode', int(mode)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I cannot. |
||
modefromdict = kwargs.pop("Mode", None) | ||
if modefromdict is not None: | ||
mode = modefromdict # matlab class | ||
else: | ||
mode = modepyatinput | ||
self.Mode = int(mode) | ||
# MaxOrder is later overwritten by lenth of amplitudes | ||
self.MaxOrder = kwargs.get("MaxOrder", 0) | ||
amplitudea = None | ||
amplitudeb = None | ||
if "AmplitudeA" in kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This lol ne and the ones below can be removed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean by lol ? |
||
amplitudea = kwargs.pop("AmplitudeA", None) | ||
amplitudea = self._set_params(amplitudea, mode, "A", **kwargs) | ||
if "AmplitudeB" in kwargs: | ||
amplitudeb = kwargs.pop("AmplitudeB", None) | ||
amplitudeb = self._set_params(amplitudeb, mode, "B", **kwargs) | ||
if amplitudea is not None: | ||
self.AmplitudeA = amplitudea | ||
if amplitudeb is not None: | ||
self.AmplitudeB = amplitudeb | ||
# end setting up Amplitudes and modes | ||
kwargs.setdefault("PassMethod", "VariableThinMPolePass") | ||
# this overwrites MaxOrder | ||
self._setmaxorder(amplitudea, amplitudeb) | ||
if mode == ACMode.WHITENOISE and "Seed" not in kwargs: | ||
kwargs.update({"Seed": datetime.now().timestamp()}) | ||
self.Periodic = kwargs.pop("Periodic", True) | ||
self.PolynomA = kwargs.pop("PolynomA", np.zeros(self.MaxOrder + 1)) | ||
self.PolynomB = kwargs.pop("PolynomB", np.zeros(self.MaxOrder + 1)) | ||
# check ramps | ||
ramps = kwargs.pop("Ramps", None) | ||
if ramps is not None: | ||
if len(ramps) != 4: | ||
raise AtError("Ramps has to be a vector with 4 elements") | ||
self.Ramps = ramps | ||
# fill in super class | ||
super().__init__(family_name, **kwargs) | ||
|
||
def _setmaxorder(self, ampa: np.ndarray, ampb: np.ndarray): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In what cases did this end up to be none? When ampa/b was filled with only zeros? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have to repeat the test but suppose you have the current implementation available in the master. You will have this behaviour with zeros. >>> numpy.max(numpy.nonzero([0,0,0,0]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/oblanco/Documents/public/progs/pyenv/pyenvat/lib/python3.11/site-packages/numpy/core/fromnumeric.py", line 2810, in max
return _wrapreduction(a, np.maximum, 'max', axis, None, out,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/oblanco/Documents/public/progs/pyenv/pyenvat/lib/python3.11/site-packages/numpy/core/fromnumeric.py", line 88, in _wrapreduction
return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zero-size array to reduction operation maximum which has no identity I changed to |
||
mxa, mxb = 0, 0 | ||
if ampa is not None: | ||
mxa = numpy.max(numpy.nonzero(ampa)) | ||
mxa = np.max(np.append(np.nonzero(ampa), 0)) | ||
if ampb is not None: | ||
mxb = numpy.max(numpy.nonzero(ampb)) | ||
mxb = np.max(np.append(np.nonzero(ampb), 0)) | ||
self.MaxOrder = max(mxa, mxb) | ||
if ampa is not None: | ||
delta = self.MaxOrder - len(ampa) | ||
if delta > 0: | ||
ampa = numpy.pad(ampa, (0, delta)) | ||
setattr(self, 'AmplitudeA', ampa) | ||
ampa = np.pad(ampa, (0, delta)) | ||
self.AmplitudeA = ampa | ||
if ampb is not None: | ||
delta = self.MaxOrder + 1 - len(ampb) | ||
if delta > 0: | ||
ampb = numpy.pad(ampb, (0, delta)) | ||
setattr(self, 'AmplitudeB', ampb) | ||
ampb = np.pad(ampb, (0, delta)) | ||
self.AmplitudeB = ampb | ||
|
||
def _set_params(self, amplitude, mode, ab, **kwargs): | ||
def _set_params( | ||
self, amplitude: int or str, mode, a_b: str, **kwargs: dict[str, any] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to define the type of kwargs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is flake8 who suggested. Should I ignore it ? Do you work with some custom flake8 settings for pyat development ? |
||
): | ||
if amplitude is not None: | ||
if numpy.isscalar(amplitude): | ||
amp = numpy.zeros(self.MaxOrder) | ||
amplitude = numpy.append(amp, amplitude) | ||
if np.isscalar(amplitude): | ||
amp = np.zeros(self.MaxOrder) | ||
amplitude = np.append(amp, amplitude) | ||
if mode == ACMode.SINE: | ||
self._set_sine(ab, **kwargs) | ||
self._set_sine(a_b, **kwargs) | ||
if mode == ACMode.ARBITRARY: | ||
self._set_arb(ab, **kwargs) | ||
self._set_arb(a_b, **kwargs) | ||
return amplitude | ||
|
||
def _set_sine(self, ab, **kwargs): | ||
frequency = kwargs.pop('Frequency' + ab, None) | ||
phase = kwargs.pop('Phase' + ab, 0) | ||
assert frequency is not None, \ | ||
'Please provide a value for Frequency' + ab | ||
setattr(self, 'Frequency' + ab, frequency) | ||
setattr(self, 'Phase' + ab, phase) | ||
def _set_sine(self, a_b: str, **kwargs: dict[str, any]): | ||
frequency = kwargs.pop("Frequency" + a_b, None) | ||
phase = kwargs.pop("Phase" + a_b, 0) | ||
if frequency is None: | ||
raise AtError("Please provide a value for Frequency" + a_b) | ||
setattr(self, "Frequency" + a_b, frequency) | ||
setattr(self, "Phase" + a_b, phase) | ||
|
||
def _set_arb(self, ab, **kwargs): | ||
func = kwargs.pop('Func' + ab, None) | ||
def _set_arb(self, a_b: str, **kwargs: dict[str, any]): | ||
func = kwargs.pop("Func" + a_b, None) | ||
nsamp = len(func) | ||
assert func is not None, \ | ||
'Please provide a value for Func' + ab | ||
setattr(self, 'Func' + ab, func) | ||
setattr(self, 'NSamples' + ab, nsamp) | ||
if func is None: | ||
raise AtError("Please provide a value for Func" + a_b) | ||
setattr(self, "Func" + a_b, func) | ||
setattr(self, "NSamples" + a_b, nsamp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to specify the type of kwargs this is standard python
Could you please restore the defaults for AmplitudeA/B and the mode? I think it is better if they appear in the API doc, in fact I would be tempted to add more of these keyword arguments rather than removing them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dear @swhite2401 , if I remove the annotation I get the following flake8 warning. This is the reason why I included, should I ignore it ? I may be using flake8 with a wrong configuration or I may use another tool ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dear @swhite2401 , the reason why I remove it is because of an incompatibility between AT matlab and this implementation of the element in pyat.
If I save a lattice in matlab and try to read it in pyat I get the error here below, I'll try to explain the reason in detail, but, in few words: one cannot have args and kwargs with the same name
When reading an element from matlab it is initially put in a dictionary
elem_dict
, and then the_BUILD_ATTRIBUTES
are stripped to pass them to the class instantiation aselem_args
. See the lines belowat/pyat/at/load/utils.py
Lines 368 to 369 in 268994f
I could not put the AmplutudeA, AmplitudeB and Mode in the element BUILD_ATTRIBUTES because they are not garanteed to exist in matlab, see the following line
at/atmat/lattice/element_creation/atvariablemultipole.m
Line 1 in 268994f
therefore, the call to cls with args in necessarily incomplete and the AmplitudeA, AmplitudeB and Mode are passed in a dictionary together with args that have the same name. Python does not handle this mix well, it removes the elements from the kwargs and leaves unset the args.
There are ways to workaround this situation when args and kwargs have the same name, but they require changing the way the class is instantiated in utils.py. I decided not to do it because that will change how all elements are instantiated.
A second possibility was to rename the element args in pyat, for example amplitudeA instead of AmplitudeA i.e. lower case in the first letter to make one small change. However, this would break compatibility.
I decided to keep the element without any requirement as in AT matlab, which forced me to do checks on the input parameters before assigning them inside
__init__
. This wouldn't break compatibility with previous versions of pyat, and AT matlab files could be read because the element also has no requirements except for the FamName.If you prefer, I could do as you asked. Keep the args and break backwards compatibility. Or force the user to provide AmplitudeA, AmplitudeB and Mode in matlab, which will make possible to include them in BUILD_ATTRIBUTES that will be removed in the input dictionary and set before the class is instantiated.
Or maybe there is another solution ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @oscarxblanco , thanks for the detailed answers! My personnal opinion is that this was poorly implemented to start with (which is my fault in fact, sorry...) and that in such situation we should be allowed to break backward compatibility.
In all other pyAT element, args seem to have in fact different name that kwargs and we should probably stick to that convention to keep things simple. Similarly we could modify MATLAB to preserve compatibility with the new pyAT version.
This feature is relatively recent and it could well be that @ZeusMarti is the only user... we have used it at ESRF but I don't think we have saved lattice files including it. Would it be a big effort on your side to switch to a new signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dear @swhite2401 , I asked @ZeusMarti and he was not able to use it effectively because he intends to have a custom function where the time of the particle is also considered, i.e. not only a kick every turn for all particles.
I have started modifying the C code today to make some test, is this feature something you would accept for this element ? Or should I keep the custom function option as it is ?
o
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! In fact I developed this for @ZeusMarti initially, so any improvement is perfectly fine.
You can already do a kick every n turns by using the table by the way, just fill zeros where you do not want any kick.
Do you want help for this?