OpenSees는 실제로 어떻게 동작하는가: 시행상태·확정상태, 그리고 analyze 가 숫자를 돌려주는 이유

모든 재료·단면·요소가 두 상태를 동시에 들고 있고, 해석이란 시행상태를 제안해 평형을 만족하는 것만 확정하는 루프다. Steel01 을 setStrain 으로 손으로 몰면 재하·제하·반전 10점 경로에서 완전한 해석과 최대 차이가 정확히 0 으로 일치한다. 제하점 2.0000 도, 이동경화 반전점 -3

OpenSees는 실제로 어떻게 동작하는가: 시행상태·확정상태, 그리고 analyze 가 숫자를 돌려주는 이유

OpenSees 모델의 모든 재료, 모든 단면, 모든 요소는 두 상태를 동시에 들고 있습니다. 지금 시험 중인 상태와, 이미 받아들여진 상태. 이걸 보기 전에는 프레임워크의 거의 모든 것이 납득되지 않고, 보고 나면 거의 모든 것이 납득됩니다. `analyze()`가 왜 예외를 던지지 않고 숫자를 돌려주는지. 실패한 스텝이 왜 시간만 잃고 모델은 잃지 않는지. 솔버에 왜 응력을 변형률로 나눈 값이 아닌 강성을 주는지. 왜 들고 다닐 모델 객체가 없는지.

1~15부는 "무엇을 하면 틀리고 어떻게 잡아내는가"였습니다. 이 편은 "프레임워크가 왜 그렇게 동작하는가"입니다. 아래는 전부 OpenSeesPy 3.8.0에서 측정했고, 중심 실증은 구성식을 손으로 몰아서 완전한 해석의 결과를 마지막 자리까지 재현하는 것입니다.

1. 프레임워크 전체를 한 문장으로

해석이란 시행상태를 제안하고 평형을 만족하는 것만 확정하는 루프입니다.

1부에서 해석 객체 여섯 개를 "써야 하는 것들의 목록"으로 소개했습니다. 사실 그것은 이 루프의 네 단계입니다.

객체호출루프에서의 역할

integratornewStep()스텝의 첫 시행상태를 제안한다

algorithmsolveCurrentStep()판정을 통과할 때까지 더 나은 시행상태를 제안한다

system, numberer, constraints—각 시행을 연립방정식으로 바꿔 푼다

testtest()그 시행상태가 평형인지 판정한다

integratorcommit()받아들인 시행상태를 확정상태로 만든다

모든 요소와 재료가 그 아래에서 같은 네 메서드 계약을 구현합니다. setTrialStrain, getStress/getTangent, commitState, revertToLastCommit. 새 재료 작성법을 다루는 OpenSees 위키가 규칙을 직접 말합니다. "revertToLastCommit()이 호출되면 그 해로 되돌아갈 수 있어야 하는 것은 재료의 책임이다. 알고리즘이 해 경로 위의 해를 찾지 못하면 그런 일이 일어난다."

2. 상태기계를 손으로 몰기

그 계약을 파이썬에서 직접 조작할 수 있습니다. testUniaxialMaterial이 재료를 1재료 시험대에 붙이고, setStrain이 그것을 몰고, getStress와 getTangent가 읽습니다.

ops.uniaxialMaterial('Steel01', 1, 400.0, 200000.0, 0.01) ops.testUniaxialMaterial(1) for eps in [0.0005, 0.001, 0.002, 0.0025, 0.003, 0.0025, 0.002, 0.0015, 0.001, -0.001]: ops.setStrain(eps) print(eps, ops.getStress(), ops.getTangent())

이제 같은 재료로 단위면적·단위길이 트러스 요소 하나를 만들고, DisplacementControl로 같은 열 개의 변형률을 지나가게 하며 축력을 기록합니다. 두 벌의 수치는 같은 수치입니다.

변형률손으로 몬 응력analyze()의 축력접선

0.0020400.0000400.0000200,000

0.0030402.0000402.00002,000

0.0025 (제하)302.0000302.0000200,000

0.00102.00002.0000200,000

−0.0010 (반전)−398.0000−398.0000200,000

