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 +1 -4
- barsukov/data/Change_phase.py +160 -0
- barsukov/data/Lock_in_emulator.py +175 -0
- barsukov/data/__init__.py +5 -0
- barsukov/data/constants.py +10 -0
- barsukov/data/fft.py +100 -55
- barsukov/data/lock_in_emulator_app.py +297 -0
- barsukov/data/noise.py +276 -0
- barsukov/exp/__init__.py +2 -0
- barsukov/exp/mwHP.py +11 -2
- barsukov/exp/smKE.py +148 -0
- barsukov/logger.py +4 -3
- barsukov/script.py +16 -4
- barsukov/time.py +1 -1
- {barsukov-1.3.3.dist-info → barsukov-1.3.5.dist-info}/METADATA +6 -2
- barsukov-1.3.5.dist-info/RECORD +20 -0
- {barsukov-1.3.3.dist-info → barsukov-1.3.5.dist-info}/WHEEL +1 -1
- barsukov-1.3.3.dist-info/RECORD +0 -14
- {barsukov-1.3.3.dist-info → barsukov-1.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from PyQt5 import QtWidgets
|
|
2
|
+
import pyqtgraph as pg
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from barsukov.data import Lock_in_emulator, noise
|
|
7
|
+
|
|
8
|
+
def make_lorentzian(center, HW, amp):
|
|
9
|
+
def l(x):
|
|
10
|
+
return amp / ((x - center)**2 + HW**2)
|
|
11
|
+
return l
|
|
12
|
+
|
|
13
|
+
def make_antilorentzian(center, HW, amp):
|
|
14
|
+
def al(x):
|
|
15
|
+
return amp * (x - center) / np.abs(HW) / (1 + ( (x - center) / HW)**2)
|
|
16
|
+
return al
|
|
17
|
+
|
|
18
|
+
params = [
|
|
19
|
+
("l_center", 2, "Center:", float),
|
|
20
|
+
("l_HW", 1, "HW:", float),
|
|
21
|
+
("l_amp", 1e-6, "Amplitude:", float),
|
|
22
|
+
("al_center", 4, "Center:", float),
|
|
23
|
+
("al_HW", 2, "HW:", float),
|
|
24
|
+
("al_amp", 2e-6, "Amplitude:", float),
|
|
25
|
+
("jT", 300, "Temperature (K):", float),
|
|
26
|
+
("jR", 200, "Resistance (Ohms):", float),
|
|
27
|
+
("sI", 1e-3, "Current (Amps):", float),
|
|
28
|
+
("sR", 200, "Resistance (Ohms):", float),
|
|
29
|
+
("oRMS", 1e-7, "RMS:", float),
|
|
30
|
+
("rTU", 0.01, "Tau Up (s):", float),
|
|
31
|
+
("rTD", 0.01, "Tau Down (s):", float),
|
|
32
|
+
("rSU", 1e-7, "State Up:", float),
|
|
33
|
+
("rSD", 1e-7, "State Down:", float),
|
|
34
|
+
("bD", 64, "Bit Depth (bits):", int),
|
|
35
|
+
("bMIN", -21, "Minimum Measurement:", float),
|
|
36
|
+
("bMAX", 21, "Maximum Measurement:", float),
|
|
37
|
+
("xstart_input", 0, "X Start:", float),
|
|
38
|
+
("plotpoints_input", 500, "# Plot Points:", int),
|
|
39
|
+
("time_input", 120, "Sweep Time (s):", float),
|
|
40
|
+
("xstop_input", 10, "X Stop:", float),
|
|
41
|
+
("xamp_input", 0.2, "Modulation Amp:", float),
|
|
42
|
+
("f_input", 1000, "Modulation Freq (Hz):", float),
|
|
43
|
+
("TC_input", 500e-3, "Time Constant (s):", float),
|
|
44
|
+
("order_input", 4, "Filter Order:", int),
|
|
45
|
+
("dt_input", 1e-4, "Sampling Step (s):", float),
|
|
46
|
+
("buffersize_input", 10000, "Buffer Size:", int),
|
|
47
|
+
("phase_input", 0, "Phase Offset (deg):", float),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
class MyWindow(QtWidgets.QWidget):
|
|
51
|
+
def __init__(self):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.setWindowTitle("Lock-in Amplifier Emulator")
|
|
54
|
+
self.resize(1000, 800)
|
|
55
|
+
|
|
56
|
+
#Window Area (horizontal)
|
|
57
|
+
main_layout = QtWidgets.QHBoxLayout(self)
|
|
58
|
+
self.inputs = {} # (widget, cast, method)
|
|
59
|
+
self.line_edits = {}
|
|
60
|
+
|
|
61
|
+
#Interactive Input Box Setup
|
|
62
|
+
for name, default, label, cast in params:
|
|
63
|
+
le = QtWidgets.QLineEdit(str(default))
|
|
64
|
+
#le.editingFinished.connect(self.update_plot)
|
|
65
|
+
self.line_edits[name] = le
|
|
66
|
+
self.inputs[name] = (le, lambda w=le, c=cast: c(w.text()))
|
|
67
|
+
|
|
68
|
+
#Left Area:
|
|
69
|
+
left_layout = QtWidgets.QVBoxLayout()
|
|
70
|
+
main_layout.addLayout(left_layout, 1)
|
|
71
|
+
|
|
72
|
+
# Signal Type Selector
|
|
73
|
+
signal_combo = QtWidgets.QComboBox()
|
|
74
|
+
signal_combo.addItems(["Lorentzian", "Anti-Lorentzian"])
|
|
75
|
+
#signal_combo.currentIndexChanged.connect(self.update_plot)
|
|
76
|
+
self.inputs["signal_type"] = (signal_combo, lambda w: str(w.currentText()))
|
|
77
|
+
left_layout.addWidget(QtWidgets.QLabel("<b>Signal Type:</b>"))
|
|
78
|
+
left_layout.addWidget(signal_combo)
|
|
79
|
+
|
|
80
|
+
# Signal Inputs Stack
|
|
81
|
+
signal_stack = QtWidgets.QStackedWidget()
|
|
82
|
+
left_layout.addWidget(signal_stack)
|
|
83
|
+
|
|
84
|
+
#Lorentzian Inputs
|
|
85
|
+
lorentz_widget = QtWidgets.QWidget()
|
|
86
|
+
lorentz_layout = QtWidgets.QFormLayout(lorentz_widget)
|
|
87
|
+
lorentz_layout.addRow("<b>Center:</b>", self.line_edits["l_center"])
|
|
88
|
+
lorentz_layout.addRow("<b>HW:</b>", self.line_edits["l_HW"])
|
|
89
|
+
lorentz_layout.addRow("<b>Amplitude:</b>", self.line_edits["l_amp"])
|
|
90
|
+
signal_stack.addWidget(lorentz_widget)
|
|
91
|
+
|
|
92
|
+
#Guassian Inputs
|
|
93
|
+
antilorentz_widget = QtWidgets.QWidget()
|
|
94
|
+
antilorentz_layout = QtWidgets.QFormLayout(antilorentz_widget)
|
|
95
|
+
antilorentz_layout.addRow("<b>Center:</b>", self.line_edits["al_center"])
|
|
96
|
+
antilorentz_layout.addRow("<b>HW:</b>", self.line_edits["al_HW"])
|
|
97
|
+
antilorentz_layout.addRow("<b>Amplitude:</b>", self.line_edits["al_amp"])
|
|
98
|
+
signal_stack.addWidget(antilorentz_widget)
|
|
99
|
+
|
|
100
|
+
signal_combo.currentIndexChanged.connect(signal_stack.setCurrentIndex)
|
|
101
|
+
|
|
102
|
+
#Noise Options:
|
|
103
|
+
left_layout.addWidget(QtWidgets.QLabel("<b>Noise Options:</b>"))
|
|
104
|
+
|
|
105
|
+
noises = [
|
|
106
|
+
("Johnson Noise", "Temperature (K):,jT", "Resistance (Ohms):,jR"),
|
|
107
|
+
("Shot Noise", "Current (Amps):,sI", "Resistance (Ohms):,sR"),
|
|
108
|
+
("1/f Noise", "RMS:,oRMS"),
|
|
109
|
+
("Random Telegraph Noise", "Tau Up (s):,rTU", "Tau Down (s):,rTD", "State Up:,rSU", "State Down:,rSD"),
|
|
110
|
+
("Bit Noise", "Bit Depth (bits):,bD", "Minimum Measurement:,bMIN", "Maximum Measurement:,bMAX")
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for noise in noises:
|
|
114
|
+
#Noise Checkbox
|
|
115
|
+
cb = QtWidgets.QCheckBox(noise[0])
|
|
116
|
+
self.inputs[noise[0]] = (cb, lambda w=cb: w.isChecked())
|
|
117
|
+
left_layout.addWidget(cb)
|
|
118
|
+
|
|
119
|
+
# Group of Noise Inputs
|
|
120
|
+
group = QtWidgets.QGroupBox(noise[0] + " Settings")
|
|
121
|
+
group.setCheckable(False)
|
|
122
|
+
group.setVisible(False)
|
|
123
|
+
form = QtWidgets.QFormLayout(group)
|
|
124
|
+
for p in noise[1:]:
|
|
125
|
+
label, name = p.split(",")
|
|
126
|
+
form.addRow("<b>"+label+"</b>", self.line_edits[name])
|
|
127
|
+
left_layout.addWidget(group)
|
|
128
|
+
|
|
129
|
+
cb.toggled.connect(group.setVisible)
|
|
130
|
+
#cb.toggled.connect(self.update_plot)
|
|
131
|
+
|
|
132
|
+
left_layout.addStretch(1)
|
|
133
|
+
|
|
134
|
+
# Simulate Button
|
|
135
|
+
simulate_button = QtWidgets.QPushButton("Simulate")
|
|
136
|
+
simulate_button.clicked.connect(self.update_plot)
|
|
137
|
+
left_layout.addWidget(simulate_button)
|
|
138
|
+
|
|
139
|
+
# Reset Button
|
|
140
|
+
reset_button = QtWidgets.QPushButton("Reset")
|
|
141
|
+
reset_button.clicked.connect(self.reset_fields)
|
|
142
|
+
left_layout.addWidget(reset_button)
|
|
143
|
+
|
|
144
|
+
# User Notes
|
|
145
|
+
left_layout.addWidget(QtWidgets.QLabel("<b>User Notes:</b>"))
|
|
146
|
+
user_notes = QtWidgets.QLabel()
|
|
147
|
+
user_notes.setText("- SweepTime ≥ SamplingStep*BufferSize*#PlotPoints\n"
|
|
148
|
+
"- Runs well up to 50 million calculations\n"
|
|
149
|
+
" EX: 500 PlotPoints * 100000 BufferSize")
|
|
150
|
+
left_layout.addWidget(user_notes)
|
|
151
|
+
|
|
152
|
+
#Right Area (vertical) - right side of window
|
|
153
|
+
right_layout = QtWidgets.QVBoxLayout()
|
|
154
|
+
main_layout.addLayout(right_layout, 4)
|
|
155
|
+
|
|
156
|
+
#Graph 1:
|
|
157
|
+
plot1 = pg.PlotWidget(title="Original Signal vs X", background='w')
|
|
158
|
+
legend1 = plot1.addLegend()
|
|
159
|
+
legend1.anchor((1,0), (1,0))
|
|
160
|
+
right_layout.addWidget(plot1)
|
|
161
|
+
|
|
162
|
+
#Input Area 1:
|
|
163
|
+
input_layout1 = QtWidgets.QGridLayout()
|
|
164
|
+
cols = 4
|
|
165
|
+
for i, p in enumerate(params[18:22]):
|
|
166
|
+
name, label = p[0], p[2]
|
|
167
|
+
row = i // cols
|
|
168
|
+
col = i % cols
|
|
169
|
+
|
|
170
|
+
h_layout = QtWidgets.QHBoxLayout()
|
|
171
|
+
h_layout.addWidget(QtWidgets.QLabel("<b>"+label+"</b>"))
|
|
172
|
+
h_layout.addWidget(self.line_edits[name])
|
|
173
|
+
input_layout1.addLayout(h_layout, row, col)
|
|
174
|
+
right_layout.addLayout(input_layout1)
|
|
175
|
+
|
|
176
|
+
#Fit Results:
|
|
177
|
+
result_layout = QtWidgets.QHBoxLayout()
|
|
178
|
+
self.result_text = QtWidgets.QLabel()
|
|
179
|
+
result_layout.addWidget(self.result_text)
|
|
180
|
+
result_layout.addStretch(1)
|
|
181
|
+
|
|
182
|
+
#Label:
|
|
183
|
+
output2expected = QtWidgets.QLabel()
|
|
184
|
+
output2expected.setText("Output ≈ <sup>1</sup>⁄<sub>Diminish</sub> * Expected(<sup>x</sup>⁄<sub>Stretch</sub> - Shift)")
|
|
185
|
+
right_layout.addWidget(output2expected)
|
|
186
|
+
|
|
187
|
+
#Graph 2:
|
|
188
|
+
plot2 = pg.PlotWidget(title="Demodulated Signal vs X", background='w')
|
|
189
|
+
legend2 = plot2.addLegend()
|
|
190
|
+
legend2.anchor((1,0), (1,0))
|
|
191
|
+
right_layout.addWidget(plot2)
|
|
192
|
+
|
|
193
|
+
#Input Area 2:
|
|
194
|
+
input_layout2 = QtWidgets.QGridLayout()
|
|
195
|
+
for i, p in enumerate(params[22:]):
|
|
196
|
+
name, label = p[0], p[2]
|
|
197
|
+
row = i // cols
|
|
198
|
+
col = i % cols
|
|
199
|
+
|
|
200
|
+
h_layout = QtWidgets.QHBoxLayout()
|
|
201
|
+
h_layout.addWidget(QtWidgets.QLabel("<b>"+label+"</b>"))
|
|
202
|
+
h_layout.addWidget(self.line_edits[name])
|
|
203
|
+
input_layout2.addLayout(h_layout, row, col)
|
|
204
|
+
right_layout.addLayout(input_layout2)
|
|
205
|
+
|
|
206
|
+
#Plot Curve Initialization:
|
|
207
|
+
self.curve_orig = plot1.plot(pen=pg.mkPen(color='r', width=2), name="Original Signal")
|
|
208
|
+
self.curve_out = plot2.plot(pen=pg.mkPen(color='b', width=2), name="Output Signal")
|
|
209
|
+
self.curve_expected = plot2.plot(pen=pg.mkPen(color='r', width=2), name="Expected Signal")
|
|
210
|
+
self.curve_adjusted = plot2.plot(pen=pg.mkPen(color='g', width=2), name="Adjusted Signal")
|
|
211
|
+
|
|
212
|
+
#Show Curves Options:
|
|
213
|
+
curve_params = [ ("Output", self.curve_out), ("Expected", self.curve_expected), ("Adjusted", self.curve_adjusted) ]
|
|
214
|
+
for name, curve in curve_params:
|
|
215
|
+
cb = QtWidgets.QCheckBox(f"Show {name} Signal")
|
|
216
|
+
cb.setChecked(True)
|
|
217
|
+
cb.stateChanged.connect(lambda state, c=curve, box=cb: c.setVisible(box.isChecked()))
|
|
218
|
+
result_layout.addWidget(cb)
|
|
219
|
+
right_layout.insertLayout(2, result_layout)
|
|
220
|
+
|
|
221
|
+
self.update_plot()
|
|
222
|
+
|
|
223
|
+
def update_plot(self):
|
|
224
|
+
try:
|
|
225
|
+
# Read input v
|
|
226
|
+
v = {}
|
|
227
|
+
for name, (widget, extract) in self.inputs.items():
|
|
228
|
+
v[name] = extract(widget)
|
|
229
|
+
|
|
230
|
+
#Signal Setup
|
|
231
|
+
signal = 0
|
|
232
|
+
if v["signal_type"] == "Lorentzian":
|
|
233
|
+
signal = make_lorentzian(v["l_center"], v["l_HW"], v["l_amp"])
|
|
234
|
+
else:
|
|
235
|
+
signal = make_antilorentzian(v["al_center"], v["al_HW"], v["al_amp"])
|
|
236
|
+
|
|
237
|
+
#Noise Setup
|
|
238
|
+
jT = jR = sI = sR = oRMS = rTU = rTD = rSU = rSD = bD = bMIN = bMAX = 0
|
|
239
|
+
if v["Johnson Noise"]:
|
|
240
|
+
jT, jR = v["jT"], v["jR"]
|
|
241
|
+
if v["Shot Noise"]:
|
|
242
|
+
sI, sR = v["sI"], v["sR"]
|
|
243
|
+
if v["1/f Noise"]:
|
|
244
|
+
oRMS = v["oRMS"]
|
|
245
|
+
if v["Random Telegraph Noise"]:
|
|
246
|
+
rTU, rTD, rSU, rSD = v["rTU"], v["rTD"], v["rSU"], v["rSD"]
|
|
247
|
+
if v["Bit Noise"]:
|
|
248
|
+
bD, bMIN, bMAX = v["bD"], v["bMIN"], v["bMAX"]
|
|
249
|
+
|
|
250
|
+
# Run lock-in emulator
|
|
251
|
+
LI = Lock_in_emulator(
|
|
252
|
+
signal,
|
|
253
|
+
v["f_input"],
|
|
254
|
+
v["phase_input"],
|
|
255
|
+
v["xstart_input"],
|
|
256
|
+
v["xstop_input"],
|
|
257
|
+
v["xamp_input"],
|
|
258
|
+
v["time_input"],
|
|
259
|
+
v["dt_input"],
|
|
260
|
+
v["TC_input"],
|
|
261
|
+
v["order_input"],
|
|
262
|
+
v["plotpoints_input"],
|
|
263
|
+
v["buffersize_input"],
|
|
264
|
+
jT, jR, sI, sR, oRMS, rTU, rTD, rSU, rSD, bD, bMIN, bMAX
|
|
265
|
+
)
|
|
266
|
+
LI.run()
|
|
267
|
+
|
|
268
|
+
# Update plots
|
|
269
|
+
self.curve_orig.setData(LI.x_plot, LI.original_signal)
|
|
270
|
+
self.curve_out.setData(LI.x_plot, LI.output_signal)
|
|
271
|
+
self.curve_expected.setData(LI.x_plot, LI.expected_signal)
|
|
272
|
+
self.curve_adjusted.setData(LI.x_plot, LI.adjusted_signal)
|
|
273
|
+
|
|
274
|
+
# Update results
|
|
275
|
+
self.result_text.setText(f"<b>Diminish:</b> {LI.diminish:.6f}, "
|
|
276
|
+
f"<b>Shift:</b> {LI.shift:.6f}, "
|
|
277
|
+
f"<b>Stretch:</b> {LI.stretch:.6f}, "
|
|
278
|
+
f"<b>SNR</b>: {LI.snr:.6f}")
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.result_text.setText(f"Error: {e}")
|
|
282
|
+
|
|
283
|
+
def reset_fields(self):
|
|
284
|
+
for p in params:
|
|
285
|
+
name, default = p[0], p[1]
|
|
286
|
+
self.line_edits[name].clear()
|
|
287
|
+
self.line_edits[name].setText(str(default))
|
|
288
|
+
self.update_plot()
|
|
289
|
+
|
|
290
|
+
def run():
|
|
291
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
292
|
+
w = MyWindow()
|
|
293
|
+
w.show()
|
|
294
|
+
sys.exit(app.exec_())
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
run()
|
barsukov/data/noise.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
K_b = 1.380649e-23
|
|
4
|
+
q = 1.602176634e-19
|
|
5
|
+
|
|
6
|
+
def johnson(time_arr, T, R):
|
|
7
|
+
#CHECKED
|
|
8
|
+
"""
|
|
9
|
+
Generates Johnson-Nyquist noise voltage signal in the time domain, for a given time array.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
time_arr (array-like): Array of ordered time values in Seconds (assumed to be evenly spaced).
|
|
13
|
+
T (float): Temperature in Kelvin.
|
|
14
|
+
R (float): Resistance in Ohms.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
np.ndarray: Array of normally distributed noise values with RMS amplitude corresponding to the thermal noise voltage.
|
|
18
|
+
"""
|
|
19
|
+
size = len(time_arr)
|
|
20
|
+
Df = 0.5 * (size - 1) / (time_arr[-1] - time_arr[0]) # Nyquist Frequency is this correct. Becomes noise's bandwidth
|
|
21
|
+
# Nyquist Frequency used for bandwidth as it is the maximum resolvable frequency that can be detected. Generating noise purely by itself, before any filtering is being simulated.
|
|
22
|
+
|
|
23
|
+
V_rms = np.sqrt(4 * K_b * T * R * Df)
|
|
24
|
+
return np.random.normal(0, V_rms, size)
|
|
25
|
+
|
|
26
|
+
def shot(time_arr, I, R):
|
|
27
|
+
#CHECKED
|
|
28
|
+
"""
|
|
29
|
+
Generates Shot noise voltage signal in the time domain, for a given time array.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
time_arr (array-like): Array of ordered time values in Seconds (assumed to be evenly spaced).
|
|
33
|
+
I (float): Current in Amperes.
|
|
34
|
+
R (float): Resistance in Ohms.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
np.ndarray: Array of normally distributed voltage noise values with RMS amplitude corresponding to shot noise.
|
|
38
|
+
|
|
39
|
+
Note:
|
|
40
|
+
Ideally, shot noise follows a Poisson distribution, but for large mean values (lambda), the Poisson distribution approximates a normal distribution.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
size = len(time_arr)
|
|
44
|
+
Df = 0.5 * (size - 1) / (time_arr[-1] - time_arr[0])
|
|
45
|
+
|
|
46
|
+
I_rms = np.sqrt(2 * q * I * Df) # Shot noise current
|
|
47
|
+
V_rms = R * I_rms # Recalculating to Shot noise voltage
|
|
48
|
+
return np.random.normal(0, V_rms, size) #poisson should be used, but lamda is too large for np.random.poisson
|
|
49
|
+
|
|
50
|
+
def color(time_arr, V_rms, exponent=1):
|
|
51
|
+
"""
|
|
52
|
+
Generates 1/f^(exponent) noise (pink noise by default, exp=1) in the time domain.
|
|
53
|
+
|
|
54
|
+
Parameters:
|
|
55
|
+
time_arr (array-like): Array of ordered time values in Seconds (assumed to be evenly spaced).
|
|
56
|
+
V_rms (float): RMS value of the generated noise.
|
|
57
|
+
exponent (float, optional): Power of the frequency dependence. Default is 1, which gives 1/f noise (pink noise). Set to 0 for white noise, 2 for brown noise, etc...
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
np.ndarray: Array of noise values with PSD proportional to 1/f^(exponent).
|
|
61
|
+
"""
|
|
62
|
+
#Generate Guassian White Noise in time domain
|
|
63
|
+
size = len(time_arr)
|
|
64
|
+
dt = (time_arr[-1] - time_arr[0]) / (size-1.0)
|
|
65
|
+
white = np.random.standard_normal(size)
|
|
66
|
+
|
|
67
|
+
#Fourier Transform to Frequency Domain
|
|
68
|
+
freqs = np.fft.rfftfreq(size, d=dt)
|
|
69
|
+
fft = np.fft.rfft(white, norm='backward') * dt
|
|
70
|
+
|
|
71
|
+
#Scale Fourier Transform by 1/f^(exponent/2) for 1/f^exponent PSD (psd proportional to fft^2)
|
|
72
|
+
freqs[0] = freqs[1] #Avoid division by zero
|
|
73
|
+
fft = fft / (freqs**(exponent*0.5))
|
|
74
|
+
|
|
75
|
+
#Convert back to time domain
|
|
76
|
+
ifft = (np.fft.irfft(fft, n=size, norm='forward') / dt).real
|
|
77
|
+
ifft_rms = np.sqrt(np.mean(ifft**2))
|
|
78
|
+
|
|
79
|
+
return V_rms * ifft / ifft_rms #Ensure V_rms is as specified
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def rtn(time_arr, tau_up, tau_down, state_up=1, state_down=0, initial_state=None):
|
|
83
|
+
"""
|
|
84
|
+
Generate random telegraph noise on a user-supplied time array.
|
|
85
|
+
|
|
86
|
+
Parameters:
|
|
87
|
+
time_arr (np.ndarray): Array of ordered time values in Seconds (assumed to be evenly spaced).
|
|
88
|
+
tau_up (float): Mean dwell time in the 'up' state.
|
|
89
|
+
tau_down (float): Mean dwell time in the 'down' state.
|
|
90
|
+
state_up (float): Value of the up state (default: 1).
|
|
91
|
+
state_down (float): Value of the down state (default: 0).
|
|
92
|
+
initial_state (float): Value of the first state (default: None = random(up, down))
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
signal (np.ndarray): RTN signal array, same shape as time_arr.
|
|
96
|
+
|
|
97
|
+
Notes:
|
|
98
|
+
- PSD of RTN will have lorentzian profile: S(f) = 4*A^2*tau / (1 + (2pi*f*tau)^2),
|
|
99
|
+
with correlation time: tau = tau_up*tau_down / (tau_up+tau_down)
|
|
100
|
+
- Characteristic (roll-off) frequency corresponds to 1 / (2pi*tau)
|
|
101
|
+
"""
|
|
102
|
+
if tau_up <= 0 or tau_down <= 0:
|
|
103
|
+
raise ValueError("tau_up and tau_down must be positive to avoid infinite loops.")
|
|
104
|
+
|
|
105
|
+
time_arr = np.asarray(time_arr)
|
|
106
|
+
signal = np.zeros_like(time_arr)
|
|
107
|
+
|
|
108
|
+
if initial_state is None:
|
|
109
|
+
current_state = np.random.choice([state_up,state_down])
|
|
110
|
+
else: current_state = initial_state
|
|
111
|
+
|
|
112
|
+
current_time = time_arr[0]
|
|
113
|
+
i = 0
|
|
114
|
+
|
|
115
|
+
while i < len(time_arr):
|
|
116
|
+
# Sample dwell time
|
|
117
|
+
dwell_time = np.random.exponential(tau_up if current_state == state_up else tau_down)
|
|
118
|
+
dwell_end_time = current_time + dwell_time
|
|
119
|
+
|
|
120
|
+
# Assign current state until dwell time is over
|
|
121
|
+
while i < len(time_arr) and time_arr[i] < dwell_end_time:
|
|
122
|
+
signal[i] = current_state
|
|
123
|
+
i += 1
|
|
124
|
+
|
|
125
|
+
# Flip state
|
|
126
|
+
current_time = dwell_end_time
|
|
127
|
+
current_state = state_down if current_state == state_up else state_up
|
|
128
|
+
|
|
129
|
+
return signal
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def bit(signal_arr, bit_depth, measure_min, measure_max, noise_only = False):
|
|
133
|
+
"""
|
|
134
|
+
Quantize an analog signal to simulate ADC behavior with given bit depth.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
signal_arr (array-like): Input analog signal values.
|
|
138
|
+
bit_depth (int): number of bits used in quantization (e.g., 8, 12, 16).
|
|
139
|
+
measure_min (float): Minimum measurable value of the ADC range.
|
|
140
|
+
measure_max (float): Maximum measurable value of the ADC range.
|
|
141
|
+
noise_only (bool, optional): If True, quantization noise only. If False (default), return the quantized signal.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
np.ndarray: Quantized signal or Quantization noise, depending on 'noise_only'.
|
|
145
|
+
|
|
146
|
+
Notes:
|
|
147
|
+
- The signal is clipped to the measurement range before quantization.
|
|
148
|
+
- The number of quantization levels is 2^bit_depth.
|
|
149
|
+
"""
|
|
150
|
+
levels = int(2**int(bit_depth)) # 1<<int(bit_depth)
|
|
151
|
+
quantization_step = (measure_max - measure_min) / (levels - 1)
|
|
152
|
+
|
|
153
|
+
signal_clipped = np.clip(signal_arr, measure_min, measure_max)
|
|
154
|
+
quantized_signal = np.round((signal_clipped - measure_min) / quantization_step) * quantization_step + measure_min
|
|
155
|
+
|
|
156
|
+
if noise_only is False:
|
|
157
|
+
return quantized_signal
|
|
158
|
+
else:
|
|
159
|
+
return quantized_signal - signal_arr
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def psd(time_arr, signal_arr, return_onesided=True):
|
|
163
|
+
"""
|
|
164
|
+
Computes the Power Spectral Density (PSD) of a time-domain signal using Welch's method.
|
|
165
|
+
|
|
166
|
+
Parameters:
|
|
167
|
+
time_arr (array-like): Ordered time values in Seconds (assumed to be increasing).
|
|
168
|
+
signal_arr (array-like): Signal values as a function of time.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
tuple:
|
|
172
|
+
- f_welch (np.ndarray): Array of frequency values corresponding to the PSD.
|
|
173
|
+
- psd_welch (np.ndarray): Power Spectral Density values in (signal_unit)^2/Hz.
|
|
174
|
+
- msg (str): Message describing the PSD normalization and units.
|
|
175
|
+
|
|
176
|
+
Notes:
|
|
177
|
+
- Welch's method averages overlapping FFTs to reduce variance in the PSD estimate.
|
|
178
|
+
- The RMS value of the signal over a bandwidth B can be computed from the PSD as:
|
|
179
|
+
V_rms = sqrt( ∫ PSD(f) df ) over bandwidth.
|
|
180
|
+
- The PSD values follow the convention:
|
|
181
|
+
mean(PSD) ≈ 2 × (b.fft)^2 / total duration
|
|
182
|
+
-When dealing with real signals, (one sided, only positive frequencies) the relationship is psd = np.abs(fft)**2 / total_time * 2, where factor of 2 is applied everywhere except at the DC and Nyquist bins. When dealing with complex signals (positive and negative) the relationship is psd = np.abs(fft)**2 / total_time.
|
|
183
|
+
"""
|
|
184
|
+
from scipy import signal as sp_signal
|
|
185
|
+
#make equidistant
|
|
186
|
+
size = len(time_arr)
|
|
187
|
+
sampling_freq = (size-1) / (time_arr[-1] - time_arr[0])
|
|
188
|
+
|
|
189
|
+
f_welch, psd_welch = sp_signal.welch(signal_arr, sampling_freq, nperseg=min(size, 4024), return_onesided=return_onesided)
|
|
190
|
+
return f_welch, psd_welch
|
|
191
|
+
|
|
192
|
+
def noise_help(noise):
|
|
193
|
+
import matplotlib.pyplot as plt
|
|
194
|
+
|
|
195
|
+
doc = {}
|
|
196
|
+
|
|
197
|
+
if noise == "johnson":
|
|
198
|
+
time = np.arange(0,100)
|
|
199
|
+
|
|
200
|
+
doc = {
|
|
201
|
+
"name": "Johnson Noise",
|
|
202
|
+
"time": time,
|
|
203
|
+
"function": johnson,
|
|
204
|
+
"params": [300, 200],
|
|
205
|
+
"text_title_size": 20,
|
|
206
|
+
"text_midbreak_size": 15,
|
|
207
|
+
"text_main_size": 10,
|
|
208
|
+
"text_bullet_size": 10,
|
|
209
|
+
"text_title_x": .5,
|
|
210
|
+
"text_title_y": 9,
|
|
211
|
+
"text_main_x": .3,
|
|
212
|
+
"text_main_y": 8,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if noise == "johnson2":
|
|
216
|
+
time = np.arange(0,100)
|
|
217
|
+
|
|
218
|
+
doc = {
|
|
219
|
+
"name": "Johnson Noise",
|
|
220
|
+
"time": time,
|
|
221
|
+
"function": johnson,
|
|
222
|
+
"params": [300, 200],
|
|
223
|
+
"text_title_size": 20,
|
|
224
|
+
"text_midbreak_size": 15,
|
|
225
|
+
"text_main_size": 10,
|
|
226
|
+
"text_bullet_size": 10,
|
|
227
|
+
"text_title_x": .5,
|
|
228
|
+
"text_title_y": 9,
|
|
229
|
+
"text_main_x": .3,
|
|
230
|
+
"text_main_y": 6,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if noise == "shot":
|
|
234
|
+
time = np.arange(0,100)
|
|
235
|
+
|
|
236
|
+
doc = {
|
|
237
|
+
"name": "Shot Noise",
|
|
238
|
+
"time": time,
|
|
239
|
+
"function": shot,
|
|
240
|
+
"params": [1, 200],
|
|
241
|
+
"text_title_size": 30,
|
|
242
|
+
"text_midbreak_size": 15,
|
|
243
|
+
"text_main_size": 15,
|
|
244
|
+
"text_bullet_size":10,
|
|
245
|
+
"text_title_x": .5,
|
|
246
|
+
"text_title_y": 9,
|
|
247
|
+
"text_main_x": .5,
|
|
248
|
+
"text_main_y": 8,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(22,7))
|
|
253
|
+
fig.suptitle(doc["name"], fontsize=20)
|
|
254
|
+
ax1.plot(doc["time"], doc["function"](doc["time"], *doc["params"]), 'red', label='(t^2)*exp(-(t^2))')
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
ax1.set(title="2 lines chart", xlabel="t", ylabel="y")
|
|
258
|
+
ax1.legend(loc="upper right")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Set both x- and y-axis limits to [0, 10] instead of default [0, 1]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
ax2.axis([0, 10, 0, 10])
|
|
265
|
+
ax2.tick_params(axis='x', colors='white')
|
|
266
|
+
ax2.tick_params(axis='y', colors='white')
|
|
267
|
+
|
|
268
|
+
ax2.text(doc["text_title_x"], doc["text_title_y"], doc["name"], weight='bold', fontsize=doc["text_title_size"],
|
|
269
|
+
bbox={'facecolor': 'red', 'alpha': 0.3, 'pad': 10})
|
|
270
|
+
ax2.text(doc["text_main_x"], doc["text_main_y"], doc["function"].__doc__, wrap="false", fontsize=doc["text_main_size"])
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
#use for bullet points#ax2.text(0.5, 5.5, 'Topic 2', weight='bold', fontsize=doc["text_midbreak_size"])
|
|
274
|
+
#use for bullet points#ax2.text(0.5, 4.5, '- Bullet pont 1\n- Bullet point 2', fontsize= doc["text_bullet_size"])
|
|
275
|
+
|
|
276
|
+
#use to display function # ax2.text(2, 3, r'a function to plot: $t^2*exp(-t^2)$', fontsize=12)
|
barsukov/exp/__init__.py
CHANGED
barsukov/exp/mwHP.py
CHANGED
|
@@ -55,6 +55,8 @@ class mwHP:
|
|
|
55
55
|
log_in_eq(self, msg, log=log)
|
|
56
56
|
### END These functions could be shared across all equipment.
|
|
57
57
|
|
|
58
|
+
|
|
59
|
+
### BEGIN: mwHP Functions:
|
|
58
60
|
def output(self, state=None, log=None, check=False):
|
|
59
61
|
### Always has a return! Which is the state of Output.
|
|
60
62
|
### output() reads and returns the state of Output.
|
|
@@ -231,6 +233,7 @@ class mwHP:
|
|
|
231
233
|
if abs(dutyreal - duty) < 0.03*float(duty): self.log(f'Writing Pulse duty cycle as {dutyreal}.', log=log)
|
|
232
234
|
else: self.log(f'Warning:Writing Pulse duty cycle as {dutyreal}, but was asked {duty}.', log='important')
|
|
233
235
|
return freal, dutyreal
|
|
236
|
+
### END: mwHP Functions:
|
|
234
237
|
|
|
235
238
|
|
|
236
239
|
### BEGIN: OBJ2FILE Tools
|
|
@@ -241,8 +244,10 @@ class mwHP:
|
|
|
241
244
|
seriable_data = self.__dict__.copy()
|
|
242
245
|
# take the attributes of unseriable data
|
|
243
246
|
if self.script is None:
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
if self.logger is not None:
|
|
248
|
+
seriable_data['logger'] == 'needsrebuild'
|
|
249
|
+
seriable_data['logger_information'] = self.logger.__getargs__()
|
|
250
|
+
seriable_data['logger_information']['start_file'] = False
|
|
246
251
|
else:
|
|
247
252
|
seriable_data['script'] == 'needsrebuild'
|
|
248
253
|
seriable_data['script_information'] = self.script.__getstate__()
|
|
@@ -259,5 +264,9 @@ class mwHP:
|
|
|
259
264
|
self.script = Script(**seriable_data['script_information'])
|
|
260
265
|
if self.logger == 'needsrebuild':
|
|
261
266
|
self.logger = Logger(**seriable_data['logger_information'])
|
|
267
|
+
if (self.script is not None):
|
|
268
|
+
self.log(f'I am using Script saved in memory: {self.script.folder_name}.', log='screen')
|
|
269
|
+
elif (self.logger is not None):
|
|
270
|
+
self.log(f'I am using Logger saved in memory: {self.logger.description}.', log='screen')
|
|
262
271
|
eq_reconnect(self)
|
|
263
272
|
### END: OBJ2FILE Tools
|