QuLab 2.1.0__cp312-cp312-macosx_10_9_universal2.whl → 2.1.2__cp312-cp312-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/scan/scan.py CHANGED
@@ -1,35 +1,56 @@
1
1
  import asyncio
2
2
  import copy
3
- import datetime
4
3
  import inspect
5
4
  import itertools
6
5
  import os
7
6
  import re
8
7
  import sys
9
8
  import uuid
10
- import warnings
11
9
  from concurrent.futures import ProcessPoolExecutor
12
10
  from graphlib import TopologicalSorter
13
11
  from pathlib import Path
14
- from types import MethodType
15
- from typing import Any, Awaitable, Callable, Iterable, Type
12
+ from typing import Any, Awaitable, Callable, Iterable
16
13
 
17
14
  import dill
18
15
  import numpy as np
19
- import skopt
20
16
  import zmq
21
- from skopt.space import Categorical, Integer, Real
22
- from tqdm.notebook import tqdm
23
17
 
24
18
  from ..sys.rpc.zmq_socket import ZMQContextManager
25
19
  from .expression import Env, Expression, Symbol
26
20
  from .optimize import NgOptimizer
27
- from .recorder import Record, default_record_port
28
- from .utils import async_zip, call_function
21
+ from .record import Record
22
+ from .recorder import default_record_port
23
+ from .space import Optimizer, OptimizeSpace, Space
24
+ from .utils import async_zip, call_function, dump_globals
25
+
26
+ try:
27
+ from tqdm.notebook import tqdm
28
+ except:
29
+
30
+ class tqdm():
31
+
32
+ def update(self, n):
33
+ pass
34
+
35
+ def close(self):
36
+ pass
37
+
38
+ def reset(self):
39
+ pass
40
+
29
41
 
30
42
  __process_uuid = uuid.uuid1()
31
43
  __task_counter = itertools.count()
32
44
 
45
+ if os.getenv('QULAB_SERVER'):
46
+ default_server = os.getenv('QULAB_SERVER')
47
+ else:
48
+ default_server = f'tcp://127.0.0.1:{default_record_port}'
49
+ if os.getenv('QULAB_EXECUTOR'):
50
+ default_executor = os.getenv('QULAB_EXECUTOR')
51
+ else:
52
+ default_executor = default_server
53
+
33
54
 
34
55
  def task_uuid():
35
56
  return uuid.uuid3(__process_uuid, str(next(__task_counter)))
@@ -55,79 +76,6 @@ def _get_depends(func: Callable):
55
76
  return args
56
77
 
57
78
 
58
- class OptimizeSpace():
59
-
60
- def __init__(self, optimizer: 'Optimizer', space):
61
- self.optimizer = optimizer
62
- self.space = space
63
- self.name = None
64
-
65
- def __len__(self):
66
- return self.optimizer.maxiter
67
-
68
-
69
- class Optimizer():
70
-
71
- def __init__(self,
72
- scanner: 'Scan',
73
- name: str,
74
- level: int,
75
- method: str | Type = skopt.Optimizer,
76
- maxiter: int = 1000,
77
- minimize: bool = True,
78
- **kwds):
79
- self.scanner = scanner
80
- self.method = method
81
- self.maxiter = maxiter
82
- self.dimensions = {}
83
- self.name = name
84
- self.level = level
85
- self.kwds = kwds
86
- self.minimize = minimize
87
-
88
- def create(self):
89
- return self.method(list(self.dimensions.values()), **self.kwds)
90
-
91
- def Categorical(self,
92
- categories,
93
- prior=None,
94
- transform=None,
95
- name=None) -> OptimizeSpace:
96
- return OptimizeSpace(self,
97
- Categorical(categories, prior, transform, name))
98
-
99
- def Integer(self,
100
- low,
101
- high,
102
- prior="uniform",
103
- base=10,
104
- transform=None,
105
- name=None,
106
- dtype=np.int64) -> OptimizeSpace:
107
- return OptimizeSpace(
108
- self, Integer(low, high, prior, base, transform, name, dtype))
109
-
110
- def Real(self,
111
- low,
112
- high,
113
- prior="uniform",
114
- base=10,
115
- transform=None,
116
- name=None,
117
- dtype=float) -> OptimizeSpace:
118
- return OptimizeSpace(
119
- self, Real(low, high, prior, base, transform, name, dtype))
120
-
121
- def __getstate__(self) -> dict:
122
- state = self.__dict__.copy()
123
- del state['scanner']
124
- return state
125
-
126
- def __setstate__(self, state: dict) -> None:
127
- self.__dict__.update(state)
128
- self.scanner = None
129
-
130
-
131
79
  class Promise():
132
80
  __slots__ = ['task', 'key', 'attr']
133
81
 
@@ -180,7 +128,7 @@ class Scan():
180
128
  app: str = 'task',
181
129
  tags: tuple[str] = (),
182
130
  database: str | Path
183
- | None = f'tcp://127.0.0.1:{default_record_port}',
131
+ | None = default_server,
184
132
  dump_globals: bool = False,
185
133
  max_workers: int = 4,
186
134
  max_promise: int = 100,
@@ -192,6 +140,7 @@ class Scan():
192
140
  'app': app,
193
141
  'tags': tags,
194
142
  'loops': {},
143
+ 'intrinsic_loops': {},
195
144
  'consts': {},
196
145
  'functions': {},
197
146
  'getters': {},
@@ -201,6 +150,8 @@ class Scan():
201
150
  'actions': {},
202
151
  'dependents': {},
203
152
  'order': {},
153
+ 'axis': {},
154
+ 'independent_variables': set(),
204
155
  'filters': {},
205
156
  'total': {},
206
157
  'database': database,
@@ -341,10 +292,10 @@ class Scan():
341
292
  else:
342
293
  return Symbol(name)
343
294
 
344
- def _add_loop_var(self, name: str, level: int, range):
295
+ def _add_search_space(self, name: str, level: int, space):
345
296
  if level not in self.description['loops']:
346
297
  self.description['loops'][level] = []
347
- self.description['loops'][level].append((name, range))
298
+ self.description['loops'][level].append((name, space))
348
299
 
349
300
  def add_depends(self, name: str, depends: list[str]):
350
301
  if isinstance(depends, str):
@@ -353,7 +304,7 @@ class Scan():
353
304
  self.description['dependents'][name] = set()
354
305
  self.description['dependents'][name].update(depends)
355
306
 
