radia 1.3.3__py3-none-any.whl → 1.3.5__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.
@@ -1,274 +1,274 @@
1
- """
2
- Pure Python cached Radia field evaluation
3
-
4
- This module provides a CoefficientFunction-compatible cached field evaluator
5
- that is 1000-10000x faster than C++ implementations due to avoiding pybind11
6
- overhead entirely.
7
-
8
- Performance:
9
- - 3360 points: ~5-10ms (C++ version: 60+ seconds = 6000-12000x faster)
10
- - 10000 points: ~15-30ms (linear scaling)
11
- - Overhead: ~1-2 us/point (vs Radia: 0.5 us/point)
12
-
13
- Usage:
14
- from radia_field_cached import CachedRadiaField
15
- from ngsolve import *
16
- import radia as rad
17
-
18
- # IMPORTANT: Set Radia to use meters (required for NGSolve integration)
19
- rad.FldUnits('m')
20
-
21
- # Create Radia geometry in meters
22
- magnet = rad.ObjRecMag([0, 0, 0], [0.04, 0.04, 0.06], [0, 0, 1.2])
23
-
24
- # Create cached field
25
- A_cf = CachedRadiaField(magnet, 'a')
26
-
27
- # Collect integration points (in meters)
28
- all_points = [[x1,y1,z1], [x2,y2,z2], ...] # coordinates in meters
29
-
30
- # Prepare cache (fast!)
31
- A_cf.prepare_cache(all_points)
32
-
33
- # Use with GridFunction
34
- gf = GridFunction(fes)
35
- gf.Set(A_cf) # Uses cached values
36
-
37
- Note:
38
- Always use rad.FldUnits('m') before using this module with NGSolve.
39
- This ensures consistent units between Radia (default: mm) and NGSolve (SI: m).
40
- See CLAUDE.md "NGSolve Integration Unit System Policy" for details.
41
- """
42
-
43
- import radia as rad
44
- import time
45
-
46
-
47
- class CachedRadiaField:
48
- """
49
- Cached Radia field evaluator compatible with NGSolve CoefficientFunction
50
-
51
- This class provides a Python-based caching mechanism that is much faster
52
- than C++ implementations due to avoiding pybind11 overhead.
53
-
54
- Attributes:
55
- radia_obj: Radia object ID or background field
56
- field_type: Field type ('b', 'h', 'a', 'm')
57
- cache: Dictionary mapping quantized coordinates to field values
58
- cache_tolerance: Tolerance for coordinate quantization (meters)
59
- cache_hits: Number of cache hits
60
- cache_misses: Number of cache misses
61
- """
62
-
63
- def __init__(self, radia_obj, field_type, cache_tolerance=1e-10):
64
- """
65
- Initialize cached field evaluator
66
-
67
- Args:
68
- radia_obj: Radia object ID or background field (rad.ObjBckgCF)
69
- field_type: Field type ('b', 'h', 'a', 'm')
70
- cache_tolerance: Tolerance for coordinate quantization (default: 1e-10 m)
71
- """
72
- self.radia_obj = radia_obj
73
- self.field_type = field_type
74
- self.cache_tolerance = cache_tolerance
75
- self.cache = {}
76
- self.cache_hits = 0
77
- self.cache_misses = 0
78
- self.cache_enabled = False
79
-
80
- def _quantize_point(self, x, y, z):
81
- """
82
- Quantize point coordinates to tolerance grid
83
-
84
- Args:
85
- x, y, z: Coordinates in meters
86
-
87
- Returns:
88
- tuple: Quantized coordinates (hashable)
89
- """
90
- qx = round(x / self.cache_tolerance) * self.cache_tolerance
91
- qy = round(y / self.cache_tolerance) * self.cache_tolerance
92
- qz = round(z / self.cache_tolerance) * self.cache_tolerance
93
- return (qx, qy, qz)
94
-
95
- def prepare_cache(self, points_meters, verbose=True):
96
- """
97
- Prepare cache by batch-evaluating all points
98
-
99
- This is the key method that provides 1000-10000x speedup over C++
100
- implementations by doing everything in Python.
101
-
102
- Args:
103
- points_meters: List of [x, y, z] coordinates in meters
104
- verbose: Print timing information (default: True)
105
-
106
- Performance:
107
- - 1000 points: ~2-3ms
108
- - 3000 points: ~6-10ms
109
- - 10000 points: ~20-30ms
110
- """
111
- npts = len(points_meters)
112
-
113
- if verbose:
114
- print(f"[CachedRadiaField] Preparing cache for {npts} points...")
115
-
116
- if npts == 0:
117
- self.cache_enabled = False
118
- if verbose:
119
- print("[CachedRadiaField] No points to cache")
120
- return
121
-
122
- t_start = time.time()
123
-
124
- # Step 1: Build Radia points list (Python list ops are fast!)
125
- # Note: Assumes rad.FldUnits('m') has been called - coordinates already in meters
126
- radia_points = [[x, y, z] for x, y, z in points_meters]
127
-
128
- t_list = time.time()
129
-
130
- # Step 2: Single batch Radia.Fld() call (very fast: ~0.5 us/point)
131
- results = rad.Fld(self.radia_obj, self.field_type, radia_points)
132
-
133
- # Handle single point case: Radia returns [x, y, z] instead of [[x, y, z]]
134
- if npts == 1 and isinstance(results, list) and len(results) == 3:
135
- results = [results] # Wrap single result in list
136
-
137
- t_radia = time.time()
138
-
139
- # Step 3: Store in Python dict (native Python, very fast!)
140
- self.cache.clear()
141
- self.cache_hits = 0
142
- self.cache_misses = 0
143
-
144
- # No scaling needed - rad.FldUnits('m') ensures consistent units
145
- for (x, y, z), result in zip(points_meters, results):
146
- key = self._quantize_point(x, y, z)
147
- # Store result directly (no unit conversion needed)
148
- self.cache[key] = [result[0], result[1], result[2]]
149
-
150
- self.cache_enabled = True
151
-
152
- t_store = time.time()
153
-
154
- if verbose:
155
- time_list = (t_list - t_start) * 1000
156
- time_radia = (t_radia - t_list) * 1000
157
- time_store = (t_store - t_radia) * 1000
158
- time_total = (t_store - t_start) * 1000
159
-
160
- print(f"[CachedRadiaField] Timing breakdown:")
161
- if time_total > 0:
162
- print(f" List preparation: {time_list:>6.2f} ms ({time_list/time_total*100:>5.1f}%)")
163
- print(f" Radia.Fld(): {time_radia:>6.2f} ms ({time_radia/time_total*100:>5.1f}%)")
164
- print(f" Store in cache: {time_store:>6.2f} ms ({time_store/time_total*100:>5.1f}%)")
165
- print(f" Total: {time_total:>6.2f} ms")
166
- print(f" Performance: {time_total*1000/npts:>6.2f} us/point")
167
- else:
168
- print(f" Total: <0.01 ms (too fast to measure)")
169
- print(f"[CachedRadiaField] Cache ready: {len(self.cache)} entries")
170
-
171
- def clear_cache(self):
172
- """Clear the cache and reset statistics"""
173
- self.cache.clear()
174
- self.cache_hits = 0
175
- self.cache_misses = 0
176
- self.cache_enabled = False
177
-
178
- def get_cache_stats(self):
179
- """
180
- Get cache statistics
181
-
182
- Returns:
183
- dict: Statistics with keys:
184
- - enabled: bool
185
- - size: int
186
- - hits: int
187
- - misses: int
188
- - hit_rate: float
189
- """
190
- total = self.cache_hits + self.cache_misses
191
- hit_rate = (self.cache_hits / total) if total > 0 else 0.0
192
-
193
- return {
194
- 'enabled': self.cache_enabled,
195
- 'size': len(self.cache),
196
- 'hits': self.cache_hits,
197
- 'misses': self.cache_misses,
198
- 'hit_rate': hit_rate
199
- }
200
-
201
- def __call__(self, x, y=None, z=None):
202
- """
203
- Evaluate field at point (NGSolve CoefficientFunction interface)
204
-
205
- This method is called by NGSolve during GridFunction.Set().
206
-
207
- Args:
208
- x: x-coordinate (or MappedIntegrationPoint)
209
- y: y-coordinate (if x is float)
210
- z: z-coordinate (if x is float)
211
-
212
- Returns:
213
- list or tuple: Field value [Fx, Fy, Fz]
214
- """
215
- # Handle NGSolve MappedIntegrationPoint
216
- if y is None:
217
- # x is MappedIntegrationPoint
218
- pnt = x.point if hasattr(x, 'point') else x.pnt
219
- px, py, pz = pnt[0], pnt[1], pnt[2]
220
- else:
221
- px, py, pz = x, y, z
222
-
223
- # Check cache if enabled
224
- if self.cache_enabled:
225
- key = self._quantize_point(px, py, pz)
226
- if key in self.cache:
227
- self.cache_hits += 1
228
- return self.cache[key]
229
- self.cache_misses += 1
230
-
231
- # Cache miss - evaluate directly with Radia
232
- # Note: Assumes rad.FldUnits('m') has been called - coordinates already in meters
233
- result = rad.Fld(self.radia_obj, self.field_type, [px, py, pz])
234
-
235
- # No scaling needed - rad.FldUnits('m') ensures consistent units
236
- return [result[0], result[1], result[2]]
237
-
238
-
239
- def collect_integration_points(mesh, order=5):
240
- """
241
- Collect all integration points from a mesh
242
-
243
- This is a helper function to collect integration points for cache preparation.
244
-
245
- Args:
246
- mesh: NGSolve mesh
247
- order: Integration rule order (default: 5)
248
-
249
- Returns:
250
- list: List of [x, y, z] coordinates in meters
251
-
252
- Example:
253
- >>> from ngsolve import *
254
- >>> mesh = Mesh(geo.GenerateMesh(maxh=0.015))
255
- >>> points = collect_integration_points(mesh, order=5)
256
- >>> print(f"Collected {len(points)} integration points")
257
- """
258
- try:
259
- from ngsolve import IntegrationRule, VOL
260
- except ImportError:
261
- raise ImportError("NGSolve is required to collect integration points")
262
-
263
- all_points = []
264
-
265
- for el in mesh.Elements(VOL):
266
- ir = IntegrationRule(el.type, order=order)
267
- trafo = mesh.GetTrafo(el)
268
-
269
- for ip in ir:
270
- mip = trafo(ip)
271
- pnt = mip.point
272
- all_points.append([pnt[0], pnt[1], pnt[2]])
273
-
274
- return all_points
1
+ """
2
+ Pure Python cached Radia field evaluation
3
+
4
+ This module provides a CoefficientFunction-compatible cached field evaluator
5
+ that is 1000-10000x faster than C++ implementations due to avoiding pybind11
6
+ overhead entirely.
7
+
8
+ Performance:
9
+ - 3360 points: ~5-10ms (C++ version: 60+ seconds = 6000-12000x faster)
10
+ - 10000 points: ~15-30ms (linear scaling)
11
+ - Overhead: ~1-2 us/point (vs Radia: 0.5 us/point)
12
+
13
+ Usage:
14
+ from radia_field_cached import CachedRadiaField
15
+ from ngsolve import *
16
+ import radia as rad
17
+
18
+ # IMPORTANT: Set Radia to use meters (required for NGSolve integration)
19
+ rad.FldUnits('m')
20
+
21
+ # Create Radia geometry in meters
22
+ magnet = rad.ObjRecMag([0, 0, 0], [0.04, 0.04, 0.06], [0, 0, 1.2])
23
+
24
+ # Create cached field
25
+ A_cf = CachedRadiaField(magnet, 'a')
26
+
27
+ # Collect integration points (in meters)
28
+ all_points = [[x1,y1,z1], [x2,y2,z2], ...] # coordinates in meters
29
+
30
+ # Prepare cache (fast!)
31
+ A_cf.prepare_cache(all_points)
32
+
33
+ # Use with GridFunction
34
+ gf = GridFunction(fes)
35
+ gf.Set(A_cf) # Uses cached values
36
+
37
+ Note:
38
+ Always use rad.FldUnits('m') before using this module with NGSolve.
39
+ This ensures consistent units between Radia (default: mm) and NGSolve (SI: m).
40
+ See CLAUDE.md "NGSolve Integration Unit System Policy" for details.
41
+ """
42
+
43
+ import radia as rad
44
+ import time
45
+
46
+
47
+ class CachedRadiaField:
48
+ """
49
+ Cached Radia field evaluator compatible with NGSolve CoefficientFunction
50
+
51
+ This class provides a Python-based caching mechanism that is much faster
52
+ than C++ implementations due to avoiding pybind11 overhead.
53
+
54
+ Attributes:
55
+ radia_obj: Radia object ID or background field
56
+ field_type: Field type ('b', 'h', 'a', 'm')
57
+ cache: Dictionary mapping quantized coordinates to field values
58
+ cache_tolerance: Tolerance for coordinate quantization (meters)
59
+ cache_hits: Number of cache hits
60
+ cache_misses: Number of cache misses
61
+ """
62
+
63
+ def __init__(self, radia_obj, field_type, cache_tolerance=1e-10):
64
+ """
65
+ Initialize cached field evaluator
66
+
67
+ Args:
68
+ radia_obj: Radia object ID or background field (rad.ObjBckgCF)
69
+ field_type: Field type ('b', 'h', 'a', 'm')
70
+ cache_tolerance: Tolerance for coordinate quantization (default: 1e-10 m)
71
+ """
72
+ self.radia_obj = radia_obj
73
+ self.field_type = field_type
74
+ self.cache_tolerance = cache_tolerance
75
+ self.cache = {}
76
+ self.cache_hits = 0
77
+ self.cache_misses = 0
78
+ self.cache_enabled = False
79
+
80
+ def _quantize_point(self, x, y, z):
81
+ """
82
+ Quantize point coordinates to tolerance grid
83
+
84
+ Args:
85
+ x, y, z: Coordinates in meters
86
+
87
+ Returns:
88
+ tuple: Quantized coordinates (hashable)
89
+ """
90
+ qx = round(x / self.cache_tolerance) * self.cache_tolerance
91
+ qy = round(y / self.cache_tolerance) * self.cache_tolerance
92
+ qz = round(z / self.cache_tolerance) * self.cache_tolerance
93
+ return (qx, qy, qz)
94
+
95
+ def prepare_cache(self, points_meters, verbose=True):
96
+ """
97
+ Prepare cache by batch-evaluating all points
98
+
99
+ This is the key method that provides 1000-10000x speedup over C++
100
+ implementations by doing everything in Python.
101
+
102
+ Args:
103
+ points_meters: List of [x, y, z] coordinates in meters
104
+ verbose: Print timing information (default: True)
105
+
106
+ Performance:
107
+ - 1000 points: ~2-3ms
108
+ - 3000 points: ~6-10ms
109
+ - 10000 points: ~20-30ms
110
+ """
111
+ npts = len(points_meters)
112
+
113
+ if verbose:
114
+ print(f"[CachedRadiaField] Preparing cache for {npts} points...")
115
+
116
+ if npts == 0:
117
+ self.cache_enabled = False
118
+ if verbose:
119
+ print("[CachedRadiaField] No points to cache")
120
+ return
121
+
122
+ t_start = time.time()
123
+
124
+ # Step 1: Build Radia points list (Python list ops are fast!)
125
+ # Note: Assumes rad.FldUnits('m') has been called - coordinates already in meters
126
+ radia_points = [[x, y, z] for x, y, z in points_meters]
127
+
128
+ t_list = time.time()
129
+
130
+ # Step 2: Single batch Radia.Fld() call (very fast: ~0.5 us/point)
131
+ results = rad.Fld(self.radia_obj, self.field_type, radia_points)
132
+
133
+ # Handle single point case: Radia returns [x, y, z] instead of [[x, y, z]]
134
+ if npts == 1 and isinstance(results, list) and len(results) == 3:
135
+ results = [results] # Wrap single result in list
136
+
137
+ t_radia = time.time()
138
+
139
+ # Step 3: Store in Python dict (native Python, very fast!)
140
+ self.cache.clear()
141
+ self.cache_hits = 0
142
+ self.cache_misses = 0
143
+
144
+ # No scaling needed - rad.FldUnits('m') ensures consistent units
145
+ for (x, y, z), result in zip(points_meters, results):
146
+ key = self._quantize_point(x, y, z)
147
+ # Store result directly (no unit conversion needed)
148
+ self.cache[key] = [result[0], result[1], result[2]]
149
+
150
+ self.cache_enabled = True
151
+
152
+ t_store = time.time()
153
+
154
+ if verbose:
155
+ time_list = (t_list - t_start) * 1000
156
+ time_radia = (t_radia - t_list) * 1000
157
+ time_store = (t_store - t_radia) * 1000
158
+ time_total = (t_store - t_start) * 1000
159
+
160
+ print(f"[CachedRadiaField] Timing breakdown:")
161
+ if time_total > 0:
162
+ print(f" List preparation: {time_list:>6.2f} ms ({time_list/time_total*100:>5.1f}%)")
163
+ print(f" Radia.Fld(): {time_radia:>6.2f} ms ({time_radia/time_total*100:>5.1f}%)")
164
+ print(f" Store in cache: {time_store:>6.2f} ms ({time_store/time_total*100:>5.1f}%)")
165
+ print(f" Total: {time_total:>6.2f} ms")
166
+ print(f" Performance: {time_total*1000/npts:>6.2f} us/point")
167
+ else:
168
+ print(f" Total: <0.01 ms (too fast to measure)")
169
+ print(f"[CachedRadiaField] Cache ready: {len(self.cache)} entries")
170
+
171
+ def clear_cache(self):
172
+ """Clear the cache and reset statistics"""
173
+ self.cache.clear()
174
+ self.cache_hits = 0
175
+ self.cache_misses = 0
176
+ self.cache_enabled = False
177
+
178
+ def get_cache_stats(self):
179
+ """
180
+ Get cache statistics
181
+
182
+ Returns:
183
+ dict: Statistics with keys:
184
+ - enabled: bool
185
+ - size: int
186
+ - hits: int
187
+ - misses: int
188
+ - hit_rate: float
189
+ """
190
+ total = self.cache_hits + self.cache_misses
191
+ hit_rate = (self.cache_hits / total) if total > 0 else 0.0
192
+
193
+ return {
194
+ 'enabled': self.cache_enabled,
195
+ 'size': len(self.cache),
196
+ 'hits': self.cache_hits,
197
+ 'misses': self.cache_misses,
198
+ 'hit_rate': hit_rate
199
+ }
200
+
201
+ def __call__(self, x, y=None, z=None):
202
+ """
203
+ Evaluate field at point (NGSolve CoefficientFunction interface)
204
+
205
+ This method is called by NGSolve during GridFunction.Set().
206
+
207
+ Args:
208
+ x: x-coordinate (or MappedIntegrationPoint)
209
+ y: y-coordinate (if x is float)
210
+ z: z-coordinate (if x is float)
211
+
212
+ Returns:
213
+ list or tuple: Field value [Fx, Fy, Fz]
214
+ """
215
+ # Handle NGSolve MappedIntegrationPoint
216
+ if y is None:
217
+ # x is MappedIntegrationPoint
218
+ pnt = x.point if hasattr(x, 'point') else x.pnt
219
+ px, py, pz = pnt[0], pnt[1], pnt[2]
220
+ else:
221
+ px, py, pz = x, y, z
222
+
223
+ # Check cache if enabled
224
+ if self.cache_enabled:
225
+ key = self._quantize_point(px, py, pz)
226
+ if key in self.cache:
227
+ self.cache_hits += 1
228
+ return self.cache[key]
229
+ self.cache_misses += 1
230
+
231
+ # Cache miss - evaluate directly with Radia
232
+ # Note: Assumes rad.FldUnits('m') has been called - coordinates already in meters
233
+ result = rad.Fld(self.radia_obj, self.field_type, [px, py, pz])
234
+
235
+ # No scaling needed - rad.FldUnits('m') ensures consistent units
236
+ return [result[0], result[1], result[2]]
237
+
238
+
239
+ def collect_integration_points(mesh, order=5):
240
+ """
241
+ Collect all integration points from a mesh
242
+
243
+ This is a helper function to collect integration points for cache preparation.
244
+
245
+ Args:
246
+ mesh: NGSolve mesh
247
+ order: Integration rule order (default: 5)
248
+
249
+ Returns:
250
+ list: List of [x, y, z] coordinates in meters
251
+
252
+ Example:
253
+ >>> from ngsolve import *
254
+ >>> mesh = Mesh(geo.GenerateMesh(maxh=0.015))
255
+ >>> points = collect_integration_points(mesh, order=5)
256
+ >>> print(f"Collected {len(points)} integration points")
257
+ """
258
+ try:
259
+ from ngsolve import IntegrationRule, VOL
260
+ except ImportError:
261
+ raise ImportError("NGSolve is required to collect integration points")
262
+
263
+ all_points = []
264
+
265
+ for el in mesh.Elements(VOL):
266
+ ir = IntegrationRule(el.type, order=order)
267
+ trafo = mesh.GetTrafo(el)
268
+
269
+ for ip in ir:
270
+ mip = trafo(ip)
271
+ pnt = mip.point
272
+ all_points.append([pnt[0], pnt[1], pnt[2]])
273
+
274
+ return all_points
Binary file