Skip to content

v1.0.0: make Qubit.NET installable, correct, fast and Unity-ready - #3

Merged
InfoTCube merged 9 commits into
mainfrom
polish-v1
Aug 17, 2026
Merged

v1.0.0: make Qubit.NET installable, correct, fast and Unity-ready#3
InfoTCube merged 9 commits into
mainfrom
polish-v1

Conversation

@InfoTCube

Copy link
Copy Markdown
Owner

Prepares the first packaged release. The library previously could not be consumed by
anyone: it was not on NuGet, targeted net8.0 only so Unity could not import it, had no
tests, and carried several correctness bugs.

Breaking release, so read the CHANGELOG before merging.

Bugs fixed

Bug Impact
IsUnitary compared floats exactly qc.Custom(hadamard, 0) threw "not unitary" — the first thing anyone tries
Simulator.Run mutated its input Running the same circuit twice gave wrong results the second time
Copy constructor shared state Gates applied to a copy leaked into the original
Initialize accepted unnormalized states Probabilities summed to more than 1; the README's own example was broken
Toffoli drew its second control as a target Wrong diagrams
Qubit limit documented as 30 Could never allocate — 2^30 Complex is 17 GB against a 2 GB array cap. Now a real 26
GetStringResult threw on empty counts Crash

Packaging

  • Multi-targets netstandard2.1 (Unity 2021.2+), net8.0 and net10.0 from one package
  • Full metadata, XML docs, icon, Source Link, symbol package
  • CI on every push; tag-driven release using NuGet trusted publishing (OIDC, no stored key)
  • ToDiagram() and QuantumGates.Format() return strings, so rendering works without a console

Performance

Gates apply in place. A 20-qubit GHZ chain went from 83 ms and 328 MB allocated to 34 ms
and 16 MB
— one state vector instead of one per gate. Build warnings: 170 → 0.

New

  • Classical bits and feedforward: MeasureInto, When, ClassicalBit
  • MeasurementResult replacing the (int[], int) tuple
  • OpenQASM 2.0 export, so circuits run on real hardware via Qiskit
  • Algorithms: Grover, Deutsch–Jozsa, Bernstein–Vazirani, teleportation, superdense coding, QFT
  • BlochVector, QubitProbability, ToHistogram
  • Reset()

Tests

0 → 149, covering gate algebra, textbook states, measurement statistics, the drawer,
QASM output, and one regression test per bug above. Teleportation is verified across five
input states and twenty measurement branches.

Known limitation

Grover's diffusion operator supports up to 3 qubits and throws NotSupportedException
beyond that. Lifting it needs ancilla-based multi-controlled decomposition — worth doing
before someone's first experiment is a 4-qubit search.

Not yet verified

The netstandard2.1 build compiles, which guarantees no net8+-only API leaked, but it
has not been run inside Unity. Worth confirming before leaning on the Unity pitch
publicly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r

InfoTCube and others added 9 commits August 17, 2026 20:03
- IsUnitary compared floats with exact equality, so any gate built from
  1/sqrt(2) was rejected. qc.Custom(hadamard, 0) threw "not unitary" —
  the first thing anyone tries with Custom(). Now compares with tolerance.
- Simulator.Run mutated the caller's gate list via Gates.RemoveAt(0), so a
  second Run(qc, shots) replayed a circuit stripped of its own gates and
  reported 100% |0..0>. Now works on a local copy.
- The copy constructor documented a deep copy but assigned references, so
  gates applied to the copy mutated the original. Now clones.
- Initialize accepted unnormalized states silently, making GetProbabilities
  return probabilities summing to >1. Now validates |a|^2 + |b|^2 == 1.
- Toffoli rendered its second control as a target marker in circuit diagrams.
- Qubit limit was documented as 30, which cannot allocate (2^30 Complex =
  17 GB, and the CLR caps one array at 2 GB). Lowered to a real 26.
- GetStringResult threw on an all-zero count array.
- Renamed ApplayGate -> ApplyGate; dropped two unused usings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
The library could not previously be consumed by anyone: the README told
people to clone the repo and copy .cs files, and the net8.0-only target
meant Unity — the flagship use case — could not import it at all.

- Multi-target netstandard2.1 (Unity 2021.2+), net8.0 and net10.0 from a
  single package; NuGet picks the right one per consumer. Replaced
  BitOperations.PopCount, which netstandard2.1 lacks, with a plain
  power-of-two test.
- Full package metadata: id, license, icon, README, tags, XML docs,
  Source Link and a symbol package.
- Added CI, plus a tag-driven workflow that packs and pushes to NuGet.
- Console-free rendering: ToDiagram() and QuantumGates.Format() return
  strings, so the drawer works under Unity, ASP.NET and tests. Draw() and
  Print() keep their console behaviour and colors by wrapping them.
- Moved Examples.Example into the public API as Circuits.BellStates, so the
  library no longer ships a namespace called Examples.
- Documented every public gate matrix and the State enum, clearing 82
  undocumented-member warnings now that XML docs are generated.
- Spelled out Enumerable.Reverse: under C# 14 first-class spans,
  array.Reverse() binds to MemoryExtensions.Reverse and returns void.
- Added a CHANGELOG.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
…e drawer

84 tests, previously zero. A simulation library that nobody can verify is a
library nobody will depend on.

- Gate algebra: every matrix is unitary, SX/SY/SZ square to their parent gate,
  H*H = I, S*S = Z, T^4 = Z, each dagger inverts its gate, Rx(2pi) = -I, and
  U3 reproduces H at the standard angles.
