barsukov 1.0.8__py3-none-any.whl → 1.1.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 CHANGED
@@ -1,15 +1,18 @@
1
1
  # Modules:
2
2
  from . import time
3
3
  from . import data
4
+
4
5
 
5
6
  # Objects/Functions:
6
7
  from .script import Script
7
8
  from .logger import Logger
8
- from .testpickle import *
9
+
10
+ from .obj2file import *
11
+
9
12
 
10
13
  # Equipment Objects:
11
14
  from .exp.mwHP import mwHP
12
15
 
13
- __all__ = ["time", "formula", "data", "Script", "Logger", "mwHP"]
16
+ __all__ = ["time", "data", "save_object", "load_object", "Script", "Logger", "mwHP"]
14
17
 
15
18
 
barsukov/exp/exp_utils.py CHANGED
@@ -5,10 +5,6 @@ from barsukov.time import *
5
5
  ### END Dependencies
6
6
 
7
7
 
8
-
9
-
10
-
11
-
12
8
  ### BEGIN Helper functions
13
9
 
14
10
  def log_in_eq(eq_obj, msg, log='default'): # FINISHED 2024/10/26
@@ -19,6 +15,7 @@ def log_in_eq(eq_obj, msg, log='default'): # FINISHED 2024/10/26
19
15
  else:
20
16
  eq_obj.logger.log(decorated_msg, log)
21
17
 
18
+
22
19
  def initialize_gpib(eq_obj):
23
20
  # Initializes a visa.open_resource(). Returns rm.open_resource(). Exits if error.
24
21
  if eq_obj.rm is None: # eq_obj has no ResourceManager
@@ -60,6 +57,7 @@ def initialize_gpib(eq_obj):
60
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')
61
58
  sys.exit()
62
59
 
60
+
63
61
  def eq_disconnect(eq_obj):
64
62
  try:
65
63
  eq_obj.rm.close()
@@ -67,7 +65,8 @@ def eq_disconnect(eq_obj):
67
65
  eq_obj.log( f'Successfully disconnected GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='screen')
68
66
  except:
69
67
  eq_obj.log( f'Failed to disconnect GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='screen')
70
-
68
+
69
+
71
70
  def eq_reconnect(eq_obj):
72
71
  try:
73
72
  import pyvisa as visa
@@ -77,6 +76,7 @@ def eq_reconnect(eq_obj):
77
76
  eq_obj.log( f'Initialized: {eq_obj.identify()}', log='important' )
78
77
  except:
79
78
  eq_obj.log(f'Failed to reconnect GPIB resource for {eq_obj.gpib_card}::{eq_obj.gpib}.', log='important')
79
+
80
80
  ### END Helper functions
81
81
 
82
82
 
barsukov/exp/mwHP.py CHANGED
@@ -4,7 +4,6 @@ import sys
4
4
  from barsukov.exp.exp_utils import *
5
5
  ### END Dependencies
6
6
 
7
- # STATUS: f() finished, nor test it and then add more funcitons
8
7
 
9
8
  class mwHP:
10
9
  def __init__(self, gpib=None, visa_rm=None, logger=None, gpib_card=0, log='default', script=None):
@@ -23,28 +22,19 @@ class mwHP:
23
22
  self.gpib = gpib
24
23
 
25
24
  self.msg_deco = f'[mwHP {self.gpib_card}::{self.gpib}]'
26
- #print( 'visa_rm is ', visa_rm )
27
- #print( 'script is ', script )
28
25
  self.eq = initialize_gpib(self) # This will initialize self.eq = visa.open_resource()
29
26
  self.log( f'Initialized: {self.identify()}', log='important' ) # This is the 'welcome message' and a check if communication works.
30
27
 
31
28
  self.f_digits = 9 # Digits of precision of mw frequency
32
29
  self.f_limits = [0.01, 20.5] # Lower and upper GHz limits
33
- self.p_digits = 1 #TODO -- Digits of precision of mw power
34
- self.p_limits = [-15.0, 17.0] #TODO -- Lower and upper __ limits
35
- self.pulsef_digits = 3 # digits in KHz
36
- self.pulsew_digits = 3 # digits in ms
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]
37
33
  self.pulsef_limits = [0.016, 500]
38
- self.pulsew_limits = [0.001, 65.53] # digits in ms
39
- self.pulsedc_limits = [0, 100]
40
-
41
- def disconnect(self):
42
- eq_disconnect(self)
43
-
44
- def reconnect(self):
45
- eq_reconnect(self)
46
-
47
- ### BEGIN The definition of the following functions may be specific to this equipment.
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.
48
38
  def query(self, cmd):
49
39
  return self.eq.query(cmd)
50
40
 
@@ -53,12 +43,47 @@ class mwHP:
53
43
 
54
44
  def identify(self):
55
45
  return str(self.eq.query('*IDN?'))
56
- ### END The definition of the following functions may be specific to this equipment.
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)
57
52
 
58
53
  def log(self, msg, log=None):
59
54
  if log is None: log=self.eq_default_log
60
55
  log_in_eq(self, msg, log=log)
61
- ### END These functions could be shared across all equipment.
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
62
87
 
63
88
 
64
89
  def f(self, f=None, log=None, check=False):
@@ -85,7 +110,6 @@ class mwHP:
85
110
  self.eq.write(f'freq {x} GHz')
86
111
  if check: y = self.f(log='no')
87
112
  else: y = x
88
- print(y, f)
89
113
  if abs(y-f)<10.0**(-self.f_digits): self.log(f'Writing f as {x}.', log=log)
90
114
  else: self.log(f'Warning: writing Frequency as {x}, but was asked {f}.', log='important')
91
115
  return y
@@ -93,6 +117,7 @@ class mwHP:
93
117
  self.log(f'Error while writing Frequency as {f}.', log='important')
94
118
  return np.nan
95
119
 
120
+
96
121
  def p(self, p=None, log=None, check=False):
97
122
  ### Always has a return! Which is the power in dBm.
98
123
  ### p() reads and returns the power in dBm.
@@ -124,68 +149,115 @@ class mwHP:
124
149
  self.log(f'Error while writing Power as {p}.', log='important')
125
150
  return np.nan
126
151
 
127
- def output(self, state=None, log=None, check=False):
128
- ### Always has a return! Which is the state of Output.
129
- ### output() reads and returns the state of Output.
130
- ### output(1) writes state of Output to ON.
131
- ### output(1) returns the state that was actually sent to equipment.
132
- ### output(1, check=True) returns the state queried after writing
152
+
153
+ def sweep(self, start, stop, step, dwell, mode, log=None):
133
154
  if log is None: log=self.eq_default_log
134
- if state is None:
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:
135
189
  try:
136
- y = self.eq.query('output?')
137
- y = int(y)
138
- self.log(f'Output is {y}.')
139
- return y
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
140
200
  except:
141
- self.log(f'Error while reading Output.', log='important')
201
+ self.log(f'Error while reading Pulse state.', log='important')
142
202
  return np.nan
143
203
  else:
144
- if (state == 1) or (state == 'on') or (state=='ON') or (state=='On'): sstate = 1
145
- else: sstate = 0
204
+ if duty is None: duty = 50.0
146
205
  try:
147
- self.eq.write(f'output {sstate}')
148
- if check: y=self.output(log='no')
149
- else: y = sstate
150
- if y == state: self.log(f'Output set to {sstate}.')
151
- else: self.log(f'Warning: Setting Output to {sstate}, but was asked for {state}.')
152
- return y
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')
153
225
  except:
154
- self.log(f'Error while changing Output state.', log='important')
226
+ self.log(f'Error while writing pulse frequency as {f} and duty cycle as {duty}', log='important')
155
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
156
234
 
157
- def pulse(self, f=None, duty=None, log=None):
158
- if log is None: log=self.eq_default_log
159
- if f is None and duty is None:
160
- T = float(self.eq.query('puls:per?')) * 10.0**3
161
- f = 1.0 / T
162
- w = float(self.eq.query('puls:widt?')) * 10.0**3
163
- duty = w / T * 100.0
164
- y = self.eq.query('pulm:stat?')
165
- y = int(y)
166
- x = self.eq.query('pulm:sour?')
167
- x = x[:-1].lower()
168
- self.log(f'Pulse Frequency {f} KHz, duty-cycle {duty}%. state {y}, source {x}.', log=log)
169
- return f, duty
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__()
170
246
  else:
171
- if f is None and duty is not None:
172
- duty_write = max(self.pulsedc_limits[0], min(float(duty), self.pulsedc_limits[1]))
173
- T = float(self.eq.query('puls:per?')) * 10.0**3
174
- w = duty_write * T / 100.0
175
- self.eq.write(f'puls:widt {w} ms')
176
- elif f is not None and duty is None:
177
- f_write = max(self.pulsef_limits[0], min(float(f), self.pulsef_limits[1]))
178
- duty_write = 50.0
179
- T = 1.0 / f
180
- w = duty_write * T / 100.0
181
- self.eq.write(f'puls:per {T} ms')
182
- self.eq.write(f'puls:widt {w} ms')
183
- elif f is not None and duty is not None:
184
- f_write = max(self.pulsef_limits[0], min(float(f), self.pulsef_limits[1]))
185
- duty_write = max(self.pulsedc_limits[0], min(float(duty), self.pulsedc_limits[1]))
186
- T = 1.0 / f
187
- w = duty_write * T / 100.0
188
- self.eq.write(f'puls:per {T} ms')
189
- self.eq.write(f'puls:widt {w} ms')
190
- check = self.pulse()
191
- return check
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 CHANGED
@@ -5,6 +5,27 @@ from barsukov.time import time_stamp
5
5
 
6
6
  class Logger:
7
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
+ """
8
29
  ### Don't create multiple Logger objects.
9
30
  ### Logger can be opened and closed with start() and close() methods.
10
31
  ### Restarting the logger will log to the same file, unless self.full_file_path has been changed by hand.
@@ -14,59 +35,52 @@ class Logger:
14
35
  ### The log='important' is for the Logger.log() method only. Try not to use it.
15
36
  ### Default hyerarchy is logger-default overriden by object-default overriden by instance.
16
37
  ### Instances, that log errors, will usually use 'both'.
17
- ###
18
- """
38
+
39
+ ### BEGIN: Initializing Tools
19
40
  def __init__(self,
20
41
  description=None, # Will be passed by Script. If not, write a brief description!
21
42
  full_folder_path=os.getcwd(), # Will be passed by Script. If not, specify!
22
43
  full_file_path=None, # Specify only if you want to log into an already existing file.
23
44
  log='both', # Logger default will be passed by Script. If not, you may choose to change.
24
- start_file=True
45
+ start_file=True,
25
46
  ):
26
47
 
27
- ### Initializing all variables before setting/getting them ###
48
+ ### Initializing all variables before setting/getting them
28
49
  self.full_file_path = full_file_path # If changed by hand later, needs start() to take effect
29
50
  self.description = description # If changed by hand later, needs set_full_file_path and start() to take effect
30
51
  self.log_mode = log # Default log mode can be changed by hand any time, no restart needed.
31
52
 
32
- if self.full_file_path is not None:
33
- self.full_folder_path = None
53
+ if self.full_file_path is not None: self.full_folder_path = None
34
54
  else:
35
55
  self.full_folder_path = full_folder_path # If changed by hand later, needs set_full_file_path and start() to take effect
36
56
  self.set_full_file_path(description=description, full_folder_path=full_folder_path)
37
-
38
- self.file_error = False # If a problem with file, will be set to True. Just in case for later.
39
- # If problem is remedied, you need to set file_error to False by hand.
40
-
41
- #
42
- if start_file:
43
- self.start_file()
44
- else:
45
- self.file = None
46
-
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
+
47
65
  self.log(f'Logger initialization complete.', log='important')
48
-
49
- def to_dict(self):
50
- seriable_data = self.__dict__.copy()
51
- return seriable_data
52
-
66
+
67
+
53
68
  def set_full_file_path(self, description=None, full_folder_path=None):
54
69
  ### Checking if optional arguments are filled or if defaults are provided ###
55
70
  if description is None:
56
71
  description = self.description
57
72
  if full_folder_path is None:
58
73
  full_folder_path = self.full_folder_path
59
- #
74
+
60
75
  ### Create a file name like log_timeStamp_description_.txt ###
61
76
  if not (description is None or description == ''):
62
- description = '_' + description
77
+ description = f"_{description}"
63
78
  else:
64
79
  description = ''
65
- file_name = 'log_' + time_stamp() + description + '_.txt'
66
- #print(file_name)
67
- #print(full_folder_path)
80
+ file_name = f"log_{time_stamp()}{description}_.txt"
68
81
  self.full_file_path = os.path.join(full_folder_path, file_name)
69
82
 
83
+
70
84
  def start_file(self):
71
85
  try:
72
86
  self.file = open(self.full_file_path, 'a')
@@ -74,15 +88,20 @@ class Logger:
74
88
  except:
75
89
  print(f'{time_stamp()} Logger failed to open the log file \"{self.full_file_path}\".', log='important')
76
90
 
91
+
77
92
  def close_file(self): # If closed, you'll need to restart before logging.
78
93
  self.log('Logger is closing log file.', log='important')
79
94
  self.file.close()
80
95
  self.file = None
96
+ ### END: Initializing Tools
97
+
81
98
 
99
+ ### BEGIN: logging Tools
82
100
  def decorated_msg(self, msg):
83
101
  decorated_msg = time_stamp() + ' ' + msg + '\n'
84
102
  return decorated_msg
85
103
 
104
+
86
105
  def write_to_file(self, msg):
87
106
  if self.file:
88
107
  self.file.write(msg)
@@ -93,6 +112,7 @@ class Logger:
93
112
  self.file_error = True
94
113
  print(f'{time_stamp()} Logger is trying to write to a closed or non-existent file.')
95
114
 
115
+
96
116
  def log(self, msg, log='default'):
97
117
  ### This is the main function. Log options: 'screen', 'file', 'both', 'no', 'default', 'important'
98
118
  if log == 'important' and self.log_mode == 'no':
@@ -103,7 +123,7 @@ class Logger:
103
123
  log = self.log_mode
104
124
 
105
125
  decorated_message = self.decorated_msg(msg)
106
-
126
+
107
127
  if log == 'both':
108
128
  print(self.decorated_msg(msg))
109
129
  self.write_to_file(decorated_message)
@@ -113,9 +133,23 @@ class Logger:
113
133
  pass
114
134
  else: # log == 'screen' or anything else
115
135
  print(decorated_message)
116
-
117
-
118
-
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
+
119
153
  ### Use this function in other libraries if needed for debugging -- Not really needed
120
154
  DEBUG = False
121
155
  def debug(msg):
barsukov/obj2file.py CHANGED
@@ -1,105 +1,93 @@
1
- # This is unfinished
2
-
3
-
4
- #import pickle
1
+ ### BEGIN Dependencies ###
2
+ import dill
5
3
  import os
6
- #import glob
7
-
8
- import obj_name, time_stamp
9
-
10
- class Obj2File:
11
- def __init__(self, logger=None, log='default', script=None):
12
- self.script = script
13
- self.logger = logger
14
- self.default_log = log
15
- self.msg_deco = f'[Obj2File]'
16
-
17
- if self.logger is None:
18
- try: self.logger = self.script.logger
19
- except: pass
20
-
21
- self.mtj_calib_directory = '/Users/alexandrakorotneva/Desktop/2024-11-18 obj import test'
22
- self.tosave = {}
23
-
24
-
25
- def log(self, msg, log=None):
26
- if log is None: log = self.default_log
27
- if self.logger is not None:
28
- try:
29
- self.logger.log(f'{self.msg_deco} {msg}', log=log)
30
- return
31
- except: pass
32
- print(f'{time_stamp()} {self.msg_deco} {msg}')
33
-
34
- def save(self, obj, directory=None, full_file=None, short_file=None, log='default'):
35
- if full_file is None:
36
- if directory is None: directory = self.script.full_folder_path #current measurement folder
37
- if short_file is None: short_file = f'{time_stamp()}_{obj_name(obj)}.pkl'
38
- full_file = os.path.join(directory, short_file)
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:
39
46
  try:
