레이블이 디지털트윈인 게시물을 표시합니다. 모든 게시물 표시
레이블이 디지털트윈인 게시물을 표시합니다. 모든 게시물 표시

2026년 7월 31일 금요일

NVIDIA DGX Spark에서 Isaac Sim, Isaac Lab 로봇 피지컬AI 시뮬레이션 개발환경 구축기

이 글은 NVIDIA DGX Spark(GB10, ARM64) 장비에 Isaac Sim과 Isaac Lab을 설치하고, 로봇 시뮬레이션과 강화학습까지 실행해 본 개발환경 구축기를 간단히 나눔한다. 요즘 피지컬 AI가 화두인데, 시뮬레이션에서 로봇 정책을 학습해 실물에 이식하는 sim-to-real 워크플로 실행 환경을 세팅해 보고 간단한 예제를 실행해 본다. 참고로, DGX Spark는 ARM 기반 Grace CPU와 Blackwell GPU가 통합 메모리를 공유하는 독특한 구조라서, 설치 과정에 x86 장비와는 다른 함정이 몇 군데 있었다. 이 글에서 해당 부분을 함께 정리한다.

개요

Isaac Sim은 NVIDIA Omniverse 기반의 로봇 시뮬레이터로, USD(Universal Scene Description)를 네이티브 씬 포맷으로 사용한다. RTX 레이트레이싱 렌더링, PhysX 물리엔진, 카메라 및 LiDAR 같은 합성 센서 시뮬레이션을 제공한다. Isaac Lab은 그 위에 구축된 로봇 학습 프레임워크로, GPU에서 수천 개 환경을 병렬로 돌려 강화학습(PPO 등)과 모방학습을 수행한다.

엔비디아 아이작 심 시뮬레이터

설치 대상 환경은 다음과 같다.

  • 장비: NVIDIA DGX Spark (GB10, ARM64/aarch64, 통합 메모리 121GB)
  • OS: Ubuntu 24.04 (DGX OS), 드라이버 580.159.03, CUDA 13.0
  • Isaac Sim 6.0.1, Isaac Lab 3.0.0b2, PyTorch 2.11 (cu130)

Isaac Sim의 ARM64(aarch64) 빌드는 현재 DGX Spark에서 공식 지원되며, 드라이버는 580.159.03이 권장 버전이다. 더 최신인 R590 계열 드라이버에서는 RTX 렌더러가 크래시하는 이슈가 보고되어 있으므로 드라이버 버전을 먼저 확인하는 것이 좋다.

1단계. 파이썬 환경 준비

Isaac Sim 6.x는 파이썬 3.12용 pip 패키지(cp312, manylinux_2_35_aarch64 휠)를 제공한다. 처음에는 시스템 python3.12로 가상환경인 venv를 만들었다. 본인의 경우, 설치 중 psutil과 PyOpenGL-accelerate가 소스 빌드되면서 Python.h 헤더가 없다는 에러로 실패했다. python3.12-dev를 apt로 설치하면 되지만 sudo 권한이 필요한 상황이었고, 대신 conda(miniforge)의 파이썬을 쓰면 개발 헤더가 자체 포함되어 있어 시스템 패키지 없이 해결된다. 가상환경은 터미널에서 다음과 같이 생성한다. 

conda create -n venv_sim -y python=3.12
conda activate venv_sim

2단계. PyTorch와 Isaac Sim 설치

DGX Spark는 CUDA 13 환경이므로 cu130 빌드의 PyTorch를 먼저 설치하고, 이어서 Isaac Sim 전체 패키지를 설치한다.

pip install -U torch==2.11.0 --index-url https://download.pytorch.org/whl/cu130
pip install "isaacsim[all,extscache]==6.0.1.0" --extra-index-url https://pypi.nvidia.com

다운로드 용량이 수 GB라서 시간이 좀 걸린다. 설치 후 실행 전에 환경변수 두 개를 설정해야 한다. EULA 동의와, aarch64에서 필수인 OpenMP 라이브러리 프리로드이다. 특히 두 번째 것을 빼먹으면 공유 라이브러리 로드 순서 문제로 실행이 그냥 종료되므로 주의한다.

export OMNI_KIT_ACCEPT_EULA=YES
export LD_PRELOAD=/lib/aarch64-linux-gnu/libgomp.so.1

3단계. 실행 확인

먼저 헤드리스 모드로 SimulationApp을 띄워 USD 스테이지 생성과 프레임 스텝이 정상 동작하는지 확인한다. 

GB10 GPU가 Vulkan 백엔드로 인식되고, 93GB의 GPU 가용 메모리가 잡히는 것을 볼 수 있었다. 이어서 GUI 모드를 실행하면 첫 구동 시 셰이더 컴파일 때문에 수 분이 걸리는데, 완료되면 RTX 실시간 렌더링 뷰포트가 110 FPS 내외로 부드럽게 돌아간다.

isaacsim   # GUI 실행

Isaac Sim 6.0.1 GUI 첫 실행 화면. RTX 뷰포트가 112 FPS로 렌더링되고 있다

4단계. 로봇 데모 실행

환경이 잡혔으니 로봇을 움직여 봤다. Isaac Sim 6.0부터는 사전학습된 강화학습 정책 예제가 isaacsim.robot.policy.examples 모듈로 제공된다. 

상단 메뉴 바의 Isaac Examples > Robots > Quadrupeds > Spot RL Policy를 선택하면 Boston Dynamics Spot 4족보행 로봇이 창고 환경에 자동으로 스폰된다. 이어서 우측 Property 패널에서 학습된 보행 정책(SpotFlatTerrainPolicy)을 확인한 뒤, 좌측의 Play 버튼을 누르고 Robot Controller UI를 통해 속도 명령(Velocity Command)을 주면 로봇이 실제로 걸어 다닌다. 

로봇 애셋과 정책 체크포인트는 NVIDIA 클라우드에서 자동으로 스트리밍되므로 별도의 대용량 파일 다운로드 없이 바로 테스트해 볼 수 있다

한 가지 주의할 점은 API 변화이다. 6.0에서는 정책 명령을 numpy 배열이 아닌 torch 텐서로 넘겨야 하고, 구버전 예제 모듈(omni.isaac.* 계열)은 상당수 제거되었다. 구버전 튜토리얼 코드를 그대로 쓰면 에러가 나므로 모듈 경로를 확인해야 한다.

창고 환경에서 강화학습 정책으로 보행 중인 Spot 로봇

여기에 센서와 데이터 수집 파이프라인을 연결해 봤다. 상단 메뉴의 Create > Sensors 항목에서 Camera와 RTX LiDAR를 로봇 몸체에 부착한 뒤, Isaac Tools > Synthetic Data Recorder 패널을 열어 저장할 데이터를 설정한다. 이어서 시뮬레이션을 재생하고 Start Recording 버튼을 누르면 로봇이 걸어 다니는 동안 RGB 영상, 뎁스맵, 프레임당 약 44만 포인트의 LiDAR 포인트클라우드가 자동으로 추출되어 저장된다. 이렇게 쌓인 정밀한 센서 데이터가 바로 객체 인식 및 자율주행 모델 학습용 합성 데이터셋의 핵심 재료가 된다.

Spot 온보드 센서로 수집한 RGB, 뎁스, LiDAR 데이터

5단계. Isaac Lab 설치와 강화학습

이제 직접 정책을 학습할 차례이다. Isaac Lab은 pip로 설치할 수 있는데, Isaac Sim 6.0에 대응하는 버전은 3.0.0b2 계열이다. 학습 스크립트는 pip 패키지에 포함되지 않으므로 저장소를 따로 클론한다.

pip install "isaaclab[all]==3.0.0b2.post1" --extra-index-url https://pypi.nvidia.com
git clone --depth 1 https://github.com/isaac-sim/IsaacLab.git

ANYmal-D 4족보행 로봇의 속도 추종 태스크를 PPO로 학습시켜 봤다. 터미널에서 train.py 스크립트를 실행해 --num_envs 2048 인자로 2048개 환경을 GPU에서 병렬로 돌렸다. 

python scripts/reinforcement_learning/rsl_rl/train.py \
  --task Isaac-Velocity-Flat-Anymal-D-v0 --headless --num_envs 4096 --max_iterations 500

순수 학습 시간 기준으로 100 iteration이 약 1분 35초밖에 걸리지 않았다. 이어서 --num_envs 4096으로 인자를 변경하여 환경 수를 4096개로 올려봐도, 통합 메모리 덕분에 VRAM 부족(OOM) 걱정 없이 대규모 강화학습을 안정적으로 진행할 수 있었다. 결과 로그는 TensorBoard / Console Log 에서 확인한다. 

학습 곡선을 보면 강화학습의 전형적인 패턴이 그대로 나타난다. 초반에는 로봇들이 명령 추종을 시도하다가 넘어지면서 보상이 급락하고, 넘어지지 않는 법을 터득한 뒤 회복하며, 이후 명령 추종 정밀도를 다듬으면서 완만히 상승한다. 500 iteration(약 8분) 학습 후 평균 보상 22.7, 속도 추종 오차는 정상 상태 기준 0.1 m/s 수준까지 내려갔다.

ANYmal-D PPO 학습 곡선. 탐색기의 보상 급락과 회복, 우상향 패턴이 보인다

학습 완료 후 play.py 스크립트 (python play.py --task Isaac-Velocity-Flat-Anymal-D-v0 --num_envs 16)를 통해 학습된 정책을 재생해 봤다. Isaac Sim 화면에서 16대의 로봇이 각자 무작위 속도 명령을 전달받아 서로 다른 방향으로 자유롭게 걸어 다니는 것을 확인할 수 있다. 특히 구동 과정에서 학습 산출물로 model.onnx 파일이 자동 내보내기되는데, 이 파일을 실물 로봇의 온보드 컴퓨터에 올리기만 하면 시뮬레이션 정책을 실재 로봇으로 바로 이식할 수 있는 편리한 Sim-to-Real 구조를 제공한다.

학습 완료된 정책으로 보행 중인 ANYmal 로봇 16대

마무리

정리하면, 현재 시점에서 DGX Spark에서 Isaac Sim과 Isaac Lab 개발환경 구축 고려사항은 다음과 같다.

  • 드라이버는 580.159.03 권장. R590 계열은 RTX 렌더러 크래시 이슈가 있다
  • 파이썬은 conda의 3.12를 사용하면 개발 헤더 문제를 피할 수 있다
  • PyTorch는 cu130 빌드, Isaac Sim은 pypi.nvidia.com 인덱스에서 설치한다
  • aarch64에서는 LD_PRELOAD로 libgomp를 프리로드해야 한다
  • Isaac Sim 6.0의 API 변화(torch 텐서 명령, 모듈 경로 개편)에 주의한다

시뮬레이션, 합성 데이터 생성, 강화학습까지 장비 한 대에서 모두 돌아가는 것을 확인했다. 특히 통합 메모리 121GB는 대규모 병렬 환경 학습에서 체감 이점이 컸다. 오픈소스와 사전학습 정책, 클라우드 애셋이 잘 갖춰져 있어서, 예전 같으면 몇 주 걸렸을 환경 구축과 검증이 하루 안에 끝났다. 하드웨어 문제가 해결되면, 이제부터는 얼마나 다양한 환경에서 요구사항에 맞는 데이터와 모델을 잘 구축하는 방법이 필요하다. 향후, 피지컬AI기술은 사용자의 정확한 요구사항 탐색, 데이터, 모델 및 출력 간 정렬을 효과적으로 얻는 노하우가 중요해 질 것이다.

레퍼런스

2026년 4월 17일 금요일

오픈클로 같은 에이전틱 시스템 구조 및 동작 메커니즘 분석

이 글은 오픈클로같은 에이전틱 시스템 구조 및 동작 메커니즘 분석해 나눔한다. 자율형 AI 에이전트 프레임워크인 오픈클로(OpenClaw)는 사용자의 로컬 컴퓨터에서 백그라운드 데몬으로 상시 구동되며, 메신저 인프라를 UI로 삼아 파일 시스템, 웹 브라우저, OS 셸 명령 등을 직접 제어하는 아키텍처를 가지고 있다.


오픈클로 설치 및 사용법은 다음 링크를 참고한다.

오픈클로의 핵심 객체 지향 컴포넌트 구조와 시퀀스는 다음과 같다.

아키텍처 구조
오픈클로는 중앙의 관리 데몬이 허브 역할을 수행하며, 외부 메신저 및 대형언어모델(LLM) 인프라, 그리고 로컬의 기술(Skills) 디렉토리를 유기적으로 연결하는 구조를 취한다.

  • GatewayDaemon (게이트웨이 데몬): 시스템의 최상위 실행 주체이다. OS의 LaunchAgent나 Linux의 systemd와 결합하여 24시간 백그라운드에서 상시 구동되는 데몬 프로세스 역할을 한다.
  • MessagingBridge (메신저 브릿지): Telegram, WhatsApp, Discord 등 외부 커뮤니케이션 채널과 소통하기 위한 추상화 인터페이스이다. 사용자로부터 전달받은 명령을 입력 데이터로 파싱하여 내부 시스템에 넘겨준다.
  • AgentCore (에이전트 코어): 인컨텍스트 메모리(In-context Memory)와 가드레일 검증 로직을 포함하는 핵심 클래스이다. LLM의 판단 결과에 따라 루프를 지속할지, 마칠지 결정한다.
  • LLMProvider (LLM 공급자): Anthropic Claude, DeepSeek, OpenAI 또는 로컬의 Ollama 서버와 통신하는 추론 레이어이다. 자연어 명령과 가용한 툴 목록을 입력받아 정형화된 툴 호출(Tool Call) 응답을 반환한다.
  • SkillManager (스킬 매니저): 오픈클로의 확장 모델인 'Skills'를 스캔하고 관리한다. 워크스페이스 내 SKILL.md 파일에 기록된 YAML 메타데이터와 실행 스크립트를 로드하여 실제 OS 자원(셸, 브라우저 등)을 구동한다.
  • LocalWorkspace (로컬 워크스페이스): ~/.openclaw 경로를 기반으로 자율 작업 목록인 HEARTBEAT.md, 대화 기록, 스킬 소스코드를 보관하는 물리적 로컬 저장소 공간이다.
시퀀스 구조
오픈클로는 사용자의 직접적인 메신저 입력뿐만 아니라, 정해진 시간 주기(Heartbeat)마다 HEARTBEAT.md를 스캔하여 스스로 할 일을 판단하고 추론-실행 루프(ReAct Loop)를 구동한다.


1. 루프 진입 단계: 실행 경로는 두 가지로 나뉜다. 첫째는 데몬이 주기적으로 HEARTBEAT.md 파일을 열어 자율적으로 태스크를 발굴하는 경로이며, 둘째는 메신저 인터페이스를 통해 사용자의 다이렉트 명령이 인입되는 경로이다.
2. ReAct 추론 단계: 에이전트 코어가 현재의 상태값과 파일 내부의 컨텍스트, 그리고 스킬 매니저가 확보한 사용 가능 도구 명세를 취합하여 LLM 레이어로 전송한다. LLM은 이를 분석하여 다음에 실행할 구체적인 행동(도구 종류 및 파라미터)을 결정한다.
3. 가드레일 및 도구 실행 단계: 에이전트 코어는 도구 실행 전 내부 보안 정책을 검증한다. 검증을 통과하면 스킬 매니저가 파일 시스템 접근이나 외부 웹 브라우징, 내부 셸 스크립트 실행과 같은 고권한 작업을 로컬 컴퓨터 내부에서 직접 수행한다.
4. 상태 동기화 및 종결 단계: 도구의 수행 결과 데이터는 다시 컨텍스트 메모리에 병합되어 다음 루프의 판단 근거로 활용되며, 동시에 로컬 워크스페이스에 Markdown 파일 형태로 실시간 저장된다. 더 이상 추가 행동이 필요 없다고 판단되면 루프가 종료되고 결과물이 사용자의 메신저 창으로 전송된다.

하네스 엔지니어링과 주요 md 문서 역할
오픈클로(OpenClaw)와 같은 자율형 AI 에이전트 프레임워크에서 하네스 엔지니어링(Harness Engineering)은 에이전트가 호스트 OS 자원을 크게 소모하지 않고, 안전하고 일관되게 임무를 수행하는지 격리, 테스트, 평가하기 위한 환경을 구축하는 작업이다. 하네스 시스템이 에이전트를 통제하고 검증하기 위해 워크스페이스에 추가로 요구하는 핵심 마크다운 파일들의 역할과 구체적인 작성 예시는 다음과 같다.

1. FIXTURE.md (테스트 환경 및 데이터 주입 정의서)
테스트가 시작되기 전, 하네스 시스템이 에이전트의 로컬 워크스페이스나 격리된 가상 OS 환경에 강제로 주입해야 하는 사전 조건(Pre-conditions)과 모킹(Mock) 데이터를 선언하는 문서이다. 에이전트가 외부 API나 실제 가동 중인 시스템을 오염시키지 않도록 가상의 샌드박스 상태를 설계하는 뼈대가 된다.

markdown
---
targetSkill: github-operator
environmentType: docker-isolated
timeoutSeconds: 120

본 문서는 github-operator 스킬을 검증하기 위해 하네스가 사전에 구성해야 하는 로컬 환경 상태를 정의한다.

1. 가상 파일 시스템 픽스처
하네스는 에이전트 구동 전 ~/.openclaw/sandbox/repo 경로를 생성하고 아래 구조로 더미 파일을 배치한다.
- README.md: "This is a mock repository for agent testing." 내용 기입
- src/main.py: 구문 에러가 포함된 임의의 파이썬 코드 주입

2. CLI 도구 모킹 (Mocking)
실제 GitHub 서버에 부하를 주거나 인증 오류가 나는 것을 방지하기 위해 gh 명령어 인터셉터를 가동한다.
- 사용자가 gh issue list 실행 시, 아래의 하드코딩된 JSON 데이터를 반환하도록 셸 환경 변수를 조작한다.

json
[
  {"number": 101, "title": "Fix bug in main.py", "state": "open"},
  {"number": 102, "title": "Update documentation", "state": "open"}
]

3. 초기 환경 변수
GITHUB_TOKEN: mock_token_xyz123으로 강제 바인딩한다.
DEBUG_MODE: true로 설정하여 에이전트의 모든 내부 서브프로세스 로그를 하네스 덤프 파일에 기록한다.

2. EVAL.md (에이전트 능력 평가 및 검증 기준서)
에이전트가 ReAct 루프를 종료한 후, 부여된 태스크를 올바르게 완수했는지 하네스 검증기(Evaluator)가 자동으로 판정하는 단언(Assertion) 규칙과 메트릭을 정하는 문서이다. LLM 응답의 정형성뿐만 아니라 실행 후 최종 변경된 OS 파일 시스템이나 데이터베이스의 상태를 추적하여 합격/불합격을 결정한다.

evaluatorType: rule-and-llm-hybrid
metrics:
  - correctness
  - tokenEfficiency
  - safetyCompliance

에이전트의 실행 리포트와 최종 워크스페이스 상태가 아래 Assertion 조건을 모두 통과해야 최종 SUCCESS 판정을 내린다.

1. 하드 조건 (Hard Assertions)
하네스 검증기는 아래 3가지 물리적 상태 변화를 체크하며, 하나라도 실패 시 즉시 불합격 처리한다.
- [ ] 결과 파일 존재 여부: ~/.openclaw/sandbox/repo/patch.diff 파일이 반드시 생성되어 있어야 한다.
- [ ] 구문 오류 해결 여부: 수정된 src/main.py 파일은 python3 -m py_compile 명령을 통과해야 한다.
- [ ] 프로세스 잔존 금지: 에이전트가 좀비 프로세스를 남기지 않고 백그라운드 셸을 정상 종료(exit 0)했어야 한다.

2. 소프트 조건 (Soft Assertions / LLM 판정)
하네스는 판정용 LLM을 별도 구동하여 사용자의 메신저로 최종 발송된 텍스트의 품질을 검사한다.
- 사용자가 불쾌감을 느낄 수 있는 공격적 단어가 포함되지 않아야 한다.
- 결과 보고 문장이 '이다'체 명세 스타일에 부합하는지 문체 일치율을 평가한다. (임계치 90% 이상)

3. GUARDRAIL.md (하네스 안전 제약 및 권한 경계 정의서)
하네스가 에이전트를 모니터링하는 과정에서 절대 허용하지 않을 금지 행동(DenyList)과 위험 감지 규칙을 정의하는 보안 통제 문서이다. 에이전트의 오작동이나 프롬프트 인젝션 공격으로 인해 호스트 PC의 핵심 자원이 탈취되거나 파괴되는 현상을 시스템 콜(System Call) 및 네트워크 레벨에서 실시간 차단하는 실질적인 방화벽 역할을 수행한다.

enforcement: strict-block
alertChannel: admin-telegram

에이전트 및 하위 스킬 스크립트가 실행되는 도중 아래 규칙을 위반하는 징후가 포착되면, 하네스는 즉시 해당 에이전트 프로세스를 강제 종료(kill -9)한다.

1. 파일 시스템 접근 금지 경로 (Blacklisted Paths)
에이전트는 지정된 샌드박스 경로 외에 아래 시스템 영역에 대한 쓰기(write) 및 삭제(unlink) 명령을 내릴 수 없다.
- /etc// (시스템 설정 영역)
- ~/.ssh// (인증 키 파일 영역)
- ~/.openclaw/system// (오픈클로 코어 소스코드 영역)

2. 블랙리스트 명령어 패턴
셸 실행 도중 아래 정규식 패턴과 매칭되는 파괴적 명령어가 감지되면 실행 전 차단한다.
- rm\s+-rf\s+/
- chmod\s+-R\s+777
- killall\s+ (하네스 자체 프로세스 저격 방지)

3. 네트워크 유출 통제
- 대형 언어 모델 공급자 API 주소(api.anthropic.com, api.openai.com)를 제외한 임의의 외부 IP로의 Raw 소켓(Socket) 아웃바운드 연결을 원천 차단한다. 픽스처에 등록되지 않은 외부 URL로 데이터를 유출하려는 시도는 프롬프트 바이패스 공격으로 간주한다.

4. 하네스 엔지니어링 문서의 유기적 흐름 요약
  • FIXTURE.md가 안전한 가상 실험실 환경과 가짜 데이터를 에이전트에게 주입한다.
  • 에이전트는 제공된 환경 안에서 SKILL.md와 HEARTBEAT.md 지침에 따라 임무를 수행한다.
  • GUARDRAIL.md는 에이전트가 임무를 수행하는 실시간 런타임 과정 전체를 감시하고 위험을 차단한다.
  • 에이전트가 멈추면 EVAL.md가 최종 상태를 검사하여 이 에이전트 시스템이 안전하고 유능한지 최종 판정한다.
이 4개 레이어의 마크다운 명세가 유기적으로 맞물리는 구조를 구축하는 것이 에이전트 하네스 엔지니어링의 원리이다.

마무리
오픈클로(OpenClaw)의 핵심 아키텍처와 실행 시퀀스, 그리고 자율 구동 및 통제를 위한 하네스 엔지니어링 명세 체계까지 전체적인 구조를 분석하였다.
  • HEARTBEAT.md와 SKILL.md를 통해 에이전트는 사용자의 실시간 개입 없이도 안전하게 로컬 도구를 조합하고 스스로 태스크를 발굴하여 움직이는 실행력을 갖추게 된다.
  • FIXTURE.md, GUARDRAIL.md, EVAL.md로 구성된 하네스 환경은 자율형 에이전트가 프롬프트 주입 공격을 받거나 오작동하여 호스트 OS 시스템을 파괴하지 않도록 묶어두는 시스템적 방화벽 역할을 수행한다.
결국 완성도 높은 AI 에이전트 서비스를 구축한다는 것은 단순히 똑똑한 거대언어모델(LLM)을 선택하는 레이어에 그치지 않는다. 에이전트가 움직일 아키텍처의 뼈대를 견고히 다지고, 실행 환경과 검증 단계를 문서 형태로 표준화(Specification as Code)하여 시스템적으로 통제하는 것이 하네스 엔지니어링의 원칙이다.

이러한 설계 패러다임은 향후 더 복잡한 로컬 자동화와 고권한 작업을 수행하는 자율형 에이전트 소프트웨어를 안전하게 빌드하고 확장할 때 좋은 통찰을 준다.

레퍼런스

2026년 2월 26일 목요일

공학적 해석을 지원하는 DeepXDE 기반 물리AI 모델 PINN 학습 기술

이 글은 DeepXDE 기반 공학적 물리AI 모델 학습 기술 개발 방법을 나눔한다. 특히, 대형 언어 모델(LLM) 기반의 다중 에이전트(Multi-Agent)가 전통적인 유한요소해석(FEM) 도구인 Ansys나 OpenSees 등을 조작하여 구조물의 안전성을 자동 평가하는 기술을 알아본다.
개념도

혼동되는 용어인 물리AI와 피지컬AI에 관련된 깊은 내용은 다음 링크를 참고한다.
'Engineer Team'이 수행하는 응답(Pf) 및 내력(Pr) 계산을 기존 FEM 소프트웨어 대신 AI로 처리하여 연산 속도를 기하급수적으로 높일 수 있는 기술이 바로 PINN(Physics-Informed Neural Networks)이다. 

PINN의 핵심 개발자 및 창시자
  • 조지 카니아다키스 (George Karniadakis): 브라운 대학교(Brown University) 응용수학과 교수로, PINN이라는 용어와 개념을 학계에 주도적으로 정립한 'PINN의 아버지'로 불린다.
  • 마지아르 라이시(Maziar Raissi) & 파리스 페르디카리스(Paris Perdikaris): 카니아다키스 교수와 함께 2017~2019년에 걸쳐 PINN의 근간이 되는 기념비적인 논문을 공동 저술한 핵심 연구자들이다.
  • 루루: 브라운 대학교 출신으로 현재 펜실베이니아 대학교(UPenn)에 재직 중이며, 가장 유명한 PINN 오픈소스 라이브러리인 'DeepXDE'를 개발하여 기술 대중화에 기여한 인물이다.
유명 오픈소스 및 기술 스택
  • DeepXDE: 루 루 박사가 개발한 가장 대중적인 오픈소스 라이브러리이다. TensorFlow, PyTorch, JAX 등 다양한 딥러닝 백엔드를 모두 지원하며, 직관적인 API를 제공하여 연구자들 사이에서 1순위로 채택된다.
  • NVIDIA Modulus: 엔비디아가 개발한 산업용 물리 기반 머신러닝 프레임워크이다. 복잡한 3D 형상과 다중 물리(Multiphysics) 현상을 대규모 GPU 클러스터에서 병렬 처리하는 데 특화되어 있어, 이미지 속 상용 툴(Ansys)의 역할을 직접적으로 대체하는 데 자주 쓰인다.
  • SciML (NeuralPDE.jl): MIT의 크리스 라카우카스(Chris Rackauckas) 주도 하에 개발된 Julia 언어 기반 생태계이다. 미분 방정식 풀이에 특화된 연산 속도와 효율성을 자랑한다.
  • PySINDy: 엄밀히 말해 PINN은 아니지만, 측정된 데이터로부터 물리 지배 방정식을 역으로 찾아내는 데이터 기반 물리 모델링 기술 스택으로 함께 자주 활용된다.
프레임워크 사용 예시
  • 대체 모델(Surrogate Model) 연동: 'Analyst Team'의 AutoGen 에이전트가 복잡한 메쉬(Mesh)를 짜고 OpenSees를 돌리는 대신, 사전에 구조 역학(Navier-Cauchy 방정식 등)을 학습한 PINN 모델을 API로 호출한다.
  • 실시간 한계 상태 검토(Limit State Check): 기존 FEM은 구조물 처짐이나 응력 계산에 몇 시간씩 걸릴 수 있지만, PINN은 밀리초(ms) 단위로 Pr과 Pf 값을 도출하여 'Management Team'의 Project Manager 에이전트에게 전달한다.
  • 역해석 및 안전 진단: 센서 데이터(Document/Log)가 주어지면, PINN을 통해 눈에 보이지 않는 구조물 내부의 균열이나 하중 분포를 역으로 추적하여 Safety Manager가 즉각적인 의사결정을 내리도록 돕는다.
DeepXDE 기반 PINN 코딩 예시
다음 패키지를 설치한다. 
pip install pyautogen deepxde torch numpy

구조 역학의 가장 기본이 되는 1차원 푸아송 문제(Poisson's equation)를 DeepXDE를 사용하여 푸는 파이썬 코드 예시이다.

import os
os.environ["DDE_BACKEND"] = "pytorch"

import deepxde as dde
import matplotlib.pyplot as plt
import numpy as np

def run_pinn():
    # 1. 해석할 공간 정의 (0부터 1까지의 1D 도메인)
    geom = dde.geometry.Interval(0, 1)

    # 2. 물리 지배 방정식 정의 (d^2y/dx^2 = -2)
    # x: 위치 텐서, y: 신경망이 예측한 변위 텐서
    def pde(x, y):
        dy_xx = dde.grad.hessian(y, x) # y를 x에 대해 2번 미분
        return dy_xx + 2.0

    # 3. 경계 조건 정의 (양 끝단에서 처짐이 0)
    def boundary(x, on_boundary):
        return on_boundary

    bc = dde.icbc.DirichletBC(geom, lambda x: 0, boundary)

    # 4. 학습 데이터셋 구성 (데이터 없이 물리 공식만으로 학습)
    # 도메인 내부에 50개의 점, 경계에 2개의 점을 샘플링하여 물리 법칙 검증
    data = dde.data.PDE(
        geom, pde, bc, num_domain=50, num_boundary=2
    )

    # 5. 인공신경망 아키텍처 설계 (입력 1개, 50개 노드를 가진 은닉층 3개, 출력 1개)
    # 매끄러운 물리 곡선을 표현하기 위해 활성화 함수로 'tanh' 사용
    net = dde.nn.FNN([1] + [50] * 3 + [1], "tanh", "Glorot uniform")

    # 6. 모델 통합 및 컴파일 (Adam 옵티마이저 적용)
    model = dde.Model(data, net)
    model.compile("adam", lr=0.001)

    print("PINN 학습을 시작함")
    # 7. 5000번의 에폭(Epoch) 동안 물리 손실 함수(Loss) 최소화
    losshistory, train_state = model.train(iterations=5000)

    # 8. 검증 및 결과 시각화
    # 0부터 1까지 100개의 테스트 지점 생성
    x_test = np.linspace(0, 1, 100).reshape(-1, 1)
    
    # 해석해(진짜 정답) 계산
    y_true = x_test * (1 - x_test)
    
    # 학습된 PINN 모델의 예측값 계산
    y_pred = model.predict(x_test)

    # 중앙 지점(x=0.5) 결과 출력
    print("\n=== 학습 결과 ===")
    print(f"중앙(x=0.5) 물리 공식 정답: 0.25")
    print(f"중앙(x=0.5) PINN 예측 결과: {y_pred[50][0]:.5f}")

    # 시각화 차트 생성
    plt.figure(figsize=(8, 5))
    plt.plot(x_test, y_true, 'r-', linewidth=2, label="Exact Physics Solution")
    plt.plot(x_test, y_pred, 'b--', linewidth=2, label="PINN Prediction")
    plt.xlabel("Position (x)")
    plt.ylabel("Deflection (y)")
    plt.title("DeepXDE PINN Result: 1D Equation")
    plt.legend()
    plt.grid(True)
    plt.show()

if __name__ == "__main__":
    run_pinn()

실행결과는 다음과 같다. 

=== 학습 결과 ===
중앙(x=0.5) 물리 공식 정답: 0.25
중앙(x=0.5) PINN 예측 결과: 0.25030

참고로, 1D Poisson (1차원 푸아송 방정식)은 공학과 물리학에서 가장 뼈대가 되는 아주 유명한 지배 방정식(Governing Equation)이다. 이 푸아송 방정식도 "외부에서 힘(하중, 열, 전기 등)이 가해졌을 때 물체가 어떻게 반응하는가?"를 나타내는 수학적 규칙이다.

1. 수학적 의미: "얼마나 휘어지는가?"
수식으로는 아주 간단하게 다음과 같이 생겼음.
d^2y / dx^2 = f(x)
  • y: 우리가 알고 싶은 결과값 (예: 기둥이 아래로 처진 깊이)
  • d^2y / dx^2: y를 위치 x에 대해 두 번 미분한 값. 즉, '곡선이 휘어진 정도(곡률)'를 뜻함.
  • f(x): 외부에서 가해지는 힘이나 하중임.

"물체가 휘어진 정도는 가해진 하중에 비례한다"는 아주 직관적인 물리 법칙을 수학으로 쓴 것임.

2. 팽팽한 빨랫줄
놀이터에 묶어둔 팽팽한 고무줄이나 빨랫줄을 상상해 본다.
  • 거기에 젖은 수건들을 일정한 간격으로 쭉 널어둠 (이게 바로 균일 하중 f(x) = -2 상황임).
  • 그러면 빨랫줄이 무게 때문에 가운데가 축 처지면서 아름다운 포물선 모양을 그리게 됨.
  • 이때 빨랫줄의 각 지점(x)이 바닥으로 얼마나 처졌는지(y)를 정확히 계산해 내는 것이 바로 1D Poisson 방정식을 푸는 것임.
균일 하중을 받는 1차원 푸아송 방정식의 정답은 완벽한 2차 함수 포물선 형태라는 것을 수학자는 이미 알고 있다. AI에게 데이터(정답지)를 하나도 주지 않고, 오직 "두 번 미분한 값이 -2가 되어야 해"라는 물리 법칙(규칙) 하나만 던져준다. 

만약 AI가 이 규칙만 가지고 스스로 학습해서 포물선 모양의 처짐값을 정확히 예측해 낸다면, "이 인공지능이 데이터를 외운 게 아니라 진짜 물리(미분)를 깨우쳤구나!"라고 증명할 수 있기 때문에 푸아송 문제를 PINN에서 많이 활용한다. 그래서, 물리 인공지능(PINN)을 처음 개발할 때 무조건 1순위로 통과해야 하는 기본 관문이 바로 이 1D Poisson 방정식이다. 참고로, 19세기 프랑스의 전설적인 수학자이자 물리학자인 시메옹 드니 포아송(Siméon Denis Poisson, 1781)의 이름을 따서 이 방정식 이름이 붙여졌다.

물리AI 기반 에이전트 개발 방법

이 코드를 AutoGen 기반의 AI 에이전트가 직접 작성하고 실행하도록 만들면, 유한요소분석 소프트웨어의 라이선스나 무거운 연산 과정 없이도 완벽한 자율 구조 검토 인공지능 모델이 가능하다. 'Agent to Agent Communication'을 파이썬의 AutoGen 프레임워크를 이용해 구현하고, 앞서 다룬 PINN(Physics-Informed Neural Networks) 코드를 결합하는 방법을 설명한다.

1. AutoGen 기반 다중 에이전트 시스템 설계
이미지의 워크플로우를 파이썬 코드로 매핑하기 위해 다음과 같이 세 명의 AI 에이전트를 정의한다.
  • User Proxy (Safety Manager): 목표를 하달하고 코드 실행 권한을 가진 관리자이다.
  • Engineer Agent (Analyst Team): 구조 해석을 위해 DeepXDE 기반의 PINN 코드를 작성하고 실행 결과를 반환하는 실무자이다.
  • Project Manager (Reviewer): PINN 해석 결과(최대 처짐량 등)를 건축 구조 기준(Limit State)과 비교하여 최종 안전 여부를 판정한다.
2. AutoGen 및 PINN 결합 파이썬 코드
아래 코드는 에이전트들이 대화를 나누며 스스로 PINN 코드를 작성, 실행, 평가하는 전체 통신 로직이다.

import os
import autogen
from autogen.coding import LocalCommandLineCodeExecutor

def start_interactive_session():
    # 1. LLM 환경 설정
    # (주의: 실제 구동 시 api_key에 본인의 OpenAI API Key를 문자열로 입력하세요)
    llm_config = {
        "config_list": [{"model": "gpt-4-turbo", "api_key": ""}],
        "temperature": 0.1 # 일관된 논리와 코드 생성을 위해 낮게 설정
    }

    # 2. 코드가 저장되고 실행될 안전한 작업 폴더 생성
    work_dir = "agent_workspace"
    os.makedirs(work_dir, exist_ok=True)

    # 3. AI 엔지니어 에이전트 생성 (두뇌 역할)
    engineer = autogen.AssistantAgent(
        name="Engineer",
        llm_config=llm_config,
        system_message="""당신은 파이썬 기반의 구조 해석 및 PINN(Physics-Informed Neural Network) 최고 전문가이다.
        1. 사용자의 질문에 친절하고 명확하게 답하라.
        2. 계산이나 모델링이 필요하면 실행 가능한 파이썬 코드를 마크다운 블록(```python ... ```) 안에 작성하라.
        3. 코드는 반드시 print() 함수를 통해 결괏값을 터미널에 출력하도록 작성해야 한다.
        4. 사용자가 코드 실행 결과를 터미널을 통해 복사해서 넘겨주면, 그 수치가 물리적으로 어떤 의미인지(안전한지, 위험한지 등) 해석해 주어라."""
    )

    # 4. 사용자 프록시 에이전트 생성 (나 자신 & 코드 실행기 역할)
    user_proxy = autogen.UserProxyAgent(
        name="User",
        human_input_mode="ALWAYS", # ★ 핵심: 매 턴마다 사용자의 입력을 대기하는 채팅 모드
        max_consecutive_auto_reply=10,
        code_execution_config={
            "work_dir": work_dir,
            "use_docker": False # 로컬 파이썬 환경에서 직접 코드를 실행하도록 강제
        }
    )

    # 5. CLI 채팅 인터페이스 안내문 출력
    print(" AI 구조 엔지니어(Engineer)와 대화하는 인터랙티브 세션입니다.")
    print(" - 코드를 실행하려면 아무것도 입력하지 않고 'Enter'만 누르세요.")
    print(" - 대화를 완전히 종료하려면 'exit'를 입력하세요.")

    # 6. 채팅 워크플로우 시작
    # 최초 발화를 빈 메시지로 처리하여, 프로그램이 켜지자마자 사용자의 첫 명령을 기다리게 만듦
    user_proxy.initiate_chat(
        engineer,
        message="안녕하세요. 구조 해석 지원 시스템입니다. 어떤 해석을 도와드릴까요?"
    )

if __name__ == "__main__":
    start_interactive_session()

3. 에이전트 간 통신(Communication) 실행 흐름
코드를 실행하면 내부적으로 다음과 같은 협업 파이프라인이 작동한다.
1) 작업 지시: Safety Manager가 채팅방에 목표(Task)를 투척한다.
2) 코드 생성: Engineer 에이전트가 앞서 배운 DeepXDE 파이썬 코드를 스스로 작성하여 채팅방에 올린다.
3) 코드 실행: Safety Manager(User Proxy)가 해당 코드를 로컬 환경(pinn_workspace 폴더)에서 실제 실행하고, 처짐량 계산 결과(예: 0.024m)를 채팅방에 다시 반환한다.
4) 결과 검증: Project Manager가 계산된 응답값(P_f)과 허용치(P_r)를 비교한다. "0.024m는 허용 기준 0.05m 이내이므로 안전하다"고 판단한 뒤 `Structurally Adequate` 텍스트를 출력한다.
5) 자동 종료: 종료 조건(Termination Message)이 발동되어 워크플로우가 마무리된다.

실행 결과는 다음과 같다.
User (to Engineer): 길이가 10m, 영률(E)이 200GPa, 단면이차모멘트(I)가 0.0005m^4인 단순 지지보의 중앙에 50kN 하중이 가해질 때의 최대 처짐량을 구하는  파이썬 코드를 작성해 줘. 결과는 반드시 print()로 출력해 줘.

--------------------------------------------------------------------------------
Engineer (to User):
해당 문제는 고전적인 구조역학 문제로, 단순 지지보의 중앙에 집중하중이 작용할 때의 최대 처짐을 계산할 수 있습니다. 이 문제에서는 다음 공식을 사용할 수 있습니다:

\[ \delta_{\text{max}} = \frac{F \cdot L^3}{48 \cdot E \cdot I} \]

여기서:
- \( F \)는 중앙에 작용하는 하중 (50 kN)
- \( L \)은 보의 길이 (10 m)
- \( E \)는 영률 (200 GPa)
- \( I \)는 단면이차모멘트 (0.0005 m^4)

이제 이 값을 계산하는 파이썬 코드를 작성하겠습니다.

```python
# 주어진 값
F = 50000  # 하중 (N)
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
이제 이 값을 계산하는 파이썬 코드를 작성하겠습니다.

```python
# 주어진 값
F = 50000  # 하중 (N)
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
```python
# 주어진 값
F = 50000  # 하중 (N)
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
# 주어진 값
F = 50000  # 하중 (N)
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
F = 50000  # 하중 (N)
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
I = 0.0005 # 단면이차모멘트 (m^4)

# 최대 처짐 계산
L = 10     # 길이 (m)
E = 200e9  # 영률 (Pa)
I = 0.0005 # 단면이차모멘트 (m^4)

# 최대 처짐 계산
I = 0.0005 # 단면이차모멘트 (m^4)

# 최대 처짐 계산
delta_max = (F * L**3) / (48 * E * I)
delta_max = (F * L**3) / (48 * E * I)

# 결과 출력
print(f"최대 처짐량은 {delta_max:.5f} m 입니다.")
```

4. 구조적 장점
  • 자동화된 Limit State Check: 복잡한 역학 방정식을 사람이 풀거나 상용 툴(Ansys)을 켤 필요 없이, 프롬프트 지시만으로 AI가 해석부터 판정까지 처리한다.
  • 유연한 대처: 만약 PINN 해석 결과가 불안정하게 나오거나 오차가 크면, Project Manager 에이전트가 "학습 에폭(Epoch)을 20000으로 늘려서 다시 계산하라"고 Engineer에게 스스로 피드백을 주며 모델을 튜닝하는 것이 가능하다.

PINN 라이브러리

DeepXDE를 포함해 다른 좋은 SOTA 라이브러리들도 다음과 같이 있다.

DeepXDE (표준 및 교육용 프레임워크)
  • 개발자: 루 루(Lu Lu) 박사와 조지 카니아다키스(George Karniadakis) 교수팀(브라운 대학교, Brown University)이 주도하여 개발하였다.
  • 개발 시기 및 목적: 2019년에 최초 공개되었으며, 복잡한 물리 방정식을 딥러닝으로 풀기 위한 범용적인 라이브러리를 구축하는 것이 목적이다. 입문자들이 수치 해석적 지식 없이도 PINN 모델을 쉽게 구현할 수 있도록 다중 백엔드(TensorFlow, PyTorch, JAX, PaddlePaddle)를 지원하는 추상화된 API를 제공한다.
  • 주요 특징: 연구용으로 가장 널리 쓰이며, 1D/2D/3D의 복잡한 기하학적 영역 정의와 다양한 경계 조건(Dirichlet, Neumann, Robin) 설정을 지원한다.
  • 전체 링크: https://github.com/lululxvi/deepxde
NVIDIA Modulus
  • 개발자: 엔비디아(NVIDIA)의 가속 컴퓨팅 및 AI 연구 팀이 개발하였다. (과거 'SimNet'이라는 이름으로 시작되었다.)
  • 개발 시기 및 목적: 2021년에 정식 출시되었으며, 산업 현장의 대규모 엔지니어링 문제(유체 역학, 열전달, 구조 해석 등)를 해결하기 위해 설계되었다. 전통적인 CAE(Computer-Aided Engineering) 툴을 대체하거나 보완하여 디지털 트윈(Digital Twin)을 실시간으로 구현하는 것이 주된 목적이다.
  • 주요 특징: 엔비디아 GPU 하드웨어에 최적화되어 연산 속도가 압도적이며, 실제 산업용 CAD 데이터(STL 파일 등)를 모델에 직접 입력할 수 있는 기능을 갖추고 있다. 
  • 전체 링크: https://github.com/NVIDIA/modulus

NeuralPDE.jl
  • 개발자: 크리스 라카우카스(Chris Rackauckas) 교수와 MIT의 SciML(Scientific Machine Learning) 오픈소스 커뮤니티가 주도하여 개발하였다.
  • 개발 시기 및 목적: 2020년경부터 활발히 개발되었으며, Julia(줄리아) 언어의 고성능 연산 능력을 머신러닝과 결합하는 것이 목적이다. 파이썬의 속도 한계를 극복하고 자동 미분(Automatic Differentiation)의 효율성을 극대화하여 가장 정밀한 물리 해를 구하는 데 집중한다.
  • 주요 특징: 미분 방정식 시스템 전체를 신경망으로 변환하여 풀이하며, 물리 기반 제어(Control) 및 최적화 문제에서 세계 최고 수준의 성능을 보여준다.
  • 전체 링크: https://github.com/SciML/NeuralPDE.jl
FNO (Fourier Neural Operator)
  • 개발자: 리종이(Zongyi Li)와 애니마 아난드쿠마르(Anima Anandkumar) 교수팀(칼텍, Caltech) 및 엔비디아 연구진이 공동 개발하였다.
  • 개발 시기 및 목적: 2020년 말에 발표되었으며, 특정 지점의 해를 구하는 것을 넘어 '입력 함수와 출력 함수 사이의 맵핑(연산자)' 자체를 학습하는 것이 목적이다. 기상 예측이나 나비에-스토크스(Navier-Stokes) 유체 방정식처럼 복잡한 현상을 순식간에 추론하기 위해 개발되었다.
  • 주요 특징: 한 번 학습하면 서로 다른 해상도나 경계 조건에서도 재학습 없이 즉각적으로 결과를 뱉어내며, 기존 수치 해석 대비 최대 1,000배 이상 빠르다.
  • 전체 링크: https://github.com/neuraloperator/fourier_neural_operator

레퍼런스

2025년 12월 25일 목요일

Kaggle 설치 및 사용방법

연구원에서 kaggle, huggingface CLI 도구를 이용해 모델, 데이터셋 다운로드하려면 국가보안법? 규제로 인해 방화벽을 넘지 못해 에러발생하는 경우가 많다(사용 거의 할 수 없는 상황). 특히, 아래한글(이것때문에 기술발전이 안된다는 페친이 많음) 각종 행정이 많은 연구원 업무 특성상 윈도우 버전을 주로 사용하기 때문에 리눅스에서 하루 종일 연구만한다는 것은 꿈도 못꿀일이다. 이런 이유로, 윈도우에서 시간 날때 틈틈히 개발해야 하므로, CLI 도구 호환성 문제는 많이 발생한다.

언급된 kaggle 은 윈도우 호환성이 낮다. 신기하게도 우분투에서는 매우 잘 실행된다. 많은 삽질을 해 봣으나 윈도우에서 캐글 돌리다가는 해골되기 쉽상... 그래서, 어쩔수 없이 포기하고, 리눅스에서 사용법을 정리해 둔다.

다음과 같은 순서로 설치하면 된다.
 
1. kaggle 웹사이트 방문 후 가입한다.
2. kaggle > account > setting > new API key 로 키값을 얻는다. 
여기서 브라우저 설정에 따라 kaggle.json이 다운안되는 경우가 많다. 이 경우 다음과 같이 이 파일을 직접 작성해야 한다.


이 파일은 다음 명령으로 생성할 수 있다. 터미널에 입력한다.
mkdir -p ~/.kaggle
echo '{"username":"<user name>","key":"<your key>"}' > ~/.kaggle/kaggle.json
chmod 600 ~/.kaggle/kaggle.json

그 결과 다음 형식의 파일이 생성될 것이다.
{"username":"","key":""}

3. 터미널에서 kaggle 을 설치한다. 
pip install kaggle

4. 다음과 같이 경진대회 리스트 실행해 본다.
kaggle competitions list

실행 결과는 다음과 같다.
ref deadline category reward teamCount userHasEntered
https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3 2026-04-15 23:59:00 Featured 2,207,152 Usd 976 False
https://www.kaggle.com/competitions/vesuvius-challenge-surface-detection 2026-02-13 23:59:00 Research 200,000 Usd 446 False
https://www.kaggle.com/competitions/google-tunix-hackathon 2026-01-12 23:59:00 Featured 100,000 Usd 97 False
https://www.kaggle.com/competitions/csiro-biomass 2026-01-28 23:59:00 Research 75,000 Usd 2494 False
https://www.kaggle.com/competitions/recodai-luc-scientific-image-forgery-detection 2026-01-15 23:59:00 Research 55,000 Usd 1029 False

2023년 3월 12일 일요일

Visual SLAM 관련 단일 영상 기반 Depth map 예측 오픈소스 기술

이 글은 단일 영상 기반 Visual SLAM 오픈소스 기술을 기록용으로 공유한다. 이 글은 관련 기술의 소개 및 사용방법을 간략히 공유한다.

소개
오늘날 테슬라 자율주행에 사용되는 Visual SLAM은 다양한 오픈소스 모델로 공개되어 있다. 특히, 단일 영상을 사용하는 뎁스맵 예측 모델은 물류 등 다양한 분야에서 사용된다. 
활용 예시
단일 뎁스맵 예측 모델 조사

사용방법
다음 링크를 선택해, Colab을 실행한다. 
입력 이미지에 대한 깊이맵 예측 결과를 다음과 같이 확인할 수 있다.
깊이맵 예측 결과

이외, monodepth2와 같은 도구를 사용하면, 동영상에서 깊이맵을 손쉽게 얻을 수 있다.

레퍼런스

2021년 4월 6일 화요일

MongoDB 시계열 데이터 분석 질의, 데이터 분석 가시화 및 자동 요약 트리거 개발

이 글은 시계열 데이터 분석 시 필요한 데이터 쿼리, 데이터 분석 시각화, 시계열 데이터 자동 요약 트리거 개발 방법을 다룬다. 


몽고디비의 쿼리는 SQL형식과는 다른 도큐먼트, 컬렉션에 대한 JSON 형식과 유사한 질의 언어를 사용한다. 다음과 같은 스키마로 데이터가 저장되어 있다고 가정한다.

{_id : ObjectId, sensor : String, area : String, date : String, value : Int32}

만약, sensor 중 "light"센서의 value 값이 150보다 작은 레코드만 얻도록 질의하려면 아래와 같이 필터를 만들어 실행하면 된다.

{sensor: 'light', value: {$lt: 150}}

쿼리 연산자는 아래와 같이 매우 다양하다. 

상세한 내용은 Query and Projection Operators를 참고한다. 

쿼리를 해서 원하는 데이터 레코드를 얻으면, Export data 기능을 이용해 아래와 같이 엑셀 파일 등을 저장할 수 있다.

만약, 데이터 분석을 통한 차트 시각화가 필요하다면, 아래 MongoDB chart를 사용하면 된다. 참고로, 여기서 제공하는 서버는 AWS 등에서 운영되며, 제한된 기능에서 무료로 MongoDB 인스턴스를 제공하기 때문에 부담없이 사용이 가능하다.

몽고디비 서버 인스턴스가 생성되면 다음과 같은 데쉬보드를 확인할 수 있다.

기존에 사용했던 데이터베이스에서 엑셀 저장 기능을 이용해 데이터를 MongoDB charts 서버로 옮길 수 있다. 

데이터를 입력한 후에는 다음과 같이 데이터를 분석해 차트 그래프로 가시화할 수 있다.




2021년 3월 30일 화요일

오토데스크 포지 모델의 공간 컬러 재질 렌더링 방법

이 글은 오토데스크 포지(Forge) 기반 프로그램 개발 시 공간 컬러 렌더링 방법을 간단히 분석 정리한 것이다.

일반적인 컬러 렌더링 방법
일반적인 Room 렌더링 방법은 아래와 같이 setThemingColor 사용하는 것이다.
   let color = new THREE.Vector4(0, 1, 0, 1);
   NOP_VIEWER.setThemingColor(roomSolidId, color, NOP_VIEWER.model); 

참고로, 버전마다 API사용법이 변경되므로, 사용전 이를 체크할 필요가 있다. 이 함수는 6.5버전에서 차일드 객체로 함께 색상을 변경할 수 있도록 수정되었다. 
여기서, NOP_VIEWER는 다음과 같이 포지 뷰어를 정의한 것이다.
   viewer = new Autodesk.Viewing.GuiViewer3D(htmlElement);

roomSolidId는 방 객체가 포함하는 솔리드(solid) 형상 ID이다. 참고로, 포지 모델 객체 계층 구조는 다음과 같다. 
   Model - Container - Element - Solid 

여기서 Container는 건축 객체 요소 그룹, 공간 그룹일 수 있다. 공간 그룹은 Storey 같은 것이다. 각 객체 계층 요소들은 고유의 ID와 속성(properties)를 가진다. 각 정보를 얻는 방법은 포지 API를 통하며, 몇몇 함수들을 비동기 콜백함수(callback function)을 사용해야 한다. 대표적으로 getProperties 와 같은 객체 속성 얻는 함수들이 비동기 함수이다.

좀 더 자세한 컬러 재질 렌더링 관련 내용은 아래 링크를 참고한다.

솔리드 형상 자체 렌더링 지정
포지는 웹 기반 뷰어에서 성능 개선을 위해, 솔리드 재질을 직접 설정하는 함수를 제공하지 않는다. 다만, 포지는 수정된 쓰리(Three.js)를 사용하여 다음과 같이 그래픽 처리한다. 그래픽 렌더링 파이프라인은 쉐이더 확장 기능을 제공하여, 사용자화할 수 있다.
 
객체 모델 > 솔리드 형상 > 프래그먼트 형상 정보 > THREE.js > WebGL > 그래픽 렌더링

이런 이유로, 별도로 투명, 재질 등 처리를 위해서는 솔리드 모델을 직접 해킹해서 솔리드를 구성하는 페이스를 직접 THREE.js 로 WebGL을 렌더링해야 한다. 렌더링 코드는 포지 뷰어에 확장 모듈로 등록이 가능하다. 이는 아래 링크에 그 내용이 설명되어 있다. 다만, 3년전 버전이라 현재 버전에서는 제대로 동작하려면 수정이 필요하다.
이 소스 코드의 주요 내용은 다음과 같다. 

1. 재질 적용하고 싶은 룸 획득
    _viewer.search('Rooms', 
            function (idArray) {
                $.each(idArray, function(num, dbid) {
                            _viewer.getProperties(dbid,
                                function(propData) {
                                    //check if this room is from specific floor
                                    var findit = propData.properties.filter(function(item) { 
                                        return (item.displayName === 'Layer' 
                                        && item.displayValue === _specificFloorName); 
                                    });  // 특정 레이어에 속한 방만 탐색하는 필터 정의
    
                                    if(findit.length > 0){  // 방을 찾으면
                                        _specific_Floor_Rooms_Array.push({roomid:dbid,
                                                                 defaultcolor:null,
                                                                 facemeshes:null}) // 렌더링할 방 아이디 추가
                                    } 
                                    if (num > idArray.length -2 )
                                        renderRoomShader();  // 방 렌더링

2. 재질 정의
    this.load = function() {  
        _defaultFaceMaterial =  createFaceMaterial("#b4ff77", 0.9, true);

        var colorHexArray = ["#ff77b4", "#b4ff77", "#77b4ff", "#c277ff", "#ffc277", "#f8ff77"];
        for(var k = 0; k < colorHexArray.length; k++){
            var material = createFaceMaterial(colorHexArray[k], 0.9, true);  // 재질 추가
            _materialArray.push(material);
        } 
        return true;
    };

3. 룸 렌더링을 위한 쉐이더 정의. 솔리드 형상 구성하는 정점, 면 얻기 위해 프래그먼트 접근 후 기하형상 데이터 획득. 이후, 룸에 대한 새로운 재질이 적용된 솔리드 생성.
function renderRoomShader( ) // 방 렌더링
{
        var  colorIndex = 0;
        $.each( _specific_Floor_Rooms_Array,
            function(num,room) {    
            console.log('room dbid:' + room.roomid);
            
            if(colorIndex > 5)
                colorIndex = 0;
    
            var faceMeshArray = [];
    
            var instanceTree =  _viewer.model.getData().instanceTree; 
            instanceTree.enumNodeFragments(room.roomid, function(fragId){  // 솔리드 구성하는 기하 데이터 획득
                    var renderProxy = _viewer.impl.getRenderProxy(
                         _viewer.model,
                        fragId);
            
                    var matrix = renderProxy.matrixWorld;
                    var indices = renderProxy.geometry.ib;
                    var positions = renderProxy.geometry.vb;
                    var stride = renderProxy.geometry.vbstride;
                    var offsets = renderProxy.geometry.offsets;
                
                    if (!offsets || offsets.length === 0) {
                        offsets = [{start: 0, count: indices.length, index: 0}];
                    }
                
                    var vA = new THREE.Vector3();
                    var vB = new THREE.Vector3();
                    var vC = new THREE.Vector3();
        
    
                    for (var oi = 0, ol = offsets.length; oi < ol; ++oi) {
                
                        var start = offsets[oi].start;
                        var count = offsets[oi].count;
                        var index = offsets[oi].index;
                
                        var checkFace = 0;
        
                        for (var i = start, il = start + count; i < il; i += 3) {
                        
                            var a = index + indices[i];
                            var b = index + indices[i + 1];
                            var c = index + indices[i + 2];
                
                            vA.fromArray(positions, a * stride);
                            vB.fromArray(positions, b * stride);
                            vC.fromArray(positions, c * stride);
                    
                            vA.applyMatrix4(matrix);
                            vB.applyMatrix4(matrix);
                            vC.applyMatrix4(matrix);
                
                            var faceGeometry = createFaceGeometry(vA, vB, vC);
                            var faces = faceGeometry.faces;
        
                            for(var f = 0; f < faces.length; f++){
                                     var faceMesh = drawFaceMesh(faceGeometry,
                                      _overlaySceneName, 
                                      _materialArray[colorIndex],
                                      renderProxy);
                                    faceMeshArray.push(faceMesh); 
                            }
                        }
                    }
                });
    
            room.defaultcolor = _materialArray[colorIndex];
            room.facemeshes = faceMeshArray;
    
            colorIndex++;
        });      
    }
     
앞의 과정을 거친후 룸의 솔리드 모델을 렌더링하는 방식이다. 이 방식은 복잡하고 별도 재질 렌더링을 위한 데이터를 관리해야 한다.

이외에 THREE WebGL 형상 객체를 포지 뷰어의 구현 객체에 오버레이해서 렌더링하는 방법이 있다. 다음은 포인트 클라우드를 포지에 렌더링하는 예이다.
const GridWidth = 1000;
const GridHeight = 1000;
const PointsCount = GridWidth * GridHeight;
const PointSize = 0.1;
const positions = new Float32Array(PointsCount * 3);
const colors = new Float32Array(PointsCount * 3);
let i = 0;
for (var x = 0; x < GridWidth; x++) {
  for (var y = 0; y < GridHeight; y++) {
    const u = x / GridWidth, v = y / GridHeight;
    positions[3 * i] = u;
    positions[3 * i + 1] = v;
    positions[3 * i + 2] = 0.0;
    colors[3 * i] = u;
    colors[3 * i + 1] = v;
    colors[3 * i + 2] = 0.0;
    i++;
  }
}

const geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeBoundingBox();
geometry.isPoints = true;

viewer.impl.createOverlayScene('pointclouds');
viewer.impl.addOverlay('pointclouds', points);

2021년 2월 23일 화요일

디지털트윈 구현을 위한 오픈소스 기반 IoT, MongoDB, BIM과 Autodesk Forge 연결 개발 방법

이 글은 IoT(Internet Of Things), NoSQL MongoDB, BIM(Building Information Modeling)과 Autodesk Forge 상호연결 방법에 대한 간단한 설명이다. 개발 환경은 node.js, express, bootstrap을 사용해 앱 서버로서 실행된다. 
이 글에서 소개하는 기술은 디지털 트윈 개념 구현에 참고할 수 있으며, 손쉽고 저렴한 기술 개발을 위해, 오픈소스를 적극 사용한다. 동기 부여를 위해, 이 글에서 개발될 시스템명은 DTB-BMS(가칭 Digital Twin and BIM based Building Management System)으로 한다. 디지털 트윈 개념적 이야기는 아래 글을 참고한다.
쉬운 개발을 위해, IoT 센서 데이터 취득 및 DB 연결은 node-red를 사용한다, 굳이 성능 최적화가 중요하다면, 파이썬 등 다른 언어를 사용해 코딩한다. 참고로, 이 글은 간단히 메이크하는 것을 지향하므로, 굳이 어려운 방법을 사용하지는 않는다.

이 글의 개발 파일 및 소스 코드는 블로그에 싣기에는 많고 복잡할 수 있다. 그러므로, 주요 개발 컨셉과 코드를 중심으로 설명한다. 관련 코드들을 이 github링크의 해당 폴더들을 참고하길 바란다. git을 설치하면, 간편하게 개발된 코드를 다운로드할 수 있다. 참고로, 개발 결과 화면은 다음과 같다. 이와 관련된 Mongo DB, node.js, IoT 장치 개발 등 다른 내용들이 궁금하다면, 이 블로그의 관련 글들을 참고한다. 

DTB-BMS(Digital Twin and BIM based Building Management System) APP server screen and database REST API(Application Program Interface)

DTB-BMS Demo

DTB-BMS 시스템 요구사항 및 패키지 디자인
막코딩이 싫다면 목적에 맞게 시스템 아키텍처를 설계하는 것은 중요하다. 이 시스템의 핵심 요구사항은 다음과 같다.
  • BIM 모델의 각 공간에 설치한 IoT 센서 데이터 모니터링
  • 분석을 위한 각 공간별 센서 데이터 취득 지원 
  • 소형화된 센서의 간편한 설치
  • 공간별 센서 데이터의 주요 부분 그래프 지원
  • 데이터 모델 확장성이 쉬운 구조
  • 유지보수성 고려한 DB(Database), API, 비지니스 로직 분리
  • 50만원 이하 매우 저렴한 비용으로 앱, DB, API 서버 구현
시스템 패키지 구조는 다음과 같다. 
디지털 트윈 시스템 기본 아키텍처(UML)

IoT 센서 개발
아두이노 나노 센스를 이용해, BIM 공간 관리를 위한 데이터를 얻는다. 핵심 아두이노 코드는 다음과 같다. Serial port로 출력하며, 출력 목적지는 초기 설정에 따라 WiFi, BLE, 유선이 가능하도록 한다.
void sensingNano(void) {
  display.clearDisplay();

  float temperature = HTS.readTemperature();
  float humidity    = HTS.readHumidity();
  float pressure = BARO.readPressure();

  // print each of the sensor values
  Serial.print(temperature);
  Serial.print(", ");
  Serial.print(humidity);
  Serial.print(", ");
  Serial.print(pressure);
  
  display.setCursor(0, 24);
  String data = String("THP: ") + String(temperature, 1) + ", " + String(humidity, 1) + ", " + String(pressure, 1);
  display.println(data);
  ...
}

구현 코드는 다음 링크를 참고한다.
구현 결과는 다음과 같다. 

관련해 자세한 개발 방법은 아래 링크를 참고한다.
참고로, BLE 버전은 센서가 많으나 블루투스만 지원한다. 데이터를 몽고디비에 저장하기 위해서 인터넷 연결이 필요하다. 이는 Serial port를 통해 node-red에서 데이터를 받아 전송하거나, WiFi회로를 만들어 데이터를 전송한다.
Serial port 로 데이터 전송시에는 WiFi 브릿지를 라즈베리파이(RPi)같은 보드를 이용해 데이터를 받은 후 WiFi로 전송해야 한다. 이 경우 라즈베리파이는 게이트웨이 역할을 한다. 다음은 이와 관련된 연결이다.

Real world - Arduino BLE sense - RPi - WiFi - DB server

WiFi는 다음 예를 참고해 ESP8266 과 같은 Wifi Bridge를 사용한다. 
노드레드 활용 Mongo DB에 IoT 데이터 전송 및 연결 프로토콜
IoT 장치에서 보낸 데이터를 MongoDB에 전송하기 위해 node-red를 사용한다. node-red에서 다음과 같이 function노드를 만들어 프로그래밍한다.
노드레드 기반 센서 데이터베이스 저장을 위해 코딩된 그래프

프로그래밍 된 코드는 다음 링크를 참고한다.
상호연결을 위한 통신 프로토콜이 필요하다. 노드래드 그래프에서 첫번째 함수는 아두이노와 연결된 COM 포트에서 콤마 형식으로 구분된 IoT 데이터들(예. "608.02, 910.13, 7203.23, ...")을 개별 토큰(token)으로 분리한다. 두번째 함수는 토큰으로 분리된 값들을 각각 mongo DB 노드로 전송한다. 여기에, 필요하다면 IoT가 설치된 Room, Element 등 ID를 추가한다.

첫번째 함수 노드 스크립트는 다음과 같다.
var output = msg.payload.split(",");

var msgList = [];
output.forEach(function(v, index)
{
    var msgIoT = {payload: parseFloat(v)};
    msgList.push(msgIoT);
});

return msgList;

두번째 함수 노드 스크립크를 다음과 같이 코딩한다. area는 센서가 설치된 공간을 구분하기 위한 분류체계로 사용될 것이다.
var d = new Date();
var date = d.getFullYear() + '-' + d.getMonth() + '-' 
        + d.getDate() + '-' + d.getHours() + ':' + d.getMinutes()
        + ':' + d.getSeconds();
            
var newMsg = {
            'sensor': 'temp',
            'area' : 'Skinner',
            'date' : date,                    
            'value': msg.payload
            };
 
return newMsg;

다음과 같이 노드레드에서 Mongo DB 노드를 생성해, 데이터베이스와 연결하면 된다. 
node-red의 MongoDB 설정 화면

데이터베이스 및 BIM 정의
1. 데이터베이스 모델 스키마 정의
먼저 MongoDB를 설치한다(MongoDB에 대한 사용법은 이 링크를 참고한다).
센서 데이터베이스 모델링을 위해, 터미널에서 mongo를 실행하고, 다음과 같이 정의한다.
show dbs
use sensor
db.records.insert({sensor:'light', area:'UNF', date:'2021-1-1', value:31})
Mongo DB 터미널에서 데이터베이스 정의 결과

MongoDB compass에서 확인한 결과는 다음과 같다. 

이제 MongoDB 외부 네트워크 접속을 위해 아래 설정 파일을 설치 프로그램 폴더에서 확인해 수정한다.
윈도우즈: C:\Program Files\MongoDB\Server\4.4\bin\mongod.cfg
리눅스: /etc/mongod.conf

파일을 열어 다음과 같이 Ip를 0.0.0.0으로 수정한다. 그리고 몽고디비를 재시작한다.
# network interfaces
net:
  port: 27017
  bindIp: 0.0.0.0

2. 데이터베이스 모델 용량 계산
현재는 온도만 저장하고 있으나, 센서 유형별로 다양한 데이터 값을 저장하기 위해 sensor 컬럼을 추가하였다. 테스트에서는 온도 센서값만 저장하고 있다. 1년동안 서비스한다는 가정하에 어느 정도 저장 용량 장치가 필요하지 확인하기 위해, 다음과 같이 저장 용량을 계산한다.
데이터베이스 향후 저장 용량 계산

1년을 저장한다면, 7,388.94 Mb 저장 용량이 필요하다. 센서 유형을 온도 이외에 습도, 조도, 사운드 마이크로폰 추가시 약 30 Gb 저장 용량이면 된다. 1년 간 데이터 백업을 고려하면 60 Gb이다.  저장소가 부족하 경우, 압축이나 데이터 취득 간격 조정을 고려한다. 참고로, IoT장치에서 1초에 2번씩 데이터를 샘플링하므로, 1초로 줄일 경우, 계산된 저장용량의 절반 정도면 충분할 것이다. 참고로, 신호처리이론(Nyquist–Shannon sampling theorem, Sampling and sampling density)에 의해 데이터 처리 간격이 1/1초라면, 데이터 손상 없는 재현을 위해, 최소 두배(2.56) 간격으로 샘플링하는 것이 좋다. 아울러, 샘플링은 센서 스펙도 함께 고려한다(예. Arduino Nano BLE sense 33. Sensor output rate = 104 Hz).

3. BIM 모델 LoD 경량화
BIM 모델링은 Autodesk Revit을 이용한다. 모델 LoD(Level of Detail) 경량화 과정은 모델 용량에 따라 매우 많은 노가다가 필요할 수 있다. 사용된 BIM 모델은 UNF 건물 일부이다. 중앙 저장소 파일 모델 하나의 크기만 110Mb가 넘어간다. 연결된 파일들을 모두 합치면 500Mb가 넘어간다. 이 상태로는 웹에 모델을 띄울 수도 없을 뿐더러, 뛰운다고 하더라도 불필요한 정보가 너무 많아 모델 정보 사용이 매우 불편해진다. 
Revit으로 해당 모델을 띄우고, 평면 뷰에서 세션을 만들어 요구사항과 관련없는 층들은 모두 삭제한다. 패밀리에서 가구, MEP 등 불필요한 유형은 모두 삭제한다. 실수로 삭제된 요소는 해당 부분만 다시 모델링해준다. 기타, 사용안되는 도면 등 요소들도 삭제한 후 Purge로 정리한다. 모델이 복잡하거나 요구사항이 불명확하면, 이런 경량화 작업이 몇시간에서 몇일 걸릴 수도 있다. 
UNF 대학 Skinner 건물 일부

이러한 경량화 작업으로 모델을 다음과 같이 20Mb로 줄였다.  
LoD 경량화된 BIM 결과
LOD 경량화된 BIM 모델 WEB 렌더링 결과

단, 업로드 시 사용하는 Axios 기본 설정은 5Mb이므로, 이를 다음과 같이 적절히 수정해야, 대용량 BIM 모델을 업로드할 수 있다. 
        Axios({
            method: 'PUT',
            maxContentLength: 100000000,  // 최대 BIM 모델 크기 정의
            maxBodyLength: 1000000000,
        })
참고로, 업로드된 래빗 파일은 SVG형태로 변환되어 버킷에 저장된다. 변환 시 입력 모델이 잘못되어 있다면, SVG로 파싱되지 않으며, 모델이 제대로 렌더링되지 않는다.

4. 모델-데이터베이스 Connection 
BIM 모델 경량화 후 IoT 데이터 연결 과정을 작업한다. 각 레코드는 IoT 데이터와 연결을 위해서는 모델 객체 별로 연결 데이터 처리 작업이 필요하다. BIM 객체에 센서 데이터 연결을 위한 ID같은 연결점을 모델링한다(마법은 없다. 눈으로 객체를 확인하며 손으로 직접 입력 수정한다. 참고로, 분류체계가 잘 되어있다고 하더라도, 분류체계 자체가 유일ID가 아니면 어차피 눈으로 보고 손으로 직접 입력해야 함. 래빗 애드인이나 스크립트를 통해 이 과정을 좀 간소화시킬 수는 있을 것이다). 상호 연결을 위해 객체 속성 GUID, 분류체계, 태그 ID 등을 생성하고 이를 이용해 DB, 비지니스 로직과 연결한다. 연결할 ID가 상호 잘못되면 제대로 처리가 안된다.
Autodesk Revit의 Room 속성 예시
BIM 공간 - IoT 데이터 연결 ID 속성값 예시(Room face for model using NWC)
BIM 객체 속성값에 의한 IoT 데이터 연결 예시

참고로, 독자는 테스트를 위해 Autodesk 제공 예제 파일을 이용하기를 바란다.

MongoDB CRUD REST API 서버 개발
몽고디비 리소스는 보안 등의 문제로 인터넷 브라우저에서 직접 접속할 수 없다. 데이터를 얻기 위해, REST API서버를 개발한다. 주요 코드는 다음과 같다.
// sensor.mode.js 파일
const mongoose = require('mongoose');  // 몽고DB 연결을 위해 mongoose node 패키지 사용

var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

// 저장되는 몽고DB 스키마 구조 정의
const RecordSchema = new Schema({
    _id : ObjectId, 
    sensor : String,
    area : String,
    date : String,
    value : Number
    // _msgid : String
}); 

const record = mongoose.model('record', RecordSchema);
module.exports = record;

// sensor.routes.js 파일
module.exports = (app) => {
    const sensors = require('../controllers/sensor.controller.js');

    // Retrieve all sensors
    app.get('/sensors', sensors.findAll);  // 센서 데이터를 얻는 URL GET 함수
    ...

// sensor.controll.js 파일
const Record = require('../models/sensor.model.js');

// 데이터베이스에서 데이터 검색해 리턴.
exports.findAll = (req, res) => {
    Record.find()
    .then(records => {
        res.send(records);
    }).catch(err => {
        res.status(500).send({
            message: err.message || "Some error occurred while retrieving Record."
        });
    });
};
...

데이터 검색 결과에 조건을 적용하고 싶을 때가 있다. 다음과 같은 REST API query 질의를 처리하려면, req 페이지 요청 객체(Express Request)에서 해당 질의를 파싱해 몽고디비 find() 함수에 검색 조건을 입력해야 한다. 
http://localhost:4000/sensors?limit=10&date=2021
REST API 조건 질의 검색 결과

다음은 검색 조건을 고려한 데이터베이스 검색 코드 루틴이다.
exports.findAll = (req, res) => {
    var query = {};
    if(typeof req.query.date != 'undefined') {
        query = {date: {$gt: req.query.date}};  // {date: {$gt: '2021-2-3-17:32'}}
    }

    try {
            Record.find(query).then(records => {
                res.send(records);
            });
        }

실제 구현 코드는 아래 링크를 참고한다.
관련 내용의 자세한 설명은 다음 링크를 참고한다.
개발 결과는 다음과 같다.
센서 데이터 REST API 결과

Postman 도구를 이용하면, 개발된 REST API를 쉽게 테스트할 수 있다(참고. Postman 설치). Postman에서 GET URL을 http://localhost:4000/sensors로 입력한다. 다음 그림과 같은 결과가 출력되면 성공한것이다.
Postman REST API 테스트 결과

참고로, REST API서버는 이를 이용하는 포지 앱 서버와 서로 CORS 설정이 되어 있어야, 에러 없이 리소스에 접근할 수 있다. 구현 코드는 이를 기본 옵션으로 설정해 두었다. 관련 자세한 내용은 다음 링크를 참고한다. 
Autodesk Forge와 Mongo DB 연결 및 데쉬보드 개발
이제, Autodesk Forge와 Mongo DB를 연결하고, 데쉬보드 개발한다. BIM Forge view 개발 방법은 다음을 참고한다.
실제, 구현 코드는 다음 링크를 참고한다.
구현 코드의 폴더 구조는 다음과 같다.
구현 코드에서 포지 앱과 몽고 DB연결은 앞서 개발한 API 서버를 사용한다. 주요 코드는 다음과 같다.
//  viewer.html 파일
<!DOCTYPE html>
<html>
...
<head>
</head>
...
<body>
    <script type="text/javascript">
        function renderSensorTable(responseText) {
            var htmlElement= document.getElementById('sensorDataTable'); 
            htmlElement.innerHTML = '';

            var table = document.createElement("table");
            table.setAttribute("class", "table table-striped table-sm table-responsive");

            var head = "<thead><tr><th>No</th><th>Area</th><th>Sensor</th><th>Time</th><th>Value</th></tr></thead>";
            table.innerHTML = head;   // 테이블 헤더 정의

            var tbody = document.createElement("tbody");
                           
            var records = JSON.parse(responseText);  // REST API 리턴 데이터를 JSON 포맷으로 변환
            for (var i = 0; i < records.length; i++) {    // 센서 데이터 레코드 행 추가
                var record = records[i];

                var recValues = [];
                recValues.push(i);
                recValues.push(record.area);
                recValues.push(record.sensor);
                recValues.push(record.date);
                recValues.push(record.value);               

                var row = document.createElement("tr");

                for (var j = 0; j < recValues.length; j++)  // 센서 데이터 레코드 값 추가
                {
                    var c = document.createElement("td");
                    var t = document.createTextNode(recValues[j]);
                    c.appendChild(t);
                    row.appendChild(c);                    
                }

                tbody.appendChild(row);
            } 
            table.appendChild(tbody);
            htmlElement.appendChild(table);
        }

        function renderSensorData() {  // 센서 데이터 렌더링
            // Create a new ajax requst
            var oReq = new XMLHttpRequest();  // REST API 요청

            oReq.onreadystatechange = function ()  // 비동기 데이터 처리
            {
                if (oReq.readyState == 4 && oReq.status == 200)
                    renderSensorTable(oReq.responseText);  // IoT 데이터 전송 시 처리
            };

            // Create the connection to our API
            oReq.open("GET", "http://localhost:4000/sensors");  // REST API를 통한 IoT 센서 데이터 요청
            oReq.setRequestHeader('Content-Type', 'application/json');
            oReq.send();    // Fire the request
        }

        /**
        * Autodesk.Viewing.Document.load() success callback.
        * Proceeds with model initialization.
        */
        function onDocumentLoadSuccess(doc) {
            // Load the default viewable geometry into the viewer.
            // Using the doc, we have access to the root BubbleNode,
            // which references the root node of a graph that wraps each object from the Manifest JSON.
            var viewable = doc.getRoot().getDefaultGeometry();
            if (viewable) {
                viewer.loadDocumentNode(doc, viewable).then(function(result) { // 3D 뷰어 로딩시 발생 이벤트
                    console.log('Viewable Loaded!');
                    viewer.loadExtension("NestedViewerExtension", { filter: ["2d"], crossSelection: true });  

                    renderSensorData();  // 센서 데이터 렌더링
                }).catch(function(err) {
                    console.log('Viewable failed to load.');
                    console.log(err);
                }
              )
            }
        }
  </script>
</body>
</html>

데쉬보드 그래픽은 Bootstrap을 사용하며, 뷰어 우측과 아래에 차트 그래프(forgeViewer div 요소 뒤에 추가됨), 테이블(sensorDataTable)로 동적 구성된다. 동적 구성될 요소는  viewer.html의 body에서 다음과 같이 정의되어 있다(Ex. table event).
<body>
    <!-- Custom script -->
    <h4>Digital Twin based Building Monitoring and Data Analysis</h4>
    <div class="container-fluid fill">
        <div class="row fill">               
            <div class="col-sm-7 fill">          
                <div id="forgeViewer"> 
                </div>
            </div> 
        </div>
    </div>        
    <div class="container-fluid fill">
        <div class="row">       
            <div class="col-sm-12 fill">
                <button id="reflashSensorData" onclick="renderSensorData()">Reflash IoT sensor data</button>                  <div id="sensorDataTable" class="table-responsive"></div> 
            </div>             
        </div>
    </div>

그래프 동적 구성을 위한 주요 코드는 다음과 같다. 데쉬보드 div 요소가 추가되고, 데쉬보드 div 요소 안에 바차트, 파이차트 div 요소가 추가된다.
// dashboard.js
$(document).ready(function () {
    $(document).on('DOMNodeInserted', function (e) {
        if ($(e.target).hasClass('orbit-gizmo')) {
            new Dashboard(NOP_VIEWER, [new BarChart('Light'), new PieChart('Temp')]);  // HTML DOM ready 이벤트 발생 시 데쉬보드 추가.    
        }
    });
})
class Dashboard {
    adjustLayout() {
        var row = $(".row").children();
        if(row.length > 0)
            $(row[0]).removeClass('col-sm-7').addClass('col-sm-9 transition-width').after('<div class="col-sm-3 transition-width" id="dashboard"></div>');  // Forge viewer div 아래에 차트 그래프 요소 추가
    }
}

// Dashboard panel base
class DashboardPanel {
    load(parentDivId, divId, viewer) {
        this.divId = divId;
        this.viewer = viewer;
        $('#' + parentDivId).append('<div id="' + divId + '" class="dashboardPanel"></div>');  // 데쉬보드 요소 추가
    }
}

// Dashboard panels for charts
class DashboardPanelChart extends DashboardPanel {
    load(parentDivId, divId, viewer) {
        divId = this.propertyToUse.replace(/[^A-Za-z0-9]/gi, '') + divId; // div name = property + chart type
        super.load(parentDivId, divId, viewer);
        this.canvasId = divId + 'Canvas';
        $('#' + divId).append('<canvas id="' + this.canvasId + '" width="400" height="400"></canvas>');  // 차트 그래프 요소 추가
        return true;
    }
}

// 바차트 데이터 렌더링 구현
class BarChart extends DashboardPanelChart {
    drawChart() {
    ..
    }
}

// 파이차트 데이터 렌더링 구현
class PieChart extends DashboardPanelChart {
    drawChart() {
   }
}

이 코드의 실행 결과로 다음과 같이 HTML DOM(document object model)이 구성된다. 앱 화면 레이아웃은 목적에 맞게 CSS파일의 해당 요소 스타일을 적절히 수정한다.
DOM 구조
데쉬보드 화면

BIM 객체 선택 시 처리(object selection override)는 다음과 같이 Forge viewer에 이벤트 핸들러를 등록해 수행한다.
            ...
            viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectionChanged);
        }

        function onSelectionChanged(event) {
            if (event.dbIdArray.length === 1) {
                viewer.getProperties(event.dbIdArray[0], function(data) {
                    console.log(data.name);
                    if(data.name.indexOf("Column") >= 0)
                    {
                        var instanceTree = viewer.model.getData().instanceTree;
                        var parentId = instanceTree.getNodeParentId(event.dbIdArray[0])
                        viewer.select([parentId]);
                    }
                });
            }
        }
        ...

HTML 요소 이벤트는 javascript event 규약을 참고해 처리한다. 

N-Screen 처리
N-Screen는 다양한 장치 스크린에 맞게 화면에 표시되는 정보양을 조정하는 것이다. 이는 javascript의 다음 변수와 resize 이벤트를 이용해 처리한다.
document.documentElement.clientWidth
document.documentElement.clientHeight
window.innerWidth
window.innerHeight

function calcWindowSize() {
   ...
}

window.addEventListener("resize", calcWindowSize);

결과는 다음과 같다.

외부 네트워크 DNS 서비스
현재는 앞서 개발된 모든 서버들이 내부 인터넷망에서만 동작될 것이다. 외부에 DNS 서비스하기 위해서는 다음 링크를 참고한다.

마무리
이제 BIM 공간 선택 시 해당 데이터를 차트 그래프와 연결하고, IoT 센서 장치를 각 공간에 설치하면, 현실과 디지털 공간과 연결한 소위 디지털 트윈이란 개념을 구현한 것이다. 개발해 본 결과를 보면, 기존 센서 데이터를 디지털 모델과 연계하거나 시뮬레이션했던 시스템들과 큰 차이가 없다. 이런 접근이 브랜드로 발전하고 대중화된 것이다. 디지털 트윈을 구현하는 방법은 다양할 수 있다. 구현 방법은 요구사항과 개발 전략에 따라 달라질 수 있다. 만약, GIS map을 사용한다면, Leaflet(리플릿), mapbox(맵박스), 3D Cesuim(세슘) 등 다양한 오픈소스 도구를 사용할 수 있다. 도면만 필요하다면, DWG/DXF 캐드 뷰어만 사용해도 된다. 데이터 모니터링만 필요하다면, 테이블 및 차트만 지원되는 데쉬보드 형태의 HCI(Human Communication Interface)도 충분하다.
오픈소스 기반 GIS 공간정보 뷰어 예시 

굳이, 3D 모델이 필요없이 효율적인 업무가 가능하다면, 디지털 트윈 타이틀이 붙은 과제라도 3D 모델 뷰어를 구현할 필요가 없다. 앞서 개발하는 과정을 보면 알겠지만, 디지털 트윈 개념을 서비스하기 위해서는 많은 비용과 노력이 필요하다. DATABASE, REST API, 보안, VIEWER, DASHBOARD, BUSINESS LOGIC, IoT, 서버, 데이터 모델링 등 욕심을 내다 보면 끝도 없다. 

보통, 사용자는 유지보수 비용을 크게 간과하는 경향이 있다. 실제 서비스 중인 소프트웨어는 유지보수 비용이 80~90%를 상회한다.

소프트웨어 공학 관점에서 보았을 때 불필요한 모델, 데이터의 구현은 유저의 시스템 사용을 매우 불편하게 할 뿐아니라 유지보수, 성능에 나쁜 영향을 준다. 이 시점에서 모든 것을 막연한 짐작으로 디지털 트윈이나 BIM으로 구현하면 편리하다는 주장은 근거가 매우 빈약하며, 쉽게 납득하기도 어렵다(발주자가 진정 돈을 아끼고 싶다면, 화려하고 복잡한 PPT는 조심해야 한다). 물리적 데이터를 디지털 공간에 맵핑하는 것이 문제라면, 그 요구사항과 구현이 유저 입장인지가 중요하다.

From UNF. 2/23/2021

레퍼런스
추신 - 이 정도까지 진행하는 데 Full M/M로 대략 1개월(Not including holiday) 걸린 것 같다. 이 정도로도 물리적 공간에 붙착된 데이터를 가상으로 모니터링하고 분석용 데이터를 획득할 수 있다. 이제 좀 더 편리한 인터페이스 구현을 위해, 다음과 같은 부분을 처리한다. 
  • BIM 공간과 그래프 연결. 이벤트 처리
  • IoT 센서 장치를 인터넷에 연결
  • IoT 센서 장치를 각 공간에 설치
  • 모니터링된 데이터에서 이상 패턴 감지를 위한 딥러닝 학습 모듈 개발
여기서, 다른 일을 하며 진행하므로, 개발 효율이 30~50% 정도... 그럼 앞으로 약 두달 정도해야 정리될 듯하다. 시간이 빡빡하기는 하지만, 센서 데이터와 글은 병행해 정리하기로 한다. - 2/25

추신 - 오토데스크 포지 제공 API가 원하는 모든 기능을 제공하지는 않는다(직접 해봐야 안다). 제공되지 않는 기능은 직접 구현해야 하며, 가끔은 DOM 해킹, 3차원 기하 계산 등을 코딩해야 할 수 있다. 참고로, 이런 노가다 코딩이 인공지능으로 자동화될 수 있다고 착각하지 말자(한참 멀었다). 

추신 - UNF에서 DT 웹서버 설정하고 실행함. 이제 자잘한 기술 개발 및 네트워크 설정만 남은 듯. - 3/10

추신 - ELO Twilight 들으며 작정하고 편안하게 코딩 중. 지금까지 너무나 주변 요구, 환경과 시선에 신경을 쓰고 살았음을 느낌. 아무도 터치하지 않으니 생각 정리되어 편하고, 일 잘 됨. 의미없는 생명력이라고는 1도 없는 공허한 수많은 R&D과제 무생물 포장들 신경 쓸 필요 없이, 내가 생각한 개념 데로 연구개발하니 속편하고 무언가 채워지는 느낌(인생이 아깝지 않다). 불과 두달전까지 잡다한 일에 치여 코딩은 커녕, 과제 업체 관리 보고 논문 특허 증빙자료 정량지표 채우기도 바빳던, 막연히 의미있을 거란 위안을 갖고 있을 때와는 너무 차이 남(안보고 안해도 되는 건데 술쳐먹고 노는 체질 아니니 스스로 힘들게 하는 거지). 예전에 Universe 를 만들겠다고 상상하며 개발했던 적이 있었음. 그땐 누가 시키지 않아도, 밤새고 프로그래밍한 후 실행되는 어플리케이션이 애완동물처럼 좋았던 때가 있었다. 

프로그래밍은 여행전 시동 걸어 출발한 후, 오랫동안 고통의 계곡을 넘은 후, 주변의 아름다운 경치가 한 눈에 들어오는 어드밴처와 비슷하다. 오래간에 그 느낌 맛보네. - 3/14

추신 - 드디어, 개발 끝남. 개발 기간은 1.5M/M. BIM 모델 연계, 차트 개발, 테이블 이벤트 처리, 센서 오작동 문제 처리, 웹 화면 스타일 처리, IoT 데이터 동적 업데이트 등 자잘한 것 개발 완료. 아직 완전히 마음에 드는 것은 아니지만, 대학교 내부에서 서비스 및 연구 분석 데이터 얻을 정도는 됨. 성능 최적화, 코드 리팩토링은 시간 나면 하기로. 이제 스캔, 딥러닝, 비전 작업을 시작. 다음주에는 이 내용을 글로 정리해야겠음. 이젠 여기 생활이 일상이 되어 간다. 점점 가족중심이 되어가는 듯... - 4/2

추신 - 측정해 보니, 각 공간별 온습도차이, 조도차이를 알 수 있음. 더불어, 조도차이로 사람 거주 유무도 예측 가능. 몇몇 센서는 서로 간섭되는 현상도 발견해 수정함. - 6/12/2021

추신 - 24/365 서버 실행 테스트 중. 5월부터 지금까지 운영해 모아보니, 전원이 공급되는 한 서버가 다운되거나 센서 동작 코드가 에러나는 경우는 없었음. - 8/10/2021

추신 - 데이터 취득 중 특정 센서 취득 시 무결성 문제 발견. 지금까지 모아둔 1기가 바이트 이상 데이터베이스 모두 리셋하고 코드 수정해 재취득 시작함 - 9/20/2021

추신 - 얼마전 부터 여기 학생, 다른 여러 교수들과 협업 중. 개발 기술을 브랜드화하기 위해, IoT 센서 패키징, 디자인 등을 브레인스토밍하였고, 그때 나온 의견을 바탕으로 작업을 진행하고 있다. 아울러, 데이터 무결성 등을 오랫동안 실행해보며 확인했고 몇몇 문제점을 수정하였다. 무결성이 확보되었으니, 딥러닝 학습용 데이터 다운로드받아 이상패턴 검출할 모델 만들면 될 듯. - 10/20/2021