sodetlib 0.6.1rc1__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.
sodetlib/det_config.py ADDED
@@ -0,0 +1,709 @@
1
+ import argparse
2
+ import os
3
+ import yaml
4
+ from collections import OrderedDict
5
+ import sys
6
+ import time
7
+ import shutil
8
+
9
+
10
+ class YamlReps:
11
+ class FlowSeq(list):
12
+ """Represents a list as a flow sequencey by default"""
13
+ pass
14
+
15
+ class Odict(OrderedDict):
16
+ """
17
+ Represents an ordered dict in the same way as a dict, but with
18
+ ordering upheld.
19
+ """
20
+ pass
21
+
22
+ @classmethod
23
+ def setup_reps(cls):
24
+ def flowseq_rep(dumper, data):
25
+ return dumper.represent_sequence(u'tag:yaml.org,2002:seq', data,
26
+ flow_style=True)
27
+ yaml.add_representer(cls.FlowSeq, flowseq_rep)
28
+
29
+ def odict_rep(dumper, data):
30
+ return dumper.represent_mapping('tag:yaml.org,2002:map',data.items())
31
+ yaml.add_representer(cls.Odict, odict_rep)
32
+
33
+
34
+ YamlReps.setup_reps()
35
+
36
+ # Default values for device cfg entries
37
+ exp_defaults = {
38
+ # General stuff
39
+ 'downsample_factor': 20, 'coupling_mode': 'dc', 'synthesis_scale': 1,
40
+ 'active_bands': [0, 1, 2, 3, 4, 5, 6, 7],
41
+ 'active_bgs': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
42
+ 'downsample_mode': 'internal',
43
+ 'enable_compression': True,
44
+
45
+ # Downsample filter parameters
46
+ # cutoff freq in Hz reverse engineered to produce pysmurf default filter
47
+ "downsample_filter": {"order": 4, "cutoff_freq": 63.0},
48
+
49
+ # Dict of device-specific default params to use for take_iv
50
+ 'iv_defaults': {},
51
+
52
+ # Dict of device-specific default params to use for take_bgmap
53
+ 'bgmap_defaults': {},
54
+
55
+ # Dict of device-specific default params to use for take_bias_steps
56
+ 'biasstep_defaults': {},
57
+
58
+ # Amp stuff
59
+ "amps_to_bias": ['hemt', 'hemt1', 'hemt2', '50k', '50k1', '50k2'],
60
+ "amp_enable_wait_time": 10.0, "amp_step_wait_time": 0.2,
61
+
62
+ "amp_50k_init_gate_volt": -0.5, "amp_50k_drain_current": 15.0,
63
+ "amp_50k_gate_volt": None, "amp_50k_drain_current_tolerance": 0.2,
64
+ "amp_hemt_init_gate_volt": -1.0, "amp_hemt_drain_current": 8.0,
65
+ "amp_hemt_gate_volt": None, "amp_hemt_drain_current_tolerance": 0.2,
66
+
67
+ "amp_50k1_init_gate_volt": -0.5, "amp_50k1_drain_current": 15.0,
68
+ "amp_50k1_gate_volt": None, "amp_50k1_drain_current_tolerance": 0.2,
69
+ "amp_50k1_drain_volt": 4,
70
+ "amp_50k2_init_gate_volt": -0.5, "amp_50k2_drain_current": 15.0,
71
+ "amp_50k2_gate_volt": None, "amp_50k2_drain_current_tolerance": 0.2,
72
+ "amp_50k2_drain_volt": 4,
73
+
74
+ "amp_hemt1_init_gate_volt": -0.8, "amp_hemt1_drain_current": 8.0,
75
+ "amp_hemt1_gate_volt": None, "amp_hemt1_drain_current_tolerance": 0.2,
76
+ "amp_hemt1_drain_volt": 0.6,
77
+ "amp_hemt2_init_gate_volt": -0.8, "amp_hemt2_drain_current": 8.0,
78
+ "amp_hemt2_gate_volt": None, "amp_hemt2_drain_current_tolerance": 0.2,
79
+ "amp_hemt2_drain_volt": 0.6,
80
+
81
+ # Find freq
82
+ 'res_amp_cut': 0.01, 'res_grad_cut': 0.01,
83
+
84
+ # Tracking stuff
85
+ "flux_ramp_rate_khz": 4, "init_frac_pp": 0.4, "nphi0": 5,
86
+ "f_ptp_range": [10, 200], "df_ptp_range": [0, 100], "r2_min": 0.9,
87
+ "min_good_tracking_frac": 0.8,
88
+ 'feedback_start_frac': 0.02, 'feedback_end_frac': 0.98,
89
+
90
+ # Misc files
91
+ "tunefile": None, "bgmap_file": None, "iv_file": None,
92
+ "res_fit_file": None,
93
+
94
+ # Overbiasing
95
+ 'overbias_wait': 2.0,
96
+ 'overbias_sleep_time_sec': 300,
97
+
98
+ # Biasing
99
+ 'rfrac':[0.3, 0.6],
100
+ }
101
+ band_defaults = {
102
+ # General
103
+ "band_delay_us": None, "uc_att": None, "dc_att": None,
104
+ "attens_optimized": False, "tone_power": 12,
105
+
106
+ # Band-specific tracking stuff
107
+ "lms_gain": 0, "feedback_gain": 2048, "frac_pp": None, "lms_freq_hz": None,
108
+
109
+ # Gradient Descent Parameters
110
+ "gradientDescentMaxIters": 100,
111
+ "gradientDescentAverages": 2,
112
+ "gradientDescentGain": 0.001,
113
+ "gradientDescentConvergeHz": 500,
114
+ "gradientDescentStepHz": 5000,
115
+ "gradientDescentMomentum": 1,
116
+ "gradientDescentBeta": 0.1,
117
+
118
+ # Fixed tones
119
+ "fixed_tones": {"enabled": False, "freq_offsets": [], "channels": [], "tone_power": 0},
120
+ }
121
+ bg_defaults = {
122
+ 'overbias_voltage': 19.9,
123
+ 'cool_voltage': 10.0,
124
+ 'testbed_100mK_bias_voltage': 10.0
125
+ }
126
+
127
+
128
+ class DeviceConfig:
129
+ """
130
+ Configuration object containing all "device" specific information. That is,
131
+ parameters that will probably change based on which test-bed you are
132
+ using and what hardware is loaded into that test-bed. Device configuration
133
+ info is split into three groups: ``experiment``, ``bias_groups`` and
134
+ ``bands``.
135
+
136
+ The ``experiment`` group contains data that is relevant to the whole
137
+ experiment, e.g. the tunefile that should be loaded or the amplifier
138
+ currents and voltages that should be set.
139
+
140
+ The ``bias_groups`` group contains data for each individual bias group.
141
+ For instance this is where the bias_high/low/step values are stored for each
142
+ bias group.
143
+
144
+ The ``bands`` group contains data for each individual band. For instance,
145
+ the dc_att and drive values.
146
+
147
+ Attributes:
148
+ exp (dict):
149
+ Dict with ``experiment`` config options
150
+ bias_groups (list):
151
+ List of 12 bias group configuration dictionaries.
152
+ bands (list):
153
+ List of 8 band configuration dictionaries.
154
+ """
155
+ def __init__(self):
156
+ self.load_defaults()
157
+ self.source_file = None
158
+
159
+ def load_defaults(self):
160
+ self.bands = [band_defaults.copy() for _ in range(8)]
161
+ self.bias_groups = [bg_defaults.copy() for _ in range(12)]
162
+ self.exp = exp_defaults.copy()
163
+
164
+ @classmethod
165
+ def from_dict(cls, data):
166
+ """
167
+ Creates a DeviceConfig object from a dictionary.
168
+ """
169
+ self = cls()
170
+ for k, v in data['bands'].items():
171
+ if k.lower().startswith('amc'): # Key formatted like AMC[i]
172
+ amc_index = int(k[4])
173
+ self._load_amc(amc_index, v)
174
+ if k.lower().startswith('band'): # Key formatted like Band[i]
175
+ band_index = int(k[5])
176
+ self.bands[band_index].update(v)
177
+
178
+
179
+ # Loads bias groups from config file
180
+ for i, bg in enumerate(self.bias_groups):
181
+ for k, vlist in data['bias_groups'].items():
182
+ self.bias_groups[i][k] = vlist[i]
183
+
184
+ # Loads experiment config data
185
+ self.exp.update(data['experiment'])
186
+ return self
187
+
188
+ @classmethod
189
+ def from_yaml(cls, filename):
190
+ """
191
+ Creates a DeviceConfig object from a dev-cfg yaml file.
192
+
193
+ Args:
194
+ filename (path): path to device-config file.
195
+ """
196
+ filename = os.path.abspath(os.path.expandvars(filename))
197
+ if not os.path.exists(filename):
198
+ # Just return default device cfg
199
+ print(f"File {filename} doesn't exist! Creating it wth default params")
200
+ self = cls()
201
+ self.source_file = filename
202
+ self.update_file()
203
+ return self
204
+ else:
205
+ with open(filename) as f:
206
+ data = yaml.safe_load(f)
207
+ self = cls.from_dict(data)
208
+ self.source_file = filename
209
+ return self
210
+
211
+ def _load_amc(self, amc_index, data):
212
+ """Loads amc data all at once"""
213
+ for k, v in data.items():
214
+ if len(v) != 4:
215
+ raise ValueError("All data in AMC entry must be formatted as a "
216
+ "list of length 4.")
217
+ for i in range(4):
218
+ band_index = amc_index*4 + i
219
+ self.bands[band_index][k] = v[i]
220
+
221
+ def dump(self, path, clobber=False):
222
+ """
223
+ Dumps all device configuration info to a file.
224
+
225
+ Args:
226
+ path (path):
227
+ Location to dump config info to. This file must not already
228
+ exist.
229
+ clobber (bool):
230
+ If true will overwrite existing file. Defaults to false.
231
+ """
232
+ if os.path.exists(path) and not clobber:
233
+ raise FileExistsError(f"Can't dump device config! Path {path}"
234
+ "already exists!!")
235
+
236
+ def _format_yml(val):
237
+ """Converts np dtypes to python types for yaml files"""
238
+ if hasattr(val, 'dtype'):
239
+ return val.item()
240
+ elif isinstance(val, tuple):
241
+ return list(val)
242
+ else:
243
+ return val
244
+
245
+ data = YamlReps.Odict()
246
+ data['experiment'] = {
247
+ k: _format_yml(v) for k, v in self.exp.items()
248
+ }
249
+
250
+ data['bias_groups'] = {
251
+ k: YamlReps.FlowSeq([_format_yml(bg[k]) for bg in self.bias_groups])
252
+ for k in self.bias_groups[0].keys()
253
+ }
254
+ data['bands'] = YamlReps.Odict([
255
+ (f'AMC[{i}]', {
256
+ k: YamlReps.FlowSeq([
257
+ _format_yml(b[k]) for b in self.bands[4*i:4*i+4]
258
+ ])
259
+ for k in self.bands[0].keys()
260
+ }) for i in [0, 1]])
261
+
262
+ with open(path, 'w') as f:
263
+ yaml.dump(data, f)
264
+
265
+ def update_band(self, band_index, data, update_file=False):
266
+ """
267
+ Updates band configuration object.
268
+
269
+ Args:
270
+ band_index (int 0-7):
271
+ Index of band toupdate
272
+ data (dict):
273
+ Dictionary of parameters to update. All parameters must exist in
274
+ the loaded dev-cfg file.
275
+ """
276
+ band = self.bands[band_index]
277
+ for k, v in data.items():
278
+ if k not in band.keys():
279
+ for b in self.bands:
280
+ b[k] = None
281
+ band[k] = v
282
+ if update_file and self.source_file is not None:
283
+ self.update_file()
284
+
285
+ def update_bias_group(self, bg_index, data, update_file=False):
286
+ """
287
+ Updates bias group configuration object.
288
+
289
+ Args:
290
+ bg_index (int 0-11):
291
+ Index of bias group to update.
292
+ data (dict):
293
+ Dictionary of parameters to update. All parameters must exist in
294
+ the loaded dev-cfg file.
295
+ """
296
+ bg = self.bias_groups[bg_index]
297
+ for k, v in data.items():
298
+ if k not in bg.keys():
299
+ for _bg in self.bias_groups:
300
+ _bg[k] = None
301
+ bg[k] = v
302
+ if update_file and self.source_file is not None:
303
+ self.update_file()
304
+
305
+ def update_experiment(self, data, update_file=False):
306
+ """
307
+ Updates ``experiment`` configuration object.
308
+
309
+ Args:
310
+ data (dict):
311
+ Dictionary of parameters to update. All parameters must exist in
312
+ the loaded dev-cfg file.
313
+ """
314
+ for k, v in data.items():
315
+ self.exp[k] = v
316
+ if update_file and self.source_file is not None:
317
+ self.update_file()
318
+
319
+ def update_file(self):
320
+ """
321
+ Updates the device cfg file.1
322
+ """
323
+ self.dump(self.source_file, clobber=True)
324
+
325
+
326
+ def apply_to_pysmurf_instance(self, S, load_tune=True):
327
+ """
328
+ Applies basic device config params (amplifier biases, attens,
329
+ tone_powers) to a pysmurf instance based on the device cfg values. Note
330
+ that this does not set any of the tracking-related params since this
331
+ shouldn't replace tracking_setup.
332
+ """
333
+ S.set_amplifier_bias(
334
+ bias_hemt=self.exp['amp_hemt_Vg'],
335
+ bias_50k=self.exp['amp_50k_Vg']
336
+ )
337
+
338
+ if load_tune:
339
+ tunefile = self.exp['tunefile']
340
+ if os.path.exists(tunefile):
341
+ S.load_tune(tunefile)
342
+ else:
343
+ print(f"Cannot load tunefile {tunefile} because it doesn't exist")
344
+
345
+ for b in S._bands:
346
+ band_cfg = self.bands[b]
347
+ if 'uc_att' in band_cfg:
348
+ S.set_att_uc(b, band_cfg['uc_att'])
349
+ if 'dc_att' in band_cfg:
350
+ S.set_att_dc(b, band_cfg['dc_att'])
351
+ if 'drive' in band_cfg:
352
+ # Sets the tone power of all enabled channels to `drive`
353
+ amp_scales = S.get_amplitude_scale_array(b)
354
+ amp_scales[amp_scales != 0] = band_cfg['drive']
355
+ S.set_amplitude_scale_array(b, amp_scales)
356
+
357
+
358
+ def make_parser(parser=None):
359
+ if parser is None:
360
+ parser = argparse.ArgumentParser()
361
+
362
+ parser.add_argument_group("DetConfig Options")
363
+ parser.add_argument('--sys-file',
364
+ help="Path to sys-config file. "
365
+ "Defaults to ``$SMURF_CONFIG_DIR/sys_config.yml``")
366
+ parser.add_argument('--dev-file',
367
+ help="Path to device-config file. "
368
+ "Defaults to the path specified in the sys_config.")
369
+ parser.add_argument('--pysmurf-file',
370
+ help="Path to Pysmurf config file. "
371
+ "Defaults to the path specified in the sys_config.")
372
+ parser.add_argument('--uxm-file',
373
+ help="Path to the uxm file. "
374
+ "Defaults to the path specified in the sys_config")
375
+ parser.add_argument('--slot', '-N', type=int, help="Smurf slot")
376
+ parser.add_argument('--dump-configs', '-D', action='store_true',
377
+ help="If true, all config info will be written to "
378
+ "the pysmurf output directory")
379
+ return parser
380
+
381
+ class DetConfig:
382
+ """
383
+ General Configuration class for SODETLIB.
384
+
385
+ Attributes:
386
+ sys_file (path): Path to sys-config file used.
387
+ dev_file (path): Path to device-config file used
388
+ pysmurf_file (path): Path to pysmurf-config file used.
389
+ uxm_file (path): Path to uxm file
390
+ sys (dict):
391
+ System configuration dictionary, generated from the sys_config.yml
392
+ file or ``--sys-file`` command line argument.
393
+ dev (dict):
394
+ Device configuration dictionary, generated from the device_config
395
+ file specified in the sys_config or specified by the ``--dev-file``
396
+ command line argument
397
+ """
398
+ def __init__(self, slot=None, sys_file=None, dev_file=None,
399
+ pysmurf_file=None, uxm_file=None):
400
+ self.sys_file = sys_file
401
+ self.dev_file = dev_file
402
+ self.pysmurf_file = pysmurf_file
403
+ self.uxm_file = uxm_file
404
+ self.slot = slot
405
+ self.sys = None
406
+ self.dev: DeviceConfig = None
407
+ self.dump = None # Whether config files should be dumped
408
+
409
+ self.S = None
410
+
411
+ self._parser: argparse.ArgumentParser = None
412
+ self._argparse_args = None
413
+
414
+
415
+ def parse_args(self, parser=None, args=None):
416
+ """
417
+ Parses command line arguments along with det_config arguments and
418
+ loads the correct configuration. See ``load_config`` for how config
419
+ files are determined.
420
+
421
+ Args:
422
+ parser (argparse.ArgumentParser, optional):
423
+ custom argparse parser to parse args with. If not specified,
424
+ will create its own.
425
+ args (list, optional):
426
+ List of command line arguments to parse.
427
+ Defaults to the command line args.
428
+ """
429
+ self._argparse_args = args
430
+ parser = make_parser(parser)
431
+ self._parser = parser
432
+
433
+ args = parser.parse_args(args=args)
434
+
435
+ if args.sys_file is None:
436
+ args.sys_file = self.sys_file
437
+ if args.pysmurf_file is None:
438
+ args.pysmurf_file = self.pysmurf_file
439
+ if args.dev_file is None:
440
+ args.dev_file = self.dev_file
441
+ if args.uxm_file is None:
442
+ args.uxm_file = self.uxm_file
443
+
444
+ self.load_config_files(
445
+ slot=args.slot, sys_file=args.sys_file, dev_file=args.dev_file,
446
+ pysmurf_file=args.pysmurf_file, uxm_file=args.uxm_file
447
+ )
448
+
449
+ return args
450
+
451
+ def load_config_files(self, slot=None, sys_file=None, dev_file=None,
452
+ pysmurf_file=None, uxm_file=None):
453
+ """
454
+ Loads configuration files. If arguments are not specified, sensible
455
+ defaults will be chosen.
456
+
457
+ Args:
458
+ slot (int, optional):
459
+ pysmurf slot number. If None and there is only a single slot in
460
+ the sys_file, it will use that slot. Otherwise, it will throw
461
+ and error.
462
+ sys_file (path, optional):
463
+ Path to sys config file. If None, defaults to
464
+ $SMURF_CONFIG_DIR/sys_config.yml
465
+ dev_file (path, optional):
466
+ Path to the Device file. If None, defaults to the device file
467
+ specified in the sys-config file (device_configs[slot-2]).
468
+ pysmurf_file (path, optional):
469
+ Path to pysmurf config file. If None, defaults to the file
470
+ specified in the sys-config (pysmurf_configs[slot-2]).
471
+ uxm_file (path, optional):
472
+ Path to uxm file
473
+ """
474
+ # This should be the same for every smurf-srv
475
+ if 'SMURF_CONFIG_DIR' in os.environ:
476
+ cfg_dir = os.environ['SMURF_CONFIG_DIR']
477
+ elif 'OCS_CONFIG_DIR' in os.environ:
478
+ cfg_dir = os.environ['OCS_CONFIG_DIR']
479
+ else:
480
+ raise ValueError(
481
+ "SMURF_CONFIG_DIR or OCS_CONFIG_DIR must be set in the environment"
482
+ )
483
+
484
+ if sys_file is None:
485
+ self.sys_file = os.path.join(cfg_dir, 'sys_config.yml')
486
+ else:
487
+ self.sys_file = sys_file
488
+
489
+ # Load system settings
490
+ with open(self.sys_file) as f:
491
+ self.sys = yaml.safe_load(f)
492
+
493
+ if slot is not None:
494
+ self.slot = slot
495
+ elif len(self.sys['slot_order']) == 1:
496
+ self.slot = self.sys['slot_order'][0]
497
+ else:
498
+ raise ValueError(
499
+ "Slot could not be automatically determined from sys_config "
500
+ "file! Must specify slot directly from command line with "
501
+ "--slot argument. "
502
+ f"Available slots are {self.sys['slot_order']}."
503
+ )
504
+
505
+ slot_cfg = self.sys['slots'][f'SLOT[{self.slot}]']
506
+ self.stream_id = slot_cfg['stream_id']
507
+
508
+ if dev_file is None:
509
+ self.dev_file = os.path.abspath(os.path.expandvars(slot_cfg['device_config']))
510
+ else:
511
+ self.dev_file = os.path.abspath(os.path.expandvars(dev_file))
512
+ self.dev = DeviceConfig.from_yaml(self.dev_file)
513
+
514
+ # Gets the pysmurf config file
515
+ if pysmurf_file is None:
516
+ self.pysmurf_file = os.path.expandvars(slot_cfg['pysmurf_config'])
517
+ else:
518
+ self.pysmurf_file = pysmurf_file
519
+
520
+ if uxm_file is None:
521
+ if 'uxm_file' in slot_cfg:
522
+ self.uxm_file = os.path.expandvars(slot_cfg['uxm_file'])
523
+ else:
524
+ self.uxm_file = uxm_file
525
+ if self.uxm_file is not None:
526
+ with open(self.uxm_file) as f:
527
+ self.uxm = yaml.safe_load(f)
528
+ else:
529
+ self.uxm = None
530
+
531
+ def dump_configs(self, output_dir=None, clobber=False, dump_rogue_tree=False):
532
+ """
533
+ Dumps any config information to an output directory
534
+
535
+ Args:
536
+ output_dir (path): Directory location to put config files.
537
+ dump_rogue_truee : bool
538
+ If True, will dump the pysmurf rogue tree to the configs dir.
539
+
540
+ Returns:
541
+ run_out (path): path to output run file
542
+ sys_out (path): path to output sys file
543
+ dev_out (path): path to output device file.
544
+ uxm_out (path): path to uxm file
545
+ """
546
+ if output_dir is None:
547
+ if not self.S:
548
+ raise ValueError("output_dir cannot be None in offline mode")
549
+ output_dir = os.path.join(
550
+ self.S.base_dir, self.S.date, self.stream_id,
551
+ self.S.name, 'config', self.S.get_timestamp(),
552
+ )
553
+ print(f"Dumping sodetlib configs to {output_dir}")
554
+ if not os.path.exists(output_dir):
555
+ os.makedirs(output_dir)
556
+
557
+ # Manually set publisher action because set_action decorator won't
558
+ # work without S being the first function argument.
559
+ if self.S:
560
+ start_action = self.S.pub._action
561
+ start_action_ts = self.S.pub._action_ts
562
+
563
+ try:
564
+ outfiles = {}
565
+ if self.S:
566
+ self.S.pub._action = "config"
567
+ self.S.pub._action_ts = self.S.get_timestamp()
568
+
569
+ # Dump run info
570
+ run_info = YamlReps.Odict([
571
+ ('sys_file', self.sys_file),
572
+ ('dev_file', self.dev_file),
573
+ ('pysmurf_file', self.pysmurf_file),
574
+ ('slot', self.slot),
575
+ ('cwd', os.getcwd()),
576
+ ('argv', ' '.join(sys.argv)),
577
+ ('time', int(time.time())),
578
+ ])
579
+ if self._argparse_args is not None:
580
+ run_info['cfg_args'] = self._argparse_args
581
+ run_out = os.path.abspath(os.path.join(output_dir, 'run_info.yml'))
582
+ outfiles['run'] = run_out
583
+ with open(run_out, 'w') as f:
584
+ yaml.dump(run_info, f)
585
+ if self.S:
586
+ self.S.pub.register_file(run_out, 'config', format='yaml')
587
+
588
+ # Dump sys file
589
+ sys_out = os.path.abspath(os.path.join(output_dir,
590
+ 'sys_config.yml'))
591
+ outfiles['sys'] = sys_out
592
+ with open(sys_out, 'w') as f:
593
+ yaml.dump(self.sys, f)
594
+ if self.S:
595
+ self.S.pub.register_file(sys_out, 'config', format='yaml')
596
+
597
+ # Dump device file
598
+ dev_out = os.path.abspath(os.path.join(output_dir, 'dev_cfg.yml'))
599
+ outfiles['dev'] = dev_out
600
+ self.dev.dump(dev_out, clobber=clobber)
601
+ if self.S:
602
+ self.S.pub.register_file(dev_out, 'config', format='yaml')
603
+
604
+ # Copy pysmurf file
605
+ pysmurf_out = os.path.join(output_dir, 'pysmurf_cfg.yml')
606
+ pysmurf_out = os.path.abspath(pysmurf_out)
607
+ outfiles['pysmurf'] = pysmurf_out
608
+ shutil.copy(self.pysmurf_file, pysmurf_out)
609
+ if self.S:
610
+ self.S.pub.register_file(pysmurf_out, 'config', format='yaml')
611
+
612
+ # Dump uxm file
613
+ if self.uxm is not None:
614
+ uxm_out = os.path.abspath(os.path.join(output_dir, 'uxm.yml'))
615
+ outfiles['uxm'] = uxm_out
616
+ with open(uxm_out, 'w') as f:
617
+ yaml.dump(self.uxm, f)
618
+ if self.S:
619
+ self.S.pub.register_file(uxm_out, 'config', format='yaml')
620
+
621
+ if dump_rogue_tree:
622
+ print("Dumping state")
623
+ state_file = os.path.abspath(os.path.join(output_dir,
624
+ 'rogue_state.yaml'))
625
+ outfiles.append(state_file)
626
+ self.S.save_state(state_file)
627
+ finally:
628
+ # Restore publisher action so it doesn't mess up ongoing actions
629
+ if self.S:
630
+ self.S.pub._action = start_action
631
+ self.S.pub._action_ts = start_action_ts
632
+
633
+ return outfiles
634
+
635
+ def get_smurf_control(self, offline=False, epics_root=None,
636
+ smurfpub_id=None, make_logfile=False, setup=False,
637
+ dump_configs=None, config_dir=None,
638
+ apply_dev_configs=False, load_device_tune=True,
639
+ **pysmurf_kwargs):
640
+ """
641
+ Creates pysmurf instance based off of configuration parameters.
642
+ If not specified as keyword arguments ``epics_root`` and ``smurf_pub``
643
+ will be created based on the slot and crate id's.
644
+
645
+ Args:
646
+ offline (bool):
647
+ Whether to start pysmurf in offline mode. Defaults to False
648
+ epics_root (str, optional):
649
+ Pysmurf epics root. If none, it will be set to
650
+ ``smurf_server_s<slot>``.
651
+ smurfpub_id (str, optional):
652
+ Pysmurf publisher ID. If None, will default to
653
+ crate<crate_id>_slot<slot>.
654
+ make_logfile (bool):
655
+ Whether pysmurf should write logs to a file (True) or
656
+ stdout(False). Defaults to stdout.
657
+ setup (bool):
658
+ Whether pysmurf should run a full setup. Defaults to False.
659
+ dump_configs (bool):
660
+ Whether all configuration settings should be dumped to pysmurf
661
+ data directory. Defaults to True.
662
+ **pysmurf_kwargs:
663
+ Any additional arguments to be passed to pysmurf initialization.
664
+
665
+ """
666
+ import pysmurf.client
667
+
668
+ slot_cfg = self.sys['slots'][f'SLOT[{self.slot}]']
669
+ if epics_root is None:
670
+ epics_root = f'smurf_server_s{self.slot}'
671
+ if smurfpub_id is None:
672
+ smurfpub_id = self.stream_id
673
+ if dump_configs is None:
674
+ dump_configs = self.dump
675
+
676
+ # Pysmurf publisher will check this to determine publisher id.
677
+ os.environ['SMURFPUB_ID'] = smurfpub_id
678
+
679
+ if offline:
680
+ S = pysmurf.client.SmurfControl(offline=True)
681
+ else:
682
+ S = pysmurf.client.SmurfControl(
683
+ epics_root=epics_root, cfg_file=self.pysmurf_file, setup=setup,
684
+ make_logfile=make_logfile, data_path_id=smurfpub_id,
685
+ **pysmurf_kwargs)
686
+ self.S = S
687
+ # Lets just stash this in pysmurf...
688
+ S._sodetlib_cfg = self
689
+
690
+ # Dump config outputs
691
+ if dump_configs:
692
+ if config_dir is None:
693
+ if not offline:
694
+ config_dir = os.path.join(
695
+ S.base_dir, S.date, smurfpub_id, S.name, 'config',
696
+ S.get_timestamp(),
697
+ )
698
+
699
+ else:
700
+ config_dir = os.path.join('config', S.get_timestamp())
701
+ print("Warning! Being run in offline mode with no config "
702
+ f"directory specified! Writing to {config_dir}")
703
+ self.dump_configs(config_dir)
704
+
705
+ if apply_dev_configs:
706
+ print("Applying device cfg parameters...")
707
+ self.dev.apply_to_pysmurf_instance(S, load_tune=load_device_tune)
708
+
709
+ return S