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
165 changes: 81 additions & 84 deletions spynnaker/pyNN/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,18 @@
import logging
import os
from typing import (
Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type,
TypedDict, Union, cast)
Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union, cast)

import numpy as __numpy
from typing_extensions import Literal
from numpy.typing import NDArray

from pyNN import common as pynn_common
from pyNN.common import control as _pynn_control
from pyNN.recording import get_io
from pyNN.random import NumpyRNG
from pyNN.space import (
Space, Line, Grid2D, Grid3D, Cuboid, Sphere, RandomStructure)
from pyNN.space import distance as _pynn_distance
from neo import Block

from spinn_utilities.exceptions import SimulatorNotSetupException
from spinn_utilities.log import FormatAdapter
Expand All @@ -56,7 +53,7 @@
import spynnaker.pyNN as _sim # pylint: disable=import-self

from spynnaker.pyNN.exceptions import SpynnakerException

from spynnaker.pyNN.models.common.types import Names
from spynnaker.pyNN.random_distribution import RandomDistribution
from spynnaker.pyNN.data import SpynnakerDataView
from spynnaker.pyNN.models.abstract_pynn_model import AbstractPyNNModel
Expand Down Expand Up @@ -205,30 +202,6 @@
'get_max_delay', 'initialize', 'list_standard_models', 'name',
'record', "get_machine"]


class __PynnOperations(TypedDict, total=False):
run: Callable[[float, Any], float]
run_until: Callable[[float, Any], float]
get_current_time: Callable[[], float]
get_time_step: Callable[[], float]
get_max_delay: Callable[[], int]
get_min_delay: Callable[[], int]
num_processes: Callable[[], int]
rank: Callable[[], int]
reset: Callable[[Dict[str, Any]], None]
create: Callable[
[Union[Type, AbstractPyNNModel], Optional[Dict[str, Any]], int],
Population]
connect: Callable[
[Population, Population, float, Optional[float], Optional[str], int,
Optional[NumpyRNG]], None]
record: Callable[
[Union[str, Sequence[str]], PopulationBase, str, Optional[float],
Optional[Dict[str, Any]]], Block]


# Dynamically-extracted operations from PyNN
__pynn: __PynnOperations = {}
# Cache of the simulator created by setup
__simulator: Optional[SpiNNaker] = None

Expand Down Expand Up @@ -312,9 +285,6 @@ def setup(timestep: Optional[Union[float, Literal["auto"]]] = None,
logger.warning(
"max_delay is not supported by sPyNNaker so will be ignored")

# setup PyNN common stuff
pynn_common.setup(timestep, min_delay, **extra_params)

# create stuff simulator
if SpynnakerDataView.is_setup():
logger.warning("Calling setup a second time causes the previous "
Expand All @@ -340,8 +310,6 @@ def setup(timestep: Optional[Union[float, Literal["auto"]]] = None,
logger.warning("Extra params {} have been applied to the setup "
"command which we do not consider", extra_params)

# get overloaded functions from PyNN in relation of our simulator object
_create_overloaded_functions(__simulator)
SpynnakerDataView.add_database_socket_addresses(database_socket_addresses)
return rank()

Expand Down Expand Up @@ -385,32 +353,6 @@ def Projection(
partition_id=partition_id)


def _create_overloaded_functions(spinnaker_simulator: SpiNNaker) -> None:
"""
Creates functions that the main PyNN interface supports
(given from PyNN)

:param spinnaker_simulator: the simulator object we use underneath
"""
# overload the failed ones with now valid ones, now that we're in setup
# phase.
__pynn["run"], __pynn["run_until"] = pynn_common.build_run(
spinnaker_simulator)

__pynn["get_current_time"], __pynn["get_time_step"], \
__pynn["get_min_delay"], __pynn["get_max_delay"], \
__pynn["num_processes"], __pynn["rank"] = \
pynn_common.build_state_queries(spinnaker_simulator)

__pynn["reset"] = pynn_common.build_reset(spinnaker_simulator)
__pynn["create"] = pynn_common.build_create(Population)

__pynn["connect"] = pynn_common.build_connect(
Projection, FixedProbabilityConnector, StaticSynapse)

__pynn["record"] = pynn_common.build_record(spinnaker_simulator)


def end(_: Any = True) -> None:
"""
Cleans up the SpiNNaker machine and software
Expand Down Expand Up @@ -520,7 +462,7 @@ def set_allow_delay_extensions(


def connect(pre: Population, post: Population, weight: float = 0.0,
delay: Optional[float] = None, receptor_type: Optional[str] = None,
delay: Optional[float] = None, receptor_type: str = "excitatory",
p: int = 1, rng: Optional[NumpyRNG] = None) -> None:
"""
Builds a projection.
Expand All @@ -531,10 +473,20 @@ def connect(pre: Population, post: Population, weight: float = 0.0,
:param delay: the delay of the connections
:param receptor_type: excitatory / inhibitory
:param p: probability
:param rng: random number generator
:param rng: random number generator (ignored)
"""
SpynnakerDataView.check_user_can_act()
__pynn["connect"](pre, post, weight, delay, receptor_type, p, rng)
if isinstance(pre, IDMixin):
pre = pre.as_view()
if isinstance(post, IDMixin):
post = post.as_view()
connector = FixedProbabilityConnector(p_connect=p)
synapse = StaticSynapse(weight=weight, delay=delay)
if rng is not None:
warn_once(
logger, "The rng argument to connect is ignored in sPyNNaker.")
Projection(pre, post, connector, receptor_type=receptor_type,
Comment on lines 463 to +488
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect() is documented as returning a new Projection, but the function signature is -> None and the created Projection(...) is not returned. Either return the created Projection (and update the return type accordingly) or remove the :returns: line from the docstring to avoid misleading API users.

Copilot uses AI. Check for mistakes.
synapse_type=synapse)


def create(
Expand All @@ -550,7 +502,7 @@ def create(
:returns: A new Population
"""
SpynnakerDataView.check_user_can_act()
return __pynn["create"](cellclass, cellparams, n)
return Population(n, cellclass, cellparams)


def NativeRNG(seed_value: Union[int, List[int], NDArray]) -> None:
Expand All @@ -569,21 +521,23 @@ def get_current_time() -> float:
:return: returns the current time
"""
SpynnakerDataView.check_user_can_act()
return __pynn["get_current_time"]()
assert __simulator is not None
return __simulator.t


def get_min_delay() -> int:
def get_min_delay() -> float:
"""
The minimum allowed synaptic delay; delays will be clamped to be at
least this.

:return: returns the min delay of the simulation
"""
SpynnakerDataView.check_user_can_act()
return __pynn["get_min_delay"]()
assert __simulator is not None
return __simulator.dt


def get_max_delay() -> int:
def get_max_delay() -> float:
"""
Part of the PyNN API but does not make sense for sPyNNaker as
different Projection, Vertex splitter combination could have different
Expand All @@ -605,7 +559,8 @@ def get_time_step() -> float:
:return: get the time step of the simulation (in ms)
"""
SpynnakerDataView.check_user_can_act()
return float(__pynn["get_time_step"]())
assert __simulator is not None
return __simulator.dt


def initialize(cells: PopulationBase, **initial_values: Any) -> None:
Expand All @@ -616,7 +571,8 @@ def initialize(cells: PopulationBase, **initial_values: Any) -> None:
:param initial_values: the parameters and their values to change
"""
SpynnakerDataView.check_user_can_act()
pynn_common.initialize(cells, **initial_values)
assert isinstance(cells, (Population, Assembly)), type(cells)
cells.initialize(**initial_values)


def num_processes() -> int:
Expand All @@ -628,8 +584,7 @@ def num_processes() -> int:

:return: the number of MPI processes
"""
SpynnakerDataView.check_user_can_act()
return __pynn["num_processes"]()
return 1


def rank() -> int:
Expand All @@ -641,13 +596,12 @@ def rank() -> int:

:return: MPI rank
"""
SpynnakerDataView.check_user_can_act()
return __pynn["rank"]()
return 0


def record(variables: Union[str, Sequence[str]], source: PopulationBase,
def record(variables: Names, source: PopulationBase,
filename: str, sampling_interval: Optional[float] = None,
annotations: Optional[Dict[str, Any]] = None) -> Block:
annotations: Optional[Dict[str, Any]] = None) -> None:
"""
Sets variables to be recorded.

Expand All @@ -659,11 +613,15 @@ def record(variables: Union[str, Sequence[str]], source: PopulationBase,
:param sampling_interval:
how often to sample the recording, not ignored so far
:param annotations: the annotations to data writers
:return: neo object
"""
SpynnakerDataView.check_user_can_act()
return __pynn["record"](variables, source, filename, sampling_interval,
annotations)
if not isinstance(source, (Population, Assembly)):
if isinstance(source, (IDMixin)):
source = source.as_view()
source.record(variables, to_file=filename,
sampling_interval=sampling_interval)
if annotations:
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If record() is called with an IDMixin or PopulationView, source may be a PopulationView after as_view(), but PopulationView doesn’t implement annotate(). With annotations provided this will raise AttributeError. Consider adding an annotate() method to PopulationBase/PopulationView, or handling view annotations here without calling a missing method.

Suggested change
if annotations:
if annotations and hasattr(source, "annotate"):

Copilot uses AI. Check for mistakes.
source.annotate(**annotations)
Comment on lines 601 to +624
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record() is annotated/documented to return a neo.Block, but it never returns anything. Either return the created/extracted block (PyNN API expectation) or update the return type and docstring to indicate None is returned.

Copilot uses AI. Check for mistakes.


def reset(annotations: Optional[Dict[str, Any]] = None) -> None:
Expand All @@ -674,11 +632,49 @@ def reset(annotations: Optional[Dict[str, Any]] = None) -> None:
"""
if annotations is None:
annotations = {}
SpynnakerDataView.check_user_can_act()
__pynn["reset"](annotations)
assert __simulator is not None
__simulator.reset()


def _run_until(time_point: float,
callbacks: Optional[List[Callable]] = None) -> float:
"""
Advance the simulation until a given time.

def run(simtime: float, callbacks: Optional[Callable] = None) -> float:
:param time_point: the time to run until (in ms)
:param callbacks: an optional list of callback functions, each of which
accepts the current time as an argument, and returns the next time it
wishes to be called.
:returns: the actual simulation time that the simulation stopped at
"""
assert __simulator is not None
now = __simulator.t
# allow for floating point error
if time_point - now < -__simulator.dt / 2.0:
raise ValueError(
f"Time {time_point} is in the past (current time {now})")
if callbacks:
callback_events = [(callback(__simulator.t), callback)
for callback in callbacks]
while __simulator.t + 1e-9 < time_point:
callback_events.sort(key=lambda cbe: cbe[0], reverse=True)
nxt, callback = callback_events.pop()
# collapse multiple events that happen within the same timestep
active_callbacks = [callback]
while (len(callback_events) > 0 and
abs(nxt - callback_events[-1][0]) < __simulator.dt):
active_callbacks.append(callback_events.pop()[1])

nxt = min(nxt, time_point)
__simulator.run_until(nxt)
callback_events.extend((callback(__simulator.t), callback)
for callback in active_callbacks)
else:
__simulator.run_until(time_point)
return __simulator.t


def run(simtime: float, callbacks: Optional[List[Callable]] = None) -> float:
"""
The run() function advances the simulation for a given number of
milliseconds.
Expand All @@ -688,7 +684,8 @@ def run(simtime: float, callbacks: Optional[Callable] = None) -> float:
:return: the actual simulation time that the simulation stopped at
"""
SpynnakerDataView.check_user_can_act()
return __pynn["run"](simtime, callbacks)
assert __simulator is not None
return _run_until(__simulator.t + simtime, callbacks)


# left here because needs to be done, and no better place to put it
Expand All @@ -704,7 +701,7 @@ def run_until(tstop: float) -> float:
:return: the actual simulation time that the simulation stopped at
"""
SpynnakerDataView.check_user_can_act()
return __pynn["run_until"](tstop, None)
return _run_until(tstop, None)


def get_machine() -> Machine:
Expand Down
3 changes: 2 additions & 1 deletion spynnaker/pyNN/config_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import os
from typing import Set

from spinn_utilities.config_holder import clear_cfg_files
from spinn_utilities.config_holder import clear_cfg_files, set_config
from spinn_front_end_common.interface.config_setup import (
add_default_cfg, add_spinnaker_cfg)
from spinn_front_end_common.interface.config_setup import (
Expand Down Expand Up @@ -45,6 +45,7 @@ def unittest_setup() -> None:
add_spynnaker_cfg()
SpynnakerDataWriter.mock()
AbstractPyNNNeuronModel.reset_all()
set_config("Reports", "write_pynn_report", "False")


def add_spynnaker_cfg() -> None:
Expand Down
28 changes: 26 additions & 2 deletions spynnaker/pyNN/data/spynnaker_data_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
# limitations under the License.
from __future__ import annotations
import logging
from typing import Iterator, Optional, Set, Tuple, TYPE_CHECKING
from typing import Iterator, Optional, Set, Tuple, TYPE_CHECKING, Any

from spinn_utilities.log import FormatAdapter
from spinn_utilities.config_holder import get_config_bool, get_report_path

from spinn_front_end_common.data import FecDataView

Expand Down Expand Up @@ -51,7 +52,8 @@ class _SpynnakerDataModel(object):
"_id_counter",
"_min_delay",
"_populations",
"_projections")
"_projections",
"_pynn_report")

def __new__(cls) -> '_SpynnakerDataModel':
if cls.__singleton is not None:
Expand All @@ -70,6 +72,7 @@ def _clear(self) -> None:
# Using a dict to verify if later could be stored here only
self._populations: Set[Population] = set()
self._projections: Set[Projection] = set()
self._pynn_report: Optional[str] = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not cache cfg data


def _hard_reset(self) -> None:
"""
Expand Down Expand Up @@ -233,3 +236,24 @@ def get_sim_name(cls) -> str:
:returns: The name to be returned by `pyNN.spiNNaker.name`.
"""
return _version.NAME

@classmethod
def write_pynn_report(cls, text: str, *args: Any,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in a utils / reports cfile not in a Data file

**kwargs: Any) -> None:
"""
Writes text to the PyNN report file, or does nothing if the report is
disabled.

:param text: The text to write to the report file.
:param args: Any additional arguments to format into the text using
`str.format`.
:param kwargs: Any additional keyword arguments to format into the text
using `str.format`.
"""
if not get_config_bool("Reports", "write_pynn_report"):
return
if cls.__spy_data._pynn_report is None:
cls.__spy_data._pynn_report = get_report_path("path_pynn_report")
with open(cls.__spy_data._pynn_report, "a", encoding="utf-8") as f:
f.write(text.format(*args, **kwargs))
f.write("\n")
Loading
Loading