Skip to content

Refactor Simulator Heirarchy + Make Hardware Requirements Clearer in Experiments.py#783

Open
jasonhan3 wants to merge 10 commits into
mainfrom
jasonh/765-refactor-simulator-task
Open

Refactor Simulator Heirarchy + Make Hardware Requirements Clearer in Experiments.py#783
jasonhan3 wants to merge 10 commits into
mainfrom
jasonh/765-refactor-simulator-task

Conversation

@jasonhan3

@jasonhan3 jasonhan3 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Addresses the first bullet point in #765.

TLDR

Refactored simulator and simulator task heirarchy to account for the physical/logical compilation paths as well as different simulation backends. With the refactor, also attempted to make hardware requirements clearer in experiments.py for the MSD experiments notebook.

Overall Design Decision

Ideally, we want to reuse as much as the existing "GeminiLogicalSimulator" and "GeminiLogicalSimulatorTask" code as possible while minimizing extra abstraction/complexity added. "GeminiLogicalSimulator" is pretty generic except for the fact that physical/logical compilation pipeline is different (compile_task function in GeminiLogicalSimulator vs. using PhysicalPipeline for physical squin kernels).

  • To address this, we ideally want the same code to be used for the Logical/Physical compilation pipelines. So, in a future PR, we could try to somehow have code that dispatches the different physical/logical compilation pipeline based on whether or not we see any "gemini.logical" statements in the dialect group? (as a proxy for applying different compilation pipelines based on the dialect/language used in the kernel)

Use cases: Physical/Logical Compilation, and Different Simulation Backends

The two big questions are:

  1. How do we handle the different compilation paths for physical/logical compilation in the refactoring?
  2. How do we support different simulation backends?

For this PR, the answers, respectively, are:

  1. We define a PhysicalSimulator and a GeminiLogicalSimulator to call the different compilation pipelines (e.g., rewrite passes) based on whether the kernel is written in physical or logical squin.
  2. We define a CliffTSimulatorTask and a TsimSimulatorTask to define the different simulation backends encoded in the task object created.
  • Because of this, the simulator classes need some way of knowing how to construct simulator tasks with different simulation backends. As a result, in this PR, we additionally define simulator classes for each simulator backend.
    - Implicit here is the question of, "How do we want the user to interface with the simulator"? We can either have the user construct different simulator classes based on the simulation backend to use, or we could just have one class that is configurable with a string that specifies the simulation backend (something like, Simulator(backend="tsim")). We choose the former in this PR, thus leading us to define more concrete classes.

NOTE: I am basically delegating the concerns in the simulator/simulatortask heirarchy as follows:

  • Simulator class: takes care of the compilation pipeline to apply
  • SimulatorTask class: takes care of the simulation backend to use

Simulator/SimulatorTask Class Heirarchy

As a result of the answers to the above questions, the design in this PR is the following:

Simulator Heirarchy

AbstractSimulator (top-level; defines the shared simulator functionalities)
PhysicalSimulator/PhysicalCliffTSimulator/GeminiLogicalSimulator/GeminiLogicalCliffTSimulator

  • We basically define a new simulator class for every combination of physical/logical compilation and the available simulation backends. This is, again, because (1) the physical/logical compilation pipeline functions called are different based on the input squin kernel, and (2) we decide to create a new simulator class for each different simulation backend.
    - Alternative design decision: alternatively, for (2), one could observe that all that we actually need is the simulator task type; the rest of the code is identical if we change simulation backends. Due to type erasure, we unfortunately can't use type parameters to construct a simulator task object of our desired type. An alternative design would be to pass in the simulation task class when constructing the simulator (which the simulator class could use), but this might be awkward in terms of usability, which is why I did not choose to implement that in this PR.

SimulatorTask Heirarchy

AbstractSimulatorTask (top-level; defines the shared simulationtask functionalities)
CliffTSimulatorTask/TsimSimulatorTask

  • Defines the implementation of the different simulation backends to use.

GeminiLogicalSimulatorTask/GeminiLogicalCliffTSimulatorTask/PhysicalSimulatorTask/PhysicalCliffTSimulatorTask

  • These are not strictly necessary and can be removed in a future PR. They are included for now because:
    - Avoids breaking changes (PhysicalSimulatorTask and GeminiLogicalSimulatorTask both currently exist)
    - Adding GeminiLogicalCliffTSimulatorTask and PhysicalCliffTSimulatorTask, although we can delete them later, is more consistent for now given that we do support PhysicalSimulatorTask and GeminiLogicalSimulatorTask (defining simulatortask's for each combination of logical/physical compilation + simulation backend).

Implementation Details

  • For the simulator task, we basically always use the MoveToSquinLogical and the LogicalNoiseModelABC to add noise based on the observation that the only difference between MoveToSquinLogical and MoveToSquinPhysical is the additional rewrite rule on move.PhysicalInitialize; we take advantage of the observation that if the physical squin kernel doesn't have the move.PhysicalInitialize statement, the behavior should be the same as MoveToSquinPhysical.
    - Thus, for the user's perspective, when they have to construct a LogicalNoiseModelABC, they can just define empty squin kernels for the noiseless/noisy squin kernels for the initialize kernels (assuming they will never have the move.PhysicalInitialize statement in their kernels)

Breaking changes

  • For PhysicalSimulator, the keyword arguments on "run", "run_async", "tsim_circuit", "visualize", "physical_squin_kernel", "physical_move_kernel", and "fidelity_bounds" won't work because the name of the kernel in AbstractSimulator is "logical_squin_kernel", not "physical_kernel".
  • PhysicalSimulatorTask does not take in the "source_squin_kernel" keyword argument for construction anymore, as it inherits from AbstractSimulatorTask which has a "logical_squin_kernel" field.
  • PhysicalSimulator takes in a LogicalNoiseModelABC instead of a NoiseModelABC. This means that to convert their NoiseModelABC to a LogicalNoiseModelABC, they can define a "get_logical_initialize" method on their noise model that returns two empty squin kernels (representing no-op kernels for the move.PhysicalInitialize rewrite).
  • GeminiLogicalSimulator() constructor IF using the 'backend=..." option breaks if arguments are passed positionally and not by keyword because the order of the arguments in the constructor for GeminiLogicalSimulator is now different.

Future Breaking Changes

  • Rename the name of the kernel in AbstractSimulator to be "squin_kernel" and have the same argument name be used for both the physical and logical simulators. This includes the "task()" method; the name of the kernel argument should be "squin_kernel", and the same name should be used in the subclasses of AbstractSimulator.
  • Rename the name of the source kernel in AbstractSimulatorTask to be "source_squin_kernel" instead and have that be used in all the subclasses (including for the logical simulator task).
  • Getting rid of GeminiLogicalSimulatorTask/GeminiLogicalCliffTSimulatorTask/PhysicalSimulatorTask/PhysicalCliffTSimulatorTask and only having TsimSimulatorTask and CliffTSimulatorTask

Features To Add

  • Adding "seed" functionality to run() for TsimSimulatorTask; I believe that currently, the usage of the tsim simulator doesn't support a seed.
  • In CliffTSimulatorTask, move the utility code that imports from tsim somewhere else, so that if a user uses CliffTSimulatorTask, they don't have to install tsim.

Changes to Experiments.py

  • Tried to make experiments.py more clear in terms of the functions that it needs, as a first step towards the contract of the class used for the hardware/simulator.

…o make experiments.py contract for hardware type clearer
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python/bloqade/gemini/device/abstract_simulator.py 86.11% 35 Missing ⚠️
python/bloqade/gemini/device/physical_simulator.py 85.18% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
11666 10080 86% 0% 🟢

New Files

File Coverage Status
python/bloqade/gemini/device/abstract_simulator.py 86% 🟢
TOTAL 86% 🟢

Modified Files

File Coverage Status
python/bloqade/gemini/_init_.py 100% 🟢
python/bloqade/gemini/decoding/experiments.py 98% 🟢
python/bloqade/gemini/decoding/special_tasks.py 97% 🟢
python/bloqade/gemini/decoding/tasks.py 100% 🟢
python/bloqade/gemini/device/_init_.py 100% 🟢
python/bloqade/gemini/device/physical_simulator.py 92% 🟢
python/bloqade/gemini/device/simulator.py 97% 🟢
TOTAL 98% 🟢

updated for commit: 6da64a7 by action🐍

@jasonhan3 jasonhan3 marked this pull request as ready for review July 6, 2026 03:35

@weinbe58 weinbe58 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments. In particular the property to get the physical squin kernel needs to be checked to make sure its actually OK to use the logical Move to Squin in stead of the physical version.

Comment on lines +236 to +242
from bloqade.lanes.transform import MoveToSquinLogical

return MoveToSquinLogical(
arch_spec=self.physical_arch_spec,
noise_model=self.noise_model,
add_noise=True,
).emit(self.physical_move_kernel)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this step might also need to be abstracted because the logical simulator and the physical simulator rewrites are slightly different.

@jasonhan3 jasonhan3 Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added two tests on a physical squin kernel for the noisy/noiseless rewrites here: 9a5df03

I didn't add more tests to test every single statement for the InsertGates and InsertNoise rewrite passes (I can do this if desired), but functionally, I think it should be the same because I think the only difference between MoveToSquinLogical and MoveToSquinPhysical is that "initialize_kernel" is passed into the InsertGates rewriter and "initialize_noise_kernel" is passed into the InsertNoise rewriter. However, those kernels are only used if the rewriter encounters a move.PhysicalInitialize statement, which should not exist if a user is programming in physical squin. So the functionality should remain the same.

However, there is a "con" to this, which is that if a user wants to supply their custom noise model to a PhysicalSimulator, then they have to supply a LogicalNoiseModelABC which defines a "get_logical_initialize" method. Because this method should never be used for the PhysicalSimulator, they have to basically define a kernel that takes in (theta, phi, lam, qubits) and just returns None (a no-op kernel) to satisfy pyright.

However, to get around this "con", I am wondering if we can improve the abstractions for the noise model (for example, instead of having a LogicalNoiseModelABC class, can we just rewrite move.PhysicalInitialize into gate statements, and now that those are all statements in the move IR, like move.LocalRz, move.GlobalR, etc.), we can then call the InsertNoise pass on that? So we can get rid of LogicalNoiseModelABC altogether, and just use one MoveToSquin class. (As opposed to keeping the current LogicalNoiseModelABC design and defining additional abstractions on the Logical/Physical simulator tasks). I wonder if we can reduce some abstraction added by doing something like this.

edit: If this is too much to do/consider in this PR, I can also just introduce the additional abstraction for now, and then add a deprecation warning if we ever want to simplify the internal abstractions.

Comment on lines 60 to +75
def _apply_special_tsim_circuit_strategy(
task_map: Mapping[str, _TaskT],
) -> dict[str, _TaskT]:
"""Prepend the inverse compiled unitary prefix to each task's circuits."""

transformed = dict(task_map)
for task in transformed.values():
compiled_prefix = task.tsim_circuit[
: _first_nonunitary_instruction_index(task.tsim_circuit)
]
inverse_prefix = compiled_prefix.without_noise().inverse()
special_circuits = _apply_noiseless_unitary_prefix(
{basis: task.stim_circuit for basis, task in transformed.items()}
)
special_noiseless_circuits = _apply_noiseless_unitary_prefix(
{basis: task.noiseless_stim_circuit for basis, task in transformed.items()}
)
for basis, task in transformed.items():
_clear_task_tsim_artifacts(task)
task.__dict__["tsim_circuit"] = inverse_prefix + task.tsim_circuit
task.__dict__["noiseless_tsim_circuit"] = (
inverse_prefix + task.noiseless_tsim_circuit
)
vars(task)["stim_circuit"] = special_circuits[basis]
vars(task)["noiseless_stim_circuit"] = special_noiseless_circuits[basis]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this still?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we don't need this function (_apply_special_tsim_circuit_strategy) at all in the current experiments code (as I redesigned it slightly); I kept it for "backwards compatibility" I suppose. But it's a private function so I can delete it

@jasonhan3

Copy link
Copy Markdown
Contributor Author

Maybe we can hold off on merging this for now; talking about the changes with Stefan.

@jasonhan3 jasonhan3 requested a review from Krastanov July 6, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants