pcb-analysis 0.6.0__tar.gz

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 (85) hide show
  1. pcb_analysis-0.6.0/LICENSE +21 -0
  2. pcb_analysis-0.6.0/PKG-INFO +546 -0
  3. pcb_analysis-0.6.0/README.md +492 -0
  4. pcb_analysis-0.6.0/pyproject.toml +67 -0
  5. pcb_analysis-0.6.0/setup.cfg +4 -0
  6. pcb_analysis-0.6.0/src/electrical/__init__.py +10 -0
  7. pcb_analysis-0.6.0/src/electrical/dice_peec/__init__.py +117 -0
  8. pcb_analysis-0.6.0/src/electrical/dice_peec/_bench_utils.py +70 -0
  9. pcb_analysis-0.6.0/src/electrical/dice_peec/backends.py +62 -0
  10. pcb_analysis-0.6.0/src/electrical/dice_peec/cli/__init__.py +5 -0
  11. pcb_analysis-0.6.0/src/electrical/dice_peec/cli/calibrate.py +64 -0
  12. pcb_analysis-0.6.0/src/electrical/dice_peec/controller.py +445 -0
  13. pcb_analysis-0.6.0/src/electrical/dice_peec/cuda_delta.py +213 -0
  14. pcb_analysis-0.6.0/src/electrical/dice_peec/cuda_pypeec.py +669 -0
  15. pcb_analysis-0.6.0/src/electrical/dice_peec/cupy_calibrator.py +73 -0
  16. pcb_analysis-0.6.0/src/electrical/dice_peec/delta_peec.py +113 -0
  17. pcb_analysis-0.6.0/src/electrical/dice_peec/layout_ops.py +216 -0
  18. pcb_analysis-0.6.0/src/electrical/dice_peec/lowmem_25d.py +143 -0
  19. pcb_analysis-0.6.0/src/electrical/dice_peec/lowmem_peec.py +165 -0
  20. pcb_analysis-0.6.0/src/electrical/dice_peec/multilayer_peec.py +608 -0
  21. pcb_analysis-0.6.0/src/electrical/dice_peec/plane_opt_contract.py +593 -0
  22. pcb_analysis-0.6.0/src/electrical/dice_peec/pypeec_memory.py +252 -0
  23. pcb_analysis-0.6.0/src/electrical/dice_peec/sheet_cuda.py +687 -0
  24. pcb_analysis-0.6.0/src/electrical/dice_peec/sheet_inductance.py +545 -0
  25. pcb_analysis-0.6.0/src/electrical/dice_peec/sheet_operator.py +322 -0
  26. pcb_analysis-0.6.0/src/electrical/dice_peec/sheet_peec.py +676 -0
  27. pcb_analysis-0.6.0/src/electrical/dice_peec/sheet_results.py +199 -0
  28. pcb_analysis-0.6.0/src/electrical/dice_peec/skin_filaments.py +387 -0
  29. pcb_analysis-0.6.0/src/electrical/dice_peec/stackup.py +102 -0
  30. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/__init__.py +80 -0
  31. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/cuda.py +156 -0
  32. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/frequency_domain.py +505 -0
  33. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/pcb.py +480 -0
  34. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/runtime.py +314 -0
  35. pcb_analysis-0.6.0/src/electrical/matrix_free_mpir_fem/solver.py +468 -0
  36. pcb_analysis-0.6.0/src/electrical/py.typed +1 -0
  37. pcb_analysis-0.6.0/src/emc/__init__.py +11 -0
  38. pcb_analysis-0.6.0/src/emc/py.typed +0 -0
  39. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/__init__.py +74 -0
  40. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/far_field.py +204 -0
  41. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/fields.py +241 -0
  42. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/limits.py +168 -0
  43. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/moments.py +86 -0
  44. pcb_analysis-0.6.0/src/emc/tiled_dipole_superposition/sources.py +188 -0
  45. pcb_analysis-0.6.0/src/multiphysics/__init__.py +11 -0
  46. pcb_analysis-0.6.0/src/multiphysics/py.typed +0 -0
  47. pcb_analysis-0.6.0/src/multiphysics/staggered_coupling/__init__.py +71 -0
  48. pcb_analysis-0.6.0/src/multiphysics/staggered_coupling/electro_thermal.py +373 -0
  49. pcb_analysis-0.6.0/src/multiphysics/staggered_coupling/emission.py +192 -0
  50. pcb_analysis-0.6.0/src/multiphysics/staggered_coupling/scenarios.py +213 -0
  51. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/PKG-INFO +546 -0
  52. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/SOURCES.txt +83 -0
  53. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/dependency_links.txt +1 -0
  54. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/entry_points.txt +2 -0
  55. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/requires.txt +9 -0
  56. pcb_analysis-0.6.0/src/pcb_analysis.egg-info/top_level.txt +4 -0
  57. pcb_analysis-0.6.0/src/thermal/__init__.py +11 -0
  58. pcb_analysis-0.6.0/src/thermal/matrix_free_mpir_fem/__init__.py +38 -0
  59. pcb_analysis-0.6.0/src/thermal/matrix_free_mpir_fem/conduction.py +847 -0
  60. pcb_analysis-0.6.0/src/thermal/matrix_free_mpir_fem/coupling.py +98 -0
  61. pcb_analysis-0.6.0/src/thermal/matrix_free_mpir_fem/cuda.py +160 -0
  62. pcb_analysis-0.6.0/src/thermal/matrix_free_mpir_fem/two_level.py +216 -0
  63. pcb_analysis-0.6.0/src/thermal/py.typed +0 -0
  64. pcb_analysis-0.6.0/tests/test_controller.py +133 -0
  65. pcb_analysis-0.6.0/tests/test_cuda.py +563 -0
  66. pcb_analysis-0.6.0/tests/test_cuda_matrix_free_mpir_fem.py +137 -0
  67. pcb_analysis-0.6.0/tests/test_cuda_thermal_conduction.py +104 -0
  68. pcb_analysis-0.6.0/tests/test_delta_peec.py +39 -0
  69. pcb_analysis-0.6.0/tests/test_electrothermal_coupling.py +129 -0
  70. pcb_analysis-0.6.0/tests/test_emc_dipole_superposition.py +192 -0
  71. pcb_analysis-0.6.0/tests/test_emc_sources.py +121 -0
  72. pcb_analysis-0.6.0/tests/test_fp32_accuracy.py +86 -0
  73. pcb_analysis-0.6.0/tests/test_frequency_domain_fem.py +201 -0
  74. pcb_analysis-0.6.0/tests/test_matrix_free_mpir_fem.py +132 -0
  75. pcb_analysis-0.6.0/tests/test_multilayer_peec.py +318 -0
  76. pcb_analysis-0.6.0/tests/test_peec_fem_comparison.py +48 -0
  77. pcb_analysis-0.6.0/tests/test_plane_opt_contract.py +238 -0
  78. pcb_analysis-0.6.0/tests/test_pypeec_memory.py +140 -0
  79. pcb_analysis-0.6.0/tests/test_sheet_cuda_device.py +75 -0
  80. pcb_analysis-0.6.0/tests/test_sheet_inductance.py +181 -0
  81. pcb_analysis-0.6.0/tests/test_sheet_peec.py +625 -0
  82. pcb_analysis-0.6.0/tests/test_sheet_results.py +211 -0
  83. pcb_analysis-0.6.0/tests/test_skin_filaments.py +181 -0
  84. pcb_analysis-0.6.0/tests/test_staggered_coupling.py +232 -0
  85. pcb_analysis-0.6.0/tests/test_thermal_conduction.py +329 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hiroyuki Yamada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,546 @@
