QuLab 2.0.9__cp310-cp310-win_amd64.whl → 2.1.1__cp310-cp310-win_amd64.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,34 +1,52 @@
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
9
+ from concurrent.futures import ProcessPoolExecutor
11
10
  from graphlib import TopologicalSorter
12
11
  from pathlib import Path
13
- from types import MethodType
14
- from typing import Any, Awaitable, Callable, Iterable, Type
12
+ from typing import Any, Awaitable, Callable, Iterable
15
13
 
16
14
  import dill
17
15
  import numpy as np
18
- import skopt
19
16
  import zmq
20
- from skopt.space import Categorical, Integer, Real
21
- from tqdm.notebook import tqdm
22
17
 
23
18
  from ..sys.rpc.zmq_socket import ZMQContextManager
24
19
  from .expression import Env, Expression, Symbol
25
20
  from .optimize import NgOptimizer
26
- from .recorder import Record, default_record_port
27
- 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
+
28
41
 
29
42
  __process_uuid = uuid.uuid1()
30
43
  __task_counter = itertools.count()
31
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
+
32
50
 
33
51
  def task_uuid():
34
52
  return uuid.uuid3(__process_uuid, str(next(__task_counter)))
@@ -54,79 +72,6 @@ def _get_depends(func: Callable):
54
72
  return args
55
73
 
56
74
 
57
- class OptimizeSpace():
58
-
59
- def __init__(self, optimizer: 'Optimizer', space):
60
- self.optimizer = optimizer
61
- self.space = space
62
- self.name = None
63
-
64
- def __len__(self):
65
- return self.optimizer.maxiter
66
-
67
-
68
- class Optimizer():
69
-
70
- def __init__(self,
71
- scanner: 'Scan',
72
- name: str,
73
- level: int,
74
- method: str | Type = skopt.Optimizer,
75
- maxiter: int = 1000,
76
- minimize: bool = True,
77
- **kwds):
78
- self.scanner = scanner
79
- self.method = method
80
- self.maxiter = maxiter
81
- self.dimensions = {}
82
- self.name = name
83
- self.level = level
84
- self.kwds = kwds
85
- self.minimize = minimize
86
-
87
- def create(self):
88
- return self.method(list(self.dimensions.values()), **self.kwds)
89
-
90
- def Categorical(self,
91
- categories,
92
- prior=None,
93
- transform=None,
94
- name=None) -> OptimizeSpace:
95
- return OptimizeSpace(self,
96
- Categorical(categories, prior, transform, name))
97
-
98
- def Integer(self,
99
- low,
100
- high,
101
- prior="uniform",
102
- base=10,
103
- transform=None,
104
- name=None,
105
- dtype=np.int64) -> OptimizeSpace:
106
- return OptimizeSpace(
107
- self, Integer(low, high, prior, base, transform, name, dtype))
108
-
109
- def Real(self,
110
- low,
111
- high,
112
- prior="uniform",
113
- base=10,
114
- transform=None,
115
- name=None,
116
- dtype=float) -> OptimizeSpace:
117
- return OptimizeSpace(
118
- self, Real(low, high, prior, base, transform, name, dtype))
119
-
120
- def __getstate__(self) -> dict:
121
- state = self.__dict__.copy()
122
- del state['scanner']
123
- return state
124
-
125
- def __setstate__(self, state: dict) -> None:
126
- self.__dict__.update(state)
127
- self.scanner = None
128
-
129
-
130
75
  class Promise():
131
76
  __slots__ = ['task', 'key', 'attr']
132
77
 
@@ -157,6 +102,11 @@ class Promise():
157
102
  return Promise(self.task, None, attr)
158
103
 
159
104
 
105
+ def _run_function_in_process(buf):
106
+ func, args, kwds = dill.loads(buf)
107
+ return func(*args, **kwds)
108
+
109
+
160
110
  class Scan():
161
111
 
162
112
  def __new__(cls, *args, mixin=None, **kwds):
@@ -174,8 +124,10 @@ class Scan():
174
124
  app: str = 'task',
175
125
  tags: tuple[str] = (),
176
126
  database: str | Path
177
- | None = f'tcp://127.0.0.1:{default_record_port}',
127
+ | None = default_server,
178
128
  dump_globals: bool = False,
129
+ max_workers: int = 4,
130
+ max_promise: int = 100,
179
131
  mixin=None):
180
132
  self.id = task_uuid()
181
133
  self.record = None
@@ -193,6 +145,8 @@ class Scan():
193
145
  'actions': {},
194
146
  'dependents': {},
195
147
  'order': {},
148
+ 'axis': {},
149
+ 'independent_variables': set(),
196
150
  'filters': {},
197
151
  'total': {},
198
152
  'database': database,
@@ -207,23 +161,21 @@ class Scan():
207
161
  self._variables = {}
208
162
  self._main_task = None
209
163
  self._sock = None
210
- self._sem = asyncio.Semaphore(100)
164
+ self._sem = asyncio.Semaphore(max_promise + 1)
211
165
  self._bar: dict[int, tqdm] = {}
212
166
  self._hide_pattern_re = re.compile('|'.join(self.description['hiden']))
213
- self._task_queue = asyncio.Queue()
214
- self._task_pool = []
167
+ self._msg_queue = asyncio.Queue()
168
+ self._prm_queue = asyncio.Queue()
215
169
  self._single_step = True
170
+ self._max_workers = max_workers
171
+ self._max_promise = max_promise
172
+ self._executors = ProcessPoolExecutor(max_workers=max_workers)
216
173
 
217
174
  def __del__(self):
218
175
  try:
219
176
  self._main_task.cancel()
220
177
  except:
221
178
  pass
222
- for task in self._task_pool:
223
- try:
224
- task.cancel()
225
- except:
226
- pass
227
179
 
228
180
  def __getstate__(self) -> dict:
229
181
  state = self.__dict__.copy()
@@ -231,9 +183,10 @@ class Scan():
231
183
  del state['_sock']
232
184
  del state['_main_task']
233
185
  del state['_bar']
234
- del state['_task_queue']
235
- del state['_task_pool']
186
+ del state['_msg_queue']
187
+ del state['_prm_queue']
236
188
  del state['_sem']
189
+ del state['_executors']
237
190
  return state
238
191
 
239
192
  def __setstate__(self, state: dict) -> None:
@@ -242,12 +195,20 @@ class Scan():
242
195
  self._sock = None
243
196
  self._main_task = None
244
197
  self._bar = {}
245
- self._task_queue = asyncio.Queue()
246
- self._task_pool = []
247
- self._sem = asyncio.Semaphore(100)
198
+ self._prm_queue = asyncio.Queue()
199
+ self._msg_queue = asyncio.Queue()
200
+ self._sem = asyncio.Semaphore(self._max_promise + 1)
201
+ self._executors = ProcessPoolExecutor(max_workers=self._max_workers)
248
202
  for opt in self.description['optimizers'].values():
249
203
  opt.scanner = self
250
204
 
205
+ def __del__(self):
206
+ try:
207
+ self._main_task.cancel()
208
+ except:
209
+ pass
210
+ self._executors.shutdown()
211
+
251
212
  @property
252
213
  def current_level(self):
253
214
  return self._current_level
@@ -256,8 +217,8 @@ class Scan():
256
217
  def variables(self) -> dict[str, Any]:
257
218
  return self._variables
258
219
 
259
- async def emit(self, current_level, step, position, variables: dict[str,
260
- Any]):
220
+ async def _emit(self, current_level, step, position, variables: dict[str,
221
+ Any]):
261
222
  for key, value in list(variables.items()):
262
223
  if inspect.isawaitable(value) and not self.hiden(key):
263
224
  variables[key] = await value
@@ -280,6 +241,11 @@ class Scan():
280
241
  for k, v in variables.items() if not self.hiden(k)
281
242
  })
282
243
 
244
+ def emit(self, current_level, step, position, variables: dict[str, Any]):
245
+ self._msg_queue.put_nowait(
246
+ asyncio.create_task(
247
+ self._emit(current_level, step, position, variables.copy())))
248
+
283
249
  def hide(self, name: str):
284
250
  self.description['hiden'].append(name)
285
251
  self._hide_pattern_re = re.compile('|'.join(self.description['hiden']))
@@ -289,11 +255,11 @@ class Scan():
289
255
 
290
256
  async def _filter(self, variables: dict[str, Any], level: int = 0):
291
257
  try:
292
- return all([
293
- await call_function(fun, variables) for fun in itertools.chain(
258
+ return all(await asyncio.gather(*[
259
+ call_function(fun, variables) for fun in itertools.chain(
294
260
  self.description['filters'].get(level, []),
295
261
  self.description['filters'].get(-1, []))
296
- ])
262
+ ]))
297
263
  except:
298
264
  return True
299
265
 
@@ -345,7 +311,11 @@ class Scan():
345
311
  self.description['filters'][level] = []
346
312
  self.description['filters'][level].append(func)
347
313
 
348
- def set(self, name: str, value, setter: Callable | None = None):
314
+ def set(self,
315
+ name: str,
316
+ value,
317
+ depends: Iterable[str] | None = None,
318
+ setter: Callable | None = None):
349
319
  try:
350
320
  dill.dumps(value)
351
321
  except:
@@ -354,9 +324,21 @@ class Scan():
354
324
  self.add_depends(name, value.symbols())
355
325
  self.description['functions'][name] = value
356
326
  elif callable(value):
357
- self.add_depends(name, _get_depends(value))
358
- self.description['functions'][name] = value
327
+ if depends:
328
+ self.add_depends(name, depends)
329
+ s = ','.join(depends)
330
+ self.description['functions'][f'_tmp_{name}'] = value
331
+ self.description['functions'][name] = eval(
332
+ f"lambda self, {s}: self.description['functions']['_tmp_{name}']({s})"
333
+ )
334
+ else:
335
+ self.add_depends(name, _get_depends(value))
336
+ self.description['functions'][name] = value
359
337
  else:
338
+ try:
339
+ value = Space.fromarray(value)
340
+ except:
341
+ pass
360
342
  self.description['consts'][name] = value
361
343
  if setter:
362
344
  self.description['setters'][name] = setter
@@ -376,6 +358,10 @@ class Scan():
376
358
  else:
377
359
  if level is None:
378
360
  raise ValueError('level must be provided.')
361
+ try:
362
+ range = Space.fromarray(range)
363
+ except:
364
+ pass
379
365
  self._add_loop_var(name, level, range)
380
366
  if isinstance(range, Expression) or callable(range):
381
367
  self.add_depends(name, range.symbols())
@@ -432,11 +418,15 @@ class Scan():
432
418
 
433
419
  async def _update_progress(self):
434
420
  while True:
435
- task = await self._task_queue.get()
436
- if isinstance(task, asyncio.Event):
437
- task.set()
438
- elif inspect.isawaitable(task):
439
- await task
421
+ task = await self._prm_queue.get()
422
+ await task
423
+ self._prm_queue.task_done()
424
+
425
+ async def _send_msg(self):
426
+ while True:
427
+ task = await self._msg_queue.get()
428
+ await task
429
+ self._msg_queue.task_done()
440
430
 
441
431
  async def run(self):
442
432
  assymbly(self.description)
@@ -452,44 +442,48 @@ class Scan():
452
442
  await self._run()
453
443
 
454
444
  async def _run(self):
455
- task = asyncio.create_task(self._update_progress())
456
- self._task_pool.append(task)
445
+ send_msg = asyncio.create_task(self._send_msg())
446
+ update_progress_task = asyncio.create_task(self._update_progress())
447
+
457
448
  self._variables = {'self': self}
458
- self._variables.update(self.description['consts'].copy())
449
+
450
+ consts = {}
451
+ for k, v in self.description['consts'].items():
452
+ if isinstance(v, Space):
453
+ consts[k] = v.toarray()
454
+ else:
455
+ consts[k] = v
456
+
457
+ await update_variables(self._variables, consts,
458
+ self.description['setters'])
459
459
  for level, total in self.description['total'].items():
460
460
  if total == np.inf:
461
461
  total = None
462
462
  self._bar[level] = tqdm(total=total)
463
- for group in self.description['order'].get(-1, []):
464
- for name in group:
465
- if name in self.description['functions']:
466
- self.variables[name] = await call_function(
467
- self.description['functions'][name], self.variables)
468
- if name in self.description['setters']:
469
- coro = self.description['setters'][name](
470
- self.variables[name])
471
- if inspect.isawaitable(coro):
472
- await coro
463
+
464
+ updates = await call_many_functions(
465
+ self.description['order'].get(-1, []),
466
+ self.description['functions'], self.variables)
467
+ await update_variables(self.variables, updates,
468
+ self.description['setters'])
469
+
473
470
  self.record = await self.create_record()
474
471
  await self.work()
475
472
  for level, bar in self._bar.items():
476
473
  bar.close()
477
474
 
478
- while not self._task_queue.empty():
479
- evt = self._task_queue.get_nowait()
480
- if isinstance(evt, asyncio.Event):
481
- evt.set()
482
- elif inspect.isawaitable(evt):
483
- await evt
484
475
  if self._single_step:
485
- for group in self.description['order'].get(-1, []):
486
- for name in group:
487
- if name in self.description['getters']:
488
- self.variables[name] = await call_function(
489
- self.description['getters'][name], self.variables)
490
- await self.emit(0, 0, 0, self.variables.copy())
491
- await self.emit(-1, 0, 0, {})
492
- task.cancel()
476
+ self.variables.update(await call_many_functions(
477
+ self.description['order'].get(-1, []),
478
+ self.description['getters'], self.variables))
479
+
480
+ self.emit(0, 0, 0, self.variables)
481
+ self.emit(-1, 0, 0, {})
482
+
483
+ await self._prm_queue.join()
484
+ update_progress_task.cancel()
485
+ await self._msg_queue.join()
486
+ send_msg.cancel()
493
487
  return self.variables
494
488
 
495
489
  async def done(self):
@@ -536,8 +530,8 @@ class Scan():
536
530
  return
537
531
  step = 0
538
532
  position = 0
539
- self._task_queue.put_nowait(
540
- self._reset_progress_bar(self.current_level))
533
+ self._prm_queue.put_nowait(self._reset_progress_bar(
534
+ self.current_level))
541
535
  async for variables in _iter_level(
542
536
  self.variables,
543
537
  self.description['loops'].get(self.current_level, []),
@@ -548,23 +542,18 @@ class Scan():
548
542
  if await self._filter(variables, self.current_level - 1):
549
543
  yield variables
550
544
  self._single_step = False
551
- asyncio.create_task(
552
- self.emit(self.current_level - 1, step, position,
553
- variables.copy()))
545
+ self.emit(self.current_level - 1, step, position, variables)
554
546
  step += 1
555
547
  position += 1
556
548
  self._current_level -= 1
557
- self._task_queue.put_nowait(
549
+ self._prm_queue.put_nowait(
558
550
  self._update_progress_bar(self.current_level, 1))
559
551
  if self.current_level == 0:
560
- await self.emit(self.current_level - 1, 0, 0, {})
552
+ self.emit(self.current_level - 1, 0, 0, {})
561
553
  for name, value in self.variables.items():
562
554
  if inspect.isawaitable(value):
563
555
  self.variables[name] = await value
564
- while not self._task_queue.empty():
565
- task = self._task_queue.get_nowait()
566
- if inspect.isawaitable(task):
567
- await task
556
+ await self._prm_queue.join()
568
557
 
569
558
  async def work(self, **kwds):
570
559
  if self.current_level in self.description['actions']:
@@ -589,7 +578,8 @@ class Scan():
589
578
  """
590
579
  self.description['actions'][level] = action
591
580
 
592
- async def promise(self, awaitable: Awaitable) -> Promise:
581
+ async def promise(self, awaitable: Awaitable | Callable, *args,
582
+ **kwds) -> Promise:
593
583
  """
594
584
  Promise to calculate asynchronous function and return the result in future.
595
585
 
@@ -602,8 +592,19 @@ class Scan():
602
592
  if inspect.isawaitable(awaitable):
603
593
  async with self._sem:
604
594
  task = asyncio.create_task(self._await(awaitable))
605
- self._task_queue.put_nowait(task)
595
+ self._prm_queue.put_nowait(task)
606
596
  return Promise(task)
597
+ elif inspect.iscoroutinefunction(awaitable):
598
+ return await self.promise(awaitable(*args, **kwds))
599
+ elif callable(awaitable):
600
+ try:
601
+ buf = dill.dumps((awaitable, args, kwds))
602
+ task = asyncio.get_running_loop().run_in_executor(
603
+ self._executors, _run_function_in_process, buf)
604
+ self._prm_queue.put_nowait(task)
605
+ return Promise(task)
606
+ except:
607
+ return awaitable(*args, **kwds)
607
608
  else:
608
609
  return awaitable
609
610
 
@@ -612,51 +613,6 @@ class Scan():
612
613
  return await awaitable
613
614
 
614
615
 
615
- class Unpicklable:
616
-
617
- def __init__(self, obj):
618
- self.type = str(type(obj))
619
- self.id = id(obj)
620
-
621
- def __repr__(self):
622
- return f'<Unpicklable: {self.type} at 0x{id(self):x}>'
623
-
624
-
625
- class TooLarge:
626
-
627
- def __init__(self, obj):
628
- self.type = str(type(obj))
629
- self.id = id(obj)
630
-
631
- def __repr__(self):
632
- return f'<TooLarge: {self.type} at 0x{id(self):x}>'
633
-
634
-
635
- def dump_globals(ns=None, *, size_limit=10 * 1024 * 1024, warn=False):
636
- import __main__
637
-
638
- if ns is None:
639
- ns = __main__.__dict__
640
-
641
- namespace = {}
642
-
643
- for name, value in ns.items():
644
- try:
645
- buf = dill.dumps(value)
646
- except:
647
- namespace[name] = Unpicklable(value)
648
- if warn:
649
- warnings.warn(f'Unpicklable: {name} {type(value)}')
650
- if len(buf) > size_limit:
651
- namespace[name] = TooLarge(value)
652
- if warn:
653
- warnings.warn(f'TooLarge: {name} {type(value)}')
654
- else:
655
- namespace[name] = buf
656
-
657
- return namespace
658
-
659
-
660
616
  def assymbly(description):
661
617
  import __main__
662
618
  from IPython import get_ipython
@@ -782,16 +738,52 @@ def assymbly(description):
782
738
  if ready:
783
739
  description['order'][level].append(ready)
784
740
  keys -= set(ready)
741
+
742
+ axis = {}
743
+ independent_variables = set()
744
+
745
+ for name in description['consts']:
746
+ axis[name] = ()
747
+ for level, range_list in description['loops'].items():
748
+ for name, iterable in range_list:
749
+ if isinstance(iterable, OptimizeSpace):
750
+ axis[name] = tuple(range(level + 1))
751
+ continue
752
+ elif isinstance(iterable, (np.ndarray, list, tuple, range, Space)):
753
+ independent_variables.add(name)
754
+ axis[name] = (level, )
755
+
756
+ for level, group in description['order'].items():
757
+ for names in group:
758
+ for name in names:
759
+ if name not in description['dependents']:
760
+ if name not in axis:
761
+ axis[name] = (level, )
762
+ else:
763
+ d = set()
764
+ for n in description['dependents'][name]:
765
+ d.update(axis[n])
766
+ if name not in axis:
767
+ axis[name] = tuple(sorted(d))
768
+ else:
769
+ axis[name] = tuple(sorted(set(axis[name]) | d))
770
+ description['axis'] = axis
771
+ description['independent_variables'] = independent_variables
772
+
785
773
  return description
786
774
 
787
775
 
788
- async def _update_variables(variables, updates, setters):
776
+ async def update_variables(variables: dict[str, Any], updates: dict[str, Any],
777
+ setters: dict[str, Callable]):
778
+ coros = []
789
779
  for name, value in updates.items():
790
780
  if name in setters:
791
781
  coro = setters[name](value)
792
782
  if inspect.isawaitable(coro):
793
- await coro
783
+ coros.append(coro)
794
784
  variables[name] = value
785
+ if coros:
786
+ await asyncio.gather(*coros)
795
787
 
796
788
 
797
789
  async def _iter_level(variables,
@@ -813,6 +805,8 @@ async def _iter_level(variables,
813
805
  opts[iter.optimizer.name] = iter.optimizer.create()
814
806
  elif isinstance(iter, Expression):
815
807
  iters_d[name] = iter.eval(env)
808
+ elif isinstance(iter, Space):
809
+ iters_d[name] = iter.toarray()
816
810
  elif callable(iter):
817
811
  iters_d[name] = await call_function(iter, variables)
818
812
  else:
@@ -824,31 +818,23 @@ async def _iter_level(variables,
824
818
  maxiter = min(maxiter, opt_cfg.maxiter)
825
819
 
826
820
  async for args in async_zip(*iters_d.values(), range(maxiter)):
827
- await _update_variables(variables, dict(zip(iters_d.keys(),
828
- args[:-1])), setters)
821
+ await update_variables(variables, dict(zip(iters_d.keys(), args[:-1])),
822
+ setters)
829
823
  for name, opt in opts.items():
830
824
  args = opt.ask()
831
825
  opt_cfg = optimizers[name]
832
- await _update_variables(variables, {
826
+ await update_variables(variables, {
833
827
  n: v
834
828
  for n, v in zip(opt_cfg.dimensions.keys(), args)
835
829
  }, setters)
836
830
 
837
- for group in order:
838
- for name in group:
839
- if name in functions:
840
- await _update_variables(variables, {
841
- name:
842
- await call_function(functions[name], variables)
843
- }, setters)
831
+ await update_variables(
832
+ variables, await call_many_functions(order, functions, variables),
833
+ setters)
844
834
 
845
835
  yield variables
846
836
 
847
- for group in order:
848
- for name in group:
849
- if name in getters:
850
- variables[name] = await call_function(
851
- getters[name], variables)
837
+ variables.update(await call_many_functions(order, getters, variables))
852
838
 
853
839
  for name, opt in opts.items():
854
840
  opt_cfg = optimizers[name]
@@ -866,10 +852,28 @@ async def _iter_level(variables,
866
852
  for name, opt in opts.items():
867
853
  opt_cfg = optimizers[name]
868
854
  result = opt.get_result()
869
- variables.update({
870
- n: v
871
- for n, v in zip(opt_cfg.dimensions.keys(), result.x)
872
- })
855
+ await update_variables(
856
+ variables, {
857
+ name: value
858
+ for name, value in zip(opt_cfg.dimensions.keys(), result.x)
859
+ }, setters)
873
860
  variables[name] = result.fun
874
861
  if opts:
875
862
  yield variables
863
+
864
+
865
+ async def call_many_functions(order: list[list[str]],
866
+ functions: dict[str, Callable],
867
+ variables: dict[str, Any]) -> dict[str, Any]:
868
+ ret = {}
869
+ for group in order:
870
+ waited = []
871
+ coros = []
872
+ for name in group:
873
+ if name in functions:
874
+ waited.append(name)
875
+ coros.append(call_function(functions[name], variables | ret))
876
+ if coros:
877
+ results = await asyncio.gather(*coros)
878
+ ret.update(dict(zip(waited, results)))
879
+ return ret
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