stardust-coder commited on
Commit
fcb2819
·
1 Parent(s): 4396455

[add] first commit

Browse files
Files changed (2) hide show
  1. requirements.txt +3 -2
  2. src/streamlit_app.py +480 -38
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
- altair
2
  pandas
3
- streamlit
 
 
1
+ streamlit
2
  pandas
3
+ plotly
4
+ openai
src/streamlit_app.py CHANGED
@@ -1,40 +1,482 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
 
 
 
 
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ # app.py
2
+ import os
3
+ import time
4
+ import math
5
+ import random
6
+ from datetime import datetime
7
+
8
  import streamlit as st
9
+ import pandas as pd
10
+ import plotly.graph_objects as go
11
+ from openai import OpenAI
12
+
13
+ # ----------------------------
14
+ # ページ設定
15
+ # ----------------------------
16
+ st.set_page_config(
17
+ page_title="軌道制御支援AIシステム(PoC開発中)",
18
+ page_icon="🛰️",
19
+ layout="wide"
20
+ )
21
+
22
+ # ----------------------------
23
+ # OpenAI client
24
+ # ----------------------------
25
+ api_key = os.getenv("OPENAI_API_KEY")
26
+ client = OpenAI(api_key=api_key) if api_key else None
27
+
28
+ # ----------------------------
29
+ # 初期状態
30
+ # ----------------------------
31
+ if "sat_state" not in st.session_state:
32
+ st.session_state.sat_state = {
33
+ "angle_deg": 20.0, # 軌道位置角
34
+ "altitude_km": 540.0,
35
+ "speed_kms": 7.66,
36
+ "battery_pct": 92,
37
+ "temp_c": 24.5,
38
+ "signal_db": 81,
39
+ "mode": "IDLE",
40
+ "health": "NOMINAL",
41
+ "last_event": "初期化完了",
42
+
43
+ # 追加: レンジング / 姿勢
44
+ "range_km": 1240.0, # 地上局から衛星までの距離
45
+ "pos_x": 420.0, # 軌道面上の相対位置X
46
+ "pos_y": 160.0, # 軌道面上の相対位置Y
47
+ "yaw_deg": 12.0, # 姿勢 Yaw
48
+ "pitch_deg": -4.0, # 姿勢 Pitch
49
+ "roll_deg": 7.0, # 姿勢 Roll
50
+ }
51
+
52
+ if "process_logs" not in st.session_state:
53
+ st.session_state.process_logs = []
54
+
55
+ # ----------------------------
56
+ # ユーティリティ
57
+ # ----------------------------
58
+ def call_openai(prompt: str) -> str:
59
+ if client is None:
60
+ return "OPENAI_API_KEY が未設定です。ダミー応答を返します。"
61
+
62
+ res = client.responses.create(
63
+ model="gpt-4.1-mini",
64
+ input=prompt,
65
+ )
66
+ return res.output_text
67
+
68
+ def update_satellite_state(step_idx: int):
69
+ """工程ごとに衛星状態を少し変化させる"""
70
+ s = st.session_state.sat_state
71
+
72
+ s["angle_deg"] = (s["angle_deg"] + random.uniform(18, 38)) % 360
73
+ theta = math.radians(s["angle_deg"])
74
+
75
+ s["altitude_km"] = round(540 + random.uniform(-8, 8), 1)
76
+ s["speed_kms"] = round(7.66 + random.uniform(-0.04, 0.04), 3)
77
+ s["battery_pct"] = max(20, min(100, s["battery_pct"] - random.randint(1, 3)))
78
+ s["temp_c"] = round(24 + random.uniform(-4, 6), 1)
79
+ s["signal_db"] = max(35, min(99, s["signal_db"] + random.randint(-6, 4)))
80
+
81
+ # 追加: 軌道位置を簡易更新
82
+ orbit_r = 480
83
+ s["pos_x"] = round(orbit_r * math.cos(theta) + random.uniform(-20, 20), 1)
84
+ s["pos_y"] = round(orbit_r * math.sin(theta) + random.uniform(-20, 20), 1)
85
+
86
+ # 追加: 地上局(原点近傍)からの距離レンジング
87
+ gs_x, gs_y = 0.0, -420.0
88
+ s["range_km"] = round(
89
+ math.sqrt((s["pos_x"] - gs_x) ** 2 + (s["pos_y"] - gs_y) ** 2) + random.uniform(-15, 15),
90
+ 1
91
+ )
92
+
93
+ # 追加: 姿勢角を変化
94
+ s["yaw_deg"] = round((s["yaw_deg"] + random.uniform(-8, 8)), 1)
95
+ s["pitch_deg"] = round(max(-45, min(45, s["pitch_deg"] + random.uniform(-4, 4))), 1)
96
+ s["roll_deg"] = round(max(-45, min(45, s["roll_deg"] + random.uniform(-5, 5))), 1)
97
+
98
+ modes = {
99
+ 0: "BOOT",
100
+ 1: "SENSING",
101
+ 2: "ANALYZING",
102
+ 3: "UPLINK",
103
+ 4: "REPORTING",
104
+ }
105
+ events = {
106
+ 0: "衛星起動シーケンス",
107
+ 1: "テレメトリ取得中",
108
+ 2: "観測データ解析中",
109
+ 3: "地上局と通信中",
110
+ 4: "ミッション結果反映"
111
+ }
112
+
113
+ s["mode"] = modes.get(step_idx, "ACTIVE")
114
+ s["last_event"] = events.get(step_idx, "更新")
115
+ s["health"] = "NOMINAL" if s["signal_db"] >= 50 and s["battery_pct"] >= 30 else "CHECK"
116
+
117
+ def make_tracking_figure():
118
+ """
119
+ 右側に出す可視化:
120
+ - 左: 地上局から衛星までのレンジング + 軌道位置
121
+ - 右: 衛星姿勢 (機体向き)
122
+ """
123
+ s = st.session_state.sat_state
124
+
125
+ gs_x, gs_y = 0.0, -420.0
126
+ sat_x, sat_y = s["pos_x"], s["pos_y"]
127
+
128
+ fig = go.Figure()
129
+
130
+ # ----------------------------
131
+ # 左エリア: レンジング / 軌道位置
132
+ # ----------------------------
133
+ # 軌道の目安円
134
+ orbit_x = []
135
+ orbit_y = []
136
+ for d in range(361):
137
+ r = math.radians(d)
138
+ orbit_x.append(480 * math.cos(r))
139
+ orbit_y.append(480 * math.sin(r))
140
+
141
+ fig.add_trace(go.Scatter(
142
+ x=orbit_x,
143
+ y=orbit_y,
144
+ mode="lines",
145
+ name="Reference Orbit",
146
+ line=dict(width=2, dash="dot"),
147
+ ))
148
+
149
+ # 地上局
150
+ fig.add_trace(go.Scatter(
151
+ x=[gs_x],
152
+ y=[gs_y],
153
+ mode="markers+text",
154
+ name="Ground Station",
155
+ text=["Ground"],
156
+ textposition="bottom center",
157
+ marker=dict(size=14, symbol="square"),
158
+ ))
159
+
160
+ # 衛星
161
+ fig.add_trace(go.Scatter(
162
+ x=[sat_x],
163
+ y=[sat_y],
164
+ mode="markers+text",
165
+ name="Satellite Position",
166
+ text=["🛰️"],
167
+ textposition="middle center",
168
+ marker=dict(size=18),
169
+ ))
170
+
171
+ # レンジ線
172
+ fig.add_trace(go.Scatter(
173
+ x=[gs_x, sat_x],
174
+ y=[gs_y, sat_y],
175
+ mode="lines",
176
+ name="Range",
177
+ line=dict(width=3),
178
+ ))
179
+
180
+ # レンジ注記
181
+ mid_x = (gs_x + sat_x) / 2
182
+ mid_y = (gs_y + sat_y) / 2
183
+ fig.add_annotation(
184
+ x=mid_x,
185
+ y=mid_y,
186
+ text=f"Range: {s['range_km']} km",
187
+ showarrow=False,
188
+ yshift=12
189
+ )
190
+
191
+ # 位置注記
192
+ fig.add_annotation(
193
+ x=sat_x,
194
+ y=sat_y,
195
+ text=f"Pos({s['pos_x']}, {s['pos_y']})",
196
+ showarrow=True,
197
+ ax=30,
198
+ ay=-30
199
+ )
200
+
201
+ # ----------------------------
202
+ # 右エリア: 姿勢表示
203
+ # ----------------------------
204
+ # 右側に独立した姿勢表示領域を作る
205
+ attitude_center_x = 980
206
+ attitude_center_y = 0
207
+ body_len = 120
208
+
209
+ yaw_rad = math.radians(s["yaw_deg"])
210
+ body_dx = body_len * math.cos(yaw_rad)
211
+ body_dy = body_len * math.sin(yaw_rad)
212
+
213
+ # 機体中心
214
+ fig.add_trace(go.Scatter(
215
+ x=[attitude_center_x],
216
+ y=[attitude_center_y],
217
+ mode="markers+text",
218
+ name="Attitude Center",
219
+ text=["SAT"],
220
+ textposition="middle center",
221
+ marker=dict(size=22, symbol="diamond"),
222
+ ))
223
+
224
+ # 機体進行方向(Yaw)
225
+ fig.add_trace(go.Scatter(
226
+ x=[attitude_center_x, attitude_center_x + body_dx],
227
+ y=[attitude_center_y, attitude_center_y + body_dy],
228
+ mode="lines+markers",
229
+ name="Yaw Direction",
230
+ line=dict(width=4),
231
+ marker=dict(size=8),
232
+ ))
233
+
234
+ # 簡易ソーラーパドル
235
+ panel_span = 70
236
+ perp_dx = panel_span * math.cos(yaw_rad + math.pi / 2)
237
+ perp_dy = panel_span * math.sin(yaw_rad + math.pi / 2)
238
+
239
+ fig.add_trace(go.Scatter(
240
+ x=[attitude_center_x - perp_dx, attitude_center_x + perp_dx],
241
+ y=[attitude_center_y - perp_dy, attitude_center_y + perp_dy],
242
+ mode="lines",
243
+ name="Solar Panel Axis",
244
+ line=dict(width=6),
245
+ ))
246
+
247
+ # 姿勢テキスト
248
+ fig.add_annotation(
249
+ x=attitude_center_x,
250
+ y=attitude_center_y - 170,
251
+ text=(
252
+ f"Yaw: {s['yaw_deg']}°<br>"
253
+ f"Pitch: {s['pitch_deg']}°<br>"
254
+ f"Roll: {s['roll_deg']}°"
255
+ ),
256
+ showarrow=False
257
+ )
258
+
259
+ fig.add_annotation(
260
+ x=attitude_center_x,
261
+ y=attitude_center_y + 150,
262
+ text="Attitude",
263
+ showarrow=False,
264
+ font=dict(size=16)
265
+ )
266
+
267
+ fig.update_layout(
268
+ title="レンジング / 軌道位置 / 姿勢ビュー",
269
+ height=420,
270
+ margin=dict(l=10, r=10, t=45, b=10),
271
+ showlegend=False,
272
+ xaxis=dict(
273
+ visible=False,
274
+ range=[-650, 1250]
275
+ ),
276
+ yaxis=dict(
277
+ visible=False,
278
+ range=[-650, 650],
279
+ scaleanchor="x",
280
+ scaleratio=1
281
+ ),
282
+ )
283
+ return fig
284
+
285
+ def render_right_panel(target):
286
+ """右半分の衛星可視化"""
287
+ s = st.session_state.sat_state
288
+ with target.container():
289
+ st.subheader("🛰️ Satellite Status")
290
+
291
+ c1, c2, c3 = st.columns(3)
292
+ c1.metric("高度", f"{s['altitude_km']} km")
293
+ c2.metric("速度", f"{s['speed_kms']} km/s")
294
+ c3.metric("バッテリー", f"{s['battery_pct']}%")
295
+
296
+ c4, c5, c6 = st.columns(3)
297
+ c4.metric("距離", f"{s['range_km']} km")
298
+ c5.metric("信号", f"{s['signal_db']} dB")
299
+ c6.metric("モード", s["mode"])
300
+
301
+ c7, c8, c9 = st.columns(3)
302
+ c7.metric("Yaw", f"{s['yaw_deg']}°")
303
+ c8.metric("Pitch", f"{s['pitch_deg']}°")
304
+ c9.metric("Roll", f"{s['roll_deg']}°")
305
+
306
+ st.plotly_chart(make_tracking_figure(), use_container_width=True)
307
+
308
+ health_df = pd.DataFrame(
309
+ {
310
+ "項目": ["Battery", "Signal", "Thermal"],
311
+ "値": [
312
+ s["battery_pct"],
313
+ s["signal_db"],
314
+ min(max(int((80 - abs(s["temp_c"] - 25) * 4)), 0), 100),
315
+ ],
316
+ }
317
+ )
318
+ st.bar_chart(health_df.set_index("項目"))
319
+
320
+ st.caption(
321
+ f"Health: {s['health']} | Last event: {s['last_event']} | Updated: {datetime.now().strftime('%H:%M:%S')}"
322
+ )
323
+
324
+ # ----------------------------
325
+ # UI
326
+ # ----------------------------
327
+ st.title("🛰️ 衛星軌道保持AIシステム(PoC開発中)")
328
+ st.write("⚠️本アプリはデモであり、表示される内容は全てダミーです。")
329
+ st.write("左側で処理フロー、右側で衛星状態を可視化します。")
330
+
331
+ mission_text = st.text_area(
332
+ "ミッション指示",
333
+ value="衛星の軌道を保持してください。",
334
+ height=110,
335
+ )
336
+
337
+ run = st.button("ミッション開始", type="primary")
338
+
339
+ left_col, right_col = st.columns([1, 1], gap="large")
340
+
341
+ with left_col:
342
+ left_header = st.empty()
343
+ progress_box = st.empty()
344
+ status_box = st.empty()
345
+ log_box = st.empty()
346
+ result_box = st.empty()
347
+
348
+ with right_col:
349
+ sat_box = st.empty()
350
+ render_right_panel(sat_box)
351
+
352
+ # ----------------------------
353
+ # 実行
354
+ # ----------------------------
355
+ if run:
356
+ steps = [
357
+ {
358
+ "name": "制御計画/立案",
359
+ "desc": "条件を満たすパラメータ(スラスタセット/噴射秒数)を設定します。",
360
+ "action": lambda text: call_openai(
361
+ f"""
362
+ 以下のミッション条件をもとに、軌道制御計画を立案してください。
363
+ - 使用するスラスタセット
364
+ - 想定される噴射秒数
365
+ - 計画時の前提条件
366
+ - 注意点
367
+
368
+ ミッション条件:
369
+ {text}
370
+ """.strip()
371
+ )
372
+ },
373
+ {
374
+ "name": "残推薬管理/確認",
375
+ "desc": "軌道制御に必要な消費推薬量を見積もります。",
376
+ "action": lambda text: call_openai(
377
+ f"""
378
+ 以下の制御計画をもとに、残推薬管理の観点で評価してください。
379
+ - 想定される推薬消費量
380
+ - 残量への影響
381
+ - 運用上の注意点
382
+ - 地上局向けコメント
383
+
384
+ 制御計画:
385
+ {text}
386
+ """.strip()
387
+ )
388
+ },
389
+ {
390
+ "name": "軌道決定",
391
+ "desc": "観測情報または制御結果をもとに衛星位置・軌道を決定します。",
392
+ "action": lambda text: call_openai(
393
+ f"""
394
+ 以下の情報をもとに、軌道決定結果を日本語で簡潔にまとめてください。
395
+ - 現在の衛星位置・速度の整理
396
+ - 推定される軌道状態
397
+ - 判断根拠
398
+ - 不確かさや注意点
399
+
400
+ 入力情報:
401
+ {text}
402
+ """.strip()
403
+ )
404
+ },
405
+ {
406
+ "name": "制御評価",
407
+ "desc": "軌道制御の結果と効率を評価します。",
408
+ "action": lambda text: call_openai(
409
+ f"""
410
+ 以下の軌道決定結果をもとに、制御評価を行ってください。
411
+ - 制御結果の妥当性
412
+ - 目標達成度
413
+ - 効率の評価
414
+ - 改善点
415
+ - 地上局向けの簡潔な総括
416
+
417
+ 軌道決定結果:
418
+ {text}
419
+ """.strip()
420
+ )
421
+ },
422
+ {
423
+ "name": "レポーティング",
424
+ "desc": "軌道制御の結果と効率をレポーティングします。",
425
+ "action": lambda text: call_openai(
426
+ f"""
427
+ 以下の軌道決定結果をもとに、制御評価レポートを地上局向けに作成してください。
428
+ - 制御の成功可否
429
+ - 効率評価
430
+ - 推薬消費の妥当性
431
+ - 次回運用への示唆
432
+
433
+ 軌道決定結果:
434
+ {text}
435
+ """.strip()
436
+ )
437
+ }
438
+ ]
439
+
440
+ logs = []
441
+ outputs = {}
442
+ left_header.markdown("## 進行状況")
443
+ progress = progress_box.progress(0, text="待機中")
444
+
445
+ with status_box.container():
446
+ with st.status("処理中...", expanded=True) as status:
447
+ current_text = mission_text
448
+
449
+ for i, step in enumerate(steps):
450
+ pct = int(i / len(steps) * 100)
451
+
452
+ update_satellite_state(i)
453
+ render_right_panel(sat_box)
454
+
455
+ progress.progress(pct, text=f"Step {i+1}/{len(steps)}: {step['name']}")
456
+ logs.append(f"⏳ Step {i+1}: {step['name']} 開始")
457
+ log_box.markdown("\n\n".join(logs))
458
+
459
+ st.write(f"### Step {i+1}. {step['name']}")
460
+ st.write(step["desc"])
461
+
462
+ time.sleep(0.7)
463
+ result = step["action"](current_text)
464
+ outputs[step["name"]] = result
465
+ current_text = result
466
+
467
+ logs.append(f"✅ Step {i+1}: {step['name']} 完了")
468
+ log_box.markdown("\n\n".join(logs))
469
+
470
+ with st.expander(f"{step['name']} の出力", expanded=False):
471
+ st.write(result)
472
+
473
+ update_satellite_state(i + 1)
474
+ render_right_panel(sat_box)
475
+ time.sleep(0.5)
476
+
477
+ progress.progress(100, text="ミッション完了")
478
+ status.update(label="完了", state="complete", expanded=True)
479
 
480
+ with result_box.container():
481
+ st.markdown("## 最終レポート")
482
+ st.write(outputs.get("レポーティング", "出力なし"))