356
- def add_filter(self, func: Callable, level: int):
307
+ def add_filter(self, func: Callable, level: int = -1):
357
308
  """
358
309
  Add a filter function to the scan.
359
310
 
@@ -365,7 +316,11 @@ class Scan():
365
316
  self.description['filters'][level] = []
366
317
  self.description['filters'][level].append(func)
367
318
 
368
- def set(self, name: str, value, setter: Callable | None = None):
319
+ def set(self,
320
+ name: str,
321
+ value,
322
+ depends: Iterable[str] | None = None,
323
+ setter: Callable | None = None):
369
324
  try:
370
325
  dill.dumps(value)
371
326
  except:
@@ -374,31 +329,54 @@ class Scan():
374
329
  self.add_depends(name, value.symbols())
375
330
  self.description['functions'][name] = value
376
331
  elif callable(value):
377
- self.add_depends(name, _get_depends(value))
378
- self.description['functions'][name] = value
332
+ if depends:
333
+ self.add_depends(name, depends)
334
+ s = ','.join(depends)
335
+ self.description['functions'][f'_tmp_{name}'] = value
336
+ self.description['functions'][name] = eval(
337
+ f"lambda self, {s}: self.description['functions']['_tmp_{name}']({s})"
338
+ )
339
+ else:
340
+ self.add_depends(name, _get_depends(value))
341
+ self.description['functions'][name] = value
379
342
  else:
343
+ try:
344
+ value = Space.fromarray(value)
345
+ except:
346
+ pass
380
347
  self.description['consts'][name] = value
381
348
  if setter:
382
349
  self.description['setters'][name] = setter
383
350
 
384
351
  def search(self,
385
352
  name: str,
386
- range: Iterable | Expression | Callable | OptimizeSpace,
353
+ space: Iterable | Expression | Callable | OptimizeSpace,
387
354
  level: int | None = None,
388
- setter: Callable | None = None):
355
+ setter: Callable | None = None,
356
+ intrinsic: bool = False):
389
357
  if level is not None:
390
- assert level >= 0, 'level must be greater than or equal to 0.'
391
- if isinstance(range, OptimizeSpace):
392
- range.name = name
393
- range.optimizer.dimensions[name] = range.space
394
- self._add_loop_var(name, range.optimizer.level, range)
395
- self.add_depends(range.optimizer.name, [name])
358
+ if not intrinsic:
359
+ assert level >= 0, 'level must be greater than or equal to 0.'
360
+ if intrinsic:
361
+ assert isinstance(space, (np.ndarray, list, tuple, range, Space)), \
362
+ 'space must be an instance of np.ndarray, list, tuple, range or Space.'
363
+ self.description['intrinsic_loops'][name] = level
364
+ self.set(name, space)
365
+ elif isinstance(space, OptimizeSpace):
366
+ space.name = name
367
+ space.optimizer.dimensions[name] = space.space
368
+ self._add_search_space(name, space.optimizer.level, space)
369
+ self.add_depends(space.optimizer.name, [name])
396
370
  else:
397
371
  if level is None:
398
372
  raise ValueError('level must be provided.')
399
- self._add_loop_var(name, level, range)
400
- if isinstance(range, Expression) or callable(range):
401
- self.add_depends(name, range.symbols())
373
+ try:
374
+ space = Space.fromarray(space)
375
+ except:
376
+ pass
377
+ self._add_search_space(name, level, space)
378
+ if isinstance(space, Expression) or callable(space):
379
+ self.add_depends(name, space.symbols())
402
380
  if setter:
403
381
  self.description['setters'][name] = setter
404
382
 
@@ -481,7 +459,14 @@ class Scan():
481
459
 
482
460
  self._variables = {'self': self}
483
461
 
484
- await update_variables(self._variables, self.description['consts'],
462
+ consts = {}
463
+ for k, v in self.description['consts'].items():
464
+ if isinstance(v, Space):
465
+ consts[k] = v.toarray()
466
+ else:
467
+ consts[k] = v
468
+
469
+ await update_variables(self._variables, consts,
485
470
  self.description['setters'])
486
471
  for level, total in self.description['total'].items():
487
472
  if total == np.inf:
@@ -527,7 +512,7 @@ class Scan():
527
512
  import asyncio
528
513
  self._main_task = asyncio.create_task(self.run())
529
514
 
530
- async def submit(self, server='tcp://127.0.0.1:6788'):
515
+ async def submit(self, server=default_executor):
531
516
  assymbly(self.description)
532
517
  async with ZMQContextManager(zmq.DEALER, connect=server) as socket:
533
518
  await socket.send_pyobj({
@@ -640,51 +625,6 @@ class Scan():
640
625
  return await awaitable
641
626
 
642
627
 
643
- class Unpicklable:
644
-
645
- def __init__(self, obj):
646
- self.type = str(type(obj))
647
- self.id = id(obj)
648
-
649
- def __repr__(self):
650
- return f'<Unpicklable: {self.type} at 0x{id(self):x}>'
651
-
652
-
653
- class TooLarge:
654
-
655
- def __init__(self, obj):
656
- self.type = str(type(obj))
657
- self.id = id(obj)
658
-
659
- def __repr__(self):
660
- return f'<TooLarge: {self.type} at 0x{id(self):x}>'
661
-
662
-
663
- def dump_globals(ns=None, *, size_limit=10 * 1024 * 1024, warn=False):
664
- import __main__
665
-
666
- if ns is None:
667
- ns = __main__.__dict__
668
-
669
- namespace = {}
670
-
671
- for name, value in ns.items():
672
- try:
673
- buf = dill.dumps(value)
674
- except:
675
- namespace[name] = Unpicklable(value)
676
- if warn:
677
- warnings.warn(f'Unpicklable: {name} {type(value)}')
678
- if len(buf) > size_limit:
679
- namespace[name] = TooLarge(value)
680
- if warn:
681
- warnings.warn(f'TooLarge: {name} {type(value)}')
682
- else:
683
- namespace[name] = buf
684
-
685
- return namespace
686
-
687
-
688
628
  def assymbly(description):
689
629
  import __main__
690
630
  from IPython import get_ipython
@@ -810,6 +750,38 @@ def assymbly(description):
810
750
  if ready:
811
751
  description['order'][level].append(ready)
812
752
  keys -= set(ready)
753
+
754
+ axis = {}
755
+ independent_variables = set(description['intrinsic_loops'].keys())
756
+
757
+ for name in description['consts']:
758
+ axis[name] = ()
759
+ for level, range_list in description['loops'].items():
760
+ for name, iterable in range_list:
761
+ if isinstance(iterable, OptimizeSpace):
762
+ axis[name] = tuple(range(level + 1))
763
+ continue
764
+ elif isinstance(iterable, (np.ndarray, list, tuple, range, Space)):
765
+ independent_variables.add(name)
766
+ axis[name] = (level, )
767
+
768
+ for level, group in description['order'].items():
769
+ for names in group:
770
+ for name in names:
771
+ if name not in description['dependents']:
772
+ if name not in axis:
773
+ axis[name] = (level, )
774
+ else:
775
+ d = set()
776
+ for n in description['dependents'][name]:
777
+ d.update(axis[n])
778
+ if name not in axis:
779
+ axis[name] = tuple(sorted(d))
780
+ else:
781
+ axis[name] = tuple(sorted(set(axis[name]) | d))
782
+ description['axis'] = axis
783
+ description['independent_variables'] = independent_variables
784
+
813
785
  return description
814
786
 
815
787
 
@@ -845,6 +817,8 @@ async def _iter_level(variables,
845
817
  opts[iter.optimizer.name] = iter.optimizer.create()
846
818
  elif isinstance(iter, Expression):
847
819
  iters_d[name] = iter.eval(env)
820
+ elif isinstance(iter, Space):
821
+ iters_d[name] = iter.toarray()
848
822
  elif callable(iter):
849
823
  iters_d[name] = await call_function(iter, variables)
850
824
  else:
qulab/scan/server.py CHANGED
@@ -1,20 +1,18 @@
1
1
  import asyncio
2
2
  import pickle
3
- import sys
4
- import time
5
- import uuid
6
- from pathlib import Path
7
- from .scan import Scan
3
+
8
4
  import click
9
5
  import dill
10
- import numpy as np
11
6
  import zmq
12
7
  from loguru import logger
13
8
 
14
9
  from qulab.sys.rpc.zmq_socket import ZMQContextManager
15
10
 
11
+ from .scan import Scan
12
+
16
13
  pool = {}
17
14
 
15
+
18
16
  class Request():
19
17
  __slots__ = ['sock', 'identity', 'msg', 'method']
20
18
 
qulab/scan/space.py ADDED
@@ -0,0 +1,172 @@
1
+ from typing import Type
2
+
3
+ import numpy as np
4
+ import skopt
5
+ from skopt.space import Categorical, Integer, Real
6
+
7
+
8
+ class Space():
9
+
10
+ def __init__(self, function, *args, **kwds):
11
+ self.function = function
12
+ self.args = args
13
+ self.kwds = kwds
14
+
15
+ def __repr__(self):
16
+ if self.function == 'asarray':
17
+ return repr(self.args[0])
18
+ args = ', '.join(map(repr, self.args))
19
+ kwds = ', '.join(f'{k}={v!r}' for k, v in self.kwds.items())
20
+ return f"{self.function}({args}, {kwds})"
21
+
22
+ def __len__(self):
23
+ return len(self.toarray())
24
+
25
+ @classmethod
26
+ def fromarray(cls, array):
27
+ if isinstance(array, Space):
28
+ return array
29
+ if isinstance(array, (list, tuple)):
30
+ array = np.array(array)
31
+ try:
32
+ a = np.linspace(array[0], array[-1], len(array), dtype=array.dtype)
33
+ if np.allclose(a, array):
34
+ return cls('linspace',
35
+ array[0],
36
+ array[-1],
37
+ len(array),
38
+ dtype=array.dtype)
39
+ except:
40
+ pass
41
+ try:
42
+ a = np.logspace(np.log10(array[0]),
43
+ np.log10(array[-1]),
44
+ len(array),
45
+ base=10,
46
+ dtype=array.dtype)
47
+ if np.allclose(a, array):
48
+ return cls('logspace',
49
+ np.log10(array[0]),
50
+ np.log10(array[-1]),
51
+ len(array),
52
+ base=10,
53
+ dtype=array.dtype)
54
+ except:
55
+ pass
56
+ try:
57
+ a = np.logspace(np.log2(array[0]),
58
+ np.log2(array[-1]),
59
+ len(array),
60
+ base=2,
61
+ dtype=array.dtype)
62
+ if np.allclose(a, array):
63
+ return cls('logspace',
64
+ np.log2(array[0]),
65
+ np.log2(array[-1]),
66
+ len(array),
67
+ base=2,
68
+ dtype=array.dtype)
69
+ except:
70
+ pass
71
+ try:
72
+ a = np.geomspace(array[0],
73
+ array[-1],
74
+ len(array),
75
+ dtype=array.dtype)
76
+ if np.allclose(a, array):
77
+ return cls('geomspace',
78
+ array[0],
79
+ array[-1],
80
+ len(array),
81
+ dtype=array.dtype)
82
+ except:
83
+ pass
84
+ return array
85
+
86
+ def toarray(self):
87
+ return getattr(np, self.function)(*self.args, **self.kwds)
88
+
89
+
90
+ def logspace(start, stop, num=50, endpoint=True, base=10):
91
+ return Space('logspace', start, stop, num, endpoint=endpoint, base=base)
92
+
93
+
94
+ def linspace(start, stop, num=50, endpoint=True):
95
+ return Space('linspace', start, stop, num, endpoint=endpoint)
96
+
97
+
98
+ def geomspace(start, stop, num=50, endpoint=True):
99
+ return Space('geomspace', start, stop, num, endpoint=endpoint)
100
+
101
+
102
+ class OptimizeSpace():
103
+
104
+ def __init__(self, optimizer: 'Optimizer', space):
105
+ self.optimizer = optimizer
106
+ self.space = space
107
+ self.name = None
108
+
109
+ def __len__(self):
110
+ return self.optimizer.maxiter
111
+
112
+
113
+ class Optimizer():
114
+
115
+ def __init__(self,
116
+ scanner,
117
+ name: str,
118
+ level: int,
119
+ method: str | Type = skopt.Optimizer,
120
+ maxiter: int = 1000,
121
+ minimize: bool = True,
122
+ **kwds):
123
+ self.scanner = scanner
124
+ self.method = method
125
+ self.maxiter = maxiter
126
+ self.dimensions = {}
127
+ self.name = name
128
+ self.level = level
129
+ self.kwds = kwds
130
+ self.minimize = minimize
131
+
132
+ def create(self):
133
+ return self.method(list(self.dimensions.values()), **self.kwds)
134
+
135
+ def Categorical(self,
136
+ categories,
137
+ prior=None,
138
+ transform=None,
139
+ name=None) -> OptimizeSpace:
140
+ return OptimizeSpace(self,
141
+ Categorical(categories, prior, transform, name))
142
+
143
+ def Integer(self,
144
+ low,
145
+ high,
146
+ prior="uniform",
147
+ base=10,
148
+ transform=None,
149
+ name=None,
150
+ dtype=np.int64) -> OptimizeSpace:
151
+ return OptimizeSpace(
152
+ self, Integer(low, high, prior, base, transform, name, dtype))
153
+
154
+ def Real(self,
155
+ low,
156
+ high,
157
+ prior="uniform",
158
+ base=10,
159
+ transform=None,
160
+ name=None,
161
+ dtype=float) -> OptimizeSpace:
162
+ return OptimizeSpace(
163
+ self, Real(low, high, prior, base, transform, name, dtype))
164
+
165
+ def __getstate__(self) -> dict:
166
+ state = self.__dict__.copy()
167
+ del state['scanner']
168
+ return state
169
+
170
+ def __setstate__(self, state: dict) -> None:
171
+ self.__dict__.update(state)
172
+ self.scanner = None
qulab/scan/utils.py CHANGED
@@ -1,11 +1,59 @@
1
1
  import ast
2
2
  import asyncio
3
3
  import inspect
4
+ import warnings
4
5
  from typing import Any, Callable
5
6
 
7
+ import dill
8
+
6
9
  from .expression import Env, Expression
7
10
 
8
11
 
12
+ class Unpicklable:
13
+
14
+ def __init__(self, obj):
15
+ self.type = str(type(obj))
16
+ self.id = id(obj)
17
+
18
+ def __repr__(self):
19
+ return f'<Unpicklable: {self.type} at 0x{id(self):x}>'
20
+
21
+
22
+ class TooLarge:
23
+
24
+ def __init__(self, obj):
25
+ self.type = str(type(obj))
26
+ self.id = id(obj)
27
+
28
+ def __repr__(self):
29
+ return f'<TooLarge: {self.type} at 0x{id(self):x}>'
30
+
31
+
32
+ def dump_globals(ns=None, *, size_limit=10 * 1024 * 1024, warn=False):
33
+ import __main__
34
+
35
+ if ns is None:
36
+ ns = __main__.__dict__
37
+
38
+ namespace = {}
39
+
40
+ for name, value in ns.items():
41
+ try:
42
+ buf = dill.dumps(value)
43
+ except:
44
+ namespace[name] = Unpicklable(value)
45
+ if warn:
46
+ warnings.warn(f'Unpicklable: {name} {type(value)}')
47
+ if len(buf) > size_limit:
48
+ namespace[name] = TooLarge(value)
49
+ if warn:
50
+ warnings.warn(f'TooLarge: {name} {type(value)}')
51
+ else:
52
+ namespace[name] = buf
53
+
54
+ return namespace
55
+
56
+
9
57
  def is_valid_identifier(s: str) -> bool:
10
58
  """
11
59
  Check if a string is a valid identifier.
qulab/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.1.0"
1
+ __version__ = "2.1.2"
File without changes
File without changes