Add a kron subcommand (tensor-concatenate two Pauli strings)
Background
qsym-rs is a deliberately small symbolic Pauli-algebra engine and qsym CLI — the worked example
in QudeLeap's beginner-training "how to develop" lab (see docs/design/qsym-rs.md for the full
roadmap this issue is one part of). Its engine (crate qsym_rs, src/lib.rs) represents a "Pauli
string" as PauliString { ops: Vec<Pauli>, phase: u8 }, one Pauli (I/X/Y/Z) per qubit,
with phase the exponent k in i^k (0..=3, printed as +/+i/-/-i).
The engine's existing mul and the commutes predicate both require equal-length operands
(they assert it), because multiplying or comparing Pauli strings is site-by-site. But a common
operation on independent subsystems is the tensor product (⊗): laying two Pauli strings over
disjoint qubits, side by side, into one longer string over the combined register. For example,
an X on qubit 0 tensored with ZZ on qubits 1–2 is the 3-qubit operator XZZ. Unlike mul,
tensoring must accept operands of different lengths — that is the whole point. There is no
engine method or CLI surface for it today.
Task
Two additive changes, mirroring the existing commute/mul patterns:
- Engine (
src/lib.rs): add pub fn kron(&self, other: &PauliString) -> PauliString that
concatenates the operators (self.ops followed by other.ops) and combines the phase as
(self.phase + other.phase) % 4 — the same phase-composition rule mul already uses. Crucially,
kron must not assert or require equal lengths (it concatenates; it does not multiply
site-by-site). Do not change mul, commutes, parse, or the field layout.
- CLI (
src/main.rs): add a Kron { a: String, b: String } variant to Cmd and a match arm.
Parse both operands the same way the other subcommands do (a bad Pauli letter still exits
non-zero with an error: ... message), but — unlike mul/commute — do not reject a length
mismatch; pass both to kron and print the result via Display. (Concretely: call
PauliString::parse on each operand directly rather than going through the shared parse_pair
guard, which rejects unequal lengths.)
Verification
Run each command with cargo run -q --, trimming trailing newlines before comparing stdout:
cargo run -q -- kron X ZZ → stdout is exactly +XZZ, exit code 0. (Length-1 tensored with
length-2 gives a length-3 string; both operands parse with phase 0, so the combined phase is
0 → +.)
cargo run -q -- kron IY XZ → stdout is exactly +IYXZ, exit code 0 (a length-2 ⊗ length-2
concatenation, in order).
- Negative control (the whole point of
kron): cargo run -q -- kron X ZZ succeeds where
the equal-length commands reject the same shapes — confirm cargo run -q -- mul X ZZ exits
non-zero with error: length mismatch, while kron X ZZ exits 0. If your kron reused
parse_pair's equal-length guard (or the engine's assert_eq!), it would reject or panic here
instead of returning +XZZ.
- Engine unit test (phase composition, which the CLI can't reach —
parse always yields phase
0): add a #[test] to tests/engine.rs asserting that
PauliString { ops: vec![Pauli::X], phase: 1 }.kron(&PauliString { ops: vec![Pauli::Z], phase: 2 })
displays as -iXZ (ops = [X, Z], phase (1 + 2) % 4 = 3 → -i). Run cargo test / make check → the new test passes alongside the existing suite.
- Regression check:
cargo run -q -- mul XZ ZX still prints exactly +YY — confirms this
addition is purely additive and the existing Mul arm and parse_pair guard are untouched.
Checks 1+2 require the real concatenation and order; check 3 requires kron to not reuse the
equal-length guard (a stub that did would fail it); check 4 requires the phase actually to combine,
not be hardcoded to 0.
Add a
kronsubcommand (tensor-concatenate two Pauli strings)Background
qsym-rs is a deliberately small symbolic Pauli-algebra engine and
qsymCLI — the worked examplein QudeLeap's beginner-training "how to develop" lab (see
docs/design/qsym-rs.mdfor the fullroadmap this issue is one part of). Its engine (crate
qsym_rs,src/lib.rs) represents a "Paulistring" as
PauliString { ops: Vec<Pauli>, phase: u8 }, onePauli(I/X/Y/Z) per qubit,with
phasethe exponentkini^k(0..=3, printed as+/+i/-/-i).The engine's existing
muland thecommutespredicate both require equal-length operands(they assert it), because multiplying or comparing Pauli strings is site-by-site. But a common
operation on independent subsystems is the tensor product (
⊗): laying two Pauli strings overdisjoint qubits, side by side, into one longer string over the combined register. For example,
an
Xon qubit 0 tensored withZZon qubits 1–2 is the 3-qubit operatorXZZ. Unlikemul,tensoring must accept operands of different lengths — that is the whole point. There is no
engine method or CLI surface for it today.
Task
Two additive changes, mirroring the existing
commute/mulpatterns:src/lib.rs): addpub fn kron(&self, other: &PauliString) -> PauliStringthatconcatenates the operators (
self.opsfollowed byother.ops) and combines the phase as(self.phase + other.phase) % 4— the same phase-composition rulemulalready uses. Crucially,kronmust not assert or require equal lengths (it concatenates; it does not multiplysite-by-site). Do not change
mul,commutes,parse, or the field layout.src/main.rs): add aKron { a: String, b: String }variant toCmdand a match arm.Parse both operands the same way the other subcommands do (a bad Pauli letter still exits
non-zero with an
error: ...message), but — unlikemul/commute— do not reject a lengthmismatch; pass both to
kronand print the result viaDisplay. (Concretely: callPauliString::parseon each operand directly rather than going through the sharedparse_pairguard, which rejects unequal lengths.)
Verification
Run each command with
cargo run -q --, trimming trailing newlines before comparing stdout:cargo run -q -- kron X ZZ→ stdout is exactly+XZZ, exit code0. (Length-1 tensored withlength-2 gives a length-3 string; both operands parse with phase
0, so the combined phase is0→+.)cargo run -q -- kron IY XZ→ stdout is exactly+IYXZ, exit code0(a length-2 ⊗ length-2concatenation, in order).
kron):cargo run -q -- kron X ZZsucceeds wherethe equal-length commands reject the same shapes — confirm
cargo run -q -- mul X ZZexitsnon-zero with
error: length mismatch, whilekron X ZZexits0. If yourkronreusedparse_pair's equal-length guard (or the engine'sassert_eq!), it would reject or panic hereinstead of returning
+XZZ.parsealways yields phase0): add a#[test]totests/engine.rsasserting thatPauliString { ops: vec![Pauli::X], phase: 1 }.kron(&PauliString { ops: vec![Pauli::Z], phase: 2 })displays as
-iXZ(ops=[X, Z], phase(1 + 2) % 4 = 3→-i). Runcargo test/make check→ the new test passes alongside the existing suite.cargo run -q -- mul XZ ZXstill prints exactly+YY— confirms thisaddition is purely additive and the existing
Mularm andparse_pairguard are untouched.Checks 1+2 require the real concatenation and order; check 3 requires
kronto not reuse theequal-length guard (a stub that did would fail it); check 4 requires the phase actually to combine,
not be hardcoded to
0.