• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brian-team / brian2 / 18596953619

17 Oct 2025 03:13PM UTC coverage: 92.099% (-0.09%) from 92.187%
18596953619

push

github

web-flow
Merge pull request #1687 from brian-team/gsl_fixes

Fix GSL for C++ standalone mode

2513 of 2652 branches covered (94.76%)

14885 of 16162 relevant lines covered (92.1%)

2.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.14
/brian2/codegen/codeobject.py
1
"""
2
Module providing the base `CodeObject` and related functions.
3
"""
4

5
import collections
2✔
6
import copy
2✔
7
import platform
2✔
8
from abc import ABC, abstractmethod
2✔
9

10
from brian2.core.base import weakproxy_with_fallback
2✔
11
from brian2.core.functions import DEFAULT_FUNCTIONS, Function
2✔
12
from brian2.core.names import Nameable
2✔
13
from brian2.equations.unitcheck import check_units_statements
2✔
14
from brian2.utils.logger import get_logger
2✔
15
from brian2.utils.stringtools import code_representation, indent
2✔
16

17
from .translation import analyse_identifiers
2✔
18

19
__all__ = ["CodeObject", "constant_or_scalar"]
2✔
20

21
logger = get_logger(__name__)
2✔
22

23

24
#: Dictionary with basic information about the current system (OS, etc.)
25
sys_info = {
2✔
26
    "system": platform.system(),
27
    "architecture": platform.architecture(),
28
    "machine": platform.machine(),
29
}
30

31

32
def constant_or_scalar(varname, variable):
2✔
33
    """
34
    Convenience function to generate code to access the value of a variable.
35
    Will return ``'varname'`` if the ``variable`` is a constant, and
36
    ``array_name[0]`` if it is a scalar array.
37
    """
38
    from brian2.devices.device import get_device  # avoid circular import
4✔
39

40
    if variable.array:
4✔
41
        return f"{get_device().get_array_name(variable)}[0]"
4✔
42
    else:
43
        return f"{varname}"
4✔
44

45

46
class CodeObject(Nameable, ABC):
2✔
47
    """
48
    Executable code object.
49

50
    The ``code`` can either be a string or a
51
    `brian2.codegen.templates.MultiTemplate`.
52

53
    After initialisation, the code is compiled with the given namespace
54
    using ``code.compile(namespace)``.
55

56
    Calling ``code(key1=val1, key2=val2)`` executes the code with the given
57
    variables inserted into the namespace.
58
    """
59

60
    #: The `CodeGenerator` class used by this `CodeObject`
61
    generator_class = None
2✔
62
    #: A short name for this type of `CodeObject`
63
    class_name = None
2✔
64

65
    def __init__(
2✔
66
        self,
67
        owner,
68
        code,
69
        variables,
70
        variable_indices,
71
        template_name,
72
        template_source,
73
        compiler_kwds,
74
        name="codeobject*",
75
    ):
76
        Nameable.__init__(self, name=name)
4✔
77
        self.owner = weakproxy_with_fallback(owner)
4✔
78
        self.code = code
4✔
79
        self.compiled_code = {}
4✔
80
        self.variables = variables
4✔
81
        self.variable_indices = variable_indices
4✔
82
        self.template_name = template_name
4✔
83
        self.template_source = template_source
4✔
84
        self.compiler_kwds = compiler_kwds
4✔
85

86
    def __getstate__(self):
2✔
87
        state = self.__dict__.copy()
×
88
        state["owner"] = self.owner.__repr__.__self__
×
89
        # Replace Function objects for standard functions by their name
90
        state["variables"] = self.variables.copy()
×
91
        for k, v in state["variables"].items():
×
92
            if isinstance(v, Function) and v is DEFAULT_FUNCTIONS[k]:
×
93
                state["variables"][k] = k
×
94
        return state
×
95

96
    def __setstate__(self, state):
2✔
97
        state["owner"] = weakproxy_with_fallback(state["owner"])
×
98
        for k, v in state["variables"].items():
×
99
            if isinstance(v, str):
×
100
                state["variables"][k] = DEFAULT_FUNCTIONS[k]
×
101
        self.__dict__ = state
×
102

103
    @classmethod
2✔
104
    def is_available(cls):
2✔
105
        """
106
        Whether this target for code generation is available. Should use a
107
        minimal example to check whether code generation works in general.
108
        """
109
        return True
×
110

111
    def update_namespace(self):
2✔
112
        """
113
        Update the namespace for this timestep. Should only deal with variables
114
        where *the reference* changes every timestep, i.e. where the current
115
        reference in `namespace` is not correct.
116
        """
117
        pass
×
118

119
    @abstractmethod
2✔
120
    def compile_block(self, block): ...
2✔
121

122
    def compile(self):
2✔
123
        for block in ["before_run", "run", "after_run"]:
4✔
124
            self.compiled_code[block] = self.compile_block(block)
4✔
125

126
    def __call__(self, **kwds):
2✔
127
        self.update_namespace()
4✔
128
        self.namespace.update(**kwds)
4✔
129

130
        return self.run()
4✔
131

132
    @abstractmethod
2✔
133
    def run_block(self, block): ...
2✔
134

135
    def before_run(self):
2✔
136
        """
137
        Runs the preparation code in the namespace. This code will only be
138
        executed once per run.
139

140
        Returns
141
        -------
142
        return_value : dict
143
            A dictionary with the keys corresponding to the `output_variables`
144
            defined during the call of `CodeGenerator.code_object`.
145
        """
146
        return self.run_block("before_run")
4✔
147

148
    def run(self):
2✔
149
        """
150
        Runs the main code in the namespace.
151

152
        Returns
153
        -------
154
        return_value : dict
155
            A dictionary with the keys corresponding to the `output_variables`
156
            defined during the call of `CodeGenerator.code_object`.
157
        """
158
        return self.run_block("run")
4✔
159

160
    def after_run(self):
2✔
161
        """
162
        Runs the finalizing code in the namespace. This code will only be
163
        executed once per run.
164

165
        Returns
166
        -------
167
        return_value : dict
168
            A dictionary with the keys corresponding to the `output_variables`
169
            defined during the call of `CodeGenerator.code_object`.
170
        """
171
        return self.run_block("after_run")
4✔
172

173

174
def _error_msg(code, name):
2✔
175
    """
176
    Little helper function for error messages.
177
    """
178
    error_msg = f"Error generating code for code object '{name}' "
2✔
179
    code_lines = [line for line in code.split("\n") if len(line.strip())]
2✔
180
    # If the abstract code is only one line, display it in full
181
    if len(code_lines) <= 1:
2✔
182
        error_msg += f"from this abstract code: '{code_lines[0]}'\n"
2✔
183
    else:
184
        error_msg += (
×
185
            f"from {len(code_lines)} lines of abstract code, first line is: "
186
            "'code_lines[0]'\n"
187
        )
188
    return error_msg
2✔
189

190

191
def check_compiler_kwds(compiler_kwds, accepted_kwds, target):
2✔
192
    """
193
    Internal function to check the provided compiler keywords against the list
194
    of understood keywords.
195

196
    Parameters
197
    ----------
198
    compiler_kwds : dict
199
        Dictionary of compiler keywords and respective list of values.
200
    accepted_kwds : list of str
201
        The compiler keywords understood by the code generation target
202
    target : str
203
        The name of the code generation target (used for the error message).
204

205
    Raises
206
    ------
207
    ValueError
208
        If a compiler keyword is not understood
209
    """
210
    for key in compiler_kwds:
4✔
211
        if key not in accepted_kwds:
4✔
212
            formatted_kwds = ", ".join(f"'{kw}'" for kw in accepted_kwds)
4✔
213
            raise ValueError(
4✔
214
                f"The keyword argument '{key}' is not understood by "
215
                f"the code generation target '{target}'. The valid "
216
                f"arguments are: {formatted_kwds}."
217
            )
218

219

220
def _merge_compiler_kwds(list_of_kwds):
2✔
221
    """
222
    Merges a list of keyword dictionaries. Values in these dictionaries are
223
    lists of values, the merged dictionaries will contain the concatenations
224
    of lists specified for the same key.
225

226
    Parameters
227
    ----------
228
    list_of_kwds : list of dict
229
        A list of compiler keyword dictionaries that should be merged.
230

231
    Returns
232
    -------
233
    merged_kwds : dict
234
        The merged dictionary
235
    """
236
    merged_kwds = collections.defaultdict(list)
4✔
237
    for kwds in list_of_kwds:
4✔
238
        for key, values in kwds.items():
4✔
239
            if not isinstance(values, list):
4✔
240
                raise TypeError(
4✔
241
                    f"Compiler keyword argument '{key}' requires a list of values."
242
                )
243
            merged_kwds[key].extend(values)
4✔
244
    return merged_kwds
4✔
245

246

247
def _gather_compiler_kwds(function, codeobj_class):
2✔
248
    """
249
    Gather all the compiler keywords for a function and its dependencies.
250

251
    Parameters
252
    ----------
253
    function : `Function`
254
        The function for which the compiler keywords should be gathered
255
    codeobj_class : type
256
        The class of `CodeObject` to use
257

258
    Returns
259
    -------
260
    kwds : dict
261
        A dictionary with the compiler arguments, a list of values for each
262
        key.
263
    """
264
    implementation = function.implementations[codeobj_class]
