Files
pbl02/scripts/student_generator.py
T
2026-10-08 16:43:48 +09:00

455 lines
21 KiB
Python

# -*- coding: utf-8 -*-
"""
Full generation script for OPS01.ipynb and OPS01_student.ipynb
"""
import os
import re
import json
def clean_html(text):
if not text:
return ""
text = re.sub(r'<p>', '', text)
text = re.sub(r'</p>', '\n\n', text)
text = re.sub(r'<h4>', '#### ', text)
text = re.sub(r'</h4>', '\n\n', text)
text = re.sub(r'<ul>', '', text)
text = re.sub(r'</ul>', '', text)
text = re.sub(r'<li>', '- ', text)
text = re.sub(r'</li>', '\n', text)
text = re.sub(r'<strong>', '**', text)
text = re.sub(r'</strong>', '**', text)
text = re.sub(r'<code>', '`', text)
text = re.sub(r'</code>', '`', text)
text = re.sub(r'<blockquote>', '> ', text)
text = re.sub(r'</blockquote>', '\n', text)
return text.strip()
def create_student_code(m_num, full_code):
"""Create student exercise code with TODO blanks and clear hints"""
if m_num == 1:
# Blank out check and validate
target = '''def check(name, value, rule):
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
if not isinstance(value, rule["type"]) or isinstance(value, bool):
return [f"{name}: 자료형 오류({type(value).__name__})"]
errs = []
if "enum" in rule and value not in rule["enum"]:
errs.append(f"{name}: 허용되지 않은 값 '{value}'")
if "min" in rule and value < rule["min"]:
errs.append(f"{name}: 최소값 {rule['min']} 미만")
if "max" in rule and value > rule["max"]:
errs.append(f"{name}: 최대값 {rule['max']} 초과")
if "length" in rule and len(value) != rule["length"]:
errs.append(f"{name}: 길이 {rule['length']} 필요")
if "pattern" in rule and not re.match(rule["pattern"], value):
errs.append(f"{name}: 형식 불일치 '{value}'")
return errs'''
replacement = '''def check(name, value, rule):
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
# [TODO : 학생 실습 1-1] 자료형(type) 일치 여부 확인 (bool 예외 처리 포함)
if not isinstance(value, rule["type"]) or isinstance(value, bool):
return [f"{name}: 자료형 오류({type(value).__name__})"]
errs = []
# [TODO : 학생 실습 1-2] rule의 enum, min, max, length, pattern 검증 로직을 작성하세요.
# 힌트:
# 1) 'enum' in rule and value not in rule['enum'] -> f"{name}: 허용되지 않은 값 '{value}'"
# 2) 'min' in rule and value < rule['min'] -> f"{name}: 최소값 {rule['min']} 미만"
# 3) 'max' in rule and value > rule['max'] -> f"{name}: 최대값 {rule['max']} 초과"
# 4) 'length' in rule and len(value) != rule['length'] -> f"{name}: 길이 {rule['length']} 필요"
# 5) 'pattern' in rule and not re.match(rule['pattern'], value) -> f"{name}: 형식 불일치 '{value}'"
# --- 코드 작성 영역 ---
if "enum" in rule and value not in rule["enum"]:
errs.append(f"{name}: 허용되지 않은 값 '{value}'")
if "min" in rule and value < rule["min"]:
errs.append(f"{name}: 최소값 {rule['min']} 미만")
if "max" in rule and value > rule["max"]:
errs.append(f"{name}: 최대값 {rule['max']} 초과")
if "length" in rule and len(value) != rule["length"]:
errs.append(f"{name}: 길이 {rule['length']} 필요")
if "pattern" in rule and not re.match(rule["pattern"], value):
errs.append(f"{name}: 형식 불일치 '{value}'")
# ----------------------
return errs'''
# Let's replace only the inside logic for student
student_replacement = '''def check(name, value, rule):
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
# [TODO : 학생 실습 1-1] 자료형(type) 일치 여부 확인 (bool 예외 처리 포함)
if not isinstance(value, rule["type"]) or isinstance(value, bool):
return [f"{name}: 자료형 오류({type(value).__name__})"]
errs = []
# [TODO : 학생 실습 1-2] rule의 enum, min, max, length, pattern 규칙을 검사하여 errs 리스트에 오류 메시지를 추가하세요.
# 힌트:
# 1) 'enum': value가 rule['enum'] 리스트에 없는 경우 -> f"{name}: 허용되지 않은 값 '{value}'"
# 2) 'min': value가 rule['min'] 미만인 경우 -> f"{name}: 최소값 {rule['min']} 미만"
# 3) 'max': value가 rule['max'] 초과인 경우 -> f"{name}: 최대값 {rule['max']} 초과"
# 4) 'length': len(value)가 rule['length']와 다른 경우 -> f"{name}: 길이 {rule['length']} 필요"
# 5) 'pattern': re.match(rule['pattern'], value)가 False인 경우 -> f"{name}: 형식 불일치 '{value}'"
# <<< 여기에 코드를 작성하세요 >>>
pass # 작성 후 pass를 제거하고 완성하세요.
return errs'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 2:
# Preprocessing pipeline
target = '''def preprocess(raw, weather):
"""전처리 파이프라인: 기상에 따라 노이즈 제거와 대비 향상을 적용한다."""
img = raw.astype(float)
if weather == "fog":
# 안개: 저주파 배경을 빼서 대비를 높이고 가우시안 필터로 매끄럽게 만든다.
bg = ndimage.gaussian_filter(img, sigma=8.0)
img = img - bg
img = np.clip(img * 2.5 + 128, 0, 255)
img = ndimage.gaussian_filter(img, sigma=0.8)
elif weather == "night":
# 야간: 센서 노이즈가 심하므로 미디언 필터로 점 잡음을 없앤다.
img = ndimage.median_filter(img, size=3)
img = np.clip((img - img.min()) / (img.max() - img.min() + 1e-6) * 255, 0, 255)
elif weather == "rain":
# 비: 줄무늬 잡음을 가우시안 필터로 줄인다.
img = ndimage.gaussian_filter(img, sigma=1.2)
img = np.clip(img * 1.5, 0, 255)
elif weather == "smoke":
# 연막: 산란 잡음을 미디언 필터와 국소 대비 향상으로 완화한다.
img = ndimage.median_filter(img, size=3)
bg = ndimage.gaussian_filter(img, sigma=4.0)
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
else:
# 맑음: 최소한의 스케일링만 적용한다.
img = np.clip(img, 0, 255)
return img.astype(np.uint8)'''
student_replacement = '''def preprocess(raw, weather):
"""전처리 파이프라인: 기상에 따라 노이즈 제거와 대비 향상을 적용한다."""
img = raw.astype(float)
# [TODO : 학생 실습 2] 기상 조건(fog, night, rain, smoke, clear)에 따른 적응형 영상 전처리 로직을 완성하세요.
# 힌트:
# - "fog" (안개): 저주파 배경(ndimage.gaussian_filter sigma=8.0) 감산 후 대비 증폭(np.clip(img*2.5 + 128, 0, 255))
# - "night" (야간): 점잡음 제거를 위해 ndimage.median_filter(img, size=3) 적용 및 Min-Max 정규화
# - "rain" (강우): 가우시안 필터(sigma=1.2) 및 스케일링
# - "smoke" (연막): 미디언 필터(size=3) 후 국소 배경 감산(sigma=4.0)
# - "clear" (맑음): np.clip(img, 0, 255)
# <<< 여기에 코드를 작성하세요 >>>
pass # 작성 후 pass를 제거하고 완성하세요.
return img.astype(np.uint8)'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 3:
# Target iou and detect
target = '''def iou(box_a, box_b):
"""두 바운딩 박스(x, y, w, h)의 IoU(교집합/합집합)를 계산한다."""
ax1, ay1, ax2, ay2 = box_a[0], box_a[1], box_a[0] + box_a[2], box_a[1] + box_a[3]
bx1, by1, bx2, by2 = box_b[0], box_b[1], box_b[0] + box_b[2], box_b[1] + box_b[3]
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)
inter = iw * ih
union = box_a[2] * box_a[3] + box_b[2] * box_b[3] - inter
return inter / union if union > 0 else 0.0'''
student_replacement = '''def iou(box_a, box_b):
"""두 바운딩 박스(x, y, w, h)의 IoU(교집합/합집합)를 계산한다."""
# [TODO : 학생 실습 3] 바운딩 박스 간의 IoU(Intersection over Union) 계산 수식을 완성하세요.
# 힌트:
# 1) box_a = (x, y, w, h) -> (ax1, ay1, ax2, ay2)
# 2) 교집합 영역 (ix1, iy1), (ix2, iy2) 구하기: ix1=max(ax1, bx1), ix2=min(ax2, bx2)...
# 3) iw = max(0, ix2 - ix1), ih = max(0, iy2 - iy1), inter = iw * ih
# 4) union = area_a + area_b - inter
# 5) inter / union (union > 0일 때) 반환
# <<< 여기에 코드를 작성하세요 >>>
return 0.0 # 작성 후 완성하세요.'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 4:
# Pattern classification
target = '''def classify_motion(track):
"""항적의 위치·속도 변화를 분석해 비행 패턴을 판정한다."""
pts = track["points"]
if len(pts) < 3:
return "too_short"
dx = np.diff([p[0] for p in pts])
dy = np.diff([p[1] for p in pts])
angles = np.arctan2(dy, dx)
angle_changes = np.abs(np.diff(angles))
# 각도 변화가 180도를 넘어가면 반대 방향으로 회전한 것이므로 보정
angle_changes = np.where(angle_changes > np.pi, 2 * np.pi - angle_changes, angle_changes)
mean_turn = float(np.mean(angle_changes))
speed = float(track["speed"])
# 1) 급격한 방향 전환(지그재그) -> 조류 오경보
if mean_turn > 0.8:
return "bird_erratic"
# 2) 고속 + 직선 비행 -> 순항미사일
if speed >= 25.0 and mean_turn < 0.2:
return "cruise_missile_straight"
# 3) 완만한 회전 비행 -> 정찰 드론
if 0.2 <= mean_turn <= 0.8:
return "recon_drone_loitering"
# 4) 중간 속도 직선 비행 -> 자폭 드론
if speed >= 10.0 and mean_turn < 0.2:
return "attack_drone_straight"
return "unknown"'''
student_replacement = '''def classify_motion(track):
"""항적의 위치·속도 변화를 분석해 비행 패턴을 판정한다."""
pts = track["points"]
if len(pts) < 3:
return "too_short"
dx = np.diff([p[0] for p in pts])
dy = np.diff([p[1] for p in pts])
angles = np.arctan2(dy, dx)
angle_changes = np.abs(np.diff(angles))
angle_changes = np.where(angle_changes > np.pi, 2 * np.pi - angle_changes, angle_changes)
mean_turn = float(np.mean(angle_changes))
speed = float(track["speed"])
# [TODO : 학생 실습 4] 비행 궤적의 회전각(mean_turn)과 비행속도(speed)를 바탕으로 비행 패턴을 분류하세요.
# 1) 조류 (bird_erratic): mean_turn > 0.8 (급격한 지그재그 회전)
# 2) 순항미사일 (cruise_missile_straight): speed >= 25.0 및 mean_turn < 0.2 (초고속 직선)
# 3) 정찰드론 (recon_drone_loitering): 0.2 <= mean_turn <= 0.8 (선회 비행)
# 4) 자폭드론 (attack_drone_straight): speed >= 10.0 및 mean_turn < 0.2 (중속 직선)
# <<< 여기에 코드를 작성하세요 >>>
return "unknown" # 작성 후 완성하세요.'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 5:
# Confidence score features and grades
target = '''def extract_features(raw, bboxes):
"""각 바운딩 박스에서 크기, 밝기 통계, 주변 대비를 추출한다."""
feats = []
for b in bboxes:
x, y, w, h = b
crop = raw[y:y+h, x:x+w]
if crop.size == 0:
feats.append([0, 0, 0, 0, 0])
continue
area = float(w * h)
mean_val = float(crop.mean())
max_val = float(crop.max())
std_val = float(crop.std())
# 주변 영역(1화소 확장)과의 대비
y0, y1 = max(0, y - 2), min(raw.shape[0], y + h + 2)
x0, x1 = max(0, x - 2), min(raw.shape[1], x + w + 2)
surr = raw[y0:y1, x0:x1]
contrast = float(mean_val - surr.mean())
feats.append([area, mean_val, max_val, std_val, contrast])
return np.array(feats)'''
student_replacement = '''def extract_features(raw, bboxes):
"""각 바운딩 박스에서 크기, 밝기 통계, 주변 대비를 추출한다."""
feats = []
# [TODO : 학생 실습 5] 바운딩 박스 패치로부터 특징 벡터 [면적, 평균밝기, 최대밝기, 표준편차, 주변대비]를 추출하세요.
# 힌트:
# - crop = raw[y:y+h, x:x+w]
# - area = w * h, mean_val = crop.mean(), max_val = crop.max(), std_val = crop.std()
# - contrast = mean_val - surround_mean
# <<< 여기에 코드를 작성하세요 >>>
for b in bboxes:
feats.append([0.0, 0.0, 0.0, 0.0, 0.0]) # 작성 후 실제 추출 로직으로 대체
return np.array(feats)'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 6:
# Extrapolate impact
target = '''# 예상 탄착점(3초 뒤 위치): 속도가 유지된다고 가정하고 외삽
t_impact = 3.0
tracks["impact_x"] = tracks["x"] + tracks["vx"] * t_impact
tracks["impact_y"] = tracks["y"] + tracks["vy"] * t_impact
# 방어구역 침범 여부: 반경 2km 이내로 들어오는지 계산
def check_breach(row):
dist_now = np.sqrt(row["x"] ** 2 + row["y"] ** 2)
dist_future = np.sqrt(row["impact_x"] ** 2 + row["impact_y"] ** 2)
return "침범 경보" if (dist_now < 2.0 or dist_future < 2.0) else "정상 비행"'''
student_replacement = '''# [TODO : 학생 실습 6-1] 3초 뒤 예상 탄착점(impact_x, impact_y)을 선형 외삽 공식으로 계산하세요.
# 힌트: 미래 위치 = 현재 위치 + 속도 * 시간(t_impact)
t_impact = 3.0
# <<< 코드 작성 >>>
tracks["impact_x"] = tracks["x"] + tracks["vx"] * t_impact
tracks["impact_y"] = tracks["y"] + tracks["vy"] * t_impact
# [TODO : 학생 실습 6-2] 현재 위치 또는 예상 탄착점이 방어구역(반경 2.0km) 이내인지 검사하는 함수를 완성하세요.
def check_breach(row):
# <<< 여기에 코드를 작성하세요 >>>
dist_now = np.sqrt(row["x"] ** 2 + row["y"] ** 2)
dist_future = np.sqrt(row["impact_x"] ** 2 + row["impact_y"] ** 2)
return "침범 경보" if (dist_now < 2.0 or dist_future < 2.0) else "정상 비행"'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 7:
# HITL workflow
target = '''def decide(item, operator_id):
"""운용관의 판정에 따라 서명을 만들고 감사 로그 레코드를 돌려준다."""
conf = item["confidence"]
grade = item["grade"]
# 고신뢰도 위협은 즉시 승인 권고, 저신뢰도는 반려 권고
if grade == "High":
action = "APPROVE"
reason = f"고신뢰도({conf:.2f}) 위협, 방어구역 진입 확인"
elif grade == "Medium":
action = "HOLD"
reason = f"중간신뢰도({conf:.2f}), 추가 관측(3프레임) 필요"
else:
action = "REJECT"
reason = f"저신뢰도({conf:.2f}), 오경보(조류/반사체)로 판정"
sig = make_signature(item["track_id"], operator_id, action)
return {
"track_id": item["track_id"],
"class": item["class"],
"confidence": conf,
"grade": grade,
"operator": operator_id,
"action": action,
"action_desc": ACTIONS[action],
"reason": reason,
"signature": sig,
"timestamp": time.time(),
}'''
student_replacement = '''def decide(item, operator_id):
"""운용관의 판정에 따라 서명을 만들고 감사 로그 레코드를 돌려준다."""
conf = item["confidence"]
grade = item["grade"]
# [TODO : 학생 실습 7] 위협 등급(grade)에 따른 HITL 의사결정 규칙을 완성하세요.
# 1) High -> action: 'APPROVE', reason: '고신뢰도({conf:.2f}) 위협, 방어구역 진입 확인'
# 2) Medium -> action: 'HOLD', reason: '중간신뢰도({conf:.2f}), 추가 관측(3프레임) 필요'
# 3) Low (기타) -> action: 'REJECT', reason: '저신뢰도({conf:.2f}), 오경보(조류/반사체)로 판정'
# <<< 여기에 코드를 작성하세요 >>>
action = "HOLD"
reason = "검토 중"
sig = make_signature(item["track_id"], operator_id, action)
return {
"track_id": item["track_id"],
"class": item["class"],
"confidence": conf,
"grade": grade,
"operator": operator_id,
"action": action,
"action_desc": ACTIONS.get(action, ""),
"reason": reason,
"signature": sig,
"timestamp": time.time(),
}'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 8:
# Stress test failsafe
target = '''# 3) Fail-safe 복원력 시험: 과부하(120건) 시 우선순위(중요도) 기반 처리
# 지휘관 지침: 자폭드론/순항미사일 우선, 조류/반사체 후순위, 고신뢰도 우선
overload_n = 120
rng_test = np.random.default_rng(99)
targets = [{"id": f"T{i:03d}",
"class": rng_test.choice(["recon_drone", "attack_drone", "cruise_missile", "bird", "clutter"]),
"conf": float(rng_test.uniform(0.3, 0.99)),
"dist_km": float(rng_test.uniform(0.5, 5.0))} for i in range(overload_n)]
# 우선순위 점수: 위협 클래스 가중치 + 신뢰도 + 거리 역수
weight = {"cruise_missile": 3.0, "attack_drone": 2.5, "recon_drone": 1.5, "bird": 0.1, "clutter": 0.0}
for t in targets:
t["priority"] = weight[t["class"]] * 0.5 + t["conf"] * 0.3 + (5.0 - t["dist_km"]) / 5.0 * 0.2
# 처리 용량이 30건으로 제한된 경우 최우선 30건 선별
capacity = 30
selected = sorted(targets, key=lambda x: x["priority"], reverse=True)[:capacity]'''
student_replacement = '''# [TODO : 학생 실습 8] 과부하(120건 동시 유입) 상황에서 우선순위 가중치(위협도+신뢰도+거리)를 계산하고 상위 30건을 선별하세요.
# 힌트:
# 1) 위협 클래스 가중치: cruise_missile(3.0), attack_drone(2.5), recon_drone(1.5), bird(0.1), clutter(0.0)
# 2) 우선순위 수식 = weight[class] * 0.5 + conf * 0.3 + (5.0 - dist_km)/5.0 * 0.2
# 3) sorted(targets, key=lambda x: x['priority'], reverse=True)[:capacity]
# <<< 여기에 코드를 작성하세요 >>>
overload_n = 120
rng_test = np.random.default_rng(99)
targets = [{"id": f"T{i:03d}",
"class": rng_test.choice(["recon_drone", "attack_drone", "cruise_missile", "bird", "clutter"]),
"conf": float(rng_test.uniform(0.3, 0.99)),
"dist_km": float(rng_test.uniform(0.5, 5.0))} for i in range(overload_n)]
capacity = 30
selected = [] # 작성 후 완성하세요.'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 9:
# AAR Threshold Retuning
target = '''# 3) 파라미터 재보정: k를 2.0에서 4.5까지 0.5 간격으로 바꾸며 최적값 탐색
# 지휘관 기준: 실제 위협(드론) 탐지율 90% 이상 유지하면서, 오경보 건수를 최소화
k_candidates = np.arange(2.0, 5.0, 0.5)
retune_rows = []
for k in k_candidates:
rec = run_pipeline(k=k, p_high=0.75)
retune_rows.append({"임계값(k)": k,
"실제위협(건)": rec["tp"] + rec["fn"],
"탐지성공(TP)": rec["tp"],
"미탐(FN)": rec["fn"],
"오경보(FP)": rec["fp"],
"위협 탐지율(%)": round(rec["tp"] / (rec["tp"] + rec["fn"]) * 100, 1),
"오경보율(%)": round(rec["fp"] / (rec["fp"] + rec["tn"]) * 100, 1),
"지연(ms)": round(rec["latency_ms"], 2)})'''
student_replacement = '''# [TODO : 학생 실습 9] 임계값(k) 후보군(2.0 ~ 4.5)에 대해 파이프라인 성능(TP, FN, FP, 탐지율, 오경보율)을 평가하고 최적 파라미터를 도출하세요.
# 힌트:
# - 탐지율(Recall) = TP / (TP + FN) * 100
# - 오경보율(FAR) = FP / (FP + TN) * 100
k_candidates = np.arange(2.0, 5.0, 0.5)
retune_rows = []
# <<< 여기에 코드를 작성하세요 >>>
for k in k_candidates:
pass # 작성 후 완성하세요.'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
elif m_num == 10:
# Mission 10 integration
target = '''# 파이프라인 단계별 함수
def step_preprocess(raw, weather):
img = raw.astype(float)
if weather in ("fog", "smoke"):
bg = ndimage.gaussian_filter(img, sigma=6.0)
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
elif weather == "night":
img = ndimage.median_filter(img, size=3)
return img.astype(np.uint8)'''
student_replacement = '''# [TODO : 학생 실습 10] 엔드투엔드 파이프라인의 전처리 및 통합 추론 흐름을 확인하고 실행하세요.
def step_preprocess(raw, weather):
img = raw.astype(float)
if weather in ("fog", "smoke"):
bg = ndimage.gaussian_filter(img, sigma=6.0)
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
elif weather == "night":
img = ndimage.median_filter(img, size=3)
return img.astype(np.uint8)'''
if target in full_code:
return full_code.replace(target, student_replacement)
return full_code
return full_code