Skip to content
Open
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
72 changes: 71 additions & 1 deletion docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,75 @@ included:
On Riviera-PRO/Active-HDL (VHPI), only the default session is supported:
passing any other session fails with a clear error.

.. _python_bridge:instance_sessions:

Independent models of component instances
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The bridge runs one Python interpreter per simulation, so a component with a
Python model that is instantiated more than once needs a session per instance
to give every instance a model state of its own. Without sessions, or with a
session made from a fixed name, all instances execute the model in the same
namespace and overwrite or share its functions and variables. The
``'instance_name`` attribute gives every instance a distinct identity to make
the session from:

.. code-block:: vhdl

architecture python of accumulator_model is
begin
model : process is
variable session : python_session_t;
begin
session := new_session(accumulator_model'instance_name);

wait on x;
exec_file(model_file, session);

loop
y <= call("accumulate", arg(x), session => session);
wait on x;
end loop;
end process;
end architecture;

Every instance executes the same file in its own session and passes that
session to every operation. The session is made in the statement part of the
process since GHDL leaves the instance labels out of ``'instance_name`` in a
declaration, which would give all instances the same session. A relative ``model_file`` is relative to the
testbench file (``tb_path``), which is only known after
``test_runner_setup``, so the file is executed when the first input arrives
rather than at the start of the simulation. The ``Test independent models of
two instances`` test case of the `embedded_python example
<https://github.com/VUnit/vunit-python-bridge/tree/main/examples/embedded_python>`__
drives two instances of this component, ``accumulator_model.vhd``, with
different inputs and checks that each keeps a total of its own.

An alternative is to define the model as a class and create one object per
session, keeping the mutable model state on ``self`` rather than in globals:

.. code-block:: python

# accumulator.py
class Accumulator:
def __init__(self):
self.total = 0

def accumulate(self, x):
self.total += x
return self.total

.. code-block:: vhdl

import_module_from_file(join(tb_path(runner_cfg), "accumulator.py"), "accumulator", session);
exec("model = accumulator.Accumulator()", session);

y <= call("model.accumulate", arg(x), session => session);

The module, and so the class, is shared by all sessions (see the caveats
below), while every session has a ``model`` object of its own. Like all
non-default sessions, this needs the Python bridge: NVC, GHDL or Questa.

Caveats
~~~~~~~

Expand All @@ -181,7 +250,8 @@ functions, classes and variables, are separate. Everything else is shared:
imported in one session is the same module object in another session, so
changes to module state, such as ``np.random.seed(...)`` or attributes set
on a module, are visible in all sessions. The same applies to sibling
modules imported by a Python file executed in several sessions.
modules imported by a Python file executed in several sessions. State kept
in the globals of an imported module is therefore not isolated by sessions.
* ``sys.path``, ``sys.modules``, environment variables, the current directory
and open files are process wide.
* Only the default session runs in ``__main__``. Classes defined in other
Expand Down
21 changes: 21 additions & 0 deletions examples/embedded_python/accumulator_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com

"""
The behaviour of the accumulator_model component in tb_example.vhd. Every
instance executes this file in a session of its own and so gets a total of its own.
"""

total = 0


def accumulate(x):
"""
Add x to the total and return the new total.
"""
global total # pylint: disable=global-statement
total += x
return total
43 changes: 43 additions & 0 deletions examples/embedded_python/accumulator_model.vhd
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
-- You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com
--
-- A component with a stateful Python model. Every instance runs the model in a
-- session of its own, so the instances do not share the state of the model.

library vunit_lib;

library python_bridge;
context python_bridge.python_context;

entity accumulator_model is
generic(model_file : string);
port(
x : in integer;
y : out integer
);
end entity;

architecture python of accumulator_model is
begin
model : process is
variable session : python_session_t;
begin
-- A fixed name would give all instances the same session, while the instance
-- name is a distinct identity for every instance. GHDL drops the instance
-- labels from 'instance_name in a declaration, so it is taken here.
session := new_session(accumulator_model'instance_name);

-- The first input comes after test_runner_setup, which a model_file relative
-- to the testbench needs
wait on x;
exec_file(model_file, session);

loop
y <= call("accumulate", arg(x), session => session);
wait on x;
end loop;
end process;
end architecture;
5 changes: 3 additions & 2 deletions examples/embedded_python/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
``unsigned``/``signed`` and ``std_ulogic`` argument values, an
``integer_array_t`` image shared with NumPy, Python files executed with
``exec_file`` or imported with ``import_module_from_file``, two models loaded
into a session each, error reporting, and ``python_model``, a verification
component whose behaviour is a Python function. Some tests need Python
into a session each, error reporting, ``python_model``, a verification
component whose behaviour is a Python function, and ``accumulator_model``, a
component instantiated twice with a Python model state per instance. Some tests need Python
packages the bridge does not depend on (``PySimpleGUI``, ``python-constraint``,
``crccheck`` and ``matplotlib``); the run script says which when they are
missing. Three tests demonstrate error reporting and fail by design. See the
Expand Down
24 changes: 24 additions & 0 deletions examples/embedded_python/tb_example.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ architecture tb of tb_example is
-- Ports of the python_model component
signal model_x : integer := 0;
signal model_y : integer;

-- Ports of the two accumulator_model instances
signal acc_a_x, acc_b_x : integer := 0;
signal acc_a_y, acc_b_y : integer;
begin
test_runner : process
constant pi : real := 3.141592653589793;
Expand Down Expand Up @@ -690,6 +694,17 @@ begin
check_equal(model_y, 2 * idx + 1, result("for the output of the model"));
end loop;

elsif run("Test independent models of two instances") then
-- Both accumulator_model instances execute the same Python file, each in a
-- session of its own, so each keeps a total of its own
for idx in 1 to 3 loop
acc_a_x <= idx;
acc_b_x <= 10 * idx;
wait for clk_period;
check_equal(acc_a_y, idx * (idx + 1) / 2, result("for the total of instance a"));
check_equal(acc_b_y, 10 * idx * (idx + 1) / 2, result("for the total of instance b"));
end loop;


---------------------------------------------------------------------
-- Examples of string manipulation
Expand Down Expand Up @@ -772,4 +787,13 @@ begin
python_model_inst : entity work.python_model
generic map(model_file => join(tb_path(runner_cfg), "python_model.py"))
port map(x => model_x, y => model_y);

-- Two instances of a component with a stateful Python model
acc_a_inst : entity work.accumulator_model
generic map(model_file => "accumulator_model.py")
port map(x => acc_a_x, y => acc_a_y);

acc_b_inst : entity work.accumulator_model
generic map(model_file => "accumulator_model.py")
port map(x => acc_b_x, y => acc_b_y);
end;
Loading