QuLab 2.9.10__cp311-cp311-macosx_10_9_universal2.whl → 2.10.1__cp311-cp311-macosx_10_9_universal2.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.
qulab/cli/config.py CHANGED
@@ -11,7 +11,10 @@ CONFIG_PATH = os.path.expanduser("~/.qulab.ini")
11
11
  ENV_PREFIX = "QULAB_"
12
12
 
13
13
 
14
- def _get_config_value(option_name, type_cast=str, command_name=None):
14
+ def _get_config_value(option_name,
15
+ type_cast=str,
16
+ command_name=None,
17
+ default=None):
15
18
  """支持命令专属配置的优先级获取"""
16
19
  # 构造环境变量名
17
20
  if command_name:
@@ -47,34 +50,90 @@ def _get_config_value(option_name, type_cast=str, command_name=None):
47
50
  return os.path.expanduser(config_value)
48
51
  return type_cast(config_value)
49
52
 
50
- return None # 交给 Click 处理默认值
51
-
52
-
53
- def get_config_value(option_name, type_cast=str, command_name=None):
54
- value = _get_config_value(option_name, type_cast, command_name)
53
+ return default # 交给 Click 处理默认值
54
+
55
+
56
+ def get_config_value(option_name,
57
+ type_cast=str,
58
+ command_name=None,
59
+ default=None):
60
+ """
61
+ 获取配置值,支持命令专属配置的优先级获取
62
+
63
+ 优先级:
64
+ 1. 命令行参数
65
+ 2. 配置文件专属配置
66
+ 3. 配置文件公共配置
67
+ 4. 环境变量专属配置
68
+ 5. 环境变量公共配置
69
+ 6. 默认值
70
+ 7. Click 处理默认值
71
+ 8. None
72
+
73
+ Parameters
74
+ ----------
75
+ option_name : str
76
+ 配置项名称
77
+ type_cast : type
78
+ 转换类型
79
+ command_name : str
80
+ 命令名称
81
+ 如果为 None,则不使用命令专属配置
82
+ default : any
83
+ 默认值
84
+ 如果为 None,则不使用默认值
85
+
86
+ Returns
87
+ -------
88
+ any
89
+ 配置值
90
+ 如果没有找到配置值,则返回 None
91
+ """
92
+ value = _get_config_value(option_name,
93
+ type_cast,
94
+ command_name,
95
+ default=default)
55
96
  if value is None and command_name is not None:
56
- return _get_config_value(option_name, type_cast)
97
+ return _get_config_value(option_name, type_cast, default=default)
57
98
  return value
58
99
 
59
100
 
60
- def log_options(func):
61
- """通用配置装饰器(所有命令共用)"""
101
+ def log_options(func=None, command_name=None):
102
+ """通用配置装饰器(所有命令共用)
103
+
104
+ 添加 --debug, --log, --debug-log, --quiet 选项
105
+ 1. --debug: 是否开启调试模式
106
+ 2. --log: 日志文件路径
107
+ 3. --debug-log: 调试日志文件路径
108
+ 4. --quiet: 是否静默输出
109
+ """
110
+
111
+ if isinstance(func, str):
112
+ return functools.partial(log_options, command_name=func)
113
+ if func is None:
114
+ return functools.partial(log_options, command_name=command_name)
62
115
 
63
116
  @click.option("--debug",
64
117
  is_flag=True,
65
- default=get_config_value("debug", bool),
118
+ default=get_config_value("debug",
119
+ bool,
120
+ command_name=command_name),
66
121
  help=f"Enable debug mode")
67
122
  @click.option("--log",
68
123
  type=click.Path(),
69
- default=lambda: get_config_value("log", Path),
124
+ default=lambda: get_config_value(
125
+ "log", Path, command_name=command_name),
70
126
  help=f"Log file path")
71
127
  @click.option("--debug-log",
72
128
  type=click.Path(),
73
- default=lambda: get_config_value("debug_log", Path),
129
+ default=lambda: get_config_value(
130
+ "debug_log", Path, command_name=command_name),
74
131
  help=f"Debug log file path")
75
132
  @click.option("--quiet",
76
133
  is_flag=True,
77
- default=get_config_value("quiet", bool),
134
+ default=get_config_value("quiet",
135
+ bool,
136
+ command_name=command_name),
78
137
  help=f"Disable log output")
79
138
  @functools.wraps(func)
80
139
  def wrapper(*args, **kwargs):
@@ -0,0 +1,28 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+
5
+
6
+ def async_command(func=None):
7
+ """
8
+ Decorator to mark a function as an asynchronous command.
9
+ """
10
+ if func is None:
11
+ return async_command
12
+
13
+ @functools.wraps(func)
14
+ def wrapper(*args, **kwargs):
15
+ return run(func, *args, **kwargs)
16
+
17
+ return wrapper
18
+
19
+
20
+ def run(main, *args, **kwds):
21
+ if inspect.iscoroutinefunction(main):
22
+ try:
23
+ import uvloop
24
+ uvloop.run(main(*args, **kwds))
25
+ except ImportError:
26
+ asyncio.run(main(*args, **kwds))
27
+ else:
28
+ main(*args, **kwds)
qulab/executor/cli.py CHANGED
@@ -9,6 +9,7 @@ import click
9
9
  from loguru import logger
10
10
 
11
11
  from ..cli.config import get_config_value, log_options
12
+ from ..cli.decorators import async_command
12
13
  from .load import (WorkflowType, find_unreferenced_workflows, get_entries,
13
14
  load_workflow, make_graph)
14
15
  from .schedule import CalibrationFailedError
@@ -165,7 +166,8 @@ def get(key, api):
165
166
  @click.option('--freeze', is_flag=True, help='Freeze the config table.')
166
167
  @log_options
167
168
  @command_option('run')
