Skip to content

Commit 028b253

Browse files
authored
Version 1.1 (#20)
* Updated Gitignore * Big Documentation Update * New doc test github action * Update sphinx packages * Update github action * Another githuh action fix * Update ETH AWG for new software * Added rise time to linear chirp pulses * F236 update * Updated F236 Interface * GaussPulse Improvements * Added tau to tune mode + bug fixes * Bug Fixes * Better NUS handling of parameter rounding * Removed div 0 warnings * Patch13 dev (#19) - Added `AmplifierLinearityAnalysis` class for characterizing amplifier non-linearity. - Added `T1InversionRecovery` sequence. - Added rise time to linear chirp pulses. - Added right based arithmatic. - Fixed Version Detection Bug - Fixed Dependency issues - Improved Documentation - Removed Numba dependency * Update doc dependecies * Updated main deps
1 parent e0834cb commit 028b253

26 files changed

Lines changed: 1211 additions & 195 deletions

docsrc/API_docs.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Sequences
3636
pyepr.sequences.CarrPurcellSequence
3737
pyepr.sequences.ResonatorProfileSequence
3838
pyepr.sequences.TWTProfileSequence
39+
pyepr.sequences.T1InversionRecoverySequence
3940

4041
Pulses
4142
~~~~~~
@@ -90,6 +91,7 @@ I/O
9091
pyepr.dataset.create_dataset_from_sequence
9192
pyepr.dataset.create_dataset_from_axes
9293
pyepr.dataset.create_dataset_from_bruker
94+
pyepr.dataset.downconvert_dataset
9395

9496
Utilities
9597
~~~~~~~~~

docsrc/conf.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from pyepr import __version__, __copyright__
1212
sys.path.insert(0, os.path.abspath('..'))
1313

14+
import matplotlib
15+
matplotlib.use('Agg') # Use non-interactive backend
1416

1517
project = 'PyEPR'
1618
copyright = __copyright__
@@ -30,7 +32,10 @@
3032
'sphinx_copybutton',
3133
'numpydoc',
3234
'sphinx_favicon',
33-
'sphinx_gallery.gen_gallery']
35+
'sphinx_gallery.gen_gallery',
36+
'matplotlib.sphinxext.plot_directive',
37+
'sphinx.ext.imgmath'
38+
]
3439

3540
templates_path = ['_templates']
3641
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
@@ -41,6 +46,10 @@
4146
autodoc_typehints = "description"
4247
autoapi_template_dir = "_templates/autoapi"
4348

49+
plot_include_source = True
50+
plot_html_show_source_code = False
51+
plot_formats = [('png', 100)]
52+
4453
autoapi_keep_files = True
4554
autoapi_add_toctree_entry = False
4655
autoapi_python_class_content= "both"

