Analog

The qilisdk.analog layer lets you build symbolic Hamiltonians and time-dependent schedules that can be simulated or submitted to Qili backends. Its core types are Hamiltonian, Schedule, and LinearSchedule.

  • Hamiltonian: Tools to build arbitrary Hamiltonians using symbolic Pauli operators.

  • Schedule: A system for defining time-dependent evolution of Hamiltonians.

Quick Start

 from qilisdk.analog import Hamiltonian, LinearSchedule, X, Z

 driver = sum(X(i) for i in range(2))
problem = Z(0) * Z(1)

 schedule = LinearSchedule(
     T=10,
     dt=1,
     hamiltonians={"driver": driver, "problem": problem},
     schedule={
         0: {"driver": 1.0, "problem": 0.0},
         10: {"driver": 0.0, "problem": 1.0},
     },
 )

Hamiltonian

The Hamiltonian class represents a symbolic Hamiltonian as a sum of weighted Pauli operators. You can create Hamiltonians using the built-in Pauli operators and combine them with standard arithmetic operations.

Common constructors

  • X(i), Y(i), Z(i), I(i) produces a Hamiltonian with a single-qubit Pauli.

  • Construction using arithmetic operations. The operations follow Python syntax, for example: 2 * Z(0) + Z(1) and Z(0) * Z(1) build multi-qubit Hamiltonian.

Pauli operators:

  • X(i) - Pauli X acting on qubit i

  • Y(i) - Pauli Y acting on qubit i

  • Z(i) - Pauli Z acting on qubit i

  • I(i) - Identity acting on qubit i

Arithmetic operations:

  • Addition: H1 + H2

  • Scalar multiplication: 5 * H

  • multiplication: H0 * H1

  • Subtraction: H1 - H2

  • Division by scalar: H / 5

  • Negation: -H

** Extra Symbolic Operators**:

  • commutator: H1.commutator(H2)

  • anticommutator: H1.anticommutator(H2)

  • vector_norm: H.vector_norm()

  • forbenius_norm: H.frobenius_norm()

  • trace: H.trace()

** Exporting Hamiltonians **:

  • to matrix: H.to_matrix(nqubits)

  • to qtensor: H.to_qtensor(nqubits)

** Importing Hamiltonians **:

  • from qtensor: Hamiltonian.from_qtensor(qtensor)

  • from string: Hamiltonian.parse(hamiltonian_string)

Example: Ising Hamiltonian

To define an Ising Hamiltonian of the form:

\[H_{\text{Ising}} = - \sum_{\langle i, j \rangle} J_{ij} \sigma^Z_i \sigma^Z_j - \sum_j h_j \sigma^Z_j\]

you can use the Pauli Z operators from the library:

from qilisdk.analog import Z

nqubits = 3
J = {(0, 1): 1, (0, 2): 2, (1, 2): 4}
h = {0: 1, 1: 2, 2: 3}

coupling = sum(weight * Z(i) * Z(j) for (i, j), weight in J.items())
fields = sum(weight * Z(i) for i, weight in h.items())

H = -(coupling + fields)
print(H)

Output:

- Z(0) Z(1) - 2 Z(0) Z(2) - 4 Z(1) Z(2) - Z(0) - 2 Z(1) - 3 Z(2)

Schedule

The Schedule class maps time steps to Hamiltonian coefficients.

  • T (float): total duration of the schedule in units of nano seconds.

  • dt (int): resolution of time steps in units of nano seconds. Default is 1.

  • hamiltonians (dict[str, Hamiltonian]): Map of labels to Hamiltonian instances.

  • schedule_map (dict[int, dict[str, float]]): time-step index to Hamiltonian coefficient mapping. Each key is a time step index (0 to T/dt), and each value is a dictionary mapping Hamiltonian labels to their coefficients at that time step index.

Example 1: Dictionary-Based Schedule

import numpy as np
from qilisdk.analog import Schedule, X, Z

T, dt = 10, 1
steps = np.linspace(0, T, int(T / dt))

h1 = X(0) + X(1) + X(2)
h2 = -Z(0) - Z(1) - 2 * Z(2) + 3 * Z(0) * Z(1)

schedule = Schedule(
    T=T,
    dt=dt,
    hamiltonians={"driver": h1, "problem": h2},
    schedule={
        i: {"driver": 1 - t / T, "problem": t / T}
        for i, t in enumerate(steps)
    },
)
schedule.draw()

Example 2: Functional Schedule with add_hamiltonian()

Alternatively, You can add Hamiltonians one at a time, supplying a callable for the coefficient:

T, dt = 10, 1
steps = np.linspace(0, T, int(T / dt))

h1 = X(0) + X(1) + X(2)
h2 = -Z(0) - Z(1) - 2 * Z(2) + 3 * Z(0) * Z(1)
schedule = Schedule(T, dt)

# Add h1 with a time‐dependent coefficient function
schedule.add_hamiltonian(
    label="driver",
    hamiltonian=h1,
    schedule=lambda t: 1 - steps[t] / T
)

# Add h2 similarly
schedule.add_hamiltonian(
    label="problem",
    hamiltonian=h2,
    schedule=lambda t: steps[t] / T
)
schedule.draw()

This provides more flexibility and modularity for dynamic or conditional evolution.

Note: if you add a Hamiltonian with the same label multiple times, the last one will overwrite the previous ones.

Modifying a Schedule

Once constructed, you can refine or extend the schedule:

Add a new time step:

schedule.add_schedule_step(time_step=11, hamiltonian_coefficient_list={"h1": 0.3})

Update an existing coefficient:

schedule.update_hamiltonian_coefficient_at_time_step(
    time_step=1,
    hamiltonian_label="h1",
    new_coefficient=0.2
)

This lets you insert or override coefficients without rebuilding the full map.

Parameterized Schedules

Schedule coefficients can be symbolic, enabling classical optimization loops or experiments that scan over a family of time profiles. Coefficients can be instances of Parameter or algebraic expressions (Term) built from parameters. The schedule tracks every parameter it encounters so you can query or set them later.

from qilisdk.analog import Schedule, Z
from qilisdk.common.variables import Parameter
T, dt = 10, 1
gamma = Parameter("gamma", value=0.5, bounds=(0.0, 1.0))
schedule = Schedule(
    T=T,
    dt=dt,
    hamiltonians={"problem": Z(0)},
    schedule={0: {"problem": gamma}},
)
schedule.set_parameter_bounds({"gamma": (0.2, 0.8)})
schedule.set_parameter_values([0.7])
print(schedule.get_parameter_values())  # [0.7]

When coefficients arise from expressions (for example 0.5 * gamma or gamma * (1 - t / T) inside add_hamiltonian()), the resulting Term is also recorded. The helpers below give programmatic access to these symbolic coefficients: - get_parameter_names() and get_parameters() surface labels and values. - set_parameters({"gamma": 0.6}) updates selected entries in-place. - get_parameter_bounds() returns the per-parameter bounds dictionary.

LinearSchedule

The LinearSchedule subclass provides linear interpolation between explicitly defined time steps. You supply the same inputs as Schedule, and the class fills in intermediate coefficients on demand.

from qilisdk.analog import LinearSchedule, X, Z

gamma = Parameter("gamma", value=0.1, bounds=(0.0, 1.0))

linear = LinearSchedule(
    T=10,
    dt=1,
    hamiltonians={
        "driver": X(0) + X(1),
        "problem": Z(0) * Z(1),
    },
    schedule={
        0: {"driver": 1.0, "problem": 0.0},
        9: {"driver": gamma, "problem": 1.0},
    },
)

mid = linear[5]
print("Midpoint schedule:", mid)
coeff = linear.get_coefficient(time_step=5, hamiltonian_key="driver")
print("Driver coefficient at midpoint (evaluated parameters):", coeff)
coeff = linear.get_coefficient_expression(time_step=5, hamiltonian_key="driver")
print("Driver coefficient at midpoint (unevaluated parameters):", coeff)

Output:

Midpoint schedule: 0.5 X(0) + 0.5 X(1) + 0.5555555555555556 Z(0) Z(1)
Driver coefficient at midpoint (evaluated parameters): 0.5
Driver coefficient at midpoint (unevaluated parameters): (0.4444444444444444) + (0.5555555555555556) * gamma

LinearSchedule preserves symbolic parameters when computing interpolation results. Use get_coefficient_expression() to obtain the unevaluated expression (with parameters intact) or get_coefficient() to retrieve the numeric value under the current parameter assignment. This makes it convenient to visualize or export schedules with parametric sweeps while reusing the same workflows as Schedule.