4✔
265
    all_kwds = [implementation.compiler_kwds]
4✔
266
    if implementation.dependencies is not None:
4✔
267
        for dependency in implementation.dependencies.values():
4✔
268
            all_kwds.append(_gather_compiler_kwds(dependency, codeobj_class))
4✔
269
    return _merge_compiler_kwds(all_kwds)
4✔
270

271

272
def create_runner_codeobj(
2✔
273
    group,
274
    code,
275
    template_name,
276
    run_namespace,
277
    user_code=None,
278
    variable_indices=None,
279
    name=None,
280
    check_units=True,
281
    needed_variables=None,
282
    additional_variables=None,
283
    template_kwds=None,
284
    override_conditional_write=None,
285
    codeobj_class=None,
286
):
287
    """Create a `CodeObject` for the execution of code in the context of a
288
    `Group`.
289

290
    Parameters
291
    ----------
292
    group : `Group`
293
        The group where the code is to be run
294
    code : str or dict of str
295
        The code to be executed.
296
    template_name : str
297
        The name of the template to use for the code.
298
    run_namespace : dict-like
299
        An additional namespace that is used for variable lookup (either
300
        an explicitly defined namespace or one taken from the local
301
        context).
302
    user_code : str, optional
303
        The code that had been specified by the user before other code was
304
        added automatically. If not specified, will be assumed to be identical
305
        to ``code``.
306
    variable_indices : dict-like, optional
307
        A mapping from `Variable` objects to index names (strings).  If none is
308
        given, uses the corresponding attribute of `group`.
309
    name : str, optional
310
        A name for this code object, will use ``group + '_codeobject*'`` if
311
        none is given.
312
    check_units : bool, optional
313
        Whether to check units in the statement. Defaults to ``True``.
314
    needed_variables: list of str, optional
315
        A list of variables that are neither present in the abstract code, nor
316
        in the ``USES_VARIABLES`` statement in the template. This is only
317
        rarely necessary, an example being a `StateMonitor` where the
318
        names of the variables are neither known to the template nor included
319
        in the abstract code statements.
320
    additional_variables : dict-like, optional
321
        A mapping of names to `Variable` objects, used in addition to the
322
        variables saved in `group`.
323
    template_kwds : dict, optional
324
        A dictionary of additional information that is passed to the template.
325
    override_conditional_write: list of str, optional
326
        A list of variable names which are used as conditions (e.g. for
327
        refractoriness) which should be ignored.
328
    codeobj_class : class, optional
329
        The `CodeObject` class to run code with. If not specified, defaults to
330
        the `group`'s ``codeobj_class`` attribute.
331
    """
332

333
    if name is None:
4✔
334
        if group is not None:
4✔
335
            name = f"{group.name}_{template_name}_codeobject*"
4✔
336
        else:
337
            name = f"{template_name}_codeobject*"
×
338

339
    if user_code is None:
4✔
340
        user_code = code
4✔
341

342
    if isinstance(code, str):
4✔
343
        code = {None: code}
4✔
344
        user_code = {None: user_code}
4✔
345

346
    msg = (
4✔
347
        f"Creating code object (group={group.name}, template name={template_name}) for"
348
        " abstract code:\n"
349
    )
350
    msg += indent(code_representation(code))
4✔
351
    logger.diagnostic(msg)
4✔
352
    from brian2.devices import get_device
4✔
353

354
    device = get_device()
4✔
355

356
    if override_conditional_write is None:
4✔
357
        override_conditional_write = set()
4✔
358
    else:
359
        override_conditional_write = set(override_conditional_write)
4✔
360

361
    if codeobj_class is None:
4✔
362
        codeobj_class = device.code_object_class(group.codeobj_class)
4✔
363
    else:
364
        codeobj_class = device.code_object_class(codeobj_class)
4✔
365

366
    template = getattr(codeobj_class.templater, template_name)
4✔
367
    template_variables = getattr(template, "variables", None)
4✔
368

369
    all_variables = dict(group.variables)
4✔
370
    if additional_variables is not None:
4✔
371
        all_variables.update(additional_variables)
4✔
372

373
    # Determine the identifiers that were used
374
    identifiers = set()
4✔
375
    user_identifiers = set()
4✔
376
    for v, u_v in zip(code.values(), user_code.values()):
4✔
377
        _, uk, u = analyse_identifiers(v, all_variables, recursive=True)
4✔
378
        identifiers |= uk | u
4✔
379
        _, uk, u = analyse_identifiers(u_v, all_variables, recursive=True)
4✔
380
        user_identifiers |= uk | u
4✔
381

382
    # Add variables that are not in the abstract code, nor specified in the
383
    # template but nevertheless necessary
384
    if needed_variables is None:
4✔
385
        needed_variables = []
