stellar-spice 0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stellar-spice might be problematic. Click here for more details.

spice/__init__.py ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ from .utils import polygon_area
2
+ from .sutherland_hodgman import clip
@@ -0,0 +1,142 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ from jax.typing import ArrayLike
4
+ from spice.geometry.utils import inside, last_non_nan, append_to_last_nan, repeat_last, sort_xy
5
+
6
+
7
+ @jax.jit
8
+ def x_y_first_line_vertical(p1, p2, q1, q2):
9
+ x = p1[0]
10
+ m2 = (q2[1]-q1[1])/(q2[0]-q1[0])
11
+ b2 = q1[1]-m2*q1[0]
12
+ return x, m2*x+b2
13
+
14
+ @jax.jit
15
+ def x_y_second_line_vertical(p1, p2, q1, q2):
16
+ x = q1[0]
17
+ m1 = (p2[1]-p1[1])/(p2[0]-p1[0])
18
+ b1 = p1[1]-m1*p1[0]
19
+ return x, m1*x+b1
20
+
21
+ @jax.jit
22
+ def x_y_no_vertical(p1, p2, q1, q2):
23
+ m1 = (p2[1]-p1[1])/(p2[0]-p1[0])
24
+ b1 = p1[1]-m1*p1[0]
25
+
26
+ m2 = (q2[1]-q1[1])/(q2[0]-q1[0])
27
+ b2 = q1[1]-m2*q1[0]
28
+
29
+ x = (b2-b1)/(m1-m2)
30
+ return x, m1*x+b1
31
+
32
+
33
+ @jax.jit
34
+ def compute_intersection(p1: ArrayLike, p2: ArrayLike, q1: ArrayLike, q2: ArrayLike) -> ArrayLike:
35
+ """Calculate the intersection of two lines
36
+
37
+ Args:
38
+ p1 (ArrayLike): point 1 of line 1
39
+ p2 (ArrayLike): point 2 of line 1
40
+ q1 (ArrayLike): point 1 of line 2
41
+ q2 (ArrayLike): point 2 of line 2
42
+
43
+ Returns:
44
+ ArrayLike: coordinates of the intersection of two lines
45
+ """
46
+ x, y = jax.lax.cond(jnp.isclose(p1[0]-p2[0], 0),
47
+ x_y_first_line_vertical,
48
+ lambda a, b, c, d: jax.lax.cond(
49
+ jnp.isclose(c[0]-d[0], 0),
50
+ x_y_second_line_vertical,
51
+ x_y_no_vertical,
52
+ a, b, c, d),
53
+ p1, p2, q1, q2)
54
+
55
+ return jnp.array([[x, y]])
56
+
57
+ def s_only_edge_start_inside(final_polygon, c_edge_start, c_edge_end, s_edge_start, s_edge_end):
58
+ intersection = compute_intersection(s_edge_start,
59
+ s_edge_end,
60
+ c_edge_start,
61
+ c_edge_end)
62
+ intersection = jax.lax.cond(jnp.any(jnp.isnan(intersection)),
63
+ lambda: jnp.nan*intersection,
64
+ lambda: intersection)
65
+ return append_to_last_nan(final_polygon, intersection)
66
+
67
+ def s_only_edge_end_inside(final_polygon, c_edge_start, c_edge_end, s_edge_start, s_edge_end):
68
+ intersection = compute_intersection(s_edge_start,
69
+ s_edge_end,
70
+ c_edge_start,
71
+ c_edge_end)
72
+ intersection = jax.lax.cond(jnp.any(jnp.isnan(intersection)),
73
+ lambda: jnp.nan*intersection,
74
+ lambda: intersection)
75
+ polygon_with_intersection = append_to_last_nan(final_polygon, intersection)
76
+ return append_to_last_nan(polygon_with_intersection, s_edge_end)
77
+
78
+ def s_both_start_and_end_inside(final_polygon, c_edge_start, c_edge_end, s_edge_start, s_edge_end):
79
+ return append_to_last_nan(final_polygon, s_edge_end)
80
+
81
+
82
+ @jax.jit
83
+ def clip(subject_polygon: ArrayLike, clipping_polygon: ArrayLike) -> ArrayLike:
84
+ """Calculate the intersection of the triangles subject_polygon and clipping_polygon
85
+
86
+ Args:
87
+ subject_polygon (ArrayLike): x and y coordinates (3, 2)
88
+ clipping_polygon (ArrayLike): x and y coordinates (3, 2)
89
+
90
+ Returns:
91
+ ArrayLike: intersection between two triangles (6, 2)
92
+ """
93
+ subject_polygon = sort_xy(subject_polygon)
94
+ clipping_polygon = sort_xy(clipping_polygon)
95
+ final_polygon = repeat_last(jnp.concatenate([jnp.copy(subject_polygon), jnp.nan*jnp.ones((6-subject_polygon.shape[0], 2))], axis=0))
96
+
97
+ def outer_iteration(i, final_polygon):
98
+ next_polygon = jnp.copy(final_polygon)
99
+
100
+ final_polygon = jnp.nan*jnp.ones((6, 2))
101
+
102
+ #c_edge_start = clipping_polygon[((i-1)%(clipping_polygon.shape[0])).astype(int)]
103
+ c_edge_start = jax.lax.cond(i==0,
104
+ lambda: last_non_nan(clipping_polygon),
105
+ lambda: clipping_polygon[i-1])
106
+ c_edge_end = clipping_polygon[i]
107
+
108
+ def inner_iteration(j, final_polygon):
109
+ s_edge_start = jax.lax.cond(j==0,
110
+ lambda: last_non_nan(next_polygon),
111
+ lambda: next_polygon[j-1])
112
+ #s_edge_start = next_polygon[((j-1)%(next_polygon.shape[0])).astype(int)]
113
+ s_edge_end = next_polygon[j]
114
+
115
+ return jax.lax.cond(
116
+ jnp.any(inside(c_edge_start, c_edge_end, s_edge_end)),
117
+ lambda: jax.lax.cond(
118
+ jnp.any(inside(c_edge_start, c_edge_end, s_edge_start)),
119
+ lambda: s_both_start_and_end_inside(
120
+ final_polygon,
121
+ c_edge_start, c_edge_end,
122
+ s_edge_start, s_edge_end
123
+ ),
124
+ lambda: s_only_edge_end_inside(
125
+ final_polygon,
126
+ c_edge_start, c_edge_end,
127
+ s_edge_start, s_edge_end
128
+ ),
129
+ ),
130
+ lambda: jax.lax.cond(
131
+ jnp.any(inside(c_edge_start, c_edge_end, s_edge_start)),
132
+ lambda: s_only_edge_start_inside(
133
+ final_polygon,
134
+ c_edge_start, c_edge_end,
135
+ s_edge_start, s_edge_end
136
+ ),
137
+ lambda: final_polygon
138
+ )
139
+ )
140
+
141
+ return jax.lax.fori_loop(0, next_polygon.shape[0], inner_iteration, final_polygon)
142
+ return repeat_last(jax.lax.fori_loop(0, clipping_polygon.shape[0], outer_iteration, final_polygon))
@@ -0,0 +1,167 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ from jax.typing import ArrayLike
4
+
5
+
6
+ def sort_xy(points: ArrayLike) -> ArrayLike:
7
+ """Sort points clockwise
8
+
9
+ Args:
10
+ points (ArrayLike): x, y coordinates (n, 2)
11
+
12
+ Returns:
13
+ ArrayLike: points with x, y coordinates sorted clockwise
14
+ """
15
+ x, y = points[:, 0], points[:, 1]
16
+ x0 = jnp.mean(x)
17
+ y0 = jnp.mean(y)
18
+
19
+ r = jnp.sqrt((x-x0)**2 + (y-y0)**2)
20
+
21
+ angles = jnp.where((y-y0) > 0, jnp.arccos((x-x0)/r), 2*jnp.pi-jnp.arccos((x-x0)/r))
22
+
23
+ mask = jnp.argsort(angles)
24
+
25
+ return points[mask, :]
26
+
27
+
28
+
29
+ def inside(p1: ArrayLike, p2: ArrayLike, q: ArrayLike) -> bool:
30
+ """Check if the point is inside the polygon edge
31
+
32
+ Args:
33
+ p1 (ArrayLike): Polygon edge point 1
34
+ p2 (ArrayLike): Polygon edge point 2
35
+ q (ArrayLike): Point to be checked
36
+
37
+ Returns:
38
+ bool: Point is inside polygon
39
+ """
40
+ return (p2[0]-p1[0])*(q[1]-p1[1])-(p2[1]-p1[1])*(q[0]-p1[0])>=0
41
+
42
+
43
+ def last_non_nan_arg(arr: ArrayLike) -> ArrayLike:
44
+ """Return the last non-nan index of the array
45
+
46
+ Args:
47
+ arr (ArrayLike): Array
48
+
49
+ Returns:
50
+ ArrayLike: Last non-nan index of the array
51
+ """
52
+ return jnp.max(jnp.argwhere(jnp.all(~jnp.isnan(arr), axis=1), size=arr.shape[0], fill_value=0)).astype(int)
53
+
54
+
55
+ def last_non_nan(arr: ArrayLike, axis=1) -> ArrayLike:
56
+ """Return the last non-nan value of the array
57
+
58
+ Args:
59
+ arr (ArrayLike): Array
60
+
61
+ Returns:
62
+ ArrayLike: Last non-nan row of the array
63
+ """
64
+ return arr[jnp.max(jnp.argwhere(jnp.all(~jnp.isnan(arr), axis=axis), size=arr.shape[0], fill_value=0))]
65
+
66
+
67
+ def append_to_last_nan(arr: ArrayLike, to_add: ArrayLike) -> ArrayLike:
68
+ """Append the row to the first nan row
69
+
70
+ Args:
71
+ arr (ArrayLike): Array to add the row to
72
+ to_add (ArrayLike): The row to be added
73
+
74
+ Returns:
75
+ ArrayLike: Array with an appended row
76
+ """
77
+ next_ind = jnp.min(jnp.argwhere(jnp.all(jnp.isnan(arr), axis=1), size=arr.shape[0], fill_value=arr.shape[0]-1))
78
+ return arr.at[next_ind].set(to_add.flatten())
79
+
80
+
81
+ def append_value_to_last_nan(arr: ArrayLike, to_add: ArrayLike) -> ArrayLike:
82
+ """Append a single value to the first nan in array
83
+
84
+ Args:
85
+ arr (ArrayLike): Array to add the value to
86
+ to_add (ArrayLike): The value to be appended
87
+
88
+ Returns:
89
+ ArrayLike: Array with an appended value
90
+ """
91
+ next_ind = jnp.min(jnp.argwhere(jnp.isnan(arr), size=arr.shape[0], fill_value=arr.shape[0]-1))
92
+ return arr.at[next_ind].set(to_add)
93
+
94
+
95
+ def repeat_last(arr: ArrayLike) -> ArrayLike:
96
+ """Repeat the last non-nan row to fill the whole array
97
+
98
+ Args:
99
+ arr (ArrayLike):
100
+
101
+ Returns:
102
+ ArrayLike: Array with nan values filled with last non-nan values
103
+ """
104
+ return jax.lax.cond(jnp.any(jnp.all(jnp.isnan(arr), axis=1)),
105
+ lambda: jax.lax.fori_loop(0, jnp.sum(jnp.all(jnp.isnan(arr), axis=1)),
106
+ lambda i, _arr: append_to_last_nan(_arr, last_non_nan(_arr)),
107
+ arr),
108
+ lambda: arr)
109
+
110
+
111
+ def wrap(arr: ArrayLike) -> ArrayLike:
112
+ """Copy the first row and concatenate to the first array
113
+
114
+ Args:
115
+ arr (ArrayLike):
116
+
117
+ Returns:
118
+ ArrayLike: Array with the copy of the first row appended
119
+ """
120
+ return jnp.concatenate([arr, arr[0].reshape((-1, arr.shape[1]))])
121
+
122
+
123
+ def polygon_area(x: ArrayLike, y: ArrayLike) -> ArrayLike:
124
+ """Calculate a polygon's surface area from its x and y coordinates
125
+
126
+ Args:
127
+ x (ArrayLike): x coordinates of the polygon
128
+ y (ArrayLike): y coordinates of the polygon
129
+
130
+ Returns:
131
+ ArrayLike: polygon's surface area
132
+ """
133
+ return 0.5*jnp.abs(jnp.dot(x,jnp.roll(y,1))-jnp.dot(y,jnp.roll(x,1)))
134
+
135
+
136
+ polygon_areas = jax.jit(jax.vmap(polygon_area, in_axes=(0, 0)))
137
+
138
+ def cast_indexes(cast_vertices: ArrayLike) -> ArrayLike:
139
+ """When casting 3D vectors to 2D, get the indices of non-reduced dimensions
140
+
141
+ Args:
142
+ cast_vertices (ArrayLike): vertices after 3D to 2D casting
143
+
144
+ Returns:
145
+ ArrayLike: non-reduced (non-zero) dimensions
146
+ """
147
+ zeroth_ind = jnp.argmax(jnp.all(jnp.isclose(cast_vertices, 0), axis=1))
148
+
149
+ return jax.lax.cond(zeroth_ind == 0,
150
+ lambda: jnp.array([1, 2]),
151
+ lambda: jax.lax.cond(zeroth_ind == 1,
152
+ lambda: jnp.array([0, 2]),
153
+ lambda: jnp.array([0, 1])))
154
+
155
+
156
+ def get_cast_areas(face_vertices: ArrayLike) -> ArrayLike:
157
+ """Get visible, 2D-casted polygon areas
158
+
159
+ Args:
160
+ face_vertices (ArrayLike): polygons' vertices after 3D to 2D casting
161
+
162
+ Returns:
163
+ ArrayLike: areas of 2D-casted polygons
164
+ """
165
+ cast_ind = cast_indexes(face_vertices)
166
+ return polygon_areas(face_vertices[:, :, cast_ind[0]],
167
+ face_vertices[:, :, cast_ind[1]])
@@ -0,0 +1,5 @@
1
+ from .mesh_generation import icosphere
2
+ from .mesh_model import IcosphereModel, MeshModel
3
+ from .spots import add_spot, add_spots
4
+ from .binary import Binary
5
+ from .orbit_utils import get_orbit_jax
spice/models/binary.py ADDED
@@ -0,0 +1,123 @@
1
+ import jax.numpy as jnp
2
+ from functools import partial
3
+ import jax
4
+ from jax.typing import ArrayLike
5
+ from typing import NamedTuple, Tuple
6
+ from .mesh_model import MeshModel
7
+ from .mesh_transform import transform, evaluate_body_orbit
8
+ import astropy.units as u
9
+ from .orbit_utils import get_orbit_jax
10
+ from .mesh_view import resolve_occlusion, Grid
11
+
12
+
13
+ YEAR_TO_SECONDS = (u.year).to(u.s)
14
+
15
+
16
+ class Binary(NamedTuple):
17
+ body1: MeshModel
18
+ body2: MeshModel
19
+
20
+ # Orbital elements
21
+ P: float
22
+ ecc: float
23
+ T: float
24
+ i: float
25
+ omega: float
26
+ Omega: float
27
+
28
+ evaluated_times: ArrayLike
29
+
30
+ body1_centers: ArrayLike
31
+ body2_centers: ArrayLike
32
+ body1_velocities: ArrayLike
33
+ body2_velocities: ArrayLike
34
+
35
+ @classmethod
36
+ def from_bodies(cls, body1: MeshModel, body2: MeshModel) -> "Binary":
37
+ """Construct a Binary object from two mesh models.
38
+
39
+ Args:
40
+ body1 (MeshModel):
41
+ body2 (MeshModel):
42
+
43
+ Returns:
44
+ Binary: a binary consisting of body1 and body2
45
+ """
46
+ return cls(body1, body2, 1., 0., 0., 0., 0., 0., 0., jnp.zeros_like(body1.centers), jnp.zeros_like(body2.centers), jnp.zeros_like(body1.velocities), jnp.zeros_like(body2.velocities))
47
+
48
+
49
+ @partial(jax.jit, static_argnums=(7,))
50
+ def add_orbit(binary: Binary, P: float, ecc: float,
51
+ T: float, i: float, omega: float, Omega: float, orbit_resolution_points: int) -> Binary:
52
+ """Add orbit information to the binary object
53
+
54
+ Args:
55
+ binary (Binary):
56
+ P (float): orbit period [years]
57
+ ecc (float): orbit eccentrity
58
+ T (float): [defined time units]
59
+ i (float): inclination [rad]
60
+ omega (float): []
61
+ Omega (float): []
62
+ orbit_resolution_points (int): number of times to resolve the orbit at
63
+
64
+ Returns:
65
+ Binary: object with calculated orbit property values
66
+ """
67
+
68
+ orbit_resolution_times = jnp.linspace(0, P, orbit_resolution_points)
69
+ orbit = get_orbit_jax(orbit_resolution_times, binary.body1.mass, binary.body2.mass, P,
70
+ ecc, T, i, omega, Omega)
71
+ return binary._replace(P=P, ecc=ecc, T=T, i=i, omega=omega, Omega=Omega,
72
+ evaluated_times=orbit_resolution_times,
73
+ body1_centers=orbit[2, :, :], body2_centers=orbit[4, :, :],
74
+ body1_velocities=orbit[3, :, :], body2_velocities=orbit[5, :, :])
75
+
76
+
77
+ @partial(jax.jit, static_argnums=(2,))
78
+ def _evaluate_orbit(binary: Binary, time: ArrayLike, grid: Grid) -> Tuple[MeshModel, MeshModel]:
79
+ interpolate_orbit = jax.jit(jax.vmap(lambda x: jnp.interp(time, binary.evaluated_times, x, period=binary.P), in_axes=(0,)))
80
+ body1_center = interpolate_orbit(binary.body1_centers)
81
+ body2_center = interpolate_orbit(binary.body2_centers)
82
+ body1_velocity = interpolate_orbit(binary.body1_velocities)
83
+ body2_velocity = interpolate_orbit(binary.body2_velocities)
84
+
85
+ body1 = evaluate_body_orbit(transform(binary.body1, binary.body1.center+body1_center), body1_velocity)
86
+ body2 = evaluate_body_orbit(transform(binary.body2, binary.body2.center+body2_center), body2_velocity)
87
+
88
+ return jax.lax.cond(jnp.mean(body1.los_z)>jnp.mean(body2.los_z),
89
+ lambda: (body1, resolve_occlusion(body2, body1, grid)),
90
+ lambda: (resolve_occlusion(body1, body2, grid), body2))
91
+
92
+
93
+ v_evaluate_orbit = jax.jit(jax.vmap(_evaluate_orbit, in_axes=(None, 0, None)), static_argnums=(2,))
94
+
95
+
96
+ def evaluate_orbit(binary: Binary, time: ArrayLike, n_cells: int = 20) -> Tuple[MeshModel, MeshModel]:
97
+ """_summary_
98
+
99
+ Args:
100
+ binary (Binary): a binary object
101
+ time (ArrayLike): time to resolve the orbit at
102
+ n_cells (int, optional): number of grid cells to divide the calculation of occlusion to. Defaults to 20.
103
+
104
+ Returns:
105
+ Tuple[MeshModel, MeshModel]: (body1, body2)
106
+ """
107
+ grid = Grid.construct(binary.body1, binary.body2, n_cells)
108
+ return _evaluate_orbit(binary, time, grid)
109
+
110
+
111
+ def evaluate_orbit_at_times(binary: Binary, times: ArrayLike, n_cells: int = 20) -> Tuple[MeshModel, MeshModel]:
112
+ """_summary_
113
+
114
+ Args:
115
+ binary (Binary): a binary object
116
+ times (ArrayLike): times to resolve the orbit at
117
+ n_cells (int, optional): number of grid cells to divide the calculation of occlusion to. Defaults to 20.
118
+
119
+ Returns:
120
+ Tuple[MeshModel, MeshModel]: (body1, body2)
121
+ """
122
+ grid = Grid.construct(binary.body1, binary.body2, n_cells)
123
+ return v_evaluate_orbit(binary, times, grid)
@@ -0,0 +1,214 @@
1
+ from typing import Tuple
2
+ from jax.typing import ArrayLike
3
+ import jax.numpy as jnp
4
+ import jax
5
+
6
+ # https://sinestesia.co/blog/tutorials/python-icospheres/
7
+
8
+ PHI: float = (1 + jnp.sqrt(5)) / 2
9
+
10
+ def vertex(x: float, y: float, z: float) -> float:
11
+ """ Return vertex coordinates fixed to the unit sphere """
12
+ length = jnp.sqrt(jnp.power(x, 2) + jnp.power(y, 2) + jnp.power(z, 2))
13
+ coords = jnp.array([x, y, z])
14
+ return (coords)/length
15
+
16
+ # Number of vertices and faces, solved in wolfram
17
+
18
+ # 12 + 3*(faces-1) =
19
+ # faces: 4*(faces-1)
20
+
21
+ # faces: f(n)=5*4^(n+1)
22
+ # vertices: v(n) = 4*(5*4^n-2)
23
+
24
+ # 0: f(0) = 4*5 = 20
25
+ # 0: v(0) = 4*(5-2) = 12
26
+
27
+ # 1: f(1) = 5*4^2 = 80
28
+ # 1: v(1) = 4*(20-2) = 72
29
+
30
+ def init_vertices_faces(subdiv: int) -> Tuple[ArrayLike, ArrayLike, ArrayLike]:
31
+
32
+ init_verts = jnp.array([
33
+ vertex(-1, PHI, 0),
34
+ vertex( 1, PHI, 0),
35
+ vertex(-1, -PHI, 0),
36
+ vertex( 1, -PHI, 0),
37
+
38
+ vertex(0, -1, PHI),
39
+ vertex(0, 1, PHI),
40
+ vertex(0, -1, -PHI),
41
+ vertex(0, 1, -PHI),
42
+
43
+ vertex( PHI, 0, -1),
44
+ vertex( PHI, 0, 1),
45
+ vertex(-PHI, 0, -1),
46
+ vertex(-PHI, 0, 1),
47
+ ])
48
+
49
+ init_faces = jnp.array([
50
+ # 5 faces around point 0
51
+ [0, 11, 5],
52
+ [0, 5, 1],
53
+ [0, 1, 7],
54
+ [0, 7, 10],
55
+ [0, 10, 11],
56
+
57
+ # Adjacent faces
58
+ [1, 5, 9],
59
+ [5, 11, 4],
60
+ [11, 10, 2],
61
+ [10, 7, 6],
62
+ [7, 1, 8],
63
+
64
+ # 5 faces around 3
65
+ [3, 9, 4],
66
+ [3, 4, 2],
67
+ [3, 2, 6],
68
+ [3, 6, 8],
69
+ [3, 8, 9],
70
+
71
+ # Adjacent faces
72
+ [4, 9, 5],
73
+ [2, 4, 11],
74
+ [6, 2, 10],
75
+ [8, 6, 7],
76
+ [9, 8, 1],
77
+ ])
78
+
79
+ verts_total_size = (4*(5*jnp.power(4, subdiv)-2)).astype(int)
80
+ verts = jnp.zeros((verts_total_size, 3))
81
+ verts = verts.at[:12].set(init_verts)
82
+ verts_mask = jnp.concatenate([jnp.ones(12,), jnp.zeros(verts_total_size-12,)]).astype(jnp.int32)
83
+
84
+ faces_total_size = int(5*4**(subdiv+1))
85
+ faces = -1.*jnp.ones((faces_total_size, 3))
86
+ faces = faces.at[::int(faces_total_size/20)].set(init_faces)
87
+
88
+ return verts, verts_mask, faces
89
+
90
+ @jax.jit
91
+ def middle_point(point_1: int,
92
+ point_2: int,
93
+ verts: ArrayLike,
94
+ verts_mask: ArrayLike,
95
+ middle_point_cache: ArrayLike) -> Tuple[float,
96
+ ArrayLike,
97
+ ArrayLike,
98
+ ArrayLike]:
99
+ """ Find a middle point and project to the unit sphere """
100
+ # We check if we have already cut this edge first
101
+ # to avoid duplicated verts
102
+ smaller_index, greater_index = jax.lax.cond(point_1<point_2,
103
+ lambda: (point_1.astype(int), point_2.astype(int)),
104
+ lambda: (point_2.astype(int), point_1.astype(int)))
105
+
106
+ def expand_verts(verts, verts_mask, point_1, point_2, middle_point_cache):
107
+ # If it's not in cache, then we can cut it
108
+ loc_verts = jnp.array([verts[point_1.astype(int)],
109
+ verts[point_2.astype(int)]]).T
110
+ middle = jnp.sum(loc_verts, axis=1)/2
111
+
112
+ index = jnp.sum(verts_mask)
113
+ verts = verts.at[index].set(vertex(*middle))
114
+ verts_mask = verts_mask.at[index].set(1)
115
+
116
+ smaller_index, greater_index = jax.lax.cond(point_1<point_2,
117
+ lambda: (point_1.astype(int), point_2.astype(int)),
118
+ lambda: (point_2.astype(int), point_1.astype(int)))
119
+
120
+ middle_point_cache = middle_point_cache.at[smaller_index, greater_index].set(index)
121
+
122
+ return index, verts, verts_mask, middle_point_cache
123
+
124
+ maybe_index = middle_point_cache[smaller_index, greater_index]
125
+ return jax.lax.cond(maybe_index>0,
126
+ lambda: (maybe_index, verts, verts_mask, middle_point_cache),
127
+ lambda: expand_verts(verts, verts_mask, point_1, point_2, middle_point_cache))
128
+
129
+ @jax.jit
130
+ def subdivide_trie(carry: Tuple[ArrayLike, ArrayLike, ArrayLike], tri: ArrayLike) -> Tuple[ArrayLike,
131
+ ArrayLike,
132
+ ArrayLike,
133
+ ArrayLike]:
134
+ verts, verts_mask, middle_point_cache = carry
135
+
136
+ def _subdivide(verts, verts_mask, middle_point_cache, tri):
137
+ v1, verts, verts_mask, middle_point_cache = middle_point(tri[0],
138
+ tri[1],
139
+ verts,
140
+ verts_mask,
141
+ middle_point_cache)
142
+ v2, verts, verts_mask, middle_point_cache = middle_point(tri[1],
143
+ tri[2],
144
+ verts,
145
+ verts_mask,
146
+ middle_point_cache)
147
+ v3, verts, verts_mask, middle_point_cache = middle_point(tri[2],
148
+ tri[0],
149
+ verts,
150
+ verts_mask,
151
+ middle_point_cache)
152
+
153
+ return (verts, verts_mask, middle_point_cache), jnp.array([
154
+ [tri[0], v1, v3],
155
+ [tri[1], v2, v1],
156
+ [tri[2], v3, v2],
157
+ [v1, v2, v3]])
158
+
159
+ return jax.lax.cond(jnp.all(tri==-1),
160
+ lambda: ((verts, verts_mask, middle_point_cache), -1.*jnp.ones((4, 3))),
161
+ lambda: _subdivide(verts, verts_mask, middle_point_cache, tri[0]))
162
+
163
+
164
+ def fill_placeholders(x: jnp.array, chunk_size):
165
+ new_x = -1.*jnp.ones((chunk_size, 3))
166
+ return new_x.at[0].set(x)
167
+ relax = jax.vmap(lambda x, n: fill_placeholders(x, n), in_axes=(0, None))
168
+
169
+
170
+ @jax.jit
171
+ def subdivide(faces: ArrayLike, verts: ArrayLike, verts_mask: ArrayLike) -> Tuple[ArrayLike,
172
+ ArrayLike,
173
+ ArrayLike]:
174
+ verts_total_size = verts.shape[0]
175
+ keys_cache = (-1*jnp.ones((verts_total_size, verts_total_size))).astype(int)
176
+ (verts, verts_mask, _), new_faces = jax.lax.scan(subdivide_trie,
177
+ (verts, verts_mask, keys_cache),
178
+ faces.reshape((-1, 4, 3)))
179
+ return new_faces.reshape((-1, 3)), verts, verts_mask
180
+
181
+
182
+ relax_all = lambda chunks, n: jax.vmap(lambda x: relax(x[:4], int(n/4)), in_axes=(0,))(chunks.reshape((-1, n, 3))).reshape((-1, 3))
183
+
184
+ @jax.jit
185
+ def face_center(verts: ArrayLike, face: ArrayLike) -> Tuple[ArrayLike, float]:
186
+ a, b, c = verts[face[0]], verts[face[1]], verts[face[2]]
187
+ ab = b-a
188
+ ac = c-a
189
+ A = jnp.linalg.norm(jnp.cross(ab, ac))/2
190
+ return A, (a+b+c)/3
191
+
192
+ def _icosphere(subdiv: int) -> Tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike]:
193
+ verts, verts_mask, faces = init_vertices_faces(subdiv)
194
+
195
+ for s in range(subdiv):
196
+ faces, verts, verts_mask = subdivide(faces, verts, verts_mask)
197
+ current_s = int(faces.shape[0]/(5*4**(s+1)))
198
+ faces = relax_all(faces, current_s)
199
+
200
+ areas, centers = jax.jit(jax.vmap(face_center, in_axes=(None, 0)))(verts, faces.astype(jnp.int32))
201
+ return verts, faces, areas, centers
202
+
203
+ def icosphere(points: int) -> Tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike]:
204
+ """Create an icosphere with at least that n points.
205
+
206
+ Args:
207
+ points (int): Minimal number of vertices in the icosphere
208
+
209
+ Returns:
210
+ Tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike]: vertices (n, 3), faces (n, 3), triangle areas (n,), centers (n, 3)
211
+ """
212
+ subdivs = jnp.ceil(.5*jnp.log2(points/5)-1).astype(int)
213
+ verts, faces, areas, centers = _icosphere(subdivs)
214
+ return verts, faces, areas, centers