SEAM Studio
Guide 07 · SEAM Studio docs

레이더 센싱: RCS 타깃, 에코 솔브, 도플러

SEAM Studio에서는 어떤 액터(차량, 보행자, UAV, 커스텀 객체)든 레이더 센싱 타깃으로 지정할 수 있습니다. 지정한 타깃에 대해 Sionna RT의 레이더 단면적 솔버(sionna.rt.rcs.RCSSolver, sionna-rt 2.2에서 추가)로 TX → 타깃 → RX 에코 경로를 계산합니다. 에코마다 도플러 편이가 따로 붙으므로, 움직이는 타깃은 뷰포트에서 색으로 구분되는 속도 신호로 나타납니다. sionna-rt 2.2가 없으면 전체 흐름이 Mock 백엔드(바이스태틱 레이더 방정식)로 돌아가므로 GPU 없이도 써 볼 수 있습니다.

Sample Demo(v0.1.14 이상)는 바로 센싱을 해 볼 수 있게 되어 있습니다. 드론 타깃 (uav_001, TR 38.901 uav-small-size, 40 m 높이에서 10 m/s로 90 m짜리 L자 경로를 난 뒤 제자리 비행)과 옥상 TX와 같은 위치의 센싱 수신기(tx_001_rx, "TX 1 sensing RX")가 들어 있어서, 아래 API 예제는 sample_demo에서 그대로 돌아갑니다. v0.1.15부터는 가로등 마스트 위의 센싱 사이트 두 개(tx_002와 tx_002_rx, tx_003와 tx_003_rx)와 두 번째 저장 구성 sensing_fr1(3.5 GHz, 20 MHz)도 들어 있어서, 6절의 시나리오가 별도 설정 없이 드론을 탐지하고 추적합니다. 이전 버전이 만든 프로젝트는 디바이스·액터·구성이 그대로입니다(업그레이드가 프로젝트를 고쳐 쓰지는 않습니다). 지금의 데모를 쓰려면 옆에 새 데모를 만들고, 페이지를 새로 고친 뒤 프로젝트 셀렉트에서 Sample Demo v2를 고르세요 (API 예제에서는 sample_demo 대신 sample_demo_v2).

curl -X POST http://127.0.0.1:8000/api/projects -H "Content-Type: application/json" \
  -d '{"name": "Sample Demo v2", "template": "demo", "project_id": "sample_demo_v2"}'

소스 체크아웃이라면 projects/sample_demo.seam을 지우고 다시 시작해도 됩니다. 추가된 내용을 모두 담아 다시 복사됩니다. 아니면 내 프로젝트에 타깃과 같은 위치의 RX를 직접 추가하세요(2절, 3절).

1. RCS란

2. 액터 바인딩

액터를 선택하고 Inspector → Sensing target을 엽니다. 바인딩은 액터의 actor.sensing에 저장되며(../scene_format.ko.md 참고), null이면 타깃이 아닙니다.

Model은 둘 중 하나입니다.

object_type모델 타입기본 적용 kind평균 모노스태틱 σ_M (dBsm)산란점TR 38.901 기본 크기 L×W×H (m)
uav-small-size1uav−12.8110.3 × 0.4 × 0.2
uav-large-size2—−5.8511.6 × 1.5 × 0.7
human1, 2human−1.3710.5 × 0.5 × 1.75
vehicle-single-sp2—11.2515.0 × 2.0 × 1.6
vehicle-multi-sp2car11.255(좌, 뒤, 우, 앞, 지붕)5.0 × 2.0 × 1.6
agv-single-sp2—−4.2510.5 × 1.0 × 0.5
agv-multi-sp2—−4.2550.5 × 1.0 × 0.5

"vehicle"나 "agv"만 쓰면 유효하지 않습니다. Sionna는 단일 산란점 또는 다중 산란점 변형 중 하나를 요구합니다. 타입이 정의하지 않는 모델 타입(예: uav-small-size에 모델 2)은 저장할 때 422로 거부됩니다.

그 밖의 필드(따로 적지 않으면 두 모델 공통):

필드의미
size_m타깃 직육면체 [length, width, height] m. 비우면 위 표의 TR 38.901 기본값이 아니라 액터 자신의 박스 크기(shape.size_m)를 씁니다. 메시 액터는 직접 지정해야 합니다.
velocity_m_s타깃의 월드 좌표계 속도. 비우면 t = 0에서 액터 궤적의 접선 속도를 쓰고, 정지 액터는 정지 상태입니다.
random_componentsTR 38.901 전용: 방향 쌍마다 σ_S, XPR, 초기 위상을 무작위로 뽑습니다(끄면 결정론적 RCS).
enabled끄면 바인딩은 유지하되 솔브에서 제외합니다.

타깃 직육면체의 중심은 액터 바닥 위치에 높이의 절반을 더한 지점이고, 방향은 액터의 [yaw, pitch, roll]을 따릅니다. 속도를 궤적에서 가져오는 경우(웨이포인트가 있는 액터에서 velocity_m_s를 비운 경우)에는 자세도 궤적에서 가져옵니다. 바닥 위치는 t = 0의 궤적 위치, yaw는 진행 방향이 되어 재생(playback) 첫 프레임과 같아지므로, 산란 로브와 도플러가 같은 움직임을 기술합니다. velocity_m_s를 직접 지정하면 작성한 자세를 그대로 씁니다. 궤적이 있는 다른 액터들은 솔브에서 작성한 자세에 머물지만 t = 0의 속도로 움직이므로, 그 액터에서 반사되는 구간에도 도플러가 붙습니다. 일반 경로 솔브도 같은 규칙을 따릅니다(4절).

3. 센싱 솔브 실행

TX와 RX를 최소 하나씩 배치합니다. 모노스태틱 센싱이면 RX를 TX 위치에 두고, 다른 위치의 RX는 바이스태틱 구성이 됩니다. Results 모드에서 Actions ▾ → Sensing solve를 고르면, 바인딩이 켜진 모든 액터를 대상으로 솔브가 돌고 결과가 sensing 결과 세트로 저장됩니다(다른 솔브처럼 실행 이력에 나타남).

같은 솔브를 API로 실행하려면:

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/sensing \
  -H "Content-Type: application/json" \
  -d '{"config": {"backend": "auto", "frequency_hz": 28e9}, "include_comm_paths": false}'
curl http://127.0.0.1:8000/api/projects/sample_demo/results/sensing   # 가장 최근 저장 결과
요청 필드의미
config_id / config솔버 설정. /simulate/paths와 똑같습니다.
tx_ids / rx_ids설정의 디바이스 선택을 덮어씁니다.
target_actor_ids솔브 대상을 이 액터들로 제한합니다(기본: 켜진 바인딩 전부).
max_depthRCS 깊이. 산란 이벤트 자체도 하나로 셉니다. 1이면 직접 경로만 있고, 각 구간(leg)은 최대 max_depth − 1번 반사될 수 있습니다. 기본값 max(1, config.max_depth).
samples_per_sp산란점 하나당 발사하는 레이 수(기본 1 000 000).
include_comm_paths일반 경로 솔브도 함께 돌려 그 경로들을 에코 뒤에 붙입니다(target_id null, id path_…). 이 솔브에서는 각 타깃이 흡수체 직육면체로 액터를 대신하므로(Sionna PathSolver가 센싱 타깃을 다루는 방식), 통신 경로가 타깃 자신의 메시에서 또 반사되는 일이 없습니다.

백엔드. sionna는 RCSSolver(LoS, 정반사, 투과 구간)를 실행합니다. mock은 타깃 중심의 산란점 하나에 대해 바이스태틱 레이더 방정식을 계산합니다(LoS 구간만, 차폐 없음). 여기에 디바이스마다 타깃 쪽 소자 이득과 §8의 구간별 편파 항을 더합니다 (v0.1.14부터. 그 전의 mock 에코는 등방성이었습니다). auto는 sionna-rt 2.2 이상이 설치돼 있고 Dr.Jit 백엔드가 동작하면(CUDA GPU, 또는 CPU의 LLVM. INSTALL 참고) sionna를 고르고, 아니면 경고와 함께 mock으로 넘어갑니다. 구버전 sionna-rt에서 "backend": "sionna"를 명시하면 409(sensing requires sionna-rt>=2.2)로 응답합니다. 바인딩된 액터가 없거나, 알 수 없는 액터·디바이스 id를 주거나, TX/RX가 없으면 400입니다.

안테나. Sionna는 처음 선택한 TX(RX)의 안테나를 모든 TX(RX)에 적용하고, 안테나가 서로 다르면 경고합니다. mock은 디바이스마다 자기 소자 패턴과 방향(그리고 §8의 편파 항)을 적용합니다.

4. 도플러 읽기

에코 경로마다 doppler_hz가 붙으며, 경로가 짧아지는 중(타깃 접근)이면 양수입니다. 산란점이 하나일 때 Sionna는 다음과 같이 계산합니다.

f_D = (v_tx · k̂₁ − v_rx · k̂₂ + v_t · (k̂₂ − k̂₁)) / λ

k̂₁은 TX → 타깃, k̂₂는 타깃 → RX 단위 벡터입니다. 모노스태틱 레이더에서 속도 v로 멀어지는 타깃은 −2v/λ가 됩니다(28 GHz, 10 m/s에서 −1868 Hz).

뷰포트에서는 Results 패널의 Show: Sensing을 체크합니다. 에코 경로가 도플러 색(접근 빨강, 정지 회색, 이탈 파랑)으로 그려지고 범례가 붙으며, 산란점마다 마커가 표시됩니다. 센싱 카드에는 타깃별 정보(모델, 산란점, σ, 경로 수)와 가장 강한 에코들(전력, 지연, 도플러)이 나옵니다.

통신 경로. Sionna 백엔드에서는 경로 솔브도 액터에 속도를 줍니다. 그래서 TX와 RX가 정지해 있어도, 움직이는 액터에서 반사되는 통신 경로에는 도플러 편이가 생깁니다. 속도 v로 움직이는 액터에서 한 번 반사될 때마다 v · (k̂_out − k̂_in) / λ가 더해지는데, k̂_in은 반사점으로 들어오는 방향, k̂_out은 반사점에서 나가는 방향의 단위 벡터입니다. 예를 들어 8 m 떨어진 정지 TX/RX 쌍에서 20 m 앞에 있는 차량이 10 m/s로 멀어지면, 3.5 GHz에서 차량 면 반사 경로는 약 −228 Hz, LoS와 지면 반사 경로는 0 Hz가 됩니다. 액터가 받는 속도는 다음과 같습니다.

움직이는 액터에 붙은 디바이스(attached_device_ids)는 위의 모든 솔브와 센싱 솔브에서 액터와 같은 속도로 함께 움직입니다. Simulate scenario가 아닌 솔브에서는 디바이스에 직접 지정한 velocity_m_s가 우선합니다. 그래서 차량에 탄 UE는 LoS를 포함한 모든 경로에 도플러 편이가 생깁니다.

일반 솔브는 이 도플러를 paths와 순서가 맞는 리스트 metadata.doppler_hz로 알려 줍니다. 이 리스트는 TX나 RX에 속도가 있거나 씬의 액터가 하나라도 t = 0에 움직이면 붙습니다. 그 액터에 닿는 경로가 없어도 붙으며, 이때 경로들은 0 Hz입니다. 전부 정지한 솔브에는 예전처럼 도플러 필드가 없습니다. 센싱 결과의 통신 경로에는 경로마다 doppler_hz로도 붙습니다. mock 백엔드는 경로 솔브에 도플러가 없고, 다른 engine(서브프로세스 워커)은 속도를 적용하지 않습니다.

5. 내보내기

6. 시간에 따른 센싱: 탐지와 다중 스태틱 융합

센싱 솔브 한 번은 스냅샷 하나입니다. 센싱을 켠 Simulate scenario는 프레임마다 그 프레임의 액터 자세·속도(와 액터에 탄 디바이스 위치)로 센싱 솔브를 돌립니다. 에코는 링크별 탐지로 바뀌고, 탐지된 링크들을 융합해 위치·속도를 추정한 뒤 실제 액터 궤적과 비교해 오차를 냅니다.

실행. Results 모드에서 Scenario playback을 열고 Include paths 아래의 Sensing (ISAC)를 체크한 뒤 임계값, CPI, 적분 펄스 수(v0.1.15부터 폼 기본값 4096)와 Sensing receivers(기본 Auto, 아래 참고)를 정하고 실행합니다. 센싱 바인딩이 켜진 액터가 하나도 없으면 체크박스가 비활성화됩니다. 폼은 Simulation 패널의 구성으로 푸는데, 프로젝트를 열 때 이 구성은 첫 번째 저장 구성(Sample Demo에서는 default)에서 옵니다. 그래서 아래 데모 실행을 폼에서 하려면 먼저 그 패널의 Preset 드롭다운에서 저장 구성 Sensing demo (3.5 GHz) 를 고르세요. 그러면 sensing_fr1이 그대로 쓰이고, 결과의 simulation_config_id도 아래 API 호출처럼 sensing_fr1이 됩니다. API에서는 시나리오 요청에 sensing 블록을 더합니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/scenario \
  -H "Content-Type: application/json" \
  -d '{"config_id": "sensing_fr1", "num_frames": 19, "dt_s": 0.5, "include_paths": false,
       "sensing": {"enabled": true, "threshold_db": 13, "cpi_s": 0.01,
                   "cpi_pulses": 4096, "measurement_noise": true,
                   "tracking": {"enabled": true}}}'

v0.1.15 이전에 만든 Sample Demo에는 사이트가 하나뿐이고 sensing_fr1도 없습니다(이 요청은 404를 돌려주고, Preset 드롭다운에 저장 구성이 나오지 않습니다). 도입부처럼 새 데모를 만들거나, 사이트 두 개를 직접 추가한 뒤 "config_id": "sensing_fr1"을 인라인 "config": {"frequency_hz": 3.5e9, "bandwidth_hz": 2e7, "noise_figure_db": 7, "max_depth": 2}로 바꾸세요.

