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
69 changes: 67 additions & 2 deletions .github/workflows/neat-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.9', '3.10', '3.12' ]
python-version: [ '3.9', '3.10', '3.12', '3.13' ]

fail-fast: false

Expand All @@ -82,4 +82,69 @@ jobs:
# Unit tests
- name: Run tests
run: |
pytest -s -o norecursedirs='*' -o log_cli=true -o log_cli_level="DEBUG" tests
pytest -s -o norecursedirs='*' -o log_cli=true -o log_cli_level="DEBUG" tests


build_and_test_legacy_neuron:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.10' ]

fail-fast: false

steps:
# Checkout the repository contents
- name: Checkout NEAT code
uses: actions/checkout@v4

# Setup Python version
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

# Install NEAT
- name: Install NEAT
run: |
pip install setuptools
pip install neuron==8.2.4
pip install .

# Unit tests
- name: Run tests
run: |
pytest -s -o norecursedirs='*' -o log_cli=true -o log_cli_level="DEBUG" tests

build_and_test_corenrn:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.13' ]

fail-fast: false

steps:
# Checkout the repository contents
- name: Checkout NEAT code
uses: actions/checkout@v4

# Setup Python version
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

# Install NEAT
- name: Install NEAT
run: |
pip install setuptools
pip install .

# Unit tests
- name: Run tests
run: |
cd tests/
sh pytest_corenrn_runner.sh


1 change: 1 addition & 0 deletions src/neat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from .simulations.neuron.neuronmodel import NeuronSimTree
from .simulations.neuron.neuronmodel import NeuronSimNode
from .simulations.neuron.neuronmodel import NeuronCompartmentTree
from .simulations.neuron.neuronmodel import check_for_coreneuron
except ModuleNotFoundError:
warnings.warn("NEURON not available", UserWarning)

Expand Down
206 changes: 193 additions & 13 deletions src/neat/actions/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@
import os
import sys
import glob
import stat
import shutil
import inspect
import platform
from pathlib import Path
import importlib
import subprocess

try:
import neuron
except ModuleNotFoundError:
pass

from neat import IonChannel, ExpConcMech
from neat.simulations.nest import nestml_tools
Expand Down Expand Up @@ -158,24 +164,190 @@ def _resolve_model_name(model_name, channel_path_arg):
return model_name


def get_local_bin_dir():
"""
Get the most local bin directory based on the active environment.
Priority order:
1. Conda environment bin
2. Virtual environment bin (venv/virtualenv)
3. Docker container /usr/local/bin (if in container)
4. ~/.local/bin (fallback)

Returns:
Path: The bin directory to use
"""

# 1. Check for conda environment
conda_prefix = os.environ.get("CONDA_PREFIX")
if conda_prefix:
bin_dir = Path(conda_prefix) / "bin"
if bin_dir.exists() and bin_dir.is_dir():
env_name = os.environ.get("CONDA_DEFAULT_ENV", "unknown")
print(f"📦 Detected conda environment: {env_name}")
return bin_dir

# 2. Check for virtual environment (venv/virtualenv)
virtual_env = os.environ.get("VIRTUAL_ENV")
if virtual_env:
bin_dir = Path(virtual_env) / "bin"
if bin_dir.exists() and bin_dir.is_dir():
print(f"🐍 Detected Python virtual environment")
return bin_dir

# 3. Check if running in Docker container
if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
# In Docker, prefer /usr/local/bin for system-wide access
bin_dir = Path("/usr/local/bin")
if os.access(bin_dir, os.W_OK):
print(f"🐳 Detected Docker container")
print(f" Using system bin (writable): {bin_dir}")
return bin_dir
else:
# If /usr/local/bin is not writable, fall back to user location
print(f"🐳 Detected Docker container")
print(f" /usr/local/bin not writable, using user bin")

# 4. Fallback to ~/.local/bin
print(f"ℹ️ No specific environment detected")
return Path.home() / ".local" / "bin"


