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
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anisotropic lattice homogenization: the full elasticity tensor of a unit
|
|
3
|
+
cell and a directional wall-stress surface, and both rotated into the frame
|
|
4
|
+
the cell sits in.
|
|
5
|
+
|
|
6
|
+
lattice.SHEETS / STRUTS read every cell as an isotropic material, and for a
|
|
7
|
+
strut cell the conservative one: the modulus is the lesser of its [100] and
|
|
8
|
+
[111] moduli, the wall stress the worst of a [100] pull, a [111] pull and a
|
|
9
|
+
shear. An octet is 1.5x stiffer along its struts than that, a simple cubic
|
|
10
|
+
cell 10x and more on its axes, a BCCz 2-3x along z; a part whose lattice is
|
|
11
|
+
loaded the way its cells are strong carries mass it does not need. The
|
|
12
|
+
scalar reading also scales the wall stress by the macro von Mises stress,
|
|
13
|
+
which is zero under a hydrostatic load the walls feel in full.
|
|
14
|
+
|
|
15
|
+
Here each cell is solved six times on a periodic voxel grid (a unit macro
|
|
16
|
+
strain per Voigt component, the nodes wrapping across the cell's faces):
|
|
17
|
+
|
|
18
|
+
- the mean stresses give the 6x6 effective stiffness C* (over the solid's
|
|
19
|
+
E, engineering shear strains), symmetrized over the cell's own symmetry;
|
|
20
|
+
- the six nodal stress fields superpose to the walls' stress under any
|
|
21
|
+
macro stress sigma: sigma -> C*^-1 sigma -> sum of the fields. Sampled
|
|
22
|
+
over a few hundred directions (axes, diagonals, shears, hydrostatic and
|
|
23
|
+
random), the walls' peak (99.5th percentile of nodal von Mises, as the
|
|
24
|
+
scalar fits read it) is fitted by a quadratic form, peak^2 = sigma' P
|
|
25
|
+
sigma, an anisotropic yield surface in the spirit of Hill's, then scaled
|
|
26
|
+
up until it lies on or above every sample, so it never under-reads one.
|
|
27
|
+
|
|
28
|
+
Tables over relative density per cell type live in data/lattice_tensors.json
|
|
29
|
+
(bench/lattice_tensors.py writes them); tensors_at reads them at any density
|
|
30
|
+
(log-log on the leading entry, the shape interpolated in log rho, up to the
|
|
31
|
+
solid at rho = 1).
|
|
32
|
+
|
|
33
|
+
Frames: a cell's tensors are in its own axes; homogenized.py rotates them by
|
|
34
|
+
the lattice coordinates' local Jacobian (polar factor), so a cylindrical,
|
|
35
|
+
conformal or rotated lattice is read the way it actually lies.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import itertools
|
|
40
|
+
import json
|
|
41
|
+
from functools import lru_cache
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
__all__ = ["unit_cell_tensors", "tensors_at", "rotate_stiffness", "rotate_stress_form", "stress_rotation",
|
|
47
|
+
"strain_rotation", "has_tensors", "directional_modulus", "stress_surface", "VM_FORM", "TABLE_PATH"]
|
|
48
|
+
|
|
49
|
+
TABLE_PATH = Path(__file__).resolve().parent / "data" / "lattice_tensors.json"
|
|
50
|
+
_VOIGT = ((0, 0), (1, 1), (2, 2), (0, 1), (1, 2), (2, 0))
|
|
51
|
+
|
|
52
|
+
# von Mises squared as a quadratic form of Voigt stress (tensor shears)
|
|
53
|
+
VM_FORM = np.array([[1.0, -0.5, -0.5, 0, 0, 0],
|
|
54
|
+
[-0.5, 1.0, -0.5, 0, 0, 0],
|
|
55
|
+
[-0.5, -0.5, 1.0, 0, 0, 0],
|
|
56
|
+
[0, 0, 0, 3.0, 0, 0],
|
|
57
|
+
[0, 0, 0, 0, 3.0, 0],
|
|
58
|
+
[0, 0, 0, 0, 0, 3.0]])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---- rotations ----------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _basis(shear_half: bool) -> np.ndarray:
|
|
64
|
+
"""(6, 3, 3): the tensor of each Voigt unit vector; engineering shear
|
|
65
|
+
strain puts 1/2 on each off-diagonal entry, stress 1."""
|
|
66
|
+
e = np.zeros((6, 3, 3))
|
|
67
|
+
for a, (i, j) in enumerate(_VOIGT):
|
|
68
|
+
v = 0.5 if (shear_half and i != j) else 1.0
|
|
69
|
+
e[a, i, j] = v
|
|
70
|
+
e[a, j, i] = v
|
|
71
|
+
return e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _rotation_matrix(rot, shear_half: bool):
|
|
75
|
+
"""(..., 6, 6) T with v_local = T v_global for Voigt vectors, where
|
|
76
|
+
`rot` (..., 3, 3) has the local axes as its columns (in global
|
|
77
|
+
components): tensor_local = rot' tensor rot."""
|
|
78
|
+
rot = np.asarray(rot, float)
|
|
79
|
+
e = _basis(shear_half)
|
|
80
|
+
loc = np.einsum("...pi,apq,...qj->...aij", rot, e, rot)
|
|
81
|
+
ii = np.array([i for i, _ in _VOIGT])
|
|
82
|
+
jj = np.array([j for _, j in _VOIGT])
|
|
83
|
+
t = loc[..., ii, jj] # (..., a, b): component b of rotated unit a
|
|
84
|
+
if shear_half:
|
|
85
|
+
t = t * np.array([1, 1, 1, 2, 2, 2], float) # engineering shear read back
|
|
86
|
+
return np.swapaxes(t, -1, -2)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def stress_rotation(rot):
|
|
90
|
+
return _rotation_matrix(rot, False)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def strain_rotation(rot):
|
|
94
|
+
return _rotation_matrix(rot, True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rotate_stiffness(c_local, rot):
|
|
98
|
+
"""C in global axes for a cell whose local axes are the columns of
|
|
99
|
+
`rot`: C_g = T_e' C_l T_e (strain energy is frame free)."""
|
|
100
|
+
t = strain_rotation(rot)
|
|
101
|
+
return np.einsum("...ab,...bc,...cd->...ad", np.swapaxes(t, -1, -2), c_local, t)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rotate_stress_form(p_local, rot):
|
|
105
|
+
"""A quadratic form of stress (P) in global axes: P_g = T_s' P_l T_s."""
|
|
106
|
+
t = stress_rotation(rot)
|
|
107
|
+
return np.einsum("...ab,...bc,...cd->...ad", np.swapaxes(t, -1, -2), p_local, t)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@lru_cache(maxsize=None)
|
|
111
|
+
def _group(kind_sym: str) -> np.ndarray:
|
|
112
|
+
"""Proper rotations of the cube (24) or those keeping z (8)."""
|
|
113
|
+
out = []
|
|
114
|
+
for perm in itertools.permutations(range(3)):
|
|
115
|
+
for signs in itertools.product((-1, 1), repeat=3):
|
|
116
|
+
r = np.zeros((3, 3))
|
|
117
|
+
for i, p in enumerate(perm):
|
|
118
|
+
r[p, i] = signs[i]
|
|
119
|
+
if np.linalg.det(r) < 0:
|
|
120
|
+
continue
|
|
121
|
+
if kind_sym == "tetragonal" and abs(r[2, 2]) != 1:
|
|
122
|
+
continue
|
|
123
|
+
if kind_sym == "orthorhombic" and not np.allclose(np.abs(r), np.eye(3)):
|
|
124
|
+
continue
|
|
125
|
+
out.append(r)
|
|
126
|
+
return np.array(out)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def symmetry_of(kind: str) -> str:
|
|
130
|
+
"""cubic, tetragonal (z stands apart) or none (a random foam draw)."""
|
|
131
|
+
from .struts import canonical
|
|
132
|
+
|
|
133
|
+
k = canonical(kind)
|
|
134
|
+
if k in ("bccz", "fccz"):
|
|
135
|
+
return "tetragonal"
|
|
136
|
+
if k == "voronoi":
|
|
137
|
+
return "none"
|
|
138
|
+
return "cubic"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _symmetrize(m, sym: str, rotate):
|
|
142
|
+
m = 0.5 * (m + m.T)
|
|
143
|
+
if sym == "none":
|
|
144
|
+
return m
|
|
145
|
+
g = _group(sym)
|
|
146
|
+
return np.mean(rotate(np.broadcast_to(m, (len(g), 6, 6)), g), axis=0)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---- unit cell ----------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def _directions(seed=0, n_random=600):
|
|
152
|
+
"""Unit macro stresses (Voigt, Frobenius norm 1): axes, face and cube
|
|
153
|
+
diagonals in tension and compression, pure shears, hydrostatic, random."""
|
|
154
|
+
out = []
|
|
155
|
+
dirs = [np.array(v, float) for v in itertools.product((-1, 0, 1), repeat=3) if any(v)]
|
|
156
|
+
for d in dirs:
|
|
157
|
+
d = d / np.linalg.norm(d)
|
|
158
|
+
s = np.outer(d, d)
|
|
159
|
+
out.append(np.array([s[i, j] for i, j in _VOIGT]))
|
|
160
|
+
for a in range(3, 6):
|
|
161
|
+
v = np.zeros(6)
|
|
162
|
+
v[a] = 1.0
|
|
163
|
+
out += [v, -v]
|
|
164
|
+
for sgn in (1, -1):
|
|
165
|
+
out.append(sgn * np.array([1, 1, 1, 0, 0, 0], float))
|
|
166
|
+
rng = np.random.default_rng(seed)
|
|
167
|
+
out += list(rng.normal(size=(n_random, 6)))
|
|
168
|
+
out = np.array(out)
|
|
169
|
+
frob = np.sqrt((out[:, :3] ** 2).sum(1) + 2 * (out[:, 3:] ** 2).sum(1))
|
|
170
|
+
return out / frob[:, None]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def unit_cell_tensors(kind: str, wall_over_cell: float, n: int = 40, nu: float = 0.35, blend: float = 0.0,
|
|
174
|
+
percentile: float = 99.5, aspect=None, _keep: bool = False) -> dict:
|
|
175
|
+
"""Full anisotropic homogenization of one unit cell (module docstring)
|
|
176
|
+
on a periodic voxel grid, n voxels along its shortest side. `wall_over_cell`:
|
|
177
|
+
sheet wall or strut diameter over the cell; `blend` the strut node
|
|
178
|
+
fillet over the cell; `aspect` (per axis) a stretched cell, its box
|
|
179
|
+
`aspect` cells along each axis, as the field graph's `aspect` key lays
|
|
180
|
+
it out. Returns rho, C (6x6 over E), the wall-stress surface (P, Q4,
|
|
181
|
+
floor), held-out checks of it, the sampled directions' peaks, and the
|
|
182
|
+
scalar readings (E100, E111, G) for comparison with the isotropic
|
|
183
|
+
tables."""
|
|
184
|
+
import pyamg
|
|
185
|
+
import scipy.sparse as sp
|
|
186
|
+
from scipy.sparse.linalg import cg
|
|
187
|
+
|
|
188
|
+
from .fem import CORNERS, _b_matrix, elastic_c, hex8_b, hex8_ke, von_mises
|
|
189
|
+
from .lattice import tpms
|
|
190
|
+
from .struts import is_strut, unit_cell_distance
|
|
191
|
+
|
|
192
|
+
a = np.ones(3) if aspect is None else np.asarray(aspect, float)
|
|
193
|
+
stretched = not np.allclose(a, 1.0)
|
|
194
|
+
h = float(a.min()) / n
|
|
195
|
+
dims = np.maximum(np.round(a / h).astype(int), 1)
|
|
196
|
+
a = dims * h # the box the grid holds exactly
|
|
197
|
+
axes = [(np.arange(d) + 0.5) * h for d in dims]
|
|
198
|
+
g = np.stack(np.meshgrid(*axes, indexing="ij"), -1).reshape(-1, 3)
|
|
199
|
+
U = g / a
|
|
200
|
+
if is_strut(kind):
|
|
201
|
+
solid = unit_cell_distance(kind, U, blend, aspect=tuple(a) if stretched else None) < 0.5 * wall_over_cell
|
|
202
|
+
else:
|
|
203
|
+
solid = np.abs(tpms(kind, U, 1.0) * float(a.min())) < 0.5 * wall_over_cell
|
|
204
|
+
eijk = np.argwhere(solid.reshape(tuple(dims)))
|
|
205
|
+
ne = len(eijk)
|
|
206
|
+
corner = (eijk[:, None, :] + CORNERS[None]) % dims
|
|
207
|
+
used, inv = np.unique((corner[..., 0] * dims[1] + corner[..., 1]) * dims[2] + corner[..., 2],
|
|
208
|
+
return_inverse=True)
|
|
209
|
+
enode = inv.reshape(ne, 8)
|
|
210
|
+
nn = len(used)
|
|
211
|
+
edof = (3 * enode[:, :, None] + np.arange(3)).reshape(ne, 24)
|
|
212
|
+
ke = hex8_ke(nu) * h
|
|
213
|
+
k = sp.csr_matrix((np.tile(ke.ravel(), ne), (np.repeat(edof, 24, axis=1).ravel(),
|
|
214
|
+
np.tile(edof, (1, 24)).ravel())), shape=(3 * nn, 3 * nn))
|
|
215
|
+
free = np.ones(3 * nn, bool)
|
|
216
|
+
free[:3] = False
|
|
217
|
+
kf = k[free][:, free].tocsr()
|
|
218
|
+
ml = pyamg.smoothed_aggregation_solver(kf, symmetry="hermitian", max_coarse=400, coarse_solver="splu")
|
|
219
|
+
xc = (eijk[:, None, :] + CORNERS[None]) * h
|
|
220
|
+
c0 = elastic_c(1.0, nu)
|
|
221
|
+
bc = _b_matrix(0, 0, 0) / h
|
|
222
|
+
bcorner = [hex8_b(x, y, z, nu) / h for (x, y, z) in 2.0 * CORNERS - 1.0]
|
|
223
|
+
count = np.bincount(enode.ravel(), minlength=nn)[:, None]
|
|
224
|
+
C = np.zeros((6, 6))
|
|
225
|
+
L = np.zeros((nn, 6, 6), np.float32) # nodal stress per unit macro strain
|
|
226
|
+
for comp in range(6):
|
|
227
|
+
e = np.zeros(6)
|
|
228
|
+
e[comp] = 1.0
|
|
229
|
+
eps = np.array([[e[0], e[3] / 2, e[5] / 2], [e[3] / 2, e[1], e[4] / 2], [e[5] / 2, e[4] / 2, e[2]]])
|
|
230
|
+
u0 = (xc @ eps.T).reshape(ne, 24)
|
|
231
|
+
f = np.zeros(3 * nn)
|
|
232
|
+
np.add.at(f, edof, -(u0 @ ke.T))
|
|
233
|
+
uf, info = cg(kf, f[free], rtol=1e-9, maxiter=3000, M=ml.aspreconditioner())
|
|
234
|
+
if info != 0:
|
|
235
|
+
raise RuntimeError("unit cell solve did not converge")
|
|
236
|
+
u = np.zeros(3 * nn)
|
|
237
|
+
u[free] = uf
|
|
238
|
+
ue = u[edof] + u0
|
|
239
|
+
C[:, comp] = ((ue @ bc.T) @ c0.T).sum(axis=0) * h ** 3 / float(np.prod(a))
|
|
240
|
+
acc = np.zeros((nn, 6))
|
|
241
|
+
for kk in range(8):
|
|
242
|
+
np.add.at(acc, enode[:, kk], (ue @ bcorner[kk].T) @ c0.T)
|
|
243
|
+
L[:, :, comp] = acc / count
|
|
244
|
+
sym = symmetry_of(kind)
|
|
245
|
+
if stretched and sym != "none":
|
|
246
|
+
# a cell stretched along its axes keeps only the half turns about them
|
|
247
|
+
sym = "orthorhombic"
|
|
248
|
+
C = _symmetrize(C, sym, rotate_stiffness)
|
|
249
|
+
S = np.linalg.inv(C)
|
|
250
|
+
|
|
251
|
+
def peaks_on(Ls, dirs, rank_shift=0):
|
|
252
|
+
out = np.empty(len(dirs))
|
|
253
|
+
q = percentile / 100.0 * (nn - 1) - rank_shift # the order statistic, numpy's "linear"
|
|
254
|
+
for i in range(0, len(dirs), 16):
|
|
255
|
+
eps = dirs[i:i + 16] @ S.T # (k, 6) macro strains
|
|
256
|
+
st = np.einsum("nab,kb->kna", Ls, eps.astype(np.float32))
|
|
257
|
+
for k, s_ in enumerate(st):
|
|
258
|
+
v = von_mises(s_)
|
|
259
|
+
lo = int(np.floor(q))
|
|
260
|
+
part = np.partition(v, [lo, min(lo + 1, len(v) - 1)])
|
|
261
|
+
out[i + k] = part[lo] + (q - lo) * (part[min(lo + 1, len(v) - 1)] - part[lo])
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
# The peak is an order statistic near the top: only nodes whose stress
|
|
265
|
+
# could reach it matter. A node's von Mises per unit macro stress is at
|
|
266
|
+
# most the root of the largest eigenvalue of M' V M (M its stress per
|
|
267
|
+
# macro stress), so nodes whose bound is under a threshold every
|
|
268
|
+
# direction's peak clears can be dropped, the rank shifted by as many.
|
|
269
|
+
M = np.einsum("nab,bc->nac", L, S.astype(np.float32))
|
|
270
|
+
bound = np.sqrt(np.linalg.eigvalsh(np.einsum("nba,bc,ncd->nad", M, VM_FORM.astype(np.float32), M))[:, -1])
|
|
271
|
+
ref = _directions(n_random=0)
|
|
272
|
+
thr = 0.5 * float(peaks_on(L, ref).min())
|
|
273
|
+
hot = bound >= thr
|
|
274
|
+
Lh = L[hot]
|
|
275
|
+
shift = int(nn - hot.sum())
|
|
276
|
+
|
|
277
|
+
def peaks_of(dirs):
|
|
278
|
+
out = peaks_on(Lh, dirs, shift)
|
|
279
|
+
bad = out < thr
|
|
280
|
+
if bad.any():
|
|
281
|
+
out[bad] = peaks_on(L, dirs[bad])
|
|
282
|
+
return out
|
|
283
|
+
|
|
284
|
+
base = _directions()
|
|
285
|
+
peaks = peaks_of(base)
|
|
286
|
+
# the cell's symmetry images of every sample read the same peak
|
|
287
|
+
if sym == "none":
|
|
288
|
+
dirs, pk = base, peaks
|
|
289
|
+
else:
|
|
290
|
+
t = stress_rotation(_group(sym)) # (g, 6, 6)
|
|
291
|
+
dirs = np.einsum("gab,kb->gka", t, base).reshape(-1, 6)
|
|
292
|
+
pk = np.tile(peaks, len(t))
|
|
293
|
+
P, quad_worst = _fit_quadratic(dirs, pk, sym)
|
|
294
|
+
Q4, q4_scale = _fit_quartic(dirs, pk)
|
|
295
|
+
floor = 1.0 / quad_worst
|
|
296
|
+
# held-out directions, solved directly: the model against the cell
|
|
297
|
+
hold = _directions(seed=11, n_random=200)[-200:]
|
|
298
|
+
ph = peaks_of(hold)
|
|
299
|
+
ratio = stress_surface(hold, P, Q4, floor) / ph
|
|
300
|
+
ratio_quad = np.sqrt(np.einsum("ka,ab,kb->k", hold, P, hold)) / ph
|
|
301
|
+
e100 = 1.0 / S[0, 0]
|
|
302
|
+
d111 = np.full(3, 1 / np.sqrt(3.0))
|
|
303
|
+
return {"kind": kind, "d": float(wall_over_cell), "n": n, "nu": nu, "rho": float(solid.mean()),
|
|
304
|
+
"aspect": a.tolist() if stretched else None,
|
|
305
|
+
"C": C, "P": P, "Q4": Q4, "floor": floor, "symmetry": sym,
|
|
306
|
+
"holdout": {"min": float(ratio.min()), "mean": float(ratio.mean()), "max": float(ratio.max())},
|
|
307
|
+
"holdoutQuadratic": {"min": float(ratio_quad.min()), "mean": float(ratio_quad.mean()),
|
|
308
|
+
"max": float(ratio_quad.max())},
|
|
309
|
+
"E100": float(e100), "E111": float(directional_modulus(C, d111)),
|
|
310
|
+
"G": float(C[3, 3]), "directions": base, "peaks": peaks, **({"L": L, "S": S} if _keep else {})}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _fit_quadratic(dirs, pk, sym):
|
|
314
|
+
"""peak^2 = s' P s by least squares on the relative error (a cell's
|
|
315
|
+
strong directions count as much as its weak ones), positive definite,
|
|
316
|
+
scaled to lie on or above every sample. Returns (P, worst over-read)."""
|
|
317
|
+
iu = np.triu_indices(6)
|
|
318
|
+
w = np.where(iu[0] == iu[1], 1.0, 2.0)
|
|
319
|
+
A = dirs[:, iu[0]] * dirs[:, iu[1]] * w
|
|
320
|
+
wt = 1.0 / pk ** 2
|
|
321
|
+
p, *_ = np.linalg.lstsq(A * wt[:, None], pk ** 2 * wt, rcond=None)
|
|
322
|
+
P = np.zeros((6, 6))
|
|
323
|
+
P[iu] = p
|
|
324
|
+
P = P + np.triu(P, 1).T
|
|
325
|
+
P = _symmetrize(P, sym, rotate_stress_form)
|
|
326
|
+
wv, vv = np.linalg.eigh(P)
|
|
327
|
+
P = (vv * np.maximum(wv, 1e-6 * wv.max())) @ vv.T
|
|
328
|
+
fit = np.sqrt(np.einsum("ka,ab,kb->k", dirs, P, dirs))
|
|
329
|
+
scale = float(np.max(pk / fit))
|
|
330
|
+
return P * scale ** 2, float(np.max(fit * scale / pk))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
MONOMIALS4 = np.array(list(itertools.combinations_with_replacement(range(6), 4))) # (126, 4)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def quartic_features(s):
|
|
337
|
+
"""(k, 126) the degree-4 monomials of Voigt stresses s (k, 6)."""
|
|
338
|
+
s = np.asarray(s, float)
|
|
339
|
+
return s[:, MONOMIALS4[:, 0]] * s[:, MONOMIALS4[:, 1]] * s[:, MONOMIALS4[:, 2]] * s[:, MONOMIALS4[:, 3]]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def square_of_quadratic(P) -> np.ndarray:
|
|
343
|
+
"""The quartic coefficients (126,) of (s' P s)^2."""
|
|
344
|
+
coef = {}
|
|
345
|
+
for a in range(6):
|
|
346
|
+
for b in range(6):
|
|
347
|
+
for c in range(6):
|
|
348
|
+
for d in range(6):
|
|
349
|
+
key = tuple(sorted((a, b, c, d)))
|
|
350
|
+
coef[key] = coef.get(key, 0.0) + P[a, b] * P[c, d]
|
|
351
|
+
return np.array([coef[tuple(m)] for m in MONOMIALS4])
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _fit_quartic(dirs, pk):
|
|
355
|
+
"""peak^4 = a quartic polynomial of the macro stress (126 terms), least
|
|
356
|
+
squares on the relative error, scaled onto or above every sample."""
|
|
357
|
+
A = quartic_features(dirs)
|
|
358
|
+
wt = 1.0 / pk ** 4
|
|
359
|
+
q, *_ = np.linalg.lstsq(A * wt[:, None], pk ** 4 * wt, rcond=None)
|
|
360
|
+
fit = np.maximum(A @ q, 1e-30) ** 0.25
|
|
361
|
+
scale = float(np.max(pk / fit))
|
|
362
|
+
return q * scale ** 4, scale
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def stress_surface(s, P, Q4, floor):
|
|
366
|
+
"""Peak wall stress for macro stresses s (k, 6) in the cell's axes: the
|
|
367
|
+
quartic surface, never under `floor` times the quadratic one (positive
|
|
368
|
+
definite everywhere, so a direction the quartic dips in between samples
|
|
369
|
+
still reads). P, Q4, floor may be per row ((k, 6, 6), (k, 126), (k,))."""
|
|
370
|
+
s = np.asarray(s, float)
|
|
371
|
+
quad = np.sqrt(np.maximum(np.einsum("ka,ab,kb->k", s, P, s) if np.ndim(P) == 2
|
|
372
|
+
else np.einsum("ka,kab,kb->k", s, P, s), 0.0))
|
|
373
|
+
f = quartic_features(s)
|
|
374
|
+
q4 = (f @ Q4) if np.ndim(Q4) == 1 else np.einsum("km,km->k", f, Q4)
|
|
375
|
+
return np.maximum(np.maximum(q4, 0.0) ** 0.25, np.asarray(floor) * quad)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def directional_modulus(C, d) -> float:
|
|
379
|
+
"""Young's modulus along unit direction d of stiffness C."""
|
|
380
|
+
d = np.asarray(d, float)
|
|
381
|
+
d = d / np.linalg.norm(d)
|
|
382
|
+
s = np.outer(d, d)
|
|
383
|
+
sig = np.array([s[i, j] for i, j in _VOIGT])
|
|
384
|
+
return float(1.0 / (sig @ np.linalg.solve(C, sig)))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ---- tables -------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
@lru_cache(maxsize=1)
|
|
390
|
+
def _tables() -> dict:
|
|
391
|
+
import os
|
|
392
|
+
|
|
393
|
+
path = Path(os.environ.get("LOADPATH_LATTICE_TABLES") or TABLE_PATH)
|
|
394
|
+
if not path.exists():
|
|
395
|
+
return {}
|
|
396
|
+
return json.loads(path.read_text())
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _key(kind: str) -> str:
|
|
400
|
+
from .struts import canonical, is_strut
|
|
401
|
+
|
|
402
|
+
return canonical(kind) if is_strut(kind) else kind
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _aspect_key(aspect):
|
|
406
|
+
if aspect is None:
|
|
407
|
+
return None
|
|
408
|
+
a = tuple(round(float(x), 3) for x in aspect)
|
|
409
|
+
return None if np.allclose(a, 1.0) else a
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def has_tensors(kind: str, aspect=None) -> bool:
|
|
413
|
+
"""Tables exist (cubic cells), or can be built (a stretched strut or
|
|
414
|
+
sheet cell; not a Voronoi foam)."""
|
|
415
|
+
from .struts import canonical
|
|
416
|
+
|
|
417
|
+
if _aspect_key(aspect) is not None:
|
|
418
|
+
return canonical(kind) != "voronoi" and _key(kind) in _tables().get("kinds", {})
|
|
419
|
+
return _key(kind) in _tables().get("kinds", {})
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# stretched cells: a short ladder solved on first use (about a minute and a
|
|
423
|
+
# half a cell type and aspect on 4 cores), cached on disk
|
|
424
|
+
ASPECT_RHO = (0.06, 0.12, 0.22, 0.36, 0.55, 0.8)
|
|
425
|
+
ASPECT_N = 32
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _cache_dir() -> Path:
|
|
429
|
+
import os
|
|
430
|
+
|
|
431
|
+
return Path(os.environ.get("LOADPATH_CACHE") or Path.home() / ".cache" / "loadpath") / "lattice-tensors"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@lru_cache(maxsize=None)
|
|
435
|
+
def aspect_table(kind: str, aspect: tuple) -> list:
|
|
436
|
+
"""Rows (rho, d, C, P, Q4, floor) of a stretched cell, from the disk
|
|
437
|
+
cache or solved now (unit_cell_tensors with `aspect`). `d` is the wall
|
|
438
|
+
or strut diameter over the cell: a stretched cell's density at a given
|
|
439
|
+
wall is not the cubic cell's, so it is read from these rows too."""
|
|
440
|
+
from .lattice import SHEETS, wall_for_density
|
|
441
|
+
from .struts import strut_diameter_for_density
|
|
442
|
+
|
|
443
|
+
k = _key(kind)
|
|
444
|
+
path = _cache_dir() / f"{k}-{aspect[0]:g}x{aspect[1]:g}x{aspect[2]:g}.json"
|
|
445
|
+
if path.exists():
|
|
446
|
+
try:
|
|
447
|
+
return json.loads(path.read_text())
|
|
448
|
+
except (OSError, ValueError):
|
|
449
|
+
pass
|
|
450
|
+
rows = []
|
|
451
|
+
for rho in ASPECT_RHO:
|
|
452
|
+
d = float(wall_for_density(k, rho, 1.0)) if k in SHEETS else float(strut_diameter_for_density(k, rho, 1.0))
|
|
453
|
+
r = unit_cell_tensors(k, d, n=ASPECT_N, aspect=aspect)
|
|
454
|
+
rows.append({"rho": r["rho"], "d": d, "C": r["C"].tolist(), "P": r["P"].tolist(),
|
|
455
|
+
"Q4": r["Q4"].tolist(), "floor": r["floor"], "holdout": r["holdout"]})
|
|
456
|
+
rows.sort(key=lambda x: x["rho"])
|
|
457
|
+
try:
|
|
458
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
path.write_text(json.dumps(rows))
|
|
460
|
+
except OSError:
|
|
461
|
+
pass
|
|
462
|
+
return rows
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def aspect_density(kind: str, aspect, d_over_cell):
|
|
466
|
+
"""Relative density of a stretched cell at wall (or strut diameter)
|
|
467
|
+
over cell `d_over_cell`, from its rows (log-log, to the solid)."""
|
|
468
|
+
rows = aspect_table(kind, _aspect_key(aspect))
|
|
469
|
+
d = np.array([r["d"] for r in rows])
|
|
470
|
+
rho = np.array([r["rho"] for r in rows])
|
|
471
|
+
x = np.log(np.clip(np.asarray(d_over_cell, float), 1e-4, None))
|
|
472
|
+
lx, ly = np.log(d), np.log(rho)
|
|
473
|
+
slope = (ly[1] - ly[0]) / (lx[1] - lx[0])
|
|
474
|
+
hi = (ly[-1] - ly[-2]) / (lx[-1] - lx[-2])
|
|
475
|
+
out = np.where(x < lx[0], ly[0] + slope * (x - lx[0]),
|
|
476
|
+
np.where(x > lx[-1], ly[-1] + hi * (x - lx[-1]), np.interp(x, lx, ly)))
|
|
477
|
+
return np.clip(np.exp(out), 1e-3, 1.0)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@lru_cache(maxsize=None)
|
|
481
|
+
def _rows(kind: str, aspect=None):
|
|
482
|
+
t = aspect_table(kind, aspect) if aspect is not None else _tables()["kinds"][_key(kind)]
|
|
483
|
+
nu = float(_tables().get("nu", 0.35))
|
|
484
|
+
from .fem import elastic_c
|
|
485
|
+
|
|
486
|
+
rho = np.array([r["rho"] for r in t] + [1.0])
|
|
487
|
+
C = np.array([np.array(r["C"]) for r in t] + [elastic_c(1.0, nu)])
|
|
488
|
+
P = np.array([np.array(r["P"]) for r in t] + [VM_FORM])
|
|
489
|
+
Q = np.array([np.array(r["Q4"]) for r in t] + [square_of_quadratic(VM_FORM)])
|
|
490
|
+
F = np.array([r["floor"] for r in t] + [1.0])
|
|
491
|
+
return rho, C, P, Q, F
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _interp_at(rho, xs):
|
|
495
|
+
"""(log-log leading-entry interpolant, row j, weight t) at rho: under
|
|
496
|
+
the first row the leading entry runs on with the first two rows' slope
|
|
497
|
+
and the shape is held; between rows the shape is linear in log rho."""
|
|
498
|
+
lx = np.log(xs)
|
|
499
|
+
lr = np.log(np.clip(rho, 1e-3, 1.0))
|
|
500
|
+
j = np.clip(np.searchsorted(lx, lr) - 1, 0, len(lx) - 2)
|
|
501
|
+
t = np.clip((lr - lx[j]) / (lx[j + 1] - lx[j]), 0.0, 1.0)
|
|
502
|
+
|
|
503
|
+
def lead(v):
|
|
504
|
+
lv = np.log(v)
|
|
505
|
+
slope = (lv[1] - lv[0]) / (lx[1] - lx[0])
|
|
506
|
+
return np.exp(np.where(lr < lx[0], lv[0] + slope * (lr - lx[0]), np.interp(lr, lx, lv)))
|
|
507
|
+
|
|
508
|
+
return lead, j, t
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _interp(rho, xs, M, norm):
|
|
512
|
+
lead, j, t = _interp_at(rho, xs)
|
|
513
|
+
shape = M / norm.reshape((-1,) + (1,) * (M.ndim - 1))
|
|
514
|
+
tt = t.reshape((-1,) + (1,) * (M.ndim - 1))
|
|
515
|
+
return lead(norm).reshape((-1,) + (1,) * (M.ndim - 1)) * (shape[j] * (1 - tt) + shape[j + 1] * tt)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def tensors_at(kind: str, rho, aspect=None):
|
|
519
|
+
"""(C, P, Q4, floor) of a cell type at relative densities rho, in the
|
|
520
|
+
cell's own axes: C (k, 6, 6) over the solid's E (engineering shears);
|
|
521
|
+
the wall-stress surface (stress_surface) as its quadratic form P (k, 6,
|
|
522
|
+
6), quartic Q4 (k, 126) and floor (k,), for the macro stress the lattice
|
|
523
|
+
carries (Voigt, tensor shears). `aspect`: a stretched cell (its own
|
|
524
|
+
tables, aspect_table)."""
|
|
525
|
+
if not has_tensors(kind, aspect):
|
|
526
|
+
raise ValueError(f"no anisotropic tensors for lattice {kind!r}; run bench/lattice_tensors.py")
|
|
527
|
+
rho = np.atleast_1d(np.asarray(rho, float))
|
|
528
|
+
xs, C, P, Q, F = _rows(kind, _aspect_key(aspect))
|
|
529
|
+
_, j, t = _interp_at(rho, xs)
|
|
530
|
+
c, pp, q = _interp(rho, xs, C, C[:, 0, 0]), _interp(rho, xs, P, P[:, 0, 0]), _interp(rho, xs, Q, P[:, 0, 0] ** 2)
|
|
531
|
+
low = rho < xs[0]
|
|
532
|
+
if low.any():
|
|
533
|
+
# under the table each eigen-mode runs on with its own power law
|
|
534
|
+
# (matrix log linear in log rho, through the first two rows): a strut
|
|
535
|
+
# cell's bending modes (shear for the cubic) fall about as rho^2.3
|
|
536
|
+
# while its stretching modes fall as rho, so holding the shape of the
|
|
537
|
+
# first row read the shear 3x too stiff at rho 0.02
|
|
538
|
+
u = (np.log(np.clip(rho[low], 1e-3, 1.0)) - np.log(xs[0])) / (np.log(xs[1]) - np.log(xs[0]))
|
|
539
|
+
c_low, p_low = _spd_powerlaw(C[0], C[1], u), _spd_powerlaw(P[0], P[1], u)
|
|
540
|
+
q[low] = q[low] * (p_low[:, 0, 0] / pp[low][:, 0, 0])[:, None] ** 2
|
|
541
|
+
c[low], pp[low] = c_low, p_low
|
|
542
|
+
return c, pp, q, F[j] * (1 - t) + F[j + 1] * t
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _spd_logm(A):
|
|
546
|
+
w, V = np.linalg.eigh(0.5 * (A + A.T))
|
|
547
|
+
return (V * np.log(np.maximum(w, 1e-300 + 1e-12 * w.max()))) @ V.T
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _spd_powerlaw(A0, A1, u):
|
|
551
|
+
"""expm(log A0 + u (log A1 - log A0)) for each u: symmetric positive
|
|
552
|
+
definite, each eigen-direction on its own power law."""
|
|
553
|
+
L0, dL = _spd_logm(A0), _spd_logm(A1) - _spd_logm(A0)
|
|
554
|
+
out = np.empty((len(u),) + A0.shape)
|
|
555
|
+
for i, ui in enumerate(np.atleast_1d(u)):
|
|
556
|
+
w, V = np.linalg.eigh(L0 + ui * dL)
|
|
557
|
+
out[i] = (V * np.exp(w)) @ V.T
|
|
558
|
+
return out
|