Skip to content

feat(qutip): return schema-validated Datastore from QutipBackend.run()#49

Open
Roll249 wants to merge 2 commits into
OpenQuantumDesign:masterfrom
Roll249:feature/qutip-datastore-v2
Open

feat(qutip): return schema-validated Datastore from QutipBackend.run()#49
Roll249 wants to merge 2 commits into
OpenQuantumDesign:masterfrom
Roll249:feature/qutip-datastore-v2

Conversation

@Roll249

@Roll249 Roll249 commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Updates QutipBackend.run() to return an oqd_dataschema.Datastore (wrapping a TrICalEmulatorDataGroup) instead of a plain dict, addressing #44.

Breaking change: callers that unpack result["states"], result["tspan"], etc. will need to update to result["emulation"].states.data, result["emulation"].tspan.data, etc.

In exchange, the result is now:

  • Schema-validated (pydantic) at construction time
  • Directly serializable to HDF5 via model_dump_hdf5()
  • Reloadable with model_validate_hdf5() -- no glue code needed
  • Consistent with the oqd-dataschema standard used by other OQD backends

Changes

File What changed
src/oqd_trical/backend/qutip/datastore.py New -- TrICalEmulatorDataGroup (auto-registers with GroupRegistry) + build_emulator_datastore()
src/oqd_trical/backend/qutip/base.py run() now calls build_emulator_datastore() and returns a Datastore; stores _fock_cutoff for metadata
src/oqd_trical/backend/qutip/vm.py Exposes solver_name on the VM instance
src/oqd_trical/backend/qutip/__init__.py Exports TrICalEmulatorDataGroup and build_emulator_datastore
src/oqd_trical/__init__.py Exposes version via importlib.metadata
pyproject.toml Added oqd-dataschema to dependencies and uv sources
tests/test_datastore.py New -- unit tests for helper functions, group definition, attrs, and HDF5 round-trip
tests/test_backends.py New TestQutipBackendDatastore class (5 end-to-end tests, marked @pytest.mark.slow)
docs/explanation/backends.md New "Results" section documenting the Datastore API

Usage example

from oqd_trical.backend import QutipBackend

backend = QutipBackend()
experiment, hilbert_space = backend.compile(circuit, fock_cutoff=4)
datastore = backend.run(experiment, hilbert_space=hilbert_space, timestep=1e-7)

# Save
datastore.model_dump_hdf5("trical_run.h5")

# Reload
reloaded = type(datastore).model_validate_hdf5("trical_run.h5")
tspan = reloaded["emulation"].tspan.data
states = reloaded["emulation"].states.data

Upstream contribution note

Per #44, the TrICalEmulatorDataGroup should eventually be contributed upstream to oqd-dataschema so that it is the canonical group definition shared by both the QuTiP and Dynamiqs backends. This PR defines it locally to keep the PR self-contained; once the group is merged upstream, the local definition can be removed and imported from oqd_dataschema instead.

Test plan

  • All 20 unit tests pass (22 total, 2 pre-existing xfails unchanged)
  • Ruff lint clean on new/modified files
  • mkdocs build succeeds
  • HDF5 round-trip verified end-to-end (data + metadata preserved)
  • Existing QutipVM tests unchanged

Made with Cursor

Updates `QutipBackend.run()` to return an `oqd_dataschema.Datastore` (wrapping
a `TrICalEmulatorDataGroup`) instead of a plain `dict`, addressing OpenQuantumDesign#44.

Breaking change: callers that unpack result["states"], result["tspan"], etc.
will need to update to result["emulation"].states.data, result["emulation"].tspan.data, etc.

In exchange, the result is now:
- Schema-validated (pydantic) at construction time
- Directly serializable to HDF5 via model_dump_hdf5()
- Reloadable with model_validate_hdf5() -- no glue code needed
- Consistent with the oqd-dataschema standard used by other OQD backends

Changes:
- New src/oqd_trical/backend/qutip/datastore.py: TrICalEmulatorDataGroup
  (auto-registers with GroupRegistry) and build_emulator_datastore() helper.
- QutipVM now exposes solver_name on the instance for metadata use.
- QutipBackend.run() builds and returns a Datastore.
- pyproject.toml: added oqd-dataschema dependency.
- New tests/test_datastore.py: unit tests for helper functions,
  TrICalEmulatorDataGroup, and HDF5 round-trip.
- tests/test_backends.py: new TestQutipBackendDatastore covering the full
  API (return type, shapes, attrs, HDF5 round-trip, explicit fock_cutoff).
- docs/explanation/backends.md: new "Results" section documenting the
  Datastore API with usage example.

Review fixes:
- Fix states_to_array docstring to reflect actual output shapes:
  (n_tsteps, hilbert_dim, 1) for kets, (n_tsteps, hilbert_dim, hilbert_dim)
  for density matrices (QuTiP's Qobj.full() returns a column vector).
- Wrap importlib.metadata.version() calls in try/except PackageNotFoundError
  with "0+unknown" fallback to avoid import failures in source-tree contexts.
- Fix run() docstring: hilbert_space is HilbertSpace, not Dict[str, int].
- Mark TestQutipBackendDatastore with @pytest.mark.slow for CI flexibility.

Upstream note: TrICalEmulatorDataGroup should eventually be contributed to
oqd-dataschema as the canonical group for TrICal emulator outputs, enabling
a shared definition between the QuTiP and Dynamiqs backends (see OpenQuantumDesign#44).

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 3, 2026 18:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces schema-validated backend results via oqd-dataschema for the QuTiP emulator backend, including HDF5 round-tripping, tests, and documentation.

Changes:

  • Add TrICalEmulatorDataGroup and build_emulator_datastore() to wrap emulator outputs in an oqd_dataschema.Datastore.
  • Update QutipBackend.run() to return the schema-validated datastore (with metadata in attrs) and add end-to-end tests/docs for usage.
  • Add oqd-dataschema as a project dependency and expose version metadata.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/test_datastore.py Adds unit tests for datastore conversion helpers and HDF5 round-trip.
tests/test_backends.py Adds end-to-end (slow) tests asserting QutipBackend.run() returns a Datastore and round-trips to HDF5.
src/oqd_trical/backend/qutip/vm.py Records solver name and clarifies result as legacy dict view.
src/oqd_trical/backend/qutip/datastore.py Implements TrICalEmulatorDataGroup and helper conversions; builds Datastore from raw backend outputs.
src/oqd_trical/backend/qutip/base.py Changes backend run() to return Datastore, stashes fock_cutoff, and injects version metadata.
src/oqd_trical/backend/qutip/init.py Imports datastore module for group registration side effects and re-exports new APIs.
src/oqd_trical/init.py Exposes package __version__.
pyproject.toml Adds oqd-dataschema dependency and uv source.
docs/explanation/backends.md Documents the new Datastore result format and HDF5 round-trip usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +137 to +168
def run(
self,
experiment,
hilbert_space,
timestep,
*,
initial_state=None,
fock_cutoff=None,
):
"""
Runs a [`QutipExperiment`][oqd_trical.backend.qutip.interface.QutipExperiment].
Runs a [`QutipExperiment`][oqd_trical.backend.qutip.interface.QutipExperiment]
and returns the result as a schema-validated
[`Datastore`][oqd_dataschema.Datastore].

Args:
experiment (QutipExperiment): [`QutipExperiment`][oqd_trical.backend.qutip.interface.QutipExperiment] to be executed.
hilbert_space (Dict[str, int]): Hilbert space of the system.
hilbert_space (HilbertSpace): Hilbert space of the system.
timestep (float): Timestep between tracked states of the evolution.
initial_state: Optional initial state. If ``None``, every
subsystem is initialized to its ground state.
fock_cutoff: Optional override for the Fock cutoff used to
label the run metadata. Defaults to the value passed to
the most recent :meth:`compile` call.

Returns:
result (Dict[str,Any]): Result of execution of [`QutipExperiment`][oqd_trical.backend.qutip.interface.QutipExperiment].
result (Datastore): Schema-validated datastore wrapping a
single [`TrICalEmulatorDataGroup`][oqd_trical.backend.qutip.datastore.TrICalEmulatorDataGroup]
named ``"emulation"``. The group holds ``tspan``, ``states``,
``final_state`` and (optionally) ``frame`` datasets plus the
run metadata in ``attrs``. The whole datastore can be saved
to disk with :meth:`Datastore.model_dump_hdf5` and reloaded
with :meth:`Datastore.model_validate_hdf5`.
Comment on lines +63 to +76
def states_to_array(states) -> np.ndarray:
"""Stack a sequence of QuTiP ``Qobj`` states into a single complex array.

The result is shape ``(n_tsteps, hilbert_dim, 1)`` when all states are kets
(because ``Qobj.full()`` returns a column vector) and
``(n_tsteps, hilbert_dim, hilbert_dim)`` when all states are density
matrices. An empty input returns a 1-D length-zero complex array, which
is the same shape that the ``Dataset`` validator accepts.
"""
if len(states) == 0:
return np.empty((0,), dtype=np.complex128)

arrays = [np.asarray(s.full()) for s in states]
return np.stack(arrays, axis=0)
Comment on lines +129 to +132
final_state: Complex array holding the state at the end of the
evolution; 1-D ``(hilbert_dim,)`` if the solver returned kets or
2-D ``(hilbert_dim, hilbert_dim)`` for density matrices.
(For ket solvers, ``Qobj.full()`` returns ``(N, 1)``.)
Comment thread src/oqd_trical/backend/qutip/base.py Outdated
Comment on lines +15 to +17
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _pkg_version

Comment thread src/oqd_trical/backend/qutip/base.py Outdated
Comment on lines +34 to +37
try:
__version__ = _pkg_version("oqd-trical")
except PackageNotFoundError:
__version__ = "0+unknown"
Comment thread tests/test_backends.py
Comment on lines +99 to +105
return QutipBackend(
solver_options={
"progress_bar": False,
"nsteps": 1_000_000,
"max_step": 1e-9,
}
)
…n#49

1. datastore.py: Coerce input to list() at top of states_to_array()
   so generators/iterators are accepted without TypeError on len().

2. datastore.py: Fix final_state docstring -- shape is (hilbert_dim, 1)
   for ket solvers, not 1-D (hilbert_dim,). Matches actual stored output
   from np.asarray(final_state.full()).

3. base.py + __init__.py: Centralize __version__ in new _version.py to
   avoid duplication. base.py now imports from oqd_trical._version,
   avoiding the circular import that would occur from oqd_trical itself.

4. pyproject.toml: Register slow marker in pytest.ini_options so the
   @pytest.mark.slow annotation no longer produces an "unknown mark"
   warning in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@benjimaclellan

Copy link
Copy Markdown
Member

@Roll249 , thanks for the PR! It meets the specs for #44, but adds a few more file changes than I think are needed to target this issue, and didn't provide the upstream PR in the oqd-dataschema repo. To close out this unitaryHACK bounty I'll wrap up the conversation at #44.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants