Skip to content

lccf-qhpc/lccfq_natsim

Repository files navigation

lccfq_natsim

lccfq_natsim is a wire-compatible, developer-local quantum simulator for the LCCFQ project. It implements the same gRPC contract as lccfq-backend, the production HPC backend, so that lccfq-lang can target either endpoint without code changes. The two backends differ in where they run (a developer's laptop vs. HPC) and in one debug extension that natsim adds for local development, but on the canonical wire they are indistinguishable to any conformant client.

The simulation engine is a dense numpy statevector over np.complex128 with little-endian qubit indexing and a PCG64 RNG. The native gate set is rx, ry, and sqiswap — the XYiSW-family gates targeted by lccfq-lang's pfaff_v1 transpiler. The marker symbols measure, reset, and nop are recognized and accepted at dispatch but treated as no-ops, consistent with a hardware target that does not support mid-circuit measurement.


Architecture

The canonical gRPC wire is owned by lccfq-backend. It exposes two services: QPUExecutor, with RPCs Ping, SubmitCircuitTask, and SubmitTestTask; and QPUExecutorExtensions, with GetResult. natsim implements both services wire-identically, so any client speaking to the real backend can be redirected to natsim by changing its [network] configuration block.

natsim additionally exposes a QPUExecutorDebug service with a single GetStatevector RPC. This service is explicitly absent from the production backend and is not part of the canonical wire; its sole purpose is to let developers inspect the simulator's internal amplitude array during development. It is gated behind the [engine].debug configuration flag: when debug=false (the default), GetStatevector returns PERMISSION_DENIED.

lccfq-lang imports its Client from lccfq-backend and connects to whichever host:port its [network] block specifies. Swapping between natsim and the real backend is a configuration change, not a code change.

Inside natsim, circuit execution follows a straightforward path. The QPUExecutorServicer receives a SubmitCircuitTask RPC, decodes the gate sequence from the protobuf, drives the QPUAbstraction FSM from IDLE to BUSY, runs the circuit through the Engine, stores the result in a ResultStore, and drives the FSM back to IDLE. The GetResult RPC on QPUExecutorExtensions retrieves the stored result by task ID.


Important limitation: in-process imports

Both lccfq_backend.api.protobufs_compiled.qpu_service_pb2 and lccfq_natsim.api.generated.qpu_service_pb2 declare messages (ExecutorRequest, Gate, GetResultResponse, and others) in the anonymous proto package. Importing both _pb2 modules into the same Python process raises:

TypeError: Couldn't build proto file into descriptor pool: duplicate symbol 'ExecutorRequest'

This is by design. natsim is a server process, not an importable library. Any test or script that drives natsim from lccfq-lang — which transitively imports lccfq-backend — must spawn natsim as a subprocess. The canonical pattern for doing this, including mTLS certificate provisioning and full round-trip assertions, is in scripts/smoke_lang_roundtrip.py.


Installation

Python 3.12 is required. Development uses uv:

git clone <repo-url> lccfq_natsim
cd lccfq_natsim
uv sync

This installs runtime and dev dependencies into .venv/. The lccfq-natsim console script lands at .venv/bin/lccfq-natsim.

If you need to consume natsim as an editable dependency from another project's environment:

uv pip install -e /path/to/lccfq_natsim

Quickstart

uv run lccfq-natsim --config config/example.toml

config/example.toml ships a 4-qubit insecure configuration: mtls=false, gRPC on 127.0.0.1:50053, Prometheus on 127.0.0.1:9090. It is suitable for local smoke testing where TLS is not required.

To see human-readable log output, add --verbose:

uv run lccfq-natsim --config config/example.toml --verbose

Without --verbose, structlog emits newline-delimited JSON. With --verbose, it switches to ConsoleRenderer — coloured, human-readable output. In both modes the servicer emits a structured log line on each submitted circuit that includes task_id, n_gates, shots, n_qubits_touched, and composition (a gate-symbol-to-count map ordered by frequency). A companion line on completion carries task_id, n_outcomes, and shots.


Configuration

Configuration is a TOML file with five sections. All sections must be present; [noise] may be empty.

[service]

Controls the gRPC server binding and optional mTLS.

Key Type Default Description
host string "127.0.0.1" Bind address
port int 50053 Bind port; 0 requests an ephemeral port
mtls bool true Enable mutual TLS
cert_path path Server certificate (required when mtls=true)
key_path path Server private key (required when mtls=true)
ca_path path CA certificate used to verify client certificates (required when mtls=true)

[metrics]

Controls the Prometheus HTTP exposition endpoint.

Key Type Default Description
enabled bool true Start the metrics HTTP server
host string "127.0.0.1" Bind address
port int 9090 Bind port; 0 requests an ephemeral port

Four counters are exposed: tasks_submitted_total, tasks_completed_total, task_duration_seconds, and queue_depth.

[engine]

Key Type Default Description
backend string "statevector" Simulator backend; currently only "statevector" is implemented
max_qubits int 24 Hard upper bound on circuit width
default_seed int RNG seed used when a circuit request supplies no seed
debug bool false Gates the QPUExecutorDebug.GetStatevector RPC; false denies all calls with PERMISSION_DENIED

[device]

Key Type Description
name string Logical device identifier reported in responses
qubit_count int Number of qubits the engine exposes (must be ≥ 1)

[noise]

Reserved. The [noise] section may be present and either empty or contain arbitrary keys; the build_noise factory yields IdealNoise regardless of content. Typed channel fields land in a future release (see Noise scaffolding under Development).


Bundled configurations

Two configs ship in config/.

config/example.toml is a 10-qubit, insecure (mtls=false) configuration intended for quick local demos that do not involve lccfq-lang. Point a gRPC client directly at 127.0.0.1:50053.

config/natsim_mtls.toml enables mTLS (mtls=true) with certificate paths under /tmp/smoke-certs/, 10 qubits, and metrics disabled. It pairs with lccfq-lang's config/natsim.toml for full lang-to-natsim round-trip demonstrations. Before using it, the cert files must exist at the configured paths.


mTLS certificate provisioning

Certificates are managed by lccfq-backend's CertificateManager. The smoke script provisions a temporary CA, server certificate, and client certificate in one call sequence:

from lccfq_backend.api.certificates.certificate_manager import CertificateManager
from pathlib import Path

cm = CertificateManager(Path("/tmp/smoke-certs"))
cm.setup_ca_and_server(hostname="localhost")
cm.create_client_certificate(user_id="dev")

After this, ca.crt, server.crt, and server.key are under /tmp/smoke-certs/, and the client certificate is under /tmp/smoke-certs/clients/. The paths in config/natsim_mtls.toml reflect this layout.


End-to-end smoke test

scripts/smoke_lang_roundtrip.py is the definitive integration test for the full stack. It:

  1. Provisions mTLS certificates in a temporary directory using CertificateManager.
  2. Writes a natsim TOML config pointing at those certificates.
  3. Writes a lccfq-lang QPU TOML config pointing at natsim.
  4. Spawns natsim as a subprocess via its console-script entry (.venv/bin/lccfq-natsim), bypassing the descriptor-pool collision described above.
  5. Waits for natsim to bind its port.
  6. Constructs a lccfq_lang.QPU instance (which issues a Ping in __init__).
  7. Submits a 2-qubit native-gate circuit: ry(π/2) on qubit 0, sqiswap on qubits 0–1, followed by a measure marker.
  8. Asserts the returned distribution sums to the requested shot count and that all keys are well-formed bitstrings.

Run it from lccfq-lang's environment (which has lccfq_lang and lccfq_backend installed):

cd /path/to/lccfq-lang
.venv/bin/python /path/to/lccfq_natsim/scripts/smoke_lang_roundtrip.py

Development

Test, lint, and type-check

uv run pytest                        # 366+ tests across unit and integration suites
uv run ruff check .
uv run ruff format .
uv run mypy src/lccfq_natsim

Regenerating gRPC stubs

The wire definition lives at src/lccfq_natsim/api/protos/qpu_service.proto. After editing the .proto, regenerate the compiled stubs:

uv run python scripts/gen_proto.py

This overwrites src/lccfq_natsim/api/generated/qpu_service_pb2.py and qpu_service_pb2_grpc.py. Hand-written type stubs at src/lccfq_natsim/api/generated/qpu_service_pb2.pyi and qpu_service_pb2_grpc.pyi must be updated manually whenever the proto changes, because grpc_tools does not generate .pyi files.

Source layout

The package lives under src/lccfq_natsim/ and is divided into eight subpackages.

api owns everything at the gRPC boundary: the .proto source, the generated _pb2 and _grpc files, and the hand-written .pyi stubs.

engine is the simulation core. It holds the Statevector and Engine classes, the gate kernel registry (kernels/), the dispatch layer that enforces the native gate set, the sampling primitive, RNG utilities, and the noise hook interface.

runtime owns the in-process task lifecycle: the QPUAbstraction FSM, the ResultStore, the task queue, and CircuitResult.

service holds the four gRPC servicers (QPUExecutorServicer, QPUExecutorExtensionsServicer, QPUExecutorDebugServicer), the asyncio server bootstrap, and the Prometheus metrics HTTP server.

config contains the Pydantic Settings model and its five sub-models, along with the Settings.from_toml factory.

obs provides structured logging via structlog (log.py) and the four Prometheus counter registrations (metrics.py).

ir defines GateInstr and TestInstr — the typed internal representations of gates that the engine consumes after proto decoding.

cli is the lccfq-natsim console-script entry point (serve.py): argument parsing, configuration loading, logging setup, and the top-level asyncio.run(serve(settings)) call.

QPUAbstraction FSM

runtime/fsm.py implements a three-state machine for the simulated QPU lifecycle. Unlike lccfq-backend's richer six-state machine (which models physical QPU degradation and accessibility), natsim models an ideal QPU with no degradation, so only three states carry behavioral meaning.

INIT --[READY]--> IDLE --[TASK_STARTED]--> BUSY --[TASK_FINISHED]--> IDLE

QPUExecutorServicer.SubmitCircuitTask checks can_transition(TASK_STARTED) before accepting a circuit. If the FSM is not IDLE — for example because a prior circuit is still executing — it returns success=False without touching state. This enforces the single-tenant, synchronous FIFO execution model.

Noise scaffolding

The noise infrastructure is in place but Kraus-channel sampling is not yet wired. The design rests on three components.

engine/noise/protocol.py defines the NoiseModel structural protocol. Any conformant implementation must provide a kraus_ops_for(symbol, params, qubits) method returning a list of np.complex128 Kraus operators satisfying trace preservation: sum_i K_i† K_i = I. Single-qubit operators have shape (2, 2); two-qubit operators have shape (4, 4). An empty list signals "no noise on this gate." The qubit ordering follows natsim's little-endian convention throughout.

engine/noise/ideal.py provides IdealNoise, which unconditionally returns [] for every gate. engine/noise/factory.py provides build_noise(NoiseConfig) -> NoiseModel, which currently always returns IdealNoise() regardless of the config content.

engine/noise/helpers.py contains stubs for depolarizing, amplitude_damping, and dephasing. Each raises NotImplementedError; they exist as anchors for the Kraus-sampling implementation that lands in a future release.

The Engine already calls kraus_ops_for after every gate dispatch. When the returned list is non-empty, it raises NotImplementedError (Kraus application is the deferred work). When it is empty — which is always the case today — execution proceeds without overhead. This design means that noise=None and noise=IdealNoise() produce byte-identical output, which is a tested invariant.

The conformance anchor for future channel implementations is tests/engine/test_noise_conformance.py::assert_kraus_conformant. Any Kraus list returned by a new noise model should pass through this helper before being merged.


License

Apache-2.0. See LICENSE at the repository root.

About

Native gate set simulator for the LCCFQ project at user hardware level

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages