Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add String2DArray Output Support in Measurement packages and update relevant tests #1064

Merged
merged 5 commits into from
Feb 7, 2025
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
49 changes: 27 additions & 22 deletions examples/sample_measurement/SampleAllParameters.measui

Large diffs are not rendered by default.

Binary file modified examples/sample_measurement/SampleMeasurement.vi
Binary file not shown.
34 changes: 33 additions & 1 deletion examples/sample_measurement/_array_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Double2DArray Conversion Utilities."""
"""2DArray Conversion Utilities."""

from __future__ import annotations

Expand Down Expand Up @@ -43,3 +43,35 @@ def double2darray_to_list(double2darray: array_pb2.Double2DArray) -> List[List[f
rows = double2darray.rows
columns = double2darray.columns
return [data[i * columns : (i + 1) * columns] for i in range(rows)]


def string2darray_to_ndarray(string2darray: array_pb2.String2DArray) -> npt.NDArray[np.str_]:
"""Convert String2DArray to numpy NDArray."""
import numpy as np

return np.array(string2darray.data, dtype=np.str_).reshape(
string2darray.rows, string2darray.columns
)


def ndarray_to_string2darray(ndarray: npt.NDArray[np.str_]) -> array_pb2.String2DArray:
"""Convert numpy NDArray to String2DArray."""
return array_pb2.String2DArray(
data=ndarray.flatten().tolist(), rows=ndarray.shape[0], columns=ndarray.shape[1]
)


def string2darray_to_list(string2darray: array_pb2.String2DArray) -> List[List[str]]:
"""Convert String2DArray to list of lists."""
data = string2darray.data
rows = string2darray.rows
columns = string2darray.columns
return [data[i * columns : (i + 1) * columns] for i in range(rows)]


def list_to_string2darray(data: List[List[str]]) -> array_pb2.String2DArray:
"""Convert list of lists to String2DArray."""
rows = len(data)
columns = len(data[0]) if rows > 0 else 0
flattened_data = [item for sublist in data for item in sublist]
return array_pb2.String2DArray(data=flattened_data, rows=rows, columns=columns)
20 changes: 18 additions & 2 deletions examples/sample_measurement/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ class Color(Enum):


# Define a list of lists of floats
_list_of_lists = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
_list_of_lists_of_floats = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]

# Convert the list of lists to a Double2DArray
_converted_double_2d_array = _array_utils.list_to_double2darray(_list_of_lists)
_converted_double_2d_array = _array_utils.list_to_double2darray(_list_of_lists_of_floats)

# Define a list of lists of strings
_list_of_lists_of_string = [["String1", "String2", "String3"], ["String4", "String5", "String6"]]

# Convert the list of lists to a String2DArray
_converted_string_2d_array = _array_utils.list_to_string2darray(_list_of_lists_of_string)


@measurement_service.register_measurement
Expand Down Expand Up @@ -73,6 +79,8 @@ class Color(Enum):
@measurement_service.output("String Array out", nims.DataType.StringArray1D)
@measurement_service.output("Double 2D Array Out", nims.DataType.Double2DArray)
@measurement_service.output("Converted Double 2D Array", nims.DataType.Double2DArray)
@measurement_service.output("String 2D Array Out", nims.DataType.String2DArray)
@measurement_service.output("Converted String 2D Array", nims.DataType.String2DArray)
def measure(
float_input: float,
double_array_input: Iterable[float],
Expand All @@ -91,6 +99,8 @@ def measure(
Iterable[str],
array_pb2.Double2DArray,
array_pb2.Double2DArray,
array_pb2.String2DArray,
array_pb2.String2DArray,
]:
"""Perform a loopback measurement with various data types."""
logging.info("Executing measurement")
Expand All @@ -111,6 +121,10 @@ def cancel_callback() -> None:
rows=2, columns=3, data=[1.5, 2.5, 3.5, 4.5, 5.5, 6.5]
)
converted_double_2d_array_output = _converted_double_2d_array
string_2d_array_output = array_pb2.String2DArray(
rows=2, columns=3, data=["ABC", "DEF", "GHI", "JKL", "MNO", "PQR"]
)
converted_string_2d_array_output = _converted_string_2d_array
logging.info("Completed measurement")

return (
Expand All @@ -123,6 +137,8 @@ def cancel_callback() -> None:
string_array_output,
double_2d_array_output,
converted_double_2d_array_output,
string_2d_array_output,
converted_string_2d_array_output,
)


Expand Down
4 changes: 2 additions & 2 deletions examples/sample_measurement/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["National Instruments"]

[tool.poetry.dependencies]
python = "^3.8"
ni-measurement-plugin-sdk-service = {version = "^2.2.0"}
ni-measurement-plugin-sdk-service = {version = "^2.3.0-dev0", allow-prereleases = true}
click = ">=7.1.2, !=8.1.4" # mypy fails with click 8.1.4: https://github.com/pallets/click/issues/2558

[tool.poetry.group.dev.dependencies]
Expand All @@ -18,7 +18,7 @@ grpcio-tools = "1.49.1"
mypy-protobuf = "^3.6.0"
types-protobuf = "^4.21"
# Uncomment to use prerelease dependencies.
# ni-measurement-plugin-sdk-service = {path = "../../packages/service", develop = true}
ni-measurement-plugin-sdk-service = {path = "../../packages/service", develop = true}

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
_INVALID_CHARS = "`~!@#$%^&*()-+={}[]\\|:;',<>.?/ \n"

_XY_DATA_IMPORT = "from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.xydata_pb2 import DoubleXYData"
_2DARRAY_DATA_IMPORT = "from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.array_pb2 import Double2DArray"
_DOUBLE2DARRAY_DATA_IMPORT = "from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.array_pb2 import Double2DArray"
_STRING2DARRAY_DATA_IMPORT = "from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.array_pb2 import String2DArray"
_PATH_IMPORT = "import pathlib"

_PROTO_DATATYPE_TO_PYTYPE_LOOKUP = {
Expand Down Expand Up @@ -157,9 +158,10 @@ def get_configuration_and_output_metadata_by_index(
if output.message_type and output.message_type not in [
"ni.protobuf.types.DoubleXYData",
"ni.protobuf.types.Double2DArray",
"ni.protobuf.types.String2DArray",
]:
raise click.ClickException(
f"Measurement outputs do not support {output.message_type}. Only DoubleXYData and Double2DArray are supported."
f"Measurement outputs do not support {output.message_type}. DoubleXYData, Double2DArray and String2DArray message types are supported."
)

annotations_dict = dict(output.annotations.items())
Expand Down Expand Up @@ -300,14 +302,24 @@ def get_output_parameters_with_type(

if metadata.message_type and metadata.message_type == "ni.protobuf.types.Double2DArray":
parameter_type = "Double2DArray"
custom_import_modules.append(_2DARRAY_DATA_IMPORT)
custom_import_modules.append(_DOUBLE2DARRAY_DATA_IMPORT)

if metadata.repeated:
raise click.ClickException(
f"Repeated Double 2D Array are not supported for output parameter "
f"'{parameter_name}'. Please contact the measurement developer."
)

if metadata.message_type and metadata.message_type == "ni.protobuf.types.String2DArray":
parameter_type = "String2DArray"
custom_import_modules.append(_STRING2DARRAY_DATA_IMPORT)

if metadata.repeated:
raise click.ClickException(
f"Repeated String 2D Array are not supported for output parameter "
f"'{parameter_name}'. Please contact the measurement developer."
)

if metadata.annotations and metadata.annotations.get("ni/type_specialization") == "enum":
enum_type_name = _get_enum_type(
metadata.display_name, metadata.annotations["ni/enum.values"], enum_values_by_type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def test___measurement_plugin_client___measure___returns_output(
double_2d_array_out=array_pb2.Double2DArray(
rows=2, columns=3, data=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
),
string_2d_array_out=array_pb2.String2DArray(
rows=2, columns=3, data=["ABC", "DEF", "GHI", "JKL", "MNO", "PQR"]
),
)
measurement_plugin_client = test_measurement_client_type()

Expand Down Expand Up @@ -129,6 +132,9 @@ def test___measurement_plugin_client___stream_measure___returns_output(
double_2d_array_out=array_pb2.Double2DArray(
rows=2, columns=3, data=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
),
string_2d_array_out=array_pb2.String2DArray(
rows=2, columns=3, data=["ABC", "DEF", "GHI", "JKL", "MNO", "PQR"]
),
)
measurement_plugin_client = test_measurement_client_type()

Expand Down Expand Up @@ -227,6 +233,7 @@ def _verify_output_types(outputs: Any, measurement_plugin_client_module: ModuleT
_assert_collection_type(outputs.enum_array_out, Sequence, enum_type)
_assert_type(outputs.protobuf_enum_out, protobuf_enum_type)
_assert_type(outputs.double_2d_array_out, array_pb2.Double2DArray)
_assert_type(outputs.string_2d_array_out, array_pb2.String2DArray)


def _assert_type(value: Any, expected_type: Union[Type[Any], Tuple[Type[Any], ...]]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.array_pb2 import (
Double2DArray,
)
from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.array_pb2 import (
String2DArray,
)
from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.xydata_pb2 import (
DoubleXYData,
)
Expand Down Expand Up @@ -75,6 +78,7 @@ class Outputs(typing.NamedTuple):
enum_array_out: typing.Sequence[EnumInEnum]
protobuf_enum_out: ProtobufEnumInEnum
double_2d_array_out: Double2DArray
string_2d_array_out: String2DArray


class NonStreamingDataMeasurementClient:
Expand Down Expand Up @@ -436,6 +440,16 @@ def __init__(
field_name="Double_2D_Array_out",
enum_type=None,
),
16: ParameterMetadata(
display_name="String 2D Array out",
type=Field.Kind.ValueType(11),
repeated=False,
default_value=None,
annotations={},
message_type="ni.protobuf.types.String2DArray",
field_name="String_2D_Array_out",
enum_type=None,
),
}
if grpc_channel is not None:
self._stub = v2_measurement_service_pb2_grpc.MeasurementServiceStub(grpc_channel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class Color(Enum):
"Protobuf Enum out", nims.DataType.Enum, enum_type=color_pb2.ProtobufColor
)
@measurement_service.output("Double 2D Array out", nims.DataType.Double2DArray)
@measurement_service.output("String 2D Array out", nims.DataType.String2DArray)
def measure(
float_input: float,
double_array_input: Iterable[float],
Expand Down Expand Up @@ -122,6 +123,7 @@ def measure(
Iterable[Color],
color_pb2.ProtobufColor.ValueType,
array_pb2.Double2DArray,
array_pb2.String2DArray,
]:
"""Perform a loopback measurement with various data types."""
float_output = float_input
Expand All @@ -141,6 +143,9 @@ def measure(
double_2d_array_output = array_pb2.Double2DArray(
rows=2, columns=3, data=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
)
string_2d_array_output = array_pb2.String2DArray(
rows=2, columns=3, data=["ABC", "DEF", "GHI", "JKL", "MNO", "PQR"]
)

return (
float_output,
Expand All @@ -158,4 +163,5 @@ def measure(
enum_array_output,
protobuf_enum_output,
double_2d_array_output,
string_2d_array_output,
)
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def get_type_info(data_type: DataType) -> DataTypeInfo:
False,
message_type=array_pb2.Double2DArray.DESCRIPTOR.full_name,
),
DataType.String2DArray: DataTypeInfo(
type_pb2.Field.TYPE_MESSAGE,
False,
message_type=array_pb2.String2DArray.DESCRIPTOR.full_name,
),
DataType.IOResource: DataTypeInfo(
type_pb2.Field.TYPE_STRING, False, TypeSpecialization.IOResource
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class DataType(enum.Enum):
DoubleXYData = 11
IOResource = 12
Double2DArray = 13
String2DArray = 14

Int32Array1D = 100
Int64Array1D = 101
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,12 @@ def configuration(
"DataType.PinArray1D is deprecated. Use DataType.IOResourceArray1D instead.",
DeprecationWarning,
)
if type in [DataType.Double2DArray, DataType.DoubleXYData, DataType.DoubleXYDataArray1D]:
if type in [
DataType.Double2DArray,
DataType.DoubleXYData,
DataType.DoubleXYDataArray1D,
DataType.String2DArray,
]:
raise ValueError(f"{type} is not supported for configuration parameters.")
data_type_info = _datatypeinfo.get_type_info(type)
annotations = self._make_annotations_dict(
Expand Down
36 changes: 36 additions & 0 deletions packages/service/tests/unit/test_array_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,39 @@ def test___valid_double2darray___double2darray_to_list___converts_data():
data = _array_utils.double2darray_to_list(double2darray)

assert data == [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]


def test___valid_string2darray___string2darray_to_ndarray___converts_data():
string2darray = array_pb2.String2DArray(data=["a", "b", "c", "d", "e", "f"], rows=2, columns=3)

ndarray = _array_utils.string2darray_to_ndarray(string2darray)

assert numpy.array_equal(ndarray, numpy.array([["a", "b", "c"], ["d", "e", "f"]]))


def test___valid_ndarray___ndarray_to_string2darray___converts_data():
ndarray = numpy.array([["a", "b", "c"], ["d", "e", "f"]])

string2darray = _array_utils.ndarray_to_string2darray(ndarray)

assert string2darray == array_pb2.String2DArray(
data=["a", "b", "c", "d", "e", "f"], rows=2, columns=3
)


def test___valid_list___list_to_string2darray___converts_data():
data = [["a", "b", "c"], ["d", "e", "f"]]

string2darray = _array_utils.list_to_string2darray(data)

assert string2darray == array_pb2.String2DArray(
data=["a", "b", "c", "d", "e", "f"], rows=2, columns=3
)


def test___valid_string2darray___string2darray_to_list___converts_data():
string2darray = array_pb2.String2DArray(data=["a", "b", "c", "d", "e", "f"], rows=2, columns=3)

data = _array_utils.string2darray_to_list(string2darray)

assert data == [["a", "b", "c"], ["d", "e", "f"]]
1 change: 1 addition & 0 deletions packages/service/tests/unit/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ def test___measurement_service___add_configuration_with_mismatch_default_value__
("DoubleXYData", DataType.DoubleXYData),
("DoubleXYDataArray", DataType.DoubleXYDataArray1D),
("Double2DArray", DataType.Double2DArray),
("String2DArray", DataType.String2DArray),
],
)
def test___measurement_service___add_output__output_added(
Expand Down
34 changes: 33 additions & 1 deletion packages/service/tests/utilities/_array_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Double2DArray Conversion Utilities."""
"""2DArray Conversion Utilities."""

from __future__ import annotations

Expand Down Expand Up @@ -43,3 +43,35 @@ def double2darray_to_list(double2darray: array_pb2.Double2DArray) -> List[List[f
rows = double2darray.rows
columns = double2darray.columns
return [data[i * columns : (i + 1) * columns] for i in range(rows)]


def string2darray_to_ndarray(string2darray: array_pb2.String2DArray) -> npt.NDArray[np.str_]:
"""Convert String2DArray to numpy NDArray."""
import numpy as np

return np.array(string2darray.data, dtype=np.str_).reshape(
string2darray.rows, string2darray.columns
)


def ndarray_to_string2darray(ndarray: npt.NDArray[np.str_]) -> array_pb2.String2DArray:
"""Convert numpy NDArray to String2DArray."""
return array_pb2.String2DArray(
data=ndarray.flatten().tolist(), rows=ndarray.shape[0], columns=ndarray.shape[1]
)


def string2darray_to_list(string2darray: array_pb2.String2DArray) -> List[List[str]]:
"""Convert String2DArray to list of lists."""
data = string2darray.data
rows = string2darray.rows
columns = string2darray.columns
return [data[i * columns : (i + 1) * columns] for i in range(rows)]


def list_to_string2darray(data: List[List[str]]) -> array_pb2.String2DArray:
"""Convert list of lists to String2DArray."""
rows = len(data)
columns = len(data[0]) if rows > 0 else 0
flattened_data = [item for sublist in data for item in sublist]
return array_pb2.String2DArray(data=flattened_data, rows=rows, columns=columns)