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;