Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion source/qdk_package/qdk/qre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@
EstimationTableEntry,
plot_estimates,
)
from ._trace import LatticeSurgery, PSSPC, TraceQuery, TraceTransform
from ._trace import (
LatticeSurgery,
PSSPC,
TraceQuery,
TraceTransform,
EvictionStrategy,
DynamicMemoryCompute,
Unmemory,
)

# Extend Rust Python types with additional Python-side functionality
from ._instruction import _isa_as_frame, _requirements_as_frame
Expand All @@ -57,6 +65,8 @@
"Block",
"Constraint",
"ConstraintBound",
"EvictionStrategy",
"DynamicMemoryCompute",
"Encoding",
"EstimationResult",
"EstimationTable",
Expand All @@ -81,6 +91,7 @@
"Trace",
"TraceQuery",
"TraceTransform",
"Unmemory",
"LOGICAL",
"PHYSICAL",
]
2 changes: 2 additions & 0 deletions source/qdk_package/qdk/qre/_qre.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@
property_name,
_float_to_bits,
_float_from_bits,
DynamicMemoryCompute,
Unmemory,
)
35 changes: 35 additions & 0 deletions source/qdk_package/qdk/qre/_qre.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,21 @@ class _ProvenanceGraph:
"""
...

def pareto_nodes(self, instruction_id: int) -> Optional[list[int]]:
"""
Return the Pareto-optimal node indices for a given instruction ID.

Requires ``build_pareto_index`` to have been called.

Args:
instruction_id (int): The instruction ID to look up.

Returns:
Optional[list[int]]: The Pareto-optimal node indices, or
None if the instruction ID has no entries.
"""
...

def total_isa_count(self) -> int:
"""
Return the total number of ISAs that can be formed from Pareto-optimal
Expand Down Expand Up @@ -1382,6 +1397,16 @@ class Trace:
"""
...

@property
def gate_counts(self) -> dict[int, int]:
"""
The counts of each gate ID in the trace.

Returns:
dict[int, int]: A dictionary mapping gate IDs to their counts.
"""
...

def estimate(
self, isa: ISA, max_error: Optional[float] = None
) -> Optional[EstimationResult]:
Expand Down Expand Up @@ -1513,6 +1538,16 @@ class LatticeSurgery:
def __new__(cls, slow_down_factor: float) -> LatticeSurgery: ...
def transform(self, trace: Trace) -> Optional[Trace]: ...

class DynamicMemoryCompute:
def __new__(
cls, compute_capacity_percentage: float, eviction_strategy: int
) -> DynamicMemoryCompute: ...
def transform(self, trace: Trace) -> Optional[Trace]: ...

class Unmemory:
def __new__(cls) -> Unmemory: ...
def transform(self, trace: Trace) -> Optional[Trace]: ...

class InstructionFrontier:
"""
Represents a Pareto frontier of instructions with space, time, and error
Expand Down
83 changes: 82 additions & 1 deletion source/qdk_package/qdk/qre/_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, KW_ONLY, field
from enum import IntEnum
from itertools import product
from types import NoneType
from typing import Any, Optional, Generator, Type, TYPE_CHECKING

if TYPE_CHECKING:
from ._application import _Context
from ._enumeration import _enumerate_instances
from ._qre import PSSPC as _PSSPC, LatticeSurgery as _LatticeSurgery, Trace
from ._qre import (
PSSPC as _PSSPC,
LatticeSurgery as _LatticeSurgery,
DynamicMemoryCompute as _DynamicMemoryCompute,
Unmemory as _Unmemory,
Trace,
)


class TraceTransform(ABC):
Expand Down Expand Up @@ -109,6 +116,80 @@ def transform(self, trace: Trace) -> Optional[Trace]:
return self._lattice_surgery.transform(trace)


class EvictionStrategy(IntEnum):
FIRST_AVAILABLE = 0
LEAST_RECENTLY_USED = 1
LEAST_FREQUENTLY_USED = 2


@dataclass
class DynamicMemoryCompute(TraceTransform):
"""Dynamic memory-compute trace transform.

Splits qubits into a limited compute area and a memory area,
inserting ``READ_FROM_MEMORY`` and ``WRITE_TO_MEMORY`` operations
as needed so that at most a fraction of the original qubits reside
in the compute area at any time.

Attributes:
compute_capacity_percentage (float): Fraction (0.0–1.0) of the
input trace's compute qubits to keep in the compute area.
Default is 0.5.
"""

