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.
- electrical/__init__.py +10 -0
- electrical/dice_peec/__init__.py +117 -0
- electrical/dice_peec/_bench_utils.py +70 -0
- electrical/dice_peec/backends.py +62 -0
- electrical/dice_peec/cli/__init__.py +5 -0
- electrical/dice_peec/cli/calibrate.py +64 -0
- electrical/dice_peec/controller.py +445 -0
- electrical/dice_peec/cuda_delta.py +213 -0
- electrical/dice_peec/cuda_pypeec.py +669 -0
- electrical/dice_peec/cupy_calibrator.py +73 -0
- electrical/dice_peec/delta_peec.py +113 -0
- electrical/dice_peec/layout_ops.py +216 -0
- electrical/dice_peec/lowmem_25d.py +143 -0
- electrical/dice_peec/lowmem_peec.py +165 -0
- electrical/dice_peec/multilayer_peec.py +608 -0
- electrical/dice_peec/plane_opt_contract.py +593 -0
- electrical/dice_peec/pypeec_memory.py +252 -0
- electrical/dice_peec/sheet_cuda.py +687 -0
- electrical/dice_peec/sheet_inductance.py +545 -0
- electrical/dice_peec/sheet_operator.py +322 -0
- electrical/dice_peec/sheet_peec.py +676 -0
- electrical/dice_peec/sheet_results.py +199 -0
- electrical/dice_peec/skin_filaments.py +387 -0
- electrical/dice_peec/stackup.py +102 -0
- electrical/matrix_free_mpir_fem/__init__.py +80 -0
- electrical/matrix_free_mpir_fem/cuda.py +156 -0
- electrical/matrix_free_mpir_fem/frequency_domain.py +505 -0
- electrical/matrix_free_mpir_fem/pcb.py +480 -0
- electrical/matrix_free_mpir_fem/runtime.py +314 -0
- electrical/matrix_free_mpir_fem/solver.py +468 -0
- electrical/py.typed +1 -0
- emc/__init__.py +11 -0
- emc/py.typed +0 -0
- emc/tiled_dipole_superposition/__init__.py +74 -0
- emc/tiled_dipole_superposition/far_field.py +204 -0
- emc/tiled_dipole_superposition/fields.py +241 -0
- emc/tiled_dipole_superposition/limits.py +168 -0
- emc/tiled_dipole_superposition/moments.py +86 -0
- emc/tiled_dipole_superposition/sources.py +188 -0
- multiphysics/__init__.py +11 -0
- multiphysics/py.typed +0 -0
- multiphysics/staggered_coupling/__init__.py +71 -0
- multiphysics/staggered_coupling/electro_thermal.py +373 -0
- multiphysics/staggered_coupling/emission.py +192 -0
- multiphysics/staggered_coupling/scenarios.py +213 -0
- pcb_analysis-0.6.0.dist-info/LICENSE +21 -0
- pcb_analysis-0.6.0.dist-info/METADATA +546 -0
- pcb_analysis-0.6.0.dist-info/RECORD +58 -0
- pcb_analysis-0.6.0.dist-info/WHEEL +5 -0
- pcb_analysis-0.6.0.dist-info/entry_points.txt +2 -0
- pcb_analysis-0.6.0.dist-info/top_level.txt +4 -0
- thermal/__init__.py +11 -0
- thermal/matrix_free_mpir_fem/__init__.py +38 -0
- thermal/matrix_free_mpir_fem/conduction.py +847 -0
- thermal/matrix_free_mpir_fem/coupling.py +98 -0
- thermal/matrix_free_mpir_fem/cuda.py +160 -0
- thermal/matrix_free_mpir_fem/two_level.py +216 -0
- 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,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()
|