CHAPTER 6 8 / 10

로봇 디지털 트윈

시뮬레이션, 가상 커미셔닝, 오프라인 프로그래밍(OLP)

1로봇 디지털 트윈의 개념

로봇 디지털 트윈은 물리적 로봇의 가상 복제본으로, 실시간 데이터 동기화를 통해 로봇의 상태를 정확히 반영합니다. 시뮬레이션, 모니터링, 최적화에 활용됩니다.

Robot Digital Twin Synchronization Architecture
Physical Robot
🤖
Sensor Data
Joint Position Joint Torque Force/Torque Temp/Current
Real-time Sync
Digital Twin (Virtual)
🤖
Capabilities
3D Visualization Dynamics Sim Predictive Analysis Anomaly Detection
Bidirectional Data Flow
Use Case Scenarios
Virtual Commissioning
Program Validation
Predictive Maintenance
RUL Prediction
OLP
Offline Teaching
What-If Analysis
Scenario Testing
구분 일반 시뮬레이션 디지털 트윈
데이터 연결 오프라인 / 정적 실시간 양방향 동기화
목적 설계 검증 운영 최적화, 예측
업데이트 수동 자동 (센서 데이터 기반)
수명 주기 개발 단계 한정 전체 수명 주기

로봇 디지털 트윈의 핵심 가치는 '물리 세계에서 시도하기 전에 가상 세계에서 테스트'할 수 있다는 점입니다. 비용과 리스크를 획기적으로 줄입니다.

2로봇 시뮬레이션 모델

정확한 시뮬레이션을 위해 기구학, 동역학, 센서, 환경을 모델링해야 합니다.

import numpy as np from dataclasses import dataclass from typing import List, Tuple @dataclass class DHParameter: """DH 파라미터""" theta: float # 관절 각도 (revolute) 또는 0 (prismatic) d: float # 링크 오프셋 a: float # 링크 길이 alpha: float # 링크 비틀림 class RobotKinematicModel: """로봇 기구학 모델 (DH 파라미터 기반)""" def __init__(self, dh_params: List[DHParameter]): self.dh_params = dh_params self.num_joints = len(dh_params) def forward_kinematics(self, joint_angles: np.ndarray) -> np.ndarray: """ 순기구학: 관절 각도 → 엔드이펙터 포즈 Returns: 4x4 동차 변환 행렬 """ T = np.eye(4) for i, (dh, q) in enumerate(zip(self.dh_params, joint_angles)): theta = dh.theta + q # revolute joint d = dh.d a = dh.a alpha = dh.alpha # DH 변환 행렬 Ti = np.array([ [np.cos(theta), -np.sin(theta)*np.cos(alpha), np.sin(theta)*np.sin(alpha), a*np.cos(theta)], [np.sin(theta), np.cos(theta)*np.cos(alpha), -np.cos(theta)*np.sin(alpha), a*np.sin(theta)], [0, np.sin(alpha), np.cos(alpha), d], [0, 0, 0, 1] ]) T = T @ Ti return T def jacobian(self, joint_angles: np.ndarray) -> np.ndarray: """자코비안 행렬 계산""" J = np.zeros((6, self.num_joints)) T = np.eye(4) positions = [np.zeros(3)] # 각 링크의 위치와 z축 계산 z_axes = [np.array([0, 0, 1])] for i, (dh, q) in enumerate(zip(self.dh_params, joint_angles)): theta = dh.theta + q Ti = self._dh_matrix(theta, dh.d, dh.a, dh.alpha) T = T @ Ti positions.append(T[:3, 3]) z_axes.append(T[:3, 2]) # 엔드이펙터 위치 p_ee = positions[-1] # 각 관절의 자코비안 열 계산 for i in range(self.num_joints): z_i = z_axes[i] p_i = positions[i] # 회전 관절 J[:3, i] = np.cross(z_i, p_ee - p_i) # 선형 속도 J[3:, i] = z_i # 각속도 return J class RobotDynamicModel: """로봇 동역학 모델""" def __init__(self, kinematic_model: RobotKinematicModel, link_masses: List[float], link_inertias: List[np.ndarray], link_cogs: List[np.ndarray]): self.kinematics = kinematic_model self.masses = link_masses self.inertias = link_inertias self.cogs = link_cogs self.gravity = np.array([0, 0, -9.81]) def inverse_dynamics(self, q: np.ndarray, qd: np.ndarray, qdd: np.ndarray) -> np.ndarray: """ 역동역학: 목표 가속도 → 필요 토크 Newton-Euler 재귀 알고리즘 """ n = len(q) tau = np.zeros(n) # 전방 재귀: 속도, 가속도 전파 omega = np.zeros((n+1, 3)) # 각속도 alpha = np.zeros((n+1, 3)) # 각가속도 ae = np.zeros((n+1, 3)) # 선가속도 ae[0] = -self.gravity # ... (Newton-Euler 알고리즘 구현) return tau def forward_dynamics(self, q: np.ndarray, qd: np.ndarray, tau: np.ndarray) -> np.ndarray: """ 순동역학: 토크 → 가속도 M(q)qdd + C(q,qd)qd + G(q) = tau """ M = self._mass_matrix(q) C = self._coriolis_matrix(q, qd) G = self._gravity_vector(q) # 가속도 계산 qdd = np.linalg.solve(M, tau - C @ qd - G) return qdd

3가상 커미셔닝

가상 커미셔닝(Virtual Commissioning)은 실제 장비 설치 전에 PLC 프로그램과 로봇 프로그램을 가상 환경에서 테스트하는 것입니다.

from enum import Enum import asyncio class VirtualCommissioningPlatform: """가상 커미셔닝 플랫폼""" def __init__(self): self.robot_models = {} self.plc_connections = {} self.simulation_time = 0.0 self.time_step = 0.001 # 1ms def add_robot(self, robot_id: str, robot_model): """로봇 모델 추가""" self.robot_models[robot_id] = { 'model': robot_model, 'state': RobotState(), 'controller': None } def connect_plc(self, plc_id: str, connection_config: dict): """PLC 연결 (OPC UA, S7, etc.)""" self.plc_connections[plc_id] = PLCConnector(connection_config) async def run_simulation(self, duration: float): """시뮬레이션 실행""" while self.simulation_time < duration: # 1. PLC 신호 읽기 plc_signals = await self._read_plc_signals() # 2. 로봇 상태 업데이트 for robot_id, robot_data in self.robot_models.items(): robot_signals = plc_signals.get(robot_id, {}) robot_data['state'] = self._step_robot( robot_data, robot_signals, self.time_step ) # 3. 센서 값 계산 및 PLC에 쓰기 sensor_values = self._compute_sensor_values() await self._write_plc_signals(sensor_values) # 4. 시각화 업데이트 self._update_visualization() self.simulation_time += self.time_step await asyncio.sleep(self.time_step) def _step_robot(self, robot_data: dict, signals: dict, dt: float) -> 'RobotState': """단일 로봇 시뮬레이션 스텝""" model = robot_data['model'] state = robot_data['state'] # 목표 위치/속도 추출 target_position = signals.get('target_position', state.position) target_velocity = signals.get('target_velocity', np.zeros(6)) # 동역학 시뮬레이션 if model.has_dynamics: # 제어 토크 계산 tau = model.controller.compute(state, target_position) # 순동역학으로 가속도 계산 acceleration = model.dynamics.forward_dynamics( state.position, state.velocity, tau ) # 적분 state.velocity += acceleration * dt state.position += state.velocity * dt else: # 순수 기구학 시뮬레이션 state.position = target_position state.velocity = target_velocity return state class PLCConnector: """PLC 통신 커넥터""" def __init__(self, config: dict): self.protocol = config.get('protocol', 'opcua') self.address = config.get('address') self.client = None async def connect(self): """연결 설정""" if self.protocol == 'opcua': from asyncua import Client self.client = Client(self.address) await self.client.connect() elif self.protocol == 's7': import snap7 self.client = snap7.client.Client() self.client.connect(self.address, 0, 1) async def read_tag(self, tag_name: str): """태그 값 읽기""" if self.protocol == 'opcua': node = self.client.get_node(tag_name) return await node.read_value() elif self.protocol == 's7': # S7 데이터 블록 읽기 로직 pass async def write_tag(self, tag_name: str, value): """태그 값 쓰기""" if self.protocol == 'opcua': node = self.client.get_node(tag_name) await node.write_value(value)

가상 커미셔닝의 ROI는 프로젝트 복잡도에 따라 다르지만, 일반적으로 현장 커미셔닝 시간을 40-60% 단축할 수 있습니다.

4오프라인 프로그래밍 (OLP)

OLP(Offline Programming)는 로봇을 정지시키지 않고 PC에서 프로그램을 작성하고 시뮬레이션하는 방법입니다.

class OfflineProgrammer: """오프라인 프로그래밍 시스템""" def __init__(self, robot_model, cell_model): self.robot = robot_model self.cell = cell_model self.program = [] self.collision_checker = CollisionChecker(cell_model) def teach_point(self, name: str, pose: np.ndarray, motion_type: str = 'joint'): """티치 포인트 추가""" # 역기구학으로 관절 값 계산 joint_config = self.robot.inverse_kinematics(pose) if joint_config is None: raise ValueError(f"IK solution not found for pose: {name}") # 관절 한계 검사 if not self.robot.check_joint_limits(joint_config): raise ValueError(f"Joint limits exceeded for: {name}") self.program.append({ 'name': name, 'pose': pose, 'joints': joint_config, 'motion_type': motion_type }) def add_motion(self, target_name: str, speed: float = 100.0, # mm/s blend: float = 0.0): # mm """모션 명령 추가""" target = self._get_point_by_name(target_name) self.program.append({ 'type': 'motion', 'target': target_name, 'speed': speed, 'blend': blend }) def simulate(self) -> List[dict]: """전체 프로그램 시뮬레이션""" results = [] current_joints = self.robot.home_position.copy() for i, instruction in enumerate(self.program): if instruction.get('type') == 'motion': target_point = self._get_point_by_name(instruction['target']) target_joints = target_point['joints'] # 경로 생성 path = self._interpolate_path( current_joints, target_joints, instruction['speed'] ) # 충돌 검사 collision_result = self.collision_checker.check_path(path) results.append({ 'instruction_id': i, 'path': path, 'collision_free': not collision_result['has_collision'], 'collision_details': collision_result.get('details'), 'cycle_time': len(path) * 0.001 # 예상 사이클 타임 }) current_joints = target_joints return results def _interpolate_path(self, start: np.ndarray, end: np.ndarray, speed: float) -> np.ndarray: """관절 공간 경로 보간""" # 최대 관절 속도에서 소요 시간 계산 max_joint_velocity = 1.0 # rad/s (예시) joint_diff = np.abs(end - start) duration = np.max(joint_diff / max_joint_velocity) num_steps = max(int(duration / 0.001), 2) # 1ms 단위 path = np.zeros((num_steps, len(start))) for i in range(num_steps): t = i / (num_steps - 1) # 5차 다항식 보간 (가속도 연속) s = 10*t**3 - 15*t**4 + 6*t**5 path[i] = start + s * (end - start) return path def export_to_robot(self, format: str = 'native') -> str: """로봇 코드로 내보내기""" if format == 'fanuc': return self._export_fanuc() elif format == 'kuka': return self._export_kuka() elif format == 'abb': return self._export_abb() elif format == 'ur': return self._export_urscript() else: raise ValueError(f"Unsupported format: {format}") def _export_urscript(self) -> str: """Universal Robots URScript 내보내기""" code_lines = ["def main():"] for instruction in self.program: if instruction.get('type') == 'motion': target = self._get_point_by_name(instruction['target']) joints = target['joints'] speed = instruction['speed'] / 1000.0 # mm/s → m/s # 관절 값을 문자열로 joints_str = ','.join([f'{j:.4f}' for j in joints]) code_lines.append( f" movej([{joints_str}], a=1.0, v={speed})" ) code_lines.append("end") return '\n'.join(code_lines)

5실시간 동기화

디지털 트윈의 핵심은 실제 로봇과의 실시간 데이터 동기화입니다.

import asyncio from datetime import datetime class RobotDigitalTwinSync: """실시간 로봇 디지털 트윈 동기화""" def __init__(self, robot_driver, digital_twin_model): self.robot = robot_driver self.twin = digital_twin_model self.sync_interval = 0.01 # 10ms (100Hz) self.sync_history = [] async def start_sync(self): """동기화 시작""" while True: await self._sync_cycle() await asyncio.sleep(self.sync_interval) async def _sync_cycle(self): """단일 동기화 사이클""" timestamp = datetime.now() # 1. 실제 로봇 데이터 읽기 robot_state = await self.robot.read_state() # 2. 디지털 트윈 상태 업데이트 self.twin.update_state({ 'joint_positions': robot_state.joint_positions, 'joint_velocities': robot_state.joint_velocities, 'joint_torques': robot_state.joint_torques, 'tcp_pose': robot_state.tcp_pose, 'tcp_force': robot_state.tcp_force, 'io_states': robot_state.io_states, 'timestamp': timestamp }) # 3. 이상 탐지 anomalies = self._detect_anomalies(robot_state) if anomalies: await self._handle_anomalies(anomalies) # 4. 히스토리 저장 self.sync_history.append({ 'timestamp': timestamp, 'state': robot_state, 'anomalies': anomalies }) # 히스토리 크기 제한 if len(self.sync_history) > 100000: self.sync_history = self.sync_history[-50000:] def _detect_anomalies(self, state) -> List[dict]: """이상 탐지""" anomalies = [] # 토크 이상 for i, torque in enumerate(state.joint_torques): expected = self.twin.predict_torque( state.joint_positions, state.joint_velocities )[i] if abs(torque - expected) > self.twin.torque_threshold[i]: anomalies.append({ 'type': 'torque_deviation', 'joint': i, 'expected': expected, 'actual': torque }) # 위치 오차 for i, (cmd, actual) in enumerate( zip(self.twin.commanded_positions, state.joint_positions) ): if abs(cmd - actual) > 0.01: # 0.01 rad 임계값 anomalies.append({ 'type': 'position_error', 'joint': i, 'commanded': cmd, 'actual': actual }) return anomalies class DigitalTwinDashboard: """디지털 트윈 대시보드""" def __init__(self, twin_sync: RobotDigitalTwinSync): self.sync = twin_sync self.metrics = {} def get_real_time_metrics(self) -> dict: """실시간 메트릭 반환""" recent = self.sync.sync_history[-100:] return { 'current_state': self.sync.twin.current_state, 'sync_latency_ms': self._compute_latency(recent), 'anomaly_rate': self._compute_anomaly_rate(recent), 'joint_utilization': self._compute_joint_utilization(recent), 'cycle_time': self._estimate_cycle_time(recent) } def predict_maintenance(self) -> dict: """예측 정비 정보""" return { 'estimated_rul': self.sync.twin.estimate_remaining_life(), 'wear_indicators': self.sync.twin.get_wear_indicators(), 'recommended_actions': self.sync.twin.get_maintenance_recommendations() }

실시간 동기화 지연(Latency)은 디지털 트윈의 정확도에 직접적인 영향을 미칩니다. 제어 목적의 경우 10ms 이하를 목표로 합니다.

6주요 플랫폼과 도구

로봇 디지털 트윈 구축에 사용되는 주요 플랫폼입니다.

플랫폼 특징 강점
NVIDIA Isaac Sim USD 기반, 포토리얼리스틱 AI 학습, 센서 시뮬레이션
Siemens Tecnomatix PLM 통합 가상 커미셔닝, OLP
Dassault 3DEXPERIENCE DELMIA 통합 전체 공장 시뮬레이션
RoboDK 500+ 로봇 지원 가성비, 범용 OLP
Gazebo/ROS 오픈소스 커스터마이징, 연구용