diff --git a/src/bindings/python/requirements.txt b/src/bindings/python/requirements.txt index e83f59eb8b3..968d95b8760 100644 --- a/src/bindings/python/requirements.txt +++ b/src/bindings/python/requirements.txt @@ -1 +1,2 @@ numpy>=1.16.6 +singledispatchmethod; python_version<'3.8' diff --git a/src/bindings/python/requirements_test.txt b/src/bindings/python/requirements_test.txt index 530a28b3bf1..2bd82fb628b 100644 --- a/src/bindings/python/requirements_test.txt +++ b/src/bindings/python/requirements_test.txt @@ -40,3 +40,4 @@ types-pkg_resources wheel>=0.38.1 protobuf~=3.18.1 numpy>=1.16.6,<=1.23.4 +singledispatchmethod; python_version<'3.8' diff --git a/src/bindings/python/src/openvino/runtime/ie_api.py b/src/bindings/python/src/openvino/runtime/ie_api.py index 7bab65a0382..90099609a1a 100644 --- a/src/bindings/python/src/openvino/runtime/ie_api.py +++ b/src/bindings/python/src/openvino/runtime/ie_api.py @@ -2,7 +2,6 @@ # Copyright (C) 2018-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from functools import singledispatch from typing import Any, Iterable, Union, Dict, Optional from pathlib import Path @@ -16,6 +15,7 @@ from openvino._pyopenvino import ConstOutput from openvino._pyopenvino import Tensor from openvino.runtime.utils.data_helpers import ( + OVDict, _InferRequestWrapper, _data_dispatch, tensor_from_file, @@ -25,7 +25,7 @@ from openvino.runtime.utils.data_helpers import ( class InferRequest(_InferRequestWrapper): """InferRequest class represents infer request which can be run in asynchronous or synchronous manners.""" - def infer(self, inputs: Any = None, shared_memory: bool = False) -> dict: + def infer(self, inputs: Any = None, shared_memory: bool = False) -> OVDict: """Infers specified input(s) in synchronous mode. Blocks all methods of InferRequest while request is running. @@ -68,14 +68,14 @@ class InferRequest(_InferRequestWrapper): Default value: False :type shared_memory: bool, optional - :return: Dictionary of results from output tensors with ports as keys. - :rtype: Dict[openvino.runtime.ConstOutput, numpy.ndarray] + :return: Dictionary of results from output tensors with port/int/str keys. + :rtype: OVDict """ - return super().infer(_data_dispatch( + return OVDict(super().infer(_data_dispatch( self, inputs, is_shared=shared_memory, - )) + ))) def start_async( self, @@ -138,6 +138,15 @@ class InferRequest(_InferRequestWrapper): userdata, ) + @property + def results(self) -> OVDict: + """Gets all outputs tensors of this InferRequest. + + :return: Dictionary of results from output tensors with ports as keys. + :rtype: Dict[openvino.runtime.ConstOutput, numpy.array] + """ + return OVDict(super().results) + class CompiledModel(CompiledModelBase): """CompiledModel class. @@ -161,7 +170,7 @@ class CompiledModel(CompiledModelBase): """ return InferRequest(super().create_infer_request()) - def infer_new_request(self, inputs: Union[dict, list, tuple, Tensor, np.ndarray] = None) -> dict: + def infer_new_request(self, inputs: Union[dict, list, tuple, Tensor, np.ndarray] = None) -> OVDict: """Infers specified input(s) in synchronous mode. Blocks all methods of CompiledModel while request is running. @@ -187,8 +196,8 @@ class CompiledModel(CompiledModelBase): :param inputs: Data to be set on input tensors. :type inputs: Union[Dict[keys, values], List[values], Tuple[values], Tensor, numpy.ndarray], optional - :return: Dictionary of results from output tensors with ports as keys. - :rtype: Dict[openvino.runtime.ConstOutput, numpy.array] + :return: Dictionary of results from output tensors with port/int/str keys. + :rtype: OVDict """ # It returns wrapped python InferReqeust and then call upon # overloaded functions of InferRequest class @@ -196,7 +205,7 @@ class CompiledModel(CompiledModelBase): def __call__(self, inputs: Union[dict, list, tuple, Tensor, np.ndarray] = None, - shared_memory: bool = True) -> dict: + shared_memory: bool = True) -> OVDict: """Callable infer wrapper for CompiledModel. Infers specified input(s) in synchronous mode. @@ -248,8 +257,8 @@ class CompiledModel(CompiledModelBase): Default value: True :type shared_memory: bool, optional - :return: Dictionary of results from output tensors with ports as keys. - :rtype: Dict[openvino.runtime.ConstOutput, numpy.ndarray] + :return: Dictionary of results from output tensors with port/int/str as keys. + :rtype: OVDict """ if self._infer_request is None: self._infer_request = self.create_infer_request() diff --git a/src/bindings/python/src/openvino/runtime/utils/data_helpers/__init__.py b/src/bindings/python/src/openvino/runtime/utils/data_helpers/__init__.py index e49265ccca9..829a77af96a 100644 --- a/src/bindings/python/src/openvino/runtime/utils/data_helpers/__init__.py +++ b/src/bindings/python/src/openvino/runtime/utils/data_helpers/__init__.py @@ -5,3 +5,4 @@ from openvino.runtime.utils.data_helpers.data_dispatcher import _data_dispatch from openvino.runtime.utils.data_helpers.wrappers import tensor_from_file from openvino.runtime.utils.data_helpers.wrappers import _InferRequestWrapper +from openvino.runtime.utils.data_helpers.wrappers import OVDict diff --git a/src/bindings/python/src/openvino/runtime/utils/data_helpers/wrappers.py b/src/bindings/python/src/openvino/runtime/utils/data_helpers/wrappers.py index 24b09d40de9..e2849b8d5e0 100644 --- a/src/bindings/python/src/openvino/runtime/utils/data_helpers/wrappers.py +++ b/src/bindings/python/src/openvino/runtime/utils/data_helpers/wrappers.py @@ -4,7 +4,17 @@ import numpy as np -from openvino._pyopenvino import Tensor +# TODO: remove this WA and refactor OVDict when Python3.8 +# becomes minimal supported version. +try: + from functools import singledispatchmethod +except ImportError: + from singledispatchmethod import singledispatchmethod # type: ignore[no-redef] + +from collections.abc import Mapping +from typing import Union, Dict, List, Iterator, KeysView, ItemsView, ValuesView + +from openvino._pyopenvino import Tensor, ConstOutput from openvino._pyopenvino import InferRequest as InferRequestBase @@ -20,3 +30,109 @@ class _InferRequestWrapper(InferRequestBase): # Private memeber to store newly created shared memory data self._inputs_data = None super().__init__(other) + + +class OVDict(Mapping): + """Custom OpenVINO dictionary with inference results. + + This class is a dict-like object. It provides possibility to + address data tensors with three key types: + + * `openvino.runtime.ConstOutput` - port of the output + * `int` - index of the output + * `str` - names of the output + + This class follows `frozenset`/`tuple` concept of immutability. + It is prohibited to assign new items or edit them. + + To revert to the previous behavior use `to_dict` method which + return shallow copy of underlaying dictionary. + Note: It removes addressing feature! New dictionary keeps + only `ConstOutput` keys. + + If a tuple returns value is needed, use `to_tuple` method which + converts values to the tuple. + + :Example: + + .. code-block:: python + + # Reverts to the previous behavior of the native dict + result = request.infer(inputs).to_dict() + # or alternatively: + result = dict(request.infer(inputs)) + + .. code-block:: python + + # To dispatch outputs of multi-ouput inference: + out1, out2, out3, _ = request.infer(inputs).values() + # or alternatively: + out1, out2, out3, _ = request.infer(inputs).to_tuple() + """ + def __init__(self, _dict: Dict[ConstOutput, np.ndarray]) -> None: + self._dict = _dict + + def __iter__(self) -> Iterator: + return self._dict.__iter__() + + def __len__(self) -> int: + return len(self._dict) + + def __repr__(self) -> str: + return self._dict.__repr__() + + def __get_key(self, index: int) -> ConstOutput: + return list(self._dict.keys())[index] + + @singledispatchmethod + def __getitem_impl(self, key: Union[ConstOutput, int, str]) -> np.ndarray: + raise TypeError("Unknown key type!") + + @__getitem_impl.register + def _(self, key: ConstOutput) -> np.ndarray: + return self._dict[key] + + @__getitem_impl.register + def _(self, key: int) -> np.ndarray: + try: + return self._dict[self.__get_key(key)] + except IndexError: + raise KeyError(key) + + @__getitem_impl.register + def _(self, key: str) -> np.ndarray: + try: + return self._dict[self.__get_key(self.names().index(key))] + except ValueError: + raise KeyError(key) + + def __getitem__(self, key: Union[ConstOutput, int, str]) -> np.ndarray: + return self.__getitem_impl(key) + + def keys(self) -> KeysView[ConstOutput]: + return self._dict.keys() + + def values(self) -> ValuesView[np.ndarray]: + return self._dict.values() + + def items(self) -> ItemsView[ConstOutput, np.ndarray]: + return self._dict.items() + + def names(self) -> List[str]: + """Return a name of every output key. + + Throws RuntimeError if any of ConstOutput keys has no name. + """ + return [key.get_any_name() for key in self._dict.keys()] + + def to_dict(self) -> Dict[ConstOutput, np.ndarray]: + """Return underlaying native dictionary. + + Function performs shallow copy, thus any modifications to + returned values may affect this class as well. + """ + return self._dict + + def to_tuple(self) -> tuple: + """Convert values of this dictionary to a tuple.""" + return tuple(self._dict.values()) diff --git a/src/bindings/python/src/pyopenvino/core/common.cpp b/src/bindings/python/src/pyopenvino/core/common.cpp index 2ad7e395a92..ef5313cec01 100644 --- a/src/bindings/python/src/pyopenvino/core/common.cpp +++ b/src/bindings/python/src/pyopenvino/core/common.cpp @@ -53,6 +53,27 @@ const std::map& dtype_to_ov_type() { return dtype_to_ov_type_mapping; } +namespace containers { +const TensorIndexMap cast_to_tensor_index_map(const py::dict& inputs) { + TensorIndexMap result_map; + for (auto&& input : inputs) { + int idx; + if (py::isinstance(input.first)) { + idx = input.first.cast(); + } else { + throw py::type_error("incompatible function arguments!"); + } + if (py::isinstance(input.second)) { + auto tensor = Common::cast_to_tensor(input.second); + result_map[idx] = tensor; + } else { + throw ov::Exception("Unable to cast tensor " + std::to_string(idx) + "!"); + } + } + return result_map; +} +}; // namespace containers + namespace array_helpers { bool is_contiguous(const py::array& array) { @@ -110,6 +131,67 @@ py::array as_contiguous(py::array& array, ov::element::Type type) { } } +py::array array_from_tensor(ov::Tensor&& t) { + switch (t.get_element_type()) { + case ov::element::Type_t::f32: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::f64: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::bf16: { + return py::array(py::dtype("float16"), t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::f16: { + return py::array(py::dtype("float16"), t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::i8: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::i16: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::i32: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::i64: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::u8: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::u16: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::u32: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::u64: { + return py::array_t(t.get_shape(), t.data()); + break; + } + case ov::element::Type_t::boolean: { + return py::array_t(t.get_shape(), t.data()); + break; + } + default: { + throw ov::Exception("Numpy array cannot be created from given OV Tensor!"); + break; + } + } +} + }; // namespace array_helpers template <> @@ -226,38 +308,6 @@ const ov::Tensor& cast_to_tensor(const py::handle& tensor) { return tensor.cast(); } -const Containers::TensorNameMap cast_to_tensor_name_map(const py::dict& inputs) { - Containers::TensorNameMap result_map; - for (auto&& input : inputs) { - std::string name; - if (py::isinstance(input.first)) { - name = input.first.cast(); - } else { - throw py::type_error("incompatible function arguments!"); - } - OPENVINO_ASSERT(py::isinstance(input.second), "Unable to cast tensor ", name, "!"); - auto tensor = Common::cast_to_tensor(input.second); - result_map[name] = tensor; - } - return result_map; -} - -const Containers::TensorIndexMap cast_to_tensor_index_map(const py::dict& inputs) { - Containers::TensorIndexMap result_map; - for (auto&& input : inputs) { - int idx; - if (py::isinstance(input.first)) { - idx = input.first.cast(); - } else { - throw py::type_error("incompatible function arguments!"); - } - OPENVINO_ASSERT(py::isinstance(input.second), "Unable to cast tensor ", idx, "!"); - auto tensor = Common::cast_to_tensor(input.second); - result_map[idx] = tensor; - } - return result_map; -} - void set_request_tensors(ov::InferRequest& request, const py::dict& inputs) { if (!inputs.empty()) { for (auto&& input : inputs) { @@ -293,67 +343,10 @@ uint32_t get_optimal_number_of_requests(const ov::CompiledModel& actual) { } } -py::dict outputs_to_dict(const std::vector>& outputs, ov::InferRequest& request) { +py::dict outputs_to_dict(InferRequestWrapper& request) { py::dict res; - for (const auto& out : outputs) { - ov::Tensor t{request.get_tensor(out)}; - switch (t.get_element_type()) { - case ov::element::Type_t::i8: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::i16: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::i32: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::i64: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::u8: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::u16: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::u32: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::u64: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::bf16: { - res[py::cast(out)] = py::array(py::dtype("float16"), t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::f16: { - res[py::cast(out)] = py::array(py::dtype("float16"), t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::f32: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::f64: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - case ov::element::Type_t::boolean: { - res[py::cast(out)] = py::array_t(t.get_shape(), t.data()); - break; - } - default: { - break; - } - } + for (const auto& out : request.m_outputs) { + res[py::cast(out)] = array_helpers::array_from_tensor(request.m_request.get_tensor(out)); } return res; } diff --git a/src/bindings/python/src/pyopenvino/core/common.hpp b/src/bindings/python/src/pyopenvino/core/common.hpp index 910d9e55e96..de033c3ddf3 100644 --- a/src/bindings/python/src/pyopenvino/core/common.hpp +++ b/src/bindings/python/src/pyopenvino/core/common.hpp @@ -20,14 +20,20 @@ #include "openvino/runtime/infer_request.hpp" #include "openvino/runtime/tensor.hpp" #include "openvino/pass/serialize.hpp" -#include "pyopenvino/core/containers.hpp" #include "pyopenvino/graph/any.hpp" #include "pyopenvino/graph/ops/constant.hpp" +#include "pyopenvino/core/infer_request.hpp" namespace py = pybind11; namespace Common { +namespace containers { + using TensorIndexMap = std::map; + + const TensorIndexMap cast_to_tensor_index_map(const py::dict& inputs); +}; // namespace containers + namespace values { // Minimum amount of bits for common numpy types. Used to perform checks against OV types. @@ -52,6 +58,8 @@ std::vector get_strides(const py::array& array); py::array as_contiguous(py::array& array, ov::element::Type type); +py::array array_from_tensor(ov::Tensor&& t); + }; // namespace array_helpers template @@ -80,15 +88,11 @@ ov::PartialShape partial_shape_from_list(const py::list& shape); const ov::Tensor& cast_to_tensor(const py::handle& tensor); -const Containers::TensorNameMap cast_to_tensor_name_map(const py::dict& inputs); - -const Containers::TensorIndexMap cast_to_tensor_index_map(const py::dict& inputs); - void set_request_tensors(ov::InferRequest& request, const py::dict& inputs); uint32_t get_optimal_number_of_requests(const ov::CompiledModel& actual); -py::dict outputs_to_dict(const std::vector>& outputs, ov::InferRequest& request); +py::dict outputs_to_dict(InferRequestWrapper& request); ov::pass::Serialize::Version convert_to_version(const std::string& version); diff --git a/src/bindings/python/src/pyopenvino/core/compiled_model.cpp b/src/bindings/python/src/pyopenvino/core/compiled_model.cpp index 9cd0202f32f..7cca9af077e 100644 --- a/src/bindings/python/src/pyopenvino/core/compiled_model.cpp +++ b/src/bindings/python/src/pyopenvino/core/compiled_model.cpp @@ -9,13 +9,9 @@ #include "common.hpp" #include "pyopenvino/core/compiled_model.hpp" -#include "pyopenvino/core/containers.hpp" #include "pyopenvino/core/infer_request.hpp" #include "pyopenvino/utils/utils.hpp" -PYBIND11_MAKE_OPAQUE(Containers::TensorIndexMap); -PYBIND11_MAKE_OPAQUE(Containers::TensorNameMap); - namespace py = pybind11; void regclass_CompiledModel(py::module m) { diff --git a/src/bindings/python/src/pyopenvino/core/containers.cpp b/src/bindings/python/src/pyopenvino/core/containers.cpp deleted file mode 100644 index 8ee414e007a..00000000000 --- a/src/bindings/python/src/pyopenvino/core/containers.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2018-2023 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "pyopenvino/core/containers.hpp" - -#include - -PYBIND11_MAKE_OPAQUE(Containers::TensorIndexMap); -PYBIND11_MAKE_OPAQUE(Containers::TensorNameMap); - -namespace py = pybind11; - -namespace Containers { - -void regclass_TensorIndexMap(py::module m) { - py::bind_map(m, "TensorIndexMap"); -} - -void regclass_TensorNameMap(py::module m) { - py::bind_map(m, "TensorNameMap"); -} -} // namespace Containers diff --git a/src/bindings/python/src/pyopenvino/core/containers.hpp b/src/bindings/python/src/pyopenvino/core/containers.hpp deleted file mode 100644 index becf2f71784..00000000000 --- a/src/bindings/python/src/pyopenvino/core/containers.hpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2018-2023 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include -#include -#include - -#include - -#include - -namespace py = pybind11; - -namespace Containers { - using TensorIndexMap = std::map; - using TensorNameMap = std::map; - - void regclass_TensorIndexMap(py::module m); - void regclass_TensorNameMap(py::module m); -} diff --git a/src/bindings/python/src/pyopenvino/core/infer_request.cpp b/src/bindings/python/src/pyopenvino/core/infer_request.cpp index 585441569f9..8be02e8adb8 100644 --- a/src/bindings/python/src/pyopenvino/core/infer_request.cpp +++ b/src/bindings/python/src/pyopenvino/core/infer_request.cpp @@ -11,12 +11,8 @@ #include #include "pyopenvino/core/common.hpp" -#include "pyopenvino/core/containers.hpp" #include "pyopenvino/utils/utils.hpp" -PYBIND11_MAKE_OPAQUE(Containers::TensorIndexMap); -PYBIND11_MAKE_OPAQUE(Containers::TensorNameMap); - namespace py = pybind11; inline py::dict run_sync_infer(InferRequestWrapper& self) { @@ -26,7 +22,7 @@ inline py::dict run_sync_infer(InferRequestWrapper& self) { self.m_request.infer(); *self.m_end_time = Time::now(); } - return Common::outputs_to_dict(self.m_outputs, self.m_request); + return Common::outputs_to_dict(self); } void regclass_InferRequest(py::module m) { @@ -103,7 +99,7 @@ void regclass_InferRequest(py::module m) { cls.def( "set_output_tensors", [](InferRequestWrapper& self, const py::dict& outputs) { - auto outputs_map = Common::cast_to_tensor_index_map(outputs); + auto outputs_map = Common::containers::cast_to_tensor_index_map(outputs); for (auto&& output : outputs_map) { self.m_request.set_output_tensor(output.first, output.second); } @@ -120,7 +116,7 @@ void regclass_InferRequest(py::module m) { cls.def( "set_input_tensors", [](InferRequestWrapper& self, const py::dict& inputs) { - auto inputs_map = Common::cast_to_tensor_index_map(inputs); + auto inputs_map = Common::containers::cast_to_tensor_index_map(inputs); for (auto&& input : inputs_map) { self.m_request.set_input_tensor(input.first, input.second); } @@ -719,7 +715,7 @@ void regclass_InferRequest(py::module m) { cls.def_property_readonly( "results", [](InferRequestWrapper& self) { - return Common::outputs_to_dict(self.m_outputs, self.m_request); + return Common::outputs_to_dict(self); }, R"( Gets all outputs tensors of this InferRequest. diff --git a/src/bindings/python/src/pyopenvino/pyopenvino.cpp b/src/bindings/python/src/pyopenvino/pyopenvino.cpp index a229f9eaa7d..0f2cdf38278 100644 --- a/src/bindings/python/src/pyopenvino/pyopenvino.cpp +++ b/src/bindings/python/src/pyopenvino/pyopenvino.cpp @@ -24,7 +24,6 @@ #endif #include "pyopenvino/core/async_infer_queue.hpp" #include "pyopenvino/core/compiled_model.hpp" -#include "pyopenvino/core/containers.hpp" #include "pyopenvino/core/core.hpp" #include "pyopenvino/core/extension.hpp" #include "pyopenvino/core/infer_request.hpp" @@ -210,9 +209,6 @@ PYBIND11_MODULE(_pyopenvino, m) { regclass_Core(m); regclass_Tensor(m); - // Registering specific types of containers - Containers::regclass_TensorIndexMap(m); - Containers::regclass_TensorNameMap(m); regclass_CompiledModel(m); regclass_InferRequest(m); diff --git a/src/bindings/python/tests/test_runtime/test_ovdict.py b/src/bindings/python/tests/test_runtime/test_ovdict.py new file mode 100644 index 00000000000..e8c76a6d8d3 --- /dev/null +++ b/src/bindings/python/tests/test_runtime/test_ovdict.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2018-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Mapping +import numpy as np +import pytest + +import openvino.runtime.opset10 as ops +from openvino.runtime import Core, ConstOutput, CompiledModel, InferRequest, Model +from openvino.runtime.ie_api import OVDict + + +def _get_ovdict( + device, + input_shape=None, + data_type=np.float32, + input_names=None, + output_names=None, + multi_output=False, + direct_infer=False, + split_num=5, +): + # Create model + # If model is multi-output (multi_output=True), input_shape must match + # requirements of split operation. + # TODO OpenSource: refactor it to be more generic + if input_shape is None: + input_shape = [1, 20] + if input_names is None: + input_names = ["data_0"] + if output_names is None: + output_names = ["output_0"] + if multi_output: + assert isinstance(output_names, (list, tuple)) + assert len(output_names) > 1 + assert len(output_names) == split_num + param = ops.parameter(input_shape, data_type, name=input_names[0]) + model = Model( + ops.split(param, 1, split_num) if multi_output else ops.abs(param), [param], + ) + # Manually name outputs + for i in range(len(output_names)): + model.output(i).tensor.names = {output_names[i]} + # Compile model + core = Core() + compiled_model = core.compile_model(model, device) + # Create test data + input_data = np.random.random(input_shape).astype(data_type) + # Two ways of infering + if direct_infer: + result = compiled_model(input_data) + assert result is not None + return result, compiled_model + + request = compiled_model.create_infer_request() + result = request.infer(input_data) + assert result is not None + return result, request + + +def _check_keys(keys, outs): + outs_iter = iter(outs) + for key in keys: + assert isinstance(key, ConstOutput) + assert key == next(outs_iter) + return True + + +def _check_values(result): + for value in result.values(): + assert isinstance(value, np.ndarray) + return True + + +def _check_items(result, outs, output_names): + i = 0 + for key, value in result.items(): + assert isinstance(key, ConstOutput) + assert isinstance(value, np.ndarray) + # Check values + assert np.equal(result[outs[i]], result[key]).all() + assert np.equal(result[outs[i]], result[i]).all() + assert np.equal(result[outs[i]], result[output_names[i]]).all() + i += 1 + return True + + +def _check_dict(result, obj, output_names=None): + if output_names is None: + output_names = ["output_0"] + + outs = obj.model_outputs if isinstance(obj, InferRequest) else obj.outputs + assert len(outs) == len(result) + assert len(outs) == len(output_names) + # Check for __iter__ + assert _check_keys(result, outs) + # Check for keys function + assert _check_keys(result.keys(), outs) + assert _check_values(result) + assert _check_items(result, outs, output_names) + assert result.names() == output_names + + return True + + +@pytest.mark.parametrize("is_direct", [True, False]) +def test_ovdict_assign(device, is_direct): + result, _ = _get_ovdict(device, multi_output=False, direct_infer=is_direct) + + with pytest.raises(TypeError) as e: + result["some_name"] = 99 + assert "'OVDict' object does not support item assignment" in str(e.value) + + +@pytest.mark.parametrize("is_direct", [True, False]) +def test_ovdict_single_output_basic(device, is_direct): + result, obj = _get_ovdict(device, multi_output=False, direct_infer=is_direct) + + assert isinstance(result, OVDict) + if isinstance(obj, (InferRequest, CompiledModel)): + assert _check_dict(result, obj) + else: + raise TypeError("Unknown `obj` type!") + + +@pytest.mark.parametrize("is_direct", [True, False]) +def test_ovdict_single_output_noname(device, is_direct): + result, obj = _get_ovdict( + device, + multi_output=False, + direct_infer=is_direct, + output_names=[], + ) + + assert isinstance(result, OVDict) + + outs = obj.model_outputs if isinstance(obj, InferRequest) else obj.outputs + + assert isinstance(result[outs[0]], np.ndarray) + assert isinstance(result[0], np.ndarray) + + with pytest.raises(RuntimeError) as e0: + _ = result["some_name"] + assert "Attempt to get a name for a Tensor without names" in str(e0.value) + + with pytest.raises(RuntimeError) as e1: + _ = result.names() + assert "Attempt to get a name for a Tensor without names" in str(e1.value) + + +@pytest.mark.parametrize("is_direct", [True, False]) +def test_ovdict_single_output_wrongname(device, is_direct): + result, obj = _get_ovdict( + device, + multi_output=False, + direct_infer=is_direct, + output_names=["output_21"], + ) + + assert isinstance(result, OVDict) + + outs = obj.model_outputs if isinstance(obj, InferRequest) else obj.outputs + + assert isinstance(result[outs[0]], np.ndarray) + assert isinstance(result[0], np.ndarray) + + with pytest.raises(KeyError) as e: + _ = result["output_37"] + assert "output_37" in str(e.value) + + with pytest.raises(KeyError) as e: + _ = result[6] + assert "6" in str(e.value) + + +@pytest.mark.parametrize("is_direct", [True, False]) +@pytest.mark.parametrize("use_function", [True, False]) +def test_ovdict_single_output_dict(device, is_direct, use_function): + result, obj = _get_ovdict( + device, + multi_output=False, + direct_infer=is_direct, + ) + + assert isinstance(result, OVDict) + + outs = obj.model_outputs if isinstance(obj, InferRequest) else obj.outputs + native_dict = result.to_dict() if use_function else dict(result) + + assert issubclass(type(native_dict), dict) + assert not isinstance(native_dict, OVDict) + assert isinstance(native_dict[outs[0]], np.ndarray) + + with pytest.raises(KeyError) as e: + _ = native_dict["output_0"] + assert "output_0" in str(e.value) + + with pytest.raises(KeyError) as e: + _ = native_dict[0] + assert "0" in str(e.value) + + +@pytest.mark.parametrize("is_direct", [True, False]) +def test_ovdict_multi_output_basic(device, is_direct): + output_names = ["output_0", "output_1", "output_2", "output_3", "output_4"] + result, obj = _get_ovdict( + device, + multi_output=True, + direct_infer=is_direct, + output_names=output_names, + ) + + assert isinstance(result, OVDict) + if isinstance(obj, (InferRequest, CompiledModel)): + assert _check_dict(result, obj, output_names) + else: + raise TypeError("Unknown `obj` type!") + + +@pytest.mark.parametrize("is_direct", [True, False]) +@pytest.mark.parametrize("use_function", [True, False]) +def test_ovdict_multi_output_tuple0(device, is_direct, use_function): + output_names = ["output_0", "output_1"] + result, obj = _get_ovdict( + device, + input_shape=(1, 10), + multi_output=True, + direct_infer=is_direct, + split_num=2, + output_names=output_names, + ) + + out0, out1 = None, None + if use_function: + assert isinstance(result.to_tuple(), tuple) + out0, out1 = result.to_tuple() + else: + out0, out1 = result.values() + + assert out0 is not None + assert out1 is not None + assert isinstance(out0, np.ndarray) + assert isinstance(out1, np.ndarray) + + outs = obj.model_outputs if isinstance(obj, InferRequest) else obj.outputs + + assert np.equal(result[outs[0]], out0).all() + assert np.equal(result[outs[1]], out1).all()