diff --git a/.circleci/config.yml b/.circleci/config.yml index 447c9ed3..d70ac9fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,7 +83,7 @@ jobs: command: | pip install ".[docs]" cd docs/ - make html + make html SPHINXOPTS="-W" test_deprecation_warnings: parameters: @@ -202,6 +202,7 @@ workflows: requires: - build_and_test + test_and_publish: # Test and publish on new git version tags # This requires its own workflow to successfully trigger the test and build diff --git a/.editorconfig b/.editorconfig index 09084781..50e915bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,6 +19,3 @@ end_of_line = crlf [LICENSE] insert_final_newline = false - -[Makefile] -indent_style = tab diff --git a/.gitignore b/.gitignore index 22f553c5..e4ad94ad 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,10 @@ venv.bak/ # macOS .DS_Store +# external data that is loaded from the web +spharpy/samplings/_eqsp/samplings_extremal* +spharpy/samplings/_eqsp/samplings_t_design* + # workaround for failing test discovery in vscode tests/*/__init__.py tests/private/ @@ -120,4 +124,8 @@ docs/header.rst docs/_static/favicon.ico docs/_static/header.rst docs/_static/css/custom.css +docs/_static/js/custom.js docs/resources/logos/pyfar_logos_fixed_size_spharpy.png + +# ignore plot output +tests/test_plot_data/output/ diff --git a/.readthedocs.yml b/.readthedocs.yml index ade6e9b3..24476fc2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -20,7 +20,7 @@ sphinx: formats: - pdf -# Optionally set the version of Python and requirements required to build your docs +# use pip to install dependencies python: install: - method: pip diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 3725ae6f..bc2015a0 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -10,6 +10,14 @@ For examples on how to use spharpy refer to the notebooks in the `examples gallery`_ and the documentation on classes and modules below. +Classes +------- + +.. toctree:: + :maxdepth: 1 + + classes/spharpy.coordinates + Modules ------- @@ -17,11 +25,11 @@ Modules :maxdepth: 1 modules/spharpy.beamforming - modules/spharpy.indexing modules/spharpy.interpolate modules/spharpy.plot modules/spharpy.samplings modules/spharpy.spatial + modules/spharpy.special modules/spharpy.spherical modules/spharpy.transforms diff --git a/docs/classes/spharpy.coordinates.rst b/docs/classes/spharpy.coordinates.rst new file mode 100644 index 00000000..a1edb1c8 --- /dev/null +++ b/docs/classes/spharpy.coordinates.rst @@ -0,0 +1,9 @@ +spharpy.SamplingSphere +---------------------- + +.. automodule:: spharpy.classes.coordinates + +.. autoclass:: spharpy.SamplingSphere + :members: + :undoc-members: + :inherited-members: diff --git a/docs/conf.py b/docs/conf.py index d25ca7bf..064427c8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,8 +12,7 @@ import shutil import numpy as np sys.path.insert(0, os.path.abspath('..')) - -import spharpy # noqa +import spharpy # noqa: E402 # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -37,10 +36,6 @@ # package autodoc_default_options = {'autosummary': True} -# show the code of plots that follows the command .. plot:: based on the -# package matplotlib.sphinxext.plot_directive -plot_include_source = True - # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -84,12 +79,19 @@ # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = False +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = False + # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # default language for highlighting in source code highlight_language = "python3" +# show the code of plots that follows the command .. plot:: based on the +# package matplotlib.sphinxext.plot_directive +plot_include_source = True + # intersphinx mapping intersphinx_mapping = { 'numpy': ('https://numpy.org/doc/stable/', None), diff --git a/docs/modules/spharpy.indexing.rst b/docs/modules/spharpy.special.rst similarity index 55% rename from docs/modules/spharpy.indexing.rst rename to docs/modules/spharpy.special.rst index d3a8e379..12228fc4 100644 --- a/docs/modules/spharpy.indexing.rst +++ b/docs/modules/spharpy.special.rst @@ -1,7 +1,7 @@ -spharpy.indexing -================ +spharpy.special +--------------- -.. automodule:: spharpy.indexing +.. automodule:: spharpy.special :members: :special-members: __init__ :undoc-members: diff --git a/examples/general_beamforming_weights.ipynb b/examples/general_beamforming_weights.ipynb index 4eb635ce..eefbb43f 100644 --- a/examples/general_beamforming_weights.ipynb +++ b/examples/general_beamforming_weights.ipynb @@ -7,6 +7,7 @@ "outputs": [], "source": [ "import numpy as np\n", + "import pyfar as pf\n", "import spharpy\n", "import matplotlib.pyplot as plt\n", "from matplotlib.collections import LineCollection\n", @@ -51,7 +52,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Analytic Plane Wave" + "# Beamforming\n", + "\n", + "The directional component of a plane wave can be expanded as a Fourier series using the spherical harmonics." ] }, { @@ -61,17 +64,10 @@ "outputs": [], "source": [ "N = 7\n", - "doa = spharpy.samplings.Coordinates(-1,0,0)\n", + "doa = pf.Coordinates(-1,0,0)\n", "p_nm = spharpy.spherical.spherical_harmonic_basis_real(N, doa)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Beamforming" - ] - }, { "cell_type": "code", "execution_count": null, @@ -80,7 +76,7 @@ "source": [ "n_dirs = 1024\n", "azi = np.linspace(0, 2*np.pi, n_dirs)\n", - "steering_directions = spharpy.samplings.Coordinates.from_spherical(np.ones(n_dirs), np.ones(n_dirs)*np.pi/2, azi)\n", + "steering_directions = pf.Coordinates.from_spherical_colatitude(azi, np.ones(n_dirs)*np.pi/2, 1)\n", "Y_steering = spharpy.spherical.spherical_harmonic_basis_real(N, steering_directions)" ] }, @@ -220,7 +216,7 @@ "source": [ "h_n = spharpy.beamforming.normalize_beamforming_weights(\n", " hann(2*(N+1)+1)[N+1:-1], N)\n", - "h_nm = spharpy.indexing.sph_identity_matrix(N).T @ h_n\n", + "h_nm = spharpy.spherical.sph_identity_matrix(N).T @ h_n\n", "hanning = np.squeeze(Y_steering @ np.diag(h_nm) @ p_nm.T)" ] }, diff --git a/examples/plane_wave_decomposition.ipynb b/examples/plane_wave_decomposition.ipynb index 5c2f5297..cca63dc2 100644 --- a/examples/plane_wave_decomposition.ipynb +++ b/examples/plane_wave_decomposition.ipynb @@ -7,7 +7,7 @@ "outputs": [], "source": [ "import spharpy\n", - "from spharpy.samplings import Coordinates\n", + "from pyfar import Coordinates\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.colorbar import Colorbar\n", @@ -66,7 +66,7 @@ "plt.figure(figsize=(16, 4))\n", "ax = plt.subplot(1, 2, 1)\n", "n_max = 4\n", - "mask = spharpy.spherical.nm2acn(np.arange(0, n_max+1), np.zeros(n_max+1))\n", + "mask = spharpy.spherical.nm_to_acn(np.arange(0, n_max+1), np.zeros(n_max+1))\n", "ax.plot(kr, 10*np.log10(np.abs(np.diagonal(B_open, axis1=1, axis2=2)[:, mask])))\n", "plt.grid(True)\n", "ax.set_ylabel('Magnitude in dB')\n", @@ -75,7 +75,7 @@ "ax.set_title('Open Sphere')\n", "ax.set_ylim((-20, 15))\n", "ax = plt.subplot(1, 2, 2)\n", - "mask = spharpy.spherical.nm2acn(np.arange(0, n_max+1), np.zeros(n_max+1))\n", + "mask = spharpy.spherical.nm_to_acn(np.arange(0, n_max+1), np.zeros(n_max+1))\n", "ax.plot(kr, 10*np.log10(np.abs(np.diagonal(B_rigid, axis1=1, axis2=2)[:, mask])))\n", "plt.grid(True)\n", "ax.set_ylabel('Magnitude in dB')\n", @@ -100,7 +100,7 @@ "\n", "p_nm = B @ plane_wave_density.T.conj()\n", "\n", - "sphere = spharpy.samplings.hyperinterpolation(30)\n", + "sphere = spharpy.samplings.hyperinterpolation(n_max=30)\n", "Y_sphere = spharpy.spherical.spherical_harmonic_basis(n_max, sphere)\n", "\n", "p_sphere = np.squeeze(Y_sphere @ p_nm)" @@ -130,7 +130,7 @@ "\n", "$$ \\mathbf{a}(k) = \\frac{4\\pi}{(N+1)^2}\\mathbf{Y_s}^H \\mathbf{B}^{-1} \\mathbf{p_{nm}}$$\n", "\n", - "with the steering matrix $ \\mathbf{Y_s} $" + "with the steering matrix $\\mathbf{Y_s}$" ] }, { @@ -151,7 +151,7 @@ "outputs": [], "source": [ "plt.figure(figsize=(10, 4))\n", - "spharpy.plot.contour(steering_directions, np.abs(plane_wave_density))" + "spharpy.plot.contour_map(steering_directions, np.abs(plane_wave_density), projection='mollweide')" ] } ], @@ -174,7 +174,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.10" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/examples/spherical_harmonics.ipynb b/examples/spherical_harmonics.ipynb index ca17b613..9f673a6d 100644 --- a/examples/spherical_harmonics.ipynb +++ b/examples/spherical_harmonics.ipynb @@ -7,7 +7,7 @@ "outputs": [], "source": [ "import spharpy\n", - "from spharpy.samplings import Coordinates\n", + "from pyfar import Coordinates\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib as mpl\n", @@ -24,7 +24,7 @@ " fig = plt.figure(figsize=(12, 8))\n", " gs = plt.GridSpec(4, 5, height_ratios=[1, 1, 1, 0.1], width_ratios=[1, 1, 1, 1, 1])\n", " for acn in range((n_max+1)**2):\n", - " n, m = spharpy.spherical.acn2nm(acn)\n", + " n, m = spharpy.spherical.acn_to_nm(acn)\n", " idx_m = int(np.floor(n_max/2+1)) + m\n", " ax = plt.subplot(gs[n, idx_m], projection='3d')\n", " balloon = spharpy.plot.balloon_wireframe(sampling, Y[:, acn], phase=True, colorbar=False, ax=ax)\n", @@ -76,7 +76,7 @@ "metadata": {}, "outputs": [], "source": [ - "sampling = spharpy.samplings.equalarea(25, condition_num=np.inf)\n", + "sampling = spharpy.samplings.equal_area(25, condition_num=np.inf)\n", "plt.figure(figsize=(7, 7))\n", "spharpy.plot.scatter(sampling)" ] @@ -188,7 +188,7 @@ "outputs": [], "source": [ "n_points = 128\n", - "circle = Coordinates.from_spherical(np.ones(n_points), np.full(n_points, np.pi/2), np.linspace(0, 2*np.pi, n_points))\n", + "circle = Coordinates.from_spherical_colatitude(np.linspace(0, 2*np.pi, n_points), np.pi/2, 1)\n", "Y_real_circle = spharpy.spherical.spherical_harmonic_basis_real(n_max, circle)" ] }, @@ -277,7 +277,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Gradient of the Spherical Harmonics\n", + "## Gradient of the Spherical Harmonics\n", "\n", "The gradient of the spherical harmonics on the unit sphere can be written as,\n", "\n", @@ -290,7 +290,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Gradient of the Complex-Valued Spherical Harmonics" + "### Gradient of the Complex-Valued Spherical Harmonics" ] }, { @@ -325,7 +325,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Gradient of the Real-Valued Spherical Harmonics" + "### Gradient of the Real-Valued Spherical Harmonics" ] }, { diff --git a/pyproject.toml b/pyproject.toml index afb02077..bba87573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,10 +26,10 @@ classifiers = [ ] dependencies = [ 'numpy>=1.22', - 'scipy', + 'scipy>=1.15', 'urllib3', 'matplotlib>=3.10.3', - 'pyfar<0.8.0', + 'pyfar>=0.7.3,<0.8.0', 'pytz', ] @@ -80,6 +80,7 @@ find = {} # Scan the project directory with the default parameters exclude = [ ".git", "docs", + "examples", ] line-length = 79 lint.ignore = [ @@ -93,23 +94,23 @@ lint.ignore = [ "PT018", # Assertion should be broken down into multiple parts "PT019", # Fixture `_` without value is injected as parameter # add more ignores, to match flake8 rules - "D417", + #"D417", ] lint.select = [ - # "B", # bugbear extension - # "ARG", # Remove unused function/method arguments + "ARG", # Remove unused function/method arguments + "B", # bugbear extension "C4", # Check for common security issues - # "E", # PEP8 errors - # "F", # Pyflakes - # "W", # PEP8 warnings - # "D", # Docstring guidelines + "E", # PEP8 errors + "F", # Pyflakes + "W", # PEP8 warnings + "D", # Docstring guidelines "NPY", # Check all numpy related deprecations "D417", # Missing argument descriptions in the docstring - # "PT", # Pytest style - # "A", # Avoid builtin function and type shadowing - # "ERA", # No commented out code + "PT", # Pytest style + "A", # Avoid builtin function and type shadowing + "ERA", # No commented out code "NPY", # Check all numpy related deprecations - # "COM", # trailing comma rules + "COM", # trailing comma rules "I002", # missing required import "TID252", # Use absolute over relative imports "FIX", # Code should not contain FIXME, TODO, etc diff --git a/spharpy/__init__.py b/spharpy/__init__.py index bf0d5815..7deefe47 100644 --- a/spharpy/__init__.py +++ b/spharpy/__init__.py @@ -6,23 +6,29 @@ __email__ = 'info@pyfar.org' __version__ = '0.6.2' +from .classes.sh import SphericalHarmonics +from .classes.coordinates import SamplingSphere +from .classes.audio import SphericalHarmonicSignal from . import spherical from . import samplings from . import plot -from . import indexing from . import transforms from . import beamforming from . import interpolate -from . import _deprecation +from . import spatial +from . import special __all__ = [ + 'SphericalHarmonics', + 'SphericalHarmonicSignal', 'spherical', 'samplings', 'plot', - 'indexing', 'transforms', 'beamforming', 'interpolate', - '_deprecation', + 'spatial', + 'special', + 'SamplingSphere', ] diff --git a/spharpy/_deprecation.py b/spharpy/_deprecation.py deleted file mode 100644 index 53e9b54b..00000000 --- a/spharpy/_deprecation.py +++ /dev/null @@ -1,13 +0,0 @@ -from spharpy.samplings import Coordinates -from spharpy.samplings import SamplingSphere -import pyfar as pf - - -def convert_coordinates(coordinates): - coords_type = type(coordinates) - if coords_type is not pf.Coordinates: - return coordinates - if coordinates.sh_order is None: - return Coordinates.from_pyfar(coordinates) - else: - return SamplingSphere.from_pyfar(coordinates) diff --git a/spharpy/beamforming/__init__.py b/spharpy/beamforming/__init__.py index 05fe4fb4..a9c9990d 100644 --- a/spharpy/beamforming/__init__.py +++ b/spharpy/beamforming/__init__.py @@ -1,13 +1,15 @@ +"""Beamforming methods for spherical harmonic signals.""" + from .beamforming import ( dolph_chebyshev_weights, rE_max_weights, maximum_front_back_ratio_weights, - normalize_beamforming_weights + normalize_beamforming_weights, ) __all__ = [ 'dolph_chebyshev_weights', 'rE_max_weights', 'maximum_front_back_ratio_weights', - 'normalize_beamforming_weights' + 'normalize_beamforming_weights', ] diff --git a/spharpy/beamforming/beamforming.py b/spharpy/beamforming/beamforming.py index ad9fb036..6ef96186 100644 --- a/spharpy/beamforming/beamforming.py +++ b/spharpy/beamforming/beamforming.py @@ -1,43 +1,77 @@ +"""Beamforming methods for spherical harmonic signals.""" + import itertools import numpy as np import numpy.polynomial as poly from scipy.linalg import eigh from scipy.special import factorial -import spharpy import spharpy.special as special +from spharpy.spherical import sph_identity_matrix def dolph_chebyshev_weights( n_max, design_parameter, design_criterion='sidelobe'): - """Calculate the weights for a spherical Dolph-Chebyshev beamformer. The - design criterion can either be a desired side-lobe attenuation or a desired - main-lobe width. Once one criterion is chosen, the other will become a - dependent property which will be chosen accordingly. + """Calculate the weights for a spherical Dolph-Chebyshev beamformer. + The design criterion can either be a desired side-lobe attenuation or a + desired main-lobe width. Once one criterion is chosen, the other will + become a dependent property which will be chosen accordingly [#]_. Parameters ---------- n_max : int Spherical harmonic order - design_parameter : float, double + design_parameter : float This can either be the desired side-lobe attenuation or the width of the main-lobe in radians. - design_criterion : 'sidelobe', 'mainlobe' - Whether the design parameter argument is the desired side-lobe - attenuation or the desired main-lobe width. + design_criterion : str + Can be either ``'sidelobe'``or ``'mainlobe'``. + Determines whether the design parameter argument is the desired + side-lobe attenuation or the desired main-lobe width. + Default is ``'sidelobe'``. Returns ------- - weigths : ndarray, double - An array containing the weight coefficients $d_nm$. + weights : array-like, float + An array containing the weight coefficients. References ---------- - .. [1] A. Koretz and B. Rafaely, “Dolph-Chebyshev beampattern design for - spherical arrays,” IEEE Transactions on Signal Processing, vol. 57, - no. 6, pp. 2417–2420, 2009. + .. [#] A. Koretz and B. Rafaely, “Dolph-Chebyshev beampattern design for + spherical arrays,” IEEE Transactions on Signal Processing, vol. 57, + no. 6, pp. 2417-2420, 2009. + + Examples + -------- + Apply the weights for a Dolph-Chebyshev beamformer with a side-lobe + attenuation of 50 dB to a plane wave decomposition. The direction of + arrival is zero degrees azimuth. + + .. plot:: + + >>> import spharpy + >>> import pyfar as pf + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> N = 7 + >>> steering = pf.Coordinates.from_spherical_colatitude( + ... np.linspace(0, 2*np.pi, 500), np.ones(500)*np.pi/2, + ... np.ones(500)) + >>> Y_steering = spharpy.spherical.spherical_harmonic_basis_real( + ... N, steering) + >>> a_nm = spharpy.spherical.spherical_harmonic_basis_real( + ... N, pf.Coordinates(1, 0, 0)) + >>> R = 10**(50/20) + >>> d_nm = spharpy.beamforming.dolph_chebyshev_weights( + ... N, R, design_criterion='sidelobe') + >>> beamformer = np.squeeze(Y_steering @ np.diag(d_nm) @ a_nm.T) + >>> ax = plt.axes(projection='polar') + >>> ax.plot(steering.azimuth, 20*np.log10(np.abs(beamformer))) + >>> ax.set_rticks([-50, -25, 0]) + >>> ax.set_theta_zero_location('N') + >>> ax.set_xlabel('Azimuth (deg)') """ M = 2*n_max @@ -63,36 +97,63 @@ def dolph_chebyshev_weights( for i in range(n+1): for j in range(n_max+1): for m in range(j+1): - temp = temp+(1-(-1)**(m+i+1))/(m+i+1) * \ - factorial(j)/(factorial(m)*factorial(j-m)) * \ - (1/2**j)*t_2N[2*j]*P_N[i, n]*x0**(2*j) + temp += (1-(-1)**(m+i+1))/(m+i+1) * \ + factorial(j)/(factorial(m)*factorial(j-m)) * \ + (1/2**j)*t_2N[2*j]*P_N[i, n]*x0**(2*j) d_n[n] = (2*np.pi/R)*temp - return spharpy.indexing.sph_identity_matrix(n_max, type='n-nm').T @ d_n + return sph_identity_matrix(n_max, matrix_type='n-nm').T @ d_n def rE_max_weights(n_max, normalize=True): """Weights that maximize the length of the energy vector. - This is most often used in Ambisonics decoding. + This is most often used in Ambisonics decoding [#]_. Parameters ---------- n_max : int Spherical harmonic order normalize : bool - If `True`, the weights will be normalized such that the complex - amplitude of a plane wave is not distorted. + If ``True``, the weights will be normalized such that the complex + amplitude of a plane wave is not distorted. Default is ``True``. Returns ------- - weights : ndarray, double + weights : array-like, float An array containing the weight coefficients. References ---------- - .. [2] J. Daniel, J.-B. Rault, and J.-D. Polack, “Ambisonics Encoding of - Other Audio Formats for Multiple Listening Conditions,” in 105th - Convention of the Audio Engineering Society, 1998, vol. 3. + .. [#] J. Daniel, J.-B. Rault, and J.-D. Polack, “Ambisonics Encoding of + Other Audio Formats for Multiple Listening Conditions,” in 105th + Convention of the Audio Engineering Society, 1998, vol. 3. + + Examples + -------- + Apply the max-rE weights to a plane wave decomposition. The direction of + arrival is zero degrees azimuth. + + .. plot:: + + >>> import spharpy + >>> import pyfar as pf + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> N = 7 + >>> steering = pf.Coordinates.from_spherical_colatitude( + ... np.linspace(0, 2*np.pi, 500), np.ones(500)*np.pi/2, + ... np.ones(500)) + >>> Y_steering = spharpy.spherical.spherical_harmonic_basis_real( + ... N, steering) + >>> a_nm = spharpy.spherical.spherical_harmonic_basis_real( + ... N, pf.Coordinates(1, 0, 0)) + >>> d_nm = spharpy.beamforming.rE_max_weights(N) + >>> beamformer = np.squeeze(Y_steering @ np.diag(d_nm) @ a_nm.T) + >>> ax = plt.axes(projection='polar') + >>> ax.plot(steering.azimuth, 20*np.log10(np.abs(beamformer))) + >>> ax.set_rticks([-50, -25, 0]) + >>> ax.set_theta_zero_location('N') + >>> ax.set_xlabel('Azimuth (deg)') """ leg = poly.legendre.Legendre.basis(n_max+1) @@ -106,33 +167,60 @@ def rE_max_weights(n_max, normalize=True): if normalize: g_n = normalize_beamforming_weights(g_n, n_max) - return spharpy.indexing.sph_identity_matrix(n_max).T @ g_n + return sph_identity_matrix(n_max).T @ g_n def maximum_front_back_ratio_weights(n_max, normalize=True): """Weights that maximize the front-back ratio of the beam pattern. This is also often referred to as the super-cardioid beam pattern. + The weights are calculated from an eigenvalue problem [#]_. For high + spherical harmonic orders, the eigenvalue problem may not be feasible and + a solution will not be found. Parameters ---------- n_max : int The spherical harmonic order normalize : bool - If `True`, the weights will be normalized such that the complex - amplitude of a plane wave is not distorted. + If ``True``, the weights will be normalized such that the complex + amplitude of a plane wave is not distorted. Default is ``True``. Returns ------- - weigths : ndarray, double + weights : array-like, float An array containing the weight coefficients - Note - ---- - The weights are calculated from an eigenvalue problem - References ---------- - [3] B. Rafaely, Fundamentals of Spherical Array Processing, Springer, 2015. + .. [#] B. Rafaely, Fundamentals of Spherical Array Processing, + Springer, 2015. + + Examples + -------- + Apply weights maximizing the front-back ratio to a plane wave + decomposition. The direction of arrival is zero degrees azimuth. + + .. plot:: + + >>> import spharpy + >>> import pyfar as pf + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> N = 5 + >>> steering = pf.Coordinates.from_spherical_colatitude( + ... np.linspace(0, 2*np.pi, 500), np.ones(500)*np.pi/2, + ... np.ones(500)) + >>> Y_steering = spharpy.spherical.spherical_harmonic_basis_real( + ... N, steering) + >>> a_nm = spharpy.spherical.spherical_harmonic_basis_real( + ... N, pf.Coordinates(1, 0, 0)) + >>> d_nm = spharpy.beamforming.maximum_front_back_ratio_weights(N) + >>> beamformer = np.squeeze(Y_steering @ np.diag(d_nm) @ a_nm.T) + >>> ax = plt.axes(projection='polar') + >>> ax.plot(steering.azimuth, 20*np.log10(np.abs(beamformer))) + >>> ax.set_rticks([-75, -50, -25, 0]) + >>> ax.set_theta_zero_location('N') + >>> ax.set_xlabel('Azimuth (deg)') """ P_N = np.zeros((n_max+1, n_max+1)) @@ -166,7 +254,7 @@ def maximum_front_back_ratio_weights(n_max, normalize=True): else: f_n /= np.sign(f_n[0]) - return spharpy.indexing.sph_identity_matrix(n_max).T @ f_n + return sph_identity_matrix(n_max).T @ f_n def normalize_beamforming_weights(weights, n_max): @@ -175,15 +263,26 @@ def normalize_beamforming_weights(weights, n_max): Parameters ---------- - weights : ndarray, double + weights : array-like, float An array containing the beamforming weights n_max : int The spherical harmonic order Returns ------- - weights : ndarray, double + weights : array-like, float An array containing the normalized beamforming weights + Examples + -------- + Calculate hann window function based tapering weights for a plane wave + decomposition beamformer and normalize. + + >>> import spharpy + >>> from scipy.signal.windows import hann + >>> tapering_window = hann(2*(N+1)+1)[N+1:-1], N) + >>> h_n = spharpy.beamforming.normalize_beamforming_weights( + ... tapering_window, N) + """ return weights / np.dot(weights, 2*np.arange(0, n_max+1)+1) * (4*np.pi) diff --git a/spharpy/classes/__init__.py b/spharpy/classes/__init__.py new file mode 100644 index 00000000..8c4cb4cd --- /dev/null +++ b/spharpy/classes/__init__.py @@ -0,0 +1,21 @@ +"""spharpy classes.""" + +from .sh import ( + SphericalHarmonicDefinition, + SphericalHarmonics, +) + +from .audio import ( + SphericalHarmonicSignal, +) + +from .coordinates import ( + SamplingSphere, +) + +__all__ = [ + 'SphericalHarmonicDefinition', + 'SphericalHarmonics', + 'SphericalHarmonicSignal', + 'SamplingSphere', +] diff --git a/spharpy/classes/audio.py b/spharpy/classes/audio.py new file mode 100644 index 00000000..914df3f0 --- /dev/null +++ b/spharpy/classes/audio.py @@ -0,0 +1,185 @@ +"""Implementations of audio data container classes.""" + +from pyfar import Signal +from spharpy.spherical import renormalize, change_channel_convention +import numpy as np + + +class SphericalHarmonicSignal(Signal): + """Create audio object with spherical harmonics coefficients in time or + frequency domain. + + Objects of this class contain spherical harmonics coefficients which are + directly convertible between time and frequency domain (equally spaced + samples and frequency bins), the channel conventions ACN and FuMa, as + well as the normalizations N3D, SN3D, or MaxN, see [#]_. The definition of + the spherical harmonics basis functions is based on the scipy convention + which includes the Condon-Shortley phase, [#]_, [#]_. + + + Parameters + ---------- + data : ndarray, double + Raw data of the spherical harmonics signal in the time or + frequency domain. The data should have at least 3 dimensions, + according to the 'C' memory layout, e.g. data of + ``shape = (1, 4, 1024)`` has 1 channel with 4 spherical harmonic + coefficients with 1024 samples or frequency + bins each. Time data is converted to ``float``. Frequency is + converted to ``complex`` and must be provided as single + sided spectra, i.e., for all frequencies between 0 Hz and + half the sampling rate. + sampling_rate : double + Sampling rate in Hz + basis_type : str + Type of spherical harmonic basis, either ``'complex'`` or + ``'real'``. + normalization : str + Normalization convention, either ``'N3D'``, ``'NM'``, + ``'maxN'``, ``'SN3D'``, or ``'SNM'``. + (maxN is only supported up to 3rd order) + channel_convention : str + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + (FuMa is only supported up to 3rd order) + condon_shortley : bool + Flag to indicate if the Condon-Shortley phase term is included + (``True``) or not (``False``). + n_samples : int, optional + Number of time domain samples. Required if domain is ``'freq'``. + The default is ``None``, which assumes an even number of samples + if the data is provided in the frequency domain. + domain : ``'time'``, ``'freq'``, optional + Domain of data. The default is ``'time'`` + fft_norm : str, optional + The normalization of the Discrete Fourier Transform (DFT). Can be + ``'none'``, ``'unitary'``, ``'amplitude'``, ``'rms'``, ``'power'``, + or ``'psd'``. See :py:func:`~pyfar.dsp.fft.normalization` + for more information. The default is ``'none'``, which is typically + used for energy signals, such as impulse responses. + comment : str + A comment related to `data`. The default is ``None``. + is_complex : bool, optional + Specifies if the underlying time domain data are complex + or real-valued. If ``True`` and `domain` is ``'time'``, the + input data will be cast to complex. The default is ``False``. + + References + ---------- + .. [#] F. Zotter, M. Frank, "Ambisonics A Practical 3D Audio Theory + for Recording, Studio Production, Sound Reinforcement, and + Virtual Reality", (2019), Springer-Verlag + .. [#] B. Rafely, "Fundamentals of Spherical Array Processing", (2015), + Springer-Verlag + .. [#] E.G. Williams, "Fourier Acoustics", (1999), Academic Press + + """ + + def __init__( + self, + data, + sampling_rate, + basis_type, + normalization, + channel_convention, + condon_shortley, + n_samples=None, + domain='time', + fft_norm='none', + comment="", + is_complex=False): + """ + Create SphericalHarmonicSignal with data, and sampling rate. + """ + # check dimensions + if len(data.shape) < 3: + raise ValueError("Invalid number of dimensions. Data should have " + "at least 3 dimensions.") + + # set n_max + n_max = np.sqrt(data.shape[-2])-1 + if n_max - int(n_max) != 0: + raise ValueError("Invalid number of SH channels: " + f"{data.shape[-2]}. It must match (n_max + 1)^2.") + self._n_max = int(n_max) + + # set basis_type + if basis_type not in ["complex", "real"]: + raise ValueError("Invalid basis type, only " + "'complex' and 'real' are supported") + self._basis_type = basis_type + + # set normalization + if normalization not in ["N3D", "NM", "maxN", "SN3D", "SNM"]: + raise ValueError("Invalid normalization, has to be 'N3D', 'NM', " + "'maxN', 'SN3D', or 'SNM', " + f"but is {normalization}") + self._normalization = normalization + + # set channel_convention + if channel_convention not in ["ACN", "FuMa"]: + raise ValueError("Invalid channel convention, has to be 'ACN' " + f"or 'FuMa', but is {channel_convention}") + self._channel_convention = channel_convention + + # set Condon Shortley + if not isinstance(condon_shortley, bool): + raise ValueError("Condon_shortley has to be a bool.") + self._condon_shortley = condon_shortley + + Signal.__init__(self, data, sampling_rate=sampling_rate, + n_samples=n_samples, domain=domain, fft_norm=fft_norm, + comment=comment, is_complex=is_complex) + + @property + def n_max(self): + """Get the maximum spherical harmonic order.""" + return self._n_max + + @property + def basis_type(self): + """Get the type of the spherical harmonic basis.""" + return self._basis_type + + @property + def normalization(self): + """ + Get or set and apply the normalization of the spherical harmonic + coefficients. + """ + return self._normalization + + @normalization.setter + def normalization(self, value): + """ + Get or set and apply the normalization of the spherical harmonic + coefficients. + """ + if self.normalization is not value: + self._data = renormalize(self._data, self.channel_convention, + self.normalization, value, axis=-2) + self._normalization = value + + @property + def condon_shortley(self): + """Get info whether to include the Condon-Shortley phase term.""" + return self._condon_shortley + + @property + def channel_convention(self): + """ + Get or set and apply the channel convention of the spherical harmonic + coefficients. + """ + return self._channel_convention + + @channel_convention.setter + def channel_convention(self, value): + """ + Get or set and apply the channel convention of the spherical harmonic + coefficients. + """ + if self.channel_convention is not value: + self._data = change_channel_convention(self._data, + self.channel_convention, + value, axis=-2) + self._channel_convention = value diff --git a/spharpy/classes/coordinates.py b/spharpy/classes/coordinates.py new file mode 100644 index 00000000..fa24bbb6 --- /dev/null +++ b/spharpy/classes/coordinates.py @@ -0,0 +1,666 @@ +r""" +The SamplingSphere class inherits from the :py:mod:`pyfar.classes.coordinates` +class, which supports various coordinate systems and the conversion between +them. +:py:class:`~spharpy.SamplingSphere` is designed to represent a set of +points on a sphere. + +Therefore, all points must have the same radius within an absolute tolerance, +defined by :py:attr:`~spharpy.SamplingSphere.radius_tolerance`. If the +:py:attr:`~spharpy.SamplingSphere.weights` are not None, their sum must +equal the integral over the unit sphere, which is :math:`4\pi`. + +The property :py:attr:`~spharpy.SamplingSphere.quadrature` specifies if the +points belong to a quadrature, which requires that the +valid weights, the maximum +spherical harmonic order of the sampling +grid :py:attr:`~spharpy.SamplingSphere.n_max` is specified and the inner +product of the weighted spherical harmonics matrix :math:`\mathrm{Y}` +yields the identity matrix +:math:`\mathrm{Y}^\mathrm{T} \text{diag}\{w\}\mathrm{Y}=\mathrm{I}`, +with the weights vector :math:`w`. The sampling is considered a valid +quadrature if the maximum absolute deviation of +:math:`\mathrm{Y}^\mathrm{T} \text{diag}\{w\}\mathrm{Y}` from :math:`I` is +smaller than the specified +:py:attr:`~spharpy.SamplingSphere.quadrature_tolerance`. + +It also adds the additional property: + +- :py:attr:`~spharpy.SamplingSphere.n_max`: the maximum spherical harmonic + order of the sampling grid. + +Note that the :py:mod:`spharpy.samplings` module provides a set of +predefined spherical sampling grids, which can be used to create a +:py:class:`spharpy.SamplingSphere` object. +""" + +import numpy as np +from pyfar.classes.coordinates import sph2cart, cyl2cart +import pyfar as pf +from spharpy.spherical import spherical_harmonic_basis_real + + +class SamplingSphere(pf.Coordinates): + """Class for samplings on a sphere.""" + + def __init__( + self, x=None, y=None, z=None, n_max=None, weights: np.array = None, + comment: str = "", radius_tolerance=1e-6, + quadrature_tolerance=1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + x : ndarray, number + X coordinate of a right handed Cartesian coordinate system in + meters (-\infty < x < \infty). + y : ndarray, number + Y coordinate of a right handed Cartesian coordinate system in + meters (-\infty < y < \infty). + z : ndarray, number + Z coordinate of a right handed Cartesian coordinate system in + meters (-\infty < z < \infty). + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + """ + self._radius_tolerance = None + self.radius_tolerance = radius_tolerance + + pf.Coordinates.__init__( + self, x, y, z, weights=weights, comment=comment) + self._n_max = n_max + + self._quadrature_tolerance = None + self.quadrature_tolerance = quadrature_tolerance + + self._quadrature = None + + @classmethod + def from_coordinates( + cls, coordinates, n_max=None, radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Convert Coordinates class object to SamplingSphere class object. + + Parameters + ---------- + coordinates : pyfar.Coordinates + Coordinates to be converted. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + """ + + if type(coordinates) is not pf.Coordinates: + raise TypeError('coordinates must be a pyfar Coordinates object') + + # make sure mutable data is copied + if coordinates.weights is None: + weights = None + else: + weights = coordinates.weights.copy() + + return cls( + coordinates.x, coordinates.y, coordinates.z, + weights=weights, comment=coordinates.comment, + n_max=n_max, radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_cartesian( + cls, x, y, z, n_max=None, weights: np.array = None, + comment: str = "", radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + x : ndarray, number + X coordinate of a right handed Cartesian coordinate system in + meters (-\infty < x < \infty). + y : ndarray, number + Y coordinate of a right handed Cartesian coordinate system in + meters (-\infty < y < \infty). + z : ndarray, number + Z coordinate of a right handed Cartesian coordinate system in + meters (-\infty < z < \infty). + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_cartesian(0, 0, 1) + + or the same using + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere(0, 0, 1) + """ + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_spherical_elevation( + cls, azimuth, elevation, radius, n_max=None, + weights: np.array = None, comment: str = "", + radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + azimuth : ndarray, double + Angle in radiant of rotation from the x-y-plane facing towards + positive x direction. Used for spherical and cylindrical coordinate + systems. + elevation : ndarray, double + Angle in radiant with respect to horizontal plane (x-z-plane). + Used for spherical coordinate systems. + radius : ndarray, double + Distance to origin for each point. Used for spherical coordinate + systems. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, float, None, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_spherical_elevation(0, 0, 1) + """ + + x, y, z = sph2cart( + azimuth, np.pi / 2 - np.atleast_1d(elevation), radius) + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_spherical_colatitude( + cls, azimuth, colatitude, radius, n_max=None, + weights: np.array = None, comment: str = "", + radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + azimuth : ndarray, double + Angle in radiant of rotation from the x-y-plane facing towards + positive x direction. Used for spherical and cylindrical coordinate + systems. + colatitude : ndarray, double + Angle in radiant with respect to polar axis (z-axis). Used for + spherical coordinate systems. + radius : ndarray, double + Distance to origin for each point. Used for spherical coordinate + systems. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_spherical_colatitude(0, 0, 1) + """ + + x, y, z = sph2cart(azimuth, colatitude, radius) + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_spherical_side( + cls, lateral, polar, radius, n_max=None, + weights: np.array = None, comment: str = "", + radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + lateral : ndarray, double + Angle in radiant with respect to horizontal plane (x-y-plane). + Used for spherical coordinate systems. + polar : ndarray, double + Angle in radiant of rotation from the x-z-plane facing towards + positive x direction. Used for spherical coordinate systems. + radius : ndarray, double + Distance to origin for each point. Used for spherical coordinate + systems. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_spherical_side(0, 0, 1) + """ + + x, z, y = sph2cart( + polar, np.pi / 2 - np.atleast_1d(lateral), radius) + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_spherical_front( + cls, frontal, upper, radius, n_max=None, weights: np.array = None, + comment: str = "", radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + frontal : ndarray, double + Angle in radiant of rotation from the y-z-plane facing towards + positive y direction. Used for spherical coordinate systems. + upper : ndarray, double + Angle in radiant with respect to polar axis (x-axis). Used for + spherical coordinate systems. + radius : ndarray, double + Distance to origin for each point. Used for spherical coordinate + systems. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_spherical_front(0, 0, 1) + """ + + y, z, x = sph2cart(frontal, upper, radius) + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @classmethod + def from_cylindrical( + cls, azimuth, z, rho, n_max=None, weights: np.array = None, + comment: str = "", radius_tolerance: float = 1e-6, + quadrature_tolerance: float = 1e-10): + r""" + Create a SamplingSphere class object from a set of points on a sphere. + + See :py:mod:`pyfar.classes.coordinates` for more information. + + Parameters + ---------- + azimuth : ndarray, double + Angle in radiant of rotation from the x-y-plane facing towards + positive x direction. Used for spherical and cylindrical coordinate + systems. + z : ndarray, double + The z coordinate + rho : ndarray, double + Distance to origin for each point in the x-y-plane. Used for + cylindrical coordinate systems. + n_max : int, optional + Maximum spherical harmonic order of the sampling grid. + The default is ``None``. + weights: array like, number, optional + Weighting factors for coordinate points. Their sum must equal to + the integral over the unit sphere, which is :math:`4\pi`. + The `shape` of the array must match the `shape` of the individual + coordinate arrays. The default is ``None``, which means that no + weights are used. + comment : str, optional + Comment about the stored coordinate points. The default is + ``""``, which initializes an empty string. + radius_tolerance : float, optional + All points that are stored in a SamplingSphere must have the same + radius and an error is raised if the maximum deviation from the + mean radius exceeds this tolerance. The default of ``1e-6`` meter + is intended to allow for some numerical inaccuracy. + quadrature_tolerance : float, optional + Tolerance for testing whether the provided sampling grid is + a valid quadrature. The sampling is considered a valid quadrature + if the maximum absolute deviation of the inner product of the + weighted spherical harmonics matrix from the identity matrix is + smaller than the specified tolerance. The default is ``1e-10``. + + Examples + -------- + Create a SamplingSphere object + + >>> import pyfar as pf + >>> sampling = pf.SamplingSphere.from_cylindrical(0, 0, 1, sh_order=1) + """ + + x, y, z = cyl2cart(azimuth, z, rho) + return cls( + x, y, z, weights=weights, comment=comment, n_max=n_max, + radius_tolerance=radius_tolerance, + quadrature_tolerance=quadrature_tolerance) + + @property + def n_max(self): + """Get the maximum spherical harmonic order.""" + return self._n_max + + @n_max.setter + def n_max(self, value): + """Set the maximum spherical harmonic order.""" + assert value >= 0 + + if value is None: + self._n_max = None + self._quadrature = False + else: + if self._n_max != value: + self._quadrature = None + self._n_max = int(value) + + @property + def radius_tolerance(self): + """Get or set the radius tolerance in meter.""" + return self._radius_tolerance + + @radius_tolerance.setter + def radius_tolerance(self, value): + """Get or set the radius tolerance in meter.""" + + # check input + if not isinstance(value, (int, float)) or value <= 0: + raise ValueError( + 'The radius tolerance must be a number greater than zero') + + current_tolerance = self.radius_tolerance + self._radius_tolerance = float(value) + + # Check if points meet new tolerance if points exist + if hasattr(self, 'x'): + try: + self._check_points(self._x, self._y, self._z) + except ValueError as e: + # revert setting the tolerance and raise the error + self._radius_tolerance = current_tolerance + raise e + + @property + def quadrature_tolerance(self): + """Get or set the quadrature tolerance.""" + return self._quadrature_tolerance + + @quadrature_tolerance.setter + def quadrature_tolerance(self, value): + """Get or set the quadrature tolerance.""" + + # check input + if not isinstance(value, (int, float)) or value <= 0: + raise ValueError( + 'The quadrature tolerance must be a number greater than zero') + + self._quadrature_tolerance = float(value) + self._quadrature = None + + def _check_points(self, x, y, z): + """Check input data before setting coordinates.""" + + # convert to numpy arrays of the same shape + x, y, z = super()._check_points(x, y, z) + + # check for equal radius + radius = np.sqrt(x.flatten()**2 + y.flatten()**2 + z.flatten()**2) + radius_delta = np.max(radius) - np.min(radius) + if radius_delta > self.radius_tolerance: + raise ValueError( + 'All points must have the same radius but the difference ' + f'between the minimum and maximum radius is {radius_delta:.3g}' + ' m, which exceeds the tolerance of ' + f'{self.radius_tolerance:.3g} m. The tolerance can be changed ' + 'using SamplingSphere.radius_tolerance.') + + # reset the quadrature flag to make sure it is checked again after + # adding or changing points in the SamplingSphere + self._quadrature = None + + return x, y, z + + def _check_weights(self, weights): + r"""Check if the weights are valid. + The weights must be positive and their sum must equal integration of + the unit sphere, i.e. :math:`4\pi`. + + Parameters + ---------- + weights : array like, number + the weights for each point, should be of size of self.csize. + + Returns + ------- + weights : np.ndarray[float64], None + The weights reshaped to the cshape of the coordinates if not None. + Otherwise None. + """ + weights = super()._check_weights(weights) + + if weights is None: + return weights + if np.any(weights < 0) or np.any(np.isnan(weights)): + raise ValueError("All weights must be positive numeric values.") + + if not np.isclose(np.sum(weights), 4*np.pi, atol=1e-6, rtol=1e-6): + raise ValueError( + "The sum of the weights must be equal to 4*pi. " + f"Current sum: {np.sum(weights)}") + + return weights + + def _check_quadrature(self): + r"""Check if the sampling is a valid quadrature. + + Returns + ------- + check : bool + Indicates if sampling is a valid quadrature + """ + if self.n_max is None or self.weights is None: + return False + # create basis matrix + sh_basis = spherical_harmonic_basis_real(self.n_max, self) + + # test if basis is quadrature + quad_evaluation = sh_basis.T @ np.diag(self.weights) @ sh_basis + identity = np.eye((self.n_max + 1)**2) + + error = np.max(np.abs(quad_evaluation-identity)) + return error < self.quadrature_tolerance + + @property + def weights(self): + r"""The area/quadrature weights of the sampling. + Their sum must equal to :math:`4\pi`. + """ + return super().weights + + @weights.setter + def weights(self, weights): + r"""Get or set the area/quadrature weights of the sampling. + Their sum must equal to :math:`4\pi`. + """ + if not np.array_equal(weights, self.weights): + self._quadrature = None + + super(__class__, type(self)).weights.fset(self, weights) + + @property + def quadrature(self): + """Get the quadrature flag.""" + if self._quadrature is None: + # recompute _quadrature flag + self._quadrature = self._check_quadrature() + + return self._quadrature diff --git a/spharpy/classes/sh.py b/spharpy/classes/sh.py new file mode 100644 index 00000000..4c3ba392 --- /dev/null +++ b/spharpy/classes/sh.py @@ -0,0 +1,520 @@ +""" +Documentation for the SphericalHarmonics class, will be added in an other PR. +""" +import numpy as np +import pyfar as pf +import spharpy as sy +from abc import ABC, abstractmethod + + +class _SphericalHarmonicBase(ABC): + """Base class defining properties to parametrize spherical harmonics. + + This base class serves as a base for all classes requiring a definition of + the spherical harmonics without explicitly setting a spherical harmonic + order. This class is intended for cases where the spherical harmonic order + is implemented as a read-only property in child classes, for example when + the order is implicitly defined by other parameters or inferred from data. + + Attributes + ---------- + basis_type : str + Type of spherical harmonic basis, either ``'real'`` or ``'complex'``. + The default is ``'real'``. + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. Note that ``'FuMa'`` is only supported up to + 3rd order. + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. The default is ``'N3D'``. Note that + ``'maxN'`` is only supported up to 3rd order. + condon_shortley : bool, str, optional + Condon-Shortley phase term. If ``True``, Condon-Shortley is included, + if ``False`` it is not included. The default is ``'auto'``, which + corresponds to ``True`` for type ``complex`` and ``False`` for type + ``real``. + """ + + def __init__( + self, + basis_type="real", + channel_convention="ACN", + normalization="N3D", + condon_shortley="auto", + ): + self._basis_type = None + self._channel_convention = None + self._condon_shortley = None + self._normalization = None + + # basis_type needs to be initialized first, since the default for the + # Condon-Shortley phase depends on the basis type + self.basis_type = basis_type + self.condon_shortley = condon_shortley + # n_max needs to be initialized before channel_convention and + # normalization, since both have restrictions on n_max + self.channel_convention = channel_convention + self.normalization = normalization + + @property + @abstractmethod + def n_max(self): + """Get or set the spherical harmonic order.""" + + @property + def condon_shortley(self): + """Get or set the Condon-Shortley phase term.""" + return self._condon_shortley + + @condon_shortley.setter + def condon_shortley(self, value): + """Get or set the Condon-Shortley phase term.""" + if isinstance(value, str): + if value != 'auto': + raise ValueError( + "condon_shortley must be a bool or the string 'auto'") + + value = self.basis_type == "complex" + elif not isinstance(value, bool): + raise ValueError( + "condon_shortley must be a bool or the string 'auto'") + + if self._condon_shortley != value: + self._condon_shortley = value + self._on_property_change() + + @property + def basis_type(self): + """Get or set the type of spherical harmonic basis.""" + return self._basis_type + + @basis_type.setter + def basis_type(self, value): + """Get or set the type of spherical harmonic basis.""" + if value not in ["complex", "real"]: + raise ValueError( + "Invalid basis type, only 'complex' and 'real' are supported") + + if self._basis_type != value: + self._basis_type = value + self._on_property_change() + + @property + def channel_convention(self): + """Get or set the channel ordering convention.""" + return self._channel_convention + + @channel_convention.setter + def channel_convention(self, value): + """Get or set the channel order convention.""" + if value not in ["ACN", "FuMa"]: + raise ValueError("Invalid channel convention, " + "currently only 'ACN' " + "and 'FuMa' are supported") + + if value == "FuMa" and self.n_max > 3: + raise ValueError( + "n_max > 3 is not allowed with 'FuMa' channel convention") + + if self._channel_convention != value: + self._channel_convention = value + self._on_property_change() + + @property + def normalization(self): + """Get or set the normalization convention.""" + return self._normalization + + @normalization.setter + def normalization(self, value): + """Get or set the normalization convention.""" + if value not in ["N3D", "NM", "maxN", "SN3D", "SNM"]: + raise ValueError( + "Invalid normalization, " + "currently only 'N3D', 'NM', 'maxN', 'SN3D', 'SNM' are " + "supported", + ) + + if value == "maxN" and self.n_max > 3: + raise ValueError( + "n_max > 3 is not allowed with 'maxN' normalization") + + if self._normalization != value: + self._normalization = value + self._on_property_change() + + def _on_property_change(self): # noqa: B027 + """Method called when a class property changes. + This method can be overridden in child classes to re-compute dependent + properties. + """ + pass + + +class SphericalHarmonicDefinition(_SphericalHarmonicBase): + """Class storing the (discrete) definition of spherical harmonics. + + This class can serve as a container to create related objects, e.g., + spherical harmonic basis matrices for given sampling points, transforms, + or other spherical harmonic related data and computations. + + Attributes + ---------- + n_max : int, optional + Maximum spherical harmonic order. The default is ``0``. + basis_type : str + Type of spherical harmonic basis, either ``'real'`` or ``'complex'``. + The default is ``'real'``. + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. Note that ``'FuMa'`` is only supported up to + 3rd order. + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. The default is ``'N3D'``. Note that + ``'maxN'`` is only supported up to 3rd order. + condon_shortley : bool, str, optional + Condon-Shortley phase term. If ``True``, Condon-Shortley is included, + if ``False`` it is not included. The default is ``'auto'``, which + corresponds to ``True`` for type ``complex`` and ``False`` for type + ``real``. + """ + + def __init__( + self, + n_max=0, + basis_type="real", + channel_convention="ACN", + normalization="N3D", + condon_shortley="auto", + ): + self._n_max = 0 + super().__init__( + basis_type=basis_type, + channel_convention=channel_convention, + normalization=normalization, + condon_shortley=condon_shortley, + ) + self.n_max = n_max + + @property + def n_max(self): + """Get or set the spherical harmonic order.""" + return self._n_max + + @n_max.setter + def n_max(self, value : int): + """Get or set the spherical harmonic order.""" + if value < 0 or value % 1 != 0: + raise ValueError("n_max must be a positive integer") + if self.channel_convention == "FuMa" and value > 3: + raise ValueError( + "n_max > 3 is not allowed with 'FuMa' channel convention") + if self.normalization == "maxN" and value > 3: + raise ValueError( + "n_max > 3 is not allowed with 'maxN' normalization") + + if self._n_max != value: + self._n_max = value + self._on_property_change() + + +class SphericalHarmonics(SphericalHarmonicDefinition): + r""" + Compute spherical harmonic basis matrices, their inverses, and gradients. + + The spherical harmonic Ynm is given by: + + .. math:: + Y_{nm} = N_{nm} P_{nm}(cos(\theta)) T_{nm}(\phi) + + where: + + + - :math:`n` is the degree + - :math:`m` is the order + - :math:`P_{nm}` is the associated Legendre function + - :math:`N_{nm}` is the normalization term + - :math:`T_{nm}` is a term that depends on whether the harmonics are + real or complex + - :math:`\theta` is the colatitude (angle from the positive z-axis) + - :math:`\phi` is the azimuth (angle in the x-y plane from the x-axis) + + The normalization term :math:`N_{nm}` is given by: + + .. math:: + N_{nm}^{\text{SN3D}} = + \sqrt{\frac{2n+1}{4\pi} \frac{(n-|m|)!}{(n+|m|)!}} + + N_{nm}^{\text{N3D}} = N_{nm}^{\text{SN3D}} \sqrt{\frac{2n+1}{2}} + + N_{nm}^{\text{MaxN}} = ... (max of N3D) + + The associated Legendre function :math:`P_{nm}` is defined as: + + .. math:: + P_{nm}(x) = (1-x^2)^{|m|/2} (d/dx)^n (x^2-1)^n + + The term :math:`T_{nm}` is defined as: + + - For complex-valued harmonics: + .. math:: + T_{nm} = e^{im\phi} + - For real-valued harmonics: + .. math:: + T_{nm} = \begin{cases} + \cos(m\phi) & \text{if } m \ge 0 \\ + \sin(m\phi) & \text{if } m < 0 + \end{cases} + + The spherical harmonics are orthogonal on the unit sphere, i.e., + + .. math:: + \int_{sphere} Y_{nm} Y_{n'm'}* d\omega = \delta_{nn'} \delta_{mm'} + + where: + + - :math:`*` denotes complex conjugation + - :math:`\delta_{nn'}` is the Kronecker delta function + - :math:`d\omega` is the solid angle element + - The integral is over the entire sphere + + The class supports the following conventions: + + - normalization: Defines the normalization convention: + + - ``'N3D'``: Uses the 3D normalization + (also known as Schmidt semi-normalized). + - ``'maxN'``: Uses the maximum norm + (also known as fully normalized). + - ``'SN3D'``: Uses the SN3D normalization + (also known as Schmidt normalized). + - ``'NM'``: Uses the monopole normalization + (similar to n3d but normalized to a monopole) + - ``'SNM'``: Uses the monopole semi-normalization + (similar to sn3d but normalized to a monopole) + + - channel_convention: Defines the channel ordering convention. + + - ``'ACN'``: Follows the Ambisonic Channel Number (ACN) convention. + - ``'FuMa'``: Follows the Furse-Malham (FuMa) convention. + (FuMa is only supported up to 3rd order) + + - inverse_method: Defines the type of inverse transform. + + - ``'pseudo_inverse'``: Uses the Moore-Penrose pseudo-inverse + for the inverse transform. + - ``'quadrature'``: Uses quadrature for the inverse transform. + - ``'auto'``: ``'quadrature'`` if `coordinates.quadrature` + is True otherwise ``'quadrature'``. If `coordinates` is not + SamplingSphere an error is returned. + + + + Parameters + ---------- + n_max : int + Maximum spherical harmonic order + coordinates : :py:class:`pyfar.Coordinates`, spharpy.SamplingSphere + objects with sampling points for which the basis matrix is + calculated + basis_type : str, optional + Type of spherical harmonic basis, either ``'complex'`` or + ``'real'``. The default is ``'real'``. + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, + ``'maxN'``, ``'SN3D'``, or ``'SNM'``. + The default is ``'N3D'``. + (maxN is only supported up to 3rd order) + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. + (FuMa is only supported up to 3rd order) + inverse_method : {'auto', 'quadrature', 'pseudo_inverse'}, default='auto' + Method for computing the inverse transform: + + - ‘auto’: use ‘quadrature’ when applicable, otherwise ‘pseudo_inverse’. + - ‘quadrature’: compute the inverse via numerical quadrature. + - ‘pseudo_inverse’: compute the inverse via a pseudo-inverse + approximation. + condon_shortley : bool or str, optional + Whether to include the Condon-Shortley phase term. If ``True``, + Condon-Shortley is included, if ``False`` it is not + included. The default is ``'auto'``, which corresponds to + ``True`` for complex basis and ``False`` for real basis. + + """ + + def __init__( + self, + n_max, + coordinates, + basis_type="real", + normalization="N3D", + channel_convention="ACN", + inverse_method="auto", + condon_shortley="auto", + ): + super().__init__( + n_max=n_max, + basis_type=basis_type, + channel_convention=channel_convention, + normalization=normalization, + condon_shortley=condon_shortley, + ) + + # initialize private attributes + self._coordinates = pf.Coordinates() + self._inverse_method = None + self._reset_compute_attributes() + + self.coordinates = coordinates + self.inverse_method = inverse_method + + @property + def coordinates(self): + """Get or set the coordinates object.""" + return self._coordinates + + @coordinates.setter + def coordinates(self, value): + """Get or set the coordinates object.""" + if not isinstance(value, pf.Coordinates): + raise TypeError("coordinates must be a pyfar.Coordinates " + "object or spharpy.SamplingSphere object") + if value.cdim != 1: + raise ValueError("Coordinates must be a 1D array") + if value.csize == 0: + raise ValueError("Coordinates cannot be empty") + if value != self._coordinates: + self._reset_compute_attributes() + self._coordinates = value + + @property + def inverse_method(self): + """Get or set the type of inverse transform.""" + return self._inverse_method + + @inverse_method.setter + def inverse_method(self, value): + """Get or set the inverse transform type.""" + # If the user passes "auto", require SamplingSphere and resolve it + if isinstance(value, str) and value == "auto": + if not isinstance(self.coordinates, sy.SamplingSphere): + raise ValueError( + "'auto' is only valid if `coordinates` is " + "a SamplingSphere.") + value = ( + "quadrature" + if self.coordinates.quadrature + else "pseudo_inverse" + ) + + elif value == "quadrature": + if not isinstance(self.coordinates, sy.SamplingSphere) or \ + not self.coordinates.quadrature: + raise ValueError("'quadrature' requires `coordinates` to be " \ + "a SamplingSphere and coordinates.quadrature to be True.") + + elif value != "pseudo_inverse": + raise ValueError( + "Invalid inverse_method. Allowed: 'pseudo_inverse', " + "'quadrature', or 'auto'.") + + if value != self._inverse_method: + self._reset_compute_attributes() + self._inverse_method = value + + @property + def basis(self): + """Get the spherical harmonic basis matrix.""" + if self._basis is None: + self._compute_basis() + return self._basis + + @property + def basis_gradient_theta(self): + """Get the gradient of the basis matrix with respect to theta.""" + if self._basis_gradient_theta is None: + self._compute_basis_gradient() + return self._basis_gradient_theta + + @property + def basis_gradient_phi(self): + """Get the gradient of the basis matrix with respect to phi.""" + if self._basis_gradient_phi is None: + self._compute_basis_gradient() + return self._basis_gradient_phi + + def _compute_basis(self): + """ + Compute the basis matrix for the SphericalHarmonics class. + """ + if self.basis_type == "complex": + function = sy.spherical.spherical_harmonic_basis + elif self.basis_type == "real": + function = sy.spherical.spherical_harmonic_basis_real + else: + raise ValueError( + "Invalid basis type, should be either 'complex' or 'real'") + self._basis = function( + self.n_max, self.coordinates, + self.normalization, self.channel_convention) + + def _compute_basis_gradient(self): + """ + Compute the gradient of the basis matrix for the SphericalHarmonics + class. + """ + if any((self.normalization in ["maxN", "SN3D"], + self.channel_convention == "fuma")): + raise ValueError( + f"Gradient computation not supported for normalization " + f"'{self.normalization}' and " + f"channel convention '{self.channel_convention}'.") + + if self.basis_type == "complex": + function = sy.spherical.spherical_harmonic_basis_gradient + elif self.basis_type == "real": + function = sy.spherical.spherical_harmonic_basis_gradient_real + else: + raise ValueError( + "Invalid basis type, should be either 'complex' or 'real'") + self._basis_gradient_theta, self._basis_gradient_phi = function( + self.n_max, self.coordinates) + + @property + def basis_inv(self): + """Get or set the inverse basis matrix.""" + if self._basis is None: + self._compute_basis() + if self._basis_inv is None: + self._compute_inverse() + return self._basis_inv + + def _compute_inverse(self): + """ + Compute the inverse basis matrix for the SphericalHarmonics class. + """ + if self._basis is None: + self._compute_basis() + _inv_flag = self.inverse_method + if _inv_flag == "pseudo_inverse": + self._basis_inv = np.linalg.pinv(self._basis) + elif _inv_flag == "quadrature": + self._basis_inv = np.einsum('ij,i->ji', np.conj(self._basis), + self.coordinates.weights) + + def _reset_compute_attributes(self): + """Reset the computed attributes for the SphericalHarmonics class in + case of changes in the parameters. + """ + self._basis = None + self._basis_gradient_theta = None + self._basis_gradient_phi = None + self._basis_inv = None + + def _on_property_change(self): + """Reset computed attributes on property changes.""" + self._reset_compute_attributes() diff --git a/spharpy/indexing/__init__.py b/spharpy/indexing/__init__.py deleted file mode 100644 index 3fd5da5c..00000000 --- a/spharpy/indexing/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .channel_indexing import sid2acn, sid, sph_identity_matrix - -__all__ = ['sid2acn', 'sid', 'sph_identity_matrix'] diff --git a/spharpy/indexing/channel_indexing.py b/spharpy/indexing/channel_indexing.py deleted file mode 100644 index eb116d85..00000000 --- a/spharpy/indexing/channel_indexing.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Commonly used channel indexing functions used in Ambisonics -""" - -import numpy as np -import spharpy - - -def sid(n_max): - """Calculates the SID indices up to spherical harmonic order n_max. - TODO: Add citation Daniel - - Parameters - ---------- - n_max : int - - Returns - ------- - sid : numpy array - - """ - n_sh = (n_max+1)**2 - sid_n = sph_identity_matrix(n_max, 'n-nm').T @ np.arange(0, n_max+1) - sid_m = np.zeros(n_sh, dtype=int) - idx_n = 0 - for n in range(1, n_max+1): - for m in range(1, n+1): - sid_m[idx_n + 2*m-1] = n-m+1 - sid_m[idx_n + 2*m] = -(n-m+1) - sid_m[idx_n + 2*n + 1] = 0 - idx_n += 2*n+1 - - return sid_n, sid_m - - -def sid2acn(n_max): - """Convert from SID channel indexing as proposed by Daniel in - TODO: add citation - Returns the indices to achieve a corresponding linear acn indexing. - - Parameters - ---------- - sid : numpy array - - Returns - ------- - acn : numpy array - - """ - sid_n, sid_m = spharpy.indexing.sid(n_max) - linear_sid = spharpy.spherical.nm2acn(sid_n, sid_m) - return np.argsort(linear_sid) - - -def sph_identity_matrix(n_max, type='n-nm'): - """Calculate a spherical harmonic identity matrix. - TODO: Implement the other identity matrices - - Parameters - ---------- - n_max : TODO - type : TODO, optional - - Returns - ------- - identity_matrix : numpy array - - """ - n_sh = (n_max+1)**2 - - if type != 'n-nm': - raise NotImplementedError - - identity_matrix = np.zeros((n_max+1, n_sh), dtype=int) - # linear_n0 = np.cumsum(np.arange(0, 2*(n_max+1), 2)) - - for n in range(n_max+1): - m = np.arange(-n, n+1) - linear_nm = spharpy.spherical.nm2acn(np.tile(n, m.shape), m) - identity_matrix[n, linear_nm] = 1 - - return identity_matrix diff --git a/spharpy/interpolate.py b/spharpy/interpolate.py index 12c52303..9992df79 100644 --- a/spharpy/interpolate.py +++ b/spharpy/interpolate.py @@ -1,6 +1,7 @@ +"""Interpolation module for spherical data.""" + from scipy import interpolate as spinterpolate import numpy as np -from spharpy._deprecation import convert_coordinates class SmoothSphereBivariateSpline(spinterpolate.SmoothSphereBivariateSpline): @@ -9,14 +10,15 @@ class SmoothSphereBivariateSpline(spinterpolate.SmoothSphereBivariateSpline): Parameters ---------- - sampling : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + sampling : :py:class:`pyfar.Coordinates` Coordinates object containing the positions for which the data is sampled data : array, float Array containing the data at the sampling positions. Has to be real-valued. w : array, float - Weighting coefficients + Weighting coefficients. The default is ``None``, which sets the + weighting coefficients to ``1``. s : float, 1e-4 Smoothing factor > 0 Positive smoothing factor defined for estimation condition: @@ -25,9 +27,11 @@ class SmoothSphereBivariateSpline(spinterpolate.SmoothSphereBivariateSpline): an estimate of the standard deviation of ``r[i]``. The default value is ``1e-4`` eps : float, 1e-16 - The eps valued to be considered for interpolator estimation. + Sets the threshold used to determine the effective rank of the + underlying linear system of equations. Values smaller than this + threshold are treated as zero during the fitting process. Depends on the used data type and numerical precision. The default - is 1e-16. + is ``1e-16``. Note ---- @@ -58,9 +62,9 @@ class SmoothSphereBivariateSpline(spinterpolate.SmoothSphereBivariateSpline): >>> interp_data = interpolator(interp_grid) """ # noqa: 501 + def __init__(self, sampling, data, w=None, s=1e-4, eps=1e-16): - sampling = convert_coordinates(sampling) - theta = sampling.elevation + theta = sampling.colatitude phi = sampling.azimuth if np.any(np.iscomplex(data)): raise ValueError("Complex data is not supported.") @@ -71,26 +75,50 @@ def __call__(self, interp_grid, dtheta=0, dphi=0): Parameters ---------- - interp_grid : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + interp_grid : :py:class:`pyfar.Coordinates` Coordinates object containing a new set of points for which data is to be interpolated. dtheta : int, optional - Order of theta derivative + Order of theta derivative. Default is ``0``. dphi : int, optional - Order of phi derivative + Order of phi derivative. Default is ``0``. """ # noqa: 501 - interp_grid = convert_coordinates(interp_grid) - theta = interp_grid.elevation + theta = interp_grid.colatitude phi = interp_grid.azimuth return super().__call__( theta, phi, dtheta=dtheta, dphi=dphi, grid=False) def get_coeffs(self): + """Get the coefficients of the spline. + + Returns + ------- + coeffs : ndarray, float + The coefficients of the spline. The shape is + (n_coeffs_theta, n_coeffs_phi, n_coefficients). + The coefficients are ordered by increasing degree and order. + """ return super().get_coeffs() def get_knots(self): + """Get the knots of the spline. + + Returns + ------- + knots : tuple of ndarray, float + The knots of the spline. The first element is the knots in theta + direction and the second element is the knots in phi direction. + """ return super().get_knots() def get_residual(self): + """Get the residual of the spline. + + Returns + ------- + residual : float + The residual of the spline, which is the sum of squared errors + between the data and the spline evaluation. + """ return super().get_residual() diff --git a/spharpy/plot/__init__.py b/spharpy/plot/__init__.py index f3c09fde..87207a45 100644 --- a/spharpy/plot/__init__.py +++ b/spharpy/plot/__init__.py @@ -1,3 +1,5 @@ +"""Plotting functions for spherical data.""" + from .spatial import ( scatter, pcolor_map, diff --git a/spharpy/plot/cmap.py b/spharpy/plot/cmap.py index c5514823..b25d9283 100644 --- a/spharpy/plot/cmap.py +++ b/spharpy/plot/cmap.py @@ -1,12 +1,31 @@ +"""cmap for displaying phase information.""" import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap import numpy as np def phase_twilight(lut=512): - """Cyclic color map for displaying phase information. This is a modified - version of the twilight color map from matplotlib. + r""" + Cyclic color map for displaying phase information. + + This is a modified version of the twilight color map from matplotlib. + The colormap is rotated such that :math:`0` is encoded as red hues, while + :math:`\pi` is encoded as blue hues. + + Parameters + ---------- + lut : int, optional + Number of entries in the lookup table of colors for the colormap. + Default is ``512``. + + Returns + ------- + matplotlib.colors.ListedColormap + Colormap instance. """ + if not isinstance(lut, int) or lut <= 0: + raise ValueError('lut must be a positive integer.') + lut = int(np.ceil(lut/4)*4) twilight = plt.get_cmap('twilight', lut=lut) diff --git a/spharpy/plot/spatial.py b/spharpy/plot/spatial.py index 674d8a5a..b34e3197 100644 --- a/spharpy/plot/spatial.py +++ b/spharpy/plot/spatial.py @@ -1,13 +1,12 @@ """ -Plot functions for spatial data +Plot functions for spatial data. """ -from spharpy._deprecation import convert_coordinates - import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.tri as mtri import numpy as np import scipy.spatial as sspat +import pyfar as pf from matplotlib import colors from mpl_toolkits.mplot3d import Axes3D __all__ = [Axes3D] @@ -17,7 +16,8 @@ from .cmap import phase_twilight -from spharpy.samplings import sph2cart, spherical_voronoi +from spharpy.samplings import spherical_voronoi +from pyfar.classes.coordinates import sph2cart def set_aspect_equal_3d(ax): @@ -43,15 +43,38 @@ def set_aspect_equal_3d(ax): ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius]) -def scatter(coordinates, ax=None): +def scatter(coordinates, ax=None, **kwargs): """Plot the x, y, and z coordinates of the sampling grid in the 3d space. Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere The coordinates to be plotted + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object. + **kwargs : optional + Additional keyword arguments passed to the scatter function. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> coords = spharpy.samplings.gaussian(n_max=5) + >>> spharpy.plot.scatter(coords) + + + """ + if not isinstance(coordinates, pf.Coordinates): + raise ValueError("coordinates must be a coordinates object.") - """ # noqa: 501 fig = plt.gcf() if ax is None: ax = plt.gca() if fig.axes else plt.axes(projection='3d') @@ -59,8 +82,7 @@ def scatter(coordinates, ax=None): if '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") - coordinates = convert_coordinates(coordinates) - ax.scatter(coordinates.x, coordinates.y, coordinates.z) + ax.scatter(coordinates.x, coordinates.y, coordinates.z, **kwargs) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') @@ -70,32 +92,35 @@ def scatter(coordinates, ax=None): np.ptp(coordinates.y), np.ptp(coordinates.z)]) + return ax + def _triangulation_sphere(sampling, data): """Triangulation for data points sampled on a spherical surface. Parameters ---------- - sampling : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + sampling : pyfar.Coordinates, spharpy.SamplingSphere Coordinate object for which the triangulation is calculated - xyz : list of arrays + data : list of arrays x, y, and z values of the data points in the triangulation Returns ------- triangulation : matplotlib Triangulation - """ # noqa: 501 - sampling = convert_coordinates(sampling) + """ + x, y, z = sph2cart( + sampling.azimuth, + sampling.colatitude, np.abs(data), - sampling.elevation, - sampling.azimuth) + ) hull = sspat.ConvexHull( np.asarray(sph2cart( - np.ones(sampling.n_points), - sampling.elevation, - sampling.azimuth)).T) + sampling.azimuth, + sampling.colatitude, + np.ones(len(sampling.colatitude)))).T) tri = mtri.Triangulation(x, y, triangles=hull.simplices) return tri, [x, y, z] @@ -114,17 +139,17 @@ def interpolate_data_on_sphere( Parameters ---------- - sampling : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + sampling : pyfar.Coordinates, spharpy.SamplingSphere The coordinates at which the data is sampled. data : ndarray, double The sampled data points. overlap : float, (pi/4) The overlap for the periodic extension in azimuth angle, given in radians - refine : bool (False) - Refine the mesh before interpolating - interpolator : linear, cubic - The interpolation method to be used + refine : bool + Refine the mesh before interpolating. The default is ``False``. + interpolator : 'linear', 'cubic' + The interpolation method to be used. The default is 'linear'. Returns ------- @@ -136,10 +161,8 @@ def interpolate_data_on_sphere( Internally, matplotlibs LinearTriInterpolator or CubicTriInterpolator are used. - """ # noqa: 501 - sampling = convert_coordinates(sampling) - lats = sampling.latitude - lons = sampling.longitude + """ + _, lats, lons = coordinates2latlon(sampling) mask = lons > np.pi - overlap lons = np.concatenate((lons, lons[mask] - np.pi*2)) @@ -181,7 +204,8 @@ def _balloon_color_data(tri, data, itype): data : ndarray, double, complex double The data array itype : 'magnitude', 'phase', 'amplitude' - Whether to plot magnitude levels or the phase. + What type of data should be extracted. Either the 'magnitude', 'phase', + or 'amplitude' of the data array is used for the colormap. Returns ------- @@ -189,7 +213,6 @@ def _balloon_color_data(tri, data, itype): The data array for the colormap. vmin : double The minimum of the color data - vmax : double The maximum of the color data @@ -220,36 +243,65 @@ def pcolor_sphere( data, cmap=None, colorbar=True, - phase=False, + limits=None, + cmap_encoding='phase', ax=None, - *args, **kwargs): """Plot data on the surface of a sphere defined by the coordinate angles theta and phi. The data array will be mapped onto the surface of a sphere. - Note - ---- - When plotting the phase encoded in the colormap, the function will switch - to the HSV colormap and ignore the user input for the cmap input variable. - Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. - cmap : matplotlib colomap, optional - Colormap for the plot, see matplotlib.cm - phase : boolean, optional - Encode the phase of the data in the colormap. This option will be - activated by default of the data is complex valued. + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap used for the plot. If ``None`` (default), the colormap + is automatically selected based on the value of `cmap_encoding`: + ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and + ``'magnitude'`` uses the ``'viridis'`` colormap. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + cmap_encoding : str, optional + The information encoded in the colormap. Can be either ``'phase'`` + (in radians) or ``'magnitude'``. The default is ``'phase'``. ax : matplotlib.axis, None, optional - The matplotlib axis object used for plotting. By default `None`, which - will create a new axis object. + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object. + **kwargs : optional + Additional arguments passed to the plot_trisurf function. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + plot : matplotlib.trisurf + The trisurf object created by the function. + + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + >>> spharpy.plot.pcolor_sphere(coords, data, cmap_encoding='phase') + + """ + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + if cmap_encoding not in ['phase', 'magnitude']: + raise ValueError( + "cmap_encoding must be either 'phase' or 'magnitude'.") + - """ # noqa: 501 - coordinates = convert_coordinates(coordinates) tri, xyz = _triangulation_sphere(coordinates, np.ones_like(data)) fig = plt.gcf() @@ -259,25 +311,27 @@ def pcolor_sphere( elif '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") - if np.iscomplex(data).any() or phase: - itype = 'phase' + if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' - else: - itype = 'amplitude' + elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') - clabel = 'Amplitude' + clabel = 'Magnitude' + + cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) - cdata, vmin, vmax = _balloon_color_data(tri, data, itype) + if limits is not None: + vmin, vmax = limits plot = ax.plot_trisurf(tri, xyz[2], cmap=cmap, antialiased=True, vmin=vmin, - vmax=vmax) + vmax=vmax, + **kwargs) plot.set_array(cdata) @@ -293,40 +347,78 @@ def pcolor_sphere( np.ptp(coordinates.y), np.ptp(coordinates.z)]) - return plot + return (ax, plot) def balloon_wireframe( coordinates, data, cmap=None, - phase=False, colorbar=True, - ax=None): + limits=None, + cmap_encoding='phase', + ax=None, + **kwargs): """Plot data on a sphere defined by the coordinate angles theta and phi. The magnitude information is mapped onto the radius of the sphere. The colormap represents either the phase or the magnitude of the data array. - Note - ---- - When plotting the phase encoded in the colormap, the function will switch - to the HSV colormap and ignore the user input for the cmap input variable. - Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. - cmap : matplotlib colomap, optional - Colormap for the plot, see matplotlib.cm - phase : boolean, optional - Encode the phase of the data in the colormap. This option will be - activated by default of the data is complex valued. - """ # noqa: 501 - coordinates = convert_coordinates(coordinates) + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap used for the plot. If ``None`` (default), the colormap + is automatically selected based on the value of `cmap_encoding`: + ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and + ``'magnitude'`` uses the ``'viridis'`` colormap. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + cmap_encoding : str, optional + The information encoded in the colormap. Can be either ``'phase'`` + (in radians) or ``'magnitude'``. The default is ``'phase'``. + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object. + **kwargs : optional + Additional arguments passed to the plot_trisurf function. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + plot : matplotlib.trisurf + The trisurf object created by the function. + + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + >>> spharpy.plot.balloon_wireframe(coords, data, cmap_encoding='phase') + + """ + # input checks + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + if cmap_encoding not in ['phase', 'magnitude']: + raise ValueError( + "cmap_encoding must be either 'phase' or 'magnitude'.") + + if isinstance(cmap, str): + cmap = plt.get_cmap(cmap) + tri, xyz = _triangulation_sphere(coordinates, data) fig = plt.gcf() @@ -336,26 +428,29 @@ def balloon_wireframe( elif '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") - if np.iscomplex(data).any() or phase: - itype = 'phase' + if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' - else: - itype = 'amplitude' + elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') - clabel = 'Amplitude' + clabel = 'Magnitude' + + cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) - cdata, vmin, vmax = _balloon_color_data(tri, data, itype) + if limits is not None: + vmin, vmax = limits plot = ax.plot_trisurf(tri, xyz[2], antialiased=True, vmin=vmin, - vmax=vmax) + vmax=vmax, + **kwargs) cnorm = plt.Normalize(vmin, vmax) + cmap_colors = cmap(cnorm(cdata)) cmappable = mpl.cm.ScalarMappable(cnorm, cmap) @@ -380,43 +475,75 @@ def balloon_wireframe( plot.set_facecolor([0.9, 0.9, 0.9, 0.9]) - return plot + return (ax, plot) def balloon( coordinates, data, cmap=None, - phase=False, colorbar=True, + limits=None, + cmap_encoding='phase', ax=None, - *args, **kwargs): """Plot data on a sphere defined by the coordinate angles theta and phi. The magnitude information is mapped onto the radius of the sphere. The colormap represents either the phase or the magnitude of the data array. - - Note - ---- - When plotting the phase encoded in the colormap, the function will switch - to the HSV colormap and ignore the user input for the cmap input variable. - Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. - cmap : matplotlib colomap, optional - Colormap for the plot, see matplotlib.cm - phase : boolean, optional - Encode the phase of the data in the colormap. This option will be - activated by default of the data is complex valued. - """ # noqa: 501 - coordinates = convert_coordinates(coordinates) + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap used for the plot. If ``None`` (default), the colormap + is automatically selected based on the value of `cmap_encoding`: + ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and + ``'magnitude'`` uses the ``'viridis'`` colormap. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + cmap_encoding : str, optional + The information encoded in the colormap. Can be either ``'phase'`` + (in radians) or ``'magnitude'``. The default is ``'phase'``. + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which + will create a new axis object. + **kwargs : optional + Additional arguments passed to the plot_trisurf function. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + plot : matplotlib.trisurf + The trisurf object created by the function. + + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + >>> spharpy.plot.balloon(coords, data, cmap_encoding='phase') + + """ + # input checks + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + if cmap_encoding not in ['phase', 'magnitude']: + raise ValueError( + "cmap_encoding must be either 'phase' or 'magnitude'.") tri, xyz = _triangulation_sphere(coordinates, data) fig = plt.gcf() @@ -427,18 +554,19 @@ def balloon( elif '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") - if np.iscomplex(data).any() or phase: - itype = 'phase' + if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' - else: - itype = 'amplitude' + elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') - clabel = 'Amplitude' + clabel = cmap_encoding.title() + + cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) - cdata, vmin, vmax = _balloon_color_data(tri, data, itype) + if limits is not None: + vmin, vmax = limits plot = ax.plot_trisurf(tri, xyz[2], @@ -446,7 +574,6 @@ def balloon( antialiased=True, vmin=vmin, vmax=vmax, - *args, **kwargs) plot.set_array(cdata) @@ -463,7 +590,7 @@ def balloon( ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') - return plot + return (ax, plot) def voronoi_cells_sphere(sampling, round_decimals=13, ax=None): @@ -471,17 +598,33 @@ def voronoi_cells_sphere(sampling, round_decimals=13, ax=None): Parameters ---------- - sampling : :class:`spharpy.samplings.SamplingSphere`, :doc:`pf.Coordinates ` + sampling : pyfar.Coordinates, spharpy.SamplingSphere Sampling as SamplingSphere object round_decimals : int Decimals to be rounded to for eliminating duplicate points in - the voronoi diagram + the voronoi diagram. Default is ``13``. ax : AxesSubplot, None, optional The subplot axes to use for plotting. The used projection needs to be - '3d'. + ``'3d'``. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> coords = spharpy.samplings.gaussian(n_max=5) + >>> spharpy.plot.voronoi_cells_sphere(coords) + + """ + if not isinstance(sampling, pf.Coordinates): + raise ValueError("sampling must be a coordinates object.") - """ # noqa: 501 - sampling = convert_coordinates(sampling) sv = spherical_voronoi(sampling, round_decimals=round_decimals) sv.sort_vertices_of_regions() points = sampling.cartesian.T @@ -504,7 +647,7 @@ def voronoi_cells_sphere(sampling, round_decimals=13, ax=None): z = np.outer(np.ones(np.size(u)), np.cos(v)) ax.plot_surface(x, y, z, color='y', alpha=0.1) - ax.scatter(points[:, 0], points[:, 1], points[:, 2], c='r') + ax.scatter(points[0], points[1], points[2], c='r') for region in sv.regions: polygon = Poly3DCollection( @@ -523,8 +666,10 @@ def voronoi_cells_sphere(sampling, round_decimals=13, ax=None): ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') + return ax + -def _combined_contour(x, y, data, limits, cmap, ax): +def _combined_contour(x, y, data, limits, cmap, levels, ax): """Combine a filled contour plot with a black line contour plot for better highlighting. @@ -539,8 +684,15 @@ def _combined_contour(x, y, data, limits, cmap, ax): limits : tuple, list Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. - cmap : matplotlib.colormap - The colormap + cmap : :py:class:`matplotlib.colors.Colormap` + Colormap for the plot, see matplotlib.cm. + levels : int or array-like + Determines the number and positions of the contours. + If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, + which tries to automatically choose + no more than n+1 contour levels between minimum and maximum + numeric values of the plot data. If array-like, draw contour lines at + the specified levels. The values must be in increasing order. ax : matplotlib.axes The axes object into which the contour is plotted @@ -565,18 +717,20 @@ def _combined_contour(x, y, data, limits, cmap, ax): elif ~np.any(mask_max) & np.any(mask_min): extend = 'min' - ax.tricontour(x, y, data, linewidths=0.5, colors='k', + ax.tricontour(x, y, data, levels=levels, linewidths=0.5, colors='k', vmin=limits[0], vmax=limits[1], extend=extend) return ax.tricontourf( - x, y, data, cmap=cmap, vmin=limits[0], vmax=limits[1], extend=extend) + x, y, data, levels=levels, cmap=cmap, vmin=limits[0], vmax=limits[1], + extend=extend) def pcolor_map( coordinates, data, - projection='mollweide', + cmap='viridis', + colorbar=True, limits=None, - cmap=plt.get_cmap('viridis'), + projection='mollweide', refine=False, ax=None, **kwargs): @@ -591,15 +745,58 @@ def pcolor_map( Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere - data: ndarray, double + data : numpy.ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + projection : str, optional + The projection of the map. Default is ``'mollweide'``. See + :py:doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo` + for more information on available projections in matplotlib. + refine : bool, optional + Whether to refine the triangulation before plotting. + Default is ``False``. + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object with the specified projection. + **kwargs : optional + Additional arguments passed to the tripcolor function. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + cf : matplotlib.tri.TriContourSet + The contour plot object. - """ # noqa: 501 - coordinates = convert_coordinates(coordinates) - tri = mtri.Triangulation(coordinates.longitude, coordinates.latitude) + Examples + -------- + + .. plot:: + + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) + >>> spharpy.plot.pcolor_map(coords, data) + + """ + # input checks + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + if not isinstance(refine, bool): + raise ValueError("refine must be a boolean.") + + height, latitude, longitude = coordinates2latlon(coordinates) + tri = mtri.Triangulation(longitude, latitude) if refine is not None: subdiv = refine if isinstance(refine, int) else 2 refiner = mtri.UniformTriRefiner(tri) @@ -615,8 +812,8 @@ def pcolor_map( if ax.name != projection: raise ValueError( - "Projection does not match the projection of the axis", - f"Needs to be '{projection}', but is '{ax.name}'") + f"The projection of the axis needs to be '{projection}'" + f", but is '{ax.name}'") ax.set_xlabel('Longitude [$^\\circ$]') ax.set_ylabel('Latitude [$^\\circ$]') @@ -640,19 +837,20 @@ def pcolor_map( tri, data, cmap=cmap, vmin=limits[0], vmax=limits[1], **kwargs) plt.grid(True) - cb = fig.colorbar(cf, ax=ax, extend=extend) - cb.set_label('Amplitude') + if colorbar: + cb = fig.colorbar(cf, ax=ax, extend=extend) + cb.set_label('Amplitude') - return cf + return ax, cf def contour_map( coordinates, data, - projection='mollweide', - limits=None, - cmap=plt.get_cmap('viridis'), + cmap='viridis', colorbar=True, + limits=None, + projection='mollweide', levels=None, ax=None): """ @@ -666,68 +864,92 @@ def contour_map( Parameters ---------- - latitude: ndarray, double - Geodetic latitude angle of the map, must be in [-pi/2, pi/2] - longitude: ndarray, double - Geodetic longitude angle of the map, must be in [-pi, pi] - data: ndarray, double + coordinates : pyfar.Coordinates, spharpy.SamplingSphere + Coordinates defining a sphere + data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + projection : str, optional + The projection of the map. Default is ``'mollweide'``. See + :py:doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo` + for more information on available projections in matplotlib. + levels : int or array-like, optional + Determines the number and positions of the contours. + If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, + which tries to automatically choose + no more than n+1 contour levels between minimum and maximum + numeric values of the plot data. If array-like, draw contour lines at + the specified levels. The values must be in increasing order. + Default is ``None``, the levels are chosen automatically by + Matplotlib. + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object with the specified projection. - """ - coordinates = convert_coordinates(coordinates) - fig = plt.gcf() + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + cf : matplotlib.contour.QuadContourSet + The contour plot object. - res = int(np.ceil(np.sqrt(coordinates.n_points))) + Examples + -------- - xi, yi = np.meshgrid( - np.linspace(-np.pi, np.pi, res*2), - np.linspace(-np.pi/2, np.pi/2, res)) + .. plot:: - interp = interpolate_data_on_sphere(coordinates, data) - zi = interp(xi, yi) + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) + >>> spharpy.plot.contour_map(coords, data) + """ + # input checks + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + data = data.copy() + + fig = plt.gcf() if ax is None: ax = plt.gca() if fig.axes else plt.axes(projection=projection) if ax.name != projection: raise ValueError( - "Projection does not match the projection of the axis", - f"Needs to be '{projection}', but is '{ax.name}'") + f"The projection of the axis needs to be '{projection}'" + f", but is '{ax.name}'") ax.set_xlabel('Longitude [$^\\circ$]') ax.set_ylabel('Latitude [$^\\circ$]') - extend = 'neither' - if limits is None: - limits = (zi.min(), zi.max()) - else: - mask_min = zi < limits[0] - zi[mask_min] = limits[0] - mask_max = zi > limits[1] - zi[mask_max] = limits[1] - if np.any(mask_max) and np.any(mask_min): - extend = 'both' - elif np.any(mask_max) and not np.any(mask_min): - extend = 'max' - elif not np.any(mask_max) and np.any(mask_min): - extend = 'min' + _, latitude, longitude = coordinates2latlon(coordinates) + cf = _combined_contour(longitude, latitude, data, limits, cmap, levels, ax) - ax.contour(xi, yi, zi, levels=levels, linewidths=0.5, colors='k', - vmin=limits[0], vmax=limits[1], extend=extend) - cf = ax.pcolormesh(xi, yi, zi, cmap=cmap, shading='gouraud', - vmin=limits[0], vmax=limits[1]) + if type(levels) is int: + levels = mpl.ticker.MaxNLocator(levels) plt.grid(True) if colorbar: cb = fig.colorbar(cf, ax=ax, ticks=levels) cb.set_label('Amplitude') - return cf + return ax, cf def contour( - coordinates, data, limits=None, cmap=plt.get_cmap('viridis'), + coordinates, + data, + cmap='viridis', + colorbar=True, + limits=None, + levels=None, ax=None): """ Plot the map projection of data points sampled on a spherical surface. @@ -740,43 +962,182 @@ def contour( Parameters ---------- - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data: ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. + cmap : str, :py:class:`matplotlib.colors.Colormap`, optional + Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. + colorbar : bool, optional + Whether to show a colorbar or not. Default is ``True``. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If ``None``, the limits are set to the minimum and + maximum of the data. Default is ``None``. + levels : int or array-like, optional + Determines the number and positions of the contours. + If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, + which tries to automatically choose + no more than n+1 contour levels between minimum and maximum + numeric values of the plot data. If array-like, draw contour lines at + the specified levels. The values must be in increasing order. + Default is ``None``, the levels are chosen automatically by + Matplotlib. + ax : matplotlib.axis, None, optional + The matplotlib axis object used for plotting. By default ``None``, + which will create a new axis object with the specified projection. + + Returns + ------- + ax : matplotlib.axis + The axis object used for plotting. + cf : matplotlib.contour.QuadContourSet + The contour plot object. + + Examples + -------- - """ # noqa: 501 - coordinates = convert_coordinates(coordinates) - lat_deg = coordinates.latitude * 180/np.pi - lon_deg = coordinates.longitude * 180/np.pi + .. plot:: + + >>> import spharpy + >>> import numpy as np + >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) + >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) + >>> spharpy.plot.contour(coords, data) + + """ + # input checks + _check_input_parameters(coordinates, data, cmap, colorbar, limits) + data = data.copy() + + _, latitude, longitude = coordinates2latlon(coordinates) + lat_deg = latitude * 180/np.pi + lon_deg = longitude * 180/np.pi fig = plt.gcf() if ax is None: ax = plt.gca() + + if ax.name != 'rectilinear': + raise ValueError( + f"The projection of the axis needs to be 'rectilinear'" + f", but is '{ax.name}'") + ax.set_xlabel('Longitude [$^\\circ$]') ax.set_ylabel('Latitude [$^\\circ$]') - cf = _combined_contour(lon_deg, lat_deg, data, limits, cmap, ax) + cf = _combined_contour(lon_deg, lat_deg, data, limits, cmap, levels, ax) + + if type(levels) is int: + levels = mpl.ticker.MaxNLocator(levels) plt.grid(True) - cb = fig.colorbar(cf, ax=ax) - cb.set_label('Amplitude') + if colorbar: + cb = fig.colorbar(cf, ax=ax, ticks=levels) + cb.set_label('Amplitude') - return cf + return ax, cf class MidpointNormalize(colors.Normalize): """Colormap norm with a defined midpoint. Useful for normalization of colormaps representing deviations from a defined midpoint. Taken from the official matplotlib documentation at - https://matplotlib.org/users/colormapnorms.html + https://matplotlib.org/users/colormapnorms.html. """ + def __init__(self, vmin=None, vmax=None, midpoint=0., clip=False): self.midpoint = midpoint colors.Normalize.__init__(self, vmin, vmax, clip) - def __call__(self, value, clip=None): + def __call__(self, value, clip=None): # noqa: ARG002 # I'm ignoring masked values and all kinds of edge cases to make a # simple example... x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1] return np.ma.masked_array(np.interp(value, x, y)) + + +def coordinates2latlon(coords: pf.Coordinates): + r"""Transforms from Cartesian coordinates to Geocentric coordinates. + + .. math:: + + h = \sqrt{x^2 + y^2 + z^2}, + + \theta = \pi/2 - \arccos(\frac{z}{r}), + + \phi = \arctan(\frac{y}{x}) + + -\pi/2 < \theta < \pi/2, + + -\pi < \phi < \pi + + where :math:`h` is the heigth, :math:`\theta` is the latitude angle + and :math:`\phi` is the longitude angle + + Parameters + ---------- + coords : pyfar.Coordinates, spharpy.SamplingSphere + Cartesian Coordiantes are transformed to Geocentric coordinates + + Returns + ------- + height : ndarray, number + The radius is rendered as height information + latitude : ndarray, number + Geocentric latitude angle + longitude : ndarray, number + Geocentric longitude angle + + """ + x = coords.x + y = coords.y + z = coords.z + height = np.sqrt(x**2 + y**2 + z**2) + latitude = np.pi/2 - np.arccos(z/height) + longitude = np.arctan2(y, x) + return height, latitude, longitude + + +def _check_input_parameters(coordinates, data, cmap, colorbar, limits): + """Check the input parameters for the plotting functions. + + The function raises ValueError if the input parameters are not valid. + + Parameters + ---------- + coordinates : pyfar.Coordinates, spharpy.SamplingSphere + Coordinates defining a sphere + data : ndarray, double + Data for each angle, must have size corresponding to the number of + points given in coordinates. + cmap : str, :py:class:`matplotlib.colors.Colormap` + Colormap for the plot, see matplotlib.cm. + colorbar : bool, optional + Whether to show a colorbar or not. Default is `True`. + limits : tuple, list, optional + Tuple or list containing the maximum and minimum to which the colormap + needs to be clipped. If `None`, the limits are set to the minimum and + maximum of the data. + """ + if not isinstance(colorbar, bool): + raise ValueError("colorbar must be a boolean.") + if not isinstance(cmap, (str, type(None), mpl.colors.Colormap)): + raise ValueError( + "cmap must be a string, Colormap object, or None.") + if not isinstance(coordinates, pf.Coordinates): + raise ValueError("coordinates must be a coordinates object.") + if not isinstance(data, np.ndarray): + raise ValueError( + "data must be a 1D array with the same cshape as the coordinates.") + if data.shape[-1] != coordinates.cshape[-1]: + raise ValueError( + "data must be a 1D array with the same cshape as the coordinates.") + if limits is not None and not isinstance(limits, (tuple, list)): + raise ValueError( + "limits must be a tuple or list containing the minimum and " + "maximum values for the colormap or None.") + if limits is not None and len(limits) != 2: + raise ValueError( + "limits must be a tuple or list containing the minimum and " + "maximum values for the colormap or None.") diff --git a/spharpy/samplings/__init__.py b/spharpy/samplings/__init__.py index a0c5eebb..4dd5c0e8 100644 --- a/spharpy/samplings/__init__.py +++ b/spharpy/samplings/__init__.py @@ -1,52 +1,50 @@ +"""Spherical samplings.""" + from .samplings import ( - cube_equidistant, + equidistant_cuboid, hyperinterpolation, - spherical_t_design, + t_design, dodecahedron, icosahedron, icosahedron_ke4, equiangular, gaussian, eigenmike_em32, - equalarea, + equal_area, spiral_points, + equal_angle, + great_circle, + lebedev, + fliege, eigenmike_em64, ) from .helpers import ( - sph2cart, - cart2sph, - cart2latlon, - latlon2cart, spherical_voronoi, - calculate_sampling_weights + calculate_sampling_weights, ) -from .coordinates import Coordinates, SamplingSphere - from .interior import interior_stabilization_points __all__ = [ - 'cube_equidistant', + 'equidistant_cuboid', 'hyperinterpolation', - 'spherical_t_design', + 't_design', 'dodecahedron', 'icosahedron', 'icosahedron_ke4', 'equiangular', 'gaussian', 'eigenmike_em32', - 'equalarea', + 'equal_area', 'spiral_points', - 'sph2cart', - 'cart2sph', - 'cart2latlon', - 'latlon2cart', 'spherical_voronoi', 'calculate_sampling_weights', - 'Coordinates', - 'SamplingSphere', 'interior_stabilization_points', + 'equal_angle', + 'great_circle', + 'lebedev', + 'fliege', 'eigenmike_em64', ] diff --git a/spharpy/samplings/_eqsp/__init__.py b/spharpy/samplings/_eqsp/__init__.py index e9048b41..aa400510 100644 --- a/spharpy/samplings/_eqsp/__init__.py +++ b/spharpy/samplings/_eqsp/__init__.py @@ -1,3 +1,6 @@ from .partitions import point_set +from .samplings_lebedev import _lebedevSphere as lebedev_sphere -__all__ = [point_set] +__all__ = [ + point_set, + lebedev_sphere] diff --git a/spharpy/samplings/_eqsp/partitions.py b/spharpy/samplings/_eqsp/partitions.py index 4330356c..d086846e 100644 --- a/spharpy/samplings/_eqsp/partitions.py +++ b/spharpy/samplings/_eqsp/partitions.py @@ -86,12 +86,10 @@ def point_set_polar(dimension, N): if dimension == 1: points_s = a_cap - np.pi/N else: - # import ipdb; ipdb.set_trace() n_collars = np.size(n_regions) - 2 points_s = np.zeros((dimension, N)) point_n = 2 - # points = np.zeros((dimension, N)) offset = 0 @@ -105,21 +103,11 @@ def point_set_polar(dimension, N): # n_in_collar is the number of regions in the current collar. n_in_collar = n_regions[collar_n+1] - # if use_cache: - # twin_collar_n = n_collars - collar_n - # - # # if twin_collar_n <= cache_size && ... - # # size(cache{twin_collar_n},2) == n_in_collar - # # points_1 = cache{twin_collar_n}; - # # else - # points_l = point_set_polar(dimension - 1, n_in_collar) - # else: points_l = point_set_polar(dimension - 1, n_in_collar) a_point = (a_top + a_bot)/2 point_l_n = np.arange(0, np.size(points_l), dtype=int) - # points_l = points_l[np.newaxis] if dimension == 2: points_s[0:dimension-1, point_n+point_l_n-1] = \ @@ -131,10 +119,7 @@ def point_set_polar(dimension, N): points_s[0:dimension-2, point_n+point_l_n-1] = \ points_l[:, point_l_n] - # import ipdb; ipdb.set_trace() - points_s[dimension-1, point_n+point_l_n-1] = a_point - # point_n = point_n + points_l.shape[1] point_n += np.size(points_l) points_s[:, -1] = np.zeros(dimension) @@ -144,7 +129,7 @@ def point_set_polar(dimension, N): def caps(dimension, N): - """Partition a sphere into to nested spherical caps + """Partition a sphere into two nested spherical caps. Does the following: 1) partitions the unit sphere S^dim into a list of spherical caps of @@ -157,7 +142,6 @@ def caps(dimension, N): Examples -------- - % > [s_cap,n_regions] = eq_caps(2,10) % s_cap = % 0.6435 1.5708 2.4981 3.1416 @@ -181,6 +165,12 @@ def caps(dimension, N): Returns ------- + s_cap : array, double + The colatitudes of the spherical caps. The dimensions of the array are + (1, N_collars+2). + n_regions : array, int + The number of regions in each collar and the polar caps. + The dimensions of the array are (1, N_collars+2). """ if dimension == 1: @@ -193,7 +183,7 @@ def caps(dimension, N): c_polar = polar_colat(dimension, N) n_collars = num_collars(N, c_polar, ideal_collar_angle(dimension, N)) r_regions = ideal_region_list(dimension, N, c_polar, n_collars) - n_regions = round_to_naturals(N, r_regions) + n_regions = round_to_naturals(r_regions) s_cap = cap_colats(dimension, N, c_polar, n_regions) return s_cap, n_regions @@ -216,8 +206,6 @@ def polar_colat(dimension, N): colatitude : double The colatitude angle of the top cap. """ - # enough = N > 2 - # c_polar = np.empty() if N == 1: c_polar = np.pi elif N == 2: @@ -230,7 +218,7 @@ def polar_colat(dimension, N): def ideal_region_list(dimension, N, c_polar, n_collars): - """The ideal real number of regions in each zone + """The ideal real number of regions in each zone. List the ideal real number of regions in each collar, plus the polar caps. Given dim, N, c_polar and n_collars, determine r_regions, a list of the @@ -246,9 +234,15 @@ def ideal_region_list(dimension, N, c_polar, n_collars): The dimension N : int The number of points - c_polar : + c_polar : float + The colatitude angle of the polar caps in radians. This defines the + angular boundary between the north/south polar caps and the collar + region. The north polar cap extends from 0 to c_polar, the collar + region spans from c_polar to (π - c_polar), and the south polar cap + extends from (π - c_polar) to π. Must be in the range [0, π/2]. n_collars : int The number of collar elements + Returns ------- ideal_regions : double @@ -273,7 +267,7 @@ def ideal_region_list(dimension, N, c_polar, n_collars): return r_regions -def round_to_naturals(N, r_regions): +def round_to_naturals(r_regions): """Round off a given list of numbers of regions Given N and r_regions, determine n_regions, a list of the natural number of regions in each collar and the polar caps. @@ -285,10 +279,9 @@ def round_to_naturals(N, r_regions): Parameters ---------- - N : int - The dimension r_regions : double The ideal number of regions per collar before rounding + Returns ------- n_regions : int @@ -325,6 +318,7 @@ def cap_colats(dimension, N, c_polar, n_regions): Colatitude angles of the spherical caps n_regions: int Number of regions + Returns ------- c_caps : double @@ -358,6 +352,7 @@ def num_collars(N, c_polar, a_ideal): The colatitude angle of the polar caps a_ideal : double The ideal collar angles. + Returns ------- n_collars : int @@ -396,6 +391,7 @@ def circle_offset(n_top, n_bot, extra_twist=False): Number of points in the lower circle extra_twist : boolean Perform an additional rotation (see part 3) + Returns ------- offset : int @@ -461,6 +457,7 @@ def area_of_sphere(dimension): Parameters ---------- dimension : int + Dimension of the sphere. Returns ------- @@ -572,7 +569,7 @@ def sradius_of_cap(dimension, area): def polar2cart(points_polar): - """Comnversion from the polar angles theta and phi to Cartesian coordinates + """Conversion from the polar angles theta and phi to Cartesian coordinates. x = cos(phi) * sin(theta) y = sin(phi) * sin(theta) @@ -583,7 +580,8 @@ def polar2cart(points_polar): points_polar : array, double The points in polar coordinates, with shape (2, N) - Returns: + Returns + ------- points_cart : array, double The points in Cartesian coordinates with shape (3, N) diff --git a/spharpy/samplings/_eqsp/samplings_fliege.mat b/spharpy/samplings/_eqsp/samplings_fliege.mat new file mode 100644 index 00000000..2f27e616 Binary files /dev/null and b/spharpy/samplings/_eqsp/samplings_fliege.mat differ diff --git a/spharpy/samplings/_eqsp/samplings_lebedev.py b/spharpy/samplings/_eqsp/samplings_lebedev.py new file mode 100644 index 00000000..b51599ca --- /dev/null +++ b/spharpy/samplings/_eqsp/samplings_lebedev.py @@ -0,0 +1,5328 @@ +""" +Generate Lebedev spherical sampling grids. + +These are helper functions. For generating Lebedev Grids see +pyfar.spatial.samplings. + +Copyright (c) 2010, Robert Parrish +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +""" + +import numpy as np + + +def _lebedevSphere(degree): + """ + Generate Lebedev sampling grids. + + These are helper functions. For generating Lebedev Grids see + pyfar.spatial.samplings. + + @author Rob Parrish, The Sherrill Group, CCMST Georgia Tech + @email robparrish@gmail.com + @date 03/24/2010 + + Ported to Python by the pyfar developers + + @description - function to compute normalized points and weights + for Lebedev quadratures on the surface of the unit sphere at double + precision. + ******* Relative error is generally expected to be ~2.0E-14 [1] ***** + Lebedev quadratures are superbly accurate and efficient quadrature rules + for approximating integrals of the form v = 4pi f(Omega) + ud Omega, where Omega is the solid angle on the surface of the + unit sphere. Lebedev quadratures integrate all spherical harmonics up + to l = order, where degree approx. order(order+1)/3. These grids may + be easily combined with radial quadratures to provide robust cubature + formulae. For example, see 'A. Becke, 1988c, J. Chem. Phys., 88(4), pp. + 2547' (The first paper on tractable molecular Density Functional Theory + methods, of which Lebedev grids and numerical cubature are an intrinsic + part). + + @param degree - positive integer specifying number of points in the + requested quadrature. Allowed values are (degree -> order): + degree: { 6, 14, 26, 38, 50, 74, 86, 110, 146, 170, 194, 230, 266, 302, + 350, 434, 590, 770, 974, 1202, 1454, 1730, 2030, 2354, 2702, 3074, + 3470, 3890, 4334, 4802, 5294, 5810 } + + @return leb_tmp - struct containing fields: + x - x values of quadrature, constrained to unit sphere + y - y values of quadrature, constrained to unit sphere + z - z values of quadrature, constrained to unit sphere + w - quadrature weights, normalized to 4pi. + + @example: S x^2+y^2-z^2 ud Omega = 4.188790204786399 + f = @(x,y,z) x.^2+y.^2-z.^2 + leb = lebedevSphere(590) + v = f(leb.x,leb.y,leb.z) + int = sum(v.*leb.w) + + @citation - Translated from a Fortran code kindly provided by Christoph + van Wuellen (Ruhr-Universitaet, Bochum, Germany), which came from the + original C routines coded by Dmitri Laikov (Moscow State University, + Moscow, Russia). The MATLAB implementation of this code is designed for + benchmarking of new DFT integration techniques to be implemented in the + open source Psi4 ab initio quantum chemistry program. + + As per Professor Wuellen's request, any papers published using this code + or its derivatives are requested to include the following citation: + + .. [1] V.I. Lebedev, and D.N. Laikov + "A quadrature formula for the sphere of the 131st + algebraic order of accuracy" + Doklady Mathematics, Vol. 59, No. 3, 1999, pp. 477-481. + """ + + leb_tmp = {'x': np.zeros(degree), + 'y': np.zeros(degree), + 'z': np.zeros(degree), + 'w': np.zeros(degree)} + + start = 0 + a = 0. + b = 0. + v = 0. + + if degree == 6: + v = 0.1666666666666667E+0 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + + elif degree == 14: + v = 0.6666666666666667E-1 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.7500000000000000E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + + elif degree == 26: + v = 0.4761904761904762E-1 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3809523809523810E-1 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.3214285714285714E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + + elif degree == 38: + v = 0.9523809523809524E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3214285714285714E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.4597008433809831E+0 + v = 0.2857142857142857E-1 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + + elif degree == 50: + v = 0.1269841269841270E-1 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2257495590828924E-1 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.2109375000000000E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.3015113445777636E+0 + v = 0.2017333553791887E-1 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + + elif degree == 74: + v = 0.5130671797338464E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1660406956574204E-1 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = -0.2958603896103896E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.4803844614152614E+0 + v = 0.2657620708215946E-1 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3207726489807764E+0 + v = 0.1652217099371571E-1 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + + elif degree == 86: + v = 0.1154401154401154E-1 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1194390908585628E-1 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.3696028464541502E+0 + v = 0.1111055571060340E-1 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6943540066026664E+0 + v = 0.1187650129453714E-1 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3742430390903412E+0 + v = 0.1181230374690448E-1 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + + elif degree == 110: + v = 0.3828270494937162E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.9793737512487512E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1851156353447362E+0 + v = 0.8211737283191111E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6904210483822922E+0 + v = 0.9942814891178103E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3956894730559419E+0 + v = 0.9595471336070963E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4783690288121502E+0 + v = 0.9694996361663028E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + + elif degree == 146: + v = 0.5996313688621381E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.7372999718620756E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.7210515360144488E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.6764410400114264E+0 + v = 0.7116355493117555E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4174961227965453E+0 + v = 0.6753829486314477E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1574676672039082E+0 + v = 0.7574394159054034E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1403553811713183E+0 + b = 0.4493328323269557E+0 + v = 0.6991087353303262E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 170: + v = 0.5544842902037365E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.6071332770670752E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.6383674773515093E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2551252621114134E+0 + v = 0.5183387587747790E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6743601460362766E+0 + v = 0.6317929009813725E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4318910696719410E+0 + v = 0.6201670006589077E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2613931360335988E+0 + v = 0.5477143385137348E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4990453161796037E+0 + b = 0.1446630744325115E+0 + v = 0.5968383987681156E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 194: + v = 0.1782340447244611E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.5716905949977102E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.5573383178848738E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.6712973442695226E+0 + v = 0.5608704082587997E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2892465627575439E+0 + v = 0.5158237711805383E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4446933178717437E+0 + v = 0.5518771467273614E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1299335447650067E+0 + v = 0.4106777028169394E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3457702197611283E+0 + v = 0.5051846064614808E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1590417105383530E+0 + b = 0.8360360154824589E+0 + v = 0.5530248916233094E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 230: + v = -0.5522639919727325E-1 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.4450274607445226E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.4492044687397611E+0 + v = 0.4496841067921404E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2520419490210201E+0 + v = 0.5049153450478750E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6981906658447242E+0 + v = 0.3976408018051883E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6587405243460960E+0 + v = 0.4401400650381014E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4038544050097660E-1 + v = 0.1724544350544401E-1 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5823842309715585E+0 + v = 0.4231083095357343E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3545877390518688E+0 + v = 0.5198069864064399E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2272181808998187E+0 + b = 0.4864661535886647E+0 + v = 0.4695720972568883E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 266: + v = -0.1313769127326952E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = -0.2522728704859336E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.4186853881700583E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.7039373391585475E+0 + v = 0.5315167977810885E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1012526248572414E+0 + v = 0.4047142377086219E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4647448726420539E+0 + v = 0.4112482394406990E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3277420654971629E+0 + v = 0.3595584899758782E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6620338663699974E+0 + v = 0.4256131351428158E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8506508083520399E+0 + v = 0.4229582700647240E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3233484542692899E+0 + b = 0.1153112011009701E+0 + v = 0.4080914225780505E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2314790158712601E+0 + b = 0.5244939240922365E+0 + v = 0.4071467593830964E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 302: + v = 0.8545911725128148E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3599119285025571E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.3515640345570105E+0 + v = 0.3449788424305883E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6566329410219612E+0 + v = 0.3604822601419882E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4729054132581005E+0 + v = 0.3576729661743367E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9618308522614784E-1 + v = 0.2352101413689164E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2219645236294178E+0 + v = 0.3108953122413675E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7011766416089545E+0 + v = 0.3650045807677255E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2644152887060663E+0 + v = 0.2982344963171804E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5718955891878961E+0 + v = 0.3600820932216460E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2510034751770465E+0 + b = 0.8000727494073952E+0 + v = 0.3571540554273387E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1233548532583327E+0 + b = 0.4127724083168531E+0 + v = 0.3392312205006170E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 350: + v = 0.3006796749453936E-2 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3050627745650771E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.7068965463912316E+0 + v = 0.1621104600288991E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4794682625712025E+0 + v = 0.3005701484901752E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1927533154878019E+0 + v = 0.2990992529653774E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6930357961327123E+0 + v = 0.2982170644107595E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3608302115520091E+0 + v = 0.2721564237310992E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6498486161496169E+0 + v = 0.3033513795811141E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1932945013230339E+0 + v = 0.3007949555218533E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3800494919899303E+0 + v = 0.2881964603055307E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2899558825499574E+0 + b = 0.7934537856582316E+0 + v = 0.2958357626535696E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9684121455103957E-1 + b = 0.8280801506686862E+0 + v = 0.3036020026407088E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1833434647041659E+0 + b = 0.9074658265305127E+0 + v = 0.2832187403926303E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 434: + v = 0.5265897968224436E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2548219972002607E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.2512317418927307E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.6909346307509111E+0 + v = 0.2530403801186355E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1774836054609158E+0 + v = 0.2014279020918528E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4914342637784746E+0 + v = 0.2501725168402936E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6456664707424256E+0 + v = 0.2513267174597564E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2861289010307638E+0 + v = 0.2302694782227416E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7568084367178018E-1 + v = 0.1462495621594614E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3927259763368002E+0 + v = 0.2445373437312980E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8818132877794288E+0 + v = 0.2417442375638981E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9776428111182649E+0 + v = 0.1910951282179532E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2054823696403044E+0 + b = 0.8689460322872412E+0 + v = 0.2416930044324775E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5905157048925271E+0 + b = 0.7999278543857286E+0 + v = 0.2512236854563495E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5550152361076807E+0 + b = 0.7717462626915901E+0 + v = 0.2496644054553086E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9371809858553722E+0 + b = 0.3344363145343455E+0 + v = 0.2236607760437849E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 590: + v = 0.3095121295306187E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1852379698597489E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.7040954938227469E+0 + v = 0.1871790639277744E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6807744066455243E+0 + v = 0.1858812585438317E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6372546939258752E+0 + v = 0.1852028828296213E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5044419707800358E+0 + v = 0.1846715956151242E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4215761784010967E+0 + v = 0.1818471778162769E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3317920736472123E+0 + v = 0.1749564657281154E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2384736701421887E+0 + v = 0.1617210647254411E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1459036449157763E+0 + v = 0.1384737234851692E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6095034115507196E-1 + v = 0.9764331165051050E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6116843442009876E+0 + v = 0.1857161196774078E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3964755348199858E+0 + v = 0.1705153996395864E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1724782009907724E+0 + v = 0.1300321685886048E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5610263808622060E+0 + b = 0.3518280927733519E+0 + v = 0.1842866472905286E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4742392842551980E+0 + b = 0.2634716655937950E+0 + v = 0.1802658934377451E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5984126497885380E+0 + b = 0.1816640840360209E+0 + v = 0.1849830560443660E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3791035407695563E+0 + b = 0.1720795225656878E+0 + v = 0.1713904507106709E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2778673190586244E+0 + b = 0.8213021581932511E-1 + v = 0.1555213603396808E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5033564271075117E+0 + b = 0.8999205842074875E-1 + v = 0.1802239128008525E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 770: + v = 0.2192942088181184E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1436433617319080E-2 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.1421940344335877E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.5087204410502360E-1 + v = 0.6798123511050502E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1228198790178831E+0 + v = 0.9913184235294912E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2026890814408786E+0 + v = 0.1180207833238949E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2847745156464294E+0 + v = 0.1296599602080921E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3656719078978026E+0 + v = 0.1365871427428316E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4428264886713469E+0 + v = 0.1402988604775325E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5140619627249735E+0 + v = 0.1418645563595609E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6306401219166803E+0 + v = 0.1421376741851662E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6716883332022612E+0 + v = 0.1423996475490962E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6979792685336881E+0 + v = 0.1431554042178567E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1446865674195309E+0 + v = 0.9254401499865368E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3390263475411216E+0 + v = 0.1250239995053509E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5335804651263506E+0 + v = 0.1394365843329230E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6944024393349413E-1 + b = 0.2355187894242326E+0 + v = 0.1127089094671749E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2269004109529460E+0 + b = 0.4102182474045730E+0 + v = 0.1345753760910670E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8025574607775339E-1 + b = 0.6214302417481605E+0 + v = 0.1424957283316783E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1467999527896572E+0 + b = 0.3245284345717394E+0 + v = 0.1261523341237750E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1571507769824727E+0 + b = 0.5224482189696630E+0 + v = 0.1392547106052696E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2365702993157246E+0 + b = 0.6017546634089558E+0 + v = 0.1418761677877656E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7714815866765732E-1 + b = 0.4346575516141163E+0 + v = 0.1338366684479554E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3062936666210730E+0 + b = 0.4908826589037616E+0 + v = 0.1393700862676131E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3822477379524787E+0 + b = 0.5648768149099500E+0 + v = 0.1415914757466932E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 974: + v = 0.1438294190527431E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1125772288287004E-2 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.4292963545341347E-1 + v = 0.4948029341949241E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1051426854086404E+0 + v = 0.7357990109125470E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1750024867623087E+0 + v = 0.8889132771304384E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2477653379650257E+0 + v = 0.9888347838921435E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3206567123955957E+0 + v = 0.1053299681709471E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3916520749849983E+0 + v = 0.1092778807014578E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4590825874187624E+0 + v = 0.1114389394063227E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5214563888415861E+0 + v = 0.1123724788051555E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6253170244654199E+0 + v = 0.1125239325243814E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6637926744523170E+0 + v = 0.1126153271815905E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6910410398498301E+0 + v = 0.1130286931123841E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7052907007457760E+0 + v = 0.1134986534363955E-2 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1236686762657990E+0 + v = 0.6823367927109931E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2940777114468387E+0 + v = 0.9454158160447096E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4697753849207649E+0 + v = 0.1074429975385679E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6334563241139567E+0 + v = 0.1129300086569132E-2 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5974048614181342E-1 + b = 0.2029128752777523E+0 + v = 0.8436884500901954E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1375760408473636E+0 + b = 0.4602621942484054E+0 + v = 0.1075255720448885E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3391016526336286E+0 + b = 0.5030673999662036E+0 + v = 0.1108577236864462E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1271675191439820E+0 + b = 0.2817606422442134E+0 + v = 0.9566475323783357E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2693120740413512E+0 + b = 0.4331561291720157E+0 + v = 0.1080663250717391E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1419786452601918E+0 + b = 0.6256167358580814E+0 + v = 0.1126797131196295E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6709284600738255E-1 + b = 0.3798395216859157E+0 + v = 0.1022568715358061E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7057738183256172E-1 + b = 0.5517505421423520E+0 + v = 0.1108960267713108E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2783888477882155E+0 + b = 0.6029619156159187E+0 + v = 0.1122790653435766E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1979578938917407E+0 + b = 0.3589606329589096E+0 + v = 0.1032401847117460E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2087307061103274E+0 + b = 0.5348666438135476E+0 + v = 0.1107249382283854E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4055122137872836E+0 + b = 0.5674997546074373E+0 + v = 0.1121780048519972E-2 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 1202: + v = 0.1105189233267572E-3 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.9205232738090741E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.9133159786443561E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.3712636449657089E-1 + v = 0.3690421898017899E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9140060412262223E-1 + v = 0.5603990928680660E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1531077852469906E+0 + v = 0.6865297629282609E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2180928891660612E+0 + v = 0.7720338551145630E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2839874532200175E+0 + v = 0.8301545958894795E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3491177600963764E+0 + v = 0.8686692550179628E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4121431461444309E+0 + v = 0.8927076285846890E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4718993627149127E+0 + v = 0.9060820238568219E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5273145452842337E+0 + v = 0.9119777254940867E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6209475332444019E+0 + v = 0.9128720138604181E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6569722711857291E+0 + v = 0.9130714935691735E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6841788309070143E+0 + v = 0.9152873784554116E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7012604330123631E+0 + v = 0.9187436274321654E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1072382215478166E+0 + v = 0.5176977312965694E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2582068959496968E+0 + v = 0.7331143682101417E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4172752955306717E+0 + v = 0.8463232836379928E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5700366911792503E+0 + v = 0.9031122694253992E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9827986018263947E+0 + b = 0.1771774022615325E+0 + v = 0.6485778453163257E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9624249230326228E+0 + b = 0.2475716463426288E+0 + v = 0.7435030910982369E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9402007994128811E+0 + b = 0.3354616289066489E+0 + v = 0.7998527891839054E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9320822040143202E+0 + b = 0.3173615246611977E+0 + v = 0.8101731497468018E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9043674199393299E+0 + b = 0.4090268427085357E+0 + v = 0.8483389574594331E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8912407560074747E+0 + b = 0.3854291150669224E+0 + v = 0.8556299257311812E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8676435628462708E+0 + b = 0.4932221184851285E+0 + v = 0.8803208679738260E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8581979986041619E+0 + b = 0.4785320675922435E+0 + v = 0.8811048182425720E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8396753624049856E+0 + b = 0.4507422593157064E+0 + v = 0.8850282341265444E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8165288564022188E+0 + b = 0.5632123020762100E+0 + v = 0.9021342299040653E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8015469370783529E+0 + b = 0.5434303569693900E+0 + v = 0.9010091677105086E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7773563069070351E+0 + b = 0.5123518486419871E+0 + v = 0.9022692938426915E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7661621213900394E+0 + b = 0.6394279634749102E+0 + v = 0.9158016174693465E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7553584143533510E+0 + b = 0.6269805509024392E+0 + v = 0.9131578003189435E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7344305757559503E+0 + b = 0.6031161693096310E+0 + v = 0.9107813579482705E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.7043837184021765E+0 + b = 0.5693702498468441E+0 + v = 0.9105760258970126E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 1454: + v = 0.7777160743261247E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.7557646413004701E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.3229290663413854E-1 + v = 0.2841633806090617E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8036733271462222E-1 + v = 0.4374419127053555E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1354289960531653E+0 + v = 0.5417174740872172E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1938963861114426E+0 + v = 0.6148000891358593E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2537343715011275E+0 + v = 0.6664394485800705E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3135251434752570E+0 + v = 0.7025039356923220E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3721558339375338E+0 + v = 0.7268511789249627E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4286809575195696E+0 + v = 0.7422637534208629E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4822510128282994E+0 + v = 0.7509545035841214E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5320679333566263E+0 + v = 0.7548535057718401E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6172998195394274E+0 + v = 0.7554088969774001E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6510679849127481E+0 + v = 0.7553147174442808E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6777315251687360E+0 + v = 0.7564767653292297E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6963109410648741E+0 + v = 0.7587991808518730E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7058935009831749E+0 + v = 0.7608261832033027E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9955546194091857E+0 + v = 0.4021680447874916E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9734115901794209E+0 + v = 0.5804871793945964E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9275693732388626E+0 + v = 0.6792151955945159E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.8568022422795103E+0 + v = 0.7336741211286294E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.7623495553719372E+0 + v = 0.7581866300989608E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5707522908892223E+0 + b = 0.4387028039889501E+0 + v = 0.7538257859800743E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5196463388403083E+0 + b = 0.3858908414762617E+0 + v = 0.7483517247053123E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4646337531215351E+0 + b = 0.3301937372343854E+0 + v = 0.7371763661112059E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4063901697557691E+0 + b = 0.2725423573563777E+0 + v = 0.7183448895756934E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3456329466643087E+0 + b = 0.2139510237495250E+0 + v = 0.6895815529822191E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2831395121050332E+0 + b = 0.1555922309786647E+0 + v = 0.6480105801792886E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2197682022925330E+0 + b = 0.9892878979686097E-1 + v = 0.5897558896594636E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1564696098650355E+0 + b = 0.4598642910675510E-1 + v = 0.5095708849247346E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6027356673721295E+0 + b = 0.3376625140173426E+0 + v = 0.7536906428909755E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5496032320255096E+0 + b = 0.2822301309727988E+0 + v = 0.7472505965575118E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4921707755234567E+0 + b = 0.2248632342592540E+0 + v = 0.7343017132279698E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4309422998598483E+0 + b = 0.1666224723456479E+0 + v = 0.7130871582177445E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3664108182313672E+0 + b = 0.1086964901822169E+0 + v = 0.6817022032112776E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2990189057758436E+0 + b = 0.5251989784120085E-1 + v = 0.6380941145604121E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6268724013144998E+0 + b = 0.2297523657550023E+0 + v = 0.7550381377920310E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5707324144834607E+0 + b = 0.1723080607093800E+0 + v = 0.7478646640144802E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5096360901960365E+0 + b = 0.1140238465390513E+0 + v = 0.7335918720601220E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4438729938312456E+0 + b = 0.5611522095882537E-1 + v = 0.7110120527658118E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6419978471082389E+0 + b = 0.1164174423140873E+0 + v = 0.7571363978689501E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5817218061802611E+0 + b = 0.5797589531445219E-1 + v = 0.7489908329079234E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 1730: + v = 0.6309049437420976E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.6398287705571748E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.6357185073530720E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2860923126194662E-1 + v = 0.2221207162188168E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7142556767711522E-1 + v = 0.3475784022286848E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1209199540995559E+0 + v = 0.4350742443589804E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1738673106594379E+0 + v = 0.4978569136522127E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2284645438467734E+0 + v = 0.5435036221998053E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2834807671701512E+0 + v = 0.5765913388219542E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3379680145467339E+0 + v = 0.6001200359226003E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3911355454819537E+0 + v = 0.6162178172717512E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4422860353001403E+0 + v = 0.6265218152438485E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4907781568726057E+0 + v = 0.6323987160974212E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5360006153211468E+0 + v = 0.6350767851540569E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6142105973596603E+0 + v = 0.6354362775297107E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6459300387977504E+0 + v = 0.6352302462706235E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6718056125089225E+0 + v = 0.6358117881417972E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6910888533186254E+0 + v = 0.6373101590310117E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7030467416823252E+0 + v = 0.6390428961368665E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8354951166354646E-1 + v = 0.3186913449946576E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2050143009099486E+0 + v = 0.4678028558591711E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3370208290706637E+0 + v = 0.5538829697598626E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4689051484233963E+0 + v = 0.6044475907190476E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5939400424557334E+0 + v = 0.6313575103509012E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1394983311832261E+0 + b = 0.4097581162050343E-1 + v = 0.4078626431855630E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1967999180485014E+0 + b = 0.8851987391293348E-1 + v = 0.4759933057812725E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2546183732548967E+0 + b = 0.1397680182969819E+0 + v = 0.5268151186413440E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3121281074713875E+0 + b = 0.1929452542226526E+0 + v = 0.5643048560507316E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3685981078502492E+0 + b = 0.2467898337061562E+0 + v = 0.5914501076613073E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4233760321547856E+0 + b = 0.3003104124785409E+0 + v = 0.6104561257874195E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4758671236059246E+0 + b = 0.3526684328175033E+0 + v = 0.6230252860707806E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5255178579796463E+0 + b = 0.4031134861145713E+0 + v = 0.6305618761760796E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5718025633734589E+0 + b = 0.4509426448342351E+0 + v = 0.6343092767597889E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2686927772723415E+0 + b = 0.4711322502423248E-1 + v = 0.5176268945737826E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3306006819904809E+0 + b = 0.9784487303942695E-1 + v = 0.5564840313313692E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3904906850594983E+0 + b = 0.1505395810025273E+0 + v = 0.5856426671038980E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4479957951904390E+0 + b = 0.2039728156296050E+0 + v = 0.6066386925777091E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5027076848919780E+0 + b = 0.2571529941121107E+0 + v = 0.6208824962234458E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5542087392260217E+0 + b = 0.3092191375815670E+0 + v = 0.6296314297822907E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6020850887375187E+0 + b = 0.3593807506130276E+0 + v = 0.6340423756791859E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4019851409179594E+0 + b = 0.5063389934378671E-1 + v = 0.5829627677107342E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4635614567449800E+0 + b = 0.1032422269160612E+0 + v = 0.6048693376081110E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5215860931591575E+0 + b = 0.1566322094006254E+0 + v = 0.6202362317732461E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5758202499099271E+0 + b = 0.2098082827491099E+0 + v = 0.6299005328403779E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6259893683876795E+0 + b = 0.2618824114553391E+0 + v = 0.6347722390609353E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5313795124811891E+0 + b = 0.5263245019338556E-1 + v = 0.6203778981238834E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5893317955931995E+0 + b = 0.1061059730982005E+0 + v = 0.6308414671239979E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6426246321215801E+0 + b = 0.1594171564034221E+0 + v = 0.6362706466959498E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6511904367376113E+0 + b = 0.5354789536565540E-1 + v = 0.6375414170333233E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 2030: + v = 0.4656031899197431E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.5421549195295507E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2540835336814348E-1 + v = 0.1778522133346553E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6399322800504915E-1 + v = 0.2811325405682796E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1088269469804125E+0 + v = 0.3548896312631459E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1570670798818287E+0 + v = 0.4090310897173364E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2071163932282514E+0 + v = 0.4493286134169965E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2578914044450844E+0 + v = 0.4793728447962723E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3085687558169623E+0 + v = 0.5015415319164265E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3584719706267024E+0 + v = 0.5175127372677937E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4070135594428709E+0 + v = 0.5285522262081019E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4536618626222638E+0 + v = 0.5356832703713962E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4979195686463577E+0 + v = 0.5397914736175170E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5393075111126999E+0 + v = 0.5416899441599930E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6115617676843916E+0 + v = 0.5419308476889938E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6414308435160159E+0 + v = 0.5416936902030596E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6664099412721607E+0 + v = 0.5419544338703164E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6859161771214913E+0 + v = 0.5428983656630975E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6993625593503890E+0 + v = 0.5442286500098193E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7062393387719380E+0 + v = 0.5452250345057301E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7479028168349763E-1 + v = 0.2568002497728530E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1848951153969366E+0 + v = 0.3827211700292145E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3059529066581305E+0 + v = 0.4579491561917824E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4285556101021362E+0 + v = 0.5042003969083574E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5468758653496526E+0 + v = 0.5312708889976025E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6565821978343439E+0 + v = 0.5438401790747117E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1253901572367117E+0 + b = 0.3681917226439641E-1 + v = 0.3316041873197344E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1775721510383941E+0 + b = 0.7982487607213301E-1 + v = 0.3899113567153771E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2305693358216114E+0 + b = 0.1264640966592335E+0 + v = 0.4343343327201309E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2836502845992063E+0 + b = 0.1751585683418957E+0 + v = 0.4679415262318919E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3361794746232590E+0 + b = 0.2247995907632670E+0 + v = 0.4930847981631031E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3875979172264824E+0 + b = 0.2745299257422246E+0 + v = 0.5115031867540091E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4374019316999074E+0 + b = 0.3236373482441118E+0 + v = 0.5245217148457367E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4851275843340022E+0 + b = 0.3714967859436741E+0 + v = 0.5332041499895321E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5303391803806868E+0 + b = 0.4175353646321745E+0 + v = 0.5384583126021542E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5726197380596287E+0 + b = 0.4612084406355461E+0 + v = 0.5411067210798852E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2431520732564863E+0 + b = 0.4258040133043952E-1 + v = 0.4259797391468714E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3002096800895869E+0 + b = 0.8869424306722721E-1 + v = 0.4604931368460021E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3558554457457432E+0 + b = 0.1368811706510655E+0 + v = 0.4871814878255202E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4097782537048887E+0 + b = 0.1860739985015033E+0 + v = 0.5072242910074885E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4616337666067458E+0 + b = 0.2354235077395853E+0 + v = 0.5217069845235350E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5110707008417874E+0 + b = 0.2842074921347011E+0 + v = 0.5315785966280310E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5577415286163795E+0 + b = 0.3317784414984102E+0 + v = 0.5376833708758905E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6013060431366950E+0 + b = 0.3775299002040700E+0 + v = 0.5408032092069521E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3661596767261781E+0 + b = 0.4599367887164592E-1 + v = 0.4842744917904866E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4237633153506581E+0 + b = 0.9404893773654421E-1 + v = 0.5048926076188130E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4786328454658452E+0 + b = 0.1431377109091971E+0 + v = 0.5202607980478373E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5305702076789774E+0 + b = 0.1924186388843570E+0 + v = 0.5309932388325743E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5793436224231788E+0 + b = 0.2411590944775190E+0 + v = 0.5377419770895208E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6247069017094747E+0 + b = 0.2886871491583605E+0 + v = 0.5411696331677717E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4874315552535204E+0 + b = 0.4804978774953206E-1 + v = 0.5197996293282420E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5427337322059053E+0 + b = 0.9716857199366665E-1 + v = 0.5311120836622945E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5943493747246700E+0 + b = 0.1465205839795055E+0 + v = 0.5384309319956951E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6421314033564943E+0 + b = 0.1953579449803574E+0 + v = 0.5421859504051886E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6020628374713980E+0 + b = 0.4916375015738108E-1 + v = 0.5390948355046314E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6529222529856881E+0 + b = 0.9861621540127005E-1 + v = 0.5433312705027845E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 2354: + v = 0.3922616270665292E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.4703831750854424E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.4678202801282136E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2290024646530589E-1 + v = 0.1437832228979900E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5779086652271284E-1 + v = 0.2303572493577644E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9863103576375984E-1 + v = 0.2933110752447454E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1428155792982185E+0 + v = 0.3402905998359838E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1888978116601463E+0 + v = 0.3759138466870372E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2359091682970210E+0 + v = 0.4030638447899798E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2831228833706171E+0 + v = 0.4236591432242211E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3299495857966693E+0 + v = 0.4390522656946746E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3758840802660796E+0 + v = 0.4502523466626247E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4204751831009480E+0 + v = 0.4580577727783541E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4633068518751051E+0 + v = 0.4631391616615899E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5039849474507313E+0 + v = 0.4660928953698676E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5421265793440747E+0 + v = 0.4674751807936953E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6092660230557310E+0 + v = 0.4676414903932920E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6374654204984869E+0 + v = 0.4674086492347870E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6615136472609892E+0 + v = 0.4674928539483207E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6809487285958127E+0 + v = 0.4680748979686447E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6952980021665196E+0 + v = 0.4690449806389040E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7041245497695400E+0 + v = 0.4699877075860818E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6744033088306065E-1 + v = 0.2099942281069176E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1678684485334166E+0 + v = 0.3172269150712804E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2793559049539613E+0 + v = 0.3832051358546523E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3935264218057639E+0 + v = 0.4252193818146985E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5052629268232558E+0 + v = 0.4513807963755000E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6107905315437531E+0 + v = 0.4657797469114178E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1135081039843524E+0 + b = 0.3331954884662588E-1 + v = 0.2733362800522836E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1612866626099378E+0 + b = 0.7247167465436538E-1 + v = 0.3235485368463559E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2100786550168205E+0 + b = 0.1151539110849745E+0 + v = 0.3624908726013453E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2592282009459942E+0 + b = 0.1599491097143677E+0 + v = 0.3925540070712828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3081740561320203E+0 + b = 0.2058699956028027E+0 + v = 0.4156129781116235E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3564289781578164E+0 + b = 0.2521624953502911E+0 + v = 0.4330644984623263E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4035587288240703E+0 + b = 0.2982090785797674E+0 + v = 0.4459677725921312E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4491671196373903E+0 + b = 0.3434762087235733E+0 + v = 0.4551593004456795E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4928854782917489E+0 + b = 0.3874831357203437E+0 + v = 0.4613341462749918E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5343646791958988E+0 + b = 0.4297814821746926E+0 + v = 0.4651019618269806E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5732683216530990E+0 + b = 0.4699402260943537E+0 + v = 0.4670249536100625E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2214131583218986E+0 + b = 0.3873602040643895E-1 + v = 0.3549555576441708E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2741796504750071E+0 + b = 0.8089496256902013E-1 + v = 0.3856108245249010E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3259797439149485E+0 + b = 0.1251732177620872E+0 + v = 0.4098622845756882E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3765441148826891E+0 + b = 0.1706260286403185E+0 + v = 0.4286328604268950E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4255773574530558E+0 + b = 0.2165115147300408E+0 + v = 0.4427802198993945E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4727795117058430E+0 + b = 0.2622089812225259E+0 + v = 0.4530473511488561E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5178546895819012E+0 + b = 0.3071721431296201E+0 + v = 0.4600805475703138E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5605141192097460E+0 + b = 0.3508998998801138E+0 + v = 0.4644599059958017E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6004763319352512E+0 + b = 0.3929160876166931E+0 + v = 0.4667274455712508E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3352842634946949E+0 + b = 0.4202563457288019E-1 + v = 0.4069360518020356E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3891971629814670E+0 + b = 0.8614309758870850E-1 + v = 0.4260442819919195E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4409875565542281E+0 + b = 0.1314500879380001E+0 + v = 0.4408678508029063E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4904893058592484E+0 + b = 0.1772189657383859E+0 + v = 0.4518748115548597E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5375056138769549E+0 + b = 0.2228277110050294E+0 + v = 0.4595564875375116E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5818255708669969E+0 + b = 0.2677179935014386E+0 + v = 0.4643988774315846E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6232334858144959E+0 + b = 0.3113675035544165E+0 + v = 0.4668827491646946E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4489485354492058E+0 + b = 0.4409162378368174E-1 + v = 0.4400541823741973E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5015136875933150E+0 + b = 0.8939009917748489E-1 + v = 0.4514512890193797E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5511300550512623E+0 + b = 0.1351806029383365E+0 + v = 0.4596198627347549E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5976720409858000E+0 + b = 0.1808370355053196E+0 + v = 0.4648659016801781E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6409956378989354E+0 + b = 0.2257852192301602E+0 + v = 0.4675502017157673E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5581222330827514E+0 + b = 0.4532173421637160E-1 + v = 0.4598494476455523E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6074705984161695E+0 + b = 0.9117488031840314E-1 + v = 0.4654916955152048E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6532272537379033E+0 + b = 0.1369294213140155E+0 + v = 0.4684709779505137E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6594761494500487E+0 + b = 0.4589901487275583E-1 + v = 0.4691445539106986E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 2702: + v = 0.2998675149888161E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.4077860529495355E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2065562538818703E-1 + v = 0.1185349192520667E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5250918173022379E-1 + v = 0.1913408643425751E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8993480082038376E-1 + v = 0.2452886577209897E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1306023924436019E+0 + v = 0.2862408183288702E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1732060388531418E+0 + v = 0.3178032258257357E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2168727084820249E+0 + v = 0.3422945667633690E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2609528309173586E+0 + v = 0.3612790520235922E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3049252927938952E+0 + v = 0.3758638229818521E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3483484138084404E+0 + v = 0.3868711798859953E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3908321549106406E+0 + v = 0.3949429933189938E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4320210071894814E+0 + v = 0.4006068107541156E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4715824795890053E+0 + v = 0.4043192149672723E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5091984794078453E+0 + v = 0.4064947495808078E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5445580145650803E+0 + v = 0.4075245619813152E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6072575796841768E+0 + v = 0.4076423540893566E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6339484505755803E+0 + v = 0.4074280862251555E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6570718257486958E+0 + v = 0.4074163756012244E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6762557330090709E+0 + v = 0.4077647795071246E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6911161696923790E+0 + v = 0.4084517552782530E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7012841911659961E+0 + v = 0.4092468459224052E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7064559272410020E+0 + v = 0.4097872687240906E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6123554989894765E-1 + v = 0.1738986811745028E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1533070348312393E+0 + v = 0.2659616045280191E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2563902605244206E+0 + v = 0.3240596008171533E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3629346991663361E+0 + v = 0.3621195964432943E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4683949968987538E+0 + v = 0.3868838330760539E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5694479240657952E+0 + v = 0.4018911532693111E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6634465430993955E+0 + v = 0.4089929432983252E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1033958573552305E+0 + b = 0.3034544009063584E-1 + v = 0.2279907527706409E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1473521412414395E+0 + b = 0.6618803044247135E-1 + v = 0.2715205490578897E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1924552158705967E+0 + b = 0.1054431128987715E+0 + v = 0.3057917896703976E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2381094362890328E+0 + b = 0.1468263551238858E+0 + v = 0.3326913052452555E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2838121707936760E+0 + b = 0.1894486108187886E+0 + v = 0.3537334711890037E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3291323133373415E+0 + b = 0.2326374238761579E+0 + v = 0.3700567500783129E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3736896978741460E+0 + b = 0.2758485808485768E+0 + v = 0.3825245372589122E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4171406040760013E+0 + b = 0.3186179331996921E+0 + v = 0.3918125171518296E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4591677985256915E+0 + b = 0.3605329796303794E+0 + v = 0.3984720419937579E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4994733831718418E+0 + b = 0.4012147253586509E+0 + v = 0.4029746003338211E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5377731830445096E+0 + b = 0.4403050025570692E+0 + v = 0.4057428632156627E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5737917830001331E+0 + b = 0.4774565904277483E+0 + v = 0.4071719274114857E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2027323586271389E+0 + b = 0.3544122504976147E-1 + v = 0.2990236950664119E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2516942375187273E+0 + b = 0.7418304388646328E-1 + v = 0.3262951734212878E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3000227995257181E+0 + b = 0.1150502745727186E+0 + v = 0.3482634608242413E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3474806691046342E+0 + b = 0.1571963371209364E+0 + v = 0.3656596681700892E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3938103180359209E+0 + b = 0.1999631877247100E+0 + v = 0.3791740467794218E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4387519590455703E+0 + b = 0.2428073457846535E+0 + v = 0.3894034450156905E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4820503960077787E+0 + b = 0.2852575132906155E+0 + v = 0.3968600245508371E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5234573778475101E+0 + b = 0.3268884208674639E+0 + v = 0.4019931351420050E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5627318647235282E+0 + b = 0.3673033321675939E+0 + v = 0.4052108801278599E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5996390607156954E+0 + b = 0.4061211551830290E+0 + v = 0.4068978613940934E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3084780753791947E+0 + b = 0.3860125523100059E-1 + v = 0.3454275351319704E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3589988275920223E+0 + b = 0.7928938987104867E-1 + v = 0.3629963537007920E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4078628415881973E+0 + b = 0.1212614643030087E+0 + v = 0.3770187233889873E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4549287258889735E+0 + b = 0.1638770827382693E+0 + v = 0.3878608613694378E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5000278512957279E+0 + b = 0.2065965798260176E+0 + v = 0.3959065270221274E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5429785044928199E+0 + b = 0.2489436378852235E+0 + v = 0.4015286975463570E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5835939850491711E+0 + b = 0.2904811368946891E+0 + v = 0.4050866785614717E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6216870353444856E+0 + b = 0.3307941957666609E+0 + v = 0.4069320185051913E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4151104662709091E+0 + b = 0.4064829146052554E-1 + v = 0.3760120964062763E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4649804275009218E+0 + b = 0.8258424547294755E-1 + v = 0.3870969564418064E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5124695757009662E+0 + b = 0.1251841962027289E+0 + v = 0.3955287790534055E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5574711100606224E+0 + b = 0.1679107505976331E+0 + v = 0.4015361911302668E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5998597333287227E+0 + b = 0.2102805057358715E+0 + v = 0.4053836986719548E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6395007148516600E+0 + b = 0.2518418087774107E+0 + v = 0.4073578673299117E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5188456224746252E+0 + b = 0.4194321676077518E-1 + v = 0.3954628379231406E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5664190707942778E+0 + b = 0.8457661551921499E-1 + v = 0.4017645508847530E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6110464353283153E+0 + b = 0.1273652932519396E+0 + v = 0.4059030348651293E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6526430302051563E+0 + b = 0.1698173239076354E+0 + v = 0.4080565809484880E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6167551880377548E+0 + b = 0.4266398851548864E-1 + v = 0.4063018753664651E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6607195418355383E+0 + b = 0.8551925814238349E-1 + v = 0.4087191292799671E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 3074: + v = 0.2599095953754734E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3603134089687541E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.3586067974412447E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1886108518723392E-1 + v = 0.9831528474385880E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4800217244625303E-1 + v = 0.1605023107954450E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8244922058397242E-1 + v = 0.2072200131464099E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1200408362484023E+0 + v = 0.2431297618814187E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1595773530809965E+0 + v = 0.2711819064496707E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2002635973434064E+0 + v = 0.2932762038321116E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2415127590139982E+0 + v = 0.3107032514197368E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2828584158458477E+0 + v = 0.3243808058921213E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3239091015338138E+0 + v = 0.3349899091374030E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3643225097962194E+0 + v = 0.3430580688505218E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4037897083691802E+0 + v = 0.3490124109290343E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4420247515194127E+0 + v = 0.3532148948561955E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4787572538464938E+0 + v = 0.3559862669062833E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5137265251275234E+0 + v = 0.3576224317551411E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5466764056654611E+0 + v = 0.3584050533086076E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6054859420813535E+0 + v = 0.3584903581373224E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6308106701764562E+0 + v = 0.3582991879040586E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6530369230179584E+0 + v = 0.3582371187963125E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6718609524611158E+0 + v = 0.3584353631122350E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6869676499894013E+0 + v = 0.3589120166517785E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6980467077240748E+0 + v = 0.3595445704531601E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7048241721250522E+0 + v = 0.3600943557111074E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5591105222058232E-1 + v = 0.1456447096742039E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1407384078513916E+0 + v = 0.2252370188283782E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2364035438976309E+0 + v = 0.2766135443474897E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3360602737818170E+0 + v = 0.3110729491500851E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4356292630054665E+0 + v = 0.3342506712303391E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5321569415256174E+0 + v = 0.3491981834026860E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6232956305040554E+0 + v = 0.3576003604348932E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9469870086838469E-1 + b = 0.2778748387309470E-1 + v = 0.1921921305788564E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1353170300568141E+0 + b = 0.6076569878628364E-1 + v = 0.2301458216495632E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1771679481726077E+0 + b = 0.9703072762711040E-1 + v = 0.2604248549522893E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2197066664231751E+0 + b = 0.1354112458524762E+0 + v = 0.2845275425870697E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2624783557374927E+0 + b = 0.1750996479744100E+0 + v = 0.3036870897974840E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3050969521214442E+0 + b = 0.2154896907449802E+0 + v = 0.3188414832298066E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3472252637196021E+0 + b = 0.2560954625740152E+0 + v = 0.3307046414722089E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3885610219026360E+0 + b = 0.2965070050624096E+0 + v = 0.3398330969031360E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4288273776062765E+0 + b = 0.3363641488734497E+0 + v = 0.3466757899705373E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4677662471302948E+0 + b = 0.3753400029836788E+0 + v = 0.3516095923230054E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5051333589553359E+0 + b = 0.4131297522144286E+0 + v = 0.3549645184048486E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5406942145810492E+0 + b = 0.4494423776081795E+0 + v = 0.3570415969441392E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5742204122576457E+0 + b = 0.4839938958841502E+0 + v = 0.3581251798496118E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1865407027225188E+0 + b = 0.3259144851070796E-1 + v = 0.2543491329913348E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2321186453689432E+0 + b = 0.6835679505297343E-1 + v = 0.2786711051330776E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2773159142523882E+0 + b = 0.1062284864451989E+0 + v = 0.2985552361083679E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3219200192237254E+0 + b = 0.1454404409323047E+0 + v = 0.3145867929154039E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3657032593944029E+0 + b = 0.1854018282582510E+0 + v = 0.3273290662067609E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4084376778363622E+0 + b = 0.2256297412014750E+0 + v = 0.3372705511943501E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4499004945751427E+0 + b = 0.2657104425000896E+0 + v = 0.3448274437851510E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4898758141326335E+0 + b = 0.3052755487631557E+0 + v = 0.3503592783048583E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5281547442266309E+0 + b = 0.3439863920645423E+0 + v = 0.3541854792663162E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5645346989813992E+0 + b = 0.3815229456121914E+0 + v = 0.3565995517909428E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5988181252159848E+0 + b = 0.4175752420966734E+0 + v = 0.3578802078302898E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2850425424471603E+0 + b = 0.3562149509862536E-1 + v = 0.2958644592860982E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3324619433027876E+0 + b = 0.7330318886871096E-1 + v = 0.3119548129116835E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3785848333076282E+0 + b = 0.1123226296008472E+0 + v = 0.3250745225005984E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4232891028562115E+0 + b = 0.1521084193337708E+0 + v = 0.3355153415935208E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4664287050829722E+0 + b = 0.1921844459223610E+0 + v = 0.3435847568549328E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5078458493735726E+0 + b = 0.2321360989678303E+0 + v = 0.3495786831622488E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5473779816204180E+0 + b = 0.2715886486360520E+0 + v = 0.3537767805534621E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5848617133811376E+0 + b = 0.3101924707571355E+0 + v = 0.3564459815421428E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6201348281584888E+0 + b = 0.3476121052890973E+0 + v = 0.3578464061225468E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3852191185387871E+0 + b = 0.3763224880035108E-1 + v = 0.3239748762836212E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4325025061073423E+0 + b = 0.7659581935637135E-1 + v = 0.3345491784174287E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4778486229734490E+0 + b = 0.1163381306083900E+0 + v = 0.3429126177301782E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5211663693009000E+0 + b = 0.1563890598752899E+0 + v = 0.3492420343097421E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5623469504853703E+0 + b = 0.1963320810149200E+0 + v = 0.3537399050235257E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6012718188659246E+0 + b = 0.2357847407258738E+0 + v = 0.3566209152659172E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6378179206390117E+0 + b = 0.2743846121244060E+0 + v = 0.3581084321919782E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4836936460214534E+0 + b = 0.3895902610739024E-1 + v = 0.3426522117591512E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5293792562683797E+0 + b = 0.7871246819312640E-1 + v = 0.3491848770121379E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5726281253100033E+0 + b = 0.1187963808202981E+0 + v = 0.3539318235231476E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6133658776169068E+0 + b = 0.1587914708061787E+0 + v = 0.3570231438458694E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6515085491865307E+0 + b = 0.1983058575227646E+0 + v = 0.3586207335051714E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5778692716064976E+0 + b = 0.3977209689791542E-1 + v = 0.3541196205164025E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6207904288086192E+0 + b = 0.7990157592981152E-1 + v = 0.3574296911573953E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6608688171046802E+0 + b = 0.1199671308754309E+0 + v = 0.3591993279818963E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6656263089489130E+0 + b = 0.4015955957805969E-1 + v = 0.3595855034661997E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 3470: + v = 0.2040382730826330E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.3178149703889544E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1721420832906233E-1 + v = 0.8288115128076110E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4408875374981770E-1 + v = 0.1360883192522954E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7594680813878681E-1 + v = 0.1766854454542662E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1108335359204799E+0 + v = 0.2083153161230153E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1476517054388567E+0 + v = 0.2333279544657158E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1856731870860615E+0 + v = 0.2532809539930247E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2243634099428821E+0 + v = 0.2692472184211158E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2633006881662727E+0 + v = 0.2819949946811885E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3021340904916283E+0 + v = 0.2920953593973030E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3405594048030089E+0 + v = 0.2999889782948352E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3783044434007372E+0 + v = 0.3060292120496902E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4151194767407910E+0 + v = 0.3105109167522192E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4507705766443257E+0 + v = 0.3136902387550312E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4850346056573187E+0 + v = 0.3157984652454632E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5176950817792470E+0 + v = 0.3170516518425422E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5485384240820989E+0 + v = 0.3176568425633755E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6039117238943308E+0 + v = 0.3177198411207062E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6279956655573113E+0 + v = 0.3175519492394733E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6493636169568952E+0 + v = 0.3174654952634756E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6677644117704504E+0 + v = 0.3175676415467654E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6829368572115624E+0 + v = 0.3178923417835410E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6946195818184121E+0 + v = 0.3183788287531909E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7025711542057026E+0 + v = 0.3188755151918807E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7066004767140119E+0 + v = 0.3191916889313849E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5132537689946062E-1 + v = 0.1231779611744508E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1297994661331225E+0 + v = 0.1924661373839880E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2188852049401307E+0 + v = 0.2380881867403424E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3123174824903457E+0 + v = 0.2693100663037885E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4064037620738195E+0 + v = 0.2908673382834366E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4984958396944782E+0 + v = 0.3053914619381535E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5864975046021365E+0 + v = 0.3143916684147777E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6686711634580175E+0 + v = 0.3187042244055363E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.8715738780835950E-1 + b = 0.2557175233367578E-1 + v = 0.1635219535869790E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1248383123134007E+0 + b = 0.5604823383376681E-1 + v = 0.1968109917696070E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1638062693383378E+0 + b = 0.8968568601900765E-1 + v = 0.2236754342249974E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2035586203373176E+0 + b = 0.1254086651976279E+0 + v = 0.2453186687017181E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2436798975293774E+0 + b = 0.1624780150162012E+0 + v = 0.2627551791580541E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2838207507773806E+0 + b = 0.2003422342683208E+0 + v = 0.2767654860152220E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3236787502217692E+0 + b = 0.2385628026255263E+0 + v = 0.2879467027765895E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3629849554840691E+0 + b = 0.2767731148783578E+0 + v = 0.2967639918918702E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4014948081992087E+0 + b = 0.3146542308245309E+0 + v = 0.3035900684660351E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4389818379260225E+0 + b = 0.3519196415895088E+0 + v = 0.3087338237298308E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4752331143674377E+0 + b = 0.3883050984023654E+0 + v = 0.3124608838860167E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5100457318374018E+0 + b = 0.4235613423908649E+0 + v = 0.3150084294226743E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5432238388954868E+0 + b = 0.4574484717196220E+0 + v = 0.3165958398598402E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5745758685072442E+0 + b = 0.4897311639255524E+0 + v = 0.3174320440957372E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1723981437592809E+0 + b = 0.3010630597881105E-1 + v = 0.2182188909812599E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2149553257844597E+0 + b = 0.6326031554204694E-1 + v = 0.2399727933921445E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2573256081247422E+0 + b = 0.9848566980258631E-1 + v = 0.2579796133514652E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2993163751238106E+0 + b = 0.1350835952384266E+0 + v = 0.2727114052623535E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3407238005148000E+0 + b = 0.1725184055442181E+0 + v = 0.2846327656281355E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3813454978483264E+0 + b = 0.2103559279730725E+0 + v = 0.2941491102051334E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4209848104423343E+0 + b = 0.2482278774554860E+0 + v = 0.3016049492136107E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4594519699996300E+0 + b = 0.2858099509982883E+0 + v = 0.3072949726175648E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4965640166185930E+0 + b = 0.3228075659915428E+0 + v = 0.3114768142886460E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5321441655571562E+0 + b = 0.3589459907204151E+0 + v = 0.3143823673666223E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5660208438582166E+0 + b = 0.3939630088864310E+0 + v = 0.3162269764661535E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5980264315964364E+0 + b = 0.4276029922949089E+0 + v = 0.3172164663759821E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2644215852350733E+0 + b = 0.3300939429072552E-1 + v = 0.2554575398967435E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3090113743443063E+0 + b = 0.6803887650078501E-1 + v = 0.2701704069135677E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3525871079197808E+0 + b = 0.1044326136206709E+0 + v = 0.2823693413468940E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3950418005354029E+0 + b = 0.1416751597517679E+0 + v = 0.2922898463214289E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4362475663430163E+0 + b = 0.1793408610504821E+0 + v = 0.3001829062162428E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4760661812145854E+0 + b = 0.2170630750175722E+0 + v = 0.3062890864542953E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5143551042512103E+0 + b = 0.2545145157815807E+0 + v = 0.3108328279264746E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5509709026935597E+0 + b = 0.2913940101706601E+0 + v = 0.3140243146201245E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5857711030329428E+0 + b = 0.3274169910910705E+0 + v = 0.3160638030977130E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6186149917404392E+0 + b = 0.3623081329317265E+0 + v = 0.3171462882206275E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3586894569557064E+0 + b = 0.3497354386450040E-1 + v = 0.2812388416031796E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4035266610019441E+0 + b = 0.7129736739757095E-1 + v = 0.2912137500288045E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4467775312332510E+0 + b = 0.1084758620193165E+0 + v = 0.2993241256502206E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4883638346608543E+0 + b = 0.1460915689241772E+0 + v = 0.3057101738983822E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5281908348434601E+0 + b = 0.1837790832369980E+0 + v = 0.3105319326251432E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5661542687149311E+0 + b = 0.2212075390874021E+0 + v = 0.3139565514428167E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6021450102031452E+0 + b = 0.2580682841160985E+0 + v = 0.3161543006806366E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6360520783610050E+0 + b = 0.2940656362094121E+0 + v = 0.3172985960613294E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4521611065087196E+0 + b = 0.3631055365867002E-1 + v = 0.2989400336901431E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4959365651560963E+0 + b = 0.7348318468484350E-1 + v = 0.3054555883947677E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5376815804038283E+0 + b = 0.1111087643812648E+0 + v = 0.3104764960807702E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5773314480243768E+0 + b = 0.1488226085145408E+0 + v = 0.3141015825977616E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6148113245575056E+0 + b = 0.1862892274135151E+0 + v = 0.3164520621159896E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6500407462842380E+0 + b = 0.2231909701714456E+0 + v = 0.3176652305912204E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5425151448707213E+0 + b = 0.3718201306118944E-1 + v = 0.3105097161023939E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5841860556907931E+0 + b = 0.7483616335067346E-1 + v = 0.3143014117890550E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6234632186851500E+0 + b = 0.1125990834266120E+0 + v = 0.3168172866287200E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6602934551848843E+0 + b = 0.1501303813157619E+0 + v = 0.3181401865570968E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6278573968375105E+0 + b = 0.3767559930245720E-1 + v = 0.3170663659156037E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6665611711264577E+0 + b = 0.7548443301360158E-1 + v = 0.3185447944625510E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 3890: + v = 0.1807395252196920E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2848008782238827E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.2836065837530581E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1587876419858352E-1 + v = 0.7013149266673816E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4069193593751206E-1 + v = 0.1162798021956766E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7025888115257997E-1 + v = 0.1518728583972105E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1027495450028704E+0 + v = 0.1798796108216934E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1371457730893426E+0 + v = 0.2022593385972785E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1727758532671953E+0 + v = 0.2203093105575464E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2091492038929037E+0 + v = 0.2349294234299855E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2458813281751915E+0 + v = 0.2467682058747003E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2826545859450066E+0 + v = 0.2563092683572224E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3191957291799622E+0 + v = 0.2639253896763318E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3552621469299578E+0 + v = 0.2699137479265108E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3906329503406230E+0 + v = 0.2745196420166739E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4251028614093031E+0 + v = 0.2779529197397593E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4584777520111870E+0 + v = 0.2803996086684265E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4905711358710193E+0 + v = 0.2820302356715842E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5212011669847385E+0 + v = 0.2830056747491068E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5501878488737995E+0 + v = 0.2834808950776839E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6025037877479342E+0 + v = 0.2835282339078929E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6254572689549016E+0 + v = 0.2833819267065800E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6460107179528248E+0 + v = 0.2832858336906784E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6639541138154251E+0 + v = 0.2833268235451244E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6790688515667495E+0 + v = 0.2835432677029253E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6911338580371512E+0 + v = 0.2839091722743049E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6999385956126490E+0 + v = 0.2843308178875841E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7053037748656896E+0 + v = 0.2846703550533846E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4732224387180115E-1 + v = 0.1051193406971900E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1202100529326803E+0 + v = 0.1657871838796974E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2034304820664855E+0 + v = 0.2064648113714232E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2912285643573002E+0 + v = 0.2347942745819741E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3802361792726768E+0 + v = 0.2547775326597726E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4680598511056146E+0 + v = 0.2686876684847025E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5528151052155599E+0 + v = 0.2778665755515867E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6329386307803041E+0 + v = 0.2830996616782929E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.8056516651369069E-1 + b = 0.2363454684003124E-1 + v = 0.1403063340168372E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1156476077139389E+0 + b = 0.5191291632545936E-1 + v = 0.1696504125939477E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1520473382760421E+0 + b = 0.8322715736994519E-1 + v = 0.1935787242745390E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1892986699745931E+0 + b = 0.1165855667993712E+0 + v = 0.2130614510521968E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2270194446777792E+0 + b = 0.1513077167409504E+0 + v = 0.2289381265931048E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2648908185093273E+0 + b = 0.1868882025807859E+0 + v = 0.2418630292816186E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3026389259574136E+0 + b = 0.2229277629776224E+0 + v = 0.2523400495631193E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3400220296151384E+0 + b = 0.2590951840746235E+0 + v = 0.2607623973449605E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3768217953335510E+0 + b = 0.2951047291750847E+0 + v = 0.2674441032689209E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4128372900921884E+0 + b = 0.3307019714169930E+0 + v = 0.2726432360343356E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4478807131815630E+0 + b = 0.3656544101087634E+0 + v = 0.2765787685924545E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4817742034089257E+0 + b = 0.3997448951939695E+0 + v = 0.2794428690642224E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5143472814653344E+0 + b = 0.4327667110812024E+0 + v = 0.2814099002062895E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5454346213905650E+0 + b = 0.4645196123532293E+0 + v = 0.2826429531578994E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5748739313170252E+0 + b = 0.4948063555703345E+0 + v = 0.2832983542550884E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1599598738286342E+0 + b = 0.2792357590048985E-1 + v = 0.1886695565284976E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1998097412500951E+0 + b = 0.5877141038139065E-1 + v = 0.2081867882748234E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2396228952566202E+0 + b = 0.9164573914691377E-1 + v = 0.2245148680600796E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2792228341097746E+0 + b = 0.1259049641962687E+0 + v = 0.2380370491511872E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3184251107546741E+0 + b = 0.1610594823400863E+0 + v = 0.2491398041852455E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3570481164426244E+0 + b = 0.1967151653460898E+0 + v = 0.2581632405881230E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3949164710492144E+0 + b = 0.2325404606175168E+0 + v = 0.2653965506227417E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4318617293970503E+0 + b = 0.2682461141151439E+0 + v = 0.2710857216747087E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4677221009931678E+0 + b = 0.3035720116011973E+0 + v = 0.2754434093903659E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5023417939270955E+0 + b = 0.3382781859197439E+0 + v = 0.2786579932519380E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5355701836636128E+0 + b = 0.3721383065625942E+0 + v = 0.2809011080679474E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5672608451328771E+0 + b = 0.4049346360466055E+0 + v = 0.2823336184560987E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5972704202540162E+0 + b = 0.4364538098633802E+0 + v = 0.2831101175806309E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2461687022333596E+0 + b = 0.3070423166833368E-1 + v = 0.2221679970354546E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2881774566286831E+0 + b = 0.6338034669281885E-1 + v = 0.2356185734270703E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3293963604116978E+0 + b = 0.9742862487067941E-1 + v = 0.2469228344805590E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3697303822241377E+0 + b = 0.1323799532282290E+0 + v = 0.2562726348642046E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4090663023135127E+0 + b = 0.1678497018129336E+0 + v = 0.2638756726753028E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4472819355411712E+0 + b = 0.2035095105326114E+0 + v = 0.2699311157390862E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4842513377231437E+0 + b = 0.2390692566672091E+0 + v = 0.2746233268403837E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5198477629962928E+0 + b = 0.2742649818076149E+0 + v = 0.2781225674454771E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5539453011883145E+0 + b = 0.3088503806580094E+0 + v = 0.2805881254045684E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5864196762401251E+0 + b = 0.3425904245906614E+0 + v = 0.2821719877004913E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6171484466668390E+0 + b = 0.3752562294789468E+0 + v = 0.2830222502333124E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3350337830565727E+0 + b = 0.3261589934634747E-1 + v = 0.2457995956744870E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3775773224758284E+0 + b = 0.6658438928081572E-1 + v = 0.2551474407503706E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4188155229848973E+0 + b = 0.1014565797157954E+0 + v = 0.2629065335195311E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4586805892009344E+0 + b = 0.1368573320843822E+0 + v = 0.2691900449925075E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4970895714224235E+0 + b = 0.1724614851951608E+0 + v = 0.2741275485754276E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5339505133960747E+0 + b = 0.2079779381416412E+0 + v = 0.2778530970122595E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5691665792531440E+0 + b = 0.2431385788322288E+0 + v = 0.2805010567646741E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6026387682680377E+0 + b = 0.2776901883049853E+0 + v = 0.2822055834031040E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6342676150163307E+0 + b = 0.3113881356386632E+0 + v = 0.2831016901243473E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4237951119537067E+0 + b = 0.3394877848664351E-1 + v = 0.2624474901131803E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4656918683234929E+0 + b = 0.6880219556291447E-1 + v = 0.2688034163039377E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5058857069185980E+0 + b = 0.1041946859721635E+0 + v = 0.2738932751287636E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5443204666713996E+0 + b = 0.1398039738736393E+0 + v = 0.2777944791242523E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5809298813759742E+0 + b = 0.1753373381196155E+0 + v = 0.2806011661660987E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6156416039447128E+0 + b = 0.2105215793514010E+0 + v = 0.2824181456597460E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6483801351066604E+0 + b = 0.2450953312157051E+0 + v = 0.2833585216577828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5103616577251688E+0 + b = 0.3485560643800719E-1 + v = 0.2738165236962878E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5506738792580681E+0 + b = 0.7026308631512033E-1 + v = 0.2778365208203180E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5889573040995292E+0 + b = 0.1059035061296403E+0 + v = 0.2807852940418966E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6251641589516930E+0 + b = 0.1414823925236026E+0 + v = 0.2827245949674705E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6592414921570178E+0 + b = 0.1767207908214530E+0 + v = 0.2837342344829828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5930314017533384E+0 + b = 0.3542189339561672E-1 + v = 0.2809233907610981E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6309812253390175E+0 + b = 0.7109574040369549E-1 + v = 0.2829930809742694E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6666296011353230E+0 + b = 0.1067259792282730E+0 + v = 0.2841097874111479E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6703715271049922E+0 + b = 0.3569455268820809E-1 + v = 0.2843455206008783E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 4334: + v = 0.1449063022537883E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2546377329828424E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1462896151831013E-1 + v = 0.6018432961087496E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3769840812493139E-1 + v = 0.1002286583263673E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6524701904096891E-1 + v = 0.1315222931028093E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9560543416134648E-1 + v = 0.1564213746876724E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1278335898929198E+0 + v = 0.1765118841507736E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1613096104466031E+0 + v = 0.1928737099311080E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1955806225745371E+0 + v = 0.2062658534263270E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2302935218498028E+0 + v = 0.2172395445953787E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2651584344113027E+0 + v = 0.2262076188876047E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2999276825183209E+0 + v = 0.2334885699462397E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3343828669718798E+0 + v = 0.2393355273179203E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3683265013750518E+0 + v = 0.2439559200468863E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4015763206518108E+0 + v = 0.2475251866060002E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4339612026399770E+0 + v = 0.2501965558158773E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4653180651114582E+0 + v = 0.2521081407925925E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4954893331080803E+0 + v = 0.2533881002388081E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5243207068924930E+0 + v = 0.2541582900848261E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5516590479041704E+0 + v = 0.2545365737525860E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6012371927804176E+0 + v = 0.2545726993066799E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6231574466449819E+0 + v = 0.2544456197465555E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6429416514181271E+0 + v = 0.2543481596881064E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6604124272943595E+0 + v = 0.2543506451429194E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6753851470408250E+0 + v = 0.2544905675493763E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6876717970626160E+0 + v = 0.2547611407344429E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6970895061319234E+0 + v = 0.2551060375448869E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7034746912553310E+0 + v = 0.2554291933816039E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7067017217542295E+0 + v = 0.2556255710686343E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4382223501131123E-1 + v = 0.9041339695118195E-4 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1117474077400006E+0 + v = 0.1438426330079022E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1897153252911440E+0 + v = 0.1802523089820518E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2724023009910331E+0 + v = 0.2060052290565496E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3567163308709902E+0 + v = 0.2245002248967466E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4404784483028087E+0 + v = 0.2377059847731150E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5219833154161411E+0 + v = 0.2468118955882525E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5998179868977553E+0 + v = 0.2525410872966528E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6727803154548222E+0 + v = 0.2553101409933397E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.7476563943166086E-1 + b = 0.2193168509461185E-1 + v = 0.1212879733668632E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1075341482001416E+0 + b = 0.4826419281533887E-1 + v = 0.1472872881270931E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1416344885203259E+0 + b = 0.7751191883575742E-1 + v = 0.1686846601010828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1766325315388586E+0 + b = 0.1087558139247680E+0 + v = 0.1862698414660208E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2121744174481514E+0 + b = 0.1413661374253096E+0 + v = 0.2007430956991861E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2479669443408145E+0 + b = 0.1748768214258880E+0 + v = 0.2126568125394796E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2837600452294113E+0 + b = 0.2089216406612073E+0 + v = 0.2224394603372113E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3193344933193984E+0 + b = 0.2431987685545972E+0 + v = 0.2304264522673135E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3544935442438745E+0 + b = 0.2774497054377770E+0 + v = 0.2368854288424087E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3890571932288154E+0 + b = 0.3114460356156915E+0 + v = 0.2420352089461772E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4228581214259090E+0 + b = 0.3449806851913012E+0 + v = 0.2460597113081295E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4557387211304052E+0 + b = 0.3778618641248256E+0 + v = 0.2491181912257687E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4875487950541643E+0 + b = 0.4099086391698978E+0 + v = 0.2513528194205857E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5181436529962997E+0 + b = 0.4409474925853973E+0 + v = 0.2528943096693220E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5473824095600661E+0 + b = 0.4708094517711291E+0 + v = 0.2538660368488136E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5751263398976174E+0 + b = 0.4993275140354637E+0 + v = 0.2543868648299022E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1489515746840028E+0 + b = 0.2599381993267017E-1 + v = 0.1642595537825183E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1863656444351767E+0 + b = 0.5479286532462190E-1 + v = 0.1818246659849308E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2238602880356348E+0 + b = 0.8556763251425254E-1 + v = 0.1966565649492420E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2612723375728160E+0 + b = 0.1177257802267011E+0 + v = 0.2090677905657991E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2984332990206190E+0 + b = 0.1508168456192700E+0 + v = 0.2193820409510504E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3351786584663333E+0 + b = 0.1844801892177727E+0 + v = 0.2278870827661928E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3713505522209120E+0 + b = 0.2184145236087598E+0 + v = 0.2348283192282090E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4067981098954663E+0 + b = 0.2523590641486229E+0 + v = 0.2404139755581477E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4413769993687534E+0 + b = 0.2860812976901373E+0 + v = 0.2448227407760734E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4749487182516394E+0 + b = 0.3193686757808996E+0 + v = 0.2482110455592573E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5073798105075426E+0 + b = 0.3520226949547602E+0 + v = 0.2507192397774103E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5385410448878654E+0 + b = 0.3838544395667890E+0 + v = 0.2524765968534880E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5683065353670530E+0 + b = 0.4146810037640963E+0 + v = 0.2536052388539425E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5965527620663510E+0 + b = 0.4443224094681121E+0 + v = 0.2542230588033068E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2299227700856157E+0 + b = 0.2865757664057584E-1 + v = 0.1944817013047896E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2695752998553267E+0 + b = 0.5923421684485993E-1 + v = 0.2067862362746635E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3086178716611389E+0 + b = 0.9117817776057715E-1 + v = 0.2172440734649114E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3469649871659077E+0 + b = 0.1240593814082605E+0 + v = 0.2260125991723423E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3845153566319655E+0 + b = 0.1575272058259175E+0 + v = 0.2332655008689523E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4211600033403215E+0 + b = 0.1912845163525413E+0 + v = 0.2391699681532458E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4567867834329882E+0 + b = 0.2250710177858171E+0 + v = 0.2438801528273928E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4912829319232061E+0 + b = 0.2586521303440910E+0 + v = 0.2475370504260665E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5245364793303812E+0 + b = 0.2918112242865407E+0 + v = 0.2502707235640574E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5564369788915756E+0 + b = 0.3243439239067890E+0 + v = 0.2522031701054241E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5868757697775287E+0 + b = 0.3560536787835351E+0 + v = 0.2534511269978784E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6157458853519617E+0 + b = 0.3867480821242581E+0 + v = 0.2541284914955151E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3138461110672113E+0 + b = 0.3051374637507278E-1 + v = 0.2161509250688394E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3542495872050569E+0 + b = 0.6237111233730755E-1 + v = 0.2248778513437852E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3935751553120181E+0 + b = 0.9516223952401907E-1 + v = 0.2322388803404617E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4317634668111147E+0 + b = 0.1285467341508517E+0 + v = 0.2383265471001355E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4687413842250821E+0 + b = 0.1622318931656033E+0 + v = 0.2432476675019525E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5044274237060283E+0 + b = 0.1959581153836453E+0 + v = 0.2471122223750674E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5387354077925727E+0 + b = 0.2294888081183837E+0 + v = 0.2500291752486870E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5715768898356105E+0 + b = 0.2626031152713945E+0 + v = 0.2521055942764682E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6028627200136111E+0 + b = 0.2950904075286713E+0 + v = 0.2534472785575503E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6325039812653463E+0 + b = 0.3267458451113286E+0 + v = 0.2541599713080121E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3981986708423407E+0 + b = 0.3183291458749821E-1 + v = 0.2317380975862936E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4382791182133300E+0 + b = 0.6459548193880908E-1 + v = 0.2378550733719775E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4769233057218166E+0 + b = 0.9795757037087952E-1 + v = 0.2428884456739118E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5140823911194238E+0 + b = 0.1316307235126655E+0 + v = 0.2469002655757292E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5496977833862983E+0 + b = 0.1653556486358704E+0 + v = 0.2499657574265851E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5837047306512727E+0 + b = 0.1988931724126510E+0 + v = 0.2521676168486082E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6160349566926879E+0 + b = 0.2320174581438950E+0 + v = 0.2535935662645334E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6466185353209440E+0 + b = 0.2645106562168662E+0 + v = 0.2543356743363214E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4810835158795404E+0 + b = 0.3275917807743992E-1 + v = 0.2427353285201535E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5199925041324341E+0 + b = 0.6612546183967181E-1 + v = 0.2468258039744386E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5571717692207494E+0 + b = 0.9981498331474143E-1 + v = 0.2500060956440310E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5925789250836378E+0 + b = 0.1335687001410374E+0 + v = 0.2523238365420979E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6261658523859670E+0 + b = 0.1671444402896463E+0 + v = 0.2538399260252846E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6578811126669331E+0 + b = 0.2003106382156076E+0 + v = 0.2546255927268069E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5609624612998100E+0 + b = 0.3337500940231335E-1 + v = 0.2500583360048449E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5979959659984670E+0 + b = 0.6708750335901803E-1 + v = 0.2524777638260203E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6330523711054002E+0 + b = 0.1008792126424850E+0 + v = 0.2540951193860656E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6660960998103972E+0 + b = 0.1345050343171794E+0 + v = 0.2549524085027472E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6365384364585819E+0 + b = 0.3372799460737052E-1 + v = 0.2542569507009158E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6710994302899275E+0 + b = 0.6755249309678028E-1 + v = 0.2552114127580376E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 4802: + v = 0.9687521879420705E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2307897895367918E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.2297310852498558E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2335728608887064E-1 + v = 0.7386265944001919E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4352987836550653E-1 + v = 0.8257977698542210E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6439200521088801E-1 + v = 0.9706044762057630E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.9003943631993181E-1 + v = 0.1302393847117003E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1196706615548473E+0 + v = 0.1541957004600968E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1511715412838134E+0 + v = 0.1704459770092199E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1835982828503801E+0 + v = 0.1827374890942906E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2165081259155405E+0 + v = 0.1926360817436107E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2496208720417563E+0 + v = 0.2008010239494833E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2827200673567900E+0 + v = 0.2075635983209175E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3156190823994346E+0 + v = 0.2131306638690909E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3481476793749115E+0 + v = 0.2176562329937335E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3801466086947226E+0 + v = 0.2212682262991018E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4114652119634011E+0 + v = 0.2240799515668565E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4419598786519751E+0 + v = 0.2261959816187525E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4714925949329543E+0 + v = 0.2277156368808855E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4999293972879466E+0 + v = 0.2287351772128336E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5271387221431248E+0 + v = 0.2293490814084085E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5529896780837761E+0 + v = 0.2296505312376273E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6000856099481712E+0 + v = 0.2296793832318756E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6210562192785175E+0 + v = 0.2295785443842974E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6401165879934240E+0 + v = 0.2295017931529102E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6571144029244334E+0 + v = 0.2295059638184868E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6718910821718863E+0 + v = 0.2296232343237362E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6842845591099010E+0 + v = 0.2298530178740771E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6941353476269816E+0 + v = 0.2301579790280501E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7012965242212991E+0 + v = 0.2304690404996513E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7056471428242644E+0 + v = 0.2307027995907102E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4595557643585895E-1 + v = 0.9312274696671092E-4 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1049316742435023E+0 + v = 0.1199919385876926E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1773548879549274E+0 + v = 0.1598039138877690E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2559071411236127E+0 + v = 0.1822253763574900E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3358156837985898E+0 + v = 0.1988579593655040E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4155835743763893E+0 + v = 0.2112620102533307E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4937894296167472E+0 + v = 0.2201594887699007E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5691569694793316E+0 + v = 0.2261622590895036E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6405840854894251E+0 + v = 0.2296458453435705E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.7345133894143348E-1 + b = 0.2177844081486067E-1 + v = 0.1006006990267000E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1009859834044931E+0 + b = 0.4590362185775188E-1 + v = 0.1227676689635876E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1324289619748758E+0 + b = 0.7255063095690877E-1 + v = 0.1467864280270117E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1654272109607127E+0 + b = 0.1017825451960684E+0 + v = 0.1644178912101232E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1990767186776461E+0 + b = 0.1325652320980364E+0 + v = 0.1777664890718961E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2330125945523278E+0 + b = 0.1642765374496765E+0 + v = 0.1884825664516690E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2670080611108287E+0 + b = 0.1965360374337889E+0 + v = 0.1973269246453848E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3008753376294316E+0 + b = 0.2290726770542238E+0 + v = 0.2046767775855328E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3344475596167860E+0 + b = 0.2616645495370823E+0 + v = 0.2107600125918040E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3675709724070786E+0 + b = 0.2941150728843141E+0 + v = 0.2157416362266829E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4001000887587812E+0 + b = 0.3262440400919066E+0 + v = 0.2197557816920721E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4318956350436028E+0 + b = 0.3578835350611916E+0 + v = 0.2229192611835437E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4628239056795531E+0 + b = 0.3888751854043678E+0 + v = 0.2253385110212775E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4927563229773636E+0 + b = 0.4190678003222840E+0 + v = 0.2271137107548774E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5215687136707969E+0 + b = 0.4483151836883852E+0 + v = 0.2283414092917525E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5491402346984905E+0 + b = 0.4764740676087880E+0 + v = 0.2291161673130077E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5753520160126075E+0 + b = 0.5034021310998277E+0 + v = 0.2295313908576598E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1388326356417754E+0 + b = 0.2435436510372806E-1 + v = 0.1438204721359031E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1743686900537244E+0 + b = 0.5118897057342652E-1 + v = 0.1607738025495257E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2099737037950268E+0 + b = 0.8014695048539634E-1 + v = 0.1741483853528379E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2454492590908548E+0 + b = 0.1105117874155699E+0 + v = 0.1851918467519151E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2807219257864278E+0 + b = 0.1417950531570966E+0 + v = 0.1944628638070613E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3156842271975842E+0 + b = 0.1736604945719597E+0 + v = 0.2022495446275152E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3502090945177752E+0 + b = 0.2058466324693981E+0 + v = 0.2087462382438514E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3841684849519686E+0 + b = 0.2381284261195919E+0 + v = 0.2141074754818308E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4174372367906016E+0 + b = 0.2703031270422569E+0 + v = 0.2184640913748162E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4498926465011892E+0 + b = 0.3021845683091309E+0 + v = 0.2219309165220329E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4814146229807701E+0 + b = 0.3335993355165720E+0 + v = 0.2246123118340624E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5118863625734701E+0 + b = 0.3643833735518232E+0 + v = 0.2266062766915125E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5411947455119144E+0 + b = 0.3943789541958179E+0 + v = 0.2280072952230796E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5692301500357246E+0 + b = 0.4234320144403542E+0 + v = 0.2289082025202583E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5958857204139576E+0 + b = 0.4513897947419260E+0 + v = 0.2294012695120025E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2156270284785766E+0 + b = 0.2681225755444491E-1 + v = 0.1722434488736947E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2532385054909710E+0 + b = 0.5557495747805614E-1 + v = 0.1830237421455091E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2902564617771537E+0 + b = 0.8569368062950249E-1 + v = 0.1923855349997633E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3266979823143256E+0 + b = 0.1167367450324135E+0 + v = 0.2004067861936271E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3625039627493614E+0 + b = 0.1483861994003304E+0 + v = 0.2071817297354263E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3975838937548699E+0 + b = 0.1803821503011405E+0 + v = 0.2128250834102103E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4318396099009774E+0 + b = 0.2124962965666424E+0 + v = 0.2174513719440102E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4651706555732742E+0 + b = 0.2445221837805913E+0 + v = 0.2211661839150214E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4974752649620969E+0 + b = 0.2762701224322987E+0 + v = 0.2240665257813102E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5286517579627517E+0 + b = 0.3075627775211328E+0 + v = 0.2262439516632620E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5586001195731895E+0 + b = 0.3382311089826877E+0 + v = 0.2277874557231869E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5872229902021319E+0 + b = 0.3681108834741399E+0 + v = 0.2287854314454994E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6144258616235123E+0 + b = 0.3970397446872839E+0 + v = 0.2293268499615575E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2951676508064861E+0 + b = 0.2867499538750441E-1 + v = 0.1912628201529828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3335085485472725E+0 + b = 0.5867879341903510E-1 + v = 0.1992499672238701E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3709561760636381E+0 + b = 0.8961099205022284E-1 + v = 0.2061275533454027E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4074722861667498E+0 + b = 0.1211627927626297E+0 + v = 0.2119318215968572E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4429923648839117E+0 + b = 0.1530748903554898E+0 + v = 0.2167416581882652E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4774428052721736E+0 + b = 0.1851176436721877E+0 + v = 0.2206430730516600E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5107446539535904E+0 + b = 0.2170829107658179E+0 + v = 0.2237186938699523E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5428151370542935E+0 + b = 0.2487786689026271E+0 + v = 0.2260480075032884E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5735699292556964E+0 + b = 0.2800239952795016E+0 + v = 0.2277098884558542E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6029253794562866E+0 + b = 0.3106445702878119E+0 + v = 0.2287845715109671E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6307998987073145E+0 + b = 0.3404689500841194E+0 + v = 0.2293547268236294E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3752652273692719E+0 + b = 0.2997145098184479E-1 + v = 0.2056073839852528E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4135383879344028E+0 + b = 0.6086725898678011E-1 + v = 0.2114235865831876E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4506113885153907E+0 + b = 0.9238849548435643E-1 + v = 0.2163175629770551E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4864401554606072E+0 + b = 0.1242786603851851E+0 + v = 0.2203392158111650E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5209708076611709E+0 + b = 0.1563086731483386E+0 + v = 0.2235473176847839E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5541422135830122E+0 + b = 0.1882696509388506E+0 + v = 0.2260024141501235E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5858880915113817E+0 + b = 0.2199672979126059E+0 + v = 0.2277675929329182E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6161399390603444E+0 + b = 0.2512165482924867E+0 + v = 0.2289102112284834E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6448296482255090E+0 + b = 0.2818368701871888E+0 + v = 0.2295027954625118E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4544796274917948E+0 + b = 0.3088970405060312E-1 + v = 0.2161281589879992E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4919389072146628E+0 + b = 0.6240947677636835E-1 + v = 0.2201980477395102E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5279313026985183E+0 + b = 0.9430706144280313E-1 + v = 0.2234952066593166E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5624169925571135E+0 + b = 0.1263547818770374E+0 + v = 0.2260540098520838E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5953484627093287E+0 + b = 0.1583430788822594E+0 + v = 0.2279157981899988E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6266730715339185E+0 + b = 0.1900748462555988E+0 + v = 0.2291296918565571E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6563363204278871E+0 + b = 0.2213599519592567E+0 + v = 0.2297533752536649E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5314574716585696E+0 + b = 0.3152508811515374E-1 + v = 0.2234927356465995E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5674614932298185E+0 + b = 0.6343865291465561E-1 + v = 0.2261288012985219E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6017706004970264E+0 + b = 0.9551503504223951E-1 + v = 0.2280818160923688E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6343471270264178E+0 + b = 0.1275440099801196E+0 + v = 0.2293773295180159E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6651494599127802E+0 + b = 0.1593252037671960E+0 + v = 0.2300528767338634E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6050184986005704E+0 + b = 0.3192538338496105E-1 + v = 0.2281893855065666E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6390163550880400E+0 + b = 0.6402824353962306E-1 + v = 0.2295720444840727E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6711199107088448E+0 + b = 0.9609805077002909E-1 + v = 0.2303227649026753E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6741354429572275E+0 + b = 0.3211853196273233E-1 + v = 0.2304831913227114E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 5294: + v = 0.9080510764308163E-4 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.2084824361987793E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.2303261686261450E-1 + v = 0.5011105657239616E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3757208620162394E-1 + v = 0.5942520409683854E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5821912033821852E-1 + v = 0.9564394826109721E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.8403127529194872E-1 + v = 0.1185530657126338E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1122927798060578E+0 + v = 0.1364510114230331E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1420125319192987E+0 + v = 0.1505828825605415E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1726396437341978E+0 + v = 0.1619298749867023E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2038170058115696E+0 + v = 0.1712450504267789E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2352849892876508E+0 + v = 0.1789891098164999E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2668363354312461E+0 + v = 0.1854474955629795E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2982941279900452E+0 + v = 0.1908148636673661E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3295002922087076E+0 + v = 0.1952377405281833E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3603094918363593E+0 + v = 0.1988349254282232E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3905857895173920E+0 + v = 0.2017079807160050E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4202005758160837E+0 + v = 0.2039473082709094E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4490310061597227E+0 + v = 0.2056360279288953E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4769586160311491E+0 + v = 0.2068525823066865E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5038679887049750E+0 + v = 0.2076724877534488E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5296454286519961E+0 + v = 0.2081694278237885E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5541776207164850E+0 + v = 0.2084157631219326E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5990467321921213E+0 + v = 0.2084381531128593E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6191467096294587E+0 + v = 0.2083476277129307E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6375251212901849E+0 + v = 0.2082686194459732E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6540514381131168E+0 + v = 0.2082475686112415E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6685899064391510E+0 + v = 0.2083139860289915E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6810013009681648E+0 + v = 0.2084745561831237E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6911469578730340E+0 + v = 0.2087091313375890E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6988956915141736E+0 + v = 0.2089718413297697E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7041335794868720E+0 + v = 0.2092003303479793E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7067754398018567E+0 + v = 0.2093336148263241E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3840368707853623E-1 + v = 0.7591708117365267E-4 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9835485954117399E-1 + v = 0.1083383968169186E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1665774947612998E+0 + v = 0.1403019395292510E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2405702335362910E+0 + v = 0.1615970179286436E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3165270770189046E+0 + v = 0.1771144187504911E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3927386145645443E+0 + v = 0.1887760022988168E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4678825918374656E+0 + v = 0.1973474670768214E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5408022024266935E+0 + v = 0.2033787661234659E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6104967445752438E+0 + v = 0.2072343626517331E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6760910702685738E+0 + v = 0.2091177834226918E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6655644120217392E-1 + b = 0.1936508874588424E-1 + v = 0.9316684484675566E-4 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.9446246161270182E-1 + b = 0.4252442002115869E-1 + v = 0.1116193688682976E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1242651925452509E+0 + b = 0.6806529315354374E-1 + v = 0.1298623551559414E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1553438064846751E+0 + b = 0.9560957491205369E-1 + v = 0.1450236832456426E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1871137110542670E+0 + b = 0.1245931657452888E+0 + v = 0.1572719958149914E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2192612628836257E+0 + b = 0.1545385828778978E+0 + v = 0.1673234785867195E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2515682807206955E+0 + b = 0.1851004249723368E+0 + v = 0.1756860118725188E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2838535866287290E+0 + b = 0.2160182608272384E+0 + v = 0.1826776290439367E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3159578817528521E+0 + b = 0.2470799012277111E+0 + v = 0.1885116347992865E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3477370882791392E+0 + b = 0.2781014208986402E+0 + v = 0.1933457860170574E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3790576960890540E+0 + b = 0.3089172523515731E+0 + v = 0.1973060671902064E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4097938317810200E+0 + b = 0.3393750055472244E+0 + v = 0.2004987099616311E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4398256572859637E+0 + b = 0.3693322470987730E+0 + v = 0.2030170909281499E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4690384114718480E+0 + b = 0.3986541005609877E+0 + v = 0.2049461460119080E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4973216048301053E+0 + b = 0.4272112491408562E+0 + v = 0.2063653565200186E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5245681526132446E+0 + b = 0.4548781735309936E+0 + v = 0.2073507927381027E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5506733911803888E+0 + b = 0.4815315355023251E+0 + v = 0.2079764593256122E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5755339829522475E+0 + b = 0.5070486445801855E+0 + v = 0.2083150534968778E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1305472386056362E+0 + b = 0.2284970375722366E-1 + v = 0.1262715121590664E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1637327908216477E+0 + b = 0.4812254338288384E-1 + v = 0.1414386128545972E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1972734634149637E+0 + b = 0.7531734457511935E-1 + v = 0.1538740401313898E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2308694653110130E+0 + b = 0.1039043639882017E+0 + v = 0.1642434942331432E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2643899218338160E+0 + b = 0.1334526587117626E+0 + v = 0.1729790609237496E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2977171599622171E+0 + b = 0.1636414868936382E+0 + v = 0.1803505190260828E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3307293903032310E+0 + b = 0.1942195406166568E+0 + v = 0.1865475350079657E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3633069198219073E+0 + b = 0.2249752879943753E+0 + v = 0.1917182669679069E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3953346955922727E+0 + b = 0.2557218821820032E+0 + v = 0.1959851709034382E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4267018394184914E+0 + b = 0.2862897925213193E+0 + v = 0.1994529548117882E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4573009622571704E+0 + b = 0.3165224536636518E+0 + v = 0.2022138911146548E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4870279559856109E+0 + b = 0.3462730221636496E+0 + v = 0.2043518024208592E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5157819581450322E+0 + b = 0.3754016870282835E+0 + v = 0.2059450313018110E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5434651666465393E+0 + b = 0.4037733784993613E+0 + v = 0.2070685715318472E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5699823887764627E+0 + b = 0.4312557784139123E+0 + v = 0.2077955310694373E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5952403350947741E+0 + b = 0.4577175367122110E+0 + v = 0.2081980387824712E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2025152599210369E+0 + b = 0.2520253617719557E-1 + v = 0.1521318610377956E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2381066653274425E+0 + b = 0.5223254506119000E-1 + v = 0.1622772720185755E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2732823383651612E+0 + b = 0.8060669688588620E-1 + v = 0.1710498139420709E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3080137692611118E+0 + b = 0.1099335754081255E+0 + v = 0.1785911149448736E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3422405614587601E+0 + b = 0.1399120955959857E+0 + v = 0.1850125313687736E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3758808773890420E+0 + b = 0.1702977801651705E+0 + v = 0.1904229703933298E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4088458383438932E+0 + b = 0.2008799256601680E+0 + v = 0.1949259956121987E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4410450550841152E+0 + b = 0.2314703052180836E+0 + v = 0.1986161545363960E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4723879420561312E+0 + b = 0.2618972111375892E+0 + v = 0.2015790585641370E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5027843561874343E+0 + b = 0.2920013195600270E+0 + v = 0.2038934198707418E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5321453674452458E+0 + b = 0.3216322555190551E+0 + v = 0.2056334060538251E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5603839113834030E+0 + b = 0.3506456615934198E+0 + v = 0.2068705959462289E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5874150706875146E+0 + b = 0.3789007181306267E+0 + v = 0.2076753906106002E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6131559381660038E+0 + b = 0.4062580170572782E+0 + v = 0.2081179391734803E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2778497016394506E+0 + b = 0.2696271276876226E-1 + v = 0.1700345216228943E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3143733562261912E+0 + b = 0.5523469316960465E-1 + v = 0.1774906779990410E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3501485810261827E+0 + b = 0.8445193201626464E-1 + v = 0.1839659377002642E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3851430322303653E+0 + b = 0.1143263119336083E+0 + v = 0.1894987462975169E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4193013979470415E+0 + b = 0.1446177898344475E+0 + v = 0.1941548809452595E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4525585960458567E+0 + b = 0.1751165438438091E+0 + v = 0.1980078427252384E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4848447779622947E+0 + b = 0.2056338306745660E+0 + v = 0.2011296284744488E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5160871208276894E+0 + b = 0.2359965487229226E+0 + v = 0.2035888456966776E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5462112185696926E+0 + b = 0.2660430223139146E+0 + v = 0.2054516325352142E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5751425068101757E+0 + b = 0.2956193664498032E+0 + v = 0.2067831033092635E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6028073872853596E+0 + b = 0.3245763905312779E+0 + v = 0.2076485320284876E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6291338275278409E+0 + b = 0.3527670026206972E+0 + v = 0.2081141439525255E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3541797528439391E+0 + b = 0.2823853479435550E-1 + v = 0.1834383015469222E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3908234972074657E+0 + b = 0.5741296374713106E-1 + v = 0.1889540591777677E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4264408450107590E+0 + b = 0.8724646633650199E-1 + v = 0.1936677023597375E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4609949666553286E+0 + b = 0.1175034422915616E+0 + v = 0.1976176495066504E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4944389496536006E+0 + b = 0.1479755652628428E+0 + v = 0.2008536004560983E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5267194884346086E+0 + b = 0.1784740659484352E+0 + v = 0.2034280351712291E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5577787810220990E+0 + b = 0.2088245700431244E+0 + v = 0.2053944466027758E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5875563763536670E+0 + b = 0.2388628136570763E+0 + v = 0.2068077642882360E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6159910016391269E+0 + b = 0.2684308928769185E+0 + v = 0.2077250949661599E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6430219602956268E+0 + b = 0.2973740761960252E+0 + v = 0.2082062440705320E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4300647036213646E+0 + b = 0.2916399920493977E-1 + v = 0.1934374486546626E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4661486308935531E+0 + b = 0.5898803024755659E-1 + v = 0.1974107010484300E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5009658555287261E+0 + b = 0.8924162698525409E-1 + v = 0.2007129290388658E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5344824270447704E+0 + b = 0.1197185199637321E+0 + v = 0.2033736947471293E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5666575997416371E+0 + b = 0.1502300756161382E+0 + v = 0.2054287125902493E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5974457471404752E+0 + b = 0.1806004191913564E+0 + v = 0.2069184936818894E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6267984444116886E+0 + b = 0.2106621764786252E+0 + v = 0.2078883689808782E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6546664713575417E+0 + b = 0.2402526932671914E+0 + v = 0.2083886366116359E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5042711004437253E+0 + b = 0.2982529203607657E-1 + v = 0.2006593275470817E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5392127456774380E+0 + b = 0.6008728062339922E-1 + v = 0.2033728426135397E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5726819437668618E+0 + b = 0.9058227674571398E-1 + v = 0.2055008781377608E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6046469254207278E+0 + b = 0.1211219235803400E+0 + v = 0.2070651783518502E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6350716157434952E+0 + b = 0.1515286404791580E+0 + v = 0.2080953335094320E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6639177679185454E+0 + b = 0.1816314681255552E+0 + v = 0.2086284998988521E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5757276040972253E+0 + b = 0.3026991752575440E-1 + v = 0.2055549387644668E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6090265823139755E+0 + b = 0.6078402297870770E-1 + v = 0.2071871850267654E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6406735344387661E+0 + b = 0.9135459984176636E-1 + v = 0.2082856600431965E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6706397927793709E+0 + b = 0.1218024155966590E+0 + v = 0.2088705858819358E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6435019674426665E+0 + b = 0.3052608357660639E-1 + v = 0.2083995867536322E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6747218676375681E+0 + b = 0.6112185773983089E-1 + v = 0.2090509712889637E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + elif degree == 5810: + v = 0.9735347946175486E-5 + leb_tmp, start = _lebedevReccurencePoints(1, start, a, b, v, leb_tmp) + v = 0.1907581241803167E-3 + leb_tmp, start = _lebedevReccurencePoints(2, start, a, b, v, leb_tmp) + v = 0.1901059546737578E-3 + leb_tmp, start = _lebedevReccurencePoints(3, start, a, b, v, leb_tmp) + a = 0.1182361662400277E-1 + v = 0.3926424538919212E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3062145009138958E-1 + v = 0.6667905467294382E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5329794036834243E-1 + v = 0.8868891315019135E-4 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7848165532862220E-1 + v = 0.1066306000958872E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1054038157636201E+0 + v = 0.1214506743336128E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1335577797766211E+0 + v = 0.1338054681640871E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1625769955502252E+0 + v = 0.1441677023628504E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.1921787193412792E+0 + v = 0.1528880200826557E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2221340534690548E+0 + v = 0.1602330623773609E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2522504912791132E+0 + v = 0.1664102653445244E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.2823610860679697E+0 + v = 0.1715845854011323E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3123173966267560E+0 + v = 0.1758901000133069E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3419847036953789E+0 + v = 0.1794382485256736E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3712386456999758E+0 + v = 0.1823238106757407E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3999627649876828E+0 + v = 0.1846293252959976E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4280466458648093E+0 + v = 0.1864284079323098E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4553844360185711E+0 + v = 0.1877882694626914E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.4818736094437834E+0 + v = 0.1887716321852025E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5074138709260629E+0 + v = 0.1894381638175673E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5319061304570707E+0 + v = 0.1898454899533629E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5552514978677286E+0 + v = 0.1900497929577815E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.5981009025246183E+0 + v = 0.1900671501924092E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6173990192228116E+0 + v = 0.1899837555533510E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6351365239411131E+0 + v = 0.1899014113156229E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6512010228227200E+0 + v = 0.1898581257705106E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6654758363948120E+0 + v = 0.1898804756095753E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6778410414853370E+0 + v = 0.1899793610426402E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6881760887484110E+0 + v = 0.1901464554844117E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.6963645267094598E+0 + v = 0.1903533246259542E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7023010617153579E+0 + v = 0.1905556158463228E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.7059004636628753E+0 + v = 0.1907037155663528E-3 + leb_tmp, start = _lebedevReccurencePoints(4, start, a, b, v, leb_tmp) + a = 0.3552470312472575E-1 + v = 0.5992997844249967E-4 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.9151176620841283E-1 + v = 0.9749059382456978E-4 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.1566197930068980E+0 + v = 0.1241680804599158E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2265467599271907E+0 + v = 0.1437626154299360E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.2988242318581361E+0 + v = 0.1584200054793902E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.3717482419703886E+0 + v = 0.1694436550982744E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.4440094491758889E+0 + v = 0.1776617014018108E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5145337096756642E+0 + v = 0.1836132434440077E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.5824053672860230E+0 + v = 0.1876494727075983E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6468283961043370E+0 + v = 0.1899906535336482E-3 + leb_tmp, start = _lebedevReccurencePoints(5, start, a, b, v, leb_tmp) + a = 0.6095964259104373E-1 + b = 0.1787828275342931E-1 + v = 0.8143252820767350E-4 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.8811962270959388E-1 + b = 0.3953888740792096E-1 + v = 0.9998859890887728E-4 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1165936722428831E+0 + b = 0.6378121797722990E-1 + v = 0.1156199403068359E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1460232857031785E+0 + b = 0.8985890813745037E-1 + v = 0.1287632092635513E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1761197110181755E+0 + b = 0.1172606510576162E+0 + v = 0.1398378643365139E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2066471190463718E+0 + b = 0.1456102876970995E+0 + v = 0.1491876468417391E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2374076026328152E+0 + b = 0.1746153823011775E+0 + v = 0.1570855679175456E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2682305474337051E+0 + b = 0.2040383070295584E+0 + v = 0.1637483948103775E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2989653312142369E+0 + b = 0.2336788634003698E+0 + v = 0.1693500566632843E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3294762752772209E+0 + b = 0.2633632752654219E+0 + v = 0.1740322769393633E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3596390887276086E+0 + b = 0.2929369098051601E+0 + v = 0.1779126637278296E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3893383046398812E+0 + b = 0.3222592785275512E+0 + v = 0.1810908108835412E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4184653789358347E+0 + b = 0.3512004791195743E+0 + v = 0.1836529132600190E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4469172319076166E+0 + b = 0.3796385677684537E+0 + v = 0.1856752841777379E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4745950813276976E+0 + b = 0.4074575378263879E+0 + v = 0.1872270566606832E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5014034601410262E+0 + b = 0.4345456906027828E+0 + v = 0.1883722645591307E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5272493404551239E+0 + b = 0.4607942515205134E+0 + v = 0.1891714324525297E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5520413051846366E+0 + b = 0.4860961284181720E+0 + v = 0.1896827480450146E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5756887237503077E+0 + b = 0.5103447395342790E+0 + v = 0.1899628417059528E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1225039430588352E+0 + b = 0.2136455922655793E-1 + v = 0.1123301829001669E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1539113217321372E+0 + b = 0.4520926166137188E-1 + v = 0.1253698826711277E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1856213098637712E+0 + b = 0.7086468177864818E-1 + v = 0.1366266117678531E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2174998728035131E+0 + b = 0.9785239488772918E-1 + v = 0.1462736856106918E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2494128336938330E+0 + b = 0.1258106396267210E+0 + v = 0.1545076466685412E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2812321562143480E+0 + b = 0.1544529125047001E+0 + v = 0.1615096280814007E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3128372276456111E+0 + b = 0.1835433512202753E+0 + v = 0.1674366639741759E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3441145160177973E+0 + b = 0.2128813258619585E+0 + v = 0.1724225002437900E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3749567714853510E+0 + b = 0.2422913734880829E+0 + v = 0.1765810822987288E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4052621732015610E+0 + b = 0.2716163748391453E+0 + v = 0.1800104126010751E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4349335453522385E+0 + b = 0.3007127671240280E+0 + v = 0.1827960437331284E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4638776641524965E+0 + b = 0.3294470677216479E+0 + v = 0.1850140300716308E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4920046410462687E+0 + b = 0.3576932543699155E+0 + v = 0.1867333507394938E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5192273554861704E+0 + b = 0.3853307059757764E+0 + v = 0.1880178688638289E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5454609081136522E+0 + b = 0.4122425044452694E+0 + v = 0.1889278925654758E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5706220661424140E+0 + b = 0.4383139587781027E+0 + v = 0.1895213832507346E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5946286755181518E+0 + b = 0.4634312536300553E+0 + v = 0.1898548277397420E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.1905370790924295E+0 + b = 0.2371311537781979E-1 + v = 0.1349105935937341E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2242518717748009E+0 + b = 0.4917878059254806E-1 + v = 0.1444060068369326E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2577190808025936E+0 + b = 0.7595498960495142E-1 + v = 0.1526797390930008E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2908724534927187E+0 + b = 0.1036991083191100E+0 + v = 0.1598208771406474E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3236354020056219E+0 + b = 0.1321348584450234E+0 + v = 0.1659354368615331E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3559267359304543E+0 + b = 0.1610316571314789E+0 + v = 0.1711279910946440E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3876637123676956E+0 + b = 0.1901912080395707E+0 + v = 0.1754952725601440E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4187636705218842E+0 + b = 0.2194384950137950E+0 + v = 0.1791247850802529E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4491449019883107E+0 + b = 0.2486155334763858E+0 + v = 0.1820954300877716E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4787270932425445E+0 + b = 0.2775768931812335E+0 + v = 0.1844788524548449E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5074315153055574E+0 + b = 0.3061863786591120E+0 + v = 0.1863409481706220E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5351810507738336E+0 + b = 0.3343144718152556E+0 + v = 0.1877433008795068E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5619001025975381E+0 + b = 0.3618362729028427E+0 + v = 0.1887444543705232E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5875144035268046E+0 + b = 0.3886297583620408E+0 + v = 0.1894009829375006E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6119507308734495E+0 + b = 0.4145742277792031E+0 + v = 0.1897683345035198E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2619733870119463E+0 + b = 0.2540047186389353E-1 + v = 0.1517327037467653E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.2968149743237949E+0 + b = 0.5208107018543989E-1 + v = 0.1587740557483543E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3310451504860488E+0 + b = 0.7971828470885599E-1 + v = 0.1649093382274097E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3646215567376676E+0 + b = 0.1080465999177927E+0 + v = 0.1701915216193265E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3974916785279360E+0 + b = 0.1368413849366629E+0 + v = 0.1746847753144065E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4295967403772029E+0 + b = 0.1659073184763559E+0 + v = 0.1784555512007570E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4608742854473447E+0 + b = 0.1950703730454614E+0 + v = 0.1815687562112174E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4912598858949903E+0 + b = 0.2241721144376724E+0 + v = 0.1840864370663302E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5206882758945558E+0 + b = 0.2530655255406489E+0 + v = 0.1860676785390006E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5490940914019819E+0 + b = 0.2816118409731066E+0 + v = 0.1875690583743703E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5764123302025542E+0 + b = 0.3096780504593238E+0 + v = 0.1886453236347225E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6025786004213506E+0 + b = 0.3371348366394987E+0 + v = 0.1893501123329645E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6275291964794956E+0 + b = 0.3638547827694396E+0 + v = 0.1897366184519868E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3348189479861771E+0 + b = 0.2664841935537443E-1 + v = 0.1643908815152736E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.3699515545855295E+0 + b = 0.5424000066843495E-1 + v = 0.1696300350907768E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4042003071474669E+0 + b = 0.8251992715430854E-1 + v = 0.1741553103844483E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4375320100182624E+0 + b = 0.1112695182483710E+0 + v = 0.1780015282386092E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4699054490335947E+0 + b = 0.1402964116467816E+0 + v = 0.1812116787077125E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5012739879431952E+0 + b = 0.1694275117584291E+0 + v = 0.1838323158085421E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5315874883754966E+0 + b = 0.1985038235312689E+0 + v = 0.1859113119837737E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5607937109622117E+0 + b = 0.2273765660020893E+0 + v = 0.1874969220221698E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5888393223495521E+0 + b = 0.2559041492849764E+0 + v = 0.1886375612681076E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6156705979160163E+0 + b = 0.2839497251976899E+0 + v = 0.1893819575809276E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6412338809078123E+0 + b = 0.3113791060500690E+0 + v = 0.1897794748256767E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4076051259257167E+0 + b = 0.2757792290858463E-1 + v = 0.1738963926584846E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4423788125791520E+0 + b = 0.5584136834984293E-1 + v = 0.1777442359873466E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4760480917328258E+0 + b = 0.8457772087727143E-1 + v = 0.1810010815068719E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5085838725946297E+0 + b = 0.1135975846359248E+0 + v = 0.1836920318248129E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5399513637391218E+0 + b = 0.1427286904765053E+0 + v = 0.1858489473214328E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5701118433636380E+0 + b = 0.1718112740057635E+0 + v = 0.1875079342496592E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5990240530606021E+0 + b = 0.2006944855985351E+0 + v = 0.1887080239102310E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6266452685139695E+0 + b = 0.2292335090598907E+0 + v = 0.1894905752176822E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6529320971415942E+0 + b = 0.2572871512353714E+0 + v = 0.1898991061200695E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.4791583834610126E+0 + b = 0.2826094197735932E-1 + v = 0.1809065016458791E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5130373952796940E+0 + b = 0.5699871359683649E-1 + v = 0.1836297121596799E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5456252429628476E+0 + b = 0.8602712528554394E-1 + v = 0.1858426916241869E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5768956329682385E+0 + b = 0.1151748137221281E+0 + v = 0.1875654101134641E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6068186944699046E+0 + b = 0.1442811654136362E+0 + v = 0.1888240751833503E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6353622248024907E+0 + b = 0.1731930321657680E+0 + v = 0.1896497383866979E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6624927035731797E+0 + b = 0.2017619958756061E+0 + v = 0.1900775530219121E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5484933508028488E+0 + b = 0.2874219755907391E-1 + v = 0.1858525041478814E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.5810207682142106E+0 + b = 0.5778312123713695E-1 + v = 0.1876248690077947E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6120955197181352E+0 + b = 0.8695262371439526E-1 + v = 0.1889404439064607E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6416944284294319E+0 + b = 0.1160893767057166E+0 + v = 0.1898168539265290E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6697926391731260E+0 + b = 0.1450378826743251E+0 + v = 0.1902779940661772E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6147594390585488E+0 + b = 0.2904957622341456E-1 + v = 0.1890125641731815E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6455390026356783E+0 + b = 0.5823809152617197E-1 + v = 0.1899434637795751E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6747258588365477E+0 + b = 0.8740384899884715E-1 + v = 0.1904520856831751E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + a = 0.6772135750395347E+0 + b = 0.2919946135808105E-1 + v = 0.1905534498734563E-3 + leb_tmp, start = _lebedevReccurencePoints(6, start, a, b, v, leb_tmp) + + else: + raise ValueError("Invalid degree.") + + return leb_tmp + + +def _lebedevReccurencePoints(kind, start, a, b, v, leb): + """Get points for _lebedevSphere.""" + + c = 0. + + if kind == 1: + a = 1.0 + + leb["x"][start] = a + leb["y"][start] = 0.0 + leb["z"][start] = 0.0 + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = -a + leb["y"][start+1] = 0.0 + leb["z"][start+1] = 0.0 + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = 0.0 + leb["y"][start+2] = a + leb["z"][start+2] = 0.0 + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = 0.0 + leb["y"][start+3] = -a + leb["z"][start+3] = 0.0 + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = 0.0 + leb["y"][start+4] = 0.0 + leb["z"][start+4] = a + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = 0.0 + leb["y"][start+5] = 0.0 + leb["z"][start+5] = -a + leb["w"][start+5] = 4.0 * np.pi * v + start = start+6 + + elif kind == 2: + a = np.sqrt(0.5) + + leb["x"][start] = 0.0 + leb["y"][start] = a + leb["z"][start] = a + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = 0.0 + leb["y"][start+1] = -a + leb["z"][start+1] = a + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = 0.0 + leb["y"][start+2] = a + leb["z"][start+2] = -a + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = 0.0 + leb["y"][start+3] = -a + leb["z"][start+3] = -a + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = a + leb["y"][start+4] = 0.0 + leb["z"][start+4] = a + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = a + leb["y"][start+5] = 0.0 + leb["z"][start+5] = -a + leb["w"][start+5] = 4.0 * np.pi * v + + leb["x"][start+6] = -a + leb["y"][start+6] = 0.0 + leb["z"][start+6] = a + leb["w"][start+6] = 4.0 * np.pi * v + + leb["x"][start+7] = -a + leb["y"][start+7] = 0.0 + leb["z"][start+7] = -a + leb["w"][start+7] = 4.0 * np.pi * v + + leb["x"][start+8] = a + leb["y"][start+8] = a + leb["z"][start+8] = 0.0 + leb["w"][start+8] = 4.0 * np.pi * v + + leb["x"][start+9] = -a + leb["y"][start+9] = a + leb["z"][start+9] = 0.0 + leb["w"][start+9] = 4.0 * np.pi * v + + leb["x"][start+10] = a + leb["y"][start+10] = -a + leb["z"][start+10] = 0.0 + leb["w"][start+10] = 4.0 * np.pi * v + + leb["x"][start+11] = -a + leb["y"][start+11] = -a + leb["z"][start+11] = 0.0 + leb["w"][start+11] = 4.0 * np.pi * v + start = start+12 + + elif kind == 3: + a = np.sqrt(1.0/3.0) + + leb["x"][start] = a + leb["y"][start] = a + leb["z"][start] = a + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = -a + leb["y"][start+1] = a + leb["z"][start+1] = a + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = a + leb["y"][start+2] = -a + leb["z"][start+2] = a + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = a + leb["y"][start+3] = a + leb["z"][start+3] = -a + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = -a + leb["y"][start+4] = -a + leb["z"][start+4] = a + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = a + leb["y"][start+5] = -a + leb["z"][start+5] = -a + leb["w"][start+5] = 4.0 * np.pi * v + + leb["x"][start+6] = -a + leb["y"][start+6] = a + leb["z"][start+6] = -a + leb["w"][start+6] = 4.0 * np.pi * v + + leb["x"][start+7] = -a + leb["y"][start+7] = -a + leb["z"][start+7] = -a + leb["w"][start+7] = 4.0 * np.pi * v + start = start+8 + + elif kind == 4: + # In this case A is inputed + b = np.sqrt(1.0 - 2.0 * a * a) + + leb["x"][start] = a + leb["y"][start] = a + leb["z"][start] = b + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = -a + leb["y"][start+1] = a + leb["z"][start+1] = b + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = a + leb["y"][start+2] = -a + leb["z"][start+2] = b + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = a + leb["y"][start+3] = a + leb["z"][start+3] = -b + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = -a + leb["y"][start+4] = -a + leb["z"][start+4] = b + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = -a + leb["y"][start+5] = a + leb["z"][start+5] = -b + leb["w"][start+5] = 4.0 * np.pi * v + + leb["x"][start+6] = a + leb["y"][start+6] = -a + leb["z"][start+6] = -b + leb["w"][start+6] = 4.0 * np.pi * v + + leb["x"][start+7] = -a + leb["y"][start+7] = -a + leb["z"][start+7] = -b + leb["w"][start+7] = 4.0 * np.pi * v + + leb["x"][start+8] = -a + leb["y"][start+8] = b + leb["z"][start+8] = a + leb["w"][start+8] = 4.0 * np.pi * v + + leb["x"][start+9] = a + leb["y"][start+9] = -b + leb["z"][start+9] = a + leb["w"][start+9] = 4.0 * np.pi * v + + leb["x"][start+10] = a + leb["y"][start+10] = b + leb["z"][start+10] = -a + leb["w"][start+10] = 4.0 * np.pi * v + + leb["x"][start+11] = -a + leb["y"][start+11] = -b + leb["z"][start+11] = a + leb["w"][start+11] = 4.0 * np.pi * v + + leb["x"][start+12] = -a + leb["y"][start+12] = b + leb["z"][start+12] = -a + leb["w"][start+12] = 4.0 * np.pi * v + + leb["x"][start+13] = a + leb["y"][start+13] = -b + leb["z"][start+13] = -a + leb["w"][start+13] = 4.0 * np.pi * v + + leb["x"][start+14] = -a + leb["y"][start+14] = -b + leb["z"][start+14] = -a + leb["w"][start+14] = 4.0 * np.pi * v + + leb["x"][start+15] = a + leb["y"][start+15] = b + leb["z"][start+15] = a + leb["w"][start+15] = 4.0 * np.pi * v + + leb["x"][start+16] = b + leb["y"][start+16] = a + leb["z"][start+16] = a + leb["w"][start+16] = 4.0 * np.pi * v + + leb["x"][start+17] = -b + leb["y"][start+17] = a + leb["z"][start+17] = a + leb["w"][start+17] = 4.0 * np.pi * v + + leb["x"][start+18] = b + leb["y"][start+18] = -a + leb["z"][start+18] = a + leb["w"][start+18] = 4.0 * np.pi * v + + leb["x"][start+19] = b + leb["y"][start+19] = a + leb["z"][start+19] = -a + leb["w"][start+19] = 4.0 * np.pi * v + + leb["x"][start+20] = -b + leb["y"][start+20] = -a + leb["z"][start+20] = a + leb["w"][start+20] = 4.0 * np.pi * v + + leb["x"][start+21] = -b + leb["y"][start+21] = a + leb["z"][start+21] = -a + leb["w"][start+21] = 4.0 * np.pi * v + + leb["x"][start+22] = b + leb["y"][start+22] = -a + leb["z"][start+22] = -a + leb["w"][start+22] = 4.0 * np.pi * v + + leb["x"][start+23] = -b + leb["y"][start+23] = -a + leb["z"][start+23] = -a + leb["w"][start+23] = 4.0 * np.pi * v + start = start + 24 + + elif kind == 5: + # A is inputed in this case as well + b = np.sqrt(1-a*a) + + leb["x"][start] = a + leb["y"][start] = b + leb["z"][start] = 0.0 + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = -a + leb["y"][start+1] = b + leb["z"][start+1] = 0.0 + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = a + leb["y"][start+2] = -b + leb["z"][start+2] = 0.0 + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = -a + leb["y"][start+3] = -b + leb["z"][start+3] = 0.0 + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = b + leb["y"][start+4] = a + leb["z"][start+4] = 0.0 + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = -b + leb["y"][start+5] = a + leb["z"][start+5] = 0.0 + leb["w"][start+5] = 4.0 * np.pi * v + + leb["x"][start+6] = b + leb["y"][start+6] = -a + leb["z"][start+6] = 0.0 + leb["w"][start+6] = 4.0 * np.pi * v + + leb["x"][start+7] = -b + leb["y"][start+7] = -a + leb["z"][start+7] = 0.0 + leb["w"][start+7] = 4.0 * np.pi * v + + leb["x"][start+8] = a + leb["y"][start+8] = 0.0 + leb["z"][start+8] = b + leb["w"][start+8] = 4.0 * np.pi * v + + leb["x"][start+9] = -a + leb["y"][start+9] = 0.0 + leb["z"][start+9] = b + leb["w"][start+9] = 4.0 * np.pi * v + + leb["x"][start+10] = a + leb["y"][start+10] = 0.0 + leb["z"][start+10] = -b + leb["w"][start+10] = 4.0 * np.pi * v + + leb["x"][start+11] = -a + leb["y"][start+11] = 0.0 + leb["z"][start+11] = -b + leb["w"][start+11] = 4.0 * np.pi * v + + leb["x"][start+12] = b + leb["y"][start+12] = 0.0 + leb["z"][start+12] = a + leb["w"][start+12] = 4.0 * np.pi * v + + leb["x"][start+13] = -b + leb["y"][start+13] = 0.0 + leb["z"][start+13] = a + leb["w"][start+13] = 4.0 * np.pi * v + + leb["x"][start+14] = b + leb["y"][start+14] = 0.0 + leb["z"][start+14] = -a + leb["w"][start+14] = 4.0 * np.pi * v + + leb["x"][start+15] = -b + leb["y"][start+15] = 0.0 + leb["z"][start+15] = -a + leb["w"][start+15] = 4.0 * np.pi * v + + leb["x"][start+16] = 0.0 + leb["y"][start+16] = a + leb["z"][start+16] = b + leb["w"][start+16] = 4.0 * np.pi * v + + leb["x"][start+17] = 0.0 + leb["y"][start+17] = -a + leb["z"][start+17] = b + leb["w"][start+17] = 4.0 * np.pi * v + + leb["x"][start+18] = 0.0 + leb["y"][start+18] = a + leb["z"][start+18] = -b + leb["w"][start+18] = 4.0 * np.pi * v + + leb["x"][start+19] = 0.0 + leb["y"][start+19] = -a + leb["z"][start+19] = -b + leb["w"][start+19] = 4.0 * np.pi * v + + leb["x"][start+20] = 0.0 + leb["y"][start+20] = b + leb["z"][start+20] = a + leb["w"][start+20] = 4.0 * np.pi * v + + leb["x"][start+21] = 0.0 + leb["y"][start+21] = -b + leb["z"][start+21] = a + leb["w"][start+21] = 4.0 * np.pi * v + + leb["x"][start+22] = 0.0 + leb["y"][start+22] = b + leb["z"][start+22] = -a + leb["w"][start+22] = 4.0 * np.pi * v + + leb["x"][start+23] = 0.0 + leb["y"][start+23] = -b + leb["z"][start+23] = -a + leb["w"][start+23] = 4.0 * np.pi * v + start = start + 24 + + elif kind == 6: + # both A and B are inputed in this case + c = np.sqrt(1.0 - a*a - b*b) + + leb["x"][start] = a + leb["y"][start] = b + leb["z"][start] = c + leb["w"][start] = 4.0 * np.pi * v + + leb["x"][start+1] = -a + leb["y"][start+1] = b + leb["z"][start+1] = c + leb["w"][start+1] = 4.0 * np.pi * v + + leb["x"][start+2] = a + leb["y"][start+2] = -b + leb["z"][start+2] = c + leb["w"][start+2] = 4.0 * np.pi * v + + leb["x"][start+3] = a + leb["y"][start+3] = b + leb["z"][start+3] = -c + leb["w"][start+3] = 4.0 * np.pi * v + + leb["x"][start+4] = -a + leb["y"][start+4] = -b + leb["z"][start+4] = c + leb["w"][start+4] = 4.0 * np.pi * v + + leb["x"][start+5] = a + leb["y"][start+5] = -b + leb["z"][start+5] = -c + leb["w"][start+5] = 4.0 * np.pi * v + + leb["x"][start+6] = -a + leb["y"][start+6] = b + leb["z"][start+6] = -c + leb["w"][start+6] = 4.0 * np.pi * v + + leb["x"][start+7] = -a + leb["y"][start+7] = -b + leb["z"][start+7] = -c + leb["w"][start+7] = 4.0 * np.pi * v + + leb["x"][start+8] = b + leb["y"][start+8] = a + leb["z"][start+8] = c + leb["w"][start+8] = 4.0 * np.pi * v + + leb["x"][start+9] = -b + leb["y"][start+9] = a + leb["z"][start+9] = c + leb["w"][start+9] = 4.0 * np.pi * v + + leb["x"][start+10] = b + leb["y"][start+10] = -a + leb["z"][start+10] = c + leb["w"][start+10] = 4.0 * np.pi * v + + leb["x"][start+11] = b + leb["y"][start+11] = a + leb["z"][start+11] = -c + leb["w"][start+11] = 4.0 * np.pi * v + + leb["x"][start+12] = -b + leb["y"][start+12] = -a + leb["z"][start+12] = c + leb["w"][start+12] = 4.0 * np.pi * v + + leb["x"][start+13] = b + leb["y"][start+13] = -a + leb["z"][start+13] = -c + leb["w"][start+13] = 4.0 * np.pi * v + + leb["x"][start+14] = -b + leb["y"][start+14] = a + leb["z"][start+14] = -c + leb["w"][start+14] = 4.0 * np.pi * v + + leb["x"][start+15] = -b + leb["y"][start+15] = -a + leb["z"][start+15] = -c + leb["w"][start+15] = 4.0 * np.pi * v + + leb["x"][start+16] = c + leb["y"][start+16] = a + leb["z"][start+16] = b + leb["w"][start+16] = 4.0 * np.pi * v + + leb["x"][start+17] = -c + leb["y"][start+17] = a + leb["z"][start+17] = b + leb["w"][start+17] = 4.0 * np.pi * v + + leb["x"][start+18] = c + leb["y"][start+18] = -a + leb["z"][start+18] = b + leb["w"][start+18] = 4.0 * np.pi * v + + leb["x"][start+19] = c + leb["y"][start+19] = a + leb["z"][start+19] = -b + leb["w"][start+19] = 4.0 * np.pi * v + + leb["x"][start+20] = -c + leb["y"][start+20] = -a + leb["z"][start+20] = b + leb["w"][start+20] = 4.0 * np.pi * v + + leb["x"][start+21] = c + leb["y"][start+21] = -a + leb["z"][start+21] = -b + leb["w"][start+21] = 4.0 * np.pi * v + + leb["x"][start+22] = -c + leb["y"][start+22] = a + leb["z"][start+22] = -b + leb["w"][start+22] = 4.0 * np.pi * v + + leb["x"][start+23] = -c + leb["y"][start+23] = -a + leb["z"][start+23] = -b + leb["w"][start+23] = 4.0 * np.pi * v + + leb["x"][start+24] = c + leb["y"][start+24] = b + leb["z"][start+24] = a + leb["w"][start+24] = 4.0 * np.pi * v + + leb["x"][start+25] = -c + leb["y"][start+25] = b + leb["z"][start+25] = a + leb["w"][start+25] = 4.0 * np.pi * v + + leb["x"][start+26] = c + leb["y"][start+26] = -b + leb["z"][start+26] = a + leb["w"][start+26] = 4.0 * np.pi * v + + leb["x"][start+27] = c + leb["y"][start+27] = b + leb["z"][start+27] = -a + leb["w"][start+27] = 4.0 * np.pi * v + + leb["x"][start+28] = -c + leb["y"][start+28] = -b + leb["z"][start+28] = a + leb["w"][start+28] = 4.0 * np.pi * v + + leb["x"][start+29] = c + leb["y"][start+29] = -b + leb["z"][start+29] = -a + leb["w"][start+29] = 4.0 * np.pi * v + + leb["x"][start+30] = -c + leb["y"][start+30] = b + leb["z"][start+30] = -a + leb["w"][start+30] = 4.0 * np.pi * v + + leb["x"][start+31] = -c + leb["y"][start+31] = -b + leb["z"][start+31] = -a + leb["w"][start+31] = 4.0 * np.pi * v + + leb["x"][start+32] = a + leb["y"][start+32] = c + leb["z"][start+32] = b + leb["w"][start+32] = 4.0 * np.pi * v + + leb["x"][start+33] = -a + leb["y"][start+33] = c + leb["z"][start+33] = b + leb["w"][start+33] = 4.0 * np.pi * v + + leb["x"][start+34] = a + leb["y"][start+34] = -c + leb["z"][start+34] = b + leb["w"][start+34] = 4.0 * np.pi * v + + leb["x"][start+35] = a + leb["y"][start+35] = c + leb["z"][start+35] = -b + leb["w"][start+35] = 4.0 * np.pi * v + + leb["x"][start+36] = -a + leb["y"][start+36] = -c + leb["z"][start+36] = b + leb["w"][start+36] = 4.0 * np.pi * v + + leb["x"][start+37] = a + leb["y"][start+37] = -c + leb["z"][start+37] = -b + leb["w"][start+37] = 4.0 * np.pi * v + + leb["x"][start+38] = -a + leb["y"][start+38] = c + leb["z"][start+38] = -b + leb["w"][start+38] = 4.0 * np.pi * v + + leb["x"][start+39] = -a + leb["y"][start+39] = -c + leb["z"][start+39] = -b + leb["w"][start+39] = 4.0 * np.pi * v + + leb["x"][start+40] = b + leb["y"][start+40] = c + leb["z"][start+40] = a + leb["w"][start+40] = 4.0 * np.pi * v + + leb["x"][start+41] = -b + leb["y"][start+41] = c + leb["z"][start+41] = a + leb["w"][start+41] = 4.0 * np.pi * v + + leb["x"][start+42] = b + leb["y"][start+42] = -c + leb["z"][start+42] = a + leb["w"][start+42] = 4.0 * np.pi * v + + leb["x"][start+43] = b + leb["y"][start+43] = c + leb["z"][start+43] = -a + leb["w"][start+43] = 4.0 * np.pi * v + + leb["x"][start+44] = -b + leb["y"][start+44] = -c + leb["z"][start+44] = a + leb["w"][start+44] = 4.0 * np.pi * v + + leb["x"][start+45] = b + leb["y"][start+45] = -c + leb["z"][start+45] = -a + leb["w"][start+45] = 4.0 * np.pi * v + + leb["x"][start+46] = -b + leb["y"][start+46] = c + leb["z"][start+46] = -a + leb["w"][start+46] = 4.0 * np.pi * v + + leb["x"][start+47] = -b + leb["y"][start+47] = -c + leb["z"][start+47] = -a + leb["w"][start+47] = 4.0 * np.pi * v + start = start + 48 + + else: + raise ValueError("Bad grid order") + + return leb, start diff --git a/spharpy/samplings/coordinates.py b/spharpy/samplings/coordinates.py deleted file mode 100644 index afcccafe..00000000 --- a/spharpy/samplings/coordinates.py +++ /dev/null @@ -1,469 +0,0 @@ -import numpy as np -from spharpy.samplings.helpers import sph2cart -from scipy.spatial import cKDTree -import pyfar - - -class Coordinates(object): - """Container class for coordinates in a three-dimensional space, allowing - for compact representation and convenient conversion into spherical as well - as geospatial coordinate systems. - The constructor as well as the internal representation are only - available in Cartesian coordinates. To create a Coordinates object from - a set of points in spherical coordinates, please use the - Coordinates.from_spherical() method. - - """ - def __init__(self, x=None, y=None, z=None): - """Init coordinates container - - Parameters - ---------- - x : ndarray, double - x-coordinate - y : ndarray, double - y-coordinate - z : ndarray, double - z-coordinate - """ - - super(Coordinates, self).__init__() - x = np.asarray(x, dtype=float) - y = np.asarray(y, dtype=float) - z = np.asarray(z, dtype=float) - - if not np.shape(x) == np.shape(y) == np.shape(z): - raise ValueError("Input arrays need to have same dimensions.") - - self._x = x - self._y = y - self._z = z - - @property - def x(self): - """The x-axis coordinates for each point. - """ - return self._x - - @x.setter - def x(self, value): - self._x = np.asarray(value, dtype=float) - - @property - def y(self): - """The y-axis coordinate for each point.""" - return self._y - - @y.setter - def y(self, value): - self._y = np.asarray(value, dtype=float) - - @property - def z(self): - """The z-axis coordinate for each point.""" - return self._z - - @z.setter - def z(self, value): - self._z = np.asarray(value, dtype=float) - - @property - def radius(self): - """The radius for each point.""" - return np.sqrt(self.x**2 + self.y**2 + self.z**2) - - @radius.setter - def radius(self, radius): - x, y, z = sph2cart(np.asarray(radius, dtype=float), - self.elevation, - self.azimuth) - self._x = x - self._y = y - self._z = z - - @property - def azimuth(self): - """The azimuth angle for each point.""" - return np.mod(np.arctan2(self.y, self.x), 2*np.pi) - - @azimuth.setter - def azimuth(self, azimuth): - x, y, z = sph2cart(self.radius, - self.elevation, - np.asarray(azimuth, dtype=float)) - self._x = x - self._y = y - self._z = z - - @property - def elevation(self): - """The elevation angle for each point""" - rad = self.radius - return np.arccos(self.z/rad) - - @elevation.setter - def elevation(self, elevation): - x, y, z = sph2cart(self.radius, - np.asarray(elevation, dtype=float), - self.azimuth) - self._x = x - self._y = y - self._z = z - - @classmethod - def from_cartesian(cls, x, y, z): - """Create a Coordinates class object from a set of points in the - Cartesian coordinate system. - - Parameters - ---------- - x : ndarray, double - x-coordinate - y : ndarray, double - y-coordinate - z : ndarray, double - z-coordinate - """ - return Coordinates(x, y, z) - - @classmethod - def from_spherical(cls, radius, elevation, azimuth): - """Create a Coordinates class object from a set of points in the - spherical coordinate system. - - Parameters - ---------- - radius : ndarray, double - The radius for each point - elevation : ndarray, double - The elevation angle in radians - azimuth : ndarray, double - The azimuth angle in radians - """ - radius = np.asarray(radius, dtype=float) - elevation = np.asarray(elevation, dtype=float) - azimuth = np.asarray(azimuth, dtype=float) - x, y, z = sph2cart(radius, elevation, azimuth) - return Coordinates(x, y, z) - - @classmethod - def from_array(cls, values, coordinate_system='cartesian'): - """Create a Coordinates class object from a set of points given as - numpy array - - Parameters - ---------- - values : double, ndarray - Array with shape Nx3 where N is the number of points. - coordinate_system : string - Coordinate convention of the given values. - Can be Cartesian or spherical coordinates. - """ - coords = Coordinates() - if coordinate_system == 'cartesian': - coords.cartesian = values - elif coordinate_system == 'spherical': - coords.spherical = values - else: - return ValueError("This coordinate system is not supported.") - - return coords - - @property - def latitude(self): - """The latitude angle as used in geospatial coordinates.""" - return np.pi/2 - self.elevation - - @property - def longitude(self): - """The longitude angle as used in geospatial coordinates.""" - return np.arctan2(self.y, self.x) - - @property - def cartesian(self): - """Cartesian coordinates of all points.""" - return np.vstack((self.x, self.y, self.z)) - - @cartesian.setter - def cartesian(self, value): - """Cartesian coordinates of all points.""" - self.x = value[0, :] - self.y = value[1, :] - self.z = value[2, :] - - @property - def spherical(self): - """Spherical coordinates of all points.""" - return np.vstack((self.radius, self.elevation, self.azimuth)) - - @spherical.setter - def spherical(self, value): - """Cartesian coordinates of all points.""" - x, y, z = sph2cart(value[0, :], value[1, :], value[2, :]) - self.cartesian = np.vstack((x, y, z)) - - @property - def n_points(self): - """Return number of points stored in the object""" - return self.x.size - - def merge(self, other): - """Merge another coordinates objects into this object.""" - data = np.concatenate( - (self.cartesian, other.cartesian), - axis=-1 - ) - self.cartesian = data - - def find_nearest_point(self, point): - """Find the closest Coordinate point to a given Point. - The search for the nearest point is performed using the scipy - cKDTree implementation. - - Parameters - ---------- - point : Coordinates - Point to find nearest neighboring Coordinate - - Returns - ------- - distance : ndarray, double - Distance between the point and it's closest neighbor - index : int - Index of the closest point. - - """ - kdtree = cKDTree(self.cartesian.T) - distance, index = kdtree.query(point.cartesian.T) - - return distance, index - - def __repr__(self): - """repr for Coordinate class - - """ - if self.n_points == 1: - repr_string = "Coordinates of 1 point" - else: - repr_string = "Coordinates of {} points".format(self.n_points) - return repr_string - - def __getitem__(self, index): - """Return Coordinates at index - """ - return Coordinates(self._x[index], self._y[index], self._z[index]) - - def __setitem__(self, index, item): - """Set Coordinates at index - """ - self.x[index] = item.x - self.y[index] = item.y - self.z[index] = item.z - - def __len__(self): - """Length of the object which is the number of points stored. - """ - return self.n_points - - def to_pyfar(self): - """Export to a pyfar Coordinates object. - - Returns - ------- - :doc:`pf.Coordinates ` - The equivalent pyfar class object. - """ - return pyfar.Coordinates( - self.x, - self.y, - self.z, - domain='cart', - convention='right', - unit='met') - - @classmethod - def from_pyfar(cls, coords): - """Create a spharpy Coordinates object from pyfar Coordinates. - - Parameters - ---------- - coords : :doc:`pf.Coordinates ` - A set of coordinates. - - Returns - ------- - Coordinates - The same set of coordinates. - """ - cartesian = coords.get_cart(convention='right', unit='met').T - return Coordinates(cartesian[0], cartesian[1], cartesian[2]) - - -class SamplingSphere(Coordinates): - """Class for samplings on a sphere""" - - def __init__(self, x=None, y=None, z=None, n_max=None, weights=None): - """Init for sampling class - """ - Coordinates.__init__(self, x, y, z) - self._n_max = int(n_max) if n_max is not None else None - if weights is None: - self._weights = None - else: - self.weights = weights - - @property - def n_max(self): - """Spherical harmonic order.""" - return self._n_max - - @n_max.setter - def n_max(self, value): - self._n_max = int(value) - - @property - def weights(self): - """Sampling weights for numeric integration.""" - return self._weights - - @weights.setter - def weights(self, weights): - if weights is None: - self._weights = None - return - if len(weights) != self.n_points: - raise ValueError("The number of weights has to be equal to \ - the number of sampling points.") - - weights = np.asarray(weights, dtype=float) - norm = np.linalg.norm(weights, axis=-1) - - if not np.allclose(norm, 4*np.pi): - weights *= 4*np.pi/norm - - self._weights = weights - - @classmethod - def from_coordinates(cls, coords, n_max=None, weights=None): - """Generate a spherical sampling object from a coordinates object - - Parameters - ---------- - coords : Coordinates - Coordinate object - - Returns - ------- - sampling : SamplingSphere - Sampling on a sphere - - """ - return SamplingSphere(coords.x, coords.y, coords.z, - n_max=n_max, weights=weights) - - @classmethod - def from_cartesian(cls, x, y, z, n_max=None, weights=None): - """Create a Coordinates class object from a set of points in the - Cartesian coordinate system. - - Parameters - ---------- - x : ndarray, double - x-coordinate - y : ndarray, double - y-coordinate - z : ndarray, double - z-coordinate - """ - return SamplingSphere(x, y, z, n_max, weights) - - @classmethod - def from_spherical( - cls, radius, elevation, azimuth, n_max=None, weights=None): - """Create a Coordinates class object from a set of points in the - spherical coordinate system. - - Parameters - ---------- - radius : ndarray, double - The radius for each point - elevation : ndarray, double - The elevation angle in radians - azimuth : ndarray, double - The azimuth angle in radians - """ - radius = np.asarray(radius, dtype=float) - elevation = np.asarray(elevation, dtype=float) - azimuth = np.asarray(azimuth, dtype=float) - x, y, z = sph2cart(radius, elevation, azimuth) - return SamplingSphere(x, y, z, n_max, weights) - - @classmethod - def from_array( - cls, values, n_max=None, weights=None, - coordinate_system='cartesian'): - """Create a Coordinates class object from a set of points given as - numpy array - - Parameters - ---------- - values : double, ndarray - Array with shape Nx3 where N is the number of points. - coordinate_system : string - Coordinate convention of the given values. - Can be Cartesian or spherical coordinates. - """ - coords = SamplingSphere(n_max=n_max, weights=weights) - if coordinate_system == 'cartesian': - coords.cartesian = values - elif coordinate_system == 'spherical': - coords.spherical = values - else: - return ValueError("This coordinate system is not supported.") - - return coords - - def __repr__(self): - """repr for SamplingSphere class - """ - if self.n_points == 1: - repr_string = "Sampling with {} point".format(self.n_points) - else: - repr_string = "Sampling with {} points".format(self.n_points) - return repr_string - - def to_pyfar(self): - """Export to a pyfar Coordinates object. - - Returns - ------- - :doc:`pf.Coordinates ` - The equivalent pyfar class object. - """ - pyfar_coords = super().to_pyfar() - if self.weights is not None: - pyfar_coords.weights = self.weights / np.linalg.norm(self.weights) - pyfar_coords.sh_order = self.n_max - - return pyfar_coords - - @classmethod - def from_pyfar(cls, coords): - """Create a spharpy SamplingSphere object from pyfar Coordinates. - - Parameters - ---------- - coords : :doc:`pf.Coordinates ` - A set of coordinates. - - Returns - ------- - SamplingSphere - The same set of coordinates. - """ - cartesian = coords.get_cart(convention='right', unit='met').T - spharpy_coords = SamplingSphere( - cartesian[0], cartesian[1], cartesian[2]) - spharpy_coords.weights = coords.weights - spharpy_coords.n_max = coords.sh_order - return spharpy_coords diff --git a/spharpy/samplings/helpers.py b/spharpy/samplings/helpers.py index 0dd7b79a..efa8ce46 100644 --- a/spharpy/samplings/helpers.py +++ b/spharpy/samplings/helpers.py @@ -1,125 +1,34 @@ """ -Helper functions for coordinate operations +Helper functions for coordinate operations. """ import numpy as np from scipy.spatial import cKDTree, SphericalVoronoi -def sph2cart(r, theta, phi): - """Transforms from spherical to Cartesian coordinates. - Spherical coordinates follow the common convention in Physics/Mathematics - Theta denotes the elevation angle with theta = 0 at the north pole and - theta = pi at the south pole. Phi is the azimuth angle counting from - phi = 0 at the x-axis in positive direction - (counter clockwise rotation). +def coordinates2latlon(coordinates): + r"""Transforms from Cartesian coordinates to Geocentric coordinates. .. math:: - x = r \\sin(\\theta) \\cos(\\phi), + h = \sqrt{x^2 + y^2 + z^2}, - y = r \\sin(\\theta) \\sin(\\phi), + \theta = \pi/2 - \arccos(\frac{z}{r}), - z = r \\cos(\\theta) + \phi = \arctan(\frac{y}{x}) - Parameters - ---------- - r : ndarray, number - theta : ndarray, number - phi : ndarray, number - - Returns - ------- - x : ndarray, number - y : ndarray, number - z : ndarray, number - - """ - x = r*np.sin(theta)*np.cos(phi) - y = r*np.sin(theta)*np.sin(phi) - z = r*np.cos(theta) - x = np.asarray(x) - y = np.asarray(y) - z = np.asarray(z) - x[np.abs(x) <= np.finfo(x.dtype).eps] = 0 - y[np.abs(y) <= np.finfo(y.dtype).eps] = 0 - z[np.abs(z) <= np.finfo(x.dtype).eps] = 0 - - return np.squeeze(x), np.squeeze(y), np.squeeze(z) - - -def cart2sph(x, y, z): - """ - Transforms from Cartesian to spherical coordinates. - Spherical coordinates follow the common convention in Physics/Mathematics - Theta denotes the elevation angle with theta = 0 at the north pole and - theta = pi at the south pole. Phi is the azimuth angle counting from - phi = 0 at the x-axis in positive direction - (counter clockwise rotation). - - .. math:: - - r = \\sqrt{x^2 + y^2 + z^2}, - - \\theta = \\arccos(\\frac{z}{r}), - - \\phi = \\arctan(\\frac{y}{x}) + -\pi/2 < \theta < \pi/2, - 0 < \\theta < \\pi, + -\pi < \phi < \pi - 0 < \\phi < 2 \\pi - - - Notes - ----- - To ensure proper handling of the radiant for the azimuth angle, the arctan2 - implementatition from numpy is used here. + where :math:`h` is the height, :math:`\theta` is the latitude angle + and :math:`\phi` is the longitude angle Parameters ---------- - x : ndarray, number - y : ndarray, number - z : ndarray, number - - Returns - ------- - r : ndarray, number - theta : ndarray, number - phi : ndarray, number - - """ - rad = np.sqrt(x**2 + y**2 + z**2) - theta = np.arccos(z/rad) - phi = np.mod(np.arctan2(y, x), 2*np.pi) - return rad, theta, phi - - -def cart2latlon(x, y, z): - """Transforms from Cartesian coordinates to Geocentric coordinates - - .. math:: - - h = \\sqrt{x^2 + y^2 + z^2}, - - \\theta = \\pi/2 - \\arccos(\\frac{z}{r}), - - \\phi = \\arctan(\\frac{y}{x}) - - -\\pi/2 < \\theta < \\pi/2, - - -\\pi < \\phi < \\pi - - where :math:`h` is the heigth, :math:`\\theta` is the latitude angle - and :math:`\\phi` is the longitude angle - - Parameters - ---------- - x : ndarray, number - x-axis coordinates - y : ndarray, number - y-axis coordinates - z : ndarray, number - z-axis coordinates + coordinates : ndarray, number + Coordinates Object which cartesian coordinates are taken to convert to + Geocentric coordinates Returns ------- @@ -131,57 +40,15 @@ def cart2latlon(x, y, z): Geocentric longitude angle """ + x = coordinates.x + y = coordinates.y + z = coordinates.z height = np.sqrt(x**2 + y**2 + z**2) latitude = np.pi/2 - np.arccos(z/height) longitude = np.arctan2(y, x) return height, latitude, longitude -def latlon2cart(height, latitude, longitude): - """Transforms from Geocentric coordinates to Cartesian coordinates - - .. math:: - - x = h \\cos(\\theta) \\cos(\\phi), - - y = h \\cos(\\theta) \\sin(\\phi), - - z = h \\sin(\\theta) - - -\\pi/2 < \\theta < \\pi/2, - - -\\pi < \\phi < \\pi - - where :math:`h` is the heigth, :math:`\\theta` is the latitude angle - and :math:`\\phi` is the longitude angle - - Parameters - ---------- - height : ndarray, number - The radius is rendered as height information - latitude : ndarray, number - Geocentric latitude angle - longitude : ndarray, number - Geocentric longitude angle - - Returns - ------- - x : ndarray, number - x-axis coordinates - y : ndarray, number - y-axis coordinates - z : ndarray, number - z-axis coordinates - - """ - - x = height * np.cos(latitude) * np.cos(longitude) - y = height * np.cos(latitude) * np.sin(longitude) - z = height * np.sin(latitude) - - return x, y, z - - def spherical_voronoi(sampling, round_decimals=13, center=0.0): """Calculate a Voronoi diagram on the sphere for the given samplings points. @@ -191,9 +58,9 @@ def spherical_voronoi(sampling, round_decimals=13, center=0.0): sampling : SamplingSphere Sampling points on a sphere round_decimals : int - Number of decimals to be rounded to. + Number of decimals to be rounded to. Default is ``13``. center : double - Center point of the voronoi diagram. + Center point of the voronoi diagram. Default is ``0.0``. Returns ------- @@ -201,7 +68,8 @@ def spherical_voronoi(sampling, round_decimals=13, center=0.0): Spherical voronoi diagram as implemented in scipy. """ - points = sampling.cartesian.T + points = sampling.cartesian + points = points if points.shape[-1] == 3 else points.T radius = np.unique(np.round(sampling.radius, decimals=round_decimals)) if len(radius) > 1: raise ValueError("All sampling points need to be on the \ @@ -219,9 +87,9 @@ def calculate_sampling_weights(sampling, round_decimals=12): sampling : SamplingSphere Sampling points on a sphere round_decimals : int, optional - - apply : boolean, optional - Whether or not to store the weights into the class object + Decimal precision used for ``sampling.radius``. All radii must be + identical to compute the sampling weights. This can be used to ignore + numerical noise on the radii. The default is ``12``. Returns ------- @@ -238,7 +106,10 @@ def calculate_sampling_weights(sampling, round_decimals=12): return_index=True) searchtree = cKDTree(unique_verts) - area = np.zeros(sampling.n_points, float) + if hasattr(sampling, 'csize'): + area = np.zeros(sampling.csize, float) + else: + area = np.zeros(sampling.n_points, float) for idx, region in enumerate(sv.regions): _, idx_nearest = searchtree.query(sv.vertices[np.array(region)]) diff --git a/spharpy/samplings/interior.py b/spharpy/samplings/interior.py index 294f67e8..01cdc514 100644 --- a/spharpy/samplings/interior.py +++ b/spharpy/samplings/interior.py @@ -1,14 +1,15 @@ +"""Calculation of interior stabilization points for open arrays.""" import numpy as np import scipy.special as spspecial -from spharpy.special import spherical_bessel_zeros -from spharpy.samplings import Coordinates +from spharpy.special import spherical_bessel_zeros, spherical_harmonic +import pyfar as pf def interior_stabilization_points(kr_max, resolution_factor=1): - """ Find points inside the interior domain of an open spherical microphone - array that stabilize the array array response at the eigenfrequencies of - the array. The algorithm is based on [7]_ and implemented following the - Matlab code provided by Gilles Chardon on his homepage at [8]_. + """Find stabilization points inside for an open spherical microphone array. + The interior points stabilize the array response at the eigenfrequencies of + the array. The algorithm is based on [#]_ and implemented following the + Matlab code provided by Gilles Chardon on his homepage at [#]_. The stabilization points are independent of the sampling of the sphere and can therefore be combined with arbitrary spherical samplings. @@ -19,7 +20,7 @@ def interior_stabilization_points(kr_max, resolution_factor=1): the upper frequency limit of the array. resolution_factor : int Factor to increase the spatial resolution of the grid - used to estimate the stabilization points. + used to estimate the stabilization points. Default is ``1``. Returns ------- @@ -28,15 +29,15 @@ def interior_stabilization_points(kr_max, resolution_factor=1): References ---------- - .. [7] G. Chardon, W. Kreuzer, und M. Noisternig, "Design of spatial + .. [#] G. Chardon, W. Kreuzer, und M. Noisternig, "Design of spatial microphone arrays for sound field interpolation", IEEE Journal of Selected Topics in Signal Processing - .. [8] https://gilleschardon.fr/jstsp_array/ + .. [#] https://gilleschardon.fr/jstsp_array/ """ x, y, z = find_interior_points(kr_max, resolution_factor=resolution_factor) - return Coordinates(x, y, z) + return pf.Coordinates(x, y, z) def find_eigenfrequencies(kr_max): @@ -60,11 +61,11 @@ def find_eigenfrequencies(kr_max): def calculate_eigenspaces(kr_max, theta, phi, rad): """Calculate the eigenspaces for the corresponding eigenfrequencies of - the sphere + the sphere. Parameters ---------- - k_max : float + kr_max : float The largest wave number to be included theta : array, float Azimuth angle @@ -123,13 +124,41 @@ def sph_modes_matrix(n_max, k, theta, phi, rad): B = np.reshape(B, meshgrid_shape) M = np.zeros((*meshgrid_shape, n_coefficients), dtype=complex) for m in range(-n_max, n_max+1): - Y_m = spspecial.sph_harm(m, n_max, theta.flatten(), phi.flatten()) + Y_m = spherical_harmonic(n_max, m, phi.flatten(), theta.flatten()) M[:, :, :, m+n_max] = B * np.reshape(Y_m, meshgrid_shape) return M def ball_dot(S1, S2, radius, phi): + """ + Calculate the dot product of two spherical harmonic modes on a sphere. + + The dot product is defined as the integral over the sphere of the + product of the two modes multiplied by a weighting function. + The weighting function is defined as the square of the radius times the + sine of the elevation angle. + The weighting function is used to account for the spherical geometry. + The integral is approximated by a sum over the spherical coordinates. + The function is used to calculate the orthogonality of the spherical + harmonic modes. + + Parameters + ---------- + S1 : array, complex + The first spherical harmonic mode. + S2 : array, complex + The second spherical harmonic mode. + radius : array, float + The radius of the sphere. + phi : array, float + The elevation angle of the sphere. + + Returns + ------- + float + The dot product of the two spherical harmonic modes. + """ wphi = np.sin(phi) wr = radius**2 w = wr*wphi @@ -137,6 +166,35 @@ def ball_dot(S1, S2, radius, phi): def find_interior_points(k_max, resolution_factor=1): + """Find the interior points of an open spherical microphone array. + + The interior points stabilize the array response at the eigenfrequencies of + the array. The algorithm is based on [#]_ and implemented following the + Matlab code provided by Gilles Chardon on his homepage at [#]_. + The stabilization points are independent of the sampling of the sphere + and can therefore be combined with arbitrary spherical samplings. + + Parameters + ---------- + k_max : float + The maximum kr value to be considered. This will define + the upper frequency limit of the array. + resolution_factor : int + Factor to increase the spatial resolution of the grid + used to estimate the stabilization points. + + Returns + ------- + x, y, z : ndarray, float + Coordinates of the stabilization points + + References + ---------- + .. [#] G. Chardon, W. Kreuzer, und M. Noisternig, "Design of spatial + microphone arrays for sound field interpolation", IEEE Journal of + Selected Topics in Signal Processing + .. [#] https://gilleschardon.fr/jstsp_array/ + """ resolution = 50 * resolution_factor vec_theta = np.linspace(0, 2 * np.pi, resolution*2) diff --git a/spharpy/samplings/samplings.py b/spharpy/samplings/samplings.py index 58ed0ce3..b6a7f5b8 100644 --- a/spharpy/samplings/samplings.py +++ b/spharpy/samplings/samplings.py @@ -1,31 +1,66 @@ """ -Collection of sampling schemes for the sphere +Collection of sampling schemes for the sphere. """ -import urllib3 +import os +from urllib3.exceptions import InsecureRequestWarning import numpy as np -from spharpy.samplings.coordinates import Coordinates, SamplingSphere +from pyfar import Coordinates import spharpy +import warnings +import scipy.io as sio +import requests +from multiprocessing.pool import ThreadPool from ._eqsp import point_set as eq_point_set +from ._eqsp import lebedev_sphere -def cube_equidistant(n_points): - """Create a cuboid sampling with equidistant spacings in x, y, and z. - The cube will have dimensions 1 x 1 x 1 +def equidistant_cuboid(n_points, flatten_output=True): + """ + Create a cubic sampling with equidistant spacings in x, y, and z. + + The cuboid spans from -1 m to 1 m along each axis and is centered at + the origin. Parameters ---------- n_points : int, tuple Number of points in the sampling. If a single value is given, the - number of sampling positions will be the same in every axis. If a tuple - is given, the number of points will be set as (n_x, n_y, n_z) + number of sampling positions will be the same in every axis. If a + tuple is given, the number of points will be set as + ``(n_x, n_y, n_z)``. + flatten_output : bool, optional + Whether to flatten the output Coordinates object or not. + The default is ``False``. Returns ------- - sampling : Coordinates - Sampling positions as Coordinate object + sampling : :py:class:`pyfar.Coordinates` + Sampling positions as Coordinate object with cshape + ``(n_x, n_y, n_z)`` if `flatten_output` is ``False``, otherwise + with cshape ``(n_x*n_y*n_z, )``. + Does not contain sampling weights. + + Examples + -------- + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.equidistant_cuboid(3) + >>> sp.plot.scatter(coords) """ + if not isinstance(flatten_output, bool): + raise ValueError("flatten_output must be a boolean.") + try: + n_points = np.asarray(n_points) + assert (n_points > 0).all() + assert (n_points % 1 == 0).all() + n_points = n_points.astype(int) + except Exception as e: + raise ValueError( + "The number of points needs to be either an integer " + "or a tuple with 3 elements.") from e if np.size(n_points) == 1: n_x = n_points n_y = n_points @@ -42,299 +77,558 @@ def cube_equidistant(n_points): y = np.linspace(-1, 1, n_y) z = np.linspace(-1, 1, n_z) - x_grid, y_grid, z_grid = np.meshgrid(x, y, z) + x_grid, y_grid, z_grid = np.meshgrid(x, y, z, indexing='ij') - return Coordinates(x_grid.flatten(), y_grid.flatten(), z_grid.flatten()) + if flatten_output: + x_grid = x_grid.flatten() + y_grid = y_grid.flatten() + z_grid = z_grid.flatten() + return Coordinates(x_grid, y_grid, z_grid) -def hyperinterpolation(n_max): - """Gives the points of a Hyperinterpolation sampling grid - after Sloan and Womersley [1]_. - Notes - ----- - This implementation uses precalculated sets of points which are downloaded - from Womersley's homepage [2]_. +def hyperinterpolation(n_max, radius=1.): + r""" + Return a Hyperinterpolation sampling grid. - References - ---------- - .. [1] I. H. Sloan and R. S. Womersley, “Extremal Systems of Points and - Numerical Integration on the Sphere,” Advances in Computational - Mathematics, vol. 21, no. 1/2, pp. 107–125, 2004. - .. [2] http://web.maths.unsw.edu.au/~rsw/Sphere/Extremal/New/index.html + After Sloan and Womersley [#]_. The samplings are available for + spherical harmonics orders :math:`1 \leq n_\text{max} \leq 200`. The number + of points in the sampling grid equals :math:`(n_\text{max} + 1)^2`. Parameters ---------- - n_max : integer - Spherical harmonic order of the sampling + n_max : int + Maximum applicable spherical harmonic order. It must be + between ``1`` and ``200``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. Returns ------- - sampling: SamplingSphere - SamplingSphere object containing all sampling points - """ - n_sh = (n_max+1)**2 - filename = "md%03d.%05d" % (n_max, n_sh) - url = "https://web.maths.unsw.edu.au/~rsw/Sphere/S2Pts/MD/" - fileurl = url + filename + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions including sampling weights, with + ``(n_max + 1)**2`` points. - http = urllib3.PoolManager(cert_reqs=False) - http_data = http.urlopen('GET', fileurl) + Notes + ----- + This implementation uses precalculated sets of points from [#]_. The data + up to ``n_max = 20`` are loaded the first time this function is called. + The remaining data is loaded upon request. - if http_data.status == 200: - file_data = http_data.data.decode() - else: - raise ConnectionError("Connection error. Please check your internet \ - connection.") + References + ---------- + .. [#] I. H. Sloan and R. S. Womersley, “Extremal Systems of Points and + Numerical Integration on the Sphere,” Advances in Computational + Mathematics, vol. 21, no. 1/2, pp. 107-125, 2004. + .. [#] https://web.maths.unsw.edu.au/~rsw/Sphere/MaxDet/ + + Examples + -------- + + .. plot:: + >>> import spharpy as sp + >>> coords = sp.samplings.hyperinterpolation(n_max=3) + >>> sp.plot.scatter(coords) + + """ + # check inputs + if not isinstance(n_max, int) or n_max < 1 or n_max > 200: + raise ValueError('n_max must be an integer between 1 and 200') + if not isinstance( + radius, (int, float)) or np.size(radius) > 1 or (radius <= 0): + raise ValueError('radius must be a single positive value') + + # calculate number of points + n_points = (n_max + 1)**2 + + # download data if necessary + filename = "samplings_extremal_md%03d.%05d" % (n_max, n_points) + filename = os.path.join(os.path.dirname(__file__), "_eqsp", filename) + if not os.path.exists(filename): + if n_max < 21: + _sph_extremal_load_data(np.arange(1, 21)) + else: + _sph_extremal_load_data(n_max) + + # open data + with open(filename, 'rb') as f: + file_data = f.read() + + # format data + file_data = file_data.decode() file_data = np.fromstring( - file_data, dtype='double', sep=' ').reshape((n_sh, 4)) - sampling = SamplingSphere( - file_data[:, 0], - file_data[:, 1], - file_data[:, 2]) - sampling.weights = file_data[:, 3] + file_data, + dtype=float, count=int(n_points)*4, + sep=' ').reshape((int(n_points), 4)) + + # normalize weights + weights = file_data[:, 3] + + # generate Coordinates object + sampling = spharpy.SamplingSphere( + file_data[:, 0] * radius, + file_data[:, 1] * radius, + file_data[:, 2] * radius, + n_max=n_max, weights=weights, + comment='extremal spherical sampling grid') return sampling -def spherical_t_design(n_max, criterion='const_energy'): - r"""Return the sampling positions for a spherical t-design [3]_ . - For a spherical harmonic order N, a t-Design of degree `:math: t=2N` for - constant energy or `:math: t=2N+1` additionally ensuring a constant angular - spread of energy is required [4]_. For a given degree t +def t_design(degree=None, n_max=None, criterion='const_energy', + radius=1.): + r""" + Return spherical t-design sampling grid. + + For detailed information, see [#]_. + For a spherical harmonic order :math:`n_{sh}`, a t-Design of degree + :math:`t=2n_{sh}` for constant energy or :math:`t=2n_{sh}+1` additionally + ensuring a constant angular spread of energy is required [#]_. For a given + degree t .. math:: L = \lceil \frac{(t+1)^2}{2} \rceil+1, points will be generated, except for t = 3, 5, 7, 9, 11, 13, and 15. - T-designs allow for a inverse spherical harmonic transform matrix - calculated as `:math: D = \frac{4\pi}{L} \mathbf{Y}^\mathrm{H}`. + T-designs allow for an inverse spherical harmonic transform matrix + calculated as :math:`D = \frac{4\pi}{L} \mathbf{Y}^\mathrm{H}` with + :math:`\mathbf{Y}^\mathrm{H}` being the hermitian transpose of the + spherical harmonics matrix. Parameters ---------- - degree : integer - T-design degree - criterion : 'const_energy', 'const_angular_spread' + degree : int + T-design degree between ``1`` and ``180``. Either `degree` or + `n_max` must be provided. The default is ``None``. + n_max : int + Maximum applicable spherical harmonic order. Related to the degree + by ``degree = 2 * n_max`` (``const_energy``) and + ``degree = 2 * n_max + 1`` (``const_angular_spread``). Either + `degree` or `n_max` must be provided. The default is ``None``. + criterion : ``const_energy``, ``const_angular_spread`` Design criterion ensuring only a constant energy or additionally - constant angular spread of energy + constant angular spread of energy. The default is ``const_energy``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. Returns ------- - sampling : SamplingSphere - SamplingSphere object containing all sampling points + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions. Sampling weights can be obtained from + :py:func:`calculate_sampling_weights`. Notes ----- - This function downloads a pre-calculated set of points from - Rob Womersley's homepage [5]_ . + This function downloads a pre-calculated set of points from [#]_ . The data + up to ``degree = 20`` are loaded the first time this function is called. + The remaining data is loaded upon request. References ---------- - .. [3] C. An, X. Chen, I. H. Sloan, and R. S. Womersley, “Well Conditioned + .. [#] C. An, X. Chen, I. H. Sloan, and R. S. Womersley, “Well Conditioned Spherical Designs for Integration and Interpolation on the Two-Sphere,” SIAM Journal on Numerical Analysis, vol. 48, no. 6, - pp. 2135–2157, Jan. 2010. - .. [4] F. Zotter, M. Frank, and A. Sontacchi, “The Virtual T-Design + pp. 2135-2157, Jan. 2010. + .. [#] F. Zotter, M. Frank, and A. Sontacchi, “The Virtual T-Design Ambisonics-Rig Using VBAP,” in Proceedings on the Congress on Sound and Vibration, 2010. - .. [5] http://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html + .. [#] http://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.t_design(n_max=3) + >>> sp.plot.scatter(coords) """ - if criterion == 'const_energy': - degree = 2*n_max - elif criterion == 'const_angular_spread': - degree = 2*n_max + 1 + + # check input + if (degree is None) and (n_max is None): + print('Possible input values:') + for d in range(1, 181): + print(f"degree {d}, n_max {int(d / 2)} ('const_energy'), \ + {int((d - 1) / 2)} ('const_angular_spread')") + return None + + if criterion not in ['const_energy', 'const_angular_spread']: + raise ValueError("Invalid design criterion. Must be 'const_energy' \ + or 'const_angular_spread'.") + + if degree is not None and n_max is not None: + raise ValueError("Either n_points or n_max must be None.") + + # get the degree + if degree is None: + if criterion == 'const_energy': + degree = 2 * n_max + else: + degree = 2 * n_max + 1 + # get the SH order for the meta data entry in the Coordinates object else: - raise ValueError("Invalid design criterion.") + if criterion == 'const_energy': + n_max = int(degree / 2) + else: + n_max = int((degree - 1) / 2) + + if degree < 1 or degree > 180: + raise ValueError('degree must be between 1 and 180.') + # get the number of points n_points = int(np.ceil((degree + 1)**2 / 2) + 1) n_points_exceptions = {3: 8, 5: 18, 7: 32, 9: 50, 11: 72, 13: 98, 15: 128} if degree in n_points_exceptions: n_points = n_points_exceptions[degree] - filename = "sf%03d.%05d" % (degree, n_points) - url = "http://web.maths.unsw.edu.au/~rsw/Sphere/Points/SF/SF29-Nov-2012/" - fileurl = url + filename - - http = urllib3.PoolManager( - cert_reqs=False) - http_data = http.urlopen('GET', fileurl) - - if http_data.status == 200: - file_data = http_data.data.decode() - elif http_data.status == 404: - raise FileNotFoundError("File was not found. Check if the design you \ - are trying to calculate is a valid t-design.") - else: - raise ConnectionError("Connection error. Please check your internet \ - connection.") - + # download data if necessary + filename = "samplings_t_design_sf%03d.%05d" % (degree, n_points) + filename = os.path.join(os.path.dirname(__file__), "_eqsp", filename) + if not os.path.exists(filename): + if degree < 21: + _sph_t_design_load_data(np.arange(1, 21)) + else: + _sph_t_design_load_data(degree) + + # open data + with open(filename, 'rb') as f: + file_data = f.read() + + # format data + file_data = file_data.decode() points = np.fromstring( file_data, - dtype=float, - sep=' ').reshape((n_points, 3)).T + dtype=np.double, + sep=' ').reshape((n_points, 3)) - return SamplingSphere.from_array(points) + # generate Coordinates object + sampling = spharpy.SamplingSphere( + points[..., 0] * radius, + points[..., 1] * radius, + points[..., 2] * radius, + n_max=n_max) + return sampling -def dodecahedron(): - """Generate a sampling based on the center points of the twelve + +def dodecahedron(radius=1.): + """ + Generate a sampling based on the center points of the twelve dodecahedron faces. + Parameters + ---------- + radius : number, optional + Radius of the sampling grid. The default is ``1``. + Returns ------- - rad : ndarray - Radius of the sampling points - theta : ndarray - Elevation angle in the range [0, pi] - phi : ndarray - Azimuth angle in the range [0, 2 pi] - Returns - ------- - sampling : SamplingSphere - SamplingSphere object containing all sampling points + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions. Sampling weights can be obtained from + :py:func:`calculate_sampling_weights`. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.dodecahedron() + >>> sp.plot.scatter(coords) """ - dihedral = 2*np.arcsin(np.cos(np.pi/3)/np.sin(np.pi/5)) - R = np.tan(np.pi/3)*np.tan(dihedral/2) - rho = np.cos(np.pi/5)/np.sin(np.pi/10) + dihedral = 2 * np.arcsin(np.cos(np.pi / 3) / np.sin(np.pi / 5)) + R = np.tan(np.pi / 3) * np.tan(dihedral / 2) + rho = np.cos(np.pi / 5) / np.sin(np.pi / 10) - theta1 = np.arccos((np.cos(np.pi/5)/np.sin(np.pi/5))/np.tan(np.pi/3)) + theta1 = np.arccos((np.cos(np.pi / 5) + / np.sin(np.pi / 5)) + / np.tan(np.pi / 3)) - a2 = 2*np.arccos(rho/R) + a2 = 2 * np.arccos(rho / R) - theta2 = theta1+a2 + theta2 = theta1 + a2 theta3 = np.pi - theta2 theta4 = np.pi - theta1 phi1 = 0 - phi2 = 2*np.pi/3 - phi3 = 4*np.pi/3 + phi2 = 2 * np.pi / 3 + phi3 = 4 * np.pi / 3 theta = np.concatenate(( - np.tile(theta1, 3), np.tile(theta2, 3), - np.tile(theta3, 3), np.tile(theta4, 3))) + np.tile(theta1, 3), + np.tile(theta2, 3), + np.tile(theta3, 3), + np.tile(theta4, 3))) phi = np.tile(np.array([ - phi1, phi2, phi3, - phi1 + np.pi/3, phi2 + np.pi/3, phi3 + np.pi/3]), 2) + phi1, + phi2, + phi3, + phi1 + np.pi / 3, + phi2 + np.pi / 3, + phi3 + np.pi / 3]), 2) + rad = radius * np.ones(np.size(theta)) - rad = np.ones(np.size(theta)) + return spharpy.SamplingSphere.from_spherical_colatitude( + phi, theta, rad) - return SamplingSphere.from_spherical(rad, theta, phi) +def icosahedron(radius=1.): + """ + Generate a sampling from the center points of the twenty icosahedron faces. -def icosahedron(): - """Generate a sampling based on the center points of the twenty \ - icosahedron faces. + Parameters + ---------- + radius : number, optional + Radius of the sampling grid. The default is ``1``. Returns ------- - sampling : SamplingSphere - SamplingSphere object containing all sampling points + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions. Sampling weights can be obtained from + :py:func:`calculate_sampling_weights`. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.icosahedron() + >>> sp.plot.scatter(coords) + """ gamma_R_r = np.arccos(np.cos(np.pi/3) / np.sin(np.pi/5)) gamma_R_rho = np.arccos(1/(np.tan(np.pi/5) * np.tan(np.pi/3))) theta = np.tile(np.array([np.pi - gamma_R_rho, - np.pi - gamma_R_rho - 2*gamma_R_r, - 2*gamma_R_r + gamma_R_rho, + np.pi - gamma_R_rho - 2 * gamma_R_r, + 2 * gamma_R_r + gamma_R_rho, gamma_R_rho]), 5) theta = np.sort(theta) - phi = np.arange(0, 2*np.pi, 2*np.pi/5) - phi = np.concatenate((np.tile(phi, 2), np.tile(phi + np.pi/5, 2))) - rad = np.ones(20) + phi = np.arange(0, 2 * np.pi, 2 * np.pi / 5) + phi = np.concatenate((np.tile(phi, 2), np.tile(phi + np.pi / 5, 2))) - return SamplingSphere.from_spherical(rad, theta, phi) + return spharpy.SamplingSphere.from_spherical_colatitude( + phi, theta, radius) -def equiangular(n_max): - """Generate an equiangular sampling of the sphere. +def equiangular(n_points=None, n_max=None, radius=1.): + r""" + Generate an equiangular sampling of the sphere. - Paramters - --------- - n_max : integer - Spherical harmonic order of the sampling + For detailed information, see [#]_, Chapter 3.2. This is a quadrature + sampling with the sum of the sampling weights in `sampling.weights` + being :math:`4\pi` if the number of rings is even and the number of + points in azimuth and elevation are equal. This condition is always + fulfilled if the number of points is chosen through ``n_max``. + This sampling does not contain points at the North and South Pole and is + typically used for spherical harmonics processing. See + :py:func:`equal_angle` and :py:func:`great_circle` for samplings + containing points at the poles. + + Parameters + ---------- + n_points : int, tuple of two ints + Number of sampling points in azimuth and elevation. Either `n_points` + or `n_max` must be provided. The default is ``None``. + n_max : int + Maximum applicable spherical harmonic order. If this is provided, + 'n_points' is set to ``2 * n_max + 1``. Either `n_points` or + `n_max` must be provided. The default is ``None``. + radius : number, optional + Radius of the sampling grid. The default is ``1``. Returns ------- - sampling : SamplingSphere - SamplingSphere object containing all sampling points + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions including sampling weights. + + References + ---------- + .. [#] B. Rafaely, Fundamentals of spherical array processing, 1st ed. + Berlin, Heidelberg, Germany: Springer, 2015. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.equiangular(n_max=3) + >>> sp.plot.scatter(coords) """ - n_theta = np.round((n_max+1)*2) - n_phi = n_theta - theta_angles = np.arange(np.pi/(n_theta*2), - np.pi, - np.pi/n_theta) - phi_angles = np.arange(0, - 2*np.pi, - 2*np.pi/n_phi) + if (n_points is None) and (n_max is None): + raise ValueError( + "Either the n_points or n_max needs to be specified.") + + # get number of points from required spherical harmonic order + # ([#], equation 3.4) + if n_max is not None: + n_points = 2 * (int(n_max) + 1) + + # get the angles + n_points = np.asarray(n_points) + if n_points.size == 2: + n_phi = n_points[0] + n_theta = n_points[1] + else: + n_phi = n_points + n_theta = n_points + + theta_angles = np.arange(np.pi / (n_theta * 2), np.pi, np.pi / n_theta) + phi_angles = np.arange(0, 2 * np.pi, 2 * np.pi / n_phi) + + # construct the sampling grid theta, phi = np.meshgrid(theta_angles, phi_angles) - rad = np.ones(theta.size) - - # calculate weights - L = 2*np.arange(0, n_max + 1) + 1 - factor_phi = 2*np.pi/n_phi - factor_theta = 2/n_theta - factor_sin = 4/np.pi * np.sin(theta_angles) * \ - (1/L @ L[np.newaxis].T @ theta_angles[np.newaxis]) - weights = np.tile(factor_phi * factor_theta * np.pi/2 * factor_sin, n_phi) - - sampling = SamplingSphere.from_spherical( - rad, theta.reshape(-1), phi.reshape(-1)) - sampling.weights = weights + rad = radius * np.ones(theta.size) + + # compute maximum applicable spherical harmonic order + if n_max is None: + n_max = int(np.min([n_phi / 2 - 1, n_theta / 2 - 1])) + else: + n_max = int(n_max) + + # compute sampling weights ([1], equation 3.11) + # Note that this is only valid if the number of points in ring is even + # and the number of points in azimuth and elevation are equal + if (n_theta != n_phi) or np.any(np.mod(n_points, 2) > 0): + weights = None + else: + q = 2 * np.arange(0, n_max + 1) + 1 + weights_theta = np.sin(theta_angles) * ( + 1/q @ np.sin(q[np.newaxis].T @ theta_angles[np.newaxis])) + + weights = np.tile(weights_theta*2*np.pi / (n_max+1)**2, n_phi) + + # make Coordinates object + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + phi.reshape(-1), theta.reshape(-1), rad, + weights=weights, n_max=n_max) return sampling -def gaussian(n_max): - """Generate sampling of the sphere based on the Gaussian quadrature. +def gaussian(n_points=None, n_max=None, radius=1.): + r""" + Generate sampling of the sphere based on the Gaussian quadrature. + + For detailed information, see [#]_ (Section 3.3). This is a quadrature + sampling with the sum of the sampling weights in `sampling.weights` + being :math:`4\pi`. + This sampling does not contain points at the North and South Pole and is + typically used for spherical harmonics processing. See + :py:func:`equal_angle` and :py:func:`great_circle` for samplings + containing points at the poles. - Paramters - --------- - n_max : integer - Spherical harmonic order of the sampling + Parameters + ---------- + n_points : int + Number of sampling points in elevation. The number of points in + azimuth is always ``2*n_points``. Either `n_points` + or `n_max` must be provided. The default is ``None``. + n_max : int + Maximum applicable spherical harmonic order. If this is provided, + `n_points` is set to ``(2 * (n_max + 1), n_max + 1)``. Either + `n_points` or `n_max` must be provided. The default is ``None``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. Returns ------- - sampling : SamplingSphere - SamplingSphere object containing all sampling points + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions including sampling weights. + + References + ---------- + .. [#] B. Rafaely, Fundamentals of spherical array processing, 1st ed. + Berlin, Heidelberg, Germany: Springer, 2015. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.gaussian(n_max=3) + >>> sp.plot.scatter(coords) """ + if (n_points is None) and (n_max is None): + raise ValueError( + "Either the n_points or n_max needs to be specified.") + + elif (n_points is not None) and (n_max is None): + if ( + not isinstance(n_points, (int, np.integer)) or + (np.asarray(n_points).size > 1) + ): + raise ValueError( + "The number of points needs to be a positive natural number. ", + f"Instead it is {n_points}", + ) + + # get number of points from required spherical harmonic order + # ([1], chapter 3.3) + if n_max is not None: + n_phi = int(n_max+1)*2 + n_theta = int(n_max) + 1 + else: + n_theta = n_points + n_phi = 2*n_points + + # compute the maximum applicable spherical harmonic order + if n_max is None: + n_max = int(np.min([n_phi / 2 - 1, n_theta - 1])) + else: + n_max = int(n_max) + + # construct the sampling grid legendre, weights = np.polynomial.legendre.leggauss(n_max+1) theta_angles = np.arccos(legendre) - n_phi = np.round((n_max+1)*2) - phi_angles = np.arange(0, - 2*np.pi, - 2*np.pi/n_phi) + + phi_angles = np.arange(0, 2 * np.pi, 2 * np.pi / n_phi) theta, phi = np.meshgrid(theta_angles, phi_angles) - rad = np.ones(theta.size) + + # compute the sampling weights weights = np.tile(weights*np.pi/(n_max+1), 2*(n_max+1)) - sampling = SamplingSphere.from_spherical( - rad, theta.reshape(-1), phi.reshape(-1)) - sampling.weights = weights + # make Coordinates object + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + phi.reshape(-1), theta.reshape(-1), radius, + weights=weights, n_max=n_max) + return sampling def eigenmike_em32(): - """Microphone positions of the Eigenmike em32 by mhacoustics according to - the Eigenstudio user manual on the homepage [6]_. - - References - ---------- - .. [6] Eigenstudio User Manual, https://mhacoustics.com/download + """Microphone positions of the Eigenmike em32 by mhacoustics. + The data are according to the Eigenstudio user manual on the homepage [#]_. Returns ------- - sampling : SamplingSphere + sampling : :py:class:`spharpy.SamplingSphere` SamplingSphere object containing all sampling points - """ + References + ---------- + .. [#] Eigenstudio User Manual, https://eigenmike.com/sites/default/files/documentation-2023-10/EigenStudio%20User%20Manual%20R02D.pdf + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.eigenmike_em32() + >>> sp.plot.scatter(coords) + """ # noqa: E501 rad = np.ones(32) theta = np.array([69.0, 90.0, 111.0, 90.0, 32.0, 55.0, 90.0, 125.0, 148.0, 125.0, 90.0, 55.0, @@ -349,24 +643,31 @@ def eigenmike_em32(): 180.0, 135.0, 111.0, 135.0, 269.0, 270.0, 270.0, 271.0]) * np.pi / 180 - return SamplingSphere.from_spherical(rad, theta, phi) + return spharpy.SamplingSphere.from_spherical_colatitude(phi, theta, rad) def eigenmike_em64(): """Microphone positions of the Eigenmike em64 by mhacoustics. - according to - the Eigenmuke user manual on the homepage [#]_. - - References - ---------- - .. [#] Eigenmike em64 User Manual, https://eigenmike.com/sites/default/files/documentation-2024-09/getting%20started%20Guide%20to%20em64%20and%20ES3%20R01H.pdf + The data are according to the Eigenmike user manual on the homepage [#]_. Returns ------- sampling : SamplingSphere SamplingSphere object containing all sampling points + References + ---------- + .. [#] Eigenmike em64 Getting Started Guide, https://eigenmike.com/sites/default/files/documentation-2024-09/getting%20started%20Guide%20to%20em64%20and%20ES3%20R01H.pdf + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.eigenmike_em64() + >>> sp.plot.scatter(coords) """ # noqa: E501 rad = np.ones(64)*0.042 @@ -405,24 +706,35 @@ def eigenmike_em64(): 1.009, 0.9932, 1.0024, 1.0324, 1.0029, 0.954, 1.0024, 1.0324, 1.0151, 0.954, 1.0079, 1.0024, 1.0079, 1.0268, 1.012, 0.9463, 1.009, 1.0253, 0.9932, - ]) / 64*4*np.pi + ]) + # weights from the Eigenmike em64 user manual are not sufficiently + # precise, so we need to re-normalize here + weights = weights / np.sum(weights) * 4 * np.pi - sampling = SamplingSphere.from_spherical( - rad, theta, phi, n_max=6) + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + phi, theta, rad, n_max=6) sampling.weights = weights return sampling def icosahedron_ke4(): - """Microphone positions of the KE4 spherical microphone array. + """Microphone positions of IHTA's KE4 spherical microphone array. The microphone marked as "1" defines the positive x-axis. Returns ------- - sampling : SamplingSphere + sampling : :py:class:`spharpy.SamplingSphere` SamplingSphere object containing all sampling points + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.icosahedron_ke4() + >>> sp.plot.scatter(coords) """ theta = np.array([1.565269801525254, 2.294997457752220, 1.226592351686568, @@ -443,40 +755,55 @@ def icosahedron_ke4(): rad = np.ones(20) * 0.065 - return SamplingSphere.from_spherical(rad, theta, phi) + return spharpy.SamplingSphere.from_spherical_colatitude( + phi, theta, rad) + +def equal_area(n_max, condition_num=2.5, n_points=None): + """Sampling based on partitioning into faces with equal area. -def equalarea(n_max, condition_num=2.5, n_points=None): - """Sampling based on partitioning into faces with equal area [9]_. + The implementation is based on [#]_ and Leopardi's MATLAB implementation. Parameters ---------- n_max : int Spherical harmonic order condition_num : double - Desired maximum condition number of the spherical harmonic basis matrix + Desired maximum condition number of the spherical harmonic basis + matrix. Default is ``2.5``. n_points : int, optional Number of points to start the condition number optimization. If set to - None n_points will be (n_max+1)**2 + ``None`` the value ``(n_max+1)**2`` will be used as start. + Default is ``None``. Returns ------- - sampling : SamplingSphere + sampling : :py:class:`spharpy.SamplingSphere` SamplingSphere object containing all sampling points References ---------- - .. [9] P. Leopardi, “A partition of the unit sphere into regions of equal + .. [#] P. Leopardi, “A partition of the unit sphere into regions of equal area and small diameter,” Electronic Transactions on Numerical - Analysis, vol. 25, no. 12, pp. 309–327, 2006. + Analysis, vol. 25, no. 12, pp. 309-327, 2006. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.equal_area(n_max=3) + >>> sp.plot.scatter(coords) """ if not n_points: n_points = (n_max+1)**2 while True: - point_set = eq_point_set(2, n_points) - sampling = SamplingSphere(point_set[0], point_set[1], point_set[2]) + point_data = eq_point_set(2, n_points) + sampling = spharpy.SamplingSphere( + point_data[0], point_data[1], point_data[2]) if condition_num == np.inf: break @@ -491,36 +818,49 @@ def equalarea(n_max, condition_num=2.5, n_points=None): def spiral_points(n_max, condition_num=2.5, n_points=None): - """Sampling based on a spiral distribution of points on a sphere [10]_. + """Sampling based on a spiral distribution of points on a sphere. + + The implementation is based on [#]_. Parameters ---------- n_max : int Spherical harmonic order condition_num : double - Desired maximum condition number of the spherical harmonic basis matrix + Desired maximum condition number of the spherical harmonic basis + matrix. Default is ``2.5``. n_points : int, optional Number of points to start the condition number optimization. If set to - None n_points will be (n_max+1)**2 + ``None`` the value ``(n_max+1)**2`` will be used as start. + Default is ``None``. Returns ------- - sampling : SamplingSphere + sampling : :py:class:`spharpy.SamplingSphere` SamplingSphere object containing all sampling points References ---------- - .. [10] E. a. Rakhmanov, E. B. Saff, and Y. M. Zhou, “Minimal Discrete + .. [#] E. a. Rakhmanov, E. B. Saff, and Y. M. Zhou, “Minimal Discrete Energy on the Sphere,” Mathematical Research Letters, vol. 1, - no. 6, pp. 647–662, 1994. + no. 6, pp. 647-662, 1994. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.spiral_points(n_max=3) + >>> sp.plot.scatter(coords) """ if n_points is None: n_points = (n_max+1)**2 def _spiral_points(n_points): - """Helper function doing the actual calculation of the points""" + """Helper function doing the actual calculation of the points.""" r = np.zeros(n_points) h = np.zeros(n_points) theta = np.zeros(n_points) @@ -548,7 +888,8 @@ def _spiral_points(n_points): while True: theta, phi = _spiral_points(n_points) - sampling = SamplingSphere.from_spherical(np.ones(n_points), theta, phi) + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + phi, theta, np.ones(n_points)) if condition_num == np.inf: break Y = spharpy.spherical.spherical_harmonic_basis(n_max, sampling) @@ -560,3 +901,558 @@ def _spiral_points(n_points): sampling.n_max = n_max return sampling + + +def equal_angle(delta_angles, radius=1.): + """ + Generate sampling of the sphere with equally spaced angles. + + This sampling contain points at the North and South Pole. See + :py:func:`equiangular`, :py:func:`gaussian`, and + :py:func:`great_circle` for samplings that do not contain points at the + poles. + + + Parameters + ---------- + delta_angles : tuple, number + Tuple that gives the angular spacing in azimuth and colatitude in + degrees. If a number is provided, the same spacing is applied in both + dimensions. + radius : number, optional + Radius of the sampling grid. The default is ``1``. + + Returns + ------- + sampling : :py:class:`pyfar.Coordinates` + Sampling positions. Sampling weights can be obtained from + :py:func:`calculate_sampling_weights`. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.equal_angle(delta_angles=45) + >>> sp.plot.scatter(coords) + + """ + + # get the angles + delta_angles = np.asarray(delta_angles) + if delta_angles.size == 2: + delta_phi = delta_angles[0] + delta_theta = delta_angles[1] + else: + delta_phi = delta_angles + delta_theta = delta_angles + + # check if the angles can be distributed + eps = np.finfo('float').eps + if not (np.abs(360 % delta_phi) < 2*eps): + raise ValueError("delta_phi must be an integer divisor of 360") + if not (np.abs(180 % delta_theta) < 2*eps): + raise ValueError("delta_theta must be an integer divisor of 180") + + # get the angles + phi_angles = np.arange(0, 360, delta_phi) + theta_angles = np.arange(delta_theta, 180, delta_theta) + + # stack the angles + phi = np.tile(phi_angles, theta_angles.size) + theta = np.repeat(theta_angles, phi_angles.size) + + # add North and South Pole + phi = np.concatenate(([0], phi, [0])) + theta = np.concatenate(([0], theta, [180])) + + # make Coordinates object + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + phi/180*np.pi, theta/180*np.pi, radius, + comment='equal angle spherical sampling grid') + + return sampling + + +def great_circle( + elevation=np.linspace(-90, 90, 19), gcd=10, radius=1, + azimuth_res=1, match=360): + r""" + Spherical sampling grid according to the great circle distance criterion. + + Sampling grid where neighboring points of the same elevation have approx. + the same great circle distance across elevations [#]_. + + Parameters + ---------- + elevation : array like, optional + Contains the elevation from wich the sampling grid is generated, with + :math:`-90^\circ\leq elevation \leq 90^\circ` (:math:`90^\circ`: + North Pole, :math:`-90^\circ`: South Pole). The default is + ``np.linspace(-90, 90, 19)``. + gcd : number, optional + Desired great circle distance (GCD). Note that the actual GCD of the + sampling grid is equal or smaller then the desired GCD and that the GCD + may vary across elevations. The default is ``10``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. + azimuth_res : number, optional + Minimum resolution of the azimuth angle in degree. The default is + ``1``. + match : number, optional + Forces azimuth entries to appear with a period of match degrees. E.g., + if ``match=90``, the grid includes the azimuth angles 0, 90, 180, and + 270 degrees. The default is ``360``. + + Returns + ------- + sampling : :py:class:`pyfar.Coordinates` + Sampling positions. Sampling weights can be obtained from + :py:func:`calculate_sampling_weights`. + + References + ---------- + .. [#] B. P. Bovbjerg, F. Christensen, P. Minnaar, and X. Chen, “Measuring + the head-related transfer functions of an artificial head with a + high directional resolution,” 109th AES Convention, Los Angeles, + USA, Sep. 2000. + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.great_circle() + >>> sp.plot.scatter(coords) + + """ + + # check input + assert 1 / azimuth_res % 1 == 0, "1/azimuth_res must be an integer." + assert not 360 % match, "360/match must be an integer." + assert match / azimuth_res % 1 == 0, "match/azimuth_res must be an \ + integer." + + elevation = np.atleast_1d(np.asarray(elevation)) + + # calculate delta azimuth to meet the desired great circle distance. + # (according to Bovbjerg et al. 2000: Measuring the head related transfer + # functions of an artificial head with a high directional azimuth_res) + d_az = 2 * np.arcsin(np.clip( + np.sin(gcd / 360 * np.pi) / np.cos(elevation / 180 * np.pi), -1, 1)) + d_az = d_az / np.pi * 180 + # correct values at the poles + d_az[np.abs(elevation) == 90] = 360 + # next smallest value in desired angular azimuth_res + d_az = d_az // azimuth_res * azimuth_res + + # adjust phi to make sure that: match // d_az == 0 + for nn in range(d_az.size): + if abs(elevation[nn]) != 90: + while match % d_az[nn] > 1e-15: + # round to precision of azimuth_res to avoid numerical errors + d_az[nn] = np.round((d_az[nn] - azimuth_res)/azimuth_res) \ + * azimuth_res + + # construct the full sampling grid + azim = np.empty(0) + elev = np.empty(0) + for nn in range(elevation.size): + azim = np.append(azim, np.arange(0, 360, d_az[nn])) + elev = np.append(elev, np.full(int(360 / d_az[nn]), elevation[nn])) + + # round to precision of azimuth_res to avoid numerical errors + azim = np.round(azim/azimuth_res) * azimuth_res + + # make Coordinates object + sampling = spharpy.SamplingSphere.from_spherical_elevation( + azim/180*np.pi, elev/180*np.pi, radius, + comment='spherical great circle sampling grid') + + return sampling + + +def lebedev(n_points=None, n_max=None, radius=1.): + r""" + Return Lebedev spherical sampling grid. + + For detailed information, see [#]_. For a list of available values + for `n_points` and `n_max` call :py:func:`sph_lebedev`. This is a + quadrature sampling with the sum of the sampling weights in + `sampling.weights` being :math:`4\pi`. + + Parameters + ---------- + n_points : int, optional + Number of sampling points in the grid. Related to the spherical + harmonic order by ``n_points = (n_max + 1)**2``. Either `n_points` + or `n_max` must be provided. The default is ``None``. + n_max : int, optional + Maximum applicable spherical harmonic order. Related to the number of + points by ``n_max = np.sqrt(n_points) - 1``. Either `n_points` or + `n_max` must be provided. The default is ``None``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. + + Returns + ------- + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions including sampling weights. + + Notes + ----- + This is a Python port of the Matlab Code written by Rob Parrish [#]_. + + References + ---------- + .. [#] V.I. Lebedev, and D.N. Laikov + "A quadrature formula for the sphere of the 131st + algebraic order of accuracy" + Doklady Mathematics, Vol. 59, No. 3, 1999, pp. 477-481. + .. [#] https://de.mathworks.com/matlabcentral/fileexchange/27097-getlebedevsphere + + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.lebedev(n_max=3) + >>> sp.plot.scatter(coords) + + """ # noqa: E501 + + # possible degrees + degrees = np.array([6, 14, 26, 38, 50, 74, 86, 110, 146, 170, 194, 230, + 266, 302, 350, 434, 590, 770, 974, 1202, 1454, 1730, + 2030, 2354, 2702, 3074, 3470, 3890, 4334, 4802, 5294, + 5810], dtype=int) + + # corresponding spherical harmonic orders + orders = np.array((np.floor(np.sqrt(degrees / 1.3) - 1)), dtype=int) + + # list possible sh orders and degrees + if n_points is None and n_max is None: + print('Possible input values:') + for o, d in zip(orders, degrees, strict=True): + print(f"SH order {o}, number of points {d}") + + return None + + # check input + if n_points is not None and n_max is not None: + raise ValueError("Either n_points or n_max must be None.") + + # check if the order is available + if n_max is not None: + if n_max not in orders: + str_orders = [f"{o}" for o in orders] + raise ValueError("Invalid spherical harmonic order 'n_max'. \ + Valid orders are: {}.".format( + ', '.join(str_orders))) + + n_points = int(degrees[orders == n_max]) + + # check if n_points is available + if n_points not in degrees: + str_degrees = [f"{d}" for d in degrees] + raise ValueError("Invalid number of points n_points. Valid degrees \ + are: {}.".format(', '.join(str_degrees))) + + # calculate n_max + n_max = int(orders[degrees == n_points]) + + # get the samlpling + leb = lebedev_sphere(n_points) + + # normalize the weights + weights = leb["w"] + + # generate Coordinates object + sampling = spharpy.SamplingSphere( + leb["x"] * radius, + leb["y"] * radius, + leb["z"] * radius, + n_max=n_max, weights=weights, + comment='spherical Lebedev sampling grid') + + return sampling + + +def fliege(n_points=None, n_max=None, radius=1.): + r""" + Return Fliege-Maier spherical sampling grid. + + For detailed information, see [#]_. Call :py:func:`sph_fliege` + for a list of possible values for `n_points` and `n_max`. This is a + quadrature sampling with the sum of the sampling weights in + `sampling.weights` being :math:`4\pi`. + + Parameters + ---------- + n_points : int, optional + Number of sampling points in the grid. Related to the spherical + harmonic order by ``n_points = (n_max + 1)**2``. Either `n_points` + or `n_max` must be provided. The default is ``None``. + n_max : int, optional + Maximum applicable spherical harmonic order. Related to the number of + points by ``n_max = np.sqrt(n_points) - 1``. Either `n_points` or + `n_max` must be provided. The default is ``None``. + radius : number, optional + Radius of the sampling grid in meters. The default is ``1``. + + Returns + ------- + sampling : :py:class:`spharpy.SamplingSphere` + Sampling positions including sampling weights. + + Notes + ----- + This implementation uses pre-calculated points from the SOFiA + toolbox [#]_. Possible combinations of `n_points` and `n_max` are: + + +------------+------------+ + | `n_points` | `n_max` | + +============+============+ + | 4 | 1 | + +------------+------------+ + | 9 | 2 | + +------------+------------+ + | 16 | 3 | + +------------+------------+ + | 25 | 4 | + +------------+------------+ + | 36 | 5 | + +------------+------------+ + | 49 | 6 | + +------------+------------+ + | 64 | 7 | + +------------+------------+ + | 81 | 8 | + +------------+------------+ + | 100 | 9 | + +------------+------------+ + | 121 | 10 | + +------------+------------+ + | 144 | 11 | + +------------+------------+ + | 169 | 12 | + +------------+------------+ + | 196 | 13 | + +------------+------------+ + | 225 | 14 | + +------------+------------+ + | 256 | 15 | + +------------+------------+ + | 289 | 16 | + +------------+------------+ + | 324 | 17 | + +------------+------------+ + | 361 | 18 | + +------------+------------+ + | 400 | 19 | + +------------+------------+ + | 441 | 20 | + +------------+------------+ + | 484 | 21 | + +------------+------------+ + | 529 | 22 | + +------------+------------+ + | 576 | 23 | + +------------+------------+ + | 625 | 24 | + +------------+------------+ + | 676 | 25 | + +------------+------------+ + | 729 | 26 | + +------------+------------+ + | 784 | 27 | + +------------+------------+ + | 841 | 28 | + +------------+------------+ + | 900 | 29 | + +------------+------------+ + + References + ---------- + .. [#] J. Fliege and U. Maier, "The distribution of points on the sphere + and corresponding cubature formulae,” IMA J. Numerical Analysis, + Vol. 19, pp. 317-334, Apr. 1999, doi: 10.1093/imanum/19.2.317. + .. [#] https://audiogroup.web.th-koeln.de/SOFiA_wiki/DOWNLOAD.html + + + Examples + -------- + + .. plot:: + + >>> import spharpy as sp + >>> coords = sp.samplings.fliege(n_max=3) + >>> sp.plot.scatter(coords) + + """ + + # possible values for n_points and n_max + points = np.array([4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, + 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, + 676, 729, 784, 841, 900], dtype=int) + + orders = np.array(np.floor(np.sqrt(points) - 1), dtype=int) + + # list possible sh orders and number of points + if n_points is None and n_max is None: + for o, d in zip(orders, points, strict=True): + print(f"SH order {o}, number of points {d}") + + return None + + # check input + if n_points is not None and n_max is not None: + raise ValueError("Either n_points or n_max must be None.") + + if n_max is not None: + # check if the order is available + if n_max not in orders: + str_orders = [f"{o}" for o in orders] + raise ValueError("Invalid spherical harmonic order 'n_max'. \ + Valid orders are: {}.".format( + ', '.join(str_orders))) + + # assign n_points + n_points = int(points[orders == n_max]) + else: + # check if n_points is available + if n_points not in points: + str_points = [f"{d}" for d in points] + raise ValueError("Invalid number of points n_points. Valid points \ + are: {}.".format(', '.join(str_points))) + + # assign n_max + n_max = int(orders[points == n_points]) + + # get the sampling points + fliege = sio.loadmat(os.path.join( + os.path.dirname(__file__), "_eqsp", "samplings_fliege.mat"), + variable_names=f"Fliege_{int(n_points)}") + fliege = fliege[f"Fliege_{int(n_points)}"] + + # normlize weights + weights = fliege[:, 2] * 4 * np.pi + + # generate Coordinates object + sampling = spharpy.SamplingSphere.from_spherical_colatitude( + fliege[:, 0], + fliege[:, 1], + radius, + n_max=n_max, weights=weights) + + # switch and invert Coordinates in Cartesian representation to be + # consistent with [1] + sampling.z = -sampling.z + + return sampling + + +def _sph_t_design_load_data(degrees='all'): + """Download t-design sampling grids. + + Note: Samplings are downloaded form + https://web.maths.unsw.edu.au/~rsw/Sphere/EffSphDes/sf.html + + Parameters + ---------- + degrees : str or list of int, optional + list of int load sampling of specified degree, `all` load all + samplings up to degree 180, by default 'all'. + """ + + # set the degrees to be read + if isinstance(degrees, int): + degrees = [degrees] + elif isinstance(degrees, str): + degrees = range(1, 181) + + prefix = 'samplings_t_design_' + + n_points_exceptions = {3: 8, 5: 18, 7: 32, 9: 50, 11: 72, 13: 98, 15: 128} + + entries = [] + for degree in degrees: + # number of sampling points + n_points = int(np.ceil((degree + 1)**2 / 2) + 1) + if degree in n_points_exceptions: + n_points = n_points_exceptions[degree] + + # load the data + filename = "sf%03d.%05d" % (degree, n_points) + url = "http://web.maths.unsw.edu.au/~rsw/Sphere/Points/SF/"\ + "SF29-Nov-2012/" + fileurl = url + filename + path_save = os.path.join( + os.path.dirname(__file__), "_eqsp", prefix + filename) + + entries.append((path_save, fileurl)) + + pool = ThreadPool(50) + pool.imap_unordered(_fetch_url, entries) + pool.close() + pool.join() + + +def _sph_extremal_load_data(orders='all'): + """Download extremal sampling grids. + + Note: Samplings are downloaded form + https://web.maths.unsw.edu.au/~rsw/Sphere/MaxDet/MaxDet1.html + + Parameters + ---------- + orders : str or list of int, optional + list of int load sampling of specified orders, `all` load all + samplings up to orders 200, by default 'all'. + """ + + # set the SH orders to be read + if isinstance(orders, int): + orders = [orders] + elif isinstance(orders, str): + orders = range(1, 201) + + prefix = 'samplings_extremal_' + + entries = [] + for n_max in orders: + # number of sampling points + n_points = (n_max + 1)**2 + + # load the data + filename = "md%03d.%05d" % (n_max, n_points) + url = "https://web.maths.unsw.edu.au/~rsw/Sphere/S2Pts/MD/" + fileurl = url + filename + path_save = os.path.join( + os.path.dirname(__file__), "_eqsp", prefix + filename) + + entries.append((path_save, fileurl)) + + # download on parallel + pool = ThreadPool(50) + pool.imap_unordered(_fetch_url, entries) + pool.close() + pool.join() + + +def _fetch_url(entry): + path, uri = entry + if not os.path.exists(path): + # Kontrolle ist gut, Vertrauen ist besser + with warnings.catch_warnings(): + warnings.simplefilter("ignore", InsecureRequestWarning) + r = requests.get(uri, stream=True, verify=False) + if r.status_code == 200: + with open(path, 'wb') as f: + for chunk in r: + f.write(chunk) + return path diff --git a/spharpy/spatial/__init__.py b/spharpy/spatial/__init__.py index 87e54ad6..63d8e7fb 100644 --- a/spharpy/spatial/__init__.py +++ b/spharpy/spatial/__init__.py @@ -1,3 +1,4 @@ +"""Wave field representations in the spatial domain.""" from .spatial import greens_function_plane_wave, greens_function_point_source diff --git a/spharpy/spatial/spatial.py b/spharpy/spatial/spatial.py index 7633a743..a60ea400 100644 --- a/spharpy/spatial/spatial.py +++ b/spharpy/spatial/spatial.py @@ -1,6 +1,6 @@ +"""Wave field representations in the spatial domain.""" import numpy as np import scipy.spatial as sspat -from spharpy._deprecation import convert_coordinates def greens_function_plane_wave( @@ -8,109 +8,202 @@ def greens_function_plane_wave( receiver_points, wave_number, gradient=False): - """The matrix describing the propagation of a plane wave from a direction + r"""Green's function for plane waves in free space in matrix form. + The matrix describing the propagation of a plane wave from a direction of arrival defined by the azimuth and elevation angles of the source points to the receiver points. The phase sign convention reflects a direction of arrival from the source position. + .. math:: + + G(k) = e^{i \mathbf{k}^\mathrm{T} \mathbf{r}} + Parameters ---------- - source_points : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + source_points : :py:class:`pyfar.Coordinates` The source points defining the direction of incidence for the plane wave. Note that the radius on which the source is positioned has no relevance. - receiver_points : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + receiver_points : :py:class:`pyfar.Coordinates` The receiver points. - wave_number : double - The wave number of the wave - speed_of_sound : double - The speed of sound + wave_number : float, complex + The wave number. A complex wave number can be used for evanescent + waves. gradient : bool - If True, the gradient will be returned as well + If ``True``, the gradient will be returned as well. + Default is ``False``. Returns ------- - M : ndarray, complex, shape(n_receiver, n_sources) - The plane wave propagation matrix - - """ # noqa: 501 - source_points = convert_coordinates(source_points) - receiver_points = convert_coordinates(receiver_points) - e_doa = source_points.cartesian / \ - np.linalg.norm(source_points.cartesian, axis=0) + M : ndarray, complex + The plane wave propagation matrix with shape [n_receiver, n_sources]. + + Examples + -------- + Plot Green's function in the x-y plane for a plane wave with a direction + of incidence defined by the vector :math:`[x, y, z] = [2, 1, 0]`. + + .. plot:: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from pyfar import Coordinates + >>> import spharpy + ... + >>> spat_res = 30 + >>> x_min, x_max = -10, 10 + >>> xx, yy = np.meshgrid( + >>> np.linspace(x_min, x_max, spat_res), + >>> np.linspace(x_min, x_max, spat_res)) + >>> receivers = Coordinates( + ... xx.flatten(), yy.flatten(), np.zeros(spat_res**2)) + >>> doa = Coordinates(2, 1, 0) + ... + >>> k = 1 + >>> plane_wave_matrix = spharpy.spatial.greens_function_plane_wave( + ... doa, receivers, k) + >>> plt.figure() + >>> plt.contourf( + ... xx, yy, np.real(plane_wave_matrix.reshape(spat_res, spat_res)), + ... cmap='RdBu_r', levels=100) + >>> plt.colorbar() + >>> ax = plt.gca() + >>> ax.set_aspect('equal') + + For evanescent waves, a complex wave number can be used + + .. plot:: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from pyfar import Coordinates + >>> import spharpy + ... + >>> spat_res = 30 + >>> x_min, x_max = -10, 10 + >>> xx, yy = np.meshgrid( + >>> np.linspace(x_min, x_max, spat_res), + >>> np.linspace(x_min, x_max, spat_res)) + >>> receivers = Coordinates( + ... xx.flatten(), yy.flatten(), np.zeros(spat_res**2)) + >>> doa = Coordinates(2, 1, 0) + ... + >>> k = 1-.1j + >>> plane_wave_matrix = spharpy.spatial.greens_function_plane_wave( + ... doa, receivers, k, gradient=False) + >>> plt.contourf( + ... xx, yy, np.real(plane_wave_matrix.reshape(spat_res, spat_res)), + ... cmap='RdBu_r', levels=100) + >>> plt.colorbar() + >>> ax = plt.gca() + >>> ax.set_aspect('equal') + + + """ + e_doa = source_points.cartesian.T / \ + np.linalg.norm(source_points.cartesian.T, axis=0) k_vec = np.squeeze(wave_number*e_doa) - # avoid using the complex exponential since it's slower than sin and cos - arg = receiver_points.cartesian.T @ k_vec + arg = receiver_points.cartesian @ k_vec plane_wave_matrix = np.cos(arg) + 1j*np.sin(arg) - if not gradient: + if gradient is False: return plane_wave_matrix - else: - plane_wave_gradient_matrix_x = (plane_wave_matrix * 1j*k_vec[0]) - plane_wave_gradient_matrix_y = (plane_wave_matrix * 1j*k_vec[1]) - plane_wave_gradient_matrix_z = (plane_wave_matrix * 1j*k_vec[2]) - return plane_wave_matrix, \ - [plane_wave_gradient_matrix_x, - plane_wave_gradient_matrix_y, - plane_wave_gradient_matrix_z] + + plane_wave_gradient_matrix_x = (plane_wave_matrix * 1j*k_vec[0]) + plane_wave_gradient_matrix_y = (plane_wave_matrix * 1j*k_vec[1]) + plane_wave_gradient_matrix_z = (plane_wave_matrix * 1j*k_vec[2]) + return plane_wave_matrix, \ + [plane_wave_gradient_matrix_x, + plane_wave_gradient_matrix_y, + plane_wave_gradient_matrix_z] def greens_function_point_source(sources, receivers, k, gradient=False): - r"""Green's function for point sources in free space in matrix form. The - phase sign convention corresponds to a direction of propagation away from - the source at position $r_s$. + r"""Green's function for point sources in free space in matrix form. + The phase sign convention corresponds to a direction of propagation away + from the source at position :math:`r_s`. .. math:: - G(k) = \\frac{e^{- k\\|\\mathbf{r_s} - \\mathbf{r_r}\\|}} - {4 \\pi \\|\\mathbf{r_s} - \\mathbf{r_r}\\|} + G(k) = \frac{e^{-i k\|\mathbf{r_s} - \mathbf{r_r}\|}} + {4 \pi \|\mathbf{r_s} - \mathbf{r_r}\|} Parameters ---------- - - source : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + sources : :py:class:`pyfar.Coordinates` source points as Coordinates object - receivers : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` + receivers : :py:class:`pyfar.Coordinates` receiver points as Coordinates object - k : ndarray, double - wave number + k : ndarray, float + The wave number + gradient : bool, optional + Flag to indicate if the gradient should be computed and returned. The + default is ``False``. Returns ------- G : ndarray, double Green's function - """ # noqa: 501 - sources = convert_coordinates(sources) - receivers = convert_coordinates(receivers) - dist = sspat.distance.cdist(receivers.cartesian.T, sources.cartesian.T) + + Examples + -------- + Plot Green's function in the x-y plane for a point source at + :math:`[x, y, z] = [10, 15, 0]`. + + .. plot:: + + >>> import numpy as np + >>> import matplotlib.pyplot as plt + >>> from pyfar import Coordinates + >>> import spharpy + ... + >>> spat_res = 30 + >>> x_min, x_max = -10, 10 + >>> xx, yy = np.meshgrid( + >>> np.linspace(x_min, x_max, spat_res), + >>> np.linspace(x_min, x_max, spat_res)) + >>> receivers = Coordinates( + ... xx.flatten(), yy.flatten(), np.zeros(spat_res**2)) + >>> doa = Coordinates(10, 15, 0) + >>> plane_wave_matrix = spharpy.spatial.greens_function_point_source( + ... doa, receivers, 1, gradient=False) + >>> plt.contourf( + ... xx, yy, np.real(plane_wave_matrix.reshape(spat_res, spat_res)), + ... cmap='RdBu_r', levels=100) + >>> plt.colorbar() + >>> ax = plt.gca() + >>> ax.set_aspect('equal') + + """ + dist = sspat.distance.cdist(receivers.cartesian, sources.cartesian) dist = np.squeeze(dist) cexp = np.cos(k*dist) - 1j*np.sin(k*dist) G = cexp/dist/4/np.pi - if not gradient: + if gradient is False: return G - else: - def lambda_cdiff(u, v): - return u-v - - diff_x = sspat.distance.cdist( - np.atleast_2d(receivers.x).T, - np.atleast_2d(sources.x).T, - lambda_cdiff) - diff_y = sspat.distance.cdist( - np.atleast_2d(receivers.y).T, - np.atleast_2d(sources.y).T, - lambda_cdiff) - diff_z = sspat.distance.cdist( - np.atleast_2d(receivers.z).T, - np.atleast_2d(sources.z).T, - lambda_cdiff) - - G_dx = G/dist * np.squeeze(diff_x) * (-1j-1/dist) - G_dy = G/dist * np.squeeze(diff_y) * (-1j-1/dist) - G_dz = G/dist * np.squeeze(diff_z) * (-1j-1/dist) - - return G, (G_dx, G_dy, G_dz) + + def lambda_cdiff(u, v): + return u-v + + diff_x = sspat.distance.cdist( + np.atleast_2d(receivers.x).T, + np.atleast_2d(sources.x).T, + lambda_cdiff) + diff_y = sspat.distance.cdist( + np.atleast_2d(receivers.y).T, + np.atleast_2d(sources.y).T, + lambda_cdiff) + diff_z = sspat.distance.cdist( + np.atleast_2d(receivers.z).T, + np.atleast_2d(sources.z).T, + lambda_cdiff) + + G_dx = G/dist * np.squeeze(diff_x) * (-1j-1/dist) + G_dy = G/dist * np.squeeze(diff_y) * (-1j-1/dist) + G_dz = G/dist * np.squeeze(diff_z) * (-1j-1/dist) + + return G, (G_dx, G_dy, G_dz) diff --git a/spharpy/special.py b/spharpy/special.py index f2edf07f..88d79958 100644 --- a/spharpy/special.py +++ b/spharpy/special.py @@ -24,7 +24,8 @@ def spherical_bessel(n, z, derivative=False): z : double, ndarray Argument of the spherical bessel function. Has to be real valued. derivative : bool - Return the derivative of the spherical Bessel function + Return the derivative of the spherical Bessel function. Default is + ``False``. Returns @@ -59,9 +60,9 @@ def spherical_bessel(n, z, derivative=False): def spherical_bessel_zeros(n_max, n_zeros): - """Compute the zeros of the spherical Bessel function. + r"""Compute the zeros of the spherical Bessel function. This function will always start at order zero which is equal - to sin(x)/x and iteratively compute the roots for higher orders. + to :math:`\sin(x)/x` and iteratively compute the roots for higher orders. The roots are computed using Brents algorithm from the scipy package. Parameters @@ -108,6 +109,12 @@ def spherical_hankel(n, z, kind=2, derivative=False): Order of the spherical bessel function z : double, ndarray Argument of the spherical bessel function. Has to be real valued. + kind : int, optional + The kind of the spherical Hankel function. Must be ``1`` or ``2``. The + default is ``2``. + derivative : bool, optional + Flag to indicate if the derivative os the spherical Hankel function + should be returned. The default is ``False``. Returns ------- @@ -127,10 +134,7 @@ def spherical_hankel(n, z, kind=2, derivative=False): n = np.asarray(n, dtype=int) z = np.asarray(z, dtype=float) - if derivative: - ufunc = _spherical_hankel_derivative - else: - ufunc = _spherical_hankel + ufunc = _spherical_hankel_derivative if derivative else _spherical_hankel if n.size > 1: hankel = np.zeros((n.size, z.size), dtype=complex) @@ -156,51 +160,48 @@ def _spherical_hankel(n, z, kind): def _spherical_hankel_derivative(n, z, kind): - hankel = _spherical_hankel(n-1, z, kind) - \ - (n+1)/z * _spherical_hankel(n, z, kind) - - return hankel + return _spherical_hankel(n - 1, z, kind) - (n + 1) / z * _spherical_hankel( + n, z, kind) def spherical_harmonic(n, m, theta, phi): """The spherical harmonics of order n and degree m. + The spherical harmonic functions are fully normalized (N3D) and + include the Condon-Shortley phase according to [#]_. n : unsigned int The spherical harmonic order m : int The spherical harmonic degree theta : ndarray, double - The elevation angle + The colatitude angle phi : ndarray, double The azimuth angle Returns ------- spherical_harmonic : ndarray, double - The complex valued spherial harmonic of order n and degree m + The complex valued spherical harmonic of order n and degree m Note ---- This function wraps the spherical harmonic implementation from scipy. - The only difference is that we return zeros instead of nan values - if $n < |m|$. + References + ---------- + .. [#] B. Rafaely, "Fundamentals of Spherical Array Processing", (2015), + Springer-Verlag """ - theta = np.asarray(theta, dtype=float) - phi = np.asarray(phi, dtype=float) - if n < np.abs(m): - sph_harm = np.zeros(theta.shape) - else: - sph_harm = _spspecial.sph_harm(m, n, phi, theta) - return sph_harm + return _spspecial.sph_harm_y(n, m, theta, phi) def spherical_harmonic_real(n, m, theta, phi): r"""Real valued spherical harmonic function of order n and degree m evaluated at the angles theta and phi. The spherical harmonic functions are fully normalized (N3D) and follow - the AmbiX phase convention [1]_. + the AmbiX phase convention, which does not include the Condon-Shortley + phase [#]_. .. math:: @@ -213,13 +214,11 @@ def spherical_harmonic_real(n, m, theta, phi): References ---------- - .. [1] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A + .. [#] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A Suggested Ambisonics Format (revised by F. Zotter),” International Symposium on Ambisonics and Spherical Acoustics, vol. 3, pp. 1–11, 2011. - - Parameters ---------- n : unsigned int @@ -227,7 +226,7 @@ def spherical_harmonic_real(n, m, theta, phi): m : int The spherical harmonic degree theta : ndarray, double - The elevation angle + The colatitude angle phi : ndarray, double The azimuth angle @@ -239,7 +238,7 @@ def spherical_harmonic_real(n, m, theta, phi): """ # careful here, scipy uses phi as the elevation angle and # theta as the azimuth angle - Y_nm_cplx = _spspecial.sph_harm(m, n, phi, theta) + Y_nm_cplx = spherical_harmonic(n, m, theta, phi) if m == 0: Y_nm = np.real(Y_nm_cplx) @@ -259,7 +258,6 @@ def spherical_harmonic_derivative_phi(n, m, theta, phi): Parameters ---------- - n : int Spherical harmonic order m : int @@ -271,7 +269,6 @@ def spherical_harmonic_derivative_phi(n, m, theta, phi): Returns ------- - sh_diff : complex double Spherical harmonic derivative @@ -286,11 +283,10 @@ def spherical_harmonic_derivative_phi(n, m, theta, phi): def spherical_harmonic_gradient_phi(n, m, theta, phi): """Calculate the derivative of the spherical harmonics with respect to - the azimuth angle phi divided by sin(theta) + the azimuth angle phi divided by sin(theta). Parameters ---------- - n : int Spherical harmonic order m : int @@ -302,24 +298,22 @@ def spherical_harmonic_gradient_phi(n, m, theta, phi): Returns ------- - sh_diff : complex double Spherical harmonic derivative """ if m == 0: - res = np.zeros(theta.shape, dtype=complex) - else: - factor = np.sqrt((2*n+1)/(2*n-1))/2 - exp_phi = np.exp(1j*phi) - first = np.sqrt((n+m)*(n+m-1)) * exp_phi * \ - spherical_harmonic(n-1, m-1, theta, phi) - second = np.sqrt((n-m) * (n-m-1)) / exp_phi * \ - spherical_harmonic(n-1, m+1, theta, phi) - Ynm_sin_theta = (-1) * factor * (first + second) - res = Ynm_sin_theta * 1j + return np.zeros(theta.shape, dtype=complex) - return res + factor = np.sqrt((2*n+1)/(2*n-1))/2 + exp_phi = np.exp(1j*phi) + first = np.sqrt((n+m)*(n+m-1)) * exp_phi * spherical_harmonic( + n-1, m-1, theta, phi) + second = np.sqrt((n-m) * (n-m-1)) / exp_phi * spherical_harmonic( + n-1, m+1, theta, phi) + Ynm_sin_theta = (-1) * factor * (first + second) + + return Ynm_sin_theta * 1j def spherical_harmonic_derivative_theta(n, m, theta, phi): @@ -328,7 +322,6 @@ def spherical_harmonic_derivative_theta(n, m, theta, phi): Parameters ---------- - n : int Spherical harmonic order m : int @@ -340,22 +333,19 @@ def spherical_harmonic_derivative_theta(n, m, theta, phi): Returns ------- - sh_diff : complex double Spherical harmonic derivative """ if n == 0: - res = np.zeros(theta.shape, dtype=complex) - else: - exp_phi = np.exp(1j*phi) - first = np.sqrt((n-m+1) * (n+m)) * exp_phi * \ - spherical_harmonic(n, m-1, theta, phi) - second = np.sqrt((n-m) * (n+m+1)) / exp_phi * \ - spherical_harmonic(n, m+1, theta, phi) - res = (first-second)/2 * (-1) + return np.zeros(theta.shape, dtype=complex) - return res + exp_phi = np.exp(1j*phi) + first = np.sqrt((n-m+1) * (n+m)) * exp_phi * spherical_harmonic( + n, m-1, theta, phi) + second = np.sqrt((n-m) * (n+m+1)) / exp_phi * spherical_harmonic( + n, m+1, theta, phi) + return (first-second)/2 * (-1) def legendre_function(n, m, z, cs_phase=True): @@ -365,8 +355,8 @@ def legendre_function(n, m, z, cs_phase=True): P_n^m(z) = (-1)^m(1-z^2)^{m/2}\frac{d^m}{dz^m}P_n{z} - where the Condon-Shotley phase term $(-1)^m$ is dropped when cs_phase=False - is used. + where the Condon-Shotley phase term :math:`(-1)^m` is dropped when + cs_phase=False is used. Parameters ---------- @@ -377,12 +367,13 @@ def legendre_function(n, m, z, cs_phase=True): z : ndarray, double The argument as an array cs_phase : bool, optional - Whether to use include the Condon-Shotley phase term (-1)^m or not + Whether to use include the Condon-Shotley phase term :math:`(-1)^m` + or not. Default is ``True``. Returns ------- legendre : ndarray, double - The Legendre function. This will return zeros if $|m| > n$. + The Legendre function. This will return zeros if :math:`|m| > n`. Note ---- @@ -421,8 +412,8 @@ def spherical_harmonic_normalization(n, m, norm='full'): m : int The spherical harmonic degree. norm : 'full', 'semi', optional - Normalization to use. Can be either fully normalzied on the sphere or - semi-normalized. + Normalization to use. Can be either fully normalized on the sphere or + semi-normalized. Default is ``'full'``. Returns ------- @@ -433,28 +424,28 @@ def spherical_harmonic_normalization(n, m, norm='full'): """ if np.abs(m) > n: factor = 0.0 + elif norm == 'full': + z = n+m+1 + factor = _spspecial.poch(z, -2*m) + factor *= (2*n+1)/(4*np.pi) + if int(m) != 0: + factor *= 2 + factor = np.sqrt(factor) + elif norm == 'semi': + z = n+m+1 + factor = _spspecial.poch(z, -2*m) + if int(m) != 0: + factor *= 2 + factor = np.sqrt(factor) else: - if norm == 'full': - z = n+m+1 - factor = _spspecial.poch(z, -2*m) - factor *= (2*n+1)/(4*np.pi) - if int(m) != 0: - factor *= 2 - factor = np.sqrt(factor) - elif norm == 'semi': - z = n+m+1 - factor = _spspecial.poch(z, -2*m) - if int(m) != 0: - factor *= 2 - factor = np.sqrt(factor) - else: - raise ValueError("Unknown normalization.") + raise ValueError("Unknown normalization.") + return factor def spherical_harmonic_derivative_theta_real(n, m, theta, phi): r"""The derivative of the real valued spherical harmonics with respect - to the elevation angle $\theta$. + to the elevation angle :math:`\theta`. Parameters ---------- @@ -480,37 +471,32 @@ def spherical_harmonic_derivative_theta_real(n, m, theta, phi): """ m_abs = np.abs(m) + if n == 0: - res = np.zeros(theta.shape, dtype=float) - else: - first = (n+m_abs)*(n-m_abs+1) * \ - legendre_function( - n, - m_abs-1, - np.cos(theta), - cs_phase=False) - second = legendre_function( + return np.zeros(theta.shape, dtype=float) + + first = (n+m_abs)*(n-m_abs+1) * \ + legendre_function( n, - m_abs+1, + m_abs-1, np.cos(theta), cs_phase=False) - legendre_diff = 0.5*(first - second) + second = legendre_function( + n, + m_abs+1, + np.cos(theta), + cs_phase=False) + legendre_diff = 0.5*(first - second) - N_nm = spherical_harmonic_normalization(n, m_abs) + N_nm = spherical_harmonic_normalization(n, m_abs) - if m < 0: - phi_term = np.sin(m_abs*phi) - else: - phi_term = np.cos(m_abs*phi) - - res = N_nm * legendre_diff * phi_term - - return res + phi_term = np.sin(m_abs*phi) if m < 0 else np.cos(m_abs*phi) + return N_nm * legendre_diff * phi_term def spherical_harmonic_derivative_phi_real(n, m, theta, phi): r"""The derivative of the real valued spherical harmonics with respect - to the azimuth angle $\phi$. + to the azimuth angle :math:`\phi`. Parameters ---------- @@ -536,24 +522,24 @@ def spherical_harmonic_derivative_phi_real(n, m, theta, phi): """ m_abs = np.abs(m) if m == 0: - res = np.zeros(theta.shape, dtype=float) - else: - legendre = legendre_function(n, m_abs, np.cos(theta), cs_phase=False) - N_nm = spherical_harmonic_normalization(n, m_abs) + return np.zeros(theta.shape, dtype=float) - if m < 0: - phi_term = np.cos(m_abs*phi) * m_abs - else: - phi_term = -np.sin(m_abs*phi) * m_abs + legendre = legendre_function(n, m_abs, np.cos(theta), cs_phase=False) + N_nm = spherical_harmonic_normalization(n, m_abs) - res = N_nm * legendre * phi_term + if m < 0: + phi_term = np.cos(m_abs*phi) * m_abs + else: + phi_term = -np.sin(m_abs*phi) * m_abs - return res + return N_nm * legendre * phi_term def spherical_harmonic_gradient_phi_real(n, m, theta, phi): r"""The gradient of the real valued spherical harmonics with respect - to the azimuth angle $\phi$. + to the azimuth angle :math:`\phi`. + The singularities at the poles are avoided using the formulation proposed + in [#]_. Parameters ---------- @@ -578,38 +564,33 @@ def spherical_harmonic_gradient_phi_real(n, m, theta, phi): References ---------- - .. [1] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical + .. [#] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical harmonic expressions of geomagnetic vector and gradient tensor fields in the local north-oriented reference frame,” Geoscientific Model Development, vol. 8, no. 7, pp. 1979–1990, Jul. 2015. """ m_abs = np.abs(m) + if m == 0: - res = np.zeros(theta.shape, dtype=float) - else: - first = (n+m_abs)*(n+m_abs-1) * \ - legendre_function( - n-1, - m_abs-1, - np.cos(theta), - cs_phase=False) - second = legendre_function( + return np.zeros(theta.shape, dtype=float) + + first = (n+m_abs)*(n+m_abs-1) * \ + legendre_function( n-1, - m_abs+1, + m_abs-1, np.cos(theta), cs_phase=False) - legendre_diff = 0.5*(first + second) - N_nm = spherical_harmonic_normalization(n, m_abs) + second = legendre_function( + n-1, + m_abs+1, + np.cos(theta), + cs_phase=False) + legendre_diff = 0.5*(first + second) + N_nm = spherical_harmonic_normalization(n, m_abs) - if m < 0: - phi_term = np.cos(m_abs*phi) - else: - phi_term = -np.sin(m_abs*phi) - - res = N_nm * legendre_diff * phi_term - - return res + phi_term = np.cos(m_abs*phi) if m < 0 else -np.sin(m_abs*phi) + return N_nm * legendre_diff * phi_term def legendre_coefficients(order): @@ -628,9 +609,7 @@ def legendre_coefficients(order): """ leg = np.polynomial.legendre.Legendre.basis(order) - coefficients = np.polynomial.legendre.leg2poly(leg.coef) - - return coefficients + return np.polynomial.legendre.leg2poly(leg.coef) def chebyshev_coefficients(order): @@ -649,6 +628,4 @@ def chebyshev_coefficients(order): """ cheb = np.polynomial.chebyshev.Chebyshev.basis(order) - coefficients = np.polynomial.chebyshev.cheb2poly(cheb.coef) - - return coefficients + return np.polynomial.chebyshev.cheb2poly(cheb.coef) diff --git a/spharpy/spherical.py b/spharpy/spherical.py index d0d5d37d..60b63d76 100644 --- a/spharpy/spherical.py +++ b/spharpy/spherical.py @@ -1,41 +1,40 @@ +"""Spherical harmonics and Ambisonics functions.""" + import numpy as np import scipy.special as special import spharpy.special as _special -from spharpy._deprecation import convert_coordinates -def acn2nm(acn): +def acn_to_nm(acn): r""" - Calculate the spherical harmonic order n and degree m for a linear - coefficient index, according to the Ambisonics Channel Convention [1]_. + Calculate the order n and degree m from the linear coefficient index. - .. math:: + The linear index corresponds to the Ambisonics Channel Convention [#]_. - n = \lfloor \sqrt{acn + 1} \rfloor - 1 + .. math:: - m = acn - n^2 -n - - - References - ---------- - .. [1] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A - Suggested Ambisonics Format (revised by F. Zotter),” International - Symposium on Ambisonics and Spherical Acoustics, - vol. 3, pp. 1–11, 2011. + n = \lfloor \sqrt{\mathrm{ACN} + 1} \rfloor - 1 + m = \mathrm{ACN} - n^2 -n Parameters ---------- - n : integer, ndarray - Spherical harmonic order - m : integer, ndarray - Spherical harmonic degree + acn : ndarray, int + Linear index Returns ------- - acn : integer, ndarray - Linear index + n : ndarray, int + Spherical harmonic order + m : ndarray, int + Spherical harmonic degree + References + ---------- + .. [#] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A + Suggested Ambisonics Format (revised by F. Zotter),” International + Symposium on Ambisonics and Spherical Acoustics, + vol. 3, pp. 1-11, 2011. """ acn = np.asarray(acn, dtype=int) @@ -48,33 +47,35 @@ def acn2nm(acn): return n, m -def nm2acn(n, m): - """ - Calculate the linear index coefficient for a spherical harmonic order n - and degree m, according to the Ambisonics Channel Convention [1]_. +def nm_to_acn(n, m): + r""" + Calculate the linear index coefficient for a order n and degree m. + + The linear index corresponds to the Ambisonics Channel Convention (ACN) + [#]_. .. math:: - acn = n^2 + n + m + \mathrm{ACN} = n^2 + n + m References ---------- - .. [1] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A + .. [#] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A Suggested Ambisonics Format (revised by F. Zotter),” International Symposium on Ambisonics and Spherical Acoustics, - vol. 3, pp. 1–11, 2011. + vol. 3, pp. 1-11, 2011. Parameters ---------- - n : integer, ndarray + n : ndarray, int Spherical harmonic order - m : integer, ndarray + m : ndarray, int Spherical harmonic degree Returns ------- - acn : integer, ndarray + acn : ndarray, int Linear index """ @@ -87,120 +88,525 @@ def nm2acn(n, m): return n**2 + n + m -def spherical_harmonic_basis(n_max, coords): +def nm_to_fuma(n, m): r""" - Calulcates the complex valued spherical harmonic basis matrix of order Nmax - for a set of points given by their elevation and azimuth angles. - The spherical harmonic functions are fully normalized (N3D) and include the - Condon-Shotley phase term :math:`(-1)^m` [2]_, [3]_. + Calculate the FuMa channel index for a given spherical harmonic order n + and degree m, according to the FuMa (Furse-Malham) + Channel Ordering Convention. + + See [#]_ for details. + + Parameters + ---------- + n : integer, ndarray + Spherical harmonic order + m : integer, ndarray + Spherical harmonic degree + + Returns + ------- + fuma : integer + FuMa channel index + + References + ---------- + .. [#] D. Malham, "Higher order Ambisonic systems” Space in Music – + Music in Space (Mphil thesis). University of York. pp. 2–3., 2003. + """ + + fuma_mapping = [0, 2, 3, 1, 8, 6, 4, 5, 7, 15, 13, 11, 9, 10, 12, 14] + + n = np.asarray([n], dtype=int) + m = np.asarray([m], dtype=int) + + if n.shape != m.shape: + raise ValueError("n and m need to be of the same size") + + # convert (n, m) to the ACN index + acn = nm_to_acn(n, m) + + if np.any(acn < 0) or np.any(acn >= len(fuma_mapping)): + raise ValueError("nm2fuma only supports up to 3rd order") + + acn = np.atleast_2d(acn).T + fuma = np.array([], dtype=int) + for a in acn: + fuma = np.append(fuma, fuma_mapping.index(a)) + + return fuma + + +def fuma_to_nm(fuma): + r""" + Calculate the spherical harmonic order n and degree m for a linear + coefficient index, according to the FuMa (Furse-Malham) + Channel Ordering Convention. + + See [#]_ for details. + + FuMa = WXYZ | RSTUV | KLMNOPQ + ACN = WYZX | VTRSU | QOMKLNP + + Parameters + ---------- + fuma : integer, ndarray + FuMa channel index + + Returns + ------- + n : integer, ndarray + Spherical harmonic order + m : integer, ndarray + Spherical harmonic degree + + References + ---------- + .. [#] D. Malham, "Higher order Ambisonic systems” Space in Music – + Music in Space (Mphil thesis). University of York. pp. 2–3., 2003. + """ + + fuma_mapping = [0, 2, 3, 1, 8, 6, 4, 5, 7, 15, 13, 11, 9, 10, 12, 14] + + if not isinstance(fuma, np.ndarray): + fuma = np.asarray([fuma], dtype=int) + + if np.any(fuma) < 0 or np.any(fuma >= len(fuma_mapping)): + raise ValueError( + "Invalid FuMa channel index, must be between 0 and 15 " + "(supported up to 3rd order)") + + acn = np.array([], dtype=int) + for f in fuma: + acn = np.append(acn, fuma_mapping[int(f)]) + + n, m = acn_to_nm(acn) + return n, m + + +def n3d_to_maxn(acn): + """ + Calculate the scaling factor which converts from N3D (normalized 3D) + normalization to max N normalization. ACN must be less or equal to 15. + + Parameters + ---------- + acn : integer, ndarray + linear index + + Returns + ------- + maxN : float + Scaling factor which converts from N3D to max N + """ + + if not isinstance(acn, np.ndarray): + acn = np.asarray([acn], dtype=int) + + if np.any(acn) > 15: + raise ValueError("acn must be less than or " + "equal to 15") + valid_maxN = [ + np.sqrt(1 / 2), + np.sqrt(1 / 3), + np.sqrt(1 / 3), + np.sqrt(1 / 3), + 2 / np.sqrt(15), + 2 / np.sqrt(15), + np.sqrt(1 / 5), + 2 / np.sqrt(15), + 2 / np.sqrt(15), + np.sqrt(8 / 35), + 3 / np.sqrt(35), + np.sqrt(45 / 224), + np.sqrt(1 / 7), + np.sqrt(45 / 224), + 3 / np.sqrt(35), + np.sqrt(8 / 35), + ] + + maxN = np.array([], dtype=int) + for a in acn: + maxN = np.append(maxN, valid_maxN[int(a)]) + + return maxN + + +def n3d_to_sn3d_norm(n): + """ + Calculate the scaling factor which converts from N3D (normalized 3D) + normalization to SN3D (Schmidt semi-normalized 3D) normalization. + + Parameters + ---------- + n : integer, ndarray + Spherical harmonic order + + Returns + ------- + sn3d : float, ndarray + normalization factor which converts from N3D to SN3D + """ + return 1 / np.sqrt(2 * n + 1) + + +def renormalize(data, channel_convention, current_norm, target_norm, axis): + """ + Renormalize spherical harmonics coefficients or basis functions. + + Parameters + ---------- + data : ndarray + Data which should be renormalized, either spherical harmonics + coefficients or basis functions. + channel_convention : str + Channel convention of the data which should be renormalized. Valid + conventions are `"ACN"` or `"FuMa"`. + current_norm : str + Current normalization. Valid normalizations are `"N3D"`, `"NM"`, + `"maxN"`, `"SN3D"`, or `"SNM"`. + target_norm : str + Desired normalization. Valid normalizations are `"N3D"`, `"NM"` + `"maxN"`, `"SN3D"`, or `"SNM"`. + axis : integer + Axis along which the renormalization should be applied. The axis + contains the spherical harmonics coefficients and must hence have + :math:`Q = (N+1)^2` channels with :math:`N` being the spherical + harmonics order of data. + + Returns + ------- + data : ndarray + Renormalized data + """ + sh_channels = data.shape[axis] + if np.sqrt(sh_channels) % 1: + raise ValueError("Invalid number of SH channels: " + f"{data.shape[-2]}. It must match (n_max + 1)^2.") + + if channel_convention not in ["ACN", "FuMa"]: + raise ValueError("Invalid channel convention. Has to be 'ACN' " + f"or 'FuMa', but is {channel_convention}") + + if current_norm not in ["N3D", "NM", "maxN", "SN3D", "SNM"]: + raise ValueError("Invalid current normalization. Has to be 'N3D', " + "'NM', 'maxN', 'SN3D', or 'SNM' " + f"but is {current_norm}") + + if target_norm not in ["N3D", "NM", "maxN", "SN3D", "SNM"]: + raise ValueError("Invalid target normalization. Has to be 'N3D', " + "'NM', 'maxN', 'SN3D', or 'SNM' " + f"but is {target_norm}") + if current_norm == target_norm: + return data + + acn = np.arange(data.shape[axis]) + + if channel_convention == "FuMa": + orders, _ = fuma_to_nm(acn) + else: + orders, _ = acn_to_nm(acn) + + # One (re)normalization factor is computed per Ambisonics channel. To make + # sure that the factors can be applied, new axes must be added. This is + # done by reshaping to the following shape + shape = [1] * data.ndim + shape[axis] = data.shape[axis] + + data_renorm = data.copy() + # normalize to 'n3d' + if current_norm == 'NM': + data_renorm /= np.sqrt(4*np.pi) + if current_norm == 'SN3D': + data_renorm /= n3d_to_sn3d_norm(orders).reshape(shape) + if current_norm == 'SNM': + data_renorm /= n3d_to_sn3d_norm(orders).reshape(shape) + data_renorm /= np.sqrt(4*np.pi) + if current_norm == 'maxN': + data_renorm /= n3d_to_maxn(acn).reshape(shape) + + # convert to target norm + if target_norm == "NM": + data_renorm *= np.sqrt(4*np.pi) + if target_norm == "SN3D": + data_renorm *= n3d_to_sn3d_norm(orders).reshape(shape) + if target_norm == "SNM": + data_renorm *= n3d_to_sn3d_norm(orders).reshape(shape) + data_renorm *= np.sqrt(4*np.pi) + if target_norm == 'maxN': + data_renorm *= n3d_to_maxn(acn).reshape(shape) + + return data_renorm + + +def change_channel_convention(data, current, target, axis): + """ + Change the channel convention of spherical harmonics coefficients + or basis functions. + + Parameters + ---------- + data : ndarray + Data of which channel convention should be changed. Either spherical + harmonics coefficients or bases functions. + current : str + Current channel convention. Valid conventions are `"ACN"` or `"FuMa"`. + target : str + Desired channel convention. Valid conventions are `"ACN"` or `"FuMa"`. + axis : integer + Axis along which the channel convention should be changed + + Returns + ------- + data : ndarray + Data with changed channel convention + """ + if current not in ["ACN", "FuMa"]: + raise ValueError("Invalid current channel convention. Has to be " + f"'ACN' or 'FuMa', but is {current}") + + if target not in ["ACN", "FuMa"]: + raise ValueError("Invalid target channel convention. Has to be 'ACN' " + f"or 'FuMa', but is {target}") + + acn = np.arange(data.shape[axis]) + if current == 'ACN': + n, m = acn_to_nm(acn) + idx = nm_to_fuma(n, m) + elif current == 'FuMa': + n, m = fuma_to_nm(acn) + idx = nm_to_acn(n, m) + + return np.take(data, idx, axis=axis) + + +def spherical_harmonic_basis( + n_max, coordinates, normalization="N3D", channel_convention="ACN", + condon_shortley='auto'): + r""" + Calculates the complex valued spherical harmonic basis matrix. + + See [#]_ and [#]_ for details. + See also :py:func:`spherical_harmonic_basis_real`. .. math:: + Y_n^m(\theta, \phi) = CS_m N_{nm} P_{nm}(cos(\theta)) e^{im\phi} - Y_n^m(\theta, \phi) = \sqrt{\frac{2n+1}{4\pi} - \frac{(n-m)!}{(n+m)!}} P_n^m(\cos \theta) e^{i m \phi} + where: + + - :math:`n` is the degree + - :math:`m` is the order + - :math:`P_{nm}` is the associated Legendre function + - :math:`N_{nm}` is the normalization term + - :math:`CS_m` is the Condon-Shortley phase term + - :math:`\theta` is the colatitude (angle from the positive z-axis) + - :math:`\phi` is the azimuth (angle from the positive x-axis in the + xy-plane), see :py:mod:`~pyfar.classes.coordinates` References ---------- - .. [2] E. G. Williams, Fourier Acoustics. Academic Press, 1999. - .. [3] B. Rafaely, Fundamentals of Spherical Array Processing, vol. 8. + .. [#] E. G. Williams, Fourier Acoustics. Academic Press, 1999. + .. [#] B. Rafaely, Fundamentals of Spherical Array Processing, vol. 8. Springer, 2015. - Parameters ---------- n_max : integer Spherical harmonic order - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` - Coordinate object with sampling points for which the basis matrix is - calculated + coordinates : :py:class:`pyfar.Coordinates`, :py:class:`spharpy.SamplingSphere` + objects with sampling points for which the basis matrix is calculated + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. + (maxN is only supported up to 3rd order) + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. + (FuMa is only supported up to 3rd order) + condon_shortley : bool or str, optional + Whether to include the Condon-Shortley phase term. If ``True`` or + ``'auto'``, Condon-Shortley is included, if ``False`` it is not + included. The default is ``'auto'``. Returns ------- - Y : double, ndarray, matrix + Y : ndarray, complex Complex spherical harmonic basis matrix - """ # noqa: 501 - coords = convert_coordinates(coords) + Examples + -------- + >>> import spharpy + >>> n_max = 2 + >>> coordinates = spharpy.samplings.icosahedron() + >>> Y = spharpy.spherical.spherical_harmonic_basis(n_max, coordinates) - n_coeff = (n_max+1)**2 + """ # noqa: E501 + if channel_convention == "FuMa" and n_max > 3: + raise ValueError( + "FuMa channel convention is only supported up to 3rd order.") + + if normalization == "maxN" and n_max > 3: + raise ValueError( + "MaxN normalization is only supported up to 3rd order.") + + if not isinstance(condon_shortley, bool) and condon_shortley != 'auto': + raise ValueError( + "Condon_shortley has to be a bool, or 'auto'.") + + if condon_shortley == 'auto': + condon_shortley = True - basis = np.zeros((coords.n_points, n_coeff), dtype=complex) + n_coeff = (n_max + 1) ** 2 + + basis = np.zeros((coordinates.csize, n_coeff), dtype=complex) for acn in range(n_coeff): - order, degree = acn2nm(acn) + if channel_convention == "FuMa": + order, degree = fuma_to_nm(acn) + else: + order, degree = acn_to_nm(acn) basis[:, acn] = _special.spherical_harmonic( - order, - degree, - coords.elevation, - coords.azimuth) - + order, degree, coordinates.colatitude, coordinates.azimuth, + ) + if normalization == "NM": + basis[:, acn] *= np.sqrt(4*np.pi) + if normalization == "SN3D": + basis[:, acn] *= n3d_to_sn3d_norm(order) + if normalization == "SNM": + basis[:, acn] *= n3d_to_sn3d_norm(order) * np.sqrt(4*np.pi) + elif normalization == "maxN": + basis[:, acn] *= n3d_to_maxn(acn) + if not condon_shortley: + # Condon-Shortley phase term is already included in + # the special.spherical_harmonic function + # so need to divide by (-1)^m + basis[:, acn] /= (-1) ** float(degree) return basis -def spherical_harmonic_basis_gradient(n_max, coords): +def spherical_harmonic_basis_gradient(n_max, coordinates, normalization="N3D", + channel_convention="ACN", + condon_shortley='auto'): r""" - Calulcates the gradient on the unit sphere of the complex valued spherical - harmonic basis matrix of order N for a set of points given by their - elevation and azimuth angles. - The spherical harmonic functions are fully normalized (N3D) and include the - Condon-Shotley phase term :math:`(-1)^m` [2]_. This implementation avoids - singularities at the poles using identities derived in [5]_. + Calculates the unit sphere gradients of the complex spherical harmonics. + + This implementation avoids singularities at the poles using identities + derived in [#]_. + + The angular parts of the gradient are defined as + + .. math:: + + \nabla_{(\theta, \phi)} Y_n^m(\theta, \phi) = + \frac{1}{\sin \theta} \frac{\partial Y_n^m(\theta, \phi)} + {\partial \phi} \vec{e}_\phi + + \frac{\partial Y_n^m(\theta, \theta)} + {\partial \theta} \vec{e}_\theta . References ---------- - .. [2] E. G. Williams, Fourier Acoustics. Academic Press, 1999. - .. [9] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical + .. [#] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical harmonic expressions of geomagnetic vector and gradient tensor fields in the local north-oriented reference frame,” Geoscientific - Model Development, vol. 8, no. 7, pp. 1979–1990, Jul. 2015. - + Model Development, vol. 8, no. 7, pp. 1979-1990, Jul. 2015. Parameters ---------- - n_max : integer + n_max : int Spherical harmonic order - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` - Coordinate object with sampling points for which the basis matrix is + coordinates : :py:class:`pyfar.Coordinates`, :py:class:`spharpy.SamplingSphere` + objects with sampling points for which the basis matrix is calculated + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. + (maxN is only supported up to 3rd order) + channel_convention : str, optional + Channel ordering convention, either ``'acn'`` or ``'fuma'``. + The default is ``'acn'``. + (FuMa is only supported up to 3rd order) + condon_shortley : bool or str, optional + Whether to include the Condon-Shortley phase term. If ``True`` or + ``'auto'``, Condon-Shortley is included, if ``False`` it is not + included. The default is ``'auto'``. Returns ------- - grad_elevation : double, ndarray, matrix - Gradient with regard to the elevation angle. - grad_azimuth : double, ndarray, matrix + grad_theta : ndarray, complex + Gradient with regard to the co-latitude angle. + grad_azimuth : ndarray, complex Gradient with regard to the azimuth angle. + Examples + -------- + >>> import spharpy + >>> n_max = 2 + >>> coordinates = spharpy.samplings.icosahedron() + >>> grad_theta, grad_phi = / + spharpy.spherical.spherical_harmonic_basis_gradient(n_max, coordinates) + + """ # noqa: E501 + if channel_convention == "FuMa" and n_max > 3: + raise ValueError( + "FuMa channel convention is only supported up to 3rd order.") + + if normalization == "maxN" and n_max > 3: + raise ValueError( + "MaxN normalization is only supported up to 3rd order.") - """ # noqa: 501 - coords = convert_coordinates(coords) + if not isinstance(condon_shortley, bool) and condon_shortley != 'auto': + raise ValueError( + "Condon_shortley has to be a bool, or 'auto'.") - n_points = coords.n_points + if condon_shortley == 'auto': + condon_shortley = True + + n_points = coordinates.csize n_coeff = (n_max+1)**2 - theta = coords.elevation - phi = coords.azimuth + theta = coordinates.colatitude + phi = coordinates.azimuth grad_theta = np.zeros((n_points, n_coeff), dtype=complex) grad_phi = np.zeros((n_points, n_coeff), dtype=complex) for acn in range(n_coeff): - n, m = acn2nm(acn) - - grad_theta[:, acn] = \ - _special.spherical_harmonic_derivative_theta( - n, m, theta, phi) - grad_phi[:, acn] = \ - _special.spherical_harmonic_gradient_phi( - n, m, theta, phi) + if channel_convention == "FuMa": + n, m = fuma_to_nm(acn) + else: + n, m = acn_to_nm(acn) + + grad_theta[:, acn] = _special.spherical_harmonic_derivative_theta( + n, m, theta, phi) + grad_phi[:, acn] = _special.spherical_harmonic_gradient_phi( + n, m, theta, phi) + + factor = 1. + if normalization == "NM": + factor = np.sqrt(4*np.pi) + if normalization == "SN3D": + factor = n3d_to_sn3d_norm(n) + if normalization == "SNM": + factor = n3d_to_sn3d_norm(n) * np.sqrt(4*np.pi) + elif normalization == "maxN": + factor *= n3d_to_maxn(acn) + + if not condon_shortley: + # Condon-Shortley phase term is already included in + # the special.spherical_harmonic function + # so need to divide by (-1)^m + factor /= (-1) ** float(m) + + grad_theta[:, acn] *= factor + grad_phi[:, acn] *= factor return grad_theta, grad_phi -def spherical_harmonic_basis_real(n_max, coords): +def spherical_harmonic_basis_real( + n_max, coordinates, normalization="N3D", channel_convention="ACN", + condon_shortley='auto'): r""" - Calulcates the real valued spherical harmonic basis matrix of order Nmax - for a set of points given by their elevation and azimuth angles. - The spherical harmonic functions are fully normalized (N3D) and follow - the AmbiX phase convention [1]_. + Calculates the real valued spherical harmonic basis matrix. + See also :py:func:`spherical_harmonic_basis`. .. math:: @@ -211,92 +617,159 @@ def spherical_harmonic_basis_real(n_max, coords): \displaystyle \sin(|m|\phi) , & \text{if $m < 0$} \end{cases} - References - ---------- - .. [1] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A - Suggested Ambisonics Format (revised by F. Zotter),” International - Symposium on Ambisonics and Spherical Acoustics, - vol. 3, pp. 1–11, 2011. - - Parameters ---------- - n : integer + n_max : int Spherical harmonic order - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` - Coordinate object with sampling points for which the basis matrix is + coordinates : :py:class:`pyfar.Coordinates`, :py:class:`spharpy.SamplingSphere` + objects with sampling points for which the basis matrix is calculated + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. + (maxN is only supported up to 3rd order) + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. + (FuMa is only supported up to 3rd order) + condon_shortley : bool or str, optional + Whether to include the Condon-Shortley phase term. If ``True``, + Condon-Shortley is included, if ``False`` or ``'auto'``, it is not + included. The default is ``'auto'``. Returns ------- - Y : double, ndarray, matrix - Real valued spherical harmonic basis matrix + Y : ndarray, float + Real valued spherical harmonic basis matrix. + """ # noqa: E501 + if channel_convention == "FuMa" and n_max > 3: + raise ValueError( + "FuMa channel convention is only supported up to 3rd order.") - """ # noqa: 501 - coords = convert_coordinates(coords) + if normalization == "maxN" and n_max > 3: + raise ValueError( + "MaxN normalization is only supported up to 3rd order.") - n_coeff = (n_max+1)**2 + if not isinstance(condon_shortley, bool) and condon_shortley != 'auto': + raise ValueError( + "Condon_shortley has to be a bool, or 'auto'.") + + if condon_shortley == 'auto': + condon_shortley = False - basis = np.zeros((coords.n_points, n_coeff), dtype=float) + n_coeff = (n_max + 1) ** 2 + + basis = np.zeros((coordinates.csize, n_coeff), dtype=float) for acn in range(n_coeff): - order, degree = acn2nm(acn) + if channel_convention == "FuMa": + order, degree = fuma_to_nm(acn) + else: + order, degree = acn_to_nm(acn) basis[:, acn] = _special.spherical_harmonic_real( - order, - degree, - coords.elevation, - coords.azimuth) + order, degree, coordinates.colatitude, coordinates.azimuth, + ) + if normalization == "NM": + basis[:, acn] *= np.sqrt(4*np.pi) + if normalization == "SN3D": + basis[:, acn] *= n3d_to_sn3d_norm(order) + if normalization == "SNM": + basis[:, acn] *= n3d_to_sn3d_norm(order) * np.sqrt(4*np.pi) + elif normalization == "maxN": + basis[:, acn] *= n3d_to_maxn(acn) + if condon_shortley: + # Condon-Shortley phase term is not included in + # the special.spherical_harmonic function + basis[:, acn] *= (-1) ** float(degree) return basis -def spherical_harmonic_basis_gradient_real(n_max, coords): +def spherical_harmonic_basis_gradient_real(n_max, coordinates, + normalization="N3D", + channel_convention="ACN", + condon_shortley='auto'): r""" - Calulcates the gradient on the unit sphere of the real valued spherical - harmonic basis matrix of order N for a set of points given by their - elevation and azimuth angles. - The spherical harmonic functions are fully normalized (N3D) and follow - the AmbiX phase convention [1]_. This implementation avoids - singularities at the poles using identities derived in [5]_. + Calculates the unit sphere gradients of the real valued spherical + harmonics. + + This implementation avoids singularities at the poles using identities + derived in [#]_. + + The angular parts of the gradient are defined as + + .. math:: + + \nabla_{(\theta, \phi)} Y_n^m(\theta, \phi) = + \frac{1}{\sin \theta} \frac{\partial Y_n^m(\theta, \phi)} + {\partial \phi} \vec{e}_\phi + + \frac{\partial Y_n^m(\theta, \theta)} + {\partial \theta} \vec{e}_\theta . References ---------- - .. [1] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A - Suggested Ambisonics Format (revised by F. Zotter),” International - Symposium on Ambisonics and Spherical Acoustics, - vol. 3, pp. 1–11, 2011. - .. [9] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical + .. [#] J. Du, C. Chen, V. Lesur, and L. Wang, “Non-singular spherical harmonic expressions of geomagnetic vector and gradient tensor fields in the local north-oriented reference frame,” Geoscientific - Model Development, vol. 8, no. 7, pp. 1979–1990, Jul. 2015. + Model Development, vol. 8, no. 7, pp. 1979-1990, Jul. 2015. Parameters ---------- - n_max : integer + n_max : int Spherical harmonic order - coordinates : :class:`spharpy.samplings.Coordinates`, :doc:`pf.Coordinates ` - Coordinate object with sampling points for which the basis matrix is + coordinates : :py:class:`pyfar.Coordinates`, :py:class:`spharpy.SamplingSphere` + objects with sampling points for which the basis matrix is calculated + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, + ``'maxN'``, ``'SN3D'``, or ``'SNM'``. + (maxN is only supported up to 3rd order) + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. + (FuMa is only supported up to 3rd order) + condon_shortley : bool or str, optional + Whether to include the Condon-Shortley phase term. If ``True``, + Condon-Shortley is included, if ``False`` or ``'auto'``, it is not + included. The default is ``'auto'``. Returns ------- - Y : double, ndarray, matrix - Complex spherical harmonic basis matrix - - """ # noqa: 501 - coords = convert_coordinates(coords) - - n_points = coords.n_points - n_coeff = (n_max+1)**2 - theta = coords.elevation - phi = coords.azimuth + grad_theta : ndarray, float + Gradient with respect to the co-latitude angle. + grad_phi : ndarray, float + Gradient with respect to the azimuth angle. + + """ # noqa: E501 + if channel_convention == "FuMa" and n_max > 3: + raise ValueError( + "FuMa channel convention is only supported up to 3rd order.") + + if normalization == "maxN" and n_max > 3: + raise ValueError( + "MaxN normalization is only supported up to 3rd order.") + + if not isinstance(condon_shortley, bool) and condon_shortley != 'auto': + raise ValueError( + "Condon_shortley has to be a bool, or 'auto'.") + + if condon_shortley == 'auto': + condon_shortley = False + + n_points = coordinates.csize + n_coeff = (n_max + 1) ** 2 + theta = coordinates.colatitude + phi = coordinates.azimuth grad_theta = np.zeros((n_points, n_coeff), dtype=float) grad_phi = np.zeros((n_points, n_coeff), dtype=float) for acn in range(n_coeff): - n, m = acn2nm(acn) + if channel_convention == "FuMa": + n, m = fuma_to_nm(acn) + else: + n, m = acn_to_nm(acn) grad_theta[:, acn] = \ _special.spherical_harmonic_derivative_theta_real( @@ -305,6 +778,24 @@ def spherical_harmonic_basis_gradient_real(n_max, coords): _special.spherical_harmonic_gradient_phi_real( n, m, theta, phi) + factor = 1.0 + if normalization == "NM": + factor = np.sqrt(4*np.pi) + if normalization == "SN3D": + factor = n3d_to_sn3d_norm(n) + if normalization == "SNM": + factor = n3d_to_sn3d_norm(n) * np.sqrt(4*np.pi) + elif normalization == "maxN": + factor *= n3d_to_maxn(acn) + + if condon_shortley: + # Condon-Shortley phase term is not included in + # the special.spherical_harmonic function + factor *= (-1) ** float(m) + + grad_theta[:, acn] *= factor + grad_phi[:, acn] *= factor + return grad_theta, grad_phi @@ -312,7 +803,7 @@ def modal_strength(n_max, kr, arraytype='rigid'): r""" - Modal strenght function for microphone arrays. + Modal strength function for microphone arrays. .. math:: @@ -326,30 +817,29 @@ def modal_strength(n_max, \end{cases} - Notes - ----- - This implementation uses the second order Hankel function, see [4]_ for an + This implementation uses the second order Hankel function, see [#]_ for an overview of the corresponding sign conventions. References ---------- - .. [4] V. Tourbabin and B. Rafaely, “On the Consistent Use of Space and + .. [#] V. Tourbabin and B. Rafaely, “On the Consistent Use of Space and Time Conventions in Array Processing,” vol. 101, pp. 470–473, 2015. Parameters ---------- - n : integer, ndarray - Spherical harmonic order - kr : double, ndarray + n_max : int + The spherical harmonic order + kr : ndarray, float Wave number * radius - arraytype : string - Array configuration. Can be a microphones mounted on a rigid sphere, - on a virtual open sphere or cardioid microphones on an open sphere. + arraytype : str + Array configuration. Can be omnidirectional microphones mounted on a + ``'rigid'`` sphere or on a virtual ``'open'`` sphere, or ``'cardioid'`` + microphones on an open sphere. Default is ``'rigid'``. Returns ------- - B : double, ndarray + B : ndarray, float Modal strength diagonal matrix """ @@ -369,7 +859,8 @@ def modal_strength(n_max, def _modal_strength(n, kr, config): """Helper function for the calculation of the modal strength for - plane waves""" + plane waves. + """ if config == 'open': ms = 4*np.pi*pow(1.0j, n) * _special.spherical_bessel(n, kr) elif config == 'rigid': @@ -390,8 +881,10 @@ def aperture_vibrating_spherical_cap( rad_sphere, rad_cap): r""" - Aperture function for a vibrating cap with radius :math:`r_c` in a rigid - sphere with radius :math:`r_s` [5]_, [6]_ + Aperture function for a vibrating spherical cap. + + The cap has radius :math:`r_c` and is mounted in a rigid sphere with + radius :math:`r_s` [#]_, [#]_ .. math:: @@ -411,28 +904,28 @@ def aperture_vibrating_spherical_cap( ---------- n_max : integer, ndarray Maximal spherical harmonic order - r_sphere : double, ndarray + rad_sphere : double, ndarray Radius of the sphere - r_cap : double + rad_cap : double Radius of the vibrating cap Returns ------- - A : double, ndarray + A : ndarray, float Aperture function in diagonal matrix form with shape :math:`[(n_{max}+1)^2~\times~(n_{max}+1)^2]` References ---------- - .. [5] E. G. Williams, Fourier Acoustics. Academic Press, 1999. - .. [6] B. Rafaely and D. Khaykin, “Optimal Model-Based Beamforming and - Independent Steering for Spherical Loudspeaker Arrays,” IEEE - Transactions on Audio, Speech, and Language Processing, vol. 19, - no. 7, pp. 2234-2238, 2011 + .. [#] E. G. Williams, Fourier Acoustics. Academic Press, 1999. + .. [#] B. Rafaely and D. Khaykin, “Optimal Model-Based Beamforming and + Independent Steering for Spherical Loudspeaker Arrays,” IEEE + Transactions on Audio, Speech, and Language Processing, vol. 19, + no. 7, pp. 2234-2238, 2011 Notes ----- - Eq. (3) in Ref. [6]_ contains an error, here, the power of 2 on pi is + Eq. (3) in the second Ref. contains an error, here, the power of 2 on pi is omitted on the normalization term. """ @@ -448,7 +941,7 @@ def aperture_vibrating_spherical_cap( legendre_plus = special.legendre(n+1)(arg) legendre_term = legendre_minus - legendre_plus for m in range(-n, n+1): - acn = nm2acn(n, m) + acn = nm_to_acn(n, m) aperture[acn, acn] = legendre_term * 4 * np.pi / (2*n+1) return aperture @@ -462,40 +955,42 @@ def radiation_from_sphere( density_medium=1.2, speed_of_sound=343.0): r""" - Radiation function in SH for a vibrating sphere including the radiation - impedance and the propagation to a arbitrary distance from the sphere. + Radiation function in SH for a vibrating spherical cap. + Includes the radiation impedance and the propagation to a arbitrary + distance from the sphere. The sign and phase conventions result in a positive pressure response for a positive cap velocity with the intensity vector pointing away from the - source. + source [#]_, [#]_. TODO: This function does not have a test yet. References ---------- - .. [7] E. G. Williams, Fourier Acoustics. Academic Press, 1999. - .. [8] F. Zotter, A. Sontacchi, and R. Höldrich, “Modeling a spherical + .. [#] E. G. Williams, Fourier Acoustics. Academic Press, 1999. + .. [#] F. Zotter, A. Sontacchi, and R. Höldrich, “Modeling a spherical loudspeaker system as multipole source,” in Proceedings of the 33rd DAGA German Annual Conference on Acoustics, 2007, pp. 221-222. Parameters ---------- - n_max : integer, ndarray + n_max : integer Maximal spherical harmonic order - r_sphere : double, ndarray + rad_sphere : float Radius of the sphere - k : double, ndarray + k : ndarray, float Wave number - distance : double - Distance from the origin - density_medium : double - Density of the medium surrounding the sphere. Default is 1.2 for air. - speed_of_sound : double - Speed of sound in m/s + distance : float + Radial distance from the center of the sphere + density_medium : float + Density of the medium surrounding the sphere. Default is ``1.2``. + for air. + speed_of_sound : float + Speed of sound in m/s. Default is ``343.0``. Returns ------- - R : double, ndarray + R : ndarray, float Radiation function in diagonal matrix form with shape :math:`[K \times (n_{max}+1)^2~\times~(n_{max}+1)^2]` @@ -513,7 +1008,126 @@ def radiation_from_sphere( radiation_order = -1j * hankel/hankel_prime * \ density_medium * speed_of_sound for m in range(-n, n+1): - acn = nm2acn(n, m) + acn = nm_to_acn(n, m) radiation[:, acn, acn] = radiation_order return radiation + + +def sid(n_max): + """Calculates the SID indices up to spherical harmonic order n_max. + The SID indices were originally proposed by Daniel [#]_, more recently + ACN indexing has been favored and is used in the AmbiX format [#]_. + + Parameters + ---------- + n_max : int + The maximum spherical harmonic order + + Returns + ------- + sid_n : ndarray, int + The SID indices for all orders + sid_m : ndarray, int + The SID indices for all degrees + + References + ---------- + .. [#] J. Daniel, “Représentation de champs acoustiques, application à la + transmission et à la reproduction de scènes sonores complexes dans + un contexte multimédia,” Dissertation, l’Université Paris 6, Paris, + 2001. + + .. [#] C. Nachbar, F. Zotter, E. Deleflie, and A. Sontacchi, “Ambix - A + Suggested Ambisonics Format (revised by F. Zotter),” International + Symposium on Ambisonics and Spherical Acoustics, + vol. 3, pp. 1-11, 2011. + + """ + n_sh = (n_max+1)**2 + sid_n = sph_identity_matrix(n_max, 'n-nm').T @ np.arange(0, n_max+1) + sid_m = np.zeros(n_sh, dtype=int) + idx_n = 0 + for n in range(1, n_max+1): + for m in range(1, n+1): + sid_m[idx_n + 2*m-1] = n-m+1 + sid_m[idx_n + 2*m] = -(n-m+1) + sid_m[idx_n + 2*n + 1] = 0 + idx_n += 2*n+1 + + return sid_n, sid_m + + +def sid_to_acn(n_max): + """Convert from SID channel indexing to ACN indeces. + Returns the indices to achieve a corresponding linear ACN indexing. + + Parameters + ---------- + n_max : int + The maximum spherical harmonic order. + + Returns + ------- + acn : ndarray, int + The SID indices sorted according to a respective linear ACN indexing. + """ + sid_n, sid_m = sid(n_max) + linear_sid = nm_to_acn(sid_n, sid_m) + return np.argsort(linear_sid) + + +def sph_identity_matrix(n_max, matrix_type='n-nm'): + """Calculate a spherical harmonic identity matrix. + + Parameters + ---------- + n_max : int + The spherical harmonic order. + matrix_type : str, optional + The type of identity matrix. Currently only ``'n-nm'`` is implemented. + + Returns + ------- + identity_matrix : ndarray, int + The spherical harmonic identity matrix. + + Examples + -------- + The identity matrix can for example be used to decompress from order only + vectors to a full order and degree representation. + + >>> import spharpy + >>> import matplotlib.pyplot as plt + >>> n_max = 2 + >>> E = spharpy.spherical.sph_identity_matrix(n_max, matrix_type='n-nm') + >>> a_n = [1, 2, 3] + >>> a_nm = E.T @ a_n + >>> a_nm + array([1, 2, 2, 2, 3, 3, 3, 3, 3]) + + The matrix E in this case has the following form. + + .. plot:: + + >>> import spharpy + >>> import matplotlib.pyplot as plt + >>> n_max = 2 + >>> E = spharpy.spherical.sph_identity_matrix(n_max, matrix_type='n-nm') + >>> plt.matshow(E, cmap=plt.get_cmap('Greys')) + >>> plt.gca().set_aspect('equal') + + """ # noqa: E501 + n_sh = (n_max+1)**2 + + if matrix_type != 'n-nm': + raise NotImplementedError + + identity_matrix = np.zeros((n_max+1, n_sh), dtype=int) + + for n in range(n_max+1): + m = np.arange(-n, n+1) + linear_nm = nm_to_acn(np.tile(n, m.shape), m) + identity_matrix[n, linear_nm] = 1 + + return identity_matrix diff --git a/spharpy/transforms/__init__.py b/spharpy/transforms/__init__.py index 109cfafd..400a24ec 100644 --- a/spharpy/transforms/__init__.py +++ b/spharpy/transforms/__init__.py @@ -1,3 +1,5 @@ +"""Rotations for spherical harmonics.""" + from .rotations import ( rotation_z_axis, rotation_z_axis_real, diff --git a/spharpy/transforms/rotations.py b/spharpy/transforms/rotations.py index 2adf90e9..6c61e7dc 100644 --- a/spharpy/transforms/rotations.py +++ b/spharpy/transforms/rotations.py @@ -1,5 +1,5 @@ """ -Rotation/Translation operations for data in the spherical harmonic domains +Rotation/Translation operations for data in the spherical harmonic domains. """ import numpy as np @@ -14,7 +14,7 @@ class RotationSH(Rotation): """ def __init__(self, quat, n_max=0, *args, **kwargs): - """Initialize + """Initialize. Parameters ---------- @@ -23,7 +23,13 @@ def __init__(self, quat, n_max=0, *args, **kwargs): (x, y, z, w) format. Each quaternion will be normalized to unit norm. n_max : int - The spherical harmonic order + The spherical harmonic order. Default is ``0``. + *args : optional + Arguments are passed to + :py:class:`~scipy.spatial.transform.Rotation` + **kwargs : optional + Keyword arguments are passed to + :py:class:`~scipy.spatial.transform.Rotation` Returns ------- @@ -56,7 +62,15 @@ def from_rotvec(cls, n_max, rotvec, degrees=False, *args, **kwargs): ith rotation vector. degrees : bool, optional Specify if rotation angles are defined in degrees instead of - radians, by default False. + radians, by default ``False``. + *args : optional + Arguments are passed to + :py:meth:`Rotation.from_rotvec + ` + **kwargs : optional + Keyword arguments are passed to + :py:meth:`Rotation.from_rotvec + ` Returns ------- @@ -124,7 +138,11 @@ def from_euler(cls, n_max, seq, angles, degrees=False, **kwargs): degrees : bool, optional If True, then the given angles are assumed to be in degrees. - Default is False. + Default is ``False``. + **kwargs : optional + Keyword arguments are passed to + :py:meth:`Rotation.from_euler + ` Returns ------- @@ -162,6 +180,10 @@ def from_quat(cls, n_max, quat, **kwargs): Each row is a (possibly non-unit norm) quaternion in scalar-last (x, y, z, w) format. Each quaternion will be normalized to unit norm. + **kwargs : optional + Keyword arguments are passed to + :py:meth:`Rotation.from_quat + ` Returns ------- @@ -197,6 +219,10 @@ def from_matrix(cls, n_max, matrix, **kwargs): matrix : (array_like, shape (N, 3, 3) or (3, 3)) A single matrix or a stack of matrices, where ``matrix[i]`` is the i-th matrix. + **kwargs : optional + Keyword arguments are passed to + :py:meth:`Rotation.from_matrix + ` Returns ------- @@ -233,7 +259,7 @@ def n_max(self): @n_max.setter def n_max(self, value): - """Set the spherical harmonic order + """Set the spherical harmonic order. Parameters ---------- @@ -245,15 +271,18 @@ def n_max(self, value): raise ValueError("The order needs to be a positive value.") self._n_max = value - def as_spherical_harmonic(self, type='real'): + def as_spherical_harmonic(self, basis_type='real'): """Export the rotation operations as a spherical harmonic rotation matrices. Supports complex and real-valued spherical harmonics. Parameters ---------- - real : string, optional + basis_type : string, optional Spherical harmonic definition. Can either be 'complex' or 'real', by default 'real' is used. + type : str, optional + Type of spherical harmonic basis, either ``'complex'`` or + ``'real'``. The default is ``'real'``. Returns ------- @@ -264,14 +293,15 @@ def as_spherical_harmonic(self, type='real'): n_matrices = euler_angles.shape[0] n_sh = (self.n_max+1)**2 - if type == 'real': + if basis_type == 'real': dtype = float rot_func = wigner_d_rotation_real - elif type == 'complex': + elif basis_type == 'complex': dtype = complex rot_func = wigner_d_rotation else: - raise ValueError("Invalid spherical harmonic type {}".format(type)) + raise ValueError( + "Invalid spherical harmonic type {}".format(basis_type)) D = np.zeros((n_matrices, n_sh, n_sh), dtype=dtype) @@ -281,21 +311,24 @@ def as_spherical_harmonic(self, type='real'): return np.squeeze(D) - def apply(self, coefficients, type='real'): - """Apply the rotation to L sets of spherical harmonic coefficients + def apply(self, coefficients, basis_type='real'): + """Apply the rotation to L sets of spherical harmonic coefficients. Parameters ---------- coefficients : array, complex, shape :math:`((n_max+1)^2, L)` L sets of spherical harmonic coefficients with a respective order :math:`((n_max+1)^2` + basis_type : string, optional + Spherical harmonic definition. Can either be ``'complex'`` or + ``'real'``, by default ``'real'`` is used. Returns ------- array, complex The rotated data """ - D = self.as_spherical_harmonic(type=type) + D = self.as_spherical_harmonic(basis_type=basis_type) if D.ndim > 2: M = np.diag(np.ones((self.n_max+1)**2)) for d in D: @@ -307,13 +340,13 @@ def apply(self, coefficients, type='real'): def rotation_z_axis(n_max, angle): - """Rotation matrix for complex spherical harmonics around the z-axis + r"""Rotation matrix for complex spherical harmonics around the z-axis by a given angle. The rotation is performed such that positive angles result in a counter clockwise rotation of the data [#]_. .. math:: - c_{nm}(\\theta, \\phi + \\xi) = e^{-im\\xi} c_{nm}(\\theta, \\phi) + c_{nm}(\theta, \phi + \xi) = e^{-im\xi} c_{nm}(\theta, \phi) Parameters ---------- @@ -345,14 +378,14 @@ def rotation_z_axis(n_max, angle): """ acn = np.arange(0, (n_max+1)**2) - m = spharpy.spherical.acn2nm(acn)[1] + m = spharpy.spherical.acn_to_nm(acn)[1] rotation_phi = np.exp(-1j*angle*m) return np.diag(rotation_phi) def rotation_z_axis_real(n_max, angle): - """Rotation matrix for real-valued spherical harmonics around the z-axis + r"""Rotation matrix for real-valued spherical harmonics around the z-axis by a given angle. The rotation is performed such that positive angles result in a counter clockwise rotation of the data [#]_. @@ -361,7 +394,7 @@ def rotation_z_axis_real(n_max, angle): n_max : integer Spherical harmonic order angle : number - Rotation angle in radians `[0, 2 \\pi]` + Rotation angle in radians `[0, 2 \pi]` Returns ------- @@ -386,7 +419,7 @@ def rotation_z_axis_real(n_max, angle): """ acn = np.arange(0, (n_max + 1) ** 2) - n, m = spharpy.spherical.acn2nm(acn) + n, m = spharpy.spherical.acn_to_nm(acn) acn_reverse_degree = n ** 2 + n - m rotation_phi = np.zeros(((n_max + 1) ** 2, (n_max + 1) ** 2)) @@ -458,9 +491,9 @@ def wigner_d_rotation(n_max, alpha, beta, gamma): R = np.zeros((n_sh, n_sh), dtype=complex) for row in np.arange(0, (n_max+1)**2): - n_dash, m_dash = spharpy.spherical.acn2nm(row) + n_dash, m_dash = spharpy.spherical.acn_to_nm(row) for column in np.arange(0, (n_max+1)**2): - n, m = spharpy.spherical.acn2nm(column) + n, m = spharpy.spherical.acn_to_nm(column) if n == n_dash: rot_alpha = np.exp(-1j*m_dash*alpha) rot_beta = wigner_d_function(n, m_dash, m, beta) @@ -515,8 +548,8 @@ def wigner_d_rotation_real(n_max, alpha, beta, gamma): for row_acn in np.arange(0, (n_max+1)**2): for col_acn in np.arange(0, (n_max+1)**2): - n, m = spharpy.spherical.acn2nm(col_acn) - n_dash, m_dash = spharpy.spherical.acn2nm(row_acn) + n, m = spharpy.spherical.acn_to_nm(col_acn) + n_dash, m_dash = spharpy.spherical.acn_to_nm(row_acn) if n == n_dash: # minus beta opposite rotation direction d_l_1 = wigner_d_function(n, np.abs(m_dash), np.abs(m), -beta) @@ -533,7 +566,7 @@ def wigner_d_rotation_real(n_max, alpha, beta, gamma): def _sign(x): """ - Returns sign of x, differs from numpy definition for x=0 + Returns sign of x, differs from numpy definition for x=0. """ if x < 0: sign = -1 @@ -547,7 +580,7 @@ def _Phi(m, angle): """ Rotation Matrix around z-axis for real Spherical Harmonics as defined in Blanco et al., Evaluation of the rotation matrices in the basis of real - spherical harmonics, eq.(8) + spherical harmonics, eq.(8). """ if m > 0: phi = np.sqrt(2)*np.cos(m*angle) diff --git a/tests/ballon_plot.py b/tests/ballon_plot.py deleted file mode 100644 index c6224143..00000000 --- a/tests/ballon_plot.py +++ /dev/null @@ -1,63 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import matplotlib.cm as cmap - -import spharpy -import cartopy.crs as ccrs - -sampling = spharpy.samplings.equalarea(20, condition_num=np.inf) -# sampling = spharpy.samplings.hyperinterpolation(15) -Y = spharpy.spherical.spherical_harmonic_basis_real(15, sampling) - -# plt.figure() -# spharpy.plot.contour_map(sampling, (Y[:, spharpy.spherical.nm2acn(4, 2)]), cmap=cmap.Spectral_r) -# -# plt.figure() -# spharpy.plot.contour_map(sampling, (Y[:, 4]), cmap=cmap.RdBu_r, projection=ccrs.Mollweide()) - - -# plt.figure(figsize=(5, 5)) -# plot = spharpy.plot.balloon_wireframe(sampling, Y[:, 1], phase=False, cmap=cmap.RdBu_r) - - - -# plt.figure(figsize=(5,5)) -# plot = spharpy.plot.balloon(sampling, Y[:, 1], phase=False, cmap=cmap.RdBu_r) - - -# plt.figure(figsize=(5,5)) -# plot = spharpy.plot.balloon_wireframe(sampling, Y[:, 7], phase=False, cmap=cmap.RdBu_r, colorbar=False) - - -# spharpy.plot.contour_map(sampling, Y[:, 7], cmap=cmap.RdBu_r, projection=ccrs.Sinusoidal()) -plt.figure() -spharpy.plot.contour(sampling, Y[:, 7], cmap=cmap.RdBu_r) - -plt.figure() -spharpy.plot.contour_map(sampling, Y[:, 7], cmap=cmap.RdBu_r) - -plt.figure() -spharpy.plot.pcolor_map(sampling, Y[:, 7], cmap=cmap.RdBu_r, limits=[-0.3, 1]) - -# plt.figure() -# spharpy.plot.contour_map(sampling, Y[:, 2], cmap=cmap.RdBu_r, projection=ccrs.Sinusoidal()) -# -# plt.figure() -# spharpy.plot.contour_map(sampling, Y[:, 2], cmap=cmap.RdBu_r, projection=ccrs.PlateCarree()) - - - -# -# plt.figure(figsize=(5,5)) -# spharpy.plot.balloon_interp(sampling, Y[:, 1]) - -# -# Y = spharpy.spherical.spherical_harmonic_basis(15, sampling) -# -# plt.figure(figsize=(5,5)) -# plot = spharpy.plot.balloon_wireframe(sampling, Y[:, 1], phase=False,colorbar=False) -# -# plt.figure(figsize=(5,5)) -# plot = spharpy.plot.balloon(sampling, Y[:, 1], phase=True) -# -# diff --git a/tests/conftest.py b/tests/conftest.py index 9b8b5320..1cc839b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import pytest -from spharpy.samplings import Coordinates import pyfar as pf import numpy as np +from spharpy import SamplingSphere @pytest.fixture @@ -14,17 +14,15 @@ def create_coordinates( if implementation == 'pyfar': return pf.Coordinates( - phi, theta, rad, domain='sph', convention='top_colat' - ) + phi, theta, rad, domain='sph', convention='top_colat') elif implementation == 'spharpy': - return Coordinates.from_spherical(rad, theta, phi) + return SamplingSphere.from_spherical_colatitude( + phi, theta, rad) + return Factory - yield Factory - -@pytest.fixture -def icosahedron(): - """Return the coordinate points of an icosahedron in spherical coordinates +def icosahedron_points(): + """Return the coordinate points of an icosahedron in spherical coordinates. Returns ------- @@ -49,3 +47,52 @@ def icosahedron(): rad = np.ones(20) return rad, theta, phi + + +@pytest.fixture +def icosahedron(): + """Return the coordinate points of an icosahedron in spherical coordinates. + + Returns + ------- + rad : float, ndarray + The radius in meters. + theta : float, ndarray + The colatitude angle in radians. + phi : float, ndarray + The azimuth angle in radians. + """ + rad, theta, phi = icosahedron_points() + return rad, theta, phi + + +@pytest.fixture +def icosahedron_sampling(): + """Return the coordinate points of an icosahedron in spherical coordinates. + + Returns + ------- + rad : float, ndarray + The radius in meters. + theta : float, ndarray + The colatitude angle in radians. + phi : float, ndarray + The azimuth angle in radians. + """ + rad, theta, phi = icosahedron_points() + weights = np.ones_like(rad) * 4 * np.pi / len(rad) + return SamplingSphere.from_spherical_colatitude( + phi, theta, rad, weights=weights, n_max=2) + + +@pytest.fixture +def download_sampling(): + def download_sampling(kind, degree): + if kind in ['extremal', 'hyperinterpolation']: + from spharpy.samplings.samplings import _sph_extremal_load_data + return _sph_extremal_load_data(degree) + elif kind == 't-design': + from spharpy.samplings.samplings import _sph_t_design_load_data + return _sph_t_design_load_data(degree) + + return download_sampling diff --git a/tests/data/Y_cmplx_Condon-Shortley_maxN_acn.csv b/tests/data/Y_cmplx_Condon-Shortley_maxN_acn.csv new file mode 100644 index 00000000..aca22d8d --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_maxN_acn.csv @@ -0,0 +1,3 @@ + (1.9947114020072e-01+0.0000000000000e+00j), (1.9947114020072e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.9947114020072e-01+0.0000000000000e+00j) + (1.9947114020072e-01+0.0000000000000e+00j), (1.2214084668454e-17-1.9947114020072e-01j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.2214084668454e-17-1.9947114020072e-01j) + (1.9947114020072e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_Condon-Shortley_maxN_fuma.csv b/tests/data/Y_cmplx_Condon-Shortley_maxN_fuma.csv new file mode 100644 index 00000000..cc91a15c --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_maxN_fuma.csv @@ -0,0 +1,3 @@ + (1.9947114020072e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.9947114020072e-01+0.0000000000000e+00j), (1.9947114020072e-01+0.0000000000000e+00j) + (1.9947114020072e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.2214084668454e-17-1.9947114020072e-01j), (1.2214084668454e-17-1.9947114020072e-01j) + (1.9947114020072e-01+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_Condon-Shortley_n3d_acn.csv b/tests/data/Y_cmplx_Condon-Shortley_n3d_acn.csv new file mode 100644 index 00000000..72f76b43 --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_n3d_acn.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (3.4549414947134e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-3.4549414947134e-01+0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (2.1155415213710e-17-3.4549414947134e-01j), (-0.0000000000000e+00+0.0000000000000e+00j), (-2.1155415213710e-17-3.4549414947134e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (4.8860251190292e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_Condon-Shortley_n3d_fuma.csv b/tests/data/Y_cmplx_Condon-Shortley_n3d_fuma.csv new file mode 100644 index 00000000..d55e25b5 --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_n3d_fuma.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-3.4549414947134e-01+0.0000000000000e+00j), (3.4549414947134e-01+0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-2.1155415213710e-17-3.4549414947134e-01j), (2.1155415213710e-17-3.4549414947134e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (4.8860251190292e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_Condon-Shortley_sn3d_acn.csv b/tests/data/Y_cmplx_Condon-Shortley_sn3d_acn.csv new file mode 100644 index 00000000..95407c12 --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_sn3d_acn.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (1.9947114020072e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.9947114020072e-01+0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (1.2214084668454e-17-1.9947114020072e-01j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.2214084668454e-17-1.9947114020072e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_Condon-Shortley_sn3d_fuma.csv b/tests/data/Y_cmplx_Condon-Shortley_sn3d_fuma.csv new file mode 100644 index 00000000..57acbc1e --- /dev/null +++ b/tests/data/Y_cmplx_Condon-Shortley_sn3d_fuma.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.9947114020072e-01+0.0000000000000e+00j), (1.9947114020072e-01+0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (-1.2214084668454e-17-1.9947114020072e-01j), (1.2214084668454e-17-1.9947114020072e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_maxN_acn.csv b/tests/data/Y_cmplx_None_maxN_acn.csv new file mode 100644 index 00000000..11f3cda9 --- /dev/null +++ b/tests/data/Y_cmplx_None_maxN_acn.csv @@ -0,0 +1,3 @@ + (1.9947114020072e-01+0.0000000000000e+00j), (-1.9947114020072e-01-0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.9947114020072e-01-0.0000000000000e+00j) + (1.9947114020072e-01+0.0000000000000e+00j), (-1.2214084668454e-17+1.9947114020072e-01j), (0.0000000000000e+00+0.0000000000000e+00j), (1.2214084668454e-17+1.9947114020072e-01j) + (1.9947114020072e-01+0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_maxN_fuma.csv b/tests/data/Y_cmplx_None_maxN_fuma.csv new file mode 100644 index 00000000..0e5bedb1 --- /dev/null +++ b/tests/data/Y_cmplx_None_maxN_fuma.csv @@ -0,0 +1,3 @@ + (1.9947114020072e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.9947114020072e-01-0.0000000000000e+00j), (-1.9947114020072e-01-0.0000000000000e+00j) + (1.9947114020072e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.2214084668454e-17+1.9947114020072e-01j), (-1.2214084668454e-17+1.9947114020072e-01j) + (1.9947114020072e-01+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_n3d_acn.csv b/tests/data/Y_cmplx_None_n3d_acn.csv new file mode 100644 index 00000000..dd52eeb0 --- /dev/null +++ b/tests/data/Y_cmplx_None_n3d_acn.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (-3.4549414947134e-01-0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (3.4549414947134e-01-0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (-2.1155415213710e-17+3.4549414947134e-01j), (0.0000000000000e+00+0.0000000000000e+00j), (2.1155415213710e-17+3.4549414947134e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j), (4.8860251190292e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_n3d_fuma.csv b/tests/data/Y_cmplx_None_n3d_fuma.csv new file mode 100644 index 00000000..701d1df2 --- /dev/null +++ b/tests/data/Y_cmplx_None_n3d_fuma.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (3.4549414947134e-01-0.0000000000000e+00j), (-3.4549414947134e-01-0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (2.1155415213710e-17+3.4549414947134e-01j), (-2.1155415213710e-17+3.4549414947134e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (4.8860251190292e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_sn3d_acn.csv b/tests/data/Y_cmplx_None_sn3d_acn.csv new file mode 100644 index 00000000..04401cc1 --- /dev/null +++ b/tests/data/Y_cmplx_None_sn3d_acn.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (-1.9947114020072e-01-0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.9947114020072e-01-0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (-1.2214084668454e-17+1.9947114020072e-01j), (0.0000000000000e+00+0.0000000000000e+00j), (1.2214084668454e-17+1.9947114020072e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_cmplx_None_sn3d_fuma.csv b/tests/data/Y_cmplx_None_sn3d_fuma.csv new file mode 100644 index 00000000..565f8cef --- /dev/null +++ b/tests/data/Y_cmplx_None_sn3d_fuma.csv @@ -0,0 +1,3 @@ + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.9947114020072e-01-0.0000000000000e+00j), (-1.9947114020072e-01-0.0000000000000e+00j) + (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00+0.0000000000000e+00j), (1.2214084668454e-17+1.9947114020072e-01j), (-1.2214084668454e-17+1.9947114020072e-01j) + (2.8209479177388e-01+0.0000000000000e+00j), (2.8209479177388e-01+0.0000000000000e+00j), (0.0000000000000e+00-0.0000000000000e+00j), (-0.0000000000000e+00-0.0000000000000e+00j) diff --git a/tests/data/Y_real_Condon-Shortley_maxN_acn.csv b/tests/data/Y_real_Condon-Shortley_maxN_acn.csv new file mode 100644 index 00000000..ae867033 --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_maxN_acn.csv @@ -0,0 +1,3 @@ +1.9947114020072e-01,0.0000000000000e+00,-0.0000000000000e+00,-2.8209479177388e-01 +1.9947114020072e-01,-2.8209479177388e-01,-0.0000000000000e+00,-1.7273324190101e-17 +1.9947114020072e-01,0.0000000000000e+00,2.8209479177388e-01,-0.0000000000000e+00 diff --git a/tests/data/Y_real_Condon-Shortley_maxN_fuma.csv b/tests/data/Y_real_Condon-Shortley_maxN_fuma.csv new file mode 100644 index 00000000..7f9f318b --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_maxN_fuma.csv @@ -0,0 +1,3 @@ +1.9947114020072e-01,-0.0000000000000e+00,-2.8209479177388e-01,0.0000000000000e+00 +1.9947114020072e-01,-0.0000000000000e+00,-1.7273324190101e-17,-2.8209479177388e-01 +1.9947114020072e-01,2.8209479177388e-01,-0.0000000000000e+00,0.0000000000000e+00 diff --git a/tests/data/Y_real_Condon-Shortley_n3d_acn.csv b/tests/data/Y_real_Condon-Shortley_n3d_acn.csv new file mode 100644 index 00000000..5711b88f --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_n3d_acn.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,0.0000000000000e+00,-0.0000000000000e+00,-4.8860251190292e-01 +2.8209479177388e-01,-4.8860251190292e-01,-0.0000000000000e+00,-2.9918275112863e-17 +2.8209479177388e-01,0.0000000000000e+00,4.8860251190292e-01,-0.0000000000000e+00 diff --git a/tests/data/Y_real_Condon-Shortley_n3d_fuma.csv b/tests/data/Y_real_Condon-Shortley_n3d_fuma.csv new file mode 100644 index 00000000..4d9ed924 --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_n3d_fuma.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,-4.8860251190292e-01,0.0000000000000e+00 +2.8209479177388e-01,-0.0000000000000e+00,-2.9918275112863e-17,-4.8860251190292e-01 +2.8209479177388e-01,4.8860251190292e-01,-0.0000000000000e+00,0.0000000000000e+00 diff --git a/tests/data/Y_real_Condon-Shortley_sn3d_acn.csv b/tests/data/Y_real_Condon-Shortley_sn3d_acn.csv new file mode 100644 index 00000000..633afc5b --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_sn3d_acn.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,0.0000000000000e+00,-0.0000000000000e+00,-2.8209479177388e-01 +2.8209479177388e-01,-2.8209479177388e-01,-0.0000000000000e+00,-1.7273324190101e-17 +2.8209479177388e-01,0.0000000000000e+00,2.8209479177388e-01,-0.0000000000000e+00 diff --git a/tests/data/Y_real_Condon-Shortley_sn3d_fuma.csv b/tests/data/Y_real_Condon-Shortley_sn3d_fuma.csv new file mode 100644 index 00000000..3aeb706f --- /dev/null +++ b/tests/data/Y_real_Condon-Shortley_sn3d_fuma.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,-2.8209479177388e-01,0.0000000000000e+00 +2.8209479177388e-01,-0.0000000000000e+00,-1.7273324190101e-17,-2.8209479177388e-01 +2.8209479177388e-01,2.8209479177388e-01,-0.0000000000000e+00,0.0000000000000e+00 diff --git a/tests/data/Y_real_None_maxN_acn.csv b/tests/data/Y_real_None_maxN_acn.csv new file mode 100644 index 00000000..5572e982 --- /dev/null +++ b/tests/data/Y_real_None_maxN_acn.csv @@ -0,0 +1,3 @@ +1.9947114020072e-01,-0.0000000000000e+00,-0.0000000000000e+00,2.8209479177388e-01 +1.9947114020072e-01,2.8209479177388e-01,-0.0000000000000e+00,1.7273324190101e-17 +1.9947114020072e-01,-0.0000000000000e+00,2.8209479177388e-01,0.0000000000000e+00 diff --git a/tests/data/Y_real_None_maxN_fuma.csv b/tests/data/Y_real_None_maxN_fuma.csv new file mode 100644 index 00000000..4d646ee7 --- /dev/null +++ b/tests/data/Y_real_None_maxN_fuma.csv @@ -0,0 +1,3 @@ +1.9947114020072e-01,-0.0000000000000e+00,2.8209479177388e-01,-0.0000000000000e+00 +1.9947114020072e-01,-0.0000000000000e+00,1.7273324190101e-17,2.8209479177388e-01 +1.9947114020072e-01,2.8209479177388e-01,0.0000000000000e+00,-0.0000000000000e+00 diff --git a/tests/data/Y_real_None_n3d_acn.csv b/tests/data/Y_real_None_n3d_acn.csv new file mode 100644 index 00000000..07f3df3a --- /dev/null +++ b/tests/data/Y_real_None_n3d_acn.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,-0.0000000000000e+00,4.8860251190292e-01 +2.8209479177388e-01,4.8860251190292e-01,-0.0000000000000e+00,2.9918275112863e-17 +2.8209479177388e-01,-0.0000000000000e+00,4.8860251190292e-01,0.0000000000000e+00 diff --git a/tests/data/Y_real_None_n3d_fuma.csv b/tests/data/Y_real_None_n3d_fuma.csv new file mode 100644 index 00000000..91d798a4 --- /dev/null +++ b/tests/data/Y_real_None_n3d_fuma.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,4.8860251190292e-01,-0.0000000000000e+00 +2.8209479177388e-01,-0.0000000000000e+00,2.9918275112863e-17,4.8860251190292e-01 +2.8209479177388e-01,4.8860251190292e-01,0.0000000000000e+00,-0.0000000000000e+00 diff --git a/tests/data/Y_real_None_sn3d_acn.csv b/tests/data/Y_real_None_sn3d_acn.csv new file mode 100644 index 00000000..13a79c17 --- /dev/null +++ b/tests/data/Y_real_None_sn3d_acn.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,-0.0000000000000e+00,2.8209479177388e-01 +2.8209479177388e-01,2.8209479177388e-01,-0.0000000000000e+00,1.7273324190101e-17 +2.8209479177388e-01,-0.0000000000000e+00,2.8209479177388e-01,0.0000000000000e+00 diff --git a/tests/data/Y_real_None_sn3d_fuma.csv b/tests/data/Y_real_None_sn3d_fuma.csv new file mode 100644 index 00000000..260cc426 --- /dev/null +++ b/tests/data/Y_real_None_sn3d_fuma.csv @@ -0,0 +1,3 @@ +2.8209479177388e-01,-0.0000000000000e+00,2.8209479177388e-01,-0.0000000000000e+00 +2.8209479177388e-01,-0.0000000000000e+00,1.7273324190101e-17,2.8209479177388e-01 +2.8209479177388e-01,2.8209479177388e-01,0.0000000000000e+00,-0.0000000000000e+00 diff --git a/tests/test_acn_nm_indexing.py b/tests/test_acn_nm_indexing.py deleted file mode 100644 index 8e97d6c5..00000000 --- a/tests/test_acn_nm_indexing.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Tests for acn and order, degree indexing functions -""" - -import pytest -import spharpy.spherical as sh -import numpy as np - -def test_acn2nm_single_val(): - n,m = sh.acn2nm(0) - assert n == 0 - assert m == 0 - - n, m = sh.acn2nm(2) - assert n == 1 - assert m == 0 - - n, m = sh.acn2nm(1) - assert n == 1 - assert m == -1 - - -def test_nm2acn_single_val(): - acn = sh.nm2acn(0, 0) - assert acn == 0 - - acn = sh.nm2acn(1, 0) - assert acn == 2 - - acn = sh.nm2acn(1, -1) - assert acn == 1 - -def test_acn2nm_array(): - n_ref = np.array([0, 1, 1, 1]) - m_ref = np.array([0, -1, 0, 1]) - - n_max = 1 - n_sh = (n_max + 1)**2 - acn = np.arange(0, n_sh) - n, m = sh.acn2nm(acn) - - np.testing.assert_equal(n, n_ref) - np.testing.assert_equal(m, m_ref) - -def test_nm2acn_array(): - n = np.array([0, 1, 1, 1]) - m = np.array([0, -1, 0, 1]) - - n_max = 1 - n_sh = (n_max + 1)**2 - acn_ref = np.arange(0, n_sh) - - acn = sh.nm2acn(n, m) - - np.testing.assert_equal(acn, acn_ref) diff --git a/tests/test_aperture.py b/tests/test_aperture.py index 67a14467..46378981 100644 --- a/tests/test_aperture.py +++ b/tests/test_aperture.py @@ -1,9 +1,9 @@ """ -Tests for the aperture function of a spherical loudspeaker array +Tests for the aperture function of a spherical loudspeaker array. """ import spharpy.spherical as sh from scipy.special import legendre -from spharpy.indexing import sph_identity_matrix +from spharpy.spherical import sph_identity_matrix import numpy as np diff --git a/tests/test_beamforming.py b/tests/test_beamforming.py index bc4b184e..64baf5d5 100644 --- a/tests/test_beamforming.py +++ b/tests/test_beamforming.py @@ -1,6 +1,7 @@ import numpy as np import numpy.testing as npt import pytest +import pyfar as pf import spharpy @@ -36,7 +37,7 @@ def test_re_max(): g_nm_norm = spharpy.beamforming.rE_max_weights(N, normalize=True) Y = spharpy.spherical.spherical_harmonic_basis_real( - N, spharpy.samplings.Coordinates(1, 0, 0)) + N, pf.Coordinates(1, 0, 0)) npt.assert_allclose(Y @ np.diag(g_nm_norm) @ Y.T, 1) @@ -47,7 +48,7 @@ def test_max_front_back(): N, normalize=True) Y = spharpy.spherical.spherical_harmonic_basis_real( - N, spharpy.samplings.Coordinates(1, 0, 0)) + N, pf.Coordinates(1, 0, 0)) npt.assert_allclose(Y @ np.diag(f_nm_norm) @ Y.T, 1) diff --git a/tests/test_coordinate_transforms.py b/tests/test_coordinate_transforms.py index 68350aa8..7cd47935 100644 --- a/tests/test_coordinate_transforms.py +++ b/tests/test_coordinate_transforms.py @@ -1,54 +1,17 @@ -""" Tests for coordinate transforms """ +"""Tests for coordinate transforms.""" -import pytest import numpy as np +import pyfar as pf import spharpy.samplings as samplings from spharpy.samplings import spherical_voronoi -def test_sph2cart(): - rad, theta, phi = 1, np.pi/2, 0 - x, y, z = samplings.sph2cart(rad, theta, phi) - (1, 0, 0) == pytest.approx((x, y, z), abs=2e-16, rel=2e-16) - - -def test_sph2cart_array(): - rad = np.ones(6) - theta = np.array([np.pi/2, np.pi/2, np.pi/2, np.pi/2, 0, np.pi]) - phi = np.array([0, np.pi, np.pi/2, np.pi*3/2, 0, 0]) - x, y, z = samplings.sph2cart(rad, theta, phi) - - xx = np.array([1, -1, 0, 0, 0, 0]) - yy = np.array([0, 0, 1, -1, 0, 0]) - zz = np.array([0, 0, 0, 0, 1, -1]) - - np.testing.assert_allclose(xx, x, atol=1e-15) - np.testing.assert_allclose(yy, y, atol=1e-15) - np.testing.assert_allclose(zz, z, atol=1e-15) - - -def test_cart2sph_array(): - x = np.array([1, -1, 0, 0, 0, 0]) - y = np.array([0, 0, 1, -1, 0, 0]) - z = np.array([0, 0, 0, 0, 1, -1]) - - rr, tt, pp = samplings.cart2sph(x, y, z) - - rad = np.ones(6) - theta = np.array([np.pi/2, np.pi/2, np.pi/2, np.pi/2, 0, np.pi]) - phi = np.array([0, np.pi, np.pi/2, np.pi*3/2, 0, 0]) - - np.testing.assert_allclose(rad, rr, atol=1e-15) - np.testing.assert_allclose(phi, pp, atol=1e-15) - np.testing.assert_allclose(theta, tt, atol=1e-15) - - def test_cart2latlon_array(): x = np.array([1, -1, 0, 0, 0, 0]) y = np.array([0, 0, 1, -1, 0, 0]) z = np.array([0, 0, 0, 0, 1, -1]) - - rr, tt, pp = samplings.cart2latlon(x, y, z) + coords = pf.Coordinates(x, y, z) + rr, tt, pp = samplings.helpers.coordinates2latlon(coords) rad = np.ones(6) theta = np.array([0, 0, 0, 0, np.pi/2, -np.pi/2]) @@ -59,24 +22,8 @@ def test_cart2latlon_array(): np.testing.assert_allclose(theta, tt, atol=1e-15) -def test_latlon2cart_array(): - height = np.ones(6) - theta = np.array([0, 0, 0, 0, np.pi/2, -np.pi/2]) - phi = np.array([0, np.pi, np.pi/2, -np.pi/2, 0, 0]) - xx, yy, zz = samplings.latlon2cart(height, theta, phi) - - x = np.array([1, -1, 0, 0, 0, 0]) - y = np.array([0, 0, 1, -1, 0, 0]) - z = np.array([0, 0, 0, 0, 1, -1]) - - np.testing.assert_allclose(xx, x, atol=1e-15) - np.testing.assert_allclose(yy, y, atol=1e-15) - np.testing.assert_allclose(zz, z, atol=1e-15) - - def test_sph_voronoi(): - s = samplings.coordinates.SamplingSphere.from_coordinates( - samplings.dodecahedron()) + s = samplings.dodecahedron() verts = np.array([[ 8.72677996e-01, -3.56822090e-01, 3.33333333e-01], [ 3.33333333e-01, -5.77350269e-01, 7.45355992e-01], [ 7.45355992e-01, -5.77350269e-01, -3.33333333e-01], diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py index b6d6cf42..f45d22ec 100644 --- a/tests/test_coordinates.py +++ b/tests/test_coordinates.py @@ -1,284 +1,9 @@ +from spharpy import SamplingSphere +from pyfar import Coordinates import numpy as np import numpy.testing as npt import pytest - -from spharpy.samplings import cart2latlon, cart2sph -from spharpy.samplings.coordinates import Coordinates, SamplingSphere - -import pyfar as pf - - -def test_coordinates_init(): - coords = Coordinates() - assert isinstance(coords, Coordinates) - - -def test_coordinates_init_val(): - - coords = Coordinates(1, 0, 0) - assert isinstance(coords, Coordinates) - - -def test_to_pyfar(): - coords = Coordinates(1, 0, 0) - pyfar_coords = coords.to_pyfar() - np.testing.assert_allclose(pyfar_coords.get_cart(), coords.cartesian.T) - - -def test_from_pyfar(): - pyfar_coords = pf.Coordinates(1, 0, 0) - spharpy_coords = Coordinates.from_pyfar(pyfar_coords) - np.testing.assert_allclose( - pyfar_coords.get_cart(), spharpy_coords.cartesian.T) - - -def test_coordinates_init_incomplete(): - x = [1, 2] - y = 1 - z = 1 - with pytest.raises(ValueError): - Coordinates(x, y, z) - pytest.fail("Input arrays need to have same dimensions.") - - -def test_coordinates_init_from_cartesian(): - x = 1 - y = 0 - z = 0 - coords = Coordinates.from_cartesian(x, y, z) - npt.assert_allclose(coords._x, x) - npt.assert_allclose(coords._y, y) - npt.assert_allclose(coords._z, z) - - -def test_coordinates_init_from_spherical(): - x = 1 - y = 0 - z = 0 - rad, theta, phi = cart2sph(x, y, z) - coords = Coordinates.from_spherical(rad, theta, phi) - # use atol here because of numerical rounding issues introduced in - # the coordinate conversion - npt.assert_allclose(coords._x, x, atol=1e-15) - npt.assert_allclose(coords._y, y, atol=1e-15) - npt.assert_allclose(coords._z, z, atol=1e-15) - -def test_coordinates_init_from_array_spherical(): - rad = [1., 1., 1., 1.] - ele = [np.pi/2, np.pi/2, 0, np.pi/2] - azi = [0, np.pi/2, 0, np.pi/4] - - points = np.array([rad, ele, azi]) - coords = Coordinates.from_array(points, coordinate_system='spherical') - - npt.assert_allclose(coords.radius, rad, atol=1e-15) - npt.assert_allclose(coords.elevation, ele, atol=1e-15) - npt.assert_allclose(coords.azimuth, azi, atol=1e-15) - -def test_coordinates_init_from_array_cartesian(): - x = [1, 0, 0, 0] - y = [0, 1, 0, 0] - z = [0, 0, 1, 0] - - points = np.array([x, y, z]) - coords = Coordinates.from_array(points) - - npt.assert_allclose(coords._x, x, atol=1e-15) - npt.assert_allclose(coords._y, y, atol=1e-15) - npt.assert_allclose(coords._z, z, atol=1e-15) - -def test_getter_x(): - x = np.array([1, 0], dtype=float) - coords = Coordinates() - coords._x = x - npt.assert_allclose(coords.x, x) - -def test_getter_y(): - y = np.array([1, 0], dtype=float) - coords = Coordinates() - coords._y = y - npt.assert_allclose(coords.y, y) - -def test_getter_z(): - z = np.array([1, 0], dtype=float) - coords = Coordinates() - coords._z = z - npt.assert_allclose(coords.z, z) - -def test_setter_x(): - value = np.array([1.0, 1], dtype=float) - coords = Coordinates() - coords.x = value - npt.assert_allclose(value, coords._x) - -def test_setter_y(): - value = np.array([1.0, 1], dtype=float) - coords = Coordinates() - coords.y = value - npt.assert_allclose(value, coords._y) - -def test_setter_z(): - value = np.array([1.0, 1], dtype=float) - coords = Coordinates() - coords.z = value - npt.assert_allclose(value, coords._z) - -def test_getter_ele(): - value = np.pi/2 - coords = Coordinates() - coords.z = 0 - coords.y = 0 - coords.x = 1 - npt.assert_allclose(coords.elevation, value) - -def test_getter_radius(): - value = 1 - coords = Coordinates() - coords.z = 0 - coords.y = 1 - coords.x = 0 - npt.assert_allclose(coords.radius, value) - -def test_getter_azi(): - azi = np.pi/2 - coords = Coordinates() - coords.z = 0 - coords.y = 1 - coords.x = 0 - npt.assert_allclose(coords.azimuth, azi) - -def test_setter_rad(): - eps = np.spacing(1) - rad = 0.5 - x = 0.5 - y = 0 - z = 0 - coords = Coordinates(1, 0, 0) - coords.radius = rad - npt.assert_allclose(coords._x, x, atol=eps) - npt.assert_allclose(coords._y, y, atol=eps) - npt.assert_allclose(coords._z, z, atol=eps) - -def test_setter_ele(): - eps = np.spacing(1) - ele = 0 - x = 0 - y = 0 - z = 1 - coords = Coordinates(1, 0, 0) - coords.elevation = ele - npt.assert_allclose(coords._x, x, atol=eps) - npt.assert_allclose(coords._y, y, atol=eps) - npt.assert_allclose(coords._z, z, atol=eps) - - -def test_setter_azi(): - eps = np.spacing(1) - azi = np.pi/2 - x = 0 - y = 1 - z = 0 - coords = Coordinates(1, 0, 0) - coords.azimuth = azi - npt.assert_allclose(coords._x, x, atol=eps) - npt.assert_allclose(coords._y, y, atol=eps) - npt.assert_allclose(coords._z, z, atol=eps) - -def test_getter_latitude(): - x = 1 - y = 0 - z = 0.5 - - height, lat, lon = cart2latlon(x, y, z) - coords = Coordinates(x, y, z) - npt.assert_allclose(coords.latitude, lat) - -def test_getter_longitude(): - x = 1 - y = 0 - z = 0.5 - - height, lat, lon = cart2latlon(x, y, z) - coords = Coordinates(x, y, z) - npt.assert_allclose(coords.longitude, lon) - -def test_getter_cartesian(): - x = [1, 0, 0, 0] - y = [0, 1, 0, 0] - z = [0, 0, 1, 0] - - coords = Coordinates(x, y, z) - ref = np.vstack((x, y, z)) - npt.assert_allclose(coords.cartesian, ref) - - -def test_setter_cartesian(): - x = np.array([1, 0, 0, 0]) - y = np.array([0, 1, 0, 0]) - z = np.array([0, 0, 1, 0]) - cart = np.vstack((x, y, z)) - coords = Coordinates() - coords.cartesian = cart - npt.assert_allclose(coords.cartesian, cart) - - -def test_getter_spherical(): - x = np.array([1, 0, 0, 1], dtype=float) - y = np.array([0, 1, 0, 1], dtype=float) - z = np.array([0, 0, 1, 1], dtype=float) - - rad, theta, phi = cart2sph(x, y, z) - - coords = Coordinates(x, y, z) - ref = np.vstack((rad, theta, phi)) - npt.assert_allclose(coords.spherical, ref) - - -def test_setter_spherical(): - eps = np.spacing(1) - x = np.array([1, 0, 0, 1], dtype=float) - y = np.array([0, 1, 0, 1], dtype=float) - z = np.array([0, 0, 1, 1], dtype=float) - rad, theta, phi = cart2sph(x, y, z) - spherial = np.vstack((rad, theta, phi)) - coords = Coordinates() - coords.spherical = spherial - npt.assert_allclose(coords._x, x, atol=eps) - npt.assert_allclose(coords._y, y, atol=eps) - npt.assert_allclose(coords._z, z, atol=eps) - - -def test_n_points(): - coords = Coordinates([1, 0], [1, 1], [0, 1]) - assert coords.n_points == 2 - - -def test_find_nearest(): - coords = Coordinates([1, 0], [1, 1], [0, 1]) - point = Coordinates(1, 1, 0) - - dist, idx = coords.find_nearest_point(point) - - assert idx == 0 - - -def test_len(): - coords = Coordinates([1, 0], [1, 1], [0, 1]) - assert len(coords) == 2 - - -def test_getitem(): - coords = Coordinates([1, 0], [1, 1], [0, 1]) - getcoords = coords[0] - npt.assert_allclose(np.squeeze(getcoords.cartesian), np.array([1, 1, 0])) - - -def test_setitem(): - coords = Coordinates([0, 0], [1, 1], [0, 1]) - setcoords = Coordinates(1, 1, 0) - coords[0] = setcoords - npt.assert_allclose(np.squeeze(coords.cartesian), - np.array([[1, 0], [1, 1], [0, 1]])) +from spharpy.samplings import gaussian def test_sampling_sphere_init(): @@ -291,32 +16,45 @@ def test_sampling_sphere_init_value(): assert isinstance(sampling, SamplingSphere) -def test_sampling_to_pyfar_coords(): - sampling = SamplingSphere( - [1], [0], [0], n_max=0, weights=np.array([4*np.pi])) - pyfar_coords = sampling.to_pyfar() - np.testing.assert_allclose(pyfar_coords.get_cart(), sampling.cartesian.T) - assert pyfar_coords.sh_order == sampling.n_max - assert pyfar_coords.weights == 1. +def test_sampling_sphere_from_coordinates(): + """Test converting Coordinates to SamplingSphere.""" + + coordinates = Coordinates([1, 0, -1, 0], [0, 1, 0, -1], 0, + weights=[np.pi, np.pi, np.pi, np.pi]) + sampling_sphere = SamplingSphere.from_coordinates(coordinates) + + # check data in sampling_sphere + assert type(sampling_sphere) is SamplingSphere + npt.assert_equal(sampling_sphere.cartesian, coordinates.cartesian) + npt.assert_equal(sampling_sphere.weights, coordinates.weights) + assert sampling_sphere.n_max is None + assert sampling_sphere.radius_tolerance == 1e-6 + assert sampling_sphere.quadrature_tolerance == 1e-10 + assert sampling_sphere.comment == coordinates.comment + + # make sure mutable arrays are copied from coordinates + coordinates.weights = None + assert sampling_sphere.weights is not None + coordinates.x = [1, 1, 1, 1] + npt.assert_equal(sampling_sphere.x, [1, 0, -1, 0]) -def test_from_pyfar(): - pyfar_coords = pf.Coordinates(1, 0, 0, weights=1, sh_order=0) - spharpy_sampling = SamplingSphere.from_pyfar(pyfar_coords) - np.testing.assert_allclose( - pyfar_coords.get_cart(), spharpy_sampling.cartesian.T) - assert pyfar_coords.sh_order == spharpy_sampling.n_max - npt.assert_almost_equal(4*np.pi, spharpy_sampling.weights) + coordinates.y = [1, 1, 1, 1] + npt.assert_equal(sampling_sphere.y, [0, 1, 0, -1]) + + coordinates.z = [1, 1, 1, 1] + npt.assert_equal(sampling_sphere.z, [0, 0, 0, 0]) def sampling_cube(): - """Helper function returning a cube sampling""" + """Helper function returning a cube sampling.""" x = [1, -1, 0, 0, 0, 0] y = [0, 0, 1, -1, 0, 0] z = [0, 0, 0, 0, 1, -1] return x, y, z + def test_getter_n_max(): x, y, z = sampling_cube() n_max = 1 @@ -324,6 +62,7 @@ def test_getter_n_max(): assert sampling.n_max == n_max + def test_setter_n_max(): x, y, z = sampling_cube() n_max = 1 @@ -333,11 +72,142 @@ def test_setter_n_max(): assert sampling._n_max == n_max -def test_merge(): - s1 = Coordinates(1, 0, 0) - s2 = Coordinates(0, 2, 0) +def test_error_multiple_radius_initialization(): + """ + Test if entering points with multiple radii during initialization raises + an error. + """ + + match = '1 m, which exceeds the tolerance of 1e-06 m' + with pytest.raises(ValueError, match=match): + SamplingSphere([1, 0], 0, 0) + + +def test_error_multiple_radius_setter(): + """ + Test if entering points with multiple radii after initialization raises + an error. + """ + + sampling_sphere = SamplingSphere([1, 1], 0, 0) - s1.merge(s2) + match = '1 m, which exceeds the tolerance of 1e-06 m' + with pytest.raises(ValueError, match=match): + sampling_sphere.x = [1, 0] + + +@pytest.mark.parametrize('sampling', [ + SamplingSphere([1, 1], 0, 0), + SamplingSphere.from_cartesian([1, 1], 0, 0), + SamplingSphere.from_spherical_elevation([0, 0], 0, 1), + SamplingSphere.from_spherical_colatitude([0, 0], 0, 1), + SamplingSphere.from_spherical_side([0, 0], 0, 1), + SamplingSphere.from_spherical_front([0, 0], 0, 1), + SamplingSphere.from_cylindrical([0, 0], 0, 1), +]) +def test_radius_tolerance(sampling): + """ + Test getter and setter for radius tolerance and the related error message. + """ + tolerance = 1e-3 + + # test default value + assert sampling.radius_tolerance == 1e-6 + # change tolerance + sampling.radius_tolerance = tolerance + assert sampling.radius_tolerance == tolerance + + with pytest.raises(ValueError, match=f'{tolerance:.3g}'): + sampling.x = [0, 1] + + +def test_radius_tolerance_error(): + """ + Test if setting the radius tolerance too strict raises an error for + existing data. + """ + sampling = SamplingSphere([1, 1.1], 0, 0, radius_tolerance=.2) + + with pytest.raises(ValueError, match='the tolerance of 0.01'): + sampling.radius_tolerance = .01 + + +@pytest.mark.parametrize('tolerance', [ + None, [0, 1], np.array([0, 1]), -.1, +]) +def test_radius_tolerance_input(tolerance): + """Test if passing wrong values raises the expected error.""" + + match = 'The radius tolerance must be a number greater than zero' + with pytest.raises(ValueError, match=match): + SamplingSphere([1, 1], 0, 0, radius_tolerance=tolerance) + + +def test_weights_getter(): + x, y, z = sampling_cube() + n_max = 1 + weights = np.ones(6)*4*np.pi/6 + sampling = SamplingSphere(x, y, z, n_max, weights=weights) + + np.testing.assert_allclose(sampling.weights, weights) + + +def test_setting_weights(): + x, y, z = sampling_cube() + n_max = 1 + sampling = SamplingSphere(x, y, z, n_max) + + weights = np.ones(6)*4*np.pi/6 + sampling.weights = weights + + np.testing.assert_array_equal(sampling._weights, weights) + assert sampling._weights.shape == (6,) + + +def test_setting_weights_invalid(): + x, y, z = sampling_cube() + n_max = 1 + sampling = SamplingSphere(x, y, z, n_max) - truth = np.array([[1, 0], [0, 2], [0, 0]]) - npt.assert_allclose(truth, s1.cartesian) + message = r"The sum of the weights must be equal to 4\*pi." + with pytest.raises(ValueError, match=message): + sampling.weights = np.ones(6)/6 + + with pytest.raises(ValueError, match=message): + sampling._set_weights(np.ones(6)/6) + + message = "All weights must be positive." + weights_invalid = np.ones(6)*4*np.pi/6 + weights_invalid[0] = np.nan + with pytest.raises(ValueError, match=message): + sampling.weights = weights_invalid + + weights_invalid = np.ones(6)*4*np.pi/6 * -1 + weights_invalid[0] = -1 + with pytest.raises(ValueError, match=message): + sampling.weights = weights_invalid + + +def test_quadrature_getter_changing_weights(): + # create a quadrature grid (gaussian) + sampling = gaussian(n_max=3) + # check if quadrature is set properly + assert sampling.quadrature + # update weights such that quadrature requirement is not valid anymore + weights = 4 * np.pi * np.ones(sampling.cshape) / sampling.cshape + sampling.weights = weights + assert not sampling.quadrature + + +def test_quadrature_getter_changing_points(): + # create a quadrature grid (gaussian) + sampling = gaussian(n_max=3) + # check if quadrature is set properly + assert sampling.quadrature + # update points such that quadrature is not valid anymore + rng = np.random.default_rng() + sampling.spherical_colatitude = np.concatenate([ + rng.random((sampling.csize, 1)), + np.ones((sampling.csize, 1)), + np.ones((sampling.csize, 1))], axis=1) + assert not sampling.quadrature diff --git a/tests/test_eq_area_partitions.py b/tests/test_eq_area_partitions.py index 7e3f0450..491545a2 100644 --- a/tests/test_eq_area_partitions.py +++ b/tests/test_eq_area_partitions.py @@ -45,7 +45,6 @@ def test_polar_colat(): def test_num_collars(): - dim = 2 N = 10 c_polar = 0.643501108793284 angle = 1.120998243279586 diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 6a9e19cd..dd178bca 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -1,33 +1,127 @@ -""" Tests for sh channel indexing""" +"""Tests for sh channel indexing.""" -import pytest +import spharpy.spherical as sh import numpy as np -import spharpy.indexing + + +def test_acn_to_nm_single_val(): + n, m = sh.acn_to_nm(0) + assert n == 0 + assert m == 0 + + n, m = sh.acn_to_nm(2) + assert n == 1 + assert m == 0 + + n, m = sh.acn_to_nm(1) + assert n == 1 + assert m == -1 + + +def test_nm_to_acn_single_val(): + acn = sh.nm_to_acn(0, 0) + assert acn == 0 + + acn = sh.nm_to_acn(1, 0) + assert acn == 2 + + acn = sh.nm_to_acn(1, -1) + assert acn == 1 + + +def test_acn_to_nm_array(): + n_ref = np.array([0, 1, 1, 1]) + m_ref = np.array([0, -1, 0, 1]) + + n_max = 1 + n_sh = (n_max + 1)**2 + acn = np.arange(0, n_sh) + n, m = sh.acn_to_nm(acn) + + np.testing.assert_equal(n, n_ref) + np.testing.assert_equal(m, m_ref) + + +def test_nm_to_acn_array(): + n = np.array([0, 1, 1, 1]) + m = np.array([0, -1, 0, 1]) + + n_max = 1 + n_sh = (n_max + 1)**2 + acn_ref = np.arange(0, n_sh) + + acn = sh.nm_to_acn(n, m) + + np.testing.assert_equal(acn, acn_ref) + def test_identity_matrix_n_nm(): reference = np.array([[1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 1]]) - identity = spharpy.indexing.sph_identity_matrix(2, type='n-nm') + identity = sh.sph_identity_matrix(2, matrix_type='n-nm') np.testing.assert_allclose(reference, identity) + def test_sid_indexing(): n_max = 2 reference_n = np.array([0, 1, 1, 1, 2, 2, 2, 2, 2]) reference_m = np.array([0, 1, -1, 0, 2, -2, 1, -1, 0]) - - sid_n, sid_m = spharpy.indexing.sid(n_max) + sid_n, sid_m = sh.sid(n_max) np.testing.assert_equal(reference_n, sid_n) np.testing.assert_equal(reference_m, sid_m) -def test_sid2acn(): + +def test_sid_to_acn(): n_max = 2 # indexing starts at 0 here, reference was calculated # with indexing starting at 1. reference_acn = np.array([1, 3, 4, 2, 6, 8, 9, 7, 5]) - 1 - acn_indeces = spharpy.indexing.sid2acn(n_max) - np.testing.assert_equal(reference_acn, acn_indeces) + acn_indices = sh.sid_to_acn(n_max) + np.testing.assert_equal(reference_acn, acn_indices) + + +def test_nm_to_fuma_single_val(): + fuma = sh.nm_to_fuma(0, 0) + assert fuma == 0 + + fuma = sh.nm_to_fuma(1, 0) + assert fuma == 1 + + fuma = sh.nm_to_fuma(1, -1) + assert fuma == 3 + + +def test_nm_to_fuma_array(): + n = np.array([0, 1, 1]) + m = np.array([0, 0, -1]) + + fuma = sh.nm_to_fuma(n, m) + np.testing.assert_equal(np.array([0, 1, 3], int), fuma) + + +def test_fuma_to_nm_single_val(): + n, m = sh.fuma_to_nm(0) + assert n == 0 + assert m == 0 + + n, m = sh.fuma_to_nm(1) + assert n == 1 + assert m == 0 + + n, m = sh.fuma_to_nm(3) + assert n == 1 + assert m == -1 + + +def test_fuma_to_nm_array(): + n_ref = np.array([0, 1, 1]) + m_ref = np.array([0, 0, -1]) + + n, m = sh.fuma_to_nm(np.array([0, 1, 3])) + np.testing.assert_equal(n_ref, n) + np.testing.assert_equal(m_ref, m) diff --git a/tests/test_interpolate.py b/tests/test_interpolate.py index 55ded0b9..77cd43b0 100644 --- a/tests/test_interpolate.py +++ b/tests/test_interpolate.py @@ -1,23 +1,24 @@ import spharpy -from spharpy.samplings import Coordinates +from pyfar import Coordinates import spharpy.interpolate as interpolate import numpy as np -def test_smooth_sphere_bivariate_spline_interpolation(): +def test_smooth_sphere_bivariate_spline_interpolation(download_sampling): n_max = 10 - sampling = spharpy.samplings.equalarea( + sampling = spharpy.samplings.equal_area( n_max, n_points=500, condition_num=np.inf) Y = spharpy.spherical.spherical_harmonic_basis_real(n_max, sampling) y_vec = spharpy.spherical.spherical_harmonic_basis_real( n_max, Coordinates(1, 0, 0)) data = Y @ y_vec.T - data = np.sin(sampling.elevation)*np.sin(2*sampling.azimuth) + data = np.sin(sampling.colatitude)*np.sin(2*sampling.azimuth) - interp_grid = spharpy.samplings.hyperinterpolation(35) + download_sampling('extremal', 35) + interp_grid = spharpy.samplings.hyperinterpolation(n_max=35) - data_grid = np.sin(interp_grid.elevation)*np.sin(2*interp_grid.azimuth) + data_grid = np.sin(interp_grid.colatitude)*np.sin(2*interp_grid.azimuth) interpolator = interpolate.SmoothSphereBivariateSpline( sampling, data, s=1e-4) @@ -27,15 +28,3 @@ def test_smooth_sphere_bivariate_spline_interpolation(): # check if error over entire sphere sufficiently small assert np.linalg.norm(np.abs(interp_data - data_grid)) / \ np.linalg.norm(np.abs(data_grid)) < 1e-2 - - # convert to pyfar to coordinates and check the results - pf_sampling = sampling.to_pyfar() - interpolator = interpolate.SmoothSphereBivariateSpline( - pf_sampling, data, s=1e-4) - - pf_interp_grid = interp_grid.to_pyfar() - interp_data = interpolator(pf_interp_grid) - - # check if error over entire sphere sufficiently small - assert np.linalg.norm(np.abs(interp_data - data_grid)) / \ - np.linalg.norm(np.abs(data_grid)) < 1e-2 diff --git a/tests/test_modal_strength.py b/tests/test_modal_strength.py index 78c6bd67..f1b3b367 100644 --- a/tests/test_modal_strength.py +++ b/tests/test_modal_strength.py @@ -1,5 +1,5 @@ """ -Tests for modal strength function +Tests for modal strength function. """ from scipy.io import loadmat import spharpy.spherical as sh diff --git a/tests/test_normalization_conversion.py b/tests/test_normalization_conversion.py new file mode 100644 index 00000000..464ee854 --- /dev/null +++ b/tests/test_normalization_conversion.py @@ -0,0 +1,33 @@ +import spharpy.spherical as sh +import numpy as np +import pytest + + +@pytest.mark.parametrize(('index', 'expected'), [ + (0, np.sqrt(1/2)), (1, np.sqrt(1/3)), (2, np.sqrt(1/3))]) +def test_n3d_to_maxn_single_val(index, expected): + assert sh.n3d_to_maxn(index) == expected + + +def test_n3d_to_maxn_array(): + acn = np.array([0, 1, 2]) + maxN_ref = np.array([np.sqrt(1 / 2), + np.sqrt(1 / 3), + np.sqrt(1 / 3)]) + + maxN_norm = sh.n3d_to_maxn(acn) + np.testing.assert_equal(maxN_norm, maxN_ref) + + +@pytest.mark.parametrize(('index', 'expected'), [ + (0, 1), (1, 1/np.sqrt(3)), (2, 1/np.sqrt(5))]) +def test_n3d_to_sn3d_norm_single_val(index, expected): + assert sh.n3d_to_sn3d_norm(index) == expected + + +def test_n3d_to_sn3d_norm_array(): + n = np.array([0, 1, 2]) + sn3d_norm_ref = np.array([1, 1/np.sqrt(3), 1/np.sqrt(5)]) + sn3d_norm = sh.n3d_to_sn3d_norm(n) + + np.testing.assert_equal(sn3d_norm, sn3d_norm_ref) diff --git a/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_None.png b/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_None.png new file mode 100644 index 00000000..72f23f66 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_magma.png b/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_magma.png new file mode 100644 index 00000000..aca898dd Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_cmap_encoding_magnitude_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_None.png b/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_None.png new file mode 100644 index 00000000..a052e32f Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_magma.png b/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_magma.png new file mode 100644 index 00000000..589c23b2 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_cmap_encoding_phase_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/balloon_colorbar_False.png b/tests/test_plot_data/baseline/balloon_colorbar_False.png new file mode 100644 index 00000000..40b7a602 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/balloon_colorbar_True.png b/tests/test_plot_data/baseline/balloon_colorbar_True.png new file mode 100644 index 00000000..d6926a31 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/balloon_default.png b/tests/test_plot_data/baseline/balloon_default.png new file mode 100644 index 00000000..d6926a31 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_default.png differ diff --git a/tests/test_plot_data/baseline/balloon_limits_None.png b/tests/test_plot_data/baseline/balloon_limits_None.png new file mode 100644 index 00000000..d6926a31 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_limits_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_limits_list.png b/tests/test_plot_data/baseline/balloon_limits_list.png new file mode 100644 index 00000000..ff3f81a5 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_limits_list.png differ diff --git a/tests/test_plot_data/baseline/balloon_limits_tuple.png b/tests/test_plot_data/baseline/balloon_limits_tuple.png new file mode 100644 index 00000000..ff3f81a5 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_None.png b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_None.png new file mode 100644 index 00000000..3130855c Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_magma.png b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_magma.png new file mode 100644 index 00000000..045ae15b Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_magnitude_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_None.png b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_None.png new file mode 100644 index 00000000..06c8a1af Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_magma.png b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_magma.png new file mode 100644 index 00000000..348124cc Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_cmap_encoding_phase_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_colorbar_False.png b/tests/test_plot_data/baseline/balloon_wireframe_colorbar_False.png new file mode 100644 index 00000000..e34961fe Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_colorbar_True.png b/tests/test_plot_data/baseline/balloon_wireframe_colorbar_True.png new file mode 100644 index 00000000..2924f410 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_default.png b/tests/test_plot_data/baseline/balloon_wireframe_default.png new file mode 100644 index 00000000..2924f410 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_default.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_limits_None.png b/tests/test_plot_data/baseline/balloon_wireframe_limits_None.png new file mode 100644 index 00000000..2924f410 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_limits_None.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_limits_list.png b/tests/test_plot_data/baseline/balloon_wireframe_limits_list.png new file mode 100644 index 00000000..6155bad9 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_limits_list.png differ diff --git a/tests/test_plot_data/baseline/balloon_wireframe_limits_tuple.png b/tests/test_plot_data/baseline/balloon_wireframe_limits_tuple.png new file mode 100644 index 00000000..6155bad9 Binary files /dev/null and b/tests/test_plot_data/baseline/balloon_wireframe_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/cmap_phase_twilight.png b/tests/test_plot_data/baseline/cmap_phase_twilight.png new file mode 100644 index 00000000..0db43b4a Binary files /dev/null and b/tests/test_plot_data/baseline/cmap_phase_twilight.png differ diff --git a/tests/test_plot_data/baseline/contour_cmap_ColormapObject.png b/tests/test_plot_data/baseline/contour_cmap_ColormapObject.png new file mode 100644 index 00000000..fa9924f1 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_cmap_ColormapObject.png differ diff --git a/tests/test_plot_data/baseline/contour_cmap_None.png b/tests/test_plot_data/baseline/contour_cmap_None.png new file mode 100644 index 00000000..92cfc473 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/contour_cmap_plasma.png b/tests/test_plot_data/baseline/contour_cmap_plasma.png new file mode 100644 index 00000000..fa9924f1 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_cmap_plasma.png differ diff --git a/tests/test_plot_data/baseline/contour_colorbar_False.png b/tests/test_plot_data/baseline/contour_colorbar_False.png new file mode 100644 index 00000000..5d951354 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/contour_colorbar_True.png b/tests/test_plot_data/baseline/contour_colorbar_True.png new file mode 100644 index 00000000..92cfc473 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/contour_default.png b/tests/test_plot_data/baseline/contour_default.png new file mode 100644 index 00000000..92cfc473 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_default.png differ diff --git a/tests/test_plot_data/baseline/contour_levels_int.png b/tests/test_plot_data/baseline/contour_levels_int.png new file mode 100644 index 00000000..e1719274 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_levels_int.png differ diff --git a/tests/test_plot_data/baseline/contour_levels_list.png b/tests/test_plot_data/baseline/contour_levels_list.png new file mode 100644 index 00000000..9909b4d6 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_levels_list.png differ diff --git a/tests/test_plot_data/baseline/contour_levels_ndarray.png b/tests/test_plot_data/baseline/contour_levels_ndarray.png new file mode 100644 index 00000000..9909b4d6 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_levels_ndarray.png differ diff --git a/tests/test_plot_data/baseline/contour_levels_tuple.png b/tests/test_plot_data/baseline/contour_levels_tuple.png new file mode 100644 index 00000000..9909b4d6 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_levels_tuple.png differ diff --git a/tests/test_plot_data/baseline/contour_limits_None.png b/tests/test_plot_data/baseline/contour_limits_None.png new file mode 100644 index 00000000..92cfc473 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_limits_None.png differ diff --git a/tests/test_plot_data/baseline/contour_limits_list.png b/tests/test_plot_data/baseline/contour_limits_list.png new file mode 100644 index 00000000..046a803b Binary files /dev/null and b/tests/test_plot_data/baseline/contour_limits_list.png differ diff --git a/tests/test_plot_data/baseline/contour_limits_tuple.png b/tests/test_plot_data/baseline/contour_limits_tuple.png new file mode 100644 index 00000000..046a803b Binary files /dev/null and b/tests/test_plot_data/baseline/contour_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/contour_map_cmap_ColormapObject.png b/tests/test_plot_data/baseline/contour_map_cmap_ColormapObject.png new file mode 100644 index 00000000..5753e24e Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_cmap_ColormapObject.png differ diff --git a/tests/test_plot_data/baseline/contour_map_cmap_None.png b/tests/test_plot_data/baseline/contour_map_cmap_None.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/contour_map_cmap_plasma.png b/tests/test_plot_data/baseline/contour_map_cmap_plasma.png new file mode 100644 index 00000000..5753e24e Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_cmap_plasma.png differ diff --git a/tests/test_plot_data/baseline/contour_map_colorbar_False.png b/tests/test_plot_data/baseline/contour_map_colorbar_False.png new file mode 100644 index 00000000..0c2d2a1b Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/contour_map_colorbar_True.png b/tests/test_plot_data/baseline/contour_map_colorbar_True.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/contour_map_default.png b/tests/test_plot_data/baseline/contour_map_default.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_default.png differ diff --git a/tests/test_plot_data/baseline/contour_map_levels_int.png b/tests/test_plot_data/baseline/contour_map_levels_int.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_levels_int.png differ diff --git a/tests/test_plot_data/baseline/contour_map_levels_list.png b/tests/test_plot_data/baseline/contour_map_levels_list.png new file mode 100644 index 00000000..3a021e0e Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_levels_list.png differ diff --git a/tests/test_plot_data/baseline/contour_map_levels_ndarray.png b/tests/test_plot_data/baseline/contour_map_levels_ndarray.png new file mode 100644 index 00000000..3a021e0e Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_levels_ndarray.png differ diff --git a/tests/test_plot_data/baseline/contour_map_levels_tuple.png b/tests/test_plot_data/baseline/contour_map_levels_tuple.png new file mode 100644 index 00000000..3a021e0e Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_levels_tuple.png differ diff --git a/tests/test_plot_data/baseline/contour_map_limits_None.png b/tests/test_plot_data/baseline/contour_map_limits_None.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_limits_None.png differ diff --git a/tests/test_plot_data/baseline/contour_map_limits_list.png b/tests/test_plot_data/baseline/contour_map_limits_list.png new file mode 100644 index 00000000..e0dab3b6 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_limits_list.png differ diff --git a/tests/test_plot_data/baseline/contour_map_limits_tuple.png b/tests/test_plot_data/baseline/contour_map_limits_tuple.png new file mode 100644 index 00000000..e0dab3b6 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/contour_map_projection_hammer.png b/tests/test_plot_data/baseline/contour_map_projection_hammer.png new file mode 100644 index 00000000..9d185b9b Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_projection_hammer.png differ diff --git a/tests/test_plot_data/baseline/contour_map_projection_mollweide.png b/tests/test_plot_data/baseline/contour_map_projection_mollweide.png new file mode 100644 index 00000000..88131864 Binary files /dev/null and b/tests/test_plot_data/baseline/contour_map_projection_mollweide.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_cmap_ColormapObject.png b/tests/test_plot_data/baseline/pcolor_map_cmap_ColormapObject.png new file mode 100644 index 00000000..5039e715 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_cmap_ColormapObject.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_cmap_None.png b/tests/test_plot_data/baseline/pcolor_map_cmap_None.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_cmap_plasma.png b/tests/test_plot_data/baseline/pcolor_map_cmap_plasma.png new file mode 100644 index 00000000..5039e715 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_cmap_plasma.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_colorbar_False.png b/tests/test_plot_data/baseline/pcolor_map_colorbar_False.png new file mode 100644 index 00000000..db6a6afb Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_colorbar_True.png b/tests/test_plot_data/baseline/pcolor_map_colorbar_True.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_default.png b/tests/test_plot_data/baseline/pcolor_map_default.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_default.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_limits_None.png b/tests/test_plot_data/baseline/pcolor_map_limits_None.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_limits_None.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_limits_list.png b/tests/test_plot_data/baseline/pcolor_map_limits_list.png new file mode 100644 index 00000000..41d53ce5 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_limits_list.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_limits_tuple.png b/tests/test_plot_data/baseline/pcolor_map_limits_tuple.png new file mode 100644 index 00000000..41d53ce5 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_projection_hammer.png b/tests/test_plot_data/baseline/pcolor_map_projection_hammer.png new file mode 100644 index 00000000..0ed6279b Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_projection_hammer.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_projection_mollweide.png b/tests/test_plot_data/baseline/pcolor_map_projection_mollweide.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_projection_mollweide.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_refine_False.png b/tests/test_plot_data/baseline/pcolor_map_refine_False.png new file mode 100644 index 00000000..aa529031 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_refine_False.png differ diff --git a/tests/test_plot_data/baseline/pcolor_map_refine_True.png b/tests/test_plot_data/baseline/pcolor_map_refine_True.png new file mode 100644 index 00000000..4f466791 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_map_refine_True.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_None.png b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_None.png new file mode 100644 index 00000000..d20ee432 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_magma.png b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_magma.png new file mode 100644 index 00000000..eb8b3179 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_magnitude_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_None.png b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_None.png new file mode 100644 index 00000000..8d57f4f1 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_None.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_magma.png b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_magma.png new file mode 100644 index 00000000..a75c5ab1 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_cmap_encoding_phase_cmap_magma.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_colorbar_False.png b/tests/test_plot_data/baseline/pcolor_sphere_colorbar_False.png new file mode 100644 index 00000000..e7acca76 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_colorbar_False.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_colorbar_True.png b/tests/test_plot_data/baseline/pcolor_sphere_colorbar_True.png new file mode 100644 index 00000000..3a6d35f0 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_colorbar_True.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_default.png b/tests/test_plot_data/baseline/pcolor_sphere_default.png new file mode 100644 index 00000000..3a6d35f0 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_default.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_limits_None.png b/tests/test_plot_data/baseline/pcolor_sphere_limits_None.png new file mode 100644 index 00000000..3a6d35f0 Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_limits_None.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_limits_list.png b/tests/test_plot_data/baseline/pcolor_sphere_limits_list.png new file mode 100644 index 00000000..b3a90d6a Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_limits_list.png differ diff --git a/tests/test_plot_data/baseline/pcolor_sphere_limits_tuple.png b/tests/test_plot_data/baseline/pcolor_sphere_limits_tuple.png new file mode 100644 index 00000000..b3a90d6a Binary files /dev/null and b/tests/test_plot_data/baseline/pcolor_sphere_limits_tuple.png differ diff --git a/tests/test_plot_data/baseline/scatter_default.png b/tests/test_plot_data/baseline/scatter_default.png new file mode 100644 index 00000000..d20f26bc Binary files /dev/null and b/tests/test_plot_data/baseline/scatter_default.png differ diff --git a/tests/test_plot_data/baseline/voronoi_cells_sphere_default.png b/tests/test_plot_data/baseline/voronoi_cells_sphere_default.png new file mode 100644 index 00000000..d1163954 Binary files /dev/null and b/tests/test_plot_data/baseline/voronoi_cells_sphere_default.png differ diff --git a/tests/test_plots.py b/tests/test_plots.py index 6beb28f7..b5315d8f 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -1,242 +1,384 @@ +import os +import pytest +import spharpy as sp +from pyfar.testing.plot_utils import create_figure, save_and_compare +import numpy as np import matplotlib as mpl -# Use Agg backend to prevent matplotlib from creating interactive plots. This -# has to be set before the importing matplotlib.pyplot. Use switch backend in -# case the wrong backend has already been set. -mpl.use('agg') import matplotlib.pyplot as plt -plt.switch_backend('agg') - -import spharpy -import numpy as np -import pytest -from spharpy import plot -import pytest - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_balloon_plot(icosahedron, make_coordinates, implementation): - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - data = np.cos(phi)*np.sin(theta) - spharpy.plot.balloon(coords, data) - - spharpy.plot.balloon(coords, data, phase=True) - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_contour_plot(icosahedron, make_coordinates, implementation): - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - data = np.cos(phi)*np.sin(theta) - - spharpy.plot.contour(coords, np.abs(data)) - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_scatter(icosahedron, make_coordinates, implementation): - """Test if the plot executes without raising an exception +""" +For general information on testing plot functions see +https://pyfar-gallery.readthedocs.io/en/latest/contribute/contribution_guidelines.html#testing-plot-functions + +Important: +- `create_baseline` and `compare_output` must be ``False`` when pushing + changes to pyfar. +- `create_baseline` must only be ``True`` if the behavior of a plot function + changed. In this case it is best practice to recreate only the baseline plots + of the plot function (plot behavior) that changed. +""" +# global parameters ----------------------------------------------------------- +create_baseline = False + +# file type used for saving the plots +file_type = "png" + +# if true, the plots will be compared to the baseline and an error is raised +# if there are any differences. In any case, differences are written to +# output_path as images +compare_output = False + +# path handling +base_path = os.path.join('tests', 'test_plot_data') +baseline_path = os.path.join(base_path, 'baseline') +output_path = os.path.join(base_path, 'output') + +if not os.path.isdir(base_path): + os.mkdir(base_path) +if not os.path.isdir(baseline_path): + os.mkdir(baseline_path) +if not os.path.isdir(output_path): + os.mkdir(output_path) + +# remove old output files +for file in os.listdir(output_path): + os.remove(os.path.join(output_path, file)) + +# the naming scheme of the baseline is as follows: +# __.png + +# testing --------------------------------------------------------------------- +@pytest.mark.parametrize('function', [ + (sp.plot.scatter), + (sp.plot.voronoi_cells_sphere)]) +def test_sampling_scatter(function): + """Test the scatter plot with default arguments.""" + coords = sp.samplings.equal_area(n_max=0, n_points=12) + + # do plotting + filename = f'{function.__name__}_default' + create_figure() + function(coords) + save_and_compare( + create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + match = 'must be a coordinates object.' + with pytest.raises(ValueError, match=match): + function('coords') + + +@pytest.mark.parametrize('function', [ + (sp.plot.balloon), + (sp.plot.balloon_wireframe), + (sp.plot.contour), + (sp.plot.contour_map), + (sp.plot.pcolor_map), + (sp.plot.pcolor_sphere)]) +def test_spherical_default(function): + """Test all spherical plots with default arguments.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + filename = f'{function.__name__}_default' + create_figure() + function(coords, data) + save_and_compare( + create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = 'data must be a 1D array with the same cshape as the coordinates' + with pytest.raises(ValueError, match=match): + function(coords, 'data') + + match = 'coordinates must be a coordinates object.' + with pytest.raises(ValueError, match=match): + function('coords', data) + + +@pytest.mark.parametrize('function', [ + (sp.plot.contour), + (sp.plot.contour_map), + (sp.plot.pcolor_map)]) +@pytest.mark.parametrize('cmap', [plt.get_cmap('plasma'), 'plasma', None]) +def test_spherical_cmap(function, cmap): + """Test all spherical plots with custom cmap argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + if isinstance(cmap, mpl.colors.Colormap): + # if a colormap object is passed, use its name for the filename + cmap_str = 'ColormapObject' + else: + # otherwise use the string representation of the colormap + cmap_str = str(cmap) + filename = f'{function.__name__}_cmap_{cmap_str}' + create_figure() + function(coords, data, cmap=cmap) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = 'cmap' + with pytest.raises(ValueError, match=match): + function(coords, data, cmap=5) + + +@pytest.mark.parametrize('function', [ + (sp.plot.balloon), + (sp.plot.balloon_wireframe), + (sp.plot.contour), + (sp.plot.contour_map), + (sp.plot.pcolor_map), + (sp.plot.pcolor_sphere)]) +@pytest.mark.parametrize('colorbar', [True, False]) +def test_spherical_colorbar(function, colorbar): + """Test all spherical plots with custom colorbar argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + filename = f'{function.__name__}_colorbar_{colorbar}' + create_figure() + function(coords, data, colorbar=colorbar) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = 'colorbar must be a boolean.' + with pytest.raises(ValueError, match=match): + function(coords, data, colorbar='colorbar') + + +@pytest.mark.parametrize('function', [ + (sp.plot.balloon), + (sp.plot.balloon_wireframe), + (sp.plot.contour), + (sp.plot.contour_map), + (sp.plot.pcolor_map), + (sp.plot.pcolor_sphere)]) +@pytest.mark.parametrize('limits', [None, (-1.5, 1.5), [-1.5, 1.5]]) +def test_spherical_limits(function, limits): + """Test all spherical plots with custom limits argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + if limits is None: + limits_str = 'None' + else: + limits_str = f'{type(limits).__name__}' + filename = f'{function.__name__}_limits_{limits_str}' + create_figure() + function(coords, data, limits=limits) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = ( + "limits must be a tuple or list containing the minimum and " + "maximum values for the colormap or None.") + with pytest.raises(ValueError, match=match): + function(coords, data, limits='limits') + with pytest.raises(ValueError, match=match): + function(coords, data, limits=[0, 1, 2]) + + +@pytest.mark.parametrize('function', [ + (sp.plot.balloon), + (sp.plot.balloon_wireframe), + (sp.plot.pcolor_sphere)]) +@pytest.mark.parametrize('cmap_encoding', ['phase', 'magnitude']) +@pytest.mark.parametrize('cmap', [None, 'magma']) +def test_balloon_cmap_encoding(function, cmap_encoding, cmap): + """Test all balloon like plots with custom arguments.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # use the azimuth angle for a continuous phase change + data = np.abs(data) * np.exp(1j * coords.azimuth) + + # do plotting + filename = f'{function.__name__}_cmap_encoding_{cmap_encoding}_cmap_{cmap}' + create_figure() + function(coords, data, cmap_encoding=cmap_encoding, cmap=cmap) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + +@pytest.mark.parametrize('function', [ + (sp.plot.balloon), + (sp.plot.balloon_wireframe), + (sp.plot.pcolor_sphere)]) +def test_balloon_cmap_encoding_error(function): + """Test raising errors for invalid cmap_encoding argument.""" + # test error for invalid inputs + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + match = "cmap_encoding must be either 'phase' or 'magnitude'." + with pytest.raises(ValueError, match=match): + function(coords, data, cmap_encoding='invalid') + + +@pytest.mark.parametrize('function', [ + (sp.plot.pcolor_map), + (sp.plot.contour_map), + ]) +@pytest.mark.parametrize('projection', ['mollweide', 'hammer']) +def test_spherical_projection(function, projection): + """Test spherical plots with custom projection argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + filename = f'{function.__name__}_projection_{projection}' + create_figure() + function(coords, data, projection=projection) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = "The projection of the axis needs to be 'projection'" + with pytest.raises(ValueError, match=match): + function(coords, data, projection='projection') + + +@pytest.mark.parametrize('function', [ + sp.plot.contour_map, + sp.plot.contour, + ]) +@pytest.mark.parametrize('levels', [ + 8, + (-1, -0.5, 0, 0.5, 1), + [-1, -0.5, 0, 0.5, 1], + np.array((-1, -0.5, 0, 0.5, 1)), + ]) +def test_spherical_levels(function, levels): + """Test contour plots with custom level argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + filename = f'{function.__name__}_levels_{type(levels).__name__}' + create_figure() + function(coords, data, levels=levels) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + # test error for invalid inputs + match = 'levels' + with pytest.raises(ValueError, match=match): + function(coords, data, levels='levels') + + +@pytest.mark.parametrize('function', [ + sp.plot.pcolor_map, + ]) +@pytest.mark.parametrize('refine', [ + True, False, + ]) +def test_spherical_refine(function, refine): + """Test contour plots with custom refine argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + # do plotting + filename = f'{function.__name__}_refine_{refine}' + create_figure() + function(coords, data, refine=refine) + save_and_compare(create_baseline, baseline_path, output_path, filename, + file_type, compare_output) + + +@pytest.mark.parametrize('function', [ + sp.plot.pcolor_map, + ]) +def test_spherical_refine_error(function): + """Test contour plots with invalid refine argument.""" + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + + match = 'refine' + with pytest.raises(ValueError, match=match): + function(coords, data, refine='refine') + + +@pytest.mark.parametrize(("function", "projection"), [ + (sp.plot.balloon, '3d'), + (sp.plot.balloon_wireframe, '3d'), + (sp.plot.pcolor_sphere, '3d'), + (sp.plot.contour, 'rectilinear'), + (sp.plot.contour_map, 'mollweide'), + (sp.plot.pcolor_map, 'mollweide'), + ]) +def test_data_plots_projection_input_and_return(function, projection): """ - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - spharpy.plot.scatter(coords) - - # test of auto detection of axes works - ax = plt.axes(projection='3d') - spharpy.plot.scatter(coords) - - # explicitly pass axes - spharpy.plot.scatter(coords, ax=ax) - - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match='3d'): - spharpy.plot.scatter(coords, ax=ax) - - # current axis with wrong projection - with pytest.raises(ValueError, match='3d'): - spharpy.plot.scatter(coords) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_pcolor_map(icosahedron, make_coordinates, implementation): - """Test if the plot executes without raising an exception - """ - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - plot.pcolor_map(coords, data) - - # test of auto detection of axes works - ax = plt.axes(projection='mollweide') - plot.pcolor_map(coords, data) - - # explicitly pass axes - plot.pcolor_map(coords, data, ax=ax) - - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match='Projection does not match'): - plot.pcolor_map(coords, data, ax=ax) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_contour_map(icosahedron, make_coordinates, implementation): - """Test if the plot executes without raising an exception - """ - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - - plot.contour_map(coords, data) - - # test of auto detection of axes works - ax = plt.axes(projection='mollweide') - plot.contour_map(coords, data) - - # explicitly pass axes - plot.contour_map(coords, data, ax=ax) - - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match='Projection does not match'): - plot.contour_map(coords, data, ax=ax) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_contour(icosahedron, make_coordinates, implementation): - """Test if the plot executes without raising an exception + Test all spherical plots with ax argument and check if this is + also returned. """ - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - - plot.contour_map(coords, data) - - # test of auto detection of axes works - ax = plt.axes() - plot.contour(coords, data) - - # explicitly pass axes - plot.contour(coords, data, ax=ax) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_plot_voronoi_sphere(icosahedron, make_coordinates, implementation): - """Test if the plot executes without raising an exception + coords = sp.samplings.equal_area(n_max=0, n_points=500) + data = np.sin(coords.colatitude) * np.cos(coords.azimuth) + # do plotting + create_figure() + ax = plt.axes(projection=projection) + + (ax_out, _) = function(coords, data, ax=ax) + # check if the returned axis is a 3D axis + assert ax_out.name == projection + + # test error for invalid inputs + match = f"The projection of the axis needs to be '{projection}'" + with pytest.raises(ValueError, match=match): + function(coords, data, ax=plt.axes(projection='polar')) + + +@pytest.mark.parametrize('function', [ + (sp.plot.scatter), + (sp.plot.voronoi_cells_sphere), + ]) +def test_coordinates_plots_projection_input_and_return(function): + """Test scatter plots with ax argument and check if this is also returned. """ - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - plot.voronoi_cells_sphere(coords) - - # test of auto detection of axes works + coords = sp.samplings.equal_area(n_max=0, n_points=12) + # do plotting + create_figure() ax = plt.axes(projection='3d') - plot.voronoi_cells_sphere(coords) - - # explicitly pass axes - plot.voronoi_cells_sphere(coords, ax=ax) - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match='3d'): - plot.voronoi_cells_sphere(coords, ax=ax) + ax_out = function(coords, ax=ax) - # current axis with wrong projection - with pytest.raises(ValueError, match='3d'): - plot.voronoi_cells_sphere(coords) + # check if the returned axis is a 3D axis + assert ax_out.name == '3d' - plt.close('all') + # test error for invalid inputs + match = "The projection of the axis needs to be '3d'" + with pytest.raises(ValueError, match=match): + function(coords, ax=plt.axes()) -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_pcolor_sphere(icosahedron, make_coordinates, implementation): - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - - plot.pcolor_sphere(coords, data, colorbar=True) - - # test of auto detection of axes works - ax = plt.axes(projection='3d') - plot.pcolor_sphere(coords, data) +def test_cmap_phase_twilight(): + """Test the phase twilight colormap.""" + lut = 512 + filename = 'cmap_phase_twilight' + create_figure(6, .5) - # explicitly pass axes - plot.pcolor_sphere(coords, data, ax=ax) - - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match="'3d'"): - plot.pcolor_sphere(coords, data, ax=ax) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_balloon_wireframe(icosahedron, make_coordinates, implementation): - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - - plot.balloon_wireframe(coords, data) - - # test of auto detection of axes works - ax = plt.axes(projection='3d') - plot.balloon_wireframe(coords, data) - - # explicitly pass axes - plot.balloon_wireframe(coords, data, ax=ax) - - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match="'3d'"): - plot.balloon_wireframe(coords, data, ax=ax) - - plt.close('all') - - -@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_balloon(icosahedron, make_coordinates, implementation): - rad, theta, phi = icosahedron - coords = make_coordinates.create_coordinates( - implementation, rad, theta, phi) - - data = np.cos(phi)*np.sin(theta) - - plot.balloon(coords, data) - - # test of auto detection of axes works - ax = plt.axes(projection='3d') - plot.balloon(coords, data) + # do plotting + colormap = sp.plot.phase_twilight(lut) + ax = plt.gca() + gradient = np.linspace(0, 1, lut) + gradient = np.vstack((gradient, gradient)) + ax.imshow(gradient, aspect='auto', cmap=colormap) + ax.set_axis_off() - # explicitly pass axes - plot.balloon(coords, data, ax=ax) + save_and_compare( + create_baseline, baseline_path, output_path, filename, + file_type, compare_output) - # pass axes with wrong projection - ax = plt.axes() - with pytest.raises(ValueError, match="'3d'"): - plot.balloon(coords, data, ax=ax) + match = 'lut must be a positive integer.' + with pytest.raises(ValueError, match=match): + sp.plot.phase_twilight('lut') - plt.close('all') + with pytest.raises(ValueError, match=match): + sp.plot.phase_twilight(0) diff --git a/tests/test_rotations.py b/tests/test_rotations.py index a3dd2bcb..c011a434 100644 --- a/tests/test_rotations.py +++ b/tests/test_rotations.py @@ -1,12 +1,12 @@ -""" Tests for sh rotations""" +"""Tests for sh rotations.""" import numpy as np import spharpy.transforms as transforms from spharpy.spherical import spherical_harmonic_basis -from spharpy.samplings import Coordinates import spharpy from spharpy.transforms import RotationSH import pytest +from pyfar import Coordinates def test_rotation_matrix_z_axis_complex(): @@ -144,10 +144,10 @@ def test_RotationSH(): assert rot._n_max == n_max assert rot.n_max == n_max - with pytest.raises(ValueError): + with pytest.raises(ValueError, match='order needs to be a positive value'): rot.n_max = -1 - D_Rot = rot.as_spherical_harmonic(type='real') + D_Rot = rot.as_spherical_harmonic(basis_type='real') reference = np.array([ [1, 0, 0, 0, 0, 0, 0, 0, 0], @@ -164,17 +164,17 @@ def test_RotationSH(): rot = RotationSH.from_rotvec(n_max, [0, 0, 90], degrees=True) np.testing.assert_allclose( - rot.as_spherical_harmonic(type='real'), + rot.as_spherical_harmonic(basis_type='real'), reference, atol=1e-10) rot = RotationSH.from_euler(n_max, 'zyz', [0, 0, 90], degrees=True) np.testing.assert_allclose( - rot.as_spherical_harmonic(type='real'), + rot.as_spherical_harmonic(basis_type='real'), reference, atol=1e-10) rot = RotationSH.from_quat(n_max, [0, 0, 1/np.sqrt(2), 1/np.sqrt(2)]) np.testing.assert_allclose( - rot.as_spherical_harmonic(type='real'), + rot.as_spherical_harmonic(basis_type='real'), reference, atol=1e-10) rot_mat_z_spat = np.array([ @@ -183,11 +183,5 @@ def test_RotationSH(): [0, 0, 1]]) rot = RotationSH.from_matrix(n_max, rot_mat_z_spat) np.testing.assert_allclose( - rot.as_spherical_harmonic(type='real'), + rot.as_spherical_harmonic(basis_type='real'), reference, atol=1e-7) - - # mrp = [0, 0, np.tan(np.pi/2/4)] - # rot = RotationSH.from_mrp(n_max, mrp) - # np.testing.assert_allclose( - # rot.as_spherical_harmonic(type='real'), - # reference, atol=1e-7) diff --git a/tests/test_samplings.ipynb b/tests/test_samplings.ipynb deleted file mode 100644 index de3b902c..00000000 --- a/tests/test_samplings.ipynb +++ /dev/null @@ -1,4864 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "%matplotlib notebook" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import pysh.samplings as samplings\n", - "import pysh.plot as shp\n", - "from pysh.samplings import sph2cart" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " // select the cell after this one\n", - " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", - " IPython.notebook.select(index + 1);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "r, theta, phi, weights = samplings.gaussian(4)\n", - "\n", - "x,y,z = sph2cart(r,theta,phi)\n", - "shp.scatter(x,y,z)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " // select the cell after this one\n", - " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", - " IPython.notebook.select(index + 1);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "r, theta, phi = samplings.icosahedron()\n", - "\n", - "x,y,z = sph2cart(r,theta,phi)\n", - "shp.scatter(x,y,z)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " // select the cell after this one\n", - " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", - " IPython.notebook.select(index + 1);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "n_max = 3\n", - "\n", - "rad, theta, phi = samplings.healpix(n_max)\n", - "\n", - "x,y,z = sph2cart(rad,theta,phi)\n", - "shp.scatter(x,y,z)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.6.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tests/test_samplings.py b/tests/test_samplings.py index 7389894c..3341b90b 100644 --- a/tests/test_samplings.py +++ b/tests/test_samplings.py @@ -1,108 +1,512 @@ -""" Tests for spatial sampling functions """ - import numpy as np import pytest import spharpy.samplings as samplings -from spharpy.samplings.coordinates import Coordinates, SamplingSphere +from spharpy import SamplingSphere +from pyfar import Coordinates +import numpy.testing as npt +from spharpy.spherical import ( + spherical_harmonic_basis_real, spherical_harmonic_basis) -def test_cube_equidistant(): +@pytest.mark.parametrize("flatten_output", [True, False]) +def test_equidistant_cuboid_sampling_int(flatten_output): n_points = 3 - coords = samplings.cube_equidistant(n_points) - x = np.tile(np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1]), 3) - y = np.hstack((np.ones(9) * -1, np.zeros(9), np.ones(9))) - z = np.tile(np.array([-1, 0, 1]), 9) + coords = samplings.equidistant_cuboid( + n_points, flatten_output=flatten_output) + data = np.linspace(-1, 1, 3) + x, y, z = np.meshgrid(data, data, data, indexing='ij') + if flatten_output: + x = x.flatten() + y = y.flatten() + z = z.flatten() np.testing.assert_allclose(x, coords.x) np.testing.assert_allclose(y, coords.y) np.testing.assert_allclose(z, coords.z) + assert type(coords) is Coordinates + assert coords.csize == 3**3 + if flatten_output: + assert coords.cshape == (3*3*3,) + else: + assert coords.cshape == (3, 3, 3) + + +def test_equidistant_cuboid_sampling_tuple(): + c = samplings.equidistant_cuboid((2, 3, 4), flatten_output=False) + assert c.csize == 2*3*4 + assert c.cshape == (2, 3, 4) + npt.assert_allclose(c.x[0], c.x[0, 0, 0]) + npt.assert_allclose(c.y[:, 0], c.y[0, 0, 0]) + npt.assert_allclose(c.z[:, :, 0], c.z[0, 0, 0]) + + +def test_equidistant_cuboid_sampling_tuple_flatten(): + c = samplings.equidistant_cuboid((2, 3, 4), flatten_output=True) + assert c.csize == 2*3*4 + assert c.cshape == (2*3*4,) + + +def test_equidistant_cuboid_sampling_invalid(): + with pytest.raises(ValueError, match='flatten_output must be a boolean.'): + samplings.equidistant_cuboid(3, flatten_output='bla') + with pytest.raises(ValueError, match='The number of points needs to be'): + samplings.equidistant_cuboid(-3) + with pytest.raises(ValueError, match='The number of points needs to be'): + samplings.equidistant_cuboid((3, -3, 3)) + with pytest.raises(ValueError, match='The number of points needs to be'): + samplings.equidistant_cuboid((3, 3, 3.2)) + with pytest.raises(ValueError, match='The number of points needs to be'): + samplings.equidistant_cuboid((3, 3, -3)) + + +def test_t_design_const_e(download_sampling): + order = 2 + download_sampling('t-design', np.arange(1, 11)) + coords = samplings.t_design( + n_max=order, criterion='const_energy') + assert type(coords) is SamplingSphere -def test_hyperinterpolation(): - n_max = 1 - sampling = samplings.hyperinterpolation(n_max) - assert sampling.radius.size == (n_max+1)**2 +def test_t_design_const_angle(download_sampling): + order = 2 + download_sampling('t-design', np.arange(1, 11)) + coords = samplings.t_design( + n_max=order, criterion='const_angular_spread') + assert type(coords) is SamplingSphere -def test_spherical_t_design_const_e(): +def test_t_design_invalid(download_sampling): order = 2 - coords = samplings.spherical_t_design( - order, criterion='const_energy') - assert isinstance(coords, SamplingSphere) + download_sampling('t-design', np.arange(1, 11)) + with pytest.raises(ValueError, match='Invalid design'): + samplings.t_design(n_max=order, criterion='bla') -def test_spherical_t_design_const_angle(): - order = 2 - coords = samplings.spherical_t_design( - order, criterion='const_angular_spread') - assert isinstance(coords, SamplingSphere) +def test_sph_t_design(download_sampling): + # load test data + download_sampling('t-design', np.arange(1, 11)) + # test without parameters + assert samplings.t_design() is None -def test_spherical_t_design_invalid(): - order = 2 - with pytest.raises(ValueError, match='Invalid design'): - samplings.spherical_t_design(order, criterion='bla') + # test with degree + c = samplings.t_design(2) + isinstance(c, SamplingSphere) + assert type(c) is SamplingSphere + assert c.csize == 6 + + # test with spherical harmonic order + c = samplings.t_design(n_max=1) + assert c.csize == 6 + c = samplings.t_design( + n_max=1, criterion='const_angular_spread') + assert c.csize == 8 + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test user radius + c = samplings.t_design(2, radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test loading degree order > 9 + c = samplings.t_design(10) + + # test quadrature + assert not c.quadrature + + # test exceptions + with pytest.raises(ValueError, match='n_points or n_max must be None'): + c = samplings.t_design(4, 1) + with pytest.raises(ValueError, match='degree must be between 1 and 180'): + c = samplings.t_design(degree=0) + with pytest.raises(ValueError, match='degree must be between 1 and 180'): + c = samplings.t_design(n_max=0) + with pytest.raises(ValueError, match='Invalid design criterion'): + c = samplings.t_design(2, criterion='const_thread') def test_dodecahedron(): sampling = samplings.dodecahedron() - assert isinstance(sampling, SamplingSphere) + assert type(sampling) is SamplingSphere + + +def test_sph_dodecahedron(): + # test with default radius + c = samplings.dodecahedron() + assert type(c) is SamplingSphere + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test with user radius + c = samplings.dodecahedron(1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test quadrature + assert not c.quadrature def test_icosahedron(): sampling = samplings.icosahedron() - assert isinstance(sampling, SamplingSphere) + assert type(sampling) is SamplingSphere + + # test quadrature + assert not sampling.quadrature + + +def test_sph_icosahedron(): + # test with default radius + c = samplings.icosahedron() + assert type(c) is SamplingSphere + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test with user radius + c = samplings.icosahedron(1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) def test_equiangular(): - n_max = 1 - sampling = samplings.equiangular(n_max) - assert isinstance(sampling, SamplingSphere) + # test without parameters + with pytest.raises(ValueError, match='Either the n_points or n_max needs'): + samplings.equiangular() + + # test with single number of points + c = samplings.equiangular(5) + assert type(c) is SamplingSphere + assert c.csize == 5**2 + + # test with tuple + c = samplings.equiangular((3, 5)) + assert c.csize == 3*5 + + # test with spherical harmonic order + c = samplings.equiangular(n_max=5) + assert c.csize == 4 * (5 + 1)**2 + npt.assert_allclose(np.sum(c.weights), 4*np.pi) + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test user radius + c = samplings.equiangular(5, radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + +@pytest.mark.parametrize("n_points", np.arange(2, 40, 2)) +def test_equiangular_weights_n_points_even(n_points): + sampling = samplings.equiangular(n_points=n_points) + npt.assert_almost_equal(np.sum(sampling.weights), 4*np.pi) + assert sampling.cshape == sampling.weights.shape + assert sampling.cshape == n_points*n_points + assert sampling.quadrature + + +@pytest.mark.parametrize("n_points", np.arange(1, 40, 2)) +def test_equiangular_weights_n_points_odd(n_points): + sampling = samplings.equiangular(n_points=n_points) + assert sampling.weights is None + assert sampling.cshape == n_points*n_points + assert not sampling.quadrature + + +@pytest.mark.parametrize( + "n_points", + [(5, 5), (4, 5), (4, 6)], +) +def test_equiangular_weights_n_points_tuple_invalid(n_points): + sampling = samplings.equiangular(n_points=n_points) + assert sampling.weights is None + assert sampling.quadrature is False + + +def test_equiangular_weights_n_points_tuple_valid(): + n_points = (4, 4) + sampling = samplings.equiangular(n_points=n_points) + npt.assert_almost_equal(np.sum(sampling.weights), 4*np.pi) + + +@pytest.mark.parametrize("n_max", np.arange(1, 15)) +def test_equiangular_weights_n_max(n_max): + sampling = samplings.equiangular(n_max=n_max) + npt.assert_almost_equal(np.sum(sampling.weights), 4*np.pi) + assert sampling.cshape == sampling.weights.shape + assert sampling.cshape == 4*(n_max+1)**2 + assert type(sampling) is SamplingSphere + + +@pytest.mark.parametrize( + 'basis_func', [spherical_harmonic_basis, spherical_harmonic_basis_real]) +def test_equiangular_orthogonality(basis_func): + n_max = 4 + sampling = samplings.equiangular(n_max=n_max) + + Y = basis_func(n_max, sampling) + npt.assert_allclose( + Y.conj().T @ np.diag(sampling.weights) @ Y, + np.eye((n_max+1)**2), + atol=1e-6, rtol=1e-6) + + +@pytest.mark.parametrize("n_points", np.arange(1, 40)) +def test_gaussian_weights_n_points(n_points): + sampling = samplings.gaussian(n_points=n_points) + npt.assert_almost_equal(np.sum(sampling.weights), 4*np.pi) + assert sampling.cshape == sampling.weights.shape + assert sampling.cshape == 2*n_points*n_points + assert type(sampling) is SamplingSphere + + +@pytest.mark.parametrize("n_max", np.arange(1, 15)) +def test_gaussian_weights_n_max(n_max): + sampling = samplings.gaussian(n_max=n_max) + npt.assert_almost_equal(np.sum(sampling.weights), 4*np.pi) + assert sampling.cshape == sampling.weights.shape + assert sampling.cshape == 2*(n_max+1)*(n_max+1) + assert type(sampling) is SamplingSphere + + +@pytest.mark.parametrize( + 'basis_func', [spherical_harmonic_basis, spherical_harmonic_basis_real]) +def test_gaussian_orthogonality(basis_func): + n_max = 4 + sampling = samplings.gaussian(n_max=n_max) + + Y = basis_func(n_max, sampling) + npt.assert_allclose( + Y.conj().T @ np.diag(sampling.weights) @ Y, + np.eye((n_max+1)**2), + atol=1e-6, rtol=1e-6) + + +def test_gaussian_quadrature(): + n_max = 3 + sampling = samplings.gaussian(n_max=n_max) + + assert sampling.quadrature def test_gaussian(): - n_max = 1 - sampling = samplings.gaussian(n_max) - assert isinstance(sampling, SamplingSphere) + # test without parameters + with pytest.raises(ValueError, match='Either the n_points or n_max needs'): + samplings.gaussian() + + # n_points must be a positive natural number + with pytest.raises(ValueError, match='positive natural number'): + samplings.gaussian(n_points=(2, 2)) + + # n_points must be a positive natural number + with pytest.raises(ValueError, match='positive natural number'): + samplings.gaussian(n_points=3.2) + + # test with single number of points + c = samplings.gaussian(5) + assert type(c) is SamplingSphere + assert c.csize == 5*(5*2) + npt.assert_allclose(np.sum(c.weights), 4*np.pi) + + # test with spherical harmonic order + c = samplings.gaussian(n_max=5) + assert c.csize == 2 * (5 + 1)**2 + npt.assert_allclose(np.sum(c.weights), 4*np.pi) + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test user radius + c = samplings.gaussian(5, radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test quadrature + npt.assert_allclose(np.sum(c.weights), 4 * np.pi) + assert c.quadrature def test_em32(): sampling = samplings.eigenmike_em32() - assert isinstance(sampling, SamplingSphere) + assert type(sampling) is SamplingSphere def test_icosahedron_ke4(): sampling = samplings.icosahedron_ke4() - assert isinstance(sampling, SamplingSphere) + assert type(sampling) is SamplingSphere + + # test quadrature + assert not sampling.quadrature def test_equalarea(): - sampling = samplings.equalarea(2) - assert isinstance(sampling, SamplingSphere) + sampling = samplings.equal_area(2) + assert type(sampling) is SamplingSphere + + # test quadrature + assert not sampling.quadrature def test_spiral_points(): sampling = samplings.spiral_points(2) - assert isinstance(sampling, SamplingSphere) + assert type(sampling) is SamplingSphere + + # test quadrature + assert not sampling.quadrature + + +def test_equal_angle(): + # test with tuple + c = samplings.equal_angle((10, 20)) + assert type(c) is SamplingSphere + # test with number + c = samplings.equal_angle(10) + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + # test user radius + c = samplings.equal_angle(10, 1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test assertions + with pytest.raises(ValueError, + match='delta_phi must be an integer divisor'): + c = samplings.equal_angle((11, 20)) + with pytest.raises(ValueError, + match='delta_theta must be an integer divisor'): + c = samplings.equal_angle((20, 11)) + + # test quadrature + assert not c.quadrature + + +def test_great_circle(): + # test with default values + c = samplings.great_circle() + assert type(c) is SamplingSphere + # check default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test if azimuth matching angles work + c = samplings.great_circle(0, 4, match=90) + azimuth = c.azimuth * 180 / np.pi + for deg in [0, 90, 180, 270]: + assert deg in azimuth + + # test user radius + c = samplings.great_circle(radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test fractional azimuth resolution + c = samplings.great_circle(60, 4, azimuth_res=.1, match=90) + npt.assert_allclose(c.azimuth[1] * 180 / np.pi, 7.5, atol=1e-15) + + # test quadrature + assert not c.quadrature + + # test assertion: 1 / azimuth_res is not an integer + with pytest.raises(AssertionError): + samplings.great_circle(azimuth_res=.6) + # test assertion: 360 / match is not an integer + with pytest.raises(AssertionError): + samplings.great_circle(match=270) + # test assertion: match / azimuth_res is not an integer + with pytest.raises(AssertionError): + samplings.great_circle(azimuth_res=.5, match=11.25) + + +def test_lebedev(): + # test without parameters + assert samplings.lebedev() is None + + # test with degree + c = samplings.lebedev(14) + assert type(c) is SamplingSphere + assert c.csize == 14 + + # test with spherical harmonic order + c = samplings.lebedev(n_max=3) + assert c.csize == 26 + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test user radius + c = samplings.lebedev(6, radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test quadrature + npt.assert_allclose(np.sum(c.weights), 4 * np.pi) + assert c.quadrature + + +def test_fliege(): + # test without parameters + assert samplings.fliege() is None + + # test with degree + c = samplings.fliege(16) + assert type(c) is SamplingSphere + assert c.csize == 16 + + # test with spherical harmonic order + c = samplings.fliege(n_max=3) + assert c.csize == 16 + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + + # test user radius + c = samplings.fliege(4, radius=1.5) + npt.assert_allclose(c.radius, 1.5, atol=1e-15) + + # test quadrature + npt.assert_allclose(np.sum(c.weights), 4 * np.pi) + assert not c.quadrature + + # test exceptions + with pytest.raises(ValueError, match='n_points or n_max must be None'): + c = samplings.fliege(9, 2) + with pytest.raises(ValueError, match='Invalid number of points n_points'): + c = samplings.fliege(30) def test_em64(): sampling = samplings.eigenmike_em64() - assert isinstance(sampling, SamplingSphere) - - # weights = np.array([ - # 0.954, 0.9738, 1.0029, 1.0426, 1.0426, 1.0024, 0.9738, 0.954, 1.009, - # 0.9932, 1.0024, 1.0324, 0.954, 1.0024, 1.0079, 1.0268, 1.0151, 0.9463, - # 1.012, 1.0253, 1.009, 0.9932, 1.0324, 1.0151, 0.954, 1.0079, 1.0029, - # 1.0024, 1.0268, 0.9463, 1.012, 1.0253, 0.954, 0.9738, 1.0029, 1.0426, - # 1.0426, 1.0024, 0.954, 0.9738, 1.0268, 1.0151, 1.012, 0.9463, 1.0253, - # 1.009, 0.9932, 1.0024, 1.0324, 1.0029, 0.954, 1.0024, 1.0324, 1.0151, - # 0.954, 1.0079, 1.0024, 1.0079, 1.0268, 1.012, 0.9463, 1.009, 1.0253, - # 0.9932, - # ]) / 64 * 4 * np.pi - - # # check the individual weights - # np.testing.assert_allclose(sampling.weights, weights) - - # # check if the weigths sum up to 4*pi to ensure valid integration on the unit sphere - # np.testing.assert_allclose(np.sum(sampling.weights), 4*np.pi, atol=1e-4, rtol=1e-4) + assert type(sampling) is SamplingSphere + + npt.assert_allclose( + np.sum(sampling.weights), 4*np.pi, atol=1e-6, rtol=1e-6) + + +def test_hyperinterpolation_default(download_sampling): + n_max = 1 + # download just required sampling for testing + download_sampling('hyperinterpolation', [n_max]) + + c = samplings.hyperinterpolation(1) + + # test sampling properties + assert type(c) is SamplingSphere + assert c.n_max == n_max + assert c.csize == (n_max+1)**2 + assert c.radius.size == (n_max+1)**2 + + # test default radius + npt.assert_allclose(c.radius, 1, atol=1e-15) + npt.assert_allclose(np.sum(c.weights), 4 * np.pi) + + +@pytest.mark.parametrize("radius", [1, 5]) +def test_hyperinterpolation_radius(download_sampling, radius): + download_sampling('hyperinterpolation', [1]) + sampling = samplings.hyperinterpolation(1, radius=radius) + assert type(sampling) is SamplingSphere + npt.assert_allclose(sampling.radius, radius, atol=1e-15) + + +@pytest.mark.parametrize("n_max", [-1, 'one']) +def test_hyperinterpolation_errors_n_max(n_max): + with pytest.raises( + ValueError, match='n_max must be an integer between 1 and 200'): + samplings.hyperinterpolation(n_max) + + +@pytest.mark.parametrize("radius", [-1, 'one']) +def test_hyperinterpolation_errors_radius(radius): + with pytest.raises( + ValueError, match='radius must be a single positive value'): + samplings.hyperinterpolation(1, radius) diff --git a/tests/test_samplings_interior.py b/tests/test_samplings_interior.py index 4932e833..b764af25 100644 --- a/tests/test_samplings_interior.py +++ b/tests/test_samplings_interior.py @@ -19,4 +19,4 @@ def test_interior_points_chardon(): truth = np.array([[0, 0, 0]], dtype=float) - np.testing.assert_allclose(int_points.cartesian.T, truth, atol=1e-7) + np.testing.assert_allclose(int_points.cartesian, truth, atol=1e-7) diff --git a/tests/test_sh_class.py b/tests/test_sh_class.py new file mode 100644 index 00000000..4e33e0fc --- /dev/null +++ b/tests/test_sh_class.py @@ -0,0 +1,266 @@ +""" +Tests for spherical harmonic class. +""" +import pytest +import numpy as np +from spharpy import SphericalHarmonics +from spharpy.classes.sh import SphericalHarmonicDefinition + + +def test_spherical_harmonics_definition_init(): + """Test default behavior.""" + definition = SphericalHarmonicDefinition() + assert definition.basis_type == 'real' + assert definition.normalization == 'N3D' + assert definition.channel_convention == 'ACN' + assert definition.condon_shortley is False + + +@pytest.mark.parametrize("phase_convention", [True, False]) +def test_setter_phase_convention(phase_convention): + """Test setting Condon-Shortley phase convention with boolean values.""" + sph_harm = SphericalHarmonicDefinition() + sph_harm.condon_shortley = phase_convention + assert sph_harm.condon_shortley == phase_convention + + +def test_init_phase_convention_auto(): + """Test initialization with auto Condon-Shortley phase convention.""" + sph_harm = SphericalHarmonicDefinition( + basis_type="complex", + condon_shortley="auto") + assert sph_harm.condon_shortley is True + + sph_harm = SphericalHarmonicDefinition( + basis_type="real", + condon_shortley="auto") + assert sph_harm.condon_shortley is False + +def test_setter_phase_convention_auto(): + """Test setting Condon-Shortley phase convention to auto.""" + sph_harm = SphericalHarmonicDefinition() + sph_harm.basis_type = "complex" + sph_harm.condon_shortley = "auto" + assert sph_harm.condon_shortley is True + + sph_harm = SphericalHarmonicDefinition() + sph_harm.basis_type = "real" + sph_harm.condon_shortley = "auto" + assert sph_harm.condon_shortley is False + + +def test_setter_phase_convention_invalid(): + """Test error handling for invalid Condon-Shortley phase values.""" + sph_harm = SphericalHarmonicDefinition() + with pytest.raises(ValueError, match='must be a bool or the string'): + sph_harm.condon_shortley = 123 # Invalid type + + with pytest.raises(ValueError, match='must be a bool or the string'): + sph_harm.condon_shortley = "invalid" # Invalid string + + +@pytest.mark.parametrize("channel_convention", ["ACN", "FuMa"]) +def test_setter_channel_convention_definition(channel_convention): + """Test setting channel convention for spherical harmonic definition.""" + sph_harm = SphericalHarmonicDefinition() + sph_harm.channel_convention = channel_convention + assert sph_harm.channel_convention == channel_convention + + +@pytest.mark.parametrize("channel_convention", ["ACN", "FuMa"]) +def test_init_channel_convention_definition(channel_convention): + """Test initialization with different channel conventions.""" + sph_harm = SphericalHarmonicDefinition( + channel_convention=channel_convention) + assert sph_harm.channel_convention == channel_convention + + +def test_setter_channel_convention_fuma_error(): + """Test error when setting FUMA channel convention with n_max > 3.""" + sph_harm = SphericalHarmonicDefinition(n_max=4) + + message = 'n_max > 3 is not allowed with' + + with pytest.raises(ValueError, match=message): + sph_harm.channel_convention = "FuMa" + + with pytest.raises(ValueError, match=message): + SphericalHarmonicDefinition( + n_max=4, + channel_convention="FuMa", + ) + + +def test_setter_channel_convention_definition_invalid(): + """Test error handling for invalid channel convention values.""" + sph_harm = SphericalHarmonicDefinition() + with pytest.raises(ValueError, match='Invalid channel convention'): + sph_harm.channel_convention = "invalid" # Invalid value + + +@pytest.mark.parametrize("normalization", ["N3D", "SN3D", "maxN", "NM", "SNM"]) +def test_setter_normalization_definition(normalization): + """Test setting different normalization conventions.""" + sph_harm = SphericalHarmonicDefinition() + sph_harm.normalization = normalization + assert sph_harm.normalization == normalization + + +@pytest.mark.parametrize("normalization", ["N3D", "SN3D", "maxN", "NM", "SNM"]) +def test_init_normalization_definition(normalization): + """Test initialization with different normalization conventions.""" + sph_harm = SphericalHarmonicDefinition( + normalization=normalization) + assert sph_harm.normalization == normalization + + +def test_setter_normalization_definition_invalid(): + """Test error handling for invalid normalization values.""" + sph_harm = SphericalHarmonicDefinition() + with pytest.raises(ValueError, match='Invalid normalization'): + sph_harm.normalization = "invalid" # Invalid value + + +def test_setter_normalization(): + """Test error when setting maxN normalization with n_max > 3.""" + sph_harm = SphericalHarmonicDefinition(n_max=4) + + with pytest.raises(ValueError, match='n_max > 3 is not allowed with'): + sph_harm.normalization = "maxN" # Invalid with n_max > 3 + + +def test_sh_definition_setter_n_max(): + """Test setting n_max property and error handling for invalid values.""" + sph_harm = SphericalHarmonicDefinition(n_max=2) + sph_harm.n_max = 3 + assert sph_harm.n_max == 3 + + with pytest.raises(ValueError, match='n_max must be a positive integer'): + sph_harm.n_max = -1 # Invalid value + + +@pytest.mark.parametrize( + ('norm', 'convention'), [('maxN', 'ACN'), ('N3D', 'FuMa')]) +def test_sh_definition_setter_n_max_invalid_combinations(norm, convention): + """Test error when setting n_max > 3 with incompatible combinations.""" + sph_harm = SphericalHarmonicDefinition(n_max=2) + + sph_harm.channel_convention = convention + sph_harm.normalization = norm + with pytest.raises(ValueError, match='n_max > 3 is not allowed'): + sph_harm.n_max = 4 + + +def test_setter_basis_type(): + """Test setting basis type and error handling for invalid values.""" + sph_harm = SphericalHarmonicDefinition() + sph_harm.basis_type = "complex" + assert sph_harm.basis_type == "complex" + + sph_harm.basis_type = "real" + assert sph_harm.basis_type == "real" + + with pytest.raises(ValueError, match='Invalid basis type'): + sph_harm.basis_type = "invalid" # Invalid value + + +def test_sphharm_init(icosahedron_sampling): + """Test default behaviour after initialization.""" + sph_harm = SphericalHarmonics(n_max=2, coordinates=icosahedron_sampling) + assert sph_harm.n_max == 2 + assert np.all(sph_harm.coordinates == icosahedron_sampling) + assert sph_harm.inverse_method == 'quadrature' + +def test_sphharm_init_invalid_coordinates(): + """Test error handling for invalid coordinate types.""" + with pytest.raises(TypeError, + match="coordinates must be a pyfar.Coordinates " \ + "object or spharpy.SamplingSphere object"): + SphericalHarmonics(n_max=2, coordinates=[0, 0, 1]) + +def test_sphharm_init_invalid_n_max(icosahedron_sampling): + """Test error handling for invalid n_max values.""" + with pytest.raises(ValueError, match='n_max must be a positive integer'): + SphericalHarmonics(n_max=-1, coordinates=icosahedron_sampling) + +def test_sphharm_compute_basis(icosahedron_sampling): + """Test spherical harmonic basis computation.""" + sph_harm = SphericalHarmonics(n_max=2, coordinates=icosahedron_sampling) + assert sph_harm.basis is not None + +def test_sphharm_compute_basis_gradient(icosahedron_sampling): + """Test spherical harmonic basis gradient computation.""" + sph_harm = SphericalHarmonics(n_max=2, coordinates=icosahedron_sampling) + assert sph_harm.basis_gradient_theta is not None + assert sph_harm.basis_gradient_phi is not None + +def test_sphharm_compute_inverse_quad(icosahedron_sampling): + """Test spherical harmonic inverse computation using quadrature method.""" + sh = SphericalHarmonics( + 1, coordinates=icosahedron_sampling, inverse_method='quadrature') + assert sh.basis_inv is not None + +def test_sphharm_compute_inverse_pseudo_inv(icosahedron_sampling): + """Test spherical harmonic inverse using pseudo-inverse method.""" + sh = SphericalHarmonics(2, coordinates=icosahedron_sampling, + inverse_method='pseudo_inverse') + assert sh.basis_inv is not None + +def test_compute_basis_caching(icosahedron_sampling): + """Test that basis computation results are cached and invalidated.""" + n_max = 1 + sh = SphericalHarmonics( + n_max, icosahedron_sampling, + basis_type='real', + normalization='N3D', + channel_convention='ACN', + condon_shortley=False, + ) + + # Call the method once and store the result + last_result = sh.basis + + sh.n_max = n_max # Setting to the same value should not reset cache + # Call the method again and check that the result is the same (cache hit) + assert sh.basis is last_result + + # Change a property that affects the output of _compute_basis() + sh.n_max = 3 + + new_result = sh.basis + + # Call the method again and check that the result is different (cache miss) + assert new_result is not last_result + last_result = new_result + + sh.normalization = 'SN3D' + new_result = sh.basis + assert new_result is not last_result + last_result = new_result + + sh.channel_convention = 'FuMa' + new_result = sh.basis + assert new_result is not last_result + last_result = new_result + + sh.basis_type = 'complex' + new_result = sh.basis + assert new_result is not last_result + last_result = new_result + + sh.condon_shortley = True + new_result = sh.basis + assert new_result is not last_result + + +def test_setter_inverse_method(icosahedron_sampling): + """Test setting inverse method and error handling for invalid values.""" + sph_harm = SphericalHarmonics(n_max=2, coordinates=icosahedron_sampling) + sph_harm.inverse_method = "quadrature" + assert sph_harm.inverse_method == "quadrature" + + with pytest.raises( + ValueError, + match=("Invalid inverse_method. Allowed: 'pseudo_inverse', " + "'quadrature', or 'auto'.")): + sph_harm.inverse_method = "invalid" # Invalid value diff --git a/tests/test_special.py b/tests/test_special.py index 6830cc96..31253401 100644 --- a/tests/test_special.py +++ b/tests/test_special.py @@ -1,17 +1,16 @@ """ -Tests for special functions +Tests for special functions. """ import pytest from spharpy import special -from spharpy import samplings import numpy as np import numpy.testing as npt def genfromtxt_complex(filename, delimiter=','): - """generate complex numpy array from csv file.""" + """Generate complex numpy array from csv file.""" data_str = np.genfromtxt(filename, delimiter=delimiter, dtype=str) mapping = np.vectorize(lambda t: complex(t.replace('i', 'j'))) return mapping(data_str) @@ -19,6 +18,7 @@ def genfromtxt_complex(filename, delimiter=','): class TestBessel(object): def test_shape(self): + """Test shape of spherical Bessel function.""" n = np.array([0, 1]) z = np.linspace(0.1, 5, 10) res = special.spherical_bessel(n, z) @@ -27,6 +27,7 @@ def test_shape(self): assert shape == res.shape def test_val(self): + """Test spherical Bessel function.""" z = np.linspace(0, 10, 25) n = [0, 1, 2] res = special.spherical_bessel(n, z) @@ -36,6 +37,7 @@ def test_val(self): class TestBesselPrime(object): def test_shape(self): + """Test shape of spherical Bessel function derivative.""" n = np.array([0, 1]) z = np.linspace(0.1, 5, 10) res = special.spherical_bessel(n, z, derivative=True) @@ -44,6 +46,7 @@ def test_shape(self): assert shape == res.shape def test_val(self): + """Test spherical Bessel function derivative.""" z = np.linspace(0.1, 10, 25) n = [0, 1, 2] res = special.spherical_bessel(n, z, derivative=True) @@ -53,6 +56,7 @@ def test_val(self): class TestHankel(object): def test_shape(self): + """Test shape of spherical Hankel function.""" n = np.array([0, 1]) z = np.linspace(0.1, 5, 10) res = special.spherical_hankel(n, z, kind=1) @@ -61,10 +65,12 @@ def test_shape(self): assert shape == res.shape def test_kind_exception(self): - with pytest.raises(ValueError): + """Test if ValueError is raised for invalid kind.""" + with pytest.raises(ValueError, match='first or second kind'): special.spherical_hankel([0], [1], kind=3) def test_val_second_kind(self): + """Test spherical Hankel function of second kind.""" z = np.linspace(0.1, 5, 25) n = np.array([0, 1, 2]) res = special.spherical_hankel(n, z, kind=2) @@ -72,6 +78,7 @@ def test_val_second_kind(self): npt.assert_allclose(res, truth) def test_val_first_kind(self): + """Test spherical Hankel function of first kind.""" z = np.linspace(0.1, 5, 25) n = np.array([0, 1, 2]) res = special.spherical_hankel(n, z, kind=1) @@ -81,6 +88,7 @@ def test_val_first_kind(self): class TestHankelPrime(object): def test_shape(self): + """Test shape of spherical Hankel function derivative.""" n = np.array([0, 1]) z = np.linspace(0.1, 5, 10) res = special.spherical_hankel(n, z, kind=1, derivative=True) @@ -89,24 +97,65 @@ def test_shape(self): assert shape == res.shape def test_kind_exception(self): - with pytest.raises(ValueError): + """Test if ValueError is raised for invalid kind.""" + with pytest.raises(ValueError, match='first or second kind'): special.spherical_hankel([0], [1], kind=3, derivative=True) def test_val_second_kind(self): + """Test spherical Hankel function derivative of second kind.""" z = np.linspace(0.1, 5, 25) n = [0, 1, 2] res = special.spherical_hankel(n, z, kind=2, derivative=True) - truth = genfromtxt_complex('./tests/data/hankel_2_diff.csv', delimiter=',') + truth = genfromtxt_complex( + './tests/data/hankel_2_diff.csv', delimiter=',') npt.assert_allclose(res, truth) def test_val_first_kind(self): + """Test spherical Hankel function derivative of first kind.""" z = np.linspace(0.1, 5, 25) n = [0, 1, 2] res = special.spherical_hankel(n, z, kind=1, derivative=True) - truth = genfromtxt_complex('./tests/data/hankel_1_diff.csv', delimiter=',') + truth = genfromtxt_complex( + './tests/data/hankel_1_diff.csv', delimiter=',') npt.assert_allclose(res, truth) +@pytest.mark.parametrize('m', [-1, 0, 1]) +def test_spherical_harmonic_complex(m): + """ + Test first order complex valued spherical harmonics for selected angels. + """ + # six positions: front, left, back, right, top, bottom + pi = np.pi + azimuth = np.array([0, pi / 2, pi, 3 * pi / 2, 0, 0]) + colatitude = np.array([pi / 2, pi / 2, pi / 2, pi / 2, 0, pi]) + + # Manually computed desired values according to + # Rafaely (2019), Fundamentals of Spherical Array Processing, Table 1.1 + if m == -1: + desired = np.sqrt(3 / (8 * np.pi)) * \ + np.sin(colatitude) * np.exp(-1j * azimuth) + if m == 0: + desired = np.sqrt(3 / (4 * np.pi)) * \ + np.cos(colatitude) + if m == 1: + desired = -np.sqrt(3 / (8 * np.pi)) * \ + np.sin(colatitude) * np.exp(1j * azimuth) + + # compute and compare actual values + actual = special.spherical_harmonic(1, m, colatitude, azimuth) + npt.assert_almost_equal(actual, desired, 10) + + +def test_spherical_harmonic_complex_degree_out_of_range(): + """Test if zero is returned if the degree m is larger than the order n.""" + n = 1 + m = [-2, 2] + + npt.assert_equal(special.spherical_harmonic(n, m, 0, 0), + np.array([0, 0], dtype=complex)) + + def test_spherical_harmonic_derivative_theta(): n_max = 5 theta = np.array([np.pi/2, np.pi/2, 0, np.pi/2, np.pi/4]) diff --git a/tests/test_spharpy.py b/tests/test_spharpy.py deleted file mode 100644 index db2d3896..00000000 --- a/tests/test_spharpy.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Tests for `spharpy` package.""" - -import pytest - - -import spharpy - - -@pytest.fixture -def response(): - """Sample pytest fixture. - - See more at: http://doc.pytest.org/en/latest/fixture.html - """ - # import requests - # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') - - -def test_content(response): - """Sample pytest test function with the pytest fixture as an argument.""" - # from bs4 import BeautifulSoup - # assert 'GitHub' in BeautifulSoup(response.content).title.string diff --git a/tests/test_spherical.py b/tests/test_spherical.py new file mode 100644 index 00000000..9cd81dc3 --- /dev/null +++ b/tests/test_spherical.py @@ -0,0 +1,181 @@ +""" +Tests renormalization and change channel convention methods. +""" +import pytest +import numpy as np +import spharpy.spherical as sh +import re + + +def test_renormalize_errors(): + sh_data = np.ones((4, 2)) + + # test channel convention + with pytest.raises(ValueError, match="Invalid channel convention. Has to " + "be 'ACN' or 'FuMa', but is " + "wrong_channel_convention"): + sh.renormalize(sh_data, 'wrong_channel_convention', 'maxN', + 'N3D', axis=0) + + # test current norm + with pytest.raises(ValueError, + match="Invalid current normalization. Has to be " + "'N3D', 'NM', 'maxN', 'SN3D', or 'SNM' " + "but is wrong_norm"): + sh.renormalize(sh_data, 'ACN', 'wrong_norm', 'N3D', axis=0) + + # test target norm + with pytest.raises(ValueError, + match="Invalid target normalization. Has to be " + "'N3D', 'NM', 'maxN', 'SN3D', or 'SNM' " + "but is wrong_norm"): + sh.renormalize(sh_data, 'ACN', 'N3D', 'wrong_norm', axis=0) + + +@pytest.mark.parametrize("channel_convention", ['ACN', 'FuMa']) +def test_renormalize(channel_convention): + sh_data = np.ones((4, 2)) + + # test from n3d to maxN + current_norm = 'N3D' + target_norm = 'maxN' + sh_data_n3d_to_maxN = sh.renormalize(sh_data, channel_convention, + current_norm, + target_norm, axis=0) + sh_data_ref = np.array([[np.sqrt(1 / 2), np.sqrt(1 / 2)], + [np.sqrt(1 / 3), np.sqrt(1 / 3)], + [np.sqrt(1 / 3), np.sqrt(1 / 3)], + [np.sqrt(1 / 3), np.sqrt(1 / 3)]]) + + np.testing.assert_equal(sh_data_n3d_to_maxN, + sh_data_ref) + + # test from n3d to nm + target_norm = 'NM' + sh_data_n3d_to_nm = sh.renormalize(sh_data, channel_convention, + current_norm, + target_norm, axis=0) + np.testing.assert_equal(sh_data_n3d_to_nm, + sh_data * np.sqrt(4*np.pi)) + + # test from maxN to n3d + current_norm = 'maxN' + target_norm = 'N3D' + sh_data_maxN_to_n3d = sh.renormalize(sh_data_n3d_to_maxN, + channel_convention, + current_norm, + target_norm, axis=0) + + np.testing.assert_equal(sh_data_maxN_to_n3d, + np.ones((4, 2))) + + # test from maxN to sn3d + current_norm = 'maxN' + target_norm = 'SN3D' + sh_data_maxN_to_sn3d = sh.renormalize(sh_data_n3d_to_maxN, + channel_convention, + current_norm, + target_norm, axis=0) + # back to n3d to check against 0 + sh_data_sn3d_to_n3d = sh.renormalize(sh_data_maxN_to_sn3d, + channel_convention, + 'SN3D', + 'N3D', axis=0) + np.testing.assert_equal(sh_data_sn3d_to_n3d, + np.ones((4, 2))) + + # test from n3d to sn3d + current_norm = 'N3D' + target_norm = 'SN3D' + sh_data_n3d_to_sn3d = sh.renormalize(sh_data, channel_convention, + current_norm, + target_norm, axis=0) + sh_data_ref = np.array([[1 / np.sqrt(2 * 0 + 1), 1 / np.sqrt(2 * 0 + 1)], + [1 / np.sqrt(2 * 1 + 1), 1 / np.sqrt(2 * 1 + 1)], + [1 / np.sqrt(2 * 1 + 1), 1 / np.sqrt(2 * 1 + 1)], + [1 / np.sqrt(2 * 1 + 1), 1 / np.sqrt(2 * 1 + 1)]]) + + np.testing.assert_equal(sh_data_n3d_to_sn3d, + sh_data_ref) + + # test from sn3d to n3d + current_norm = 'SN3D' + target_norm = 'N3D' + sh_data_sn3d_to_n3d = sh.renormalize(sh_data_n3d_to_sn3d, + channel_convention, + current_norm, + target_norm, + axis=-2) + np.testing.assert_equal(sh_data_sn3d_to_n3d, + np.ones((4, 2))) + + # test from sn3d to maxN + current_norm = 'SN3D' + target_norm = 'maxN' + sh_data_sn3d_to_maxN = sh.renormalize(sh_data_n3d_to_sn3d, + channel_convention, + current_norm, + target_norm, axis=0) + + # test from sn3d to snm + current_norm = 'SN3D' + target_norm = 'SNM' + sh_data_sn3d_to_snm = sh.renormalize(sh_data_n3d_to_sn3d, + channel_convention, + current_norm, + target_norm, axis=0) + np.testing.assert_equal(sh_data_sn3d_to_snm, + sh_data_n3d_to_sn3d * np.sqrt(4*np.pi)) + + # back to n3d to check against 0 + sh_data_maxN_to_n3d = sh.renormalize(sh_data_sn3d_to_maxN, + channel_convention, + 'maxN', + 'N3D', axis=0) + np.testing.assert_equal(sh_data_maxN_to_n3d, + np.ones((4, 2))) + + +def test_renormalize_wrong_channel_number(): + sh_data = np.ones((5, 2)) + with pytest.raises( + ValueError, match=re.escape("Invalid number of SH channels: 5. " + "It must match (n_max + 1)^2.")): + sh.renormalize(sh_data, 'ACN', 'N3D', 'maxN', axis=0) + + +def test_change_channel_convention_errors(): + sh_data = np.ones((4, 2)) + # test current channel convention + with pytest.raises( + ValueError, match="Invalid current channel convention. Has to " + "be 'ACN' or 'FuMa', but is wrong"): + sh.change_channel_convention(sh_data, 'wrong', 'FuMa', axis=0) + + # test target channel convention + with pytest.raises( + ValueError, match="Invalid target channel convention. Has to " + "be 'ACN' or 'FuMa', but is wrong"): + sh.change_channel_convention(sh_data, 'FuMa', 'wrong', axis=0) + + +def test_change_channel_convention(): + sh_data = np.array([[1., 1., 1.], + [2., 2., 2.], + [3., 3., 3.], + [4., 4., 4.]]) + + # test conversion to FuMa + current_channel_convention = 'ACN' + sh_data_new_convention = sh.change_channel_convention( + sh_data, current_channel_convention, 'FuMa', axis=0) + sh_data_new_convention_fuma = sh_data[[0, 3, 1, 2], :] + np.testing.assert_equal(sh_data_new_convention_fuma, + sh_data_new_convention) + + # test conversion to acn + current_channel_convention = 'FuMa' + sh_data_new_convention = sh.change_channel_convention( + sh_data_new_convention_fuma, current_channel_convention, 'ACN', axis=0) + np.testing.assert_equal(sh_data, + sh_data_new_convention) diff --git a/tests/test_spherical_harmonics.py b/tests/test_spherical_harmonics.py index 04179a87..f8065670 100644 --- a/tests/test_spherical_harmonics.py +++ b/tests/test_spherical_harmonics.py @@ -1,5 +1,5 @@ """ -Tests for spherical harmonic basis and related functions +Tests for spherical harmonic basis and related functions. """ import spharpy.spherical as sh import numpy as np @@ -8,8 +8,13 @@ @pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_spherical_harmonic(make_coordinates, implementation): - Nmax = 1 +@pytest.mark.parametrize("normalization", ['N3D', 'NM', 'maxN', 'SN3D', 'SNM']) +@pytest.mark.parametrize("channel_convention", ['ACN', 'FuMa']) +@pytest.mark.parametrize("condon_shortley", [True, False, 'auto']) +def test_spherical_harmonic(make_coordinates, implementation, + normalization, channel_convention, + condon_shortley): + n_max = 1 theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) phi = np.array([0, np.pi/2, 0], dtype=float) rad = np.ones(3, dtype=float) @@ -17,18 +22,139 @@ def test_spherical_harmonic(make_coordinates, implementation): coords = make_coordinates.create_coordinates( implementation, rad, theta, phi) - Y = np.array([[2.820947917738781e-01 + 0.000000000000000e+00j, 3.454941494713355e-01 + 0.000000000000000e+00j, 2.991827511286337e-17 + 0.000000000000000e+00j, -3.454941494713355e-01 + 0.000000000000000e+00j], - [2.820947917738781e-01 + 0.000000000000000e+00j, 2.115541521371041e-17 - 3.454941494713355e-01j, 2.991827511286337e-17 + 0.000000000000000e+00j, -2.115541521371041e-17 - 3.454941494713355e-01j], - [2.820947917738781e-01 + 0.000000000000000e+00j, 0.000000000000000e+00 + 0.000000000000000e+00j, 4.886025119029199e-01 + 0.000000000000000e+00j, 0.000000000000000e+00 + 0.000000000000000e+00j]], dtype=complex) + phase_conv_id = 'None' if not condon_shortley else 'Condon-Shortley' - basis = sh.spherical_harmonic_basis(Nmax, coords) + norm_id = normalization + if normalization == 'NM' or normalization == 'N3D': + norm_id = 'n3d' + if normalization == 'SNM' or normalization == 'SN3D': + norm_id = 'sn3d' + + Y = np.genfromtxt(f'./tests/data/Y_cmplx_{phase_conv_id}_' + f'{norm_id}_{channel_convention.lower()}.csv', + dtype=complex, + delimiter=',') + if normalization in ('NM', 'SNM'): + Y *= np.sqrt(4 * np.pi) + + basis = sh.spherical_harmonic_basis(n_max, coords, + normalization=normalization, + channel_convention=channel_convention, + condon_shortley=condon_shortley) np.testing.assert_allclose(Y, basis, atol=1e-13) @pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_spherical_harmonic_n10(make_coordinates, implementation): - Nmax = 10 +@pytest.mark.parametrize("condon_shortley", [True, False, 'auto']) +@pytest.mark.parametrize("channel_convention", ['ACN', 'FuMa']) +@pytest.mark.parametrize("normalization", ['N3D', 'SN3D']) +def test_spherical_harmonics_real(make_coordinates, implementation, + normalization, channel_convention, + condon_shortley): + n_max = 1 + theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) + phi = np.array([0, np.pi/2, 0], dtype=float) + rad = np.ones(3, dtype=float) + + coords = make_coordinates.create_coordinates( + implementation, rad, theta, phi) + + phase_conv_id = 'Condon-Shortley' if condon_shortley is True else 'None' + + norm_id = normalization + if normalization == 'NM' or normalization == 'N3D': + norm_id = 'n3d' + if normalization == 'SNM' or normalization == 'SN3D': + norm_id = 'sn3d' + + Y = np.genfromtxt(f'./tests/data/Y_real_{phase_conv_id}_' + f'{norm_id}_{channel_convention.lower()}.csv', + dtype=float, + delimiter=',') + + if normalization in ('NM', 'SNM'): + Y *= np.sqrt(4 * np.pi) + + basis = sh.spherical_harmonic_basis_real(n_max, coords, + normalization, + channel_convention, + condon_shortley=condon_shortley) + np.testing.assert_allclose(basis, Y, atol=1e-13) + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonics_invalid_nmax(make_coordinates, implementation): + n_max = 4 + theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) + phi = np.array([0, np.pi/2, 0], dtype=float) + rad = np.ones(3, dtype=float) + + coords = make_coordinates.create_coordinates( + implementation, rad, theta, phi) + + with pytest.raises(ValueError, + match='MaxN normalization is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis(n_max, coords, + normalization='maxN') + with pytest.raises(ValueError, + match='MaxN normalization is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_real(n_max, coords, + normalization='maxN') + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonics_invalid_fuma(make_coordinates, implementation): + n_max = 4 + theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) + phi = np.array([0, np.pi/2, 0], dtype=float) + rad = np.ones(3, dtype=float) + + coords = make_coordinates.create_coordinates( + implementation, rad, theta, phi) + + with pytest.raises(ValueError, + match='FuMa channel convention is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis(n_max, coords, + channel_convention='FuMa') + with pytest.raises(ValueError, + match='FuMa channel convention is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_real(n_max, coords, + channel_convention='FuMa') + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonics_invalid_condon_shortley(make_coordinates, + implementation): + n_max = 4 + theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) + phi = np.array([0, np.pi/2, 0], dtype=float) + rad = np.ones(3, dtype=float) + + coords = make_coordinates.create_coordinates( + implementation, rad, theta, phi) + + with pytest.raises(ValueError, + match="Condon_shortley has to be a bool, or 'auto'."): + sh.spherical_harmonic_basis(n_max, coords, + condon_shortley='xx') + with pytest.raises(ValueError, + match="Condon_shortley has to be a bool, or 'auto'."): + sh.spherical_harmonic_basis_real(n_max, coords, + condon_shortley='xx') + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonic_default_n10(make_coordinates, implementation): + """Test the default parameters of SH basis function generator. This + simultaneously tests if the methods still match the implementation + up to spharpy 0.6.2. + """ + n_max = 10 theta = np.array([np.pi/2, np.pi/2, 0], dtype=float) phi = np.array([0, np.pi/2, 0], dtype=float) @@ -40,13 +166,18 @@ def test_spherical_harmonic_n10(make_coordinates, implementation): delimiter=',', dtype=complex) - basis = sh.spherical_harmonic_basis(Nmax, coords) + basis = sh.spherical_harmonic_basis(n_max, coords) np.testing.assert_allclose(Y, basis, atol=1e-13) @pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) -def test_spherical_harmonics_real(make_coordinates, implementation): +def test_spherical_harmonics_real_n10_default(make_coordinates, + implementation): + """Test the default parameters of SH basis function generator. This + simultaneously tests if the methods still match the implementation + up to spharpy 0.6.2. + """ n_max = 10 theta = np.array([np.pi/2, np.pi/2, 0, np.pi/2], dtype=float) phi = np.array([0, np.pi/2, 0, np.pi/4], dtype=float) @@ -55,16 +186,18 @@ def test_spherical_harmonics_real(make_coordinates, implementation): coords = make_coordinates.create_coordinates( implementation, rad, theta, phi) - reference = np.genfromtxt('./tests/data/sh_basis_real.csv', delimiter=',') + Y = np.genfromtxt('./tests/data/sh_basis_real.csv', + dtype=float, + delimiter=',') basis = sh.spherical_harmonic_basis_real(n_max, coords) - np.testing.assert_allclose(basis, reference, atol=1e-13) + np.testing.assert_allclose(basis, Y, atol=1e-13) @pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) def test_orthogonality(make_coordinates, implementation): """ Check if the orthonormality condition of the spherical harmonics is - fulfilled + fulfilled. """ n_max = 82 theta = np.array([np.pi/2, np.pi/2, 0, np.pi/2], dtype=float) @@ -86,7 +219,7 @@ def test_orthogonality(make_coordinates, implementation): def test_orthogonality_real(make_coordinates, implementation): """ Check if the orthonormality condition of the reavl valued spherical - harmonics is fulfilled + harmonics is fulfilled. """ n_max = 82 theta = np.array([np.pi / 2, np.pi / 2, 0, np.pi / 2], dtype='double') @@ -153,3 +286,47 @@ def test_spherical_harmonic_basis_gradient_real( dtype=complex, delimiter=',') npt.assert_allclose(grad_azi, desire_azi, rtol=1e-10, atol=1e-10) + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonics_gradient_invalid_nmax(make_coordinates, + implementation): + n_max = 15 + theta = np.array([np.pi/2, np.pi/2, 0, np.pi/2, np.pi/4]) + phi = np.array([0, np.pi/2, 0, np.pi/4, np.pi/4]) + + coords = make_coordinates.create_coordinates( + implementation, np.ones_like(theta), theta, phi) + + with pytest.raises(ValueError, + match='MaxN normalization is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_gradient(n_max, coords, + normalization='maxN') + with pytest.raises(ValueError, + match='MaxN normalization is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_gradient_real(n_max, coords, + normalization='maxN') + + +@pytest.mark.parametrize("implementation", ['spharpy', 'pyfar']) +def test_spherical_harmonics_gradient_invalid_fuma(make_coordinates, + implementation): + n_max = 15 + theta = np.array([np.pi/2, np.pi/2, 0, np.pi/2, np.pi/4]) + phi = np.array([0, np.pi/2, 0, np.pi/4, np.pi/4]) + + coords = make_coordinates.create_coordinates( + implementation, np.ones_like(theta), theta, phi) + + with pytest.raises(ValueError, + match='FuMa channel convention is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_gradient(n_max, coords, + channel_convention='FuMa') + with pytest.raises(ValueError, + match='FuMa channel convention is only' + ' supported up to 3rd order.'): + sh.spherical_harmonic_basis_gradient_real(n_max, coords, + channel_convention='FuMa') diff --git a/tests/test_spherical_harmonics_signal.py b/tests/test_spherical_harmonics_signal.py new file mode 100644 index 00000000..269deb23 --- /dev/null +++ b/tests/test_spherical_harmonics_signal.py @@ -0,0 +1,211 @@ +import pytest +from spharpy.classes import SphericalHarmonicSignal +import numpy as np +import re + + +def test_spherical_harmonic_signal_init(): + """Test init SphericalHarmonicsSignal.""" + + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + assert isinstance(signal, SphericalHarmonicSignal) + + +def test_spherical_harmonic_signal_init_condon_shortley(): + """ + Test if Condon-Shortley is set properly in init + SphericalHarmonicsSignal. + """ + + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + condon_shortley=False, + normalization='N3D') + assert not signal.condon_shortley + + signal = SphericalHarmonicSignal(data.astype(np.complex128), + 44100, basis_type='complex', + channel_convention='ACN', + normalization='N3D', + condon_shortley=True, + is_complex=True) + assert signal.condon_shortley + + +def test_spherical_harmonic_signal_wrong_dimensions(): + """Test dimensions of SH coefficient data.""" + + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]) + + # test if dimension of data is < 3 + with pytest.raises(ValueError, + match="Invalid number of dimensions. Data should have " + "at least 3 dimensions."): + SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + condon_shortley=False, + normalization='N3D') + # test if sh channels are valid + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 5, 3) + + with pytest.raises(ValueError, + match=re.escape("Invalid number of SH channels: " + f"{data.shape[-2]}. It must match " + "(n_max + 1)^2.")): + SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + condon_shortley=False, + normalization='N3D') + + +def test_spherical_harmonic_signal_init_multichannel(): + """Test init SphercalHarmonicsSignal.""" + sh_coeffs = np.zeros((2, 4, 16)) + signal = SphericalHarmonicSignal(sh_coeffs, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + assert isinstance(signal, SphericalHarmonicSignal) + + +def test_nmax_getter(): + """Test nmax getter.""" + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + assert signal.n_max == 1 + assert isinstance(signal.n_max, int) + + +def test_init_wrong_basis_type(): + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + with pytest.raises(ValueError, + match="Invalid basis type, only " + "'complex' and 'real' are supported"): + SphericalHarmonicSignal(data, + 44100, basis_type='invalid_basis_type', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + + +def test_basis_type_getter(): + """Test basis_type getter.""" + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + assert signal.basis_type == 'real' + + +def test_init_wrong_normalization(): + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + + with pytest.raises(ValueError, + match="Invalid normalization, has to be 'N3D', 'NM', " + "'maxN', 'SN3D', or 'SNM', but is " + "invalid_normalization"): + SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='invalid_normalization', + condon_shortley=False) + + +def test_spherical_harmonic_signal_normalization_setter(): + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + signal.normalization = 'SN3D' + assert signal.normalization == 'SN3D' + + signal.normalization = 'SNM' + assert signal.normalization == 'SNM' + + signal.normalization = 'maxN' + assert signal.normalization == 'maxN' + + signal.normalization = 'N3D' + assert signal.normalization == 'N3D' + + signal.normalization = 'NM' + assert signal.normalization == 'NM' + + +def test_init_wrong_channel_convention(): + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + + with pytest.raises(ValueError, + match="Invalid channel convention, has to be 'ACN' " + "or 'FuMa', but is invalid_convention"): + SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='invalid_convention', + normalization='N3D', + condon_shortley=False) + + +def test_spherical_harmonic_signal_channel_convention_setter(): + data = np.array([[1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.], + [1., 2., 3.]]).reshape(1, 4, 3) + signal = SphericalHarmonicSignal(data, + 44100, basis_type='real', + channel_convention='ACN', + normalization='N3D', + condon_shortley=False) + signal.channel_convention = 'FuMa' + assert signal.channel_convention == 'FuMa' + + signal.channel_convention = 'ACN' + assert signal.channel_convention == 'ACN'