Files
2026-07-13 13:17:40 +08:00

378 lines
14 KiB
Python

from datetime import datetime
import numpy as np
import pandas as pd
import pytest
import torch
import ray
from ray.data._internal.tensor_extensions.utils import create_ragged_ndarray
from ray.data.context import DataContext
from ray.data.tests.conftest import * # noqa
from ray.tests.conftest import * # noqa
class UserObj:
def __eq__(self, other):
return isinstance(other, UserObj)
def do_map_batches(data):
ds = ray.data.range(1)
ds = ds.map_batches(lambda x: {"output": data})
return ds.take_batch()["output"]
def assert_structure_equals(a, b):
assert type(a) is type(b), (type(a), type(b))
assert type(a[0]) == type(b[0]), (type(a[0]), type(b[0])) # noqa: E721
assert a.dtype == b.dtype
assert a.shape == b.shape
for i in range(len(a)):
assert np.array_equal(a[i], b[i]), (i, a[i], b[i])
def test_list_of_scalars(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [1, 2, 3]
output = do_map_batches(data)
assert_structure_equals(output, np.array([1, 2, 3], dtype=np.int64))
def test_list_of_numpy_scalars(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [np.int64(1), np.int64(2), np.int64(3)]
output = do_map_batches(data)
assert_structure_equals(output, np.array([1, 2, 3], dtype=np.int64))
def test_list_of_objects(ray_start_regular_shared, restore_data_context):
# NOTE: Fallback is enabled by default, this is purely for notational purposes
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = True
data = [1, 2, 3, UserObj()]
output = do_map_batches(data)
assert_structure_equals(output, np.array([1, 2, 3, UserObj()]))
# Define datetime values with different precisions
DATETIME_DAY_PRECISION = datetime(year=2024, month=1, day=1)
DATETIME_HOUR_PRECISION = datetime(year=2024, month=1, day=1, hour=1)
DATETIME_MIN_PRECISION = datetime(year=2024, month=1, day=1, minute=1)
DATETIME_SEC_PRECISION = datetime(year=2024, month=1, day=1, second=1)
DATETIME_MILLISEC_PRECISION = datetime(year=2024, month=1, day=1, microsecond=1000)
DATETIME_MICROSEC_PRECISION = datetime(year=2024, month=1, day=1, microsecond=1)
# Define pandas values for different precisions
PANDAS_DAY_PRECISION = pd.Timestamp(year=2024, month=1, day=1)
PANDAS_HOUR_PRECISION = pd.Timestamp(year=2024, month=1, day=1, hour=1)
PANDAS_MIN_PRECISION = pd.Timestamp(year=2024, month=1, day=1, minute=1)
PANDAS_SEC_PRECISION = pd.Timestamp(year=2024, month=1, day=1, second=1)
PANDAS_MILLISEC_PRECISION = pd.Timestamp(year=2024, month=1, day=1, microsecond=1000)
PANDAS_MICROSEC_PRECISION = pd.Timestamp(year=2024, month=1, day=1, microsecond=1)
PANDAS_NANOSEC_PRECISION = pd.Timestamp(
year=2024, month=1, day=1, hour=1, minute=1, second=1
) + pd.Timedelta(nanoseconds=1)
# Define numpy.datetime64 values for comparison
DATETIME64_DAY_PRECISION = np.datetime64("2024-01-01")
DATETIME64_HOUR_PRECISION = np.datetime64("2024-01-01T01:00", "s")
DATETIME64_MIN_PRECISION = np.datetime64("2024-01-01T00:01", "s")
DATETIME64_SEC_PRECISION = np.datetime64("2024-01-01T00:00:01")
DATETIME64_MILLISEC_PRECISION = np.datetime64("2024-01-01T00:00:00.001")
DATETIME64_MICROSEC_PRECISION = np.datetime64("2024-01-01T00:00:00.000001")
DATETIME64_NANOSEC_PRECISION = np.datetime64("2024-01-01T01:01:01.000000001", "ns")
# Parametrized test to validate datetime values and expected numpy.datetime64 results
@pytest.mark.parametrize(
"data,expected_output",
[
(
[DATETIME_DAY_PRECISION],
np.array([DATETIME64_DAY_PRECISION], dtype="datetime64[s]"),
),
([DATETIME_HOUR_PRECISION], np.array([DATETIME64_HOUR_PRECISION])),
([DATETIME_MIN_PRECISION], np.array([DATETIME64_MIN_PRECISION])),
([DATETIME_SEC_PRECISION], np.array([DATETIME64_SEC_PRECISION])),
([DATETIME_MILLISEC_PRECISION], np.array([DATETIME64_MILLISEC_PRECISION])),
([DATETIME_MICROSEC_PRECISION], np.array([DATETIME64_MICROSEC_PRECISION])),
(
[DATETIME_MICROSEC_PRECISION, DATETIME_MILLISEC_PRECISION],
np.array(
[DATETIME64_MICROSEC_PRECISION, DATETIME64_MILLISEC_PRECISION],
dtype="datetime64[us]",
),
),
(
[DATETIME_SEC_PRECISION, DATETIME_MILLISEC_PRECISION],
np.array(
[DATETIME64_SEC_PRECISION, DATETIME64_MILLISEC_PRECISION],
dtype="datetime64[ms]",
),
),
(
[DATETIME_DAY_PRECISION, DATETIME_SEC_PRECISION],
np.array(
[DATETIME64_DAY_PRECISION, DATETIME64_SEC_PRECISION],
dtype="datetime64[s]",
),
),
(
[PANDAS_DAY_PRECISION],
np.array([DATETIME64_DAY_PRECISION], dtype="datetime64[s]"),
),
([PANDAS_HOUR_PRECISION], np.array([DATETIME64_HOUR_PRECISION])),
([PANDAS_MIN_PRECISION], np.array([DATETIME64_MIN_PRECISION])),
([PANDAS_SEC_PRECISION], np.array([DATETIME64_SEC_PRECISION])),
([PANDAS_MILLISEC_PRECISION], np.array([DATETIME64_MILLISEC_PRECISION])),
([PANDAS_MICROSEC_PRECISION], np.array([DATETIME64_MICROSEC_PRECISION])),
([PANDAS_NANOSEC_PRECISION], np.array([DATETIME64_NANOSEC_PRECISION])),
(
[PANDAS_NANOSEC_PRECISION, PANDAS_MICROSEC_PRECISION],
np.array(
[DATETIME64_NANOSEC_PRECISION, DATETIME64_MICROSEC_PRECISION],
dtype="datetime64[ns]",
),
),
(
[PANDAS_MICROSEC_PRECISION, PANDAS_MILLISEC_PRECISION],
np.array(
[DATETIME64_MICROSEC_PRECISION, DATETIME64_MILLISEC_PRECISION],
dtype="datetime64[us]",
),
),
(
[PANDAS_SEC_PRECISION, PANDAS_MILLISEC_PRECISION],
np.array(
[DATETIME64_SEC_PRECISION, DATETIME64_MILLISEC_PRECISION],
dtype="datetime64[ms]",
),
),
(
[PANDAS_DAY_PRECISION, PANDAS_SEC_PRECISION],
np.array(
[DATETIME64_DAY_PRECISION, DATETIME64_SEC_PRECISION],
dtype="datetime64[s]",
),
),
],
)
def test_list_of_datetimes(
data, expected_output, ray_start_regular_shared, restore_data_context
):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
output = do_map_batches(data)
assert_structure_equals(output, expected_output)
def test_array_like(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = torch.Tensor([1, 2, 3])
output = do_map_batches(data)
assert_structure_equals(output, np.array([1.0, 2.0, 3.0], dtype=np.float32))
def test_list_of_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [np.array([1, 2, 3]), np.array([4, 5, 6])]
output = do_map_batches(data)
assert_structure_equals(output, np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int64))
def test_list_of_array_like(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [torch.Tensor([1, 2, 3]), torch.Tensor([4, 5, 6])]
output = do_map_batches(data)
assert_structure_equals(output, np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
def test_ragged_tensors_map_batches(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [torch.Tensor([1, 2, 3]), torch.Tensor([1, 2])]
output = do_map_batches(data)
assert_structure_equals(
output, create_ragged_ndarray([np.array([1, 2, 3]), np.array([1, 2])])
)
data = [torch.zeros((3, 5, 10)), torch.zeros((3, 8, 8))]
output = do_map_batches(data)
assert_structure_equals(
output, create_ragged_ndarray([np.zeros((3, 5, 10)), np.zeros((3, 8, 8))])
)
def test_scalar_nested_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [[[1]], [[2]]]
output = do_map_batches(data)
assert_structure_equals(
output,
create_ragged_ndarray(
[np.array([1], dtype=np.object_), np.array([2], dtype=np.object_)]
),
)
def test_scalar_lists_not_converted(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [[1, 2], [1, 2]]
output = do_map_batches(data)
assert_structure_equals(
output, create_ragged_ndarray([np.array([1, 2]), np.array([1, 2])])
)
data = [[1, 2, 3], [1, 2]]
output = do_map_batches(data)
assert_structure_equals(
output, create_ragged_ndarray([np.array([1, 2, 3]), np.array([1, 2])])
)
def test_scalar_numpy(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = np.int64(1)
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data})
output = ds.take_batch()["output"]
assert_structure_equals(output, np.array([1, 1], dtype=np.int64))
def test_scalar_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = np.array([1, 2, 3])
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data})
output = ds.take_batch()["output"]
assert_structure_equals(output, np.array([[1, 2, 3], [1, 2, 3]], dtype=np.int64))
def test_bytes(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
"""Tests that bytes are converted to object dtype instead of zero-terminated."""
data = b"\x1a\n\x00\n\x1a"
ds = ray.data.range(1, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data})
output = ds.take_batch()["output"]
assert_structure_equals(output, np.array([b"\x1a\n\x00\n\x1a"], dtype=object))
def test_uniform_tensors(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = torch.Tensor([1, 2, 3])
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data})
output = ds.take_batch()["output"]
assert_structure_equals(output, np.array([[1, 2, 3], [1, 2, 3]], dtype=np.float32))
def test_scalar_ragged_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [np.array([1, 2, 3]), np.array([1, 2])]
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data[x["id"]]})
output = ds.take_batch()["output"]
assert_structure_equals(
output, np.array([np.array([1, 2, 3]), np.array([1, 2])], dtype=object)
)
def test_ragged_tensors(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [torch.Tensor([1, 2, 3]), torch.Tensor([1, 2])]
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data[x["id"]]})
output = ds.take_batch()["output"]
assert_structure_equals(
output, np.array([np.array([1, 2, 3]), np.array([1, 2])], dtype=object)
)
data = [torch.zeros((3, 5, 10)), torch.zeros((3, 8, 8))]
ds = ray.data.range(2, override_num_blocks=1)
ds = ds.map(lambda x: {"output": data[x["id"]]})
output = ds.take_batch()["output"]
assert_structure_equals(
output, create_ragged_ndarray([np.zeros((3, 5, 10)), np.zeros((3, 8, 8))])
)
def test_nested_ragged_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [
{"a": [[1], [2, 3]]},
{"a": [[4, 5], [6]]},
]
def f(row):
return data[row["id"]]
output = ray.data.range(2).map(f).take_all()
assert output == data
# https://github.com/ray-project/ray/issues/35340
def test_complex_ragged_arrays(ray_start_regular_shared, restore_data_context):
# Disable (automatic) fallback to `ArrowPythonObjectType` extension type
DataContext.get_current().enable_fallback_to_arrow_object_ext_type = False
data = [[{"a": 1}, {"a": 2}, {"a": 3}], [{"b": 1}]]
output = do_map_batches(data)
# Assert resulting objects are coerced to appropriate shape, following
# table's schema
assert_structure_equals(
output,
create_ragged_ndarray(
[
np.array(
[{"a": 1, "b": None}, {"a": 2, "b": None}, {"a": 3, "b": None}]
),
np.array([{"a": None, "b": 1}]),
]
),
)
data = ["hi", 1, None, [[[[]]]], {"a": [[{"b": 2, "c": UserObj()}]]}, UserObj()]
output = do_map_batches(data)
assert_structure_equals(output, create_ragged_ndarray(data))
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))