diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index d7b0786..f9b511c 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.12] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index 628f33e..51d5f9d 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python environment uses: actions/setup-python@v3 with: - python-version: "3.8" + python-version: "3.12" - name: flake8 Lint uses: reviewdog/action-flake8@v3 with: diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index e2ccc54..1e9eb77 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ea9f559..5302002 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,26 +13,27 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools twine wheel + pip install poetry twine + - name: Build package + run: | + poetry build - name: Build and publish to Test PYPI env: TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel - twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose - - name: Build and publish + twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose + - name: Publish to PYPI env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel twine upload dist/* --verbose diff --git a/.gitignore b/.gitignore index cada93f..219f1ca 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,7 @@ dmypy.json # MacOS crap .DS_Store +__MACOSX/ # NEURON arm64/ @@ -140,3 +141,4 @@ doc/source/_build # examples examples/src +L5bPCmodelsEH/ diff --git a/README.md b/README.md index b1daf77..907cb79 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # LFPykit -This Python module contain freestanding implementations of electrostatic +The online documentation of `LFPykit` can be found here: +https://lfpykit.readthedocs.io/en/latest + +`LFPykit` is a freestanding implementations of electrostatic forward models incorporated in LFPy (https://github.com/LFPy/LFPy, https://LFPy.readthedocs.io). @@ -266,7 +269,7 @@ https://lfpykit.readthedocs.io/en/latest ## Dependencies -`LFPykit` is implemented in Python and is written (and continuously tested) for `Python >= 3.7`. +`LFPykit` is implemented in Python and is written (and continuously tested) for `Python >= 3.10`. The main `LFPykit` module depends on `numpy`, `scipy` and `MEAutility` (https://github.com/alejoe91/MEAutility, https://meautility.readthedocs.io/en/latest/). Running all unit tests and example files may in addition require `py.test`, `matplotlib`, diff --git a/lfpykit/models.py b/lfpykit/models.py index ca2a303..bd3deeb 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -1994,6 +1994,9 @@ def __init__(self, cell, z, r): assert r.ndim == 1, 'r.ndim != 1' assert r.shape[0] == z.shape[0], 'r.shape[0] != z.shape[0]' assert np.all(r > 0), 'r must be greater than 0' + len_msg = "All compartments must be shorter than CSD resolution. " +\ + "Increase cell spatial resolution or decrease CSD resolution." + assert np.max(np.diff(z, axis=-1)) > np.max(cell.length), len_msg self.z = z self.r = r @@ -2063,8 +2066,34 @@ def get_transformation_matrix(self): z2 = np.array([z_i[0], z_i[1], z_i[0]]) z3 = np.array([z_i[0], z_i[1], z_i[1]]) + + def _get_fraction_from_z_crossing(k, z_cross, endpoint): + z0 = self.cell.z[k, 0] + z1 = self.cell.z[k, 1] + dz = z1 - z0 + if np.isclose(dz, 0.): + return None + + t = (z_cross - z0) / dz + if not (0. <= t <= 1.): + return None + + if endpoint == 0: + return abs(t) + return abs(1. - t) + # iterate over lower, right, upper boundary for k in np.where(inds)[0]: + # If both endpoints are inside the cylindrical radius, the + # in-volume fraction follows directly from z-intersection. + if kk1[k]: + z_cross = z_i[1] if self.cell.z[k, 1] >= z_i[1] else z_i[0] + frac = _get_fraction_from_z_crossing( + k=k, z_cross=z_cross, endpoint=0) + if frac is not None: + M[i, k] = frac + continue + for ll in range(3): Pr, Pz, hit = _PrPz(r0=R[k, 0], z0=self.cell.z[k, 0], r1=R[k, 1], z1=self.cell.z[k, 1], @@ -2080,6 +2109,16 @@ def get_transformation_matrix(self): inds = (~ll0) & ll1 for k in np.where(inds)[0]: + # If both endpoints are inside the cylindrical radius, the + # in-volume fraction follows directly from z-intersection. + if kk0[k]: + z_cross = z_i[1] if self.cell.z[k, 0] >= z_i[1] else z_i[0] + frac = _get_fraction_from_z_crossing( + k=k, z_cross=z_cross, endpoint=1) + if frac is not None: + M[i, k] = frac + continue + for ll in range(3): Pr, Pz, hit = _PrPz(r0=R[k, 0], z0=self.cell.z[k, 0], r1=R[k, 1], z1=self.cell.z[k, 1], @@ -2106,15 +2145,7 @@ def _PrPz(r0, z0, r1, z1, r2, z2, r3, z3): - (r0 - r1) * (r2 * z3 - r3 * z2)) / denom) Pz = (((r0 * z1 - z0 * r1) * (z2 - z3) - (z0 - z1) * (r2 * z3 - r3 * z2)) / denom) - # check if intersection point lies on lines - if (Pr >= r0) & (Pr <= r1) & (Pz >= z0) & (Pz <= z1): - hit = True - elif (Pr <= r0) & (Pr >= r1) & (Pz >= z0) & (Pz <= z1): - hit = True - elif (Pr >= r0) & (Pr <= r1) & (Pz <= z0) & (Pz >= z1): - hit = True - elif (Pr <= r0) & (Pr >= r1) & (Pz <= z0) & (Pz >= z1): - hit = True - else: - hit = False + # check if intersection point lies on line segment (r0, z0) -> (r1, z1) + hit = (min(r0, r1) <= Pr <= max(r0, r1) + and min(z0, z1) <= Pz <= max(z0, z1)) return Pr, Pz, hit diff --git a/lfpykit/tests/test_module.py b/lfpykit/tests/test_module.py index f82ce0b..0960a92 100644 --- a/lfpykit/tests/test_module.py +++ b/lfpykit/tests/test_module.py @@ -732,7 +732,7 @@ def test_LaminarCurrentSourceDensity_02(self): np.testing.assert_allclose(M_gt, M) - def test_LaminarCurrentSourceDensity_3(self): + def test_LaminarCurrentSourceDensity_03(self): '''test LaminarCurrentSourceDensity implementation''' cell = get_cell(n_seg=3) cell.z = cell.z * 10 @@ -754,7 +754,7 @@ def test_LaminarCurrentSourceDensity_3(self): np.testing.assert_allclose(M_gt, M) - def test_LaminarCurrentSourceDensity_4(self): + def test_LaminarCurrentSourceDensity_04(self): '''test LaminarCurrentSourceDensity implementation''' cell = get_cell(n_seg=4) cell.z = cell.z * 10 @@ -826,6 +826,54 @@ def test_LaminarCurrentSourceDensity_06(self): np.testing.assert_allclose(M_gt, M) + def test_LaminarCurrentSourceDensity_07(self): + '''test LaminarCurrentSourceDensity implementation + + Issue #196 (https://github.com/LFPy/LFPykit/issues/196) + ''' + cell = lfp.CellGeometry(x=np.array([[50, 50], [50, -50]]), + y=np.array([[0, 0], [0, 0]]), + z=np.array([[-100, -50], [-50, 50]]), + d=np.array([1, 1])) + + h = 150. + r = 200. + M_gt = np.array([[1., 0.5], + [0., 0.5]]) / (np.pi * r**2 * h) + + z = np.array([[-h, 0.], [0., h]]) + r = np.array([r, r]) + + # instantiate electrode, get linear response matrix + csd_lam = lfp.LaminarCurrentSourceDensity(cell=cell, z=z, r=r) + M = csd_lam.get_transformation_matrix() + + np.testing.assert_allclose(M_gt, M) + + def test_LaminarCurrentSourceDensity_08(self): + '''test LaminarCurrentSourceDensity implementation + + Issue #196 (https://github.com/LFPy/LFPykit/issues/196) + ''' + cell = lfp.CellGeometry(x=np.array([[0, 0], [0, 100]]), + y=np.array([[0, 0], [0, 0]]), + z=np.array([[-100, -50], [-50, 50]]), + d=np.array([1, 1])) + + h = 150. + r = 200. + M_gt = np.array([[1., 0.5], + [0., 0.5]]) / (np.pi * r**2 * h) + + z = np.array([[-h, 0.], [0., h]]) + r = np.array([r, r]) + + # instantiate electrode, get linear response matrix + csd_lam = lfp.LaminarCurrentSourceDensity(cell=cell, z=z, r=r) + M = csd_lam.get_transformation_matrix() + + np.testing.assert_allclose(M_gt, M) + def test_VolumetricCurrentSourceDensity_00(self): cell = get_cell(n_seg=1) cell.z = cell.z * 10. diff --git a/lfpykit/version.py b/lfpykit/version.py index abe1e1c..aaf05b3 100644 --- a/lfpykit/version.py +++ b/lfpykit/version.py @@ -1 +1 @@ -version = "0.5.1" +version = "0.6.0" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..77af28f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "LFPykit" +version = "0.6.0" +description = "Electrostatic models for multicompartment neuron models" +authors = ["LFPy-team "] + +[tool.poetry.dependencies] +python = ">=3.10" +numpy = ">=1.15.2" +scipy = ">=1.5.2" +meautility = ">=1.5.1" +pytest = {version = "*", optional = true} +sympy = {version = "*", optional = true} +sphinx = {version = "*", optional = true} +numpydoc = {version = "*", optional = true} +sphinx_rtd_theme = {version = "*", optional = true} +recommonmark = {version = "*", optional = true} + +[tool.poetry.extras] +tests = ["pytest", "sympy"] +docs = ["sphinx", "numpydoc", "sphinx_rtd_theme", "recommonmark"] + +[tool.poetry.group.dev.dependencies] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index bb5b56b..0000000 --- a/setup.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -'''LFPykit setuptools file - -''' - -import os -import setuptools - -d = {} -exec(open(os.path.join('lfpykit', 'version.py')).read(), None, d) -version = d['version'] - - -with open('README.md', 'r') as fh: - long_description = fh.read() - -setuptools.setup( - name='LFPykit', - version=version, - author='LFPy-team', - author_email='lfpy@users.noreply.github.com', - description='Electrostatic models for multicompartment neuron models', - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/LFPy/LFPykit', - packages=setuptools.find_packages(), - classifiers=[ - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Operating System :: OS Independent', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Physics', - 'Topic :: Utilities', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Development Status :: 4 - Beta', - ], - python_requires='>=3.7', - install_requires=[ - 'numpy>=1.15.2', - 'scipy', - 'meautility'], - package_data={'lfpykit': [os.path.join('tests', '*.npz'), - os.path.join('tests', '*.py')]}, - include_package_data=True, - extras_require={'tests': ['pytest', 'sympy'], - 'docs': ['sphinx', 'numpydoc', 'sphinx_rtd_theme', - 'recommonmark'], - }, - dependency_links=[], - provides=['lfpykit'], - zip_safe=False -)