rbflab 0.1.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.
- rbflab/__init__.py +70 -0
- rbflab/assembly.py +73 -0
- rbflab/block_methods.py +292 -0
- rbflab/cuda_backend.py +85 -0
- rbflab/descriptor.py +36 -0
- rbflab/diagnostics.py +56 -0
- rbflab/differentiable_lhi.py +147 -0
- rbflab/discrete_operators.py +227 -0
- rbflab/eigen_build.py +15 -0
- rbflab/evolution.py +378 -0
- rbflab/geometry.py +190 -0
- rbflab/hermite.py +39 -0
- rbflab/kernel_compiler.py +283 -0
- rbflab/kernels.py +187 -0
- rbflab/legacy_cpp.py +120 -0
- rbflab/lhi_backends.py +293 -0
- rbflab/lhi_stokes.py +367 -0
- rbflab/mesh_adapters.py +43 -0
- rbflab/methods.py +302 -0
- rbflab/nodal.py +233 -0
- rbflab/operator_backends.py +128 -0
- rbflab/operators.py +168 -0
- rbflab/precision.py +263 -0
- rbflab/problems.py +101 -0
- rbflab/rbf_fd.py +148 -0
- rbflab/rbf_ra.py +76 -0
- rbflab/reference.py +131 -0
- rbflab/scalar_backends.py +186 -0
- rbflab/scalar_fd.py +55 -0
- rbflab/scaling.py +30 -0
- rbflab/space_stokes.py +328 -0
- rbflab/spaces.py +126 -0
- rbflab/sparse_precision.py +175 -0
- rbflab/stencils.py +86 -0
- rbflab/stokes.py +166 -0
- rbflab/stokes_polynomials.py +53 -0
- rbflab/stokes_ra.py +98 -0
- rbflab/strategies.py +45 -0
- rbflab/symbolic.py +275 -0
- rbflab/symbolic_kernel.py +172 -0
- rbflab/symbolic_system.py +149 -0
- rbflab/time_data.py +62 -0
- rbflab/torch_backend.py +277 -0
- rbflab/unsteady_stokes.py +167 -0
- rbflab/viz.py +216 -0
- rbflab-0.1.0.dist-info/METADATA +85 -0
- rbflab-0.1.0.dist-info/RECORD +50 -0
- rbflab-0.1.0.dist-info/WHEEL +5 -0
- rbflab-0.1.0.dist-info/licenses/LICENSE +21 -0
- rbflab-0.1.0.dist-info/top_level.txt +1 -0
rbflab/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""RBFLAB: radial basis functions for interpolation and PDEs."""
|
|
2
|
+
from .precision import Precision, PrecisionData
|
|
3
|
+
from .kernels import IMQ, Gaussian, PHS, Hybrid, DivergenceFree
|
|
4
|
+
from .operators import Identity, Derivative, Laplacian, NormalDerivative, Robin
|
|
5
|
+
from .geometry import PointCloud, gmsh_square, gmsh_cube, unit_box_grid
|
|
6
|
+
from .problems import LinearPDE, BoundaryCondition, Dirichlet
|
|
7
|
+
from .nodal import rbf_fd_weights
|
|
8
|
+
from .rbf_fd import RBFFD
|
|
9
|
+
from .methods import GlobalCollocation, LHI, interpolate
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"from_rbfmeshgen", "nodal_diagnostics", "TimeData", "UnsteadyStokesProblem", "LHIUnsteadyStokes", "GlobalUnsteadyStokes", "StokesProblem", "GlobalStokes", "StencilPolicy", "Precision", "PrecisionData", "IMQ", "Gaussian", "PHS", "Hybrid", "DivergenceFree", "Identity", "Derivative", "Laplacian",
|
|
13
|
+
"NormalDerivative", "Robin", "PointCloud", "gmsh_square", "gmsh_cube", "unit_box_grid", "LinearPDE",
|
|
14
|
+
"BoundaryCondition", "Dirichlet", "GlobalCollocation", "LHI", "RBFFD", "rbf_fd_weights", "interpolate",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .stencils import StencilPolicy
|
|
19
|
+
|
|
20
|
+
from .mesh_adapters import from_rbfmeshgen
|
|
21
|
+
from .diagnostics import nodal_diagnostics
|
|
22
|
+
|
|
23
|
+
from .stokes import StokesProblem, GlobalStokes
|
|
24
|
+
|
|
25
|
+
from .unsteady_stokes import TimeData, UnsteadyStokesProblem, GlobalUnsteadyStokes
|
|
26
|
+
|
|
27
|
+
from .lhi_stokes import LHIUnsteadyStokes
|
|
28
|
+
|
|
29
|
+
from .strategies import growing_hybrid_stokes, growing_stencil_size
|
|
30
|
+
__all__ += ["growing_hybrid_stokes", "growing_stencil_size"]
|
|
31
|
+
|
|
32
|
+
from .evolution import EvolutionPDE, EvolutionSystem, EvolutionTrajectory
|
|
33
|
+
__all__ += ["EvolutionPDE", "EvolutionSystem", "EvolutionTrajectory"]
|
|
34
|
+
|
|
35
|
+
from .symbolic import SymbolicScalar
|
|
36
|
+
__all__ += ["SymbolicScalar"]
|
|
37
|
+
|
|
38
|
+
from .operators import SpatialOperator
|
|
39
|
+
__all__ += ["SpatialOperator"]
|
|
40
|
+
|
|
41
|
+
from .time_data import InitialData
|
|
42
|
+
__all__ += ["InitialData"]
|
|
43
|
+
|
|
44
|
+
from .symbolic_system import SymbolicSystem, BlockPDE
|
|
45
|
+
from .block_methods import BlockGlobal, BlockLHI
|
|
46
|
+
__all__ += ["SymbolicSystem", "BlockPDE", "BlockGlobal", "BlockLHI"]
|
|
47
|
+
|
|
48
|
+
from .spaces import ScalarSpace, DivergenceFreeSpace, PressureSpace
|
|
49
|
+
__all__ += ["ScalarSpace", "DivergenceFreeSpace", "PressureSpace"]
|
|
50
|
+
|
|
51
|
+
from .legacy_cpp import LegacyCppLHIBackend
|
|
52
|
+
__all__ += ["LegacyCppLHIBackend"]
|
|
53
|
+
|
|
54
|
+
from .lhi_backends import PythonBackend, CppBackend
|
|
55
|
+
__all__ += ['PythonBackend', 'CppBackend']
|
|
56
|
+
|
|
57
|
+
from .symbolic_kernel import Kernel, Wendland
|
|
58
|
+
__all__ += ["Kernel", "Wendland"]
|
|
59
|
+
|
|
60
|
+
from .cuda_backend import CudaLHIBackend
|
|
61
|
+
__all__ += ["CudaLHIBackend"]
|
|
62
|
+
|
|
63
|
+
from .torch_backend import TorchBackend, TorchKernel
|
|
64
|
+
__all__ += ['TorchBackend', 'TorchKernel']
|
|
65
|
+
|
|
66
|
+
from .differentiable_lhi import DifferentiableLHI
|
|
67
|
+
__all__ += ['DifferentiableLHI']
|
|
68
|
+
|
|
69
|
+
from .discrete_operators import DiscreteOperator, OperatorSet
|
|
70
|
+
__all__ += ["DiscreteOperator", "OperatorSet"]
|
rbflab/assembly.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Shared functional assembly and equilibrated dense solves."""
|
|
2
|
+
import warnings
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.linalg import lu_factor, lu_solve, LinAlgWarning
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def functional_matrix(kernel, x, left, y, right):
|
|
8
|
+
from .operators import bind_operators
|
|
9
|
+
left, right = bind_operators(left,x), bind_operators(right,y)
|
|
10
|
+
out = np.empty((len(x), len(y)))
|
|
11
|
+
lg, rg = {}, {}
|
|
12
|
+
for i, op in enumerate(left):
|
|
13
|
+
lg.setdefault(op, []).append(i)
|
|
14
|
+
for i, op in enumerate(right):
|
|
15
|
+
rg.setdefault(op, []).append(i)
|
|
16
|
+
if len(lg)>8 or len(rg)>8:
|
|
17
|
+
# Point-dependent coefficients otherwise produce one tiny kernel call
|
|
18
|
+
# per point pair. Group derivative indices and multiply coefficient
|
|
19
|
+
# vectors outside the vectorized kernel blocks instead.
|
|
20
|
+
from .operators import Operator
|
|
21
|
+
out.fill(0)
|
|
22
|
+
terms=[]
|
|
23
|
+
for operators in (left,right):
|
|
24
|
+
groups={}
|
|
25
|
+
for i,op in enumerate(operators):
|
|
26
|
+
for alpha,c in op.terms:
|
|
27
|
+
rows,coefficients=groups.setdefault(alpha,([],[]))
|
|
28
|
+
rows.append(i);coefficients.append(float(c))
|
|
29
|
+
terms.append(groups)
|
|
30
|
+
for alpha,(rows,ca) in terms[0].items():
|
|
31
|
+
for beta,(cols,cb) in terms[1].items():
|
|
32
|
+
block=kernel.matrix(x[rows],y[cols],Operator(((alpha,1),)),Operator(((beta,1),)))
|
|
33
|
+
out[np.ix_(rows,cols)]+=np.asarray(ca)[:,None]*block*np.asarray(cb)[None,:]
|
|
34
|
+
return out
|
|
35
|
+
for opx, rows in lg.items():
|
|
36
|
+
for opy, cols in rg.items():
|
|
37
|
+
out[np.ix_(rows, cols)] = kernel.matrix(x[rows], y[cols], opx, opy)
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Factor:
|
|
42
|
+
"""Diagonal equilibration; no jitter, pseudoinverse or silent regularization."""
|
|
43
|
+
def __init__(self, matrix, general=False, compute_condition=True):
|
|
44
|
+
self.matrix = np.asarray(matrix, dtype=float)
|
|
45
|
+
if self.matrix.ndim != 2 or self.matrix.shape[0] != self.matrix.shape[1] or not np.isfinite(self.matrix).all():
|
|
46
|
+
raise ValueError("Expected a finite square matrix")
|
|
47
|
+
diagonal = np.max(np.abs(self.matrix), axis=1) if general else np.abs(np.diag(self.matrix))
|
|
48
|
+
if np.any(diagonal == 0):
|
|
49
|
+
raise np.linalg.LinAlgError("Zero functional diagonal")
|
|
50
|
+
self.scale = 1 / np.sqrt(diagonal)
|
|
51
|
+
self.scaled = self.scale[:, None] * self.matrix * self.scale[None, :]
|
|
52
|
+
if type(compute_condition) is not bool:raise TypeError("compute_condition must be bool")
|
|
53
|
+
self.condition = float(np.linalg.cond(self.scaled)) if compute_condition else None
|
|
54
|
+
with warnings.catch_warnings():
|
|
55
|
+
warnings.simplefilter("error", LinAlgWarning)
|
|
56
|
+
try:
|
|
57
|
+
self.lu = lu_factor(self.scaled)
|
|
58
|
+
except LinAlgWarning as exc:
|
|
59
|
+
raise np.linalg.LinAlgError(str(exc)) from exc
|
|
60
|
+
if self.condition is not None and (not np.isfinite(self.condition) or self.condition > 1e15):
|
|
61
|
+
warnings.warn(f"Ill-conditioned equilibrated system: {self.condition:.3e}",
|
|
62
|
+
RuntimeWarning, stacklevel=2)
|
|
63
|
+
|
|
64
|
+
def solve(self, rhs, transpose=False):
|
|
65
|
+
rhs = np.asarray(rhs)
|
|
66
|
+
scale = self.scale if rhs.ndim == 1 else self.scale[:, None]
|
|
67
|
+
return scale * lu_solve(self.lu, scale * rhs, trans=1 if transpose else 0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def relative_residual(matrix, solution, rhs):
|
|
71
|
+
error = np.linalg.norm(matrix @ solution - rhs, ord=np.inf)
|
|
72
|
+
# RHS-relative residual: sensitive to inaccurate local/global solves.
|
|
73
|
+
return float(error / max(np.linalg.norm(rhs, ord=np.inf), np.finfo(float).tiny))
|
rbflab/block_methods.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Stationary block Hermite collocation and local Hermite assembly.
|
|
2
|
+
|
|
3
|
+
Fields use independent scalar kernels. Differential coupling lives in explicit
|
|
4
|
+
functional rows, including incompressibility; it is not built into the kernels.
|
|
5
|
+
"""
|
|
6
|
+
from .symbolic_kernel import BoundKernel
|
|
7
|
+
import warnings
|
|
8
|
+
from dataclasses import dataclass,field
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.spatial import cKDTree
|
|
11
|
+
from .operators import Identity
|
|
12
|
+
from .kernels import ScalarKernel, PHS, Hybrid
|
|
13
|
+
from .nodal import Arithmetic,NodalBasis
|
|
14
|
+
from .precision import Precision
|
|
15
|
+
from .stencils import StencilPolicy
|
|
16
|
+
from .symbolic_system import BlockPDE
|
|
17
|
+
from .evolution import _sparse,_factor,_mv
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _kernels(selection,problem):
|
|
21
|
+
if not isinstance(selection,dict):return [selection]*len(problem.fields)
|
|
22
|
+
resolved={}
|
|
23
|
+
for key,kernel in selection.items():
|
|
24
|
+
keys=key if isinstance(key,tuple) else (key,)
|
|
25
|
+
for item in keys:
|
|
26
|
+
index=problem.field_index(item)
|
|
27
|
+
if index in resolved:raise ValueError("Kernel assigned more than once to a field")
|
|
28
|
+
resolved[index]=kernel
|
|
29
|
+
if len(resolved)!=len(problem.fields):raise ValueError("Supply a kernel for every field")
|
|
30
|
+
return [resolved[i] for i in range(len(problem.fields))]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _arithmetic(kernels,digits):
|
|
34
|
+
if any(not isinstance(kernel,(ScalarKernel,PHS,Hybrid,BoundKernel)) for kernel in kernels):
|
|
35
|
+
raise TypeError("Block fields require scalar IMQ, Gaussian, PHS or Hybrid kernels")
|
|
36
|
+
arithmetics=[Arithmetic(kernel,digits) for kernel in kernels]
|
|
37
|
+
master=arithmetics[0]
|
|
38
|
+
if master.ctx:
|
|
39
|
+
for a in arithmetics[1:]:a.ctx=master.ctx;a.backend.ctx=master.ctx
|
|
40
|
+
return arithmetics
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate(problem,cloud):
|
|
44
|
+
if not isinstance(problem,BlockPDE):raise TypeError("Expected a compiled BlockPDE")
|
|
45
|
+
if cloud.dimension!=problem.dimension:raise ValueError("Cloud and block problem dimensions must match")
|
|
46
|
+
if not len(cloud.interior):raise ValueError("Block PDEs require interior nodes")
|
|
47
|
+
for c in problem.constraints:
|
|
48
|
+
if not np.isfinite(np.asarray(c.point,dtype=float)).all():raise ValueError("Constraint points must be finite")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _boundary(problem,cloud,a):
|
|
52
|
+
result={}
|
|
53
|
+
for bc in problem.boundary:
|
|
54
|
+
labels=list(cloud.boundary) if bc.on=='boundary' else ([bc.on] if isinstance(bc.on,str) else list(bc.on))
|
|
55
|
+
for label in labels:
|
|
56
|
+
if label not in cloud.boundary:raise ValueError(f"Unknown boundary label: {label}")
|
|
57
|
+
ids=cloud.boundary[label];data=bc.rhs
|
|
58
|
+
if getattr(data,'requires_normals',False):
|
|
59
|
+
if label not in cloud.normals:raise ValueError(f"Normals required on {label}")
|
|
60
|
+
data=data.bind_normals(cloud.normals[label])
|
|
61
|
+
vals=a.data(data,cloud.points[ids])
|
|
62
|
+
for j,node in enumerate(ids):
|
|
63
|
+
key=(int(node),bc.slot)
|
|
64
|
+
if key in result:continue
|
|
65
|
+
normal=cloud.normals[label][j] if label in cloud.normals else None
|
|
66
|
+
ops={}
|
|
67
|
+
for f,op in bc.operators.items():
|
|
68
|
+
if getattr(op,'requires_normals',False) and normal is None:raise ValueError(f"Normals required on {label}")
|
|
69
|
+
ops[f]=op.at_point(cloud.points[node],normal)
|
|
70
|
+
if not any(op.terms for op in ops.values()):raise ValueError("Zero boundary functional")
|
|
71
|
+
result[key]=(ops,vals[j])
|
|
72
|
+
if {node for node,slot in result}!=set(cloud.boundary_indices):raise ValueError("Every boundary node needs a block boundary condition")
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BlockBasis:
|
|
77
|
+
"""Hermite representers of block functionals plus a polynomial tail per field."""
|
|
78
|
+
def __init__(self,arithmetics,points,functionals,degree,scaling='physical'):
|
|
79
|
+
self.arithmetics=arithmetics;self.a=arithmetics[0]
|
|
80
|
+
self.points=np.asarray(points,dtype=float);self.functionals=functionals
|
|
81
|
+
self.n=len(points);self.bases=[];self.offsets=[];offset=self.n
|
|
82
|
+
for f,a in enumerate(arithmetics):
|
|
83
|
+
ids=[i for i,row in enumerate(functionals) if f in row]
|
|
84
|
+
if not ids:raise ValueError(f"Field {f} has no source functionals")
|
|
85
|
+
basis=NodalBasis(a,self.points[ids],degree,source_operators=[functionals[i][f] for i in ids],kernel_scaling=scaling)
|
|
86
|
+
self.bases.append((ids,basis));self.offsets.append(offset);offset+=len(basis.powers)
|
|
87
|
+
self.size=offset
|
|
88
|
+
|
|
89
|
+
def evaluation(self,points,functionals):
|
|
90
|
+
points=np.asarray(points,dtype=float);out=self.a.zeros(len(points),self.size)
|
|
91
|
+
for f,(sources,basis) in enumerate(self.bases):
|
|
92
|
+
targets=[i for i,row in enumerate(functionals) if f in row]
|
|
93
|
+
if not targets:continue
|
|
94
|
+
block=basis.evaluation(points[targets],[functionals[i][f] for i in targets])
|
|
95
|
+
cols=sources+list(range(self.offsets[f],self.offsets[f]+len(basis.powers)))
|
|
96
|
+
if self.a.ctx:
|
|
97
|
+
for i,row in enumerate(targets):
|
|
98
|
+
for j,col in enumerate(cols):out[row,col]+=block[i,j]
|
|
99
|
+
else:out[np.ix_(targets,cols)]+=block
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
def matrix(self):
|
|
103
|
+
out=self.a.zeros(self.size,self.size)
|
|
104
|
+
out[:self.n,:]=self.evaluation(self.points,self.functionals)
|
|
105
|
+
P=out[:self.n,self.n:]
|
|
106
|
+
if self.size>self.n:
|
|
107
|
+
# Check polynomial unisolvency before numerical factorization.
|
|
108
|
+
check=np.array(P.tolist() if self.a.ctx else P,dtype=float)
|
|
109
|
+
scale=np.max(abs(check),axis=0)
|
|
110
|
+
check=check/np.where(scale>0,scale,1)
|
|
111
|
+
if np.linalg.matrix_rank(check)<self.size-self.n:
|
|
112
|
+
raise np.linalg.LinAlgError("Block polynomial functionals are not unisolvent; increase the stencil or change the polynomial degree")
|
|
113
|
+
out[self.n:,:self.n]=P.T
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
def padded(self,data):return self.a.vector(list(data)+[self.a.number(0)]*(self.size-self.n))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class BlockGlobal:
|
|
121
|
+
"""Symmetric block Hermite collocation; one scalar kernel or a field mapping."""
|
|
122
|
+
kernels: object
|
|
123
|
+
polynomial_degree: int | None = None
|
|
124
|
+
precision: Precision = field(default_factory=Precision)
|
|
125
|
+
|
|
126
|
+
def assemble(self,problem,cloud):
|
|
127
|
+
_validate(problem,cloud)
|
|
128
|
+
if not isinstance(self.precision,Precision):raise TypeError("Expected Precision")
|
|
129
|
+
if self.precision.local_digits is not None or self.precision.global_dtype!='float64':raise ValueError("BlockGlobal uses global_digits")
|
|
130
|
+
kernels=_kernels(self.kernels,problem);arithmetics=_arithmetic(kernels,self.precision.global_digits);a=arithmetics[0]
|
|
131
|
+
points=[];ops=[];rhs=[]
|
|
132
|
+
for eq in problem.equations:
|
|
133
|
+
points.extend(cloud.interior);ops.extend([eq.operators]*len(cloud.interior));rhs.extend(a.data(eq.rhs,cloud.interior))
|
|
134
|
+
for (node,slot),(row,value) in _boundary(problem,cloud,a).items():
|
|
135
|
+
points.append(cloud.points[node]);ops.append(row);rhs.append(value)
|
|
136
|
+
for c in problem.constraints:
|
|
137
|
+
point=np.asarray(c.point,dtype=float)[None,:]
|
|
138
|
+
points.append(point[0]);ops.append({c.field:Identity(problem.dimension)});rhs.extend(a.data(c.value,point))
|
|
139
|
+
basis=BlockBasis(arithmetics,points,ops,self.polynomial_degree)
|
|
140
|
+
system=BlockGlobalSystem(problem,basis,basis.matrix(),basis.padded(rhs))
|
|
141
|
+
return system
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class BlockGlobalSystem:
|
|
145
|
+
def __init__(self,problem,basis,matrix,rhs):
|
|
146
|
+
self.problem,self.basis,self.matrix,self.rhs=problem,basis,matrix,rhs
|
|
147
|
+
self.arithmetic=basis.a;self.factor=None
|
|
148
|
+
|
|
149
|
+
def solve(self):
|
|
150
|
+
if self.factor is None:self.factor=self.arithmetic.factor(self.matrix)
|
|
151
|
+
z=self.factor.solve(self.rhs);a=self.arithmetic
|
|
152
|
+
diagnostics={'relative_residual':float(a.norm(_mv(self.matrix,z)-self.rhs)/(a.norm(self.rhs) or a.number(1))),
|
|
153
|
+
'unknowns':len(z),'scaled_condition':self.factor.condition,'method':'block_global',
|
|
154
|
+
'digits':a.ctx.dps if a.ctx else None}
|
|
155
|
+
return BlockSolution(self,z,diagnostics)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class BlockLHI:
|
|
160
|
+
"""Experimental block LHI, interior values and local PDE/BC constraints.
|
|
161
|
+
|
|
162
|
+
A point gauge adds a bordered row and a compatibility multiplier in its
|
|
163
|
+
specified equation slot. Inspect constraint_multipliers in diagnostics.
|
|
164
|
+
"""
|
|
165
|
+
kernels: object
|
|
166
|
+
stencil_size: int = 25
|
|
167
|
+
polynomial_degree: int | None = None
|
|
168
|
+
precision: Precision = field(default_factory=Precision)
|
|
169
|
+
stencil_policy: StencilPolicy = field(default_factory=StencilPolicy)
|
|
170
|
+
|
|
171
|
+
def assemble(self,problem,cloud):
|
|
172
|
+
_validate(problem,cloud)
|
|
173
|
+
if not isinstance(self.precision,Precision):raise TypeError("Expected Precision")
|
|
174
|
+
if self.precision.global_digits is not None:raise ValueError("BlockLHI uses local_digits and global_dtype")
|
|
175
|
+
if type(self.stencil_size) is not int or not 3<=self.stencil_size<=len(cloud.points):raise ValueError("Invalid stencil_size")
|
|
176
|
+
if not isinstance(self.stencil_policy,StencilPolicy):raise TypeError("Expected StencilPolicy")
|
|
177
|
+
kernels=_kernels(self.kernels,problem);arithmetics=_arithmetic(kernels,self.precision.local_digits)
|
|
178
|
+
local=arithmetics[0];a=local if self.precision.global_dtype=='mpmath' else Arithmetic(kernels[0])
|
|
179
|
+
bd=_boundary(problem,cloud,local);ni=len(cloud.interior);nf=len(problem.fields);nc=len(problem.constraints)
|
|
180
|
+
interior={int(node):i for i,node in enumerate(cloud.interior_indices)}
|
|
181
|
+
rhs_data=[local.data(eq.rhs,cloud.points) for eq in problem.equations]
|
|
182
|
+
size=nf*ni+nc;rows=[{} for _ in range(size)];rhs=[a.number(0)]*size
|
|
183
|
+
tree=cKDTree(cloud.points);stencils=[];identity=Identity(problem.dimension)
|
|
184
|
+
for center in cloud.interior_indices:
|
|
185
|
+
neighbors=self.stencil_policy.select(tree,cloud.points[center],self.stencil_size,self.polynomial_degree)
|
|
186
|
+
selected=set(int(j) for j in neighbors);sc=[int(j) for j in neighbors if int(j) in interior]
|
|
187
|
+
points=[];ops=[];unknowns=[];known=[]
|
|
188
|
+
for f in range(nf):
|
|
189
|
+
for node in sc:
|
|
190
|
+
points.append(cloud.points[node]);ops.append({f:identity})
|
|
191
|
+
unknowns.append(f*ni+interior[node]);known.append(local.number(0))
|
|
192
|
+
ns=len(unknowns)
|
|
193
|
+
for (node,slot),(row,value) in bd.items():
|
|
194
|
+
if node in selected:points.append(cloud.points[node]);ops.append(row);known.append(value)
|
|
195
|
+
for f,eq in enumerate(problem.equations):
|
|
196
|
+
for node in sc:
|
|
197
|
+
if node!=center:
|
|
198
|
+
points.append(cloud.points[node]);ops.append(eq.operators);known.append(rhs_data[f][node])
|
|
199
|
+
basis=BlockBasis(arithmetics,points,ops,self.polynomial_degree,self.stencil_policy.scaling)
|
|
200
|
+
factor=local.factor(basis.matrix())
|
|
201
|
+
q=basis.evaluation(np.repeat(cloud.points[[center]],nf,axis=0),[eq.operators for eq in problem.equations]).T
|
|
202
|
+
weights=factor.solve(q,transpose=True).T[:,:basis.n]
|
|
203
|
+
stencil=dict(basis=basis,factor=factor,unknowns=unknowns,known=known,ns=ns)
|
|
204
|
+
stencils.append(stencil)
|
|
205
|
+
for f in range(nf):
|
|
206
|
+
row=f*ni+interior[int(center)];rhs[row]=a.number(rhs_data[f][center])
|
|
207
|
+
for j,col in enumerate(unknowns):rows[row][col]=a.number(weights[f,j])
|
|
208
|
+
rhs[row]-=sum(a.number(weights[f,j])*a.number(known[j]) for j in range(ns,len(known)))
|
|
209
|
+
owners=cKDTree(cloud.interior)
|
|
210
|
+
for k,c in enumerate(problem.constraints):
|
|
211
|
+
point=np.asarray(c.point,dtype=float)[None,:];_,owner=owners.query(point[0]);s=stencils[int(owner)]
|
|
212
|
+
q=s['basis'].evaluation(point,[{c.field:identity}]).T
|
|
213
|
+
w=s['factor'].solve(q,transpose=True)
|
|
214
|
+
row=nf*ni+k
|
|
215
|
+
for j,col in enumerate(s['unknowns']):rows[row][col]=a.number(w[j,0])
|
|
216
|
+
rhs[row]=a.number(local.data(c.value,point)[0])-sum(a.number(w[j,0])*a.number(s['known'][j]) for j in range(s['ns'],len(s['known'])))
|
|
217
|
+
for i in range(ni):rows[c.equation*ni+i][row]=a.number(1)
|
|
218
|
+
return BlockLHISystem(problem,cloud,a,local,_sparse(rows,a),a.vector(rhs),stencils,owners,nf*ni)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class BlockLHISystem:
|
|
222
|
+
def __init__(self,problem,cloud,a,local,matrix,rhs,stencils,tree,physical_unknowns):
|
|
223
|
+
self.problem,self.cloud,self.arithmetic,self.local=problem,cloud,a,local
|
|
224
|
+
self.matrix,self.rhs,self.stencils,self.tree=matrix,rhs,stencils,tree
|
|
225
|
+
self.physical_unknowns=physical_unknowns;self.factor=None
|
|
226
|
+
|
|
227
|
+
def solve(self):
|
|
228
|
+
a=self.arithmetic
|
|
229
|
+
if self.factor is None:self.factor=_factor(self.matrix,a)
|
|
230
|
+
condition=None
|
|
231
|
+
if not a.ctx and len(self.rhs)<=400:
|
|
232
|
+
condition=float(np.linalg.cond(self.matrix.toarray()))
|
|
233
|
+
if condition>1e12:
|
|
234
|
+
warnings.warn("Ill-conditioned mixed LHI system; a small algebraic residual does not establish a unique accurate field",RuntimeWarning,stacklevel=2)
|
|
235
|
+
z=self.factor.solve(self.rhs)
|
|
236
|
+
multipliers=[float(v) for v in z[self.physical_unknowns:]]
|
|
237
|
+
if any(abs(v)>1e-6*(1+float(a.norm(self.rhs))) for v in multipliers):
|
|
238
|
+
warnings.warn("Mixed LHI has non-negligible constraint multipliers: the original PDE rows are not satisfied exactly; inspect residuals",RuntimeWarning,stacklevel=2)
|
|
239
|
+
diagnostics={'experimental':True,'global_condition':condition,'relative_residual':float(a.norm(_mv(self.matrix,z)-self.rhs)/(a.norm(self.rhs) or a.number(1))),
|
|
240
|
+
'unknowns':len(z),'method':'block_lhi','local_digits':self.local.ctx.dps if self.local.ctx else None,
|
|
241
|
+
'global_digits':a.ctx.dps if a.ctx else None,
|
|
242
|
+
'constraint_multipliers':multipliers,
|
|
243
|
+
'max_local_condition':max(s['factor'].condition for s in self.stencils)}
|
|
244
|
+
return BlockSolution(self,z,diagnostics)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class BlockSolution:
|
|
248
|
+
def __init__(self,system,unknowns,diagnostics):
|
|
249
|
+
self.system,self.unknowns,self.diagnostics=system,unknowns,diagnostics
|
|
250
|
+
if isinstance(system,BlockLHISystem):
|
|
251
|
+
self.a=system.local;self.coefficients=[]
|
|
252
|
+
for s in system.stencils:
|
|
253
|
+
data=[self.a.number(unknowns[j]) for j in s['unknowns']]+s['known'][s['ns']:]
|
|
254
|
+
self.coefficients.append(s['factor'].solve(s['basis'].padded(data)))
|
|
255
|
+
else:self.a=system.arithmetic
|
|
256
|
+
|
|
257
|
+
def _rows(self,points,functionals):
|
|
258
|
+
from .methods import _query
|
|
259
|
+
points=_query(points,self.system.problem.dimension)
|
|
260
|
+
if len(functionals)!=len(points):raise ValueError("One functional per query point required")
|
|
261
|
+
if isinstance(self.system,BlockGlobalSystem):
|
|
262
|
+
return _mv(self.system.basis.evaluation(points,functionals),self.unknowns)
|
|
263
|
+
result=self.a.vector([0]*len(points))
|
|
264
|
+
if not len(points):return result
|
|
265
|
+
_,owners=self.system.tree.query(points)
|
|
266
|
+
for owner in np.unique(owners):
|
|
267
|
+
ids=np.flatnonzero(owners==owner);s=self.system.stencils[int(owner)]
|
|
268
|
+
block=s['basis'].evaluation(points[ids],[functionals[int(i)] for i in ids])
|
|
269
|
+
values=_mv(block,self.coefficients[int(owner)])
|
|
270
|
+
for j,i in enumerate(ids):result[int(i)]=values[j]
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
def evaluate(self,points,*,field=None,operator=None,extended=False):
|
|
274
|
+
from .methods import _query
|
|
275
|
+
points=_query(points,self.system.problem.dimension)
|
|
276
|
+
if extended and not self.a.ctx:raise ValueError("Extended evaluation requires extended arithmetic")
|
|
277
|
+
p=self.system.problem;fields=range(len(p.fields)) if field is None else [p.field_index(field)]
|
|
278
|
+
op=operator or Identity(p.dimension);out=self.a.zeros(len(points),len(fields))
|
|
279
|
+
for j,f in enumerate(fields):
|
|
280
|
+
values=self._rows(points,[{f:op}]*len(points))
|
|
281
|
+
for i,v in enumerate(values):out[i,j]=v
|
|
282
|
+
if extended:return out
|
|
283
|
+
result=np.asarray(out.tolist() if self.a.ctx else out,dtype=float).reshape(len(points),len(fields))
|
|
284
|
+
return result[:,0] if field is not None else result
|
|
285
|
+
|
|
286
|
+
def residuals(self,points,*,extended=False):
|
|
287
|
+
if extended and not self.a.ctx:raise ValueError("Extended evaluation requires extended arithmetic")
|
|
288
|
+
p=self.system.problem;out=self.a.zeros(len(points),len(p.equations))
|
|
289
|
+
for j,eq in enumerate(p.equations):
|
|
290
|
+
vals=self._rows(points,[eq.operators]*len(points));rhs=self.a.data(eq.rhs,points)
|
|
291
|
+
for i,v in enumerate(vals):out[i,j]=v-rhs[i]
|
|
292
|
+
return out if extended else np.asarray(out.tolist() if self.a.ctx else out,dtype=float)
|
rbflab/cuda_backend.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Experimental CUDA Float64 local Stokes weights; global solve stays on CPU."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
import hashlib,json,os,re,subprocess,tempfile,time
|
|
6
|
+
import numpy as np
|
|
7
|
+
from .kernel_compiler import as_bound,stokes_header,ROOT
|
|
8
|
+
from .legacy_cpp import _linux_path
|
|
9
|
+
from .lhi_backends import _prepare,_finish
|
|
10
|
+
|
|
11
|
+
@lru_cache(maxsize=1)
|
|
12
|
+
def cuda_toolchain():
|
|
13
|
+
prefix=['wsl.exe','-d','Ubuntu','--'] if os.name=='nt' else []
|
|
14
|
+
nvcc=subprocess.check_output(prefix+['nvcc','--version'],text=True)
|
|
15
|
+
capability=subprocess.check_output(prefix+['nvidia-smi','--query-gpu=compute_cap','--format=csv,noheader'],text=True).splitlines()[0].strip().replace('.','')
|
|
16
|
+
if not capability.isdigit():raise RuntimeError('Cannot determine CUDA architecture')
|
|
17
|
+
return prefix,nvcc,capability
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def compile_cuda(velocity,pressure,cache_dir=None):
|
|
21
|
+
prefix,nvcc,arch=cuda_toolchain();v,p=as_bound(velocity),as_bound(pressure)
|
|
22
|
+
dependencies=[ROOT/'cpp/cuda_lhi.cu',Path(__file__),Path(__file__).with_name('kernel_compiler.py'),Path(__file__).with_name('symbolic_kernel.py')]
|
|
23
|
+
identity=dict(velocity=repr(v.family),pressure=repr(p.family),nvcc=nvcc,arch=arch,flags=['-O3','--fmad=false','-ccbin=g++-12'],sources={str(x.name):hashlib.sha256(x.read_bytes()).hexdigest() for x in dependencies})
|
|
24
|
+
key=hashlib.sha256(json.dumps(identity,sort_keys=True).encode()).hexdigest();root=Path(cache_dir) if cache_dir else Path.home()/'.cache/rbflab/cuda';folder=root/key
|
|
25
|
+
manifest=folder/'manifest.json'
|
|
26
|
+
if manifest.is_file():
|
|
27
|
+
stored=json.loads(manifest.read_text())
|
|
28
|
+
if not all((folder/n).is_file() and hashlib.sha256((folder/n).read_bytes()).hexdigest()==h for n,h in stored['files'].items()):raise RuntimeError('CUDA cache integrity failure')
|
|
29
|
+
return folder/'cuda_lhi'
|
|
30
|
+
if folder.exists():raise RuntimeError('Incomplete CUDA cache; select a fresh cache directory')
|
|
31
|
+
code=stokes_header(v.family,p.family)
|
|
32
|
+
code=re.sub(r'Real\("([^"\n]+)"\)',lambda m:'('+m.group(1)+'.0)' if re.fullmatch(r'-?\d+',m.group(1)) else '('+m.group(1)+')',code)
|
|
33
|
+
code=code.replace('std::vector<Real> params','const double* params').replace('const std::vector<Real>& params','const double* params')
|
|
34
|
+
code=re.sub(r'params.at\((\d+)\)',r'params[\1]',code).replace('Real','double').replace('inline double','__device__ inline double')
|
|
35
|
+
code=re.sub(r'throw std::runtime_error\("[^"\n]+"\);','return nan("");',code)
|
|
36
|
+
source=(ROOT/'cpp/cuda_lhi.cu').read_text().replace('// RBFLAB_GENERATED_SPACE',code)
|
|
37
|
+
root.mkdir(parents=True,exist_ok=True)
|
|
38
|
+
with tempfile.TemporaryDirectory(prefix='build-',dir=root) as tmp:
|
|
39
|
+
temp=Path(tmp);(temp/'kernel.cu').write_text(source)
|
|
40
|
+
path=_linux_path(temp)
|
|
41
|
+
result=subprocess.run(prefix+['nvcc','-O3','-std=c++17','--fmad=false','-ccbin','g++-12','-arch=sm_'+arch,path+'/kernel.cu','-lcublas','-o',path+'/cuda_lhi'],capture_output=True,text=True,timeout=300)
|
|
42
|
+
if result.returncode:raise RuntimeError('CUDA compile failed: '+result.stderr)
|
|
43
|
+
files={n:hashlib.sha256((temp/n).read_bytes()).hexdigest() for n in ('kernel.cu','cuda_lhi')}
|
|
44
|
+
(temp/'manifest.json').write_text(json.dumps(dict(identity=identity,files=files),indent=2));temp.rename(folder)
|
|
45
|
+
return folder/'cuda_lhi'
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class CudaLHIBackend:
|
|
49
|
+
"""Feasibility backend: 2D, physical, unaugmented, Float64; no condition estimate."""
|
|
50
|
+
batch_size: int = 256
|
|
51
|
+
shape_rule: str = 'fixed'
|
|
52
|
+
cache_dir: str | None = None
|
|
53
|
+
timeout: float = 300
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
if type(self.batch_size) is not int or not 1<=self.batch_size<=4096:raise ValueError('batch_size must be 1..4096')
|
|
56
|
+
if self.shape_rule not in ('fixed','legacy_hardy'):raise ValueError('Unknown shape rule')
|
|
57
|
+
def assemble(self,method,problem,cloud):
|
|
58
|
+
begin=time.perf_counter()
|
|
59
|
+
if method.precision.local_digits is not None or method.precision.global_dtype!='float64':raise ValueError('CUDA prototype requires Float64 local/global arithmetic')
|
|
60
|
+
if cloud.dimension!=2 or method.stencil_policy.scaling!='physical' or method.polynomial_degree is not None:raise NotImplementedError('CUDA prototype requires unaugmented physical-coordinate 2D stencils')
|
|
61
|
+
pk=method.pressure_kernel or method.kernel
|
|
62
|
+
start=time.perf_counter();executable=compile_cuda(method.kernel,pk,self.cache_dir);compile_seconds=time.perf_counter()-start
|
|
63
|
+
a,tasks,stencils=_prepare(method,problem,cloud,self.shape_rule)
|
|
64
|
+
nv=len(as_bound(method.kernel).values);np_=len(as_bound(pk).values)
|
|
65
|
+
num=lambda v:format(float(v),'.17g')
|
|
66
|
+
lines=[f'{len(tasks)} {self.batch_size} {nv} {np_}']
|
|
67
|
+
for task in tasks:
|
|
68
|
+
lines.append(' '.join([str(len(task['points'])),num(task['mu']),*(num(v) for v in task['origin']),*(num(v) for v in as_bound(task['vk']).values),*(num(v) for v in as_bound(task['pk']).values)]))
|
|
69
|
+
lines.extend(' '.join([str(code),*(num(v) for v in point)]) for code,point in zip(task['codes'],task['points']))
|
|
70
|
+
with tempfile.TemporaryDirectory(prefix='rbflab_cuda_') as tmp:
|
|
71
|
+
folder=Path(tmp);source=folder/'input';target=folder/'output';source.write_text('\n'.join(lines)+'\n')
|
|
72
|
+
start=time.perf_counter();prefix=cuda_toolchain()[0]
|
|
73
|
+
runtime_prefix=prefix+['env','LD_LIBRARY_PATH=/usr/lib/wsl/lib'] if os.name=='nt' else prefix
|
|
74
|
+
result=subprocess.run([*runtime_prefix,_linux_path(executable),_linux_path(source),_linux_path(target)],capture_output=True,text=True,timeout=self.timeout);process=time.perf_counter()-start
|
|
75
|
+
if result.returncode:raise RuntimeError('CUDA local backend: '+result.stderr)
|
|
76
|
+
gpu=json.loads(result.stdout);results=[]
|
|
77
|
+
for i,line in enumerate(target.read_text().splitlines()):
|
|
78
|
+
items=line.split();n=len(tasks[i]['points'])
|
|
79
|
+
if int(items[0])!=i or int(items[1])!=n or len(items)!=4+4*n:raise RuntimeError('Invalid CUDA weight output')
|
|
80
|
+
if not all(np.isfinite(float(v)) for v in items[3:]):raise RuntimeError('Nonfinite CUDA output')
|
|
81
|
+
results.append(dict(condition=None,residual=items[3],weights=[items[4+k*n:4+(k+1)*n] for k in range(4)]))
|
|
82
|
+
if len(results)!=len(tasks):raise RuntimeError('Incomplete CUDA result')
|
|
83
|
+
system=_finish(method,problem,cloud,a,tasks,stencils,results,dict(backend='cuda_float64',compile_seconds=compile_seconds,cuda_process_seconds=process,gpu=gpu,batch_size=self.batch_size,local_digits=None,compute_condition=False,off_node_backend='lazy Python',shape_rule=self.shape_rule))
|
|
84
|
+
system.backend_diagnostics['assembly_seconds']=time.perf_counter()-begin
|
|
85
|
+
return system
|
rbflab/descriptor.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Small Float64 index-one descriptor spectrum diagnostic.
|
|
2
|
+
|
|
3
|
+
For M udot + A u=0, left-null(M) gives algebraic constraints C u=0.
|
|
4
|
+
Restrict u to null(C), project onto range(M), then solve the reduced pencil.
|
|
5
|
+
Rank thresholds make this a numerical diagnostic, not a formal DAE certificate.
|
|
6
|
+
"""
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.linalg import eig,svd,null_space
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def descriptor_spectrum(stiffness,mass,rtol=None):
|
|
12
|
+
a,m=np.asarray(stiffness),np.asarray(mass);n=len(m)
|
|
13
|
+
rtol=rtol or 100*n*np.finfo(float).eps
|
|
14
|
+
u,s,vh=svd(m);rank=int(np.sum(s>rtol*s[0])) if s[0]>0 else 0
|
|
15
|
+
info={'mass_rank':rank,'algebraic_modes':n-rank,'rank_relative_tolerance':rtol}
|
|
16
|
+
if rank==0:
|
|
17
|
+
if np.linalg.matrix_rank(a)<n:raise ValueError('Pure algebraic constraints are rank deficient')
|
|
18
|
+
return np.array([],dtype=complex),dict(info,eigenpair_relative_residual_max=None)
|
|
19
|
+
if rank==n:
|
|
20
|
+
values,vectors=eig(-a,m)
|
|
21
|
+
else:
|
|
22
|
+
constraints=u[:,rank:].T@a
|
|
23
|
+
z=null_space(constraints,rcond=rtol)
|
|
24
|
+
if z.shape[1]!=rank:
|
|
25
|
+
raise ValueError('Descriptor constraints are rank deficient; finite spectrum unresolved')
|
|
26
|
+
reduced_m=u[:,:rank].T@m@z;reduced_a=u[:,:rank].T@a@z
|
|
27
|
+
if np.linalg.matrix_rank(reduced_m,tol=rtol*np.linalg.norm(reduced_m,2))<rank:
|
|
28
|
+
raise ValueError('Descriptor has a singular reduced mass; higher-index spectrum unresolved')
|
|
29
|
+
values,w=eig(-reduced_a,reduced_m);vectors=z@w
|
|
30
|
+
residual=[]
|
|
31
|
+
norm_a,norm_m=np.linalg.norm(a,2),np.linalg.norm(m,2)
|
|
32
|
+
for value,vector in zip(values,vectors.T):
|
|
33
|
+
if not np.isfinite(value):continue
|
|
34
|
+
residual.append(float(np.linalg.norm(-a@vector-value*(m@vector))/(norm_a+abs(value)*norm_m)/np.linalg.norm(vector)))
|
|
35
|
+
info['eigenpair_relative_residual_max']=max(residual,default=None)
|
|
36
|
+
return values,info
|
rbflab/diagnostics.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Discrete nodal consistency and small-system stability diagnostics."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def nodal_diagnostics(system, exact_unknowns, *, condition_limit=200):
|
|
6
|
+
"""Diagnose an assembled LHI/RBF-FD system, before reconstruction.
|
|
7
|
+
|
|
8
|
+
Pass exact values in matrix-column order: interior values for LHI, all
|
|
9
|
+
values for RBF-FD. This API does not accept global RBF coefficients.
|
|
10
|
+
Row normalization removes equation magnitude, not physical units.
|
|
11
|
+
A fixed alternating perturbation of the row-normalized RHS measures one
|
|
12
|
+
response direction, not worst-case amplification. The system is factored
|
|
13
|
+
lazily if necessary. Float64 small systems additionally report the row-scaled infinity-norm
|
|
14
|
+
condition; extended systems keep consistency arithmetic extended and omit
|
|
15
|
+
that Float64 condition estimate. No claim of a stability certificate.
|
|
16
|
+
"""
|
|
17
|
+
matrix=system.matrix
|
|
18
|
+
if hasattr(matrix,"matvec"):
|
|
19
|
+
ctx=matrix.ctx
|
|
20
|
+
from .precision import mp_number
|
|
21
|
+
exact=ctx.matrix([mp_number(ctx,v) for v in exact_unknowns])
|
|
22
|
+
if any(not ctx.isfinite(v) for v in exact):
|
|
23
|
+
raise ValueError("Exact values must be finite")
|
|
24
|
+
if len(exact)!=len(system.rhs):
|
|
25
|
+
raise ValueError("Expected one exact value per unknown")
|
|
26
|
+
defect=matrix.matvec(exact)-system.rhs
|
|
27
|
+
norm=lambda x:ctx.norm(x,"inf")
|
|
28
|
+
scales=[max((abs(v) for v in row.values()),default=ctx.zero) for row in matrix.rows]
|
|
29
|
+
if any(v==0 for v in scales):
|
|
30
|
+
raise ValueError("Cannot normalize an empty equation")
|
|
31
|
+
from .sparse_precision import MPSparseLU
|
|
32
|
+
factor=system.factor or MPSparseLU(matrix)
|
|
33
|
+
response=factor.solve(ctx.matrix([scale*(-1)**i for i,scale in enumerate(scales)]))
|
|
34
|
+
normalized=ctx.matrix([defect[i]/scale for i,scale in enumerate(scales)])
|
|
35
|
+
return {"consistency_inf":float(norm(defect)),
|
|
36
|
+
"row_scaled_consistency_inf":float(norm(normalized)),
|
|
37
|
+
"consistency_decimal":ctx.nstr(norm(defect),16),
|
|
38
|
+
"digits":ctx.dps,"row_scaled_condition_inf":None,
|
|
39
|
+
"alternating_rhs_gain_inf":float(norm(response))}
|
|
40
|
+
exact=np.asarray(exact_unknowns,dtype=float)
|
|
41
|
+
if exact.shape!=(matrix.shape[1],) or not np.isfinite(exact).all():
|
|
42
|
+
raise ValueError("Expected one finite exact value per unknown")
|
|
43
|
+
defect=matrix@exact-system.rhs
|
|
44
|
+
scales=np.asarray(abs(matrix).max(axis=1).toarray()).ravel()
|
|
45
|
+
if np.any(scales==0):
|
|
46
|
+
raise ValueError("Cannot normalize an empty equation")
|
|
47
|
+
from scipy.sparse.linalg import splu
|
|
48
|
+
factor=system.factor or splu(matrix)
|
|
49
|
+
response=factor.solve(scales*(-1.)**np.arange(len(scales)))
|
|
50
|
+
condition=None
|
|
51
|
+
if matrix.shape[0]<=condition_limit:
|
|
52
|
+
condition=float(np.linalg.cond(matrix.toarray()/scales[:,None],np.inf))
|
|
53
|
+
return {"consistency_inf":float(np.max(abs(defect))),
|
|
54
|
+
"row_scaled_consistency_inf":float(np.max(abs(defect)/scales)),
|
|
55
|
+
"digits":None,"row_scaled_condition_inf":condition,
|
|
56
|
+
"alternating_rhs_gain_inf":float(np.max(abs(response)))}
|