pcb-analysis 0.9.2__tar.gz → 0.9.3__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 (156) hide show
  1. {pcb_analysis-0.9.2/src/pcb_analysis.egg-info → pcb_analysis-0.9.3}/PKG-INFO +1 -1
  2. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/pyproject.toml +1 -1
  3. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/__init__.py +2 -1
  4. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/ports.py +42 -4
  5. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/current_field_contract.py +1 -1
  6. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_peec.py +53 -4
  7. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/circuit_coupled.py +8 -1
  8. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3/src/pcb_analysis.egg-info}/PKG-INFO +1 -1
  9. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_dc_ports.py +24 -0
  10. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_sheet_peec.py +31 -0
  11. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/LICENSE +0 -0
  12. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/README.md +0 -0
  13. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/setup.cfg +0 -0
  14. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/__init__.py +0 -0
  15. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/__init__.py +0 -0
  16. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/backends.py +0 -0
  17. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/benchmark.py +0 -0
  18. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/cli/__init__.py +0 -0
  19. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/cli/calibrate.py +0 -0
  20. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/controller.py +0 -0
  21. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/cuda_delta.py +0 -0
  22. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/cupy_calibrator.py +0 -0
  23. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/delta_peec.py +0 -0
  24. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/layout_ops.py +0 -0
  25. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/lowmem_25d.py +0 -0
  26. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/lowmem_peec.py +0 -0
  27. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/multilayer_peec.py +0 -0
  28. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/dice_peec/stackup.py +0 -0
  29. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/cuda.py +0 -0
  30. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/frequency_domain.py +0 -0
  31. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/grid.py +0 -0
  32. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native/__init__.py +0 -0
  33. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native/build.py +0 -0
  34. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native/layered_dc_q1.cpp +0 -0
  35. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native/scalar_maxwell_q1.cpp +0 -0
  36. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native_dc.py +0 -0
  37. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/native_q1.py +0 -0
  38. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/pcb.py +0 -0
  39. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/runtime.py +0 -0
  40. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/solver.py +0 -0
  41. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/matrix_free_mpir_fem/two_level.py +0 -0
  42. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/py.typed +0 -0
  43. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/__init__.py +0 -0
  44. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/native/__init__.py +0 -0
  45. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/native/build.py +0 -0
  46. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/native/pfft_near.cpp +0 -0
  47. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_cuda.py +0 -0
  48. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_inductance.py +0 -0
  49. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_operator.py +0 -0
  50. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_pfft.py +0 -0
  51. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/sheet_results.py +0 -0
  52. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/skin_filaments.py +0 -0
  53. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/sheet_peec/skin_screen.py +0 -0
  54. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/voxel_peec/__init__.py +0 -0
  55. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/voxel_peec/contract.py +0 -0
  56. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/voxel_peec/cuda_pypeec.py +0 -0
  57. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/electrical/voxel_peec/pypeec_memory.py +0 -0
  58. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/__init__.py +0 -0
  59. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/py.typed +0 -0
  60. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/__init__.py +0 -0
  61. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/far_field.py +0 -0
  62. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/fields.py +0 -0
  63. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/limits.py +0 -0
  64. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/moments.py +0 -0
  65. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/native/__init__.py +0 -0
  66. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/native/build.py +0 -0
  67. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/native/dipole_fields.cpp +0 -0
  68. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/native_dipole.py +0 -0
  69. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/emc/tiled_dipole_superposition/sources.py +0 -0
  70. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/__init__.py +0 -0
  71. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/__init__.py +0 -0
  72. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/adapters.py +0 -0
  73. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/barrel.py +0 -0
  74. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/bodymap.py +0 -0
  75. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/conductors.py +0 -0
  76. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/contact.py +0 -0
  77. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/kicad.py +0 -0
  78. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/mesh.py +0 -0
  79. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/native/__init__.py +0 -0
  80. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/native/build.py +0 -0
  81. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/native/point_in_mesh.cpp +0 -0
  82. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/reader.py +0 -0
  83. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/refinement.py +0 -0
  84. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/section.py +0 -0
  85. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/skin.py +0 -0
  86. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/cad_import/voxelize.py +0 -0
  87. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/geometry/py.typed +0 -0
  88. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/__init__.py +0 -0
  89. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/py.typed +0 -0
  90. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/__init__.py +0 -0
  91. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/board_enclosure.py +0 -0
  92. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/electro_thermal.py +0 -0
  93. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/electro_thermal_enclosure.py +0 -0
  94. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/emission.py +0 -0
  95. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/multiphysics/staggered_coupling/scenarios.py +0 -0
  96. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/pcb_analysis.egg-info/SOURCES.txt +0 -0
  97. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/pcb_analysis.egg-info/dependency_links.txt +0 -0
  98. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/pcb_analysis.egg-info/entry_points.txt +0 -0
  99. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/pcb_analysis.egg-info/requires.txt +0 -0
  100. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/pcb_analysis.egg-info/top_level.txt +0 -0
  101. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/__init__.py +0 -0
  102. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/__init__.py +0 -0
  103. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/boundaries.py +0 -0
  104. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/contact.py +0 -0
  105. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/coupling.py +0 -0
  106. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/cuda.py +0 -0
  107. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/mesh.py +0 -0
  108. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/native/__init__.py +0 -0
  109. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/native/build.py +0 -0
  110. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/native/hex_q1.cpp +0 -0
  111. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/native_hex.py +0 -0
  112. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/operator.py +0 -0
  113. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/problem.py +0 -0
  114. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/radiation.py +0 -0
  115. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/solve.py +0 -0
  116. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/transient.py +0 -0
  117. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/two_level.py +0 -0
  118. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/matrix_free_mpir_fem/voxel.py +0 -0
  119. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/src/thermal/py.typed +0 -0
  120. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_board_enclosure_coupling.py +0 -0
  121. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_circuit_coupled.py +0 -0
  122. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_controller.py +0 -0
  123. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_cuda.py +0 -0
  124. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_cuda_matrix_free_mpir_fem.py +0 -0
  125. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_cuda_thermal_conduction.py +0 -0
  126. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_current_field_contract.py +0 -0
  127. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_delta_peec.py +0 -0
  128. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_electro_thermal_enclosure.py +0 -0
  129. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_electrothermal_coupling.py +0 -0
  130. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_emc_dipole_superposition.py +0 -0
  131. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_emc_sources.py +0 -0
  132. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_fp32_accuracy.py +0 -0
  133. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_frequency_domain_fem.py +0 -0
  134. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_geometry_step.py +0 -0
  135. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_kicad_step.py +0 -0
  136. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_matrix_free_mpir_fem.py +0 -0
  137. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_multilayer_peec.py +0 -0
  138. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_native_dc.py +0 -0
  139. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_native_emc.py +0 -0
  140. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_native_geometry.py +0 -0
  141. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_native_q1.py +0 -0
  142. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_native_thermal.py +0 -0
  143. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_peec_fem_comparison.py +0 -0
  144. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_pypeec_memory.py +0 -0
  145. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_sheet_cuda_device.py +0 -0
  146. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_sheet_inductance.py +0 -0
  147. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_sheet_pfft.py +0 -0
  148. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_sheet_results.py +0 -0
  149. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_skin_filaments.py +0 -0
  150. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_staggered_coupling.py +0 -0
  151. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_tensor_grid.py +0 -0
  152. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_thermal_conduction.py +0 -0
  153. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_thermal_radiation.py +0 -0
  154. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_thermal_transient.py +0 -0
  155. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_thermal_voxel.py +0 -0
  156. {pcb_analysis-0.9.2 → pcb_analysis-0.9.3}/tests/test_voxel_peec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pcb-analysis
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: CUDA-accelerated PEEC, matrix-free FEM, and radiated-emission evaluation for electrical, thermal, and EMC PCB analysis
5
5
  Author-email: Hiroyuki Yamada <H44bcda@gmail.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pcb-analysis"
