emu-mps 1.2.7__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
emu_mps/__init__.py CHANGED
@@ -1,19 +1,20 @@
1
- from emu_base import (
2
- Callback,
1
+ from pulser.backend import (
3
2
  BitStrings,
4
3
  CorrelationMatrix,
5
4
  Energy,
6
5
  EnergyVariance,
7
6
  Expectation,
8
7
  Fidelity,
9
- QubitDensity,
8
+ Occupation,
10
9
  StateResult,
11
- SecondMomentOfEnergy,
10
+ EnergySecondMoment,
12
11
  )
13
12
  from .mps_config import MPSConfig
14
13
  from .mpo import MPO
15
14
  from .mps import MPS, inner
16
15
  from .mps_backend import MPSBackend
16
+ from .observables import EntanglementEntropy
17
+ from emu_base import aggregate
17
18
 
18
19
 
19
20
  __all__ = [
@@ -23,16 +24,17 @@ __all__ = [
23
24
  "inner",
24
25
  "MPSConfig",
25
26
  "MPSBackend",
26
- "Callback",
27
27
  "StateResult",
28
28
  "BitStrings",
29
- "QubitDensity",
29
+ "Occupation",
30
30
  "CorrelationMatrix",
31
31
  "Expectation",
32
32
  "Fidelity",
33
33
  "Energy",
34
34
  "EnergyVariance",
35
- "SecondMomentOfEnergy",
35
+ "EnergySecondMoment",
36
+ "aggregate",
37
+ "EntanglementEntropy",
36
38
  ]
37
39
 
38
- __version__ = "1.2.7"
40
+ __version__ = "2.0.1"
@@ -0,0 +1,96 @@
1
+ import torch
2
+
3
+ from pulser.backend.default_observables import (
4
+ CorrelationMatrix,
5
+ EnergySecondMoment,
6
+ EnergyVariance,
7
+ Occupation,
8
+ Energy,
9
+ )
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from emu_mps.mps_config import MPSConfig
14
+ from emu_mps.mps import MPS
15
+ from emu_mps.mpo import MPO
16
+
17
+
18
+ def qubit_occupation_mps_impl(
19
+ self: Occupation,
20
+ *,
21
+ config: "MPSConfig",
22
+ state: "MPS",
23
+ hamiltonian: "MPO",
24
+ ) -> torch.Tensor:
25
+ """
26
+ Custom implementation of the occupation ❬ψ|nᵢ|ψ❭ for the EMU-MPS.
27
+ """
28
+ op = torch.tensor(
29
+ [[[0.0, 0.0], [0.0, 1.0]]], dtype=torch.complex128, device=state.factors[0].device
30
+ )
31
+ return state.expect_batch(op).real.reshape(-1).cpu()
32
+
33
+
34
+ def correlation_matrix_mps_impl(
35
+ self: CorrelationMatrix,
36
+ *,
37
+ config: "MPSConfig",
38
+ state: "MPS",
39
+ hamiltonian: "MPO",
40
+ ) -> torch.Tensor:
41
+ """
42
+ Custom implementation of the density-density correlation ❬ψ|nᵢnⱼ|ψ❭ for the EMU-MPS.
43
+
44
+ TODO: extend to arbitrary two-point correlation ❬ψ|AᵢBⱼ|ψ❭
45
+ """
46
+ return state.get_correlation_matrix().cpu()
47
+
48
+
49
+ def energy_variance_mps_impl(
50
+ self: EnergyVariance,
51
+ *,
52
+ config: "MPSConfig",
53
+ state: "MPS",
54
+ hamiltonian: "MPO",
55
+ ) -> torch.Tensor:
56
+ """
57
+ Custom implementation of the energy variance ❬ψ|H²|ψ❭-❬ψ|H|ψ❭² for the EMU-MPS.
58
+ """
59
+ h_squared = hamiltonian @ hamiltonian
60
+ h_2 = h_squared.expect(state).cpu()
61
+ h = hamiltonian.expect(state).cpu()
62
+ en_var = h_2 - h**2
63
+ return en_var.real # type: ignore[no-any-return]
64
+
65
+
66
+ def energy_second_moment_mps_impl(
67
+ self: EnergySecondMoment,
68
+ *,
69
+ config: "MPSConfig",
70
+ state: "MPS",
71
+ hamiltonian: "MPO",
72
+ ) -> torch.Tensor:
73
+ """
74
+ Custom implementation of the second moment of energy ❬ψ|H²|ψ❭
75
+ for the EMU-MPS.
76
+ """
77
+ h_square = hamiltonian @ hamiltonian
78
+ h_2 = h_square.expect(state).cpu()
79
+ assert torch.allclose(h_2.imag, torch.zeros_like(h_2.imag), atol=1e-4)
80
+ return h_2.real
81
+
82
+
83
+ def energy_mps_impl(
84
+ self: Energy,
85
+ *,
86
+ config: "MPSConfig",
87
+ state: "MPS",
88
+ hamiltonian: "MPO",
89
+ ) -> torch.Tensor:
90
+ """
91
+ Custom implementation of the second moment of energy ❬ψ|H²|ψ❭
92
+ for the EMU-MPS.
93
+ """
94
+ h = hamiltonian.expect(state)
95
+ assert torch.allclose(h.imag, torch.zeros_like(h.imag), atol=1e-4)
96
+ return h.real
emu_mps/mpo.py CHANGED
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
  import itertools
3
- from typing import Any, List, Iterable, Optional
3
+ from typing import Any, List, Sequence, Optional
4
4
 
5
5
  import torch
6
6
 
7
+ from pulser.backend import State, Operator
8
+ from emu_base import DEVICE_COUNT
7
9
  from emu_mps.algebra import add_factors, scale_factors, zip_right
8
- from emu_base.base_classes.operator import FullOp, QuditOp
9
- from emu_base import Operator, State, DEVICE_COUNT
10
+ from pulser.backend.operator import FullOp, QuditOp
10
11
  from emu_mps.mps import MPS
11
12
  from emu_mps.utils import new_left_bath, assign_devices
12
13
 
@@ -29,7 +30,7 @@ def _validate_operator_targets(operations: FullOp, nqubits: int) -> None:
29
30
  )
30
31
 
31
32
 
32
- class MPO(Operator):
33
+ class MPO(Operator[complex, torch.Tensor, MPS]):
33
34
  """
34
35
  Matrix Product Operator.
35
36
 
@@ -61,7 +62,7 @@ class MPO(Operator):
61
62
  def __repr__(self) -> str:
62
63
  return "[" + ", ".join(map(repr, self.factors)) + "]"
63
64
 
64
- def __mul__(self, other: State) -> MPS:
65
+ def apply_to(self, other: MPS) -> MPS:
65
66
  """
66
67
  Applies this MPO to the given MPS.
67
68
  The returned MPS is:
@@ -84,7 +85,7 @@ class MPO(Operator):
84
85
  )