1
+ Metadata-Version: 2.1
2
+ Name: pcb-analysis
3
+ Version: 0.6.0
4
+ Summary: CUDA-accelerated PEEC, matrix-free FEM, and radiated-emission evaluation for electrical, thermal, and EMC PCB analysis
5
+ Author-email: Hiroyuki Yamada <H44bcda@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Hiroyuki Yamada
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/AFLOY/pcb-analysis
29
+ Project-URL: Repository, https://github.com/AFLOY/pcb-analysis
30
+ Project-URL: Issues, https://github.com/AFLOY/pcb-analysis/issues
31
+ Keywords: peec,fem,maxwell,skin-effect,thermal,heat-conduction,emc,radiated-emission,near-field,multiphysics,electrothermal,pcb,pdn,electromagnetics,cuda,optimization
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: POSIX :: Linux
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3 :: Only
41
+ Classifier: Topic :: Scientific/Engineering :: Physics
42
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.11
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: numpy<2.4,>=1.26
48
+ Requires-Dist: scipy<1.18,>=1.11
49
+ Provides-Extra: cuda
50
+ Requires-Dist: cupy-cuda13x<15,>=14; extra == "cuda"
51
+ Requires-Dist: pypeec==5.8.0; extra == "cuda"
52
+ Provides-Extra: test
53
+ Requires-Dist: pytest<9,>=8; extra == "test"
54
+
55
+ # Accelerated electrical, thermal, EMC, and coupled PCB analysis
56
+
57
+ [![CI](https://github.com/AFLOY/pcb-analysis/actions/workflows/ci.yml/badge.svg)](https://github.com/AFLOY/pcb-analysis/actions/workflows/ci.yml)
58
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
59
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
60
+
61
+ Accelerated PEEC and matrix-free FEM solvers for electrical and thermal PCB
62
+ analysis, radiated-emission evaluation for EMC, and PDN optimization.
63
+
64
+ The repository holds two solver families under the `electrical` package, one
65
+ under the `thermal` package, one evaluation front end under `emc`, and the
66
+ coupled scenarios that chain them under `multiphysics`. `electrical.dice_peec` provides exact incremental
67
+ delta-scoring for local PCB reroutes, a 2.5D multilayer interaction operator, a
68
+ thin-sheet PEEC field solver, and an adaptive runtime controller for
69
+ topology-optimization loops. `electrical.matrix_free_mpir_fem` provides a
70
+ matrix-free Q1 finite-element solver for DC conduction and 2D frequency-domain
71
+ Maxwell fields, driven by mixed-precision iterative refinement (MPIR) on NumPy
72
+ or CuPy. `thermal.matrix_free_mpir_fem` solves steady heat conduction through
73
+ the whole board stack with the same MPIR solver and runtimes, and takes the
74
+ Joule loss of an electrical solve as its heat load.
75
+ `emc.tiled_dipole_superposition` reads the current distribution of either
76
+ electrical solve as Hertzian dipoles and evaluates near-field scans, far-field
77
+ patterns, radiated power, and margins to CISPR 32 and FCC Part 15 limits.
78
+ `multiphysics.staggered_coupling` runs the scenarios that couple them: the
79
+ electro-thermal fixed point with temperature-dependent copper, and the emission
80
+ of a cold or thermally converged current distribution.
81
+
82
+ ## Features
83
+
84
+ - **Exact sparse delta scoring** — `O(s)` + `O(s²)` incremental evaluation instead of `O(N log N)` full FFT per candidate
85
+ - **2.5D multilayer operator** — planar FFT with interlayer kernel matrix and frequency-dependent via model
86
+ - **CUDA acceleration** — CuPy/cuFFT backend with RawKernel batched scoring
87
+ - **Adaptive controller** — memory-bounded, error-driven fidelity cascade with automatic OOM recovery
88
+ - **Low-memory mode** — near/far field splitting for 4–8 GB VRAM devices
89
+ - **Matrix-free MPIR-FEM** — Q1 PCB conduction FEM with an FP32 inner PCG and
90
+ FP64 outer reliable updates
91
+ - **Frequency-domain Maxwell** — 2D scalar-polarisation wave, dielectric-loss,
92
+ eddy-current, and skin-effect solve with complex64 inner GMRES and complex128
93
+ reliable updates
94
+ - **CUDA matrix-free FEM** — fused, node-owned Q1 gather kernel plus batched
95
+ complex64 GMRES; no assembled matrix and no global atomics
96
+ - **Accelerator boundary** — NumPy/CuPy runtimes with a narrow low-precision
97
+ interface that remains suitable for the later Tenstorrent port
98
+ - **Thermal MPIR-FEM** — matrix-free trilinear hexahedral heat conduction
99
+ through the copper/laminate stack with anisotropic laminate conductivity,
100
+ convective faces, fixed-temperature nodes, and a closed heat budget
101
+ - **Electrothermal coupling** — per-element and per-via Joule loss from the DC
102
+ solve mapped onto the thermal stack without interpolation
103
+ - **Radiated emissions** — exact Hertzian-dipole superposition of the solved
104
+ currents for near-field scans and far-field patterns, radiated power,
105
+ dipole-moment proxies, PEC ground-plane images, and margins to CISPR 32 /
106
+ FCC Part 15 limit lines, tiled on NumPy or CuPy
107
+ - **Coupled scenarios** — staggered electro-thermal iteration with `σ(T)` and
108
+ via `R(T)`, warm starts and Aitken relaxation; electro-emission and
109
+ electro-thermal-emission chains; one `run_scenario` entry point
110
+
111
+ ## Installation
112
+
113
+ ### From PyPI
114
+
115
+ ```bash
116
+ pip install pcb-analysis
117
+ # with CuPy and PyPEEC
118
+ pip install 'pcb-analysis[cuda]'
119
+ ```
120
+
121
+ Each release is uploaded to [PyPI](https://pypi.org/project/pcb-analysis/)
122
+ by the `Release` workflow with Trusted Publishing, and the same wheel, source
123
+ distribution, and SHA-256 checksums are attached to the GitHub release. To
124
+ install a specific release without PyPI, point pip at the release asset or
125
+ at the tag:
126
+
127
+ ```bash
128
+ pip install https://github.com/AFLOY/pcb-analysis/releases/download/v0.6.0/pcb_analysis-0.6.0-py3-none-any.whl
129
+ pip install 'git+https://github.com/AFLOY/pcb-analysis.git@v0.6.0'
130
+ ```
131
+
132
+ The release tag has to match the version in `pyproject.toml`.
133
+
134
+ ### From a checkout, CPU only
135
+
136
+ ```bash
137
+ pip install -e .
138
+ ```
139
+
140
+ ### With CUDA support
141
+
142
+ ```bash
143
+ pip install -e '.[cuda]'
144
+ ```
145
+
146
+ > **Note**: Requires an NVIDIA driver and CUDA 13.x. Verify with `nvidia-smi`.
147
+ > The package is installed as `electrical`; the former top-level
148
+ > `peec_fastopt` package now lives at `electrical.dice_peec`, and the thermal
149
+ > and EMC front ends and the coupled scenarios are the separate top-level
150
+ > packages `thermal`, `emc`, and `multiphysics`. Re-run the editable install
151
+ > after pulling these changes so the new packages are importable and the old
152
+ > path is not left on `sys.path`.
153
+
154
+ ## Quick start
155
+
156
+ ```python
157
+ from electrical.dice_peec import (
158
+ Stackup,
159
+ FFTInteraction25D,
160
+ MultilayerDeltaScorer,
161
+ CandidateEdit,
162
+ SegmentOp,
163
+ ViaOp,
164
+ compile_candidate,
165
+ )
166
+ import numpy as np
167
+
168
+ stack = Stackup.dual_sided(board_thickness_mm=1.6)
169
+ shape = (64, 64)
170
+ base = np.zeros((stack.n_layers, *shape))
171
+ base[0, 10:20, 10:30] = 1.0
172
+
173
+ op = FFTInteraction25D(shape, stack, cell_size_m=0.2e-3)
174
+ scorer = MultilayerDeltaScorer(op, base, frequency_hz=3e5)
175
+
176
+ edit = CandidateEdit(
177
+ segments=[SegmentOp("add", "B.Cu", ((12, 12), (12, 13), (12, 14)))],
178
+ vias=[ViaOp("add", 12, 14, "F.Cu", "B.Cu", important=True)],
179
+ )
180
+ compiled = compile_candidate(edit, stack)
181
+ score = scorer.energy(compiled.occupancy_delta, vias=compiled.vias)
182
+ ```
183
+
184
+ ### Matrix-free PCB conduction
185
+
186
+ ```python
187
+ import numpy as np
188
+ from electrical.matrix_free_mpir_fem import (
189
+ CurrentTerminal,
190
+ LayeredPCBMesh,
191
+ PCBConductionProblem,
192
+ solve_pcb_dc,
193
+ )
194
+
195
+ mesh = LayeredPCBMesh(
196
+ element_active=np.ones((1, 1, 32), dtype=bool),
197
+ layer_thickness_m=(35e-6,),
198
+ pitch_x_m=0.2e-3,
199
+ pitch_y_m=0.2e-3,
200
+ )
201
+ problem = PCBConductionProblem(
202
+ mesh=mesh,
203
+ terminals=(
204
+ CurrentTerminal(((0, 0, 0), (0, 1, 0)), 1.0, "source"),
205
+ CurrentTerminal(((0, 0, 32), (0, 1, 32)), -1.0, "sink"),
206
+ ),
207
+ reference_node=(0, 0, 32),
208
+ )
209
+ solution = solve_pcb_dc(problem)
210
+ assert solution.solve.converged
211
+ ```
212
+
213
+ ### Frequency-domain Maxwell
214
+
215
+ The frequency-domain front end solves the `E_z` Maxwell reduction for
216
+ z-invariant geometries and reports electric field, magnetic field,
217
+ eddy-current density, conductor loss, and dielectric loss. Dirichlet nodes
218
+ are given by a boolean mask over the node grid plus the field value held at
219
+ each masked node.
220
+
221
+ ```python
222
+ import numpy as np
223
+ from electrical.matrix_free_mpir_fem import (
224
+ ScalarMaxwellMesh2D,
225
+ ScalarMaxwellProblem,
226
+ solve_scalar_maxwell,
227
+ )
228
+
229
+ mesh = ScalarMaxwellMesh2D(
230
+ (8, 64), # elements: rows x columns
231
+ pitch_x_m=0.1e-3,
232
+ pitch_y_m=0.1e-3,
233
+ relative_permittivity=4.0,
234
+ dielectric_loss_tangent=0.01,
235
+ )
236
+ mask = np.zeros(mesh.node_shape, dtype=bool)
237
+ mask[:, 0] = mask[:, -1] = True # drive the left edge, ground the right
238
+ value = np.zeros(mesh.node_shape, dtype=np.complex128)
239
+ value[:, 0] = 1.0
240
+ problem = ScalarMaxwellProblem(mesh, 1e9, mask, value)
241
+
242
+ solution = solve_scalar_maxwell(problem) # NumPy complex64 inner solve
243
+ assert solution.solve.converged
244
+ loss_w_per_m = solution.dielectric_loss_w_per_m
245
+ ```
246
+
247
+ ### CUDA full-wave solve
248
+
249
+ ```python
250
+ solution = solve_scalar_maxwell(
251
+ problem,
252
+ backend="cuda", # or "auto" to fall back to NumPy when no GPU is visible
253
+ device_id=0,
254
+ )
255
+ assert solution.solve.low_runtime == "cupy-complex64"
256
+ ```
257
+
258
+ The complex128 reliable residual stays on the CPU. The correction RHS crosses
259
+ to CUDA once per outer iteration, while the complex64 Krylov basis,
260
+ preconditioner, material coefficients, and repeated Q1 actions remain on the
261
+ GPU. On the repository's GTX 1650 audit, the fused operator is 8.8–40.4×
262
+ faster than the NumPy complex64 action for 4,225–66,049 unknowns. The complete
263
+ solve is slower at 4,369 unknowns (0.85×) but 1.99× faster at 16,705 unknowns,
264
+ so CUDA should not be selected solely for tiny meshes.
265
+
266
+ ### Steady thermal conduction
267
+
268
+ The thermal front end meshes the whole stack as slabs of hexahedral Q1
269
+ elements. Copper, laminate, and vias are element conductivities; laminates may
270
+ have a different through-plane value. Heat enters per element or per node and
271
+ leaves through convective faces and fixed-temperature nodes. The solution
272
+ reports nodal temperature, element heat flux, and a heat budget whose error is
273
+ the solver residual.
274
+
275
+ ```python
276
+ import numpy as np
277
+ from thermal.matrix_free_mpir_fem import (
278
+ ConvectionBoundary,
279
+ HeatSource,
280
+ LayeredThermalMesh,
281
+ ThermalConductionProblem,
282
+ solve_thermal_conduction,
283
+ )
284
+
285
+ # 35 µm copper / 1.5 mm FR-4 / 35 µm copper, 50 mm x 50 mm on a 0.5 mm grid.
286
+ mesh = LayeredThermalMesh(
287
+ slab_thickness_m=(35e-6, 1.5e-3, 35e-6),
288
+ pitch_x_m=0.5e-3,
289
+ pitch_y_m=0.5e-3,
290
+ conductivity_w_per_m_k=(385.0, 0.8, 385.0),
291
+ through_plane_conductivity_w_per_m_k=(385.0, 0.3, 385.0),
292
+ element_shape=(100, 100),
293
+ )
294
+ ambient = 298.15
295
+ problem = ThermalConductionProblem(
296
+ mesh,
297
+ convection=(
298
+ ConvectionBoundary("top", 10.0, ambient),
299
+ ConvectionBoundary("bottom", 10.0, ambient),
300
+ ),
301
+ heat_sources=(HeatSource(((3, 50, 50), (3, 50, 51)), 0.5, "regulator"),),
302
+ )
303
+ solution = solve_thermal_conduction(problem, initial_temperature_k=ambient)
304
+ assert solution.solve.converged
305
+ hottest_k = solution.max_temperature_k
306
+ assert abs(solution.heat_balance_error_w) < 1e-9
307
+ ```
308
+
309
+ The default preconditioner adds a patch-constant coarse correction to Jacobi
310
+ scaling. A cooled copper plate is stiff in-plane and weakly coupled to the air,
311
+ so plain Jacobi PCG needs hundreds of inner iterations per decade on such a
312
+ stack; the two-level variant needs tens. Pass `preconditioner="jacobi"` to
313
+ compare, and `coarse_block_nodes` to set the patch width.
314
+
315
+ ### Electrothermal coupling
316
+
317
+ `element_joule_heat_w` places the per-element copper loss of a
318
+ `PCBConductionSolution` onto the thermal slabs that hold each electrical layer,
319
+ and `via_joule_heat_sources` turns each via's loss into nodal heat at its
320
+ endpoints. Both meshes must share the in-plane element grid.
321
+
322
+ ```python
323
+ from thermal.matrix_free_mpir_fem import element_joule_heat_w, via_joule_heat_sources
324
+
325
+ electrical = solve_pcb_dc(pcb_problem) # copper on layers 0 and 1
326
+ layer_slabs = (0, 2) # bottom and top copper slabs
327
+ thermal_problem = ThermalConductionProblem(
328
+ mesh,
329
+ convection=(ConvectionBoundary("top", 10.0, ambient),),
330
+ element_heat_w=element_joule_heat_w(electrical, mesh, layer_slabs),
331
+ heat_sources=via_joule_heat_sources(pcb_problem, electrical, mesh, layer_slabs),
332
+ )
333
+ solution = solve_thermal_conduction(thermal_problem, backend="auto")
334
+ ```
335
+
336
+ The thermal operator runs its FP32 inner PCG on CuPy with `backend="cuda"`; the
337
+ FP64 reliable update stays on the host exactly as for the electrical solves.
338
+
339
+ ### Radiated emissions
340
+
341
+ Every current element of a solved board is a Hertzian dipole. The EMC front end
342
+ sums their exact fields for a near-field scan and their far-zone terms for the
343
+ pattern at a test-site distance, then compares the maximum with a limit line.
344
+
345
+ ```python
346
+ import numpy as np
347
+ from emc.tiled_dipole_superposition import (
348
+ CISPR32_CLASS_B,
349
+ dipole_moments,
350
+ dipoles_from_pcb_dc,
351
+ emission_margin,
352
+ evaluate_fields,
353
+ far_field_pattern,
354
+ scan_plane,
355
+ )
356
+
357
+ electrical = solve_pcb_dc(pcb_problem) # two copper layers
358
+ dipoles = dipoles_from_pcb_dc(
359
+ pcb_problem, electrical, layer_height_m=(0.0, 1.6e-3), close_terminals=True
360
+ )
361
+ frequency_hz = 100e6 # DC pattern used as a phasor
362
+
363
+ probe = scan_plane(np.linspace(0, 0.05, 50), np.linspace(0, 0.02, 20), z_m=6.6e-3)
364
+ near = evaluate_fields(dipoles, probe, frequency_hz, backend="auto")
365
+ h_max_a_per_m = near.magnetic_magnitude_a_per_m.max()
366
+
367
+ pattern = far_field_pattern(dipoles, frequency_hz, distance_m=10.0)
368
+ margin = emission_margin(
369
+ pattern.max_polarised_field_v_per_m, frequency_hz, CISPR32_CLASS_B, distance_m=10.0
370
+ )
371
+ print(pattern.radiated_power_w, margin.predicted_dbuv_per_m, margin.margin_db)
372
+
373
+ moments = dipole_moments(dipoles, frequency_hz) # |P| ≈ 0 once the loop closes
374
+ ```
375
+
376
+ `dipole_moments` reports the net electric moment: a non-zero value for a board
377
+ whose terminal currents balance means the return path through the component is
378
+ not in the model, and `close_terminals=True` adds it as a straight element.
379
+ The magnetic near field and the far field are the robust outputs; the electric
380
+ near field of a current-only description is sensitive to how continuously the
381
+ element chain closes. `dipoles_from_sheet_peec` does the same for a
382
+ frequency-resolved sheet-PEEC solve, without the quasi-static assumption.
383
+
384
+ ### Coupled scenarios
385
+
386
+ Scenario dataclasses name what is coupled to what; `run_scenario` dispatches on
387
+ the type. The electro-thermal chain iterates the two conduction solves until
388
+ the copper conductivity `σ(T) = σ_ref / (1 + α (T − T_ref))` is self-consistent
389
+ with the temperature it produces, warm-starting both solves and relaxing the
390
+ temperature update with Aitken's Δ² estimate.
391
+
392
+ ```python
393
+ from multiphysics.staggered_coupling import (
394
+ ElectroThermalEmissionScenario,
395
+ ElectroThermalScenario,
396
+ EmissionScenario,
397
+ run_scenario,
398
+ )
399
+
400
+ coupled_scenario = ElectroThermalScenario(
401
+ pcb_problem, # PCBConductionProblem, conductivity at 293.15 K
402
+ thermal_mesh, # LayeredThermalMesh on the same (rows, cols)
403
+ layer_slabs=(0, 2), # thermal slab holding each copper layer
404
+ convection=(ConvectionBoundary("top", 10.0, 298.15), ConvectionBoundary("bottom", 10.0, 298.15)),
405
+ )
406
+ coupled = run_scenario(coupled_scenario)
407
+ print(coupled.converged, coupled.iterations, coupled.loss_increase_ratio)
408
+
409
+ chained = run_scenario(
410
+ ElectroThermalEmissionScenario(
411
+ coupled_scenario,
412
+ layer_height_m=(0.0, 1.6e-3),
413
+ emission=EmissionScenario((30e6, 100e6, 300e6)), # CISPR 32 Class B at 10 m
414
+ )
415
+ )
416
+ print(chained.emission.margin_db, chained.heating_shift_db)
417
+ ```
418
+
419
+ `ElectricalScenario`, `ThermalScenario`, `ElectroEmissionScenario`, and
420
+ `SheetPeecEmissionScenario` (one sheet-PEEC solve per frequency) complete the
421
+ set; `run_scenarios` runs a list. See
422
+ [MULTIPHYSICS_SCENARIOS.md](docs/MULTIPHYSICS_SCENARIOS.md).
423
+
424
+ ## Project structure
425
+
426
+ ```text
427
+ src/
428
+ electrical/ Analysis target
429
+ dice_peec/ PEEC + DICE/2.5D FFT acceleration
430
+ matrix_free_mpir_fem/ FEM + matrix-free/MPIR acceleration
431
+ thermal/ Analysis target
432
+ matrix_free_mpir_fem/ Heat-conduction FEM + matrix-free/MPIR acceleration
433
+ emc/ Analysis target
434
+ tiled_dipole_superposition/ Radiated emission by dipole superposition, tiled NumPy/CuPy
435
+ multiphysics/ Coupled analysis
436
+ staggered_coupling/ Partitioned fixed-point coupling with warm starts and Aitken relaxation
437
+ tests/ Test suite (pytest)
438
+ docs/ Design documents, validation reports, raw results
439
+ examples/ Demo scripts and benchmarks
440
+ experiments/ Repeatable accuracy and timing audits
441
+ requirements.txt Editable install with CUDA/test/build tooling
442
+ ```
443
+
444
+ Future physics should follow the same rule: the first level names the analysis
445
+ target and the second names the method plus its acceleration strategy.
446
+
447
+ ## Electrical solvers
448
+
449
+ `cuda_pypeec` runs PyPEEC's own three-dimensional voxel solve on CUDA. PyPEEC
450
+ owns the physics; this owns the device policy. `pypeec_memory` predicts what a
451
+ model costs before it is attempted, which matters once a model spans a board's
452
+ height instead of one copper layer: on a two-sided 34.7mm board the prepared
453
+ operators go from 4.5 MiB to 541 MiB while the conductor merely doubles.
454
+
455
+ `sheet_peec` solves the same physics on a mesh built for what a PCB is -- a few
456
+ thin sheets at heights the stackup states. Its inductance operator is a
457
+ two-dimensional transform per layer pair rather than a three-dimensional one
458
+ over the board's height, which brings the same board's operators to 5.6 MiB.
459
+ At zero frequency it reproduces an independent resistor network to machine
460
+ precision; above it, the transform path matches a dense assembly of the same
461
+ operator. See [docs/SHEET_PEEC.md](docs/SHEET_PEEC.md).
462
+
463
+ `multilayer_peec` is neither. It is a scalar interaction proxy for ranking many
464
+ candidate shapes cheaply, and it solves for no current or potential.
465
+
466
+ `skin_filaments` extends `sheet_peec` to conductors thick against the skin
467
+ depth by cutting them into graded filaments joined through the thickness, so
468
+ the solve itself decides how the current divides between the faces and the
469
+ interior.
470
+
471
+ `matrix_free_mpir_fem` solves real SPD DC conduction on layered Q1 PCB meshes
472
+ and complex 2D scalar-polarised frequency-domain Maxwell fields. Its global
473
+ matrix is never assembled. Low-precision inner corrections contain most
474
+ operator applications, while high-precision host residuals determine final
475
+ convergence. Scope and the Blackhole port boundary are documented in
476
+ [docs/MATRIX_FREE_MPIR_FEM.md](docs/MATRIX_FREE_MPIR_FEM.md).
477
+
478
+ ## Experiments and audits
479
+
480
+ The scripts in `experiments/` regenerate the numbers quoted in `docs/`. They
481
+ run from the repository root because the FP32 audit is also imported by the
482
+ test suite. Results default to the ignored `benchmark-results/` directory;
483
+ pass `--output` (or `--json` for the FP32 audit) to write elsewhere.
484
+
485
+ ```bash
486
+ # What single precision costs the sheet-PEEC solver (host stage, then CUDA)
487
+ python experiments/fp32_accuracy.py --cells 128 --json benchmark-results/fp32.json
488
+
489
+ # Maxwell accuracy against closed-form 1D solutions, plus CPU timing
490
+ python experiments/maxwell_small_benchmark.py
491
+
492
+ # Fused CUDA operator throughput and complete-solve timing (needs a GPU)
493
+ python experiments/maxwell_cuda_benchmark.py --operator-sides 64 128 256
494
+
495
+ # Sheet PEEC versus matrix-free FEM on matched DC strips and a skin-effect slab.
496
+ # Both methods solve both scenarios; the PEEC skin bar takes ~90 s at 64 cells.
497
+ python experiments/peec_fem_comparison.py --no-cuda --peec-skin-lengths 16 32
498
+ ```
499
+
500
+ The skin-effect scenario is the same 0.5 mm copper slab at 1 MHz for both
501
+ methods. FEM solves the field through the slab's thickness. Sheet PEEC has no
502
+ infinite slab, so it solves a bar of graded filaments and reads the AC/DC
503
+ resistance ratio from the filament current division at the middle of the bar,
504
+ where it is slab-like. The closed-form slab impedance is the shared reference.
505
+
506
+ ## Development
507
+
508
+ ```bash
509
+ # CPU-only, as CI runs it
510
+ pip install -e '.[test]'
511
+
512
+ # Or: the ignored local environment with CUDA, test, and build tooling
513
+ python -m venv .venv
514
+ source .venv/bin/activate
515
+ python -m pip install -r requirements.txt
516
+
517
+ # Run tests from the repository root
518
+ python -m pytest tests/ -v
519
+ ```
520
+
521
+ CI runs the suite on Python 3.11–3.13, then builds a wheel and source
522
+ distribution with the oldest supported setuptools and imports both
523
+ subpackages from a clean environment, so a module missing from the
524
+ distribution fails the build rather than a user's install.
525
+
526
+ ## Documentation
527
+
528
+ - [Architecture & Design](docs/DESIGN.md)
529
+ - [CUDA Backend Handoff](docs/CUDA_HANDOFF.md)
530
+ - [Matrix-free MPIR-FEM](docs/MATRIX_FREE_MPIR_FEM.md)
531
+ - [Thermal MPIR-FEM](docs/THERMAL_MPIR_FEM.md)
532
+ - [EMC radiated emissions](docs/EMC_DIPOLE_SUPERPOSITION.md)
533
+ - [Coupled analysis scenarios](docs/MULTIPHYSICS_SCENARIOS.md)
534
+ - [Sheet PEEC](docs/SHEET_PEEC.md) and [Sheet PEEC CUDA results](docs/SHEET_CUDA_RESULTS.md)
535
+ - [Requirements](docs/REQUIREMENTS.md)
536
+ - [Sheet PEEC / matrix-free FEM comparison](docs/PEEC_FEM_COMPARISON_REPORT.html)
537
+ ([raw data](docs/PEEC_FEM_COMPARISON_RESULTS.json))
538
+ - [Maxwell accuracy and CPU timing report](docs/MAXWELL_VALIDATION_REPORT.html)
539
+ ([raw data](docs/MAXWELL_SMALL_RESULTS.json))
540
+ - [Maxwell CUDA benchmark data](docs/MAXWELL_CUDA_RESULTS.json) and
541
+ [CUDA scaling data](docs/MAXWELL_CUDA_SCALE_RESULTS.json)
542
+ - [Benchmark Results](docs/RESULTS.md)
543
+
544
+ ## License
545
+
546
+ [MIT](LICENSE)