Files
FESADev/docs/mitc4-shell-feast-eigen2/numerical-review-evidence.md
T
2026-09-08 21:27:10 +09:00

185 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 독립 수학 검산 evidence
- Feature ID: `mitc4-shell-feast-eigen2`
- Owner: `numerical-review-agent`
- 검토일: `2026-09-08`; 기본 보고서: [numerical-review](numerical-review.md).
- 대상: 새 requirement r1. 아래는 Python 표준 라이브러리만 사용하는 수학 검산이며 C++/FEAST 실행이 아니다.
## NR-MFE-001의 geometry부터 재계산
`a=(0,1,0)`, `b=(-4/5,0,3/5)`, `d=(3/5,0,4/5)`이고
`X=[-2a-b,2a-b,2a+b,-2a+b]`, `E=12`, `nu=0`, `rho=t=1`이다.
Node 1 URZ만 free이고 나머지 23 DOF는 fixed다. Z45° 회전은 같은 physical
free subspace를 유지한다. 아래는 [이전 독립 적분](../mitc4-shell-feast-eigen/numerical-review-evidence.md)의
평판 membrane/curvature/MITC edge-work를 검토한 뒤 geometry edge에서 길이·axes·director를
다시 얻도록 구성한 새 실행이다. 기존 수치 출력이나 저장 K를 입력으로 쓰지 않는다.
전역 24×24 physical K를 직접 적분하고 새 director의 least-aligned tangent를 재생성해
여덟 rotary diagonal의 trace mean을 계산한다. 비교 대상 congruence에만 원 K를 회전한다.
```python
from math import sqrt, fsum
from fractions import Fraction as F
def dot(a,b): return fsum(x*y for x,y in zip(a,b))
def cross(a,b): return (a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0])
def norm(a): return sqrt(dot(a,a))
def unit(a): return tuple(x/norm(a) for x in a)
def sub(a,b): return tuple(x-y for x,y in zip(a,b))
def rz(v): return ((v[0]-v[1])/sqrt(2),(v[0]+v[1])/sqrt(2),v[2])
def action(A,x): return [dot(row,x) for row in A]
def rotate_vector(v): return [x for i in range(0,len(v),3) for x in rz(v[i:i+3])]
def rotate_matrix(A):
left=list(zip(*(rotate_vector(c) for c in zip(*A))))
return [rotate_vector(row) for row in left]
def err(a,b): return norm(sub(a,b))/(norm(a)+norm(b))
D=[4. if i%6<3 else 1. for i in range(24)]
def merr(A,B): return err([D[i]*A[i][j]*D[j] for i in range(24) for j in range(24)],[D[i]*B[i][j]*D[j] for i in range(24) for j in range(24)])
def qerr(a,b): return err([x/s for x,s in zip(a,D)],[x/s for x,s in zip(b,D)])
def rerr(a,b): return err([x*s for x,s in zip(a,D)],[x*s for x,s in zip(b,D)])
def build(X):
edge1=sub(X[1],X[0]); edge2=sub(X[3],X[0])
lx,ly=norm(edge1),norm(edge2); ex,ey=unit(edge1),unit(edge2)
d=unit(cross(ex,ey)); area=lx*ly
K=[[0.]*24 for _ in range(24)]
for xi in [-1/sqrt(3),1/sqrt(3)]:
for eta in [-1/sqrt(3),1/sqrt(3)]:
fields=[]
for sx,sy in [(-1,-1),(1,-1),(1,1),(-1,1)]:
nx=sx*(1+sy*eta)/(2*lx); ny=sy*(1+sx*xi)/(2*ly)
tx=(1+sy*eta)/4; ty=(1+sx*xi)/4
for c in range(3):
fields.append(([nx*ex[c],ny*ey[c],ny*ex[c]+nx*ey[c]],[0,0,0],[nx*d[c],ny*d[c]]))
for c in range(3):
fields.append(([0,0,0],[nx*ey[c],-ny*ex[c],ny*ey[c]-nx*ex[c]],[tx*ey[c],-ty*ex[c]]))
for i,(mi,bi,si) in enumerate(fields):
for j,(mj,bj,sj) in enumerate(fields):
K[i][j]+=area/4*(12*mi[0]*mj[0]+12*mi[1]*mj[1]+6*mi[2]*mj[2]+bi[0]*bj[0]+bi[1]*bj[1]+.5*bi[2]*bj[2]+5*dot(si,sj))
axis=min(range(3),key=lambda k:(abs(d[k]),k))
ta=unit(tuple(float(c==axis)-d[axis]*d[c] for c in range(3)))
tb=cross(d,ta); diag=[]
for i in range(4):
for tangent in [ta,tb]:
v=[0.]*24; v[6*i+3:6*i+6]=tangent
diag.append(dot(v,action(K,v)))
kd=fsum(diag)/8000
M=[[0.]*24 for _ in range(24)]
for i in range(4):
for a in range(3):
M[6*i+a][6*i+a]=area/4
for b in range(3):
K[6*i+3+a][6*i+3+b]+=kd*d[a]*d[b]
M[6*i+3+a][6*i+3+b]=area/48*(float(a==b)-d[a]*d[b])
return K,M,kd,d,ey,diag
a=(0.,1.,0.); b=(-.8,0.,.6)
X=[tuple(s*a[k]+t*b[k] for k in range(3)) for s,t in [(-2,-1),(2,-1),(2,1),(-2,1)]]
K,M,kd,d,ey,diags=build(X)
Kr,Mr,kdr,dr,eyr,diagsr=build([rz(x) for x in X])
load=[0.]*24; load[3:6]=[5*x/3 for x in ey]; lr=rotate_vector(load)
q=[0.]*24; qr=[0.]*24; q[5]=load[5]/K[5][5]; qr[5]=lr[5]/Kr[5][5]
r=sub(action(K,q),load); rr=sub(action(Kr,qr),lr)
targets=[float(F(19,4800)),float(F(10369,7500)),float(F(3,50)),float(F(10369,450)),float(F(7500,10369))]
actual=[kd,K[5][5],M[5][5],K[5][5]/M[5][5],q[5]]
for v,w in zip(actual,targets): assert abs(v-w)/(abs(v)+abs(w))<1e-12
cov=[merr(Kr,rotate_matrix(K)),merr(Mr,rotate_matrix(M)),qerr(qr,rotate_vector(q)),rerr(rr,rotate_vector(r))]
assert max(cov)<1e-12
assert abs(kd-kdr)/(abs(kd)+abs(kdr))<1e-12
assert abs(dot(d,load[3:6]))/norm(load[3:6])<1e-12
for A,v,f,residual in [(K,q,load,r),(Kr,qr,lr,rr)]:
assert abs(residual[5])/(abs(A[5][5]*v[5])+abs(f[5]))<=1e-10
print('partial_bc=',actual)
print('rotated_partial_bc=',[kdr,Kr[5][5],Mr[5][5],Kr[5][5]/Mr[5][5],qr[5]])
print('covariance_K_M_q_reaction=',cov)
print('fresh_rotary_diagonals=',diags,diagsr)
```
## 독립 rational mass oracle
다음 coefficient-dictionary 적분은 production shape/J/질량 함수를 사용하지 않는다.
이전 문서의 rational fixture 자체는 재사용하지만 모든 계수·행합·first moment를 이번에
다시 적분한다. Kernel-level director fixture이며 input explicit-normal 지원을 추가하지 않는다.
```python
from fractions import Fraction as F
from itertools import permutations, product
from math import sqrt
def const(x): return {(0,0,0):F(x)}
def add(*args):
out={}
for a in args:
for p,c in a.items(): out[p]=out.get(p,F(0))+c
return {p:c for p,c in out.items() if c}
def scale(a,c): return {p:v*c for p,v in a.items() if v*c}
def mul(a,b):
out={}
for p,c in a.items():
for q,d in b.items():
key=tuple(x+y for x,y in zip(p,q));out[key]=out.get(key,F(0))+c*d
return {p:c for p,c in out.items() if c}
def diff(a,k):
out={}
for p,c in a.items():
if p[k]:
q=list(p);q[k]-=1;out[tuple(q)]=c*p[k]
return out
def integ(a): return sum((c*F(8,(p[0]+1)*(p[1]+1)*(p[2]+1)) for p,c in a.items() if all(v%2==0 for v in p)),F(0))
def value(a,x): return sum(float(c)*x[0]**p[0]*x[1]**p[1]*x[2]**p[2] for p,c in a.items())
v=[{tuple(int(k==i) for k in range(3)):F(1)} for i in range(3)]
N=[scale(mul(add(const(1),scale(v[0],sx)),add(const(1),scale(v[1],sy))),F(1,4)) for sx,sy in [(-1,-1),(1,-1),(1,1),(-1,1)]]
X=[(-1,-1,F(1,10)),(1,-1,-F(1,10)),(1,1,F(1,5)),(-1,1,-F(1,10))]
d=[(0,0,1),(F(3,5),0,F(4,5)),(F(2,3),F(1,3),F(2,3)),(0,F(3,5),F(4,5))]
z=scale(v[2],F(1,4));pos=[add(*(mul(N[i],add(const(X[i][c]),scale(z,d[i][c]))) for i in range(4))) for c in range(3)]
J={}
for perm in permutations(range(3)):
inversions=sum(perm[i]>perm[j] for i in range(3) for j in range(i+1,3))
term=const((-1)**inversions)
for c in range(3): term=mul(term,diff(pos[c],perm[c]))
J=add(J,term)
rhoJ=scale(J,F(3));zk=[const(1),z,mul(z,z)]
polys=[[[mul(mul(mul(rhoJ,N[i]),N[j]),zk[k]) for j in range(4)] for i in range(4)] for k in range(3)]
C=[[[integ(p) for p in row] for row in mat] for mat in polys]
rows=[[sum(row,F(0)) for row in mat] for mat in C]
assert [mat[0][0] for mat in C]==[F(17609,28800),F(11003,1440000),F(88177,6912000)]
assert [sum(row,F(0)) for row in rows]==[F(7743,1600),F(739,12000),F(517,5120)]
first=[sum((rows[0][i]*X[i][c]+rows[1][i]*d[i][c] for i in range(4)),F(0)) for c in range(3)]
assert first==[integ(mul(rhoJ,p)) for p in pos]
g=[(-sqrt(3/5),5/9),(0.,8/9),(sqrt(3/5),5/9)]
maxerr=0.;jvals=[]
for q in product(g,repeat=3): jvals.append(value(J,[s[0] for s in q]))
for k,i,j in product(range(3),range(4),range(4)):
approx=sum(value(polys[k][i][j],[s[0] for s in q])*q[0][1]*q[1][1]*q[2][1] for q in product(g,repeat=3))
maxerr=max(maxerr,abs(approx-float(C[k][i][j])))
assert min(jvals)>0 and maxerr<1e-12
central=[rows[2][i]-rows[1][i]**2/rows[0][i] for i in range(4)]
assert min(central)>0
print('mass_C11=',[str(mat[0][0]) for mat in C])
print('mass_rows=',[[str(x) for x in row] for row in rows])
print('mass_first_moment=',[str(x) for x in first])
print('mass_Jrange_Gauss_error_central=',min(jvals),max(jvals),maxerr,float(min(central)))
```
## 이번 실행 결과
2026-09-08 PowerShell here-string에서 이 문서의 두 Python block을 추출하고
`subprocess.run([sys.executable,'-B','-'])`으로 각각 실행했다. Oracle 1/2 exit `0`이다.
| 항목 | 이번 직접 계산 |
| --- | --- |
| `kd`, `Kff`, `Mff` | `0.003958333333333333`, `1.3825333333333332`, `0.05999999999999998` |
| `lambda` 원/회전 | `23.04222222222223` / `23.042222222222208`; exact `10369/450 [T^-2]` |
| 단위 moment의 `thetaZ` 원/회전 | `0.7233098659465717` / `0.7233098659465714`; exact `7500/10369` |
| Complete `K`, `ML`, response, full reaction 공변성 ratio | `1.483e-16`, `7.915e-17`, `1.535e-16`, `1.654e-16` |
| 원/회전 free residual raw | `0`, `-1.1102230246251565e-16`; 기존 scaled `1e-10` 기준 충족 |
| Rational `C0_11,C1_11,C2_11` | `17609/28800`, `11003/1440000`, `88177/6912000` |
| `sum m,sum h,sum j` | `7743/1600`, `739/12000`, `517/5120` |
| Spatial first moment 두 독립식 | Exact equal: `(-79747/360000,-685951/2880000,49651/320000)` |
| 27점 J min/max | `0.1285991922161525` / `0.2701341411171808` |
| Gauss vs rational 최대 entry absolute error | `2.220446049250313e-16` |
| 최소 nodal central inertia | `0.022318161338791092 > 0` |
최초 oracle 1 exit `1`은 회전 후 raw free residual에 잘못 추가한 exact-zero assertion이었다.
반력은 일반 floating arithmetic이며 원 계약은 scaled residual `1e-10`이다.
이를 기존 기준으로 수정하고 raw 값을 보존했다. Exact constrained displacement zero 조건,
정식화 또는 승인 tolerance를 변경한 것이 아니다.
외부 solver, FEAST runtime, build/test, retained static fresh solve 및 HDF5–reference
comparison은 **NOT RUN**이다. 위 항등식의 통과는 구현/reference 통과가 아니다.