tensorcircuit-nightly 1.3.0.dev20250903__py3-none-any.whl → 1.3.0.dev20250904__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.

Potentially problematic release.


This version of tensorcircuit-nightly might be problematic. Click here for more details.

tensorcircuit/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.3.0.dev20250903"
1
+ __version__ = "1.3.0.dev20250904"
2
2
  __author__ = "TensorCircuit Authors"
3
3
  __creator__ = "refraction-ray"
4
4
 
@@ -68,6 +68,7 @@ gate_aliases = [
68
68
 
69
69
  class AbstractCircuit:
70
70
  _nqubits: int
71
+ _d: int = 2
71
72
  _qir: List[Dict[str, Any]]
72
73
  _extra_qir: List[Dict[str, Any]]
73
74
  inputs: Tensor
@@ -865,7 +865,35 @@ class ExtendedBackend:
865
865
  "Backend '{}' has not implemented `mod`.".format(self.name)
866
866
  )
867
867
 
868
- def floor(self: Any, x: Tensor) -> Tensor:
868
+ def floor_divide(self: Any, x: Tensor, y: Tensor) -> Tensor:
869
+ r"""
870
+ Compute the element-wise floor division of two tensors.
871
+
872
+ This operation returns a new tensor containing the result of
873
+ dividing `x` by `y` and rounding each element down towards
874
+ negative infinity. The semantics are equivalent to the Python
875
+ `//` operator:
876
+
877
+ result[i] = floor(x[i] / y[i])
878
+
879
+ Broadcasting is supported according to the backend's rules.
880
+
881
+ :param x: Dividend tensor.
882
+ :type x: Tensor
883
+ :param y: Divisor tensor, must be broadcastable with `x`.
884
+ :type y: Tensor
885
+ :return: A tensor with the broadcasted shape of `x` and `y`,
886
+ where each element is the floored result of the division.
887
+ :rtype: Tensor
888
+
889
+ :raises NotImplementedError: If the backend does not provide an
890
+ implementation for `floor_divide`.
891
+ """
892
+ raise NotImplementedError(
893
+ "Backend '{}' has not implemented `floor_divide`.".format(self.name)
894
+ )
895
+
896
+ def floor(self: Any, a: Tensor) -> Tensor:
869
897
  """
870
898
  Compute the element-wise floor of the input tensor.
871
899
 
@@ -873,10 +901,10 @@ class ExtendedBackend:
873
901
  less than or equal to each element of the input tensor,
874
902
  i.e. it rounds each value down towards negative infinity.
875
903
 
876
- :param x: Input tensor containing numeric values.
877
- :type x: Tensor
878
- :return: A tensor with the same shape as `x`, where each element
879
- is the floored value of the corresponding element in `x`.
904
+ :param a: Input tensor containing numeric values.
905
+ :type a: Tensor
906
+ :return: A tensor with the same shape as `a`, where each element
907
+ is the floored value of the corresponding element in `a`.
880
908
  :rtype: Tensor
881
909
 
882
910
  :raises NotImplementedError: If the backend does not provide an
@@ -352,6 +352,9 @@ class JaxBackend(jax_backend.JaxBackend, ExtendedBackend): # type: ignore
352
352
  def floor(self, a: Tensor) -> Tensor:
353
353
  return jnp.floor(a)
354
354
 
355
+ def floor_divide(self, x: Tensor, y: Tensor) -> Tensor:
356
+ return jnp.floor_divide(x, y)
357
+
355
358
  def clip(self, a: Tensor, a_min: Tensor, a_max: Tensor) -> Tensor:
356
359
  return jnp.clip(a, a_min, a_max)
357
360
 
@@ -250,6 +250,9 @@ class NumpyBackend(numpy_backend.NumPyBackend, ExtendedBackend): # type: ignore
250
250
  def mod(self, x: Tensor, y: Tensor) -> Tensor:
251
251
  return np.mod(x, y)
252
252
 
253
+ def floor_divide(self, x: Tensor, y: Tensor) -> Tensor:
254
+ return np.floor_divide(x, y)
255
+
253
256
  def floor(self, a: Tensor) -> Tensor:
254
257
  return np.floor(a)
255
258
 
@@ -429,6 +429,9 @@ class PyTorchBackend(pytorch_backend.PyTorchBackend, ExtendedBackend): # type:
429
429
  def mod(self, x: Tensor, y: Tensor) -> Tensor:
430
430
  return torchlib.fmod(x, y)
431
431
 
432
+ def floor_divide(self, x: Tensor, y: Tensor) -> Tensor:
433
+ return torchlib.floor_divide(x, y)
434
+
432
435
  def floor(self, a: Tensor) -> Tensor:
433
436
  return torchlib.floor(a)
434
437
 
@@ -581,6 +581,9 @@ class TensorFlowBackend(tensorflow_backend.TensorFlowBackend, ExtendedBackend):
581
581
  return a
582
582
  return tf.math.floor(a)
583
583
 
584
+ def floor_divide(self, x: Tensor, y: Tensor) -> Tensor:
585
+ return tf.math.floordiv(x, y)
586
+
584
587
  def concat(self, a: Sequence[Tensor], axis: int = 0) -> Tensor:
585
588
  return tf.concat(a, axis=axis)
586
589
 
@@ -1,5 +1,9 @@
1
1
  """
