From 8639203d87bd9d7f18fbb0ae83f88257efab6623 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:23:41 +0100 Subject: [PATCH 01/17] fix from @llandsmeer --- lfpykit/lfpcalc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 63d3722..d73d628 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -507,9 +507,11 @@ def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): """Subroutine used by calc_lfp_*()""" - aa = np.array([x - xend, y - yend, z - zend]) - bb = np.array([xend - xstart, yend - ystart, zend - zstart]) - cc = np.sum(aa * bb, axis=0) + ccX = (x - xend) * (xend - xstart) + ccY = (y - yend) * (yend - ystart) + ccZ = (z - zend) * (zend - zstart) + cc = ccX + ccY + ccZ + hh = cc / deltaS return hh @@ -517,7 +519,7 @@ def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): def _r2_calc(xend, yend, zend, x, y, z, h): """Subroutine used by calc_lfp_*()""" r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 - return abs(r2) + return np.abs(r2) def _r_root_calc(xmid, ymid, zmid, x, y, z): From 87aa6a508d72a82b59f5783e177a92c3edb47400 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:57:12 +0100 Subject: [PATCH 02/17] numba testing --- lfpykit/lfpcalc.py | 15 +++++++++++++-- requirements.txt | 1 + setup.py | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index d73d628..d4843e5 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -14,7 +14,7 @@ """ - +import numba import numpy as np @@ -371,6 +371,11 @@ def calc_lfp_linesource(cell, x, y, z, sigma, r_limit): zstart = cell.z[:, 0] zend = cell.z[:, -1] + return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit) + + +@numba.jit(nopython=True, nogil=True, cache=True) +def _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit): deltaS = _deltaS_calc(xstart, xend, ystart, yend, zstart, zend) h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) r2 = _r2_calc(xend, yend, zend, x, y, z, h) @@ -384,7 +389,7 @@ def calc_lfp_linesource(cell, x, y, z, sigma, r_limit): lnegi = l_ < 0 lposi = l_ >= 0 - mapping = np.zeros(len(cell.x[:, 0])) + mapping = np.zeros(xstart.size) # case i, h < 0, l < 0, see Eq. C.13 in Gary Holt's thesis, 1998. [i] = np.where(hnegi & lnegi) @@ -474,6 +479,7 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, return 1 / (4 * np.pi * sigma * deltaS) * mapping +@numba.jit(nopython=True, nogil=True, cache=True) def _linesource_calc_case1(l_i, r2_i, h_i): """Calculates linesource contribution for case i""" bb = np.sqrt(h_i * h_i + r2_i) - h_i @@ -482,6 +488,7 @@ def _linesource_calc_case1(l_i, r2_i, h_i): return dd +@numba.jit(nopython=True, nogil=True, cache=True) def _linesource_calc_case2(l_ii, r2_ii, h_ii): """Calculates linesource contribution for case ii""" bb = np.sqrt(h_ii * h_ii + r2_ii) - h_ii @@ -490,6 +497,7 @@ def _linesource_calc_case2(l_ii, r2_ii, h_ii): return dd +@numba.jit(nopython=True, nogil=True, cache=True) def _linesource_calc_case3(l_iii, r2_iii, h_iii): """Calculates linesource contribution for case iii""" bb = np.sqrt(l_iii * l_iii + r2_iii) + l_iii @@ -498,6 +506,7 @@ def _linesource_calc_case3(l_iii, r2_iii, h_iii): return dd +@numba.jit(nopython=True, nogil=True, cache=True) def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): """Returns length of each segment""" deltaS = np.sqrt((xstart - xend)**2 + (ystart - yend)**2 + @@ -505,6 +514,7 @@ def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): return deltaS +@numba.jit(nopython=True, nogil=True, cache=True) def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): """Subroutine used by calc_lfp_*()""" ccX = (x - xend) * (xend - xstart) @@ -516,6 +526,7 @@ def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): return hh +@numba.jit(nopython=True, nogil=True, cache=True) def _r2_calc(xend, yend, zend, x, y, z, h): """Subroutine used by calc_lfp_*()""" r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 diff --git a/requirements.txt b/requirements.txt index 33eb3ef..ade43ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # pip requirements file numpy>=1.15.2 +numba scipy sympy MEAutility diff --git a/setup.py b/setup.py index 9b2e756..16364a0 100644 --- a/setup.py +++ b/setup.py @@ -44,9 +44,9 @@ python_requires='>=3.6', install_requires=[ 'numpy>=1.15.2', + 'numba', 'scipy', - 'meautility' - ], + 'meautility', ], package_data={'lfpykit': [os.path.join('tests', '*.npz'), os.path.join('tests', '*.py')]}, include_package_data=True, From 0d7a702dbbf6f7fdde7fad238414bc7ad584b098 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:58:07 +0100 Subject: [PATCH 03/17] profiling notebook --- examples/cProfile.ipynb | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 examples/cProfile.ipynb diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb new file mode 100644 index 0000000..f283657 --- /dev/null +++ b/examples/cProfile.ipynb @@ -0,0 +1,150 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "638c82eb-3757-4fe9-b1e8-0da9b736fa2d", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6ac6cc3c-e07c-47da-a97f-a087621390ef", + "metadata": {}, + "outputs": [], + "source": [ + "import LFPy\n", + "import lfpykit\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "15b63f94-ebcd-42bf-8e80-9761def92965", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total number of segments: 3078\n" + ] + } + ], + "source": [ + "# LFPy.Cell parameters\n", + "cellParameters = {\n", + " 'morphology': 'L5_Mainen96_LFPy.hoc', # morphology file\n", + " 'v_init': -65, # initial voltage\n", + " 'cm': 1.0, # membrane capacitance\n", + " 'Ra': 150, # axial resistivity\n", + " 'passive': True, # insert passive channels\n", + " 'passive_parameters': {\"g_pas\": 1. / 3E4,\n", + " \"e_pas\": -65}, # passive params\n", + " 'dt': 2**-4, # simulation time res\n", + " 'nsegs_method': 'lambda_f', # discretization rule\n", + " 'lambda_f': 1000 # frequency (Hz)\n", + "}\n", + "\n", + "# create LFPy.Cell instance\n", + "cell = LFPy.Cell(**cellParameters)\n", + "cell.set_rotation(x=4.98919, y=-4.33261, z=0.)\n", + "\n", + "print(f'total number of segments: {cell.totnsegs}')\n", + "\n", + "# parameters for line source potential\n", + "el_params = dict(\n", + " x = np.linspace(0, 1000, 1001),\n", + " y = np.zeros(1001),\n", + " z = np.zeros(1001),\n", + " sigma = 0.3\n", + ")\n", + "\n", + "# create line-source potential predictor\n", + "lsp = lfpykit.LineSourcePotential(cell, **el_params)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "79185fd5-9485-425c-8727-146c793126d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 551318 function calls (546188 primitive calls) in 6.492 seconds\n", + "\n", + " Ordered by: cumulative time\n", + " List reduced from 1338 to 20 due to restriction <20>\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 66/1 0.000 0.000 6.502 6.502 {built-in method builtins.exec}\n", + " 100 0.221 0.002 6.492 0.065 models.py:437(get_transformation_matrix)\n", + " 100100 0.275 0.000 6.270 0.000 lfpcalc.py:346(calc_lfp_linesource)\n", + " 100100 5.865 0.000 5.878 0.000 lfpcalc.py:377(_calc_lfp_linesource)\n", + " 1 0.000 0.000 0.117 0.117 dispatcher.py:388(_compile_for_args)\n", + " 1 0.000 0.000 0.077 0.077 dispatcher.py:915(compile)\n", + " 1 0.000 0.000 0.077 0.077 caching.py:639(load_overload)\n", + " 171/1 0.000 0.000 0.067 0.067 base.py:269(refresh)\n", + "2750/2572 0.001 0.000 0.048 0.000 :1033(_handle_fromlist)\n", + " 85/50 0.000 0.000 0.046 0.001 :1002(_find_and_load)\n", + " 69/34 0.000 0.000 0.046 0.001 :967(_find_and_load_unlocked)\n", + " 118/34 0.000 0.000 0.046 0.001 :220(_call_with_frames_removed)\n", + " 68/34 0.000 0.000 0.045 0.001 :659(_load_unlocked)\n", + " 65/34 0.000 0.000 0.044 0.001 :844(exec_module)\n", + " 171 0.001 0.000 0.040 0.000 cpu.py:69(load_additional_registries)\n", + " 47/31 0.000 0.000 0.040 0.001 {built-in method builtins.__import__}\n", + " 172 0.000 0.000 0.040 0.000 entrypoints.py:10(init_all)\n", + " 1 0.000 0.000 0.040 0.040 dispatcher.py:281(_compilation_chain_init_hook)\n", + " 1 0.000 0.000 0.040 0.040 __init__.py:649()\n", + " 175 0.000 0.000 0.039 0.000 __init__.py:2856(get_entry_map)\n", + " \n", + "*** Profile printout saved to text file 'prun0'.\n" + ] + } + ], + "source": [ + "%%prun -s cumulative -q -l 20 -T prun0\n", + "for i in range(100):\n", + " lsp.get_transformation_matrix()\n", + "print(open('prun0', 'r').read())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c7909f1-c3a6-4f7b-bb6f-0053feb30f6a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From e29f0b027505f5e58b4cf7ec5a2e9ebc93a33b6c Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:18:21 +0100 Subject: [PATCH 04/17] not using cell.totnsegs attribute --- lfpykit/lfpcalc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index d4843e5..7375b1a 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -159,12 +159,12 @@ def calc_lfp_linesource_anisotropic(cell, x, y, z, sigma, r_limit): [iiii] = np.where(np.bitwise_and(4 * a * c - b * b > 1e-6, np.abs(b) > 1e-6)) - if len(i) + len(iia) + len(iib) + len(iii) + len(iiii) != cell.totnsegs: + if len(i) + len(iia) + len(iib) + len(iii) + len(iiii) != xstart.size: print(a, b, c) print(i, iia, iib, iii, iiii) raise RuntimeError - mapping = np.zeros(cell.totnsegs) + mapping = np.zeros(xstart.size) mapping[i] = _anisotropic_line_source_case_i(a[i], c[i]) mapping[iia] = _anisotropic_line_source_case_iia(a[iia], c[iia]) mapping[iib] = _anisotropic_line_source_case_iib(a[iib], b[iib], c[iib]) @@ -271,12 +271,12 @@ def calc_lfp_root_as_point_anisotropic(cell, x, y, z, sigma, r_limit): [iiii] = np.where(np.bitwise_and(4 * a * c - b * b > 1e-6, np.abs(b) > 1e-6)) - if len(i) + len(iia) + len(iib) + len(iii) + len(iiii) != cell.totnsegs: + if len(i) + len(iia) + len(iib) + len(iii) + len(iiii) != xstart.size: print(a, b, c) print(i, iia, iib, iii, iiii) raise RuntimeError - mapping = np.zeros(cell.totnsegs) + mapping = np.zeros(xstart.size) mapping[i] = _anisotropic_line_source_case_i(a[i], c[i]) mapping[iia] = _anisotropic_line_source_case_iia(a[iia], c[iia]) mapping[iib] = _anisotropic_line_source_case_iib(a[iib], b[iib], c[iib]) @@ -468,7 +468,7 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, iii = np.where(hposi & lposi) # Sum all potential contributions - mapping = np.zeros(cell.totnsegs) + mapping = np.zeros(xstart.size) mapping[rootinds] = 1 / r_root deltaS[rootinds] = 1. From ae08111d3ffab494f63a11009a1ba7a4c385fa7e Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:56:57 +0100 Subject: [PATCH 05/17] test fastmath=True --- lfpykit/lfpcalc.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 7375b1a..45fa1db 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -374,7 +374,7 @@ def calc_lfp_linesource(cell, x, y, z, sigma, r_limit): return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit) -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=False) def _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit): deltaS = _deltaS_calc(xstart, xend, ystart, yend, zstart, zend) h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) @@ -479,7 +479,7 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, return 1 / (4 * np.pi * sigma * deltaS) * mapping -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _linesource_calc_case1(l_i, r2_i, h_i): """Calculates linesource contribution for case i""" bb = np.sqrt(h_i * h_i + r2_i) - h_i @@ -488,7 +488,7 @@ def _linesource_calc_case1(l_i, r2_i, h_i): return dd -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _linesource_calc_case2(l_ii, r2_ii, h_ii): """Calculates linesource contribution for case ii""" bb = np.sqrt(h_ii * h_ii + r2_ii) - h_ii @@ -497,7 +497,7 @@ def _linesource_calc_case2(l_ii, r2_ii, h_ii): return dd -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _linesource_calc_case3(l_iii, r2_iii, h_iii): """Calculates linesource contribution for case iii""" bb = np.sqrt(l_iii * l_iii + r2_iii) + l_iii @@ -506,7 +506,7 @@ def _linesource_calc_case3(l_iii, r2_iii, h_iii): return dd -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): """Returns length of each segment""" deltaS = np.sqrt((xstart - xend)**2 + (ystart - yend)**2 + @@ -514,7 +514,7 @@ def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): return deltaS -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): """Subroutine used by calc_lfp_*()""" ccX = (x - xend) * (xend - xstart) @@ -526,7 +526,7 @@ def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): return hh -@numba.jit(nopython=True, nogil=True, cache=True) +@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) def _r2_calc(xend, yend, zend, x, y, z, h): """Subroutine used by calc_lfp_*()""" r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 From df6d5b9ebc6d1c67e270e7e86773e737ba114ff5 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:57:45 +0100 Subject: [PATCH 06/17] reran cProfile script --- examples/cProfile.ipynb | 93 ++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index f283657..da7298a 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -65,13 +65,15 @@ " sigma = 0.3\n", ")\n", "\n", + "cell.simulate(rec_imem=True)\n", + "\n", "# create line-source potential predictor\n", "lsp = lfpykit.LineSourcePotential(cell, **el_params)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "79185fd5-9485-425c-8727-146c793126d9", "metadata": {}, "outputs": [ @@ -79,32 +81,32 @@ "name": "stdout", "output_type": "stream", "text": [ - " 551318 function calls (546188 primitive calls) in 6.492 seconds\n", + " 300548 function calls in 6.441 seconds\n", "\n", " Ordered by: cumulative time\n", - " List reduced from 1338 to 20 due to restriction <20>\n", + " List reduced from 31 to 20 due to restriction <20>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", - " 66/1 0.000 0.000 6.502 6.502 {built-in method builtins.exec}\n", - " 100 0.221 0.002 6.492 0.065 models.py:437(get_transformation_matrix)\n", - " 100100 0.275 0.000 6.270 0.000 lfpcalc.py:346(calc_lfp_linesource)\n", - " 100100 5.865 0.000 5.878 0.000 lfpcalc.py:377(_calc_lfp_linesource)\n", - " 1 0.000 0.000 0.117 0.117 dispatcher.py:388(_compile_for_args)\n", - " 1 0.000 0.000 0.077 0.077 dispatcher.py:915(compile)\n", - " 1 0.000 0.000 0.077 0.077 caching.py:639(load_overload)\n", - " 171/1 0.000 0.000 0.067 0.067 base.py:269(refresh)\n", - "2750/2572 0.001 0.000 0.048 0.000 :1033(_handle_fromlist)\n", - " 85/50 0.000 0.000 0.046 0.001 :1002(_find_and_load)\n", - " 69/34 0.000 0.000 0.046 0.001 :967(_find_and_load_unlocked)\n", - " 118/34 0.000 0.000 0.046 0.001 :220(_call_with_frames_removed)\n", - " 68/34 0.000 0.000 0.045 0.001 :659(_load_unlocked)\n", - " 65/34 0.000 0.000 0.044 0.001 :844(exec_module)\n", - " 171 0.001 0.000 0.040 0.000 cpu.py:69(load_additional_registries)\n", - " 47/31 0.000 0.000 0.040 0.001 {built-in method builtins.__import__}\n", - " 172 0.000 0.000 0.040 0.000 entrypoints.py:10(init_all)\n", - " 1 0.000 0.000 0.040 0.040 dispatcher.py:281(_compilation_chain_init_hook)\n", - " 1 0.000 0.000 0.040 0.040 __init__.py:649()\n", - " 175 0.000 0.000 0.039 0.000 __init__.py:2856(get_entry_map)\n", + " 1 0.000 0.000 6.441 6.441 {built-in method builtins.exec}\n", + " 1 0.010 0.010 6.440 6.440 :1()\n", + " 100 0.219 0.002 6.431 0.064 models.py:437(get_transformation_matrix)\n", + " 100100 0.277 0.000 6.211 0.000 lfpcalc.py:346(calc_lfp_linesource)\n", + " 100100 5.921 0.000 5.934 0.000 lfpcalc.py:377(_calc_lfp_linesource)\n", + " 100100 0.013 0.000 0.013 0.000 serialize.py:29(_numba_unpickle)\n", + " 1 0.000 0.000 0.000 0.000 {built-in method io.open}\n", + " 1 0.000 0.000 0.000 0.000 {built-in method builtins.print}\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:502(write)\n", + " 3 0.000 0.000 0.000 0.000 iostream.py:208(schedule)\n", + " 3 0.000 0.000 0.000 0.000 socket.py:480(send)\n", + " 100 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", + " 1 0.000 0.000 0.000 0.000 {method 'read' of '_io.TextIOWrapper' objects}\n", + " 3 0.000 0.000 0.000 0.000 threading.py:1126(is_alive)\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:439(_schedule_flush)\n", + " 1 0.000 0.000 0.000 0.000 _bootlocale.py:33(getpreferredencoding)\n", + " 3 0.000 0.000 0.000 0.000 threading.py:1059(_wait_for_tstate_lock)\n", + " 1 0.000 0.000 0.000 0.000 codecs.py:319(decode)\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:420(_is_master_process)\n", + " 1 0.000 0.000 0.000 0.000 codecs.py:309(__init__)\n", " \n", "*** Profile printout saved to text file 'prun0'.\n" ] @@ -119,9 +121,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "9c7909f1-c3a6-4f7b-bb6f-0053feb30f6a", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1001, 3078)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "M = lsp.get_transformation_matrix()\n", + "M.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "eb558075-c0f7-4c6c-ab5f-a92719696121", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(3078, 1601)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cell.imem.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56fe8799-6f09-423e-9f5a-1d7a5d5a3d79", + "metadata": {}, "outputs": [], "source": [] } From 0470f71772d31a76eca1fb7962340981777df00b Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:35:46 +0100 Subject: [PATCH 07/17] use @njit decorator; parallelization test --- lfpykit/lfpcalc.py | 39 ++++++++++++++++++++++----------------- lfpykit/models.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 45fa1db..0cfd592 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -14,7 +14,7 @@ """ -import numba +from numba import njit import numpy as np @@ -343,14 +343,19 @@ def _anisotropic_line_source_case_iiii(a, b, c): np.arcsinh(b / np.sqrt(4 * a * c - b * b))) -def calc_lfp_linesource(cell, x, y, z, sigma, r_limit): +@njit(nogil=True, cache=True, fastmath=False) +def calc_lfp_linesource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): """Calculate electric field potential using the line-source method, all segments treated as line sources. Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -364,17 +369,17 @@ def calc_lfp_linesource(cell, x, y, z, sigma, r_limit): """ # some variables for h, r2, r_root calculations - xstart = cell.x[:, 0] - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - yend = cell.y[:, -1] - zstart = cell.z[:, 0] - zend = cell.z[:, -1] + xstart = cell_x[:, 0] + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + yend = cell_y[:, -1] + zstart = cell_z[:, 0] + zend = cell_z[:, -1] return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit) -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=False) +@njit(nogil=True, cache=True, fastmath=False) def _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit): deltaS = _deltaS_calc(xstart, xend, ystart, yend, zstart, zend) h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) @@ -479,7 +484,7 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, return 1 / (4 * np.pi * sigma * deltaS) * mapping -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _linesource_calc_case1(l_i, r2_i, h_i): """Calculates linesource contribution for case i""" bb = np.sqrt(h_i * h_i + r2_i) - h_i @@ -488,7 +493,7 @@ def _linesource_calc_case1(l_i, r2_i, h_i): return dd -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _linesource_calc_case2(l_ii, r2_ii, h_ii): """Calculates linesource contribution for case ii""" bb = np.sqrt(h_ii * h_ii + r2_ii) - h_ii @@ -497,7 +502,7 @@ def _linesource_calc_case2(l_ii, r2_ii, h_ii): return dd -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _linesource_calc_case3(l_iii, r2_iii, h_iii): """Calculates linesource contribution for case iii""" bb = np.sqrt(l_iii * l_iii + r2_iii) + l_iii @@ -506,7 +511,7 @@ def _linesource_calc_case3(l_iii, r2_iii, h_iii): return dd -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): """Returns length of each segment""" deltaS = np.sqrt((xstart - xend)**2 + (ystart - yend)**2 + @@ -514,7 +519,7 @@ def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): return deltaS -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): """Subroutine used by calc_lfp_*()""" ccX = (x - xend) * (xend - xstart) @@ -526,7 +531,7 @@ def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): return hh -@numba.jit(nopython=True, nogil=True, cache=True, fastmath=True) +@njit(nogil=True, cache=True, fastmath=True) def _r2_calc(xend, yend, zend, x, y, z, h): """Subroutine used by calc_lfp_*()""" r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 diff --git a/lfpykit/models.py b/lfpykit/models.py index 836f3cc..5c3b3b9 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -13,6 +13,7 @@ GNU General Public License for more details. """ +import numba import sys from copy import deepcopy import numpy as np @@ -451,19 +452,33 @@ def get_transformation_matrix(self): if self.cell is None: raise AttributeError( '{}.cell is None'.format(self.__class__.__name__)) - M = np.empty((self.x.size, self.cell.totnsegs)) if self.cell.d.ndim == 2: r_limit = self.cell.d.mean(axis=-1) / 2 else: r_limit = self.cell.d / 2 - for j in range(self.x.size): - M[j, :] = lfpcalc.calc_lfp_linesource(self.cell, - x=self.x[j], - y=self.y[j], - z=self.z[j], - sigma=self.sigma, - r_limit=r_limit) - return M + + @numba.njit(nogil=True, cache=True, fastmath=False, parallel=True) + def _get_transform(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): + M = np.empty((x.size, cell_x.shape[0])) + for j in numba.prange(x.size): + M[j, :] = lfpcalc.calc_lfp_linesource(cell_x=cell_x, + cell_y=cell_y, + cell_z=cell_z, + x=x[j], + y=y[j], + z=z[j], + sigma=sigma, + r_limit=r_limit) + return M + + return _get_transform(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, + x=self.x, + y=self.y, + z=self.z, + sigma=self.sigma, + r_limit=r_limit) class RecExtElectrode(LinearModel): From cb019a0a7057a93a09fd206b39b2601b6c40dec6 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 12:25:31 +0100 Subject: [PATCH 08/17] test with numba type hints --- examples/cProfile.ipynb | 91 ++++++++++++++++++++++++++++++----------- lfpykit/lfpcalc.py | 66 +++++++++++++++++++++++++----- lfpykit/models.py | 9 +++- 3 files changed, 129 insertions(+), 37 deletions(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index da7298a..70135fe 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -19,7 +19,8 @@ "source": [ "import LFPy\n", "import lfpykit\n", - "import numpy as np" + "import numpy as np\n", + "import matplotlib.pyplot as plt" ] }, { @@ -77,36 +78,43 @@ "id": "79185fd5-9485-425c-8727-146c793126d9", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "OMP: Info #271: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - " 300548 function calls in 6.441 seconds\n", + " 2500808 function calls (2485608 primitive calls) in 4.240 seconds\n", "\n", " Ordered by: cumulative time\n", - " List reduced from 31 to 20 due to restriction <20>\n", + " List reduced from 453 to 20 due to restriction <20>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", - " 1 0.000 0.000 6.441 6.441 {built-in method builtins.exec}\n", - " 1 0.010 0.010 6.440 6.440 :1()\n", - " 100 0.219 0.002 6.431 0.064 models.py:437(get_transformation_matrix)\n", - " 100100 0.277 0.000 6.211 0.000 lfpcalc.py:346(calc_lfp_linesource)\n", - " 100100 5.921 0.000 5.934 0.000 lfpcalc.py:377(_calc_lfp_linesource)\n", - " 100100 0.013 0.000 0.013 0.000 serialize.py:29(_numba_unpickle)\n", - " 1 0.000 0.000 0.000 0.000 {built-in method io.open}\n", - " 1 0.000 0.000 0.000 0.000 {built-in method builtins.print}\n", - " 2 0.000 0.000 0.000 0.000 iostream.py:502(write)\n", - " 3 0.000 0.000 0.000 0.000 iostream.py:208(schedule)\n", - " 3 0.000 0.000 0.000 0.000 socket.py:480(send)\n", - " 100 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", - " 1 0.000 0.000 0.000 0.000 {method 'read' of '_io.TextIOWrapper' objects}\n", - " 3 0.000 0.000 0.000 0.000 threading.py:1126(is_alive)\n", - " 2 0.000 0.000 0.000 0.000 iostream.py:439(_schedule_flush)\n", - " 1 0.000 0.000 0.000 0.000 _bootlocale.py:33(getpreferredencoding)\n", - " 3 0.000 0.000 0.000 0.000 threading.py:1059(_wait_for_tstate_lock)\n", - " 1 0.000 0.000 0.000 0.000 codecs.py:319(decode)\n", - " 2 0.000 0.000 0.000 0.000 iostream.py:420(_is_master_process)\n", - " 1 0.000 0.000 0.000 0.000 codecs.py:309(__init__)\n", + " 1 0.000 0.000 4.240 4.240 {built-in method builtins.exec}\n", + " 1 0.029 0.029 4.240 4.240 :1()\n", + " 100 0.005 0.000 3.886 0.039 models.py:438(get_transformation_matrix)\n", + " 100 1.896 0.019 1.900 0.019 models.py:460(_get_transform)\n", + " 100 0.001 0.000 1.586 0.016 dispatcher.py:388(_compile_for_args)\n", + "62600/62500 1.088 0.000 1.582 0.000 ffi.py:149(__call__)\n", + " 100 0.002 0.000 1.568 0.016 dispatcher.py:915(compile)\n", + " 100 0.000 0.000 1.551 0.016 caching.py:639(load_overload)\n", + " 100 0.001 0.000 1.533 0.015 caching.py:650(_load_overload)\n", + " 100 0.000 0.000 1.447 0.014 caching.py:404(rebuild)\n", + " 100 0.001 0.000 1.446 0.014 compiler.py:210(_rebuild)\n", + " 100 0.000 0.000 1.413 0.014 codegen.py:1158(unserialize_library)\n", + " 100 0.001 0.000 1.413 0.014 codegen.py:926(_unserialize)\n", + " 100 0.001 0.000 0.668 0.007 module.py:29(parse_bitcode)\n", + " 200 0.003 0.000 0.645 0.003 codegen.py:1088(_load_defined_symbols)\n", + " 400 0.020 0.000 0.635 0.002 codegen.py:1092()\n", + " 100 0.001 0.000 0.384 0.004 decorators.py:189(wrapper)\n", + " 100 0.000 0.000 0.367 0.004 dispatcher.py:862(enable_caching)\n", + " 100 0.001 0.000 0.367 0.004 caching.py:610(__init__)\n", + " 100 0.001 0.000 0.363 0.004 caching.py:336(__init__)\n", " \n", "*** Profile printout saved to text file 'prun0'.\n" ] @@ -144,6 +152,39 @@ { "cell_type": "code", "execution_count": 6, + "id": "602b06df-5e90-441d-951e-0f2272fdeba4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.matshow(M, vmin=-0.01, vmax=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "eb558075-c0f7-4c6c-ab5f-a92719696121", "metadata": {}, "outputs": [ @@ -153,7 +194,7 @@ "(3078, 1601)" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -165,7 +206,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56fe8799-6f09-423e-9f5a-1d7a5d5a3d79", + "id": "9dff6c71-6d6e-44e5-a30a-0b97117dda8c", "metadata": {}, "outputs": [], "source": [] diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 0cfd592..e180b88 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -15,6 +15,7 @@ """ from numba import njit +import numba import numpy as np @@ -344,7 +345,14 @@ def _anisotropic_line_source_case_iiii(a, b, c): @njit(nogil=True, cache=True, fastmath=False) -def calc_lfp_linesource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): +def calc_lfp_linesource(cell_x: numba.double[:, :], + cell_y: numba.double[:, :], + cell_z: numba.double[:, :], + x: numba.double, + y: numba.double, + z: numba.double, + sigma: numba.double, + r_limit: numba.double[:]): """Calculate electric field potential using the line-source method, all segments treated as line sources. @@ -370,17 +378,27 @@ def calc_lfp_linesource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): # some variables for h, r2, r_root calculations xstart = cell_x[:, 0] - xend = cell_x[:, -1] + xend = cell_x[:, 1] ystart = cell_y[:, 0] - yend = cell_y[:, -1] + yend = cell_y[:, 1] zstart = cell_z[:, 0] - zend = cell_z[:, -1] + zend = cell_z[:, 1] return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit) @njit(nogil=True, cache=True, fastmath=False) -def _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit): +def _calc_lfp_linesource(xstart: numba.double[:], + xend: numba.double[:], + ystart: numba.double[:], + yend: numba.double[:], + zstart: numba.double[:], + zend: numba.double[:], + x: numba.double, + y: numba.double, + z: numba.double, + sigma: numba.double, + r_limit: numba.double[:]): deltaS = _deltaS_calc(xstart, xend, ystart, yend, zstart, zend) h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) r2 = _r2_calc(xend, yend, zend, x, y, z, h) @@ -485,7 +503,9 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, @njit(nogil=True, cache=True, fastmath=True) -def _linesource_calc_case1(l_i, r2_i, h_i): +def _linesource_calc_case1(l_i: numba.double[:], + r2_i: numba.double[:], + h_i: numba.double[:]): """Calculates linesource contribution for case i""" bb = np.sqrt(h_i * h_i + r2_i) - h_i cc = np.sqrt(l_i * l_i + r2_i) - l_i @@ -494,7 +514,9 @@ def _linesource_calc_case1(l_i, r2_i, h_i): @njit(nogil=True, cache=True, fastmath=True) -def _linesource_calc_case2(l_ii, r2_ii, h_ii): +def _linesource_calc_case2(l_ii: numba.double[:], + r2_ii: numba.double[:], + h_ii: numba.double[:]): """Calculates linesource contribution for case ii""" bb = np.sqrt(h_ii * h_ii + r2_ii) - h_ii cc = (l_ii + np.sqrt(l_ii * l_ii + r2_ii)) / r2_ii @@ -503,7 +525,9 @@ def _linesource_calc_case2(l_ii, r2_ii, h_ii): @njit(nogil=True, cache=True, fastmath=True) -def _linesource_calc_case3(l_iii, r2_iii, h_iii): +def _linesource_calc_case3(l_iii: numba.double[:], + r2_iii: numba.double[:], + h_iii: numba.double[:]): """Calculates linesource contribution for case iii""" bb = np.sqrt(l_iii * l_iii + r2_iii) + l_iii cc = np.sqrt(h_iii * h_iii + r2_iii) + h_iii @@ -512,7 +536,12 @@ def _linesource_calc_case3(l_iii, r2_iii, h_iii): @njit(nogil=True, cache=True, fastmath=True) -def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): +def _deltaS_calc(xstart: numba.double[:], + xend: numba.double[:], + ystart: numba.double[:], + yend: numba.double[:], + zstart: numba.double[:], + zend: numba.double[:]): """Returns length of each segment""" deltaS = np.sqrt((xstart - xend)**2 + (ystart - yend)**2 + (zstart - zend)**2) @@ -520,7 +549,16 @@ def _deltaS_calc(xstart, xend, ystart, yend, zstart, zend): @njit(nogil=True, cache=True, fastmath=True) -def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): +def _h_calc(xstart: numba.double[:], + xend: numba.double[:], + ystart: numba.double[:], + yend: numba.double[:], + zstart: numba.double[:], + zend: numba.double[:], + deltaS: numba.double[:], + x: numba.double, + y: numba.double, + z: numba.double): """Subroutine used by calc_lfp_*()""" ccX = (x - xend) * (xend - xstart) ccY = (y - yend) * (yend - ystart) @@ -532,7 +570,13 @@ def _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z): @njit(nogil=True, cache=True, fastmath=True) -def _r2_calc(xend, yend, zend, x, y, z, h): +def _r2_calc(xend: numba.double[:], + yend: numba.double[:], + zend: numba.double[:], + x: numba.double, + y: numba.double, + z: numba.double, + h: numba.double[:]): """Subroutine used by calc_lfp_*()""" r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 return np.abs(r2) diff --git a/lfpykit/models.py b/lfpykit/models.py index 5c3b3b9..eb8fea6 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -458,7 +458,14 @@ def get_transformation_matrix(self): r_limit = self.cell.d / 2 @numba.njit(nogil=True, cache=True, fastmath=False, parallel=True) - def _get_transform(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): + def _get_transform(cell_x: numba.double[:, :], + cell_y: numba.double[:, :], + cell_z: numba.double[:, :], + x: numba.double, + y: numba.double, + z: numba.double, + sigma: numba.double, + r_limit: numba.double[:]): M = np.empty((x.size, cell_x.shape[0])) for j in numba.prange(x.size): M[j, :] = lfpcalc.calc_lfp_linesource(cell_x=cell_x, From 62d5df3ce2b592c7c6831b1de2a427bd5fedcdd1 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:40:17 +0100 Subject: [PATCH 09/17] cleanup that didn't solve anything --- examples/cProfile.ipynb | 123 +++++----------------------------------- lfpykit/lfpcalc.py | 17 +++--- 2 files changed, 21 insertions(+), 119 deletions(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index 70135fe..ba5c438 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "638c82eb-3757-4fe9-b1e8-0da9b736fa2d", "metadata": {}, "outputs": [], @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "6ac6cc3c-e07c-47da-a97f-a087621390ef", "metadata": {}, "outputs": [], @@ -25,18 +25,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "15b63f94-ebcd-42bf-8e80-9761def92965", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "total number of segments: 3078\n" - ] - } - ], + "outputs": [], "source": [ "# LFPy.Cell parameters\n", "cellParameters = {\n", @@ -74,76 +66,23 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "79185fd5-9485-425c-8727-146c793126d9", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "OMP: Info #271: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2500808 function calls (2485608 primitive calls) in 4.240 seconds\n", - "\n", - " Ordered by: cumulative time\n", - " List reduced from 453 to 20 due to restriction <20>\n", - "\n", - " ncalls tottime percall cumtime percall filename:lineno(function)\n", - " 1 0.000 0.000 4.240 4.240 {built-in method builtins.exec}\n", - " 1 0.029 0.029 4.240 4.240 :1()\n", - " 100 0.005 0.000 3.886 0.039 models.py:438(get_transformation_matrix)\n", - " 100 1.896 0.019 1.900 0.019 models.py:460(_get_transform)\n", - " 100 0.001 0.000 1.586 0.016 dispatcher.py:388(_compile_for_args)\n", - "62600/62500 1.088 0.000 1.582 0.000 ffi.py:149(__call__)\n", - " 100 0.002 0.000 1.568 0.016 dispatcher.py:915(compile)\n", - " 100 0.000 0.000 1.551 0.016 caching.py:639(load_overload)\n", - " 100 0.001 0.000 1.533 0.015 caching.py:650(_load_overload)\n", - " 100 0.000 0.000 1.447 0.014 caching.py:404(rebuild)\n", - " 100 0.001 0.000 1.446 0.014 compiler.py:210(_rebuild)\n", - " 100 0.000 0.000 1.413 0.014 codegen.py:1158(unserialize_library)\n", - " 100 0.001 0.000 1.413 0.014 codegen.py:926(_unserialize)\n", - " 100 0.001 0.000 0.668 0.007 module.py:29(parse_bitcode)\n", - " 200 0.003 0.000 0.645 0.003 codegen.py:1088(_load_defined_symbols)\n", - " 400 0.020 0.000 0.635 0.002 codegen.py:1092()\n", - " 100 0.001 0.000 0.384 0.004 decorators.py:189(wrapper)\n", - " 100 0.000 0.000 0.367 0.004 dispatcher.py:862(enable_caching)\n", - " 100 0.001 0.000 0.367 0.004 caching.py:610(__init__)\n", - " 100 0.001 0.000 0.363 0.004 caching.py:336(__init__)\n", - " \n", - "*** Profile printout saved to text file 'prun0'.\n" - ] - } - ], + "outputs": [], "source": [ "%%prun -s cumulative -q -l 20 -T prun0\n", - "for i in range(100):\n", + "for i in range(100): \n", " lsp.get_transformation_matrix()\n", "print(open('prun0', 'r').read())" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "9c7909f1-c3a6-4f7b-bb6f-0053feb30f6a", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(1001, 3078)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "M = lsp.get_transformation_matrix()\n", "M.shape" @@ -151,54 +90,20 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "602b06df-5e90-441d-951e-0f2272fdeba4", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plt.matshow(M, vmin=-0.01, vmax=0.01)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "eb558075-c0f7-4c6c-ab5f-a92719696121", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(3078, 1601)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "cell.imem.shape" ] @@ -206,7 +111,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9dff6c71-6d6e-44e5-a30a-0b97117dda8c", + "id": "045d95d5-f58a-4ee3-a591-ea2bf241269e", "metadata": {}, "outputs": [], "source": [] diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index e180b88..1bf667b 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -509,8 +509,7 @@ def _linesource_calc_case1(l_i: numba.double[:], """Calculates linesource contribution for case i""" bb = np.sqrt(h_i * h_i + r2_i) - h_i cc = np.sqrt(l_i * l_i + r2_i) - l_i - dd = np.log(bb / cc) - return dd + return np.log(bb / cc) @njit(nogil=True, cache=True, fastmath=True) @@ -520,8 +519,7 @@ def _linesource_calc_case2(l_ii: numba.double[:], """Calculates linesource contribution for case ii""" bb = np.sqrt(h_ii * h_ii + r2_ii) - h_ii cc = (l_ii + np.sqrt(l_ii * l_ii + r2_ii)) / r2_ii - dd = np.log(bb * cc) - return dd + return np.log(bb * cc) @njit(nogil=True, cache=True, fastmath=True) @@ -531,8 +529,7 @@ def _linesource_calc_case3(l_iii: numba.double[:], """Calculates linesource contribution for case iii""" bb = np.sqrt(l_iii * l_iii + r2_iii) + l_iii cc = np.sqrt(h_iii * h_iii + r2_iii) + h_iii - dd = np.log(bb / cc) - return dd + return np.log(bb / cc) @njit(nogil=True, cache=True, fastmath=True) @@ -543,7 +540,8 @@ def _deltaS_calc(xstart: numba.double[:], zstart: numba.double[:], zend: numba.double[:]): """Returns length of each segment""" - deltaS = np.sqrt((xstart - xend)**2 + (ystart - yend)**2 + + deltaS = np.sqrt((xstart - xend)**2 + + (ystart - yend)**2 + (zstart - zend)**2) return deltaS @@ -565,8 +563,7 @@ def _h_calc(xstart: numba.double[:], ccZ = (z - zend) * (zend - zstart) cc = ccX + ccY + ccZ - hh = cc / deltaS - return hh + return cc / deltaS @njit(nogil=True, cache=True, fastmath=True) @@ -578,7 +575,7 @@ def _r2_calc(xend: numba.double[:], z: numba.double, h: numba.double[:]): """Subroutine used by calc_lfp_*()""" - r2 = (x - xend)**2 + (y - yend)**2 + (z - zend)**2 - h**2 + r2 = (xend - x)**2 + (yend - y)**2 + (zend - z)**2 - h**2 return np.abs(r2) From f2a9f55e743d9dc7ec6428bc8a90a8bf906e3ebf Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:46:57 +0100 Subject: [PATCH 10/17] lfpcalc.calc_* methods no longer takes cell as input --- lfpykit/lfpcalc.py | 187 +++++++++++++++++++++------------- lfpykit/models.py | 16 ++- lfpykit/tests/test_lfpcalc.py | 66 ++++++------ 3 files changed, 159 insertions(+), 110 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 1bf667b..2195eb3 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -64,14 +64,19 @@ def return_dist_from_segments(xstart, ystart, zstart, xend, yend, zend, p): return dist, closest_point -def calc_lfp_linesource_anisotropic(cell, x, y, z, sigma, r_limit): +def calc_lfp_linesource_anisotropic(cell_x, cell_y, cell_z, + x, y, z, sigma, r_limit): """Calculate electric field potential using the line-source method, all segments treated as line sources. Parameters ---------- - cell: obj - `GeometryCell instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -85,12 +90,12 @@ def calc_lfp_linesource_anisotropic(cell, x, y, z, sigma, r_limit): """ # some variables for h, r2, r_root calculations - xstart = cell.x[:, 0] - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - yend = cell.y[:, -1] - zstart = cell.z[:, 0] - zend = cell.z[:, -1] + xstart = cell_x[:, 0] + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + yend = cell_y[:, -1] + zstart = cell_z[:, 0] + zend = cell_z[:, -1] l_vecs = np.array([xend - xstart, yend - ystart, zend - zstart]) @@ -179,14 +184,19 @@ def calc_lfp_linesource_anisotropic(cell, x, y, z, sigma, r_limit): return 1 / (4 * np.pi) * mapping / np.sqrt(a) -def calc_lfp_root_as_point_anisotropic(cell, x, y, z, sigma, r_limit): +def calc_lfp_root_as_point_anisotropic(cell_x, cell_y, cell_z, + x, y, z, sigma, r_limit): """Calculate electric field potential, root is treated as point source, all segments except root are treated as line sources. Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -199,12 +209,12 @@ def calc_lfp_root_as_point_anisotropic(cell, x, y, z, sigma, r_limit): minimum distance to source current for each compartment """ - xstart = cell.x[:, 0] - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - yend = cell.y[:, -1] - zstart = cell.z[:, 0] - zend = cell.z[:, -1] + xstart = cell_x[:, 0] + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + yend = cell_y[:, -1] + zstart = cell_z[:, 0] + zend = cell_z[:, -1] l_vecs = np.array([xend - xstart, yend - ystart, zend - zstart]) pos = np.array([x, y, z]) @@ -294,9 +304,9 @@ def calc_lfp_root_as_point_anisotropic(cell, x, y, z, sigma, r_limit): # sources) rootinds = np.array([0]) - dx2_root = (cell.x[rootinds, :].mean(axis=-1) - x)**2 - dy2_root = (cell.y[rootinds, :].mean(axis=-1) - y)**2 - dz2_root = (cell.z[rootinds, :].mean(axis=-1) - z)**2 + dx2_root = (cell_x[rootinds, :].mean(axis=-1) - x)**2 + dy2_root = (cell_y[rootinds, :].mean(axis=-1) - y)**2 + dz2_root = (cell_z[rootinds, :].mean(axis=-1) - z)**2 r2_root = dx2_root + dy2_root + dz2_root @@ -427,15 +437,19 @@ def _calc_lfp_linesource(xstart: numba.double[:], return 1 / (4 * np.pi * sigma * deltaS) * mapping -def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, +def calc_lfp_root_as_point(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit, rootinds=np.array([0])): """Calculate electric field potential using the line-source method, root is treated as point/sphere source Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -450,15 +464,15 @@ def calc_lfp_root_as_point(cell, x, y, z, sigma, r_limit, indices of root segment(s). Defaults to np.array([0]) """ # some variables for h, r2, r_root calculations - xstart = cell.x[:, 0] - xmid = cell.x[rootinds, :].mean(axis=-1) - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - ymid = cell.y[rootinds, :].mean(axis=-1) - yend = cell.y[:, -1] - zstart = cell.z[:, 0] - zmid = cell.z[rootinds, :].mean(axis=-1) - zend = cell.z[:, -1] + xstart = cell_x[:, 0] + xmid = cell_x[rootinds, :].mean(axis=-1) + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + ymid = cell_y[rootinds, :].mean(axis=-1) + yend = cell_y[:, -1] + zstart = cell_z[:, 0] + zmid = cell_z[rootinds, :].mean(axis=-1) + zend = cell_z[:, -1] deltaS = _deltaS_calc(xstart, xend, ystart, yend, zstart, zend) h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) @@ -585,14 +599,18 @@ def _r_root_calc(xmid, ymid, zmid, x, y, z): return r_root -def calc_lfp_pointsource(cell, x, y, z, sigma, r_limit): +def calc_lfp_pointsource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): """Calculate extracellular potentials using the point-source equation on all compartments Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -605,22 +623,27 @@ def calc_lfp_pointsource(cell, x, y, z, sigma, r_limit): minimum distance to source current for each compartment """ - r2 = (cell.x.mean(axis=-1) - x)**2 + \ - (cell.y.mean(axis=-1) - y)**2 + \ - (cell.z.mean(axis=-1) - z)**2 + r2 = (cell_x.mean(axis=-1) - x)**2 + \ + (cell_y.mean(axis=-1) - y)**2 + \ + (cell_z.mean(axis=-1) - z)**2 r2 = _check_rlimit_point(r2, r_limit) mapping = 1 / (4 * np.pi * sigma * np.sqrt(r2)) return mapping -def calc_lfp_pointsource_anisotropic(cell, x, y, z, sigma, r_limit): +def calc_lfp_pointsource_anisotropic(cell_x, cell_y, cell_z, + x, y, z, sigma, r_limit): """Calculate extracellular potentials using the anisotropic point-source equation on all compartments Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -633,9 +656,9 @@ def calc_lfp_pointsource_anisotropic(cell, x, y, z, sigma, r_limit): minimum distance to source current for each compartment """ - dx2 = (cell.x.mean(axis=-1) - x)**2 - dy2 = (cell.y.mean(axis=-1) - y)**2 - dz2 = (cell.z.mean(axis=-1) - z)**2 + dx2 = (cell_x.mean(axis=-1) - x)**2 + dy2 = (cell_y.mean(axis=-1) - y)**2 + dz2 = (cell_z.mean(axis=-1) - z)**2 r2 = dx2 + dy2 + dz2 if (np.abs(r2) < 1e-6).any(): @@ -666,15 +689,21 @@ def _check_rlimit_point(r2, r_limit): return r2 -def calc_lfp_pointsource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, +def calc_lfp_pointsource_moi(cell_x, cell_y, cell_z, + x, y, z, + sigma_T, sigma_S, sigma_G, steps, h, r_limit, **kwargs): """Calculate extracellular potentials using the point-source equation on all compartments for in vitro Microelectrode Array (MEA) slices Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -695,7 +724,7 @@ def calc_lfp_pointsource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, r_limit: np.ndarray minimum distance to source current for each compartment """ - cell_z_mid = cell.z.mean(axis=-1) + cell_z_mid = cell_z.mean(axis=-1) z_ = z if "z_shift" in kwargs and kwargs["z_shift"] is not None: @@ -703,8 +732,8 @@ def calc_lfp_pointsource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, z_ -= kwargs["z_shift"] cell_z_mid -= kwargs["z_shift"] - dx2 = (x - cell.x.mean(axis=-1))**2 - dy2 = (y - cell.y.mean(axis=-1))**2 + dx2 = (x - cell_x.mean(axis=-1))**2 + dy2 = (y - cell_y.mean(axis=-1))**2 dz2 = (z_ - cell_z_mid)**2 dL2 = dx2 + dy2 @@ -733,15 +762,21 @@ def _omega(dz): return mapping -def calc_lfp_linesource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, +def calc_lfp_linesource_moi(cell_x, cell_y, cell_z, + x, y, z, + sigma_T, sigma_S, sigma_G, steps, h, r_limit, **kwargs): """Calculate extracellular potentials using the line-source equation on all compartments for in vitro Microelectrode Array (MEA) slices Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -764,7 +799,7 @@ def calc_lfp_linesource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, minimum distance to source current for each compartment """ - cell_z = cell.z.copy() # Copy to safely shift coordinate system if needed + cell_z = cell_z.copy() # Copy to safely shift coordinate system if needed z_ = z if "z_shift" in kwargs and kwargs["z_shift"] is not None: @@ -779,14 +814,14 @@ def calc_lfp_linesource_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, raise RuntimeError("This method can only handle sigma_G=0, i.e.," "a non-conducting MEA glass electrode plane.") - xstart = cell.x[:, 0] - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - yend = cell.y[:, -1] + xstart = cell_x[:, 0] + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + yend = cell_y[:, -1] zstart = cell_z[:, 0] zend = cell_z[:, -1] - x0, y0, z0 = cell.x[:, 0], cell.y[:, 0], cell_z[:, 0] - x1, y1, z1 = cell.x[:, -1], cell.y[:, -1], cell_z[:, -1] + x0, y0, z0 = cell_x[:, 0], cell_y[:, 0], cell_z[:, 0] + x1, y1, z1 = cell_x[:, -1], cell_y[:, -1], cell_z[:, -1] pos = np.array([x, y, z_]) rs, _ = return_dist_from_segments(xstart, ystart, zstart, @@ -839,7 +874,9 @@ def _omega(a_z): return mapping -def calc_lfp_root_as_point_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, +def calc_lfp_root_as_point_moi(cell_x, cell_y, cell_z, + x, y, z, + sigma_T, sigma_S, sigma_G, steps, h, r_limit, **kwargs): """Calculate extracellular potentials for in vitro Microelectrode Array (MEA) slices, where root (compartment zero) is @@ -847,8 +884,12 @@ def calc_lfp_root_as_point_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, Parameters ---------- - cell: obj - `GeometryCell` instance or similar + cell_x: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.x`` datas + cell_y: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.y`` datas + cell_z: ndarray + shape ``(totnsegs, 2)`` array with ``CellGeometry.z`` datas x: float extracellular position, x-axis y: float @@ -871,7 +912,7 @@ def calc_lfp_root_as_point_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, minimum distance to source current for each compartment """ - cell_z = cell.z.copy() # Copy to safely shift coordinate system if needed + cell_z = cell_z.copy() # Copy to safely shift coordinate system if needed z_ = z if "z_shift" in kwargs and kwargs["z_shift"] is not None: @@ -886,14 +927,14 @@ def calc_lfp_root_as_point_moi(cell, x, y, z, sigma_T, sigma_S, sigma_G, raise RuntimeError("This method can only handle sigma_G=0, i.e.," "a non-conducting MEA glass electrode plane.") - xstart = cell.x[:, 0] - xend = cell.x[:, -1] - ystart = cell.y[:, 0] - yend = cell.y[:, -1] + xstart = cell_x[:, 0] + xend = cell_x[:, -1] + ystart = cell_y[:, 0] + yend = cell_y[:, -1] zstart = cell_z[:, 0] zend = cell_z[:, -1] - x0, y0, z0 = cell.x[:, 0], cell.y[:, 0], cell_z[:, 0] - x1, y1, z1 = cell.x[:, -1], cell.y[:, -1], cell_z[:, -1] + x0, y0, z0 = cell_x[:, 0], cell_y[:, 0], cell_z[:, 0] + x1, y1, z1 = cell_x[:, -1], cell_y[:, -1], cell_z[:, -1] pos = np.array([x, y, z_]) rs, _ = return_dist_from_segments(xstart, ystart, zstart, @@ -950,8 +991,8 @@ def _omega(a_z): # sources) rootinds = np.array([0]) - dx2 = (x - cell.x[rootinds, :].mean(axis=-1))**2 - dy2 = (y - cell.y[rootinds, :].mean(axis=-1))**2 + dx2 = (x - cell_x[rootinds, :].mean(axis=-1))**2 + dy2 = (y - cell_y[rootinds, :].mean(axis=-1))**2 dz2 = (z_ - cell_z[rootinds, :].mean(axis=-1))**2 dL2 = dx2 + dy2 diff --git a/lfpykit/models.py b/lfpykit/models.py index eb8fea6..ccf5844 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -299,7 +299,9 @@ def get_transformation_matrix(self): else: r_limit = self.cell.d / 2 for j in range(self.x.size): - M[j, :] = lfpcalc.calc_lfp_pointsource(self.cell, + M[j, :] = lfpcalc.calc_lfp_pointsource(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, x=self.x[j], y=self.y[j], z=self.z[j], @@ -940,7 +942,9 @@ def _loop_over_contacts(self, **kwargs): else: r_limit = self.cell.d / 2 for i in range(self.x.size): - M[i, :] = self.lfp_method(self.cell, + M[i, :] = self.lfp_method(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, x=self.x[i], y=self.y[i], z=self.z[i], @@ -965,7 +969,9 @@ def loop_over_points(points): else: r_limit = self.cell.d / 2 for j in range(self.n): - tmp = self.lfp_method(self.cell, + tmp = self.lfp_method(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, x=points[j, 0], y=points[j, 1], z=points[j, 2], @@ -993,7 +999,9 @@ def loop_over_points(points): else: r_limit = self.cell.d / 2 for i, (x, y, z) in enumerate(zip(self.x, self.y, self.z)): - M[i, ] = self.lfp_method(self.cell, + M[i, ] = self.lfp_method(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, x=x, y=y, z=z, diff --git a/lfpykit/tests/test_lfpcalc.py b/lfpykit/tests/test_lfpcalc.py index e8c3267..5c779bd 100644 --- a/lfpykit/tests/test_lfpcalc.py +++ b/lfpykit/tests/test_lfpcalc.py @@ -49,7 +49,7 @@ def test_lfpcalc_calc_lfp_pointsource_00(self): sigma = 0.3 cell = DummyCell() np.testing.assert_equal(1. / (4 * np.pi * sigma), - lfpcalc.calc_lfp_pointsource(cell, + lfpcalc.calc_lfp_pointsource(cell.x, cell.y, cell.z, x=0.5, y=0, z=1, sigma=sigma, r_limit=cell.d / 2 @@ -67,10 +67,10 @@ def test_lfpcalc_calc_lfp_pointsource_moi_00(self): steps = 20 cell = DummyCell(np.array([[h / 2, h / 2]])) - in_vivo = lfpcalc.calc_lfp_pointsource(cell, + in_vivo = lfpcalc.calc_lfp_pointsource(cell.x, cell.y, cell.z, x=0.5, y=0, z=1, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell, + in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0.5, y=0, z=1, sigma_T=sigma_T, sigma_G=sigma_G, @@ -104,7 +104,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_01(self): source_scaling / np.abs(z - (2 * h - cell.z.mean(axis=-1)[0])) ) - moi_method_lfpy = lfpcalc.calc_lfp_pointsource_moi(cell, + moi_method_lfpy = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0., y=0, z=z, sigma_T=sigma_T, sigma_G=sigma_G, @@ -126,11 +126,11 @@ def test_lfpcalc_calc_lfp_pointsource_moi_02(self): steps = 20 cell = DummyCell(z=np.array([[h / 2, h / 2]])) - in_vivo = lfpcalc.calc_lfp_pointsource(cell, + in_vivo = lfpcalc.calc_lfp_pointsource(cell.x, cell.y, cell.z, x=0.5, y=0, z=h / 2, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell, + in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0.5, y=0, z=h / 2, sigma_T=sigma_T, sigma_G=sigma_G, @@ -154,10 +154,10 @@ def test_lfpcalc_calc_lfp_linesource_moi_00(self): steps = 20 cell = DummyCell() - in_vivo = lfpcalc.calc_lfp_linesource(cell, + in_vivo = lfpcalc.calc_lfp_linesource(cell.x, cell.y, cell.z, x=0.5, y=0, z=0, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_linesource_moi(cell, + in_vitro = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=0.5, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -180,11 +180,11 @@ def test_lfpcalc_calc_lfp_pointsource_moi_03(self): steps = 20 cell = DummyCell() - in_vivo = lfpcalc.calc_lfp_root_as_point(cell, + in_vivo = lfpcalc.calc_lfp_root_as_point(cell.x, cell.y, cell.z, x=0.0, y=0, z=0, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_root_as_point_moi(cell, + in_vitro = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=0.0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -202,7 +202,7 @@ def test_lfpcalc_calc_lfp_linesource_00(self): sigma_T = 0.3 cell = DummyCell() - in_vivo = lfpcalc.calc_lfp_linesource(cell, + in_vivo = lfpcalc.calc_lfp_linesource(cell.x, cell.y, cell.z, x=0.5, y=0.0, z=0, sigma=sigma_T, r_limit=cell.d / 2)[0] np.testing.assert_array_less(in_vivo, 1e12) @@ -220,11 +220,11 @@ def test_lfpcalc_calc_lfp_pointsource_moi_04(self): cell = DummyCell(z=np.array([[50, 50]])) - in_vivo = lfpcalc.calc_lfp_pointsource(cell, + in_vivo = lfpcalc.calc_lfp_pointsource(cell.x, cell.y, cell.z, x=50., y=0, z=0, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell, + in_vitro = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=50, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -248,11 +248,11 @@ def test_lfpcalc_calc_lfp_linesource_moi_01(self): cell = DummyCell(z=np.array([[50, 50]])) - in_vivo = lfpcalc.calc_lfp_linesource(cell, + in_vivo = lfpcalc.calc_lfp_linesource(cell.x, cell.y, cell.z, x=50., y=0, z=0, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_linesource_moi(cell, + in_vitro = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=50, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -276,11 +276,11 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_doubling(self): cell = DummyCell(z=np.array([[50, 50]])) - in_vivo = lfpcalc.calc_lfp_root_as_point(cell, + in_vivo = lfpcalc.calc_lfp_root_as_point(cell.x, cell.y, cell.z, x=50., y=0, z=0, sigma=sigma_T, r_limit=cell.d / 2) - in_vitro = lfpcalc.calc_lfp_root_as_point_moi(cell, + in_vitro = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=50, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -303,7 +303,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_saline_effect(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_pointsource_moi(cell, + with_saline = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -312,7 +312,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_saline_effect(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_pointsource_moi(cell, + without_saline = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -335,7 +335,7 @@ def test_lfpcalc_calc_lfp_linesource_moi_saline_effect(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_linesource_moi(cell, + with_saline = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -344,7 +344,7 @@ def test_lfpcalc_calc_lfp_linesource_moi_saline_effect(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_linesource_moi(cell, + without_saline = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -367,7 +367,7 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_saline_effect(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_root_as_point_moi(cell, + with_saline = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -376,7 +376,7 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_saline_effect(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_root_as_point_moi(cell, + without_saline = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -402,7 +402,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_20steps(self): cell = DummyCell(x=np.array([[0., -200.]]), z=np.array(([[0., 220.]]))) - calculated = lfpcalc.calc_lfp_pointsource_moi(cell, + calculated = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=100, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -429,7 +429,7 @@ def test_lfpcalc_calc_lfp_linesource_moi_20steps(self): cell = DummyCell(x=np.array([[-100, 50]]), z=np.array([[0, 110]])) - calculated = lfpcalc.calc_lfp_linesource_moi(cell, + calculated = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=100, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -457,7 +457,7 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_20steps(self): y=np.array([[0., 0.]]), z=np.array([[0, 220]])) - calculated = lfpcalc.calc_lfp_root_as_point_moi(cell, + calculated = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=100, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -480,7 +480,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_infinite_slice(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_pointsource_moi(cell, + with_saline = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=50, sigma_T=sigma_T, sigma_G=sigma_G, @@ -489,7 +489,7 @@ def test_lfpcalc_calc_lfp_pointsource_moi_infinite_slice(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_pointsource_moi(cell, + without_saline = lfpcalc.calc_lfp_pointsource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=50, sigma_T=sigma_T, sigma_G=sigma_G, @@ -512,7 +512,7 @@ def test_lfpcalc_calc_lfp_linesource_moi_infinite_slice(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_linesource_moi(cell, + with_saline = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -521,7 +521,7 @@ def test_lfpcalc_calc_lfp_linesource_moi_infinite_slice(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_linesource_moi(cell, + without_saline = lfpcalc.calc_lfp_linesource_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -544,7 +544,7 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_infinite_slice(self): cell = DummyCell(z=np.array([[100, 100]])) - with_saline = lfpcalc.calc_lfp_root_as_point_moi(cell, + with_saline = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -553,7 +553,7 @@ def test_lfpcalc_calc_lfp_root_as_point_moi_infinite_slice(self): h=h, steps=steps) - without_saline = lfpcalc.calc_lfp_root_as_point_moi(cell, + without_saline = lfpcalc.calc_lfp_root_as_point_moi(cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma_T=sigma_T, sigma_G=sigma_G, @@ -579,7 +579,7 @@ def test_lfpcalc_calc_lfp_pointsource_anisotropic(self): phi_analytic = 1. / (4 * np.pi * sigma_r) np.testing.assert_equal( phi_analytic, lfpcalc.calc_lfp_pointsource_anisotropic( - cell, x=0, y=0, z=0, sigma=sigma, r_limit=cell.d / 2)) + cell.x, cell.y, cell.z, x=0, y=0, z=0, sigma=sigma, r_limit=cell.d / 2)) def test_deltaS_calc(self): cell = DummyCell(y=np.array([[0, 5]])) From dc729a516127f597b824e83d293b6010b994a613 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Tue, 22 Feb 2022 19:40:14 +0100 Subject: [PATCH 11/17] refactoring --- lfpykit/lfpcalc.py | 4 ++-- lfpykit/models.py | 42 +++++++++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 2195eb3..f80c786 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -14,7 +14,7 @@ """ -from numba import njit +from numba import njit, jit import numba import numpy as np @@ -681,7 +681,7 @@ def calc_lfp_pointsource_anisotropic(cell_x, cell_y, cell_z, mapping = 1 / (4 * np.pi * sigma_r) return mapping - +@njit(nogil=True, cache=True, fastmath=True) def _check_rlimit_point(r2, r_limit): """Correct r2 so that r2 >= r_limit**2 for all values""" inds = r2 < r_limit * r_limit diff --git a/lfpykit/models.py b/lfpykit/models.py index ccf5844..0c31df2 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -293,21 +293,41 @@ def get_transformation_matrix(self): if self.cell is None: raise AttributeError( '{}.cell is None'.format(self.__class__.__name__)) - M = np.empty((self.x.size, self.cell.totnsegs)) if self.cell.d.ndim == 2: r_limit = self.cell.d.mean(axis=-1) / 2 else: r_limit = self.cell.d / 2 - for j in range(self.x.size): - M[j, :] = lfpcalc.calc_lfp_pointsource(cell_x=self.cell.x, - cell_y=self.cell.y, - cell_z=self.cell.z, - x=self.x[j], - y=self.y[j], - z=self.z[j], - sigma=self.sigma, - r_limit=r_limit) - return M + + def _get_transform(cell_x: numba.double[:, :], + cell_y: numba.double[:, :], + cell_z: numba.double[:, :], + x: numba.double, + y: numba.double, + z: numba.double, + sigma: numba.double, + r_limit: numba.double[:]): + M = np.empty((x.size, cell_x.shape[0])) + for j in range(x.size): + M[j, :] = lfpcalc.calc_lfp_pointsource(cell_x=cell_x, + cell_y=cell_y, + cell_z=cell_z, + x=x[j], + y=y[j], + z=z[j], + sigma=sigma, + r_limit=r_limit) + return M + + return _get_transform(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, + x=self.x, + y=self.y, + z=self.z, + sigma=self.sigma, + r_limit=r_limit) + + class LineSourcePotential(LinearModel): From 0e58f50daaad7f999b14aabb9be469a5243d13bb Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Tue, 22 Feb 2022 19:44:54 +0100 Subject: [PATCH 12/17] check vs. point sources --- examples/cProfile.ipynb | 43 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index ba5c438..7a71b31 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -58,8 +58,16 @@ " sigma = 0.3\n", ")\n", "\n", - "cell.simulate(rec_imem=True)\n", - "\n", + "cell.simulate(rec_imem=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b8e2ec8-c8a4-48a6-81ef-b7dcee36c836", + "metadata": {}, + "outputs": [], + "source": [ "# create line-source potential predictor\n", "lsp = lfpykit.LineSourcePotential(cell, **el_params)" ] @@ -72,46 +80,57 @@ "outputs": [], "source": [ "%%prun -s cumulative -q -l 20 -T prun0\n", - "for i in range(100): \n", - " lsp.get_transformation_matrix()\n", + "for i in range(100):\n", + " lsp.get_transformation_matrix()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fcd45b6-5349-4718-be60-ab8befe8dab0", + "metadata": {}, + "outputs": [], + "source": [ "print(open('prun0', 'r').read())" ] }, { "cell_type": "code", "execution_count": null, - "id": "9c7909f1-c3a6-4f7b-bb6f-0053feb30f6a", + "id": "045d95d5-f58a-4ee3-a591-ea2bf241269e", "metadata": {}, "outputs": [], "source": [ - "M = lsp.get_transformation_matrix()\n", - "M.shape" + "# create point-source potential predictor\n", + "psp = lfpykit.LineSourcePotential(cell, **el_params)" ] }, { "cell_type": "code", "execution_count": null, - "id": "602b06df-5e90-441d-951e-0f2272fdeba4", + "id": "a6d382dd-d4c6-4bab-9147-3cd5b189a700", "metadata": {}, "outputs": [], "source": [ - "plt.matshow(M, vmin=-0.01, vmax=0.01)" + "%%prun -s cumulative -q -l 20 -T prun1\n", + "for i in range(100):\n", + " psp.get_transformation_matrix()" ] }, { "cell_type": "code", "execution_count": null, - "id": "eb558075-c0f7-4c6c-ab5f-a92719696121", + "id": "2531c6fd-935f-4907-89a9-aef29e73f277", "metadata": {}, "outputs": [], "source": [ - "cell.imem.shape" + "print(open('prun1', 'r').read())" ] }, { "cell_type": "code", "execution_count": null, - "id": "045d95d5-f58a-4ee3-a591-ea2bf241269e", + "id": "90639f55-dc4f-431a-891f-d91e1258ef12", "metadata": {}, "outputs": [], "source": [] From 3c1929b25b6d02add7994d2b2df9c68e93b268e3 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Tue, 22 Feb 2022 20:37:08 +0100 Subject: [PATCH 13/17] roll back edits --- lfpykit/lfpcalc.py | 7 +++---- lfpykit/models.py | 41 +++++++++++------------------------------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index f80c786..270f5dc 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -599,7 +599,8 @@ def _r_root_calc(xmid, ymid, zmid, x, y, z): return r_root -def calc_lfp_pointsource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): +def calc_lfp_pointsource(cell_x, cell_y, cell_z, + x, y, z, sigma, r_limit): """Calculate extracellular potentials using the point-source equation on all compartments @@ -622,12 +623,11 @@ def calc_lfp_pointsource(cell_x, cell_y, cell_z, x, y, z, sigma, r_limit): r_limit: np.ndarray minimum distance to source current for each compartment """ - r2 = (cell_x.mean(axis=-1) - x)**2 + \ (cell_y.mean(axis=-1) - y)**2 + \ (cell_z.mean(axis=-1) - z)**2 r2 = _check_rlimit_point(r2, r_limit) - mapping = 1 / (4 * np.pi * sigma * np.sqrt(r2)) + mapping = 1. / (4. * np.pi * sigma * np.sqrt(r2)) return mapping @@ -681,7 +681,6 @@ def calc_lfp_pointsource_anisotropic(cell_x, cell_y, cell_z, mapping = 1 / (4 * np.pi * sigma_r) return mapping -@njit(nogil=True, cache=True, fastmath=True) def _check_rlimit_point(r2, r_limit): """Correct r2 so that r2 >= r_limit**2 for all values""" inds = r2 < r_limit * r_limit diff --git a/lfpykit/models.py b/lfpykit/models.py index 0c31df2..fb85bc3 100644 --- a/lfpykit/models.py +++ b/lfpykit/models.py @@ -298,36 +298,17 @@ def get_transformation_matrix(self): else: r_limit = self.cell.d / 2 - def _get_transform(cell_x: numba.double[:, :], - cell_y: numba.double[:, :], - cell_z: numba.double[:, :], - x: numba.double, - y: numba.double, - z: numba.double, - sigma: numba.double, - r_limit: numba.double[:]): - M = np.empty((x.size, cell_x.shape[0])) - for j in range(x.size): - M[j, :] = lfpcalc.calc_lfp_pointsource(cell_x=cell_x, - cell_y=cell_y, - cell_z=cell_z, - x=x[j], - y=y[j], - z=z[j], - sigma=sigma, - r_limit=r_limit) - return M - - return _get_transform(cell_x=self.cell.x, - cell_y=self.cell.y, - cell_z=self.cell.z, - x=self.x, - y=self.y, - z=self.z, - sigma=self.sigma, - r_limit=r_limit) - - + M = np.empty((self.x.size, self.cell.x.shape[0])) + for j in range(self.x.size): + M[j, :] = lfpcalc.calc_lfp_pointsource(cell_x=self.cell.x, + cell_y=self.cell.y, + cell_z=self.cell.z, + x=self.x[j], + y=self.y[j], + z=self.z[j], + sigma=self.sigma, + r_limit=r_limit) + return M class LineSourcePotential(LinearModel): From ed6bff56efa44549a6cb607689a111c6bd971909 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Tue, 22 Feb 2022 20:38:34 +0100 Subject: [PATCH 14/17] fix --- examples/cProfile.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index 7a71b31..0762c81 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -102,7 +102,7 @@ "outputs": [], "source": [ "# create point-source potential predictor\n", - "psp = lfpykit.LineSourcePotential(cell, **el_params)" + "psp = lfpykit.PointSourcePotential(cell, **el_params)" ] }, { From aa797d82ab6a455410635a202c7bf6b35a5e9949 Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Thu, 10 Mar 2022 14:20:47 +0100 Subject: [PATCH 15/17] linting --- lfpykit/lfpcalc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index 270f5dc..d8f14fe 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -14,7 +14,7 @@ """ -from numba import njit, jit +from numba import njit import numba import numpy as np @@ -394,7 +394,8 @@ def calc_lfp_linesource(cell_x: numba.double[:, :], zstart = cell_z[:, 0] zend = cell_z[:, 1] - return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, x, y, z, sigma, r_limit) + return _calc_lfp_linesource(xstart, xend, ystart, yend, zstart, zend, + x, y, z, sigma, r_limit) @njit(nogil=True, cache=True, fastmath=False) From 254682ebf7beb3b5149f413ef16928de4276686a Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Fri, 1 Apr 2022 10:09:29 +0200 Subject: [PATCH 16/17] got rid of np.where --- lfpykit/lfpcalc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lfpykit/lfpcalc.py b/lfpykit/lfpcalc.py index d8f14fe..09eec78 100644 --- a/lfpykit/lfpcalc.py +++ b/lfpykit/lfpcalc.py @@ -414,7 +414,7 @@ def _calc_lfp_linesource(xstart: numba.double[:], h = _h_calc(xstart, xend, ystart, yend, zstart, zend, deltaS, x, y, z) r2 = _r2_calc(xend, yend, zend, x, y, z, h) - too_close_idxs = np.where(r2 < r_limit * r_limit)[0] + too_close_idxs = r2 < (r_limit * r_limit) r2[too_close_idxs] = r_limit[too_close_idxs]**2 l_ = h + deltaS @@ -426,11 +426,11 @@ def _calc_lfp_linesource(xstart: numba.double[:], mapping = np.zeros(xstart.size) # case i, h < 0, l < 0, see Eq. C.13 in Gary Holt's thesis, 1998. - [i] = np.where(hnegi & lnegi) + i = hnegi & lnegi # case ii, h < 0, l >= 0 - [ii] = np.where(hnegi & lposi) + ii = hnegi & lposi # case iii, h >= 0, l >= 0 - [iii] = np.where(hposi & lposi) + iii = hposi & lposi mapping[i] = _linesource_calc_case1(l_[i], r2[i], h[i]) mapping[ii] = _linesource_calc_case2(l_[ii], r2[ii], h[ii]) From 311b93f8c794d7a8b45f3dd1cd1d3dd41fa5fc9c Mon Sep 17 00:00:00 2001 From: Espen Hagen <2492641+espenhgn@users.noreply.github.com> Date: Fri, 1 Apr 2022 10:09:44 +0200 Subject: [PATCH 17/17] backup --- examples/cProfile.ipynb | 157 +++++++++++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 17 deletions(-) diff --git a/examples/cProfile.ipynb b/examples/cProfile.ipynb index 0762c81..93031fc 100644 --- a/examples/cProfile.ipynb +++ b/examples/cProfile.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "638c82eb-3757-4fe9-b1e8-0da9b736fa2d", "metadata": {}, "outputs": [], @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "6ac6cc3c-e07c-47da-a97f-a087621390ef", "metadata": {}, "outputs": [], @@ -25,10 +25,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "15b63f94-ebcd-42bf-8e80-9761def92965", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total number of segments: 3078\n" + ] + } + ], "source": [ "# LFPy.Cell parameters\n", "cellParameters = {\n", @@ -63,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "3b8e2ec8-c8a4-48a6-81ef-b7dcee36c836", "metadata": {}, "outputs": [], @@ -74,29 +82,108 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "79185fd5-9485-425c-8727-146c793126d9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "OMP: Info #273: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \n", + "*** Profile printout saved to text file 'prun0'.\n" + ] + } + ], "source": [ - "%%prun -s cumulative -q -l 20 -T prun0\n", + "%%prun -s cumulative -q -l 50 -T prun0\n", "for i in range(100):\n", " lsp.get_transformation_matrix()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "9fcd45b6-5349-4718-be60-ab8befe8dab0", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 2670927 function calls (2651191 primitive calls) in 4.223 seconds\n", + "\n", + " Ordered by: cumulative time\n", + " List reduced from 1402 to 50 due to restriction <50>\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 70/1 0.000 0.000 4.250 4.250 {built-in method builtins.exec}\n", + " 100 0.004 0.000 3.933 0.039 models.py:441(get_transformation_matrix)\n", + " 100 2.328 0.023 2.332 0.023 models.py:463(_get_transform)\n", + " 100 0.001 0.000 1.304 0.013 dispatcher.py:388(_compile_for_args)\n", + " 100 0.002 0.000 1.283 0.013 dispatcher.py:915(compile)\n", + " 100 0.000 0.000 1.269 0.013 caching.py:639(load_overload)\n", + "61409/61309 0.865 0.000 1.258 0.000 ffi.py:149(__call__)\n", + " 100 0.000 0.000 1.167 0.012 caching.py:650(_load_overload)\n", + " 100 0.000 0.000 1.116 0.011 caching.py:404(rebuild)\n", + " 100 0.001 0.000 1.116 0.011 compiler.py:210(_rebuild)\n", + " 100 0.000 0.000 1.088 0.011 codegen.py:1158(unserialize_library)\n", + " 100 0.000 0.000 1.087 0.011 codegen.py:926(_unserialize)\n", + " 100 0.000 0.000 0.509 0.005 module.py:29(parse_bitcode)\n", + " 200 0.002 0.000 0.509 0.003 codegen.py:1088(_load_defined_symbols)\n", + " 400 0.014 0.000 0.501 0.001 codegen.py:1092()\n", + " 21100 0.008 0.000 0.315 0.000 ffi.py:356(__del__)\n", + " 21100 0.009 0.000 0.305 0.000 ffi.py:313(close)\n", + " 143 0.001 0.000 0.286 0.002 decorators.py:189(wrapper)\n", + " 100 0.001 0.000 0.282 0.003 module.py:76(_dispose)\n", + " 100 0.000 0.000 0.269 0.003 dispatcher.py:862(enable_caching)\n", + " 100 0.001 0.000 0.269 0.003 caching.py:610(__init__)\n", + " 100 0.001 0.000 0.267 0.003 caching.py:336(__init__)\n", + " 100 0.000 0.000 0.265 0.003 caching.py:186(from_function)\n", + " 100 0.000 0.000 0.260 0.003 caching.py:116(ensure_cache_path)\n", + " 100 0.000 0.000 0.256 0.003 tempfile.py:575(TemporaryFile)\n", + " 100 0.000 0.000 0.248 0.002 tempfile.py:244(_mkstemp_inner)\n", + " 100 0.245 0.002 0.245 0.002 {built-in method posix.open}\n", + " 61409 0.022 0.000 0.209 0.000 ffi.py:73(__exit__)\n", + " 61409 0.015 0.000 0.182 0.000 base.py:1260(exit_fn)\n", + " 61409 0.021 0.000 0.181 0.000 ffi.py:67(__enter__)\n", + " 20900 0.011 0.000 0.181 0.000 module.py:213(__next__)\n", + " 123018 0.042 0.000 0.171 0.000 event.py:193(broadcast)\n", + " 61509 0.033 0.000 0.168 0.000 event.py:388(end_event)\n", + " 20100 0.016 0.000 0.161 0.000 value.py:206(is_declaration)\n", + " 61409 0.014 0.000 0.155 0.000 base.py:1257(enter_fn)\n", + " 61509 0.032 0.000 0.141 0.000 event.py:374(start_event)\n", + " 15600 0.011 0.000 0.125 0.000 value.py:143(name)\n", + " 122818 0.045 0.000 0.121 0.000 event.py:227(notify)\n", + " 15000 0.009 0.000 0.114 0.000 module.py:233(_next)\n", + " 270/100 0.000 0.000 0.102 0.001 base.py:269(refresh)\n", + "1473/1436 0.003 0.000 0.082 0.000 :1022(_find_and_load)\n", + " 123018 0.037 0.000 0.073 0.000 event.py:84(__init__)\n", + "4640/4459 0.003 0.000 0.070 0.000 :1053(_handle_fromlist)\n", + " 73/36 0.000 0.000 0.066 0.002 :987(_find_and_load_unlocked)\n", + " 127/36 0.000 0.000 0.064 0.002 :233(_call_with_frames_removed)\n", + " 72/36 0.000 0.000 0.064 0.002 :664(_load_unlocked)\n", + " 69/35 0.000 0.000 0.063 0.002 :877(exec_module)\n", + " 270 0.003 0.000 0.062 0.000 cpu.py:69(load_additional_registries)\n", + " 100 0.000 0.000 0.058 0.001 codegen.py:782(_finalize_final_module)\n", + " 52/33 0.000 0.000 0.057 0.002 {built-in method builtins.__import__}\n" + ] + } + ], "source": [ "print(open('prun0', 'r').read())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "045d95d5-f58a-4ee3-a591-ea2bf241269e", "metadata": {}, "outputs": [], @@ -107,10 +194,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "a6d382dd-d4c6-4bab-9147-3cd5b189a700", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \n", + "*** Profile printout saved to text file 'prun1'.\n" + ] + } + ], "source": [ "%%prun -s cumulative -q -l 20 -T prun1\n", "for i in range(100):\n", @@ -119,10 +215,37 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "2531c6fd-935f-4907-89a9-aef29e73f277", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 3203403 function calls in 8.508 seconds\n", + "\n", + " Ordered by: cumulative time\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 1 0.000 0.000 8.508 8.508 {built-in method builtins.exec}\n", + " 1 0.010 0.010 8.508 8.508 :1()\n", + " 100 0.232 0.002 8.498 0.085 models.py:279(get_transformation_matrix)\n", + " 100100 1.279 0.000 8.266 0.000 lfpcalc.py:604(calc_lfp_pointsource)\n", + " 300300 0.065 0.000 6.647 0.000 {method 'mean' of 'numpy.ndarray' objects}\n", + " 300300 0.806 0.000 6.582 0.000 _methods.py:162(_mean)\n", + " 300300 5.484 0.000 5.484 0.000 {method 'reduce' of 'numpy.ufunc' objects}\n", + " 100100 0.340 0.000 0.340 0.000 lfpcalc.py:686(_check_rlimit_point)\n", + " 300300 0.210 0.000 0.245 0.000 _methods.py:66(_count_reduce_items)\n", + " 600600 0.026 0.000 0.026 0.000 {built-in method builtins.issubclass}\n", + " 600600 0.026 0.000 0.026 0.000 {built-in method builtins.isinstance}\n", + " 300300 0.019 0.000 0.019 0.000 {built-in method numpy.core._multiarray_umath.normalize_axis_index}\n", + " 300300 0.011 0.000 0.011 0.000 {built-in method numpy.asanyarray}\n", + " 100 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", + " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}\n" + ] + } + ], "source": [ "print(open('prun1', 'r').read())" ] @@ -130,7 +253,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90639f55-dc4f-431a-891f-d91e1258ef12", + "id": "281b24fe-12ed-40eb-a56e-2983c2a8806a", "metadata": {}, "outputs": [], "source": [] @@ -152,7 +275,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.10" + "version": "3.10.2" } }, "nbformat": 4,