OpenSees는 왜 프로그램이 아니라 프레임워크인가: 도메인 모델·해석 조립·검증된 첫 모델

OpenSees를 프레임워크로 이해하는 입문. Domain이 보관하는 것, ndm과 ndf가 확정하는 것, 하중 패턴과 시간이력의 차이, 해석을 이루는 교체 가능한 여섯 객체, analyze() 내부에서 벌어지는 일, Recorder, 실제 오류 메시지, 그리고 세 가지 독립 방법으로 검증한 첫 트러스

OpenSees는 왜 프로그램이 아니라 프레임워크인가: 도메인 모델·해석 조립·검증된 첫 모델

OpenSees에는 실행 버튼도, 모델 트리도, 기본 단위도 없다. 빠뜨린 것이 아니라 그렇게 설계된 것이다. OpenSees는 프로그램이 아니라 유한요소 프레임워크다. C++ 클래스들을 스크립트로 조립해 모델과 해석을 직접 만든다. 어떤 객체가 무엇을 들고 있는지 한 번 보이고 나면, 낯설던 명령어 목록이 규칙이 일정한 작은 기계로 보이기 시작한다.

이 글은 설계기준·하중조합·안전율·허용치를 제시하지 않는다. 그보다 먼저 필요한 객체 모델을 다룬다. Domain이 무엇을 보관하는지, ndm과 ndf가 무엇을 확정하는지, 하중과 하중 패턴이 어떻게 다른지, 해석이 어떤 여섯 객체로 조립되는지, analyze() 한 번에 실제로 무슨 일이 일어나는지를 순서대로 세운다. 아래의 모든 수치와 오류 메시지는 OpenSeesPy 3.8.0에서 직접 실행해 얻은 것이며, 6절의 완성된 모델은 7절에서 서로 독립적인 세 가지 방법으로 검증한다.

1. OpenSees는 무엇에 쓰는 도구인가

OpenSees는 구조물이나 지반–구조물 시스템이 어떻게 변형하고, 균열이 생기고, 항복하고, 끝내 붕괴하는지를 예측하는 데 쓴다. 특히 지진하중을 받고, 응답이 비선형이며 이력에 의존할 때 쓴다. 상용 패키지가 "이 보의 설계 부재력은 얼마인가"에 답한다면, OpenSees는 "내가 고른 재료·요소 모델로 이 시스템을 탄성한계 너머까지 밀었을 때 단계별로 무엇이 일어나는가"에 답한다.

이 차이가 인터페이스를 설명한다. 해석 종류가 고정된 프로그램은 메뉴를 고정할 수 있다. 그러나 재료 법칙, 요소 정식화, 해 알고리즘, 시간적분기를 사용자가 바꿔 끼우는 것이 존재 이유인 프레임워크는 그럴 수 없다. 그래서 OpenSees는 교체 지점을 명령어로 그대로 노출한다. 대가는 아무것도 대신 정해주지 않는다는 것이고, 이득은 아무것도 숨기지 않는다는 것이다.

이름은 Open System for Earthquake Engineering Simulation의 약자다. UC 버클리 PEER(Pacific Earthquake Engineering Research Center)에서 개발되었고, 주 개발자는 2011년 Computing in Science & Engineering 논문에서 이를 응용 프로그램이 아니라 프레임워크라고 명시한다.

2. 두 개의 앞단, 하나의 엔진

계산 코어는 C++다. 이를 Tcl(원래 인터프리터, 공개된 예제 스크립트 대부분이 아직 이것을 쓴다)이나 Python(OpenSeesPy)에서 구동한다. 명령어는 같고 호스트 언어의 문법만 다르다. 이 시리즈는 도표·시험·데이터 처리와 붙이기 쉬운 Python을 쓰지만, 여기 나오는 모든 모델은 Tcl로 한 줄씩 그대로 옮길 수 있다.

# 설치 (Python 3.8 이상) pip install openseespy # 엔진이 실제로 올라왔는지 확인 python -c "import openseespy.opensees as ops; print(ops.version())" # -> 3.8.0

첫 줄부터 중요한 규약이 둘 있다. 첫째, import openseespy.opensees as ops는 공개 코드에서 사실상 표준 별칭이므로 그대로 쓴다. 둘째, 인터프리터는 한 번에 하나의 전역 모델만 들고 있다. 넘겨 다닐 모델 객체 같은 것은 없다. 그래서 현재 모델을 완전히 파기하는 ops.wipe()는 정리용 편의 기능이 아니라 새 모델을 시작하는 방법이다. 모델을 두 개 이상 만드는 스크립트는 반드시 그 사이에 호출해야 한다.

3. 상태는 오직 Domain 안에만 있다

가장 쓸모 있는 심상은 이것이다. OpenSees는 Domain이라는 그릇 하나를 두고, 명령으로 만든 모든 것을 정수 태그를 달아 그 안에 넣는다. Domain이 소유하는 객체는 네 종류다.

객체생성 명령무엇을 아는가

Node 절점node()좌표, 자신의 자유도, 필요하면 질량

Element 요소element()어떤 절점을 잇는지, 강성행렬과 저항력을 어떻게 돌려주는지

Constraint 구속fix(), equalDOF(), rigidLink()어떤 자유도가 구속되거나 서로 묶이는지

Load pattern 하중 패턴pattern() 안의 load(), eleLoad(), sp()공간 하중 분포 × 시간이력 계수

여기서 곧바로 두 가지가 따라 나오고, 나중의 혼란을 크게 줄여준다.

태그가 유일한 신원이다. 절점 3이 절점 3인 이유는 node(3, ...)라고 썼기 때문이다. 절점·요소·재료·단면·패턴·시간이력은 각각 별도의 이름공간을 가지므로 재료 태그 1과 요소 태그 1은 충돌하지 않는다. 같은 이름공간에서 태그를 재사용하면 경우에 따라 조용히 덮어쓰거나 오류가 난다. 태그는 유일하게, 되도록 체계적으로 붙인다.

Domain은 질의할 수 있다. 스크립트의 기억을 믿을 필요 없이 모델에 직접 무엇이 들었는지 물어보면 된다. OpenSees를 빨리 디버깅하는 사람과 스크립트만 노려보는 사람을 가르는 습관이 이것 하나다.

import openseespy.opensees as ops ops.wipe() ops.model('basic', '-ndm', 2, '-ndf', 2) ops.node(1, 0.0, 0.0) ops.node(2, 4.0, 0.0) ops.node(3, 2.0, 1.5) ops.fix(1, 1, 1) ops.fix(2, 1, 1) ops.uniaxialMaterial('Elastic', 1, 200.0e6) ops.element('Truss', 1, 1, 3, 1.0e-3, 1) ops.element('Truss', 2, 2, 3, 1.0e-3, 1) print(ops.getNodeTags()) # -> [1, 2, 3] print(ops.getEleTags()) # -> [1, 2] print(ops.nodeCoord(3)) # -> [2.0, 1.5] print(ops.eleNodes(1)) # -> [1, 3]

4. ndm과 ndf: 첫 결정이자 첫 오류

model() 명령은 모델 전체에 대해 정수 두 개를 고정한다. ndm은 형상의 공간 차원 수, ndf는 각 절점이 기본으로 가지는 자유도 수다. 이후 자유도별 값을 나열하는 모든 명령 — fix(), load(), mass() — 은 정확히 ndf개의 값을 요구한다.

이것을 틀리는 것이 첫 한 시간에 가장 흔한 실수다. OpenSees는 알려준다 — 다만 Python 예외만 찾지 말고 stderr를 읽어야 한다. ndf 3으로 선언한 모델에서 fix(1, 1, 1)을 실행하면 이렇게 나온다.

WARNING: invalid # of constraint values opensees.OpenSeesError: See stderr output

기본 ndf는 절점별로 덮어쓸 수 있다. ops.node(7, x, y, '-ndf', 3) 형태이고, 트러스 구간과 라멘 구간이 섞인 모델은 이렇게 만든다. 그 필요가 생기기 전까지는 모델 전체에 ndf 하나만 쓴다. 자유도가 섞인 모델은 접합부 처리라는 별도의 문제를 불러오며, 이는 이 시리즈 뒤쪽 글의 주제다.

5. OpenSees에는 단위가 없다. 그 책임은 사용자에게 있다

OpenSees 안의 어떤 것도 미터가 무엇인지 모른다. 입력한 숫자는 사용자가 일관되게 쓰고 있는 체계로 해석되고, 결과도 같은 체계로 돌아온다. 단위 변환도, 단위 검사도 없다. 단위가 어긋나도 오류가 나지 않는다. 그럴듯해 보이는 틀린 답이 나오는데, 그쪽이 훨씬 나쁘다.

체계길이힘응력 / 탄성계수질량중력가속도

SI (m)mNPa = N/m²kg9.80665

SI (kN, m)mkNkPa = kN/m²ton (Mg)9.80665

SI (N, mm)mmNMPa = N/mm²ton (Mg)9806.65

미국 관용단위inkipksikip·s²/in386.089

사람들이 걸려 넘어지는 행은 질량이다. 동적해석에서 질량은 "무게"가 아니라 힘·길이와 일관된 양이어야 한다. kN–m 체계에서 질량 1 ton의 무게는 9.80665 kN이고, N–mm 체계에서 같은 1 ton의 무게는 9806.65 N이다. 이것을 틀리면 모델이 1000배 단위로 뻣뻣하거나 물러지고, 보고되는 주기는 약 31.6배 어긋난다. 이 시리즈는 전부 kN과 m을 쓰고, 모든 스크립트 첫 줄 주석에 그것을 적는다.

6. 첫 모델, 한 줄씩

숨은 값이 하나도 없는 2부재 평면 트러스를 보자. 지간 4.0 m, 정점 라이즈 1.5 m, 양쪽 하단 핀지지, E = 200 GPa, A = 1000 mm², 정점에 아래로 50 kN 한 개. 자중, 좌굴 검토, 접합부 상세, 초기 부정을 모두 뺀 선형탄성 이상화다.

# 단위: kN, m import openseespy.opensees as ops ops.wipe() # 빈 Domain 에서 시작 ops.model('basic', '-ndm', 2, '-ndf', 2) # 평면 트러스: 절점당 x, y # --- 형상 ----------------------------------------------------------- ops.node(1, 0.0, 0.0) ops.node(2, 4.0, 0.0) ops.node(3, 2.0, 1.5) # --- 경계조건 ------------------------------------------------------- ops.fix(1, 1, 1) # 1 = 구속, 0 = 자유 ops.fix(2, 1, 1) # --- 재료와 요소 ---------------------------------------------------- ops.uniaxialMaterial('Elastic', 1, 200.0e6) # E = 200 GPa 를 kN/m2 로 ops.element('Truss', 1, 1, 3, 1.0e-3, 1) # 태그, i절점, j절점, A, 재료태그 ops.element('Truss', 2, 2, 3, 1.0e-3, 1) # --- 하중: 패턴 = 시간이력 x 공간분포 ------------------------------- ops.timeSeries('Linear', 1) # 계수가 시간에 비례해 증가 ops.pattern('Plain', 1, 1) # 패턴 1 이 시간이력 1 을 사용 ops.load(3, 0.0, -50.0) # 절점 3 에 Fx, Fy # --- 해석 여섯 객체 ------------------------------------------------- ops.constraints('Plain') ops.numberer('RCM') ops.system('BandSPD') ops.test('NormDispIncr', 1.0e-8, 10) ops.algorithm('Linear') ops.integrator('LoadControl', 1.0) ops.analysis('Static') ok = ops.analyze(1) # 0 이면 성공 print('analyze returned', ok) print('node 3 displacement', ops.nodeDisp(3)) print('element 1 axial force', ops.basicForce(1)[0])

analyze returned 0 node 3 displacement [0.0, -0.0008680555555555556] element 1 axial force -41.666666666666664

이 스크립트에서 프레임워크의 설계가 드러나는 지점이 넷 있다.

uniaxialMaterial('Elastic', 1, E)에는 A가 없다. uniaxial 재료는 응력–변형률 관계다. 단면적은 요소의 몫이고, 그래서 A가 element('Truss', ...)에 들어간다. 이 분리 덕분에 나중에 같은 단면 형상을 탄성·경화·열화 재료 법칙 사이에서 형상을 건드리지 않고 갈아끼울 수 있다.

timeSeries와 pattern은 서로 다른 개념이다. load()는 하중의 공간적 모양을 준다. 시간이력은 그 모양의 가상시간에 대한 배율을 준다. Linear는 배율이 현재 시간과 같다는 뜻이므로 시간 1.0에서 모델은 50 kN 전량을 받는다. 둘을 분리하는 이유는, 같은 공간 분포를 램프·사인파·계측 지진기록으로 다시 정의하지 않고 스케일할 수 있게 하기 위해서다.

load()를 쓴 시점에 하중이 걸리는 것이 아니다. 하중은 적분기가 가상시간을 전진시키고 해석이 풀 때 걸린다. analyze()가 돌기 전까지 Domain 안에서 바뀌는 것은 없다.

analyze()는 예외가 아니라 상태코드를 돌려준다. 0이면 그 단계가 수렴했다는 뜻이고, 음수면 수렴하지 않았다는 뜻이다. 이 반환값을 무시하는 스크립트는 실패한 단계의 결과를 태연히 보고한다. 비선형 해석에서 가장 위험한 습관 중 하나다. 매번 확인한다.

7. 믿기 전에 검증한다: 하나의 답에 이르는 독립적인 세 경로

위 트러스를 고른 이유는 손으로 정확히 풀리기 때문이다. 두 부재의 길이는 모두 L = √(2.0² + 1.5²) = 2.5 m이고, 수평과 이루는 각의 사인은 1.5/2.5 = 0.6이다. 정점의 연직 평형은 2 F sin θ = P를 요구하므로:

부재력 F = P / (2 sin θ) = 50 / (2 x 0.6) = 41.666667 kN 부재 축소 δ = F L / (E A) = 41.666667 x 2.5 / 200000 = 0.52083 mm 정점 침하 v = δ / sin θ = 0.52083 / 0.6 = 0.868056 mm

세 번째 완전히 독립적인 확인은 OpenSees를 전혀 쓰지 않고 NumPy로 직접 강성행렬을 조립하는 것이다. 프레임워크와 닫힌해와 밑바닥부터 짠 행렬해가 모두 일치한다면, 오류가 나려면 서로 무관한 세 곳에서 동시에 나야 한다.

import math import numpy as np E, A, P = 200.0e6, 1.0e-3, 50.0 nodes = {1: (0.0, 0.0), 2: (4.0, 0.0), 3: (2.0, 1.5)} bars = [(1, 3), (2, 3)] K = np.zeros((6, 6)) for i, j in bars: dx = nodes[j][0] - nodes[i][0] dy = nodes[j][1] - nodes[i][1] Le = math.hypot(dx, dy) c, s = dx / Le, dy / Le k = (E * A / Le) * np.array([ [ c*c, c*s, -c*c, -c*s], [ c*s, s*s, -c*s, -s*s], [-c*c, -c*s, c*c, c*s], [-c*s, -s*s, c*s, s*s]]) dofs = [2*(i-1), 2*(i-1)+1, 2*(j-1), 2*(j-1)+1] K[np.ix_(dofs, dofs)] += k free = [4, 5] # 절점 3 의 ux, uy u = np.linalg.solve(K[np.ix_(free, free)], np.array([0.0, -P])) print(u) # -> [ 0. -0.00086806]

항목닫힌해NumPy 직접강성OpenSees상대차

정점 연직변위−0.868056 mm−0.868056 mm−0.868056 mm1.2 × 10⁻¹⁶

부재 축력41.666667 kN—41.666667 kN1.7 × 10⁻¹⁶

연직반력 합 − P0—−7.1 × 10⁻¹⁵ kN기계 정밀도

여기서 기계 정밀도 수준의 일치는 당연한 기대치다. 선형탄성 정적해석에서 Truss 요소는 직접강성법 그 자체이기 때문이다. 바로 그래서 이것이 좋은 첫 벤치마크다. 차이가 났다면 그것은 모델링 근사가 아니라 모델의 실수다. 뒤 글에서는 프레임워크와 손계산이 정당한 이유로 달라지는 요소들이 나오는데, 그 두 상황을 구분하는 능력이 여기서 시작된다.

8. 결과 읽기: 어떤 명령이 어떤 질문에 답하는가

수렴한 단계 이후에는 Domain에 물어볼 수 있다. 네 명령이 대부분을 덮으며, 이것을 혼동하는 것이 잘못된 그래프의 흔한 원인이다.

명령반환값좌표계주 용도

nodeDisp(tag, dof)절점 하나의 변위전역층간변위, 처짐, 제어절점 이력

nodeReaction(tag)절점 하나의 반력전역밑면전단력. 먼저 reactions() 호출 필요

eleForce(tag)요소 하나의 모든 단부력전역절점 평형 확인

basicForce(tag)요소의 basic 계 부재력국부 / basic축력, 단부모멘트

ops.reactions() # nodeReaction() 전에 반드시 호출 print(ops.nodeReaction(1)) # -> [33.333333, 25.0] print(ops.nodeReaction(2)) # -> [-33.333333, 25.0] print(ops.eleForce(1)) # -> [33.333333, 25.0, -33.333333, -25.0] print(ops.basicForce(1)) # -> [-41.666667]

네 줄을 같이 읽으면 하중경로 전체가 보인다. 각 지점이 연직으로 25 kN씩 받아 합이 작용하중 50 kN이 된다. 동시에 각 지점은 수평으로 33.33 kN을 서로 반대 방향으로 받는데, 이것이 아치 작용이 만들어내는 수평 스러스트이며 전체적으로는 상쇄된다. basicForce의 −41.67 kN은 같은 힘을 부재축 방향으로 표현한 값이고, 음수는 압축을 뜻한다. nodeDisp만 봤다면, 이 단순해 보이는 트러스가 작용하중의 3분의 2에 해당하는 힘으로 지점을 바깥으로 밀고 있다는 사실을 놓쳤을 것이다.

한 가지 주의. eleForce는 전역 성분을 돌려주고 그 부호 규약은 요소 종류와 절점 순서에 의존하는 반면, basicForce는 요소 고유의 basic 부재력을 돌려준다. 트러스의 basic 계는 성분이 하나(축력)이고, 2차원 보–기둥은 셋(축력과 양단 모멘트)이다. 지금 손에 든 것이 어느 쪽인지 모른 채 둘을 비교하면 안 된다.

9. Recorder: 이력은 실행 후가 아니라 실행 전에 요청한다

Python 반복문 안에서 출력하는 방식은 세 단계에서는 되고 3천 단계에서는 못 쓴다. Recorder는 해석 전에 등록하는 객체로, 확정된 모든 단계에서 요청한 값을 파일에 쓴다. 더 빠를 뿐 아니라 재현 가능하다. 요청이 임시 후처리가 아니라 모델의 일부가 되기 때문이다.

ops.recorder('Node', '-file', 'node3.out', '-time', '-node', 3, '-dof', 1, 2, 'disp') ops.recorder('Element', '-file', 'ele.out', '-ele', 1, 2, 'basicForce') ops.integrator('LoadControl', 0.25) # 1/4 하중씩 네 단계 ops.analyze(4) ops.wipe() # recorder 파일을 flush 하고 닫는다

node3.out ele.out 0.25 0 -0.000217014 -10.4167 -10.4167 0.50 0 -0.000434028 -20.8333 -20.8333 0.75 0 -0.000651042 -31.2500 -31.2500 1.00 0 -0.000868056 -41.6667 -41.6667

볼 것이 셋이다. -time 플래그가 가상시간 열을 앞에 붙이며, 이것이 없으면 하중–변위 곡선을 그릴 수 없다. 응답은 하중계수에 정확히 선형인데, 선형탄성 모델이라면 반드시 그래야 하므로 선형이라고 믿는 모델의 유용한 점검이 된다. 그리고 파일을 닫는 것은 ops.wipe()다. 이것을 빠뜨리면 긴 해석의 마지막 버퍼 줄이 사라질 수 있다.

10. 매개변수 스윕, 그리고 wipe()가 있는 이유

인터프리터가 전역 모델 하나만 들고 있으므로, 매개변수 연구는 매 반복마다 모델을 처음부터 다시 짓는 반복문이 된다. 이 트러스의 부재력은 P / (2 sin θ)이므로, 정점을 낮춰 트러스가 납작해질수록 같은 하중을 지탱하는 데 필요한 부재력은 한없이 커진다. 몸에 익혀둘 만한 역학 결과이면서, 프레임워크를 한 점이 아니라 구간 전체에서 공식과 대조하는 편리한 방법이기도 하다.