_: KW_ONLY
compute_capacity_percentage: float = field(
default=0.5,
metadata={"domain": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]},
)
eviction_strategy: EvictionStrategy = field(
default=EvictionStrategy.LEAST_RECENTLY_USED,
metadata={"domain": [EvictionStrategy.LEAST_RECENTLY_USED]},
)

def __post_init__(self):
self._dynamic_memory_compute = _DynamicMemoryCompute(
self.compute_capacity_percentage, self.eviction_strategy
)

def transform(self, trace: Trace) -> Optional[Trace]:
"""Apply the dynamic memory compute transformation to a trace.

Args:
trace (Trace): The input trace.

Returns:
Optional[Trace]: The transformed trace.
"""
return self._dynamic_memory_compute.transform(trace)


@dataclass
class Unmemory(TraceTransform):
"""Unmemory trace transform.

Reverses the effect of ``DynamicMemoryCompute`` by stripping
``READ_FROM_MEMORY`` and ``WRITE_TO_MEMORY`` operations and
remapping compute-slot qubit IDs back to logical qubit IDs. The
resulting trace has no memory qubits; all qubits are compute
qubits.
"""

def __post_init__(self):
self._unmemory = _Unmemory()

def transform(self, trace: Trace) -> Optional[Trace]:
"""Apply the unmemory transformation to a trace.

Args:
trace (Trace): The input trace.

Returns:
Optional[Trace]: The transformed trace.
"""
return self._unmemory.transform(trace)


class _Node(ABC):
"""Abstract base class for trace enumeration nodes."""

Expand Down
73 changes: 73 additions & 0 deletions source/qdk_package/src/qre.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub(crate) fn register_qre_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Block>()?;
m.add_class::<PSSPC>()?;
m.add_class::<LatticeSurgery>()?;
m.add_class::<DynamicMemoryCompute>()?;
m.add_class::<Unmemory>()?;
m.add_class::<EstimationResult>()?;
m.add_class::<EstimationCollection>()?;
m.add_class::<FactoryResult>()?;
Expand Down Expand Up @@ -445,6 +447,15 @@ fn convert_encoding(encoding: u64) -> PyResult<qre::Encoding> {
}
}

fn convert_eviction_strategy(strategy: u64) -> PyResult<qre::EvictionStrategy> {
match strategy {
0 => Ok(qre::EvictionStrategy::FirstAvailable),
1 => Ok(qre::EvictionStrategy::LeastRecentlyUsed),
2 => Ok(qre::EvictionStrategy::LeastFrequentlyUsed),
_ => Err(EstimationError::new_err("Invalid eviction strategy value")),
}
}

/// Build a `qre::Instruction` from either an existing `Instruction` Python
/// object or from keyword arguments (id + encoding + arity + …).
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -696,6 +707,18 @@ impl ProvenanceGraph {
Ok(self.0.read().map_err(poisoned_lock_err)?.raw_node_count())
}

/// Returns the Pareto-optimal node indices for a given instruction ID.
///
/// Must be called after `build_pareto_index`.
pub fn pareto_nodes(&self, instruction_id: u64) -> PyResult<Option<Vec<usize>>> {
Ok(self
.0
.read()
.map_err(poisoned_lock_err)?
.pareto_nodes(instruction_id)
.map(<[usize]>::to_vec))
}

/// Computes an upper bound on the possible ISAs that can be formed from
/// this graph.
///
Expand Down Expand Up @@ -1178,6 +1201,16 @@ impl Trace {
self.0.num_gates()
}

