pcb-analysis 0.6.0__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 (58) hide show
  1. electrical/__init__.py +10 -0
  2. electrical/dice_peec/__init__.py +117 -0
  3. electrical/dice_peec/_bench_utils.py +70 -0
  4. electrical/dice_peec/backends.py +62 -0
  5. electrical/dice_peec/cli/__init__.py +5 -0
  6. electrical/dice_peec/cli/calibrate.py +64 -0
  7. electrical/dice_peec/controller.py +445 -0
  8. electrical/dice_peec/cuda_delta.py +213 -0
  9. electrical/dice_peec/cuda_pypeec.py +669 -0
  10. electrical/dice_peec/cupy_calibrator.py +73 -0
  11. electrical/dice_peec/delta_peec.py +113 -0
  12. electrical/dice_peec/layout_ops.py +216 -0
  13. electrical/dice_peec/lowmem_25d.py +143 -0
  14. electrical/dice_peec/lowmem_peec.py +165 -0
  15. electrical/dice_peec/multilayer_peec.py +608 -0
  16. electrical/dice_peec/plane_opt_contract.py +593 -0
  17. electrical/dice_peec/pypeec_memory.py +252 -0
  18. electrical/dice_peec/sheet_cuda.py +687 -0
  19. electrical/dice_peec/sheet_inductance.py +545 -0
  20. electrical/dice_peec/sheet_operator.py +322 -0
  21. electrical/dice_peec/sheet_peec.py +676 -0
  22. electrical/dice_peec/sheet_results.py +199 -0
  23. electrical/dice_peec/skin_filaments.py +387 -0
  24. electrical/dice_peec/stackup.py +102 -0
  25. electrical/matrix_free_mpir_fem/__init__.py +80 -0
  26. electrical/matrix_free_mpir_fem/cuda.py +156 -0
  27. electrical/matrix_free_mpir_fem/frequency_domain.py +505 -0
  28. electrical/matrix_free_mpir_fem/pcb.py +480 -0
  29. electrical/matrix_free_mpir_fem/runtime.py +314 -0
  30. electrical/matrix_free_mpir_fem/solver.py +468 -0
  31. electrical/py.typed +1 -0
  32. emc/__init__.py +11 -0
  33. emc/py.typed +0 -0
  34. emc/tiled_dipole_superposition/__init__.py +74 -0
  35. emc/tiled_dipole_superposition/far_field.py +204 -0
  36. emc/tiled_dipole_superposition/fields.py +241 -0
  37. emc/tiled_dipole_superposition/limits.py +168 -0
  38. emc/tiled_dipole_superposition/moments.py +86 -0
  39. emc/tiled_dipole_superposition/sources.py +188 -0
  40. multiphysics/__init__.py +11 -0
  41. multiphysics/py.typed +0 -0
  42. multiphysics/staggered_coupling/__init__.py +71 -0
  43. multiphysics/staggered_coupling/electro_thermal.py +373 -0
  44. multiphysics/staggered_coupling/emission.py +192 -0
  45. multiphysics/staggered_coupling/scenarios.py +213 -0
  46. pcb_analysis-0.6.0.dist-info/LICENSE +21 -0
  47. pcb_analysis-0.6.0.dist-info/METADATA +546 -0
  48. pcb_analysis-0.6.0.dist-info/RECORD +58 -0
  49. pcb_analysis-0.6.0.dist-info/WHEEL +5 -0
  50. pcb_analysis-0.6.0.dist-info/entry_points.txt +2 -0
  51. pcb_analysis-0.6.0.dist-info/top_level.txt +4 -0
  52. thermal/__init__.py +11 -0
  53. thermal/matrix_free_mpir_fem/__init__.py +38 -0
  54. thermal/matrix_free_mpir_fem/conduction.py +847 -0
  55. thermal/matrix_free_mpir_fem/coupling.py +98 -0
  56. thermal/matrix_free_mpir_fem/cuda.py +160 -0
  57. thermal/matrix_free_mpir_fem/two_level.py +216 -0
  58. thermal/py.typed +0 -0
electrical/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Electrical analysis algorithms.
2
+
3
+ The second package level names the numerical method and its acceleration
4
+ strategy. Thermal and other physics can therefore be added beside this
5
+ package without mixing their discretisations or solver policies.
6
+ """
7
+
8
+ from . import dice_peec, matrix_free_mpir_fem
9
+
10
+ __all__ = ["dice_peec", "matrix_free_mpir_fem"]
@@ -0,0 +1,117 @@
1
+ """DICE-PEEC acceleration for single- and multilayer electrical PCB analysis.
2
+
3
+ There are two solvers here and they answer different questions.
4
+
5
+ ``cuda_pypeec`` runs PyPEEC's own three-dimensional voxel solve on CUDA. It is
6
+ the high-fidelity path: PyPEEC owns the physics, this owns the device policy,
7
+ and ``pypeec_memory`` predicts what a model will cost before it is attempted --
8
+ which matters once a model spans a board's height rather than one copper layer.
9
+
10
+ ``sheet_peec`` solves the same physics on a mesh built for what a PCB actually
11
+ is: a few thin sheets at known heights. Its inductance operator is a
12
+ two-dimensional transform per layer pair instead of a three-dimensional one
13
+ over the board's height, which is a large saving on a board of few layers. It
14
+ is exact in the sense that matters -- at zero frequency it reproduces a
15
+ resistor network to machine precision, and above it, the transform path matches
16
+ a dense assembly of the same operator.
17
+
18
+ Separately, ``multilayer_peec`` holds a scalar interaction proxy for ranking
19
+ many candidate shapes cheaply. It does not solve for current or potential and
20
+ is not a substitute for either solver above.
21
+ """
22
+
23
+ from .layout_ops import (
24
+ CandidateEdit,
25
+ CompiledCandidate,
26
+ SegmentOp,
27
+ ViaOp,
28
+ compile_candidate,
29
+ compile_many,
30
+ )
31
+ from .multilayer_peec import (
32
+ FFTInteraction25D,
33
+ MultilayerDeltaScorer,
34
+ SparseDeltaML,
35
+ ViaSet,
36
+ ViaSpec,
37
+ )
38
+ from .plane_opt_contract import (
39
+ PLANE_OPT_PROBLEM_SCHEMA,
40
+ PLANE_OPT_RESULT_SCHEMA,
41
+ PlaneOptProblem,
42
+ PlaneOptSolveResult,
43
+ build_plane_opt_sheet_inputs,
44
+ solve_plane_opt_problem,
45
+ )
46
+ from .sheet_inductance import (
47
+ CellGeometry,
48
+ build_kernel,
49
+ mutual_partial_inductance,
50
+ self_partial_inductance,
51
+ )
52
+ from .sheet_cuda import (
53
+ CudaSheetSolveError,
54
+ CudaSheetTelemetry,
55
+ CudaSheetUnavailableError,
56
+ solve_sheet_case_cuda,
57
+ )
58
+ from .sheet_operator import SheetInductanceOperator, SheetLayer, SheetStackup
59
+ from .sheet_results import (
60
+ SheetFields,
61
+ cell_current_density,
62
+ cell_current_density_phasor,
63
+ sheet_fields,
64
+ vertical_currents,
65
+ )
66
+ from .sheet_peec import (
67
+ SheetMesh,
68
+ SheetSolution,
69
+ Terminal,
70
+ ViaBranch,
71
+ solve_sheet_case,
72
+ via_resistance,
73
+ )
74
+ from .stackup import Stackup
75
+
76
+ __all__ = [
77
+ "CandidateEdit",
78
+ "CellGeometry",
79
+ "CompiledCandidate",
80
+ "CudaSheetSolveError",
81
+ "CudaSheetTelemetry",
82
+ "CudaSheetUnavailableError",
83
+ "FFTInteraction25D",
84
+ "MultilayerDeltaScorer",
85
+ "PLANE_OPT_PROBLEM_SCHEMA",
86
+ "PLANE_OPT_RESULT_SCHEMA",
87
+ "PlaneOptProblem",
88
+ "PlaneOptSolveResult",
89
+ "SegmentOp",
90
+ "SheetFields",
91
+ "SheetInductanceOperator",
92
+ "SheetLayer",
93
+ "SheetMesh",
94
+ "SheetSolution",
95
+ "SheetStackup",
96
+ "SparseDeltaML",
97
+ "Stackup",
98
+ "Terminal",
99
+ "ViaBranch",
100
+ "ViaOp",
101
+ "ViaSet",
102
+ "ViaSpec",
103
+ "build_kernel",
104
+ "cell_current_density",
105
+ "cell_current_density_phasor",
106
+ "compile_candidate",
107
+ "compile_many",
108
+ "build_plane_opt_sheet_inputs",
109
+ "mutual_partial_inductance",
110
+ "self_partial_inductance",
111
+ "sheet_fields",
112
+ "solve_sheet_case",
113
+ "solve_sheet_case_cuda",
114
+ "solve_plane_opt_problem",
115
+ "via_resistance",
116
+ "vertical_currents",
117
+ ]
@@ -0,0 +1,70 @@
1
+ """Shared DICE-PEEC benchmark utility functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+
9
+ def relative_difference(reference: float, observed: float) -> float:
10
+ reference = float(reference)
11
+ observed = float(observed)
12
+ if not math.isfinite(reference) or not math.isfinite(observed):
13
+ return math.inf
14
+ scale = max(abs(reference), 1e-30)
15
+ return abs(observed - reference) / scale
16
+
17
+
18
+ def ranking_consistent(
19
+ cpu: dict[str, dict[str, Any]],
20
+ cuda: dict[str, dict[str, Any]],
21
+ metric: str,
22
+ *,
23
+ separation: float = 0.01,
24
+ ) -> bool:
25
+ """Check every meaningfully separated CPU pair retains its order."""
26
+ if not math.isfinite(separation) or separation < 0.0:
27
+ raise ValueError("separation must be finite and non-negative")
28
+ if set(cpu) != set(cuda):
29
+ return False
30
+ names = sorted(cpu)
31
+ for left_index, left in enumerate(names):
32
+ for right in names[left_index + 1 :]:
33
+ cpu_left = float(cpu[left][metric])
34
+ cpu_right = float(cpu[right][metric])
35
+ cuda_left = float(cuda[left][metric])
36
+ cuda_right = float(cuda[right][metric])
37
+ if not all(
38
+ math.isfinite(value)
39
+ for value in (cpu_left, cpu_right, cuda_left, cuda_right)
40
+ ):
41
+ return False
42
+ scale = max(abs(cpu_left), abs(cpu_right), 1e-30)
43
+ if abs(cpu_left - cpu_right) / scale <= separation:
44
+ continue
45
+ cpu_delta = cpu_left - cpu_right
46
+ cuda_delta = cuda_left - cuda_right
47
+ if cuda_delta == 0.0 or (cpu_delta < 0.0) != (cuda_delta < 0.0):
48
+ return False
49
+ return True
50
+
51
+
52
+ def _select_candidate(
53
+ data: dict[str, Any], candidate_name: str | None
54
+ ) -> dict[str, Any]:
55
+ if "mask_runs" in data:
56
+ return data
57
+ if "candidate" in data:
58
+ return data["candidate"]
59
+ candidates = list(data.get("candidates", []))
60
+ if candidate_name is not None:
61
+ for candidate in candidates:
62
+ if candidate.get("name") == candidate_name:
63
+ return candidate
64
+ raise ValueError(f"candidate not found: {candidate_name}")
65
+ valid = [candidate for candidate in candidates if candidate.get("valid")]
66
+ if valid:
67
+ return valid[0]
68
+ if candidates:
69
+ return candidates[0]
70
+ raise ValueError("candidate file does not contain mask_runs or candidates")
@@ -0,0 +1,62 @@
1
+ """DICE-PEEC execution backend contracts and a deterministic CPU simulator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+ from .controller import (
9
+ ExecutionPlan,
10
+ ExecutionReport,
11
+ HardwareTelemetry,
12
+ ProblemProfile,
13
+ )
14
+
15
+
16
+ class CalibrationBackend(Protocol):
17
+ def probe(self) -> HardwareTelemetry: ...
18
+
19
+ def execute(self, plan: ExecutionPlan, problem: ProblemProfile) -> ExecutionReport: ...
20
+
21
+
22
+ @dataclass
23
+ class SyntheticBackend:
24
+ """Predictable backend for controller tests without a CUDA device."""
25
+
26
+ total_bytes: int
27
+ platform: str = "windows"
28
+ free_fraction: float = 0.90
29
+ memory_multiplier: float = 1.08
30
+ force_oom: bool = False
31
+ force_stagnation: bool = False
32
+
33
+ def probe(self) -> HardwareTelemetry:
34
+ return HardwareTelemetry(
35
+ total_bytes=self.total_bytes,
36
+ free_bytes=int(self.total_bytes * self.free_fraction),
37
+ platform=self.platform,
38
+ backend="synthetic",
39
+ )
40
+
41
+ def execute(self, plan: ExecutionPlan, problem: ProblemProfile) -> ExecutionReport:
42
+ peak = int(plan.estimated_bytes * self.memory_multiplier)
43
+ oom = self.force_oom or peak > self.probe().free_bytes
44
+ work = (
45
+ problem.nx * problem.ny * max(problem.layers, 1)
46
+ / max(plan.tile_size**2 * plan.candidate_batch, 1)
47
+ )
48
+ elapsed = 0.08 * work * max(1, 8 / max(plan.kernel_batch, 1))
49
+ if plan.host_offload:
50
+ elapsed *= 1.8
51
+ iterations = 0 if plan.solver == "none" else (34 if plan.solver == "gmres" else 52)
52
+ stagnated = self.force_stagnation and plan.solver.startswith("bicgstab")
53
+ return ExecutionReport(
54
+ peak_bytes=peak,
55
+ elapsed_ms=elapsed,
56
+ iterations=iterations,
57
+ relative_residual=2e-3 if stagnated else plan.tolerance * 0.5,
58
+ converged=not (oom or stagnated),
59
+ oom=oom,
60
+ stagnated=stagnated,
61
+ precision_gap=0.0,
62
+ )
@@ -0,0 +1,5 @@
1
+ """Command-line entry points for DICE-PEEC.
2
+
3
+ Scripts under ``examples/`` are repository material and are not installed, so
4
+ anything reachable from ``[project.scripts]`` lives here instead.
5
+ """
@@ -0,0 +1,64 @@
1
+ """Measure this machine's CUDA and CuPy throughput for the DICE controller.
2
+
3
+ Installed as ``pcb-analysis-calibrate``. CuPy is an optional dependency,
4
+ so importing it is left to call time: the package must import on a host
5
+ with no GPU.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ from dataclasses import asdict
13
+
14
+ from ..controller import DynamicController, FidelityStage, ProblemProfile
15
+ from ..cupy_calibrator import CuPyCalibrationBackend
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser()
20
+ parser.add_argument("--grid", type=int, default=1024)
21
+ parser.add_argument("--layers", type=int, default=4)
22
+ parser.add_argument("--unknowns", type=int, default=2_000_000)
23
+ parser.add_argument("--candidates", type=int, default=1000)
24
+ parser.add_argument("--changed-cells", type=int, default=32)
25
+ parser.add_argument("--repeats", type=int, default=5)
26
+ args = parser.parse_args()
27
+
28
+ backend = CuPyCalibrationBackend(repeats=args.repeats)
29
+ telemetry = backend.probe()
30
+ problem = ProblemProfile(
31
+ nx=args.grid, ny=args.grid, layers=args.layers,
32
+ unknowns=args.unknowns, candidate_count=args.candidates,
33
+ changed_cells_mean=args.changed_cells,
34
+ )
35
+ controller = DynamicController()
36
+ output = {
37
+ "telemetry": asdict(telemetry),
38
+ "stages": [],
39
+ }
40
+ for stage in (
41
+ FidelityStage.NEAR_COARSE,
42
+ FidelityStage.NEAR_FINE,
43
+ FidelityStage.CORRECTION,
44
+ FidelityStage.REFINED,
45
+ ):
46
+ plan = controller.make_plan(problem, telemetry, stage)
47
+ report = backend.execute(plan, problem)
48
+ if report.oom:
49
+ plan = controller.make_plan(
50
+ problem, backend.probe(), stage,
51
+ previous_report=report, previous_plan=plan,
52
+ )
53
+ report = backend.execute(plan, problem)
54
+ controller.observe(plan, report)
55
+ output["stages"].append({
56
+ "stage": stage.name,
57
+ "plan": asdict(plan),
58
+ "fft_calibration": asdict(report),
59
+ })
60
+ print(json.dumps(output, indent=2))
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()