Getting Started

This guide will walk you through installing ProPauli and running your first simulation.

Installation

To use ProPauli, you will need a C++23 compatible compiler (Clang is preferred) and CMake 3.14 or later.

You can build the library from source with the following commands:

git clone https://github.com/zefresk/ProPauli.git
cd ProPauli
cmake -S . -B build -D CMAKE_BUILD_TYPE=Release
cmake --build build --config Release

This will build the ProPauli library, which you can then link against in your own projects.

Quickstart: Your First Simulation

Let’s simulate a simple 8-qubit quantum circuit. This circuit first applies a Hadamard gate to every qubit to create a superposition, then entangles them with a chain of CNOT gates. Finally, we will calculate the expectation value of the \(Z^{\otimes 8}\) observable.

	// 1. Initialize an 8-qubit circuit
	Circuit qc{ 8 };

	// 2. Build the quantum circuit
	// Apply a Hadamard gate to every qubit
	for (unsigned i = 0; i < 8; ++i) {
		qc.add_operation("H", i);
	}
	// Create a chain of CNOT gates
	for (unsigned i = 0; i < 7; ++i) {
		qc.add_operation("CX", i, i + 1);
	}

	// 3. Define the observable to measure
	// We'll measure the observable ZZZZZZZZ
	std::string z_obs_str(8, 'Z');
	Observable obs{ z_obs_str };

	// 4. Run the simulation
	auto final_obs = qc.run(obs);

	// 5. Get the expectation value
	auto exp_val = final_obs.expectation_value();
	std::cout << "Expectation value of " << z_obs_str << ": " << exp_val << std::endl;

This example demonstrates the complete workflow: initializing a Circuit, adding operations, defining an Observable, running the simulation, and retrieving the final expectation value.

Examples from the README

Here are the examples from the README, providing a quick overview of the library’s main features.

Basic Circuit

A simple 2-qubit circuit calculating the expectation value of the “ZZ” observable.

	Circuit qc{ 2 };
	qc.add_operation("H", 0);
	qc.add_operation("Rz", 0, 1.57f);
	qc.add_operation("CX", 0u, 1u);

	auto result = qc.run(Observable{ "ZZ" });
	std::cout << "Expectation value: " << result.expectation_value() << std::endl;

Large Circuit with Truncation

This demonstrates a 64-qubit circuit that uses truncators to manage the complexity of the simulation. Terms with very small coefficients or a high number of non-identity Pauli operators (high Pauli weight) are removed.

	Circuit qc{ 64,
		    combine_truncators(CoefficientTruncator<>{ 0.001f }, // remove terms with coefficient below 0.001
				       WeightTruncator<>{ 6 } // remove terms with pauli weight > 6
				       ) };

	// Apply a layer of Hadamard gates
	for (unsigned i = 0; i < 64; ++i)
		qc.add_operation("H", i);

	// Entangling layer
	for (unsigned i = 0; i < 63; ++i) {
		qc.add_operation("CX", i, i + 1);
	}

	auto result = qc.run(Observable{ std::string(64, 'Z') });
	std::cout << "Expectation value: " << result.expectation_value() << std::endl;

Custom Truncator

Users can define their own truncation logic. The easiest way is by providing a lambda to the PredicateTruncator.

	// A custom truncator that removes Pauli terms with a specific weight
	auto predicate = [](const auto& pt) { return pt.pauli_weight() == 2; };
	Circuit qc{ 4, std::make_shared<PredicateTruncator<decltype(predicate)>>(predicate) };

	qc.add_operation("H", 0);
	qc.add_operation("H", 1);
	qc.add_operation("Rz", 0, 1.57f);
	qc.add_operation("CX", 0u, 1u);

	auto result = qc.run(Observable{ "XXXX" });
	std::cout << "Expectation value: " << result.expectation_value() << std::endl;

Noise Model

This example shows how to define a simple noise model where 1% amplitude damping noise is applied after every CNOT gate.

	NoiseModel<coeff_t> nm;
	nm.add_amplitude_damping_on_gate(QGate::Cx, 0.01);

	Circuit qc{ 4, std::make_shared<NeverTruncator<>>(), nm };

	qc.add_operation("H", 0);
	qc.add_operation("Rz", 0, 1.57f);
	qc.add_operation("CX", 0, 1);
	qc.add_operation("CX", 2, 3);

	auto result = qc.run(Observable{ "ZZZZ" });
	std::cout << "Expectation value: " << result.expectation_value() << std::endl;