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.
Files changed (38) hide show
  1. diffinytrace/__init__.py +122 -0
  2. diffinytrace/basis_functions/__init__.py +14 -0
  3. diffinytrace/basis_functions/bspline.py +521 -0
  4. diffinytrace/basis_functions/chebyshev.py +3 -0
  5. diffinytrace/basis_functions/legendre.py +77 -0
  6. diffinytrace/basis_functions/zernike.py +235 -0
  7. diffinytrace/config.py +140 -0
  8. diffinytrace/constraints.py +54 -0
  9. diffinytrace/element.py +1660 -0
  10. diffinytrace/export/__init__.py +8 -0
  11. diffinytrace/export/cad.py +253 -0
  12. diffinytrace/gaussian_smoother.py +530 -0
  13. diffinytrace/hat_smoother.py +44 -0
  14. diffinytrace/integrators.py +452 -0
  15. diffinytrace/intersection.py +285 -0
  16. diffinytrace/optimize.py +808 -0
  17. diffinytrace/physical_object.py +150 -0
  18. diffinytrace/plotting/__init__.py +16 -0
  19. diffinytrace/plotting/core.py +92 -0
  20. diffinytrace/plotting/quantity2D.py +188 -0
  21. diffinytrace/plotting/system2D.py +220 -0
  22. diffinytrace/plotting/system3D.py +327 -0
  23. diffinytrace/plotting/wavelength.py +231 -0
  24. diffinytrace/refractive_index.py +101 -0
  25. diffinytrace/render.py +77 -0
  26. diffinytrace/source.py +661 -0
  27. diffinytrace/spectrum.py +79 -0
  28. diffinytrace/surface.py +468 -0
  29. diffinytrace/target_grid.py +399 -0
  30. diffinytrace/transforms.py +472 -0
  31. diffinytrace/utils/__init__.py +7 -0
  32. diffinytrace/utils/autograd.py +116 -0
  33. diffinytrace/utils/irradiance_importer.py +134 -0
  34. diffinytrace-2.1.dist-info/METADATA +26 -0
  35. diffinytrace-2.1.dist-info/RECORD +38 -0
  36. diffinytrace-2.1.dist-info/WHEEL +5 -0
  37. diffinytrace-2.1.dist-info/licenses/LICENSE +21 -0
  38. diffinytrace-2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,452 @@
1
+ # Copyright (c) 2025 Martin Pflaum
2
+ # This file is part of the diffinytrace project, licensed under the MIT License.
3
+
4
+ __all__ = ["Integrator", "Cube","Disc","IntegrationMethod"]
5
+
6
+ import torch
7
+ import numpy as np
8
+ import math
9
+ from scipy.stats import qmc
10
+ from enum import Enum
11
+
12
+ mersenne_twister = np.random.Generator(np.random.MT19937(seed=12345))
13
+
14
+ class IntegrationMethod(Enum):
15
+ SIMPSON = "simpson"
16
+ MIDPOINT = "midpoint"
17
+ MONTE_CARLO = "monte_carlo"
18
+ SOBOL = "sobol"
19
+ SOBOL_POW2 = "sobol_pow2"
20
+
21
+ def check_2val(num_points):
22
+ num_points = np.array(num_points)
23
+ if len(num_points.shape)>0:
24
+ return num_points[0]*num_points[1]
25
+ return num_points
26
+
27
+ class Integrator():
28
+ def __init__(self):
29
+ pass
30
+
31
+ def sample(self, num_points: int | list[int], method: IntegrationMethod) -> tuple[torch.Tensor, torch.Tensor]:
32
+ """
33
+ Sample points and weights using the specified method.
34
+ Args:
35
+ num_points (int or list): Number of points in each dimension.
36
+ method (str): The integration method to use. Options are 'simpson', 'midpoint', 'monte_carlo', 'sobol', and 'sobol_pow2'.
37
+ Returns:
38
+ tuple: A tuple containing the sampled points and their corresponding weights.
39
+ """
40
+ raise NotImplementedError("sample() not implemented")
41
+
42
+ def in_bounds(self, x: torch.Tensor) -> torch.Tensor:
43
+ raise NotImplementedError("in_bounds() not implemented")
44
+
45
+ def get_volume(self) -> float:
46
+ raise NotImplementedError("get_volume() not implemented")
47
+
48
+
49
+ class Cube(Integrator):
50
+ """
51
+ Integrator for a multi-dimensional cube (hyperrectangle).
52
+
53
+ Args:
54
+ bounds (array-like): The bounds for each dimension of the cube. Should be a list or array of shape (n_dim, 2),
55
+ where each row specifies [lower_bound, upper_bound] for a dimension.
56
+
57
+ Example:
58
+ >>> cube = dit.integrators.Cube([[0, 1], [0, 1]])
59
+ >>> points, weights = cube.sample([10, 10], method=IntegrationMethod.MIDPOINT)
60
+ >>> volume = cube.get_volume()
61
+ >>> all_in_bounds = cube.in_bounds(points)
62
+ >>> print("Sampled points:", points)
63
+ >>> print("Integration weights:", weights)
64
+ >>> print("Cube volume:", volume)
65
+ >>> print("All points in bounds:", all_in_bounds)
66
+ """
67
+
68
+ def __init__(self,bounds):
69
+ super().__init__()
70
+ bounds = np.array(bounds)
71
+ if len(bounds.shape)==1:
72
+ bounds = np.array([bounds])
73
+
74
+ self.bounds = torch.tensor(bounds)
75
+ if len(self.bounds.shape)!=2:
76
+ raise ValueError("len(self.bounds.shape)==2 must hold true!")
77
+
78
+ def sample(self, num_points: int | list[int], method: IntegrationMethod = IntegrationMethod.MIDPOINT) -> tuple[torch.Tensor, torch.Tensor]:
79
+ r"""
80
+ Sample points and weights using the specified method.
81
+
82
+ Args:
83
+ num_points (int or list): Number of points in each dimension.
84
+ method (str): The integration method to use. Options are 'simpson', 'midpoint', 'monte_carlo', 'sobol', and 'sobol_pow2'.
85
+
86
+ Returns:
87
+ tuple: A tuple containing the sampled points and their corresponding weights.
88
+ """
89
+ if not isinstance(method, str):
90
+ method = str(method.value)
91
+
92
+
93
+ if method == 'simpson':
94
+ return self._sample_simpson(num_points)
95
+ elif method == 'midpoint':
96
+ return self._sample_midpoint(num_points)
97
+ elif method == 'monte_carlo':
98
+ return self._sample_monte_carlo(num_points)
99
+ elif method == 'sobol':
100
+ return self._sample_sobol(num_points,False)
101
+ elif method == 'sobol_pow2':
102
+ return self._sample_sobol(num_points,True)
103
+ else:
104
+ raise ValueError(f"Unknown integration method: {method}")
105
+
106
+ def in_bounds(self, x:torch.Tensor) -> torch.Tensor:
107
+ out = torch.ones(x.shape[0],device=x.device,dtype=torch.bool).float()
108
+ for k in range(self.bounds.shape[0]):
109
+ out = out*((self.bounds[k,0]<=x[:,k]).float())*((x[:,k]<=self.bounds[k,1]).float())
110
+ out = out==1.0
111
+ return out
112
+
113
+ def get_volume(self) -> float:
114
+ """
115
+ Returns:
116
+ float: Volume of the Cube.
117
+ """
118
+
119
+ volume = torch.prod(self.bounds[:,1]-self.bounds[:,0])
120
+ return volume
121
+
122
+ def _sample_midpoint(self, num_points):
123
+ """
124
+ Sample points and weights using the midpoint rule.
125
+
126
+ Args:
127
+ num_points (list or array): Number of points in each dimension.
128
+
129
+ Returns:
130
+ sampled_points (torch.Tensor): Tensor of sampled points.
131
+ weights (torch.Tensor): Tensor of weights associated with each point.
132
+ """
133
+ # Ensure num_points matches the number of dimensions in bounds
134
+ num_points = np.array(num_points)
135
+ if len(num_points) != self.bounds.shape[0]:
136
+ raise ValueError("Using midpoint sampling expected num_points to match the number of dimensions.")
137
+
138
+ midpoints = []
139
+
140
+ # Calculate the midpoints for each dimension
141
+ for i in range(self.bounds.shape[0]):
142
+ lower_bound, upper_bound = self.bounds[i]
143
+ # Calculate the size of each interval (dx) for the dimension
144
+ dx = (upper_bound - lower_bound) / num_points[i]
145
+ # Compute the midpoints in this dimension
146
+ points = torch.linspace(lower_bound + dx / 2.0, upper_bound - dx / 2.0, num_points[i])
147
+ midpoints.append(points)
148
+
149
+ # Create a meshgrid of midpoints for all dimensions
150
+ grid = torch.meshgrid(*midpoints, indexing='ij')
151
+ sampled_points = torch.stack(grid, dim=-1).reshape(-1, self.bounds.shape[0])
152
+
153
+ # Compute the weights based on the volume of each subregion
154
+ weights = torch.ones(sampled_points.shape[0], dtype=torch.float32)
155
+
156
+ for i in range(self.bounds.shape[0]):
157
+ lower_bound, upper_bound = self.bounds[i]
158
+ dx = (upper_bound - lower_bound) / num_points[i]
159
+ # Each dimension contributes its own weight
160
+ weights *= dx # Multiply the weights by the width of the intervals
161
+
162
+ return sampled_points, weights
163
+
164
+
165
+ def _sample_simpson(self, num_points):
166
+ # Ensure num_points matches the expected number of dimensions
167
+ num_points = np.array(num_points)
168
+ if len(num_points) != self.bounds.shape[0]:
169
+ raise ValueError("Using Simpson's rule expected num_points to have the same number of entries as dimensions.")
170
+ for elem in num_points:
171
+ if elem % 2 == 0:
172
+ raise ValueError("Simpson's rule only takes an odd number of points!")
173
+
174
+ # Create sample points and weights
175
+ sample_points = []
176
+ weights = []
177
+
178
+ for i in range(self.bounds.shape[0]):
179
+ lower_bound, upper_bound = self.bounds[i]
180
+ dx = (upper_bound - lower_bound) / (num_points[i] - 1)
181
+ x = torch.linspace(lower_bound, upper_bound, num_points[i]) # Include endpoints
182
+ sample_points.append(x)
183
+
184
+ # Weights for Simpson's rule
185
+ w = torch.ones(num_points[i]) # Initialize weights
186
+ w[1:-1:2] *= 4 # Odd indices
187
+ w[2:-1:2] *= 2 # Even indices
188
+ weights.append(w * dx / 3) # Multiply by dx/3
189
+
190
+ # Create a meshgrid of sample points
191
+ grid = torch.meshgrid(*sample_points)
192
+ sampled_points = torch.stack(grid, dim=-1).reshape(-1, self.bounds.shape[0])
193
+
194
+ # Total weight for each point
195
+ total_weights = weights[0]
196
+ for w in weights[1:]:
197
+ total_weights = torch.ger(total_weights, w).reshape(-1) # Use outer product and flatten
198
+
199
+
200
+ points, weights = sampled_points, total_weights.reshape(-1) # Ensure weights are a flat array
201
+ points = points.to(torch.get_default_dtype())
202
+ weights = weights.to(torch.get_default_dtype())
203
+ return points, weights
204
+
205
+ def _sample_trapezoidal(self, num_points):
206
+ raise RuntimeError("DO NOT USE THIS method. It's more or less the same as midpoint rule....")
207
+ num_points = np.array(num_points)
208
+ if len(num_points) != self.bounds.shape[0]:
209
+ raise ValueError("Using trapezoidal sampling expected num_points to match the number of dimensions.")
210
+
211
+ sample_points = []
212
+ weights = []
213
+
214
+ for i in range(self.bounds.shape[0]):
215
+ lower_bound, upper_bound = self.bounds[i]
216
+ dx = (upper_bound - lower_bound) / (num_points[i] - 1)
217
+ x = torch.linspace(lower_bound, upper_bound, num_points[i])
218
+ sample_points.append(x)
219
+
220
+ # Weights for the trapezoidal rule
221
+ w = torch.ones(num_points[i])
222
+ w[0] /= 2 # First point weight
223
+ w[-1] /= 2 # Last point weight
224
+ weights.append(w * dx) # Multiply by dx to get the correct area contribution
225
+
226
+ # Create a meshgrid of sample points
227
+ grid = torch.meshgrid(*sample_points, indexing='ij')
228
+ sampled_points = torch.stack(grid, dim=-1).reshape(-1, self.bounds.shape[0])
229
+
230
+ # Calculate total weights
231
+ total_weights = weights[0]
232
+ for w in weights[1:]:
233
+ total_weights = total_weights.unsqueeze(-1) * w.unsqueeze(0) # Use broadcasting to multiply correctly
234
+
235
+ points, weights = sampled_points, total_weights.reshape(-1)
236
+ points = points.to(torch.get_default_dtype())
237
+ weights = weights.to(torch.get_default_dtype())
238
+ return points, weights
239
+
240
+ def _sample_monte_carlo(self, num_points):
241
+ num_points = check_2val(num_points)
242
+ if len(num_points.shape)!=0:
243
+ raise ValueError("num_points for monte_carlo needs to be a scalar")
244
+ """Sample points uniformly using the Monte Carlo method."""
245
+ points = torch.empty((num_points, self.bounds.shape[0]))
246
+
247
+ for i in range(self.bounds.shape[0]):
248
+ # Generate random points uniformly within the bounds for each dimension
249
+ rand_points = mersenne_twister.uniform(0,1,size=num_points)
250
+ rand_points = torch.tensor(rand_points, dtype=torch.float32)
251
+ points[:, i] = rand_points * (self.bounds[i, 1] - self.bounds[i, 0]) + self.bounds[i, 0]
252
+
253
+ # Calculate the volume of the cube
254
+ volume = torch.prod(self.bounds[:,1]-self.bounds[:,0])
255
+ constant_multi = volume/float(num_points)
256
+ weights = torch.full((int(num_points),),fill_value=constant_multi)
257
+
258
+ points = points.to(torch.get_default_dtype())
259
+ weights = weights.to(torch.get_default_dtype())
260
+ return points, weights
261
+
262
+ def _sample_sobol(self, num_points,is_pow2):
263
+ num_points = check_2val(num_points)
264
+ if len(num_points.shape)!=0:
265
+ raise ValueError("num_points for sobol needs to be a scalar")
266
+ """Sample points using the Sobol sequence method."""
267
+ points = None
268
+ num_points_log2 = np.log2(num_points)
269
+ if round(num_points_log2,0) != num_points_log2:
270
+ if is_pow2:
271
+ raise RuntimeError("round(num_points_log2,0) != num_points_log2"+ f",num_points_log2=={num_points_log2}")
272
+
273
+ sobol = torch.quasirandom.SobolEngine(dimension=self.bounds.shape[0], scramble=True)
274
+ points = sobol.draw(num_points,dtype=torch.float32)
275
+ else:
276
+ sampler = qmc.Sobol(d=self.bounds.shape[0], scramble=True)
277
+ points = sampler.random_base2(m=int(num_points_log2))
278
+ points = torch.tensor(points)
279
+ # Scale points according to the cube bounds
280
+ scaled_points = points * (self.bounds[:, 1] - self.bounds[:, 0]) + self.bounds[:, 0]
281
+
282
+ # Calculate the volume of the cube
283
+ volume = torch.prod(self.bounds[:,1]-self.bounds[:,0])
284
+ constant_multi = volume/float(num_points)
285
+ weights = torch.full((int(num_points),),fill_value=constant_multi)
286
+
287
+ points = points.to(torch.get_default_dtype())
288
+ weights = weights.to(torch.get_default_dtype())
289
+ return scaled_points, weights
290
+
291
+
292
+
293
+ class Disc(Integrator):
294
+ """
295
+ Integrator for a 2D disc (circle).
296
+
297
+ Args:
298
+ radius (float): The radius of the disc.
299
+
300
+ Example:
301
+ >>> disc = dit.integrators.Disc(1.0)
302
+ >>> points, weights = disc.sample(2**4, method="sobol_pow2")
303
+ >>> volume = disc.get_volume()
304
+ >>> all_in_bounds = disc.in_bounds(points)
305
+ >>> print("Sampled points:", points)
306
+ >>> print("Integration weights:", weights)
307
+ >>> print("Disc area:", volume)
308
+ >>> print("All points in bounds:", all_in_bounds)
309
+ """
310
+
311
+ def __init__(self,radius):
312
+ self.radius = float(radius)
313
+
314
+ def sample(self, num_points: int | list[int], method: IntegrationMethod = IntegrationMethod.SOBOL) -> tuple[torch.Tensor, torch.Tensor]:
315
+ """
316
+ Sample points and weights using the specified method.
317
+
318
+ Args:
319
+ num_points (int or list): Number of points in each dimension.
320
+ method (str): The integration method to use. Options are 'simpson', 'midpoint', 'monte_carlo', 'sobol', and 'sobol_pow2'.
321
+ Returns:
322
+ tuple: A tuple containing the sampled points and their corresponding weights.
323
+ """
324
+ if not isinstance(method, str):
325
+ method = str(method.value)
326
+
327
+ if method == 'simpson':
328
+ return self._sample_simpson(num_points)
329
+ elif method == 'monte_carlo':
330
+ return self._sample_monte_carlo(num_points)
331
+ elif method == 'sobol':
332
+ return self._sample_sobol(num_points,False)
333
+ elif method == 'sobol_pow2':
334
+ return self._sample_sobol(num_points,True)
335
+ elif method == 'midpoint':
336
+ return self._sample_midpoint(num_points)
337
+ else:
338
+ raise ValueError(f"Unknown integration method: {method}")
339
+
340
+ def _sample_midpoint(self, num_points):
341
+ #raise RuntimeError("midpoint rule not implemted for disc")
342
+ num_points = np.array(num_points)
343
+
344
+ midpoints = []
345
+
346
+ # Calculate the midpoints for each dimension
347
+ lower_bound, upper_bound = [-self.radius,self.radius]
348
+ for i in range(2):
349
+ # Calculate the size of each interval (dx) for the dimension
350
+ dx = (upper_bound - lower_bound) / num_points[i]
351
+ # Compute the midpoints in this dimension
352
+ points = torch.linspace(lower_bound + dx / 2.0, upper_bound - dx / 2.0, num_points[i])
353
+ midpoints.append(points)
354
+
355
+ # Create a meshgrid of midpoints for all dimensions
356
+ grid = torch.meshgrid(*midpoints, indexing='ij')
357
+ sampled_points = torch.stack(grid, dim=-1).reshape(-1, 2)
358
+
359
+ # Compute the weights based on the volume of each subregion
360
+ weights = torch.ones(sampled_points.shape[0], dtype=torch.float32)
361
+
362
+ lower_bound, upper_bound = [-self.radius,self.radius]
363
+ for i in range(2):
364
+ dx = (upper_bound - lower_bound) / num_points[i]
365
+ # Each dimension contributes its own weight
366
+ weights *= dx # Multiply the weights by the width of the intervals
367
+ in_bounds = self.in_bounds(sampled_points)
368
+ sampled_points = sampled_points[in_bounds]
369
+ weights = weights[in_bounds]
370
+ return sampled_points, weights
371
+
372
+ def _sample_simpson(self,num_points):
373
+ raise RuntimeError("simpson's rule not implemted for disc")
374
+ if len(num_points) != self.bounds.shape[0]:
375
+ raise ValueError("using simpson rule expected num_points to have the same number of entries as dimensions (4,3,2)")
376
+
377
+ def _sample_weights_from_unif(self,points):
378
+ num_points = points.shape[0]
379
+ volume = (torch.pi*(self.radius**2.0))
380
+ constant_multi = volume/float(num_points)
381
+ weights = torch.full((int(num_points),),fill_value=constant_multi)
382
+ weights = weights.to(torch.get_default_dtype())
383
+ return weights
384
+
385
+ def _sample_points_from_unif(self,points):
386
+ # Scale points to the disc
387
+ num_points = points.shape[0]
388
+ r_points = self.radius * torch.sqrt(points[:, 0]) # Use sqrt to ensure uniform distribution
389
+ theta = 2 * torch.pi * points[:, 1]
390
+
391
+ # Convert polar to Cartesian coordinates
392
+ x = r_points * torch.cos(theta)
393
+ y = r_points * torch.sin(theta)
394
+
395
+ # Stack x and y to get the final points
396
+ points = torch.stack((x, y), dim=1)
397
+ points = points.to(torch.get_default_dtype())
398
+ return points
399
+
400
+
401
+ def _sample_monte_carlo(self, num_points):
402
+ num_points = check_2val(num_points)
403
+ #points = torch.rand(num_points,2)
404
+ rand_points = mersenne_twister.uniform(0,1,size=num_points*2)
405
+ rand_points = torch.tensor(rand_points, dtype=torch.float32)
406
+ rand_points = rand_points.reshape(num_points,2)
407
+
408
+ out_points = self._sample_points_from_unif(rand_points)
409
+ out_weights = self._sample_weights_from_unif(rand_points)
410
+ return out_points,out_weights
411
+
412
+
413
+ def _sample_sobol(self, num_points,is_pow2):
414
+ num_points = check_2val(num_points)
415
+
416
+ points = None
417
+ num_points_log2 = np.log2(num_points)
418
+ if round(num_points_log2,0) != num_points_log2:
419
+ if is_pow2:
420
+ raise RuntimeError("round(num_points_log2,0) != num_points_log2"+ f",num_points_log2=={num_points_log2}")
421
+ sobol = torch.quasirandom.SobolEngine(dimension=2, scramble=True)
422
+ points = sobol.draw(num_points,dtype=torch.float32)
423
+ else:
424
+ sampler = qmc.Sobol(d=2, scramble=True)
425
+ points = sampler.random_base2(m=int(num_points_log2))
426
+ points = torch.tensor(points)
427
+
428
+ out_points = self._sample_points_from_unif(points)
429
+ out_weights = self._sample_weights_from_unif(points)
430
+ return out_points,out_weights
431
+
432
+ def in_bounds(self,x):
433
+ """Check if points are within the disc.
434
+
435
+ Args:
436
+ x (torch.Tensor): Points to check.
437
+
438
+ Returns:
439
+ torch.Tensor: Boolean tensor indicating if points are within the disc.
440
+ """
441
+ device = x.device
442
+ dtype = x.dtype
443
+ return torch.linalg.norm(x,dim=1)<self.radius
444
+
445
+ def get_volume(self):
446
+ """Calculate the volume of the disc.
447
+
448
+ Returns:
449
+ float: Volume of the disc.
450
+ """
451
+
452
+ return math.pi*self.radius**2.