Windows의 OpenSees: 휠은 파이썬 3이면 된다 하고 바이너리는 3.12 라고 한다

Windows용 OpenSeesPy 휠은 py3-none-win_amd64 로 태그되어 있는데 그 안의 .pyd 는 python312.dll 을 임포트한다. 그래서 파이썬 3.9부터 3.13까지 결과가 네 가지로 갈리고 그중 하나만 동작한다. 임포트 테이블을 읽어보면 MSVCP140 도 VCRUNTIME

Windows의 OpenSees: 휠은 파이썬 3이면 된다 하고 바이너리는 3.12 라고 한다

Windows용 OpenSeesPy 휠은 py3-none-win_amd64로 태그되어 있어 pip에게 "파이썬 3이면 아무거나 된다"고 말하는데, 그 안의 바이너리는 python312.dll을 임포트합니다. 두 사실은 서로 맞지 않고, 그 불일치가 Windows에서 OpenSees를 설치할 때 벌어지는 일의 대부분입니다. 파이썬 3.10이나 3.11에서는 설치가 성공하고 임포트가 예외를 던집니다. 3.13에서는 설치가 성공하고, 임포트가 다른 방식으로 실패하며, 메시지는 버전이 아니라 DLL을 언급합니다.

이 글은 Windows에서 다르게 동작하거나 Windows 도구가 망가뜨리는 부분들에 관한 것입니다. 설치, Windows 편집기와 스프레드시트가 만드는 파일 형식, 그리고 경로. 여기 있는 것은 전언이 아니라 증거입니다. 휠을 내려받아 임포트 테이블을 읽었고, 모든 파일 형식 결과를 재현해 측정했습니다. 이 글은 14부입니다. 1–13부에서 객체 모델부터 좌굴까지 다뤘습니다.

1. 어느 파이썬인가, 그리고 왜

pip install openseespy는 플랫폼 패키지를 끌어오는 메타 패키지를 설치합니다.

openseespy 3.8.0.0 Requires-Python >= 3.10 openseespylinux >= 3.8.0.0 ; platform_system == "Linux" >= 3.12 86.3 MB openseespywin >= 3.8.0.0 ; platform_system == "Windows" >= 3.12 6.8 MB openseespymac >= 3.8.0.0 ; platform_system == "Darwin" >= 3.10 10.1 MB

메타 패키지는 3.10을 받아들이고, Windows 패키지는 3.12를 요구하며, 바이너리는 정확히 3.12를 필요로 합니다. 그래서 결과가 넷으로 갈립니다.

파이썬무슨 일이 일어나나이유

3.9pip 가 아예 거부openseespy 가 ≥ 3.10 요구

3.10, 3.11설치는 "성공", 임포트가 예외openseespywin 이 ≥ 3.12 라서 설치되지 않음

3.12동작.pyd 가 python312.dll 을 링크

3.13설치 성공, DLL 에서 임포트 실패≥ 3.12 는 만족하지만 python312.dll 이 없음

3.13 사례가 가장 고약한데, 휠 메타데이터가 헐거운 정도가 아니라 틀렸기 때문입니다. 이전 릴리스는 ~=3.12.0 — 정확히 3.12 — 를 선언했고, 3.8.0.0이 그것을 >=3.12로 완화하면서 바이너리가 감당할 수 없는 3.13과 3.14를 받아들이게 됐습니다. 제대로 태그된 확장이라면 cp312-cp312-win_amd64였을 것이고, pip는 그냥 제시하지 않았을 것입니다.

그래서 "어느 파이썬인가"의 답은 숫자 하나이고, 확인은 한 줄입니다.

python -c "import sys; print(sys.version_info[:2])" # (3, 12) 가 나와야 한다

시스템 파이썬을 바꾸지 말고 3.12 인터프리터로 만든 가상환경을 쓰십시오. Windows에서는 py -3.12 -m venv .venv 다음 .venv\Scripts\activate입니다.

2. 휠 안에 실제로 있는 것

6.8 MB 휠에는 329바이트짜리 __init__.py와 17.2 MB짜리 opensees.pyd가 들어 있습니다. 그게 전부입니다. 동봉된 DLL도, 데이터 파일도 없습니다. 그 .pyd의 임포트 테이블을 읽으면 런타임 의존성 전체가 나옵니다.

imagehlp.dll Windows, 스택·심볼 지원 KERNEL32.dll Windows WSOCK32.dll Windows, 병렬·소켓 명령에서 사용 python312.dll Windows 의 일부가 아닌 유일한 것

MSVCP140.dll도 VCRUNTIME140.dll도 없습니다. C++ 런타임이 .pyd에 정적 링크되어 있으므로, "DLL load failed"에 대한 표준 조언 — Visual C++ 재배포 패키지 설치 — 은 이 패키지에 해당하지 않습니다. 그 오류가 보인다면 빠진 DLL은 python312.dll이고, 해결책은 재배포 패키지가 아니라 파이썬 버전입니다.

휠이 손에 있으면 어느 기계에서든 직접 확인할 수 있습니다. 임포트 테이블은 Windows의 Dependencies 도구로, 다른 곳에서는 objdump -p opensees.pyd로 읽힙니다.

3. Windows 도구가 만드는 파일

지진기록은 텍스트 파일이고, 어떻게 저장했는지가 대부분의 사람이 예상하는 것보다 중요합니다. 같은 아홉 값의 변형 아홉 가지를 각각 timeSeries('Path', ..., '-filePath', ...)로 읽어 해석을 통해 되읽었습니다.

파일 변형결과

LF (유닉스)기준

CRLF (윈도우)2.2 × 10⁻¹⁶ 까지 동일

탭 구분2.2 × 10⁻¹⁶ 까지 동일

전부 한 줄에2.2 × 10⁻¹⁶ 까지 동일

끝에 빈 줄2.2 × 10⁻¹⁶ 까지 동일

앞쪽 공백2.2 × 10⁻¹⁶ 까지 동일

BOM 붙은 UTF-8오류, pattern 생성 때 발생

소수점 콤마모든 값이 0 으로 읽힘, 메시지 없음

줄바꿈 문자는 문제가 아닙니다. CRLF, 탭, 여분의 공백, 끝의 빈 줄, 전부 한 줄에 — 모두 동일하게 파싱됩니다. 기록을 쓰기 전에 유닉스 줄바꿈으로 변환하라는 흔한 조언은 불필요하고, 그것을 알아두면 더 이상 그쪽을 의심하지 않게 됩니다.

BOM은 치명적이고, 늦게 보고합니다. 엑셀의 "CSV UTF-8" 내보내기가 BOM을 쓰고, UTF-8 저장 옵션에 "BOM 포함"이라고 적힌 편집기는 모두 그렇습니다. 구버전 메모장이 기본값으로 그랬습니다. timeSeries 명령 자체는 성공하고, 실패는 다음 명령에서 나타납니다.

ops.timeSeries('Path', 1, '-dt', dt, '-filePath', 'record.txt') # 아무 말 없음 ops.pattern('Plain', 1, 1) WARNING - problem creating TimeSeries for LoadPattern WARNING failed to create pattern

메시지는 파일이 아니라 pattern을 지목하므로, 보고된 위치가 원인에서 몇 줄 떨어져 있습니다. 기록은 "BOM 없는 UTF-8" 또는 순수 ANSI/ASCII로 저장하십시오.

소수점 콤마는 조용하고 전면적입니다. 독일·프랑스식, 또는 콤마를 소수점으로 쓰는 어떤 로캘의 스프레드시트든 0,250000으로 내보냅니다. 그러면 시계열의 모든 값이 0.000으로 읽히고, 해석은 완벽하게 돌고, 매 스텝 수렴하며, 항등적으로 0인 지진동에 대한 응답을 내놓습니다. 어디에서도 아무 문제도 보고되지 않습니다. 확인 비용은 한 줄입니다.

raw = open(path).read() assert ',' not in raw, '기록에 소수점 콤마' assert not raw.startswith('\ufeff'), '기록에 BOM' vals = [float(v) for v in raw.split()] print(len(vals), '점, 최대', max(abs(v) for v in vals))

최대값이 0이면, 해석이 못 찾은 것을 먼저 찾은 것입니다.

4. 경로

"C:\temp\new\run.txt" -> C:<TAB>emp<LF>ew<CR>un.txt 16자 r"C:\temp\new\run.txt" -> C:\temp\new\run.txt 19자 "C:/temp/new/run.txt" -> C:/temp/new/run.txt 19자

이스케이프 셋 — \t, \n, \r — 이 OpenSees에 무엇이 건네지기도 전에 발동합니다. OpenSees가 아니라 파이썬의 거동이지만, 대부분의 엔지니어가 이것을 만나는 유일한 자리가 Windows 경로이기 때문에 여기 들어옵니다. 원시 문자열을 쓰거나, Windows API도 받아들이는 슬래시를 쓰거나, 애초에 이 문제를 만들지 않는 os.path.join / pathlib를 쓰십시오.

경로에 관한 메모 둘 더. Windows에는 긴 경로를 켜지 않는 한 260자 제한이 있고, 깊은 OneDrive\Documents\... 프로젝트 트리에 recorder 파일명까지 붙으면 생각보다 쉽게 도달합니다. 그리고 경로 안의 공백은 파이썬 API에서는 괜찮지만, 경로를 중괄호 없이 끼워넣는 Tcl 스크립트에서는 괜찮지 않습니다. 10부의 "전부 중괄호로 감싸라"가 여기에도 적용되는 이유입니다.

5. 결코 예외를 던지지 않는 실패 셋

소수점 콤마 기록이 첫 번째입니다. 두 번째는 존재하지 않는 폴더를 겨눈 recorder입니다.

ops.recorder('Node', '-file', 'results/run1/out.txt', ...) # results/run1 없음 예외도, 경고도, 그리고 나중에 파일도 없음

측정 결과: recorder는 정상적으로 돌아오고, 해석이 돌고, 모든 analyze()가 0을 반환하며, 끝나고 나면 출력 파일이 없습니다. Windows에서는 IDE로 실행한 스크립트의 작업 디렉터리가 스크립트가 있는 폴더가 아닌 경우가 많아 쉽게 걸립니다. 디렉터리를 명시적으로 만드십시오.

os.makedirs(outdir, exist_ok=True) ... ops.remove('recorders') assert os.path.exists(outfile) and os.path.getsize(outfile) > 0

세 번째는 위의 역슬래시 경로이고, 탭이 들어간 파일명을 만들어 나중에 엉뚱해 보이는 메시지로 실패합니다.

측정 중에 나온 좋은 소식 하나: recorder 출력은 매 스텝 디스크에 반영됩니다. 1스텝 뒤 파일에 1줄, 1,000스텝 뒤 1,000줄이 있었고, remove('recorders') 호출 전이었습니다. 긴 해석의 진행을 recorder 파일을 tail 해서 지켜볼 수 있고, 크래시 후 부분적으로 쓰인 파일에는 크래시 직전까지가 전부 들어 있습니다.

6. 동작하는 Windows 설정

python.org에서 파이썬 3.12를 콕 집어 설치하고 "Add to PATH"를 체크한다.

py -3.12 -m venv .venv 다음 .venv\Scripts\activate. 시스템 파이썬에 절대 설치하지 않는다.

pip install openseespy numpy matplotlib 다음 python -c "import openseespy.opensees as ops; print(ops.version())".

프로젝트를 OneDrive 밖에, 공백이나 비ASCII 문자가 없는 경로에 둔다.

모든 기록을 BOM 없는 UTF-8 또는 순수 ASCII로, 소수점은 마침표로 저장한다.

경로는 원시 문자열이나 pathlib로 쓰고, 역슬래시가 든 일반 문자열로는 절대 쓰지 않는다.

recorder 전에 출력 디렉터리를 만들고, 끝난 뒤 파일이 존재하고 비어 있지 않은지 단언한다.

기록을 읽을 때마다 확인한다. 점 개수, 최대값, 콤마 없음, BOM 없음.

7. 같은 설치를 다섯 단계 깊이로

입문자는 pip install openseespy를 칩니다. 주니어는 맞는 파이썬 버전의 가상환경 안에서 그렇게 합니다. 실무 해석자는 해석이 읽기 전에 모든 입력 파일을 검증합니다. 시니어는 이 패키지에서 DLL 로드 오류가 파이썬 버전 문제임을 알고 재배포 패키지를 찾아 헤매지 않습니다. 전문가는 앞뒤가 맞지 않을 때 휠의 메타데이터와 바이너리의 임포트 테이블을 읽습니다. 파이썬 3이면 아무거나 된다고 주장하는 패키지가 정확히 하나에만 링크되어 있기 때문입니다.

다음 글이 이 시리즈를 닫습니다. 풀이가 딸린 연습문제와, 열네 편에서 쓴 모든 용어의 한영 대조 용어집.

전체 스크립트

이 글에서 측정한 모든 파일 실패를 timeSeries 가 파일을 보기도 전에 잡아내는 검사기, 그리고 그것을 건너뛰면 무슨 일이 일어나는지.

# Part 14 - validate a ground-motion file before the analysis reads it # units: whatever the record is in; this checks the file, not the physics import os import openseespy.opensees as ops def check_record(path, dt): """Every failure mode measured in Part 14, caught before timeSeries sees it.""" if not os.path.exists(path): raise FileNotFoundError(path) raw = open(path, encoding='utf-8', errors='replace').read() if raw.startswith(' '): raise ValueError('byte-order mark: save as UTF-8 without BOM') if ',' in raw: raise ValueError('comma found: a decimal comma reads as zero, silently') vals = [float(v) for v in raw.split()] if not vals: raise ValueError('no numbers in the file') peak = max(abs(v) for v in vals) if peak == 0.0: raise ValueError('every value is zero') return {'points': len(vals), 'duration': len(vals) * dt, 'peak': peak} os.makedirs('tmp_records', exist_ok=True) good = [0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0] cases = { 'plain.txt': '\n'.join(f'{v:.6f}' for v in good).encode(), 'crlf.txt': '\r\n'.join(f'{v:.6f}' for v in good).encode(), 'bom.txt': b'\xef\xbb\xbf' + '\n'.join(f'{v:.6f}' for v in good).encode(), 'comma.txt': '\n'.join(f'{v:.6f}'.replace('.', ',') for v in good).encode(), } for name, body in cases.items(): open(os.path.join('tmp_records', name), 'wb').write(body) print(f"{'file':14s}{'check_record':>42s}") results = {} for name in cases: p = os.path.join('tmp_records', name) try: info = check_record(p, 0.02) results[name] = None print(f'{name:14s}{f"ok: {info["points"]} points, peak {info["peak"]:.3f}":>42s}') except Exception as e: results[name] = str(e) print(f'{name:14s}{f"rejected: {e}":>42s}') assert results['plain.txt'] is None and results['crlf.txt'] is None, \ 'CRLF is not a problem and must not be rejected' assert results['bom.txt'] and results['comma.txt'], \ 'the BOM and the decimal comma must both be caught' # what OpenSees itself does with the comma file, if you do not check 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('Elastic', 1, 1.0) ops.element('Truss', 1, 1, 2, 1.0, 1) ops.timeSeries('Path', 1, '-dt', 0.02, '-filePath', 'tmp_records/comma.txt') ops.pattern('Plain', 1, 1); ops.load(2, 1.0) ops.constraints('Plain'); ops.numberer('Plain'); ops.system('BandGeneral') ops.test('NormDispIncr', 1e-12, 10); ops.algorithm('Linear') ops.integrator('LoadControl', 0.02); ops.analysis('Static') seen = [] for _ in range(len(good)): ops.analyze(1); seen.append(ops.nodeDisp(2, 1)) print(f'\nthe decimal-comma record, loaded without checking: {[f"{v:.3f}" for v in seen]}') assert max(abs(v) for v in seen) == 0.0, 'expected the comma record to read as zeros' print('OK every value zero, every analyze() returned 0, and nothing warned')

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

file check_record plain.txt ok: 9 points, peak 1.000 crlf.txt ok: 9 points, peak 1.000 bom.txt rejected: byte-order mark: save as UTF-8 without BOM comma.txt rejected: comma found: a decimal comma reads as zero, silently the decimal-comma record, loaded without checking: ['0.000', '0.000', '0.000', '0.000', '0.000', '0.000', '0.000', '0.000', '0.000'] OK every value zero, every analyze() returned 0, and nothing warned

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

참고 자료

PyPI, openseespy, openseespywin — 1절에서 인용한 메타데이터.

OpenSeesPy 문서, Installation.

Python packaging, Platform compatibility tags — py3-none 과 cp312-cp312 가 다른 뜻인 이유.

OpenSeesPy, Path TimeSeries, recorder.

Microsoft, Maximum path length limitation.

출처 확인 2026-08-31. 휠에 관한 사실은 PyPI에서 openseespywin-3.8.0.0-py3-none-win_amd64.whl을 내려받아 메타데이터와 동봉된 opensees.pyd의 임포트 테이블을 읽어 얻었습니다. 파일 형식과 경로 결과는 OpenSeesPy 3.8.0과 CPython 3.12에서 측정했으며, 해당 텍스트 파서와 문자열 이스케이프는 모든 플랫폼에서 같은 코드입니다. 해석 자체를 Windows에서 돌리지는 않았고, Windows 파일시스템 고유의 사항 — 260자 제한, OneDrive 거동 — 은 측정이 아니라 인용입니다. 결과는 프레임워크의 거동을 보여주는 것으로, 프로젝트별 해석이나 설계기준 검토를 대체하지 않습니다.