diff --git a/brian2cuda/__init__.py b/brian2cuda/__init__.py index fdaa67b8..839acde5 100644 --- a/brian2cuda/__init__.py +++ b/brian2cuda/__init__.py @@ -2,13 +2,11 @@ Package implementing the CUDA "standalone" `Device` and `CodeObject`. """ import logging +import os -from . import cuda_prefs +from . import binomial, cuda_prefs, timedarray from .codeobject import CUDAStandaloneCodeObject from .device import cuda_standalone_device -from . import binomial -from . import timedarray - try: from ._version import __version__, __version_tuple__ @@ -36,6 +34,49 @@ from .tests import run as test +def _load_preference_files(): + """ + Load brian2cuda preference files from standard locations. + + This function loads brian2cuda-specific preferences from user preference + files, avoiding the validation error that occurs when external package + preferences are added to the main preference file. + + Files are loaded in the following order (later files override earlier ones): + 1. ~/.brian2cuda_preferences (user-specific preferences) + 2. ./brian2cuda_preferences (project-specific preferences) + + Missing files are silently ignored. Invalid files generate warnings but + do not prevent the package from loading. + """ + from brian2 import prefs + + # Define preference file locations + user_prefs_file = os.path.join( + os.path.expanduser('~'), '.brian2cuda_preferences' + ) + local_prefs_file = 'brian2cuda_preferences' + + preference_files = [user_prefs_file, local_prefs_file] + + for prefs_file in preference_files: + try: + prefs.read_preference_file(prefs_file) + except OSError: + # File doesn't exist, that's fine + pass + except Exception as e: + # Log a warning for other errors (invalid format, etc.) + logger = logging.getLogger('brian2cuda') + logger.warning( + f"Error reading preference file '{prefs_file}': {e}" + ) + + +# Load preference files when the package is imported +_load_preference_files() + + def example_run(device_name="cuda_standalone", directory=None, **build_options): """ Run a simple example simulation to test whether Brian2CUDA is correctly set up. @@ -51,12 +92,13 @@ def example_run(device_name="cuda_standalone", directory=None, **build_options): build_options : dict, optional Additional options that will be forwarded to the ``device.build`` call, """ - from brian2.devices.device import device, set_device - from brian2 import ms, NeuronGroup, run - import brian2cuda import numpy as np from numpy.testing import assert_allclose + import brian2cuda + from brian2 import NeuronGroup, ms, run + from brian2.devices.device import device, set_device + set_device(device_name, build_on_run=False) N = 100 tau = 10 * ms diff --git a/brian2cuda/tests/test_gpu_detection.py b/brian2cuda/tests/test_gpu_detection.py index 2434b485..0a9b36e2 100644 --- a/brian2cuda/tests/test_gpu_detection.py +++ b/brian2cuda/tests/test_gpu_detection.py @@ -1,20 +1,20 @@ import functools -import os import logging +import os from io import StringIO import pytest from numpy.testing import assert_equal -from brian2 import prefs, ms, run, set_device, device -from brian2.utils.logger import catch_logs as _catch_logs +from brian2 import device, ms, prefs, run, set_device from brian2.core.preferences import PreferenceError +from brian2.utils.logger import catch_logs as _catch_logs from brian2cuda.utils.gputools import ( - reset_cuda_installation, get_cuda_installation, - restore_cuda_installation, - reset_gpu_selection, get_gpu_selection, + reset_cuda_installation, + reset_gpu_selection, + restore_cuda_installation, restore_gpu_selection, ) @@ -177,3 +177,113 @@ def test_no_gpu_detection_preference(reset_gpu_detection, use_default_prefs): prefs.devices.cuda_standalone.cuda_backend.gpu_id = 0 prefs.devices.cuda_standalone.cuda_backend.compute_capability = device.minimal_compute_capability run(0*ms) + + +### Preference file loading tests (Issue #281) ### +@pytest.mark.codegen_independent +def test_missing_files_no_error(use_default_prefs): + """ + Test that missing preference files don't cause errors. + + Regression test for issue #281: + https://github.com/brian-team/brian2cuda/issues/281 + """ + import importlib + import tempfile + + import brian2cuda + + with tempfile.TemporaryDirectory() as tmpdir: + original_home = os.environ.get('HOME') + original_cwd = os.getcwd() + + try: + # Use empty directory (no preference files) + os.environ['HOME'] = tmpdir + os.chdir(tmpdir) + + # Should reload without error + importlib.reload(brian2cuda) + + # Check it doesn't crash - preference should have a value + assert prefs['devices.cuda_standalone.SM_multiplier'] is not None + finally: + if original_home: + os.environ['HOME'] = original_home + os.chdir(original_cwd) + + +@pytest.mark.codegen_independent +def test_load_user_preference_file(tmp_path, use_default_prefs): + """ + Test that user preference file (~/.brian2cuda_preferences) loads. + + Regression test for issue #281: + https://github.com/brian-team/brian2cuda/issues/281 + """ + import importlib + + import brian2cuda + + # Create preference file in user's home directory + prefs_file = tmp_path / ".brian2cuda_preferences" + prefs_file.write_text(""" +[devices.cuda_standalone] +SM_multiplier = 3 +launch_bounds = True +""") + + original_home = os.environ.get('HOME') + os.environ['HOME'] = str(tmp_path) + + try: + # Reload to trigger preference loading + importlib.reload(brian2cuda) + + # Check preferences were loaded + assert prefs['devices.cuda_standalone.SM_multiplier'] == 3 + assert prefs['devices.cuda_standalone.launch_bounds'] is True + finally: + if original_home: + os.environ['HOME'] = original_home + + +@pytest.mark.codegen_independent +def test_local_overrides_user(tmp_path, monkeypatch, use_default_prefs): + """ + Test that local preference file overrides user file. + + Regression test for issue #281: + https://github.com/brian-team/brian2cuda/issues/281 + """ + import importlib + + import brian2cuda + + # Create user file + user_file = tmp_path / ".brian2cuda_preferences" + user_file.write_text(""" +[devices.cuda_standalone] +SM_multiplier = 2 +""") + + # Create local file with different value + local_file = tmp_path / "brian2cuda_preferences" + local_file.write_text(""" +[devices.cuda_standalone] +SM_multiplier = 5 +""") + + original_home = os.environ.get('HOME') + os.environ['HOME'] = str(tmp_path) + monkeypatch.chdir(tmp_path) + + try: + # Reload to trigger preference loading + importlib.reload(brian2cuda) + + # Local should override user + assert prefs['devices.cuda_standalone.SM_multiplier'] == 5 + finally: + if original_home: + os.environ['HOME'] = original_home