시뮬레이션, 가상 커미셔닝, 오프라인 프로그래밍(OLP)
로봇 디지털 트윈은 물리적 로봇의 가상 복제본으로, 실시간 데이터 동기화를 통해 로봇의 상태를 정확히 반영합니다. 시뮬레이션, 모니터링, 최적화에 활용됩니다.
| 구분 | 일반 시뮬레이션 | 디지털 트윈 |
|---|---|---|
| 데이터 연결 | 오프라인 / 정적 | 실시간 양방향 동기화 |
| 목적 | 설계 검증 | 운영 최적화, 예측 |
| 업데이트 | 수동 | 자동 (센서 데이터 기반) |
| 수명 주기 | 개발 단계 한정 | 전체 수명 주기 |
로봇 디지털 트윈의 핵심 가치는 '물리 세계에서 시도하기 전에 가상 세계에서 테스트'할 수 있다는 점입니다. 비용과 리스크를 획기적으로 줄입니다.
정확한 시뮬레이션을 위해 기구학, 동역학, 센서, 환경을 모델링해야 합니다.
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가상 커미셔닝(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% 단축할 수 있습니다.
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)디지털 트윈의 핵심은 실제 로봇과의 실시간 데이터 동기화입니다.
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 이하를 목표로 합니다.
로봇 디지털 트윈 구축에 사용되는 주요 플랫폼입니다.
| 플랫폼 | 특징 | 강점 |
|---|---|---|
| NVIDIA Isaac Sim | USD 기반, 포토리얼리스틱 | AI 학습, 센서 시뮬레이션 |
| Siemens Tecnomatix | PLM 통합 | 가상 커미셔닝, OLP |
| Dassault 3DEXPERIENCE | DELMIA 통합 | 전체 공장 시뮬레이션 |
| RoboDK | 500+ 로봇 지원 | 가성비, 범용 OLP |
| Gazebo/ROS | 오픈소스 | 커스터마이징, 연구용 |