Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e0bca01
Initial Implementation Work - Add ability to populate PublishMeasurem…
hunter-ni May 15, 2026
7aaf379
Adding test cases for unexpected dtype
hunter-ni May 15, 2026
3a34263
Adding more tests for mismatched dtypes.
hunter-ni May 15, 2026
335e0d7
Single-source type checking logic for populating PublishMeasurementBa…
hunter-ni May 15, 2026
22c581b
Adding tests for populating PublishMeasurementBatchRequest from other…
hunter-ni May 15, 2026
07ee6a7
Strengthen test assertions in error cases
hunter-ni May 15, 2026
bdd5a08
Adding further test cases for error scenarios when populating Publish…
hunter-ni May 18, 2026
b54be78
Adding acceptance tests for successfully publishing non-scalar measur…
hunter-ni May 18, 2026
0f554ad
Refactor - change 'if' statements to 'else if' statements for greater…
hunter-ni May 18, 2026
2cdf3dc
Update 'ni.measurements.data.v1.client' dependency version
hunter-ni May 18, 2026
b2b931d
Update PublishConditionBatch test name for consistency with PublishMe…
hunter-ni May 19, 2026
e183efa
Adding example for publish_condition_batch and publish_measurement_batch
hunter-ni May 19, 2026
f6e0816
Run linter and fix style errors
hunter-ni May 19, 2026
ad142e1
Adding type-check assertions on PublishMeasurementBatchRequest popula…
hunter-ni May 20, 2026
4696c15
Remove unnecessary list wrappers from acceptance tests
hunter-ni May 20, 2026
ebe4dc6
Update acceptance test names for clarity
hunter-ni May 20, 2026
99ca7a9
Convert separate if statements to else-if statements for consistency
hunter-ni May 20, 2026
96e2cea
Add test case for supplying a non-iterable
hunter-ni May 20, 2026
c8260f5
Review feedback - update documentation.
hunter-ni May 20, 2026
1a4d952
Review feedback - Simplify construction of 'scalar_values'
hunter-ni May 20, 2026
31748d5
Review feedback - Parameterize several existing unit tests
hunter-ni May 20, 2026
9044ef2
Review feedback - Fix code analysis issue
hunter-ni May 20, 2026
c555417
Review feedback - Make more tests parameterized
hunter-ni May 20, 2026
77d7114
Review feedback - Split populate_publish_measurement_batch_request_va…
hunter-ni May 20, 2026
1c0f0cc
Fix code analysis (type hint) issue
hunter-ni May 20, 2026
9440668
Avoid instantiation of intermediary list from inputted values.
hunter-ni May 20, 2026
f83e258
Simplification of iteration logic in populate_publish_measurement_bat…
hunter-ni May 20, 2026
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
452 changes: 452 additions & 0 deletions examples/notebooks/publish/publish_batch.ipynb
Comment thread
hunter-ni marked this conversation as resolved.

Large diffs are not rendered by default.

36 changes: 18 additions & 18 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ requires-poetry = '>=2.1,<3.0'
[tool.poetry.dependencies]
python = "^3.10"
protobuf = {version=">=4.21"}
ni-measurements-data-v1-client = { version = ">=1.1.0dev0", allow-prereleases = true }
ni-measurements-data-v1-client = { version = ">=1.1.0dev1", allow-prereleases = true }
ni-measurements-metadata-v1-client = { version = ">=1.0.0" }
ni-protobuf-types = { version = ">=1.1.0" }
hightime = { version = ">=1.0.0" }
Expand Down
11 changes: 5 additions & 6 deletions src/ni/datastore/data/_data_store_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,15 @@ def publish_measurement_batch(
software_item_ids: Iterable[str] = tuple(),
notes: str = "",
) -> Sequence[str]:
"""Publish multiple scalar measurements at once for parametric sweeps.
"""Publish multiple measurements at once for parametric sweeps.

Args:
name: The name used for associating/grouping
conceptually alike measurements across multiple publish
iterations. For example, "Temperature" can be used for
associating temperature readings across multiple iterations.

values: The values of the (scalar) measurement being published
values: The values of the measurement being published
across N iterations.

