lsurf 1.0.0__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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. lsurf-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,353 @@
1
+ # The Clear BSD License
2
+ #
3
+ # Copyright (c) 2026 Tobias Heibges
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted (subject to the limitations in the disclaimer
8
+ # below) provided that the following conditions are met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ #
13
+ # * Redistributions in binary form must reproduce the above copyright
14
+ # notice, this list of conditions and the following disclaimer in the
15
+ # documentation and/or other materials provided with the distribution.
16
+ #
17
+ # * Neither the name of the copyright holder nor the names of its
18
+ # contributors may be used to endorse or promote products derived from this
19
+ # software without specific prior written permission.
20
+ #
21
+ # NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
22
+ # THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
23
+ # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
25
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
26
+ # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28
+ # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
29
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
30
+ # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+
34
+ """Ray renderer - visualizes ray paths and detection points.
35
+
36
+ Generates 3D representations of ray trajectories and detection scatter plots.
37
+ """
38
+
39
+ from typing import TYPE_CHECKING, Any
40
+
41
+ import numpy as np
42
+
43
+ if TYPE_CHECKING:
44
+ from lsurf.detectors import DetectorResult
45
+
46
+
47
+ class RayRenderer:
48
+ """Renders ray paths and detection points for Dear PyGui."""
49
+
50
+ def __init__(
51
+ self,
52
+ max_rays: int = 1000,
53
+ point_size: float = 0.1,
54
+ ) -> None:
55
+ """Initialize the renderer.
56
+
57
+ Args:
58
+ max_rays: Maximum number of rays to render (for performance)
59
+ point_size: Size of detection point markers
60
+ """
61
+ self.max_rays = max_rays
62
+ self.point_size = point_size
63
+
64
+ def render_detections(self, detected: "DetectorResult") -> dict[str, Any]:
65
+ """Render detection points as a point cloud.
66
+
67
+ Args:
68
+ detected: Detection results from simulation
69
+
70
+ Returns:
71
+ Dict with 'vertices', 'indices', and 'wireframe' keys
72
+ """
73
+ if detected is None or len(detected.positions) == 0:
74
+ return {
75
+ "vertices": np.zeros((0, 3), dtype=np.float32),
76
+ "indices": np.zeros(0, dtype=np.uint32),
77
+ "wireframe": False,
78
+ }
79
+
80
+ positions = np.array(detected.positions, dtype=np.float32)
81
+
82
+ # Subsample if too many points
83
+ if len(positions) > self.max_rays:
84
+ indices_sample = np.random.choice(
85
+ len(positions), self.max_rays, replace=False
86
+ )
87
+ positions = positions[indices_sample]
88
+
89
+ # Generate small markers for each detection point
90
+ all_vertices = []
91
+ all_indices = []
92
+ vertex_offset = 0
93
+
94
+ for pos in positions:
95
+ marker = self._generate_point_marker(pos)
96
+ all_vertices.append(marker["vertices"])
97
+ all_indices.append(marker["indices"] + vertex_offset)
98
+ vertex_offset += len(marker["vertices"])
99
+
100
+ if all_vertices:
101
+ vertices = np.vstack(all_vertices)
102
+ indices = np.concatenate(all_indices)
103
+ else:
104
+ vertices = np.zeros((0, 3), dtype=np.float32)
105
+ indices = np.zeros(0, dtype=np.uint32)
106
+
107
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
108
+
109
+ def render_ray_paths(self, surface_hits: dict[str, list]) -> dict[str, Any]:
110
+ """Render ray paths from surface hit records.
111
+
112
+ Args:
113
+ surface_hits: Dict mapping surface names to lists of hit records
114
+
115
+ Returns:
116
+ Dict with 'vertices', 'indices', and 'wireframe' keys
117
+ """
118
+ if not surface_hits:
119
+ return {
120
+ "vertices": np.zeros((0, 3), dtype=np.float32),
121
+ "indices": np.zeros(0, dtype=np.uint32),
122
+ "wireframe": True,
123
+ }
124
+
125
+ # Collect all hit positions
126
+ all_positions = []
127
+ for surface_name, hits in surface_hits.items():
128
+ for hit in hits:
129
+ if hasattr(hit, "position"):
130
+ all_positions.append(np.array(hit.position, dtype=np.float32))
131
+ elif hasattr(hit, "positions"):
132
+ all_positions.extend(
133
+ [np.array(p, dtype=np.float32) for p in hit.positions]
134
+ )
135
+
136
+ if not all_positions:
137
+ return {
138
+ "vertices": np.zeros((0, 3), dtype=np.float32),
139
+ "indices": np.zeros(0, dtype=np.uint32),
140
+ "wireframe": True,
141
+ }
142
+
143
+ # Subsample if too many
144
+ if len(all_positions) > self.max_rays:
145
+ indices_sample = np.random.choice(
146
+ len(all_positions), self.max_rays, replace=False
147
+ )
148
+ all_positions = [all_positions[i] for i in indices_sample]
149
+
150
+ vertices = np.array(all_positions, dtype=np.float32)
151
+
152
+ # For now, just render as points (connecting rays would require
153
+ # tracking individual ray paths through bounces)
154
+ all_indices = []
155
+ vertex_offset = 0
156
+
157
+ markers_vertices = []
158
+ for pos in vertices:
159
+ marker = self._generate_point_marker(pos, size=self.point_size * 0.5)
160
+ markers_vertices.append(marker["vertices"])
161
+ all_indices.append(marker["indices"] + vertex_offset)
162
+ vertex_offset += len(marker["vertices"])
163
+
164
+ if markers_vertices:
165
+ final_vertices = np.vstack(markers_vertices)
166
+ final_indices = np.concatenate(all_indices)
167
+ else:
168
+ final_vertices = np.zeros((0, 3), dtype=np.float32)
169
+ final_indices = np.zeros(0, dtype=np.uint32)
170
+
171
+ return {"vertices": final_vertices, "indices": final_indices, "wireframe": True}
172
+
173
+ def render_rays_batch(
174
+ self,
175
+ origins: np.ndarray,
176
+ directions: np.ndarray,
177
+ lengths: np.ndarray | float = 1.0,
178
+ ) -> dict[str, Any]:
179
+ """Render a batch of rays as lines.
180
+
181
+ Args:
182
+ origins: Ray origin positions (N, 3)
183
+ directions: Ray direction vectors (N, 3)
184
+ lengths: Ray lengths, scalar or array (N,)
185
+
186
+ Returns:
187
+ Dict with 'vertices', 'indices', and 'wireframe' keys
188
+ """
189
+ origins = np.array(origins, dtype=np.float32)
190
+ directions = np.array(directions, dtype=np.float32)
191
+
192
+ if np.isscalar(lengths):
193
+ lengths = np.full(len(origins), lengths, dtype=np.float32)
194
+ else:
195
+ lengths = np.array(lengths, dtype=np.float32)
196
+
197
+ # Subsample if too many
198
+ if len(origins) > self.max_rays:
199
+ indices_sample = np.random.choice(
200
+ len(origins), self.max_rays, replace=False
201
+ )
202
+ origins = origins[indices_sample]
203
+ directions = directions[indices_sample]
204
+ lengths = lengths[indices_sample]
205
+
206
+ # Normalize directions
207
+ norms = np.linalg.norm(directions, axis=1, keepdims=True)
208
+ norms[norms == 0] = 1 # Avoid division by zero
209
+ directions = directions / norms
210
+
211
+ # Compute end points
212
+ ends = origins + directions * lengths[:, np.newaxis]
213
+
214
+ # Interleave origins and ends for line segments
215
+ n_rays = len(origins)
216
+ vertices = np.zeros((n_rays * 2, 3), dtype=np.float32)
217
+ vertices[0::2] = origins
218
+ vertices[1::2] = ends
219
+
220
+ # Line segment indices
221
+ indices = np.arange(n_rays * 2, dtype=np.uint32)
222
+
223
+ return {"vertices": vertices, "indices": indices, "wireframe": True}
224
+
225
+ def _generate_point_marker(
226
+ self, position: np.ndarray, size: float | None = None
227
+ ) -> dict[str, Any]:
228
+ """Generate a small marker for a single point.
229
+
230
+ Uses a small octahedron shape for visibility.
231
+ """
232
+ if size is None:
233
+ size = self.point_size
234
+
235
+ pos = np.array(position, dtype=np.float32)
236
+
237
+ # Octahedron vertices
238
+ vertices = np.array(
239
+ [
240
+ pos + [size, 0, 0],
241
+ pos + [-size, 0, 0],
242
+ pos + [0, size, 0],
243
+ pos + [0, -size, 0],
244
+ pos + [0, 0, size],
245
+ pos + [0, 0, -size],
246
+ ],
247
+ dtype=np.float32,
248
+ )
249
+
250
+ # Triangle faces
251
+ indices = np.array(
252
+ [
253
+ 0,
254
+ 2,
255
+ 4,
256
+ 2,
257
+ 1,
258
+ 4,
259
+ 1,
260
+ 3,
261
+ 4,
262
+ 3,
263
+ 0,
264
+ 4,
265
+ 0,
266
+ 5,
267
+ 2,
268
+ 2,
269
+ 5,
270
+ 1,
271
+ 1,
272
+ 5,
273
+ 3,
274
+ 3,
275
+ 5,
276
+ 0,
277
+ ],
278
+ dtype=np.uint32,
279
+ )
280
+
281
+ return {"vertices": vertices, "indices": indices, "wireframe": False}
282
+
283
+ def color_by_intensity(self, intensities: np.ndarray) -> np.ndarray:
284
+ """Generate colors based on ray intensities.
285
+
286
+ Args:
287
+ intensities: Ray intensity values
288
+
289
+ Returns:
290
+ RGBA colors array (N, 4)
291
+ """
292
+ # Normalize to 0-1
293
+ if len(intensities) == 0:
294
+ return np.zeros((0, 4), dtype=np.float32)
295
+
296
+ i_min, i_max = intensities.min(), intensities.max()
297
+ if i_max > i_min:
298
+ normalized = (intensities - i_min) / (i_max - i_min)
299
+ else:
300
+ normalized = np.ones_like(intensities)
301
+
302
+ # Use a simple colormap (blue -> red)
303
+ colors = np.zeros((len(intensities), 4), dtype=np.float32)
304
+ colors[:, 0] = normalized # Red
305
+ colors[:, 2] = 1 - normalized # Blue
306
+ colors[:, 3] = 1.0 # Alpha
307
+
308
+ return colors
309
+
310
+ def color_by_wavelength(self, wavelengths: np.ndarray) -> np.ndarray:
311
+ """Generate colors based on wavelengths (approximate visible spectrum).
312
+
313
+ Args:
314
+ wavelengths: Wavelength values in meters
315
+
316
+ Returns:
317
+ RGBA colors array (N, 4)
318
+ """
319
+ if len(wavelengths) == 0:
320
+ return np.zeros((0, 4), dtype=np.float32)
321
+
322
+ # Convert to nanometers
323
+ wl_nm = wavelengths * 1e9
324
+
325
+ colors = np.zeros((len(wavelengths), 4), dtype=np.float32)
326
+
327
+ for i, wl in enumerate(wl_nm):
328
+ if wl < 380:
329
+ # UV - purple
330
+ colors[i] = [0.5, 0.0, 0.5, 1.0]
331
+ elif wl < 440:
332
+ # Violet
333
+ colors[i] = [0.5 - 0.5 * (wl - 380) / 60, 0, 1, 1]
334
+ elif wl < 490:
335
+ # Blue -> Cyan
336
+ colors[i] = [0, (wl - 440) / 50, 1, 1]
337
+ elif wl < 510:
338
+ # Cyan -> Green
339
+ colors[i] = [0, 1, 1 - (wl - 490) / 20, 1]
340
+ elif wl < 580:
341
+ # Green -> Yellow
342
+ colors[i] = [(wl - 510) / 70, 1, 0, 1]
343
+ elif wl < 645:
344
+ # Yellow -> Red
345
+ colors[i] = [1, 1 - (wl - 580) / 65, 0, 1]
346
+ elif wl < 780:
347
+ # Red
348
+ colors[i] = [1, 0, 0, 1]
349
+ else:
350
+ # IR - dark red
351
+ colors[i] = [0.5, 0, 0, 1]
352
+
353
+ return colors.astype(np.float32)