2
2
  Quantum circuit: common methods for all circuit classes as MixIn
3
+
4
+ Note:
5
+ - Supports qubit (d = 2) and qudit (d >= 2) systems.
6
+ - For string-encoded samples/counts when d <= 36, digits use base-d characters 0–9A–Z (A = 10, …, Z = 35).
3
7
  """
4
8
 
5
9
  # pylint: disable=invalid-name
@@ -19,8 +23,10 @@ from .quantum import (
19
23
  correlation_from_counts,
20
24
  measurement_counts,
21
25
  sample_int2bin,
22
- sample_bin2int,
23
26
  sample2all,
27
+ _infer_num_sites,
28
+ _decode_basis_label,
29
+ onehot_d_tensor,
24
30
  )
25
31
  from .abstractcircuit import AbstractCircuit
26
32
  from .cons import npdtype, backend, dtypestr, contractor, rdtypestr
@@ -41,8 +47,9 @@ class BaseCircuit(AbstractCircuit):
41
47
  is_mps = False
42
48
 
43
49
  @staticmethod
44
- def all_zero_nodes(n: int, d: int = 2, prefix: str = "qb-") -> List[tn.Node]:
45
- l = [0.0 for _ in range(d)]
50
+ def all_zero_nodes(n: int, prefix: str = "qb-", dim: int = 2) -> List[tn.Node]:
51
+ prefix = "qd-" if dim > 2 else prefix
52
+ l = [0.0 for _ in range(dim)]
46
53
  l[0] = 1.0
47
54
  nodes = [
48
55
  tn.Node(
@@ -289,7 +296,7 @@ class BaseCircuit(AbstractCircuit):
289
296
  for op, index in ops:
290
297
  if not isinstance(op, tn.Node):
291
298
  # op is only a matrix
292
- op = backend.reshape2(op)
299
+ op = backend.reshaped(op, d=self._d)
293
300
  op = backend.cast(op, dtype=dtypestr)
294
301
  op = gates.Gate(op)
295
302
  else:
@@ -355,12 +362,12 @@ class BaseCircuit(AbstractCircuit):
355
362
 
356
363
  def perfect_sampling(self, status: Optional[Tensor] = None) -> Tuple[str, float]:
357
364
  """
358
- Sampling bistrings from the circuit output based on quantum amplitudes.
365
+ Sampling base-d strings (0–9A–Z when d <= 36) from the circuit output based on quantum amplitudes.
359
366
  Reference: arXiv:1201.3974.
360
367
 
361
368
  :param status: external randomness, with shape [nqubits], defaults to None
362
369
  :type status: Optional[Tensor]
363
- :return: Sampled bit string and the corresponding theoretical probability.
370
+ :return: Sampled base-d string and the corresponding theoretical probability.
364
371
  :rtype: Tuple[str, float]
