diff --git a/examples/sample_measurement/SampleAllParameters.measui b/examples/sample_measurement/SampleAllParameters.measui index 7848e02f6..bf21dfea9 100644 --- a/examples/sample_measurement/SampleAllParameters.measui +++ b/examples/sample_measurement/SampleAllParameters.measui @@ -1,33 +1,33 @@  - + - - - - - - + + + + + + - + \ No newline at end of file diff --git a/examples/sample_measurement/SampleMeasurement.vi b/examples/sample_measurement/SampleMeasurement.vi index 2b06509ef..a39d69f64 100644 Binary files a/examples/sample_measurement/SampleMeasurement.vi and b/examples/sample_measurement/SampleMeasurement.vi differ diff --git a/examples/sample_measurement/_array_utils.py b/examples/sample_measurement/_array_utils.py index fdb830fa9..864b40166 100644 --- a/examples/sample_measurement/_array_utils.py +++ b/examples/sample_measurement/_array_utils.py @@ -1,4 +1,4 @@ -"""Double2DArray Conversion Utilities.""" +"""2DArray Conversion Utilities.""" from __future__ import annotations @@ -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) diff --git a/examples/sample_measurement/measurement.py b/examples/sample_measurement/measurement.py index 6bf539169..2ce555166 100644 --- a/examples/sample_measurement/measurement.py +++ b/examples/sample_measurement/measurement.py @@ -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 @@ -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], @@ -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") @@ -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 ( @@ -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, ) diff --git a/examples/sample_measurement/pyproject.toml b/examples/sample_measurement/pyproject.toml index a64e9393e..f061ef479 100644 --- a/examples/sample_measurement/pyproject.toml +++ b/examples/sample_measurement/pyproject.toml @@ -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] @@ -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"] diff --git a/packages/generator/ni_measurement_plugin_sdk_generator/client/_support.py b/packages/generator/ni_measurement_plugin_sdk_generator/client/_support.py index 0e21ae661..26dc4c46f 100644 --- a/packages/generator/ni_measurement_plugin_sdk_generator/client/_support.py +++ b/packages/generator/ni_measurement_plugin_sdk_generator/client/_support.py @@ -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 = { @@ -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()) @@ -300,7 +302,7 @@ 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( @@ -308,6 +310,16 @@ def get_output_parameters_with_type( 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 diff --git a/packages/generator/tests/acceptance/test_non_streaming_measurement_client.py b/packages/generator/tests/acceptance/test_non_streaming_measurement_client.py index 74db7bc69..46e1dec53 100644 --- a/packages/generator/tests/acceptance/test_non_streaming_measurement_client.py +++ b/packages/generator/tests/acceptance/test_non_streaming_measurement_client.py @@ -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() @@ -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() @@ -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: diff --git a/packages/generator/tests/test_assets/example_renders/measurement_plugin_client/non_streaming_data_measurement_client.py b/packages/generator/tests/test_assets/example_renders/measurement_plugin_client/non_streaming_data_measurement_client.py index 8ec1e5903..bae2e172f 100644 --- a/packages/generator/tests/test_assets/example_renders/measurement_plugin_client/non_streaming_data_measurement_client.py +++ b/packages/generator/tests/test_assets/example_renders/measurement_plugin_client/non_streaming_data_measurement_client.py @@ -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, ) @@ -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: @@ -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) diff --git a/packages/generator/tests/utilities/measurements/non_streaming_data_measurement/__init__.py b/packages/generator/tests/utilities/measurements/non_streaming_data_measurement/__init__.py index 5e0b90791..d78475c00 100644 --- a/packages/generator/tests/utilities/measurements/non_streaming_data_measurement/__init__.py +++ b/packages/generator/tests/utilities/measurements/non_streaming_data_measurement/__init__.py @@ -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], @@ -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 @@ -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, @@ -158,4 +163,5 @@ def measure( enum_array_output, protobuf_enum_output, double_2d_array_output, + string_2d_array_output, ) diff --git a/packages/service/ni_measurement_plugin_sdk_service/_datatypeinfo.py b/packages/service/ni_measurement_plugin_sdk_service/_datatypeinfo.py index f51d923c1..550b05b7a 100644 --- a/packages/service/ni_measurement_plugin_sdk_service/_datatypeinfo.py +++ b/packages/service/ni_measurement_plugin_sdk_service/_datatypeinfo.py @@ -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 ), diff --git a/packages/service/ni_measurement_plugin_sdk_service/measurement/info.py b/packages/service/ni_measurement_plugin_sdk_service/measurement/info.py index e32dc7b9c..5ca7a644a 100644 --- a/packages/service/ni_measurement_plugin_sdk_service/measurement/info.py +++ b/packages/service/ni_measurement_plugin_sdk_service/measurement/info.py @@ -108,6 +108,7 @@ class DataType(enum.Enum): DoubleXYData = 11 IOResource = 12 Double2DArray = 13 + String2DArray = 14 Int32Array1D = 100 Int64Array1D = 101 diff --git a/packages/service/ni_measurement_plugin_sdk_service/measurement/service.py b/packages/service/ni_measurement_plugin_sdk_service/measurement/service.py index 4ed5bf197..52d99eeab 100644 --- a/packages/service/ni_measurement_plugin_sdk_service/measurement/service.py +++ b/packages/service/ni_measurement_plugin_sdk_service/measurement/service.py @@ -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( diff --git a/packages/service/tests/unit/test_array_utils.py b/packages/service/tests/unit/test_array_utils.py index e01d64391..728be1c94 100644 --- a/packages/service/tests/unit/test_array_utils.py +++ b/packages/service/tests/unit/test_array_utils.py @@ -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"]] diff --git a/packages/service/tests/unit/test_service.py b/packages/service/tests/unit/test_service.py index 6a7a63908..d169227aa 100644 --- a/packages/service/tests/unit/test_service.py +++ b/packages/service/tests/unit/test_service.py @@ -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( diff --git a/packages/service/tests/utilities/_array_utils.py b/packages/service/tests/utilities/_array_utils.py index fdb830fa9..864b40166 100644 --- a/packages/service/tests/utilities/_array_utils.py +++ b/packages/service/tests/utilities/_array_utils.py @@ -1,4 +1,4 @@ -"""Double2DArray Conversion Utilities.""" +"""2DArray Conversion Utilities.""" from __future__ import annotations @@ -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)