ShadowSim.py is an open-source Python library with two main purposes:
- Shadow Hamiltonian research: support analysis and further research on shadow Hamiltonian simulation.
- Algorithm comparison: make it easy to compare quantum simulation algorithms for open and closed systems.
Install uv, then fork and clone the repository:
git clone https://github.com/<your-github-username>/ShadowSim.py.git
cd ShadowSim.pySync the project (creates .venv and installs runtime plus test tools from uv.lock):
uv syncpython -m pip install git+https://github.com/simsaidan/ShadowSim.py.gitThe package can then be imported as shadowsim.
uv sync
uv run pytest --cov=shadowsim --cov-config=.coveragerc --cov-report=term-missingPushes to main run the same in GitHub Actions and upload coverage to Codecov (enable the Codecov GitHub app for this repo the first time so uploads succeed).
Install the lint tools, then run Ruff check and format (CI runs the same checks):
uv sync --group lint
uv run --group lint ruff check .
uv run --group lint ruff format .
# CI equivalent of the format gate:
uv run --group lint ruff format --check .Public objects are imported from their domain subpackages:
from shadowsim.core import Hamiltonian, Operator, State
from shadowsim.shadow import ShadowHamiltonian
from shadowsim.simulators import QutipSimulatorThe following simulators are supported by the package:
| Name | Type | Can handle open systems |
|---|---|---|
| Qutip simulator | classical | true |
| Split JMatrix | quantum-inspired | true |
| Trotterization (coming soon) | — | — |
| Wave matrix Lindbladization (coming soon) | — | — |
| Pauli propagation (coming soon) | — | — |
The complete example is in examples/simple_shadow.py
and can be run after uv sync:
uv run python examples/simple_shadow.pyImagine a scenario in which you want to compare the results of a new quantum simulation algorithm against a source of truth like QuTiP.
We first define some constants used in our system:
import numpy as np
from shadowsim.benchmarking import Benchmark
from shadowsim.core import (
Hamiltonian,
LocalHamiltonian,
LocalOperator,
Operator,
State,
)
from shadowsim.simulators import (
QutipSimulator,
SplitJMatrixSimulator,
cavity_population,
population_one,
)
from shadowsim.utils import (
I,
one_state_two_qubits,
three_state_two_qubits,
tensor,
two_state_two_qubits,
zero_state_two_qubits,
)
# Define system parameters
omega_c = 245000
omega_e = 245000
kappa = np.sqrt(24.5)
gamma = np.sqrt(0.4)
g = 100
a = (
np.outer(zero_state_two_qubits, one_state_two_qubits)
+ np.sqrt(2) * np.outer(one_state_two_qubits, two_state_two_qubits)
+ np.sqrt(3) * np.outer(two_state_two_qubits, three_state_two_qubits)
)
zero = np.array([1, 0], dtype=np.complex128)
one = np.array([0, 1], dtype=np.complex128)
sigma = np.outer(zero, one)Now that the system constants are built, let's define our operators. Since this is an open-system simulation we have both Hamiltonians and Lindblad operators.
H1 = omega_c * a.conjugate().T @ a
H2 = omega_e * np.outer(one, one)
H3 = g * (np.kron(a, sigma.conjugate().T) + np.kron(a.conjugate().T, sigma))
local_hamiltonians = [
LocalHamiltonian(H1, [0, 1]),
LocalHamiltonian(H2, [2]),
LocalHamiltonian(H3, [0, 1, 2]),
]
full_hamiltonians = [
Hamiltonian(tensor([H1, I])),
Hamiltonian(tensor([I, I, H2])),
Hamiltonian(H3),
]
# Define Lindblad operators
L1_full = Operator(kappa * np.kron(a, I))
L2_full = Operator(gamma * (tensor([I, I, sigma])))
L1_local = LocalOperator(kappa * a, [0, 1])
L2_local = LocalOperator(gamma * sigma, [2])
# Define initial state
psi_0 = State(tensor([two_state_two_qubits, zero]), 3)Next, we create two simulators, one for the new algorithm and one for the source of truth.
# Qutip simulator
qutip_simulator = QutipSimulator(
full_hamiltonians,
[L1_full, L2_full],
psi_0,
[
Operator(tensor([a.conjugate().T @ a, I])),
Operator(tensor([I, I, sigma.conjugate().T @ sigma])),
],
3,
0.25,
301,
)
# Split JMatrix simulator
splitjmatrix_simulator = SplitJMatrixSimulator(
local_hamiltonians,
[L1_local, L2_local],
psi_0,
3,
0.25,
301,
40,
measurement_groups=[[1, 2], 3],
reducers=[cavity_population, population_one],
)To easily compare results, we initialize a benchmark with a reference simulator
and one or more challengers. All simulators must share the same time grid.
We run the benchmark, which runs the underlying simulations. Numeric L∞ / L2
error metrics vs the reference are available via error_metrics (and can be
exported with save_error_metrics). We can also visualize the result by calling
save_result_plot, which saves a plot per simulator and an absolute-difference
plot vs the reference for each challenger.
benchmark = Benchmark(qutip_simulator, splitjmatrix_simulator)
benchmark.run()
print(benchmark.error_metrics())
metrics_path = benchmark.save_error_metrics()
paths = benchmark.save_result_plot(labels=["cavity population", "emitter population"])
for path in [metrics_path, *paths]:
print(f"Saved: {path}")Output
QuTiP (reference):
Split JMatrix:
Absolute difference between the two:
Contributions are welcome and appreciated.
- Fork the repository and clone your fork (see the Installation section).
- Create a feature branch:
git checkout -b feature/your-change- Make your changes and keep commits focused.
- Run tests and Ruff locally (
uv run pytestand the linting commands above). - Open a pull request with a clear description of what changed and why.
For larger changes, please open an issue first to discuss scope and design.
This project is licensed under the MIT License. See LICENSE for details.