365
372
  """
366
373
  return self.measure_jit(*range(self._nqubits), with_prob=True, status=status)
@@ -369,10 +376,10 @@ class BaseCircuit(AbstractCircuit):
369
376
  self, *index: int, with_prob: bool = False, status: Optional[Tensor] = None
370
377
  ) -> Tuple[Tensor, Tensor]:
371
378
  """
372
- Take measurement to the given quantum lines.
379
+ Take measurement on the given site indices (computational basis).
373
380
  This method is jittable is and about 100 times faster than unjit version!
374
381
 
375
- :param index: Measure on which quantum line.
382
+ :param index: Measure on which site (wire) index.
376
383
  :type index: int
377
384
  :param with_prob: If true, theoretical probability is also returned.
378
385
  :type with_prob: bool, optional
@@ -383,9 +390,8 @@ class BaseCircuit(AbstractCircuit):
383
390
  """
384
391
  # finally jit compatible ! and much faster than unjit version ! (100x)
385
392
  sample: List[Tensor] = []
386
- p = 1.0
387
- p = backend.convert_to_tensor(p)
388
- p = backend.cast(p, dtype=rdtypestr)
393
+ one_r = backend.cast(backend.convert_to_tensor(1.0), rdtypestr)
394
+ p = one_r
389
395
  for k, j in enumerate(index):
390
396
  if self.is_dm is False:
391
397
  nodes1, edge1 = self._copy()
@@ -400,40 +406,71 @@ class BaseCircuit(AbstractCircuit):
400
406
  if i != j:
401
407
  e ^ edge2[i]
402
408
  for i in range(k):
403
- m = (1 - sample[i]) * gates.array_to_tensor(np.array([1, 0])) + sample[
404
- i
405
- ] * gates.array_to_tensor(np.array([0, 1]))
406
- newnodes.append(Gate(m))
407
- newnodes[-1].id = id(newnodes[-1])
408
- newnodes[-1].is_dagger = False
409
- newnodes[-1].flag = "measurement"
410
- newnodes[-1].get_edge(0) ^ edge1[index[i]]
411
- newnodes.append(Gate(m))
412
- newnodes[-1].id = id(newnodes[-1])
413
- newnodes[-1].is_dagger = True
414
- newnodes[-1].flag = "measurement"
415
- newnodes[-1].get_edge(0) ^ edge2[index[i]]
409
+ if self._d == 2:
410
+ m = (1 - sample[i]) * gates.array_to_tensor(
411
+ np.array([1, 0])
412
+ ) + sample[i] * gates.array_to_tensor(np.array([0, 1]))
413
+ else:
414
+ m = onehot_d_tensor(sample[i], d=self._d)
415
+ g1 = Gate(m)
416
+ g1.id = id(g1)
417
+ g1.is_dagger = False
418
+ g1.flag = "measurement"
419
+ newnodes.append(g1)
420
+ g1.get_edge(0) ^ edge1[index[i]]
421
+ g2 = Gate(m)
422
+ g2.id = id(g2)
423
+ g2.is_dagger = True
424
+ g2.flag = "measurement"
425
+ newnodes.append(g2)
426
+ g2.get_edge(0) ^ edge2[index[i]]
427
+
416
428
  rho = (
417
429
  1
418
430
  / backend.cast(p, dtypestr)
419
431
  * contractor(newnodes, output_edge_order=[edge1[j], edge2[j]]).tensor
420
432
  )
421
- pu = backend.real(rho[0, 0])
422
- if status is None:
423
- r = backend.implicit_randu()[0]
433
+ if self._d == 2:
434
+ pu = backend.real(rho[0, 0])
435
+ if status is None:
436
+ r = backend.implicit_randu()[0]
437
+ else:
438
+ r = status[k]
439
+ r = backend.real(backend.cast(r, dtypestr))
440
+ eps = 0.31415926 * 1e-12
441
+ sign = (
442
+ backend.sign(r - pu + eps) / 2 + 0.5
443
+ ) # in case status is exactly 0.5
444
+ sign = backend.convert_to_tensor(sign)
445
+ sign = backend.cast(sign, dtype=rdtypestr)
446
+ sign_complex = backend.cast(sign, dtypestr)
447
+ sample.append(sign_complex)
448
+ p = p * (pu * (-1) ** sign + sign)
424
449
  else:
425
- r = status[k]
426
- r = backend.real(backend.cast(r, dtypestr))
427
- eps = 0.31415926 * 1e-12
428
- sign = backend.sign(r - pu + eps) / 2 + 0.5 # in case status is exactly 0.5
429
- sign = backend.convert_to_tensor(sign)
430
- sign = backend.cast(sign, dtype=rdtypestr)
431
- sign_complex = backend.cast(sign, dtypestr)
432
- sample.append(sign_complex)
433
- p = p * (pu * (-1) ** sign + sign)
434
-
435
- sample = backend.stack(sample)
436
- sample = backend.real(sample)
450
+ pu = backend.clip(
451
+ backend.real(backend.diagonal(rho)),
452
+ backend.convert_to_tensor(0.0),
453
+ backend.convert_to_tensor(1.0),
454
+ )
455
+ pu = pu / backend.sum(pu)
456
+ if status is None:
457
+ ind = backend.implicit_randc(
458
+ a=backend.arange(self._d),
459
+ shape=1,
460
+ p=backend.cast(pu, rdtypestr),
461
+ )
462
+ else:
463
+ one_r = backend.cast(backend.convert_to_tensor(1.0), rdtypestr)
464
+ st = backend.cast(status[k : k + 1], rdtypestr)
465
+ ind = backend.probability_sample(
466
+ shots=1,
467
+ p=backend.cast(pu, rdtypestr),
468
+ status=one_r - st,
469
+ )
470
+ k_out = backend.cast(ind[0], "int32")
471
+ sample.append(backend.cast(k_out, rdtypestr))
472
+ p = p * backend.cast(pu[k_out], rdtypestr)
473
+ sample = backend.real(backend.stack(sample))
437
474
  if with_prob:
438
475
  return sample, p
439
476
  else:
@@ -443,35 +480,48 @@ class BaseCircuit(AbstractCircuit):
443
480
 
444
481
  def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]:
445
482
  r"""
