def test_multiple_getter_with_conversion(): # test N successive coordinate conversions N = 500 # get list of available coordinate systems coords = Coordinates() systems = coords._systems() # get reference points in cartesian coordinate system points = [ 'positive_x', 'positive_y', 'positive_z', 'negative_x', 'negative_y', 'negative_z' ] pts = np.array([systems['cart']['right'][point] for point in points]) # init the system coords.set_cart(pts[:, 0], pts[:, 1], pts[:, 2]) # list of domains domains = list(systems) for ii in range(N): # randomly select a coordinate system domain = domains[np.random.randint(len(domains))] conventions = list(systems[domain]) convention = conventions[np.random.randint(len(conventions))] # convert points to selected system pts = eval(f"coords.get_{domain}('{convention}', convert=True)") # get the reference ref = np.array( [systems[domain][convention][point] for point in points]) # check npt.assert_allclose(pts, ref, atol=1e-15) # print print(f"Tolerance met in iteration {ii}")
def test_coordinates_init_val_and_weights(): # correct number of weights coords = Coordinates([1, 2], 0, 0, weights=[.5, .5]) assert isinstance(coords, Coordinates) # incorrect number of weights with raises(AssertionError): Coordinates([1, 2], 0, 0, weights=.5)
def test_coordinates_init_val_no_convention_no_unit(): # get list of available coordinate systems coords = Coordinates() systems = coords._systems() # test constructor with all systems, units, and convention=None for domain in systems: Coordinates(0, 0, 0, domain)
def test_systems(): # get class instance coords = Coordinates() # test all four possible calls coords.systems() coords.systems(brief=True) coords.systems('all') coords.systems('all', brief=True)
def test_csize(): # 0 points coords = Coordinates() assert coords.csize == 0 # two points coords = Coordinates([1, 0], 1, 1) assert coords.csize == 2 # 6 points in two dimensions coords = Coordinates([[1, 2, 3], [4, 5, 6]], 1, 1) assert coords.csize == 6
def test_cdim(): # empty coords = Coordinates() assert coords.cdim == 0 # 2D points coords = Coordinates([1, 0], 1, 1) assert coords.cdim == 1 # 3D points coords = Coordinates([[1, 2, 3], [4, 5, 6]], 1, 1) assert coords.cdim == 2
def test_cshape(): # empty coords = Coordinates() assert coords.cshape == (0, ) # 2D points coords = Coordinates([1, 0], [1, 1], [0, 1]) assert coords.cshape == (2, ) # 3D points coords = Coordinates([[1, 2, 3], [4, 5, 6]], 1, 1) assert coords.cshape == (2, 3)
def test_coordinates_init_val_no_convention(): # get list of available coordinate systems coords = Coordinates() systems = coords._systems() # test constructor with all systems, units, and convention=None for domain in systems: convention = list(systems[domain])[0] for unit in systems[domain][convention]['units']: Coordinates(0, 0, 0, domain, unit=unit[0][0:3])
def test_coordinates_init_val_and_system(): # get list of available coordinate systems coords = Coordinates() systems = coords._systems() # test constructor with all systems for domain in systems: for convention in systems[domain]: for unit in systems[domain][convention]['units']: Coordinates(0, 0, 0, domain, convention, unit[0][0:3])
def test_getitem(): # test without weights coords = Coordinates([1, 2], 0, 0) new = coords[0] assert isinstance(new, Coordinates) assert (new.get_cart().flatten() == np.array([1, 0, 0])).all() # test with weights coords = Coordinates([1, 2], 0, 0, weights=[.1, .9]) new = coords[0] assert isinstance(new, Coordinates) assert (new.get_cart().flatten() == np.array([1, 0, 0])).all() assert new.weights.flatten() == np.array(.1) # test with 3D array coords = Coordinates([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]], 0, 0) new = coords[0:1] assert isinstance(new, Coordinates) assert new.cshape == (1, 5) # test if sliced object stays untouched coords = Coordinates([0, 1], [0, 1], [0, 1]) new = coords[0] new.set_cart(2, 2, 2) assert coords.cshape == (2, ) npt.assert_allclose(coords.get_cart()[0], np.array([0, 0, 0]))
def test_show(): coords = Coordinates([-1, 0, 1], 0, 0) # show without mask coords.show() # show with mask as list coords.show([1, 0, 1]) # show with mask as ndarray coords.show(np.array([1, 0, 1], dtype=bool)) # test assertion with raises(AssertionError): coords.show(np.array([1, 0], dtype=bool))
def test___eq___differInUnit_notEqual(): coordinates = Coordinates([1, 1], [1, 1], [1, 1], convention='top_colat', domain='sph', unit='rad') actual = Coordinates([1, 1], [1, 1], [1, 1], convention='top_colat', domain='sph', unit='deg') is_equal = coordinates == actual assert not is_equal
def test_setter_and_getter_with(): # get list of available coordinate systems coords = Coordinates() systems = coords._systems() # test points contained in system definitions points = [ 'positive_x', 'positive_y', 'positive_z', 'negative_x', 'negative_y', 'negative_z' ] # test setter and getter with all systems and default unit for domain_in in list(systems): for convention_in in list(systems[domain_in]): for domain_out in list(systems): for convention_out in list(systems[domain_out]): for point in points: # for debugging print(f"{domain_in}({convention_in}) -> " f"{domain_out}({convention_out}): {point}") # in and out points p_in = systems[domain_in][convention_in][point] p_out = systems[domain_out][convention_out][point] # empty object c = Coordinates() # --- set point --- eval(f"c.set_{domain_in}(p_in[0], p_in[1], p_in[2], \ '{convention_in}')") # check point p = c._points npt.assert_allclose(p.flatten(), p_in, atol=1e-15) # --- test without conversion --- p = eval(f"c.get_{domain_out}('{convention_out}')") # check internal and returned point npt.assert_allclose(c._points.flatten(), p_in, atol=1e-15) npt.assert_allclose(p.flatten(), p_out, atol=1e-15) # check if system was converted assert c._system["domain"] == domain_in assert c._system["convention"] == convention_in # --- test with conversion --- p = eval(f"c.get_{domain_out}('{convention_out}', \ convert=True)") # check point npt.assert_allclose(p.flatten(), p_out, atol=1e-15) # check if system was converted assert c._system["domain"] == domain_out assert c._system["convention"] == convention_out
def sph_icosahedron(radius=1.): """ Generate a sampling from the center points of the twenty icosahedron faces. Parameters ---------- radius : number, optional Radius of the sampling grid. The default is 1. Returns ------- sampling : Coordinates Sampling positions as Coordinate object """ 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, 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 = radius * np.ones(20) sampling = Coordinates(phi, theta, rad, domain='sph', convention='top_colat', comment='icosahedral spherical sampling grid') return sampling
def sph_equal_area(n_points, radius=1.): """Sampling based on partitioning into faces with equal area [1]_. Parameters ---------- n_points : int Number of points corresponding to the number of partitions of the sphere. radius : number, optional radius of the sampling grid in meters. The default is 1. Returns ------- sampling : Coordinates Sampling positions as Coordinate object References ---------- .. [1] 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. """ point_set = external.eq_point_set(2, n_points) sampling = Coordinates(point_set[0] * radius, point_set[1] * radius, point_set[2] * radius, domain='cart', convention='right', comment='Equal area partitioning of the sphere.') return sampling
def sphericalvoronoi(): """ SphericalVoronoi object. """ points = np.array( [[0, 0, 1], [0, 0, -1], [1, 0, 0], [0, 1, 0], [0, -1, 0], [-1, 0, 0]]) sampling = Coordinates(points[:, 0], points[:, 1], points[:, 2]) return SphericalVoronoi(sampling)
def test_coordinate_names(): # check if units agree across coordinates that appear more than once # get all coordinate systems c = Coordinates() systems = c._systems() # get unique list of coordinates and their properties coords = {} # loop across domains and conventions for domain in systems: for convention in systems[domain]: # loop across coordinates for cc, coord in enumerate( systems[domain][convention]['coordinates']): # units of the current coordinate cur_units = [ u[cc] for u in systems[domain][convention]['units'] ] # add coordinate to coords if coord not in coords: coords[coord] = {} coords[coord]['domain'] = [domain] coords[coord]['convention'] = [convention] coords[coord]['units'] = [cur_units] else: coords[coord]['domain'].append(domain) coords[coord]['convention'].append(convention) coords[coord]['units'].append(cur_units) # check if units agree across coordinates that appear more than once for coord in coords: # get unique first entry units = coords[coord]['units'].copy() units_ref, idx = np.unique(units[0], True) units_ref = units_ref[idx] for cc in range(1, len(units)): # get nex entry for comparison units_test, idx = np.unique(units[cc], True) units_test = units_test[idx] # compare assert all(units_ref == units_test), \ f"'{coord}' has units {units_ref} in "\ f"{coords[coord]['domain'][0]} "\ f"({coords[coord]['convention'][0]}) but units {units_test} "\ f"in {coords[coord]['domain'][cc]} "\ f"({coords[coord]['convention'][cc]})"
def test_sph_voronoi(): 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)) a2 = 2 * np.arccos(rho / R) theta2 = theta1 + a2 theta3 = np.pi - theta2 theta4 = np.pi - theta1 phi1 = 0 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))) phi = np.tile( np.array([ phi1, phi2, phi3, phi1 + np.pi / 3, phi2 + np.pi / 3, phi3 + np.pi / 3 ]), 2) rad = np.ones(np.size(theta)) s = Coordinates(phi, theta, rad, domain='sph', convention='top_colat') 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], [8.72677996e-01, 3.56822090e-01, 3.33333333e-01], [-8.72677996e-01, -3.56822090e-01, -3.33333333e-01], [-1.27322004e-01, -9.34172359e-01, 3.33333333e-01], [-7.45355992e-01, -5.77350269e-01, 3.33333333e-01], [1.27322004e-01, -9.34172359e-01, -3.33333333e-01], [-3.33333333e-01, -5.77350269e-01, -7.45355992e-01], [-8.72677996e-01, 3.56822090e-01, -3.33333333e-01], [0.00000000e+00, 0.00000000e+00, -1.00000000e+00], [6.66666667e-01, -1.91105568e-16, -7.45355992e-01], [7.45355992e-01, 5.77350269e-01, -3.33333333e-01], [-3.33333333e-01, 5.77350269e-01, -7.45355992e-01], [1.27322004e-01, 9.34172359e-01, -3.33333333e-01], [-6.66666667e-01, 2.46373130e-16, 7.45355992e-01], [0.00000000e+00, 0.00000000e+00, 1.00000000e+00], [3.33333333e-01, 5.77350269e-01, 7.45355992e-01], [-1.27322004e-01, 9.34172359e-01, 3.33333333e-01], [-7.45355992e-01, 5.77350269e-01, 3.33333333e-01]]) sv = spatial.SphericalVoronoi(s) np.testing.assert_allclose(np.sort(np.sum(verts, axis=-1)), np.sort(np.sum(sv.vertices, axis=-1)), atol=1e-6, rtol=1e-6)
def test_coordinates_init_val(): # test input: scalar c1 = 1 # test input: 2 element vectors c2 = [1, 2] # list c3 = np.asarray(c2) # flat np.array c4 = np.atleast_2d(c2) # row vector np.array c5 = np.transpose(c4) # column vector np.array # test input: 3 element vector c6 = [1, 2, 3] # test input: 2D matrix c7 = np.array([[1, 2, 3], [1, 2, 3]]) # test input: 3D matrix c8 = np.array([[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]]) # tests that have to path # input scalar coordinate Coordinates(c1, c1, c1) # input list of coordinates Coordinates(c2, c2, c2) # input scalar and lists Coordinates(c1, c2, c2) # input flat np.arrays Coordinates(c3, c3, c3) # input non flat vectors Coordinates(c3, c4, c5) # input 2D data Coordinates(c1, c1, c7) # input 3D data Coordinates(c1, c1, c8) # tests that have to fail with raises(AssertionError): Coordinates(c2, c2, c6) with raises(AssertionError): Coordinates(c6, c6, c7) with raises(AssertionError): Coordinates(c2, c2, c8)
def test_orientations_from_view_up(): """Create `Orientations` from view and up vectors.""" # test with single view and up vectors view = [1, 0, 0] up = [0, 1, 0] Orientations.from_view_up(view, up) # test with multiple view and up vectors views = [[1, 0, 0], [0, 0, 1]] ups = [[0, 1, 0], [0, 1, 0]] Orientations.from_view_up(views, ups) # provided as ndarrays views = np.atleast_2d(views).astype(np.float64) ups = np.atleast_2d(ups).astype(np.float64) Orientations.from_view_up(views, ups) # provided as Coordinates views = Coordinates(views[:, 0], views[:, 1], views[:, 2]) ups = Coordinates(ups[:, 0], ups[:, 1], ups[:, 2]) Orientations.from_view_up(views, ups) # view and up counts not matching views = [[1, 0, 0], [0, 0, 1]] ups = [[0, 1, 0]] Orientations.from_view_up(views, ups)
def test__systems(): # get all coordinate systems coords = Coordinates() systems = coords._systems() # check object type assert isinstance(systems, dict) # check completeness of systems for domain in systems: for convention in systems[domain]: assert "description_short" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'description_short'" assert "coordinates" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'coordinates'" assert "units" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'units'" assert "description" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'description'" assert "positive_x" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'positive_x'" assert "positive_y" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'positive_y'" assert "negative_x" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'negative_'" assert "negative_y" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'negative_y'" assert "positive_z" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'positive_z'" assert "negative_z" in systems[domain][convention], \ f"{domain} ({convention}) is missing entry 'negative_z'" for coord in systems[domain][convention]['coordinates']: assert coord in systems[domain][convention], \ f"{domain} ({convention}) is missing entry '{coord}'" assert systems[domain][convention][coord][0] in \ ["unbound", "bound", "cyclic"], \ f"{domain} ({convention}), {coord}[0] must be 'unbound', "\ "'bound', or 'cyclic'."
def test_orientations_show(views, ups, positions, orientations): """ Visualize orientations via `Orientations.show()` with and without `positions`. """ # default orientation Orientations().show() # single vectors no position view = [1, 0, 0] up = [0, 1, 0] orientation_single = Orientations.from_view_up(view, up) orientation_single.show() # with position position = Coordinates(0, 1, 0) orientation_single.show(position) # multiple vectors no position orientations.show() # with matching number of positions orientations.show(positions) # select what to show orientations.show(show_views=False) orientations.show(show_ups=False) orientations.show(show_rights=False) orientations.show(show_views=False, show_ups=False) orientations.show(show_views=False, show_rights=False) orientations.show(show_ups=False, show_rights=False) orientations.show(positions=positions, show_views=False, show_ups=False) # with positions provided as Coordinates positions = np.asarray(positions) positions = Coordinates(positions[:, 0], positions[:, 1], positions[:, 2]) orientations.show(positions) # with non-matching positions positions = Coordinates(0, 1, 0) with raises(ValueError): orientations.show(positions)
def sph_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 ------- sampling : Coordinates Sampling positions as Coordinate object """ 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)) a2 = 2 * np.arccos(rho / R) theta2 = theta1 + a2 theta3 = np.pi - theta2 theta4 = np.pi - theta1 phi1 = 0 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))) phi = np.tile( np.array([ phi1, phi2, phi3, phi1 + np.pi / 3, phi2 + np.pi / 3, phi3 + np.pi / 3 ]), 2) rad = radius * np.ones(np.size(theta)) sampling = Coordinates(phi, theta, rad, domain='sph', convention='top_colat', comment='dodecahedral sampling grid') return sampling
def test_weights_from_voronoi(): s = Coordinates([0, 0, 1, -1, 0, 0], [0, 0, 0, 0, -1, 1], [-1, 1, 0, 0, 0, 0], domain='cart', convention='right') # test with normalization weights = spatial.calculate_sph_voronoi_weights(s, normalize=True) desired = np.ones(6) / 6 np.testing.assert_allclose(weights, desired) np.testing.assert_allclose(np.sum(weights), 1.) # test without normalization weights = spatial.calculate_sph_voronoi_weights(s, normalize=False) np.testing.assert_allclose(np.sum(weights), 4 * np.pi)
def test_get_nearest_sph(): # test only 1D case since most of the code from self.get_nearest_k is used az = np.linspace(0, 40, 5) coords = Coordinates(az, 0, 1, 'sph', 'top_elev', 'deg') i, m = coords.get_nearest_sph(25, 0, 1, 5, 'sph', 'top_elev', 'deg') assert (i == np.array([2, 3])).all() assert (m == np.array([[0, 0, 1, 1, 0]], dtype=bool)).all() # test search with empty results i, m = coords.get_nearest_sph(25, 0, 1, 1, 'sph', 'top_elev', 'deg') assert len(i) == 0 assert (m == np.array([[0, 0, 0, 0, 0]], dtype=bool)).all() # test out of range parameters with raises(AssertionError): coords.get_nearest_sph(1, 0, 0, -1) with raises(AssertionError): coords.get_nearest_sph(1, 0, 0, 181)
def cart_equidistant_cube(n_points): """Create a cuboid sampling with equidistant spacings in x, y, and z. The cube will have dimensions 1 x 1 x 1 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). Returns ------- sampling : Coordinates Sampling positions as Coordinate object """ if np.size(n_points) == 1: n_x = n_points n_y = n_points n_z = n_points elif np.size(n_points) == 3: n_x = n_points[0] n_y = n_points[1] n_z = n_points[2] else: raise ValueError("The number of points needs to be either an integer \ or a tuple with 3 elements.") x = np.linspace(-1, 1, n_x) 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) sampling = Coordinates(x_grid.flatten(), y_grid.flatten(), z_grid.flatten(), domain='cart', comment='equidistant cuboid sampling grid') return sampling
def test_get_nearest_cart(): # test only 1D case since most of the code from self.get_nearest_k is used x = np.arange(6) coords = Coordinates(x, 0, 0) i, m = coords.get_nearest_cart(2.5, 0, 0, 1.5) assert (i == np.array([1, 2, 3, 4])).all() assert (m == np.array([[0, 1, 1, 1, 1, 0]], dtype=bool)).all() # test search with empty results i, m = coords.get_nearest_cart(2.5, 0, 0, .1) assert len(i) == 0 assert (m == np.array([[0, 0, 0, 0, 0, 0]], dtype=bool)).all() # test out of range parameters with raises(AssertionError): coords.get_nearest_cart(1, 0, 0, -1)
def test_orientations_from_view_up_show_coordinate_system_change( views, ups, positions): """ Create `Orientations` from view and up vectors in the spherical domain as well as in the carteesian domain, and visualize both to compare them manually by eye. """ # Carteesian: Visualize to manually validate orientations views = np.asarray(views) ups = np.asarray(ups) views = Coordinates(views[:, 0], views[:, 1], views[:, 2]) ups = Coordinates(ups[:, 0], ups[:, 1], ups[:, 2]) positions = np.asarray(positions) positions = Coordinates(positions[:, 0], positions[:, 1], positions[:, 2]) orient_from_cart = Orientations.from_view_up(views, ups) orient_from_cart.show(positions) # Convert to spherical: And again visualize to manually validate views.get_sph(convert=True) ups.get_sph(convert=True) positions.get_sph(convert=True) orient_from_sph = Orientations.from_view_up(views, ups) orient_from_sph.show(positions) # Check if coordinate system has not been changed by orientations assert views._system['domain'] == 'sph', ( "Coordinate system has been changed by Orientations.") assert ups._system['domain'] == 'sph', ( "Coordinate system has been changed by Orientations.") assert positions._system['domain'] == 'sph', ( "Coordinate system has been changed by Orientations.show().")
def sph_fliege(n_points=None, sh_order=None, radius=1.): """ Return Fliege-Maier spherical sampling grid [1]_. See :ref:`Input Values<values>` for a list of possible values for `n_points`and `sh_order` or call `sph_fliege()`. Parameters ---------- n_points : int, optional number of sampling points in the grid. Related to the spherical harmonic order by n_points = (sh_order + 1)**2. Either n_points or sh_order must be provided. The default is None. sh_order : int, optional maximum applicable spherical harmonic order. Related to the number of points by sh_order = np.sqrt(n_points) - 1. Either n_points or sh_order must be provided. The default is None. radius : number, optional radius of the sampling grid in meters. The default is 1. Returns ------- sampling : Coordinates Sampling positions as Coordinate object Notes ----- This implementation uses pre-calculated points from the SOFiA toolbox [2]_. .. _values: Input Values ------------ +------------+------------+ | `n_points` | `sh_order` | +============+============+ | 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 ---------- .. [1] 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. .. [2] https://audiogroup.web.th-koeln.de/SOFiA_wiki/DOWNLOAD.html """ # possible values for n_points and sh_order 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 sh_order is None: for o, d in zip(orders, points): print(f"SH order {o}, number of points {d}") return None # check input if n_points is not None and sh_order is not None: raise ValueError("Either n_points or sh_order must be None.") if sh_order is not None: # check if the order is available if sh_order not in orders: str_orders = [f"{o}" for o in orders] raise ValueError("Invalid spherical harmonic order 'sh_order'. \ Valid orders are: {}.".format( ', '.join(str_orders))) # assign n_points n_points = int(points[orders == sh_order]) 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 sh_order sh_order = int(orders[points == n_points]) # get the sampling points fliege = sio.loadmat(os.path.join(os.path.dirname(__file__), "external", "samplings_fliege.mat"), variable_names=f"Fliege_{int(n_points)}") fliege = fliege[f"Fliege_{int(n_points)}"] # generate Coordinates object sampling = Coordinates(fliege[:, 0], fliege[:, 1], radius, domain='sph', convention='top_colat', unit='rad', sh_order=sh_order, weights=fliege[:, 2], comment='spherical Fliege sampling grid') # switch and invert coordinates in Cartesian representation to be # consistent with [1] xyz = sampling.get_cart(convention='right') sampling.set_cart(xyz[:, 1], xyz[:, 0], -xyz[:, 2]) return sampling
def sph_lebedev(n_points=None, sh_order=None, radius=1.): """ Return Lebedev spherical sampling grid [1]_. For a list of available values for `n_points`and `sh_order` call `sph_lebedev()`. Parameters ---------- n_points : int, optional number of sampling points in the grid. Related to the spherical harmonic order by n_points = (sh_order + 1)**2. Either n_points or sh_order must be provided. The default is None. sh_order : int, optional maximum applicable spherical harmonic order. Related to the number of points by sh_order = np.sqrt(n_points) - 1. Either n_points or sh_order must be provided. The default is None. radius : number, optional radius of the sampling grid in meters. The default is 1. Returns ------- sampling : Coordinates Sampling positions as Coordinate object Notes ----- This implementation is based on Matlab Code written by Rob Parrish [2]_. References ---------- .. [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. .. [2] https://de.mathworks.com/matlabcentral/fileexchange/27097-\ getlebedevsphere """ # 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 sh_order is None: print('Possible input values:') for o, d in zip(orders, degrees): print(f"SH order {o}, number of points {d}") return None # check input if n_points is not None and sh_order is not None: raise ValueError("Either n_points or sh_order must be None.") # check if the order is available if sh_order is not None: if sh_order not in orders: str_orders = [f"{o}" for o in orders] raise ValueError("Invalid spherical harmonic order 'sh_order'. \ Valid orders are: {}.".format( ', '.join(str_orders))) n_points = int(degrees[orders == sh_order]) # 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 sh_order sh_order = int(orders[degrees == n_points]) # get the samlpling leb = external.lebedev_sphere(n_points) # normalize the weights weights = leb["w"] / (4 * np.pi) # generate Coordinates object sampling = Coordinates(leb["x"] * radius, leb["y"] * radius, leb["z"] * radius, sh_order=sh_order, weights=weights, comment='spherical Lebedev sampling grid') return sampling