diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 47d731d..1c93f73 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -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 +`__ +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 ~~~~~~~ @@ -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 diff --git a/examples/embedded_python/accumulator_model.py b/examples/embedded_python/accumulator_model.py new file mode 100644 index 0000000..8c7311a --- /dev/null +++ b/examples/embedded_python/accumulator_model.py @@ -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 diff --git a/examples/embedded_python/accumulator_model.vhd b/examples/embedded_python/accumulator_model.vhd new file mode 100644 index 0000000..a7eec80 --- /dev/null +++ b/examples/embedded_python/accumulator_model.vhd @@ -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; diff --git a/examples/embedded_python/run.py b/examples/embedded_python/run.py index 1042d6d..988f244 100644 --- a/examples/embedded_python/run.py +++ b/examples/embedded_python/run.py @@ -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 diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index df63c80..593ded8 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -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; @@ -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 @@ -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;