Add a phase subcommand (print the phase of a Pauli product)
Background
qsym-rs is a deliberately small symbolic Pauli-algebra engine and qsym CLI (see docs/design/qsym-rs.md). A PauliString carries an overall phase: u8 — the exponent k in i^k (0..=3, surfaced as +/+i/-/-i by PauliString::sign). This phase is the central quantity of symbolic Pauli algebra: it is the i^k that makes X·Y = iZ, and it accumulates element-wise in PauliString::mul (src/lib.rs).
The CLI exposes the product's full signed form via qsym mul XY II → +iZI, but there is no script-friendly way to ask for only the phase exponent. A script that needs the phase of a product must parse the human-readable sign token out of mul's output and map +/+i/-/-i back to 0/1/2/3 itself. This is the exact gap that weight filled for the Pauli weight and len (#9) fills for the site count — a bare-integer, arithmetic-ready subcommand for a quantity the engine already computes.
Task
Add a new phase subcommand: qsym phase <A> <B>.
It takes two equal-length Pauli strings (the same shape as mul and commute), multiplies them, and prints only the resulting phase exponent as a decimal integer in 0..=3 followed by a newline. On a parse failure or a length mismatch, reuse the existing CLI error path: non-zero exit and error: ... on stderr.
This should be a CLI-only change in src/main.rs: add a Phase { a: String, b: String } variant to Cmd and a match arm that calls parse_pair(&a, &b)? (the shared length-mismatch guard), then pa.mul(&pb).phase, printing that integer. Do not change src/lib.rs.
Verification
cargo run -q -- phase XY II → stdout is exactly 1, exit code 0. (X·Y = iZ at site 0, I·I = I at site 1 → phase 1.)
- Distinguishing fixture:
cargo run -q -- phase YX ZZ → stdout is exactly 3. (Y·X = -iZ at site 0 → phase 3; a different value than example 1, so a hardcoded answer fails.)
- Second value:
cargo run -q -- phase XY YZ → stdout is exactly 2. (X·Y = iZ → phase 1 at site 0, Y·Z = iX → phase 1 at site 1; 1+1 = 2 mod 4.)
- Negative control (bad input):
cargo run -q -- phase XQZ II → exits non-zero and stderr contains error: not a Pauli letter. This proves the command parses via the normal path rather than skipping validation.
- Negative control (length mismatch):
cargo run -q -- phase XY III → exits non-zero and stderr contains error: length mismatch. This proves it reuses the parse_pair guard shared with mul/commute.
- Regression check:
cargo run -q -- mul XY II still prints exactly +iZI, and cargo run -q -- weight XIZ still prints exactly 2. No existing behavior changes.
Add a
phasesubcommand (print the phase of a Pauli product)Background
qsym-rs is a deliberately small symbolic Pauli-algebra engine and
qsymCLI (seedocs/design/qsym-rs.md). APauliStringcarries an overallphase: u8— the exponentkini^k(0..=3, surfaced as+/+i/-/-ibyPauliString::sign). This phase is the central quantity of symbolic Pauli algebra: it is thei^kthat makesX·Y = iZ, and it accumulates element-wise inPauliString::mul(src/lib.rs).The CLI exposes the product's full signed form via
qsym mul XY II→+iZI, but there is no script-friendly way to ask for only the phase exponent. A script that needs the phase of a product must parse the human-readable sign token out ofmul's output and map+/+i/-/-iback to0/1/2/3itself. This is the exact gap thatweightfilled for the Pauli weight andlen(#9) fills for the site count — a bare-integer, arithmetic-ready subcommand for a quantity the engine already computes.Task
Add a new
phasesubcommand:qsym phase <A> <B>.It takes two equal-length Pauli strings (the same shape as
mulandcommute), multiplies them, and prints only the resulting phase exponent as a decimal integer in0..=3followed by a newline. On a parse failure or a length mismatch, reuse the existing CLI error path: non-zero exit anderror: ...on stderr.This should be a CLI-only change in
src/main.rs: add aPhase { a: String, b: String }variant toCmdand a match arm that callsparse_pair(&a, &b)?(the shared length-mismatch guard), thenpa.mul(&pb).phase, printing that integer. Do not changesrc/lib.rs.Verification
cargo run -q -- phase XY II→ stdout is exactly1, exit code0. (X·Y = iZat site 0,I·I = Iat site 1 → phase 1.)cargo run -q -- phase YX ZZ→ stdout is exactly3. (Y·X = -iZat site 0 → phase 3; a different value than example 1, so a hardcoded answer fails.)cargo run -q -- phase XY YZ→ stdout is exactly2. (X·Y = iZ→ phase 1 at site 0,Y·Z = iX→ phase 1 at site 1;1+1 = 2mod 4.)cargo run -q -- phase XQZ II→ exits non-zero and stderr containserror: not a Pauli letter. This proves the command parses via the normal path rather than skipping validation.cargo run -q -- phase XY III→ exits non-zero and stderr containserror: length mismatch. This proves it reuses theparse_pairguard shared withmul/commute.cargo run -q -- mul XY IIstill prints exactly+iZI, andcargo run -q -- weight XIZstill prints exactly2. No existing behavior changes.