85
86
  return MPS(factors, orthogonality_center=0)
86
87
 
87
- def __add__(self, other: Operator) -> MPO:
88
+ def __add__(self, other: MPO) -> MPO:
88
89
  """
89
90
  Returns the sum of two MPOs, computed with a direct algorithm.
90
91
  The result is currently not truncated
@@ -113,7 +114,7 @@ class MPO(Operator):
113
114
  factors = scale_factors(self.factors, scalar, which=0)
114
115
  return MPO(factors)
115
116
 
116
- def __matmul__(self, other: Operator) -> MPO:
117
+ def __matmul__(self, other: MPO) -> MPO:
117
118
  """
118
119
  Compose two operators. The ordering is that
119
120
  self is applied after other.
@@ -128,7 +129,7 @@ class MPO(Operator):
128
129
  factors = zip_right(self.factors, other.factors)
129
130
  return MPO(factors)
130
131
 
131
- def expect(self, state: State) -> float | complex:
132
+ def expect(self, state: State) -> torch.Tensor:
132
133
  """
133
134
  Compute the expectation value of self on the given state.
134
135
 
@@ -151,17 +152,17 @@ class MPO(Operator):
151
152
  state.factors[i + 1].device
152
153
  )
153
154
  acc = new_left_bath(acc, state.factors[n], self.factors[n])
154
- return acc.item() # type: ignore [no-any-return]
155
-
156
- @staticmethod
157
- def from_operator_string(
158
- basis: Iterable[str],
159
- nqubits: int,
160
- operations: FullOp,
161
- operators: dict[str, QuditOp] = {},
162
- /,
155
+ return acc.reshape(1)[0].cpu()
156
+
157
+ @classmethod
158
+ def _from_operator_repr(
159
+ cls,
160
+ *,
161
+ eigenstates: Sequence[str],
162
+ n_qudits: int,
163
+ operations: FullOp[complex],
163
164
  **kwargs: Any,
164
- ) -> MPO:
165
+ ) -> tuple[MPO, FullOp[complex]]:
165
166
  """
166
167
  See the base class
167
168
 
@@ -174,15 +175,16 @@ class MPO(Operator):
174
175
  Returns:
175
176
  the operator in MPO form.
176
177
  """
177
- operators_with_tensors: dict[str, torch.Tensor | QuditOp] = dict(operators)
178
178
 
179
- _validate_operator_targets(operations, nqubits)
179
+ _validate_operator_targets(operations, n_qudits)
180
180
 
181
- basis = set(basis)
181
+ basis = set(eigenstates)
182
+
183
+ operators_with_tensors: dict[str, torch.Tensor | QuditOp]
182
184
  if basis == {"r", "g"}:
183
185
  # operators_with_tensors will now contain the basis for single qubit ops,
184
186
  # and potentially user defined strings in terms of these
185
- operators_with_tensors |= {
187
+ operators_with_tensors = {
186
188
  "gg": torch.tensor(
187
189
  [[1.0, 0.0], [0.0, 0.0]], dtype=torch.complex128
188
190
  ).reshape(1, 2, 2, 1),
@@ -199,7 +201,7 @@ class MPO(Operator):
199
201
  elif basis == {"0", "1"}:
200
202
  # operators_with_tensors will now contain the basis for single qubit ops,
201
203
  # and potentially user defined strings in terms of these
202
- operators_with_tensors |= {
204
+ operators_with_tensors = {
203
205
  "00": torch.tensor(
204
206
  [[1.0, 0.0], [0.0, 0.0]], dtype=torch.complex128
205
207
  ).reshape(1, 2, 2, 1),
@@ -234,12 +236,12 @@ class MPO(Operator):
234
236
 
235
237
  factors = [
236
238
  torch.eye(2, 2, dtype=torch.complex128).reshape(1, 2, 2, 1)
237
- ] * nqubits
239
+ ] * n_qudits
238
240
 
239
241
  for op in tensorop:
240
242
  factor = replace_operator_string(op[0])
241
243
  for target_qubit in op[1]:
242
244
  factors[target_qubit] = factor
243
245
 
244
- mpos.append(coeff * MPO(factors, **kwargs))
245
- return sum(mpos[1:], start=mpos[0])
246
+ mpos.append(coeff * cls(factors, **kwargs))
247
+ return sum(mpos[1:], start=mpos[0]), operations # type: ignore[no-any-return]
emu_mps/mps.py CHANGED
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import math
4
4
  from collections import Counter
5
- from typing import Any, List, Optional, Iterable
5
+ from typing import List, Optional, Sequence, TypeVar, Mapping
6
6
 
7
7
  import torch
8
8
 
9
- from emu_base import State, DEVICE_COUNT
9
+ from pulser.backend.state import State, Eigenstate
10
+ from emu_base import DEVICE_COUNT
10
11
  from emu_mps import MPSConfig
11
12
  from emu_mps.algebra import add_factors, scale_factors
12
13
  from emu_mps.utils import (
@@ -17,8 +18,10 @@ from emu_mps.utils import (
17
18
  n_operator,
18
19
  )
19
20
 
21
+ ArgScalarType = TypeVar("ArgScalarType")
20
22
 
21
- class MPS(State):
23
+
24
+ class MPS(State[complex, torch.Tensor]):
22
25
  """
23
26
  Matrix Product State, aka tensor train.
24
27
 
@@ -53,6 +56,7 @@ class MPS(State):
53
56
  num_gpus_to_use: distribute the factors over this many GPUs
54
57
  0=all factors to cpu, None=keep the existing device assignment.
55
58
  """
59
+ self._eigenstates = ["0", "1"]
56
60
  self.config = config if config is not None else MPSConfig()
57
61
  assert all(
58
62
  factors[i - 1].shape[2] == factors[i].shape[0] for i in range(1, len(factors))
@@ -73,6 +77,11 @@ class MPS(State):
73
77
  if num_gpus_to_use is not None:
74
78
  assign_devices(self.factors, min(DEVICE_COUNT, num_gpus_to_use))
75
79
 
80
+ @property
81
+ def n_qudits(self) -> int:
82
+ """The number of qudits in the state."""
83
+ return self.num_sites
84
+
76
85
  @classmethod
77
86
  def make(
78
87
  cls,
@@ -174,7 +183,12 @@ class MPS(State):
174
183
  return max((x.shape[2] for x in self.factors), default=0)
175
184
 
176
185
  def sample(
177
- self, num_shots: int, p_false_pos: float = 0.0, p_false_neg: float = 0.0
186
+ self,
187
+ *,
188
+ num_shots: int,
189
+ one_state: Eigenstate | None = None,
190
+ p_false_pos: float = 0.0,
191
+ p_false_neg: float = 0.0,
178
192
  ) -> Counter[str]:
179
193
  """
180
194
  Samples bitstrings, taking into account the specified error rates.
@@ -182,11 +196,12 @@ class MPS(State):
182
196
  Args:
183
197
  num_shots: how many bitstrings to sample
184
198
  p_false_pos: the rate at which a 0 is read as a 1
185
- p_false_neg: teh rate at which a 1 is read as a 0
199
+ p_false_neg: the rate at which a 1 is read as a 0
186
200
 
187
201
  Returns:
188
202
  the measured bitstrings, by count
189
203
  """
204
+ assert one_state in {None, "r", "1"}
190
205
  self.orthogonalize(0)
191
206
 
192
207
  rnd_matrix = torch.rand(num_shots, self.num_sites).to(self.factors[0].device)
@@ -249,19 +264,17 @@ class MPS(State):
249
264
  )
250
265
  return bitstrings
251
266
 
252
- def norm(self) -> float:
267
+ def norm(self) -> torch.Tensor:
253
268
  """Computes the norm of the MPS."""
254
269
  orthogonality_center = (
255
270
  self.orthogonality_center
256
271
  if self.orthogonality_center is not None
257
272
  else self.orthogonalize(0)
258
273
  )
274
+ # the torch.norm function is not properly typed.
275
+ return self.factors[orthogonality_center].norm().cpu() # type: ignore[no-any-return]
259
276
 
260
- return float(
261
- torch.linalg.norm(self.factors[orthogonality_center].to("cpu")).item()
262
- )
263
-
264
- def inner(self, other: State) -> float | complex:
277
+ def inner(self, other: State) -> torch.Tensor:
265
278
  """
266
279
  Compute the inner product between this state and other.
267
280
  Note that self is the left state in the inner product,
@@ -285,7 +298,33 @@ class MPS(State):
285
298
  acc = torch.tensordot(acc, other.factors[i].to(acc.device), dims=1)
286
299
  acc = torch.tensordot(self.factors[i].conj(), acc, dims=([0, 1], [0, 1]))
287
300
 
288
- return acc.item() # type: ignore[no-any-return]
301
+ return acc.reshape(1)[0].cpu()
302
+
303
+ def overlap(self, other: State, /) -> torch.Tensor:
304
+ """
305
+ Compute the overlap of this state and other. This is defined as
306
+ $|\\langle self | other \\rangle |^2$
307
+ """
308
+ return torch.abs(self.inner(other)) ** 2 # type: ignore[no-any-return]
309
+
310
+ def entanglement_entropy(self, mps_site: int) -> torch.Tensor:
311
+ """
312
+ Returns
313
+ the Von Neumann entanglement entropy of the state `mps` at the bond between sites b and b+1
314
+ S = -Σᵢsᵢ² log(sᵢ²)),
315
+ where sᵢ are the singular values at the chosen bond.
316
+ """
317
+ self.orthogonalize(mps_site)
318
+
319
+ # perform svd on reshaped matrix at site b
320
+ matrix = self.factors[mps_site].flatten(end_dim=1)
321
+ s = torch.linalg.svdvals(matrix)
322
+
323
+ s_e = torch.Tensor(torch.special.entr(s**2))
324
+ s_e = torch.sum(s_e)
325
+
326
+ self.orthogonalize(0)
327
+ return s_e.cpu()
289
328
 
290
329
  def get_memory_footprint(self) -> float:
291
330
  """
@@ -347,14 +386,13 @@ class MPS(State):
347
386
  def __imul__(self, scalar: complex) -> MPS:
348
387
  return self.__rmul__(scalar)
349
388
 
350
- @staticmethod
351
- def from_state_string(
389
+ @classmethod
390
+ def _from_state_amplitudes(
391
+ cls,
352
392
  *,
353
- basis: Iterable[str],
354
- nqubits: int,
355
- strings: dict[str, complex],
356
- **kwargs: Any,
357
- ) -> MPS:
393
+ eigenstates: Sequence[str],
394
+ amplitudes: Mapping[str, complex],
395
+ ) -> tuple[MPS, Mapping[str, complex]]:
358
396
  """
359
397
  See the base class.
360
398
 
@@ -367,7 +405,8 @@ class MPS(State):
367
405
  The resulting MPS representation of the state.s
368
406
  """
369
407
 
370
- basis = set(basis)
408
+ nqubits = len(next(iter(amplitudes.keys())))
409
+ basis = set(eigenstates)
371
410
  if basis == {"r", "g"}:
372
411
  one = "r"
373
412
  elif basis == {"0", "1"}:
@@ -381,18 +420,17 @@ class MPS(State):
381
420
  accum_mps = MPS(
382
421
  [torch.zeros((1, 2, 1), dtype=torch.complex128)] * nqubits,
383
422
  orthogonality_center=0,
384
- **kwargs,
385
423
  )
386
424
 
387
- for state, amplitude in strings.items():
425
+ for state, amplitude in amplitudes.items():
388
426
  factors = [basis_1 if ch == one else basis_0 for ch in state]
389
- accum_mps += amplitude * MPS(factors, **kwargs)
427
+ accum_mps += amplitude * MPS(factors)
390
428
  norm = accum_mps.norm()
391
429
  if not math.isclose(1.0, norm, rel_tol=1e-5, abs_tol=0.0):
392
430
  print("\nThe state is not normalized, normalizing it for you.")
393
431
  accum_mps *= 1 / norm
394
432
 
395
- return accum_mps
433
+ return accum_mps, amplitudes
396
434
 
397
435
  def expect_batch(self, single_qubit_operators: torch.Tensor) -> torch.Tensor:
398
436
  """
@@ -460,7 +498,7 @@ class MPS(State):
460
498
 
461
499
  def get_correlation_matrix(
462
500
  self, *, operator: torch.Tensor = n_operator
463
- ) -> list[list[float]]:
501
+ ) -> torch.Tensor:
464
502
  """
465
503
  Efficiently compute the symmetric correlation matrix
466
504
  C_ij = <self|operator_i operator_j|self>
@@ -474,7 +512,7 @@ class MPS(State):
474
512
  """
475
513
  assert operator.shape == (2, 2)
476
514
 
477
- result = [[0.0 for _ in range(self.num_sites)] for _ in range(self.num_sites)]
515
+ result = torch.zeros(self.num_sites, self.num_sites, dtype=torch.complex128)
478
516
 
479
517
  for left in range(0, self.num_sites):
480
518
  self.orthogonalize(left)
@@ -486,7 +524,7 @@ class MPS(State):
486
524
  accumulator = torch.tensordot(
487
525
  accumulator, self.factors[left].conj(), dims=([0, 2], [0, 1])
488
526
  )
489
- result[left][left] = accumulator.trace().item().real
527
+ result[left, left] = accumulator.trace().item().real
490
528
  for right in range(left + 1, self.num_sites):
491
529
  partial = torch.tensordot(
492
530
  accumulator.to(self.factors[right].device),
@@ -497,7 +535,7 @@ class MPS(State):
497
535
  partial, self.factors[right].conj(), dims=([0], [0])
498
536
  )
499
537
 
500
- result[left][right] = (
538
+ result[left, right] = (
501
539
  torch.tensordot(
502
540
  partial, operator.to(partial.device), dims=([0, 2], [0, 1])
503
541
  )
@@ -505,13 +543,13 @@ class MPS(State):
505
543
  .item()
506
544
  .real
507
545
  )
508
- result[right][left] = result[left][right]
546
+ result[right, left] = result[left, right]
509
547
  accumulator = tensor_trace(partial, 0, 2)
510
548
 
511
549
  return result
512
550
 
513
551
 
514
- def inner(left: MPS, right: MPS) -> float | complex:
552
+ def inner(left: MPS, right: MPS) -> torch.Tensor:
515
553
  """
516
554
  Wrapper around MPS.inner.
517
555
 
emu_mps/mps_backend.py CHANGED
@@ -1,7 +1,6 @@
1
- from emu_base import Backend, BackendConfig, Results
1
+ from pulser.backend import EmulatorBackend, Results
2
2
  from emu_mps.mps_config import MPSConfig
3
3
  from emu_mps.mps_backend_impl import create_impl, MPSBackendImpl
4
- from pulser import Sequence
5
4
  import pickle
6
5
  import os
7
6
  import time
@@ -9,13 +8,16 @@ import logging
9
8
  import pathlib
10
9
 
11
10
 
12
- class MPSBackend(Backend):
11
+ class MPSBackend(EmulatorBackend):
13
12
  """
14
13
  A backend for emulating Pulser sequences using Matrix Product States (MPS),
15
14
  aka tensor trains.
16
15
  """
17
16
 
18
- def resume(self, autosave_file: str | pathlib.Path) -> Results:
17
+ default_config = MPSConfig()
18
+
19
+ @staticmethod
20
+ def resume(autosave_file: str | pathlib.Path) -> Results:
19
21
  """
20
22
  Resume simulation from autosave file.
21
23
  Only resume simulations from data you trust!
@@ -39,24 +41,18 @@ class MPSBackend(Backend):
39
41
  f"Saving simulation state every {impl.config.autosave_dt} seconds"
40
42
  )
41
43
 
42
- return self._run(impl)
44
+ return MPSBackend._run(impl)
43
45
 
44
- def run(self, sequence: Sequence, mps_config: BackendConfig) -> Results:
46
+ def run(self) -> Results:
45
47
  """
46
48
  Emulates the given sequence.
47
49
 
48
- Args:
49
- sequence: a Pulser sequence to simulate
50
- mps_config: the backends config. Should be of type MPSConfig
51
-
52
50
  Returns:
53
51
  the simulation results
54
52
  """
55
- assert isinstance(mps_config, MPSConfig)
56
-
57
- self.validate_sequence(sequence)
53
+ assert isinstance(self._config, MPSConfig)
58
54
 
59
- impl = create_impl(sequence, mps_config)
55
+ impl = create_impl(self._sequence, self._config)
60
56
  impl.init() # This is separate from the constructor for testing purposes.
61
57
 
62
58
  return self._run(impl)
@@ -2,16 +2,19 @@ import math
2
2
  import pathlib
3
3
  import random
4
4
  import uuid
5
+
5
6
  from resource import RUSAGE_SELF, getrusage
6
- from typing import Optional
7
+ from typing import Optional, Any
8
+ import typing
7
9
  import pickle
8
10
  import os
9
-
10
11
  import torch
11
12
  import time
12
13
  from pulser import Sequence
14
+ from types import MethodType
13
15
 
14
- from emu_base import Results, State, PulserData, DEVICE_COUNT
16
+ from pulser.backend import State, Observable, EmulationConfig, Results
17
+ from emu_base import PulserData, DEVICE_COUNT
15
18
  from emu_base.math.brents_root_finding import BrentsRootFinder
16
19
  from emu_mps.hamiltonian import make_H, update_H
17
20
  from emu_mps.mpo import MPO
@@ -33,6 +36,56 @@ from emu_mps.utils import (
33
36
  from enum import Enum, auto
34
37
 
35
38
 
39
+ class Statistics(Observable):
40
+ def __init__(
41
+ self,
42
+ evaluation_times: typing.Sequence[float] | None,
43
+ data: list[float],
44
+ timestep_count: int,
45
+ ):
46
+ super().__init__(evaluation_times=evaluation_times)
47
+ self.data = data
48
+ self.timestep_count = timestep_count
49
+
50
+ @property
51
+ def _base_tag(self) -> str:
52
+ return "statistics"
53
+
54
+ def apply(
55
+ self,
56
+ *,
57
+ config: EmulationConfig,
58
+ state: State,
59
+ **kwargs: Any,
60
+ ) -> dict:
61
+ """Calculates the observable to store in the Results."""
62
+ assert isinstance(state, MPS)
63
+ duration = self.data[-1]
64
+ if state.factors[0].is_cuda:
65
+ max_mem_per_device = (
66
+ torch.cuda.max_memory_allocated(device) * 1e-6
67
+ for device in range(torch.cuda.device_count())
68
+ )
69
+ max_mem = max(max_mem_per_device)
70
+ else:
71
+ max_mem = getrusage(RUSAGE_SELF).ru_maxrss * 1e-3
72
+
73
+ config.logger.info(
74
+ f"step = {len(self.data)}/{self.timestep_count}, "
75
+ + f"χ = {state.get_max_bond_dim()}, "
76
+ + f"|ψ| = {state.get_memory_footprint():.3f} MB, "
77
+ + f"RSS = {max_mem:.3f} MB, "
78
+ + f"Δt = {duration:.3f} s"
79
+ )
80
+
81
+ return {
82
+ "max_bond_dimension": state.get_max_bond_dim(),
83
+ "memory_footprint": state.get_memory_footprint(),
84
+ "RSS": max_mem,
85
+ "duration": duration,
86
+ }
87
+
88
+
36
89
  class SwipeDirection(Enum):
37
90
  LEFT_TO_RIGHT = auto()
38
91
  RIGHT_TO_LEFT = auto()
@@ -75,9 +128,14 @@ class MPSBackendImpl:
75
128
  self.swipe_direction = SwipeDirection.LEFT_TO_RIGHT
76
129
  self.tdvp_index = 0
77
130
  self.timestep_index = 0
78
- self.results = Results()
131
+ self.results = Results(atom_order=(), total_duration=self.target_times[-1])
132
+ self.statistics = Statistics(
133
+ evaluation_times=[t / self.target_times[-1] for t in self.target_times],
134
+ data=[],
135
+ timestep_count=self.timestep_count,
136
+ )
79
137
  self.autosave_file = self._get_autosave_filepath(self.config.autosave_prefix)
80
- self.config.logger.warning(
138
+ self.config.logger.debug(
81
139
  f"""Will save simulation state to file "{self.autosave_file.name}"
82
140
  every {self.config.autosave_dt} seconds.\n"""
83
141
  f"""To resume: `MPSBackend().resume("{self.autosave_file}")`"""
@@ -90,16 +148,26 @@ class MPSBackendImpl:
90
148
  f"but only {DEVICE_COUNT if DEVICE_COUNT > 0 else 'cpu'} available"
91
149
  )
92
150
 
151
+ def __getstate__(self) -> dict:
152
+ for obs in self.config.observables:
153
+ obs.apply = MethodType(type(obs).apply, obs) # type: ignore[method-assign]
154
+ d = self.__dict__.copy()
155
+ # mypy thinks the method below is an attribute, because of the __getattr__ override
156
+ d["results"] = self.results._to_abstract_repr() # type: ignore[operator]
157
+ return d
158
+
159
+ def __setstate__(self, d: dict) -> None:
160
+ self.__dict__ = d
161
+ self.results = Results._from_abstract_repr(d["results"]) # type: ignore [attr-defined]
162
+ self.config.monkeypatch_observables()
163
+
93
164
  @staticmethod
94
165
  def _get_autosave_filepath(autosave_prefix: str) -> pathlib.Path:
95
166
  return pathlib.Path(os.getcwd()) / (autosave_prefix + str(uuid.uuid1()) + ".dat")
96
167
 
97
168
  def init_dark_qubits(self) -> None:
98
169
  # has_state_preparation_error
99
- if (
100
- self.config.noise_model is not None
101
- and self.config.noise_model.state_prep_error > 0.0
102
- ):
170
+ if self.config.noise_model.state_prep_error > 0.0:
103
171
  self.well_prepared_qubits_filter = pick_well_prepared_qubits(
104
172
  self.config.noise_model.state_prep_error, self.qubit_count
105
173
  )
@@ -359,7 +427,14 @@ class MPSBackendImpl:
359
427
  )
360
428
  self.init_baths()
361
429
 
362
- self.log_step_statistics(duration=time.time() - self.time)
430
+ self.statistics.data.append(time.time() - self.time)
431
+ self.statistics(
432
+ self.config,
433
+ self.current_time / self.target_times[-1],
434
+ self.state,
435
+ self.hamiltonian,
436
+ self.results,
437
+ )
363
438
  self.time = time.time()
364
439
 
365
440
  def save_simulation(self) -> None:
@@ -369,7 +444,6 @@ class MPSBackendImpl:
369
444
  basename = self.autosave_file
370
445
  with open(basename.with_suffix(".new"), "wb") as file_handle:
371
446
  pickle.dump(self, file_handle)
372
-
373
447
  if basename.is_file():
374
448
  os.rename(basename, basename.with_suffix(".bak"))
375
449
 
@@ -389,13 +463,14 @@ class MPSBackendImpl:
389
463
  normalized_state = 1 / self.state.norm() * self.state
390
464
 
391
465
  current_time_int: int = round(self.current_time)
466
+ fractional_time = self.current_time / self.target_times[-1]
392
467
  assert abs(self.current_time - current_time_int) < 1e-10
393
468
 
394
469
  if self.well_prepared_qubits_filter is None:
395
- for callback in self.config.callbacks:
470
+ for callback in self.config.observables:
396
471
  callback(
397
472
  self.config,
398
- current_time_int,
473
+ fractional_time,
399
474
  normalized_state,
400
475
  self.hamiltonian,
401
476
  self.results,
@@ -403,63 +478,34 @@ class MPSBackendImpl:
403
478
  return
404
479
 
405
480
  full_mpo, full_state = None, None
406
- for callback in self.config.callbacks:
407
- if current_time_int not in callback.evaluation_times:
408
- continue
409
-
410
- if full_mpo is None or full_state is None:
411
- # Only do this potentially expensive step once and when needed.
412
- full_mpo = MPO(
413
- extended_mpo_factors(
414
- self.hamiltonian.factors, self.well_prepared_qubits_filter
415
- )
416
- )
417
- full_state = MPS(
418
- extended_mps_factors(
419
- normalized_state.factors, self.well_prepared_qubits_filter
420
- ),
421
- num_gpus_to_use=None, # Keep the already assigned devices.
422
- orthogonality_center=get_extended_site_index(
423
- self.well_prepared_qubits_filter,
424
- normalized_state.orthogonality_center,
425
- ),
481
+ for callback in self.config.observables:
482
+ time_tol = 0.5 / self.target_times[-1] + 1e-10
483
+ if (
484
+ callback.evaluation_times is not None
485
+ and self.config.is_time_in_evaluation_times(
486
+ fractional_time, callback.evaluation_times, tol=time_tol
426
487
  )
488
+ ) or self.config.is_evaluation_time(fractional_time, tol=time_tol):
489
+
490
+ if full_mpo is None or full_state is None:
491
+ # Only do this potentially expensive step once and when needed.
492
+ full_mpo = MPO(
493
+ extended_mpo_factors(
494
+ self.hamiltonian.factors, self.well_prepared_qubits_filter
495
+ )
496
+ )
497
+ full_state = MPS(
498
+ extended_mps_factors(
499
+ normalized_state.factors, self.well_prepared_qubits_filter
500
+ ),
501
+ num_gpus_to_use=None, # Keep the already assigned devices.
502
+ orthogonality_center=get_extended_site_index(
503
+ self.well_prepared_qubits_filter,
504
+ normalized_state.orthogonality_center,
505
+ ),
506
+ )
427
507
 
428
- callback(self.config, current_time_int, full_state, full_mpo, self.results)
429
-
430
- def log_step_statistics(self, *, duration: float) -> None:
431
- if self.state.factors[0].is_cuda:
432
- max_mem_per_device = (
433
- torch.cuda.max_memory_allocated(device) * 1e-6
434
- for device in range(torch.cuda.device_count())
435
- )
436
- max_mem = max(max_mem_per_device)
437
- else:
438
- max_mem = getrusage(RUSAGE_SELF).ru_maxrss * 1e-3
439
-
440
- self.config.logger.info(
441
- f"step = {self.timestep_index}/{self.timestep_count}, "
442
- + f"χ = {self.state.get_max_bond_dim()}, "
443
- + f"|ψ| = {self.state.get_memory_footprint():.3f} MB, "
444
- + f"RSS = {max_mem:.3f} MB, "
445
- + f"Δt = {duration:.3f} s"
446
- )
447
-
448
- if self.results.statistics is None:
449
- assert self.timestep_index == 1
450
- self.results.statistics = {"steps": []}
451
-
452
- assert "steps" in self.results.statistics
453
- assert len(self.results.statistics["steps"]) == self.timestep_index - 1
454
-
455
- self.results.statistics["steps"].append(
456
- {
457
- "max_bond_dimension": self.state.get_max_bond_dim(),
458
- "memory_footprint": self.state.get_memory_footprint(),
459
- "RSS": max_mem,
460
- "duration": duration,
461
- }
462
- )
508
+ callback(self.config, fractional_time, full_state, full_mpo, self.results)
463
509
 
464
510
 
465
511
  class NoisyMPSBackendImpl(MPSBackendImpl):
emu_mps/mps_config.py CHANGED
@@ -1,16 +1,37 @@
1
- from typing import Any
1
+ from typing import Any, ClassVar
2
+ from types import MethodType
2
3
 
3
- from emu_base import BackendConfig, State, DEVICE_COUNT
4
+ import copy
4
5
 
6
+ from emu_base import DEVICE_COUNT
7
+ from emu_mps.custom_callback_implementations import (
8
+ energy_mps_impl,
9
+ energy_second_moment_mps_impl,
10
+ energy_variance_mps_impl,
11
+ correlation_matrix_mps_impl,
12
+ qubit_occupation_mps_impl,
13
+ )
14
+ from pulser.backend import (
15
+ Occupation,
16
+ CorrelationMatrix,
17
+ Energy,
18
+ EnergySecondMoment,
19
+ EnergyVariance,
20
+ BitStrings,
21
+ EmulationConfig,
22
+ )
23
+ import logging
24
+ import pathlib
25
+ import sys
5
26
 
6
- class MPSConfig(BackendConfig):
27
+
28
+ class MPSConfig(EmulationConfig):
7
29
  """
8
- The configuration of the emu-ct MPSBackend. The kwargs passed to this class
30
+ The configuration of the emu-mps MPSBackend. The kwargs passed to this class
9
31
  are passed on to the base class.
10
32
  See the API for that class for a list of available options.
11
33
 
12
34
  Args:
13
- initial_state: the initial state to use in the simulation
14
35
  dt: the timestep size that the solver uses. Note that observables are
15
36
  only calculated if the evaluation_times are divisible by dt.
16
37
  precision: up to what precision the state is truncated
@@ -36,42 +57,129 @@ class MPSConfig(BackendConfig):
36
57
  >>> with_modulation=True) #the last arg is taken from the base class
37
58
  """
38
59
 
60
+ # Whether to warn if unexpected kwargs are received
61
+ _enforce_expected_kwargs: ClassVar[bool] = True
62
+
39
63
  def __init__(
40
64
  self,
41
65
  *,
42
- initial_state: State | None = None,
43
66
  dt: int = 10,
44
67
  precision: float = 1e-5,
45
68
  max_bond_dim: int = 1024,
46
69
  max_krylov_dim: int = 100,
47
70
  extra_krylov_tolerance: float = 1e-3,
48
71
  num_gpus_to_use: int = DEVICE_COUNT,
72
+ interaction_cutoff: float = 0.0,
73
+ log_level: int = logging.INFO,
74
+ log_file: pathlib.Path | None = None,
49
75
  autosave_prefix: str = "emu_mps_save_",
50
76
  autosave_dt: int = 600, # 10 minutes
51
77
  **kwargs: Any,
52
78
  ):
53
- super().__init__(**kwargs)
54
- self.initial_state = initial_state
55
- self.dt = dt
56
- self.precision = precision
57
- self.max_bond_dim = max_bond_dim
58
- self.max_krylov_dim = max_krylov_dim
59
- self.num_gpus_to_use = num_gpus_to_use
60
- self.extra_krylov_tolerance = extra_krylov_tolerance
79
+ kwargs.setdefault("observables", [BitStrings(evaluation_times=[1.0])])
80
+ super().__init__(
81
+ dt=dt,
82
+ precision=precision,
83
+ max_bond_dim=max_bond_dim,
84
+ max_krylov_dim=max_krylov_dim,
85
+ extra_krylov_tolerance=extra_krylov_tolerance,
86
+ num_gpus_to_use=num_gpus_to_use,
87
+ interaction_cutoff=interaction_cutoff,
88
+ log_level=log_level,
89
+ log_file=log_file,
90
+ autosave_prefix=autosave_prefix,
91
+ autosave_dt=autosave_dt,
92
+ **kwargs,
93
+ )
61
94
 
62
- if self.noise_model is not None:
63
- if "doppler" in self.noise_model.noise_types:
64
- raise NotImplementedError("Unsupported noise type: doppler")
65
- if (
66
- "amplitude" in self.noise_model.noise_types
67
- and self.noise_model.amp_sigma != 0.0
68
- ):
69
- raise NotImplementedError("Unsupported noise type: amp_sigma")
70
-
71
- self.autosave_prefix = autosave_prefix
72
- self.autosave_dt = autosave_dt
95
+ if "doppler" in self.noise_model.noise_types:
96
+ raise NotImplementedError("Unsupported noise type: doppler")
97
+ if (
98
+ "amplitude" in self.noise_model.noise_types
99
+ and self.noise_model.amp_sigma != 0.0
100
+ ):
101
+ raise NotImplementedError("Unsupported noise type: amp_sigma")
73
102
 
74
103
  MIN_AUTOSAVE_DT = 10
75
104
  assert (
76
105
  self.autosave_dt > MIN_AUTOSAVE_DT
77
106
  ), f"autosave_dt must be larger than {MIN_AUTOSAVE_DT} seconds"
107
+
108
+ self.monkeypatch_observables()
109
+
110
+ self.logger = logging.getLogger("global_logger")
111
+ if log_file is None:
112
+ logging.basicConfig(
113
+ level=log_level, format="%(message)s", stream=sys.stdout, force=True
114
+ ) # default to stream = sys.stderr
115
+ else:
116
+ logging.basicConfig(
117
+ level=log_level,
118
+ format="%(message)s",
119
+ filename=str(log_file),
120
+ filemode="w",
121
+ force=True,
122
+ )
123
+ if (self.noise_model.runs != 1 and self.noise_model.runs is not None) or (
124
+ self.noise_model.samples_per_run != 1
125
+ and self.noise_model.samples_per_run is not None
126
+ ):
127
+ self.logger.warning(
128
+ "Warning: The runs and samples_per_run values of the NoiseModel are ignored!"
129
+ )
130
+
131
+ def _expected_kwargs(self) -> set[str]:
132
+ return super()._expected_kwargs() | {
133
+ "dt",
134
+ "precision",
135
+ "max_bond_dim",
136
+ "max_krylov_dim",
137
+ "extra_krylov_tolerance",
138
+ "num_gpus_to_use",
139
+ "interaction_cutoff",
140
+ "log_level",
141
+ "log_file",
142
+ "autosave_prefix",
143
+ "autosave_dt",
144
+ }
145
+
146
+ def monkeypatch_observables(self) -> None:
147
+ obs_list = []
148
+ for _, obs in enumerate(self.observables): # monkey patch
149
+ obs_copy = copy.deepcopy(obs)
150
+ if isinstance(obs, Occupation):
151
+ obs_copy.apply = MethodType( # type: ignore[method-assign]
152
+ qubit_occupation_mps_impl, obs_copy
153
+ )
154
+ elif isinstance(obs, EnergyVariance):
155
+ obs_copy.apply = MethodType( # type: ignore[method-assign]
156
+ energy_variance_mps_impl, obs_copy
157
+ )
158
+ elif isinstance(obs, EnergySecondMoment):
159
+ obs_copy.apply = MethodType( # type: ignore[method-assign]
160
+ energy_second_moment_mps_impl, obs_copy
161
+ )
162
+ elif isinstance(obs, CorrelationMatrix):
163
+ obs_copy.apply = MethodType( # type: ignore[method-assign]
164
+ correlation_matrix_mps_impl, obs_copy
165
+ )
166
+ elif isinstance(obs, Energy):
167
+ obs_copy.apply = MethodType( # type: ignore[method-assign]
168
+ energy_mps_impl, obs_copy
169
+ )
170
+ obs_list.append(obs_copy)
171
+ self.observables = tuple(obs_list)
172
+
173
+ def init_logging(self) -> None:
174
+ if self.log_file is None:
175
+ logging.basicConfig(
176
+ level=self.log_level, format="%(message)s", stream=sys.stdout, force=True
177
+ ) # default to stream = sys.stderr
178
+ else:
179
+ logging.basicConfig(
180
+ level=self.log_level,
181
+ format="%(message)s",
182
+ filename=str(self.log_file),
183
+ filemode="w",
184
+ force=True,
185
+ )
emu_mps/observables.py ADDED
@@ -0,0 +1,40 @@
1
+ from pulser.backend.state import State
2
+ from pulser.backend.observable import Observable
3
+ from emu_mps.mps import MPS
4
+ from typing import Sequence, Any
5
+ import torch
6
+
7
+
8
+ class EntanglementEntropy(Observable):
9
+ """Entanglement Entropy subclass used only in emu_mps"""
10
+
11
+ def __init__(
12
+ self,
13
+ mps_site: int,
14
+ *,
15
+ evaluation_times: Sequence[float] | None = None,
16
+ tag_suffix: str | None = None,
17
+ ):
18
+ super().__init__(evaluation_times=evaluation_times, tag_suffix=tag_suffix)
19
+ self.mps_site = mps_site
20
+
21
+ @property
22
+ def _base_tag(self) -> str:
23
+ return "entanglement_entropy"
24
+
25
+ def _to_abstract_repr(self) -> dict[str, Any]:
26
+ repr = super()._to_abstract_repr()
27
+ repr["mps_site"] = self.mps_site
28
+ return repr
29
+
30
+ def apply(self, *, state: State, **kwargs: Any) -> torch.Tensor:
31
+ if not isinstance(state, MPS):
32
+ raise NotImplementedError(
33
+ "Entanglement entropy observable is only available for emu_mps emulator."
34
+ )
35
+ if not (0 <= self.mps_site <= len(state.factors) - 2):
36
+ raise ValueError(
37
+ f"Invalid bond index {self.mps_site}. "
38
+ f"Expected value in range 0 <= bond_index <= {len(state.factors)-2}."
39
+ )
40
+ return state.entanglement_entropy(self.mps_site)
@@ -210,9 +210,9 @@ def minimize_bandwidth(input_matrix: np.ndarray, samples: int = 100) -> list[int
210
210
  # We are interested in strength of the interaction, not sign
211
211
 
212
212
  L = input_mat.shape[0]
213
- rnd_permutations = itertools.chain(
213
+ rnd_permutations: itertools.chain[list[int]] = itertools.chain(
214
214
  [list(range(L))], # First element is always the identity list
215
- (np.random.permutation(L).tolist() for _ in range(samples)),
215
+ (np.random.permutation(L).tolist() for _ in range(samples)), # type: ignore[misc]
216
216
  )
217
217
 
218
218
  opt_permutations_and_opt_bandwidth = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emu-mps
3
- Version: 1.2.7
3
+ Version: 2.0.1
4
4
  Summary: Pasqal MPS based pulse emulator built on PyTorch
5
5
  Project-URL: Documentation, https://pasqal-io.github.io/emulators/
6
6
  Project-URL: Repository, https://github.com/pasqal-io/emulators
@@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.10
25
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
27
  Requires-Python: >=3.10
28
- Requires-Dist: emu-base==1.2.7
28
+ Requires-Dist: emu-base==2.0.1
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  <div align="center">
@@ -0,0 +1,19 @@
1
+ emu_mps/__init__.py,sha256=GIR0FJOvmMHby7Vx_Da3w7YqqLpOxa0E8E3poxXsapY,734
2
+ emu_mps/algebra.py,sha256=ngPtTH-j2ZCBWoaJZXlkUyIlug7dY7Q92gzfnRlpPMA,5485
3
+ emu_mps/custom_callback_implementations.py,sha256=CUs0kW3HRaPE7UeFNQOFbeWJMsz4hS2q4rgS57BBp-A,2411
4
+ emu_mps/hamiltonian.py,sha256=LcBs6CKBb643a1e9AAVtQoUfa4L_0dIhLOKecx5OOWs,15864
5
+ emu_mps/mpo.py,sha256=wSonS6i3zEt3yRTgyZ7F6vT51pUKavFcLOxFFphBv8k,8793
6
+ emu_mps/mps.py,sha256=KqAjo-nxgM-xQSg1NFNchwXKoPRcrKuuycFMsWr7iX8,19610
7
+ emu_mps/mps_backend.py,sha256=_3rlg6XeI4fHaDiJRfPL6pDkX9k48hAHKXd8fkvkOFs,2004
8
+ emu_mps/mps_backend_impl.py,sha256=M_do7QRBLAoHfwF_EpyMCb6g7w7BSs5hsPa5UE0z9Zs,22958
9
+ emu_mps/mps_config.py,sha256=89nu5OhNUX31eAeeYvvKnAHegpPVD43jH5Nmp635HVU,6984
10
+ emu_mps/noise.py,sha256=h4X2EFjoC_Ok0gZ8I9wN77RANXaVehTBbjkcbY_GAmY,784
11
+ emu_mps/observables.py,sha256=7GQDH5kyaVNrwckk2f8ZJRV9Ca4jKhWWDsOCqYWsoEk,1349
12
+ emu_mps/tdvp.py,sha256=pIQ2NXA2Mrkp3elhqQbX3pdJVbtKkG3c5r9fFlJo7pI,5755
13
+ emu_mps/utils.py,sha256=BqRJYAcXqprtZVJ0V_j954ON2bhTdtZiaTojsYyrWrg,8193
14
+ emu_mps/optimatrix/__init__.py,sha256=lHWYNeThHp57ZrwTwXd0p8bNvcCv0w_AZ31iCWflBUo,226
15
+ emu_mps/optimatrix/optimiser.py,sha256=7j9_jMQC-Uh2DzdIVB44InRzZO6AbbGhvmm7lC6N3tk,6737
16
+ emu_mps/optimatrix/permutations.py,sha256=JRXGont8B4QgbkV9CzrA0w7uzLgBrmZ1J9aa0G52hPo,1979
17
+ emu_mps-2.0.1.dist-info/METADATA,sha256=aGt2cFa9rplEaJAo8z4J5uoAkqq9WnQIlOhciUc6vkg,3505
18
+ emu_mps-2.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ emu_mps-2.0.1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- emu_mps/__init__.py,sha256=KSTZWIZSKHhjt0yt8-fS23rFFcFVQciNiXBgHS0pnHU,646
2
- emu_mps/algebra.py,sha256=ngPtTH-j2ZCBWoaJZXlkUyIlug7dY7Q92gzfnRlpPMA,5485
3
- emu_mps/hamiltonian.py,sha256=LcBs6CKBb643a1e9AAVtQoUfa4L_0dIhLOKecx5OOWs,15864
4
- emu_mps/mpo.py,sha256=H5vkJvz4AfXfnPbvgWznBWpMUO8LnGL3_NAP3IhxZzQ,8740
5
- emu_mps/mps.py,sha256=J0I4oQP_F1woEKmnOqnXPOWxx2Y1addxNjosL3yhYAY,18214
6
- emu_mps/mps_backend.py,sha256=6fVaq-D4xyicYRjGjhqMEieC7---90LpfpbV7ZD7zkQ,2192
7
- emu_mps/mps_backend_impl.py,sha256=XT2HccHWd6Y1gIAs070pBxjPUPIHBl-hFCuqXJaPS-E,21256
8
- emu_mps/mps_config.py,sha256=ydKN0OOaWCBcNd9V-4CU5ZZ4w1FRT-bbKyZQD2WCaME,3317
9
- emu_mps/noise.py,sha256=h4X2EFjoC_Ok0gZ8I9wN77RANXaVehTBbjkcbY_GAmY,784
10
- emu_mps/tdvp.py,sha256=pIQ2NXA2Mrkp3elhqQbX3pdJVbtKkG3c5r9fFlJo7pI,5755
11
- emu_mps/utils.py,sha256=BqRJYAcXqprtZVJ0V_j954ON2bhTdtZiaTojsYyrWrg,8193
12
- emu_mps/optimatrix/__init__.py,sha256=lHWYNeThHp57ZrwTwXd0p8bNvcCv0w_AZ31iCWflBUo,226
13
- emu_mps/optimatrix/optimiser.py,sha256=cVMdm2r_4OpbthcQuFMrJ9rNR9WEJRga9c_lHrJFkhw,6687
14
- emu_mps/optimatrix/permutations.py,sha256=JRXGont8B4QgbkV9CzrA0w7uzLgBrmZ1J9aa0G52hPo,1979
15
- emu_mps-1.2.7.dist-info/METADATA,sha256=-yHfBZrLmNsmc-tA-Yb0KfmxULGgVrcLVUbx4F37oA4,3505
16
- emu_mps-1.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- emu_mps-1.2.7.dist-info/RECORD,,