- Circuits: all four Bell states and GHZ match their textbook amplitudes;
  Toffoli, SWAP and the controlled gates behave on basis states.
- Measurement: with a fixed random source, measuring a Bell state yields only
  "00" or "11" across 50 seeds, collapse leaves the state normalized, and
  measuring one half of a Bell pair determines the other.
- Drawer: golden strings for alignment, plus a check that ToDiagram needs no
  console and that Draw prints exactly what ToDiagram returns.
- One regression test per bug fixed in the previous two commits.

The tests live in namespace QubitNet.Tests rather than Qubit.NET.Tests, because
the latter puts Qubit.NET.Math in scope where it shadows System.Math.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
Every gate used to allocate a fresh state vector. At 20 qubits that is 16 MB
per gate, so a GHZ chain allocated 328 MB to produce a 16 MB result — the kind
of GC pressure that makes the library unusable inside a Unity frame loop.

- Single-qubit gates apply in place: each amplitude pair is visited once from
  the member whose target bit is 0, so no scratch vector is needed.
- Controlled gates get their own in-place route. CNOT, CY, CZ, CH, the
  controlled rotations and Toffoli all reduce to a 2x2 operator applied only
  where the controls are set, extracted from the trailing block of the gate
  matrix. SWAP, Fredkin and Custom stay on the general path.
- Gate application is parallelized above 2^16 amplitudes.
- The general multi-qubit path no longer calls Array.IndexOf per amplitude,
  and skips zero amplitudes and zero coefficients.

20-qubit GHZ: 83 ms and 328 MB -> 34 ms and 16 MB.

Also collapsed the 24 copy-pasted gate methods onto two helpers, which cuts a
third of QuantumCircuit.cs without touching any signature or doc comment, and
made Gate.TargetQubits/ControlQubits non-nullable. Build warnings: 170 -> 0.

Added ControlledGateTests, which pins the new fast path against the general
matrix path for all eight controlled gates and Toffoli, and a BenchmarkDotNet
project so the README's performance numbers are measured rather than claimed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
Adds the one thing Qiskit's ClassicalRegister buys that this library could not
express: conditioning a gate on a measurement outcome. Bit layout, one result
per Measure call, and QASM's creg mapping were already covered.

- MeasureInto(qubit, classicalBit) stores an outcome; ClassicalBit(i) reads it;
  When(bit, value, body) runs the body's gates only when the bit matches. Plain
  Measure fills the classical bit matching each qubit it measured.
- Conditional gates are always recorded, so Simulator.Run re-evaluates the
  condition per shot against that shot's own outcomes.
- No separate ClassicalRegister type: it would break every call site and tax the
  common case of "measure everything, get a bitstring" for no gain here.
- Simulator.Run now returns MeasurementResult, with Counts, Shots,
  Probability(outcome) and MostFrequent, instead of an (int[], int) tuple. It
  also no longer allocates 2^n counters for a 2-qubit partial measurement.
- Added QuantumCircuit.Reset().
- The drawer lays a conditional gate out after the measurement that wrote its
  bit, and connects the two, instead of showing the correction before its cause.

Teleportation is the proof: tested across five input states and twenty
measurement branches, the message always arrives on the third qubit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
- OpenQASM 2.0 export via circuit.ToQasm(), covering every standard gate,
  measurement into creg, and conditional gates as `if (c[i]==v)`. Circuits now
  load straight into Qiskit and run on real hardware. Custom gates raise
  NotSupportedException rather than exporting something subtly wrong.
- Gates record the angles they were constructed from. Recovering them from the
  matrix afterwards is lossy around wrapping and sign, and storing them deleted
  more code than it added.
- Algorithms: Grover, Deutsch-Jozsa, Bernstein-Vazirani, teleportation,
  superdense coding, and an in-place QFT extension.
- State inspection: BlochVector(qubit) gives Bloch sphere coordinates, sitting
  at the origin for a maximally entangled qubit; QubitProbability(qubit) gives
  a single-qubit marginal; ToHistogram() draws an ASCII bar chart. These are
  what a Unity integration needs to render anything.

Grover finds its marked item with probability 1.000, and Bernstein-Vazirani
recovers the secret in one query, both verified across every input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
nuget.org now recommends OIDC-based trusted publishing over long-lived API
keys. The workflow exchanges a GitHub OIDC token for a key valid for one hour,
so there is no persistent credential to leak, and an intercepted token cannot
be replayed.

- Added id-token: write for OIDC, and contents: write, which the job no longer
  inherits once permissions are declared explicitly.
- NuGet/login@v1 runs immediately before the push so the hour-long key cannot
  expire mid-run.
- Replaced the NUGET_API_KEY secret with NUGET_USER, holding the nuget.org
  profile name.
- Documented the one-time nuget.org policy setup in the README, including that
  the workflow file name must be given without its path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
Releases are done by the maintainer only, so the publishing steps do not belong
in public docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
The release workflow triggers on any v* tag, and a tag can point at any commit
on any branch, so a tag pushed on a feature branch would have published from
it. nuget.org's policy filters cannot catch this either: a tag push carries
ref_type "tag", so a branch filter never matches, and a tag filter cannot tell
whether the tag sits on main.

Verified before anything is built or any credential is exchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhwc4XeGtzM2UQx9sNLb3r
@InfoTCube
InfoTCube merged commit 052acc3 into main Aug 17, 2026
1 check passed
@InfoTCube
InfoTCube deleted the polish-v1 branch August 17, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant