Skip to content
Merged
12 changes: 6 additions & 6 deletions qiskit_ibm_runtime/noise_learner_v3/converters/version_0_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ def noise_learner_v3_result_from_0_1(
NoiseLearnerV3Result.from_generators(
generators=[
QubitSparsePauliList.from_sparse_list(
[tuple(term) for term in sparse_list], datum["num_qubits"]
[tuple(term) for term in sparse_list], datum.num_qubits
)
for sparse_list in datum["generators_sparse"]
for sparse_list in datum.generators_sparse
],
rates=F64TensorModel(**datum["rates"]).to_numpy(),
rates_std=F64TensorModel(**datum["rates_std"]).to_numpy(),
metadata=datum["metadata"],
rates=datum.rates.to_numpy(),
rates_std=datum.rates_std.to_numpy(),
metadata=datum.metadata.model_dump(),
)
for datum in model["data"]
for datum in model.data
]
)
19 changes: 14 additions & 5 deletions qiskit_ibm_runtime/noise_learner_v3/noise_learner_v3_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,42 @@

from __future__ import annotations

from typing import Any
import logging

from ibm_quantum_schemas.models.noise_learner_v3.version_0_1.models import (
NoiseLearnerV3ResultsModel as NoiseLearnerV3ResultsModel_0_1,
)

from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3_result import ( # type: ignore[attr-defined]
NoiseLearnerV3Results,
)

# pylint: disable=unused-import,cyclic-import
from ..utils.result_decoder import ResultDecoder
from .converters.version_0_1 import noise_learner_v3_result_from_0_1

logger = logging.getLogger(__name__)

AVAILABLE_DECODERS = {"v0.1": noise_learner_v3_result_from_0_1}
AVAILABLE_DECODERS = {"v0.1": (noise_learner_v3_result_from_0_1, NoiseLearnerV3ResultsModel_0_1)}


class NoiseLearnerV3ResultDecoder(ResultDecoder):
"""Decoder for noise learner V3."""

@classmethod
def decode(cls, raw_result: str): # type: ignore[no-untyped-def]
def decode(cls, raw_result: str) -> NoiseLearnerV3Results: # type: ignore[no-untyped-def]
"""Decode raw json to result type."""
decoded: dict[str, str] = super().decode(raw_result)
decoded: dict[str, Any] = super().decode(raw_result)
Copy link
Collaborator

Choose a reason for hiding this comment

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

mmmh are you sure of this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is decoded:

{'schema_version': 'v0.1', 'data': [{'generators_sparse': [[['X', [0]]], [['Y', [0]]], [['Z', [0]]], [['X', [1]]], [['XX', [0, 1]]], [['YX', [0, 1]]], [['ZX', [0, 1]]], [['Y', [1]]], [['XY', [0, 1]]], [['YY', [0, 1]]], [['ZY', [0, 1]]], [['Z', [1]]], [['XZ', [0, 1]]], [['YZ', [0, 1]]], [['ZZ', [0, 1]]]], 'num_qubits': 2, 'rates': {'data': 'JCh+jLlr4z9N27+y0qT8PwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACE82jhiLec/AAAAAAAAAAAAAAAAAAAAACE82jhiLec/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJCh+jLlr4z9N27+y0qT8PwAAAAAAAAAA', 'shape': [15], 'dtype': 'f64'}, 'rates_std': {'data': 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'shape': [15], 'dtype': 'f64'}, 'metadata': {'learning_protocol': 'lindblad', 'post_selection': {'fraction_kept': {'0': 0.6299099392361112, '1': 0.6307237413194444, '2': 0.6239149305555556, '4': 0.6247829861111112, '16': 0.6248372395833334, '32': 0.626708984375}}}}, {'generators_sparse': [[['X', [0]]], [['Y', [0]]], [['Z', [0]]], [['X', [1]]], [['XX', [0, 1]]], [['YX', [0, 1]]], [['ZX', [0, 1]]], [['Y', [1]]], [['XY', [0, 1]]], [['YY', [0, 1]]], [['ZY', [0, 1]]], [['Z', [1]]], [['XZ', [0, 1]]], [['YZ', [0, 1]]], [['ZZ', [0, 1]]]], 'num_qubits': 2, 'rates': {'data': 'AAAAAAAAAABftTLhl/qpPwAAAAAAAAAAMc7fhEJEAUADPj+MEB71PwAAAAAAAAAAMc7fhEJEAUAAAAAAAAAAAAAAAAAAAAAAAz4/jBAe9T8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABftTLhl/qpPwAAAAAAAAAA', 'shape': [15], 'dtype': 'f64'}, 'rates_std': {'data': 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'shape': [15], 'dtype': 'f64'}, 'metadata': {'learning_protocol': 'lindblad', 'post_selection': {'fraction_kept': {'0': 0.6240776909722222, '1': 0.6219618055555556, '2': 0.6254340277777778, '4': 0.6245659722222222, '16': 0.6256510416666666, '32': 0.6236979166666666}}}}]}

Output of

print(type(decoded["data"]))

is:

<class 'list'>

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for checking!


try:
schema_version = decoded["schema_version"]
except KeyError:
raise ValueError("Missing schema version.")

try:
decoder = AVAILABLE_DECODERS[schema_version]
decoder, model = AVAILABLE_DECODERS[schema_version]
except KeyError:
raise ValueError(f"No decoder found for schema version {schema_version}.")

return decoder(decoded)
return decoder(model.model_validate_json(raw_result))
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import logging

from ibm_quantum_schemas.models.executor.version_0_1.models import (
QuantumProgramResultModel,
QuantumProgramResultModel as QuantumProgramResultModel_0_1,
)

# pylint: disable=unused-import,cyclic-import
Expand All @@ -26,7 +26,7 @@

logger = logging.getLogger(__name__)

AVAILABLE_DECODERS = {"v0.1": quantum_program_result_from_0_1}
AVAILABLE_DECODERS = {"v0.1": (quantum_program_result_from_0_1, QuantumProgramResultModel_0_1)}


class QuantumProgramResultDecoder(ResultDecoder):
Expand All @@ -43,8 +43,8 @@ def decode(cls, raw_result: str): # type: ignore[no-untyped-def]
raise ValueError("Missing schema version.")

try:
decoder = AVAILABLE_DECODERS[schema_version]
decoder, model = AVAILABLE_DECODERS[schema_version]
except KeyError:
raise ValueError(f"No decoder found for schema version {schema_version}.")

return decoder(QuantumProgramResultModel(**decoded))
return decoder(model.model_validate_json(raw_result))
8 changes: 7 additions & 1 deletion qiskit_ibm_runtime/utils/noise_learner_result_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@

"""NoiseLearner result decoder."""

from __future__ import annotations

from typing import TYPE_CHECKING

from .noise_learner_result import LayerError, NoiseLearnerResult, PauliLindbladError
from .result_decoder import ResultDecoder

if TYPE_CHECKING:
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3_result import NoiseLearnerV3Results


class NoiseLearnerResultDecoder(ResultDecoder):
"""Class used to decode noise learner results"""

@classmethod
def decode(cls, raw_result: str) -> NoiseLearnerResult:
def decode(cls, raw_result: str) -> NoiseLearnerResult | NoiseLearnerV3Results:
"""Convert the result to NoiseLearnerResult."""
if "schema_version" in raw_result:
# pylint: disable=import-outside-toplevel
Expand Down
2 changes: 1 addition & 1 deletion test/unit/noise_learner_v3/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_converting_results(self):
result1 = NoiseLearnerV3Result.from_generators(generators, rates, metadata=metadatum1)
results = NoiseLearnerV3Results([result0, result1])

encoded = noise_learner_v3_result_to_0_1(results).model_dump()
encoded = noise_learner_v3_result_to_0_1(results)
decoded = noise_learner_v3_result_from_0_1(encoded)
for datum_in, datum_out in zip(results.data, decoded.data):
assert datum_in._generators == datum_out._generators
Expand Down
86 changes: 86 additions & 0 deletions test/unit/noise_learner_v3/test_decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2026.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Tests the decoder for the noise learner v3 model."""

import json
import numpy as np
from qiskit.quantum_info import QubitSparsePauliList

from qiskit_ibm_runtime.noise_learner_v3.converters.version_0_1 import (
noise_learner_v3_result_to_0_1,
)
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3_decoders import (
NoiseLearnerV3ResultDecoder,
)
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3_result import ( # type: ignore[attr-defined]
NoiseLearnerV3Result,
NoiseLearnerV3Results,
)

from ...ibm_test_case import IBMTestCase


class TestDecoder(IBMTestCase):
"""Tests the decoder for the noise learner v3 model."""

def setUp(self):
super().setUp()

generators = [
QubitSparsePauliList.from_list(["IX", "XX"]),
QubitSparsePauliList.from_list(["XI"]),
]
rates = [0.1, 0.2]
rates_std = [0.01, 0.02]

metadatum0 = {
"learning_protocol": "trex",
"post_selection": {"fraction_kept": 1},
}
result0 = NoiseLearnerV3Result.from_generators(generators, rates, rates_std, metadatum0)

metadatum1 = {
"learning_protocol": "lindblad",
"post_selection": {"fraction_kept": {0: 1, 4: 1}},
}
result1 = NoiseLearnerV3Result.from_generators(generators, rates, metadata=metadatum1)
self.results = NoiseLearnerV3Results([result0, result1])

self.encoded = noise_learner_v3_result_to_0_1(self.results).model_dump_json()

def test_decoder(self):
"""Tests the decoder."""
decoded = NoiseLearnerV3ResultDecoder.decode(self.encoded)
for datum_in, datum_out in zip(self.results.data, decoded.data):
assert datum_in._generators == datum_out._generators
assert np.allclose(datum_in._rates, datum_out._rates)
assert np.allclose(datum_in._rates_std, datum_out._rates_std)
assert datum_in.metadata == datum_out.metadata

def test_no_schema_version(self):
"""Verify that an error is raised if the encoded string
does not specify any schema version."""
encoded_as_json = json.loads(self.encoded)
del encoded_as_json["schema_version"]
encoded_as_str = json.dumps(encoded_as_json)
with self.assertRaisesRegex(ValueError, "Missing schema version."):
NoiseLearnerV3ResultDecoder.decode(encoded_as_str)

def test_unknown_schema_version(self):
"""Verify that an error is raised if the schema version specified in the encoded string
does not exist."""
encoded_as_json = json.loads(self.encoded)
encoded_as_json["schema_version"] = "unknown"
encoded_as_str = json.dumps(encoded_as_json)
with self.assertRaisesRegex(ValueError, "No decoder found for schema version unknown."):
NoiseLearnerV3ResultDecoder.decode(encoded_as_str)