4✔
386
    # Resolve all variables (variables used in the code and variables needed by
387
    # the template)
388
    variables = group.resolve_all(
4✔
389
        sorted(identifiers | set(needed_variables) | set(template_variables)),
390
        # template variables are not known to the user:
391
        user_identifiers=user_identifiers,
392
        additional_variables=additional_variables,
393
        run_namespace=run_namespace,
394
    )
395
    # We raise this error only now, because there is some non-obvious code path
396
    # where Jinja tries to get a Synapse's "name" attribute via syn['name'],
397
    # which then triggers the use of the `group_get_indices` template which does
398
    # not exist for standalone. Putting the check for template == None here
399
    # means we will first raise an error about the unknown identifier which will
400
    # then make Jinja try syn.name
401
    if template is None:
4✔
402
        codeobj_class_name = codeobj_class.class_name or codeobj_class.__name__
×
403
        raise AttributeError(
×
404
            f"'{codeobj_class_name}' does not provide a code "
405
            f"generation template '{template_name}'"
406
        )
407

408
    conditional_write_variables = {}
4✔
409
    # Add all the "conditional write" variables
410
    for var in variables.values():
4✔
411
        cond_write_var = getattr(var, "conditional_write", None)
4✔
412
        if cond_write_var in override_conditional_write:
4✔
413
            continue
×
414
        if cond_write_var is not None:
4✔
415
            if (
4✔
416
                cond_write_var.name in variables
417
                and not variables[cond_write_var.name] is cond_write_var
418
            ):
419
                logger.diagnostic(
4✔
420
                    f"Variable '{cond_write_var.name}' is needed for the "
421
                    "conditional write mechanism of variable "
422
                    f"'{var.name}'. Its name is already used for "
423
                    f"{variables[cond_write_var.name]!r}."
424
                )
425
            else:
426
                conditional_write_variables[cond_write_var.name] = cond_write_var
4✔
427

428
    variables.update(conditional_write_variables)
4✔
429

430
    if check_units:
4✔
431
        for c in code.values():
4✔
432
            # This is the first time that the code is parsed, catch errors
433
            try:
4✔
434
                check_units_statements(c, variables)
4✔
435
            except (SyntaxError, ValueError) as ex:
2✔
436
                error_msg = _error_msg(c, name)
2✔
437
                raise ValueError(error_msg) from ex
2✔
438

439
    all_variable_indices = copy.copy(group.variables.indices)
4✔
440
    if additional_variables is not None:
4✔
441
        all_variable_indices.update(additional_variables.indices)
4✔
442
    if variable_indices is not None:
4✔
443
        all_variable_indices.update(variable_indices)
4✔
444

445
    # Make "conditional write" variables use the same index as the variable
446
    # that depends on them
447
    for varname, var in variables.items():
4✔
448
        cond_write_var = getattr(var, "conditional_write", None)
4✔
449
        if cond_write_var is not None:
4✔
450
            all_variable_indices[cond_write_var.name] = all_variable_indices[varname]
4✔
451

452
    # Check that all functions are available
453
    for varname, value in variables.items():
4✔
454
        if isinstance(value, Function):
4✔
455
            try:
4✔
456
                value.implementations[codeobj_class]
4✔
457
            except KeyError as ex:
2✔
458
                # if we are dealing with numpy, add the default implementation
459
                from brian2.codegen.runtime.numpy_rt import NumpyCodeObject
2✔
460

461
                if codeobj_class is NumpyCodeObject:
2✔
462
                    value.implementations.add_numpy_implementation(value.pyfunc)
2✔
463
                else:
464
                    raise NotImplementedError(
465
                        f"Cannot use function '{varname}': {ex}"
466
                    ) from ex
467

468
    # Gather the additional compiler arguments declared by function
469
    # implementations
470
    all_keywords = [
4✔
471
        _gather_compiler_kwds(var, codeobj_class)
472
        for var in variables.values()
473
        if isinstance(var, Function)
474
    ]
475
    compiler_kwds = _merge_compiler_kwds(all_keywords)
4✔
476

477
    # Add the indices needed by the variables
478
    for varname in list(variables):
4✔
479
        var_index = all_variable_indices[varname]
4✔
480
        if var_index not in ("_idx", "0"):
4✔
481
            variables[var_index] = all_variables[var_index]
4✔
482

483
    return device.code_object(
4✔
484
        owner=group,
485
        name=name,
486
        abstract_code=code,
487
        variables=variables,
488
        template_name=template_name,
489
        variable_indices=all_variable_indices,
490
        template_kwds=template_kwds,
491
        codeobj_class=codeobj_class,
492
        override_conditional_write=override_conditional_write,
493
        compiler_kwds=compiler_kwds,
494
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc