Download app.py from dddddd1ff/ship-roll: direct link, hf CLI and curl.
- Browser
- Download file 8.11 kB
-
https://huggingface.co/spaces/dddddd1ff/ship-roll/resolve/main/app.py
- Command line
-
hf download hf://spaces/dddddd1ff/ship-roll/app.py
-
curl -L -o app.py https://huggingface.co/spaces/dddddd1ff/ship-roll/resolve/main/app.py
8.11 kB
| # -------------------------------------------------- | |
| # 1. 필수 라이브러리 | |
| # (Hugging Face는 requirements.txt를 읽음) | |
| # -------------------------------------------------- | |
| import gradio as gr | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from matplotlib.animation import FuncAnimation | |
| import os | |
| import uuid | |
| print("Gradio 및 필수 라이브러리 로드 완료.") | |
| # -------------------------------------------------- | |
| # 2. 시뮬레이션 엔진 (v6와 동일) | |
| # -------------------------------------------------- | |
| I, c, k = 1.0, 0.2, 1.0 | |
| A_wave_1, omega_wave_1 = 0.2, 2.0 # type1 | |
| A_wave_2, omega_wave_2 = 0.5, 0.8 # type2 | |
| A_wave_3a, omega_wave_3a = 0.2, 1.0 # type3 | |
| A_wave_3b, omega_wave_3b = 0.1, 3.0 | |
| dt = 0.01 | |
| T_end = 20.0 | |
| t_array = np.arange(0, T_end, dt) | |
| def wave_torque_generator(wave_type, t_array): | |
| if wave_type == "type1": | |
| return A_wave_1 * np.sin(omega_wave_1 * t_array) | |
| elif wave_type == "type2": | |
| return A_wave_2 * np.sin(omega_wave_2 * t_array) | |
| elif wave_type == "type3": | |
| torque_a = A_wave_3a * np.sin(omega_wave_3a * t_array) | |
| torque_b = A_wave_3b * np.sin(omega_wave_3b * t_array) | |
| return torque_a + torque_b | |
| elif wave_type == "type4_random": | |
| raw_noise = np.random.randn(len(t_array)) * 0.5 | |
| window = np.ones(50) / 50 | |
| smooth_noise = np.convolve(raw_noise, window, mode='same') | |
| return smooth_noise | |
| else: | |
| return np.zeros_like(t_array) | |
| class PIDController: | |
| def __init__(self, Kp, Ki, Kd): | |
| self.Kp, self.Ki, self.Kd = Kp, Ki, Kd | |
| self.integral, self.prev_error = 0.0, 0.0 | |
| def reset(self): | |
| self.integral, self.prev_error = 0.0, 0.0 | |
| def update(self, r, r_dot, dt): | |
| error = 0.0 - r | |
| self.integral += error * dt | |
| derivative = (error - self.prev_error) / dt | |
| self.prev_error = error | |
| return self.Kp * error + self.Ki * self.integral + self.Kd * derivative | |
| class FuzzyController: | |
| def __init__(self): | |
| self.Kp = 2.0 | |
| self.Kd = 2.5 | |
| def reset(self): | |
| pass | |
| def update(self, r, r_dot, dt): | |
| angle = r | |
| angle_rate = r_dot | |
| u_p = -self.Kp * angle | |
| u_d = -self.Kd * angle_rate | |
| u = u_p + u_d | |
| return u | |
| class FuzzyPIDController: | |
| def __init__(self): | |
| self.pid_controller = PIDController(Kp=0, Ki=0, Kd=0) | |
| def reset(self): | |
| self.pid_controller.reset() | |
| def update(self, r, r_dot, dt): | |
| error = 0.0 - r | |
| if abs(error) > 0.1: | |
| self.pid_controller.Kp = 4.0 | |
| self.pid_controller.Ki = 3.0 | |
| self.pid_controller.Kd = 1.5 | |
| else: | |
| self.pid_controller.Kp = 2.0 | |
| self.pid_controller.Ki = 4.0 | |
| self.pid_controller.Kd = 2.5 | |
| return self.pid_controller.update(r, r_dot, dt) | |
| def simulate_roll_data(control_mode, wave_type): | |
| M_w_array = wave_torque_generator(wave_type, t_array) | |
| r, r_dot = np.deg2rad(10.0), 0.0 | |
| r_list = [] | |
| pid = PIDController(Kp=4.0, Ki=3.0, Kd=1.5) | |
| fuzzy = FuzzyController() | |
| fuzzy_pid = FuzzyPIDController() | |
| pid.reset() | |
| fuzzy.reset() | |
| fuzzy_pid.reset() | |
| for i, t in enumerate(t_array): | |
| M_w = M_w_array[i] | |
| if control_mode == "none": M_c = 0.0 | |
| elif control_mode == "pid": M_c = pid.update(r, r_dot, dt) | |
| elif control_mode == "fuzzy": M_c = fuzzy.update(r, r_dot, dt) | |
| elif control_mode == "fuzzy_pid": M_c = fuzzy_pid.update(r, r_dot, dt) | |
| else: M_c = 0.0 | |
| r_ddot = (M_w + M_c - c * r_dot - k * r) / I | |
| r_dot = r_dot + r_ddot * dt | |
| r = r + r_dot * dt | |
| r_list.append(r) | |
| return t_array, np.array(r_list) | |
| # -------------------------------------------------- | |
| # 3. Gradio 메인 함수 (GIF 생성 + 분석 데이터) | |
| # -------------------------------------------------- | |
| def run_simulation_and_analyze(control_mode, wave_type): | |
| print(f"[{control_mode} / {wave_type}] 조건으로 {T_end}초 시뮬레이션 및 GIF 생성 시작...") | |
| t, r_array = simulate_roll_data(control_mode, wave_type) | |
| fig = plt.figure(figsize=(8, 6)) | |
| gs = fig.add_gridspec(2, 1, height_ratios=[1, 1]) | |
| ax_ship = fig.add_subplot(gs[0]) | |
| ax_ship.set_xlim(-2, 2); ax_ship.set_ylim(-1, 1); ax_ship.set_aspect('equal') | |
| ax_ship.set_title(f"Ship Roll Animation ({control_mode} / {wave_type})") | |
| ship_width = 1.5; ship_height = 0.3 | |
| ship_line, = ax_ship.plot([], [], lw=3, color='blue') | |
| ax_ship.axhline(0, linestyle="--", color='cyan') | |
| ax_rt = fig.add_subplot(gs[1]) | |
| ax_rt.set_xlim(t[0], t[-1]) | |
| r_abs_max = np.max(np.abs(r_array)) | |
| r_lim = max(r_abs_max * 1.2, 0.2) | |
| ax_rt.set_ylim(-r_lim, r_lim) | |
| ax_rt.set_xlabel("Time [s]"); ax_rt.set_ylabel(f"Roll angle r(t) [rad] (Total: {T_end}s)"); ax_rt.grid(True) | |
| rt_line, = ax_rt.plot([], [], lw=2, color='red') | |
| def init(): | |
| ship_line.set_data([], []); rt_line.set_data([], []) | |
| return ship_line, rt_line | |
| def update(frame): | |
| r_now = r_array[frame] | |
| x_corners = np.array([-ship_width/2, ship_width/2, ship_width/2, -ship_width/2, -ship_width/2]) | |
| y_corners = np.array([-ship_height/2, -ship_height/2, ship_height/2, ship_height/2, -ship_height/2]) | |
| cos_r, sin_r = np.cos(r_now), np.sin(r_now) | |
| x_rot = x_corners * cos_r - y_corners * sin_r | |
| y_rot = x_corners * sin_r + y_corners * cos_r | |
| ship_line.set_data(x_rot, y_rot + 0.1) | |
| rt_line.set_data(t[:frame+1], r_array[:frame+1]) | |
| return ship_line, rt_line | |
| ani = FuncAnimation(fig, update, frames=range(0, len(t), 20), init_func=init, blit=False) | |
| filename = f"sim_animation_{uuid.uuid4().hex}.gif" | |
| print(f"애니메이션 생성 중... (총 {T_end}초 분량) -> {filename}") | |
| ani.save(filename, writer='pillow', fps=15) | |
| plt.close(fig) | |
| print(f"GIF 생성 완료!") | |
| max_roll_rad = np.max(np.abs(r_array)) | |
| max_roll_deg = np.rad2deg(max_roll_rad) | |
| idx_8s = int(8.0 / dt) | |
| iae_after_8s = np.sum(np.abs(r_array[idx_8s:])) * dt | |
| # ⬇️ [핵심 수정] (v6.1 -> v6.2) | |
| # 여러 줄로 나눴던 f-string을 f"""...""" (따옴표 3개)로 감싸서 | |
| # 문법 오류(SyntaxError)가 나지 않도록 수정 | |
| stats_text = f"""--- 📊 정량 분석 결과 ({T_end}초) --- | |
| 1. 최대 롤각 (전체 시간): {max_roll_deg:.2f} (degree) | |
| (배가 최대로 기울어진 각도 - 오버슈트 포함) | |
| 2. 안정화 성능 (IAE @ 8s-{T_end}s): {iae_after_8s:.3f} | |
| (8초 이후의 총 흔들림 지수. 작을수록 파도를 잘 이겨내고 멈춘 것.) | |
| """ | |
| return filename, stats_text | |
| # -------------------------------------------------- | |
| # 4. Gradio 인터페이스 생성 및 실행 | |
| # -------------------------------------------------- | |
| inputs = [ | |
| gr.Dropdown( | |
| label="1. 제어 모드 선택", | |
| choices=[("제어 없음", "none"), ("PID", "pid"), ("Fuzzy", "fuzzy"), ("Fuzzy PID", "fuzzy_pid")], | |
| value="pid" | |
| ), | |
| gr.Dropdown( | |
| label="2. 파도 타입 선택", | |
| choices=[ | |
| ("유형 1 (작고 빠름)", "type1"), | |
| ("유형 2 (크고 느림)", "type2"), | |
| ("유형 3 (복잡한 규칙)", "type3"), | |
| ("유형 4 (매번 다름 - 불규칙)", "type4_random") | |
| ], | |
| value="type2" | |
| ) | |
| ] | |
| outputs = [ | |
| gr.Image(label="시뮬레이션 애니메이션 결과 (GIF)"), | |
| gr.Text(label=f"성능 분석 결과 (v6.2)") | |
| ] | |
| iface = gr.Interface( | |
| fn=run_simulation_and_analyze, | |
| inputs=inputs, | |
| outputs=outputs, | |
| title=f"🚢 선박 롤링 제어 시뮬레이션 (v6.2 - 20s + 4 Modes)", | |
| description="제어 모드와 파도 타입을 선택하고 'Submit' 버튼을 누르세요. (20초 GIF 생성)", | |
| allow_flagging="never", | |
| examples=[["pid", "type2"], ["fuzzy_pid", "type2"], ["none", "type2"],] | |
| ) | |
| # ⬇️ [수정] (v6.1 -> v6.2) | |
| print(f"Gradio 인터페이스(v6.2)를 실행합니다...") | |
| iface.launch() # Hugging Face에서는 share=True 없이 실행 |