최대 차이 0.000e+00. "작다"가 아니라 0입니다. 해석은 여러분이 직접 할 수 없는 무언가를 재료에 하고 있는 게 아닙니다. 어떤 변형률을 물어볼지 고르고 있을 뿐입니다.

이 표의 두 항목은 따로 한 문장을 받을 값어치가 있습니다. 변형률 0.001에서의 2.0000은 탄성 거동이 아닙니다. 탄성 재료라면 200이 나옵니다. 이것은 402에서 200,000 × 0.002를 뺀 값, 즉 항복한 상태에서 내려오는 제하선이고, 재료가 0.003까지의 이력을 확정했기 때문에 거기 있습니다. 그리고 반전 시의 −398.0000은 3부에서 측정한 이동경화입니다. 항복면이 평행이동했으므로 −402가 아니라 −398에서 다시 항복합니다.

이것이 이 프레임워크에서 가장 쓸모 있는 디버깅 기법 하나입니다. 파이버 단면이 이상하게 굴면, 그 재료를 꺼내서 여러분이 생각하는 변형률 이력대로 손으로 몰아보고 눈으로 확인하십시오. 해석도, 수렴도, 모호함도 없습니다.

3. 솔버에 답이 아닌 수를 주는 이유

변형률 0.003에서 위 재료는 응력 402.0과 접선 2,000을 보고합니다. 응력을 변형률로 나누면 134,000입니다. 둘은 67배 차이입니다.

두 수 모두 쓰이는데, 하는 일이 다릅니다. 접선은 강성행렬로 들어갑니다. Newton법이 근이 어디 있을지 짐작하려면 국부 기울기가 필요하기 때문입니다. 응력은 저항력 벡터로 들어갑니다. 평형은 기울기가 아니라 힘에 관한 진술이기 때문입니다. 그 분리가 다음을 설명합니다.

algorithm과 test가 별개 객체인 이유. 알고리즘은 접선으로 다음 시행을 고르고, 판정은 응력으로 만들어진 잔차로 결과를 심판합니다.

ModifiedNewton과 KrylovNewton이 존재하는 이유. 접선을 재사용하거나 근사하면서도 참 응력은 그대로 계산하므로, 다른 경로로 같은 답에 수렴합니다. 9부의 캐스케이드가 결과를 바꾸지 않고 알고리즘을 갈아끼울 수 있는 이유가 이것입니다.

수렴한 해는 접선이 아무리 나빴어도 정확합니다. 나쁜 접선의 대가는 반복 횟수이지 정확도가 아닙니다. 나쁜 응력의 대가가 정확도입니다.

4. 반복 횟수가 곧 비선형 탐지기다

3부의 철근콘크리트 기둥을 같은 크기의 변위 12스텝으로 밀면서, 확정상태 하나마다 시행상태가 몇 개 필요했는지 기록합니다.

스텝u (m)V (kN)시행잔차 노름

1–100.004 … 0.04039.9 … 399.011e-20 … 7e-18

110.0440437.91321.6e-05, 1.7e-18

120.0480474.26022.5e-05, 2.8e-18

기둥이 탄성인 동안에는 시행상태 하나가 정확합니다. Newton이 선형 문제를 한 번에 풀고 잔차는 반올림 수준입니다. 파이버 하나가 항복하는 순간 첫 시행이 1.6 × 10⁻⁵만큼 틀리고 두 번째가 필요해집니다. 응력을 하나도 기록하지 않고도 반복 횟수에서 비선형의 시작을 볼 수 있습니다.

ok = ops.analyze(1) print(ops.testIter(), '개 시행상태,', [f'{v:.1e}' for v in ops.testNorm() if v > 0])

여기서 공짜 진단 두 개가 떨어집니다. 반복 횟수가 처음부터 끝까지 1인 해석은, 여러분의 의도가 무엇이었든, 여전히 탄성인 해석입니다. 그리고 횟수가 갑자기 세 배가 되는 해석은 무언가를 찾은 것입니다. 항복이든 균열이든 간격이 닫힌 것이든, 여러분이 지목할 수 있는 스텝에서.

5. 실패한 스텝이 잃는 것은 시간이지 상태가 아니다

한 스텝에 불가능한 허용오차와 반복 1회를 줘서 수렴할 수 없게 만듭니다.

실패한 스텝 직전analyze()가 −3 반환 후

u0.0600000000 m0.0600000000 m

V551.8819 kN551.8819 kN

이것이 revertToLastCommit()이 Domain을 타고 모든 요소, 모든 단면, 모든 파이버까지 전파된 결과입니다. 9부의 회복 캐스케이드가 안전한 이유가 이것입니다. 스텝이 실패해서 다른 알고리즘으로 재시도할 때, 여러분은 실패한 시도가 남긴 어딘가가 아니라 정확히 같은 출발점에서 다시 하는 것입니다.

analyze()가 예외 대신 상태 코드를 돌려주는 이유이기도 합니다. 0이 아닌 반환값은 프로그래밍적 의미의 오류가 아닙니다. 스텝이 성공하지 못했고 모델은 멀쩡하다는 보고입니다. 재시도 루프에 필요한 계약이 정확히 그것입니다.

같은 축 위에 명령 둘이 더 있습니다.

ops.reset() # 초기 상태로 u 0.048000 -> 0.000000 time 474.2598 -> 0.0000 ops.loadConst('-time', 0.0) # 확정된 하중은 유지하고 시계만 되돌린다 time 1.000 -> 0.000 하중계수 1.000 -> 1.000 u 변화 없음

reset()은 Domain 전체에 대한 revertToStart()입니다. 여러분이 다시 만들지 않아도 모델이 만들어진 그대로의 상태로 돌아갑니다. loadConst는 중력 후 푸시오버를 하는 모든 스크립트가 쓰는 그것입니다. 확정된 하중을 얼려 계속 작용하게 두고, 다음 패턴이 깨끗한 시계에서 시작하도록 의사시간을 0으로 되돌립니다.

6. 정체성은 태그이고, Domain이 그것을 강제한다

Domain 안의 모든 것은 맵에 저장된 TaggedObject입니다. 태그가 객체가 가진 유일한 정체성이고, 이름도 참조도 없습니다. 가정하지 말고 측정할 값어치가 있는 결과가 둘입니다.

태그 재사용은 조용히 덮어쓰지 않습니다. 사람들이 번호를 조심스럽게 매기게 만드는 바로 그 공포입니다. 재료 7을 E = 1000으로 정의한 뒤 다시 7을 E = 9999로 정의하면:

MapOfTaggedObjects::addComponent - not adding as one with similar tag exists, tag: 7 ERROR could not add uniaxialMaterial.

두 번째 정의가 거부되고 첫 번째가 살아남습니다. 그 뒤에 재료를 몰아보면 변형률 0.001에서 응력 1.0, 즉 E = 1000입니다. 태그 재사용의 위험은 흔히 두려워하는 것의 반대입니다. 조용히 존재하지 않게 되는 쪽은 여러분의 새 정의입니다.

앞선 참조도 거부됩니다. 아직 정의되지 않은 재료를 참조하면 나중에 터지는 게 아니라 요소의 사용법 줄이 나옵니다.

WARNING: Invalid material not found element Truss 1 $iNode $jNode $A 99

OpenSees 문서가 초보자에게 주는 조언 뒤의 메커니즘이 이것입니다. "OpenSees 인터프리터는 사용자가 지정한 순서대로 객체를 만들며, 객체 파라미터로 지정될 때 앞서 정의된 객체는 이미 Domain에 추가되어 있어야 한다." 스타일 규칙이 아닙니다. 맵 조회가 실패하는 것입니다.

모델 객체는 없습니다. 인터프리터는 Domain 하나를 들고 있고 wipe()가 그것을 파괴합니다. 취향이 아니라 실제 제약입니다. 한 프로세스에 모델 둘을 들 수 없고, 한 인터프리터에서 해석 둘을 동시에 돌릴 수 없습니다. OpenSeesPy를 두고 가장 자주 제기되는 설계 결정이고, 실험적 재작성인 STAIRLab의 opensees 패키지가 "모든 프로그램 상태를 사용자가 인스턴스화한 클래스에 캡슐화"한 바인딩을 내세우는 것이 바로 이것을 없애기 위해서입니다. 오늘 모델 둘이 동시에 필요하다면 답은 프로세스 둘입니다.

7. 이것이 설명하는 것들

임의로 보이는 거동실제로는

analyze()가 예외 대신 숫자를 돌려준다스텝은 거절될 수 있는 제안이다

실패한 스텝 뒤에도 모델을 쓸 수 있다revertToLastCommit()이 돌았다

알고리즘을 바꿔도 답이 바뀌지 않는다알고리즘은 시행상태만 고르고, 판정은 응력이 한다

느슨한 허용오차는 위험한데 나쁜 접선은 아니다판정은 잔차를 보고, 접선은 길만 안내한다

recorder 출력이 반복이 아니라 스텝당 한 줄이다recorder는 확정될 때 작동한다

wipe()가 새 모델을 시작하는 방법이다Domain은 하나이고 전역이다

태그는 유일해야 하고 쓰기 전에 정의돼야 한다Domain은 태그를 키로 하는 맵이다

마지막에서 두 번째 줄은 recorder에 관해 한마디 받을 값어치가 있습니다. 반복 횟수를 지켜본 사람이 놀라는 지점이기 때문입니다. recorder는 Domain이 확정할 때 씁니다. 그래서 파일에는 받아들여진 경로만 담기고, 도중에 제안됐다 버려진 시행상태는 하나도 없습니다. 위의 12스텝에서 시행상태 두 개는 어떤 출력에도 나타나지 않는데, 평형이 아니었으므로 그것이 옳습니다.

8. 상태기계에서 따라 나오는 점검표

모든 analyze()의 반환값을 확인하는가? 그것은 오류가 아니라 보고다.

스텝이 실패했을 때 재시도가 같은 확정상태에서 출발하는가? 그렇다 — 그것이 캐스케이드를 유효하게 만든다.

반복 횟수가 1보다 커진 적이 있는가? 없다면 그 모델은 탄성이다.

반복 횟수가 뛰는 지점을 설명할 수 있는가?

재료가 이상하게 굴 때, 여러분이 생각하는 변형률 이력대로 손으로 몰아봤는가?

여러분의 추론 어딘가에서 접선을 할선과 비교하고 있지는 않은가?

모든 태그가 유일하고, 모든 객체가 참조되기 전에 정의되는가?

워크플로 어딘가가 모델 둘이 동시에 존재할 수 있다고 가정하지는 않는가?

9. 같은 프레임워크를 다섯 단계 깊이로

입문자는 튜토리얼이 그러라고 해서 해석 객체 여섯 개를 씁니다. 주니어는 실패했을 때 그 여섯 중 무엇을 바꿔야 하는지 압니다. 실무 해석자는 알고리즘이 제안하고 판정이 심판한다는 것을, 따라서 알고리즘을 바꿔도 수렴한 답은 바뀔 수 없다는 것을 이해합니다. 시니어는 반복 횟수를 솔버에 관한 정보가 아니라 구조물에 관한 데이터로 읽습니다. 전문가는 재료를 모델에서 꺼내 손으로 몰아보고 대조합니다. 해석이 몰든 for 루프가 몰든 상태기계는 같기 때문입니다.

다음 글은 커뮤니티가 하는 말로 갑니다. OpenSees 포럼과 블로그에서 가장 자주 반복되는 조언들을, 주장 하나씩 측정해서.

전체 스크립트

상태기계를 처음부터 끝까지. 같은 열 개의 변형률을 손으로 몰고 해석으로도 지나가며, 접선을 할선 옆에 놓습니다.

# Part 16 - the state machine: drive a material by hand, then through an analysis # units: kN, m import openseespy.opensees as ops FY, ES, BH = 400.0, 200000.0, 0.01 PATH = [0.0005, 0.001, 0.002, 0.0025, 0.003, 0.0025, 0.002, 0.0015, 0.001, -0.001] def by_hand(path): ops.wipe(); ops.model('basic', '-ndm', 1, '-ndf', 1) ops.uniaxialMaterial('Steel01', 1, FY, ES, BH) ops.testUniaxialMaterial(1) # a one-material test bench out = [] for e in path: ops.setStrain(e) out.append((e, ops.getStress(), ops.getTangent())) return out def by_analysis(path): ops.wipe(); ops.model('basic', '-ndm', 1, '-ndf', 1) ops.node(1, 0.0); ops.node(2, 1.0); ops.fix(1, 1) ops.uniaxialMaterial('Steel01', 1, FY, ES, BH) ops.element('Truss', 1, 1, 2, 1.0, 1) # unit area, unit length ops.timeSeries('Linear', 1); ops.pattern('Plain', 1, 1); ops.load(2, 1.0) ops.constraints('Plain'); ops.numberer('Plain'); ops.system('BandGeneral') ops.test('NormDispIncr', 1e-12, 50); ops.algorithm('Newton') ops.analysis('Static') out = []; cur = 0.0 for e in path: ops.integrator('DisplacementControl', 2, 1, e - cur) ops.analyze(1); cur = e out.append((ops.nodeDisp(2, 1), ops.eleResponse(1, 'axialForce')[0])) return out h, a = by_hand(PATH), by_analysis(PATH) print(f"{'strain':>10s}{'hand stress':>14s}{'analysis N':>14s}{'tangent':>12s}") worst = 0.0 for (e, s, t), (_, n) in zip(h, a): worst = max(worst, abs(s - n)) print(f'{e:10.4f}{s:14.4f}{n:14.4f}{t:12.1f}') print(f'\nmaximum difference {worst:.3e}') assert worst == 0.0, 'the hand-driven material and the analysis must agree exactly' # the tangent is not the stress divided by the strain e, s, t = h[PATH.index(0.003)] print(f'at strain {e}: stress {s:.1f}, tangent {t:.1f}, secant {s/e:.1f}' f' -> a factor of {s/e/t:.0f}') assert abs(s/e/t - 67.0) < 1.0 print('\nOK the analysis chooses which strains to ask for; the material does the rest')

실행하면 이렇게 나옵니다.

strain hand stress analysis N tangent 0.0005 100.0000 100.0000 200000.0 0.0010 200.0000 200.0000 200000.0 0.0020 400.0000 400.0000 200000.0 0.0025 401.0000 401.0000 2000.0 0.0030 402.0000 402.0000 2000.0 0.0025 302.0000 302.0000 200000.0 0.0020 202.0000 202.0000 200000.0 0.0015 102.0000 102.0000 200000.0 0.0010 2.0000 2.0000 200000.0 -0.0010 -398.0000 -398.0000 200000.0 maximum difference 0.000e+00 at strain 0.003: stress 402.0, tangent 2000.0, secant 134000.0 -> a factor of 67 OK the analysis chooses which strains to ask for; the material does the rest

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부 — 면진과 감쇠: 속도 의존 요소와 베어링

참고 자료

OpenSees Wiki, Add a New UniaxialMaterial C++ — 프레임워크 자신의 말로 쓴 시행/확정 계약.

OpenSees Wiki, Add a New Element C++ — 요소 수준의 같은 계약.

OpenSeesPy, testUniaxialMaterial, setStrain, getStress — 2절의 손 시험대.

OpenSeesPy, reset, loadConst.

OpenSees Documentation, 사용자 매뉴얼 — 구축 순서와 ModelBuilder.

PyPI의 opensees (STAIRLab) — 전역 상태를 없앤 실험적 바인딩.

Scott, M. H., OpenSees Digital (구 Portwood Digital) — 개발자 블로그. 프레임워크가 왜 이렇게 동작하는지에 관한 뒷이야기의 최고 출처.

출처 확인 2026-08-31. 모든 결과는 OpenSeesPy 3.8.0에서 산출했으며 본문의 스크립트로 재현할 수 있습니다. 손으로 몬 재료 대조, 반복 횟수, 롤백 상태, 태그 거동은 이 버전에 대한 측정치이고, 시행/확정 계약 자체는 OpenSees 개발자 문서에서 인용했습니다. 결과는 프레임워크의 거동을 보여주는 것으로, 프로젝트별 해석이나 설계기준 검토를 대체하지 않습니다.