barsukov 0.0.0__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 ADDED
@@ -0,0 +1,18 @@
1
+ # Modules:
2
+ from . import time
3
+ from . import data
4
+
5
+
6
+ # Objects/Functions:
7
+ from .script import Script
8
+ from .logger import Logger
9
+
10
+ from .obj2file import *
11
+
12
+
13
+ # Equipment Objects:
14
+ from .exp.mwHP import mwHP
15
+
16
+ __all__ = ["time", "data", "save_object", "load_object", "Script", "Logger", "mwHP"]
17
+
18
+
@@ -0,0 +1 @@
1
+ from .fft import *
barsukov/data/fft.py ADDED
@@ -0,0 +1,87 @@
1
+ ### BEGIN Dependencies ###
2
+ 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
+
7
+
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 = ''
14
+ if equidistant_check:
15
+ diffs = np.diff(x)
16
+ if not np.allclose(diffs, diffs[0], rtol=equidistant_rel_error):
17
+ # 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)
24
+
25
+ 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
30
+ 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.')
49
+ 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.')
58
+
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
+
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
+
65
+ def make_equidistant(x, y, step=None):
66
+ ### Takes one column x and one or more columns y and makes them equidistant in x
67
+ ### Returns new_x, new_y. The number of points will likely change.
68
+ if step is None:
69
+ # Calculate the smallest difference between consecutive elements
70
+ min_step = np.min(np.diff(x))
71
+ else:
72
+ min_step = step
73
+
74
+ # Generate the new equidistant x array
75
+ new_x = np.arange(x[0], x[-1] + min_step, min_step)
76
+
77
+ if isinstance(y[0], (list, np.ndarray)): # If y contains multiple columns
78
+ new_y = []
79
+ for y_column in y:
80
+ interpolation_function = sp.interpolate.interp1d(x, y_column, kind='linear', fill_value='extrapolate')
81
+ new_y.append(interpolation_function(new_x))
82
+ new_y = np.array(new_y).T # Transpose to match the original structure
83
+ else: # If y is a single column
84
+ interpolation_function = sp.interpolate.interp1d(x, y, kind='linear', fill_value='extrapolate')
85
+ new_y = interpolation_function(new_x)
86
+
87
+ return new_x, new_y
@@ -0,0 +1 @@
1
+ from . import mwHP
@@ -0,0 +1,136 @@
1
+ ### BEGIN Dependencies ###
2
+ import numpy as np
3
+ import sys
4
+ from barsukov.time import *
5
+ ### END Dependencies
6
+
7
+
8
+ ### BEGIN Helper functions
9
+
10
+ def log_in_eq(eq_obj, msg, log='default'): # FINISHED 2024/10/26
11
+ decorated_msg = str(eq_obj.msg_deco) + ' ' + msg
12
+ if eq_obj.logger is None:
13
+ if log=='no': return
14
+ else: print(time_stamp() + ' ' + decorated_msg)
15
+ else:
16
+ eq_obj.logger.log(decorated_msg, log)
17
+
18
+
19
+ def initialize_gpib(eq_obj):
20
+ # Initializes a visa.open_resource(). Returns rm.open_resource(). Exits if error.
21
+ if eq_obj.rm is None: # eq_obj has no ResourceManager
22
+ if eq_obj.script is None: # If there is no Script
23
+ eq_obj.log('Visa ResourceManager and Script have not been passed to me. I will attempt to initialize visa myself.', log='important')
24
+ try:
25
+ import pyvisa as visa
26
+ eq_obj.rm = visa.ResourceManager()
27
+ eq_obj.log('I just set my self.rm = visa.ResourceManager.', log='screen')
28
+ except:
29
+ eq_obj.log('I failed to initialize set my self.rm = visa.ResourceManager. I am sys-exiting.', log='important')
30
+ sys.exit()
31
+ else: # If there is a Script
32
+ if eq_obj.script.rm is None: # If Script has no ResourceManager
33
+ eq_obj.log('I see Script but it does not have rm. I am asking Script to initialize visa.ResourceManager and pass it to me.', log='important')
34
+ try:
35
+ eq_obj.rm = eq_obj.script.init_rm() # Script will try to init ResourceManager
36
+ #print('eq_obj.rm initialized as ', eq_obj.rm)
37
+ except:
38
+ eq_obj.log('Error while Script was initializing visa.ResourceManager. I am sys-exiting.', log='important')
39
+ sys.exit()
40
+ else:
41
+ eq_obj.log('Script has visa.ResourceManager. I am grabbing it.', log='screen')
42
+ eq_obj.rm = eq_obj.script.rm
43
+ if eq_obj.rm is None: # Just to double check if rm is in fact there.
44
+ eq_obj.log('My last check showed that my self.rm is still None. I am sys-exiting.', log='important')
45
+ sys.exit()
46
+ # Now, we assume there is a resource manager
47
+ if (eq_obj.gpib is None) or (eq_obj.gpib_card is None):
48
+ eq_obj.log('GPIB card number or GPIB address is not set.', log='important')
49
+ sys.exit()
50
+ try:
51
+ #print(eq_obj.rm)
52
+ eq_obj.log('I am trying to rm.open_resource().', log='screen')
53
+ y = eq_obj.rm.open_resource(f'GPIB{eq_obj.gpib_card}::{eq_obj.gpib}')
54
+ #print('y=',y)
55
+ return y
56
+ except:
57
+ eq_obj.log(f'I could not initialize rm.open_resource() for GPIB {eq_obj.gpib_card}::{eq_obj.gpib}. Also check visa_rm, just in case.', log='important')
58
+ sys.exit()
59
+
60
+
61
+ def eq_disconnect(eq_obj):
62
+ try:
63
+ eq_obj.rm.close()
64
+ eq_obj.rm.visalib._registry.clear()
65
+ eq_obj.log( f'Successfully disconnected GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='screen')
66
+ except:
67
+ eq_obj.log( f'Failed to disconnect GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='screen')
68
+
69
+
70
+ def eq_reconnect(eq_obj):
71
+ try:
72
+ import pyvisa as visa
73
+ eq_obj.rm = visa.ResourceManager()
74
+ eq_obj.eq = initialize_gpib(eq_obj)
75
+ eq_obj.log( f'Successfully reconnected GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='screen')
76
+ eq_obj.log( f'Initialized: {eq_obj.identify()}', log='important' )
77
+ except:
78
+ eq_obj.log(f'Failed to reconnect GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='important')
79
+
80
+ ### END Helper functions
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+ ### BEGIN Functions that are likely unnecessary
105
+
106
+ def query_float(eq_obj, cmd):
107
+ ### Returns float or np.nan
108
+ try:
109
+ q = eq_obj.eq.query(cmd)
110
+ q = float(q) # Will it work with all equipment?
111
+ return q
112
+ except:
113
+ eq_obj.log(f'Error while quering: \"{cmd}\".', log='important')
114
+ return np.nan
115
+
116
+ def write_float(eq_obj, cmd, value, digits, limits):
117
+ value_differs = ''
118
+ try:
119
+ value = float(value)
120
+ value_round = round(value, digits)
121
+ if value_round < limits[0]:
122
+ value_round = limits[0]
123
+ if value_round > limits[1]:
124
+ value_round = limits[1]
125
+ if value_round != value:
126
+ value_differs = f' But requested {value}.'
127
+ cmd = ''.join( [value_round if el is None else el for el in cmd] )
128
+ eq_obj.eq.write(cmd)
129
+ write_error = False
130
+ return write_error, value_differs
131
+ except:
132
+ write_error = True
133
+ eq_obj.log(f'Error while writing: \"{cmd}\".', log='important')
134
+ return write_error, value_differs
135
+
136
+ ### END Functions that are likely unnecessary
barsukov/exp/mwHP.py ADDED
@@ -0,0 +1,263 @@
1
+ ### BEGIN Dependencies ###
2
+ import numpy as np
3
+ import sys
4
+ from barsukov.exp.exp_utils import *
5
+ ### END Dependencies
6
+
7
+
8
+ class mwHP:
9
+ def __init__(self, gpib=None, visa_rm=None, logger=None, gpib_card=0, log='default', script=None):
10
+ # Pass the Script object, if available.
11
+ # If Script has no visa_rm or if no Script is passed, you'll need to pass the visa_rm=visa.ResourceManager manually.
12
+ # If Script has no logger or if no Script is passed, you can pass the logger manually.
13
+ # If no logger is passed, will simply print to screen.
14
+ # Change log from 'default' to 'screen', 'file', 'both', 'no'.
15
+ # gpib_card is per default 0. You can change, if you have multiple.
16
+
17
+ self.script = script
18
+ self.logger = logger
19
+ self.eq_default_log = log
20
+ self.rm = visa_rm
21
+ self.gpib_card = gpib_card
22
+ self.gpib = gpib
23
+
24
+ self.msg_deco = f'[mwHP {self.gpib_card}::{self.gpib}]'
25
+ self.eq = initialize_gpib(self) # This will initialize self.eq = visa.open_resource()
26
+ self.log( f'Initialized: {self.identify()}', log='important' ) # This is the 'welcome message' and a check if communication works.
27
+
28
+ self.f_digits = 9 # Digits of precision of mw frequency
29
+ self.f_limits = [0.01, 20.5] # Lower and upper GHz limits
30
+ self.p_digits = 1 # Digits of precision of mw power
31
+ self.p_limits = [-15.0, 17.0] # Lower and upper dBm limits
32
+ self.phase_limits = [0.0, 360.0]
33
+ self.pulsef_limits = [0.016, 500]
34
+ self.pulsedc_limits = [0, 100] # Lower and upper % limits
35
+
36
+
37
+ ### BEGIN The definition of the following functions may be specific to this equipment.
38
+ def query(self, cmd):
39
+ return self.eq.query(cmd)
40
+
41
+ def write(self, cmd):
42
+ return self.eq.write(cmd)
43
+
44
+ def identify(self):
45
+ return str(self.eq.query('*IDN?'))
46
+ ### END The definition of the following functions may be specific to this equipment.
47
+ def disconnect(self):
48
+ eq_disconnect(self)
49
+
50
+ def reconnect(self):
51
+ eq_reconnect(self)
52
+
53
+ def log(self, msg, log=None):
54
+ if log is None: log=self.eq_default_log
55
+ log_in_eq(self, msg, log=log)
56
+ ### END These functions could be shared across all equipment.
57
+
58
+ def output(self, state=None, log=None, check=False):
59
+ ### Always has a return! Which is the state of Output.
60
+ ### output() reads and returns the state of Output.
61
+ ### output(1) writes state of Output to ON.
62
+ ### output(1) returns the state that was actually sent to equipment.
63
+ ### output(1, check=True) returns the state queried after writing
64
+ if log is None: log=self.eq_default_log
65
+ if state is None:
66
+ try:
67
+ y = self.eq.query('output?')
68
+ y = int(y)
69
+ self.log(f'Output is {y}.')
70
+ return y
71
+ except:
72
+ self.log(f'Error while reading Output.', log='important')
73
+ return np.nan
74
+ else:
75
+ if (state == 1) or (state == 'on') or (state=='ON') or (state=='On'): sstate = 1
76
+ else: sstate = 0
77
+ try:
78
+ self.eq.write(f'output {sstate}')
79
+ if check: y=self.output(log='no')
80
+ else: y = sstate
81
+ if y == state: self.log(f'Output set to {sstate}.')
82
+ else: self.log(f'Warning: Setting Output to {sstate}, but was asked for {state}.')
83
+ return y
84
+ except:
85
+ self.log(f'Error while changing Output state.', log='important')
86
+ return np.nan
87
+
88
+
89
+ def f(self, f=None, log=None, check=False):
90
+ ### Always has a return! Which is frequency in GHz.
91
+ ### f() reads and returns the frequency in GHz.
92
+ ### f(10) writes frequency to 10 GHz.
93
+ ### f(10) returns the frequency that was actually sent to equipment.
94
+ ### f(10, check=True) returns the frequency queried after writing.
95
+ ### Do set log='no' to avoid latency for repeated calls.
96
+ if log is None: log=self.eq_default_log
97
+ if f is None:
98
+ try:
99
+ y = self.eq.query('freq?')
100
+ y = float(y)/1e9 # Is the frequency returned in GHz??
101
+ self.log(f'Reading f as {y} GHz.', log=log)
102
+ return y
103
+ except:
104
+ self.log(f'Error while reading Frequency.', log='important')
105
+ return np.nan
106
+ else:
107
+ try:
108
+ x = round(f, self.f_digits) # rounding the digits
109
+ x = max(self.f_limits[0], min(x, self.f_limits[1])) # sets x within f_limits
110
+ self.eq.write(f'freq {x} GHz')
111
+ if check: y = self.f(log='no')
112
+ else: y = x
113
+ if abs(y-f)<10.0**(-self.f_digits): self.log(f'Writing f as {x}.', log=log)
114
+ else: self.log(f'Warning: writing Frequency as {x}, but was asked {f}.', log='important')
115
+ return y
116
+ except:
117
+ self.log(f'Error while writing Frequency as {f}.', log='important')
118
+ return np.nan
119
+
120
+
121
+ def p(self, p=None, log=None, check=False):
122
+ ### Always has a return! Which is the power in dBm.
123
+ ### p() reads and returns the power in dBm.
124
+ ### p(1) writes power to 1 dBm.
125
+ ### p(1) returns the power that was actually sent to equipment.
126
+ ### p(1, check=True) returns the power queried after writing
127
+ ### Do set log='no' to avoid latency for repeated calls.
128
+ if log is None: log=self.eq_default_log
129
+ if p is None:
130
+ try:
131
+ y = self.eq.query('pow?')
132
+ y = float(y)
133
+ self.log(f'Reading p as {y} dBm.', log=log)
134
+ return y
135
+ except:
136
+ self.log(f'Error while reading Power.', log='important')
137
+ return np.nan
138
+ else:
139
+ try:
140
+ x = round(p, self.p_digits) # rounding the digits
141
+ x = max(self.p_limits[0], min(x, self.p_limits[1])) # sets x within p_limits
142
+ self.eq.write(f'pow {x} dBm')
143
+ if check: y = self.p(log='no')
144
+ else: y = x
145
+ if abs(y-p)<10**(-self.p_digits): self.log(f'Writing p as {x}.', log=log)
146
+ else: self.log(f'Warning: writing Power as {x}, but was asked {p}.', log='important')
147
+ return y
148
+ except:
149
+ self.log(f'Error while writing Power as {p}.', log='important')
150
+ return np.nan
151
+
152
+
153
+ def sweep(self, start, stop, step, dwell, mode, log=None):
154
+ if log is None: log=self.eq_default_log
155
+ units, write_start, write_stop, mode = '', start, stop, mode.lower()
156
+ if (mode == 'freq') or (mode == 'frequency') or (mode == 'f'):
157
+ write_start = max(self.f_limits[0], min(write_start, self.f_limits[1]))
158
+ write_stop = max(self.f_limits[0], min(write_stop, self.f_limits[1]))
159
+ mode = 'Frequency'
160
+ units = 'GHz'
161
+ if (mode == 'pow') or (mode == 'power') or (mode == 'p'):
162
+ write_start = max(self.p_limits[0], min(write_start, self.p_limits[1]))
163
+ write_stop = max(self.p_limits[0], min(write_stop, self.p_limits[1]))
164
+ mode = 'Power'
165
+ units = 'dBm'
166
+ if (write_start != start):
167
+ self.log(f'Warning: Writing {mode} Sweep start to {write_start} but was asked {start}.', log='important')
168
+ if (write_stop != stop):
169
+ self.log(f'Warning: Writing {mode} Sweep stop to {write_stop} but was asked {stop}.', log='important')
170
+ try:
171
+ self.log(f'{mode} Sweep parameters are start: {write_start} {units}, stop: {write_stop} {units}, step: {step} {units}, dwell: {dwell} s.', log=log)
172
+ self.log(f'Initiating {mode} Sweep: ', log=log)
173
+ from time import sleep
174
+ for point in np.arange(write_start, write_stop+(step/2.0), step):
175
+ if mode == 'Frequency':
176
+ self.f(f=point)
177
+ elif mode == 'Power':
178
+ self.p(p=point)
179
+ sleep(dwell)
180
+ self.log(f'{mode} Sweep completed.', log=log)
181
+ except:
182
+ self.log(f'Error while conducting Sweep.', log='important')
183
+ return np.nan
184
+
185
+
186
+ def pulse(self, f=None, duty=None, log=None):
187
+ if log is None: log=self.eq_default_log
188
+ if f is None and duty is None:
189
+ try:
190
+ T = float(self.eq.query('puls:per?')) * 10.0**3
191
+ f = 1.0 / T
192
+ w = float(self.eq.query('puls:widt?')) * 10.0**3
193
+ duty = w / T * 100.0
194
+ y = self.eq.query('pulm:stat?')
195
+ y = int(y)
196
+ x = self.eq.query('pulm:sour?')
197
+ x = x[:-1].lower()
198
+ self.log(f'Pulse Frequency {f} KHz, duty-cycle {duty}%. state {y}, source {x}.', log=log)
199
+ return f, duty
200
+ except:
201
+ self.log(f'Error while reading Pulse state.', log='important')
202
+ return np.nan
203
+ else:
204
+ if duty is None: duty = 50.0
205
+ try:
206
+ if f is None and duty != 50.0:
207
+ duty_write = max(self.pulsedc_limits[0], min(float(duty), self.pulsedc_limits[1]))
208
+ T = float(self.eq.query('puls:per?')) * 10.0**3
209
+ w = duty_write * T / 100.0
210
+ self.eq.write(f'puls:widt {w} ms')
211
+ elif f is not None and duty == 50.0:
212
+ f_write = max(self.pulsef_limits[0], min(float(f), self.pulsef_limits[1]))
213
+ duty_write = duty
214
+ T = 1.0 / f_write
215
+ w = duty_write * T / 100.0
216
+ self.eq.write(f'puls:per {T} ms')
217
+ self.eq.write(f'puls:widt {w} ms')
218
+ else:
219
+ f_write = max(self.pulsef_limits[0], min(float(f), self.pulsef_limits[1]))
220
+ duty_write = max(self.pulsedc_limits[0], min(float(duty), self.pulsedc_limits[1]))
221
+ T = 1.0 / f_write
222
+ w = duty_write * T / 100.0
223
+ self.eq.write(f'puls:per {T} ms')
224
+ self.eq.write(f'puls:widt {w} ms')
225
+ except:
226
+ self.log(f'Error while writing pulse frequency as {f} and duty cycle as {duty}', log='important')
227
+ return np.nan
228
+ freal, dutyreal = self.pulse()
229
+ if abs(freal - f) < 0.10*float(f): self.log(f'Writing Pulse Frequency as {freal}.', log=log)
230
+ else: self.log(f'Warning:Writing Pulse Frequency as {freal}, but was asked {f}.', log='important')
231
+ if abs(dutyreal - duty) < 0.03*float(duty): self.log(f'Writing Pulse duty cycle as {dutyreal}.', log=log)
232
+ else: self.log(f'Warning:Writing Pulse duty cycle as {dutyreal}, but was asked {duty}.', log='important')
233
+ return freal, dutyreal
234
+
235
+
236
+ ### BEGIN: OBJ2FILE Tools
237
+ # Prepares the Script object for serialization by removing non-seriable attributes (e.g. logger and rm).
238
+ # Returns:
239
+ # dict: A dictionary representing the serializable state of the Script object.
240
+ def __getstate__(self):
241
+ seriable_data = self.__dict__.copy()
242
+ # take the attributes of unseriable data
243
+ if self.script is None:
244
+ seriable_data['logger'] == 'needsrebuild'
245
+ seriable_data['logger_information'] = self.logger.__getargs__()
246
+ else:
247
+ seriable_data['script'] == 'needsrebuild'
248
+ seriable_data['script_information'] = self.script.__getstate__()
249
+ seriable_data['rm'] = None
250
+ seriable_data['eq'] = None
251
+ return seriable_data
252
+
253
+
254
+ def __setstate__(self, seriable_data):
255
+ from barsukov.script import Script
256
+ from barsukov.logger import Logger
257
+ self.__dict__.update(seriable_data)
258
+ if self.script == 'needsrebuild':
259
+ self.script = Script(**seriable_data['script_information'])
260
+ if self.logger == 'needsrebuild':
261
+ self.logger = Logger(**seriable_data['logger_information'])
262
+ eq_reconnect(self)
263
+ ### END: OBJ2FILE Tools
barsukov/logger.py ADDED
@@ -0,0 +1,161 @@
1
+ ### BEGIN Dependencies ###
2
+ import os
3
+ from barsukov.time import time_stamp
4
+ ### END Dependencies ###
5
+
6
+ class Logger:
7
+ """
8
+ Logger class that handles logging to both screen and file with flexible configuration
9
+
10
+ The logger is started upon initialization.
11
+ It can be started with the 'start()' method and can be closed using the 'close()' method
12
+ It is designed to avoid creating multiple Logger instances. Restarting the logger logs to the same file unless the 'full_file_path' is manually changed
13
+
14
+ Avaliable log options:
15
+ - 'screen': Logs only to the screen.
16
+ - 'file': Logs only to a file.
17
+ - 'both': Logs to both the screen and file.
18
+ - 'no': Disables logging.
19
+
20
+ log level 'important' is used for important logs, typically for internal use with higher priority
21
+
22
+ Attributes:
23
+ full_file_path (str): path to the log file (e.g., 'D:/Rundong/Projects/AFM sims/2024-07-06 Autooscillations/text.txt')
24
+ description (str): Description of the log's purpose.
25
+ log_mode (str): Defines the logging mode ('screen', 'file', 'both, or 'no').
26
+ file (file object): Log file object.
27
+ file_error (bool): Flag indicating an error with the file
28
+ """
29
+ ### Don't create multiple Logger objects.
30
+ ### Logger can be opened and closed with start() and close() methods.
31
+ ### Restarting the logger will log to the same file, unless self.full_file_path has been changed by hand.
32
+ ### start=True will start the logger automatically, but be careful
33
+ ### about the full_folder_path=os.getcwd() in init.
34
+ ### Log options are 'screen', 'file', 'both', 'no'. The logger default is 'both'.
35
+ ### The log='important' is for the Logger.log() method only. Try not to use it.
36
+ ### Default hyerarchy is logger-default overriden by object-default overriden by instance.
37
+ ### Instances, that log errors, will usually use 'both'.
38
+
39
+ ### BEGIN: Initializing Tools
40
+ def __init__(self,
41
+ description=None, # Will be passed by Script. If not, write a brief description!
42
+ full_folder_path=os.getcwd(), # Will be passed by Script. If not, specify!
43
+ full_file_path=None, # Specify only if you want to log into an already existing file.
44
+ log='both', # Logger default will be passed by Script. If not, you may choose to change.
45
+ start_file=True,
46
+ ):
47
+
48
+ ### Initializing all variables before setting/getting them
49
+ self.full_file_path = full_file_path # If changed by hand later, needs start() to take effect
50
+ self.description = description # If changed by hand later, needs set_full_file_path and start() to take effect
51
+ self.log_mode = log # Default log mode can be changed by hand any time, no restart needed.
52
+
53
+ if self.full_file_path is not None: self.full_folder_path = None
54
+ else:
55
+ self.full_folder_path = full_folder_path # If changed by hand later, needs set_full_file_path and start() to take effect
56
+ self.set_full_file_path(description=description, full_folder_path=full_folder_path)
57
+
58
+ if start_file: self.start_file()
59
+ else: self.file = None
60
+
61
+ self.file_error = False
62
+ # If a problem with file, will be set to True. Just in case for later.
63
+ # If problem is remedied, you need to set file_error to False by hand.
64
+
65
+ self.log(f'Logger initialization complete.', log='important')
66
+
67
+
68
+ def set_full_file_path(self, description=None, full_folder_path=None):
69
+ ### Checking if optional arguments are filled or if defaults are provided ###
70
+ if description is None:
71
+ description = self.description
72
+ if full_folder_path is None:
73
+ full_folder_path = self.full_folder_path
74
+
75
+ ### Create a file name like log_timeStamp_description_.txt ###
76
+ if not (description is None or description == ''):
77
+ description = f"_{description}"
78
+ else:
79
+ description = ''
80
+ file_name = f"log_{time_stamp()}{description}_.txt"
81
+ self.full_file_path = os.path.join(full_folder_path, file_name)
82
+
83
+
84
+ def start_file(self):
85
+ try:
86
+ self.file = open(self.full_file_path, 'a')
87
+ self.log('Logging file started.', log='important')
88
+ except:
89
+ print(f'{time_stamp()} Logger failed to open the log file \"{self.full_file_path}\".', log='important')
90
+
91
+
92
+ def close_file(self): # If closed, you'll need to restart before logging.
93
+ self.log('Logger is closing log file.', log='important')
94
+ self.file.close()
95
+ self.file = None
96
+ ### END: Initializing Tools
97
+
98
+
99
+ ### BEGIN: logging Tools
100
+ def decorated_msg(self, msg):
101
+ decorated_msg = time_stamp() + ' ' + msg + '\n'
102
+ return decorated_msg
103
+
104
+
105
+ def write_to_file(self, msg):
106
+ if self.file:
107
+ self.file.write(msg)
108
+ ### flushing the log file to make sure it's written ###
109
+ self.file.flush()
110
+ os.fsync(self.file)
111
+ else:
112
+ self.file_error = True
113
+ print(f'{time_stamp()} Logger is trying to write to a closed or non-existent file.')
114
+
115
+
116
+ def log(self, msg, log='default'):
117
+ ### This is the main function. Log options: 'screen', 'file', 'both', 'no', 'default', 'important'
118
+ if log == 'important' and self.log_mode == 'no':
119
+ log = 'screen'
120
+ elif log == 'important' and self.log_mode == 'file':
121
+ log = 'both'
122
+ elif (log == 'important') or(log == 'default') or (log is None):
123
+ log = self.log_mode
124
+
125
+ decorated_message = self.decorated_msg(msg)
126
+
127
+ if log == 'both':
128
+ print(self.decorated_msg(msg))
129
+ self.write_to_file(decorated_message)
130
+ elif log == 'file':
131
+ self.write_to_file(decorated_message)
132
+ elif log == 'no':
133
+ pass
134
+ else: # log == 'screen' or anything else
135
+ print(decorated_message)
136
+ ### END: logging Tools
137
+
138
+
139
+ ### BEGIN: OBJ2FILE TOOLS:
140
+ def __getargs__(self):
141
+ logger_args = self.__getstate__()
142
+ del logger_args['log_mode']
143
+ del logger_args['file_error']
144
+ del logger_args['file']
145
+ return logger_args
146
+
147
+ def __getstate__(self):
148
+ seriable_data = self.__dict__.copy()
149
+ return seriable_data
150
+ ### BEGIN: OBJ2FILE TOOLS:
151
+
152
+
153
+ ### Use this function in other libraries if needed for debugging -- Not really needed
154
+ DEBUG = False
155
+ def debug(msg):
156
+ if DEBUG:
157
+ print(msg)
158
+ return msg+'\n'
159
+ else:
160
+ return msg
161
+ ###
barsukov/obj2file.py ADDED
@@ -0,0 +1,93 @@
1
+ ### BEGIN Dependencies ###
2
+ import dill
3
+ import os
4
+ from barsukov.logger import debug
5
+ ### END Dependencies ###
6
+
7
+
8
+ def save_object(obj, file_name, update=False, full_folder_path=None, script=None):
9
+ """
10
+ Saves a class object to a file using the '.pickle' format.
11
+
12
+ Args:
13
+ obj (object): An instance of a class to be saved. (Required)
14
+ file_name (str): Name of the file (must end with '.pickle'). (required)
15
+ update (bool, optional): if 'True', overwrites an existing file. Defaults to 'False'.
16
+ full_folder_path (str, optional): absolute path to the directory (e.g., "C://Users//John//Documents").
17
+ Defaults to the current working directory if not provided.
18
+ script (Script, optional): Script object providing directory information.
19
+ Defaults to 'None'.
20
+
21
+ Returns:
22
+ str: Confirmation message upon successful save.
23
+
24
+ Raises:
25
+ FileExistsError: If the file already exists and 'update=False'.
26
+
27
+ Example:
28
+ >>> equipment = mwHP(...)
29
+ >>> save_object(obj=equipment, file_name="mwHP.pickle")
30
+ 'equipment successfully saved in mwHP.pickle.'
31
+ """
32
+ # Check if full_folder_path is provided and is an absolute path:
33
+ if full_folder_path and not os.path.isabs(full_folder_path):
34
+ return debug(f"Please provide an absolute path (e.g., 'C://Users//John//Documents').")
35
+
36
+ #Set folder_path based on the provided or default to current directory
37
+ full_folder_path = full_folder_path or (script.full_folder_path if script else os.getcwd())
38
+ full_file_path = os.path.join(full_folder_path, file_name)
39
+
40
+ if update:
41
+ # If update is True, overwrite the file
42
+ with open(full_file_path, 'wb') as file:
43
+ dill.dump(obj, file)
44
+ return debug(f"{obj} object successfully saved.")
45
+ else:
46
+ try:
47
+ # Try to create the file and save the object
48
+ with open(full_file_path, 'xb') as file:
49
+ dill.dump(obj, file)
50
+ return debug(f"{obj} successfully saved in {file_name}.")
51
+ except FileExistsError:
52
+ # If the file already exists, provide a message
53
+ raise FileExistsError(f"File '{file_name}' already exists. Use 'update=True' to overwrite.")
54
+
55
+
56
+ def load_object(file_name, full_folder_path=None, script=None):
57
+ """
58
+ Loads a class object stored in a '.pickle' formatted file.
59
+
60
+ Args:
61
+ file_name (str): Class object file (must end in '.pickle'). (Required)
62
+ full_folder_path (str, optional): absolute path to the directory where file exists (excluding the file name).
63
+ Defaults to the current working directory.
64
+ script (Script, optional): Script object providing directory information.
65
+ Defaults to 'None'.
66
+
67
+ Returns:
68
+ object: The loaded class object.
69
+
70
+ Raises:
71
+ FileNotFoundError: If the file doesn't exist in the specified location.
72
+
73
+ Example:
74
+ >>> new_equipment = load_object(file_name="mw.pickle")
75
+ 'Object successfully loaded from mw.pickle.'
76
+ """
77
+ # Check if the provided full_folder_path is absolute
78
+ if full_folder_path and not os.path.isabs(full_folder_path):
79
+ return debug(f"Please provide an absolute path (e.g., 'C://Users//John//Documents').")
80
+
81
+ # Set full_folder_path based on provided or default to current directory
82
+ full_folder_path = full_folder_path or (script.full_folder_path if script else os.getcwd())
83
+ full_file_path = os.path.join(full_folder_path, file_name)
84
+
85
+ # Check if the file exists at the specified path
86
+ if not os.path.isabs(full_file_path):
87
+ raise FileNotFoundError(f"The object file {file_name} does not exist at the specified directory.")
88
+
89
+ # load the object from the file
90
+ with open(full_file_path, 'rb') as file:
91
+ instance = dill.load(file)
92
+ print(debug(f'Object successfully loaded from {file_name}.'))
93
+ return instance
barsukov/script.py ADDED
@@ -0,0 +1,153 @@
1
+ ### BEGIN Dependencies ###
2
+ import sys
3
+ import os
4
+ from barsukov.time import *
5
+ from barsukov.logger import Logger
6
+ ### END Dependencies
7
+
8
+
9
+ class Script():
10
+ """
11
+ A class that represents a scientific experiment script, managing logging, file handing, and device initialization.
12
+
13
+ Args:
14
+ RECOMMENDED:
15
+ operator (str): The name of the operator (default: 'Anon').
16
+ station (str): The station of the experiment (default: 'No-Station').
17
+ sample (str): The sample being tested (default: 'No-Sample').
18
+ description (str): A brief description of the experiment (default: 'No-Description').
19
+ project_folder (str): The base folder for the project files (default: current directory).
20
+
21
+ OPTIONAL:
22
+ log (str, optional): The logging configuration (default: 'both')
23
+ This is the default log setting which will be passed to the logger.
24
+ It will be overriden by other objects, which in turn will be overriden by methods.
25
+ Choose here and everywhere from 'screen', 'file', 'both', 'no'.
26
+
27
+ BEST NOT TO CHANGE:
28
+ log_full_folder_path (str, optional): Path to save logs (default: current directory).
29
+ log_full_file_path (str, optional): Full path for log file (default: None).
30
+
31
+ Attributes:
32
+ operator (str): The operator's name (e.g., 'ib', 'Rundong', 'Sasha', 'Ameerah', 'Steven', 'Alex', or 'AlexH' if ambiguous).
33
+ station (str): The station where the experiment is conducted (e.g., 'qd', 'ppms', 'mseppms', 'data', 'orange', ...).
34
+ sample (str): The sample being used in the experiment (e.g. 'cro2410a1').
35
+ description (str): A brief description of the experiment (e.g., 'Testing modulation').
36
+ project_folder (str): Absolute path to the directory (e.g., 'D:/Rundong/Projects/AFM sims/2024-07-06 Autooscillations').
37
+ folder_name (str): A generated folder name based on the experiment details.
38
+ full_folder_path (str): The full path to the experiment folder.
39
+ logger (Logger): A Logger instance for logging experiment data.
40
+ rm (ResourceManager or None): The pyvisa ResourceManager used for controlling instruments.
41
+ """
42
+ ### BEGIN: Initializing tools
43
+ def __init__(self,
44
+ operator='Anon',
45
+ station='No-Station',
46
+ sample='No-Sample',
47
+ description='No-Description',
48
+ project_folder = os.getcwd(),
49
+ log='both',
50
+ log_full_folder_path=os.getcwd(),
51
+ log_full_file_path=None,
52
+ ):
53
+
54
+ ### Description Attributes
55
+ self.operator = operator
56
+ self.station = station
57
+ self.sample = sample
58
+ self.description = description
59
+ self.project_folder = project_folder
60
+
61
+ ### Creating the sub-project folder
62
+ self.folder_name = f"{date()}_{self.station}_{self.operator}_{self.sample}_{self.description}"
63
+ self.full_folder_path = os.path.join(self.project_folder, self.folder_name)
64
+ os.makedirs(self.full_folder_path, exist_ok=True)
65
+
66
+ ### Logger Attributes
67
+ self.start_logger = True
68
+ self.log_mode = log
69
+ self.log_full_folder_path = log_full_folder_path
70
+ self.log_full_file_path = log_full_file_path
71
+ self.init_logger(start=self.start_logger)
72
+ self.logger_name = self.logger.full_file_path
73
+
74
+ ### Equipment Attributes
75
+ self.rm = None
76
+ self.equipment = None
77
+
78
+ def init_logger(self, start):
79
+ ### Starting the logger
80
+ self.logger = Logger(
81
+ description=f"{self.operator}_{self.description}",
82
+ full_folder_path=self.log_full_folder_path,
83
+ full_file_path=self.log_full_file_path,
84
+ log=self.log_mode, # Script.log becomes Logger's default
85
+ start_file=start)
86
+ self.logger.log(f'Script object initialized. Logger started.', log='both')
87
+
88
+
89
+ def log(self, msg, log='default'):
90
+ """
91
+ Logs a message using the Script object's logger.
92
+
93
+ Args:
94
+ msg (str): The message to log.
95
+ log (str, optional): The log destination (e.g., 'screen', 'file', 'both'). Defaults to the 'default' of the logger
96
+ """
97
+ self.logger.log(msg, log=log)
98
+ ### END: Initializing tools
99
+
100
+
101
+ ### BEGIN: Equipment related stuff
102
+ def init_rm(self):
103
+ # Initializes the pyvisa ResourceManager for controlling instruments.
104
+ # Returns ResourceManager The pyvisa ResourceManager object.
105
+ # Raises SystemExit If pyvisa cannot be imported or the ResourceManager cannot be initialized.
106
+ self.equipment = True
107
+ import pyvisa as visa
108
+ try:
109
+ self.rm = visa.ResourceManager()
110
+ except:
111
+ self.rm = None
112
+ self.log('Script could not import pyvisa.', log='important')
113
+ sys.exit()
114
+ self.log(f'Script started pyvisa.ResourceManager.', log='both')
115
+ return self.rm
116
+ ### END: Equipment related stuff
117
+
118
+
119
+ ### BEGIN: Equipment devices
120
+ def mwHP(self, gpib=None, **kwargs):
121
+ """
122
+ Initializes and returns a mwHP equipment object with the specified parameters.
123
+
124
+ Args:
125
+ gpib (str): gpib number of equipment. Defaults to 'None'.
126
+ **kwargs: Additional keyword arguments passed to the mwHP equipment initialization.
127
+
128
+ Returns:
129
+ mwHP: A mwHP equipment object.
130
+ """
131
+ from barsukov.exp.mwHP import mwHP as eq
132
+ return eq(gpib, logger=self.logger, script=self, **kwargs)
133
+ ### END: Equipment devices
134
+
135
+
136
+ ### BEGIN: OBJ2FILE TOOLS:
137
+ def __getstate__(self):
138
+ # Prepares the Script object for serialization by removing non-seriable attributes (e.g. logger and rm).
139
+ # Returns a dict: A dictionary representing the serializable state of the Script object.
140
+ seriable_data = self.__dict__.copy()
141
+ seriable_data['start_log'] = False
142
+ del seriable_data['logger']
143
+ del seriable_data['rm']
144
+ return seriable_data
145
+
146
+
147
+ def __setstate__(self, seriable_data):
148
+ # Restores the Script object from its serialized state, including reinitializing the logger and rm.
149
+ # Args: seriable_data (dict): A dictionary representing the serialized state of the Script object
150
+ self.__dict__.update(seriable_data)
151
+ self.init_logger(self.start_logger)
152
+ if self.equipment is True: self.init_rm()
153
+ ### END: OBJ2FILE TOOLS:
barsukov/time.py ADDED
@@ -0,0 +1,37 @@
1
+ ### BEGIN Dependencies ###
2
+ import datetime
3
+ from pytz import timezone
4
+ ### END Dependencies ###
5
+
6
+ TIMEZONE = timezone('America/Los_Angeles')
7
+
8
+ def time_stamp():
9
+ """
10
+ Generates a timestamp in the format 'YYYY-MM-DD_HH-MM-SSS', where the last digit represents 1/10th of a second based on the current date and time.
11
+
12
+ returns:
13
+ str: A string representing the current date and time in the format 'YYYY-MM-DD_HH-MM-SSS'.
14
+
15
+ Example:
16
+ >>> time_stamp()
17
+ '2025-01-28_17-27-210'
18
+ """
19
+ now = datetime.datetime.now(TIMEZONE)
20
+ formatted_datetime = now.strftime(f"%Y-%m-%d_%H-%M-%S") + str(int( now.microsecond / 100000 ))
21
+ return formatted_datetime
22
+
23
+
24
+ def date():
25
+ """
26
+ Generates the current date in the format 'YYYY-MM-DD'.
27
+
28
+ Returns:
29
+ str: A string representing the current date in the format 'YYYY-MM-DD'.
30
+
31
+ Example:
32
+ >>> date()
33
+ '2025-01-28'
34
+ """
35
+ now = datetime.datetime.now(TIMEZONE)
36
+ formatted_datetime = now.strftime(f"%Y-%m-%d")
37
+ return formatted_datetime
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.2
2
+ Name: barsukov
3
+ Version: 0.0.0
4
+ Summary: Experiment Automation Package
5
+ Author-email: Igor Barsukov <igorb@ucr.edu>, Steven Castaneda <scast206@ucr.edu>
6
+ Project-URL: Homepage, https://barsukov.ucr.edu
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.6
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pytz>=2014.10
12
+ Requires-Dist: numpy>=1.0.0
13
+ Requires-Dist: scipy>=0.9.0
14
+
15
+ # Barsukov
16
+
17
+ Barsukov is a Python library for experiment automation.
18
+
19
+ ## Installation
20
+
21
+ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install barsukov.
22
+
23
+ ```bash
24
+ pip install barsukov
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ #
31
+ #
32
+ #
33
+ #
34
+ #
35
+ #
36
+ #
37
+ ```
38
+
39
+ ## Contributing
40
+
41
+ -
42
+ -
43
+ -
44
+
45
+ ## License
46
+
47
+ [MIT](https://choosealicense.com/licenses/mit/)
@@ -0,0 +1,14 @@
1
+ barsukov/__init__.py,sha256=GVsEnM_uD4x1XN_PIKdD-VSRMNUnvPr2iCWuEHwM6sU,293
2
+ barsukov/logger.py,sha256=nrkS1Pg3WGEjRJQ5MbplhyGYGbfFGkgyxgx1qdSu1x0,6489
3
+ barsukov/obj2file.py,sha256=nivCmCEpmOSaG4VpQgIDb-7hsvdEq45Iuu5HOfRxbcw,3923
4
+ barsukov/script.py,sha256=nrKwEl60u9ces3B8FwRfY4VmlEx5bNvk_hoPaylldP0,6359
5
+ barsukov/time.py,sha256=fSf5JKqr6Pd5691qQcFuBsjDd9alMrfASnndlstLits,1039
6
+ barsukov/data/__init__.py,sha256=IMnOEliXsRMPWeCTprPSddRKg9kwfV-neQiwUwHdpqs,19
7
+ barsukov/data/fft.py,sha256=f9aPLeusVpWiWmXO5n4XwkfQ9xJQhZVFdyhFoT9DB2A,4365
8
+ barsukov/exp/__init__.py,sha256=urLfGpap40kN9ULi53JB0NT-iMsZDSFdBmdSq3ckB0E,19
9
+ barsukov/exp/exp_utils.py,sha256=7qVQJbJGbsNW0JZQ7A1cmI73J6vi_aN8Zu6WMq2-7LE,4789
10
+ barsukov/exp/mwHP.py,sha256=eoX82jon5nIsExvRHO1PIOQAWWWhJYY4N21VtoLXuSw,12136
11
+ barsukov-0.0.0.dist-info/METADATA,sha256=H_KUEI5ZW4N0sz2pQIOJu-hO9501NDH4ujZvHYgCzlE,791
12
+ barsukov-0.0.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
+ barsukov-0.0.0.dist-info/top_level.txt,sha256=Js5sHbNjP0UNMB9O5HtCHZqlfHabuNS8nTsHbg-1DDQ,9
14
+ barsukov-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ barsukov