loadpath 0.1.1__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.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
loadpath/am.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overhang control for additive manufacturing: Langelaar's AM filter
|
|
3
|
+
(Langelaar 2017, "An additive manufacturing filter for topology
|
|
4
|
+
optimization of print-ready designs", SMO 55).
|
|
5
|
+
|
|
6
|
+
The part is printed layer by layer along a build direction. A cell can only
|
|
7
|
+
be printed if the layer below supports it, so its "printed" density is the
|
|
8
|
+
smaller of its own density and the strongest support beneath it:
|
|
9
|
+
|
|
10
|
+
printed_i = smin(x_i, smax(printed_j for j in S_i))
|
|
11
|
+
|
|
12
|
+
where S_i is the cell directly below and its four neighbors in that layer.
|
|
13
|
+
That stencil allows overhangs up to 45 degrees, the usual self-supporting
|
|
14
|
+
limit for FDM and for metal powder-bed printing. The first layer sits on the
|
|
15
|
+
build plate. Running the optimizer on printed densities means material that
|
|
16
|
+
would need support simply earns nothing, so the design grows its own
|
|
17
|
+
supports: Y-shapes instead of flat ceilings.
|
|
18
|
+
|
|
19
|
+
smax/smin are smooth so gradients exist; the backward pass carries
|
|
20
|
+
sensitivities down through the layers (each layer's support depends on
|
|
21
|
+
every layer under it).
|
|
22
|
+
|
|
23
|
+
Any build direction works, not only the six grid axes. For a tilted one
|
|
24
|
+
(`TiltedAMFilter`) the support stencil is every neighbor among the 26 that
|
|
25
|
+
lies within 45 degrees of straight down, and the "layers" are bands of the
|
|
26
|
+
height along the build direction narrow enough that a cell's supports are
|
|
27
|
+
always in an earlier band. Along a grid axis that stencil is exactly
|
|
28
|
+
Langelaar's five cells.
|
|
29
|
+
|
|
30
|
+
The module also carries what a layered process does to the material
|
|
31
|
+
(`layered_elasticity`): FDM parts are slightly softer and much weaker across
|
|
32
|
+
layers than along them. Verification always solves with the layered
|
|
33
|
+
stiffness and checks the bond. The optimizer can design with it too
|
|
34
|
+
(process.anisotropic: true), or with the across-layer terms cut to the
|
|
35
|
+
bond's strength ratio ("bond"), a surrogate meant to stop compliance
|
|
36
|
+
minimization leaning on material loaded across its layers. Measured on the
|
|
37
|
+
motor mount and the printed hook at fixed volume fraction (2026-10-29),
|
|
38
|
+
neither beat the isotropic design reliably: compliance cannot tell tension
|
|
39
|
+
across layers (which breaks bonds) from compression (which doesn't), and
|
|
40
|
+
the layout shift moved the von Mises peak more than it saved the bond. So
|
|
41
|
+
the default designs isotropic; the bond enters through the safety-factor
|
|
42
|
+
search (verified margins include it) and the choice of build direction
|
|
43
|
+
(orient.py).
|
|
44
|
+
"""
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
__all__ = ["AMFilter", "TiltedAMFilter", "parse_build_direction", "build_vector", "layered_elasticity",
|
|
50
|
+
"direction_label"]
|
|
51
|
+
|
|
52
|
+
_P = 40.0
|
|
53
|
+
_XI0 = 0.5
|
|
54
|
+
_EPS = 1e-4
|
|
55
|
+
_N_SUPPORT = 5
|
|
56
|
+
_Q = _P + np.log(_N_SUPPORT) / np.log(_XI0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_build_direction(spec):
|
|
60
|
+
"""'+z' -> (axis 2, +1). Layers stack along the direction; the build
|
|
61
|
+
plate is at the far end opposite it. A vector [x, y, z] gives a tilted
|
|
62
|
+
build: returned as a unit 3-tuple, or as (axis, sign) when it lies on a
|
|
63
|
+
grid axis."""
|
|
64
|
+
if spec is None:
|
|
65
|
+
return None
|
|
66
|
+
if isinstance(spec, (list, tuple, np.ndarray)) and len(spec) == 3:
|
|
67
|
+
v = np.asarray(spec, float)
|
|
68
|
+
nrm = np.linalg.norm(v)
|
|
69
|
+
if not np.isfinite(nrm) or nrm == 0:
|
|
70
|
+
raise ValueError("printDirection vector must be nonzero")
|
|
71
|
+
v = v / nrm
|
|
72
|
+
ax = int(np.argmax(np.abs(v)))
|
|
73
|
+
if abs(v[ax]) > 1 - 1e-9:
|
|
74
|
+
return ax, (1 if v[ax] > 0 else -1)
|
|
75
|
+
return tuple(float(c) for c in v)
|
|
76
|
+
if isinstance(spec, tuple) and len(spec) == 2:
|
|
77
|
+
return int(spec[0]), int(spec[1])
|
|
78
|
+
s = str(spec).strip().lower()
|
|
79
|
+
if len(s) == 1:
|
|
80
|
+
s = "+" + s
|
|
81
|
+
if len(s) != 2 or s[0] not in "+-" or s[1] not in "xyz":
|
|
82
|
+
raise ValueError("printDirection must be one of +x, -x, +y, -y, +z, -z (the direction layers stack), "
|
|
83
|
+
"or a vector [x, y, z]")
|
|
84
|
+
return "xyz".index(s[1]), (1 if s[0] == "+" else -1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_vector(direction) -> np.ndarray:
|
|
88
|
+
"""Unit vector of a build direction in either form."""
|
|
89
|
+
if len(direction) == 2:
|
|
90
|
+
v = np.zeros(3)
|
|
91
|
+
v[int(direction[0])] = float(direction[1])
|
|
92
|
+
return v
|
|
93
|
+
v = np.asarray(direction, float)
|
|
94
|
+
return v / np.linalg.norm(v)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def direction_label(direction) -> str:
|
|
98
|
+
if direction is None:
|
|
99
|
+
return "none"
|
|
100
|
+
if len(direction) == 2:
|
|
101
|
+
return ("+" if direction[1] > 0 else "-") + "xyz"[direction[0]]
|
|
102
|
+
return "[" + ", ".join(f"{c:.3f}" for c in direction) + "]"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Layered stiffness, as fractions of the in-plane values. Printed at full
|
|
106
|
+
# density FDM loses little stiffness across layers (published tensile
|
|
107
|
+
# moduli across layers sit at 80-95% of in-plane for PLA/PETG/ABS) but the
|
|
108
|
+
# bond carries 50-70% of the strength, and less in shear.
|
|
109
|
+
_LAYER_STIFFNESS = {"fdm": (0.85, 0.75), "sls": (0.95, 0.9), "mjf": (0.97, 0.95), "lpbf": (0.97, 0.95)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def layered_elasticity(nu: float, process, design: bool = False):
|
|
113
|
+
"""The elasticity a solver should use for a material made by `process`
|
|
114
|
+
(spec.Process): a plain Poisson's ratio when the process has no layers
|
|
115
|
+
worth modeling, else a fem.Layered about the build direction.
|
|
116
|
+
|
|
117
|
+
design=True is the "bond" surrogate: across-layer modulus and shear
|
|
118
|
+
capped at the bond's strength ratio (see the module note for how it
|
|
119
|
+
fared)."""
|
|
120
|
+
if process is None or process.build is None or process.build == "auto":
|
|
121
|
+
return nu
|
|
122
|
+
ez, gz = _LAYER_STIFFNESS.get(process.kind, (1.0, 1.0))
|
|
123
|
+
ez = getattr(process, "ez", None) or ez
|
|
124
|
+
gz = getattr(process, "gz", None) or gz
|
|
125
|
+
if design:
|
|
126
|
+
ez = min(ez, process.interlayer)
|
|
127
|
+
gz = min(gz, process.interlayer)
|
|
128
|
+
if ez >= 1.0 and gz >= 1.0:
|
|
129
|
+
return nu
|
|
130
|
+
from .fem import Layered
|
|
131
|
+
return Layered(nu, ez, gz, build_vector(process.build))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _smin(x, xi):
|
|
135
|
+
d = x - xi
|
|
136
|
+
r = np.sqrt(d * d + _EPS)
|
|
137
|
+
val = 0.5 * (x + xi - r + np.sqrt(_EPS))
|
|
138
|
+
dx = 0.5 * (1.0 - d / r)
|
|
139
|
+
dxi = 0.5 * (1.0 + d / r)
|
|
140
|
+
return val, dx, dxi
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _neighbors(layer):
|
|
144
|
+
"""The 5 support cells under each cell, as shifted copies of the layer
|
|
145
|
+
below (outside the grid counts as empty)."""
|
|
146
|
+
out = [layer]
|
|
147
|
+
for ax in (0, 1):
|
|
148
|
+
for sh in (1, -1):
|
|
149
|
+
s = np.roll(layer, sh, axis=ax)
|
|
150
|
+
idx = [slice(None), slice(None)]
|
|
151
|
+
idx[ax] = slice(0, 1) if sh == 1 else slice(-1, None)
|
|
152
|
+
s[tuple(idx)] = 0.0
|
|
153
|
+
out.append(s)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _unshift(grads):
|
|
158
|
+
"""Adjoint of _neighbors: route each shifted copy's gradient back to the
|
|
159
|
+
cell it came from."""
|
|
160
|
+
total = grads[0].copy()
|
|
161
|
+
k = 1
|
|
162
|
+
for ax in (0, 1):
|
|
163
|
+
for sh in (1, -1):
|
|
164
|
+
g = grads[k].copy()
|
|
165
|
+
idx = [slice(None), slice(None)]
|
|
166
|
+
idx[ax] = slice(0, 1) if sh == 1 else slice(-1, None)
|
|
167
|
+
g[tuple(idx)] = 0.0
|
|
168
|
+
total += np.roll(g, -sh, axis=ax)
|
|
169
|
+
k += 1
|
|
170
|
+
return total
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AMFilter:
|
|
174
|
+
def __new__(cls, dims, direction, pinned=None):
|
|
175
|
+
if cls is AMFilter and len(direction) == 3:
|
|
176
|
+
return TiltedAMFilter(dims, direction, pinned)
|
|
177
|
+
return super().__new__(cls)
|
|
178
|
+
|
|
179
|
+
def __init__(self, dims, direction: tuple[int, int], pinned: np.ndarray | None = None):
|
|
180
|
+
self.dims = tuple(dims)
|
|
181
|
+
self.axis, self.sign = direction
|
|
182
|
+
# Pinned cells (preserved geometry) print as solid regardless: they
|
|
183
|
+
# are the user's, and they support what is above them.
|
|
184
|
+
self.pinned = None if pinned is None else self._to_layers(pinned.astype(float))
|
|
185
|
+
self._cache = None
|
|
186
|
+
|
|
187
|
+
def _to_layers(self, g):
|
|
188
|
+
"""Move the build axis first, ordered from the build plate up."""
|
|
189
|
+
g = np.moveaxis(g, self.axis, 0)
|
|
190
|
+
return g if self.sign > 0 else g[::-1]
|
|
191
|
+
|
|
192
|
+
def _from_layers(self, g):
|
|
193
|
+
g = g if self.sign > 0 else g[::-1]
|
|
194
|
+
return np.moveaxis(g, 0, self.axis)
|
|
195
|
+
|
|
196
|
+
def forward(self, field: np.ndarray) -> np.ndarray:
|
|
197
|
+
x = self._to_layers(np.asarray(field, float))
|
|
198
|
+
n = x.shape[0]
|
|
199
|
+
printed = np.empty_like(x)
|
|
200
|
+
cache = []
|
|
201
|
+
for k in range(n):
|
|
202
|
+
if k == 0:
|
|
203
|
+
xi = np.ones_like(x[0])
|
|
204
|
+
parts, ssum = None, None
|
|
205
|
+
else:
|
|
206
|
+
parts = _neighbors(printed[k - 1])
|
|
207
|
+
ssum = sum(np.maximum(p, 0.0) ** _P for p in parts)
|
|
208
|
+
xi = np.maximum(ssum, 1e-300) ** (1.0 / _Q)
|
|
209
|
+
val, dx, dxi = _smin(x[k], xi)
|
|
210
|
+
if self.pinned is not None:
|
|
211
|
+
pin = self.pinned[k] > 0.5
|
|
212
|
+
val = np.where(pin, 1.0, val)
|
|
213
|
+
dx = np.where(pin, 0.0, dx)
|
|
214
|
+
dxi = np.where(pin, 0.0, dxi)
|
|
215
|
+
printed[k] = val
|
|
216
|
+
cache.append((parts, ssum, dx, dxi))
|
|
217
|
+
self._cache = cache
|
|
218
|
+
return self._from_layers(printed)
|
|
219
|
+
|
|
220
|
+
def backward(self, grad: np.ndarray) -> np.ndarray:
|
|
221
|
+
"""Gradient wrt the input field of the last forward(), given the
|
|
222
|
+
gradient wrt its output."""
|
|
223
|
+
g = self._to_layers(np.asarray(grad, float)).copy()
|
|
224
|
+
n = g.shape[0]
|
|
225
|
+
out = np.zeros_like(g)
|
|
226
|
+
carry = np.zeros_like(g[0])
|
|
227
|
+
for k in range(n - 1, -1, -1):
|
|
228
|
+
parts, ssum, dx, dxi = self._cache[k]
|
|
229
|
+
total = g[k] + carry
|
|
230
|
+
out[k] = total * dx
|
|
231
|
+
carry = np.zeros_like(g[0])
|
|
232
|
+
if k > 0:
|
|
233
|
+
# d xi / d support_j = (P/Q) s_j^(P-1) * ssum^(1/Q - 1)
|
|
234
|
+
common = total * dxi * (_P / _Q) * np.maximum(ssum, 1e-300) ** (1.0 / _Q - 1.0)
|
|
235
|
+
grads = [common * np.maximum(p, 0.0) ** (_P - 1.0) for p in parts]
|
|
236
|
+
carry = _unshift(grads)
|
|
237
|
+
return self._from_layers(out)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TiltedAMFilter:
|
|
241
|
+
"""The AM filter for a build direction off the grid axes (see the module
|
|
242
|
+
note). The build plate is the plane through the lowest cell of the grid
|
|
243
|
+
along the direction unless `plate` (a bool grid) says which cells sit on
|
|
244
|
+
it: a support-free tilted print has to grow from wherever it touches."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, dims, direction, pinned: np.ndarray | None = None, plate: np.ndarray | None = None):
|
|
247
|
+
self.dims = tuple(int(d) for d in dims)
|
|
248
|
+
up = build_vector(direction)
|
|
249
|
+
offs = np.array([[i, j, k] for i in (-1, 0, 1) for j in (-1, 0, 1) for k in (-1, 0, 1)
|
|
250
|
+
if (i, j, k) != (0, 0, 0)], float)
|
|
251
|
+
proj = offs @ up
|
|
252
|
+
cosang = -proj / np.linalg.norm(offs, axis=1)
|
|
253
|
+
sel = cosang >= np.cos(np.radians(45.0)) - 1e-9
|
|
254
|
+
self.offsets = offs[sel].astype(np.int64)
|
|
255
|
+
n_sup = len(self.offsets)
|
|
256
|
+
self._q = _P + np.log(n_sup) / np.log(_XI0)
|
|
257
|
+
delta = float((-proj[sel]).min())
|
|
258
|
+
ijk = np.indices(self.dims).reshape(3, -1).T
|
|
259
|
+
h = ijk @ up
|
|
260
|
+
level = np.floor((h - h.min()) / delta + 1e-9).astype(np.int64)
|
|
261
|
+
n = len(ijk)
|
|
262
|
+
self._nbr = []
|
|
263
|
+
for o in self.offsets:
|
|
264
|
+
q = ijk + o
|
|
265
|
+
ok = np.all((q >= 0) & (q < np.array(self.dims)), axis=1)
|
|
266
|
+
flat = np.full(n, -1, np.int64)
|
|
267
|
+
flat[ok] = np.ravel_multi_index(q[ok].T, self.dims)
|
|
268
|
+
self._nbr.append(flat)
|
|
269
|
+
order = np.argsort(level, kind="stable")
|
|
270
|
+
bounds = np.searchsorted(level[order], np.arange(level.max() + 2))
|
|
271
|
+
self._groups = [order[bounds[k]:bounds[k + 1]] for k in range(len(bounds) - 1)
|
|
272
|
+
if bounds[k + 1] > bounds[k]]
|
|
273
|
+
if plate is None:
|
|
274
|
+
plate = level == 0
|
|
275
|
+
else:
|
|
276
|
+
plate = np.asarray(plate, bool).ravel()
|
|
277
|
+
self._plate = plate
|
|
278
|
+
self._pinned = None if pinned is None else np.asarray(pinned, bool).ravel()
|
|
279
|
+
self._cache = None
|
|
280
|
+
|
|
281
|
+
def forward(self, field: np.ndarray) -> np.ndarray:
|
|
282
|
+
x = np.asarray(field, float).ravel()
|
|
283
|
+
printed = np.zeros_like(x)
|
|
284
|
+
padded = np.zeros(len(x) + 1) # slot -1 reads as empty
|
|
285
|
+
cache = []
|
|
286
|
+
for idx in self._groups:
|
|
287
|
+
nb = [f[idx] for f in self._nbr]
|
|
288
|
+
parts = [padded[b] for b in nb]
|
|
289
|
+
ssum = sum(np.maximum(p, 0.0) ** _P for p in parts)
|
|
290
|
+
xi = np.maximum(ssum, 1e-300) ** (1.0 / self._q)
|
|
291
|
+
on_plate = self._plate[idx]
|
|
292
|
+
xi = np.where(on_plate, 1.0, xi)
|
|
293
|
+
val, dx, dxi = _smin(x[idx], xi)
|
|
294
|
+
dxi = np.where(on_plate, 0.0, dxi)
|
|
295
|
+
if self._pinned is not None:
|
|
296
|
+
pin = self._pinned[idx]
|
|
297
|
+
val = np.where(pin, 1.0, val)
|
|
298
|
+
dx = np.where(pin, 0.0, dx)
|
|
299
|
+
dxi = np.where(pin, 0.0, dxi)
|
|
300
|
+
printed[idx] = val
|
|
301
|
+
padded[idx] = val
|
|
302
|
+
cache.append((nb, parts, ssum, dx, dxi))
|
|
303
|
+
self._cache = cache
|
|
304
|
+
return printed.reshape(self.dims)
|
|
305
|
+
|
|
306
|
+
def backward(self, grad: np.ndarray) -> np.ndarray:
|
|
307
|
+
gp = np.zeros(int(np.prod(self.dims)) + 1)
|
|
308
|
+
gp[:-1] = np.asarray(grad, float).ravel()
|
|
309
|
+
out = np.zeros(len(gp) - 1)
|
|
310
|
+
for idx, (nb, parts, ssum, dx, dxi) in zip(reversed(self._groups), reversed(self._cache)):
|
|
311
|
+
total = gp[idx]
|
|
312
|
+
out[idx] = total * dx
|
|
313
|
+
common = total * dxi * (_P / self._q) * np.maximum(ssum, 1e-300) ** (1.0 / self._q - 1.0)
|
|
314
|
+
for b, p in zip(nb, parts):
|
|
315
|
+
gp[b] += common * np.maximum(p, 0.0) ** (_P - 1.0) # one offset: distinct targets
|
|
316
|
+
return out.reshape(self.dims)
|
loadpath/amgfast.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""pyamg's Gauss-Seidel smoother without the GIL.
|
|
2
|
+
|
|
3
|
+
fem.Solver.solve_many runs a study's load cases on threads, one CG each,
|
|
4
|
+
sharing one AMG hierarchy: the sparse matrix products release the GIL,
|
|
5
|
+
but pyamg's compiled Gauss-Seidel (amg_core.gauss_seidel,
|
|
6
|
+
bsr_gauss_seidel and block_gauss_seidel, the V-cycle's smoothers and most
|
|
7
|
+
of a solve's time) holds it, so the cases' smoothing ran one at a time:
|
|
8
|
+
on the GRIP upper arm three cases took about as long as three solves.
|
|
9
|
+
|
|
10
|
+
These are the same loops (pyamg 5.x amg_core/relaxation.h: the same
|
|
11
|
+
operations in the same order, so the same bits) as numba kernels that
|
|
12
|
+
release the GIL. `install()` swaps them in for pyamg's three functions;
|
|
13
|
+
everything else in amg_core stays pyamg's. LOADPATH_AMGFAST=0 leaves
|
|
14
|
+
pyamg alone.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import numba as nb
|
|
24
|
+
|
|
25
|
+
from . import meshops as _meshops
|
|
26
|
+
except ImportError: # pragma: no cover - numba is a dependency
|
|
27
|
+
nb = None
|
|
28
|
+
|
|
29
|
+
_INSTALLED = False
|
|
30
|
+
|
|
31
|
+
if nb is not None:
|
|
32
|
+
# Indices go through uint64: numba wraps negative signed indices, a
|
|
33
|
+
# compare and conditional move on every load that made the kernels half
|
|
34
|
+
# pyamg's speed. The wrappers hand over the index arrays viewed unsigned.
|
|
35
|
+
U = np.uint64
|
|
36
|
+
|
|
37
|
+
@nb.njit(cache=True, nogil=True)
|
|
38
|
+
def _gs(Ap, Aj, Ax, x, b, row_start, row_stop, row_step):
|
|
39
|
+
i = row_start
|
|
40
|
+
while i != row_stop:
|
|
41
|
+
ui = U(i)
|
|
42
|
+
rsum = 0.0
|
|
43
|
+
diag = 0.0
|
|
44
|
+
jj = U(Ap[ui])
|
|
45
|
+
end = U(Ap[ui + U(1)])
|
|
46
|
+
while jj < end:
|
|
47
|
+
j = U(Aj[jj])
|
|
48
|
+
if j == ui:
|
|
49
|
+
diag = Ax[jj]
|
|
50
|
+
else:
|
|
51
|
+
rsum += Ax[jj] * x[j]
|
|
52
|
+
jj += U(1)
|
|
53
|
+
if diag != 0.0:
|
|
54
|
+
x[ui] = (b[ui] - rsum) / diag
|
|
55
|
+
i += row_step
|
|
56
|
+
|
|
57
|
+
@nb.njit(cache=True, nogil=True)
|
|
58
|
+
def _bsr_gs(Ap, Aj, Ax, x, b, row_start, row_stop, row_step, bs):
|
|
59
|
+
# relaxation.h bsr_gauss_seidel: off-diagonal blocks by gemm (each
|
|
60
|
+
# into zeros, then subtracted), then the diagonal block pointwise,
|
|
61
|
+
# in the sweep's direction
|
|
62
|
+
ubs = U(bs)
|
|
63
|
+
B2 = ubs * ubs
|
|
64
|
+
rsum = np.empty(bs)
|
|
65
|
+
axloc = np.empty(bs)
|
|
66
|
+
if row_step < 0:
|
|
67
|
+
step, k0, k1 = -1, bs - 1, -1
|
|
68
|
+
else:
|
|
69
|
+
step, k0, k1 = 1, 0, bs
|
|
70
|
+
i = row_start
|
|
71
|
+
while i != row_stop:
|
|
72
|
+
ui = U(i)
|
|
73
|
+
ib = ui * ubs
|
|
74
|
+
for k in range(bs):
|
|
75
|
+
rsum[k] = b[ib + U(k)]
|
|
76
|
+
diag_ptr = U(0)
|
|
77
|
+
has_diag = False
|
|
78
|
+
jj = U(Ap[ui])
|
|
79
|
+
end = U(Ap[ui + U(1)])
|
|
80
|
+
while jj < end:
|
|
81
|
+
j = U(Aj[jj])
|
|
82
|
+
if j == ui:
|
|
83
|
+
diag_ptr = jj * B2
|
|
84
|
+
has_diag = True
|
|
85
|
+
else:
|
|
86
|
+
col = j * ubs
|
|
87
|
+
blk = jj * B2
|
|
88
|
+
for m in range(bs):
|
|
89
|
+
acc = 0.0
|
|
90
|
+
row = blk + U(m) * ubs
|
|
91
|
+
for k in range(bs):
|
|
92
|
+
acc += Ax[row + U(k)] * x[col + U(k)]
|
|
93
|
+
axloc[m] = acc
|
|
94
|
+
for m in range(bs):
|
|
95
|
+
rsum[m] -= axloc[m]
|
|
96
|
+
jj += U(1)
|
|
97
|
+
if has_diag:
|
|
98
|
+
k = k0
|
|
99
|
+
while k != k1:
|
|
100
|
+
uk = U(k)
|
|
101
|
+
diag = 1.0
|
|
102
|
+
kk = k0
|
|
103
|
+
while kk != k1:
|
|
104
|
+
ukk = U(kk)
|
|
105
|
+
if k == kk:
|
|
106
|
+
diag = Ax[uk * ubs + ukk + diag_ptr]
|
|
107
|
+
else:
|
|
108
|
+
rsum[k] -= Ax[uk * ubs + ukk + diag_ptr] * x[ib + ukk]
|
|
109
|
+
kk += step
|
|
110
|
+
if diag != 0.0:
|
|
111
|
+
x[ib + uk] = rsum[k] / diag
|
|
112
|
+
k += step
|
|
113
|
+
i += row_step
|
|
114
|
+
|
|
115
|
+
@nb.njit(cache=True, nogil=True)
|
|
116
|
+
def _block_gs(Ap, Aj, Ax, x, b, Dinv, row_start, row_stop, row_step, bs):
|
|
117
|
+
# relaxation.h block_gauss_seidel: rsum = sum of off-diagonal block
|
|
118
|
+
# products (each gemm into zeros), x_i = Dinv_i (b_i - rsum)
|
|
119
|
+
ubs = U(bs)
|
|
120
|
+
B2 = ubs * ubs
|
|
121
|
+
rsum = np.empty(bs)
|
|
122
|
+
v = np.empty(bs)
|
|
123
|
+
i = row_start
|
|
124
|
+
while i != row_stop:
|
|
125
|
+
ui = U(i)
|
|
126
|
+
ib = ui * ubs
|
|
127
|
+
for k in range(bs):
|
|
128
|
+
rsum[k] = 0.0
|
|
129
|
+
jj = U(Ap[ui])
|
|
130
|
+
end = U(Ap[ui + U(1)])
|
|
131
|
+
while jj < end:
|
|
132
|
+
j = U(Aj[jj])
|
|
133
|
+
if j != ui:
|
|
134
|
+
col = j * ubs
|
|
135
|
+
blk = jj * B2
|
|
136
|
+
for m in range(bs):
|
|
137
|
+
acc = 0.0
|
|
138
|
+
row = blk + U(m) * ubs
|
|
139
|
+
for k in range(bs):
|
|
140
|
+
acc += Ax[row + U(k)] * x[col + U(k)]
|
|
141
|
+
v[m] = acc
|
|
142
|
+
for k in range(bs):
|
|
143
|
+
rsum[k] += v[k]
|
|
144
|
+
jj += U(1)
|
|
145
|
+
for k in range(bs):
|
|
146
|
+
rsum[k] = b[ib + U(k)] - rsum[k]
|
|
147
|
+
dblk = ui * B2
|
|
148
|
+
for m in range(bs):
|
|
149
|
+
acc = 0.0
|
|
150
|
+
row = dblk + U(m) * ubs
|
|
151
|
+
for k in range(bs):
|
|
152
|
+
acc += Dinv[row + U(k)] * rsum[k]
|
|
153
|
+
x[ib + U(m)] = acc
|
|
154
|
+
i += row_step
|
|
155
|
+
|
|
156
|
+
@nb.njit(cache=True, nogil=True, inline="always")
|
|
157
|
+
def _row_cols(i, Ap, Aj, Bp, Bj, mark, cols):
|
|
158
|
+
# the block row's output columns in scipy's order: first visit
|
|
159
|
+
n = 0
|
|
160
|
+
for jj in range(Ap[i], Ap[i + 1]):
|
|
161
|
+
j = U(Aj[jj])
|
|
162
|
+
for kk in range(Bp[j], Bp[j + U(1)]):
|
|
163
|
+
k = Bj[kk]
|
|
164
|
+
if mark[k] != i:
|
|
165
|
+
mark[k] = i
|
|
166
|
+
cols[n] = k
|
|
167
|
+
n += 1
|
|
168
|
+
return n
|
|
169
|
+
|
|
170
|
+
@nb.njit(cache=True, parallel=True)
|
|
171
|
+
def _bsr_count(n_brow, n_bcol, Ap, Aj, Bp, Bj, counts):
|
|
172
|
+
nch = 64
|
|
173
|
+
for c in nb.prange(nch):
|
|
174
|
+
mark = np.full(n_bcol, -1, np.int64)
|
|
175
|
+
cols = np.empty(n_bcol, np.int64)
|
|
176
|
+
for i in range(c * n_brow // nch, (c + 1) * n_brow // nch):
|
|
177
|
+
counts[i] = _row_cols(i, Ap, Aj, Bp, Bj, mark, cols)
|
|
178
|
+
|
|
179
|
+
@nb.njit(cache=True, parallel=True)
|
|
180
|
+
def _bsr_fill(n_brow, n_bcol, R, C, N, Ap, Aj, Ax, Bp, Bj, Bx, Cp, Cj, Cx):
|
|
181
|
+
# scipy sparsetools bsr_matmat, a block row at a time: each output
|
|
182
|
+
# block gathers A(i, j) B(j, k) in the same (jj, kk) order, by its
|
|
183
|
+
# gemm (dot from the block's value, d ascending), so the same bits
|
|
184
|
+
RC, RN, NC = R * C, R * N, N * C
|
|
185
|
+
nch = 64
|
|
186
|
+
for c in nb.prange(nch):
|
|
187
|
+
mark = np.full(n_bcol, -1, np.int64)
|
|
188
|
+
slot = np.empty(n_bcol, np.int64)
|
|
189
|
+
cols = np.empty(n_bcol, np.int64)
|
|
190
|
+
for i in range(c * n_brow // nch, (c + 1) * n_brow // nch):
|
|
191
|
+
n = _row_cols(i, Ap, Aj, Bp, Bj, mark, cols)
|
|
192
|
+
base = Cp[i]
|
|
193
|
+
for q in range(n):
|
|
194
|
+
Cj[base + q] = cols[q]
|
|
195
|
+
slot[cols[q]] = base + q
|
|
196
|
+
for t in range(RC):
|
|
197
|
+
Cx[(base + q) * RC + t] = 0.0
|
|
198
|
+
for jj in range(Ap[i], Ap[i + 1]):
|
|
199
|
+
j = U(Aj[jj])
|
|
200
|
+
a0 = U(jj) * U(RN)
|
|
201
|
+
for kk in range(Bp[j], Bp[j + U(1)]):
|
|
202
|
+
k = Bj[kk]
|
|
203
|
+
b0 = U(kk) * U(NC)
|
|
204
|
+
c0 = U(slot[k]) * U(RC)
|
|
205
|
+
for r in range(R):
|
|
206
|
+
for cc in range(C):
|
|
207
|
+
o = c0 + U(C * r + cc)
|
|
208
|
+
dot = Cx[o]
|
|
209
|
+
for d in range(N):
|
|
210
|
+
dot += Ax[a0 + U(N * r + d)] * Bx[b0 + U(C * d + cc)]
|
|
211
|
+
Cx[o] = dot
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _u(a):
|
|
215
|
+
"""An index array viewed unsigned (they are never negative)."""
|
|
216
|
+
return a.view(np.uint32 if a.dtype.itemsize == 4 else np.uint64)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _ok(*arrays):
|
|
220
|
+
return all(a.dtype == np.float64 for a in arrays)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
from pyamg import amg_core
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _Core:
|
|
227
|
+
"""amg_core with the three Gauss-Seidel kernels replaced."""
|
|
228
|
+
|
|
229
|
+
def __init__(self, core):
|
|
230
|
+
self._core = core
|
|
231
|
+
|
|
232
|
+
def __getattr__(self, name):
|
|
233
|
+
return getattr(self._core, name)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def gauss_seidel(Ap, Aj, Ax, x, b, row_start, row_stop, row_step):
|
|
237
|
+
if not _ok(Ax, x, b):
|
|
238
|
+
return amg_core.gauss_seidel(Ap, Aj, Ax, x, b, row_start, row_stop, row_step)
|
|
239
|
+
_gs(_u(Ap), _u(Aj), Ax, x, b, int(row_start), int(row_stop), int(row_step))
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def bsr_gauss_seidel(Ap, Aj, Ax, x, b, row_start, row_stop, row_step, R):
|
|
243
|
+
if not _ok(Ax, x, b):
|
|
244
|
+
return amg_core.bsr_gauss_seidel(Ap, Aj, Ax, x, b, row_start, row_stop, row_step, R)
|
|
245
|
+
_bsr_gs(_u(Ap), _u(Aj), Ax, x, b, int(row_start), int(row_stop), int(row_step), int(R))
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def block_gauss_seidel(Ap, Aj, Ax, x, b, Tx, row_start, row_stop, row_step, R):
|
|
249
|
+
if not _ok(Ax, x, b, Tx):
|
|
250
|
+
return amg_core.block_gauss_seidel(Ap, Aj, Ax, x, b, Tx, row_start, row_stop, row_step, R)
|
|
251
|
+
_block_gs(_u(Ap), _u(Aj), Ax, x, b, Tx, int(row_start), int(row_stop), int(row_step), int(R))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
_MATMUL_MIN = 20_000 # block nonzeros of A below which scipy's own is as quick
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _bsr_matmul(self, other, _orig=None):
|
|
258
|
+
"""scipy's BSR @ sparse (bsr._matmul_sparse) with the product itself on
|
|
259
|
+
numba's threads. pyamg's setup (R A P, the prolongation smoother) does
|
|
260
|
+
these on one core while the others wait; same entries, same order, so
|
|
261
|
+
the same bits."""
|
|
262
|
+
M = self.shape[0]
|
|
263
|
+
N = other.shape[1]
|
|
264
|
+
R, n = self.blocksize
|
|
265
|
+
C = other.blocksize[1] if other.format == "bsr" else 1
|
|
266
|
+
if (self.data.dtype != np.float64 or other.dtype != np.float64 or len(self.indices) < _MATMUL_MIN
|
|
267
|
+
or (R == 1 and n == 1 and C == 1)):
|
|
268
|
+
return _orig(self, other)
|
|
269
|
+
if other.format == "csr" and n == 1:
|
|
270
|
+
other = other.tobsr(blocksize=(n, C), copy=False)
|
|
271
|
+
else:
|
|
272
|
+
other = other.tobsr(blocksize=(n, C))
|
|
273
|
+
n_brow, n_bcol = M // R, N // C
|
|
274
|
+
Ap = np.ascontiguousarray(self.indptr, np.int64)
|
|
275
|
+
Aj = np.ascontiguousarray(self.indices, np.int64)
|
|
276
|
+
Bp = np.ascontiguousarray(other.indptr, np.int64)
|
|
277
|
+
Bj = np.ascontiguousarray(other.indices, np.int64)
|
|
278
|
+
counts = np.empty(n_brow, np.int64)
|
|
279
|
+
_bsr_count(n_brow, n_bcol, Ap, Aj, Bp, Bj, counts)
|
|
280
|
+
Cp = np.zeros(n_brow + 1, np.int64)
|
|
281
|
+
np.cumsum(counts, out=Cp[1:])
|
|
282
|
+
nnz = int(Cp[-1])
|
|
283
|
+
Cj = np.empty(nnz, np.int64)
|
|
284
|
+
Cx = np.empty(R * C * nnz, np.float64)
|
|
285
|
+
_bsr_fill(n_brow, n_bcol, R, C, n, Ap, Aj, np.ascontiguousarray(np.ravel(self.data)), Bp, Bj,
|
|
286
|
+
np.ascontiguousarray(np.ravel(other.data)), Cp, Cj, Cx)
|
|
287
|
+
idx = self._get_index_dtype((self.indptr, self.indices, other.indptr, other.indices), maxval=nnz)
|
|
288
|
+
return self._bsr_container((Cx.reshape(-1, R, C), Cj.astype(idx), Cp.astype(idx)), shape=(M, N),
|
|
289
|
+
blocksize=(R, C))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
_SCIPY_BSR_MATMUL = None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _bsr_matmul_method(self, other):
|
|
296
|
+
return _bsr_matmul(self, other, _orig=_SCIPY_BSR_MATMUL)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
_bsr_matmul_method._loadpath = True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def installed():
|
|
303
|
+
"""Whether pyamg's Gauss-Seidel is the GIL-free one now."""
|
|
304
|
+
return _INSTALLED
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def install():
|
|
308
|
+
"""Use the GIL-free kernels in pyamg's relaxation module (once)."""
|
|
309
|
+
global _INSTALLED
|
|
310
|
+
if _INSTALLED or nb is None or os.environ.get("LOADPATH_AMGFAST", "1") == "0":
|
|
311
|
+
return _INSTALLED
|
|
312
|
+
try:
|
|
313
|
+
import pyamg.relaxation.relaxation as rel
|
|
314
|
+
except ImportError: # pragma: no cover
|
|
315
|
+
return False
|
|
316
|
+
if not isinstance(rel.amg_core, _Core):
|
|
317
|
+
rel.amg_core = _Core(rel.amg_core)
|
|
318
|
+
global _SCIPY_BSR_MATMUL
|
|
319
|
+
from scipy.sparse import _bsr
|
|
320
|
+
|
|
321
|
+
orig = _bsr._bsr_base._matmul_sparse
|
|
322
|
+
if not getattr(orig, "_loadpath", False):
|
|
323
|
+
_SCIPY_BSR_MATMUL = orig
|
|
324
|
+
_bsr._bsr_base._matmul_sparse = _bsr_matmul_method
|
|
325
|
+
_INSTALLED = True
|
|
326
|
+
return True
|