Skip to content

Commit b0baf4e

Browse files
improve light cone impl
1 parent cadf3bd commit b0baf4e

6 files changed

Lines changed: 136 additions & 84 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ TensorCircuit is a **Tensor Network-first**, **Multi-Backend** quantum computing
2222
- **Pattern**: Avoid Python control flow (if/else) that depends on tensor values. Use `tc.backend.cond` or `tc.backend.switch` if necessary, or structure code to be statically analyzable.
2323
- **Benefit**: This enables massive speedups on JAX and TensorFlow backends.
2424

25+
### Known Issues and Intermittent Failures
26+
27+
- **`tests/test_circuit.py::test_qiskit2tc`**: This test may fail intermittently (e.g., in Qiskit 0.46.3+) due to non-deterministic behavior in Qiskit's `UnitaryGate.control()` method during circuit translation. If this test fails while other tests pass, it is likely a framework-level "heisenbug" and not a regression in TensorCircuit core logic.
28+
2529
## Repository Structure
2630

2731
- `tensorcircuit/`: Core package source code.
@@ -33,6 +37,12 @@ TensorCircuit is a **Tensor Network-first**, **Multi-Backend** quantum computing
3337

3438
## Configuration and Dependencies
3539

40+
### Python Environment Management
41+
42+
- **Rule**: Never install packages in the default system or user Python environment unless explicitly directed by the user.
43+
- **Protocol**: If a command fails due to a missing package or `ModuleNotFoundError`, **explicitly ask the user** for the correct Python environment to use (e.g., a specific Conda environment name like `2602`).
44+
- **Workflow**: Once an environment is identified, prefix all Python/pytest/pylint commands with `conda run -n <env_name>` or the appropriate environment activator.
45+
3646
### Core Dependencies
3747

3848
- numpy

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818

1919
- Add `bitwise_or`, `any`, `all` methods for backends.
2020

21+
- Better lightcone cancellation.
22+
2123
### Fixed
2224

2325
- Fix pauli propagation sign error for Y operator.
2426

2527
- Fix jax backend QR gradient bug.
2628

29+
- Lazy import for tensorflow and torch.
30+
31+
2732
## v1.5.0
2833

2934
### Added
@@ -60,8 +65,6 @@
6065

6166
- Generalize `scatter` method for backends.
6267

63-
- Lazy import for tensorflow and torch.
64-
6568
## v1.4.0
6669

6770
### Added

examples/lightcone_simplify.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import numpy as np
66
import tensorcircuit as tc
77

8-
K = tc.set_backend("tensorflow")
8+
K = tc.set_backend("jax")
9+
tc.set_contractor("cotengra")
910

1011

1112
def brickwall_ansatz(c, params, gatename, nlayers):
@@ -34,8 +35,10 @@ def loss(params, n, nlayers, enable_lightcone):
3435

3536

