Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docsrc/API_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Sequences
pyepr.sequences.CarrPurcellSequence
pyepr.sequences.ResonatorProfileSequence
pyepr.sequences.TWTProfileSequence
pyepr.sequences.T1InversionRecoverySequence

Pulses
~~~~~~
Expand Down Expand Up @@ -90,6 +91,7 @@ I/O
pyepr.dataset.create_dataset_from_sequence
pyepr.dataset.create_dataset_from_axes
pyepr.dataset.create_dataset_from_bruker
pyepr.dataset.downconvert_dataset

Utilities
~~~~~~~~~
Expand Down
11 changes: 10 additions & 1 deletion docsrc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from pyepr import __version__, __copyright__
sys.path.insert(0, os.path.abspath('..'))

import matplotlib
matplotlib.use('Agg') # Use non-interactive backend

project = 'PyEPR'
copyright = __copyright__
Expand All @@ -30,7 +32,10 @@
'sphinx_copybutton',
'numpydoc',
'sphinx_favicon',
'sphinx_gallery.gen_gallery']
'sphinx_gallery.gen_gallery',
'matplotlib.sphinxext.plot_directive',
'sphinx.ext.imgmath'
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
Expand All @@ -41,6 +46,10 @@
autodoc_typehints = "description"
autoapi_template_dir = "_templates/autoapi"

plot_include_source = True
plot_html_show_source_code = False
plot_formats = [('png', 100)]

autoapi_keep_files = True
autoapi_add_toctree_entry = False
autoapi_python_class_content= "both"
Expand Down
1 change: 0 additions & 1 deletion docsrc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ PyEPR requires:
- h5netcdf
- toml
- deerlab (https://github.com/JeschkeLab/DeerLab)
- numba
- psutil
13 changes: 13 additions & 0 deletions docsrc/releasenotes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Release Notes
=============

Version 1.1 (2026-05-27):
++++++++++++++++++++++++++++
- 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



Version 1.0.0 (2025-09-12):
++++++++++++++++++++++++++++
- All references to `LO` have been changed to `freq` in the frequency object and related.
Expand Down
24 changes: 12 additions & 12 deletions docsrc/tutorial_sequencer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pulse Sequencer

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.
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.

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.

Expand Down Expand Up @@ -31,33 +31,33 @@ These pulses will eventually need a scale (amplitude), before the sequence can b
A Detection window is also created
```python
p90 = epr.RectPulse(tp=16,
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
flipangle=np.pi/2, # Flip angle in degrees
pcyc = {"phases":[0, np.pi], "dets":[1,-1]}
)
p180 = epr.RectPulse(tp=32,
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
flipangle=np.pi, # Flip angle in degrees
)
det = epr.Detetction(tp=32,
freq=0, # Frequency offset in MHz, w.r.t the sequence frequency,
det = epr.Detection(tp=32,
freq=0, # Frequency offset in GHz, w.r.t the sequence frequency,
)
```
We now need a time axis for our sequence and to add them to the sequence object.
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.
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.
```python
t = epr.Parameter(name='Interpulse Delay',
value=400, # Initial interpulse delay in ns
step=8, # Step size in ns
dim=1024 # Number of points,
unit='ns' # Unit of the parameter
dim=1024, # Number of points,
unit='ns', # Unit of the parameter
description='Interpulse delay between the pi/2 and pi pulse'
)

# Adding the pulses to the sequence
seq.add_pulse(p90.copy(t=0))
seq.add_pulse(p180.copy(t=t))
seq.add_pulse(det.copy(t=2*t))
seq.addPulse(p90.copy(t=0))
seq.addPulse(p180.copy(t=t))
seq.addPulse(det.copy(t=2*t))

# Defining the evolution
seq.evolution([t])
Expand All @@ -81,7 +81,7 @@ HE_Seq = epr.HahnEchoRelaxationSequence(
shots = 20, # Number of shots per point
start = 400, # Initial interpulse delay in ns
step = 8, # Step size in ns
dim = 1024 # Number of points
dim = 1024, # Number of points
pi2_pulse = p90, # The 90 degree pulse
pi_pulse = p180 # The 180 degree pulse
)
Expand Down
52 changes: 38 additions & 14 deletions pyepr/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ class Interface:
"""

def __init__(self,config_file:dict=None,log=None) -> None:
"""
Parameters
----------
config_file : dict or str or Path, optional
The configuration file or dict for the spectrometer interface, by default None. If None, a default configuration will be used.
log : logging.Logger, optional
The logger to be used, by default None. If None, a default logger will be created.
"""
if isinstance(config_file, (str,Path)):
with open(config_file, 'r') as f:
config_file = yaml.safe_load(f)

self.config = config_file if isinstance(config_file, dict) else {}
self.config = config_file if isinstance(config_file, dict) else {"Spectrometer":{"Bridge":{}}}

self.pulses = {}
self.savefolder = str(Path.home())
Expand All @@ -37,7 +45,10 @@ def __init__(self,config_file:dict=None,log=None) -> None:
else:
self.log = log
self.resonator = None
self.amp_nonlinearity = self.config["Spectrometer"]["Bridge"].get('Amplifier Non-Linearity',None)
if self.config != {}:
self.amp_nonlinearity = self.config["Spectrometer"]["Bridge"].get('Amplifier Non-Linearity',None)
else:
self.amp_nonlinearity = None
pass

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

try:
# nAvgs = data.num_scans.value
Expand Down Expand Up @@ -278,7 +290,7 @@ def __init__(self, name, value, unit="", description="", virtual=False,
self.value = value
self.NUS = False # uniform sampling
elif isinstance(value, np.ndarray):
self.value = np.median(value)
self.value = value[0]
axis = value - self.value
self.NUS = True # non-uniform sampling
elif value is None:
Expand Down Expand Up @@ -382,17 +394,22 @@ def adjust_step(self, waveform_precision, keep_dim=True):
current_step =old_axis[1] - old_axis[0]
# test if uniformally sampled
if not np.allclose(np.diff(self.axis[i]["axis"]), current_step):
raise ValueError("This only works for uniformaly sampled data at the moment")
new_step = round_step(current_step, waveform_precision)

if new_step == 0:
new_step = waveform_precision

if keep_dim:
dim = old_axis.shape[0]
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][0]+new_step*dim, new_step)
tolerance = 1e-9
new_axis = copy.deepcopy(old_axis)
remainders = np.abs(new_axis % waveform_precision)
not_multiples = ~(np.isclose(remainders, 0, atol=1e-9) | np.isclose(remainders, waveform_precision, atol=1e-9))
new_axis[not_multiples] = np.round(new_axis[not_multiples] / waveform_precision) * waveform_precision
else:
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][-1]+new_step, new_step)
new_step = round_step(current_step, waveform_precision)

if new_step == 0:
new_step = waveform_precision

if keep_dim:
dim = old_axis.shape[0]
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][0]+new_step*dim, new_step)
else:
new_axis = np.arange(self.axis[i]["axis"][0], self.axis[i]["axis"][-1]+new_step, new_step)
self.axis[i]["axis"] = new_axis

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

def __radd__(self, __o:object):
return self.__add__(__o)


def __sub__(self, __o:object):

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

def __rsub__(self, __o:object):
return self.__sub__(__o)

def __mul__(self, __o:object):
if type(__o) is Parameter:
if self.unit != __o.unit:
Expand Down
Loading
Loading