diff --git a/docs/types.rst b/docs/types.rst index b3caeb4..165b2ee 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -37,17 +37,22 @@ integer_array_t and NumPy An ``integer_array_t`` argument is transferred to Python and referred to by the expression as a NumPy array of dtype ``int32``, so it can be reused in -several calls. The shape follows VUnit's indexing so that elements correspond +several calls. It is indexed like VUnit, so that elements correspond directly: ============= ================================ =========================== Array VHDL Python ============= ================================ =========================== 1D ``get(a, i)`` ``a[i]``, shape ``(length,)`` -2D ``get(a, x, y)`` ``a[y, x]``, shape ``(height, width)`` -3D ``get(a, x, y, z)`` ``a[y, x, z]``, shape ``(height, width, depth)`` +2D ``get(a, x, y)`` ``a[x, y]``, shape ``(width, height)`` +3D ``get(a, x, y, z)`` ``a[x, y, z]``, shape ``(width, height, depth)`` ============= ================================ =========================== +Image libraries such as Pillow and OpenCV, and ``numpy.loadtxt`` of a CSV file +written by VUnit's ``save_csv``, index rows first, ``[y, x]``. Swap the first two +axes of such an array with ``a.swapaxes(0, 1)``, or ``a.T`` for a 2D array, before +returning it to VHDL, and of an argument before passing it to them. + ``integer_array_t`` does not record its number of dimensions. An argument is treated as 3D if its depth is larger than one, else as 2D if its height is larger than one, else as 1D. diff --git a/examples/embedded_python/image_model.py b/examples/embedded_python/image_model.py index 70913f9..ff5bc7e 100644 --- a/examples/embedded_python/image_model.py +++ b/examples/embedded_python/image_model.py @@ -20,6 +20,6 @@ def transpose(image): def pixel(image, x, y): """ - The pixel in column x of row y. VHDL get(a, x, y) is Python a[y, x]. + The pixel in column x of row y. VHDL get(a, x, y) is Python a[x, y]. """ - return int(image[y, x]) + return int(image[x, y]) diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index 9a200f7..6cccf63 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -600,7 +600,7 @@ begin elsif run("Test NumPy arrays both ways") then -- integer_array_t is the VUnit type for data sets such as images and it is passed -- to Python as a NumPy array of the same shape. An array returned by Python - -- becomes a new integer_array_t. The indexing matches: get(a, x, y) is a[y, x]. + -- becomes a new integer_array_t. The indexing matches: get(a, x, y) is a[x, y]. import_module_from_file("image_model.py", "image_model"); -- /image_model.py -- A 4 x 3 image with the column in the ones and the row in the tens diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index ab7fb01..88a2f67 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1,5 +1,6 @@ ## Changes +- **Breaking:** an `integer_array_t` is indexed in NumPy like in VUnit, `get(a, x, y, z)` being `a[x, y, z]` with shape `(width, height, depth)`, instead of `a[y, x, z]` with shape `(height, width, depth)`. This applies to arguments and returned arrays. Code indexing 2D or 3D arrays in Python must swap its first two indices, and an array from an image library, which index rows first, needs `a.swapaxes(0, 1)`. - `import_module_from_file` resolves a relative path against the directory of the testbench file, like `exec_file`, rather than against the working directory of the simulator. It must be called after `test_runner_setup`. Callers that relied on a path relative to the working directory must pass an absolute path instead. See #7. - A relative file name given to `exec_file` or `import_module_from_file` before `test_runner_setup` has completed is reported as a failure, instead of being taken from the working directory of the simulator. The new `base_path` returns a base directory set with `vunit_python_bridge.set_relative_file_base()` or `VUNIT_PYTHON_BRIDGE_FILE_BASE`, for file names that work at any time, for example in verification components. - Python 3.15 is supported, with a prebuilt bridge DLL for Windows. diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index cf2e044..1231889 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -392,8 +392,8 @@ def stage_value(self, value): @staticmethod def _shape(length, width, height, depth): """ - NumPy shape of an integer_array_t. get(arr, x, y) is a[y, x] and - get(arr, x, y, z) is a[y, x, z], matching VUnit's storage order. + NumPy shape of the storage of an integer_array_t, which VUnit orders + by y, then x, then z. """ if depth > 1: return (height, width, depth) @@ -405,10 +405,13 @@ def make_array( self, storage, length, width, height, depth, bit_width, is_signed ): # pylint: disable=too-many-arguments,too-many-positional-arguments """ - Create the NumPy array for an integer_array_t value. The bridge - fills the storage (native int32) after this call returns. + Create the NumPy array for an integer_array_t value, indexed like + VUnit: get(arr, x, y, z) is a[x, y, z]. The array is a view of the + storage (native int32), which the bridge fills after this call returns. """ array = numpy.frombuffer(storage, dtype=numpy.int32).reshape(self._shape(length, width, height, depth)) + if array.ndim > 1: + array = array.swapaxes(0, 1) self._array_meta[id(array)] = (array, bit_width, bool(is_signed)) return array @@ -605,8 +608,10 @@ def _array_result(self, value, array_meta): f"{low} to {high})" ) - # get(arr, x, y, z) is value[y, x, z] - height, width, depth = (value.shape + (1, 1))[:3] if value.ndim > 1 else (1, value.shape[0], 1) + # get(arr, x, y, z) is value[x, y, z], stored by y, then x, then z + width, height, depth = (value.shape + (1, 1))[:3] if value.ndim > 1 else (value.shape[0], 1, 1) + if value.ndim > 1: + value = value.swapaxes(0, 1) data = numpy.ascontiguousarray(value, dtype=numpy.int32).tobytes() return (0, 0.0, data, (value.size, width, height, depth, bit_width, int(is_signed))) diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 84d1fbc..df5dcae 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -815,7 +815,7 @@ begin --------------------------------------------------------------------- -- integer_array_t --------------------------------------------------------------------- - elsif run("Test that a 2D array preserves axis orientation, get(a, x, y) is row y column x") then + elsif run("Test that a 2D array is indexed like VUnit, get(a, x, y) is a[x, y]") then arr := new_2d(width => 3, height => 2, bit_width => 16, is_signed => false); for y in 0 to 1 loop for x in 0 to 2 loop @@ -824,11 +824,11 @@ begin end loop; exec( "def orientation_ok(a):" + - " return bool(a[0, 1] == 1 and a[1, 2] == 12 and a.shape == (2, 3))" + " return bool(a[1, 0] == 1 and a[2, 1] == 12 and a.shape == (3, 2))" ); check_true(call_boolean("orientation_ok", arg(arr))); - elsif run("Test that a 3D array preserves axis orientation, get(a, x, y, z) is row y column x plane z") then + elsif run("Test that a 3D array is indexed like VUnit, get(a, x, y, z) is a[x, y, z]") then arr := new_3d(width => 3, height => 2, depth => 4, bit_width => 16, is_signed => false); for y in 0 to 1 loop for x in 0 to 2 loop @@ -839,17 +839,17 @@ begin end loop; exec( "def orientation_ok3(a):" + - " return bool(a[0, 1, 2] == 12 and a[1, 2, 3] == 123 and a.shape == (2, 3, 4))" + " return bool(a[1, 0, 2] == 12 and a[2, 1, 3] == 123 and a.shape == (3, 2, 4))" ); check_true(call_boolean("orientation_ok3", arg(arr))); - elsif run("Test that returned 2D and 3D arrays preserve axis orientation") then + elsif run("Test that returned 2D and 3D arrays are indexed like VUnit") then exec( "import numpy as np" + "def make2():" + - " return np.array([[10 * y + x for x in range(3)] for y in range(2)], dtype=np.int16)" + + " return np.array([[10 * y + x for y in range(2)] for x in range(3)], dtype=np.int16)" + "def make3():" + - " return np.fromfunction(lambda y, x, z: 100 * y + 10 * x + z, (2, 3, 4), dtype=int)" + " return np.fromfunction(lambda x, y, z: 100 * y + 10 * x + z, (3, 2, 4), dtype=int)" ); result := call_integer_array("make2"); check_equal(width(result), 3); @@ -938,11 +938,12 @@ begin check_equal(length(result), 6); check_equal(bit_width(result), 32); check_true(is_signed(result)); + -- NumPy flattens a[x, y] x first check_equal(get(result, 0), 0); - check_equal(get(result, 1), 1); - check_equal(get(result, 2), 2); - check_equal(get(result, 3), 10); - check_equal(get(result, 4), 11); + check_equal(get(result, 1), 10); + check_equal(get(result, 2), 1); + check_equal(get(result, 3), 11); + check_equal(get(result, 4), 2); check_equal(get(result, 5), 12); elsif run("Test null array round trip") then