Skip to content

Commit 4235d6b

Browse files
add from_cirq
1 parent 54a67fa commit 4235d6b

File tree

6 files changed

+273
-3
lines changed

6 files changed

+273
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Added
66

7+
- Add `from_cirq` method for `Circuit` and `DMCircuit` to support translation from Cirq.
8+
79
- Add `tc.AnalogCircuit` for digital-analog hybrid simulation.
810

911
- Add sparse matrix related methods for pytorch backend.

llm_experience.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,24 @@ This document records specific technical protocols, lessons learned, and advance
5151

5252
2. **Fair Comparison**:
5353
* Ensure "apples-to-apples" settings (e.g., same precision `complex128`) to support valid performance claims.
54+
55+
## Noise Modeling and Mitigation
56+
57+
1. **Readout Error Handling**:
58+
* The `circuit_with_noise(c, noise_conf)` function internally applies general quantum noise (Kraus channels specified in `nc`) to gates, but it does **not** automatically apply readout error configuration (`readout_error`) encoded in `NoiseConf`.
59+
* **Protocol**: You must **explicitly** pass the readout error when calling `circuit.sample()`. Example:
60+
```python
61+
c_noisy.sample(..., readout_error=noise_conf.readout_error)
62+
```
63+
* Failing to do so will result in noiseless measurements even if `readout_error` is present in `NoiseConf`.
64+
65+
2. **Readout Error Format**:
66+
* When adding readout noise via `NoiseConf.add_noise("readout", errors)`, the expected format for each qubit's error is `[p(0|0), p(1|1)]` (a list of two probabilities), **not** the full $2 \times 2$ confusion matrix.
67+
* **Pitfall**: Passing a full matrix like `[[0.9, 0.1], [0.1, 0.9]]` can cause unexpected `TypeError` during internal matrix construction (e.g., `1 - list` error).
68+
* **Protocol**: Specify readout error as `[0.9, 0.9]` which implies $p(0|0)=0.9$ and $p(1|1)=0.9$.
69+
70+
3. **Circuit Expectation with Noise**:
71+
* The `Circuit.expectation` method supports `noise_conf` as a keyword argument (e.g., `c.expectation(..., noise_conf=conf, nmc=1000)`). This is often cleaner than calling `tc.noisemodel.expectation_noisfy(c, ...)` directly.
72+
73+
4. **Multi-Qubit Thermal Noise**:
74+
* The `thermalrelaxationchannel` returns single-qubit Kraus operators. To apply thermal noise to multi-qubit gates (like CNOT), you generally cannot simply pass the single-qubit channel to `add_noise("cnot", ...)` because of dimension mismatch.

tensorcircuit/abstractcircuit.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,47 @@ def from_qiskit(
916916
binding_params=binding_params,
917917
)
918918

919+
@classmethod
920+
def from_cirq(
921+
cls,
922+
qc: Any,
923+
n: Optional[int] = None,
924+
inputs: Optional[List[float]] = None,
925+
circuit_params: Optional[Dict[str, Any]] = None,
926+
) -> "AbstractCircuit":
927+
"""
928+
Import Cirq Circuit object as a ``tc.Circuit`` object.
929+
930+
:Example:
931+
932+
>>> import cirq
933+
>>> c = cirq.Circuit()
934+
>>> q = cirq.LineQubit.range(3)
935+
>>> c.append(cirq.H(q[0]))
936+
>>> c.append(cirq.CNOT(q[0], q[1]))
937+
>>> tc_c = tc.Circuit.from_cirq(c)
938+
939+
:param qc: Cirq Circuit object
940+
:type qc: cirq.Circuit
941+
:param n: The number of qubits for the circuit
942+
:type n: int
943+
:param inputs: possible input wavefunction for ``tc.Circuit``, defaults to None
944+
:type inputs: Optional[List[float]], optional
945+
:param circuit_params: kwargs given in Circuit.__init__ construction function, default to None.
946+
:type circuit_params: Optional[Dict[str, Any]]
947+
:return: The same circuit but as tensorcircuit object
948+
:rtype: Circuit
949+
"""
950+
from .translation import cirq2tc
951+
952+
return cirq2tc( # type: ignore
953+
qc,
954+
n,
955+
inputs,
956+
circuit_constructor=cls,
957+
circuit_params=circuit_params,
958+
)
959+
919960
def vis_tex(self, **kws: Any) -> str:
920961
"""
921962
Generate latex string based on quantikz latex package

tensorcircuit/analogcircuit.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def wrapper(*args, **kwargs): # type: ignore
169169
f"object has no attribute '{name}'."
170170
)
171171

172-
def state(self) -> Tensor:
172+
def state(self, form: str = "default") -> Tensor:
173173
"""
174174
Executes the full digital-analog sequence.
175175
@@ -209,8 +209,13 @@ def state(self) -> Tensor:
209209
else:
210210
psi = self.digital_circuits[-1].wavefunction()
211211
self._effective_circuit = Circuit(self.num_qubits, inputs=psi)
212-
213-
return psi
212+
if form == "default":
213+
shape = [-1]
214+
elif form == "ket":
215+
shape = [-1, 1]
216+
elif form == "bra": # no conj here
217+
shape = [1, -1]
218+
return backend.reshape(psi, shape=shape)
214219

215220
wavefunction = state
216221

tensorcircuit/translation.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,124 @@ def qiskit_from_qasm_str_ordered_measure(qasm_str: str) -> Any:
792792
for qid, cid in measure_sequence:
793793
qc.measure(qid, cid)
794794
return qc
795+
796+
797+
def cirq2tc(
798+
qc: Any,
799+
n: Optional[int] = None,
800+
inputs: Optional[List[float]] = None,
801+
is_dm: bool = False,
802+
circuit_constructor: Any = None,
803+
circuit_params: Optional[Dict[str, Any]] = None,
804+
) -> Any:
805+
"""
806+
Generate a tensorcircuit circuit from the cirq circuit.
807+
808+
:param qc: A quantum circuit in cirq
809+
:type qc: cirq.Circuit
810+
:param n: # of qubits, defaults to None
811+
:type n: Optional[int], optional
812+
:param inputs: Input state of the circuit, defaults to None
813+
:type inputs: Optional[List[float]], optional
814+
:param is_dm: whether to use DMCircuit, defaults to False
815+
:type is_dm: bool, optional
816+
:param circuit_constructor: _description_, defaults to None
817+
:type circuit_constructor: Any, optional
818+
:param circuit_params: _description_, defaults to None
819+
:type circuit_params: Optional[Dict[str, Any]], optional
820+
:return: _description_
821+
:rtype: Any
822+
"""
823+
824+
if circuit_constructor is not None:
825+
Circ = circuit_constructor
826+
elif is_dm:
827+
Circ = DMCircuit2
828+
else:
829+
Circ = Circuit
830+
831+
if n is None:
832+
n = len(sorted(qc.all_qubits()))
833+
834+
if circuit_params is None:
835+
circuit_params = {}
836+
if "nqubits" not in circuit_params:
837+
circuit_params["nqubits"] = n
838+
839+
if inputs is not None:
840+
circuit_params["inputs"] = inputs
841+
842+
tc_circuit: Any = Circ(**circuit_params)
843+
844+
qubits = sorted(qc.all_qubits())
845+
qubit_map = {q: i for i, q in enumerate(qubits)}
846+
847+
for op in qc.all_operations():
848+
if isinstance(op.gate, cirq.MeasurementGate):
849+
for q in op.qubits:
850+
tc_circuit.measure_instruction(qubit_map[q])
851+
continue
852+
853+
index = [qubit_map[q] for q in op.qubits]
854+
gate = op.gate
855+
# gate_name = str(gate)
856+
857+
if isinstance(gate, cirq.IdentityGate):
858+
continue
859+
860+
# Standard Gates (and PowGates with exp=1)
861+
if isinstance(gate, cirq.HPowGate) and np.isclose(gate.exponent, 1):
862+
tc_circuit.h(*index)
863+
elif isinstance(gate, cirq.XPowGate) and np.isclose(gate.exponent, 1):
864+
tc_circuit.x(*index)
865+
elif isinstance(gate, cirq.YPowGate) and np.isclose(gate.exponent, 1):
866+
tc_circuit.y(*index)
867+
elif isinstance(gate, cirq.ZPowGate) and np.isclose(gate.exponent, 1):
868+
tc_circuit.z(*index)
869+
elif isinstance(gate, cirq.SwapPowGate) and np.isclose(gate.exponent, 1):
870+
tc_circuit.swap(*index)
871+
elif isinstance(gate, cirq.ISwapPowGate) and np.isclose(gate.exponent, 1):
872+
tc_circuit.iswap(*index)
873+
elif isinstance(gate, cirq.CNotPowGate) and np.isclose(gate.exponent, 1):
874+
tc_circuit.cnot(*index)
875+
elif isinstance(gate, cirq.CZPowGate) and np.isclose(gate.exponent, 1):
876+
tc_circuit.cz(*index)
877+
878+
# Variable Gates (X, Y, Z, ISWAP family)
879+
elif isinstance(gate, cirq.XPowGate):
880+
tc_circuit.rx(*index, theta=gate.exponent * np.pi)
881+
elif isinstance(gate, cirq.YPowGate):
882+
tc_circuit.ry(*index, theta=gate.exponent * np.pi)
883+
elif isinstance(gate, cirq.ZPowGate):
884+
if np.isclose(gate.exponent, 0.5):
885+
tc_circuit.s(*index)
886+
elif np.isclose(gate.exponent, 0.25):
887+
tc_circuit.t(*index)
888+
else:
889+
tc_circuit.rz(*index, theta=gate.exponent * np.pi)
890+
elif isinstance(gate, cirq.ISwapPowGate):
891+
tc_circuit.iswap(*index, theta=gate.exponent)
892+
893+
elif isinstance(gate, cirq.FSimGate):
894+
tc_circuit.iswap(*index, theta=-gate.theta * 2 / np.pi)
895+
tc_circuit.cphase(*index, theta=-gate.phi)
896+
elif isinstance(gate, cirq.PhasedXPowGate):
897+
# Rz(phase_exponent * pi) Rx(exponent * pi) Rz(-phase_exponent * pi)
898+
tc_circuit.rz(*index, theta=-gate.phase_exponent * np.pi)
899+
tc_circuit.rx(*index, theta=gate.exponent * np.pi)
900+
tc_circuit.rz(*index, theta=gate.phase_exponent * np.pi)
901+
elif isinstance(gate, cirq.MatrixGate):
902+
# Arbitrary unitary
903+
m = cirq.unitary(gate)
904+
tc_circuit.any(*index, unitary=m)
905+
else:
906+
# try to get unitary
907+
try:
908+
m = cirq.unitary(gate)
909+
tc_circuit.any(*index, unitary=m)
910+
except (TypeError, ValueError):
911+
logger.warning(
912+
f"Cirq gate {gate} not supported in translation, skipping"
913+
)
914+
915+
return tc_circuit

tests/test_circuit.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,3 +1732,83 @@ def test_projected_subsystem(backend):
17321732
)
17331733
assert tc.backend.shape_tuple(s) == (4, 4)
17341734
np.testing.assert_allclose(s[3, 3], 0.8108051, atol=1e-5)
1735+
1736+
1737+
@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")])
1738+
def test_cirq_translation(backend):
1739+
try:
1740+
import cirq
1741+
except ImportError:
1742+
pytest.skip("cirq is not installed")
1743+
1744+
n = 3
1745+
q = cirq.LineQubit.range(n)
1746+
c = cirq.Circuit()
1747+
c.append(cirq.H(q[0]))
1748+
c.append(cirq.H(q[1]))
1749+
c.append(cirq.CNOT(q[0], q[1]))
1750+
c.append(cirq.rx(0.5)(q[0]))
1751+
c.append(cirq.ry(0.2)(q[1]))
1752+
c.append(cirq.rz(0.5)(q[2]))
1753+
c.append(cirq.CNOT(q[1], q[2]))
1754+
c.append(cirq.CZ(q[1], q[0]))
1755+
1756+
c_tc = tc.Circuit.from_cirq(c)
1757+
1758+
# Validation via unitary
1759+
u_cirq = cirq.unitary(c)
1760+
u_tc = c_tc.matrix()
1761+
1762+
np.testing.assert_allclose(u_cirq, tc.backend.numpy(u_tc), atol=1e-5)
1763+
1764+
1765+
def assert_allclose_up_to_global_phase(a, b, atol=1e-5):
1766+
a = np.array(a)
1767+
b = np.array(b)
1768+
flat_a = a.flatten()
1769+
flat_b = b.flatten()
1770+
idx = np.argmax(np.abs(flat_a))
1771+
if np.abs(flat_a[idx]) < 1e-10:
1772+
np.testing.assert_allclose(a, b, atol=atol)
1773+
return
1774+
1775+
phase_diff = flat_a[idx] / flat_b[idx]
1776+
b = b * phase_diff
1777+
np.testing.assert_allclose(a, b, atol=atol)
1778+
1779+
1780+
@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")])
1781+
def test_cirq_gates_translation(backend):
1782+
try:
1783+
import cirq
1784+
except ImportError:
1785+
pytest.skip("cirq is not installed")
1786+
1787+
n = 3
1788+
q = cirq.LineQubit.range(n)
1789+
1790+
# Test FSim and ISWAP
1791+
c2 = cirq.Circuit()
1792+
c2.append(cirq.ISWAP(q[0], q[1]))
1793+
c2.append(cirq.FSimGate(0.1, 0.2)(q[1], q[2]))
1794+
1795+
c2_tc = tc.Circuit.from_cirq(c2)
1796+
u_cirq2 = cirq.unitary(c2)
1797+
u_tc2 = c2_tc.matrix()
1798+
assert_allclose_up_to_global_phase(u_cirq2, tc.backend.numpy(u_tc2), atol=1e-5)
1799+
1800+
# Test PhasedXPow
1801+
c3 = cirq.Circuit()
1802+
c3.append(cirq.PhasedXPowGate(phase_exponent=0.1, exponent=0.2)(q[0]))
1803+
c3_tc = tc.Circuit.from_cirq(c3)
1804+
u_cirq3 = cirq.unitary(c3)
1805+
u_tc3 = c3_tc.matrix()
1806+
assert_allclose_up_to_global_phase(u_cirq3, tc.backend.numpy(u_tc3), atol=1e-5)
1807+
1808+
# Test random parameter ISWAP
1809+
c4 = cirq.Circuit()
1810+
c4.append(cirq.ISwapPowGate(exponent=0.3)(q[0], q[1]))
1811+
c4_tc = tc.Circuit.from_cirq(c4)
1812+
u_cirq4 = cirq.unitary(c4)
1813+
u_tc4 = c4_tc.matrix()
1814+
assert_allclose_up_to_global_phase(u_cirq4, tc.backend.numpy(u_tc4), atol=1e-5)

0 commit comments

Comments
 (0)