barsukov 1.3.3__tar.gz → 1.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of barsukov might be problematic. Click here for more details.
- {barsukov-1.3.3/src/barsukov.egg-info → barsukov-1.3.5}/PKG-INFO +6 -2
- {barsukov-1.3.3 → barsukov-1.3.5}/README.md +5 -1
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/__init__.py +1 -4
- barsukov-1.3.5/src/barsukov/data/Change_phase.py +160 -0
- barsukov-1.3.5/src/barsukov/data/Lock_in_emulator.py +175 -0
- barsukov-1.3.5/src/barsukov/data/__init__.py +6 -0
- barsukov-1.3.5/src/barsukov/data/constants.py +10 -0
- barsukov-1.3.5/src/barsukov/data/fft.py +132 -0
- barsukov-1.3.5/src/barsukov/data/lock_in_emulator_app.py +297 -0
- barsukov-1.3.5/src/barsukov/data/noise.py +276 -0
- barsukov-1.3.5/src/barsukov/exp/__init__.py +3 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/exp/mwHP.py +11 -2
- barsukov-1.3.5/src/barsukov/exp/smKE.py +148 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/logger.py +4 -3
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/script.py +16 -4
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/time.py +1 -1
- {barsukov-1.3.3 → barsukov-1.3.5/src/barsukov.egg-info}/PKG-INFO +6 -2
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov.egg-info/SOURCES.txt +7 -1
- barsukov-1.3.3/src/barsukov/data/__init__.py +0 -1
- barsukov-1.3.3/src/barsukov/data/fft.py +0 -87
- barsukov-1.3.3/src/barsukov/exp/__init__.py +0 -1
- {barsukov-1.3.3 → barsukov-1.3.5}/.github/workflows/versioning.yml +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/.gitignore +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/MANIFEST.in +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/pyproject.toml +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/setup.cfg +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/exp/exp_utils.py +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov/obj2file.py +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov.egg-info/dependency_links.txt +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov.egg-info/requires.txt +0 -0
- {barsukov-1.3.3 → barsukov-1.3.5}/src/barsukov.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: barsukov
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Experiment Automation Package
|
|
5
5
|
Author-email: Igor Barsukov <igorb@ucr.edu>, Steven Castaneda <scast206@ucr.edu>
|
|
6
6
|
Project-URL: Homepage, https://barsukov.ucr.edu
|
|
@@ -16,6 +16,10 @@ Requires-Dist: scipy>=0.9.0
|
|
|
16
16
|
|
|
17
17
|
Barsukov is a Python library for experiment automation.
|
|
18
18
|
|
|
19
|
+
## For Developers
|
|
20
|
+
|
|
21
|
+
To push to PyPi, commit to main on Github Desktop, click the history tab, right click and create tag, format the tag v\*.\*.\* and push to the repository (ctrl+p).
|
|
22
|
+
|
|
19
23
|
## Installation
|
|
20
24
|
|
|
21
25
|
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install barsukov.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Barsukov is a Python library for experiment automation.
|
|
4
4
|
|
|
5
|
+
## For Developers
|
|
6
|
+
|
|
7
|
+
To push to PyPi, commit to main on Github Desktop, click the history tab, right click and create tag, format the tag v\*.\*.\* and push to the repository (ctrl+p).
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install barsukov.
|
|
@@ -30,4 +34,4 @@ pip install barsukov
|
|
|
30
34
|
|
|
31
35
|
## License
|
|
32
36
|
|
|
33
|
-
[MIT](https://choosealicense.com/licenses/mit/)
|
|
37
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# Modules:
|
|
2
2
|
from . import time
|
|
3
3
|
from . import data
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
# Objects/Functions:
|
|
7
7
|
from .script import Script
|
|
8
8
|
from .logger import Logger
|
|
9
|
-
|
|
10
9
|
from .obj2file import *
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
# Equipment Objects:
|
|
14
13
|
from .exp.mwHP import mwHP
|
|
15
14
|
|
|
16
|
-
__all__ = ["time", "data", "save_object", "load_object", "Script", "Logger", "mwHP"]
|
|
17
|
-
|
|
18
15
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from barsukov.data.constants import deg2rad
|
|
2
|
+
from barsukov.time import time_stamp
|
|
3
|
+
|
|
4
|
+
from scipy.optimize import curve_fit
|
|
5
|
+
from scipy.optimize import differential_evolution
|
|
6
|
+
import numpy as np
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
|
|
9
|
+
import glob
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Change_phase:
|
|
15
|
+
### the phase you receive from auto, is a phase shift that you need to add to the phase of the original data.
|
|
16
|
+
### adding the two phases together gives to you the total effective lock-in phase of the calculated data.
|
|
17
|
+
### Note that lock-in phase corresponds to the reference, not to the signal itself.
|
|
18
|
+
### Lock-in phase is an artificial phase delay of the reference
|
|
19
|
+
### new reference is cos(Wt - phase)
|
|
20
|
+
### This script's phase, if added to the original phase of the lock-in, will give you a cumulative phase.
|
|
21
|
+
### The recalculated signal would correspond to lock-in signal if measured with this cumulative phase.
|
|
22
|
+
### This cumulative phase is the phase delay of your signal with respect to the original unaltered reference.
|
|
23
|
+
### The automatically recalculated data is correct only if considered together with the automatically calculated phase
|
|
24
|
+
### This means, you may get positive or negative signals in x-channel. So always consider the cummulative phase when evaluating the data.
|
|
25
|
+
|
|
26
|
+
def __init__(self, x=[], A=[], B=[], initial_phase=0):
|
|
27
|
+
self.x = np.array(x)
|
|
28
|
+
self.A = np.array(A)
|
|
29
|
+
self.B = np.array(B)
|
|
30
|
+
self.initial_phi = initial_phase
|
|
31
|
+
|
|
32
|
+
self.phi = initial_phase
|
|
33
|
+
self.newA = None
|
|
34
|
+
self.newB = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_from_file(self, full_file_path, x_column=0, A_column=1, B_column=2, initial_phase=0):
|
|
38
|
+
self.full_file_path = full_file_path
|
|
39
|
+
data = np.loadtxt(self.full_file_path, skiprows=0, unpack=True, usecols=(x_column, A_column, B_column))
|
|
40
|
+
self.x = data[0]
|
|
41
|
+
self.A = data[1]
|
|
42
|
+
self.B = data[2]
|
|
43
|
+
self.initial_phi = initial_phase
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def offset_phase(self, phi=None):
|
|
47
|
+
if phi is not None:
|
|
48
|
+
self.phi = float(phi)
|
|
49
|
+
self.newA = self.A * np.cos(self.phi*deg2rad) + self.B * np.sin(self.phi*deg2rad)
|
|
50
|
+
self.newB = - self.A * np.sin(self.phi*deg2rad) + self.B * np.cos(self.phi*deg2rad)
|
|
51
|
+
else:
|
|
52
|
+
def to_minimize(phi_val):
|
|
53
|
+
self.phi = phi_val[0] #Differential evolution passes arrays
|
|
54
|
+
self.newA = self.A * np.cos(self.phi*deg2rad) + self.B * np.sin(self.phi*deg2rad)
|
|
55
|
+
self.newB = - self.A * np.sin(self.phi*deg2rad) + self.B * np.cos(self.phi*deg2rad)
|
|
56
|
+
popt, pcov = curve_fit(lambda x,a,b: a+b*x, self.x, self.newB, p0=[0,0])
|
|
57
|
+
return 1-(pcov[0,1]/(pcov[0,0]*pcov[1,1]))**2
|
|
58
|
+
|
|
59
|
+
### MINIMIZES the sum of the data in the Y-channel (B)
|
|
60
|
+
result = differential_evolution(to_minimize, bounds=[(0,359.99)], strategy='best1bin')
|
|
61
|
+
if result.success:
|
|
62
|
+
self.offset_phase(phi=result.x[0])
|
|
63
|
+
print(f"Auto-adjusted phase: {result.x[0]} degrees")
|
|
64
|
+
else:
|
|
65
|
+
self.phi = initial_phase
|
|
66
|
+
print("Optimization failed!")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def plot_offset(self):
|
|
70
|
+
if not hasattr(self, "fig") or not hasattr(self, "axes"):
|
|
71
|
+
self.fig, self.axes = plt.subplots(nrows=4, ncols=1, figsize=(12,18))
|
|
72
|
+
|
|
73
|
+
self.lines = [
|
|
74
|
+
self.axes[0].plot(self.x, self.A ,'b-', label='X-')[0],
|
|
75
|
+
self.axes[0].plot(self.x, self.B, 'r-', label='Y-')[0],
|
|
76
|
+
self.axes[1].plot(self.x, self.newA, 'b-', label='X-')[0],
|
|
77
|
+
self.axes[2].plot(self.x, self.newB, 'r-', label='Y-')[0],
|
|
78
|
+
self.axes[3].plot(self.x, self.newA ,'b-', label='X-')[0],
|
|
79
|
+
self.axes[3].plot(self.x, self.newB, 'r-', label='Y-')[0],
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
self.axes[0].set_title('Original X- & Y-')
|
|
83
|
+
self.axes[1].set_title('Adjusted X-')
|
|
84
|
+
self.axes[2].set_title('Adjusted Y-')
|
|
85
|
+
self.axes[3].set_title('Adjusted X- & Y-')
|
|
86
|
+
|
|
87
|
+
for ax in self.axes:
|
|
88
|
+
ax.legend()
|
|
89
|
+
else:
|
|
90
|
+
for line in self.lines:
|
|
91
|
+
line.set_xdata(self.x)
|
|
92
|
+
|
|
93
|
+
self.lines[0].set_ydata(self.A)
|
|
94
|
+
self.lines[1].set_ydata(self.B)
|
|
95
|
+
self.lines[2].set_ydata(self.newA)
|
|
96
|
+
self.lines[3].set_ydata(self.newB)
|
|
97
|
+
self.lines[4].set_ydata(self.newA)
|
|
98
|
+
self.lines[5].set_ydata(self.newB)
|
|
99
|
+
|
|
100
|
+
for ax in self.axes:
|
|
101
|
+
ax.relim()
|
|
102
|
+
ax.autoscale_view()
|
|
103
|
+
|
|
104
|
+
if "IPython" in sys.modules:
|
|
105
|
+
from IPython.display import display
|
|
106
|
+
display(self.fig)
|
|
107
|
+
else:
|
|
108
|
+
plt.ion()
|
|
109
|
+
self.fig.canvas.draw()
|
|
110
|
+
self.fig.canvas.flush_events()
|
|
111
|
+
plt.show()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def save_data(self, full_folder_path=None, file_name=None):
|
|
115
|
+
if hasattr(self, "full_file_path"):
|
|
116
|
+
full_folder_path, file_name = os.path.split(self.full_file_path)
|
|
117
|
+
file_name = f"Corrected_{round(self.phi)}_{file_name}"
|
|
118
|
+
else:
|
|
119
|
+
if full_folder_path is None:
|
|
120
|
+
full_folder_path = os.getcwd()
|
|
121
|
+
if file_name is None:
|
|
122
|
+
file_name = f"{time_stamp()}_Corrected_Phase_Lock_in_Data"
|
|
123
|
+
|
|
124
|
+
full_folder_path = os.path.join(full_folder_path, 'phase-corrected_data')
|
|
125
|
+
if not os.path.isdir(full_folder_path):
|
|
126
|
+
os.makedirs(full_folder_path)
|
|
127
|
+
|
|
128
|
+
full_file_path = os.path.join(full_folder_path, file_name)
|
|
129
|
+
with open(full_file_path, "w") as file:
|
|
130
|
+
for i in range(len(self.x)):
|
|
131
|
+
file.write(f"{self.x[i]} {self.newA[i]} {self.newB[i]} \n")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def offset_phase_script(self, full_folder_path=None, x_column=0, A_column=1, B_column=2, initial_phase=0, nocheck=False):
|
|
135
|
+
if full_folder_path is None:
|
|
136
|
+
full_folder_path = os.getcwd()
|
|
137
|
+
for file in glob.glob(os.path.join(full_folder_path, '*.txt')):
|
|
138
|
+
self.read_from_file(file, x_column, A_column, B_column, initial_phase)
|
|
139
|
+
|
|
140
|
+
self.offset_phase()
|
|
141
|
+
self.plot_offset()
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
if nocheck is False:
|
|
145
|
+
manual_input=input('Enter "auto" to auto-calculate phase, a number/float for manual phase, or press ENTER to skip: ')
|
|
146
|
+
|
|
147
|
+
if manual_input == "auto":
|
|
148
|
+
self.offset_phase()
|
|
149
|
+
self.plot_offset()
|
|
150
|
+
elif manual_input:
|
|
151
|
+
try:
|
|
152
|
+
self.offset_phase(manual_input)
|
|
153
|
+
self.plot_offset()
|
|
154
|
+
except ValueError:
|
|
155
|
+
print(f"Invalid input: {manual_input}. Please enter a valid phase value or 'auto'.")
|
|
156
|
+
if not manual_input:
|
|
157
|
+
print("Phase adjustment completed.")
|
|
158
|
+
break
|
|
159
|
+
self.save_data()
|
|
160
|
+
print("Adjusted phase data saved.")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from barsukov.data import noise
|
|
3
|
+
|
|
4
|
+
import sympy as sp
|
|
5
|
+
|
|
6
|
+
class Lock_in_emulator:
|
|
7
|
+
def __init__(self, signal, f, phase, x_start, x_stop, x_amp, time, dt, TC, order, plot_points, buffer_size,
|
|
8
|
+
johnson_T=0, johnson_R=0,
|
|
9
|
+
shot_I=0, shot_R=0,
|
|
10
|
+
onef_rms=0,
|
|
11
|
+
rtn_tau_up=0, rtn_tau_down=0, rtn_state_up=0, rtn_state_down=0,
|
|
12
|
+
bit_depth=0, bit_measure_min=0, bit_measure_max=0):
|
|
13
|
+
|
|
14
|
+
#Signal Properties
|
|
15
|
+
self.signal_arr = signal
|
|
16
|
+
self.f = f
|
|
17
|
+
self.phase = np.pi * phase / 180
|
|
18
|
+
self.x_start = x_start
|
|
19
|
+
self.x_stop = x_stop
|
|
20
|
+
self.x_amp = x_amp
|
|
21
|
+
self.time = abs(time)
|
|
22
|
+
|
|
23
|
+
#Noise Properties
|
|
24
|
+
self.jT, self.jR = johnson_T, johnson_R
|
|
25
|
+
self.sI, self.sR = shot_I, shot_R
|
|
26
|
+
self.oRMS = onef_rms
|
|
27
|
+
self.rTU, self.rTD, self.rSU, self.rSD = rtn_tau_up, rtn_tau_down, rtn_state_up, rtn_state_down
|
|
28
|
+
self.bD, self.bMIN, self.bMAX = bit_depth, bit_measure_min, bit_measure_max
|
|
29
|
+
|
|
30
|
+
#Filter Properties
|
|
31
|
+
self.dt = abs(dt)
|
|
32
|
+
self.TC = TC
|
|
33
|
+
self.n = abs(order)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#Plotting Properties
|
|
37
|
+
self.plot_points = plot_points
|
|
38
|
+
self.buffer_size = buffer_size
|
|
39
|
+
self.buffer_period = self.buffer_size*self.dt
|
|
40
|
+
self.buffer_offset = np.linspace(-self.buffer_period, 0, self.buffer_size)
|
|
41
|
+
|
|
42
|
+
def run(self):
|
|
43
|
+
self.t_plot = np.linspace(self.buffer_period, self.time, self.plot_points)
|
|
44
|
+
self.x_plot = self.x_arr(self.t_plot)
|
|
45
|
+
|
|
46
|
+
self.original_signal = self.signal_arr(self.x_plot)
|
|
47
|
+
self.expected_signal = 0.5 * self.x_amp * np.gradient(self.original_signal, self.x_plot)
|
|
48
|
+
self.output_signal = self.signal_output_arr(self.t_plot)
|
|
49
|
+
|
|
50
|
+
self.fit()
|
|
51
|
+
#self.plot()
|
|
52
|
+
|
|
53
|
+
### BEGIN: FIELD
|
|
54
|
+
def x_arr(self, t_arr):
|
|
55
|
+
return self.x_start + t_arr * (self.x_stop - self.x_start) / self.time
|
|
56
|
+
|
|
57
|
+
def x_with_mod_arr(self, t_arr):
|
|
58
|
+
return self.x_arr(t_arr) + self.x_amp * np.cos(2 * np.pi * self.f * t_arr)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### BEGIN: NOISE
|
|
62
|
+
def noise(self, t_arr):
|
|
63
|
+
if self.jT and self.jR: self.johnson = noise.johnson(t_arr, self.jT, self.jR)
|
|
64
|
+
else: self.johnson = np.zeros(len(t_arr))
|
|
65
|
+
|
|
66
|
+
if self.sI and self.sR: self.shot = noise.shot(t_arr, self.sI, self.sR)
|
|
67
|
+
else: self.shot = np.zeros(len(t_arr))
|
|
68
|
+
|
|
69
|
+
if self.oRMS: self.onef = noise.color(t_arr, self.oRMS)
|
|
70
|
+
else: self.onef = np.zeros(len(t_arr))
|
|
71
|
+
|
|
72
|
+
if (self.rTU and self.rTD) or (self.rSU and self.rSD): self.rtn = noise.rtn(t_arr, self.rTU, self.rTD, self.rSU, self.rSD)
|
|
73
|
+
else: self.rtn = np.zeros(len(t_arr))
|
|
74
|
+
return self.johnson + self.shot + self.onef + self.rtn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def lp_filter_arr(self, t_arr):
|
|
78
|
+
t_arr = -(t_arr - t_arr[-1])
|
|
79
|
+
factorial = np.math.factorial(self.n - 1)
|
|
80
|
+
lp_filter_arr = (t_arr ** (self.n - 1)) * np.exp(-t_arr / self.TC) / (self.TC**self.n * factorial)
|
|
81
|
+
return lp_filter_arr / abs(np.sum(lp_filter_arr) * self.dt)
|
|
82
|
+
|
|
83
|
+
def signal_output(self, t):
|
|
84
|
+
t_arr = t + self.buffer_offset
|
|
85
|
+
|
|
86
|
+
x_arr = self.x_with_mod_arr(t_arr)
|
|
87
|
+
s_arr = self.signal_arr(x_arr)
|
|
88
|
+
noise_arr = self.noise(t_arr)
|
|
89
|
+
filter_arr = self.lp_filter_arr(t_arr)
|
|
90
|
+
ref_X = np.cos(2 * np.pi * self.f * t_arr - self.phase)
|
|
91
|
+
|
|
92
|
+
integrand = (s_arr+noise_arr) * ref_X * filter_arr * self.dt
|
|
93
|
+
return np.sum(integrand)
|
|
94
|
+
|
|
95
|
+
def signal_output_arr(self, t_arr):
|
|
96
|
+
output = np.array([self.signal_output(t) for t in t_arr])
|
|
97
|
+
|
|
98
|
+
if self.bD or (self.bMIN and self.bMAX):
|
|
99
|
+
return noise.bit(output, self.bD, self.bMIN, self.bMAX)
|
|
100
|
+
else:
|
|
101
|
+
return output
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### BEGIN: FIT:
|
|
105
|
+
def fit(self):
|
|
106
|
+
from scipy.interpolate import interp1d
|
|
107
|
+
from scipy.optimize import curve_fit
|
|
108
|
+
|
|
109
|
+
interp = interp1d(self.x_plot, self.expected_signal, kind='cubic', fill_value=0.0, bounds_error=False)
|
|
110
|
+
|
|
111
|
+
def model(x, diminish, stretch, shift):
|
|
112
|
+
x_trans = (x / stretch) - shift
|
|
113
|
+
return (1.0 / diminish) * interp(x_trans)
|
|
114
|
+
|
|
115
|
+
expected_max_idx, expected_min_idx = np.argmax(self.expected_signal), np.argmin(self.expected_signal)
|
|
116
|
+
output_max_idx, output_min_idx = np.argmax(self.output_signal), np.argmin(self.output_signal)
|
|
117
|
+
|
|
118
|
+
expected_peak_xdif = np.abs(self.x_plot[expected_max_idx] - self.x_plot[expected_min_idx])
|
|
119
|
+
output_peak_xdif = np.abs(self.x_plot[output_max_idx] - self.x_plot[output_min_idx])
|
|
120
|
+
|
|
121
|
+
initial_stretch = np.abs(output_peak_xdif / expected_peak_xdif)
|
|
122
|
+
initial_diminish = np.ptp(self.expected_signal) / np.ptp(self.output_signal)
|
|
123
|
+
|
|
124
|
+
initial_transform = model(self.x_plot, initial_diminish, initial_stretch, 0)
|
|
125
|
+
transform_max_idx = np.argmax(initial_transform)
|
|
126
|
+
|
|
127
|
+
initial_shift = (self.x_plot[output_max_idx] - self.x_plot[transform_max_idx]) / initial_stretch
|
|
128
|
+
|
|
129
|
+
initial_guess = [np.max([1,initial_diminish]), np.max([1,initial_stretch]), np.max([-np.ptp(self.x_plot),initial_shift])]
|
|
130
|
+
|
|
131
|
+
bounds = [(1, 1, -np.ptp(self.x_plot)), (10, 0.5*np.ptp(self.x_plot), np.ptp(self.x_plot) )]
|
|
132
|
+
popt, _ = curve_fit(model, self.x_plot, self.output_signal, sigma=1e-4, p0=initial_guess, bounds=bounds, method='trf')
|
|
133
|
+
|
|
134
|
+
#print(f"initial diminish: {initial_diminish}\ninitial stretch: {initial_stretch}\ninitial shift: {initial_shift}")
|
|
135
|
+
self.diminish = popt[0]
|
|
136
|
+
self.stretch = popt[1]
|
|
137
|
+
self.shift = popt[2]
|
|
138
|
+
self.adjusted_signal = model(self.x_plot, self.diminish, self.stretch, self.shift)
|
|
139
|
+
|
|
140
|
+
#SNR Calculation
|
|
141
|
+
p2p = np.max(self.adjusted_signal) - np.min(self.adjusted_signal)
|
|
142
|
+
noise_sample = self.output_signal[int(len(self.adjusted_signal) * 0.9):] #last 10% of the output signal
|
|
143
|
+
v_rms = np.sqrt(np.mean(noise_sample**2))
|
|
144
|
+
|
|
145
|
+
self.snr = p2p / v_rms
|
|
146
|
+
|
|
147
|
+
def plot(self):
|
|
148
|
+
import matplotlib.pyplot as plt
|
|
149
|
+
|
|
150
|
+
print(f'diminish: {self.diminish}')
|
|
151
|
+
print(f'stretch: {self.stretch}')
|
|
152
|
+
print(f'shift: {self.shift}')
|
|
153
|
+
print(f'Signal to Noise Ratio: {self.snr}')
|
|
154
|
+
|
|
155
|
+
self.fig, self.axes = plt.subplots(nrows=2, ncols=1, figsize=(12,18))
|
|
156
|
+
|
|
157
|
+
self.lines = [
|
|
158
|
+
self.axes[0].plot(self.x_plot, self.original_signal, 'r-', label='Original Signal')[0],
|
|
159
|
+
self.axes[1].plot(self.x_plot, self.output_signal, 'b-', label='Demodulated Signal (Lock-In)')[0],
|
|
160
|
+
self.axes[1].plot(self.x_plot, self.expected_signal, 'r-', label='Demodulated Signal (Expected)')[0],
|
|
161
|
+
self.axes[1].plot(self.x_plot, self.adjusted_signal, 'g-', label=f'Demodulated Signal (Adjusted)\n Diminish: {self.diminish}\n Stretch:{self.stretch}\n Shift: {self.shift}')[0],
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
self.axes[0].set_title('Original Signal vs x')
|
|
165
|
+
self.axes[1].set_title('Demodulated Signal vs x')
|
|
166
|
+
|
|
167
|
+
plt.legend()
|
|
168
|
+
plt.show()
|
|
169
|
+
|
|
170
|
+
def spectrum_average(self, num):
|
|
171
|
+
result = np.zeros_like(self.t_plot)
|
|
172
|
+
for i in range(0, num):
|
|
173
|
+
result += self.signal_output_arr(self.t_plot)
|
|
174
|
+
|
|
175
|
+
return result / num
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fft(x, y, equidistant_check=True, equidistant_rel_error=1e-4, remove_negative_f=False, mathematica_convention=False, inverse=False):
|
|
5
|
+
"""
|
|
6
|
+
Perform Fast Fourier Transform (FFT) or Inverse FFT on one-dimensional or multi-dimensional data,
|
|
7
|
+
with optional handling for non-equidistant sampling, mandatory normalization to continuous Fourier definition.
|
|
8
|
+
Allows for +- in exp for scientific and engineering definition for Fourier Transform.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
x (array_like):
|
|
13
|
+
The time or spatial domain axis (1D array). Must be increasing, doesn't need to be equidistant.
|
|
14
|
+
y (array_like):
|
|
15
|
+
The signal to be transformed. Can be 1D or 2D array (if 2D, the FFT is applied along axis 1).
|
|
16
|
+
equidistant_check (bool, optional):
|
|
17
|
+
If True (default), checks whether `x` is uniformly spaced. If not, interpolates to uniform spacing.
|
|
18
|
+
equidistant_rel_error : float, optional
|
|
19
|
+
Relative tolerance for equidistant spacing check. Default is 1e-4.
|
|
20
|
+
remove_negative_f : bool, optional
|
|
21
|
+
If True, removes negative frequencies from output. Default is False.
|
|
22
|
+
mathematica_convention : bool, optional
|
|
23
|
+
If True, uses Mathematica-style conventions: forward Fourier ~ exp(i2pift).
|
|
24
|
+
If False (default), uses NumPy convention: forward Fourier ~ exp(-i2pift)
|
|
25
|
+
inverse : bool, optional
|
|
26
|
+
If True, computes the inverse FFT. Default is False (forward transform).
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
fft_x : ndarray
|
|
31
|
+
Ordered frequency domain axis corresponding to the transformed data (negative frequencies can be removed if remove_negative_f is True).
|
|
32
|
+
Ordered time domain axis if inverse is True (time axis starts at 0).
|
|
33
|
+
fft_y : ndarray
|
|
34
|
+
Transformed signal. Shape matches `y`.
|
|
35
|
+
|
|
36
|
+
Notes
|
|
37
|
+
-----
|
|
38
|
+
- The function supports both real and complex-valued input `y`.
|
|
39
|
+
- If `x` is not equidistant and `equidistant_check=True`, the function interpolates `y` using `make_equidistant`.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> t = np.linspace(0, 1, 1000)
|
|
44
|
+
>>> y = np.sin(2 * np.pi * 50 * t)
|
|
45
|
+
>>> f, Y = fft(t, y)
|
|
46
|
+
|
|
47
|
+
>>> # Inverse transform
|
|
48
|
+
>>> t_rec, y_rec = fft(f, Y, inverse=True)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
x, y = np.array(x), np.array(y)
|
|
52
|
+
|
|
53
|
+
# Handle non-equidistant input
|
|
54
|
+
if equidistant_check:
|
|
55
|
+
diffs = np.diff(x)
|
|
56
|
+
if not np.allclose(diffs, diffs[0], rtol=equidistant_rel_error):
|
|
57
|
+
# x is not equidistant, must start interpolating
|
|
58
|
+
x, y = make_equidistant(x, y, step=None)
|
|
59
|
+
|
|
60
|
+
# Determine shape and axis
|
|
61
|
+
if y.ndim == 1:
|
|
62
|
+
n, axis = len(y), -1
|
|
63
|
+
else:
|
|
64
|
+
n, axis = y.shape[1], 1
|
|
65
|
+
|
|
66
|
+
#Determine Convention
|
|
67
|
+
if mathematica_convention is False:
|
|
68
|
+
fft_func, fft_norm = np.fft.fft, "backward"
|
|
69
|
+
ifft_func, ifft_norm = np.fft.ifft, "forward"
|
|
70
|
+
else:
|
|
71
|
+
fft_func, fft_norm = np.fft.ifft, "forward"
|
|
72
|
+
ifft_func, ifft_norm = np.fft.fft, "backward"
|
|
73
|
+
|
|
74
|
+
# Compute FFT and normalize
|
|
75
|
+
sample_spacing = (x[-1] - x[0]) / (n-1.0)
|
|
76
|
+
|
|
77
|
+
if inverse is True:
|
|
78
|
+
y = np.fft.ifftshift(y, axes=axis) # Reorder y to match np.fft.ifft convention
|
|
79
|
+
fft_x = np.arange(0, n) / (n * sample_spacing) # Time domain creation
|
|
80
|
+
fft_y = ifft_func(y, axis=axis, norm=ifft_norm) * sample_spacing # Sample Spacing is the Fourier consistent normalization
|
|
81
|
+
else:
|
|
82
|
+
fft_x = np.fft.fftfreq(n, d=sample_spacing) # Frequency domain creation
|
|
83
|
+
fft_y = fft_func(y, axis=axis, norm=fft_norm) * sample_spacing
|
|
84
|
+
fft_x, fft_y = np.fft.fftshift(fft_x), np.fft.fftshift(fft_y, axes=axis) # Reorder x, y for plotting
|
|
85
|
+
|
|
86
|
+
# Remove negative frequencies
|
|
87
|
+
if remove_negative_f is True:
|
|
88
|
+
mask = fft_x >= 0
|
|
89
|
+
fft_x = fft_x[mask]
|
|
90
|
+
if y.ndim == 1:
|
|
91
|
+
fft_y = fft_y[mask]
|
|
92
|
+
else:
|
|
93
|
+
fft_y = fft_y[:, mask]
|
|
94
|
+
|
|
95
|
+
return fft_x, fft_y
|
|
96
|
+
|
|
97
|
+
# NOTES For Developer:
|
|
98
|
+
# Mathematica has convention exp(i*2pi*t)
|
|
99
|
+
# np.fft.fft takes x input -3,-2,-1,0,1,2,3 and np.fft.ifft takes x input 0,1,2,3,-3,-2,-1
|
|
100
|
+
# Our fft(np.fft.ifft) takes x input -3,-2,-1,0,1,2,3 and our ifft(np.fft.fft) takes x input 0,1,2,3,-3,-2,-1
|
|
101
|
+
# Numpy outputs in 0,1,2,3,-3,-2,-1 order, our function sorts to -3,-2,-1,0,1,2,3
|
|
102
|
+
|
|
103
|
+
#Tested with Lorentzian, Gaussian, and Rectangular Signals
|
|
104
|
+
|
|
105
|
+
def ifft(x, y, equidistant_check=True, equidistant_rel_error=1e-4, remove_negative_f=False, mathematica_convention=False):
|
|
106
|
+
return fft(x, y, equidistant_check=equidistant_check, equidistant_rel_error=equidistant_rel_error, remove_negative_f=remove_negative_f, mathematica_convention=mathematica_convention, inverse=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def make_equidistant(x, y, step=None):
|
|
111
|
+
import scipy.interpolate as sp
|
|
112
|
+
### Takes one column x and one or more columns y and makes them equidistant in x
|
|
113
|
+
### Returns new_x, new_y. The number of points will likely change.
|
|
114
|
+
if step is None:
|
|
115
|
+
# Calculate the smallest difference between consecutive elements
|
|
116
|
+
min_step = np.min(np.diff(x))
|
|
117
|
+
else:
|
|
118
|
+
min_step = step
|
|
119
|
+
|
|
120
|
+
# Generate the new equidistant x array
|
|
121
|
+
new_x = np.arange(x[0], x[-1] + min_step, min_step)
|
|
122
|
+
|
|
123
|
+
if isinstance(y[0], (list, np.ndarray)): # If y contains multiple columns
|
|
124
|
+
new_y = []
|
|
125
|
+
for y_column in y:
|
|
126
|
+
interpolation_function = sp.interpolate.interp1d(x, y_column, kind='linear', fill_value='extrapolate')
|
|
127
|
+
new_y.append(interpolation_function(new_x))
|
|
128
|
+
else: # If y is a single column
|
|
129
|
+
interpolation_function = sp.interpolate.interp1d(x, y, kind='linear', fill_value='extrapolate')
|
|
130
|
+
new_y = interpolation_function(new_x)
|
|
131
|
+
|
|
132
|
+
return np.array(new_x), np.array(new_y)
|