QuLab 2.0.4__cp312-cp312-win_amd64.whl → 2.0.6__cp312-cp312-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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: QuLab
3
- Version: 2.0.4
3
+ Version: 2.0.6
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,7 +1,7 @@
1
1
  qulab/__init__.py,sha256=8zLGg-DfQhnDl2Ky0n-zXpN-8e-g7iR0AcaI4l4Vvpk,32
2
2
  qulab/__main__.py,sha256=XN2wrhlmEkTIPq_ZeSaO8rWXfYgD2Czkm9DVFVoCw_U,515
3
- qulab/fun.cp312-win_amd64.pyd,sha256=jenFpbWSaien3YWNHNttD3jn-8mdPvcL6GdWDtRfacU,31744
4
- qulab/version.py,sha256=vd1KwOWsA9E1XbsJopSk63EogXGb_qCx8XG7lzXJd8Y,21
3
+ qulab/fun.cp312-win_amd64.pyd,sha256=35pA5LTRu6a8HBGW19zL0RRI8fIeWcnn0ss0odzE7qU,31744
4
+ qulab/version.py,sha256=IoNlI2ogLAGBFo-lP1Eg_QnxTNCxj3o2OCiBaMyJrP8,21
5
5
  qulab/monitor/__init__.py,sha256=xEVDkJF8issrsDeLqQmDsvtRmrf-UiViFcGTWuzdlFU,43
6
6
  qulab/monitor/__main__.py,sha256=k2H1H5Zf9LLXTDLISJkbikLH-z0f1e5i5i6wXXYPOrE,105
7
7
  qulab/monitor/config.py,sha256=y_5StMkdrbZO1ziyKBrvIkB7Jclp9RCPK1QbsOhCxnY,785
@@ -18,8 +18,8 @@ qulab/scan/expression.py,sha256=vwUM9E0OFQal4bljlUtLR3NJu4zGRyuWYrdyZSs3QTU,1619
18
18
  qulab/scan/models.py,sha256=TkiVHF_fUZzYHs4MsCTRh391thpf4Ozd3R_LAU0Gxkg,17657
19
19
  qulab/scan/optimize.py,sha256=MlT4y422CnP961IR384UKryyZh8riNvrPSd2z_MXLEg,2356
20
20
  qulab/scan/query_record.py,sha256=rpw4U3NjLzlv9QMwKdCvEUGHjzPF8u1UpodfLW8aoTY,11853
21
- qulab/scan/recorder.py,sha256=MuXnRA9_p8MvigT4GnBpJDv3sk4faJIW0R0dQdgaW-Q,18119
22
- qulab/scan/scan.py,sha256=-jHWb5PEhnFZOnEshTOrCtkJxfyuCq6JFtIdw8k8VYM,26402
21
+ qulab/scan/recorder.py,sha256=lbIASqH4-4eTzqX1sG9K1LnUkqvkcRK5ab2OpXSeE-Y,22801
22
+ qulab/scan/scan.py,sha256=3HXIpwoQJJNWRBXCqTsNqzMJsZ_tnygkANOH1UJC7Xs,26847
23
23
  qulab/scan/server.py,sha256=zDZfG6bOB3EUubfByQMq0BSQ9C6IV_Av0tDinzgpGjQ,2950
24
24
  qulab/scan/utils.py,sha256=XM-eKL5Xkm0hihhGS7Kq4g654Ye7n7TcU_f95gxtXq8,2634
25
25
  qulab/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -77,9 +77,9 @@ qulab/visualization/plot_layout.py,sha256=yAnMONOms7_szCdng-8wPpUMPis5UnbaNNzV4K
77
77
  qulab/visualization/plot_seq.py,sha256=h9D0Yl_yO64IwlvBgzMu9EBKr9gg6y8QE55gu2PfTns,2783
78
78
  qulab/visualization/qdat.py,sha256=HubXFu4nfcA7iUzghJGle1C86G6221hicLR0b-GqhKQ,5887
79
79
  qulab/visualization/widgets.py,sha256=HcYwdhDtLreJiYaZuN3LfofjJmZcLwjMfP5aasebgDo,3266