import math import openseespy.opensees as ops E, A, P, span = 200.0e6, 1.0e-3, 50.0, 4.0 for rise in [2.0, 1.5, 1.0, 0.75, 0.5, 0.35, 0.25]: L = math.hypot(span / 2, rise) F_closed_form = P / (2 * rise / L) ops.wipe() # 필수: 매번 새 모델 ops.model('basic', '-ndm', 2, '-ndf', 2) ops.node(1, 0.0, 0.0) ops.node(2, span, 0.0) ops.node(3, span / 2, rise) ops.fix(1, 1, 1) ops.fix(2, 1, 1) ops.uniaxialMaterial('Elastic', 1, E) ops.element('Truss', 1, 1, 3, A, 1) ops.element('Truss', 2, 2, 3, A, 1) ops.timeSeries('Linear', 1) ops.pattern('Plain', 1, 1) ops.load(3, 0.0, -P) ops.constraints('Plain'); ops.numberer('RCM'); ops.system('BandSPD') ops.algorithm('Linear'); ops.integrator('LoadControl', 1.0) ops.analysis('Static') assert ops.analyze(1) == 0 F_opensees = abs(ops.basicForce(1)[0]) print(f"rise/span {rise/span:5.3f} closed form {F_closed_form:8.3f} kN" f" OpenSees {F_opensees:8.3f} kN")

rise/span 0.500 closed form 35.355 kN OpenSees 35.355 kN rise/span 0.375 closed form 41.667 kN OpenSees 41.667 kN rise/span 0.250 closed form 55.902 kN OpenSees 55.902 kN rise/span 0.188 closed form 71.200 kN OpenSees 71.200 kN rise/span 0.125 closed form 103.078 kN OpenSees 103.078 kN rise/span 0.087 closed form 145.028 kN OpenSees 145.028 kN rise/span 0.062 closed form 201.556 kN OpenSees 201.556 kN

assert ops.analyze(1) == 0 줄은 장식이 아니다. 스윕에서는 조건이 나쁜 한 구성이 조용히 실패하고 이전 반복의 낡은 결과가 그래프에 섞여 들어갈 수 있다. 반환값은 반드시 검사한다.

11. 해석은 교체 가능한 여섯 객체의 조립이다

모델 구축은 "이 구조물은 무엇인가"에 답한다. 해석은 "그것을 어떻게 풀 것인가"에 답하는데, OpenSees는 이 질문을 대신 답해주지 않는다. 선형 트러스와 붕괴까지의 푸시오버와 지진 시간이력에서 정답이 다르기 때문이다. 대신 여섯 객체를 지목하게 하고, analysis()가 그것들을 하나로 묶는다.

객체결정하는 것무난한 출발값바꾸게 되는 시점

constraints()구속과 다점구속이 식에 들어가는 방식PlainequalDOF·강막 사용 시 Transformation

numberer()자유 자유도에 붙는 방정식 번호RCM드묾. 병렬 해석에서 ParallelPlain

system()강성행렬의 저장과 분해 방식BandGeneral대형·난해 모델에서 UmfPack, SparseSYM

algorithm()평형으로 반복해 다가가는 방식Linear 다음 Newton한계점 부근에서 Newton이 멈출 때 KrylovNewton

integrator()무엇이 얼마나 전진하는가LoadControl정점 통과 시 DisplacementControl, 동적에서 Newmark

test()잔차가 언제 충분히 작은가NormDispIncr, tol 1e-8단위·규모 때문에 기본 허용오차가 무의미해질 때

선형 해석에서 algorithm('Linear')는 단 한 번 풀고 반복하지 않으므로 수렴판정은 참조되지 않는다. 그래도 하나 지정해 둔다. 탄성 재료를 비선형으로 바꾸는 순간 판정이 실질적으로 작동하기 시작하고, 조용히 빠져 있던 모델은 재료 문제처럼 보이는 방식으로 실패한다.

test()에 대해 나중에 몇 시간을 아껴주는 언급 둘. 허용오차는 차원을 가진다. NormDispIncr는 변위 증분 노름을 비교하므로 1e-8은 미터와 밀리미터에서 전혀 다른 뜻이다. 그리고 힘 잔차를 보는 NormUnbalance는 매우 강성이 큰 요소가 있는 모델에서 더 정직한 기준인 경우가 많다. 그런 모델에서는 변위가 더 이상 변하지 않는데도 힘은 아직 평형을 이루지 못했을 수 있다.

12. analyze(1)이 실제로 하는 일

analyze(1) 한 번은 한 개의 연산이 아니다. 여섯 객체 사이의 짧은 안무이고, 그 순서를 알면 실패했을 때 어떤 객체를 의심해야 하는지 알 수 있다.

integrator가 도달할 새 상태를 정의한다. LoadControl이라면 한 증분 뒤의 가상시간, 따라서 더 큰 하중계수다.

algorithm이 Domain의 모든 요소에 현재 접선강성과 저항력을 요청하고, 구속 처리기와 번호 부여기가 그것을 연립방정식으로 조립한다.

system이 분해하고 풀어 변위 증분을 얻는다.

Domain이 갱신된다. 절점 자유도가 시행값을 받고, 모든 요소는 새 시행 변위로부터 자기 상태를 다시 계산하라는 지시를 받는다.

test가 남은 불평형을 측정해 수렴 여부를 보고한다.

수렴하면 상태가 확정(commit)되어, 경로의존 재료가 기억할 이력이 된다. 수렴하지 않으면 2~5가 반복 한계까지 되풀이된다.

여기서 두 가지 구조적 사실이 따라 나온다. 첫째, 요소의 확정된 상태가 곧 그 요소의 기억이다. 항복·균열·손상이 작동하는 이유가 이것이고, 확정되지 않은 실패 단계가 아무 흔적도 남기지 않는 이유도 이것이다. 둘째, 비선형 해석이 실패할 때 그 실패는 거의 언제나 "OpenSees 안"이 아니라 특정 단계에서 특정 객체의 실패이며, 그 단계는 메시지에 이름으로 적혀 있다. 그래서 메시지 읽기가 남는다.

13. 첫 주에 만나는 여섯 오류와 그 실제 의미

아래 메시지는 일부러 망가뜨린 모델에 대해 OpenSeesPy 3.8.0이 출력한 원문이다. 각각이 서로 다른 객체를 가리키므로 보자마자 알아볼 가치가 있다.

무엇을 했는가OpenSees의 출력실제 의미

ndf 3 모델에서 fix(1,1,1)WARNING: invalid # of constraint values인수 개수가 ndf와 같아야 한다. 아무것도 생성되지 않았다.

만든 적 없는 절점을 요소가 참조WARNING Domain::addElement - In element 1 no Node 2 exists in the domain태그 오타. 요소가 거부되었으므로 모델이 불완전하다.

정의되지 않은 재료 태그를 요소가 참조UniaxialMaterial *getUniaxialMaterial(int tag) - none found with tag: 99재료는 그것을 쓰는 요소보다 먼저 만들어야 한다.

구속이 없거나 강체운동이 남음factorization failed, matrix singular U(i,i) = 0 뒤에 analyze failed, returned: -3구조물이 변형 없이 움직일 수 있다. 허용오차를 키우지 말고 구속을 넣는다.

강성이 없는 자유도(일직선 트러스에 직교 하중)같은 특이행렬 메시지솔버 결함이 아니다. 그 방향은 이 모델에서 실제로 강성이 0이다.

analysis() 전에 analyze()WARNING No Analysis type has been specified여섯 객체가 하나로 묶인 적이 없다.

네 번째와 다섯 번째 행은 원인이 전혀 다른데 메시지가 같고, 그 구분이 중요하다. 지점 누락은 구속을 추가해 고치는 모델링 누락이다. 일직선 트러스에 직교 하중이 걸린 경우는 쓴 그대로 옳은 모델이고 그 방향으로 물리적 강성이 실제로 없다. 고칠 것은 솔버가 아니라 구조 이상화다. 어느 쪽이든 허용오차를 느슨하게 하거나 system()을 바꾸는 것은 문제를 푸는 것이 아니라 가리는 것이다.

지금 들여둘 습관이 하나 더 있다. OpenSees는 대부분의 진단을 stderr로 쓰고, 어떤 조건은 Python 예외 없이 경고만 낸다. 노트북에서 실행하거나 출력을 캡처한다면 stderr가 반드시 보이게 한다. 보지 못한 채 지나간 미수렴 단계가 가능한 최악의 결과다.

14. 같은 논리를 다섯 단계 깊이로

입문자는 모델을 만들고, analyze()가 0을 반환했는지 확인하고, 반력 합이 작용하중과 같은지 본다. 초급 실무자는 나머지를 믿기 전에 최소 한 결과를 손이나 스프레드시트로 재현하고, 스크립트가 어떤 단위계인지 안다. 실무 해석자는 여섯 해석 객체를 의도적으로 고르고, 모든 반환값을 검사하며, 출력 대신 이력을 기록한다. 선임 엔지니어는 ndm·ndf·요소 종류의 선택이 어떤 이상화를 확정했는지 알아보고, 그 이상화가 무엇을 지웠는지 명시한다. 전문가는 명령 이름 뒤의 요소 정식화가 무엇인지, 그 적분과 상태결정 가정이 무엇인지 알고, 문서가 답하지 않는 질문을 소스에서 확인한다.

이 진행은 명령을 더 많이 아는 문제가 아니다. 위로 갈수록 같은 세 질문에 더 많은 근거로 답한다. 모델에 무엇이 들어 있는가, 솔버가 실제로 무엇을 했는가, 답이 틀렸다면 나는 어떻게 알아차리는가.

15. 첫 OpenSees 모델 최소 점검표

단위계가 주석으로 명시되어 있고, 질량을 포함한 모든 입력이 그것과 일관된가?

ndm과 ndf가 이상화와 맞고, 모든 fix()·load()·mass()가 정확히 ndf개의 값을 가지는가?

getNodeTags()와 getEleTags()가 짰다고 믿는 것이 아니라 지으려던 것을 돌려주는가?

선택한 ndf가 허용하는 모든 강체운동에 대해 구조물이 구속되어 있는가?

모든 단계와 모든 반복에서 analyze()의 반환값을 검사했는가?

반력의 합이 반올림 오차 범위에서 작용하중과 같은가?

최소 한 결과를 독립적인 방법(닫힌해·손계산·별도 스크립트)으로 재현했는가?

선형이라고 믿는 모델에서 응답이 하중계수에 비례하는가?

이력이 사후 출력이 아니라 실행 전에 등록한 Recorder로 기록되는가?

스크립트가 모델을 지을 때마다 시작에서, 그리고 recorder 파일을 비우기 위해 끝에서 wipe()를 호출하는가?

이 목록은 각 항목이 몇 초 만에 확인되고, 각각이 어느 시점엔가 OpenSees 결과가 틀린 이유의 전부였던 적이 있다. 이 시리즈의 다음 글은 같은 객체 모델과 같은 검증 습관을 유지한 채, 늘어나기만 하는 부재에서 휘는 요소로 넘어간다. 단면, 기하변환, 분포하중, 그리고 보–기둥 계열이 주제다.

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

참고 자료

위에서 설명한 객체 모델·명령 체계·해석 조립의 1차 및 기관 자료다. 프레임워크를 문서화한 자료이며, 프로젝트별 설계 기준을 제공하지는 않는다.

McKenna, F., OpenSees: A Framework for Earthquake Engineering Simulation, Computing in Science & Engineering 13(4), 2011, pp. 58–66 — 주 개발자가 쓴 프레임워크 설계 근거.

Pacific Earthquake Engineering Research Center, OpenSees Wiki — 오래된 표준 명령 레퍼런스와 예제 모음.

OpenSees Developers, OpenSees Documentation — 현재 유지되는 문서 세트.

Zhu, M., OpenSeesPy Documentation — Python 인터페이스의 명령별 문서.

OpenSeesPy, model 명령 및 Truss 요소 — 위에서 쓴 인수 목록의 원문.

OpenSeesPy, analysis, algorithm, integrator, test, system, numberer, constraints — 해석 여섯 객체.

OpenSeesPy, recorder 명령 — 출력 요청 문법과 사용 가능한 응답 항목.

OpenSees Developers, OpenSees 소스 저장소 — 문서와 실제 동작이 어긋날 때의 최종 근거.

Python Package Index, openseespy — 릴리스 이력과 지원 플랫폼.

자료 확인 2026-08-30. 이 글의 모든 결과·콘솔 출력·오류 메시지는 OpenSeesPy 3.8.0에서 생성되었으며 위의 완전한 스크립트로 재현할 수 있다. 프레임워크의 동작을 보여주는 것이지 프로젝트별 해석이나 기준 적합성 검토를 대신하지 않는다.