docsrc/install.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,4 @@ PyEPR requires:
3838
- h5netcdf
3939
- toml
4040
- deerlab (https://github.com/JeschkeLab/DeerLab)
41-
- numba
4241
- psutil

docsrc/releasenotes.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
Release Notes
22
=============
33

4+
Version 1.1 (2026-05-27):
5+
++++++++++++++++++++++++++++
6+
- Added `AmplifierLinearityAnalysis` class for characterizing amplifier non-linearity.
7+
- Added `T1InversionRecovery` sequence.
8+
- Added rise time to linear chirp pulses.
9+
- Added right based arithmatic.
10+
- Fixed Version Detection Bug
11+
- Fixed Dependency issues
12+
- Improved Documentation
13+
- Removed Numba dependency
14+
15+
16+
417
Version 1.0.0 (2025-09-12):
518
++++++++++++++++++++++++++++
619
- All references to `LO` have been changed to `freq` in the frequency object and related.

docsrc/tutorial_sequencer.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Pulse Sequencer
22

3-
PyEPR provides an intuitive object-oriented pulse programmer allowing the user to design pulsesequences in a hardware-agnostic manner. Additionally, several common EPR experiments are pre-defined and can be easily instantiated and modified.
3+
PyEPR provides an intuitive object-oriented pulse programmer allowing the user to design pulse sequences in a hardware-agnostic manner. Additionally, several common EPR experiments are pre-defined and can be easily instantiated and modified.
44

55
PyEPR uses ns, GHz and G as the default time, frequency and field units. Very occasionally, other units such as µs or MHz are used, in which case it will be explicitly mentioned.
66

@@ -31,33 +31,33 @@ These pulses will eventually need a scale (amplitude), before the sequence can b
3131
A Detection window is also created
3232
```python
3333
p90 = epr.RectPulse(tp=16,
34-
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
34+
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
3535
flipangle=np.pi/2, # Flip angle in degrees
3636
pcyc = {"phases":[0, np.pi], "dets":[1,-1]}
3737
)
3838
p180 = epr.RectPulse(tp=32,
39-
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
39+
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
4040
flipangle=np.pi, # Flip angle in degrees
4141
)
42-
det = epr.Detetction(tp=32,
43-
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
42+
det = epr.Detection(tp=32,
43+
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
4444
)
4545
```
4646
We now need a time axis for our sequence and to add them to the sequence object.
47-
When a pulse is copied into the sequence using the `add_pulse` method, parameters can be modified allowing the same pulse can be used multiple times with different timings or amplitudes.
47+
When a pulse is copied into the sequence using the `addPulse` method, parameters can be modified allowing the same pulse can be used multiple times with different timings or amplitudes.
4848
```python
4949
t = epr.Parameter(name='Interpulse Delay',
5050
value=400, # Initial interpulse delay in ns
5151
step=8, # Step size in ns
52-
dim=1024 # Number of points,
53-
unit='ns' # Unit of the parameter
52+
dim=1024, # Number of points,
53+
unit='ns', # Unit of the parameter
5454
description='Interpulse delay between the pi/2 and pi pulse'
5555
)
5656

5757
# Adding the pulses to the sequence
58-
seq.add_pulse(p90.copy(t=0))
59-
seq.add_pulse(p180.copy(t=t))
60-
seq.add_pulse(det.copy(t=2*t))
58+
seq.addPulse(p90.copy(t=0))
59+
seq.addPulse(p180.copy(t=t))
60+
seq.addPulse(det.copy(t=2*t))
6161

6262
# Defining the evolution
6363
seq.evolution([t])
@@ -81,7 +81,7 @@ HE_Seq = epr.HahnEchoRelaxationSequence(
8181
shots = 20, # Number of shots per point
8282
start = 400, # Initial interpulse delay in ns
8383
step = 8, # Step size in ns
84-
dim = 1024 # Number of points
84+
dim = 1024, # Number of points
8585
pi2_pulse = p90, # The 90 degree pulse
8686
pi_pulse = p180 # The 180 degree pulse
8787
)

pyepr/classes.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,19 @@ class Interface:
2323
"""
2424

2525
def __init__(self,config_file:dict=None,log=None) -> None:
26+
"""
27+
Parameters
28+
----------
29+
config_file : dict or str or Path, optional
30+
The configuration file or dict for the spectrometer interface, by default None. If None, a default configuration will be used.
31+
log : logging.Logger, optional
32+
The logger to be used, by default None. If None, a default logger will be created.
33+
"""
2634
if isinstance(config_file, (str,Path)):
2735
with open(config_file, 'r') as f:
2836
config_file = yaml.safe_load(f)
2937

30-
self.config = config_file if isinstance(config_file, dict) else {}
38+
self.config = config_file if isinstance(config_file, dict) else {"Spectrometer":{"Bridge":{}}}
3139

3240
self.pulses = {}
3341
self.savefolder = str(Path.home())
@@ -37,7 +45,10 @@ def __init__(self,config_file:dict=None,log=None) -> None:
3745
else:
3846
self.log = log
3947
self.resonator = None
40-
self.amp_nonlinearity = self.config["Spectrometer"]["Bridge"].get('Amplifier Non-Linearity',None)
48+
if self.config != {}:
49+
self.amp_nonlinearity = self.config["Spectrometer"]["Bridge"].get('Amplifier Non-Linearity',None)
50+
else:
51+
self.amp_nonlinearity = None
4152
pass
4253

4354
def connect(self) -> None:
@@ -153,7 +164,8 @@ def terminate_at(self, criterion, test_interval=2, keep_running=True, verbosity=
153164
data = self.acquire_dataset()
154165
if autosave:
155166
self.log.debug(f"Autosaving to {os.path.join(self.savefolder,self.savename)}")
156-
data.to_netcdf(os.path.join(self.savefolder,self.savename),engine='h5netcdf',invalid_netcdf=True)
167+
# data.to_netcdf(os.path.join(self.savefolder,self.savename),engine='h5netcdf',invalid_netcdf=True)
168+
data.epr.save(os.path.join(self.savefolder,self.savename))
157169

158170
try:
159171
# nAvgs = data.num_scans.value
@@ -278,7 +290,7 @@ def __init__(self, name, value, unit="", description="", virtual=False,
278290
self.value = value
279291
self.NUS = False # uniform sampling
280292
elif isinstance(value, np.ndarray):
281-
self.value = np.median(value)
293+
self.value = value[0]
282294
axis = value - self.value
283295
self.NUS = True # non-uniform sampling
284296
elif value is None:
@@ -382,17 +394,22 @@ def adjust_step(self, waveform_precision, keep_dim=True):
382394
current_step =old_axis[1] - old_axis[0]
383395
# test if uniformally sampled
384396
if not np.allclose(np.diff(self.axis[i]["axis"]), current_step):
385-
raise ValueError("This only works for uniformaly sampled data at the moment")
386-
new_step = round_step(current_step, waveform_precision)
387-
388-
if new_step == 0:
389-
new_step = waveform_precision
390-
391-
if keep_dim:
392-
dim = old_axis.shape[0]
393-
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][0]+new_step*dim, new_step)
397+
tolerance = 1e-9
398+
new_axis = copy.deepcopy(old_axis)
399+
remainders = np.abs(new_axis % waveform_precision)
400+
not_multiples = ~(np.isclose(remainders, 0, atol=1e-9) | np.isclose(remainders, waveform_precision, atol=1e-9))
401+
new_axis[not_multiples] = np.round(new_axis[not_multiples] / waveform_precision) * waveform_precision
394402
else:
395-
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][-1]+new_step, new_step)
403+
new_step = round_step(current_step, waveform_precision)
404+
405+
if new_step == 0:
406+
new_step = waveform_precision
407+
408+
if keep_dim:
409+
dim = old_axis.shape[0]
410+
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][0]+new_step*dim, new_step)
411+
else:
412+
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][-1]+new_step, new_step)
396413
self.axis[i]["axis"] = new_axis
397414

398415
if isinstance(self.value, numbers.Number):
@@ -496,6 +513,10 @@ def __add__(self, __o:object):
496513
raise RuntimeError(
497514
"Both parameters axis and the array must have the same shape")
498515

516+
def __radd__(self, __o:object):
517+
return self.__add__(__o)
518+
519+
499520
def __sub__(self, __o:object):
500521

501522
if type(__o) is Parameter:
@@ -564,6 +585,9 @@ def __sub__(self, __o:object):
564585
raise RuntimeError(
565586
"Both parameters axis and the array must have the same shape")
566587

588+
def __rsub__(self, __o:object):
589+
return self.__sub__(__o)
590+
567591
def __mul__(self, __o:object):
568592
if type(__o) is Parameter:
569593
if self.unit != __o.unit:

0 commit comments

Comments
 (0)