Skip to content
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
11 changes: 8 additions & 3 deletions docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/embedded_python/image_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
2 changes: 1 addition & 1 deletion examples/embedded_python/tb_example.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -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"); -- <testbench directory>/image_model.py

-- A 4 x 3 image with the column in the ones and the row in the tens
Expand Down
1 change: 1 addition & 0 deletions release_notes/unreleased.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
17 changes: 11 additions & 6 deletions src/vunit_python_bridge/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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)))
Expand Down
23 changes: 12 additions & 11 deletions tests/tb_python_pkg_bridge.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading