From 64088a958b73a8ba9cfb671517aea2991247cba4 Mon Sep 17 00:00:00 2001 From: jdotc1 <138259519+jdotc1@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:51:38 -0400 Subject: [PATCH 1/2] Parse noise dictionaries as literals rather than executable expressions --- src/brainiak/utils/fmrisim_real_time_generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/brainiak/utils/fmrisim_real_time_generator.py b/src/brainiak/utils/fmrisim_real_time_generator.py index a5ec92db..a9e2ffdb 100644 --- a/src/brainiak/utils/fmrisim_real_time_generator.py +++ b/src/brainiak/utils/fmrisim_real_time_generator.py @@ -26,6 +26,7 @@ isi - What is the time between each event (in seconds) burn_in - How long before the first event (in seconds) """ +import ast import os import time import argparse @@ -418,7 +419,8 @@ def generate_data(outputDir, # Read the resource stream object noise_dict = noise_dict_file.decode() - noise_dict = eval(noise_dict) + # Accept Python literals only, not expressions or NumPy constructors. + noise_dict = ast.literal_eval(noise_dict) noise_dict['matched'] = 0 # Increases processing time # Add it here for easy access From a15d4c7906d2257fe3f925ccc78ca5c3f6b66259 Mon Sep 17 00:00:00 2001 From: jdotc1 <138259519+jdotc1@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:39:22 -0400 Subject: [PATCH 2/2] test: cover literal noise parameters and document migration --- .../+literal-noise-parameters.bugfix.rst | 4 ++ .../utils/fmrisim_real_time_generator.py | 12 +++- tests/utils/test_noise_parameter_loading.py | 57 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 docs/newsfragments/+literal-noise-parameters.bugfix.rst create mode 100644 tests/utils/test_noise_parameter_loading.py diff --git a/docs/newsfragments/+literal-noise-parameters.bugfix.rst b/docs/newsfragments/+literal-noise-parameters.bugfix.rst new file mode 100644 index 00000000..7447b63e --- /dev/null +++ b/docs/newsfragments/+literal-noise-parameters.bugfix.rst @@ -0,0 +1,4 @@ +The real-time fMRI generator reads noise parameter dictionaries as Python +literals. Files containing expressions or NumPy constructors are rejected. +When saving scalar parameters returned by ``calc_noise()``, convert NumPy +scalars with ``value.item()`` before writing the dictionary with ``repr()``. diff --git a/src/brainiak/utils/fmrisim_real_time_generator.py b/src/brainiak/utils/fmrisim_real_time_generator.py index a9e2ffdb..236fc19d 100644 --- a/src/brainiak/utils/fmrisim_real_time_generator.py +++ b/src/brainiak/utils/fmrisim_real_time_generator.py @@ -302,7 +302,9 @@ def _get_input_names(data_dict): Path to template file for data noise_dict_file : str - Path to file containing parameters for noise simulation + Path to a file containing a Python literal dictionary of noise parameters. + Expressions and NumPy constructors are rejected. Convert NumPy scalar + values to native Python scalars before serializing with repr(). """ @@ -376,6 +378,14 @@ def generate_data(outputDir, or as fast as possible (0)? isi - float - What is the time between each event (in seconds) burn_in - int - How long before the first event (in seconds) + noise_dict_file - str - Path to a Python literal dictionary. Expressions + such as np.float64(...) are rejected. For scalar values returned by + calc_noise(), serialize native values, for example:: + + native_noise = {key: value.item() if isinstance(value, np.generic) + else value for key, value in noise.items()} + with open("noise.txt", "w") as stream: + stream.write(repr(native_noise)) """ data_dict = default_settings.copy() diff --git a/tests/utils/test_noise_parameter_loading.py b/tests/utils/test_noise_parameter_loading.py new file mode 100644 index 00000000..c09081f2 --- /dev/null +++ b/tests/utils/test_noise_parameter_loading.py @@ -0,0 +1,57 @@ +"""Exercise noise-file parsing without generating a simulated volume.""" + +import numpy as np +import pytest + +from brainiak.utils import fmrisim_real_time_generator as gen + + +class NoiseParametersLoaded(Exception): + pass + + +@pytest.mark.parametrize("as_file", [False, True]) +def test_literal_noise_parameters_reach_simulation(tmp_path, monkeypatch, as_file): + template = np.ones((2, 2, 2)) + noise = b"{'fwhm': 4.0, 'matched': 1}" + if as_file: + path = tmp_path / "noise.txt" + path.write_bytes(noise) + noise = str(path) + monkeypatch.setattr(gen, "_get_input_names", lambda settings: + (template, template, template, noise)) + monkeypatch.setattr(gen.sim, "mask_brain", lambda **kwargs: + (template, template)) + + def capture_noise(**kwargs): + assert kwargs["noise_dict"] == {"fwhm": 4.0, "matched": 0} + raise NoiseParametersLoaded + + monkeypatch.setattr(gen.sim, "generate_noise", capture_noise) + with pytest.raises(NoiseParametersLoaded): + gen.generate_data(str(tmp_path / "output"), {"numTRs": 1}) + + +@pytest.mark.parametrize("expression", [ + "{'fwhm': np.float64(4.0)}", + "(__import__('pathlib').Path({marker!r}).write_text('executed'), {{}})[1]", +]) +def test_noise_expressions_are_rejected(tmp_path, monkeypatch, expression): + template = np.ones((2, 2, 2)) + marker = tmp_path / "executed.txt" + # Only the marker expression needs formatting; the literal braces above + # remain part of the NumPy-constructor input. + if "{marker!r}" in expression: + expression = expression.format(marker=str(marker)) + monkeypatch.setattr(gen, "_get_input_names", lambda settings: + (template, template, template, expression.encode())) + monkeypatch.setattr(gen.sim, "mask_brain", lambda **kwargs: + (template, template)) + + def unexpected_noise(**kwargs): + pytest.fail("Expression reached noise generation") + + monkeypatch.setattr(gen.sim, "generate_noise", unexpected_noise) + with pytest.raises(ValueError): + gen.generate_data(str(tmp_path / "output"), {"numTRs": 1}) + assert not marker.exists()