80
- QuLab-2.0.4.dist-info/LICENSE,sha256=b4NRQ-GFVpJMT7RuExW3NwhfbrYsX7AcdB7Gudok-fs,1086
81
- QuLab-2.0.4.dist-info/METADATA,sha256=3yPITY1eWKzHwPjuMVMAgmRSgPrtst0lLJg0a0xUHdM,3609
82
- QuLab-2.0.4.dist-info/WHEEL,sha256=fZWyj_84lK0cA-ZNCsdwhbJl0OTrpWkxInEn424qrSs,102
83
- QuLab-2.0.4.dist-info/entry_points.txt,sha256=ohBzutEnQimP_BZWiuXdSliu4QAYSHHcN0PZD8c7ZCY,46
84
- QuLab-2.0.4.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
85
- QuLab-2.0.4.dist-info/RECORD,,
80
+ QuLab-2.0.6.dist-info/LICENSE,sha256=b4NRQ-GFVpJMT7RuExW3NwhfbrYsX7AcdB7Gudok-fs,1086
81
+ QuLab-2.0.6.dist-info/METADATA,sha256=541-jW-KE_KRTxXVjXDg4Hb4Bn_rFtpb3KeNgEhoM6I,3609
82
+ QuLab-2.0.6.dist-info/WHEEL,sha256=fZWyj_84lK0cA-ZNCsdwhbJl0OTrpWkxInEn424qrSs,102
83
+ QuLab-2.0.6.dist-info/entry_points.txt,sha256=ohBzutEnQimP_BZWiuXdSliu4QAYSHHcN0PZD8c7ZCY,46
84
+ QuLab-2.0.6.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
85
+ QuLab-2.0.6.dist-info/RECORD,,
Binary file
qulab/scan/recorder.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import itertools
2
3
  import os
3
4
  import pickle
4
5
  import sys
@@ -7,6 +8,7 @@ import uuid
7
8
  from collections import defaultdict
8
9
  from pathlib import Path
9
10
  from threading import Lock
11
+ from types import EllipsisType
10
12
 
11
13
  import click
12
14
  import dill
@@ -44,52 +46,74 @@ def random_path(base):
44
46
  return path
45
47
 
46
48
 
49
+ def index_in_slice(slice_obj: slice | int, index: int):
50
+ if isinstance(slice_obj, int):
51
+ return slice_obj == index
52
+ start, stop, step = slice_obj.start, slice_obj.stop, slice_obj.step
53
+ if start is None:
54
+ start = 0
55
+ if step is None:
56
+ step = 1
57
+ if stop is None:
58
+ stop = sys.maxsize
59
+
60
+ if step > 0:
61
+ return start <= index < stop and (index - start) % step == 0
62
+ else:
63
+ return stop < index <= start and (index - start) % step == 0
64
+
65
+
47
66
  class BufferList():
48
67
 
49
- def __init__(self, pos_file=None, value_file=None):
50
- self._pos = []
51
- self._value = []
68
+ def __init__(self, file=None, slice=None):
69
+ self._list = []
52
70
  self.lu = ()
53
71
  self.rd = ()
54
- self.pos_file = pos_file
55
- self.value_file = value_file
72
+ self.inner_shape = None
73
+ self.file = file
74
+ self._slice = slice
56
75
  self._lock = Lock()
76
+ self._database = None
77
+
78
+ def __repr__(self):
79
+ return f"<BufferList: lu={self.lu}, rd={self.rd}, slice={self._slice}>"
57
80
 
58
81
  def __getstate__(self):
82
+ self.flush()
83
+ if isinstance(self.file, Path):
84
+ file = '/'.join(self.file.parts[-4:])
85
+ else:
86
+ file = self.file
59
87
  return {
60
- 'pos_file': self.pos_file,
61
- 'value_file': self.value_file,
62
- '_pos': self._pos,
63
- '_value': self._value,
88
+ 'file': file,
64
89
  'lu': self.lu,
65
- 'rd': self.rd
90
+ 'rd': self.rd,
91
+ 'inner_shape': self.inner_shape,
66
92
  }
67
93
 
68
94
  def __setstate__(self, state):
69
- self.pos_file = state['pos_file']
70
- self.value_file = state['value_file']
71
- self._pos = state['_pos']
72
- self._value = state['_value']
95
+ self.file = state['file']
73
96
  self.lu = state['lu']
74
97
  self.rd = state['rd']
98
+ self.inner_shape = state['inner_shape']
99
+ self._list = []
100
+ self._slice = None
75
101
  self._lock = Lock()
102
+ self._database = None
76
103
 
77
104
  @property
78
105
  def shape(self):
79
106
  return tuple([i - j for i, j in zip(self.rd, self.lu)])
80
107
 
81
108
  def flush(self):
82
- with self._lock:
83
- if self.pos_file is not None:
84
- with open(self.pos_file, 'ab') as f:
85
- for pos in self._pos:
86
- dill.dump(pos, f)
87
- self._pos.clear()
88
- if self.value_file is not None:
89
- with open(self.value_file, 'ab') as f:
90
- for value in self._value:
91
- dill.dump(value, f)
92
- self._value.clear()
109
+ if not self._list:
110
+ return
111
+ if isinstance(self.file, Path):
112
+ with self._lock:
113
+ with open(self.file, 'ab') as f:
114
+ for item in self._list:
115
+ dill.dump(item, f)
116
+ self._list.clear()
93
117
 
94
118
  def append(self, pos, value, dims=None):
95
119
  if dims is not None:
@@ -98,45 +122,96 @@ class BufferList():
98
122
  pos = tuple([pos[i] for i in dims])
99
123
  self.lu = tuple([min(i, j) for i, j in zip(pos, self.lu)])
100
124
  self.rd = tuple([max(i + 1, j) for i, j in zip(pos, self.rd)])
101
- self._pos.append(pos)
102
- self._value.append(value)
103
- if len(self._value) > 1000:
125
+ if hasattr(value, 'shape'):
126
+ if self.inner_shape is None:
127
+ self.inner_shape = value.shape
128
+ elif self.inner_shape != value.shape:
129
+ self.inner_shape = ()
130
+ self._list.append((pos, value))
131
+ if len(self._list) > 1000:
104
132
  self.flush()
105
133
 
106
- def value(self):
107
- v = []
108
- if self.value_file is not None and self.value_file.exists():
134
+ def _iter_file(self):
135
+ if isinstance(self.file, Path) and self.file.exists():
109
136
  with self._lock:
110
- with open(self.value_file, 'rb') as f:
137
+ with open(self.file, 'rb') as f:
111
138
  while True:
112
139
  try:
113
- v.append(dill.load(f))
140
+ pos, value = dill.load(f)
141
+ yield pos, value
114
142
  except EOFError:
115
143
  break
116
- v.extend(self._value)
117
- return v
144
+
145
+ def iter(self):
146
+ for pos, value in itertools.chain(self._iter_file(), self._list):
147
+ if not self._slice:
148
+ yield pos, value
149
+ elif all([index_in_slice(s, i) for s, i in zip(self._slice, pos)]):
150
+ yield pos, value[self._slice[len(pos):]]
151
+
152
+ def value(self):
153
+ d = []
154
+ for pos, value in self.iter():
155
+ d.append(value)
156
+ return d
118
157
 
119
158
  def pos(self):
120
159
  p = []
121
- if self.pos_file is not None and self.pos_file.exists():
122
- with self._lock:
123
- with open(self.pos_file, 'rb') as f:
124
- while True:
125
- try:
126
- p.append(dill.load(f))
127
- except EOFError:
128
- break
129
- p.extend(self._pos)
160
+ for pos, value in self.iter():
161
+ p.append(pos)
130
162
  return p
131
163
 
164
+ def items(self):
165
+ p, d = [], []
166
+ for pos, value in self.iter():
167
+ p.append(pos)
168
+ d.append(value)
169
+ return p, d
170
+
132
171
  def array(self):
133
- pos = np.asarray(self.pos()) - np.asarray(self.lu)
134
- data = np.asarray(self.value())
172
+ pos, data = self.items()
173
+ pos = np.asarray(pos) - np.asarray(self.lu)
174
+ data = np.asarray(data)
135
175
  inner_shape = data.shape[1:]
136
176
  x = np.full(self.shape + inner_shape, np.nan, dtype=data[0].dtype)
137
177
  x.__setitem__(tuple(pos.T), data)
138
178
  return x
139
179
 
180
+ def _full_slice(self, slice_tuple: slice
181
+ | tuple[slice | int | EllipsisType, ...]):
182
+ if isinstance(slice_tuple, slice):
183
+ slice_tuple = (slice_tuple, ) + (slice(0, sys.maxsize,
184
+ 1), ) * (len(self.lu) - 1)
185
+ if slice_tuple is Ellipsis:
186
+ slice_tuple = (slice(0, sys.maxsize, 1), ) * len(self.lu)
187
+ else:
188
+ head, tail = [], []
189
+ for i, s in enumerate(slice_tuple):
190
+ if s is Ellipsis:
191
+ head = slice_tuple[:i]
192
+ tail = slice_tuple[i + 1:]
193
+ break
194
+ slice_tuple = head + (slice(0, sys.maxsize, 1), ) * (
195
+ len(self.lu) - len(head) - len(tail)) + tail
196
+ slice_list = []
197
+ for s in slice_tuple:
198
+ if isinstance(s, int):
199
+ slice_list.append(s)
200
+ else:
201
+ start, stop, step = s.start, s.stop, s.step
202
+ if start is None:
203
+ start = 0
204
+ if step is None:
205
+ step = 1
206
+ if stop is None:
207
+ stop = sys.maxsize
208
+ slice_list.append(slice(start, stop, step))
209
+ return tuple(slice_list)
210
+
211
+ def __getitem__(self, slice_tuple: slice | EllipsisType
212
+ | tuple[slice | int | EllipsisType, ...]):
213
+ return super().__getitem__(self._full_slice(slice_tuple))
214
+
140
215
 
141
216
  class Record():
142
217
 
@@ -149,7 +224,6 @@ class Record():
149
224
  self._index = []
150
225
  self._pos = []
151
226
  self._last_vars = set()
152
- self._levels = {}
153
227
  self._file = None
154
228
  self.independent_variables = {}
155
229
  self.constants = {}
@@ -170,7 +244,6 @@ class Record():
170
244
  for level, group in self.description['order'].items():
171
245
  for names in group:
172
246
  for name in names:
173
- self._levels[name] = level
174
247
  if name not in self.dims:
175
248
  if name not in self.description['dependents']:
176
249
  self.dims[name] = (level, )
@@ -185,6 +258,35 @@ class Record():
185
258
  self._file = random_path(self.database / 'objects')
186
259
  self._file.parent.mkdir(parents=True, exist_ok=True)
187
260
 
261
+ def __getstate__(self) -> dict:
262
+ return {
263
+ 'id': self.id,
264
+ 'database': self.database,
265
+ 'description': self.description,
266
+ '_keys': self._keys,
267
+ '_items': self._items,
268
+ '_index': self._index,
269
+ '_pos': self._pos,
270
+ '_last_vars': self._last_vars,
271
+ 'independent_variables': self.independent_variables,
272
+ 'constants': self.constants,
273
+ 'dims': self.dims,
274
+ }
275
+
276
+ def __setstate__(self, state: dict):
277
+ self.id = state['id']
278
+ self.database = state['database']
279
+ self.description = state['description']
280
+ self._keys = state['_keys']
281
+ self._items = state['_items']
282
+ self._index = state['_index']
283
+ self._pos = state['_pos']
284
+ self._last_vars = state['_last_vars']
285
+ self.independent_variables = state['independent_variables']
286
+ self.constants = state['constants']
287
+ self.dims = state['dims']
288
+ self._file = None
289
+
188
290
  def is_local_record(self):
189
291
  return not self.is_cache_record() and not self.is_remote_record()
190
292
 
@@ -201,7 +303,7 @@ class Record():
201
303
  def __getitem__(self, key):
202
304
  return self.get(key)
203
305
 