step_id: The ID of the step associated with this measurement. This
Expand Down Expand Up @@ -339,10 +339,9 @@ def publish_measurement_batch(
notes: Any notes to be associated with the published measurements.

Returns:
Sequence[str]: The ids of the published measurement ids.
NOTE: Using a Sequence is for future flexibility.
This sequence will currently always have a single measurement id
returned.
Sequence[str]: The IDs of the corresponding PublishedMeasurements. A single
ID will be returned when publishing scalar measurement values.
N IDs will be returned when publishing (N) non-scalar measurement values.
"""
publish_request = PublishMeasurementBatchRequest(
name=name,
Expand Down
158 changes: 149 additions & 9 deletions src/ni/datastore/data/_grpc_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import datetime as std_datetime
import logging
from typing import Iterable, cast
from typing import Any, Callable, Iterable, cast

import hightime as ht
import numpy as np
Expand Down Expand Up @@ -49,6 +49,129 @@
_logger = logging.getLogger(__name__)


def _copy_batch_values(
repeated_field: Any,
batch_values: Iterable[object],
is_supported: Callable[[object], bool],
convert_value: Callable[[Any], Any],
error_message: str,
) -> None:
for value in batch_values:
if not is_supported(value):
raise TypeError(error_message)
repeated_field.add().CopyFrom(convert_value(value))


def _populate_vector_batch_values(
publish_request: PublishMeasurementBatchRequest, values: Iterable[object]
) -> None:
_copy_batch_values(
publish_request.vector_values.vectors,
values,
lambda value: isinstance(value, Vector),
vector_to_protobuf,
"Unsupported iterable: all values must be Vector.",
)


def _populate_analog_waveform_batch_values(
publish_request: PublishMeasurementBatchRequest,
first_value: AnalogWaveform[Any],
values: Iterable[object],
) -> None:
if first_value.dtype == np.float64:
_copy_batch_values(
publish_request.double_analog_waveform_values.waveforms,
values,
lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.float64,
float64_analog_waveform_to_protobuf,
"Unsupported iterable: all values must be float64 AnalogWaveform.",
)
return
elif first_value.dtype == np.int16:
_copy_batch_values(
publish_request.i16_analog_waveform_values.waveforms,
values,
lambda value: isinstance(value, AnalogWaveform) and value.dtype == np.int16,
int16_analog_waveform_to_protobuf,
"Unsupported iterable: all values must be int16 AnalogWaveform.",
)
return
raise TypeError(f"Unsupported AnalogWaveform dtype: {first_value.dtype}")


def _populate_complex_waveform_batch_values(
publish_request: PublishMeasurementBatchRequest,
first_value: ComplexWaveform[Any],
values: Iterable[object],
) -> None:
if first_value.dtype == np.complex128:
_copy_batch_values(
publish_request.double_complex_waveform_values.waveforms,
values,
lambda value: isinstance(value, ComplexWaveform) and value.dtype == np.complex128,
float64_complex_waveform_to_protobuf,
"Unsupported iterable: all values must be complex128 ComplexWaveform.",
)
return
if first_value.dtype == ComplexInt32DType:
_copy_batch_values(
publish_request.i16_complex_waveform_values.waveforms,
values,
lambda value: isinstance(value, ComplexWaveform) and value.dtype == ComplexInt32DType,
int16_complex_waveform_to_protobuf,
"Unsupported iterable: all values must be ComplexWaveform with ComplexInt32DType.",
)
return
raise TypeError(f"Unsupported ComplexWaveform dtype: {first_value.dtype}")


def _populate_spectrum_batch_values(
publish_request: PublishMeasurementBatchRequest,
first_value: Spectrum[Any],
values: Iterable[object],
) -> None:
if first_value.dtype != np.float64:
raise TypeError(f"Unsupported Spectrum dtype: {first_value.dtype}")

_copy_batch_values(
publish_request.double_spectrum_values.waveforms,
values,
lambda value: isinstance(value, Spectrum) and value.dtype == np.float64,
float64_spectrum_to_protobuf,
"Unsupported iterable: all values must be float64 Spectrum.",
)


def _populate_digital_waveform_batch_values(
publish_request: PublishMeasurementBatchRequest, values: Iterable[object]
) -> None:
_copy_batch_values(
publish_request.digital_waveform_values.waveforms,
values,
lambda value: isinstance(value, DigitalWaveform),
digital_waveform_to_protobuf,
"Unsupported iterable: all values must be DigitalWaveform.",
)


def _populate_xydata_batch_values(
publish_request: PublishMeasurementBatchRequest,
first_value: XYData[Any],
values: Iterable[object],
) -> None:
if first_value.dtype != np.float64:
raise TypeError(f"Unsupported XYData dtype: {first_value.dtype}")

_copy_batch_values(
publish_request.x_y_data_values.x_y_data,
values,
lambda value: isinstance(value, XYData) and value.dtype == np.float64,
float64_xydata_to_protobuf,
"Unsupported iterable: all values must be float64 XYData.",
)


def populate_publish_condition_request_value(
publish_request: PublishConditionRequest, value: object
) -> None:
Expand Down Expand Up @@ -162,16 +285,33 @@ def populate_publish_measurement_batch_request_values(
if isinstance(values, Vector):
publish_request.scalar_values.CopyFrom(vector_to_protobuf(values))
elif isinstance(values, Iterable):
if not values:
raise ValueError("Cannot publish an empty Iterable.")
values_iterator = iter(values)
try:
vector = Vector(values)
except (TypeError, ValueError):
raise TypeError(
f"Unsupported iterable: {values}. Subtype must be bool, float, int, or string."
)
first_value = next(values_iterator)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a really long if / elif block. At a minimum, maybe we could split up the contents of the if blocks into small helper methods. Better is probably some kind of a strategy to produce the data to publish. I'll be out after today, so feel free to discuss with others or override this, but as is, it's pretty unreadable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, I've updated to split out some of the logic into helpers for now. I'll see if @csjall requests any further refactoring here.

except StopIteration as exc:
raise ValueError("Cannot publish an empty Iterable.") from exc

publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector))
if isinstance(first_value, Vector):
_populate_vector_batch_values(publish_request, values)
elif isinstance(first_value, AnalogWaveform):
_populate_analog_waveform_batch_values(publish_request, first_value, values)
elif isinstance(first_value, ComplexWaveform):
_populate_complex_waveform_batch_values(publish_request, first_value, values)
elif isinstance(first_value, Spectrum):
_populate_spectrum_batch_values(publish_request, first_value, values)
elif isinstance(first_value, DigitalWaveform):
_populate_digital_waveform_batch_values(publish_request, values)
elif isinstance(first_value, XYData):
_populate_xydata_batch_values(publish_request, first_value, values)
else:
try:
vector = Vector(values)
except (TypeError, ValueError):
raise TypeError(
f"Unsupported iterable. Subtype must be bool, float, int, string, Vector, "
"AnalogWaveform, ComplexWaveform, Spectrum, DigitalWaveform, or XYData."
)
publish_request.scalar_values.CopyFrom(vector_to_protobuf(vector))
else:
raise TypeError(
f"Unsupported measurement values type: {type(values)}. Please consult the documentation."
Expand Down
Loading
Loading