168
- def run(workflow, code, data, api, plot, no_dependents, retry, freeze):
169
+ @async_command
170
+ async def run(workflow, code, data, api, plot, no_dependents, retry, freeze):
169
171
  """
170
172
  Run a workflow.
171
173
 
@@ -204,29 +206,33 @@ def run(workflow, code, data, api, plot, no_dependents, retry, freeze):
204
206
  if no_dependents:
205
207
  if hasattr(wf, 'entries'):
206
208
  for entry in get_entries(wf, code):
207
- run_workflow(entry,
208
- code,
209
- data,
210
- plot=plot,
211
- freeze=freeze)
209
+ await run_workflow(entry,
210
+ code,
211
+ data,
212
+ plot=plot,
213
+ freeze=freeze)
212
214
  else:
213
- run_workflow(wf, code, data, plot=plot, freeze=freeze)
215
+ await run_workflow(wf,
216
+ code,
217
+ data,
218
+ plot=plot,
219
+ freeze=freeze)
214
220
  else:
215
221
  if hasattr(wf, 'entries'):
216
222
  for entry in get_entries(wf, code):
217
- maintain_workflow(entry,
218
- code,
219
- data,
220
- run=True,
221
- plot=plot,
222
- freeze=freeze)
223
+ await maintain_workflow(entry,
224
+ code,
225
+ data,
226
+ run=True,
227
+ plot=plot,
228
+ freeze=freeze)
223
229
  else:
224
- maintain_workflow(wf,
225
- code,
226
- data,
227
- run=True,
228
- plot=plot,
229
- freeze=freeze)
230
+ await maintain_workflow(wf,
231
+ code,
232
+ data,
233
+ run=True,
234
+ plot=plot,
235
+ freeze=freeze)
230
236
  break
231
237
  except CalibrationFailedError as e:
232
238
  if i == retry - 1:
@@ -241,7 +247,8 @@ def run(workflow, code, data, api, plot, no_dependents, retry, freeze):
241
247
  @click.option('--plot', '-p', is_flag=True, help='Plot the report.')
242
248
  @log_options
243
249
  @command_option('maintain')
244
- def maintain(workflow, code, data, api, retry, plot):
250
+ @async_command
251
+ async def maintain(workflow, code, data, api, retry, plot):
245
252
  """
246
253
  Maintain a workflow.
247
254
 
@@ -275,19 +282,19 @@ def maintain(workflow, code, data, api, retry, plot):
275
282
  try:
276
283
  if hasattr(wf, 'entries'):
277
284
  for entry in get_entries(wf, code):
278
- maintain_workflow(entry,
279
- code,
280
- data,
281
- run=False,
282
- plot=plot,
283
- freeze=False)
285
+ await maintain_workflow(entry,
286
+ code,
287
+ data,
288
+ run=False,
289
+ plot=plot,
290
+ freeze=False)
284
291
  else:
285
- maintain_workflow(wf,
286
- code,
287
- data,
288
- run=False,
289
- plot=plot,
290
- freeze=False)
292
+ await maintain_workflow(wf,
293
+ code,
294
+ data,
295
+ run=False,
296
+ plot=plot,
297
+ freeze=False)
291
298
  break
292
299
  except CalibrationFailedError as e:
293
300
  if i == retry - 1:
@@ -301,7 +308,8 @@ def maintain(workflow, code, data, api, retry, plot):
301
308
  @click.option('--plot', '-p', is_flag=True, help='Plot the report.')
302
309
  @log_options
303
310
  @command_option('reproduce')
304
- def reproduce(report_id, code, data, api, plot):
311
+ @async_command
312
+ async def reproduce(report_id, code, data, api, plot):
305
313
  """
306
314
  Reproduce a report.
307
315
 
@@ -335,6 +343,6 @@ def reproduce(report_id, code, data, api, plot):
335
343
  cfg = transform.export_config()
336
344
  transform.clear_config()
337
345
  transform.update_config(r.config)
338
- run_workflow(wf, code, data, plot=plot, freeze=True)
346
+ await run_workflow(wf, code, data, plot=plot, freeze=True)
339
347
  transform.clear_config()
340
348
  transform.update_config(cfg)
qulab/executor/load.py CHANGED
@@ -135,10 +135,14 @@ def verify_dependence_key(workflow: str | tuple[str, dict[str, Any]]
135
135
  if not isinstance(workflow, tuple) or len(workflow) not in [2, 3]:
136
136
  raise ValueError(f"Invalid workflow: {workflow}")
137
137
 
138
- if len(workflow) == 2:
138
+ if len(workflow) == 2 and isinstance(workflow[1], str):
139
139
  file_name, mapping = workflow
140
140
  if not Path(file_name).exists():
141
141
  raise FileNotFoundError(f"File not found: {file_name}")
142
+ elif len(workflow) == 2 and isinstance(workflow[1], dict):
143
+ template_path, mapping = workflow
144
+ if not (Path(base_path) / template_path).exists():
145
+ raise FileNotFoundError(f"File not found: {template_path}")
142
146
  elif len(workflow) == 3:
143
147
  template_path, target_path, mapping = workflow
144
148
  if not (Path(base_path) / template_path).exists():
@@ -127,18 +127,25 @@ def check_state(workflow: WorkflowType, code_path: str | Path,
127
127
 
128
128
 
129
129
  @logger.catch()
130
- def call_plot(node: WorkflowType, report: Report, check=False):
130
+ async def call_plot(node: WorkflowType, report: Report, check=False):
131
131
  if hasattr(node, 'plot') and callable(node.plot):
132
- node.plot(report)
132
+ if inspect.iscoroutinefunction(node.plot):
133
+ await node.plot(report, check=check)
134
+ else:
135
+ node.plot(report)
133
136
 
134
137
 
135
- def call_check(workflow: WorkflowType, session_id: str, state_path: Path):
138
+ async def call_check(workflow: WorkflowType, session_id: str,
139
+ state_path: Path):
136
140
  report = get_cache(session_id, (workflow.__workflow_id__, 'check'))
137
141
  if report is not None:
138
142
  logger.debug(f'Cache hit for "{workflow.__workflow_id__}:check"')
139
143
  return report
140
144
 
141
- data = workflow.check()
145
+ if inspect.iscoroutinefunction(workflow.check):
146
+ data = await workflow.check()
147
+ else:
148
+ data = workflow.check()
142
149
  if not is_pickleable(data):
143
150
  raise TypeError(
144
151
  f'"{workflow.__workflow_id__}" : "check" return not pickleable data'
@@ -159,13 +166,17 @@ def call_check(workflow: WorkflowType, session_id: str, state_path: Path):
159
166
  return report
160
167
 
161
168
 
162
- def call_calibrate(workflow: WorkflowType, session_id: str, state_path: Path):
169
+ async def call_calibrate(workflow: WorkflowType, session_id: str,
170
+ state_path: Path):
163
171
  report = get_cache(session_id, (workflow.__workflow_id__, 'calibrate'))
164
172
  if report is not None:
165
173
  logger.debug(f'Cache hit for "{workflow.__workflow_id__}:calibrate"')
166
174
  return report
167
175
 
168
- data = workflow.calibrate()
176
+ if inspect.iscoroutinefunction(workflow.calibrate):
177
+ data = await workflow.calibrate()
178
+ else:
179
+ data = workflow.calibrate()
169
180
  if not is_pickleable(data):
170
181
  raise TypeError(
171
182
  f'"{workflow.__workflow_id__}" : "calibrate" return not pickleable data'
@@ -186,12 +197,15 @@ def call_calibrate(workflow: WorkflowType, session_id: str, state_path: Path):
186
197
  return report
187
198
 
188
199
 
189
- def call_check_analyzer(node: WorkflowType,
190
- report: Report,
191
- history: Report | None,
192
- state_path: Path,
193
- plot=False) -> Report:
194
- report = node.check_analyze(report, history=history)
200
+ async def call_check_analyzer(node: WorkflowType,
201
+ report: Report,
202
+ history: Report | None,
203
+ state_path: Path,
204
+ plot=False) -> Report:
205
+ if inspect.iscoroutinefunction(node.check_analyze):
206
+ report = await node.check_analyze(report, history=history)
207
+ else:
208
+ report = node.check_analyze(report, history=history)
195
209
  veryfy_analyzed_report(report, node.__workflow_id__, "check_analyze")
196
210
  report.fully_calibrated = False
197
211
  if report.in_spec:
@@ -211,26 +225,29 @@ def call_check_analyzer(node: WorkflowType,
211
225
  return report
212
226
 
213
227
 
214
- def call_analyzer(node: WorkflowType,
215
- report: Report,
216
- history: Report | None,
217
- state_path: Path,
218
- plot=False) -> Report:
219
-
220
- report = node.analyze(report, history=history)
228
+ async def call_analyzer(node: WorkflowType,
229
+ report: Report,
230
+ history: Report | None,
231
+ state_path: Path,
232
+ plot=False) -> Report:
233
+ if inspect.iscoroutinefunction(node.analyze):
234
+ report = await node.analyze(report, history=history)
235
+ else:
236
+ report = node.analyze(report, history=history)
221
237
  veryfy_analyzed_report(report, node.__workflow_id__, "analyze")
222
238
  if hasattr(node, 'oracle') and callable(node.oracle):
223
239
  logger.debug(
224
240
  f'"{node.__workflow_id__}" has oracle method, calling ...')
225
- report = call_oracle(node, report, history)
241
+ report = await call_oracle(node, report, history)
226
242
  report.fully_calibrated = True
227
243
  save_report(node.__workflow_id__, report, state_path, overwrite=True)
228
244
  if plot:
229
- call_plot(node, report)
245
+ await call_plot(node, report)
230
246
  return report
231
247
 
232
248
 
233
- def call_oracle(node: WorkflowType, report: Report, history: Report | None):
249
+ async def call_oracle(node: WorkflowType, report: Report,
250
+ history: Report | None):
234
251
  sig = inspect.signature(node.oracle)
235
252
  try:
236
253
  if 'history' in sig.parameters and 'system_state' in sig.parameters:
@@ -244,6 +261,8 @@ def call_oracle(node: WorkflowType, report: Report, history: Report | None):
244
261
  system_state=get_heads(report.base_path))
245
262
  else:
246
263
  report = node.oracle(report)
264
+ if inspect.isawaitable(report):
265
+ report = await report
247
266
  except Exception as e:
248
267
  logger.exception(e)
249
268
  report.oracle = {}
@@ -259,8 +278,8 @@ def call_oracle(node: WorkflowType, report: Report, history: Report | None):
259
278
  return report
260
279
 
261
280
 
262
- def check_data(workflow: WorkflowType, state_path: str | Path, plot: bool,
263
- session_id: str) -> Report:
281
+ async def check_data(workflow: WorkflowType, state_path: str | Path,
282
+ plot: bool, session_id: str) -> Report:
264
283
  """
265
284
  check data answers two questions:
266
285
  Is the parameter associated with this cal in spec,
@@ -299,52 +318,56 @@ def check_data(workflow: WorkflowType, state_path: str | Path, plot: bool,
299
318
  logger.debug(
300
319
  f'Checking "{workflow.__workflow_id__}" with "check" method ...')
301
320
 
302
- report = call_check(workflow, session_id, state_path)
321
+ report = await call_check(workflow, session_id, state_path)
303
322
 
304
323
  logger.debug(f'Checked "{workflow.__workflow_id__}" !')
305
- report = call_check_analyzer(workflow,
306
- report,
307
- history,
308
- state_path,
309
- plot=plot)
324
+ report = await call_check_analyzer(workflow,
325
+ report,
326
+ history,
327
+ state_path,
328
+ plot=plot)
310
329
  else:
311
330
  logger.debug(
312
331
  f'Checking "{workflow.__workflow_id__}" with "calibrate" method ...'
313
332
  )
314
333
 
315
- report = call_calibrate(workflow, session_id, state_path)
334
+ report = await call_calibrate(workflow, session_id, state_path)
316
335
 
317
336
  logger.debug(f'Calibrated "{workflow.__workflow_id__}" !')
318
- report = call_analyzer(workflow,
319
- report,
320
- history,
321
- state_path,
322
- plot=plot)
337
+ report = await call_analyzer(workflow,
338
+ report,
339
+ history,
340
+ state_path,
341
+ plot=plot)
323
342
  return report
324
343
 
325
344
 
326
- def calibrate(workflow: WorkflowType, state_path: str | Path, plot: bool,
327
- session_id: str) -> Report:
345
+ async def calibrate(workflow: WorkflowType, state_path: str | Path, plot: bool,
346
+ session_id: str) -> Report:
328
347
  history = find_report(workflow.__workflow_id__, state_path)
329
348
 
330
349
  logger.debug(f'Calibrating "{workflow.__workflow_id__}" ...')
331
350
 
332
- report = call_calibrate(workflow, session_id, state_path)
351
+ report = await call_calibrate(workflow, session_id, state_path)
333
352
 
334
353
  logger.debug(f'Calibrated "{workflow.__workflow_id__}" !')
335
354
 
336
- report = call_analyzer(workflow, report, history, state_path, plot=plot)
355
+ report = await call_analyzer(workflow,
356
+ report,
357
+ history,
358
+ state_path,
359
+ plot=plot)
337
360
  return report
338
361
 
339
362
 
340
- def diagnose(workflow: WorkflowType, code_path: str | Path,
341
- state_path: str | Path, plot: bool, session_id: str):
363
+ async def diagnose(workflow: WorkflowType, code_path: str | Path,
364
+ state_path: str | Path, plot: bool, session_id: str):
342
365
  '''
343
366
  Returns: True if node or dependent recalibrated.
344
367
  '''
345
368
  logger.debug(f'diagnose "{workflow.__workflow_id__}"')
346
369
  # check_data
347
- report = check_data(workflow, state_path, plot, session_id)
370
+ report = await check_data(workflow, state_path, plot, session_id)
348
371
  # in spec case
349
372
  if report.in_spec:
350
373
  logger.debug(
@@ -357,7 +380,7 @@ def diagnose(workflow: WorkflowType, code_path: str | Path,
357
380
  logger.debug(
358
381
  f'"{workflow.__workflow_id__}": Bad data, diagnosing dependents')
359
382
  recalibrated = [
360
- diagnose(n, code_path, state_path, plot, session_id)
383
+ await diagnose(n, code_path, state_path, plot, session_id)
361
384
  for n in get_dependents(workflow, code_path)
362
385
  ]
363
386
  if not any(recalibrated):
@@ -386,7 +409,7 @@ def diagnose(workflow: WorkflowType, code_path: str | Path,
386
409
  else:
387
410
  logger.error(f'Never reach: recalibrate "{workflow.__workflow_id__}"')
388
411
 
389
- report = calibrate(workflow, state_path, plot, session_id)
412
+ report = await calibrate(workflow, state_path, plot, session_id)
390
413
  if report.bad_data or not report.in_spec:
391
414
  obey_the_oracle(report, state_path)
392
415
  raise CalibrationFailedError(
@@ -398,13 +421,13 @@ def diagnose(workflow: WorkflowType, code_path: str | Path,
398
421
 
399
422
 
400
423
  @logger.catch(reraise=True)
401
- def maintain(workflow: WorkflowType,
402
- code_path: str | Path,
403
- state_path: str | Path,
404
- session_id: str | None = None,
405
- run: bool = False,
406
- plot: bool = False,
407
- freeze: bool = False):
424
+ async def maintain(workflow: WorkflowType,
425
+ code_path: str | Path,
426
+ state_path: str | Path,
427
+ session_id: str | None = None,
428
+ run: bool = False,
429
+ plot: bool = False,
430
+ freeze: bool = False):
408
431
  if session_id is None:
409
432
  session_id = uuid.uuid4().hex
410
433
  logger.debug(f'run "{workflow.__workflow_id__}"'
@@ -414,13 +437,13 @@ def maintain(workflow: WorkflowType,
414
437
  logger.debug(
415
438
  f'maintain "{n.__workflow_id__}" because it is depended by "{workflow.__workflow_id__}"'
416
439
  )
417
- maintain(n,
418
- code_path,
419
- state_path,
420
- session_id,
421
- run=False,
422
- plot=plot,
423
- freeze=freeze)
440
+ await maintain(n,
441
+ code_path,
442
+ state_path,
443
+ session_id,
444
+ run=False,
445
+ plot=plot,
446
+ freeze=freeze)
424
447
  else:
425
448
  logger.debug(
426
449
  f'"{workflow.__workflow_id__}": All dependents maintained')
@@ -430,7 +453,7 @@ def maintain(workflow: WorkflowType,
430
453
  f'"{workflow.__workflow_id__}": In spec, no need to maintain')
431
454
  return
432
455
  # check_data
433
- report = check_data(workflow, state_path, plot, session_id)
456
+ report = await check_data(workflow, state_path, plot, session_id)
434
457
  if report.in_spec:
435
458
  if not run:
436
459
  logger.debug(
@@ -443,13 +466,13 @@ def maintain(workflow: WorkflowType,
443
466
  logger.debug(
444
467
  f'diagnose "{n.__workflow_id__}" because of "{workflow.__workflow_id__}" bad data'
445
468
  )
446
- diagnose(n, code_path, state_path, plot, session_id)
469
+ await diagnose(n, code_path, state_path, plot, session_id)
447
470
  else:
448
471
  logger.debug(
449
472
  f'"{workflow.__workflow_id__}": All dependents diagnosed')
450
473
  # calibrate
451
474
  logger.debug(f'recalibrate "{workflow.__workflow_id__}"')
452
- report = calibrate(workflow, state_path, plot, session_id)
475
+ report = await calibrate(workflow, state_path, plot, session_id)
453
476
  if report.bad_data or not report.in_spec:
454
477
  if not freeze:
455
478
  obey_the_oracle(report, state_path)
@@ -465,14 +488,14 @@ def maintain(workflow: WorkflowType,
465
488
 
466
489
 
467
490
  @logger.catch(reraise=True)
468
- def run(workflow: WorkflowType,
469
- code_path: str | Path,
470
- state_path: str | Path,
471
- plot: bool = False,
472
- freeze: bool = False):
491
+ async def run(workflow: WorkflowType,
492
+ code_path: str | Path,
493
+ state_path: str | Path,
494
+ plot: bool = False,
495
+ freeze: bool = False):
473
496
  session_id = uuid.uuid4().hex
474
497
  logger.debug(f'run "{workflow.__workflow_id__}" without dependences.')
475
- report = calibrate(workflow, state_path, plot, session_id=session_id)
498
+ report = await calibrate(workflow, state_path, plot, session_id=session_id)
476
499
  if report.bad_data or not report.in_spec:
477
500
  if not freeze:
478
501
  obey_the_oracle(report, state_path)
qulab/executor/utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ import inspect
1
2
  from pathlib import Path
2
3
 
3
4
  from ..cli.config import get_config_value
@@ -155,7 +156,7 @@ def oracle(report: Report,
155
156
  """
156
157
 
157
158
 
158
- def debug_analyze(
159
+ async def debug_analyze(
159
160
  report_index: int,
160
161
  code_path: str | Path = get_config_value('code', Path),
161
162
  data_path: str | Path = get_config_value('data', Path),
@@ -170,7 +171,8 @@ def debug_analyze(
170
171
  if wf is None:
171
172
  raise ValueError(f'Invalid workflow: {workflow}')
172
173
  if hasattr(wf, '__QULAB_TEMPLATE__'):
173
- template_mtime = (Path(code_path) / wf.__QULAB_TEMPLATE__).stat().st_mtime
174
+ template_mtime = (Path(code_path) /
175
+ wf.__QULAB_TEMPLATE__).stat().st_mtime
174
176
  if template_mtime > wf.__mtime__:
175
177
  for k in dir(wf):
176
178
  if k.startswith('__VAR_') and len(k) == len('__VAR_17fb4dde'):
@@ -182,6 +184,11 @@ def debug_analyze(
182
184
  code_path)
183
185
 
184
186
  report = wf.analyze(report, report.previous)
187
+ if inspect.isawaitable(report):
188
+ report = await report
185
189
  if hasattr(wf, 'plot'):
186
- wf.plot(report)
190
+ if inspect.iscoroutinefunction(wf.plot):
191
+ await wf.plot(report)
192
+ else:
193
+ wf.plot(report)
187
194
  return report
Binary file
qulab/scan/expression.py CHANGED
@@ -4,42 +4,154 @@ import operator
4
4
 
5
5
  import numpy as np
6
6
  from pyparsing import (CaselessKeyword, Combine, Forward, Group, Keyword,
7
- Literal, Optional, ParserElement, Suppress, Word,
8
- alphanums, alphas, delimitedList, nums, oneOf, opAssoc,
9
- pyparsing_common, restOfLine, srange, stringEnd,
10
- stringStart)
7
+ Literal, Optional, ParserElement, ParseResults,
8
+ Suppress, Word, alphanums, alphas, delimitedList,
9
+ infixNotation, nums, oneOf, opAssoc, pyparsing_common,
10
+ restOfLine, srange, stringEnd, stringStart)
11
11
  from scipy import special
12
12
 
13
- LPAREN, RPAREN, LBRACK, RBRACK, LBRACE, RBRACE, DOT, TILDE, BANG, PLUS, MINUS = map(
14
- Suppress, "()[]{}.~!+-")
15
-
16
- INT = Combine(srange("[1-9]") +
17
- Optional(Word(nums))).set_parse_action(lambda t: int(t[0]))
18
- OCT = Combine("0" + Word("01234567")).set_parse_action(lambda t: int(t[0], 8))
19
- HEX = Combine("0x" + Word("0123456789abcdefABCDEF")).set_parse_action(
13
+ # 启用 Packrat 优化以提高解析效率
14
+ ParserElement.enablePackrat()
15
+
16
+ # 定义符号(括号、方括号、花括号、点号等)的 Suppress
17
+ LPAREN, RPAREN = map(Suppress, "()")
18
+ LBRACK, RBRACK = map(Suppress, "[]")
19
+ LBRACE, RBRACE = map(Suppress, "{}")
20
+ DOT = Suppress(".")
21
+ COMMA = Suppress(",")
22
+
23
+ # 数字定义:整数(十进制、八进制、十六进制)和浮点数
24
+ INT = (Combine(Word("123456789", nums))
25
+ | Literal("0")).setParseAction(lambda t: int(t[0]))
26
+ OCT = Combine("0" + Word("01234567")).setParseAction(lambda t: int(t[0], 8))
27
+ HEX = Combine("0x" + Word("0123456789abcdefABCDEF")).setParseAction(
20
28
  lambda t: int(t[0], 16))
21
- FLOAT = Combine(Word(nums) + DOT + Word(nums)) | \
22
- Combine(DOT + Word(nums)) | \
23
- Combine(Word(nums) + DOT) | \
24
- Combine(Word(nums) + DOT + Word(nums) + CaselessKeyword("e") + Word("+-") + Word(nums)) | \
25
- Combine(Word(nums) + DOT + CaselessKeyword("e") + Word("+-") + Word(nums)) | \
26
- Combine(DOT + Word(nums) + CaselessKeyword("e") + Word("+-") + Word(nums)) | \
27
- Combine(Word(nums) + CaselessKeyword("e") + Word("+-") + Word(nums))
28
- FLOAT.set_parse_action(lambda t: float(t[0]))
29
- SYMBOL = Word(alphas, alphanums + "_")
30
- SYMBOL.set_parse_action(lambda t: Symbol(t[0]))
31
-
32
- expr = Forward()
33
- unary = Forward()
34
- binary = Forward()
35
- atom = Forward()
29
+ FLOAT = Combine(
30
+ Word(nums) + '.' + Word(nums) | '.' + Word(nums) | Word(nums) + '.'
31
+ | Word(nums) + (Literal('e') | Literal('E')) +
32
+ Word("+-" + nums)).setParseAction(lambda t: float(t[0]))
33
+
34
+
35
+ # 定义标识符,转换为 Symbol 对象(在 Expression 类中已定义)
36
+ def symbol_parse_action(t):
37
+ return Symbol(t[0])
38
+
39
+
40
+ SYMBOL = Word(alphas, alphanums + "_").setParseAction(symbol_parse_action)
41
+
42
+ #------------------------------------------------------------------------------
43
+ # 定义运算表达式的解析动作转换函数
44
+
45
+ # 一元运算符映射(注意:此处 ! 使用逻辑非 operator.not_)
46
+ unary_ops = {
47
+ '+': operator.pos,
48
+ '-': operator.neg,
49
+ '~': operator.invert,
50
+ '!': operator.not_
51
+ }
52
+
53
+
54
+ def unary_parse_action(tokens: ParseResults) -> Expression:
55
+ # tokens 形如:[['-', operand]]
56
+ op, operand = tokens[0]
57
+ # operand 已经是 Expression 对象(或常量),构造 UnaryExpression
58
+ return UnaryExpression(operand, unary_ops[op])
59
+
60
+
61
+ # 二元运算符映射
62
+ binary_ops = {
63
+ '+': operator.add,
64
+ '-': operator.sub,
65
+ '*': operator.mul,
66
+ '/': operator.truediv,
67
+ '//': operator.floordiv,
68
+ '%': operator.mod,
69
+ '**': operator.pow,
70
+ '@': operator.matmul,
71
+ '<<': operator.lshift,
72
+ '>>': operator.rshift,
73
+ '&': operator.and_,
74
+ '^': operator.xor,
75
+ '|': operator.or_,
76
+ '<': operator.lt,
77
+ '<=': operator.le,
78
+ '>': operator.gt,
79
+ '>=': operator.ge,
80
+ '==': operator.eq,
81
+ '!=': operator.ne
82
+ }
83
+
84
+
85
+ def binary_parse_action(tokens: ParseResults) -> Expression:
86
+ # tokens[0] 为形如:[operand1, op, operand2, op, operand3, ...]
87
+ t = tokens[0]
88
+ expr = t[0]
89
+ for i in range(1, len(t), 2):
90
+ op = t[i]
91
+ right = t[i + 1]
92
+ expr = BinaryExpression(expr, right, binary_ops[op])
93
+ return expr
36
94
 
37
- atom << (INT | OCT | HEX | FLOAT | SYMBOL | (LPAREN + expr + RPAREN) |
38
- (LBRACK + expr + RBRACK) | (LBRACE + expr + RBRACE) | (MINUS + atom) |
39
- (PLUS + atom) | (TILDE + atom) | (BANG + atom) | (nums + DOT + nums))
40
95
 
41
- unary << (atom | (MINUS + unary) | (PLUS + unary) | (TILDE + unary) |
42
- (BANG + unary))
96
+ expr = Forward()
97
+ #------------------------------------------------------------------------------
98
+ # 构造基元表达式:包括数值、标识符、括号内表达式
99
+ atom = (
100
+ FLOAT | INT | OCT | HEX | SYMBOL |
101
+ (LPAREN + expr + RPAREN) # 注意:后面我们将使用递归定义 expr
102
+ )
103
+
104
+
105
+ # 为支持函数调用和属性访问,构造后缀表达式:
106
+ # 例如: func(x,y).attr
107
+ def parse_function_call(expr_obj):
108
+ # 参数列表可能为空,或者用逗号分隔的表达式列表
109
+ arg_list = Optional(delimitedList(expr), default=[])
110
+ return LPAREN + Group(arg_list) + RPAREN
111
+
112
+
113
+ postfix = Forward()
114
+ # 后缀可以是函数调用,也可以是属性访问
115
+ postfix_operation = ((parse_function_call(atom)
116
+ ).setParseAction(lambda t: ('CALL', t[0].asList())) |
117
+ (DOT + SYMBOL).setParseAction(lambda t: ('ATTR', t[0])))
118
+
119
+
120
+ # 定义 factor:先解析 atom,然后再依次处理后缀操作
121
+ def attach_postfix(tokens: ParseResults) -> Expression:
122
+ # tokens[0] 为初始的 atom 对象
123
+ expr_obj = tokens[0]
124
+ # 遍历后缀操作序列
125
+ for op, arg in tokens[1:]:
126
+ if op == 'CALL':
127
+ # 对于函数调用,arg 是参数列表,调用 __call__ 运算符(由 Expression.__call__ 实现)
128
+ expr_obj = expr_obj(*arg)
129
+ elif op == 'ATTR':
130
+ # 对于属性访问,用 ObjectMethod 构造
131
+ expr_obj = ObjectMethod(expr_obj, '__getattr__', arg)
132
+ return expr_obj
133
+
134
+
135
+ # 将 atom 与后缀操作连接
136
+ postfix << (atom + Optional(
137
+ (postfix_operation[...]))).setParseAction(attach_postfix)
138
+
139
+ #------------------------------------------------------------------------------
140
+ # 现在构造整个表达式解析器,利用 infixNotation 建立运算符优先级
141
+ expr <<= infixNotation(
142
+ postfix,
143
+ [
144
+ (oneOf('! ~ + -'), 1, opAssoc.RIGHT, unary_parse_action),
145
+ # 指数运算,右结合
146
+ (Literal("**"), 2, opAssoc.RIGHT, binary_parse_action),
147
+ (oneOf('* / // % @'), 2, opAssoc.LEFT, binary_parse_action),
148
+ (oneOf('+ -'), 2, opAssoc.LEFT, binary_parse_action),
149
+ (oneOf('<< >>'), 2, opAssoc.LEFT, binary_parse_action),
150
+ (oneOf('&'), 2, opAssoc.LEFT, binary_parse_action),
151
+ (oneOf('^'), 2, opAssoc.LEFT, binary_parse_action),
152
+ (oneOf('|'), 2, opAssoc.LEFT, binary_parse_action),
153
+ (oneOf('< <= > >= == !='), 2, opAssoc.LEFT, binary_parse_action),
154
+ ])
43
155
 
44
156
  ConstType = (int, float, complex)
45
157
  _empty = object()
@@ -91,7 +203,8 @@ class Env():
91
203
  }
92
204
 
93
205
  def __contains__(self, key):
94
- return key in self.consts or key in self.variables or key in self.functions or key in self.refs
206
+ return (key in self.consts or key in self.variables
207
+ or key in self.functions or key in self.refs)
95
208
 
96
209
  def __getitem__(self, key):
97
210
  if key in self.consts:
@@ -107,6 +220,8 @@ class Env():
107
220
  def __setitem__(self, key, value):
108
221
  if key in self.consts:
109
222
  raise KeyError(f"Key {key:r} is const")
223
+ if key in self.functions:
224
+ raise KeyError(f"Key {key:r} is function")
110
225
  elif isinstance(value, Ref):
111
226
  self.create_ref(key, value.name)
112
227
  elif key in self.refs:
@@ -117,6 +232,8 @@ class Env():
117
232
  def __delitem__(self, key):
118
233
  if key in self.consts:
119
234
  raise KeyError(f"Key {key:r} is const")
235
+ if key in self.functions:
236
+ raise KeyError(f"Key {key:r} is function")
120
237
  elif key in self.refs:
121
238
  del self[self.refs[key]]
122
239
  else:
@@ -576,8 +693,14 @@ class ObjectMethod(Expression):
576
693
  if isinstance(obj, Expression) or any(
577
694
  isinstance(x, Expression) for x in args):
578
695
  return ObjectMethod(obj, self.method, *args)
696
+ elif self.method == '__getattr__':
697
+ print(f"getattr {obj} {args}")
698
+ return getattr(
699
+ obj, *[a.name if isinstance(a, Symbol) else a for a in args])
579
700
  else:
580
- return getattr(obj, self.method)(*args)
701
+ return getattr(obj, self.method)(*[
702
+ a.value(env) if isinstance(a, Expression) else a for a in args
703
+ ])
581
704
 
582
705
  def __repr__(self):
583
706
  if self.method == '__call__':
@@ -604,7 +727,11 @@ class Symbol(Expression):
604
727
 
605
728
  def eval(self, env):
606
729
  if self.name in env:
607
- return env[self.name]
730
+ value = env[self.name]
731
+ if isinstance(value, Expression):
732
+ return value.eval(env)
733
+ else:
734
+ return value
608
735
  else:
609
736
  return self
610
737
 
@@ -644,3 +771,29 @@ sign = Symbol('sign')
644
771
  heaviside = Symbol('heaviside')
645
772
  erf = Symbol('erf')
646
773
  erfc = Symbol('erfc')
774
+
775
+
776
+ def calc(exp: str | Expression, **kwargs) -> Expression:
777
+ """
778
+ Calculate the expression.
779
+
780
+ Parameters
781
+ ----------
782
+ exp : str | Expression
783
+ The expression to be calculated.
784
+ env : Env, optional
785
+ The environment to be used for the calculation. Default is _default_env.
786
+ **kwargs : dict
787
+ Additional arguments to be passed to the expression.
788
+
789
+ Returns
790
+ -------
791
+ Expression
792
+ The calculated expression.
793
+ """
794
+ env = Env()
795
+ for k, v in kwargs.items():
796
+ env[k] = v
797
+ if isinstance(exp, str):
798
+ exp = expr.parseString(exp, parseAll=True)[0]
799
+ return exp.eval(env)
qulab/scan/server.py CHANGED
@@ -14,6 +14,7 @@ from loguru import logger
14
14
 
15
15
  from qulab.sys.rpc.zmq_socket import ZMQContextManager
16
16
 
17
+ from ..cli.config import get_config_value, log_options
17
18
  from .curd import (create_cell, create_config, create_notebook, get_config,
18
19
  query_record, remove_tags, tag, update_tags)
19
20
  from .models import Cell, Notebook
@@ -411,123 +412,36 @@ async def serv(port,
411
412
  last_flush_time = time.time()
412
413
 
413
414
 
414
- async def main(port,
415
- datapath,
416
- url,
417
- timeout=1,
418
- buffer=1024,
419
- interval=60,
420
- log='stderr',
421
- no_watch=True,
422
- debug=False):
423
- if no_watch:
424
- logger.remove()
425
- if debug:
426
- level = 'DEBUG'
427
- else:
428
- level = 'INFO'
429
- if log == 'stderr':
430
- logger.add(sys.stderr, level=level)
431
- elif log == 'stdout':
432
- logger.add(sys.stdout, level=level)
433
- else:
434
- logger.add(sys.stderr, level=level)
435
- logger.add(log, level=level)
436
- logger.debug(f"logging level: {level}")
437
- logger.info('Server starting...')
438
- await serv(port, datapath, url, buffer * 1024 * 1024, interval)
439
- else:
440
- process = None
441
-
442
- while True:
443
- try:
444
- with ZMQContextManager(
445
- zmq.DEALER, connect=f"tcp://127.0.0.1:{port}") as sock:
446
- sock.setsockopt(zmq.LINGER, 0)
447
- sock.send_pyobj({"method": "ping"})
448
- logger.debug('ping.')
449
- if sock.poll(int(1000 * timeout)):
450
- sock.recv()
451
- logger.debug('recv pong.')
452
- else:
453
- logger.debug('timeout.')
454
- raise asyncio.TimeoutError()
455
- except (zmq.error.ZMQError, asyncio.TimeoutError):
456
- if process is not None:
457
- logger.debug(
458
- f'killing process... PID={process.pid}, returncode={process.returncode}'
459
- )
460
- process.kill()
461
- logger.debug(
462
- f'killed process. PID={process.pid}, returncode={process.returncode}'
463
- )
464
- cmd = [
465
- sys.executable,
466
- "-m",
467
- "qulab",
468
- "server",
469
- "--port",
470
- f"{port}",
471
- "--datapath",
472
- f"{datapath}",
473
- "--url",
474
- f"{url}",
475
- "--timeout",
476
- f"{timeout}",
477
- "--buffer",
478
- f"{buffer}",
479
- "--interval",
480
- f"{interval}",
481
- "--log",
482
- f"{log}",
483
- ]
484
- if url:
485
- cmd.extend(['--url', url])
486
- if debug:
487
- cmd.append('--debug')
488
- cmd.append("--no-watch")
489
- logger.debug(f"starting process: {' '.join(cmd)}")
490
- process = subprocess.Popen(cmd, cwd=os.getcwd())
491
- logger.debug(
492
- f'process started. PID={process.pid}, returncode={process.returncode}'
493
- )
494
-
495
- # Capture and log the output
496
- # stdout, stderr = process.communicate(timeout=5)
497
- # if stdout:
498
- # logger.info(f'Server stdout: {stdout.decode()}')
499
- # if stderr:
500
- # logger.error(f'Server stderr: {stderr.decode()}')
501
-
502
- await asyncio.sleep(5)
503
- await asyncio.sleep(timeout)
415
+ async def main(port, datapath, url, buffer=1024, interval=60):
416
+ logger.info('Server starting...')
417
+ await serv(port, datapath, url, buffer * 1024 * 1024, interval)
504
418
 
505
419
 
506
420
  @click.command()
507
421
  @click.option('--port',
508
- default=os.getenv('QULAB_RECORD_PORT', 6789),
422
+ default=get_config_value('port',
423
+ int,
424
+ command_name='server',
425
+ default=6789),
509
426
  help='Port of the server.')
510
- @click.option('--datapath', default=datapath, help='Path of the data.')
427
+ @click.option('--datapath',
428
+ default=get_config_value('data',
429
+ Path,
430
+ command_name='server',
431
+ default=Path.home() / 'qulab' / 'data'),
432
+ help='Path of the data.')
511
433
  @click.option('--url', default='sqlite', help='URL of the database.')
512
- @click.option('--timeout', default=1, help='Timeout of ping.')
513
434
  @click.option('--buffer', default=1024, help='Buffer size (MB).')
514
435
  @click.option('--interval',
515
436
  default=60,
516
437
  help='Interval of flush cache, in unit of second.')
517
- @click.option('--log', default='stderr', help='Log file.')
518
- @click.option('--no-watch', is_flag=True, help='Watch the server.')
519
- @click.option('--debug', is_flag=True, help='Debug mode.')
520
- def server(port, datapath, url, timeout, buffer, interval, log, no_watch,
521
- debug):
438
+ @log_options(command_name='server')
439
+ def server(port, datapath, url, buffer, interval):
522
440
  try:
523
441
  import uvloop
524
- uvloop.run(
525
- main(port, Path(datapath), url, timeout, buffer, interval, log,
526
- True, debug))
442
+ uvloop.run(main(port, Path(datapath), url, buffer, interval))
527
443
  except ImportError:
528
- asyncio.run(
529
- main(port, Path(datapath), url, timeout, buffer, interval, log,
530
- True, debug))
444
+ asyncio.run(main(port, Path(datapath), url, buffer, interval))
531
445
 
532
446
 
533
447
  if __name__ == "__main__":
qulab/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.9.10"
1
+ __version__ = "2.10.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuLab
3
- Version: 2.9.10
3
+ Version: 2.10.1
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -1,22 +1,23 @@
1
1
  qulab/__init__.py,sha256=fL8FSMGk4vzTMXLxWgNHpd1fNWau4yujSWjPEPqudos,367
2
2
  qulab/__main__.py,sha256=fjaRSL_uUjNIzBGNgjlGswb9TJ2VD5qnkZHW3hItrD4,68
3
3
  qulab/dicttree.py,sha256=tRRMpGZYVOLw0TEByE3_2Ss8FdOmzuGL9e1DWbs8qoY,13684
4
- qulab/fun.cpython-311-darwin.so,sha256=cw2IX4NENbwEgKYJQjsVFMw71pK2uTxyh9TeXcNwEKI,126848
4
+ qulab/fun.cpython-311-darwin.so,sha256=wJrw2ShQsQUQqw8fYBQI0OFDRqVGZu6Hwhn-VwsumeY,126848
5
5
  qulab/typing.py,sha256=vg62sGqxuD9CI5677ejlzAmf2fVdAESZCQjAE_xSxPg,69
6
6
  qulab/utils.py,sha256=BdLdlfjpe6m6gSeONYmpAKTTqxDaYHNk4exlz8kZxTg,2982
7
- qulab/version.py,sha256=ibM5yQ1vwSBgdRVCwiRzSGyWXnuL47O8ktNp4dWQPKM,22
7
+ qulab/version.py,sha256=QsekqJipSrjPuGihvOztbBNg-NIr5A4FfBf197XlERA,22
8
8
  qulab/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  qulab/cli/commands.py,sha256=F1LATmSC9lOAdnrOTMK7DRjETCEcOmMsocovWRyjWTc,597
10
- qulab/cli/config.py,sha256=Ei7eSYnbwPPlluDnm8YmWONYiI4g7WtvlZGQdr1Z6vo,3688
10
+ qulab/cli/config.py,sha256=fWeWHiX-Oxodvfv4g0B-UFisJlYcF1Hv6V1fKib2mOM,5447
11
+ qulab/cli/decorators.py,sha256=66lzX1FtGG3C0qheLOjbKquLitnivXgmWyEG2ywRvKY,598
11
12
  qulab/executor/__init__.py,sha256=LosPzOMaljSZY1thy_Fxtbrgq7uubJszMABEB7oM7tU,101
12
13
  qulab/executor/analyze.py,sha256=4Hau5LrKUdpweh7W94tcG4ahgxucHOevbM0hm57T7zE,5649
13
- qulab/executor/cli.py,sha256=xceK6XNLnNpAVtqvs0_wOAuG0sgS9jg1iTFYmxVUMWY,11795
14
- qulab/executor/load.py,sha256=6QLlATu2KyGY2nrWLFTVxDE4DwoCXx3WNx72QR_LZB0,18715
15
- qulab/executor/schedule.py,sha256=XRimchHYCgnMAOtCvNLjwMv9IkcTAyBAUCHKQs5RBRw,18745
14
+ qulab/executor/cli.py,sha256=JhaOvMfLHOFe8HdQhGeqU3Lj3z0mJXLXJhzIwmAsaso,12251
15
+ qulab/executor/load.py,sha256=Nk9hdLipNRQ8mdPTpFNeIwiFi1ybOYBwsT-w1lN7IwU,18988
16
+ qulab/executor/schedule.py,sha256=PmNRvs41DXKC2teHj5XGblpUEKLrsZpgPqk2K-opA-g,20009
16
17
  qulab/executor/storage.py,sha256=8K73KGLAVgchJdtd4rKHXkr1CQOJORWH-Gi57w8IYsw,21081
17
18
  qulab/executor/template.py,sha256=_HEtsUQ5_jSujCw8FBDAK1PRTMRCa4iD4DduHIpjo3c,10569
18
19
  qulab/executor/transform.py,sha256=rk4CLIKVjGRaFzi5FVSgadUxAKKVLSopEHZCaAzDwDg,3435
19
- qulab/executor/utils.py,sha256=l_b0y2kMwYKyyXeFtoblPYwKNU-wiFQ9PMo9QlWl9wE,6213
20
+ qulab/executor/utils.py,sha256=S254JDTKVA4Fnu9lslNBos6vEssLWn8zbMjhmLP-Gi8,6434
20
21
  qulab/monitor/__init__.py,sha256=nTHelnDpxRS_fl_B38TsN0njgq8eVTEz9IAnN3NbDlM,42
21
22
  qulab/monitor/__main__.py,sha256=w3yUcqq195LzSnXTkQcuC1RSFRhy4oQ_PEBmucXguME,97
22
23
  qulab/monitor/config.py,sha256=fQ5JcsMApKc1UwANEnIvbDQZl8uYW0tle92SaYtX9lI,744
@@ -29,13 +30,13 @@ qulab/monitor/qt_compat.py,sha256=OK71_JSO_iyXjRIKHANmaK4Lx4bILUzmXI-mwKg3QeI,78
29
30
  qulab/monitor/toolbar.py,sha256=WEag6cxAtEsOLL14XvM7pSE56EA3MO188_JuprNjdBs,7948
30
31
  qulab/scan/__init__.py,sha256=ZX4WsvqYxvJeHLgGSrtJoAnVU94gxY7EHKMxYooMERg,130
31
32
  qulab/scan/curd.py,sha256=ZcwO5SKO95OcDgpUPDHcK5FW_BrJbUzuBk71UG_pqnM,6888
32
- qulab/scan/expression.py,sha256=Yw2_ynDpKiLeyS5m53lURFtesLkzaF9PIyAWbW-LOr0,20176
33
+ qulab/scan/expression.py,sha256=Mk9-hzmTzDszM0p6J_FlrewzQerOBWhRv5_iFApA768,24978
33
34
  qulab/scan/models.py,sha256=JFofv-RH0gpS3--M6izXiQg7iGkS9a_em2DwhS5kTTk,17626
34
35
  qulab/scan/optimize.py,sha256=VT9TpuqDkG7FdJJkYy60r5Pfrmjvfu5i36Ru6XvIiTI,2561
35
36
  qulab/scan/query.py,sha256=-5uHMhXSyGovK1oy_uUbGVEbRFzaMBkP78ZMNfI3jD0,11809
36
37
  qulab/scan/record.py,sha256=yIHPANf6nuBXy8Igf-dMtGJ7wuFTLYlBaaAUc0AzwyU,21280
37
38
  qulab/scan/scan.py,sha256=iXvbnXLZvHa4v88MlYiZ_LYubEB7ZfbX7OiFTMhl_1o,39479
38
- qulab/scan/server.py,sha256=PEtitQeejAD11HYwTs1o1kJwkK7BkN0oLhn2_Pypk9M,20054
39
+ qulab/scan/server.py,sha256=QhUo-TxdTb_PDBd4sDRiI5L1WHmuEOAYEmUiFhjNqc4,16995
39
40
  qulab/scan/space.py,sha256=OQLepiNNP5TNYMHXeVFT59lL5n4soPmnMoMy_o9EHt0,6696
40
41
  qulab/scan/utils.py,sha256=SzJ_c4cLZJzERZr_CJO1_4juOxjfwCpU2K1mkc1PWGM,6124
41
42
  qulab/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -98,9 +99,9 @@ qulab/visualization/plot_seq.py,sha256=UWTS6p9nfX_7B8ehcYo6UnSTUCjkBsNU9jiOeW2ca
98
99
  qulab/visualization/qdat.py,sha256=ZeevBYWkzbww4xZnsjHhw7wRorJCBzbG0iEu-XQB4EA,5735
99
100
  qulab/visualization/rot3d.py,sha256=lMrEJlRLwYe6NMBlGkKYpp_V9CTipOAuDy6QW_cQK00,734
100
101
  qulab/visualization/widgets.py,sha256=6KkiTyQ8J-ei70LbPQZAK35wjktY47w2IveOa682ftA,3180
101
- qulab-2.9.10.dist-info/licenses/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
102
- qulab-2.9.10.dist-info/METADATA,sha256=MhYNsJiHzLm1CBvBFtOywr1NxgJ7TXZItACdTXeOJII,3753
103
- qulab-2.9.10.dist-info/WHEEL,sha256=RbtvOFtP--plS2sXkxxM8xYfkibPkCKcYIqEe_GHrhY,114
104
- qulab-2.9.10.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
105
- qulab-2.9.10.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
106
- qulab-2.9.10.dist-info/RECORD,,
102
+ qulab-2.10.1.dist-info/licenses/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
103
+ qulab-2.10.1.dist-info/METADATA,sha256=gDKZGYiA5s8-si5QgJB5wWwEnMQngGmPaHJ5DqrP--E,3753
104
+ qulab-2.10.1.dist-info/WHEEL,sha256=RbtvOFtP--plS2sXkxxM8xYfkibPkCKcYIqEe_GHrhY,114
105
+ qulab-2.10.1.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
106
+ qulab-2.10.1.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
107
+ qulab-2.10.1.dist-info/RECORD,,
File without changes