def create_nrnspecial_wrapper(
special_path, wrapper_name="python_with_my_nrn_model", install_dir=None
):
"""
Create a wrapper script for running Python with NEURON + CoreNEURON mechanisms

Args:
special_path: Path to the 'special' executable (e.g., /path/to/arm64/special)
wrapper_name: Name for the wrapper script
install_dir: Directory to install wrapper (default: auto-detect environment)
"""

# Set default install directory based on environment
if install_dir is None:
install_dir = get_local_bin_dir()
print(f" Using directory: {install_dir}")
print()
else:
install_dir = Path(install_dir)
print(f"Using specified directory: {install_dir}")
print()

# Convert special_path to Path object and resolve to absolute path
special_path = Path(special_path).resolve()

# Check if special executable exists
if not special_path.exists():
print(f"Error: special executable not found at {special_path}", file=sys.stderr)
sys.exit(1)

if not special_path.is_file():
print(f"Error: {special_path} is not a file", file=sys.stderr)
sys.exit(1)

# Create install directory if it doesn't exist
if not install_dir.exists():
print(f"Creating directory: {install_dir}")
try:
install_dir.mkdir(parents=True, exist_ok=True)
print(f"✓ Created {install_dir}")
print()
except PermissionError:
print(f"Error: Permission denied creating {install_dir}", file=sys.stderr)
print(
f"Try running with sudo or choose a different directory",
file=sys.stderr,
)
sys.exit(1)

# Check write permissions
if not os.access(install_dir, os.W_OK):
print(f"Error: No write permission for {install_dir}", file=sys.stderr)
print(f"Try running with sudo or choose a different directory", file=sys.stderr)
sys.exit(1)

# Path for the wrapper script
wrapper_path = install_dir / wrapper_name

# Wrapper script content
wrapper_content = f"""#!/bin/bash
# Wrapper to run Python scripts with NEURON + CoreNEURON mechanisms
# Auto-generated wrapper for: {special_path}

SPECIAL_PATH="{special_path}"

# Check if special exists
if [ ! -f "$SPECIAL_PATH" ]; then
echo "Error: special executable not found at $SPECIAL_PATH" >&2
exit 1
fi

# Run special with -python flag and pass all arguments
exec "$SPECIAL_PATH" -python "$@"
"""
# Write the wrapper script
try:
with open(wrapper_path, "w") as f:
f.write(wrapper_content)

# Make it executable (chmod +x)
wrapper_path.chmod(
wrapper_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)

print(f"✓ Created wrapper at: {wrapper_path}")
print()

# Check if install_dir is in PATH
path_dirs = os.environ.get("PATH", "").split(":")
if str(install_dir) in path_dirs:
print(f"✓ {install_dir} is already in your PATH")
else:
print(f"⚠ Warning: {install_dir} is not in your current PATH")

# Provide context-specific advice
if os.environ.get("CONDA_PREFIX"):
print(f" This is unusual for an active conda environment.")
print(
f" Try: conda deactivate && conda activate {os.environ.get('CONDA_DEFAULT_ENV')}"
)
elif os.environ.get("VIRTUAL_ENV"):
print(f" This is unusual for an active virtual environment.")
print(f" Try deactivating and reactivating your environment.")
elif os.path.exists("/.dockerenv"):
print(f" Add to your Dockerfile or docker run command:")
print(f' ENV PATH="{install_dir}:$PATH"')
else:
print(f" Add to your ~/.bashrc or ~/.zshrc:")
print(f' export PATH="{install_dir}:$PATH"')

return wrapper_path

except Exception as e:
print(f"Error creating wrapper: {e}", file=sys.stderr)
sys.exit(1)


def _compile_neuron(model_name, path_neat, channels, path_neuronresource=None):

print(f"path of this file: {__file__} ")

# combine `model_name` with the neuron compilation path
path_for_neuron_compilation = os.path.join(
path_neat, "simulations/neuron/tmp/", model_name
)
# delete old compiled files if exist
if os.path.exists(path_for_neuron_compilation):
shutil.rmtree(path_for_neuron_compilation)
path_for_mod_files = os.path.join(path_for_neuron_compilation, "mech/")

print(f"--- writing channels to \n" f" > {path_for_mod_files}")

# Create the "mech/" directory in a clean state
if os.path.exists(path_for_mod_files):
shutil.rmtree(path_for_mod_files)
os.makedirs(path_for_mod_files)

# copy default mechanisms
# if path_neuronresource is not None:
# shutil.copytree(path_neuronresource, path_for_mod_files)
# copy mechanisms from resource path
if path_neuronresource is not None:
for mod_file in glob.glob(os.path.join(path_neuronresource, "*.mod")):
shutil.copy2(mod_file, path_for_mod_files)
Expand All @@ -184,20 +356,28 @@ def _compile_neuron(model_name, path_neat, channels, path_neuronresource=None):
print(" - writing .mod file for:", chan.__class__.__name__)
chan.write_mod_file(path_for_mod_files)

# # copy possible mod-files within the source directory to the compile directory
# for mod_file in glob.glob(os.path.join(path_for_channels, '*.mod')):
# shutil.copy2(mod_file, path_for_mod_files)

# change to directory where 'mech/' folder is located and compile the mechanisms
os.chdir(path_for_neuron_compilation)
if os.path.exists(f"{platform.machine()}/"): # delete old compiled files if exist
shutil.rmtree(f"{platform.machine()}/")
subprocess.call(["nrnivmodl", "mech/"]) # compile all mod files

if int(neuron.__version__.split(".")[0]) < 9:
subprocess.call(["nrnivmodl", "mech/"]) # compile all mod files
else:
subprocess.call(["nrnivmodl", "-coreneuron", "mech/"]) # compile all mod files

create_nrnspecial_wrapper(
special_path=os.path.join(
path_for_neuron_compilation,
f"{platform.machine()}/special",
),
wrapper_name=f"python_with_{model_name}",
)

print(
f"\n------------------------------\n"
f"The compiled .mod-files can be loaded into neuron using:\n"
f' neat.load_neuron_model("{model_name}")\n'
f' neat.load_neuron_model("{model_name}")\n\n'
f"If you want to use the compiled .mod-files with CoreNEURON, use:\n"
f" python_with_{model_name} my_script.py \n"
f"------------------------------\n"
)

Expand Down
17 changes: 17 additions & 0 deletions src/neat/actions/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
# along with NEST. If not, see <http://www.gnu.org/licenses/>.

import os
import pathlib
import shutil

from .install import get_local_bin_dir


def _check_model_name(model_name):
if not len(model_name) > 0:
Expand All @@ -36,6 +39,19 @@ def _check_model_name(model_name):
)


def remove_nrnspecial_wrapper(model_name):
wrapper_name = pathlib.Path(f"python_with_{model_name}")

local_bin_dir = get_local_bin_dir()
wrapper_path = local_bin_dir / wrapper_name

try:
os.remove(wrapper_path)
print(f"> Uninstalled {wrapper_name} from {local_bin_dir}")
except FileNotFoundError as e:
print(f"> {wrapper_name} not found in {local_bin_dir}, nothing to uninstall.")


def _uninstall_models(
*model_names,
path_neat,
Expand Down Expand Up @@ -67,6 +83,7 @@ def _uninstall_models(
print(f"> {model_name} not found in nest, nothing to uninstall.")

if "neuron" in simulators:
remove_nrnspecial_wrapper(model_name)
try:
path_neuron = os.path.join(
path_neat, "simulations/", f"neuron/tmp/{model_name}/"
Expand Down
1 change: 0 additions & 1 deletion src/neat/channels/concmechs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import sympy as sp
import numpy as np


CFG = DefaultPhysiology()


Expand Down
2 changes: 1 addition & 1 deletion src/neat/channels/ionchannels.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ def write_mod_file(self, path, g=0.0, e=None):

taustring = "tau_" + ", tau_".join(sv)
varstring = "_inf, ".join(sv) + "_inf"
file.write(" GLOBAL %s, %s\n" % (varstring, taustring))
file.write(" RANGE %s, %s\n" % (varstring, taustring))
file.write(" THREADSAFE" + "\n")
file.write("}\n\n")

Expand Down
Loading
Loading