OpenSees가 수렴하지 않을 때: 메시지 읽기, 한계점, 그리고 실제로 그 스텝을 살리는 알고리즘
해석을 죽인 그 스텝은 잔차가 줄고 있었고 100회 중 5회에서 멈췄다. 애초에 허용오차가 문제가 아니었다. 닫힌 해와 1.7e-13까지 일치하도록 검증한 얕은 트러스에서 하중제어는 평형경로 0.173 m를 건너뛰고도 모든 스텝에서 성공을 보고하며, 여섯 알고리즘 중 셋은 한계점을 지나 한계하중의 3배까
7부에서 해석을 죽인 그 스텝은 수렴 실패가 아니었습니다. 다섯 번의 반복에서 잔차 노름은 7.2 × 10⁻³, 1.3 × 10⁻⁵, 1.3 × 10⁻⁶, 2.3 × 10⁻⁶ — 줄고 있었고, 세 번째 반복에서 이미 허용오차 10⁻⁶보다 세 자릿수 안쪽이었습니다. 메시지가 지목한 범인은 달랐습니다. ForceBeamColumn2d::update — failed to get compatible element forces & deformations for element: 8. 반복 한도를 올렸어도 아무것도 달라지지 않았을 것입니다. 이 시리즈에서 나중에 측정했듯 같은 스텝이 반복 30·100·500·2000회에서 모두 실패하며, 언제나 정확히 5회 만에 멈춥니다. 허용오차를 풀면 지나가기는 합니다. 다만 요소가 풀 수 없는 시행상태에 닿기 전에 반복을 멈추는 것일 뿐, 그것을 푼 것과는 다릅니다.
이 글은 설계기준도, 합격 판정 기준도, 권장 허용오차도 제시하지 않습니다. 7부에서 쓴 회복 캐스케이드를 부품 단위로 분해해, 서로를 이기는 모델 위에서 각각을 측정합니다. 하중제어가 아무 불평 없이 지나쳐버리는 한계점을 가진 얕은 트러스, 여덟 개 알고리즘 중 일곱이 풀지 못한 철근콘크리트 골조의 한 스텝, 비용은 3배 차이 나는데 답은 0.06% 차이인 수렴 판정 일곱 가지. 전부 OpenSeesPy 3.8.0에서 나온 값이고, 트러스는 닫힌 해 평형경로와 1.7 × 10⁻¹³까지 일치합니다. 이 글은 9부입니다. 1–8부에서 객체 모델, 탄성 골조, 파이버 단면, 동적해석, 소스 읽기, 철근콘크리트, 지진기록, 3차원 건물을 다뤘습니다.
1. 무엇이든 바꾸기 전에 메시지부터 읽는다
"수렴이 안 된다"는 말은 최소한 네 가지 서로 다른 상황을 뭉뚱그린 것입니다. 공통점은 답답한 해석자뿐이고, 메시지도 처방도 각각 다릅니다. 엉뚱한 처방을 쓰면 잘해야 반나절을 버립니다.
OpenSees가 찍는 문구실제로 일어난 일듣는 처방
ConvergenceTest object failed in test()전역 반복이 반복 횟수를 다 썼다반복 횟수 늘리기, 다른 알고리즘, 스텝 줄이기
ForceBeamColumn2d::update — failed to get compatible element forces한 요소의 내부 상태 결정이 실패했다스텝 줄이기, 재료에 경화 주기, 다른 요소
factorization failed, matrix singular메커니즘이거나 구속되지 않은 자유도모델을 고친다. 솔버 설정으로는 절대 안 된다
아무 메시지도 없다 — 모든 스텝이 0을 반환적분기가 한계점을 그냥 지나쳤다변위제어 또는 호장법
추측을 진단으로 바꾸는 명령이 둘 있고, 둘 다 공짜입니다.
ok = ops.analyze(1, dt) if ok != 0: print('사용한 반복 횟수:', ops.testIter()) print('잔차 이력 :', [f'{v:.3e}' for v in ops.testNorm() if v > 0])
여기서 측정한 실패에 이 두 줄을 걸면 답이 바로 나옵니다. 허용한 100회 중 5회만 썼고, 잔차는 줄고 있었습니다. 100회 중 5회에서 멈춘 반복은 반복 횟수가 모자란 게 아닙니다. 중단당한 것이고, 중단시킨 주체는 요소입니다.
2. 가장 위험한 실패는 실패하지 않는 실패다
얕은 2부재 트러스(von Mises 트러스)는 한계점을 가진 가장 작은 구조물입니다. 지간 2 m, 라이즈 0.10 m, EA = 2 × 10⁵ kN, 공회전 좌표계 요소, 정점에 하향 하중. 평형경로는 정확해가 있습니다.
L(v) = sqrt(a² + (h − v)²) 정점 침하 v일 때 부재 길이 N(v) = EA (L − L0) / L0 축력 P(v) = −2 N (h − v) / L 평형을 이루는 하중 상부 한계점: P = +76.2174 kN (v = 0.042361 m) 하부 한계점: P = −76.2174 kN (v = 0.157639 m)
변위제어는 정확해를 한계하중 대비 1.7 × 10⁻¹³까지 재현합니다. 0.0005 m부터 0.25 m까지 전 구간, 두 한계점과 불안정 분기 전체를 포함해서입니다. 이것이 검증입니다. 요소와 적분기는 정확히 맞습니다.
하중제어는 실패하지 않습니다. 한계점에서 v = 0.04236 m → v = 0.21563 m으로 건너뜁니다. 한 스텝에 평형경로 0.173 m를 통째로 생략하고, 한계하중의 3배까지 계속 올라갑니다. analyze() 호출은 단 한 번도 0이 아닌 값을 반환하지 않았습니다. 최종 상태는 진짜 평형 상태이기는 합니다. 다만 구조물이 스냅스루한 뒤의 반대편 분기 위에 있을 뿐입니다. 출력 어디에도 그런 말은 없습니다.
이건 분명히 짚고 갈 값어치가 있습니다. 보통의 불안을 뒤집는 사실이기 때문입니다. 실패한 해석은 뭔가 잘못됐다고 알려줍니다. 한계점을 건너뛰고 수렴한 해석은 아무것도 알려주지 않습니다. 구조물이 강성을 잃을 수 있다면 — 스냅스루, 좌굴, 연화, P-델타 붕괴 — 수렴 여부와 무관하게 하중제어는 틀린 적분기입니다.
3. 한계점에서 성공하는 알고리즘은 거짓말을 하는 알고리즘이다
같은 하중제어 해석을 여섯 개 알고리즘으로 돌리면 결과가 정확히 둘로 갈립니다.
알고리즘한계점에서의 거동최종 하중 / 한계하중
Newton통과3.000
NewtonLineSearch통과3.000
BFGS통과3.000
ModifiedNewton정지0.995
KrylovNewton정지1.000
Broyden정지1.000
멈춘 셋은 정확히 한계하중에서 실패를 보고했습니다. 해가 존재하지 않는 질문에 대한 정직한 답입니다. 통과한 셋은 반대편 분기의 평형을 찾아 성공을 보고했습니다. 뭔가 실패하면 일단 "더 센" 알고리즘으로 바꿔보는 습관이 있다면, 여기가 바로 그 습관이 유익한 실패 대신 자신만만한 오답을 만들어내는 지점입니다.
한계점의 처방은 애초에 알고리즘이 아닙니다. 적분기입니다. 어느 자유도가 단조 증가하는지 알면 DisplacementControl, 모르면 ArcLength입니다. 호장법 해석은 P = −76.07에서 +76.17 kN까지 훑으며 두 한계점을 모두 잡았고, 하중제어는 둘 다 놓쳤습니다.
4. 일곱 알고리즘이 못 푼 스텝을 하나가 풀었다
이제 실제 사례입니다. 7부의 철근콘크리트 골조를 Tabas × 2.0으로 가진하면 t = 11.455 s에서 실패합니다. 그 시각까지 다시 돌린 뒤, 같은 스텝을 알고리즘별로 한 번씩 시도합니다. 각각 100회 반복을 허용했습니다.
알고리즘회복반복시간
ModifiedNewton성공98.9 ms
Newton실패514.7 ms
NewtonLineSearch실패514.7 ms
RaphsonNewton실패515.3 ms
SecantNewton실패427.8 ms
BFGS실패128.9 ms
KrylovNewton실패632.2 ms
Broyden실패144.8 ms
이 표에서 순위 자체보다 값진 것이 셋 있습니다.
흔한 조언이 여기서는 통하지 않았습니다. "Newton이 실패하면 KrylovNewton을 써라"는 조언은 어디에나 있고, 이 시리즈에도 있었습니다. 이 스텝에서는 실패했고, 성공한 알고리즘보다 비용이 두 배 이상 들었습니다. 교훈은 KrylovNewton이 나쁘다가 아닙니다. 어느 것이 통할지 예측할 수 없다는 것이고, 캐스케이드가 존재하는 이유가 정확히 그것입니다.
이긴 쪽이 가장 쌌습니다. ModifiedNewton은 스텝당 접선을 한 번만 만들고 분해해서 재사용합니다. 반복은 5회가 아니라 9회 썼는데도, 순수 Newton보다 시간이 40% 적게 들었습니다. 반복 횟수는 비용이 아닙니다.
실패는 전역 반복이 아니라 요소 8 안에 있습니다. 위의 모든 알고리즘은 같은 전역 방정식을 풉니다. 다른 것은 각자가 요소에 건네는 시행 변위의 순서뿐입니다. ModifiedNewton이 접선을 얼려둔 덕에 요소 8이 풀 수 있는 시행 상태가 만들어진 것입니다. 이건 전략보다 운에 가깝고, 그래서 정직한 처방은 "맞는 것을 고른다"가 아니라 "여러 개를 싸게 시도한다"입니다.
5. 스텝을 반으로 자르는 것이 가장 확실한 한 수다
스텝증분순수 Newton
dt5.000 ms실패
dt / 22.500 ms회복
dt / 41.250 ms회복
dt / 100.500 ms회복
dt / 200.250 ms회복
바뀐 것은 요소가 한 번에 감당해야 하는 증분의 크기뿐입니다. 쓸 수 있는 수단 중 가장 믿을 만하고, 그런데도 캐스케이드의 마지막에 놓입니다. 덜 통해서가 아니라, 하위 스텝마다 완전한 분해 비용이 들고 앞의 항목들은 거의 공짜이기 때문입니다. 싼 것부터 시도하고, 확실한 것은 뒤에 남겨둡니다.
4부에서 온 주의 하나. 스텝 세분은 장부 정리가 아니라 적분 자체를 바꿉니다. dt/20이 100스텝 연속으로 필요했던 해석은 그 구간을 사실상 다른 시간 간격으로 적분한 것이고, 그 구간에 최대 응답이 들어 있다면 전체 해석을 잘게 다시 돌려 확인할 값어치가 있습니다.
6. 수렴 판정은 비용을 결정하고, 답은 거의 결정하지 않는다
1부에서 NormDispIncr는 차원을 가지므로 같은 허용오차가 미터 모델과 밀리미터 모델에서 다른 뜻이 된다고 경고했습니다. 그 경고는 유효합니다. 하지만 그것이 "허용오차를 조이면 더 좋은 답을 얻는다"를 뜻하지는 않습니다. 같은 골조를 Tabas × 1.0으로 20초 동안, 판정과 허용오차 조합 일곱 가지로 돌린 결과입니다.
판정허용오차최대 층간변위비반복차이
NormDispIncr1e-60.0280689 552기준
NormDispIncr1e-40.0280687 5090.0009%
NormDispIncr1e-20.0280504 0000.065%
NormUnbalance1e-60.02806811 8930.00002%
NormUnbalance1e-20.0280687 8530.0004%
EnergyIncr1e-80.0280687 5560.0009%
RelativeNormDispIncr1e-60.02806812 4950.00002%
미터 기반 모델에 변위 허용오차 10 mm를 준 경우까지 포함해 모든 조합이 최대 층간변위비를 0.065% 이내로 같게 냈습니다. 비용은 3.1배 범위로 벌어졌습니다.
이 결과는 "느슨하게 푼 허용오차를 그대로 두지 말라"는 7부의 경고를 뒤집는 것이 아니라 더 정확하게 만듭니다. 얌전한 해석에서 느슨한 허용오차의 위험은 틀린 답이 아닙니다. 느슨한 허용오차는 실패를 조용한 근사로 바꿔버린다는 것이 위험입니다. 멈춰 서서 뭔가를 알려줬을 해석이 이제 그냥 끝나버리고 아무 말도 하지 않습니다. 결과가 진짜인지 알고 싶으면 허용오차를 조이십시오. 푸는 것은 회복 시도 안에서만 하고, 끝나면 되돌려 놓으십시오.
실무적인 메모 둘. 여기서 RelativeNormDispIncr와 NormUnbalance가 가장 비쌌고 정확도를 사주지도 않았습니다. 다만 NormUnbalance는 아주 강한 요소와 아주 무른 요소가 섞인 모델에서 꺼내 쓸 판정입니다. 변위가 더 이상 변하지 않는데도 힘은 여전히 불균형일 수 있기 때문입니다. EnergyIncr는 둘을 결합하며 비용은 중간이었습니다. 허용오차의 단위가 일(work)이므로, NormDispIncr와 똑같이 모델 단위에 맞춰 다시 잡아야 합니다.
7. 비용 순으로 정렬한 캐스케이드
def recover(dt, base_algo='Newton', base_tol=1e-6, base_iter=30): """성공하면 0을 반환. 어느 경로로 나가든 원래 설정을 복원한다.""" def restore(): ops.algorithm(base_algo) ops.test('NormDispIncr', base_tol, base_iter) # 1 같은 알고리즘에 시간을 더 준다 ops.test('NormDispIncr', base_tol, 10 * base_iter) if ops.analyze(1, dt) == 0: restore(); return 0 # 2-4 다른 알고리즘들, 싼 것부터, 허용오차는 그대로 for name, args in [('ModifiedNewton', ()), ('NewtonLineSearch', (0.8,)), ('KrylovNewton', ()), ('Broyden', (8,)), ('BFGS', ())]: ops.algorithm(name, *args) ops.test('NormDispIncr', base_tol, 10 * base_iter) if ops.analyze(1, dt) == 0: restore(); return 0 # 5 스텝을 나눈다 for div in (2, 4, 10, 20): ops.algorithm('ModifiedNewton') ops.test('NormDispIncr', base_tol, 10 * base_iter) if all(ops.analyze(1, dt / div) == 0 for _ in range(div)): restore(); return 0 # 6 진짜 실패 restore(); return -1
위의 측정이 정당화하는 규칙 셋입니다.
허용오차는 절대 바꾸지 않습니다. 바꾸는 것은 반복 허용 횟수와 알고리즘뿐입니다. test()를 슬쩍 푸는 캐스케이드는 모든 것에 "성공"합니다. 멈췄어야 할 해석까지 포함해서요.
모든 종료 경로에서 복원합니다. restore()가 없으면 30초짜리 해석의 나머지 전부가, 11초 지점에서 우연히 통했던 알고리즘으로 굴러갑니다.
개입 횟수를 세어 결과와 함께 보고합니다. 7부에서 한 강도에 회복 2회, 다른 강도에 1회를 측정했고, 그 두 숫자가 붕괴능력 2.83 g와 3.54 g의 차이였습니다. 세 번 개입한 해석과 삼백 번 개입한 해석은 둘 다 끝났더라도 같은 결과가 아닙니다.
8. 하지 말아야 할 것
손이 가는 처방왜 틀렸는가
수렴할 때까지 test()를 영구히 푼다진단을 조용한 근사로 바꾼다. 1e-2면 거의 무엇이든 "수렴"한다
system('FullGeneral')로 바꾼다5부에서 같은 답에 145배 느린 것으로 측정됐다. 솔버 선택은 수렴 처방이 아니다
반복 한도를 10 000으로 올린다첫 번째 실패 유형에만 쓸모 있다. 요소 실패에서는 어차피 5회에서 멈춘다
수렴시키려고 algorithm('Linear')를 쓴다반복을 안 하니 항상 "수렴"한다. 비선형 모델에서 그 답은 의미가 없다
실패한 요소를 지운다그 요소는 도달할 수 없는 실제 상태를 보고하는 중이다. 지우면 원인이 아니라 증거가 사라진다
한계점을 지나 수렴한 하중제어 결과를 받아들인다2절 참조. 수렴했고, 경로 0.173 m를 건너뛰었다
9. 같은 논리를 다섯 단계 깊이로
입문자는 모든 analyze() 호출의 반환값을 확인합니다. 주니어는 메시지를 읽고 "singular matrix"와 "test failed"가 다른 문제임을 압니다. 실무 해석자는 허용오차를 건드리지 않고, 알고리즘을 복원하고, 개입 횟수를 세는 캐스케이드를 돌립니다. 시니어는 수치적 실패와 물리적 실패를 구분하고, 구조물에 한계점이 있으면 알고리즘이 아니라 적분기를 고릅니다. 전문가는 testNorm()과 메시지 속 요소 번호를 읽어 어느 객체가 실패했는지 짚고 그 객체를 고칩니다. 재료에 경화를 주거나, 그 요소에 더 작은 스텝을 주거나, 메커니즘에 구속을 주거나.
10. 수렴하지 않을 때의 최소 점검표
모든 루프의 모든 스텝에서 analyze() 반환값을 확인했는가?
메시지가 실제로 무엇을 말하는가 — 판정인가, 요소인가, 분해인가, 아무 말도 없는가?
testIter()와 testNorm()이 보여주는 것은 정체된 잔차인가, 중단인가?
구조물에 한계점이 있는가, 그리고 적분기가 거기에 맞는가?
다른 것을 바꾸기 전에 그 스텝에 반복 횟수부터 더 줘봤는가?
평판으로 하나를 고르는 대신 여러 알고리즘을 싸게 시도했는가?
캐스케이드가 허용오차를 손대지 않는가?
회복 후 원래 알고리즘과 허용오차가 복원되는가?
개입 횟수를 세어 결과 옆에 적었는가?
아무것도 통하지 않았다면, 그 실패를 튜닝으로 지우지 않고 결과로 기록했는가?
다음 글은 파이썬에서 옆으로 한 발 비켜섭니다. 같은 모델을 Tcl로 다시 씁니다. 20년치 공개 OpenSees 스크립트와 PEER 예제, 학위논문 부록이 낯선 언어가 아니라 읽을 수 있는 코드가 되도록.
전체 스크립트
얕은 트러스를 두 번 추적합니다. 한 번은 변위제어로 닫힌 형태 평형경로와 대조하고, 한 번은 하중제어로 한계점을 건너뛰는 것을 봅니다.
# Part 9 - a limit point traced exactly # units: kN, m import math import openseespy.opensees as ops A_SPAN, H_RISE, EA = 1.0, 0.10, 2.0e5 L0 = math.hypot(A_SPAN, H_RISE) E_MAT = 2.0e8 A_BAR = EA / E_MAT def exact_P(v): """Closed-form equilibrium load for an apex settlement v.""" L = math.hypot(A_SPAN, H_RISE - v) N = EA * (L - L0) / L0 return -2.0 * N * (H_RISE - v) / L def build(): ops.wipe(); ops.model('basic', '-ndm', 2, '-ndf', 2) ops.node(1, -A_SPAN, 0.0); ops.node(2, A_SPAN, 0.0); ops.node(3, 0.0, H_RISE) ops.fix(1, 1, 1); ops.fix(2, 1, 1); ops.fix(3, 1, 0) ops.uniaxialMaterial('Elastic', 1, E_MAT) ops.element('corotTruss', 1, 1, 3, A_BAR, 1) ops.element('corotTruss', 2, 2, 3, A_BAR, 1) ops.timeSeries('Linear', 1); ops.pattern('Plain', 1, 1) ops.load(3, 0.0, -1.0) ops.constraints('Plain'); ops.numberer('Plain'); ops.system('BandGeneral') ops.test('NormDispIncr', 1e-8, 100); ops.algorithm('Newton') ops.analysis('Static') # the exact limit point, by scanning the closed form vs = [i * 0.10 / 400000 for i in range(1, 400001)] v_top = max(vs, key=exact_P); P_top = exact_P(v_top) build(); ops.integrator('DisplacementControl', 3, 2, -0.0005) worst = 0.0; steps = 0 for _ in range(500): if ops.analyze(1) != 0: break v = -ops.nodeDisp(3, 2); P = ops.getLoadFactor(1) steps += 1 worst = max(worst, abs(P - exact_P(v)) / P_top) build(); ops.integrator('LoadControl', P_top / 200) jump = 0.0; prev = 0.0; fails = 0 for _ in range(600): if ops.analyze(1) != 0: fails += 1; break v = -ops.nodeDisp(3, 2) jump = max(jump, v - prev); prev = v print(f'exact limit load {P_top:12.4f} kN at v = {v_top:.6f} m') print(f'displacement control {steps} steps traced, max path error {worst:.3e}') print(f'load control largest single-step jump {jump:.6f} m, failures {fails}') assert worst < 1e-9, 'displacement control did not trace the exact path' assert fails == 0 and jump > 0.1, 'load control was expected to skip the limit point silently' print('OK displacement control traces it; load control skips it without complaint')
실행하면 이렇게 나옵니다.
exact limit load 76.2174 kN at v = 0.042361 m displacement control 500 steps traced, max path error 1.700e-13 load control largest single-step jump 0.173270 m, failures 0 OK displacement control traces it; load control skips it without complaint
OpenSees 시리즈 전체
각 편은 닫힌해·독립적인 풀이·OpenSees 소스 중 하나와 대조해 검증했다. 순서대로 읽도록 썼지만 각 편이 자기 전제를 스스로 밝힌다.
1부 — 도메인 모델과 해석 조립
2부 — 단면·기하변환·분포하중
3부 — 파이버 단면·모멘트–곡률·푸시오버
4부 — 질량·감쇠·시간적분
5부 — 라이브러리 읽기와 재현 가능한 워크플로
6부 — 철근콘크리트 파이버 단면과 구속
7부 — 실제 지진기록·층간변위·증분동적해석
8부 — 3차원: 강막·비틀림·leaning column
9부 — 수렴하지 않을 때: 실패·알고리즘·한계점 — 지금 보는 글
10부 — Tcl 읽기와 공개 스크립트 이식
11부 — 쉘·솔리드·지반: 락킹·메시·부지응답
12부 — 후처리: recorder·질의, 그리고 그림이 증명하는 것
13부 — 좌굴과 한계점: 분기점·스냅백·호장법
14부 — Windows: 설치·파일 형식·조용한 실패
15부 — 검증된 정답이 딸린 연습문제와 한영 용어집
16부 — 어떻게 동작하는가: 시행상태·확정상태·해석 루프
17부 — 커뮤니티가 말하는 것, 측정해보면
18부 — 절점 변위에서 파이버 응력까지
19부 — 면진과 감쇠: 속도 의존 요소와 베어링
참고 자료
OpenSeesPy, algorithm 및 개별 항목: Newton, ModifiedNewton, NewtonLineSearch, KrylovNewton, Broyden, BFGS, SecantNewton.
OpenSeesPy, test, NormDispIncr, NormUnbalance, EnergyIncr.
OpenSeesPy, DisplacementControl, ArcLength — 한계점을 통과하는 적분기.
OpenSeesPy, testIter, testNorm — 1절의 두 진단 명령.
OpenSeesPy, corotTruss — 스냅스루 벤치마크에 쓴 요소.
Crisfield, M. A., Non-linear Finite Element Analysis of Solids and Structures — 호장법과 한계점 거동.
Scott, M. H., OpenSees Digital — 수렴, 알고리즘, 요소 상태 결정에 관해 꾸준히 쓰는 OpenSees 개발자의 블로그.
Pacific Earthquake Engineering Research Center, OpenSees Wiki.
출처 확인 2026-08-31. 모든 결과는 OpenSeesPy 3.8.0에서 산출했으며 본문의 스크립트로 재현할 수 있습니다. 알고리즘 순위는 두 개의 특정 모델에 대한 측정치이며 일반화되지 않습니다. 옮겨 쓸 수 있는 결론은 "어느 알고리즘이 통할지는 예측할 수 없다"이고, 그래서 캐스케이드를 씁니다. 결과는 프레임워크의 거동을 보여주는 것으로, 프로젝트별 해석이나 설계기준 검토를 대체하지 않습니다.