Sample Demo에서는 이것으로 드론의 9초 비행을 덮습니다. 데모에는 TRP가 세 개 있고, 각각 30 dBm iso TX와 같은 위치의 센싱 RX로 이루어집니다. 건물 b01 옥상 (−9, 7, 10.5) m의 tx_001, 가로등 마스트 위 (30, −30, 10) m의 tx_002와 (35, 35, 10) m의 tx_003입니다. Auto가 이 세 RX를 레이더 수신기로 잡으므로 프레임마다 링크가 9개(모노스태틱 3, 바이스태틱 6) 생기고, 서로 다른 기하는 6개입니다. 예제는 데모의 두 번째 저장 구성 sensing_fr1(3.5 GHz, 20 MHz, NF 7 dB, 최대 깊이 2)을 씁니다. 28 GHz / 100 MHz에서는 소자 하나로 받는 드론 에코가 4096 펄스(+36 dB)를 적분해도 13 dB 임계값에 못 미칩니다. λ²에서 18 dB를 잃고(아래 FR1 대 mmWave), 대역폭이 5배라 잡음도 7 dB 더 들어오기 때문입니다. 3.5 GHz에서는 비행 내내 모든 링크가 15–33 dB이고, 건물과 나무에 RF 재질이 있든 없든 모든 프레임의 모든 링크에 직접 에코가 있습니다. 측정값(잡음 시드 0):

metadata.sensing.targets.uav_001mocksionna (sionna-rt 2.2.0, CUDA)
detection_rate0.947 (19프레임 중 18)0.947 (19프레임 중 18)
link_detection_rate0.4390.439
frames_ge3_links1515
ok_frames (융합)1010
median_position_error_m (융합)0.43 m0.79 m
median_gdop1.621.62
tracked_frames / lost_frames19 / 019 / 0
median_track_position_error_m (EKF)0.36 m0.38 m
p90_track_position_error_m2.89 m2.98 m
median_track_velocity_error_m_s0.66 m/s0.66 m/s

놓친 링크는 모두 mti_rejected입니다. 3.5 GHz에서 100 Hz 노치는 모노스태틱 링크의 시선 방향 속도 4.28 m/s 미만을 보지 못하는데, 드론 속도 10 m/s에 비하면 작지 않은 몫입니다. t = 9 s에는 드론이 제자리 비행이라 9개 링크가 모두 걸러집니다(탐지가 없는 유일한 프레임). t = 1.5–3 s에는 TX 2와 TX 3 사이의 링크만 남고(t = 5 s에는 TX 1과 TX 3 사이), 사이트가 둘뿐이라 융합은 diverged를 냅니다(융합 참고). t = 4–4.5 s에는 TX 1의 모노스태틱 링크 하나만 남습니다. 이런 프레임은 EKF가 트랙을 이어 갑니다 (frames_track_without_fusion = 9). 트랙은 t = 0에 7.6 m 빗나간 링크 5개짜리 융합에서 시작하고(GDOP 2.8: 드론이 세 사이트가 이루는 삼각형 밖에서 출발), t = 1.5–3 s에는 1.3–2.5 m, t = 3.5 s부터는 0.4 m 오차를 보이며, 모서리(t = 5.5 s, init)에서 다시 시작한 뒤 두 번째 구간 내내 0.1–0.4 m 안에 머뭅니다.

이 가이드의 다른 예제는 "config_id": "default"(28 GHz)를 그대로 씁니다. 이 구성에서 4096 펄스로는 가장 좋은 드론 링크가 7.7 dB에 그쳐 아무것도 탐지하지 못하지만, 10 ms CPI 전체인 B · cpi_s = 10⁶ 샘플(+60 dB)을 적분하면 드론을 탐지합니다. 위 요청에서 "config_id": "default", "cpi_pulses": 1000000으로 바꾸면 Sionna에서 19프레임 중 18프레임을 탐지하고, ok 융합 18회, 융합 오차 중앙값 0.11 m, 트랙 오차 중앙값 0.08 m가 나옵니다(100 MHz의 거리 셀은 15 m가 아니라 1.5 m). 그 대역폭에서 10 ms CPI가 적분할 수 있는 최대가 10⁶입니다.

결과는 평범한 scenario 결과입니다. 프레임마다 sensing: {echoes, links, estimates}가 붙고, 실행 요약은 metadata.sensing에 들어갑니다. sensing이 없거나 enabled: false이면 결과는 예전과 같습니다(프레임의 sensing은 null). 통신과 센싱은 백엔드 하나를 함께 씁니다. auto는 다른 시나리오와 똑같이 정해지고, 센싱이 없는 백엔드(sionna-rt 2.2 미만)는 mock으로 넘어가지 않고 409로 응답합니다. TX나 RX가 없으면 400이고, 바인딩된 액터가 없거나 target_actor_ids에 모르는 id가 있거나 sensing_rx_ids에 선택된 RX가 아닌 id가 있어도 400입니다. 실행 중에는 프로젝트별 솔브 잠금을 잡고 프레임마다 진행률을 알리며, Cancel도 됩니다.

센싱을 켠 시나리오는 include_paths와 상관없이 모든 프레임의 에코를 저장합니다. sionna에서 링크 16개면 프레임당 약 45 KB(61프레임에 약 2.6 MB, 1 000프레임에 약 45 MB)이고, 브라우저도 이를 불러옵니다. (v0.1.14부터 결과를 압축 JSON으로 저장합니다. 이전 버전의 들여쓰기 파일은 이 크기의 약 두 배였습니다.)

sensing 필드기본값의미
enabledfalse프레임별 센싱 솔브를 돌립니다.
threshold_db13적분 후 SNR의 탐지 임계값(dB, −30…60).
cpi_s0.01코히어런트 처리 구간(CPI, 초). 도플러 분해능 = 1 / CPI.
cpi_pulses1 (폼: 4096)코히어런트하게 적분하는 펄스 수 또는 OFDM 자원 요소 수. 이득 = 10·log10(N).
mti_min_doppler_hznullMTI 노치. |f_D − f_nodes|가 이보다 작으면 버립니다(TX/RX가 정지해 있으면 f_nodes = 0, 아래 MTI 참고). null = 1 / CPI(도플러 셀 하나), 0이면 MTI를 끕니다.
include_comm_pathsfalse프레임마다 타깃을 흡수체로 둔 통신 경로도 풀어 echoes 뒤에 붙입니다(target_id null). 프레임당 솔브가 하나 늘어납니다.
target_actor_idsnull이 액터들만 봅니다(기본: 켜진 바인딩 전부).
sensing_rx_idsnull바이스태틱 레이더 수신기. null = 자동: 선택한 TX에서 1 m 안에 있는 선택된 RX(§7/§8과 같음), 없으면 선택된 RX 전부(v0.1.13). 통신 링크는 여전히 모든 RX를 덮습니다. 모르는 id는 400.
samples_per_sp / max_depth1 000 000 / null프레임별 센싱 솔브에 그대로 넘깁니다(§3과 같음).
measurement_noisefalse융합에 들어가는 거리·도플러에 가우스 잡음을 더합니다(아래).
noise_seed0그 잡음의 시드. 시드가 같으면 숫자도 같습니다.
trackingnull프레임에 걸친 EKF 추적(아래 추적(EKF) 참고). {"enabled": true}에 process_accel_sigma_m_s2(2), gate_chi2(16), coast_max_frames(10), max_position_std_m(25), use_as_prior(true)를 골라 더합니다. null이나 enabled: false면 추적하지 않습니다.
pfanull링크별 pd의 오경보 확률(§9). null이면 링크 보고에 Pd가 붙지 않습니다.
detectorSwerling 1, 몬테카를로 없음그 pd의 모델, 그리고 에코가 있는 링크마다 몬테카를로 pd_mc를 낼 monte_carlo_trials(§9, pfa 필요). 탐지 판정 자체는 그대로 SNR ≥ threshold_db입니다.

센싱 수신기. 레이더 링크는 센싱 수신기만 이루고, 선택된 RX는 모두 같은 프레임에서 통신 링크를 그대로 유지합니다. 폼에서 Auto(기본)를 두면 아무것도 보내지 않고, 목록에서 디바이스를 고르면 sensing_rx_ids를 보냅니다. Auto는 §7/§8의 규칙입니다. 선택한 TX에서 1 m 안에 있는 선택된 RX(모노스태틱/TRP 수신기)가 하나라도 있으면 그것들, 없으면 선택된 RX 전부입니다. 정해진 목록과 규칙(explicit, colocated, all_rx)은 metadata.sensing.sensing_rx_ids와 sensing_rx_rule에 저장됩니다. v0.1.14: 같은 위치의 RX가 있는 프로젝트는 이제 기본으로 그 RX만 레이더 수신기로 씁니다. 예전에는 선택된 RX 전부(UE 포함)가 바이스태틱 레이더 수신기였습니다. 같은 위치의 RX가 없는 프로젝트는 v0.1.13과 똑같이 돕니다.

탐지. 링크(TX → 센싱 RX)와 타깃마다 가장 강한 직접 에코(TX → 산란점 → RX, 다른 반사 없음)를 보고합니다. 직접 에코가 없으면 종류와 상관없이 가장 강한 에코를 보고하고 multipath로 표시합니다. SNR은 다음과 같습니다.

SNR = P_echo − (−174 + 10·log10(B) + NF) + 10·log10(cpi_pulses) [dB]

B는 config.bandwidth_hz, NF는 config.noise_figure_db입니다. cpi_pulses는 CPI 동안 코히어런트하게 적분하는 샘플 수로, 레이더 펄스나 OFDM 자원 요소(부반송파 × 심볼)를 뜻합니다. CPI 전체를 써도 B · cpi_s가 상한입니다(20 MHz × 10 ms = 2·10⁵, +53 dB). SNR ≥ threshold_db이고 |f_D − f_nodes| ≥ mti_min_doppler_hz(아래)이면 그 링크는 탐지됩니다.

MTI. 노치 기본값은 도플러 셀 하나, 즉 1 / CPI(10 ms에서 100 Hz)입니다. 그래서 모노스태틱 블라인드 속도 λ · f_min / 2보다 느린 것은 모두 걸러집니다. 10 ms CPI 기준으로 3.5 GHz에서 4.28 m/s, 28 GHz에서 0.54 m/s입니다. TX와 RX가 정지해 있으면 정지한 씬의 반사는 0 Hz에 있으므로, 드론을 건물과 갈라 주는 것이 MTI입니다. TX나 RX가 움직이는 액터에 실려 있으면 정지한 씬의 도플러도 함께 밀리므로, 노치를 그만큼 옮겨 잡습니다. f_nodes = (v_tx · k̂_dep − v_rx · k̂_arr) / λ는 에코 자신의 경로 위에 정지한 산란체가 있을 때 받을 도플러입니다(k̂_dep = TX → 첫 반사점, k̂_arr = 마지막 반사점 → RX, 정지한 노드면 0). 그래서 움직이는 레이더에서 본 정지 비행 드론은 걸러지고, 레이더와 나란히 나는 드론은 탐지됩니다. 보고되는 doppler_hz는 원래의 f_D 그대로입니다. 통신 경로와 클러터는 애초에 타깃 에코가 아니어서 어느 쪽이든 탐지로 세지 않습니다. MTI를 끄려면 mti_min_doppler_hz: 0으로 두세요.

융합. 타깃과 프레임마다, 탐지된 직접 에코를 SNR 순으로 정렬하고 같은 두 지점을 잇는 링크(방향과 상관없이, 서로 뒤바뀐 쌍이나 반복된 링크)는 가장 강한 것 하나만 남깁니다. 지점은 1 m 안으로 이어지는 TX/RX 위치의 묶음으로, §7과 §8이 쓰는 같은 위치 규칙입니다. 그래서 TX에서 1 m까지 떨어진 센싱 RX는 같은 초점입니다(v0.1.12는 0.5 m 안의 초점을 묶었습니다). 기하가 서로 다른 링크가 3개 이상이면 바이스태틱 거리 합

|p − TX_i| + |p − RX_i| = R_i, R_i = c · τ_i

을 가우스–뉴턴으로 풀어 위치 p를 구합니다. R_i 하나하나는 초점이 TX_i, RX_i인 타원체입니다(모노스태틱 링크면 구). 옥상 TRP들은 거의 한 평면에 있어서, 타원체들이 그 평면 반대편 거울상 위치에서도 만납니다. 그래서 솔버는 이전 프레임 추정치(속도만큼 앞으로 옮긴 값)와 TRP 위·아래의 점에서 각각 출발하고, 이전 추정치가 없으면 링크마다 중점 위의 점에서도 출발합니다. 비용이 같으면 이전 추정치에 가까운 후보를 고릅니다. 이전 추정치가 없으면 car나 human 타깃은 지면(z = 0)에 가장 가까운 후보를, 그 밖에는 가장 높은 후보를 고릅니다(드론은 TRP보다 위에 있다고 가정). measurement_noise를 켜면 비용이 최솟값에서 Δχ² ≤ 9 이내인 후보(½·Σr²의 차가 4.5σ̄² 이내, σ̄²는 링크별 σ² = ((c/B)/√(2·SNR))²의 평균)도 비용이 같은 것으로 봅니다. 거리에 잡음이 있으면 거울상이 우연히 조금 더 잘 맞는 경우가 많기 때문입니다. 링크가 정확히 3개이면 모든 근이 정확히 맞아서 비용으로는 고를 수 없습니다. 이전 추정치가 없는데 지하가 아닌 (z ≥ −2 m) 서로 다른 근이 둘 이상이면 추정치는 diverged(모호함)가 됩니다. 속도는 측정 도플러를 선형 최소제곱으로 맞춰 구하며, 부호 규약은 §4와 같습니다(양수 = 접근).

λ · f_D,i + v_rx · k̂₂ − v_tx · k̂₁ = v_t · (k̂₂ − k̂₁)

노드 두 개만 쓰는 링크 세 개(TRP A 모노스태틱, B 모노스태틱, A→B)는 퇴화합니다. 세 타원체가 모두 A–B 축에 대해 대칭이라 해가 원을 이루므로, 추정치는 diverged가 됩니다.

출력 읽기.

속도 참값. velocity_true(와 솔버가 쓰는 도플러)는 궤적 접선 × 속력입니다. 정확히 경유점 시각이면 나가는 구간의 속도, once 궤적의 끝에서는 0(액터가 멈춤), pingpong 반환점에서는 반대 방향 구간의 속도입니다. v0.1.14 이전에는 이런 프레임이 두 구간의 평균(10 m/s로 90° 꺾이면 7.07 m/s)이나 속력의 절반을 보고했으므로, 경유점 프레임의 도플러와 velocity_true는 v0.1.14에서 바뀌었습니다.

measurement_noise를 끄면 측정값이 솔버의 정확한 지연과 도플러 그대로라서, ok 추정치는 반올림 오차 수준으로 정확합니다(점검용으로 쓸모가 있습니다). 예외는 거울상과 정확히 비기는 경우뿐입니다. 융합한 링크의 노드가 모두 한 평면 위에 있으면(노드가 세 개면 늘 그렇습니다) 그 평면 반대편의 거울상도 정확히 맞고, 어느 쪽을 고를지는 위의 동률 규칙이 정합니다. measurement_noise를 켜면 거리와 도플러에 σ = 셀 / sqrt(2 · SNR)의 잡음이 붙습니다. 셀은 거리가 c/B, 도플러가 1/CPI입니다. 잡음 시드는 프레임·링크·타깃마다 정해지고, 탐지 판정은 항상 정확한 값으로 합니다.

FR1 대 mmWave. 에코 SNR은 다음과 같습니다.

SNR = P_t G_t G_r λ² σ N_int / ((4π)³ R_t² R_r² · kTB · NF)

안테나·전력·대역폭이 같으면 λ² 항 때문에 28 GHz가 3.5 GHz보다 20·log10(8) = 18.06 dB 손해를 봅니다. 모노스태틱, P_t = 43 dBm, 등방성 안테나, uav-small-size(σ = −12.81 dBsm), B = 20 MHz, NF 7 dB, cpi_pulses 4096(+36.12 dB) 조건에서는 다음과 같습니다.

fλ (m)200 m에서 P_r200 m에서 SNR13 dB 도달 거리MTI 블라인드 속도(10 ms)
3.5 GHz0.0857−116.17 dBm13.94 dB(탐지)211 m4.28 m/s
28 GHz0.0107−134.23 dBm−4.12 dB(놓침)74.6 m0.54 m/s

그래서 28 GHz의 모노스태틱 탐지 거리는 10^(−18.06/40) = ×0.354로 줄어듭니다. 안테나 배열이 이 손실을 되찾아 줍니다. 이 절의 프레임별 탐지는 여전히 단일 소자로 계산하지만 (§11 한계 참고), §7은 실제 코드북 빔을 합성하고 §8의 steered 모드는 이상적인 배열 이득을 더합니다. 반대로 mmWave의 노치는 속도 기준으로 8배 좁습니다.

추적(EKF)

융합에는 한 프레임 안에 서로 다른 링크 세 개가 필요합니다. sensing 블록에 "tracking": {"enabled": true}를 넣으면(폼에서는 Tracking (EKF)) 타깃마다 등속 확장 칼만 필터가 추정치를 프레임에서 프레임으로 이어 갑니다. 탐지 링크가 한두 개뿐인 프레임에서도 추정치를 다듬고, 하나도 없는 프레임에서도 예측은 이어집니다.

그러면 estimates[]의 각 행에 다음이 붙습니다.

필드의미
track_statusnone(아직 트랙 없음), init(이 프레임의 융합에서 시작 또는 재시작), tracking(스칼라를 하나 이상 받음), coasting(하나도 못 받음), lost(버려짐, 다음 시작까지)
track_position, track_velocity사후 상태(none과 lost에서는 null)
track_position_error_m, track_velocity_error_m_sposition_true, velocity_true와의 오차
track_position_std_msqrt(trace(P_pos)). 필터가 기대하는 3차원 RMS 오차로, track_position_error_m과 견줄 수 있습니다
track_updates, track_gated이 프레임에서 받은 스칼라와 게이트에 걸린 스칼라 수(init에서는 0과 0, 트랙을 버린 프레임까지는 세고 그 뒤로는 null)

추적을 끄면 모두 null입니다. metadata.sensing의 타깃별 통계에는 tracked_frames (init, tracking, coasting), coasting_frames, lost_frames, lost_by_std_frames(max_position_std_m 상한으로 버린 횟수. 그 프레임의 융합이 곧바로 트랙을 다시 시작하면 그 프레임은 lost가 아니라 init입니다), 트랙 위치 오차 중앙값과 p90, 트랙 속도 오차 중앙값, frames_improved_over_fusion(융합이 ok이고 트랙이 참값에 더 가까운 프레임), frames_track_without_fusion(융합이 ok가 아닌 프레임의 트랙)이 더해지고, 실행 전체에는 median_track_position_error_m과 tracking_model이 더해집니다.

파형 수비학 (#23)

파형을 주지 않으면 cpi_pulses는 자유 변수이고 적분 이득은 cpi_s와 상관없이 10log10(cpi_pulses)입니다(v0.1.15 동작이며 지금도 기본값). sensing.waveform을 주면 이 자리를 OFDM 수비학이 대신합니다. 한 CPI의 파일럿 자원 요소(RE) 수가 이득을 정하고, 점유 대역폭이 잡음 바닥과 분해능을 정합니다. 같은 객체를 ISAC 요청(waveform), 커버리지 요청과 /analysis/pd-curve(waveform과 cpi_s, 기본 0.01 s), 채널 분석(waveform)도 받습니다. 채널 분석에서는 RSRP/RSRQ 격자가 scs_hz 간격의 n_active_sc개 부반송파가 됩니다. 예를 들어 20 MHz·30 kHz에서 int(B / (12 scs)) = 55 RB 대신 51 PRB / 612 SC입니다.

필드기본값의미
scs_hz30e3부반송파 간격 Δf [Hz]
n_fft필수FFT 크기(n_active_sc 이상)
n_active_sc612활성 부반송파 수. B_occ = n_active_sc · Δf
cpnormalnormal 또는 extended
symbol_duration_snullCP를 포함한 평균 심볼 길이. null이면 NR 슬롯 시간(Δf = 15 · 2^μ kHz면 (1 ms / 2^μ) / 14, 확장 CP는 / 12), 그 밖에는 (1/Δf)(1 + 144/2048 또는 512/2048)
symbols_per_cpinullnull이면 floor(cpi_s / T_sym)
pilot.patterncombcomb 또는 block(block은 subcarrier_step 1)
pilot.subcarrier_step, pilot.symbol_step1, 1m번째 심볼마다, n번째 활성 부반송파마다 파일럿(둘 다 0번부터)

파일럿 격자와 거기서 나오는 값은 다음과 같습니다.

K = ceil(n_active_sc / subcarrier_step)      N = ceil(symbols / symbol_step)
cpi_pulses = K N        적분 이득 = 10 log10(K N)
B_occ = n_active_sc Δf  잡음 바닥 = -174 dBm/Hz + 10 log10(B_occ) + NF
거리 합 셀 c / B_occ (모노스태틱 거리 c / 2 B_occ)
지연 빈 1 / (K · subcarrier_step Δf)       도플러 셀 1 / (N · symbol_step T_sym)
파일럿 축  f_k = (k · subcarrier_step - n_active_sc // 2) Δf,   t_j = j · symbol_step T_sym

진폭과 잡음 규약(트랙 B 기능 전부가 같이 씀): 경로의 채널 계수는 무차원 a = 10^(path_gain_db / 20) e^(j phase_rad)입니다(phase_rad에는 이미 −2π f_c τ가 들어 있음). 활성 부반송파마다 RE당 Es = P_tx / n_active_sc(CP 제외)를 싣고, RE당 잡음은 N0 = −174 dBm/Hz + 10log10(Δf) + NF입니다. 그러면 경로의 RE당 SNR |a|² Es / N0는 P_echo − N_floor(B_occ)이고, snr_db = P_echo − N_floor(B_occ) + 10log10(K N)은 파일럿 RE K N개의 정합 필터 SNR입니다.

프리셋, cpi_sT_sym심볼K × N이득B_occc / B_occ도플러 셀바닥(NF 7 dB)
NR 30 kHz 51 PRB, 10 ms0.5 ms / 14280612 × 280 = 171,36052.34 dB18.36 MHz16.33 m100 Hz−94.36 dBm
NR 30 kHz 51 PRB, 50 ms1400612 × 1400 = 856,80059.33 dB20 Hz
Grid S, 10 ms1/14400 s144256 × 16 = 409636.12 dB20.0 MHz14.99 m100 Hz−93.99 dBm
Grid S, 50 ms720256 × 80 = 20,48043.11 dB20 Hz

NR 30 kHz 51 PRB는 {"scs_hz": 30000, "n_fft": 1024, "n_active_sc": 612}이고, Grid S(v0.1.15 데모의 4096 펄스 기본값을 파일럿 격자로 읽은 것)는 {"scs_hz": 15625, "n_fft": 2048, "n_active_sc": 1280, "symbol_duration_s": 0.0000694444444444, "pilot": {"subcarrier_step": 5, "symbol_step": 9}}입니다(코드에서는 1/14400을 그대로 넘기세요). 파일럿 밀도가 같으면 CPI 50 ms는 10 ms보다 10log10(5) = 6.99 dB 이득이 큽니다. 10 ms의 Grid S는 v0.1.15 수치(4096 펄스, 20 MHz 바닥)를 그대로 냅니다.

파형이 있으면 metadata.sensing(그리고 ISAC·커버리지·pd-curve 메타데이터)에 cpi_pulses (= K N), b_occ_hz, n_re_per_cpi, symbols_per_cpi, symbol_duration_s, pilot_fraction, grid_k, grid_n, n0_dbm_per_re와 numerology 사전 전체가 붙고, noise_floor_dbm, integration_gain_db, 분해능과 velocity_resolution_m_s가 파형을 따릅니다. bandwidth_hz, cpi_s, MTI 키는 v0.1.15 의미 그대로입니다. 경고 두 개가 옛 요청을 짚어 줍니다. 파형 없이 cpi_pulses를 주고 cpi_s가 0.01 s가 아니면 "integration gain does not follow cpi_s without sensing.waveform", cpi_pulses가 파형의 K N과 다르면 "cpi_pulses 4096 overridden by the waveform numerology: 171360 pilot REs per CPI (612 x 280)"입니다. 저장되는 요청에는 보낸 숫자가 그대로 남습니다. cpi_s가 심볼 하나보다 짧거나, symbols_per_cpi가 CPI에 들어가지 않거나, CPI당 파일럿 RE가 10⁸개를 넘으면 파형을 받는 모든 경로(시나리오, ISAC, 센싱 커버리지, /analysis/pd-curve)가 아무것도 풀기 전에 400을 돌려줍니다. 커버리지 요청은 파형 없이 cpi_s를 받으면 "cpi_s is unused without waveform"이라고 경고합니다. 채널 분석에는 cpi_s가 없습니다. 그 metadata.numerology는 symbols_per_cpi가 있으면 그 심볼 수, 없으면 10 ms(최소 한 심볼)를 덮고, RSRP/RSRQ에는 부반송파 수와 간격만 들어가므로 다른 경로가 받는 파형은 채널 분석도 받습니다.

"sensing": {"enabled": true, "cpi_s": 0.05, "threshold_db": 13,
            "waveform": {"scs_hz": 30000, "n_fft": 1024, "n_active_sc": 612}}

배경 경로 H0/H1 (#21)

include_comm_paths는 타깃을 흡수체로 둔 통신 경로를 echoes 뒤에 붙입니다. 타깃이 있을 때의 배경(H1)입니다. 오경보 보정에는 같은 프레임에서 푼, 타깃이 없을 때의 배경(H0)이 필요하고, RCS 솔버는 확산 클러터를 추적하지 않습니다. sensing.background는 매 프레임 TX → 센싱 RX 링크에서 PathSolver로 둘 다 풉니다.

필드기본값의미
modeoffoff, h1_absorber, h0_removed, both
diffuse_reflectiontrue확산 클러터(재질의 산란 계수)
diffraction, edge_diffractionfalse
max_depth, num_samplesnullnull이면 config 값
deterministictruePathSolver(deterministic=True). 두 번 돌려도 경로가 같음
max_paths_per_link256솔브마다 링크별로 가장 센 경로 n개만 남김. null이면 전부 남김. 나머지는 dropped_paths로 세고 실행 경고 하나가 합계를 알려 줌
cachestatic_nodesstatic_nodes: 키가 반복되는 동안 한 번만 풂, per_frame: 매 프레임 풂
storeh0결과에 남길 가설: h0(H1은 메모리에서 CFAR 맵에만 씀), both, none(맵만)

LoS, 정반사, 굴절은 config를 따릅니다. 프레임마다 background_h0 / background_h1 (RayPath 목록: power_dbm, path_gain_db, phase_rad, delay_ns, 항상 실수인 doppler_hz, 종류·prim_id·rf_material_id가 있는 interactions, target_id는 null)와 background_meta(mode, mechanisms, max_depth, num_samples, seed, deterministic, 가설별 solved, cache_hit, source_frame, cache_key, solve_s, num_paths, dropped_paths, 남긴 경로의 "tx>rx"별 10log10 Σ|a|²인 total_power_db, stored, 솔브가 실패했으면 error)를 저장합니다. 경로 순서는 정규화합니다(링크, 다음으로 지연·상호작용·꼭짓점 순, path_0001…로 다시 번호 매김). GPU 솔버는 같은 경로 집합을 실행마다 다른 순서로 돌려주기 때문입니다.

store는 푼 가설 가운데 무엇을 결과에 남길지 정합니다. 기본값 h0이면 H1은 모드가 요구할 때 풀어서 CFAR 맵에 쓰지만 background_h1은 프레임에 넣지 않습니다 (background_meta.h1.stored: false). both는 H1도 남기고(§10에서 background_h1이나 total 채널을 내보내려면 필요), none은 둘 다 남기지 않습니다. 맵은 셋 모두 같습니다. max_paths_per_link는 링크마다 전력이 가장 센 경로만 남깁니다. sionna 백엔드는 솔버 경로를 행으로 바꾸기 전에 나머지를 버리고, 실행이 끝나면 "background: max_paths_per_link 256 kept the strongest 256 paths per link: … of … solved background paths dropped over the run" 경고를 한 번 남깁니다. 버리는 경로는 약한 확산 꼬리라서 (남긴 경로의) total_power_db는 거의 바뀌지 않습니다. 전부 남기려면 null을 주세요.

static_nodes 키는 RF 투영 지문, 솔버 옵션, 모든 TX와 센싱 RX의 위치·속도, 표적이 아닌 액터 전부의 자세·속도, 그리고 H1이면 타깃 직육면체를 해시합니다(H0에는 어떤 액터를 뺐는지만 들어감). 앞 프레임과 키가 같은 프레임은 경로를 저장하지 않고, background_meta.h0.source_frame이 경로를 가진 프레임을 가리킵니다 (services.sensing_background.resolve_background(frames, i, "h0")가 따라가고, 저장하지 않은 가설이면 null을 돌려줌). 노드와 씬이 정지해 있으면 H0는 실행마다 한 번만 풀고, TX/RX나 차량이 움직이면 매 프레임 다시 풉니다. 실행은 가설마다 최근에 푼 키 4개를 메모리에 둡니다. 솔브가 실패하면(sionna 백엔드는 경로 없음과 경고로 물러남) 캐시에 넣지 않습니다. 그 프레임의 메타에 error가 붙고, 맵은 H0(없으면 배경 없음)로 대신하며, 다음 프레임이 다시 풉니다. metadata.sensing.background에는 실행 요약(store, max_paths_per_link, solves_h0, cache_hits_h0, failed_solves_h0, solve_s_total, paths_stored, paths_dropped 등)이, metadata.sensing.sensing_tx_ids에는 TX 목록이 붙습니다.

배경을 켜면 타깃이 하나도 없는(바인딩된 액터가 없는) 실행도 됩니다. links와 estimates는 비고 프레임에는 배경만 남습니다(보정용 배경 전용 H0 실행). 배경이 없으면 타깃 없는 씬은 여전히 400 "no sensing targets"입니다. 배경은 프레임의 다른 필드(echoes, links, estimates, nodes)를 바꾸지 않습니다.

확산 클러터는 경로가 많습니다. 지면 판 하나와 건물 하나 위의 300 m 기선 시험 씬은 샘플 400,000개에서 링크당 경로 16,776개를 냅니다(푼 가설 하나에 JSON 약 10 MB). 기본값이 링크당 256개만 남기는 이유입니다. mock 백엔드의 배경은 링크마다 LoS, 지면 반사, 벽 반사이고(H1은 다리가 타깃 직육면체를 지나는 경로를 뺌), TX/RX 운동의 도플러를 붙입니다.

타깃이 움직이면 H1 키가 바뀌므로, 노드가 모두 정지해 있어도 H1은 프레임마다 다시 풉니다. isac_drone_fr1 데모(링크 16개, config max_depth 2, 샘플 200,000개)에서 배경 솔브 한 번은 경로 64,356개(링크당 약 4,000개)를 냅니다. 8프레임(NR 612 × 280, both 모드, CFAR)을 재 보면, 경로를 전부 남기고 H1도 저장하면 프레임당 11.1초(H1 솔브당 3.9초)에 결과 403 MB, 기본값(링크당 256개, H1 저장 안 함)이면 프레임당 1.2초(H1 솔브당 0.28초, 경로 4,025개)에 결과 4.6 MB이고, CFAR 맵은 store와 상관없이 같습니다. 상한을 행 변환 앞으로 옮기기 전에는 상한을 건 H1 솔브도 약 2.6초 걸렸습니다. 정지 노드에서 h0_removed는 실행마다 한 번만 풉니다(같은 데모에서 CFAR 포함 프레임당 0.6초).

"sensing": {"enabled": true, "waveform": {"scs_hz": 30000, "n_fft": 1024, "n_active_sc": 612},
            "background": {"mode": "both", "store": "both", "max_paths_per_link": 2000}}

7. ISAC 트레이드오프: 빔과 슬롯 공유

TRP 패널 하나가 UE와 드론을 동시에 겨눌 수는 없습니다. ISAC 트레이드오프는 TX마다 세 가지를 답합니다. UE에 가장 좋은 코드북 빔(통신 빔)은 무엇인지, 타깃을 가장 잘 보는 빔(센싱 빔)은 무엇인지, 그리고 둘이 슬롯을 나눠 쓰면 전송률을 얼마나 잃고 탐지 확률을 얼마나 얻는지입니다.

실행. Results 모드에서 ISAC trade-off 패널을 열고 TX(아무것도 체크하지 않으면 전부), 배열 크기, 스윕, CPI 펄스 수, P_fa, 슬롯 비율, 공유 방식을 정한 뒤 실행합니다. API로는 다음과 같습니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/isac \
  -H "Content-Type: application/json" \
  -d '{"config_id": "default", "tx_rows": 4, "tx_cols": 4,
       "sweep_start_deg": -60, "sweep_stop_deg": 60, "sweep_step_deg": 5,
       "cpi_pulses": 4096, "pfa": 1e-6, "sharing_mode": "dual_function"}'
curl http://127.0.0.1:8000/api/projects/sample_demo/results/isac   # 마지막으로 저장된 결과

결과는 isac 결과 세트로 저장됩니다(실행 기록, prune, 라벨 모두 됨). /simulate/sensing과 마찬가지로 auto는 sionna-rt 2.2 이상이 깔려 있으면 sionna를, 아니면 경고와 함께 mock을 씁니다. RCS 솔버가 없는데 sionna를 명시하면 409입니다. 모르는 디바이스나 액터, UE 없음, 센싱 수신기가 없는 TX, use_device_orientation: false는 아무것도 풀기 전에 400으로 응답합니다.

Sample Demo에서 "config_id": "sensing_fr1"로 돌리면(Sionna, 드론은 t = 0 위치), 기본 방위각 전용 4×4 코드북은 rx_001을 TX 2(통신 빔 55°)가 서비스하게 하고, 드론은 TX 1(센싱 빔 −35°, 31.9 dB, P_d 0.99)과 TX 3(−40°, 23.4 dB, P_d 0.94)로는 보지만 TX 2로는 보지 못합니다. 10 m 마스트에서 보면 드론이 28° 위에 있어서, 4행 패널의 앙각 0° 빔이 수직 널에 걸리기 때문입니다(−1.0 dB). 앙각 스윕 "elevation_start_deg": -10, "elevation_stop_deg": 60, "elevation_step_deg": 5를 더하면 모든 TRP가 탐지하는 센싱 빔을 얻고(앙각 40°, 30°, 20°에서 51.5, 44.9, 39.4 dB), UE는 TX 1이 서비스합니다(방위각 −20°, 앙각 −10°).

요청 필드기본값의미
tx_idsnull평가할 TX(기본: 모든 TX).
ue_rx_ids / sensing_rx_idsnull역할 분담. 아래 역할 참고.
target_actor_idsnull타깃(기본: 켜진 센싱 바인딩 전부).
ue_associationservingserving: UE마다 최적 빔 RSS가 가장 강한 TX에 속합니다. all: 모든 TX가 모든 UE를 서비스합니다(TX별 단일 셀 관점).
tx_rows × tx_cols4 × 4TX 패널. 소자 간격은 디바이스 안테나 설정을 따릅니다.
rx_rows × rx_colsnull센싱 RX 패널(기본: TX와 같은 크기). UE는 늘 단일 소자입니다.
use_device_orientationtrue패널이 자기 orientation_deg를 유지합니다. false는 거부됩니다. look_at으로는 패널 하나를 UE와 타깃 양쪽에 동시에 맞출 수 없기 때문입니다.
sweep_start_deg / sweep_stop_deg / sweep_step_deg−60 / 60 / 5패널 로컬 좌표계의 방위각 코드북. /simulate/beamforming의 스윕과 똑같습니다(빔 최대 361개, TX당 빔 수 × 0이 아닌 슬롯 비율 수는 최대 4096).
cpi_pulses4096ρ = 1일 때 코히어런트하게 적분하는 펄스 수 / OFDM 자원 요소 수.
cpi_s0.01메타데이터 전용(분해능 계산).
threshold_db13빔별 detected 판정 기준.
pfa1e-6Pd 모델의 오경보 확률.
pd_target0.9파레토 요약의 동작점.
slot_ratios0, .05, .1, .2, .3, .5, .7, 1센싱에 쓰는 슬롯(펄스) 비율 ρ. 정렬하고 중복을 없앱니다.
sharing_modedual_functiontime_sharing 또는 dual_function(아래).
samples_per_sp / max_depth1 000 000 / null에코 솔브에 그대로 넘깁니다(§3과 같음).
include_pathsfalse솔브한 경로를 결과에 저장합니다. 에코가 먼저, 통신 경로가 뒤에 옵니다.
elevation_start_deg / elevation_stop_deg / elevation_step_degnull패널 로컬 좌표계에서 2차원 코드북의 고도 스윕(셋 다 주거나 셋 다 비움. 고도는 최대 361개, 빔은 모두 합쳐 최대 4096개). null이면 방위각 코드북만 씁니다. 고도 코드북 참고.
interferencefalseUE SINR에 TX 간 간섭을 넣습니다(ue_association: serving일 때만). 간섭 참고.
detectorSwerling 1, 몬테카를로 없음P_d 모델, 몬테카를로 시행 수, 경험적 임계값, 시드(§9).

역할. ue_rx_ids와 sensing_rx_ids를 둘 다 비워 두면, 선택한 TX에서 1 m 안에 있는 RX가 그 TX의 센싱 수신기(모노스태틱)가 됩니다. 씬의 어느 TX에서도 1 m 안에 있지 않은 RX는 모두 UE입니다. 그래서 TRP 일부만 골라 평가해도 나머지 TRP의 레이더 수신기가 서비스 대상 UE로 바뀌지 않습니다. TX마다 센싱 후보 중 가장 가까운 RX를 고르므로, sensing_rx_ids를 직접 주면 여러 TX가 수신기 하나를 나눠 쓸 수 있습니다(바이스태틱). ue_rx_ids를 주면 UE 규칙 대신 그 목록을 씁니다. 한 RX가 UE이면서 센싱 수신기일 수는 없습니다.

모델. t = 0에서 솔브 두 번을 돌립니다. 에코 솔브(TX → 타깃 → 센싱 RX, §3)와, 타깃을 흡수체로 둔 통신 솔브(TX → UE, include_comm_paths와 같은 방식)입니다. 두 솔브 모두 안테나를 단일 소자로 바꿔서 돌리므로, 저장된 경로는 패널 중심을 기준으로 합니다. 빔은 그다음 경로의 월드 좌표 출발각·도래각으로부터 Sionna 합성 배열 규약에 따라 합성합니다. 방향이 o인 패널의 n번 소자가 월드 방향 k̂로 진행하는 파에 대해 보이는 응답은

a_n = exp(+j 2π p_n · R(o)ᵀ k̂)

이고, p_n은 파장 단위의 PlanarArray 소자 위치입니다. 빔 w는 양쪽 끝에서 w^H a로 점수를 매기는데, /simulate/beamforming과 같은 켤레 가중치 정합 필터입니다. sionna에서 합성한 TX 코드북은 /simulate/beamforming(codebook_sweep, use_device_orientation: true)을 빔 단위로 재현합니다. 회귀 테스트에서 최적 빔이 같고, 최고점에서 20 dB 안에 드는 빔은 모두 0.1 dB 이내로 맞습니다(실측 0.01 dB 미만). TX t, 코드북 빔 k, UE u에 대해 다음과 같습니다.

고도 코드북. elevation_* 세 필드를 주면 코드북은 스윕의 모든 방위각을 모든 고도에서 조합한 격자가 되고, 고도가 바깥 순서입니다. 빔 번호는 k = i_el · n_az + i_az입니다. 빔 (φ, θ)는 패널 로컬 방향 [cos θ cos φ, cos θ sin φ, sin θ]를 향합니다.

w_n(φ, θ) = exp(+j 2π (y_n cos θ sin φ + z_n sin θ)) / sqrt(N)

평면 격자에서는 kron(w_z(θ), w_y(φ, θ))와 같습니다. 노름은 1이고 자기 방향으로 10·log10(N)을 모두 내며, θ = 0이면 위의 방위각 빔과 같습니다. 센싱 RX도 같은 격자를 쓰고 최적 RX 빔을 격자 전체에서 고릅니다. 4×4 패널에서 로컬 고도 32.6°에 있는 드론을 보면 효과가 드러납니다. 방위각 빔 중 최선은 한쪽 끝당 −9.84 dB이지만 −10…40° / 5° 격자는 11.98 dB여서, 한쪽 끝마다 약 22 dB를 되찾습니다. sionna에서는 4×1 수직 ULA의 고도 빔 (TX 쪽과 RX 쪽)이, 같은 조향 벡터를 Sionna 자체 합성 배열 채널에 적용한 값과 맞습니다. 회귀 테스트에서 최적 빔이 같고 LoS 고도에 놓이며, 최고점에서 20 dB 안의 빔은 모두 0.1 dB 이내로 맞습니다(실측 0.005 dB 미만). 이 필드를 비우면 방위각 코드북 코드가 그대로 돕니다.

간섭. interference: true이면(ue_association: serving일 때만) TX t가 서비스하는 UE는, 그 UE까지 통신 경로가 있는 다른 선택 TX의 신호를 모두 간섭으로 받습니다. 모든 TX가 ρ와 슬롯 타이밍을 공유합니다(동기 슬롯). 통신 슬롯에서는 다른 TX가 각자 통신 빔 c를 쏘고(서비스할 UE가 없으면 쏘지 않음), 센싱 슬롯에서는 센싱 빔 s를 쏩니다(에코가 없으면 쏘지 않음). TX t2의 RSS_t2,u(k) = P_t2 + 20·log10|h_k,u|로 쓰면(mW 단위)

I_comm(u) = Σ_t2 10^(RSS_t2,u(c_t2)/10), I_sens(u) = Σ_t2 10^(RSS_t2,u(s_t2)/10)

SINR_k,u = RSS_t,u(k) − 10·log10(10^(N0/10) + I_comm(u))

입니다. TX의 통신 빔은 다른 TX의 통신 빔에 따라 달라지므로 반복해서 고릅니다. 0라운드는 간섭이 없을 때의 선택입니다. 이후 라운드마다 TX가 차례로, 다른 TX의 현재 빔을 고정하고 간섭을 반영한 합 전송률이 가장 큰 빔을 고릅니다. 한 라운드 동안 아무것도 바뀌지 않으면 멈추고, 10라운드를 넘기면 경고와 함께 마지막 라운드의 빔을 보고합니다. 센싱 빔은 간섭과 무관하고 에코 SNR은 계속 잡음 제한입니다. TX를 하나만 고르면 아무것도 바뀌지 않아 SINR = SNR이 정확히 성립합니다.

Pd. 제곱 검파기로 적분 샘플을 판정하는 Swerling-1 타깃은 P_fa = e^(−T), P_d = e^(−T / (1 + SNR))이므로

P_d = P_fa^(1 / (1 + SNR))

입니다. P_fa = 10⁻⁶에서 P_d = 0.9를 얻으려면 21.1 dB가 필요하고, 13 dB 임계값에서는 P_d = 0.517입니다. 에코가 없으면 P_d = P_fa입니다. Swerling 1은 detector.model의 기본값이고, Swerling 0·3과 몬테카를로는 §9에 있습니다. detector.monte_carlo_trials > 0 이면 에코가 있는 모든 빔과 ρ > 0인 모든 파레토 점에 pd_mc가 붙습니다. 실행 한 번에 쓸 수 있는 시행은 TX 수 × 빔 수 × (1 + 0이 아닌 슬롯 비율 수) × 시행 수로 세어 최대 2·10⁸입니다 (TX 4개의 기본 요청이면 시행 250 000). 이를 넘는 요청은 아무것도 풀기 전에 400입니다.

슬롯 공유. 통신 빔은 k_c = argmax R(k)입니다. 슬롯(또는 펄스)의 비율 ρ를 빔 k로 센싱에 쓰므로 에코는 ρ · cpi_pulses만큼 적분됩니다.

ρ = 0은 통신만 하는 점 하나입니다(beam_idx null, P_d = P_fa). dual_function에서 ρ = 1인 행은 빔 자체와 같고(빔 하나가 둘 다 함), time_sharing에서는 전송률이 0입니다. interference를 켜면 슬롯마다 그 슬롯에 다른 TX가 쏘는 빔을 간섭으로 받습니다. S는 간섭 없는 신호 전력입니다.

이렇게 슬롯을 맞추면 ρ가 커질수록 전송률이 오를 수도 있습니다. 다른 TX의 센싱 빔이 UE에 통신 빔보다 약하게 닿으면(I_sens < I_comm) dual_function 점이 통신만 하는 전송률을 넘고, 통신 전용 대비 손실이 음수로 나옵니다. 모델이 그렇게 정의된 것이지 버그가 아닙니다.

출력 읽기. txs[] 항목마다 다음이 들어 있습니다.

최상위에는 ue_serving_tx와 noise_floor_dbm이 있고, metadata에는 §6의 탐지 상수와 snr_for_pd_target_db, pd_at_threshold(둘 다 detector.model 기준)가 더해집니다. 서비스할 UE가 없는 TX는 센싱 전용 트레이드오프(전송률 모두 0)가 되고, 그 사실을 자기 warnings에 적습니다. 옵션을 켰을 때만 생기는 키도 있습니다. 고도 스윕이면 codebook_shape([n_el, n_az]), interference이면 interference, ue_interferers(UE별로 닿는 다른 TX), interference_rounds, interference_converged, interference_model, 기본값이 아닌 detector이면 detector(모델, 임계값, 몬테카를로의 실측 P_fa)입니다. 새 필드를 모두 기본값으로 둔 요청은 v0.1.12와 같은 metadata.request와 request_hash를 저장합니다.

8. 센싱 커버리지 맵

60 m 높이의 드론은 어디서 보이고, 몇 개 링크가 볼까요? 커버리지 맵은 수평 격자의 셀마다 가상 점 타깃을 두고 TX × 센싱 RX 링크(모노스태틱과 바이스태틱) 전부를 계산합니다. RCS 솔브를 돌리지 않으므로 맵 하나에 1초 정도 걸립니다.

실행. Results 모드에서 Sensing coverage 패널을 열고 높이, 셀 크기, RCS(비우면 TR 38.901 uav-small-size, −12.81 dBsm), 임계값, 펄스 수, P_fa, 배열 이득을 정한 뒤 실행합니다. 지표 선택기로 아래 네 레이어를 바꿔 봅니다. API로는 다음과 같습니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/sensing-coverage \
  -H "Content-Type: application/json" \
  -d '{"config_id": "default", "height_m": 60, "cell_size_m": 10,
       "threshold_db": 13, "cpi_pulses": 4096, "array_gain": "none"}'
curl http://127.0.0.1:8000/api/projects/sample_demo/results/sensing-coverage

결과는 sensing_coverage 결과 세트로 저장됩니다. 센싱 수신기는 ISAC 규칙을 따릅니다 (선택한 TX에서 1 m 안의 RX, 또는 sensing_rx_ids). 수신기가 없는 TX가 있으면 400입니다.

Sample Demo에서 "config_id": "sensing_fr1", height_m 40, cell_size_m 5로 돌리면 (Sionna) 셀 441개, 링크 9개, 기하 6개가 나오고, 모든 셀(100 %)이 LOS·탐지·융합 가능이며 최적 SNR 중앙값은 29.3 dB입니다. 이 맵에는 MTI가 없으므로, 6절의 시나리오는 실제 비행에서 여전히 도플러 노치 때문에 링크를 잃습니다.

요청 필드기본값의미
tx_ids / sensing_rx_idsnull디바이스(기본: 모든 TX와 같은 위치의 RX).
rcs_dbsm / object_typenull점 타깃의 σ. 값을 직접 주거나 TR 38.901 유형의 평균 σ_M을 씁니다(둘 다는 안 됨). 둘 다 비우면 uav-small-size.
height_m60격자 평면의 높이(타깃 중심). ±100 000 m 이내.
cell_size_m10셀 크기. 최대 10 000 m. 셀이 40 000개를 넘으면 경고와 함께 키웁니다.
center_xy / size_xynull범위를 직접 지정(둘 다 주거나 둘 다 비움. 중심은 ±10⁷ m 이내, 변 길이는 10⁶ m까지). 기본은 씬 경계(비주얼 메시, 디바이스, 액터)에 min(15 m, max(3 m, 긴 변의 15 %))만큼 여백을 둔 것으로, sionna 라디오맵과 같습니다.
threshold_db / cpi_pulses / pfa13 / 4096 / 1e-6탐지 조건. §6, §7과 같습니다.
array_gainnonesteered는 tx_rows×tx_cols / rx_rows×rx_cols 패널의 10·log10(N_tx) + 10·log10(N_rx)를 더합니다. 모든 링크가 모든 셀에 빔을 맞춘다는 상한입니다.
min_links_for_fusion3셀이 융합 가능으로 판정되는 데 필요한 서로 다른 기하의 수.
detectorSwerling 1, 몬테카를로 없음pd_best의 모델(§9). monte_carlo_trials > 0이면 셀 5개의 몬테카를로 점검을 더합니다.

모델. 링크와 셀마다, R_t = |셀 − TX|, R_r = |셀 − RX|로

SNR = P_t + G + G_e,t + G_e,r + L_pol + 10·log10(λ² σ / ((4π)³ R_t² R_r²)) − A(R_t + R_r) − N0 + 10·log10(cpi_pulses)

를 계산하는데, 두 구간이 모두 LOS일 때만이고 아니면 에코가 없습니다. G는 위의 배열 이득, A는 대기 흡수입니다(설정에서 켜지 않으면 0). G_e,t와 G_e,r은 TX와 RX의 소자가 셀 쪽으로 내는 이득으로, 디바이스마다 자기 antenna.pattern을 자기 좌표계(orientation_deg) 에서 계산합니다. iso는 0 dB이고 tr38901은 최대 8 dBi, 최소 −22 dBi(30 dB 바닥)입니다. 어떤 패턴을 썼는지는 metadata.element_patterns에 있습니다. L_pol은 아래의 편파 항입니다(v0.1.14).

편파. 소자는 선형 전계 하나를 냅니다(Sionna 에코처럼 첫 번째 포트). V와 VH는 로컬 θ̂ 방향, H는 φ̂ 방향, cross는 θ̂를 −45° 돌린 방향입니다. pitch나 roll을 주면 이 전계가 월드에서 기울어집니다. Sionna의 RCS 산란은 입사 방향 k_i(TX → 타깃)와 산란 방향 k_s(타깃 → RX)의 월드 θ̂/φ̂ 기저에서 항등 행렬이므로, p_t를 타깃 쪽으로 향한 TX 소자의 월드 전계, p_r을 타깃 쪽으로 향한 RX 소자의 전계라 하면

F = (p_t · θ̂(k_i)) (p_r · θ̂(k_s)) + (p_t · φ̂(k_i)) (p_r · φ̂(k_s)), L_pol = 20·log10|F| (≤ 0 dB)

입니다. θ̂(k)와 φ̂(k)는 방향 k의 월드 구면 단위 벡터입니다. 여기서 다음이 나옵니다.

L2 회귀 테스트 장소(TRP (0, 0, 10) m, 3.5 GHz, 30 m 높이의 10 dBsm 고정 타깃, iso 소자, V 편파. 모노스태틱 = RX가 TX 위치, 바이스태틱 = 같은 방향의 RX가 (0, 40, 10) m)에서 LOS 셀 5개(바이스태틱 링크는 4개)에 대해 잰 Sionna 에코 − v0.1.13 맵은 다음과 같습니다.

링크pitch에코 손실(dB)
모노스태틱0°0.000
모노스태틱−15°−0.35 … −1.51
모노스태틱+15°−0.42 … −1.52
모노스태틱−45°−3.99 … −29.03
바이스태틱−15°−0.12 … −1.88
바이스태틱−45°−1.20 … −18.69
모노스태틱, cross−15°−5.32 … −6.61

그래서 −13…−16° 기울인 옥상 TRP는 모노스태틱 링크마다 약 0.4–1.5 dB를 잃습니다. v0.1.13의 맵과 mock 에코는 이 손실을 보여 주지 않아 그만큼 낙관적이었고, Sionna 에코가 맞았습니다. L_pol을 넣은 맵은 V, H, VH, cross 소자, pitch 0, ±15°, −45°, 모노스태틱과 바이스태틱 모두에서 RCSSolver 에코와 0.01 dB 이내로 맞습니다(실측 3·10⁻⁵ dB 이하). 어느 링크든 손실이 0이 아니면 metadata.polarization_model이 모델 이름을 적습니다.

mock과 LOS. mock 에코도 같은 소자 이득과 L_pol을 적용하므로(v0.1.14), 셀 중심에 타깃을 둔 mock 센싱 솔브는 iso와 tr38901 소자, 어떤 방향에서든 맵 값과 같습니다. xpr_db를 정한 고정 타깃에는 mock이 L_pol을 넣지 않습니다(편파 변환은 모델링하지 않습니다). sionna에서 LOS 판정은 캐시된 정적 씬에 대한 Mitsuba 그림자 광선 테스트이고, 액터 메시는 모두 뺍니다(맵은 실제 드론이 지금 어디 있는지가 아니라 장소 자체를 보는 것이기 때문입니다). 회귀 테스트에서 LOS 셀에 RCSSolver로 푼 고정 RCS 에코는 iso와 tr38901 소자 모두 맵과 0.01 dB 이내로 맞고(실측 약 10⁻⁵ dB), 건물 그림자 속 셀에는 직접 에코가 없습니다. mock에는 지오메트리 차폐가 없어서 모든 구간을 LOS로 보며, 결과의 warnings와 metadata.los_model에 그렇게 적습니다.

레이어(values, 라디오맵처럼 행 우선 [ny][nx]):

summary에는 셀·링크·기하 수, LOS 링크가 하나 이상인 셀, 탐지가 하나 이상인 셀, 융합 가능한 셀의 비율(%), 그리고 에코가 있는 셀에 대한 최적 SNR 중앙값이 들어갑니다. links[]에는 링크별 기선 길이와 LOS 비율, 탐지 비율이 있습니다. detector.monte_carlo_trials > 0이면 summary.mc_spot_check에 에코가 있는 셀의 best_snr_db 분위수 0/25/50/75/100 %에 해당하는 서로 다른 셀 5개가 들어갑니다(최근접 순위. 같은 값을 가진 셀이 여럿이면 아직 고르지 않은 셀 중 번호가 가장 작은 셀. 에코가 있는 셀이 적으면 그만큼 적음). 셀마다 cell [ix, iy], snr_db, 해석적 pd, pd_mc와 그 95 % 구간(ci_low, ci_high)이 있습니다. 기본값이 아닌 detector이면 metadata.detector 가 붙습니다.

9. 탐지기 모델과 Pd/Pfa 몬테카를로

SEAM이 보고하는 P_d는 모두 코히어런트 적분 샘플에 대한 제곱 검파기를 적분 후 SNR(§6의 링크 snr_db, §7의 빔 SNR, §8의 best_snr_db)로 모델링한 값입니다. 적분된 잡음 샘플을 CN(0, 1)로 정규화하므로 잡음만 있으면 y = |z|² ~ Exp(1)이고, 임계값 T = −ln P_fa는 정확히 P_fa를 줍니다. S = 10^(SNR/10)은 타깃 진폭 A의 평균 |A|²입니다. A는 CPI 동안 일정하고 스캔마다 detector.model에 따라 요동합니다.

model타깃P_dP_fa 10⁻⁶에서 P_d 0.9 / 0.5에 필요한 SNR
swerling0요동 없음Q₁(√(2S), √(2T)) (Marcum Q)13.18 / 11.24 dB
swerling1(기본)레일리, |A|² ~ Exp(S)P_fa^(1/(1 + S))21.14 / 12.77 dB
swerling3지배적 산란체 하나, |A|² ~ Gamma(2, S/2)e^(−T/(1 + S/2)) · (1 + T·(S/2)/(1 + S/2)²)17.30 / 11.95 dB

요동하는 타깃은 높은 P_d에서 SNR이 더 필요합니다. P_d 0.9에서 Swerling 1은 요동 없는 타깃보다 8 dB가 더 듭니다.

유도. |A|² = s로 고정하면 P(y > T) = P(X ≤ K)이고, K ~ Pois(s)와 X ~ Pois(T)는 서로 독립입니다(비중심 카이제곱의 포아송 혼합). Swerling 0은 이 급수를 로그 공간에서 S ± (40√S + 40) 구간에 걸쳐 더합니다. P_fa 0.1부터 10⁻³⁰⁰까지 scipy의 ncx2.sf와 약 10⁻¹³ 이내로 맞습니다. s를 Gamma(2, θ)로 섞으면 K는 음이항 분포가 되어 P(K = k) = (k + 1) p² qᵏ이고, p = 1/(1 + θ), q = θ/(1 + θ)입니다. 그러면 Σ_{k≥x} (k + 1) p² qᵏ = qˣ((x + 1)p + q)이고, X에 대해 평균하면 e^(−Tp)(1 + Tpq), 곧 θ = S/2인 Swerling 3이 나옵니다. 같은 과정을 Gamma(1, S)로 밟으면 e^(−T/(1 + S)), 곧 Swerling 1입니다. Swerling 1은 v0.1.12의 식을 그대로 쓰므로 기본 숫자는 바뀌지 않습니다.

모델이 쓰이는 곳. ISAC(§7): 모든 빔과 점의 pd, snr_for_pd_target_db, pd_at_threshold. 커버리지(§8): pd_best. 시나리오 센싱(§6): sensing.pfa를 정하면 링크 보고의 pd. 거기서 탐지 판정은 그대로 snr_db ≥ threshold_db입니다.

몬테카를로. detector.monte_carlo_trials > 0이면 해석적 pd 옆에 몬테카를로 추정 pd_mc가 붙습니다. 시행마다 모델에 맞는 타깃 진폭을 뽑고(Swerling 0: √S·e^(jφ), Swerling 1: CN(0, S), Swerling 3: √Gamma(2, S/2)·e^(jφ), φ는 균등 분포) 적분 샘플 z = A + CN(0, 1)을 뽑고, |z|²가 임계값을 넘으면 탐지로 셉니다. 이는 N = cpi_pulses개의 펄스 A/√N + n_i를 코히어런트하게 더해 √N으로 나눈 것과 분포가 같습니다(독립인 CN(0, 1) N개를 더해 √N으로 나누면 CN(0, 1)). 그래서 시행 하나는 N과 상관없이 샘플 하나만 들고, 아래 상한이 실행 시간을 묶어 둡니다. 펄스를 하나하나 뽑아도 같은 수치가 나오지만 비용은 N배입니다. 회귀 테스트가 두 방식이 맞는지 확인합니다.

Pd 곡선. POST /analysis/pd-curve는 솔브 없이 SNR에 대한 P_d를 그리고 아무것도 저장하지 않습니다. UI에서는 Results ▸ Detector (Pd curve)입니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/analysis/pd-curve \
  -H "Content-Type: application/json" \
  -d '{"pfa": 1e-6, "cpi_pulses": 4096, "monte_carlo_trials": 200000}'
필드기본값의미
pfa1e-6오경보 확률.
snr_min_db / snr_max_db / step_db−5 / 30 / 0.5SNR 축(71점, 최대 1001점).
models세 모델 전부중복은 없애고 순서는 유지합니다.
monte_carlo_trials0점당 시행 수(0이면 해석식만).
empirical_thresholdfalse잡음 전용 실행에서 임계값을 얻습니다(시행 수 ≥ 20 / P_fa 필요).
seed0몬테카를로 스트림의 기준값.
cpi_pulses1시행당 펄스 수. SNR 축은 적분 후 값이고 몬테카를로는 적분 샘플을 바로 뽑으므로, 어떤 수치도 이 값에 좌우되지 않습니다.
pd_target0.9모델별 snr_for_pd_target_db를 정합니다.

응답에는 snr_db와 threshold(−ln P_fa)가 있습니다. 모델마다 pd와 snr_for_pd_target_db가 있고, 시행 수 > 0이면 pd_mc와 pd_mc_ci_low / pd_mc_ci_high, mc_max_abs_deviation, mc_within_ci_fraction도 붙습니다. 이때 최상위에는 empirical_threshold, pfa_measured, pfa_measured_ci가 더해지고, metadata.mc_method가 어떤 방식으로 뽑았는지 알려 줍니다. 모르는 프로젝트는 404, 시행 예산 초과를 포함한 잘못된 요청은 422입니다.

신호 수준 검출: 지연–도플러 맵과 CFAR (#24)

§6의 탐지는 가장 강한 에코의 참 SNR에 임계값을 대는 방식이라, 강한 에코를 놓치는 일도 오경보도 없습니다. sensing.detection.mode: "cfar"로 바꾸면 프레임마다 TX → 센싱 RX 링크별로 수신 파일럿 채널을 만들고, 지연–도플러 맵으로 바꾼 뒤 2차원 CFAR 검출기를 돌립니다. 이때 검출은 임계값을 넘은 셀입니다. 열잡음, 클러터, 타깃 사이드로브도 임계값을 넘을 수 있고, 그런 오경보는 세어서 보고합니다. 맵이 파형의 파일럿 격자 위에 만들어지므로 CFAR 모드에는 sensing.waveform(§6의 파형 수비학 (#23))이 있어야 합니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/scenario \
  -H "Content-Type: application/json" \
  -d '{"num_frames": 20, "dt_s": 0.5, "sensing": {"enabled": true,
       "waveform": {"scs_hz": 15625, "n_fft": 2048, "n_active_sc": 1280,
                    "symbol_duration_s": 6.944444444444444e-05,
                    "pilot": {"subcarrier_step": 5, "symbol_step": 9}},
       "background": {"mode": "both"},
       "detection": {"mode": "cfar",
                     "map": {"window": "hann", "clutter_removal": "mean_subtract"},
                     "cfar": {"type": "ca", "guard": [1, 1], "train": [8, 4],
                              "pfa_cell": 1e-4}}}}'

신호 모델. 프레임 i, 링크 (tx, rx)의 K × N 파일럿 격자(부반송파 f_k, 파일럿 심볼 시각 t_n) 위에서

Y[n, k] = Σ_p a_p e^(−j2π f_k τ_p) e^(+j2π f_D,p t_n) + W[n, k]

입니다. 합은 그 링크에 있는 모든 타깃의 에코(직접 에코와 다중 경로 에코)와 배경 경로에 대해 잡습니다. 배경은 실행이 H1을 풀었으면 H1, 아니면 H0, 둘 다 없으면 넣지 않습니다 (§6의 배경 경로 H0/H1). a_p = 10^(path_gain_db/20)·e^(j·phase_rad)는 무차원 경로 이득입니다. phase_rad에 반송파 항 −2π f_c τ가 이미 들어 있으므로 f_k는 기저대역 부반송파 주파수이고, 반송파 항을 다시 곱하지 않습니다. W는 RE당 분산이

σ_H² = N0 / Es = 10^((N0,RE − Es,RE)/10), N0,RE = −174 + 10 log10(scs) + NF, Es,RE = P_tx − 10 log10(n_active_sc)

인 원형 가우시안으로, 최소제곱 파일럿 추정의 잡음입니다. σ_H²는 N_floor(B_occ) / P_tx와 같으므로 경로의 RE당 SNR |a|²/σ_H²는 P_echo − N_floor(B_occ)입니다. (프레임 i, tx, rx)의 잡음은 SeedSequence(entropy=[noise_seed, 0x5EA024], spawn_key=(0, i, id32(tx), id32(rx))) (PCG64, 실수부를 먼저 뽑고 허수부를 다음에 뽑음, id32는 sha256 앞 4바이트의 little-endian 정수)로 뽑습니다. 그래서 noise_seed가 같은 두 실행은 같은 (프레임, tx, rx) 키에서 잡음이 같고, 채널 내보내기(§10)는 그 잡음을 비트 단위로 그대로 다시 만듭니다. H0 보정 맵(아래)은 따로 둔 스트림 0x5EA031에서 뽑으므로, 보정이 실행이 검사받는 잡음을 다시 쓰는 일은 없습니다.

맵. clutter_removal: "mean_subtract"이면 먼저 부반송파마다 슬로타임 평균을 빼므로 정지한 경로는 정확히 사라집니다. 다음으로 두 축에 창(주기 Hann w[i] = 0.5 − 0.5 cos(2πi/L), 또는 rect)을 곱하고, 부반송파 축(지연)에 K' = z·K점 IFFT, 심볼 축(도플러)에 N' = z·N점 FFT를 합니다(z = zero_pad). 0 Hz가 N'/2행에 오도록 도플러 축을 옮깁니다.

Z[v, u] = Σ_n Σ_k w_t[n] w_f[k] Y[n, k] e^(+j2π k u/K') e^(−j2π n v/N') P[v, u] = |Z|² / (σ_H² Σw_f² Σw_t²)

잡음 셀의 평균이 1이므로 P는 SNR입니다. (τ, f_D)인 경로는 u = τ K' Δf_p, v = f_D N' T_p (Δf_p는 파일럿 부반송파 간격, T_p는 파일럿 주기)에서 가장 크고, 그 셀의 거리합은 c·u/(K'Δf_p), 도플러는 v/(N'T_p)입니다. 빈 중앙에 놓인 경로의 피크는 10 log10 P = SNR_RE + 10 log10(K N) + L_t + L_f이고, 창 손실 L = 10 log10((Σw)²/(L Σw²))은 축마다 rect 0, Hann −1.76 dB입니다. 빈 중앙에서 벗어나면 제로 패딩 없이 축당 약 3.9 dB까지 걸침 손실이 생기며, zero_pad 4–8이면 대부분 되찾습니다. zero_doppler_notch_bins: k이면 거친 도플러 빈 −k…k(|v| < (k + 0.5)·z인 행)는 검정 셀에서도, 훈련 셀에서도 빠집니다. mean_subtract는 0 Hz에 잡음을 남기지 않으므로 노치가 없으면 k = 0을 뜻하고, 잡음 분산을 1 %보다 많이 줄이는 행도 훈련에서 뺍니다. 기준은 1 − |W_t(v)|² / (N Σw_t²) < 0.99이고, W_t는 슬로타임 창의 N'점 DFT입니다. z = 1의 rect이면 0 Hz 행, Hann이면 −1…1행(±1행은 잡음의 5/6만 남음)입니다. metadata.sensing.detection.map에 notch_bins, notched_rows, non_training_rows(부호 있는 v)가 나옵니다.

CFAR. 검정 셀마다 그 셀을 둘러싼 ±(guard + train) 셀 상자에서 가드 상자를 뺀 부분이 훈련 셀입니다. guard와 train은 [지연, 도플러] 반폭이고, 상자는 두 축 모두 끝에서 반대쪽으로 이어집니다. 기본값은 guard [1, 1], train [8, 4]로 N_t = 17·9 − 3·3 = 200입니다. 통계량 s가 임계값보다 크고(같으면 아님) 노치에 걸리지 않은 셀을 검출합니다.

type통계량 spfa_cell에서의 해석적 임계값 α
caP / 훈련 셀 평균α = N_t (P_fa^(−1/N_t) − 1), 즉 P_fa = (1 + α/N_t)^(−N_t)
osP / x₍k₎, 훈련 셀 중 k번째로 작은 값(os_rank, 기본 ⌈0.75 N_t⌉)Π_{i<k} (N_t − i)/(N_t − i + α) = P_fa의 해

훈련 상자 안에 훈련 행의 셀이 n_v < N_t개뿐인 검정 셀(노치 옆)은 그 셀들의 평균(CA)이나 k_v = ⌈k·n_v/N_t⌉번째로 작은 값(OS)을 쓰고, 임계값 하나로 셀마다 P_fa가 지켜지도록 통계량을 N_t 척도로 옮깁니다. CA는 s' = N_t((1 + s/n_v)^(n_v/N_t) − 1)로 어떤 임계값에서도 정확하고, OS는 s' = s·α(N_t, k)/α(n_v, k_v)로 pfa_cell에서 정확합니다. (비워진 0 Hz 행이 훈련에 들어가던 때에는 rect + mean_subtract가 |v| = 1…5행에서 pfa_cell의 약 1.9배로 오경보를 냈습니다.)

이 P_fa는 셀이 서로 독립인 지수 분포일 때(rect 창, 패딩 없음) 성립합니다. Hann 창과 제로 패딩은 이웃 셀을 상관시키므로 실제 달성률은 pfa_cell보다 높습니다. 주기 Hann 맵에서 N_t = 40이면 10⁻³ 목표에 1.9·10⁻³이 나옵니다. 실제로 달성한 값은 아래 보고에 나옵니다. threshold: "h0_calibrated"이면 α 대신 저장된 결과(h0_result_id)의 H0 맵에서 구한 경험적 임계값을 씁니다. 그 결과는 sensing_background_ensemble(§12, 실현 × 잡음 프레임 × 링크 전부)이거나 H0 배경 프레임을 저장한 시나리오 결과(프레임 전부, 캐시된 H0 목록을 함께 쓰는 프레임은 한 번만 합성)이고, H0 맵은 이번 실행의 파형, 맵, CFAR, σ_H²로 만듭니다. 그 잡음은 SeedSequence(entropy=[seed, 0x5EA031], spawn_key=(r, m, id32(tx), id32(rx)))에서 뽑습니다(앙상블은 그 seed와 realization r, 시나리오 출처는 그 실행의 noise_seed와 r = 0). 실행 중 잡음의 0x5EA024 스트림은 절대 쓰지 않으므로, 시드가 같아도 아래의 달성 P_FA는 held-out 검사로 남습니다. 노치에 걸리지 않은 셀의 통계량이 n개, m = ⌊n·p⌋이면 임계값은 sorted(s)[n − m](0부터 셈)입니다. m은 20 이상이어야 하고, 평가하는 셀은 최대 5·10⁸개입니다. system_pfa를 주면 통계량은 프레임마다 모든 링크와 셀에 걸친 최댓값이 되고, 그 값들의 system_pfa 분위수가 임계값이 됩니다. 이때 H0 출처는 이번 실행의 TX → 센싱 RX 링크를 모두 덮어야 합니다(아니면 400). 더 적은 링크에 걸친 최댓값은 임계값을 너무 낮게 잡기 때문입니다(약 링크 수 / 보정 링크 수 × system_pfa). 셀 임계값이면 H0 맵이 없는 링크는 다른 링크로 보정한 임계값을 쓰고 경고가 붙습니다(source.uncalibrated_links).

출력. SensingFrame.detections는 링크 순서(TX 우선)대로, 링크 안에서는 통계량이 큰 순서대로 모든 검출을 담습니다. 각 항목에는 tx_id, rx_id, cell_tau(u), cell_nu (부호 있는 v), delay_ns, range_sum_m, doppler_hz(축마다 이웃 셀의 10 log10 P에 포물선을 맞춰 보간하고 ±0.5셀로 자른 값), statistic, threshold, power_db(10 log10 P), snr_est_db가 있습니다. snr_est_db는 비편향 잡음 평균(CA는 훈련 평균, OS는 x₍k₎ / Σ_{i=N_t−k+1}^{N_t} 1/i)으로 구한 10 log10(P/잡음 − 1)입니다. 검출에는 참값이 없습니다. 연관은 여전히 오라클입니다. 에코의 참 (거리합, 도플러)에 가장 가까운 맵 셀에서 G = oracle_gate_cells 거친 셀(× zero_pad) 안에 검출이 있으면 그 링크 보고는 detected가 됩니다(축 끝은 반대쪽과 이어짐). 축마다 2G + 1셀이고, oracle_gate_cells: 0이면 그 셀 하나입니다. 그런 검출 가운데 가장 강한 것이 measured_range_m, measured_doppler_hz, range_bin, doppler_bin, detection_index(detections 안의 위치)를 정하고, 융합과 EKF는 이 값을 씁니다. 게이트 안에 검출이 없으면 이유는 cfar_miss이고 측정 필드는 null입니다. snr_db, bistatic_range_m, doppler_hz, path_id는 오라클 값 그대로이고, §6의 MTI 노치는 쓰지 않습니다. 맵이 이미 잡음을 더하므로 measurement_noise는 경고와 함께 무시됩니다. 링크마다 강한 순서로 최대 max_detections_per_link(256)개를 저장하며, 게이트에 든 검출은 이 상한과 상관없이 남습니다.

달성 P_FA. metadata.sensing.detection에는 맵·CFAR·임계값 설정(α, 또는 n, m, 분위수 규칙이 담긴 보정 출처), 잡음 시드, RE당 N0, TX별 σ_H², 그리고 achieved가 들어 있습니다.

셋 다 95 % Clopper–Pearson 구간과 함께 나옵니다. 오경보는 자기 링크의 어느 게이트에도 들지 않는 검출 전부입니다. 열잡음, 클러터 잔여, 다중 경로 고스트, 게이트 밖 타깃 사이드로브를 가리지 않고 셉니다. link_frame과 system은 맵마다, 프레임마다 베르누이 시행 하나를 세므로 정확한 이항 구간입니다. 셀은 서로 독립이 아닙니다. Hann 창과 제로 패딩이 이웃을 상관시켜 오경보가 뭉치므로, cell 구간은 유효 표본 n/φ 가운데 k/φ의 Clopper–Pearson 구간입니다. φ = max(1, χ²/(M − 1))은 맵 M개의 맵별 오경보 수에 대한 Pearson 과산포 (cell.dispersion)입니다(잡음에서 잰 값: Hann 1.3, Hann에 zero_pad 2이면 3, 패딩 없는 rect 약 1). §9의 Pd 몬테카를로는 Wilson 구간을 그대로 씁니다.

센 정지 경로는 0 Hz 근처의 약한 에코를 가립니다. 자유 공간에서 300 m 바이스태틱 기선, 43 dBm, 0.052 m² 타깃(G1 기하)이면 직접파가 에코보다 68 dB 셉니다. Grid S 10 ms, Hann, 클러터 제거 없음에서는 에코(적분 SNR 12 dB, 22 Hz)가 모든 프레임에서 cfar_miss였고, 직접파 사이드로브 때문에 달성 셀 P_FA가 설계값 10⁻⁴ 대신 1.3·10⁻³(95 % 구간 0.7–2.2·10⁻³, 과산포 3.9)이 됐습니다. CPI 50 ms, rect 창, clutter_removal: "mean_subtract"(노치 0이 따라옴)이면 바이스태틱과 모노스태틱 에코가 모든 프레임에서 검출됐고 달성 셀 P_FA는 6.9·10⁻⁵(95 % 구간 3.1–13.3·10⁻⁵)였습니다. isac_drone_fr1 데모(NR 612 × 280, H0 배경)에서는 평균 제거로 게이트 안 검출이 링크-프레임 128개 중 36개에서 127개로 늘었습니다. 시나리오 폼의 "Clutter removal" 선택은 배경을 켜면 기본으로 평균 제거입니다.

필드기본값의미
detection.mode"threshold""cfar"이면 검출기를 켭니다(waveform 필요).
map.window, map.zero_pad"hann", 1두 축의 창, FFT 길이 배수(1–8).
map.clutter_removal"none""mean_subtract"이면 부반송파마다 슬로타임 평균을 뺍니다(노치 0이 따라오고, 비워진 행은 훈련하지 않음).
map.zero_doppler_notch_binsnull검정도 훈련도 하지 않을 거친 도플러 빈 −k…k.
cfar.type, cfar.guard, cfar.train"ca", [1, 1], [8, 4]검출기와 [지연, 도플러] 반폭.
cfar.os_rank, cfar.pfa_cell⌈0.75 N_t⌉, 1e-4OS 순위, 셀당 설계 P_fa.
threshold, h0_result_id, system_pfa"analytic", null, null보정 임계값의 출처와 프레임당 목표.
oracle_gate_cells, max_detections_per_link1, 256가장 가까운 셀 둘레의 연관 게이트, 링크당 저장할 검출 수.

다음은 아무것도 풀기 전에 400입니다. 맵보다 넓은 CFAR 창(zero_pad를 올리거나 train을 줄이세요), 셀이 2²⁴ = 16,777,216개(z²·K·N)를 넘는 맵, 파일럿이 하나뿐인 축의 Hann 창, 모든 도플러 행을 덮는 노치, 훈련 셀이 하나도 없는 검정 행. 모르거나, 종류가 다르거나, 저장된 H0 프레임이 없거나, 이번 실행의 링크가 하나도 없거나, system_pfa인데 이번 실행의 링크가 빠졌거나, 기대 초과 횟수가 20에 못 미치는 h0_result_id도 400입니다. waveform 없이 CFAR를 켜면 422입니다.

10. 센싱 데이터셋 내보내기

POST /export/sensing-dataset는 저장된 시나리오 결과의 프레임별 센싱(§6)을 학습이나 오프라인 분석용 표로 펼쳐 export/sensing_dataset/ 아래 zip 하나로 씁니다. 솔브는 돌리지 않습니다. UI에서는 Toolbar ▸ Actions ▸ Sensing dataset (.npz)입니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/export/sensing-dataset \
  -H "Content-Type: application/json" \
  -d '{"formats": ["npz", "csv"], "include_echo_paths": true,
       "split": {"train": 0.8, "val": 0.1, "test": 0.1, "seed": 0}}'
필드기본값의미
result_idsnull내보낼 시나리오 결과, 이 순서대로. null이면 센싱 프레임이 있는 저장된 시나리오 결과 전부.
skip_without_sensingfalseresult_ids와 함께: 나열한 결과에 센싱 프레임이 없으면 400 대신 건너뛰고 warnings에 이름을 남깁니다. UI는 일부만 고를 때 이 값을 보냅니다.
formats["npz"]npz, csv, parquet 중 아무것이나(parquet은 pyarrow, 즉 parquet extra가 필요: pip install "seam-studio[parquet]").
include_echo_pathsfalseechoes 표도 씁니다.
splitnull프레임 단위 분할 {train, val, test, seed}(비율 합은 1). null이면 모든 행이 all입니다.

응답에는 zip_name과 download_url(프로젝트 assets 경로), zip 안의 files, 행 수 (num_rows, rows_per_result, rows_per_split, num_echo_rows), detected_fraction, size_bytes, warnings가 있습니다. zip에는 다음이 들어 있습니다.

열은 늘 모두 있어서 스키마가 바뀌지 않습니다. 값이 없으면 float 열은 NaN, 문자열 열은 ""입니다. npz는 열마다 1차원 배열 하나입니다(문자열은 <U라 np.load(..., allow_pickle=False)로 읽힙니다). csv는 헤더가 있고, float는 왕복 변환이 정확한 가장 짧은 형식, NaN은 빈 칸, 불리언은 true/false입니다. parquet은 NaN을 NaN 그대로 둡니다.

links 열

열dtype단위역할출처
result_id, splitstrmeta
frame_indexint64metaframes 안의 인덱스
time_sfloat64smetaframe.time_s
tx_id, rx_id, target_idstrmeta링크 보고
tx_pos_x/y/z, rx_pos_x/y/zfloat64mfeatureframe.sensing.nodes
tx_vel_x/y/z, rx_vel_x/y/zfloat64m/sfeatureframe.sensing.nodes
bistatic_range_m, doppler_hz, echo_power_dbm, snr_db, measured_range_m, measured_doppler_hzfloat64m, Hz, dBm, dBfeature링크 보고
range_bin, doppler_binfloat64(NaN = 없음)셀feature링크 보고
num_echoesint64feature링크 보고
multipathboolfeature링크 보고
aod_az_deg, aod_el_deg, aoa_az_deg, aoa_el_degfloat64degfeature보고된 에코(frame.sensing.echoes에서 path_id로 찾음)
pd, pd_mcfloat64feature링크 보고(실행에 pfa가 없으면 NaN)
detectedboollabel
reasonstrlabeldetected / no_echo / below_threshold / mti_rejected / cfar_miss(CFAR 실행, §9 참고)
position_true_x/y/z, velocity_true_x/y/zfloat64m, m/slabel그 타깃의 estimates[] 행
est_statusstrestimateok / insufficient_links / diverged
n_links_detected, n_links_usedint64estimate
position_est_x/y/z, velocity_est_x/y/z, position_error_m, velocity_error_m_s, gdopfloat64m, m/sestimate
track_statusstrtrack추적을 껐으면 ""
track_position_x/y/z, track_velocity_x/y/z, track_position_error_m, track_velocity_error_m_s, track_position_std_m, track_updates, track_gatedfloat64m, m/strack없으면 NaN

estimate와 track 열은 타깃·프레임마다 값이 하나이고, 그 타깃의 링크 행마다 되풀이됩니다. velocity_true_* 레이블은 §6을 따릅니다. 정확히 경유점 시각이면 나가는 구간의 속도이고, once 궤적의 끝에서는 0입니다. v0.1.14 이전 결과는 그런 프레임에 두 구간의 평균(또는 속력의 절반)을 담고 있으므로, 예전 결과와 새 결과를 섞은 데이터셋은 그 프레임에서 두 규약이 섞입니다.

echoes 열: result_id, split, frame_index, time_s, path_id, tx_id, rx_id, target_id(""면 include_comm_paths의 통신 경로), path_type, delay_ns, bistatic_range_m(c·τ), power_dbm, path_gain_db, phase_rad, doppler_hz, aod_az_deg, aod_el_deg, aoa_az_deg, aoa_el_deg, num_interactions(int64), direct(bool: 상호작용이 [sensing]뿐), reported(bool: 링크 보고가 고른 에코).

분할. 단위는 프레임입니다. (결과, 프레임) 쌍을 내보내는 순서대로 놓고 numpy.random.default_rng(seed)로 섞은 뒤, 앞의 n_train개가 train, 다음 n_val개가 val, 나머지가 test입니다. 개수는 train·F, val·F, test·F를 최대 잉여 방식으로 반올림한 값입니다(동률이면 작은 비율 쪽, 그다음 train, val, test 순). 그래서 합이 F이고, 각각 정확한 몫과 한 프레임 넘게 차이 나지 않으며, 비율이 0인 분할에는 프레임이 하나도 가지 않습니다. 한 프레임의 행은 links든 echoes든 모두 같은 분할에 들어가고, 시드가 같으면 분할도 같습니다.

예전 결과. v0.1.13 이전 결과에는 nodes가 없습니다. 이때 TX/RX 위치는 프레임의 device_states, 없으면 지금의 씬에서, 속도는 지금 씬의 궤적에서 가져옵니다(실행 당시 계산한 방식 그대로이되, 정확히 경유점 시각에서는 §6의 v0.1.14 규칙을 따릅니다). 그러면 "predates v0.1.13" 경고가 붙고(씬 해시가 다르면 "scene changed since the run"도), 매니페스트에는 "kinematics_source": "current_scene"이 적힙니다.

오류. 모르는 프로젝트, 그리고 모르거나 파일이 없거나 읽을 수 없는(올바른 시나리오 결과가 아닌 경우도 포함) result_ids 항목은 404입니다. result_ids가 null이면 그런 파일은 건너뛰고 warnings에 이름을 남깁니다. 나열한 결과에 센싱 프레임이 없을 때 (skip_without_sensing이 아니면), 남는 결과가 하나도 없을 때, pyarrow 없이 parquet을 요청했을 때, 행이 500 000개(links + echoes)를 넘을 때는 400이고 아무것도 쓰지 않습니다. 모르는 형식은 422입니다.

메모리. 행은 메모리에서 만들어집니다. Python 리스트로 행당 약 1.9 KB, 배열과 인코딩한 파일로 행당 약 0.6 KB가 더 들어서, 500 000행 상한에서 내보내면 RAM을 최대 약 1.3 GB 씁니다. 메모리가 작은 기기에서는 결과를 적게 골라 내보내세요.

채널 텐서 (include_channel, #22)

두 opt-in 플래그는 신호 수준 검출기에 필요한 것을 내보내기에 더합니다. 텐서는 저장된 경로 행에서 합성하므로 이때도 솔브는 돌리지 않습니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/export/sensing-dataset \
  -H "Content-Type: application/json" \
  -d '{"include_echo_paths": true, "include_background_paths": true,
       "include_channel": {"subcarriers": "pilot",
                           "components": ["echo", "background_h0", "total"],
                           "noise": {"enabled": true}}}'
필드기본값의미
include_background_pathsfalsebackground.<fmt>도 씁니다. 저장된 H0 / H1 배경 경로마다 한 행입니다(§6의 배경 경로 H0/H1).
include_channelnullchannel.npz도 씁니다(아래 옵션).
include_channel.subcarriers"pilot"pilot은 K × N 파일럿 격자, active는 CPI의 모든 심볼 × 모든 활성 부반송파.
include_channel.waveformnull텐서의 격자. null이면 실행마다 그 실행의 sensing.waveform, 없으면 Grid S(15.625 kHz × 1280 SC, 1/14400 s 심볼 9개마다 comb-5 파일럿, 10 ms에 K 256 × N 16).
include_channel.components["echo"]echo, background_h0, background_h1, total 중 아무것이나(중복 제거).
include_channel.noise.enabledfalse열잡음 실현값 noise도 씁니다.
include_channel.noise.n0_dbm_per_renullRE당 N0 [dBm]. null이면 실행 설정의 −174 + 10 log10(scs) + NF.
include_channel.noise.seednullnull이면 실행마다 그 실행의 sensing.noise_seed.
include_channel.pilot_lsfalseLS 파일럿 추정 H_ls = H_total + noise도 씁니다(잡음 필요).
include_channel.max_elements50,000,000channel.npz의 complex64 원소 전체 상한(최대 2·10⁸).

두 플래그 중 하나라도 켜면 echoes 행에 a_re, a_im(무차원 이득 10^(path_gain_db/20)·e^(j·phase_rad)의 실수부·허수부, 반송파 위상 포함)과 tx_power_dbm이 붙고, 상호작용마다(;로 이음, LoS나 값이 없으면 "") interaction_types, rf_material_ids, prim_ids(센싱 상호작용이면 타깃 액터 id)가 붙습니다. 이때 매니페스트의 schema_version은 2이고, 응답에는 num_background_rows, num_channel_rows, channel_elements가 붙습니다. 둘 다 끄면 모든 표와 매니페스트 키가 v0.1.15와 같습니다(created_at, seam_version 제외).

background.<fmt> 열은 result_id, split, frame_index, time_s, hypothesis (h0 / h1), path_id, tx_id, rx_id, path_type, delay_ns, bistatic_range_m, power_dbm, path_gain_db, phase_rad, doppler_hz, 출발·도착 각도, num_interactions, 그리고 위의 채널 열 여섯 개입니다. 가설을 실제로 풀어서 저장한 프레임에만 행이 있습니다. 캐시 적중 프레임은 앞 프레임의 경로를 다시 쓰므로, 매니페스트의 background_index가 프레임마다 [frame_index, H0 출처 프레임, H1 출처 프레임](null이면 풀지 않았거나 저장하지 않았거나 실패함)을 알려 줍니다. 기본값 sensing.background.store: "h0"인 실행에는 H1 행이 없습니다. 이 표의 상한은 따로 500 000행입니다.

channel.npz(zip 안에 압축 없이 저장)는 결과 × 센싱이 있는 프레임 × TX → 센싱 RX 링크(TX 우선, 실행의 sensing_tx_ids × sensing_rx_ids)마다 한 행입니다.

배열모양, dtype의미
row, frame_index[R] int64행 번호, 프레임 인덱스.
result_id, tx_id, rx_id[R] str
time_s, sigma_h2, es_dbm_per_re[R] float64프레임 시각, 그 행 TX의 σ_H² = N0/Es, RE당 Es [dBm].
f_hz, t_s[K], [N] float64기저대역 부반송파 주파수, 심볼 시각.
H_echo, H_background_h0, H_background_h1, H_total[R, N, K] complex64요청한 성분, 잡음 없음.
noise, H_ls[R, N, K] complex64noise.enabled / pilot_ls일 때.

H[n, k] = Σ_p a_p e^(−j2π f_k τ_p) e^(+j2π f_D,p t_n)

f_k는 부반송파 k의 기저대역 주파수입니다. phase_rad에 −2π f_c τ가 이미 들어 있으므로 LoS 경로 하나의 위상은 −2π(f_c + f_k)τ이고, 반송파는 한 번만 들어갑니다. 지연 정규화는 하지 않으며, 다가오는 경로의 도플러가 양수입니다. echo는 그 링크의 타깃 에코만 합칩니다 (include_comm_paths의 통신 경로는 빼고). total은 프레임에 H1이 있으면 에코 + H1, 없으면 에코 + H0이고, CFAR 맵과 같은 규칙입니다. 배경이 없으면 0으로 채우고 경고합니다. H1을 풀었지만 저장하지 않은 실행(기본값 sensing.background.store: "h0")은 background_h1, total, pilot_ls(그 total은 실행의 맵과 달라짐)를 다시 만들 수 없어 400입니다. store: "both"로 돌리거나 echo / background_h0만 내보내세요. store: "none"의 H0도 마찬가지입니다. 행의 잡음은 위 CFAR 절의 키로 그 행의 σ_H²에서 뽑습니다. 그래서 실행 자신의 파형으로 파일럿 격자를 내보내면, 내보낸 잡음이 그 실행의 맵에 들어간 잡음과 비트 단위로 같습니다. 매니페스트의 channel 블록에는 격자(waveform, numerology), 성분, 결과별 잡음 시드와 N0, 잡음 키, 진폭 규약, 합성식이 있습니다. 내보낸 결과 둘이 잡음 시드를 공유하면 경고가 붙습니다.

import numpy as np
ch = np.load("channel.npz", allow_pickle=False)
H = ch["H_total"][0].astype(np.complex128)      # [심볼, 부반송파]
Y = H + ch["noise"][0]                           # LS 파일럿 추정
snr_re = np.abs(H) ** 2 / ch["sigma_h2"][0]      # 잡음 없는 채널의 RE당 SNR

한 번에 내보내는 결과는 모두 같은 격자(같은 주파수와 심볼 시각)여야 하고, 아니면 400입니다. 따로 내보내거나 include_channel.waveform을 지정하세요. 원소 수(행 × N × K × 배열 수)가 max_elements를 넘으면 아무것도 쓰기 전에 400입니다. 전력을 알 수 없는 TX(저장된 경로 이득이 없고 씬에도 없음)는 경고와 함께 뺍니다. 텐서는 메모리에서 만들며 원소당 8바이트 (기본 상한에서 400 MB)입니다.

11. 한계

12. H0 보정 앙상블 (#30)

POST /simulate/sensing-background-ensemble는 CFAR 임계값을 보정하고 디지털 트윈 불일치를 비교하는 데 쓰는 표적 없는(H0) 프레임을 만듭니다. 저장된 시나리오 프레임 하나의 H0 배경(§6 배경 경로 H0/H1: 표적 액터를 빼고 diffuse 반사를 켠 PathSolver)을 그 프레임의 노드 위치에서 realization마다 다시 풉니다. realization 0은 공칭 트윈입니다. 그 뒤의 realization은 seed에서 섭동을 뽑습니다. 무선 재질마다 ε_r·σ 배율, 노드 사이트마다 위치 오프셋 하나, 바이스태틱 센싱 수신기마다 반송파 주파수 오프셋(CFO)입니다. 결과에는 realization별 경로와 seed가 저장됩니다. 잡음 프레임은 저장하지 않고 seed로 비트 단위까지 똑같이 다시 뽑습니다.

curl -X POST http://127.0.0.1:8000/api/projects/sample_demo/simulate/sensing-background-ensemble \
  -H "Content-Type: application/json" \
  -d '{"source": {"result_id": "sionna_scenario_003", "frame_index": 0},
       "n_realizations": 20, "noise_frames_per_realization": 500, "seed": 1,
       "perturb": {"material": {"eps_r_pct": 10, "sigma_pct": 30},
                   "node_pos_sigma_m": 0.1, "cfo_hz": 5}}'
필드기본값의미
source.result_id, source.frame_index필수앙상블이 노드 운동 상태, 액터 자세, 표적을 가져올 시나리오 결과와 프레임. 그 프레임에 센싱이 있어야 합니다.
n_realizations필수(1–200)realization 0은 공칭입니다.
noise_frames_per_realization필수(1–100,000)realization마다 정의되는 H0 잡음 프레임 m = 0 … n−1. 필요할 때 뽑습니다.
seed0섭동 추첨과 잡음의 seed.
perturb.material.eps_r_pct, perturb.material.sigma_pct0, 0무선 재질마다 배율의 표준편차[%]: ε_r × (1 + d), σ × (1 + d′), d, d′ ~ N(0, (pct/100)²). 솔버가 ε_r은 1, σ는 0에서 자릅니다.
perturb.material.scope"material"솔버 무선 재질마다 한 번 뽑습니다. "prim"은 400입니다(재질 그룹이 합쳐진 메시라서).
perturb.node_pos_sigma_m0노드 사이트마다 N(0, σ² I₃) 오프셋[m]. 서로 1 m 안에 있는 TX와 센싱 RX는 한 사이트라 같이 움직입니다.
perturb.cfo_hz0바이스태틱 센싱 RX(모든 TX에서 1 m 넘게 떨어진 RX)마다 N(0, cfo²)[Hz]. 합성할 때 모든 경로의 도플러 오프셋으로 들어갑니다(양수 = 접근). 공동 위치 수신기는 TX와 발진기를 공유하므로 받지 않습니다.
backgroundnull배경 솔버 옵션(§6). null이면 원본 실행의 sensing.background를 모드 h0_removed로 쓰고, 그것도 없으면 기본값입니다. 지정하면 mode는 "h0_removed"여야 합니다(아니면 422).

무엇을 푸는가. 링크는 원본 실행의 모든 TX × 센싱 RX이고 TX 우선 순서입니다 (metadata.sensing.sensing_tx_ids, 없으면 프레임 노드 중 센싱 수신기가 아닌 것). 노드는 원본 프레임의 위치·속도(frame.sensing.nodes)에 오프셋을 더한 자리에, 액터는 프레임의 actor_states에 놓이고, 표적은 그 자세로 풀어서 뺍니다. 앙상블은 원본 실행의 백엔드와 시뮬레이션 설정(config_snapshot)을 쓰고 realization을 하나씩 차례로 풉니다. 재질·위치 추첨이 없는 realization(공칭이거나 CFO만 있는 것)은 realization 0과 솔브 입력이 같으므로 그 경로를 그대로 씁니다(background_h0: null, source_realization: 0). 다시 풀지 않습니다.

추첨. realization r ≥ 1은 numpy.random.default_rng(SeedSequence(entropy=[seed, 0x5EA030], spawn_key=(r,)))에서 이 순서로 뽑습니다.

  1. 무선 재질마다(이름순): normal(0, eps_r_pct/100), 이어서 normal(0, sigma_pct/100)
  2. 노드 사이트마다(TX 먼저, 다음 센싱 수신기, 사이트 순서): normal(0, node_pos_sigma_m, 3)
  3. 바이스태틱 센싱 RX마다(id순): normal(0, cfo_hz)

σ = 0이어도 추첨은 모두 하므로, 섭동 하나를 켜도 다른 섭동의 값은 바뀌지 않습니다. 0인 추첨은 material_draws, node_offsets_m, cfo_hz에서 빠지고, 하나라도 남으면 label이 perturbed입니다.

H0 프레임. 링크 (tx, rx)에서 realization r의 잡음 프레임 m은 다음과 같습니다.

Y[n, k] = H_r[n, k] + w[n, k], H_r[n, k] = Σ_p |a_p| e^{jφ_p} e^{−j2π f_k τ_p} e^{+j2π (f_D,p + cfo_rx) t_n}

격자는 사용하는 쪽의 파일럿 격자입니다(§6 파형 수비학. f_k는 기저대역, t_n은 파일럿 심볼 시각, φ_p는 반송파 항이 이미 들어 있는 저장된 phase_rad). w는 E|w|² = σ_H²(RE당 N0 / RE당 Es)인 복소 가우스 잡음이고 SeedSequence(entropy=[seed, 0x5EA031], spawn_key=(r, m, id32(tx), id32(rx)))에서 뽑습니다. id32는 id의 SHA-256 앞 4바이트(little endian)입니다. 0x5EA031은 H0 보정 스트림입니다. 시나리오 실행은 실행 중 잡음을 0x5EA024에서 뽑으므로, 앙상블 seed가 어떤 실행의 noise_seed와 같아도 그 실행의 잡음을 다시 만들지 않습니다. 잡음 크기는 사용하는 쪽이 정합니다. 검출기는 자기 파형, 잡음 지수, TX 전력을 씁니다. 프레임, realization, 링크는 서로 독립인 스트림에서 뽑습니다(이웃 프레임의 상관은 10⁵ RE에서 |ρ| < 0.01).

임계값 보정. sensing.detection.threshold: "h0_calibrated"로 두고 h0_result_id에 앙상블의 result_id를 넣습니다(§9 신호 수준 검출, #24). 검출기는 실행의 수비학, 맵, CFAR 옵션으로 realization × 잡음 프레임 × 링크마다 맵을 만들고, 셀 통계량(또는 system_pfa이면 모든 링크에 걸친 프레임별 최댓값)의 sorted(values)[n − floor(n·p)]를 임계값으로 씁니다. P_FA 10⁻²에서 보정 프레임이 n = 10,000이면, held-out 10,000 프레임의 오경보는 보정 95 %에서 74–129건입니다(베타-이항: 보정 자체의 흔들림이 이항 구간 81–120을 넓힙니다). 10⁻³은 n ≥ 20,000이 필요하고 10⁵를 권장합니다.

결과. SensingBackgroundEnsembleResultSet에는 source, seed, noise_frames_per_realization, background(쓴 옵션), links, 공칭 nodes, 그리고 realization마다 index, label, material_draws(이름 → eps_r_scale, sigma_scale), node_offsets_m, cfo_hz, background_h0(RayPath 목록. 다시 쓰면 null), source_realization, solve_s, num_paths, dropped_paths, total_power_db("tx>rx" → 10 log10 Σ|a|²), warnings가 있습니다. metadata에는 요청과 request_hash, material_names, sites, bistatic_rx_ids, solves, total_solve_s, draw_key, noise_key, provenance 해시가 들어갑니다. 다시 읽을 때는 GET /results/sensing-background-ensemble?result_id=…를 씁니다(result_id가 없으면 최신). &summary=true이면 realization마다 background_h0 경로를 뺀 결과를 돌려주고, UI는 이것을 보여 줍니다. 레이블과 정리(prune)는 다른 결과 종류와 같습니다. 저장된 경로 하나는 JSON으로 약 0.7–1 KB이고, 배경 옵션의 기본값은 링크당 경로 256개입니다.

오류. 없는 프로젝트나 원본 결과, 또는 올바른 시나리오 결과가 아닌 원본 파일: 404. 범위를 벗어난 프레임 인덱스나 센싱이 없는 프레임, 장면에서 사라진 실행의 디바이스나 표적 액터, scope: "prim", 솔브하는 realization 전체에서 저장 경로 2,000,000개 초과 (background.max_paths_per_link를 낮추세요), 배경 솔브가 실패한 realization(잡음만 있는 H0를 저장하게 됨): 400. 원본 실행의 백엔드를 쓸 수 없거나 센싱이 없음: 409. 범위 밖 값, 모르는 키, h0_removed가 아닌 배경 모드: 422.

한계.

관련 문서