#[expect(clippy::needless_pass_by_value)]
#[getter]
pub fn gate_counts(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyDict>> {
let dict = PyDict::new(self_.py());
for (id, count) in self_.0.gate_counts() {
dict.set_item(id, count)?;
}
Ok(dict)
}

#[pyo3(signature = (isa, max_error = None))]
pub fn estimate(&self, isa: &ISA, max_error: Option<f64>) -> Option<EstimationResult> {
self.0
Expand Down Expand Up @@ -1312,6 +1345,46 @@ impl LatticeSurgery {
}
}

#[pyclass]
pub struct DynamicMemoryCompute(qre::DynamicMemoryCompute);

#[pymethods]
impl DynamicMemoryCompute {
#[new]
pub fn new(compute_capacity_percentage: f64, eviction_strategy: u64) -> PyResult<Self> {
Ok(Self(
qre::DynamicMemoryCompute::with_percentage(compute_capacity_percentage)
.with_strategy(convert_eviction_strategy(eviction_strategy)?),
))
}

pub fn transform(&self, trace: &Trace) -> PyResult<Trace> {
self.0
.transform(&trace.0)
.map(Trace)
.map_err(|e| EstimationError::new_err(format!("{e}")))
}
}

#[derive(Default)]
#[pyclass]
pub struct Unmemory(qre::Unmemory);

#[pymethods]
impl Unmemory {
#[new]
pub fn new() -> Self {
Self::default()
}

pub fn transform(&self, trace: &Trace) -> PyResult<Trace> {
self.0
.transform(&trace.0)
.map(Trace)
.map_err(|e| EstimationError::new_err(format!("{e}")))
}
}

/// Dispatches a method call to the inner frontier variant, avoiding
/// repetitive match arms. Use as:
///
Expand Down
12 changes: 10 additions & 2 deletions source/qre/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ mod trace;
pub use trace::instruction_ids;
pub use trace::instruction_ids::instruction_name;
pub use trace::{
Block, LatticeSurgery, PSSPC, Property, Trace, TraceTransform, estimate_parallel,
estimate_with_graph,
Block, ComputeCapacity, DynamicMemoryCompute, EvictionStrategy, LatticeSurgery, PSSPC,
Property, Trace, TraceTransform, Unmemory, estimate_parallel, estimate_with_graph,
};
mod utils;
pub use utils::{binom_ppf, float_from_bits, float_to_bits};
Expand Down Expand Up @@ -64,4 +64,12 @@ pub enum Error {
#[error("unsupported instruction {} in trace transformation '{name}'", instruction_name(*id).unwrap_or(&id.to_string()))]
#[diagnostic(code("Qre.UnsupportedInstruction"))]
UnsupportedInstruction { id: u64, name: &'static str },
/// Compute capacity must be at least 1.
#[error("compute capacity must be at least 1")]
#[diagnostic(code("Qre.ZeroComputeCapacity"))]
ZeroComputeCapacity,
/// Gate arity exceeds compute capacity.
#[error("gate {} requires {arity} distinct qubits but compute capacity is {capacity}", instruction_name(*id).unwrap_or(&id.to_string()))]
#[diagnostic(code("Qre.GateArityExceedsCapacity"))]
GateArityExceedsCapacity { id: u64, arity: u64, capacity: u64 },
}
14 changes: 13 additions & 1 deletion source/qre/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ use instruction_ids::instruction_name;
mod tests;

mod transforms;
pub use transforms::{LatticeSurgery, PSSPC, TraceTransform};
pub use transforms::{
ComputeCapacity, DynamicMemoryCompute, EvictionStrategy, LatticeSurgery, PSSPC, TraceTransform,
Unmemory,
};

#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Trace {
Expand Down Expand Up @@ -227,6 +230,15 @@ impl Trace {
self.deep_iter().map(|(_, m)| m).sum()
}

#[must_use]
pub fn gate_counts(&self) -> FxHashMap<u64, u64> {
let mut counts = FxHashMap::default();
for (gate, mult) in self.deep_iter() {
*counts.entry(gate.id).or_default() += mult;
}
counts
}

pub fn runtime(&self, locked: &LockedISA) -> Result<u64, Error> {
Ok(self
.block
Expand Down
4 changes: 4 additions & 0 deletions source/qre/src/trace/transforms.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

mod dynamic_memory_compute;
mod lattice_surgery;
mod psspc;
mod unmemory;

pub use dynamic_memory_compute::{ComputeCapacity, DynamicMemoryCompute, EvictionStrategy};
pub use lattice_surgery::LatticeSurgery;
pub use psspc::PSSPC;
pub use unmemory::Unmemory;

use crate::{Error, Trace};

Expand Down
Loading
Loading