40
- with open(full_file, 'wb') as file:
41
- pickle.dump(obj, file)
42
- self.log(f'Object {obj_name(obj)} is written to file {full_file}.', log=log)
43
- except:
44
- self.log('Error: could not write object to file.', log='important')
45
-
46
- def load(self, directory=None, full_file=None, short_file=None, log='default'):
47
- if full_file is None:
48
- if directory is None:
49
- directory = self.script.full_folder_path
50
- if short_file is None:
51
- files = [f for f in glob.glob(os.path.join(directory, '*.pkl'))]
52
- short_file = files[0]
53
- full_file = os.path.join(directory, short_file)
54
-
55
- try:
56
- with open(filepath, 'rb') as file:
57
- toload = pickle.load(file)
58
- self.log(f'Object is uploaded from file {full_file}.', log=log)
59
- except:
60
- self.log('Error: could not upload object from file.', log='important')
61
- return toload
62
-
63
-
64
-
65
-
66
- def mtj_fieldcalib(self, directory=None, upd=False, log='default', spl_ind=None):
67
- if directory is None: directory = self.mtj_calib_directory
68
- else:
69
- if upd:
70
- self.mtj_calib_directory = directory
71
- self.log(f'MTJ station field calibration directory has been updated to {directory}', log='important')
72
-
73
- files = [f for f in glob.glob(os.path.join(directory, '*.pkl'))]
74
- if len(files)==1:
75
- short_file = files[0]
76
-
77
- full_file = os.path.join(directory, short_file)
78
- splines = self.load(full_file=full_file, log=log)
79
-
80
- if spl_ind is None:
81
- return splines
82
- elif spl_ind=='c':
83
- return splines['cheap_to_H']
84
- elif spl_ind=='v':
85
- return Vsplines['V_to_H']
86
- elif spl_ind=='h':
87
- return splines['H_to_V']
88
-
89
- def pack_tosave(self, obj, obj_name=None, overwrite=False, save=False):
90
- #prepares a dictionary {'obj name':obj, ...}
91
- tosave = self.tosave
92
- if obj_name is None: obj_name = obj_name(obj)
93
-
94
- if overwrite:
95
- tosave.update({obj_name : obj})
96
- else:
97
- add = tosave.setdefault(obj_name, obj) #for already excisting name returns corresponding element and doesn't overwrite
98
- if add!=obj:
99
- print(f'Overwriting attempt!')
100
-
101
- self.tosave = tosave
102
- if save:
103
- self.save(tosave)
104
- return tosave
105
-
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 CHANGED
@@ -1,85 +1,110 @@
1
+ ### BEGIN Dependencies ###
2
+ import sys
3
+ import os
1
4
  from barsukov.time import *
2
5
  from barsukov.logger import Logger
6
+ ### END Dependencies
3
7
 
4
- import sys
5
- import os
6
8
 
7
9
  class Script():
8
- def __init__(self,
9
- ### Please ALWAYS specify the following:
10
- operator='Anon', # ib, Rundong, Sasha, Ameerah, Alex, or AlexH if ambiguous
11
- station='No Stn', # qd, ppms, mseppms, data, orange, ...
12
- sample='No Sample', # Use sample name from the sample table, e.g. "cro2410a1" or "yig2207"
13
- description='No Description', # Briefly: what are you doing. Sets the folder name.
14
- # i.e. 'Testing modulation' or 'fH OOP long average'
15
- project_folder = os.getcwd(), # Full path to your project folder. Please follow this convention:
16
- # D:/Rundong/Projects/AFM sims/2024-07-06 Autooscillations
17
- # Will grab the current directory if not specified.
18
-
19
- ### Optional:
20
- log='both', # This is the default log setting which will be passed to the logger.
21
- ### It will be overriden by other objects,
22
- ### which in turn will be overriden by methods.
23
- ### Choose here and everywhere from screen, file, both, no.
24
-
25
- ### Usually, it's better not to change these:
26
- log_full_folder_path=None,
27
- log_full_file_path=None,
28
- **kwargs
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,
29
52
  ):
30
-
53
+
54
+ ### Description Attributes
31
55
  self.operator = operator
32
56
  self.station = station
33
57
  self.sample = sample
34
58
  self.description = description
35
59
  self.project_folder = project_folder
36
60
 
37
- self.rm = None
38
-
39
61
  ### Creating the sub-project folder
40
- self.folder_name = date() + '_' + self.station + '_' + self.operator + '_' + self.sample + '_' + self.description
62
+ self.folder_name = f"{date()}_{self.station}_{self.operator}_{self.sample}_{self.description}"
41
63
  self.full_folder_path = os.path.join(self.project_folder, self.folder_name)
42
64
  os.makedirs(self.full_folder_path, exist_ok=True)
43
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):
44
79
  ### Starting the logger
45
- if log_full_folder_path is None:
46
- self.logger = Logger(
47
- description=self.operator + '_' + self.description,
48
- full_file_path=log_full_file_path,
49
- log=log, # Script.log becomes Logger's default
50
- start_file=True)
51
- else:
52
- self.logger = Logger(
53
- description=self.operator + '_' + self.description,
54
- full_folder_path=log_full_folder_path,
55
- full_file_path=log_full_file_path,
56
- log=log, # Script.log becomes Logger's default
57
- start_file=True)
58
-
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)
59
86
  self.logger.log(f'Script object initialized. Logger started.', log='both')
60
-
61
- def to_dict(self):
62
- seriable_data = self.__dict__.copy()
63
- # take the attributes of unseriable data
64
- del seriable_data['logger']
65
- del seriable_data['rm']
66
- del
67
- seriable_data['logger_information'] = self.logger.to_dict()
68
- return seriable_data
69
-
70
- def __setstate__(self, seriable_data):
71
- self.__dict__.update(seriable_data)
72
- self.logger = Logger(**state['logger_information'])
73
-
74
- def log(self, msg, log='default'): # default means the default of the logger
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
+ """
75
97
  self.logger.log(msg, log=log)
76
-
98
+ ### END: Initializing tools
77
99
 
78
- ### BEGIN: Equipment related stuff
79
100
 
101
+ ### BEGIN: Equipment related stuff
80
102
  def init_rm(self):
81
- # Imports pyvisa as method library #
82
- import pyvisa as visa # ----------------XXX Can this be done like this??????????????
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
83
108
  try:
84
109
  self.rm = visa.ResourceManager()
85
110
  except:
@@ -88,9 +113,41 @@ class Script():
88
113
  sys.exit()
89
114
  self.log(f'Script started pyvisa.ResourceManager.', log='both')
90
115
  return self.rm
116
+ ### END: Equipment related stuff
91
117
 
92
- ### BEGIN Equipment devices
93
-
94
- def mwHP(self, **kwargs):
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
+ """
95
131
  from barsukov.exp.mwHP import mwHP as eq
96
- return eq(**kwargs, script=self, logger=self.logger)
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 CHANGED
@@ -5,12 +5,33 @@ from pytz import timezone
5
5
 
6
6
  TIMEZONE = timezone('America/Los_Angeles')
7
7
 
8
- def time_stamp(): # YYYY-MM-DD_HH-MM-SSS, last digit is 1/10th of the second
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
+ """
9
19
  now = datetime.datetime.now(TIMEZONE)
10
20
  formatted_datetime = now.strftime(f"%Y-%m-%d_%H-%M-%S") + str(int( now.microsecond / 100000 ))
11
21
  return formatted_datetime
12
22
 
13
- def date(): # YYYY-MM-DD
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
+ """
14
35
  now = datetime.datetime.now(TIMEZONE)
15
36
  formatted_datetime = now.strftime(f"%Y-%m-%d")
16
37
  return formatted_datetime
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: barsukov
3
- Version: 1.0.8
3
+ Version: 1.1.0
4
4
  Summary: Experiment Automation Package
5
5
  Author-email: Igor Barsukov <igorb@ucr.edu>, Steven Castaneda <scast206@ucr.edu>
6
6
  Project-URL: Homepage, https://barsukov.ucr.edu
@@ -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-1.1.0.dist-info/METADATA,sha256=29CE78LQEb-zolKUTIaGwUMwQu2M2HrV9xZxdHBcU-o,791
12
+ barsukov-1.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
+ barsukov-1.1.0.dist-info/top_level.txt,sha256=Js5sHbNjP0UNMB9O5HtCHZqlfHabuNS8nTsHbg-1DDQ,9
14
+ barsukov-1.1.0.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ barsukov
barsukov/testpickle.py DELETED
@@ -1,38 +0,0 @@
1
- import dill
2
- import os
3
- import barsukov
4
-
5
-
6
- def save_object(obj, file_name, update=False, full_folder_path=None, script=None):
7
- if script is None:
8
- if full_folder_path is None:
9
- full_folder_path = os.getcwd()
10
- else:
11
- full_folder_path = script.full_folder_path
12
- full_file_path = os.path.join(full_folder_path, file_name)
13
- if update:
14
- with open(full_file_path, 'wb') as file:
15
- dill.dump(obj, file)
16
- print("{obj} object successfully saved.")
17
- else:
18
- try:
19
- with open(full_file_path, 'xb') as file:
20
- dill.dump(obj, file)
21
- print("{obj} object has been successfully saved.")
22
- except FileExistsError:
23
- print(f'Object file already exists. If update, use save_obj(obj={obj}, update=true).')
24
-
25
-
26
- def load_object(obj_file, full_folder_path=None, script=None):
27
- ### obj in format objectname.pkl
28
- if script is None:
29
- if full_folder_path is None:
30
- full_folder_path = os.getcwd()
31
- else:
32
- full_folder_path = script.full_folder_path
33
- full_file_path = os.path.join(full_folder_path, obj_file)
34
- with open(full_file_path, 'rb') as file:
35
- instance = dill.load(file)
36
- print(f'{obj_file} object successfully loaded.')
37
- return instance
38
-
@@ -1,17 +0,0 @@
1
- 2025-01-24_No Stn_Anon_No Sample_No Description/mw.dill,sha256=BXy7-09ZNue15mhWvyXR-0agQTFDwOdSi6E30sLQJNE,2
2
- 2025-01-24_No Stn_Anon_No Sample_No Description/mw.pkl,sha256=BXy7-09ZNue15mhWvyXR-0agQTFDwOdSi6E30sLQJNE,2
3
- barsukov/__init__.py,sha256=1i5yUENPC45RrQ2VXwSlzMRTWmjr_fmkJmmZonUjLIQ,269
4
- barsukov/logger.py,sha256=i8S-EyO9vZmp-PgHnv4asTvJsS6KjFduO7t78K5CRQ4,5259
5
- barsukov/obj2file.py,sha256=8BQOkCPnkq1doeIR7SjUeWRka6QwJ4W2pB8GQYhiSUA,3739
6
- barsukov/script.py,sha256=W5Rx2VJrty0LFEtznX09ubY6Vwx9HDHFAj-rzsQmh38,3766
7
- barsukov/testpickle.py,sha256=ay3ddGpbdQ2828VE1PdoMzArulPjXw5gtuPvzIjKobM,1320
8
- barsukov/time.py,sha256=f88VRzAb84og7Bc4V67fg5ZmxnldTZ8vUnzwUVUnuA8,536
9
- barsukov/data/__init__.py,sha256=IMnOEliXsRMPWeCTprPSddRKg9kwfV-neQiwUwHdpqs,19
10
- barsukov/data/fft.py,sha256=f9aPLeusVpWiWmXO5n4XwkfQ9xJQhZVFdyhFoT9DB2A,4365
11
- barsukov/exp/__init__.py,sha256=urLfGpap40kN9ULi53JB0NT-iMsZDSFdBmdSq3ckB0E,19
12
- barsukov/exp/exp_utils.py,sha256=KwHBcDvV60zVB2GunpEfqbc6WwHOISWpsG24XBO3EkQ,4797
13
- barsukov/exp/mwHP.py,sha256=ECzEXsTSe14-ZbvV1zmbbgV-11gOIjMUQfceDS2XfLU,8504
14
- barsukov-1.0.8.dist-info/METADATA,sha256=RN-KBNilCAY9Xdf2GJkHN_lGYf-I6xImfZ6AU_Y7K0A,791
15
- barsukov-1.0.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
16
- barsukov-1.0.8.dist-info/top_level.txt,sha256=IJTKH4emvt-jqLNdlXcYs-YSbyt0P_wruWi-2Dosux8,57
17
- barsukov-1.0.8.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- 2025-01-24_No Stn_Anon_No Sample_No Description
2
- barsukov