Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal

- name: Pack
run: dotnet pack src/Qubit.NET/Qubit.NET.csproj --no-build --configuration Release --output ./artifacts

- name: Upload package
uses: actions/upload-artifact@v4
with:
name: nupkg
path: ./artifacts/*.nupkg
70 changes: 70 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Release

on:
push:
tags: ['v*']

jobs:
publish:
runs-on: ubuntu-latest

permissions:
id-token: write # required for NuGet trusted publishing (OIDC)
contents: write # required to create the GitHub release

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Source Link needs full history

# A v* tag can be pushed pointing at any commit on any branch, which would otherwise
# publish straight from a feature branch. Refuse anything not contained in main.
- name: Verify the tag is on main
run: |
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::Tag $GITHUB_REF_NAME points at a commit that is not on main. Refusing to publish."
exit 1
fi

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Derive version from tag
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV

- name: Build
run: dotnet build --configuration Release -p:Version=${{ env.VERSION }}

- name: Test
run: dotnet test --no-build --configuration Release

- name: Pack
run: >
dotnet pack src/Qubit.NET/Qubit.NET.csproj --no-build --configuration Release
-p:Version=${{ env.VERSION }} --output ./artifacts

# Exchanges a GitHub OIDC token for a NuGet API key that lasts one hour, so no
# long-lived key is ever stored. Must run immediately before the push.
- name: NuGet login
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}

- name: Push to NuGet
run: >
dotnet nuget push "./artifacts/*.nupkg"
--api-key ${{ steps.login.outputs.NUGET_API_KEY }}
--source https://api.nuget.org/v3/index.json
--skip-duplicate

- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
files: ./artifacts/*.nupkg
generate_release_notes: true
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
.idea
.vscode
.vscode
artifacts/
bin/
obj/
BenchmarkDotNet.Artifacts/
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Changelog

All notable changes to this project are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] — unreleased

First packaged release. Qubit.NET is now installable with `dotnet add package Qubit.NET`
and usable from Unity.

### Added

- **NuGet package** with Source Link, symbol package, XML documentation and icon.
- **Multi-targeting**: `netstandard2.1` (Unity), `net8.0` and `net10.0`.
- `QuantumCircuit.ToDiagram()` — returns the ASCII circuit diagram as a string, so the
drawer works without a console (Unity, ASP.NET, tests). `Draw()` still prints in color.
- `QuantumGates.Format(matrix)` — the string-returning counterpart of `Print`.
- `QuantumCircuit.MaxQubitCount` constant.
- **Classical bits and feedforward**: `MeasureInto(qubit, classicalBit)` stores an outcome,
`ClassicalBit(i)` reads it back, and `When(classicalBit, value, body)` conditions gates on
it. Plain `Measure` fills the classical bit matching each qubit it measured. This is what
quantum teleportation, superdense coding and error correction need; there is deliberately
no separate `ClassicalRegister` type.
- `MeasurementResult`, replacing the `(int[], int)` tuple returned by `Simulator.Run`. It
exposes `Counts`, `Shots`, `Probability(outcome)` and `MostFrequent`.
- `QuantumCircuit.Reset()`, returning a circuit to |0…0⟩ with its gate list cleared.
- **OpenQASM 2.0 export** via `circuit.ToQasm()`, including `if (c[i]==v)` for conditional
gates, so circuits can be loaded into Qiskit and run on real hardware.
- **Algorithms**: `Grover`, `DeutschJozsa`, `BernsteinVazirani`, `Teleportation`,
`SuperdenseCoding` and an in-place `QFT` extension.
- **State inspection**: `BlochVector(qubit)` returns Bloch sphere coordinates (the origin
for a maximally entangled qubit), `QubitProbability(qubit)` gives a single-qubit marginal,
and `ToHistogram()` renders outcome probabilities as an ASCII bar chart.
- Continuous integration and a tag-driven NuGet release workflow.
- An xUnit test suite covering gate algebra, the textbook Bell and GHZ states, measurement
statistics and collapse, circuit rendering, and a regression test for every bug below.
- XML documentation for every public gate matrix and for the `State` enum.

### Performance

- Single-qubit and controlled gates are applied **in place**. A circuit now allocates one
state vector rather than one per gate: a 20-qubit GHZ chain went from 328 MB of
allocations to 16 MB, and from 83 ms to 34 ms.
- Gate application is parallelized above 2¹⁶ amplitudes.
- The general multi-qubit path no longer calls `Array.IndexOf` inside its per-amplitude
loop, and skips zero amplitudes and zero coefficients.
- Added a BenchmarkDotNet project under `benchmarks/`.

### Changed

- **Breaking**: `Simulator.Run` returns `IList<MeasurementResult>` rather than
`IList<(int[], int)>`. `GetStringResult()` still works, and `result.ToString()` now gives
the same text.
- **Breaking**: `Examples.Example` is now `Circuits.BellStates`.
- **Breaking**: `StateVector` may be mutated in place by gate application, so a reference
held across a gate call is no longer a snapshot. Clone it if you need one.
- The 24 near-identical gate methods now delegate to two shared helpers, cutting
`QuantumCircuit.cs` by roughly a third with no change to their signatures or docs.
- **Breaking**: the maximum qubit count is 26, down from a documented 30 that could never
actually allocate (2³⁰ `Complex` values is 17 GB, and the CLR caps a single array at 2 GB).
- **Breaking**: `Initialize` now rejects unnormalized states instead of silently producing
a non-physical one.
- **Breaking**: constructing a circuit with an invalid qubit count throws
`ArgumentOutOfRangeException` rather than `ArgumentException`/`AggregateException`.

### Fixed

- `IsUnitary` compared floating-point values for exact equality, so every matrix built
from `1/√2` was rejected — `qc.Custom(hadamard, 0)` threw "The provided matrix is not
unitary." Comparisons now use a tolerance.
- `Simulator.Run` mutated the gate list of the circuit it was given, so calling it twice
on the same circuit returned wrong results the second time.
- The copy constructor documented a deep copy but shared the gate list and modification
flags with the original, so gates applied to a copy also affected the source circuit.
- Toffoli gates rendered their second control as a target marker in circuit diagrams.
- `GetStringResult` threw an exception when every measurement count was zero.
- Renamed the internal `ApplayGate` to `ApplyGate`.
63 changes: 51 additions & 12 deletions Qubit.NET.Demo/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Qubit.NET.Gates;
using Qubit.NET.Gates;
using Qubit.NET.Simulation;

namespace Qubit.NET.Demo;
Expand All @@ -7,25 +7,64 @@ public static class Program
{
public static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;

BellState();
Teleportation();
}

private static void BellState()
{
Console.WriteLine("=== Bell state ===\n");

QuantumCircuit qc = new QuantumCircuit(2);

qc.H(0);
qc.CNOT(0, 1);

Console.WriteLine(qc.ToString());
Console.WriteLine($"State: {qc}");

qc.Measure();

QuantumGates.Print(QuantumGates.H);


qc.Measure();
qc.Draw();

var results = Simulator.Run(qc, 1000);
MeasurementResult result = Simulator.Run(qc, 1000)[0];

foreach (var result in results)
{
Console.WriteLine(result.GetStringResult());
}
Console.WriteLine($"\nCounts: {result}");
Console.WriteLine($"P(11): {result.Probability("11"):F3}");
Console.WriteLine($"Most common: {result.MostFrequent}\n");
}
}

private static void Teleportation()
{
Console.WriteLine("=== Teleportation ===\n");

// Qubit 0 carries the message, qubits 1 and 2 share a Bell pair.
QuantumCircuit qc = new QuantumCircuit(3);

qc.Ry(0, 0.8);

qc.H(1);
qc.CNOT(1, 2);

qc.CNOT(0, 1);
qc.H(0);
qc.MeasureInto(0, 0);
qc.MeasureInto(1, 1);

// Corrections conditioned on the two measurement outcomes.
qc.When(1, 1, c => c.X(2));
qc.When(0, 1, c => c.Z(2));

qc.Draw();

double expected = System.Math.Cos(0.4) * System.Math.Cos(0.4);
double actual = qc.GetProbabilities()
.Where(p => p.Key[0] == '0') // qubit 2 is the leading bit
.Sum(p => p.Value);

Console.WriteLine($"\nMeasured bits: c0={qc.ClassicalBit(0)} c1={qc.ClassicalBit(1)}");
Console.WriteLine($"P(qubit 2 = |0>): {actual:F6} (expected {expected:F6})");
}
}
55 changes: 55 additions & 0 deletions Qubit.NET.sln
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
Expand All @@ -8,26 +9,80 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qubit.NET", "src\Qubit.NET\Qubit.NET.csproj", "{F1519400-F1F2-D001-2DB1-E759C4458259}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qubit.NET.Tests", "tests\Qubit.NET.Tests\Qubit.NET.Tests.csproj", "{A1259BCF-C8A1-4D0C-922F-387222B34D44}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{66320409-64EC-F7C5-3DEF-65E7510DAAD1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qubit.NET.Benchmarks", "benchmarks\Qubit.NET.Benchmarks\Qubit.NET.Benchmarks.csproj", "{53AB8F9A-2208-400A-8C81-95240B149ECF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|x64.ActiveCfg = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|x64.Build.0 = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|x86.ActiveCfg = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Debug|x86.Build.0 = Debug|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|Any CPU.Build.0 = Release|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|x64.ActiveCfg = Release|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|x64.Build.0 = Release|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|x86.ActiveCfg = Release|Any CPU
{8028A06F-E855-BC4E-B4C3-3FCEB15E0C0D}.Release|x86.Build.0 = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|x64.ActiveCfg = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|x64.Build.0 = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|x86.ActiveCfg = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Debug|x86.Build.0 = Debug|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|Any CPU.Build.0 = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|x64.ActiveCfg = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|x64.Build.0 = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|x86.ActiveCfg = Release|Any CPU
{F1519400-F1F2-D001-2DB1-E759C4458259}.Release|x86.Build.0 = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|x64.Build.0 = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Debug|x86.Build.0 = Debug|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|Any CPU.Build.0 = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|x64.ActiveCfg = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|x64.Build.0 = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|x86.ActiveCfg = Release|Any CPU
{A1259BCF-C8A1-4D0C-922F-387222B34D44}.Release|x86.Build.0 = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|x64.ActiveCfg = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|x64.Build.0 = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|x86.ActiveCfg = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Debug|x86.Build.0 = Debug|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|x64.ActiveCfg = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|x64.Build.0 = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|x86.ActiveCfg = Release|Any CPU
{53AB8F9A-2208-400A-8C81-95240B149ECF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F1519400-F1F2-D001-2DB1-E759C4458259} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A1259BCF-C8A1-4D0C-922F-387222B34D44} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{53AB8F9A-2208-400A-8C81-95240B149ECF} = {66320409-64EC-F7C5-3DEF-65E7510DAAD1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {51965E37-13AF-4AC3-A5D2-1426ACB4DE21}
Expand Down
Loading
Loading