7
- version = "0.9.2"
7
+ version = "0.9.3"
8
8
  description = "CUDA-accelerated PEEC, matrix-free FEM, and radiated-emission evaluation for electrical, thermal, and EMC PCB analysis"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,7 +17,7 @@ from .pcb import (
17
17
  VoltageTerminal,
18
18
  solve_pcb_dc,
19
19
  )
20
- from .ports import DCPortBasis, PortSet, dc_port_basis
20
+ from .ports import DCPortBasis, PortSet, dc_port_basis, port_basis_workers
21
21
  from .frequency_domain import (
22
22
  EPSILON_0_F_PER_M,
23
23
  MU_0_H_PER_M,
@@ -80,6 +80,7 @@ __all__ = [
80
80
  "refined_grid",
81
81
  "cuda_available",
82
82
  "dc_port_basis",
83
+ "port_basis_workers",
83
84
  "graded_edges",
84
85
  "make_complex64_runtime",
85
86
  "make_float32_runtime",
@@ -28,10 +28,18 @@ The reduction is evaluated by diagonalising ``C`` restricted to the
28
28
  non-reference ports: ``C_rr = Σ_m λ_m u_m u_mᵀ`` gives ``<P_e> = Σ_m λ_m
29
29
  P_e(ψ_m)`` with ``ψ_m = Σ_k u_mk φ_k``, so the loss of every element and via
30
30
  comes from the exact element quadratic forms of :class:`.pcb.MatrixFreePCBOperator`.
31
+
32
+ The ``n - 1`` unit solves are independent and share one read-only operator,
33
+ so :func:`dc_port_basis` can run them on a thread pool (``workers=``, or
34
+ ``PCB_PORT_BASIS_WORKERS``). Threads multiply with the OpenMP team of the
35
+ native kernels (``PCB_NATIVE_THREADS``); their product is the core budget.
36
+ A CUDA runtime solves serially, since its stream is shared.
31
37
  """
32
38
 
33
39
  from __future__ import annotations
34
40
 
41
+ import os
42
+ from concurrent.futures import ThreadPoolExecutor
35
43
  from dataclasses import dataclass
36
44
  from typing import Sequence
37
45
 
@@ -49,6 +57,15 @@ from .runtime import LowPrecisionRuntime, RuntimeBackend
49
57
  from .solver import MPIRConfig, MPIRResult, solve_mpir
50
58
 
51
59
 
60
+ def port_basis_workers() -> int:
61
+ """Threads for the unit solves of a basis; ``PCB_PORT_BASIS_WORKERS`` overrides (default 1)."""
62
+
63
+ value = os.environ.get("PCB_PORT_BASIS_WORKERS")
64
+ if value:
65
+ return max(1, int(value))
66
+ return 1
67
+
68
+
52
69
  @dataclass(frozen=True)
53
70
  class PortSet:
54
71
  """The pads of one conductor role, one node set per port.
@@ -111,6 +128,7 @@ class DCPortBasis:
111
128
  port ``k`` is held at 1 V and every other port at 0 V.
112
129
  ``unit_current_potential_v[m]`` is the potential field (``mesh.node_shape``)
113
130
  for 1 A into port ``ports.driven[m]`` and out of the reference port.
131
+ ``workers`` is the number of threads the unit solves actually ran on.
114
132
  """
115
133
 
116
134
  mesh: LayeredPCBMesh
@@ -120,6 +138,7 @@ class DCPortBasis:
120
138
  unit_voltage_potential_v: np.ndarray
121
139
  unit_current_potential_v: np.ndarray
122
140
  solves: tuple[MPIRResult, ...]
141
+ workers: int = 1
123
142
 
124
143
  @property
125
144
  def converged(self) -> bool:
@@ -228,13 +247,17 @@ def dc_port_basis(
228
247
  native: bool | None = None,
229
248
  native_threads: int | None = None,
230
249
  initial: DCPortBasis | None = None,
250
+ workers: int | None = None,
231
251
  ) -> DCPortBasis:
232
252
  """Measure the N-port of ``mesh`` at ``ports`` with ``n - 1`` unit voltage solves.
233
253
 
234
254
  One operator with every port node fixed serves all solves. ``initial``
235
255
  warm-starts each unit solve from the same port's field of an earlier
236
256
  basis (for example the previous iterate of a coupled analysis on the
237
- same mesh with other conductivities).
257
+ same mesh with other conductivities). ``workers`` threads run the unit
258
+ solves concurrently (``None``: :func:`port_basis_workers`); the operator
259
+ is read-only once built and every solve owns its vectors, so the result
260
+ does not depend on the thread count. A CUDA runtime solves serially.
238
261
  """
239
262
 
240
263
  operator = MatrixFreePCBOperator(
@@ -256,8 +279,8 @@ def dc_port_basis(
256
279
  n = ports.count
257
280
  conductance = np.zeros((n, n), dtype=np.float64)
258
281
  unit_voltage = np.zeros((n - 1,) + mesh.node_shape, dtype=np.float64)
259
- solves = []
260
- for column, port in enumerate(ports.driven):
282
+
283
+ def unit_solve(column: int, port: int) -> tuple[np.ndarray, np.ndarray, MPIRResult]:
261
284
  excitation = np.zeros(n)
262
285
  excitation[port] = 1.0
263
286
  terminals = ports.voltage_terminals(excitation)
@@ -268,8 +291,22 @@ def dc_port_basis(
268
291
  guess = np.where(operator.free_nodes.reshape(-1), previous, guess)
269
292
  result = solve_mpir(operator, rhs, config=config, initial_guess=guess)
270
293
  potential = result.solution.reshape(mesh.node_shape)
294
+ return potential, operator.terminal_currents(potential, terminals), result
295
+
296
+ threads = port_basis_workers() if workers is None else max(1, int(workers))
297
+ threads = min(threads, n - 1)
298
+ if getattr(operator.runtime, "is_cuda", False):
299
+ threads = 1
300
+ if threads > 1:
301
+ with ThreadPoolExecutor(max_workers=threads, thread_name_prefix="port-basis") as pool:
302
+ outcomes = list(pool.map(unit_solve, range(n - 1), ports.driven))
303
+ else:
304
+ outcomes = [unit_solve(column, port) for column, port in enumerate(ports.driven)]
305
+ solves = []
306
+ for column, port in enumerate(ports.driven):
307
+ potential, currents, result = outcomes[column]
271
308
  unit_voltage[column] = potential
272
- conductance[:, port] = operator.terminal_currents(potential, terminals)
309
+ conductance[:, port] = currents
273
310
  solves.append(result)
274
311
  # The driven block is what the solves measured; it is symmetrised against
275
312
  # solver round-off. The reference row and column follow from KCL exactly
@@ -295,4 +332,5 @@ def dc_port_basis(
295
332
  unit_voltage_potential_v=unit_voltage,
296
333
  unit_current_potential_v=unit_current,
297
334
  solves=tuple(solves),
335
+ workers=threads,
298
336
  )
@@ -518,7 +518,7 @@ def solve_current_field_problem(
518
518
  "tolerance": float(settings.get("relative_tolerance", 1e-9)),
519
519
  "max_iterations": int(settings.get("maximum_iterations", 200)),
520
520
  "restart": int(settings.get("restart", 120)),
521
- "preconditioner": str(settings.get("preconditioner", "near")),
521
+ "preconditioner": str(settings.get("preconditioner", "auto")),
522
522
  }
523
523
  if execution_backend == "cuda":
524
524
  from .sheet_cuda import solve_sheet_case_cuda
@@ -466,7 +466,7 @@ def solve_sheet_case(
466
466
  tolerance: float = 1e-10,
467
467
  max_iterations: int = 400,
468
468
  restart: int = 60,
469
- preconditioner: str = "near",
469
+ preconditioner: str = "auto",
470
470
  ) -> SheetSolution:
471
471
  """Solve one excitation of the mesh.
472
472
 
@@ -477,7 +477,11 @@ def solve_sheet_case(
477
477
  it, ``Z`` couples every branch to every other and is applied through the
478
478
  operator's transforms, so the reduction is carried out by a Krylov method.
479
479
 
480
- ``preconditioner="near"`` (default) factors the saddle-point matrix with
480
+ ``preconditioner="auto"`` (default) picks ``near`` below :data:`AUTO_BLOCK_FROM_UNKNOWNS`
481
+ saddle-point unknowns and ``block`` above. ``preconditioner="block"`` keeps the near-field impedance but factors it
482
+ on its own and approximates the Schur complement by the diagonal
483
+ variant's nodal admittance: block elimination at a fraction of the
484
+ saddle LU's memory. ``preconditioner="near"`` factors the saddle-point matrix with
481
485
  ``Z`` replaced by ``R + j omega L_near``, the exact partial inductance of
482
486
  each branch with itself and its neighbours within the operator's
483
487
  preconditioner radius (``operator.near_inductance(mesh)``), by sparse LU;
@@ -485,8 +489,8 @@ def solve_sheet_case(
485
489
  through a Schur complement but leaves every mutual term to the Krylov
486
490
  iterations.
487
491
  """
488
- if preconditioner not in ("near", "diagonal"):
489
- raise ValueError("preconditioner must be 'near' or 'diagonal'")
492
+ if preconditioner not in ("auto", "near", "block", "diagonal"):
493
+ raise ValueError("preconditioner must be 'auto', 'near', 'block' or 'diagonal'")
490
494
  if operator.shape != mesh.shape:
491
495
  raise ValueError("the operator and the mesh must share a shape")
492
496
  if len(operator.stackup) != len(mesh.stackup):
@@ -629,6 +633,8 @@ def solve_sheet_case(
629
633
  [impedance(currents) - reduced @ voltages, reduced.T @ currents]
630
634
  )
631
635
 
636
+ if preconditioner == "auto":
637
+ preconditioner = choose_preconditioner(size)
632
638
  precondition = _build_preconditioner(
633
639
  mesh, operator, preconditioner, omega, resistance, active_branches, reduced, branches
634
640
  )
@@ -686,6 +692,21 @@ def _near_impedance(
686
692
  return (sp.diags(resistance[active_branches]) + 1j * omega * near).tocsr()
687
693
 
688
694
 
695
+ # Above this many saddle-point unknowns (branches + nodes) the whole-matrix LU
696
+ # of the near preconditioner costs more memory than the block elimination
697
+ # saves in iterations: on a 33 x 30 mm two-layer plane at 800 kHz the near
698
+ # variant needed 2.2 GB and 27 s at 64k branches where block needed 0.5 GB
699
+ # and 21 s, while at 20k branches near was faster (2.1 s against 4.0 s) at
700
+ # 0.3 GB (docs/SHEET_PRECONDITIONER_RESULTS.json).
701
+ AUTO_BLOCK_FROM_UNKNOWNS = 60_000
702
+
703
+
704
+ def choose_preconditioner(unknowns: int) -> str:
705
+ """``near`` for a small saddle-point system, ``block`` for a large one."""
706
+
707
+ return "block" if unknowns >= AUTO_BLOCK_FROM_UNKNOWNS else "near"
708
+
709
+
689
710
  def _build_preconditioner(
690
711
  mesh: SheetMesh,
691
712
  operator: Any,
@@ -703,6 +724,34 @@ def _build_preconditioner(
703
724
  saddle = sp.bmat([[impedance, -reduced], [reduced.T, None]], format="csc")
704
725
  factored = spla.splu(saddle)
705
726
  return factored.solve
727
+ if kind == "block" and hasattr(operator, "near_inductance"):
728
+ # The same near-field impedance, applied by block elimination instead
729
+ # of one LU of the whole saddle-point matrix. Z_near is factored on
730
+ # its own (a local stencil, so the fill stays small), and the Schur
731
+ # complement A^T Z_near^-1 A, which is dense, is approximated by
732
+ # A^T diag(Z)^-1 A, the nodal admittance of the diagonal variant.
733
+ # The saddle LU needs about 13x the memory of these two factors and
734
+ # is what runs a board at the acceptance grid out of memory.
735
+ impedance = _near_impedance(mesh, operator, omega, resistance, active_branches).tocsc()
736
+ factored_z = spla.splu(impedance, permc_spec="MMD_AT_PLUS_A")
737
+ full_diagonal = resistance + 1j * omega * np.concatenate(
738
+ [_inline_self_inductance(mesh, operator), _vertical_self_inductance(mesh, operator)]
739
+ )
740
+ diagonal = full_diagonal[active_branches]
741
+ schur = (reduced.T @ sp.diags(1.0 / diagonal) @ reduced).tocsc()
742
+ factored_s = spla.splu(schur, permc_spec="MMD_AT_PLUS_A")
743
+
744
+ def precondition_block(vector: np.ndarray) -> np.ndarray:
745
+ rhs_current = vector[:branches]
746
+ rhs_node = vector[branches:]
747
+ # [[Z, -A], [A^T, 0]] [x; y] = [r1; r2]:
748
+ # x = Z^-1 (r1 + A y), A^T Z^-1 A y = r2 - A^T Z^-1 r1.
749
+ z = factored_z.solve(rhs_current)
750
+ node = factored_s.solve(rhs_node - reduced.T @ z)
751
+ current = factored_z.solve(rhs_current + reduced @ node)
752
+ return np.concatenate([current, node])
753
+
754
+ return precondition_block
706
755
  # The same system with Z replaced by its diagonal, which is exactly
707
756
  # solvable: its Schur complement is the sparse nodal admittance matrix,
708
757
  # factored once and reused for every iteration.
@@ -360,8 +360,14 @@ def run_circuit_coupled(
360
360
  device_id: int = 0,
361
361
  native: bool | None = None,
362
362
  native_threads: int | None = None,
363
+ port_basis_workers: int | None = None,
363
364
  ) -> CircuitCoupledResult:
364
- """Iterate N-port, circuit and thermal solves to a self-consistent ρ(T) state."""
365
+ """Iterate N-port, circuit and thermal solves to a self-consistent ρ(T) state.
366
+
367
+ ``port_basis_workers`` threads run the unit solves of every N-port basis
368
+ concurrently (see :func:`electrical.matrix_free_mpir_fem.dc_port_basis`);
369
+ ``None`` reads ``PCB_PORT_BASIS_WORKERS``.
370
+ """
365
371
 
366
372
  config = config or CouplingConfig()
367
373
  fixed_point = TemperatureFixedPoint(scenario, config)
@@ -393,6 +399,7 @@ def run_circuit_coupled(
393
399
  native=native,
394
400
  native_threads=native_threads,
395
401
  initial=bases[conductor.name],
402
+ workers=port_basis_workers,
396
403
  )
397
404
  bases[conductor.name] = basis
398
405
  heated[conductor.name] = (mesh, vias)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pcb-analysis
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: CUDA-accelerated PEEC, matrix-free FEM, and radiated-emission evaluation for electrical, thermal, and EMC PCB analysis
5
5
  Author-email: Hiroyuki Yamada <H44bcda@gmail.com>
6
6
  License: MIT License
@@ -227,3 +227,27 @@ def test_invalid_ports_and_correlations_are_rejected() -> None:
227
227
  basis.mean_loss_w(-np.array([[2.0, -1.0, -1.0], [-1.0, 2.0, -1.0], [-1.0, -1.0, 2.0]]))
228
228
  with pytest.raises(ValueError, match="share the ports"):
229
229
  dc_port_basis(mesh, PortSet(pads=ports.pads, reference=0), vias=vias, initial=basis)
230
+
231
+
232
+ def test_threaded_unit_solves_give_the_serial_basis(monkeypatch: pytest.MonkeyPatch) -> None:
233
+ mesh, ports, vias = _three_port_board()
234
+ serial = dc_port_basis(mesh, ports, vias=vias, config=TIGHT, workers=1)
235
+ threaded = dc_port_basis(mesh, ports, vias=vias, config=TIGHT, workers=4)
236
+ assert serial.workers == 1
237
+ assert threaded.workers == 2 # capped at n - 1 unit solves
238
+ assert threaded.converged
239
+ np.testing.assert_array_equal(threaded.conductance_s, serial.conductance_s)
240
+ np.testing.assert_array_equal(threaded.unit_voltage_potential_v, serial.unit_voltage_potential_v)
241
+ np.testing.assert_array_equal(threaded.unit_current_potential_v, serial.unit_current_potential_v)
242
+ assert [r.inner_iterations for r in threaded.solves] == [r.inner_iterations for r in serial.solves]
243
+
244
+ # The default comes from the environment; the warm start works on the pool too.
245
+ monkeypatch.setenv("PCB_PORT_BASIS_WORKERS", "3")
246
+ from electrical.matrix_free_mpir_fem import port_basis_workers
247
+
248
+ assert port_basis_workers() == 3
249
+ warm = dc_port_basis(mesh, ports, vias=vias, config=TIGHT, initial=serial)
250
+ assert warm.workers == 2
251
+ np.testing.assert_allclose(warm.conductance_s, serial.conductance_s, rtol=1.0e-9)
252
+ monkeypatch.delenv("PCB_PORT_BASIS_WORKERS")
253
+ assert port_basis_workers() == 1
@@ -239,6 +239,37 @@ class AlternatingCurrentTests(unittest.TestCase):
239
239
  for smaller, larger in zip(departures, departures[1:]):
240
240
  self.assertAlmostEqual(larger / smaller, 10.0, places=3)
241
241
 
242
+ def test_the_block_preconditioner_reaches_the_near_preconditioner_solution(self):
243
+ # Block elimination with Z_near factored alone and the Schur complement
244
+ # approximated by the diagonal variant's nodal admittance: the same
245
+ # fixed point as the saddle LU, at a fraction of its memory.
246
+ near = solve_sheet_case(
247
+ self.mesh, self.operator, self.terminals, frequency_hz=3e5, preconditioner="near"
248
+ )
249
+ block = solve_sheet_case(
250
+ self.mesh, self.operator, self.terminals, frequency_hz=3e5, preconditioner="block"
251
+ )
252
+ self.assertTrue(near.converged and block.converged)
253
+ scale = float(np.abs(near.node_voltage).max())
254
+ self.assertLess(float(np.abs(block.node_voltage - near.node_voltage).max()) / scale, 1e-7)
255
+ self.assertLess(
256
+ float(np.abs(block.branch_current - near.branch_current).max())
257
+ / float(np.abs(near.branch_current).max()),
258
+ 1e-7,
259
+ )
260
+ self.assertLessEqual(block.iterations, 10 * max(near.iterations, 1))
261
+ with self.assertRaises(ValueError):
262
+ solve_sheet_case(self.mesh, self.operator, self.terminals, frequency_hz=3e5, preconditioner="exact")
263
+ # auto: the small fixture takes the near path and reproduces it exactly.
264
+ auto = solve_sheet_case(
265
+ self.mesh, self.operator, self.terminals, frequency_hz=3e5, preconditioner="auto"
266
+ )
267
+ self.assertEqual(auto.iterations, near.iterations)
268
+ np.testing.assert_allclose(auto.node_voltage, near.node_voltage, rtol=0.0, atol=1e-12 * scale)
269
+ from electrical.sheet_peec.sheet_peec import AUTO_BLOCK_FROM_UNKNOWNS, choose_preconditioner
270
+ self.assertEqual(choose_preconditioner(AUTO_BLOCK_FROM_UNKNOWNS - 1), "near")
271
+ self.assertEqual(choose_preconditioner(AUTO_BLOCK_FROM_UNKNOWNS), "block")
272
+
242
273
  def test_the_two_span_definitions_are_named(self):
243
274
  direct = solve_sheet_case(
244
275
  self.mesh, self.operator, self.terminals, frequency_hz=0.0
File without changes
File without changes
File without changes