Guide: Symbolic Circuit Simulation
Beyond numeric simulation, the library offers powerful capabilities for symbolic circuit analysis. By using SymbolicCoefficient as the underlying data type, you can build circuits with variable parameters (e.g., rotation angles, noise probabilities). This allows you to analyze the circuit’s behavior analytically without needing to re-run the entire simulation for each new parameter value.
Creating a Symbolic Circuit
To enable symbolic computation, instantiate your Circuit and Observable with SymbolicCoefficient<T>, where T is a floating-point type like double or float.
Gate parameters that are typically numeric can be replaced with a Variable object. The simulation will then propagate these variables through the circuit, resulting in an observable whose coefficients are symbolic expressions.
// Define the symbolic coefficient type
using Symbolic = SymbolicCoefficient<double>;
// Create a circuit that works with symbolic coefficients
Circuit<Symbolic> qc{1};
// Add an Rz gate with a symbolic angle "theta"
qc.add_operation("Rz", 0, Variable("theta"));
qc.add_operation("H", 0);
// The initial observable is also symbolic
Observable<Symbolic> initial_obs({"X"});
// Running the simulation produces an observable with symbolic coefficients
auto final_obs = qc.run(initial_obs);
// The expectation value is a symbolic expression
Symbolic expectation_value = final_obs.expectation_value();
std::cout << "Symbolic Expectation Value: " << expectation_value.to_string() << std::endl;
Working with Symbolic Results
The expectation value of a symbolic observable is not a single number but a SymbolicCoefficient itself—an expression containing the variables you defined. You can manipulate this expression in several ways.
Full Evaluation
To get a final numeric result, you can perform a full evaluation by providing a value for every variable in the expression.
// Evaluate the expression by providing a value for "theta"
double result = expectation_value.evaluate({{"theta", 3.14159 / 2.0}});
std::cout << "Evaluated at theta = pi/2: " << result << std::endl;
// This will be sin(theta) -> sin(pi/2) -> 1.0
Note
Calling .evaluate() on an expression with unbound variables will throw a std::invalid_argument exception.
Simplification
As operations are applied, symbolic expressions can become unwieldy (e.g., ((2 * x) + (3 * x))). The .simplified() method performs algebraic simplification to produce a more compact and canonical form (e.g., 5 * x). This is useful for both analysis and for speeding up subsequent evaluations.
using Symbolic = SymbolicCoefficient<double>;
Variable x("x");
// Create a complex expression
Symbolic expr = (Symbolic(x) * 2.0 + 3.0) - (Symbolic(x) + 1.0);
std::cout << "Original expression: " << expr.to_string() << std::endl;
// Simplify the expression
Symbolic simplified_expr = expr.simplified();
std::cout << "Simplified expression: " << simplified_expr.to_string() << std::endl;
Partial Evaluation (Symbolic Evaluation)
In some cases, you may know the values of some parameters but want to keep others symbolic. The .symbolic_evaluate() method substitutes the known values and simplifies the resulting expression, returning a new, partially evaluated SymbolicCoefficient.
This is particularly useful when you want to pre-calculate parts of a complex expression to optimize repeated evaluations where only a subset of parameters change.
using Symbolic = SymbolicCoefficient<double>;
Variable x("x"), y("y");
Symbolic expr = cos(Symbolic(x)) + sin(Symbolic(y));
std::cout << "Original expression: " << expr.to_string() << std::endl;
// Partially evaluate by substituting a value for 'x'
Symbolic partial_expr = expr.symbolic_evaluate({{"x", 0.0}});
std::cout << "Partially evaluated (x=0): " << partial_expr.to_string() << std::endl;
// The new expression can be fully evaluated later
double final_result = partial_expr.evaluate({{"y", 3.14159 / 2.0}});
std::cout << "Final result (y=pi/2): " << final_result << std::endl;