diffinytrace 2.1__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.
- diffinytrace/__init__.py +122 -0
- diffinytrace/basis_functions/__init__.py +14 -0
- diffinytrace/basis_functions/bspline.py +521 -0
- diffinytrace/basis_functions/chebyshev.py +3 -0
- diffinytrace/basis_functions/legendre.py +77 -0
- diffinytrace/basis_functions/zernike.py +235 -0
- diffinytrace/config.py +140 -0
- diffinytrace/constraints.py +54 -0
- diffinytrace/element.py +1660 -0
- diffinytrace/export/__init__.py +8 -0
- diffinytrace/export/cad.py +253 -0
- diffinytrace/gaussian_smoother.py +530 -0
- diffinytrace/hat_smoother.py +44 -0
- diffinytrace/integrators.py +452 -0
- diffinytrace/intersection.py +285 -0
- diffinytrace/optimize.py +808 -0
- diffinytrace/physical_object.py +150 -0
- diffinytrace/plotting/__init__.py +16 -0
- diffinytrace/plotting/core.py +92 -0
- diffinytrace/plotting/quantity2D.py +188 -0
- diffinytrace/plotting/system2D.py +220 -0
- diffinytrace/plotting/system3D.py +327 -0
- diffinytrace/plotting/wavelength.py +231 -0
- diffinytrace/refractive_index.py +101 -0
- diffinytrace/render.py +77 -0
- diffinytrace/source.py +661 -0
- diffinytrace/spectrum.py +79 -0
- diffinytrace/surface.py +468 -0
- diffinytrace/target_grid.py +399 -0
- diffinytrace/transforms.py +472 -0
- diffinytrace/utils/__init__.py +7 -0
- diffinytrace/utils/autograd.py +116 -0
- diffinytrace/utils/irradiance_importer.py +134 -0
- diffinytrace-2.1.dist-info/METADATA +26 -0
- diffinytrace-2.1.dist-info/RECORD +38 -0
- diffinytrace-2.1.dist-info/WHEEL +5 -0
- diffinytrace-2.1.dist-info/licenses/LICENSE +21 -0
- diffinytrace-2.1.dist-info/top_level.txt +1 -0
diffinytrace/source.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
# Copyright (c) 2025 Martin Pflaum
|
|
2
|
+
# This file is part of the diffinytrace project, licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"RaySource",
|
|
6
|
+
"LightSource",
|
|
7
|
+
"PlaneSource",
|
|
8
|
+
"PlaneSource1D",
|
|
9
|
+
"make_cone_directions",
|
|
10
|
+
"VisibleSunlightSimpleMonochromatic",
|
|
11
|
+
"VisibleSunlightSimple",
|
|
12
|
+
"CollimatedMonochromatic",
|
|
13
|
+
"CollimatedGaussianBeam",
|
|
14
|
+
"CollimatedMonochromatic1D",
|
|
15
|
+
"CollimatedMonochromatic1DRotSym",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
import torch.nn as nn
|
|
20
|
+
import numpy as np
|
|
21
|
+
import math
|
|
22
|
+
from .plotting import Plotable
|
|
23
|
+
from .integrators import Disc,Cube
|
|
24
|
+
from .spectrum import VisibleSunlight_am15g
|
|
25
|
+
from .physical_object import PhysicalObject
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RaySource(PhysicalObject):
|
|
29
|
+
"""
|
|
30
|
+
The RaySource is a the base class off all Objects emitting rays. It has a sample function which
|
|
31
|
+
samples points. These points are used to initialize the rays with the forward function.
|
|
32
|
+
"""
|
|
33
|
+
def __init__(self,transform):
|
|
34
|
+
"""
|
|
35
|
+
Args:
|
|
36
|
+
transform (Transform): The transformation object to be used for the ray source.
|
|
37
|
+
"""
|
|
38
|
+
PhysicalObject.__init__(self)
|
|
39
|
+
self.transform = transform
|
|
40
|
+
|
|
41
|
+
def sample(self,num_points,method):
|
|
42
|
+
"""
|
|
43
|
+
Samples points in the parameter space of the ray source.
|
|
44
|
+
Args:
|
|
45
|
+
num_points (int): The number of points to sample.
|
|
46
|
+
method (str): The sampling method to use. Can be "sobol", "monte_carlo", etc.
|
|
47
|
+
Returns:
|
|
48
|
+
tuple: A tuple containing the sampled points and their weights.
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError("sample() not implemented")
|
|
51
|
+
|
|
52
|
+
def forward(self,x,n_func_enviroment):
|
|
53
|
+
|
|
54
|
+
raise NotImplementedError("forward() not implemented")
|
|
55
|
+
|
|
56
|
+
def get_plot_points_2D(self,resolution):
|
|
57
|
+
print("get_plot_points_2D not implemented")
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
def get_plot_points_3D(self,resolution):
|
|
61
|
+
print("get_plot_points_3D not implemented")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
def get_volume(self):
|
|
65
|
+
raise NotImplementedError("area not implemented")
|
|
66
|
+
|
|
67
|
+
def get_transform(self):
|
|
68
|
+
return self.transform
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LightSource(RaySource,Plotable):
|
|
73
|
+
r"""
|
|
74
|
+
In our model of an optical system, the light source is composed of the following components:
|
|
75
|
+
|
|
76
|
+
1. **Light Source Domain**
|
|
77
|
+
The *light source domain* :math:`\Omega \subseteq \mathbb{R}^N` represents the spatial domain of the light source.
|
|
78
|
+
|
|
79
|
+
2. **Global Ray Initialization Function**
|
|
80
|
+
The *global ray initialization function* :math:`f: \mathbb{R}^N \times \mathbb{R}^{4 \times 4} \to \mathbb{R}^7` is responsible for initializing the ray's position :math:`O`, direction :math:`D`, and wavelength :math:`\lambda` using a point :math:`x \in \Omega` from the *light source domain* :math:`\Omega`. It consists of:
|
|
81
|
+
|
|
82
|
+
- An affine transformation :math:`M \in \mathbb{R}^{4 \times 4}`.
|
|
83
|
+
- A *local ray initialization function* :math:`\hat{f}: \mathbb{R}^N \to \mathbb{R}^7`.
|
|
84
|
+
|
|
85
|
+
The *global ray initialization function* is defined by:
|
|
86
|
+
|
|
87
|
+
.. math::
|
|
88
|
+
|
|
89
|
+
f(x, M) :=
|
|
90
|
+
\begin{bmatrix}
|
|
91
|
+
M \begin{bmatrix}\hat{O} \\ 1\end{bmatrix} \\
|
|
92
|
+
M \begin{bmatrix}\hat{D} \\ 0\end{bmatrix} \\
|
|
93
|
+
\lambda
|
|
94
|
+
\end{bmatrix},
|
|
95
|
+
|
|
96
|
+
where:
|
|
97
|
+
|
|
98
|
+
.. math::
|
|
99
|
+
|
|
100
|
+
\begin{bmatrix}\hat{O} \\ \hat{D} \\ \lambda \end{bmatrix} = \hat{f}(x).
|
|
101
|
+
|
|
102
|
+
3. **Flux Function**
|
|
103
|
+
The *flux function* :math:`Q: \mathbb{R}^N \to \mathbb{R}` assigns a flux weight to each point :math:`x \in \Omega`.
|
|
104
|
+
|
|
105
|
+
The *ray initialization function* bridges the gap between the *light source domain* and the actual ray tracer. This abstraction is also very helpful for implementation, as it allows for modular design. Following this principle, one does not sample rays in our library, but points and weights according to an integration rule. The light source classes also implement a forward function which is equivalent to the *ray initialization function*.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
>>> import diffinytrace as dit
|
|
109
|
+
>>> wave_len = 1.0
|
|
110
|
+
>>> radius = 8.0
|
|
111
|
+
>>> num_rays = 100
|
|
112
|
+
>>> transform = dit.transforms.Identity()
|
|
113
|
+
>>> light_source = dit.source.CollimatedMonochromatic(transform, radius, wave_len)
|
|
114
|
+
>>> x, weights = light_source.sample(num_rays, "sobol")
|
|
115
|
+
>>> O, D, wls, _, meta_data = light_source(x, n_func_enviroment=dit.materials["AIR"])
|
|
116
|
+
|
|
117
|
+
In the following, we introduce two types of light source models along with their corresponding *light source domains* and *local ray initialization functions*.
|
|
118
|
+
|
|
119
|
+
1. **Collimated Monochromatic Light Source**
|
|
120
|
+
For this light source model, rays originate from a planar aperture with a uniform direction and a single wavelength. An example for light sources which can be modeled as collimated monochromatic light sources are lasers (see :cite:`dickey2018laser`).
|
|
121
|
+
|
|
122
|
+
- The *light source domain* :math:`\Omega_{CO} \subseteq \mathbb{R}^2` can take various shapes.
|
|
123
|
+
- The *local ray initialization function* is, given a single wavelength :math:`\lambda`, always in the form of:
|
|
124
|
+
|
|
125
|
+
.. math::
|
|
126
|
+
|
|
127
|
+
\hat{f}(x, \lambda)_{CO} =
|
|
128
|
+
\begin{bmatrix}
|
|
129
|
+
x_1 \\
|
|
130
|
+
x_2 \\
|
|
131
|
+
0 \\
|
|
132
|
+
0 \\
|
|
133
|
+
0 \\
|
|
134
|
+
1 \\
|
|
135
|
+
\lambda
|
|
136
|
+
\end{bmatrix}.
|
|
137
|
+
|
|
138
|
+
- The *flux function* must align with the *light source domain* :math:`\Omega_{CO}`.
|
|
139
|
+
|
|
140
|
+
Examples of *light source domains* for collimated monochromatic light sources include:
|
|
141
|
+
|
|
142
|
+
- **Round Domains**
|
|
143
|
+
Defined by:
|
|
144
|
+
|
|
145
|
+
.. math::
|
|
146
|
+
|
|
147
|
+
\Omega^{round(\hat{r})}_{CO} = \left\{x : ||x||_2 \leq \hat{r}\right\} \subseteq \mathbb{R}^2,
|
|
148
|
+
|
|
149
|
+
where :math:`\hat{r}` is the radius.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> import diffinytrace as dit
|
|
153
|
+
>>> wl = 0.5
|
|
154
|
+
>>> r = 8.0
|
|
155
|
+
>>> transform = dit.transforms.Identity()
|
|
156
|
+
>>> light_source = dit.source.CollimatedMonochromatic(transform, r, wl)
|
|
157
|
+
|
|
158
|
+
- **Square Domains**
|
|
159
|
+
Defined by:
|
|
160
|
+
|
|
161
|
+
.. math::
|
|
162
|
+
|
|
163
|
+
\Omega^{square(\hat{d})}_{CO} = \left\{x : ||x||_\infty \leq \hat{d}\right\} \subseteq \mathbb{R}^2,
|
|
164
|
+
|
|
165
|
+
where :math:`\hat{d}` is the half diameter.
|
|
166
|
+
|
|
167
|
+
2. **Monochromatic Light Sources with Etendue**
|
|
168
|
+
Real-world physical light sources are characterized by a finite area-angle product, corresponding to a finite etendue (see :cite:`rausch2017illumination`). In this model, rays originate from a planar surface with a specific wavelength, but the directions differ.
|
|
169
|
+
|
|
170
|
+
- The *light source domain* :math:`\Omega_{ET} \subseteq \mathbb{R}^4` has four dimensions.
|
|
171
|
+
- The *flux function* maps a 4-dimensional vector to a single value.
|
|
172
|
+
- The *local ray initialization function* maps a 4-dimensional vector to a 7-dimensional vector.
|
|
173
|
+
|
|
174
|
+
Example: **Pillbox Sunlight Model**
|
|
175
|
+
The sun has an etendue, meaning both an angular and spatial extent. The sunshape describes how solar radiation is distributed across the solar disk, represented by the normalized radiance profile :math:`\hat{L}(\theta)`:
|
|
176
|
+
|
|
177
|
+
.. math::
|
|
178
|
+
|
|
179
|
+
\hat{L}_{\text{pillbox}}(\theta) =
|
|
180
|
+
\begin{cases}
|
|
181
|
+
L_1 & \text{if } 0 \leq \theta \leq \theta_{disc}, \\
|
|
182
|
+
0 & \text{if } \theta > \theta_{disc}.
|
|
183
|
+
\end{cases}
|
|
184
|
+
|
|
185
|
+
Here, :math:`\theta` is the angular displacement of a ray compared to the general direction of the sun. Typically, :math:`\theta_{disc} = 4.65 \, \text{mrad}` is used (see :cite:`pillbox_sunlight`).
|
|
186
|
+
|
|
187
|
+
The *light source domain* for the pillbox sunlight model is defined as:
|
|
188
|
+
|
|
189
|
+
.. math::
|
|
190
|
+
|
|
191
|
+
\Omega^{pillbox(\hat{d})}_{ET} := \left\{x : ||[x_1, x_2]^T||_\infty \leq \hat{d}, ||[x_3, x_4]^T||_\infty \leq 1\right\} \subseteq \mathbb{R}^4,
|
|
192
|
+
|
|
193
|
+
where :math:`\hat{d}` is the half diameter.
|
|
194
|
+
|
|
195
|
+
The *local ray initialization function* is:
|
|
196
|
+
|
|
197
|
+
.. math::
|
|
198
|
+
|
|
199
|
+
\hat{f}(x)_{ET}^{pillbox} :=
|
|
200
|
+
\begin{bmatrix}
|
|
201
|
+
x_1 \\
|
|
202
|
+
x_2 \\
|
|
203
|
+
0 \\
|
|
204
|
+
\sin(\theta(x_3))\cos(\phi(x_4)) \\
|
|
205
|
+
\sin(\theta(x_3))\sin(\phi(x_4)) \\
|
|
206
|
+
\cos(\theta(x_3)) \\
|
|
207
|
+
\lambda
|
|
208
|
+
\end{bmatrix},
|
|
209
|
+
|
|
210
|
+
where:
|
|
211
|
+
|
|
212
|
+
.. math::
|
|
213
|
+
|
|
214
|
+
\theta(x_3) := -\theta_{disc} + 2\theta_{disc}x_3, \quad \phi(x_4) := 2\pi x_4.
|
|
215
|
+
|
|
216
|
+
The *flux function* is:
|
|
217
|
+
|
|
218
|
+
.. math::
|
|
219
|
+
|
|
220
|
+
Q_{ET}(x)_{pillbox} := \frac{1 \, \text{W}}{|\Omega^{pillbox(\hat{d})}_{ET}|}.
|
|
221
|
+
"""
|
|
222
|
+
def __init__(self,transform,integrator,flux_func=None,total_power=1.0,num_points_normalize=700000,method_normalize="sobol"):
|
|
223
|
+
"""
|
|
224
|
+
Args:
|
|
225
|
+
transform (Transform): The transformation object to be used for the light source.
|
|
226
|
+
integrator (Integrator): The integrator object to be used for sampling.
|
|
227
|
+
flux_func (callable, optional): A function that returns the flux at a given point. Defaults to None.
|
|
228
|
+
total_power (float, optional): The total power of the light source. Defaults to 1.0.
|
|
229
|
+
num_points_normalize (int, optional): The number of points to use for normalization. Defaults to 700000.
|
|
230
|
+
method_normalize (str, optional): The sampling method to use for normalization. Defaults to "sobol".
|
|
231
|
+
"""
|
|
232
|
+
Plotable.__init__(self,"#f8cecc","#b85450")
|
|
233
|
+
RaySource.__init__(self,transform)
|
|
234
|
+
self.total_power = total_power
|
|
235
|
+
self.integrator = integrator
|
|
236
|
+
self.norm_val = None
|
|
237
|
+
if flux_func is None:
|
|
238
|
+
def t_flux_func(x):
|
|
239
|
+
device = x.device
|
|
240
|
+
dtype = x.dtype
|
|
241
|
+
return torch.ones(x.shape[0],device=device,dtype=dtype)/self.get_volume()
|
|
242
|
+
flux_func = t_flux_func
|
|
243
|
+
self.norm_val = 1.0
|
|
244
|
+
|
|
245
|
+
self._flux_func = flux_func
|
|
246
|
+
|
|
247
|
+
if self.norm_val is None:
|
|
248
|
+
points,weights = self.integrator.sample(num_points_normalize,method_normalize)
|
|
249
|
+
self.norm_val = torch.sum(self._flux_func(points)*weights).cpu().item()
|
|
250
|
+
|
|
251
|
+
def sample(self,num_points,method):
|
|
252
|
+
return self.integrator.sample(num_points,method)
|
|
253
|
+
|
|
254
|
+
def get_flux(self,x):
|
|
255
|
+
return self._flux_func(x)*(self.total_power/self.norm_val)
|
|
256
|
+
|
|
257
|
+
def get_volume(self):
|
|
258
|
+
return self.integrator.get_volume()
|
|
259
|
+
|
|
260
|
+
class PlaneSource(LightSource):
|
|
261
|
+
def __init__(self,transform,aperture_radius,integrator,is_square=False,flux_func=None,total_power=1.0,num_points_normalize=700000,method_normalize="sobol"):
|
|
262
|
+
super().__init__(transform,integrator,flux_func,total_power,num_points_normalize,method_normalize)
|
|
263
|
+
self.aperture_radius = aperture_radius
|
|
264
|
+
self.is_square = is_square
|
|
265
|
+
|
|
266
|
+
def get_plot_points_2D(self,resolution):
|
|
267
|
+
aperture_radius = self.aperture_radius
|
|
268
|
+
x = None
|
|
269
|
+
y = None
|
|
270
|
+
|
|
271
|
+
y = torch.linspace(-aperture_radius,aperture_radius,resolution)
|
|
272
|
+
x = torch.zeros_like(y)
|
|
273
|
+
|
|
274
|
+
tmpx,_ = self.sample(1,"sobol")
|
|
275
|
+
x_integrator = torch.zeros((x.shape[0],tmpx.shape[1]))
|
|
276
|
+
x_integrator[:,0] = x
|
|
277
|
+
x_integrator[:,1] = y
|
|
278
|
+
z = None
|
|
279
|
+
with torch.no_grad():
|
|
280
|
+
O2,D2,wl,_,_ = self(x_integrator,None)
|
|
281
|
+
|
|
282
|
+
x = O2[:,0].detach().reshape(-1)
|
|
283
|
+
y = O2[:,1].detach().reshape(-1)
|
|
284
|
+
z = O2[:,2].detach().reshape(-1)
|
|
285
|
+
|
|
286
|
+
if not self.is_square:
|
|
287
|
+
mul = (torch.sqrt(x*x+y*y)>aperture_radius).float()/torch.sqrt(x*x+y*y)*aperture_radius
|
|
288
|
+
mul += (torch.sqrt(x*x+y*y)<aperture_radius).float()
|
|
289
|
+
x = x*mul
|
|
290
|
+
y = y*mul
|
|
291
|
+
|
|
292
|
+
transform=self.transform
|
|
293
|
+
v = torch.zeros((x.shape[0],4))
|
|
294
|
+
v[:,0] = x
|
|
295
|
+
v[:,1] = y
|
|
296
|
+
v[:,2] = z
|
|
297
|
+
v[:,3] = torch.ones_like(v[:,3])
|
|
298
|
+
|
|
299
|
+
Mv = None
|
|
300
|
+
with torch.no_grad():
|
|
301
|
+
M = transform.get_transformation_matrix().detach()
|
|
302
|
+
Mv = v@M.T
|
|
303
|
+
x = Mv[:,0].reshape(-1)
|
|
304
|
+
y = Mv[:,1].reshape(-1)
|
|
305
|
+
z = Mv[:,2].reshape(-1)
|
|
306
|
+
return [(z,y)]
|
|
307
|
+
|
|
308
|
+
def get_plot_points_3D(self,resolution):
|
|
309
|
+
aperture_radius = self.aperture_radius
|
|
310
|
+
x = None
|
|
311
|
+
y = None
|
|
312
|
+
|
|
313
|
+
_x = torch.linspace(-aperture_radius,aperture_radius,resolution)
|
|
314
|
+
_y = torch.linspace(-aperture_radius,aperture_radius,resolution)
|
|
315
|
+
mesh = torch.meshgrid(_x,_y)
|
|
316
|
+
x = mesh[0].reshape(-1)
|
|
317
|
+
y = mesh[1].reshape(-1)
|
|
318
|
+
|
|
319
|
+
tmpx,_ = self.sample(1,"sobol")
|
|
320
|
+
x_integrator = torch.zeros((x.shape[0],tmpx.shape[1]))
|
|
321
|
+
x_integrator[:,0] = x
|
|
322
|
+
x_integrator[:,1] = y
|
|
323
|
+
z = None
|
|
324
|
+
with torch.no_grad():
|
|
325
|
+
O2,D2,wl,_,_ = self(x_integrator,None)
|
|
326
|
+
|
|
327
|
+
x = O2[:,0].detach().reshape(-1)
|
|
328
|
+
y = O2[:,1].detach().reshape(-1)
|
|
329
|
+
z = O2[:,2].detach().reshape(-1)
|
|
330
|
+
|
|
331
|
+
if not self.is_square:
|
|
332
|
+
mul = (torch.sqrt(x*x+y*y)>aperture_radius).float()/torch.sqrt(x*x+y*y)*aperture_radius
|
|
333
|
+
mul += (torch.sqrt(x*x+y*y)<aperture_radius).float()
|
|
334
|
+
x = x*mul
|
|
335
|
+
y = y*mul
|
|
336
|
+
|
|
337
|
+
transform=self.transform
|
|
338
|
+
v = torch.zeros((x.shape[0],4))
|
|
339
|
+
v[:,0] = x
|
|
340
|
+
v[:,1] = y
|
|
341
|
+
v[:,2] = z
|
|
342
|
+
v[:,3] = torch.ones_like(v[:,3])
|
|
343
|
+
|
|
344
|
+
Mv = None
|
|
345
|
+
with torch.no_grad():
|
|
346
|
+
M = transform.get_transformation_matrix().detach()
|
|
347
|
+
Mv = v@M.T
|
|
348
|
+
x = Mv[:,0].reshape(resolution,resolution)
|
|
349
|
+
y = Mv[:,1].reshape(resolution,resolution)
|
|
350
|
+
z = Mv[:,2].reshape(resolution,resolution)
|
|
351
|
+
return [(x,y,z)]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class PlaneSource1D(LightSource):
|
|
355
|
+
def __init__(self,transform,aperture_radius,integrator,flux_func,total_power=1.0,num_points_normalize=700000,method_normalize="sobol"):
|
|
356
|
+
super().__init__(transform,integrator,flux_func,total_power,num_points_normalize,method_normalize)
|
|
357
|
+
self.aperture_radius = aperture_radius
|
|
358
|
+
|
|
359
|
+
def get_plot_points_2D(self,resolution):
|
|
360
|
+
aperture_radius = self.aperture_radius
|
|
361
|
+
|
|
362
|
+
transform=self.transform
|
|
363
|
+
v = torch.zeros((resolution,4))
|
|
364
|
+
v[:,1] = torch.linspace(-aperture_radius,aperture_radius,resolution)
|
|
365
|
+
v[:,3] = torch.ones_like(v[:,3])
|
|
366
|
+
|
|
367
|
+
Mv = None
|
|
368
|
+
with torch.no_grad():
|
|
369
|
+
M = transform.get_transformation_matrix().detach()
|
|
370
|
+
Mv = v@M.T
|
|
371
|
+
|
|
372
|
+
x = Mv[:,0].reshape(-1)
|
|
373
|
+
y = Mv[:,1].reshape(-1)
|
|
374
|
+
z = Mv[:,2].reshape(-1)
|
|
375
|
+
return [(z,y)]
|
|
376
|
+
|
|
377
|
+
def get_plot_points_3D(self,resolution):
|
|
378
|
+
raise RuntimeError("get_plot_points_3D is not implemented for PlaneSource1D")
|
|
379
|
+
|
|
380
|
+
def make_cone_directions(num_rays, unif1, unif2, theta_max_rad):
|
|
381
|
+
"""
|
|
382
|
+
Sample directions uniformly within a cone of angular radius `theta_max_rad` centered on the z-axis.
|
|
383
|
+
|
|
384
|
+
Parameters:
|
|
385
|
+
- num_rays (int): Number of direction vectors to sample within the cone.
|
|
386
|
+
- unif1 (torch.Tensor): Tensor of uniform samples for cos(theta) sampling.
|
|
387
|
+
- unif2 (torch.Tensor): Tensor of uniform samples for the azimuthal angle sampling.
|
|
388
|
+
- theta_max_rad (float): Angular radius of the cone in radians.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
- directions (torch.Tensor): Tensor of shape (num_rays, 3), with each row a direction vector.
|
|
392
|
+
"""
|
|
393
|
+
# Ensure uniform samples are tensors and match the number of rays
|
|
394
|
+
if unif1.shape[0] != num_rays or unif2.shape[0] != num_rays:
|
|
395
|
+
raise ValueError("Length of unif1 and unif2 must match num_rays.")
|
|
396
|
+
theta_max_rad = torch.tensor(theta_max_rad)
|
|
397
|
+
# Uniform sampling of cos(theta) within the cone angle
|
|
398
|
+
thetas = -theta_max_rad + unif1 * (theta_max_rad*2.0)
|
|
399
|
+
|
|
400
|
+
# Uniform sampling of azimuthal angle phi
|
|
401
|
+
phi = (2.0 * torch.pi) * unif2
|
|
402
|
+
|
|
403
|
+
# Spherical to Cartesian conversion (cone aligned with z-axis)
|
|
404
|
+
x = torch.sin(thetas) * torch.cos(phi)
|
|
405
|
+
y = torch.sin(thetas) * torch.sin(phi)
|
|
406
|
+
z = torch.cos(thetas)
|
|
407
|
+
|
|
408
|
+
# Combine into a directions tensor
|
|
409
|
+
directions = torch.stack((x, y, z), dim=1)
|
|
410
|
+
|
|
411
|
+
return directions
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
import numpy as np
|
|
417
|
+
import matplotlib.pyplot as plt
|
|
418
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
419
|
+
|
|
420
|
+
num_rays = 20
|
|
421
|
+
directions = sample_cone_directions(num_rays)
|
|
422
|
+
|
|
423
|
+
# Plotting the sampled directions as lines in 3D
|
|
424
|
+
fig = plt.figure(figsize=(8, 6))
|
|
425
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
426
|
+
|
|
427
|
+
# Plot lines from the origin to each sampled direction
|
|
428
|
+
for direction in directions:
|
|
429
|
+
ax.plot([0, direction[0]], [0, direction[1]], [0, direction[2]], color='blue', alpha=0.6)
|
|
430
|
+
|
|
431
|
+
ax.set_xlabel("X")
|
|
432
|
+
ax.set_ylabel("Y")
|
|
433
|
+
ax.set_zlabel("Z")
|
|
434
|
+
ax.set_title("Sampled Directions in a 4.65 mrad Cone Centered Around the Z-axis")
|
|
435
|
+
ax.view_init(elev=20, azim=30) # Adjust viewing angle for better visualization
|
|
436
|
+
plt.show()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
class VisibleSunlightSimpleMonochromatic(PlaneSource):
|
|
441
|
+
"""
|
|
442
|
+
A class representing a visible sunlight source with a monochromatic spectrum.
|
|
443
|
+
It also has a cone of 4.65 mrad."""
|
|
444
|
+
|
|
445
|
+
def __init__(self,transform,aperture_radius,wl=0.5,is_square=True,total_power=1.0,theta_max_rad=4.65/1000.):
|
|
446
|
+
"""
|
|
447
|
+
cube1 = Cube([[-aperture_radius,aperture_radius],[-aperture_radius,aperture_radius]])
|
|
448
|
+
cube2 = Cube([[0.,1.],[0.,1.]])
|
|
449
|
+
|
|
450
|
+
integrator = Compose([cube1,cube2])
|
|
451
|
+
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
integrator = Cube([[-aperture_radius,aperture_radius],[-aperture_radius,aperture_radius],[0.,1.],[0.,1.]])
|
|
455
|
+
self.theta_max_rad = theta_max_rad
|
|
456
|
+
self.wl = wl
|
|
457
|
+
PlaneSource.__init__(self,transform,aperture_radius,integrator,is_square,None,total_power)
|
|
458
|
+
|
|
459
|
+
def sample(self,num_points,method="monte_carlo"):
|
|
460
|
+
if not ((method == "sobol_pow2")or (method == "sobol") or (method == "monte_carlo")):
|
|
461
|
+
print("Only sobol_pow2,sobol or monte_carlo sampling supported for VisibleSunlightSimpleMonochromatic")
|
|
462
|
+
return self.integrator.sample(num_points,method)
|
|
463
|
+
|
|
464
|
+
def forward(self,x,n_func_enviroment):
|
|
465
|
+
N = x.shape[0]
|
|
466
|
+
device = x.device
|
|
467
|
+
dtype = x.dtype
|
|
468
|
+
O1 = torch.zeros(N,3,device=device,dtype=dtype)
|
|
469
|
+
O1[:,[0,1]] = x[:,[0,1]]
|
|
470
|
+
O1[:,-1] = 0.0
|
|
471
|
+
D1 = make_cone_directions(N, x[:,2], x[:,3], self.theta_max_rad)
|
|
472
|
+
#print(D1)
|
|
473
|
+
wl = torch.ones((N),device=device,dtype=dtype)*self.wl
|
|
474
|
+
#self.spectrum(x[4])
|
|
475
|
+
|
|
476
|
+
#TODO inconsitent with plotting!!S
|
|
477
|
+
O2 = self.transform.to_global_pos(O1)
|
|
478
|
+
D2 = self.transform.to_global_dir(D1)
|
|
479
|
+
|
|
480
|
+
ray_paths = [O2.detach()]
|
|
481
|
+
|
|
482
|
+
valid = self.integrator.in_bounds(x)
|
|
483
|
+
PL,OPL = 0.0,0.0
|
|
484
|
+
meta_data = {}
|
|
485
|
+
meta_data["PL"],meta_data["OPL"],meta_data["ray_paths"], meta_data["valid"] = PL, OPL, ray_paths, valid
|
|
486
|
+
return O2,D2,wl,n_func_enviroment,meta_data
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class VisibleSunlightSimple(PlaneSource):
|
|
491
|
+
"""
|
|
492
|
+
A class representing a visible sunlight source with a spectrum acording to the visble sunlight.
|
|
493
|
+
It also has a an etendue with cone of 4.65 mrad."""
|
|
494
|
+
def __init__(self,transform,aperture_radius,is_square=True,total_power=1.0):
|
|
495
|
+
self.spectrum = VisibleSunlight_am15g()
|
|
496
|
+
self.aperture_radius = aperture_radius
|
|
497
|
+
integrator = Cube([[-aperture_radius,aperture_radius],[-aperture_radius,aperture_radius],[0.,1.],[0.,1.],self.spectrum.bounds])
|
|
498
|
+
PlaneSource.__init__(self,transform,aperture_radius,integrator,is_square,None,total_power)
|
|
499
|
+
self.theta_max_mrad = 4.65
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def sample(self,num_points,method="monte_carlo"):
|
|
503
|
+
if not ((method == "sobol_pow2")or (method == "sobol") or (method == "monte_carlo")):
|
|
504
|
+
raise RuntimeError("Only sobol_pow2,sobol or monte_carlo sampling supported for VisibleSunlightSimple")
|
|
505
|
+
return self.integrator.sample(num_points,method)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def forward(self,x,n_func_enviroment):
|
|
509
|
+
N = x.shape[0]
|
|
510
|
+
device = x.device
|
|
511
|
+
dtype = x.dtype
|
|
512
|
+
O1 = torch.zeros(N,3,device=device,dtype=dtype)
|
|
513
|
+
O1[:,[0,1]] = x[:,[0,1]]
|
|
514
|
+
O1[:,-1] = 0.0
|
|
515
|
+
|
|
516
|
+
D1 = make_cone_directions(N, x[:,2], x[:,3], self.theta_max_mrad)
|
|
517
|
+
wl = self.spectrum.bounds[0]+(self.spectrum.bounds[1]-self.spectrum.bounds[0])*x[:,4]
|
|
518
|
+
#self.spectrum(x[4])
|
|
519
|
+
|
|
520
|
+
#TODO inconsitent with plotting!!S
|
|
521
|
+
O2 = self.transform.to_global_pos(O1)
|
|
522
|
+
D2 = self.transform.to_global_dir(D1)
|
|
523
|
+
|
|
524
|
+
ray_paths = [O2.detach()]
|
|
525
|
+
|
|
526
|
+
valid = self.integrator.in_bounds(x)
|
|
527
|
+
PL,OPL = 0.0,0.0
|
|
528
|
+
meta_data = {}
|
|
529
|
+
meta_data["PL"],meta_data["OPL"],meta_data["ray_paths"], meta_data["valid"] = PL, OPL, ray_paths, valid
|
|
530
|
+
return O2,D2,wl,n_func_enviroment,meta_data
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class CollimatedMonochromatic(PlaneSource):
|
|
534
|
+
"""
|
|
535
|
+
A class representing a collimated monochromatic light source.
|
|
536
|
+
This class is a subclass of PlaneSource and is used to generate rays
|
|
537
|
+
with a specific wavelength and a collimated beam profile.
|
|
538
|
+
"""
|
|
539
|
+
def __init__(self,transform,aperture_radius,wl,is_square=False,flux_func=None,total_power=1.0):
|
|
540
|
+
integrator = None
|
|
541
|
+
aperture_radius = abs(aperture_radius)
|
|
542
|
+
if is_square:
|
|
543
|
+
integrator = Cube([[-aperture_radius,aperture_radius],[-aperture_radius,aperture_radius]])
|
|
544
|
+
else:
|
|
545
|
+
integrator = Disc(aperture_radius)
|
|
546
|
+
super().__init__(transform,aperture_radius,integrator,is_square,flux_func,total_power)
|
|
547
|
+
self.wl = wl
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def sample(self,num_points,method="monte_carlo")->torch.Tensor:
|
|
551
|
+
return self.integrator.sample(num_points,method)
|
|
552
|
+
|
|
553
|
+
def forward(self,x,n_func_enviroment):
|
|
554
|
+
N = x.shape[0]
|
|
555
|
+
device = x.device
|
|
556
|
+
dtype = x.dtype
|
|
557
|
+
O1 = torch.zeros(N,3,device=device,dtype=dtype)
|
|
558
|
+
O1[:,[0,1]] = x
|
|
559
|
+
O1[:,-1] = 0.0
|
|
560
|
+
|
|
561
|
+
D1 = torch.zeros_like(O1)
|
|
562
|
+
D1[:,-1] = 1.0
|
|
563
|
+
PL,OPL = 0.0,0.0
|
|
564
|
+
#TODO inconsitent with plotting!!S
|
|
565
|
+
O2 = self.transform.to_global_pos(O1)
|
|
566
|
+
D2 = self.transform.to_global_dir(D1)
|
|
567
|
+
|
|
568
|
+
ray_paths = [O2.detach()]
|
|
569
|
+
|
|
570
|
+
valid = self.integrator.in_bounds(x)
|
|
571
|
+
|
|
572
|
+
wl = torch.ones(N,device=device,dtype=dtype)*self.wl
|
|
573
|
+
meta_data = {}
|
|
574
|
+
meta_data["PL"],meta_data["OPL"],meta_data["ray_paths"], meta_data["valid"] = PL, OPL, ray_paths, valid
|
|
575
|
+
return O2,D2,wl,n_func_enviroment,meta_data
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class CollimatedGaussianBeam(CollimatedMonochromatic):
|
|
579
|
+
def __init__(self,transform,aperture_radius,wl,gaussian_constant,total_power=1.0):
|
|
580
|
+
def flux_func(x):
|
|
581
|
+
rho = torch.norm(x,dim=1)
|
|
582
|
+
return torch.exp(-gaussian_constant*(rho**2.0))
|
|
583
|
+
super().__init__(transform,aperture_radius,wl,is_square=False,flux_func=flux_func,total_power=total_power)
|
|
584
|
+
|
|
585
|
+
class CollimatedMonochromatic1D(PlaneSource1D):
|
|
586
|
+
"""
|
|
587
|
+
A class representing a collimated monochromatic light source.
|
|
588
|
+
This class is a subclass of PlaneSource1D and is used to generate rays
|
|
589
|
+
with a specific wavelength and a collimated beam profile."""
|
|
590
|
+
def __init__(self,transform,aperture_radius,wl,flux_func=None,total_power=1.0):
|
|
591
|
+
self.wl = wl
|
|
592
|
+
integrator = Cube([[-aperture_radius,aperture_radius]])
|
|
593
|
+
super().__init__(transform,aperture_radius,integrator,flux_func,total_power)
|
|
594
|
+
|
|
595
|
+
def sample(self,num_points,method="monte_carlo"):
|
|
596
|
+
return self.integrator.sample(num_points,method)
|
|
597
|
+
|
|
598
|
+
def forward(self,x,n_func_enviroment):
|
|
599
|
+
N = x.shape[0]
|
|
600
|
+
device = x.device
|
|
601
|
+
dtype = x.dtype
|
|
602
|
+
O1 = torch.zeros(N,3,device=device,dtype=dtype)
|
|
603
|
+
O1[:,1] = x[:,0]
|
|
604
|
+
O1[:,-1] = 0.0
|
|
605
|
+
|
|
606
|
+
D1 = torch.zeros_like(O1)
|
|
607
|
+
D1[:,-1] = 1.0
|
|
608
|
+
PL,OPL = 0.0,0.0
|
|
609
|
+
|
|
610
|
+
O2 = self.transform.to_global_pos(O1)
|
|
611
|
+
D2 = self.transform.to_global_dir(D1)
|
|
612
|
+
|
|
613
|
+
ray_paths = [O2.detach()]
|
|
614
|
+
valid = self.integrator.in_bounds(x)
|
|
615
|
+
wl = torch.ones(N,device=device,dtype=dtype)*self.wl
|
|
616
|
+
|
|
617
|
+
meta_data = {}
|
|
618
|
+
meta_data["PL"],meta_data["OPL"],meta_data["ray_paths"], meta_data["valid"] = PL, OPL, ray_paths, valid
|
|
619
|
+
return O2,D2,wl,n_func_enviroment,meta_data
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class CollimatedMonochromatic1DRotSym(PlaneSource1D):
|
|
623
|
+
"""
|
|
624
|
+
A class representing a collimated monochromatic light source.
|
|
625
|
+
This class is a subclass of PlaneSource1D and is used to generate rays
|
|
626
|
+
with a specific wavelength and a collimated beam profile."""
|
|
627
|
+
def __init__(self,transform,aperture_radius,wl,flux_func=None,total_power=1.0):
|
|
628
|
+
self.wl = wl
|
|
629
|
+
integrator = Cube([[0,1]])
|
|
630
|
+
super().__init__(transform,aperture_radius,integrator,flux_func,total_power)
|
|
631
|
+
|
|
632
|
+
def sample(self,num_points,method="monte_carlo"):
|
|
633
|
+
return self.integrator.sample(num_points,method)
|
|
634
|
+
|
|
635
|
+
def forward(self,x,n_func_enviroment):
|
|
636
|
+
N = x.shape[0]
|
|
637
|
+
device = x.device
|
|
638
|
+
dtype = x.dtype
|
|
639
|
+
O1 = torch.zeros(N,3,device=device,dtype=dtype)
|
|
640
|
+
#print(self.aperture_radius,torch.sqrt(x[:,0]))
|
|
641
|
+
O1[:,1] = torch.sqrt(x[:,0]) * self.aperture_radius
|
|
642
|
+
O1[:,-1] = 0.0
|
|
643
|
+
|
|
644
|
+
D1 = torch.zeros_like(O1)
|
|
645
|
+
D1[:,-1] = 1.0
|
|
646
|
+
PL,OPL = 0.0,0.0
|
|
647
|
+
|
|
648
|
+
O2 = self.transform.to_global_pos(O1)
|
|
649
|
+
D2 = self.transform.to_global_dir(D1)
|
|
650
|
+
|
|
651
|
+
ray_paths = [O2.detach()]
|
|
652
|
+
valid = self.integrator.in_bounds(x)
|
|
653
|
+
wl = torch.ones(N,device=device,dtype=dtype)*self.wl
|
|
654
|
+
|
|
655
|
+
meta_data = {}
|
|
656
|
+
meta_data["PL"],meta_data["OPL"],meta_data["ray_paths"], meta_data["valid"] = PL, OPL, ray_paths, valid
|
|
657
|
+
return O2,D2,wl,n_func_enviroment,meta_data
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
|