204
- def get(self, key, default=_notgiven, buffer_to_array=True):
306
+ def get(self, key, default=_notgiven, buffer_to_array=True, slice=None):
205
307
  if self.is_remote_record():
206
308
  with ZMQContextManager(zmq.DEALER,
207
309
  connect=self.database) as socket:
@@ -211,8 +313,21 @@ class Record():
211
313
  'key': key
212
314
  })
213
315
  ret = socket.recv_pyobj()
214
- if isinstance(ret, BufferList) and buffer_to_array:
215
- return ret.array()
316
+ if isinstance(ret, BufferList):
317
+ socket.send_pyobj({
318
+ 'method': 'bufferlist_slice',
319
+ 'record_id': self.id,
320
+ 'key': key,
321
+ 'slice': slice
322
+ })
323
+ lst = socket.recv_pyobj()
324
+ ret._list = lst
325
+ ret._slice = slice
326
+ if buffer_to_array:
327
+ return ret.array()
328
+ else:
329
+ ret._database = self.database
330
+ return ret
216
331
  else:
217
332
  return ret
218
333
  else:
@@ -221,15 +336,13 @@ class Record():
221
336
  else:
222
337
  d = self._items.get(key, default)
223
338
  if isinstance(d, BufferList):
339
+ if isinstance(d.file, str):
340
+ d.file = self._file.parent.parent.parent.parent / d.file
341
+ d._slice = slice
224
342
  if buffer_to_array:
225
343
  return d.array()
226
344
  else:
227
- ret = BufferList()
228
- ret._pos = d.pos()
229
- ret._value = d.value()
230
- ret.lu = d.lu
231
- ret.rd = d.rd
232
- return ret
345
+ return d
233
346
  else:
234
347
  return d
235
348
 
@@ -251,8 +364,7 @@ class Record():
251
364
  return
252
365
 
253
366
  for key in set(variables.keys()) - self._last_vars:
254
- if key not in self._levels:
255
- self._levels[key] = level
367
+ if key not in self.dims:
256
368
  self.dims[key] = tuple(range(level + 1))
257
369
 
258
370
  self._last_vars = set(variables.keys())
@@ -276,14 +388,17 @@ class Record():
276
388
  self._pos[-1] += 1
277
389
 
278
390
  for key, value in variables.items():
279
- if level == self._levels[key]:
391
+ if self.dims[key] == ():
392
+ if key not in self._items:
393
+ self._items[key] = value
394
+ elif level == self.dims[key][-1]:
280
395
  if key not in self._items:
281
396
  if self.is_local_record():
282
- f1 = random_path(self.database / 'objects')
283
- f1.parent.mkdir(parents=True, exist_ok=True)
284
- f2 = random_path(self.database / 'objects')
285
- f2.parent.mkdir(parents=True, exist_ok=True)
286
- self._items[key] = BufferList(f1, f2)
397
+ bufferlist_file = random_path(self.database /
398
+ 'objects')
399
+ bufferlist_file.parent.mkdir(parents=True,
400
+ exist_ok=True)
401
+ self._items[key] = BufferList(bufferlist_file)
287
402
  else:
288
403
  self._items[key] = BufferList()
289
404
  self._items[key].lu = pos
@@ -291,8 +406,6 @@ class Record():
291
406
  self._items[key].append(pos, value, self.dims[key])
292
407
  elif isinstance(self._items[key], BufferList):
293
408
  self._items[key].append(pos, value, self.dims[key])
294
- elif self._levels[key] == -1 and key not in self._items:
295
- self._items[key] = value
296
409
 
297
410
  def flush(self):
298
411
  if self.is_remote_record() or self.is_cache_record():
@@ -305,6 +418,16 @@ class Record():
305
418
  with open(self._file, 'wb') as f:
306
419
  dill.dump(self, f)
307
420
 
421
+ def __repr__(self):
422
+ return f"<Record: id={self.id} app={self.description['app']}, keys={self.keys()}>"
423
+
424
+ # def _repr_html_(self):
425
+ # return f"""
426
+ # <h3>Record: id={self.id}, app={self.description['app']}</h3>
427
+ # <p>keys={self.keys()}</p>
428
+ # <p>dims={self.dims}</p>
429
+ # """
430
+
308
431
 
309
432
  class Request():
310
433
  __slots__ = ['sock', 'identity', 'msg', 'method']
@@ -341,6 +464,7 @@ def get_record(session: Session, id: int, datapath: Path) -> Record:
341
464
  path = datapath / 'objects' / record_in_db.file
342
465
  with open(path, 'rb') as f:
343
466
  record = dill.load(f)
467
+ record._file = path
344
468
  else:
345
469
  record = record_cache[id][1]
346
470
  clear_cache()
@@ -390,6 +514,12 @@ async def handle(session: Session, request: Request, datapath: Path):
390
514
  match request.method:
391
515
  case 'ping':
392
516
  await reply(request, 'pong')
517
+ case 'bufferlist_slice':
518
+ record = get_record(session, msg['record_id'], datapath)
519
+ bufferlist = record.get(msg['key'],
520
+ buffer_to_array=False,
521
+ slice=msg['slice'])
522
+ await reply(request, list(bufferlist.iter()))
393
523
  case 'record_create':
394
524
  description = dill.loads(msg['description'])
395
525
  await reply(request, record_create(session, description, datapath))
qulab/scan/scan.py CHANGED
@@ -174,6 +174,7 @@ class Scan():
174
174
  tags: tuple[str] = (),
175
175
  database: str | Path
176
176
  | None = f'tcp://127.0.0.1:{default_record_port}',
177
+ dump_globals: bool = False,
177
178
  mixin=None):
178
179
  self.id = task_uuid()
179
180
  self.record = None
@@ -185,7 +186,7 @@ class Scan():
185
186
  'consts': {},
186
187
  'functions': {},
187
188
  'optimizers': {},
188
- 'namespace': {},
189
+ 'namespace': {} if dump_globals else None,
189
190
  'actions': {},
190
191
  'dependents': {},
191
192
  'order': {},
@@ -208,6 +209,7 @@ class Scan():
208
209
  self._hide_pattern_re = re.compile('|'.join(self.description['hiden']))
209
210
  self._task_queue = asyncio.Queue()
210
211
  self._task_pool = []
212
+ self._single_step = True
211
213
 
212
214
  def __del__(self):
213
215
  try:
@@ -448,6 +450,9 @@ class Scan():
448
450
  elif inspect.isawaitable(evt):
449
451
  await evt
450
452
  task.cancel()
453
+ if self._single_step:
454
+ await self.emit(0, 0, 0, self.variables.copy())
455
+ await self.emit(-1, 0, 0, {})
451
456
  return self.variables
452
457
 
453
458
  async def done(self):
@@ -504,6 +509,7 @@ class Scan():
504
509
  self._current_level += 1
505
510
  if await self._filter(variables, self.current_level - 1):
506
511
  yield variables
512
+ self._single_step = False
507
513
  asyncio.create_task(
508
514
  self.emit(self.current_level - 1, step, position,
509
515
  variables.copy()))
@@ -555,10 +561,13 @@ class Scan():
555
561
  Returns:
556
562
  Promise: A promise object.
557
563
  """
558
- async with self._sem:
559
- task = asyncio.create_task(self._await(awaitable))
560
- self._task_queue.put_nowait(task)
561
- return Promise(task)
564
+ if inspect.isawaitable(awaitable):
565
+ async with self._sem:
566
+ task = asyncio.create_task(self._await(awaitable))
567
+ self._task_queue.put_nowait(task)
568
+ return Promise(task)
569
+ else:
570
+ return awaitable
562
571
 
563
572
  async def _await(self, awaitable: Awaitable):
564
573
  async with self._sem:
@@ -614,7 +623,8 @@ def assymbly(description):
614
623
  import __main__
615
624
  from IPython import get_ipython
616
625
 
617
- description['namespace'] = dump_globals()
626
+ if isinstance(description['namespace'], dict):
627
+ description['namespace'] = dump_globals()
618
628
 
619
629
  ipy = get_ipython()
620
630
  if ipy is not None:
qulab/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.0.4"
1
+ __version__ = "2.0.6"
File without changes
File without changes