barsukov 1.3.3__py3-none-any.whl → 1.3.5__py3-none-any.whl

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/__init__.py CHANGED
@@ -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
barsukov/data/__init__.py CHANGED
@@ -1 +1,6 @@
1
+ from . import constants
2
+ from . import noise
1
3
  from .fft import *
4
+ from .Change_phase import Change_phase
5
+ from .Lock_in_emulator import Lock_in_emulator
6
+ from . import lock_in_emulator_app
@@ -0,0 +1,10 @@
1
+ import numpy as np
2
+
3
+
4
+ ### Conversions:
5
+ deg2rad=2*np.pi/360.0
6
+
7
+
8
+ ### Constants:
9
+ K_b = 1.380649e-23 #Boltzmann constant
10
+ q = 1.602176634e-19 # Elementary Charge
barsukov/data/fft.py CHANGED
@@ -1,68 +1,114 @@
1
- ### BEGIN Dependencies ###
2
1
  import numpy as np
3
- import scipy as sp
4
- from barsukov.logger import debug # Un-comment before implementing in pip
5
- ### END Dependencies ###
6
2
 
7
3
 
8
- def fft(x, y, equidistant_check=True, equidistant_rel_error=1e-4, remove_negative_f=False, inverse=False):
9
- ### Takes: x=list of real floats, y=list of data or list of lists of data. Data can be real or complex.
10
- ### Returns: freqs, fft_y_norm, msg
11
- ### This fft used to give wrong sign of the imaginary component because of the "wrong" definition of fft in np and sp
12
- ### Now it has been corrected to the Mathematica definition of fft = int ... exp(2pi f t)
13
- msg = ''
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
14
54
  if equidistant_check:
15
55
  diffs = np.diff(x)
16
56
  if not np.allclose(diffs, diffs[0], rtol=equidistant_rel_error):
17
57
  # x is not equidistant, must start interpolating
18
- x,y = make_equidistant(x, y, step=None)
19
- y = y.T
20
- msg += debug('fft(x,y) made x,y equidistant.')
21
-
22
- y = np.array(y)
23
- #print(y)
58
+ x, y = make_equidistant(x, y, step=None)
24
59
 
60
+ # Determine shape and axis
25
61
  if y.ndim == 1:
26
- # y is 1D, treat it as a single column
27
- n = len(y) # Number of points in the column
28
- if inverse is False: fft_y = np.fft.ifft(y) * n # np fft has the "wrong" imag sign
29
- else: fft_y = np.fft.fft(y) # That's why fft and ifft are inverted in this code
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"
30
70
  else:
31
- # y is 2D, treat it as multiple columns
32
- n = y.shape[1] # Number of points in each column
33
- if inverse is False: fft_y = np.fft.ifft(y, axis=1) * n # np fft has the "wrong" imag sign
34
- else: fft_y = np.fft.fft(y, axis=1) # That's why fft and ifft are inverted in this code
35
-
36
- sample_spacing = ( x[-1] - x[0] ) / (n-1)
37
- #print(n, sample_spacing, x[1] - x[0])
38
- fft_y_norm = fft_y * sample_spacing # This normalizes FFT to mathematically correct
39
- freqs = np.fft.fftfreq(n, d=sample_spacing)
40
-
41
- if remove_negative_f is False:
42
- sorted_indices = np.argsort(freqs)
43
- freqs = freqs[sorted_indices]
44
- if isinstance(fft_y_norm[0], (list, np.ndarray)): # If fft_y_norm contains multiple columns
45
- fft_y_norm = [x[sorted_indices] for x in fft_y_norm]
46
- else: # If fft_y_norm is a single column
47
- fft_y_norm = fft_y_norm[sorted_indices]
48
- msg += debug('fft(x,y) sorted negative and positive frequencies.')
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
49
81
  else:
50
- mask = freqs >= 0 # Boolean array with Falses for negative frequencies, effectively removing them
51
- freqs = freqs[mask]
52
- # If fft_y_norm contains multiple columns:
53
- if isinstance(fft_y_norm[0], (list, np.ndarray)):
54
- fft_y_norm = [x[mask] for x in fft_y_norm]
55
- else: # If fft_y_norm is a single column
56
- fft_y_norm = fft_y_norm[mask]
57
- msg += debug('fft(x,y) removed negative frequencies.')
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)
58
107
 
59
- msg += debug('freqs, fft_y_norm, msg = fft(x,y) is done.\nThe forward fft approximates the mathematically correct integral over ...exp(+i2pift).\nNow do not forget to apply np.abs(fft_y_norm), np.angle(fft_y_norm), fft_y_norm.real, fft_y_norm.imag')
60
- return freqs, fft_y_norm, msg
61
108
 
62
- def ifft(x, y, equidistant_check=True, equidistant_rel_error=1e-4, remove_negative_f=False):
63
- return fft(x, y, equidistant_check=equidistant_check, equidistant_rel_error=equidistant_rel_error, remove_negative_f=remove_negative_f, inverse=True)
64
109
 
65
110
  def make_equidistant(x, y, step=None):
111
+ import scipy.interpolate as sp
66
112
  ### Takes one column x and one or more columns y and makes them equidistant in x
67
113
  ### Returns new_x, new_y. The number of points will likely change.
68
114
  if step is None:
@@ -70,18 +116,17 @@ def make_equidistant(x, y, step=None):
70
116
  min_step = np.min(np.diff(x))
71
117
  else:
72
118
  min_step = step
73
-
119
+
74
120
  # Generate the new equidistant x array
75
121
  new_x = np.arange(x[0], x[-1] + min_step, min_step)
76
-
122
+
77
123
  if isinstance(y[0], (list, np.ndarray)): # If y contains multiple columns
78
124
  new_y = []
79
125
  for y_column in y:
80
126
  interpolation_function = sp.interpolate.interp1d(x, y_column, kind='linear', fill_value='extrapolate')
81
127
  new_y.append(interpolation_function(new_x))
82
- new_y = np.array(new_y).T # Transpose to match the original structure
83
128
  else: # If y is a single column
84
129
  interpolation_function = sp.interpolate.interp1d(x, y, kind='linear', fill_value='extrapolate')
85
130
  new_y = interpolation_function(new_x)
86
-
87
- return new_x, new_y
131
+
132
+ return np.array(new_x), np.array(new_y)