446
- Returns the tensornetwor nodes for the amplitude of the circuit given the bitstring l.
447
- For state simulator, it computes :math:`\langle l\vert \psi\rangle`,
448
- for density matrix simulator, it computes :math:`Tr(\rho \vert l\rangle \langle 1\vert)`
483
+ Returns the tensornetwor nodes for the amplitude of the circuit given a computational-basis label ``l``.
484
+ For a state simulator, it computes :math:`\langle l \vert \psi\rangle`;
485
+ for a density-matrix simulator, it computes :math:`\mathrm{Tr}(\rho \vert l\rangle\langle l\vert)`.
449
486
  Note how these two are different up to a square operation.
450
487
 
451
- :param l: The bitstring of 0 and 1s.
488
+ :Example:
489
+
490
+ >>> c = tc.Circuit(2)
491
+ >>> c.X(0)
492
+ >>> c.amplitude("10") # d=2, per-qubit digits
493
+ array(1.+0.j, dtype=complex64)
494
+ >>> c.CNOT(0, 1)
495
+ >>> c.amplitude("11")
496
+ array(1.+0.j, dtype=complex64)
497
+
498
+ For qudits (d>2, d<=36):
499
+ >>> c = tc.Circuit(3, dim=12)
500
+ >>> c.amplitude("0A2") # base-12 string, A stands for 10
501
+
502
+ :param l: Basis label.
503
+ - If a string: it must be a base-d string of length ``nqubits``, using 0–9A–Z (A=10,…,Z=35) when ``d<=36``.
504
+ - If a tensor/array/list: it should contain per-site integers in ``[0, d-1]`` with length ``nqubits``.
452
505
  :type l: Union[str, Tensor]
