# MITC4 eigen 독립 검산과 판정 ## 독립 Numerical Review - Feature ID: `mitc4-shell-feast-eigen2`; Owner: `numerical-review-agent`; revision `r1`. - 입력: [requirements r1](requirements.md#requirements), [evidence](requirements.md#requirements-evidence), [새 research](research.md#research), [새 formulation r1](formulation.md), [TOLERANCE](../TOLERANCE.md). - Verdict: **`pass-for-io-definition`**. 새 수학 계약의 확인된 blocking defect 없음. - 범위: 문서·독립 수학 검산. 구현, FEAST runtime, build/test, reference comparison, Physics/release 통과 판정이 아니다. ### 판단과 독립 근거 새 질량·stabilization·constraint/FEAST 문서를 실제 작성 후 모두 읽었다. 기존 pass를 전제로 삼지 않고 새 요구 27개와 식·실패 경계를 대조했다. 기준 문서는 [프로젝트 지침](../../AGENTS.md), [PRD](../PRD.md), [ARCHITECTURE](../ARCHITECTURE.md), [ADR-028](../ADR.md#adr-028)이다. | 검토 | 판단 | | --- | --- | | 운동학·부호·DOF·단위 | `[uX,uY,uZ,thetaX,thetaY,thetaZ]`, `S=-[d]cross`, `S theta=theta cross d`의 signs와 mass cross-block transpose 일치. C0/C1/C2 차원 M/ML/ML²; 길이 congruence 후 eigen/residual norm 일관 | | 적분·consistent mass | `NiNj z^k J` 최대 성분별 차수4로 27점의 degree5 정확성 충분. Positive weights/J에서 Hq=0인 다항식은 모든 nodal u=0, Si theta=0이므로 physical20 positive, drill null4. 독립 rational 적분으로 C1 비영·행합·first moment 재현 | | Derived lumped | Partition of unity로 scalar moment row-sum과 `integral rho Ni Gi^TGi` 일치. Strict Cauchy–Schwarz로 rank5/node·central tangent inertia 양수. Jensen 차1/2와 kinetic energy 추가1/2 구분. Orbital inertia/모든 spectrum 동일성을 주장하지 않음 | | 공변 stabilization | Rotary block trace는 independent tangent orthogonal change에 불변. 여덟 항을 모두 평균하며 양의 초기 scale. Geometry/frame부터 재적분한 complete K/ML와 partial-BC static reaction이 공변; NR-MFE-001 새 식에서 해소 | | 실제 BC·finite spectrum | Free injection을 먼저 적용. Constrained director exact-zero 교집합만 mass null이고 tiny positive는 유지. Shared director와 positive kd로 KZ=Zdiag(kappa), Kpz=0,Kzz>0; partial BC에 남은 stabilization 보존. 일반 Schur 반례5/2 대 삭제3도 정합 | | Whitening·안정성 | Mp nodal SPD Cholesky와 `A=L^-1KpL^-T`·`phi=DCPL^-T x`는 exact congruence. Null rank와 numerical conditioning 분리, inverse/floor/pseudoinverse 우회 없음 | | Lowest N·multiplicity | Fixed a0의 outward enclosure가 낮은 spectrum 부재 근거. 성공 total count=n과 guarded upper search·bounded enclosing fallback, final c=count=m와 c 전체 Gram/residual을 조합. Count empty 성공의 비보장성은 필수 수학 전제에서 제거 | | Recovery·state | 최종 normalized phi에서 gi/yf를 다시 계산한 full original-free residual·Gram·Rayleigh/energy를 검사. Constrained components exact zero; modal reaction/stress를 새 output으로 만들지 않음. 검증 candidate·writer finalize 후 state commit 경계 유지 | | 비선형 영향 | Initial kd의 Dkd=D²kd=0으로 기존 objective potential의 residual/Hessian 미분을 유지. Nonlinear Hessian SPD나 초기 Dgamma=linear Td를 주장하지 않음. 선형극한·objectivity·회귀·rollback은 후속 실행 의무 | Fresh 독립 oracle의 재현 코드와 실제 값은 [numerical-review-evidence](numerical-review.md#numerical-review-evidence)에 있다. NR-MFE-001은 `kd=19/4800`, `Mff=3/50`, `Kff=10369/7500`, `lambda=10369/450`, 단위 moment `thetaZ=7500/10369`을 확인했다. Complete K/ML/response/full reaction 공변성 ratio는 모두 `1.8e-16` 미만이다. 곡면 mass rational oracle와 27점 적분 최대 entry 차는 `2.221e-16` 미만이었다. ### 고정한 기준과 후속 인계 - [내부 controls](numerical-review-controls.md): 검토 후 채택한 기존 C1–C4/C6/C7 수치 정의, 새 fixed-lower C5·finite budgets·seed·conditioning·raw classification. 기존 workflow/제외 portfolio/과거 pass는 미채택. - [27개 verification cases](numerical-review-cases.md): requirement 27개별 analytical/invariant, actual FEAST·실패 주입·남는 static coverage. Expected value는 production 재호출로 생성하지 않는다. - [reference-model](reference-model.md): 원본 TXT의 literal `Frame: Mode i`와 유일한 source `Step-2`에서 per-modal-frame 10 singleton Value families를 새로 판정했다. 공통 정책 그대로 행5%·RMS1%, 이번 singleton의 실효 각 Value1%; 모두 blocking. 외부 mode shape gate 없음. I/O는 정확한 입력 allowlist·diagnostic·영속 HDF5 경로 및 source/modal identity를 정의한다. 최소한 used lumped mass/backend, N개 public pair와 전체 c개 검증의 재현 evidence, raw negative/nearzero와 frequency validity, 실패한 optional optimization·fallback까지의 count/solve history를 의미 손실 없이 연결해야 한다. 데이터 layout은 NR가 선점하지 않는다. ### 잔여 위험·미실행 Research G01/G02의 installed count-empty/endpoint/workspace·ABI probes, n1/N=n/allzero/ repeated/clustered actual FEAST와 shifted sparse factorization memory는 **NOT RUN**이다. 현재 수학 blocker는 아니지만 구현 완료 시 필수다. 넓은 enclosing fallback도 runtime 실패나 O(n²) 메모리 비용을 없애지 않으며 bounded failure와 기존 결과 보존이 필요하다. 얇은 shell·거의 정렬된 partial BC의 positive mass conditioning, 왜곡/곡면 MITC4의 locking·accuracy, 27점 밖 geometry injectivity 및 floating count의 정확성 한계가 남는다. 이 한계를 arbitrary rank cutoff·reference fitting·tolerance 완화로 숨기지 않는다. 남는 shell/shellNL/linear-limit fresh solves와 nonlinear tangent/objectivity·balance는 미실행이다. 제외된 네 예제의 원본·복사·cache를 읽거나 사용하지 않았다. Pressure fresh 외부 coverage 부재를 명시한다. 확인된 수학 correction request는 없다. 구현 owner는 명시 controls/cases를 실제 runtime과 연결하고 I/O owner는 reference/validation projection을 보존해야 한다. Source/tests, upstream 계약·공통 tolerance·reference artifacts·Git은 수정하지 않았다. ## 독립 수학 검산 evidence - Feature ID: `mitc4-shell-feast-eigen2` - 검토일: `2026-09-08`; 기본 보고서: [numerical-review](numerical-review.md#numerical-review). - 대상: 새 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.md#numerical-review-evidence)의 평판 membrane/curvature/MITC edge-work를 검토한 뒤 geometry edge에서 길이·axes·director를 다시 얻도록 구성한 새 실행이다. 기존 수치 출력이나 저장 K를 입력으로 쓰지 않는다. 전역 24×24 physical K를 직접 적분하고 새 director의 least-aligned tangent를 재생성해 여덟 rotary diagonal의 trace mean을 계산한다. 비교 대상 congruence에만 원 K를 회전한다. Norm에는 ell=4, D=diag(ell I3,I3)를 사용하여 matrix는 D^T A D, response는 D^-1 q, force/moment residual은 D^T r로 같은 단위를 만든다. ```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.658e-16`, `7.851e-17`, `1.535e-16`, `1.706e-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 통과가 아니다. 최종 정적 Python audit는 owned Markdown 5개, 상대 파일 링크 38개, requirement 27개와 case 27개의 유일·연속 identity, UTF-8/whitespace/final newline을 검사했고 exit `0`이었다. 두 oracle 모두 exit `0`; 이후 norm의 length congruence 명시를 코드에 반영하고 영향받은 첫 oracle를 재실행하여 exit `0` 및 위 최종 scaled ratio를 확인했다. 식·판정 한계는 불변이다.