3637
def efficiency():
37-
for n in range(6, 40, 4):
38-
for nlayers in range(2, 6, 2):
38+
for n in range(10, 40, 4):
39+
for nlayers in range(2, 6, 3):
40+
if n > 20 and nlayers > 4:
41+
continue
3942
print(n, nlayers)
4043
print("w lightcone")
4144
(v2, g2), _, _ = tc.utils.benchmark(
@@ -60,5 +63,5 @@ def correctness(n, nlayers):
6063

6164

6265
if __name__ == "__main__":
63-
efficiency()
6466
correctness(7, 3)
67+
efficiency()

tensorcircuit/basecircuit.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ def _tensors_to_nodes(
102102
def coloring_nodes(
103103
nodes: Sequence[tn.Node], is_dagger: bool = False, flag: str = "inputs"
104104
) -> None:
105+
r"""
106+
Tag nodes with metadata used for casual lightcone simplification and tracing.
107+
108+
:param nodes: A sequence of tensornetwork nodes to tag.
109+
:type nodes: Sequence[tn.Node]
110+
:param is_dagger: Whether the nodes represent conjugate operations (U^\dagger),
111+
defaults to False.
112+
:type is_dagger: bool, optional
113+
:param flag: A label for the node type (e.g., "gate", "inputs", "operator"),
114+
defaults to "inputs".
115+
:type flag: str, optional
116+
"""
105117
for node in nodes:
106118
node.is_dagger = is_dagger
107119
node.flag = flag
@@ -114,6 +126,19 @@ def coloring_copied_nodes(
114126
is_dagger: bool = True,
115127
flag: str = "inputs",
116128
) -> None:
129+
"""
130+
Tag copied nodes while preserving the original node's identity for lightcone cancellation.
131+
132+
:param nodes: A sequence of newly copied nodes.
133+
:type nodes: Sequence[tn.Node]
134+
:param nodes0: The sequence of original nodes from which `nodes` were copied.
135+
:type nodes0: Sequence[tn.Node]
136+
:param is_dagger: Whether the copied nodes represent conjugate operations,
137+
defaults to True.
138+
:type is_dagger: bool, optional
139+
:param flag: A label for the node type, defaults to "inputs".
140+
:type flag: str, optional
141+
"""
117142
for node, n0 in zip(nodes, nodes0):
118143
node.is_dagger = is_dagger
119144
node.flag = flag
@@ -238,7 +263,7 @@ def apply_general_gate(
238263

239264
if applied is False:
240265
gate.name = name
241-
self.coloring_nodes([gate])
266+
self.coloring_nodes([gate], flag="gate")
242267
# gate.flag = "gate"
243268
# gate.is_dagger = False
244269
# gate.id = id(gate)
@@ -257,18 +282,16 @@ def apply_general_gate(
257282
elif mpo: # gate in MPO format
258283
assert isinstance(gate, QuOperator)
259284
gatec = gate.copy()
285+
self.coloring_nodes(gatec.nodes, flag="gate")
260286
for n in gatec.nodes:
261-
n.flag = "gate"
262-
n.is_dagger = False
263287
n.id = id(gate)
264288
n.name = name
265289
self._nodes += gatec.nodes
266290
if self.is_dm:
267291
gateconj = gate.adjoint()
268-
for n0, n in zip(gatec.nodes, gateconj.nodes):
269-
n.flag = "gate"
270-
n.is_dagger = True
271-
n.id = id(n0)
292+
self.coloring_nodes(gateconj.nodes, flag="gate", is_dagger=True)
293+
for _, n in zip(gatec.nodes, gateconj.nodes):
294+
n.id = id(gate)
272295
n.name = name
273296
self._nodes += gateconj.nodes
274297

@@ -284,9 +307,8 @@ def apply_general_gate(
284307
mps_nodes = [gate]
285308
else:
286309
mps_nodes = gate.nodes
310+
self.coloring_nodes(mps_nodes, flag="gate")
287311
for n in mps_nodes:
288-
n.flag = "gate"
289-
n.is_dagger = False
290312
n.id = id(gate)
291313
n.name = name
292314
self._nodes += mps_nodes
@@ -299,10 +321,9 @@ def apply_general_gate(
299321
else:
300322
gateconj = gate.adjoint()
301323
gateconj_nodes = gateconj.nodes
302-
for n0, n in zip(mps_nodes, gateconj_nodes):
303-
n.flag = "gate"
304-
n.is_dagger = True
305-
n.id = id(n0)
324+
self.coloring_nodes(gateconj_nodes, flag="gate", is_dagger=True)
325+
for _, n in zip(mps_nodes, gateconj_nodes):
326+
n.id = id(gate)
306327
n.name = name
307328
self._nodes += gateconj_nodes
308329

@@ -463,15 +484,11 @@ def measure_jit(
463484
else:
464485
m = onehot_d_tensor(sample[i], d=self._d)
465486
g1 = Gate(m)
466-
g1.id = id(g1)
467-
g1.is_dagger = False
468-
g1.flag = "measurement"
487+
self.coloring_nodes([g1], flag="measurement")
469488
newnodes.append(g1)
470489
g1.get_edge(0) ^ edge1[index[i]]
471490
g2 = Gate(m)
472-
g2.id = id(g2)
473-
g2.is_dagger = True
474-
g2.flag = "measurement"
491+
self.coloring_nodes([g2], flag="measurement", is_dagger=True)
475492
newnodes.append(g2)
476493
g2.get_edge(0) ^ edge2[index[i]]
477494

@@ -551,16 +568,14 @@ def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]:
551568
msconj = []
552569
for i in range(self._nqubits):
553570
n = tn.Node(endns[i])
554-
n.flag = "measurement"
555-
n.is_dagger = False
556-
n.id = id(n)
571+
self.coloring_nodes([n], flag="measurement")
557572
ms.append(n)
558573
d_edges[i] ^ n.get_edge(0)
559574
if self.is_dm:
560575
nconj = tn.Node(endns[i])
561-
nconj.flag = "measurement"
562-
nconj.is_dagger = True
563-
nconj.id = id(n)
576+
self.coloring_copied_nodes(
577+
[nconj], [n], flag="measurement", is_dagger=True
578+
)
564579
msconj.append(nconj)
565580
d_edges[i + self._nqubits] ^ nconj.get_edge(0)
566581
no.extend(ms)

tensorcircuit/circuit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def __init__(
117117
else:
118118
raise ValueError("No inputs provided") # should not be reached
119119

120-
self.coloring_nodes(nodes)
120+
self.coloring_nodes(nodes, flag="inputs")
121121
self._nodes = nodes
122122

123123
self._start_index = len(nodes)
@@ -180,7 +180,7 @@ def replace_mps_inputs(self, mps_inputs: QuOperator) -> None:
180180
new_front[j] ^ other[0][other[1]]
181181
j += 1
182182
self._front += new_front[j:]
183-
self.coloring_nodes(new_nodes)
183+
self.coloring_nodes(new_nodes, flag="inputs")
184184
self._nodes = new_nodes + self._nodes[self._start_index :]
185185
self._start_index = len(new_nodes)
186186

tensorcircuit/simplify.py

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -203,79 +203,100 @@ def _full_rank_simplify(nodes: List[Any]) -> List[Any]:
203203

204204

205205
def _light_cone_cancel(nodes: List[Any]) -> Tuple[List[Any], bool]:
206+
"""
207+
Scan the nodes and cancel pairs of U and U^dagger that are directly connected.
208+
Assumes that for a gate node, the first half of its edges are 'output' (future-facing)
209+
and the second half are 'input' (past-facing).
210+
"""
206211
is_changed = False
207-
for ind in range(len(nodes) // 2, 0, -1):
208-
n = nodes[ind]
209-
if n.is_dagger is True:
212+
nodes_to_remove = set()
213+
214+
# Identify the "future" side. Nodes in expectation are typically [ket_nodes, bra_nodes, ops]
215+
# We scan backward through the nodes list.
216+
for i in range(len(nodes) - 1, -1, -1):
217+
n = nodes[i]
218+
if n in nodes_to_remove:
219+
continue
220+
if getattr(n, "is_dagger", None) is True:
210221
continue
222+
211223
noe = len(n.shape)
212224
if noe % 2 != 0:
213225
continue
214-
e = n[0]
215-
n1, n2 = e.node1, e.node2 # one of them is n itself
216-
if n1 is None or n2 is None:
217-
continue
218-
if n1.is_dagger == n2.is_dagger:
219-
continue
220-
if n1.id != n2.id:
221-
continue
222-
if e.axis1 != e.axis2:
223-
continue
224-
for i in range(noe // 2):
225-
e = n[i]
226-
n3, n4 = e.node1, e.node2 # should also be n1 and n2
227-
if n3 is None or n4 is None:
226+
227+
# Check if all output legs (0 to noe//2 - 1) are connected to the same conjugate node
228+
match_node = None
229+
for leg_idx in range(noe // 2):
230+
e = n[leg_idx]
231+
if e.is_dangling():
232+
break
233+
n1, n2 = e.node1, e.node2
234+
other = n2 if n1 is n else n1
235+
236+
if getattr(other, "is_dagger", None) is not True:
228237
break
229-
if not ((n3 is n1 and n4 is n2) or (n3 is n2 and n4 is n1)):
238+
if getattr(other, "id", None) != getattr(n, "id", -1):
230239
break
231240
if e.axis1 != e.axis2:
232241
break
242+
243+
if match_node is None:
244+
match_node = other
245+
elif match_node is not other:
246+
break
233247
else:
234-
if n1 is not n:
235-
n1, n2 = n2, n1 # make sure n1 is n dagger is False
236-
237-
# contract
238-
njs = [i for i, n in enumerate(nodes) if n is n1 or n is n2]
239-
# new_node = tn.contract_between(e.node1, e.node2)
240-
# contract(e) is not enough for multi edges between two tensors
241-
for i in range(noe // 2):
242-
e = n1[noe // 2 + i]
243-
m1, m3 = e.node1, e.node2
244-
i1, i3 = e.axis1, e.axis2
245-
if m1 is not n1:
246-
m1, m3 = m3, m1 # m1 is n1, m3 is the one behind m1
247-
i1, i3 = i3, i1
248-
e.disconnect()
249-
e = n2[noe // 2 + i]
250-
m2, m4 = e.node1, e.node2
251-
i2, i4 = e.axis1, e.axis2
252-
if m2 is not n2:
253-
m2, m4 = m4, m2 # m1 is n1, m3 is the one behind m1
254-
i2, i4 = i4, i2
255-
e.disconnect()
256-
m3[i3] ^ m4[i4]
257-
nodes = _multi_remove(nodes, njs)
258-
259-
is_changed = True
260-
break # switch to the next node
261-
return nodes, is_changed
248+
if match_node is not None and match_node not in nodes_to_remove:
249+
# Perform cancellation by bypass
250+
for leg_idx in range(noe // 2, noe):
251+
e_n = n[leg_idx]
252+
e_m = match_node[leg_idx]
253+
254+
m_n, i_n = (
255+
(e_n.node2, e_n.axis2)
256+
if e_n.node1 is n
257+
else (e_n.node1, e_n.axis1)
258+
)
259+
m_m, i_m = (
260+
(e_m.node2, e_m.axis2)
261+
if e_m.node1 is match_node
262+
else (e_m.node1, e_m.axis1)
263+
)
264+
265+
e_n.disconnect()
266+
e_m.disconnect()
267+
m_n[i_n] ^ m_m[i_m]
268+
269+
nodes_to_remove.add(n)
270+
nodes_to_remove.add(match_node)
271+
is_changed = True
272+
273+
if is_changed:
274+
new_nodes = [n for n in nodes if n not in nodes_to_remove]
275+
return new_nodes, True
276+
return nodes, False
262277

263278

264279
# TODO(@refraction-ray): better light cone cancellation in terms of MPO gates (three legs one)
280+
# MPO cancellation requires matching internal bond dimensions and identities of all nodes in the MPO.
265281

266282

267283
def _full_light_cone_cancel(nodes: List[Any]) -> List[Any]:
268284
"""
269285
Simplify the list of tc.Nodes using casual lightcone structure.
270286
271-
:param nodes: _description_
287+
:param nodes: List of nodes representing the tensor network.
272288
:type nodes: List[Any]
273-
:return: _description_
289+
:return: Simplified list of nodes.
274290
:rtype: List[Any]
275291
"""
276-
for n in nodes:
277-
if getattr(n, "is_dagger", None) is None:
278-
return nodes
292+
if not nodes:
293+
return nodes
294+
# Check metadata availability
295+
if any(getattr(n, "is_dagger", None) is None for n in nodes):
296+
return nodes
297+
298+
# A single pass of _light_cone_cancel (optimized for O(N)) is often sufficient
299+
# but we use a while loop to ensure all possible cancellations are resolved.
279300
nodes, is_changed = _light_cone_cancel(nodes)
280301
while is_changed:
281302
nodes, is_changed = _light_cone_cancel(nodes)

0 commit comments

Comments
 (0)