453
506
  :return: The tensornetwork nodes for the amplitude of the circuit.
454
507
  :rtype: List[Gate]
455
508
  """
509
+
456
510
  no, d_edges = self._copy()
457
511
  ms = []
458
512
  if self.is_dm:
459
513
  msconj = []
460
514
  if isinstance(l, str):
461
- for s in l:
462
- if s == "1":
463
- endn = np.array([0, 1], dtype=npdtype)
464
- elif s == "0":
465
- endn = np.array([1, 0], dtype=npdtype)
466
- ms.append(tn.Node(endn))
515
+ symbols = _decode_basis_label(l, n=self._nqubits, dim=self._d)
516
+ for k in symbols:
517
+ n = onehot_d_tensor(k, d=self._d)
518
+ ms.append(tn.Node(n))
467
519
  if self.is_dm:
468
- msconj.append(tn.Node(endn))
469
- else: # l is Tensor
520
+ msconj.append(tn.Node(n))
521
+ else:
470
522
  l = backend.cast(l, dtype=dtypestr)
471
523
  for i in range(self._nqubits):
472
- endn = l[i] * gates.array_to_tensor(np.array([0, 1])) + (
473
- 1 - l[i]
474
- ) * gates.array_to_tensor(np.array([1, 0]))
524
+ endn = onehot_d_tensor(l[i], d=self._d)
475
525
  ms.append(tn.Node(endn))
476
526
  if self.is_dm:
477
527
  msconj.append(tn.Node(endn))
@@ -522,17 +572,18 @@ class BaseCircuit(AbstractCircuit):
522
572
 
523
573
  def probability(self) -> Tensor:
524
574
  """
525
- get the 2^n length probability vector over computational basis
575
+ get the d^n length probability vector over computational basis
526
576
 
527
- :return: probability vector
577
+ :return: probability vector of shape [dim**n]
528
578
  :rtype: Tensor
529
579
  """
530
580
  s = self.state() # type: ignore
531
581
  if self.is_dm is False:
532
- p = backend.abs(s) ** 2
533
-
582
+ amp = backend.reshape(s, [-1])
583
+ p = backend.real(backend.abs(amp) ** 2)
534
584
  else:
535
- p = backend.abs(backend.diagonal(s))
585
+ diag = backend.diagonal(s)
586
+ p = backend.real(backend.reshape(diag, [-1]))
536
587
  return p
537
588
 
538
589
  @partial(arg_alias, alias_dict={"format": ["format_"]})
@@ -546,7 +597,7 @@ class BaseCircuit(AbstractCircuit):
546
597
  status: Optional[Tensor] = None,
547
598
  jittable: bool = True,
548
599
  ) -> Any:
549
- """
600
+ r"""
550
601
  batched sampling from state or circuit tensor network directly
551
602
 
552
603
  :param batch: number of samples, defaults to None
@@ -569,6 +620,7 @@ class BaseCircuit(AbstractCircuit):
569
620
  "count_tuple": # (np.array([0]), np.array([2]))
570
621
 
571
622
  "count_dict_bin": # {"00": 2, "01": 0, "10": 0, "11": 0}
623
+ for cases d\in [11, 36], use 0–9A–Z digits (e.g., 'A' -> 10, …, 'Z' -> 35);
572
624
 
573
625
  "count_dict_int": # {0: 2, 1: 0, 2: 0, 3: 0}
574
626
 
@@ -619,8 +671,8 @@ class BaseCircuit(AbstractCircuit):
619
671
  if format is None:
620
672
  return r
621
673
  r = backend.stack([ri[0] for ri in r]) # type: ignore
622
- r = backend.cast(r, "int32")
623
- ch = sample_bin2int(r, self._nqubits)
674
+ ch = backend.cast(r, "int32")
675
+ # ch = sample_bin2int(r, self._nqubits, dim=self._d)
624
676
  else: # allow_state
625
677
  if batch is None:
626
678
  nbatch = 1
@@ -645,7 +697,7 @@ class BaseCircuit(AbstractCircuit):
645
697
  # 2,
646
698
  # )
647
699
  if format is None: # for backward compatibility
648
- confg = sample_int2bin(ch, self._nqubits)
700
+ confg = sample_int2bin(ch, self._nqubits, dim=self._d)
649
701
  prob = backend.gather1d(p, ch)
650
702
  r = list(zip(confg, prob)) # type: ignore
651
703
  if batch is None:
@@ -653,7 +705,9 @@ class BaseCircuit(AbstractCircuit):
653
705
  return r
654
706
  if self._nqubits > 35:
655
707
  jittable = False
656
- return sample2all(sample=ch, n=self._nqubits, format=format, jittable=jittable)
708
+ return sample2all(
709
+ sample=ch, n=self._nqubits, format=format, jittable=jittable, dim=self._d
710
+ )
657
711
 
658
712
  def sample_expectation_ps(
659
713
  self,
@@ -853,9 +907,9 @@ class BaseCircuit(AbstractCircuit):
853
907
  """
854
908
  inputs = backend.reshape(inputs, [-1])
855
909
  N = inputs.shape[0]
856
- n = int(np.log(N) / np.log(2))
910
+ n = _infer_num_sites(N, self._d)
857
911
  assert n == self._nqubits
858
- inputs = backend.reshape(inputs, [2 for _ in range(n)])
912
+ inputs = backend.reshape(inputs, [self._d for _ in range(n)])
859
913
  if self.inputs is not None:
860
914
  self._nodes[0].tensor = inputs
861
915
  if self.is_dm:
@@ -886,9 +940,9 @@ class BaseCircuit(AbstractCircuit):
886
940
 
887
941
 
888
942
 
889
- :param index: the qubit for the z-basis measurement
943
+ :param index: the site index for the Z-basis measurement
890
944
  :type index: int
891
- :return: 0 or 1 for z measurement on up and down freedom
945
+ :return: 0 or 1 for Z-basis measurement outcome
892
946
  :rtype: Tensor
893
947
  """
894
948
  return self.general_kraus( # type: ignore
@@ -967,8 +1021,8 @@ class BaseCircuit(AbstractCircuit):
967
1021
 
968
1022
  def projected_subsystem(self, traceout: Tensor, left: Tuple[int, ...]) -> Tensor:
969
1023
  """
970
- remaining wavefunction or density matrix on qubits in left, with other qubits
971
- fixed in 0 or 1 indicated by traceout
1024
+ remaining wavefunction or density matrix on sites in ``left``, with other sites
1025
+ fixed to given digits (0..d-1) as indicated by ``traceout``
972
1026
 
973
1027
  :param traceout: can be jitted
974
1028
  :type traceout: Tensor
@@ -977,15 +1031,14 @@ class BaseCircuit(AbstractCircuit):
977
1031
  :return: _description_
978
1032
  :rtype: Tensor
979
1033
  """
980
- end0, end1 = gates.array_to_tensor(np.array([1.0, 0]), np.array([0, 1.0]))
1034
+
981
1035
  traceout = backend.cast(traceout, dtypestr)
982
1036
  nodes, front = self._copy()
983
1037
  L = self._nqubits
984
1038
  edges = []
985
1039
  for i in range(len(traceout)):
986
1040
  if i not in left:
987
- b = traceout[i]
988
- n = gates.Gate((1 - b) * end0 + b * end1)
1041
+ n = Gate(onehot_d_tensor(traceout[i], d=self._d))
989
1042
  nodes.append(n)
990
1043
  front[i] ^ n[0]
991
1044
  else:
@@ -994,8 +1047,7 @@ class BaseCircuit(AbstractCircuit):
994
1047
  if self.is_dm:
995
1048
  for i in range(len(traceout)):
996
1049
  if i not in left:
997
- b = traceout[i]
998
- n = gates.Gate((1 - b) * end0 + b * end1)
1050
+ n = Gate(onehot_d_tensor(traceout[i], d=self._d))
999
1051
  nodes.append(n)
1000
1052
  front[i + L] ^ n[0]
1001
1053
  else: