Adding time-dependent Hamiltonian evolution#431
Conversation
📊 Coverage Summary
Detailed Coverage ReportsC++ Coverage DetailsPython Coverage DetailsPybind11 Coverage Details |
… commuting pauli strings" This reverts commit b55ca4a.
…w energy_estimator
There was a problem hiding this comment.
Pull request overview
This PR adds an end-to-end workflow for time-dependent Hamiltonian evolution: building a (Trotterized) time-evolution unitary, mapping it to a Q#/QIR-backed circuit, and measuring observables via the existing EnergyEstimator infrastructure.
Changes:
- Introduces an
EvolveAndMeasuremeasure-simulation algorithm that composes optional state preparation with time evolution and measures observables. - Adds a
PauliSequenceMapperplus new Q# utilities (PauliExp,CircuitComposition) to generate executable evolution circuits. - Extends the Trotter builder with
optimize_term_orderingterm grouping/merging and adds tests around ordering + merging behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/base.py | New base abstractions/utilities for evolve-and-measure workflows (circuit composition, transpile helper, observable measurement helper). |
| python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py | New concrete evolve+measure implementation that chains piecewise evolution segments and runs measurement(s). |
| python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/init.py | Exports measure-simulation algorithms/factories. |
| python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/base.py | New evolution-circuit-mapper algorithm base + factory. |
| python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/pauli_sequence_mapper.py | New mapper that converts Pauli-product-formula evolution into a Q# Exp-based circuit/QIR. |
| python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/init.py | Exports evolution circuit mapper factory + implementations. |
| python/src/qdk_chemistry/utils/qsharp/PauliExp.qs | Adds non-controlled Pauli exponential Q# helpers (single + repeated evolution) used by the mapper. |
| python/src/qdk_chemistry/utils/qsharp/CircuitComposition.qs | Adds Q# helpers for sequentially composing two Qubit[] => Unit operations into a single circuit/op. |
| python/src/qdk_chemistry/utils/qsharp/init.py | Registers the new Q# utility files for lazy-loading via QSHARP_UTILS. |
| python/src/qdk_chemistry/data/time_evolution/containers/pauli_product_formula.py | Adds combine() to concatenate evolutions and fuse adjacent identical Pauli terms. |
| python/src/qdk_chemistry/algorithms/time_evolution/builder/trotter.py | Adds optimize_term_ordering setting and implements commuting/parallelizable grouping and duplicate merging. |
| python/tests/test_time_evolution_trotter.py | Adds tests for duplicate-term merging and optimized term ordering behavior. |
| python/tests/test_time_evolution_circuit_mapper_noncontrolled.py | Adds tests validating the non-controlled PauliSequenceMapper produces a Q# circuit of expected width. |
| python/tests/test_evolve_and_measure.py | Adds tests for state-prep composition and an example evolve+measure workflow. |
| python/pyproject.toml | Adds paulimer dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/base.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/data/time_evolution/containers/pauli_product_formula.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/data/time_evolution/containers/pauli_product_formula.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/pauli_sequence_mapper.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/builder/trotter.py
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/builder/trotter.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
python/src/qdk_chemistry/data/time_evolution/containers/pauli_product_formula.py
Outdated
Show resolved
Hide resolved
| merged: list[ExponentiatedPauliTerm] = [] | ||
| for step_terms, step_reps in ( | ||
| (self.step_terms, self.step_reps), | ||
| (other_container.step_terms, other_container.step_reps), | ||
| ): | ||
| for _ in range(step_reps): | ||
| for term in step_terms: |
There was a problem hiding this comment.
combine() expands both containers by iterating for _ in range(step_reps) and appending every term, which can blow up runtime/memory when step_reps is large (e.g., large Trotter num_divisions or long time series).
If this is expected to be used with large repetitions, consider a more efficient implementation that avoids fully unrolling repetitions (e.g., only merging at repetition boundaries and concatenating sequences, or keeping step_reps and a small boundary-merging fixup).
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/base.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/base.py
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
python/src/qdk_chemistry/algorithms/registry.py:530
- _register_python_factories() now registers EvolutionCircuitMapperFactory and MeasureSimulationFactory, so registry.show_default() will include these new algorithm types. However, _register_python_algorithms() doesn’t register any instances for those types yet, so registry.create("evolution_circuit_mapper", "pauli_sequence") / registry.create("measure_simulation", "evolve_and_measure") will fail at runtime (and will likely break registry tests that instantiate all defaults). Please add corresponding register(lambda: PauliSequenceMapper()) and register(lambda: EvolveAndMeasure()) calls (or otherwise ensure these defaults are registered).
from qdk_chemistry.algorithms.time_evolution.circuit_mapper import ( # noqa: PLC0415
EvolutionCircuitMapperFactory,
)
from qdk_chemistry.algorithms.time_evolution.controlled_circuit_mapper import ( # noqa: PLC0415
ControlledEvolutionCircuitMapperFactory,
)
from qdk_chemistry.algorithms.time_evolution.measure_simulation import MeasureSimulationFactory # noqa: PLC0415
register_factory(EnergyEstimatorFactory())
register_factory(EvolutionCircuitMapperFactory())
register_factory(MeasureSimulationFactory())
register_factory(StatePreparationFactory())
register_factory(QubitMapperFactory())
register_factory(QubitHamiltonianSolverFactory())
register_factory(TimeEvolutionBuilderFactory())
register_factory(ControlledEvolutionCircuitMapperFactory())
register_factory(CircuitExecutorFactory())
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
python/src/qdk_chemistry/algorithms/time_evolution/circuit_mapper/pauli_sequence_mapper.py
Show resolved
Hide resolved
| evo_params = { | ||
| "pauliExponents": pauli_terms, | ||
| "pauliCoefficients": angles, | ||
| "repetitions": unitary_container.step_reps, | ||
| } | ||
|
|
||
| target_indices = list(range(unitary_container.num_qubits)) | ||
|
|
||
| qsc = qsharp.circuit( | ||
| QSHARP_UTILS.PauliExp.MakeRepPauliExpCircuit, | ||
| evo_params, | ||
| target_indices, | ||
| ) | ||
|
|
||
| qir = qsharp.compile( | ||
| QSHARP_UTILS.PauliExp.MakeRepPauliExpCircuit, | ||
| evo_params, | ||
| target_indices, | ||
| ) | ||
|
|
||
| evolution_op = QSHARP_UTILS.PauliExp.MakeRepPauliExpOp(evo_params) | ||
|
|
||
| return Circuit(qsharp=qsc, qir=qir, qsharp_op=evolution_op) |
There was a problem hiding this comment.
This mapper eagerly calls qsharp.circuit() and qsharp.compile() and stores concrete Q# and QIR representations on the Circuit. Elsewhere (e.g., controlled PauliSequenceMapper) the code returns Circuit(qsharp_factory=QsharpFactoryData(...)) so compilation is lazy and consistent. Consider constructing a Q# params struct (QSHARP_UTILS.PauliExp.RepPauliExpParams(...)) and returning a Circuit with qsharp_factory/qsharp_op to avoid unnecessary compilation work and keep behavior consistent across mappers.
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/base.py
Outdated
Show resolved
Hide resolved
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Outdated
Show resolved
Hide resolved
| evolution = self._create_time_evolution(qubit_hamiltonians[0], times[0], evolution_builder) | ||
|
|
||
| for i in range(1, len(qubit_hamiltonians)): | ||
| qubit_hamiltonian = qubit_hamiltonians[i] | ||
| time = times[i] | ||
| delta_t = time - times[i - 1] | ||
|
|
||
| evolution = TimeEvolutionUnitary( | ||
| evolution.get_container().combine( | ||
| self._create_time_evolution(qubit_hamiltonian, delta_t, evolution_builder).get_container(), | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
Building the full evolution by repeatedly calling container.combine() inside the time-step loop will expand each segment’s step_reps into a flat term list (since combine returns step_reps=1). For large auto-selected Trotter step counts or many time points, this can cause significant memory/time blowups. Consider deferring concatenation (e.g., keep a list of containers/segments) or enhancing combine to merge only boundary-adjacent identical terms without fully expanding repetitions.
python/src/qdk_chemistry/data/time_evolution/containers/pauli_product_formula.py
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| evolution = self._create_time_evolution(qubit_hamiltonians[0], times[0], evolution_builder) | ||
|
|
||
| for i in range(1, len(qubit_hamiltonians)): | ||
| qubit_hamiltonian = qubit_hamiltonians[i] | ||
| time = times[i] | ||
| delta_t = time - times[i - 1] | ||
|
|
||
| evolution = TimeEvolutionUnitary( | ||
| evolution.get_container().combine( | ||
| self._create_time_evolution(qubit_hamiltonian, delta_t, evolution_builder).get_container(), | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
The evolution construction repeatedly calls evolution.get_container().combine(...) inside the time-slice loop. Since combine() walks all terms accumulated so far, building an evolution over N slices becomes O(N^2) in the number of emitted Pauli terms and can also force expansion of any internal step_reps. Consider accumulating containers (or per-slice term lists) and doing a single linear pass merge at the end, or composing per-slice circuits/ops sequentially (e.g., via CircuitComposition.MakeSequentialOp/Circuit) to avoid repeated re-walking and to preserve step_reps compression where possible.
python/src/qdk_chemistry/algorithms/time_evolution/measure_simulation/evolve_and_measure.py
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| evolution = self._create_time_evolution(qubit_hamiltonians[0], times[0], evolution_builder) | ||
|
|
||
| for i in range(1, len(qubit_hamiltonians)): | ||
| qubit_hamiltonian = qubit_hamiltonians[i] | ||
| time = times[i] | ||
| delta_t = time - times[i - 1] | ||
|
|
||
| evolution = TimeEvolutionUnitary( | ||
| evolution.get_container().combine( | ||
| self._create_time_evolution(qubit_hamiltonian, delta_t, evolution_builder).get_container(), | ||
| ) | ||
| ) | ||
|
|
||
| evolution_circuit = self._map_time_evolution_to_circuit(evolution, circuit_mapper) | ||
|
|
There was a problem hiding this comment.
Building the full evolution by repeatedly calling container.combine(...) can expand step_reps into a flat step_terms list (since combine returns step_reps=1). For long time grids or large Trotter num_divisions, this can cause the container (and Q# parameter payload) to grow very large and slow down circuit generation/compilation. Consider preserving repetition structure where possible, or mapping/compiling each segment incrementally instead of materializing one monolithic combined container.
| evolution = self._create_time_evolution(qubit_hamiltonians[0], times[0], evolution_builder) | |
| for i in range(1, len(qubit_hamiltonians)): | |
| qubit_hamiltonian = qubit_hamiltonians[i] | |
| time = times[i] | |
| delta_t = time - times[i - 1] | |
| evolution = TimeEvolutionUnitary( | |
| evolution.get_container().combine( | |
| self._create_time_evolution(qubit_hamiltonian, delta_t, evolution_builder).get_container(), | |
| ) | |
| ) | |
| evolution_circuit = self._map_time_evolution_to_circuit(evolution, circuit_mapper) | |
| evolutions: list[TimeEvolutionUnitary] = [ | |
| self._create_time_evolution(qubit_hamiltonians[0], times[0], evolution_builder) | |
| ] | |
| for i in range(1, len(qubit_hamiltonians)): | |
| qubit_hamiltonian = qubit_hamiltonians[i] | |
| time = times[i] | |
| delta_t = time - times[i - 1] | |
| evolutions.append( | |
| self._create_time_evolution(qubit_hamiltonian, delta_t, evolution_builder) | |
| ) | |
| # Map each segment evolution to a circuit and combine at the circuit level | |
| evolution_circuit: Circuit | None = None | |
| for evolution in evolutions: | |
| segment_circuit = self._map_time_evolution_to_circuit(evolution, circuit_mapper) | |
| if evolution_circuit is None: | |
| evolution_circuit = segment_circuit | |
| else: | |
| evolution_circuit = evolution_circuit.combine(segment_circuit) | |
| # evolution_circuit is guaranteed to be set because qubit_hamiltonians is non-empty |
…n' into feature/agamshayit/time_evolution
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This PR adds the ability to run time evolution experiments by mapping a time-dependent Hamiltonian (represented by a list of Hamiltonians and corresponding time coordinates) into a Trotterized unitary. The time evolution unitaryis then converted to a circuit and observables are measured using the native EnergyEstimator.
This includes the optimization of the Trotter decomposition into commuting layers. Commuting layers are simultanesously diagonalized via a Clifford circuit using Paulimer's encoding_clifford_of() function. These layers are then decomposed into parallelizable sublayers to minimize circuit depth.
Added a notebook with periodically-kicked Ising model example