레이더 센싱: 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란
- 레이더 단면적 σ(m², 또는 dBsm = 10·log10(σ / 1 m²))는 실제 타깃과 같은 전력을 되돌려 보내는 등방성 재방사체의 면적입니다.
- 수신 에코는 바이스태틱 레이더 방정식
Pr / Pt = Gt · Gr · λ² · σ / ((4π)³ · d1² · d2²)을 따릅니다. d1은 TX→타깃, d2는 타깃→RX 거리이고, 모노스태틱(RX가 TX 위치)이면 익숙한d⁴법칙이 됩니다. - 3GPP TR 38.901 §7.9는 여기서 더 나아가, 타깃 유형마다 하나 이상의 산란점을 두고 각 산란점의 RCS가 입사·산란 방향에 따라 달라지게 합니다(각도별 로브). 랜덤 성분도 선택할 수 있습니다. Sionna RT 2.2가 이 모델을 구현하며, SEAM은 이를 액터 단위로 노출합니다.
2. 액터 바인딩
액터를 선택하고 Inspector → Sensing target을 엽니다. 바인딩은 액터의
actor.sensing에 저장되며(../scene_format.ko.md 참고),
null이면 타깃이 아닙니다.
Model은 둘 중 하나입니다.
- TR 38.901 (
"tr38901") — 아래 3GPP 타깃 모델입니다.object_type의 기본값은 액터 kind에서 정해집니다(car →vehicle-multi-sp, human →human, UAV →uav-small-size).custom액터는 직접 골라야 합니다.model_type의 기본값은 해당 타입이 정의하는 가장 높은 모델입니다. - Constant RCS (
"constant") — 타깃 중심에 등방성 산란점 하나를 두고rcs_dbsm(비우면 0 dBsm)과 선택적인 교차편파비xpr_db(비우면 편파 변환 없음)를 씁니다.
object_type | 모델 타입 | 기본 적용 kind | 평균 모노스태틱 σ_M (dBsm) | 산란점 | TR 38.901 기본 크기 L×W×H (m) |
|---|---|---|---|---|---|
uav-small-size | 1 | uav | −12.81 | 1 | 0.3 × 0.4 × 0.2 |
uav-large-size | 2 | — | −5.85 | 1 | 1.6 × 1.5 × 0.7 |
human | 1, 2 | human | −1.37 | 1 | 0.5 × 0.5 × 1.75 |
vehicle-single-sp | 2 | — | 11.25 | 1 | 5.0 × 2.0 × 1.6 |
vehicle-multi-sp | 2 | car | 11.25 | 5(좌, 뒤, 우, 앞, 지붕) | 5.0 × 2.0 × 1.6 |
agv-single-sp | 2 | — | −4.25 | 1 | 0.5 × 1.0 × 0.5 |
agv-multi-sp | 2 | — | −4.25 | 5 | 0.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_components | TR 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_depth | RCS 깊이. 산란 이벤트 자체도 하나로 셉니다. 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가 됩니다. 액터가 받는 속도는 다음과 같습니다.
- Simulate scenario: 프레임마다 그 시점의 액터 속도.
- 그 밖의 모든 경로 솔브(Simulate paths, 채널 분석, 데이터셋, UE 궤적, GT 재생,
include_comm_paths): t = 0의 궤적 속도. 이 솔브들은 단계 사이에 액터를 움직이지 않으므로, 실행 내내 액터가 작성한 자세에 머뭅니다. - 궤적이 없는 액터는 정지 상태입니다.
- 데이터셋이 경로를 샘플링하는 액터(
sampling.actor_id)도 정지 상태입니다. UE는 그 경로를 따라 움직이지만, 액터의 메시는 작성한 자세에 세워 둔 채로 남습니다.
움직이는 액터에 붙은 디바이스(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. 내보내기
- RFData (
POST /export/rfdata) — 센싱 결과가 있으면export/rfdata/sensing.json을 씁니다. 타깃 요약과 에코 경로(type: "SENSING",doppler_hz,target_id)가 들어가고, 요약에has_sensing이 표시됩니다. - AODT parquet (
POST /export/aodt,"source": "sensing") — 저장된 센싱 결과를 스냅샷 하나로 씁니다. 각 행은"emission"…"reception"으로 이어집니다. AODT에는 센싱 토큰이 없어 타깃 꼭짓점은 AODT 문서의 확산 산란 토큰인"diffuse"로 기록하고, 그object_ids항목은 1 000 000 + 액터 id 정렬 순서에서의 위치입니다(targets순서가 아님). 액터 id로 되돌릴 때는id_map.json의sensing_targets를 쓰세요. - 채널 npz (
POST /export/channel-npz,"include_sensing": true) — 각 에코를 RX 위치에 UE가 있고 TX가 여전히 에코의 TX 위치에 있는 UE × TX 링크에 붙인 뒤 (1 mm 일치) 강한 순으로 정렬합니다.is_nlos는 에코를 무시하고,sensing_path_count가 매칭된 개수를 알려 줍니다. UE별 통신 솔브는 (include_comm_paths처럼) 결과의 타깃을 흡수체로 둡니다. 다른 주파수에서 푼 센싱 결과는 400으로 거부되고, 그 뒤로 TX가 움직인 에코는 경고와 함께 빠집니다. - 센싱 데이터셋 (
POST /export/sensing-dataset) — 시나리오 결과의 센싱 프레임을 links와 echoes 표로 펼칩니다(§10).
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_001 | mock | sionna (sionna-rt 2.2.0, CUDA) |
|---|---|---|
detection_rate | 0.947 (19프레임 중 18) | 0.947 (19프레임 중 18) |
link_detection_rate | 0.439 | 0.439 |
frames_ge3_links | 15 | 15 |
ok_frames (융합) | 10 | 10 |
median_position_error_m (융합) | 0.43 m | 0.79 m |
median_gdop | 1.62 | 1.62 |
tracked_frames / lost_frames | 19 / 0 | 19 / 0 |
median_track_position_error_m (EKF) | 0.36 m | 0.38 m |
p90_track_position_error_m | 2.89 m | 2.98 m |
median_track_velocity_error_m_s | 0.66 m/s | 0.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 필드 | 기본값 | 의미 |
|---|---|---|
enabled | false | 프레임별 센싱 솔브를 돌립니다. |
threshold_db | 13 | 적분 후 SNR의 탐지 임계값(dB, −30…60). |
cpi_s | 0.01 | 코히어런트 처리 구간(CPI, 초). 도플러 분해능 = 1 / CPI. |
cpi_pulses | 1 (폼: 4096) | 코히어런트하게 적분하는 펄스 수 또는 OFDM 자원 요소 수. 이득 = 10·log10(N). |
mti_min_doppler_hz | null | MTI 노치. |f_D − f_nodes|가 이보다 작으면 버립니다(TX/RX가 정지해 있으면 f_nodes = 0, 아래 MTI 참고). null = 1 / CPI(도플러 셀 하나), 0이면 MTI를 끕니다. |
include_comm_paths | false | 프레임마다 타깃을 흡수체로 둔 통신 경로도 풀어 echoes 뒤에 붙입니다(target_id null). 프레임당 솔브가 하나 늘어납니다. |
target_actor_ids | null | 이 액터들만 봅니다(기본: 켜진 바인딩 전부). |
sensing_rx_ids | null | 바이스태틱 레이더 수신기. null = 자동: 선택한 TX에서 1 m 안에 있는 선택된 RX(§7/§8과 같음), 없으면 선택된 RX 전부(v0.1.13). 통신 링크는 여전히 모든 RX를 덮습니다. 모르는 id는 400. |
samples_per_sp / max_depth | 1 000 000 / null | 프레임별 센싱 솔브에 그대로 넘깁니다(§3과 같음). |
measurement_noise | false | 융합에 들어가는 거리·도플러에 가우스 잡음을 더합니다(아래). |
noise_seed | 0 | 그 잡음의 시드. 시드가 같으면 숫자도 같습니다. |
tracking | null | 프레임에 걸친 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면 추적하지 않습니다. |
pfa | null | 링크별 pd의 오경보 확률(§9). null이면 링크 보고에 Pd가 붙지 않습니다. |
detector | Swerling 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가 됩니다.
출력 읽기.
links[]는 TX × 센싱 RX × 타깃마다 한 행이고, 이 순서를 따릅니다.bistatic_range_m,doppler_hz,snr_db, 융합에 쓴measured_*값,range_bin(floor(R / (c/B))),doppler_bin(round(f_D · CPI)), 그리고reason이 있습니다.reason은detected,below_threshold,mti_rejected,no_echo중 하나입니다.pfa를 정하면pd(와pd_mc)가snr_db의 탐지 확률을 줍니다(§9). 에코가 없으면pd = pfa입니다.estimates[]는 타깃마다 한 행입니다.status는ok,insufficient_links(서로 다른 링크가 3개 미만),diverged(수렴 실패, 퇴화한 기하, 모호한 링크 3개 해, 또는 RMS 잔차가 거리 합 셀 c/B를 넘음) 중 하나입니다. 그 밖에links_used,position_est/velocity_est, 참값(position_true= 타깃 직육면체 중심,velocity_true)과 각각의 오차가 들어갑니다.gdop= sqrt(trace((JᵀJ)⁻¹))이고, 위치 오차는 대략 GDOP × 거리 오차입니다. 추적을 켜면track_*필드도 붙습니다(아래).nodes[]에는 선택한 TX 전부, 이어서 센싱 RX 전부가 그 프레임에서 쓴 위치·속도와 함께 들어갑니다(액터에 탄 디바이스는 액터와 함께 움직입니다). v0.1.13 이전 결과에서는null입니다.metadata.sensing에는 상수(잡음 바닥, 적분 이득, λ, 거리·도플러 분해능, MTI 노치, 블라인드 속도), 그 실행의sensing_rx_ids와sensing_rx_rule, 타깃별 통계가 들어갑니다. 타깃별로detection_rate(탐지 링크가 하나 이상인 프레임 비율),link_detection_rate,frames_ge3_links_rate,ok_frames, 위치 오차 중앙값과 p90, 속도 오차 중앙값, GDOP 중앙값이 있습니다.
속도 참값. 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_r | 200 m에서 SNR | 13 dB 도달 거리 | MTI 블라인드 속도(10 ms) |
|---|---|---|---|---|---|
| 3.5 GHz | 0.0857 | −116.17 dBm | 13.94 dB(탐지) | 211 m | 4.28 m/s |
| 28 GHz | 0.0107 | −134.23 dBm | −4.12 dB(놓침) | 74.6 m | 0.54 m/s |
그래서 28 GHz의 모노스태틱 탐지 거리는 10^(−18.06/40) = ×0.354로 줄어듭니다. 안테나
배열이 이 손실을 되찾아 줍니다. 이 절의 프레임별 탐지는 여전히 단일 소자로 계산하지만
(§11 한계 참고), §7은 실제 코드북 빔을 합성하고 §8의 steered 모드는 이상적인 배열
이득을 더합니다. 반대로 mmWave의 노치는 속도 기준으로 8배 좁습니다.
추적(EKF)
융합에는 한 프레임 안에 서로 다른 링크 세 개가 필요합니다. sensing 블록에
"tracking": {"enabled": true}를 넣으면(폼에서는 Tracking (EKF)) 타깃마다 등속
확장 칼만 필터가 추정치를 프레임에서 프레임으로 이어 갑니다. 탐지 링크가 한두 개뿐인
프레임에서도 추정치를 다듬고, 하나도 없는 프레임에서도 예측은 이어집니다.
- 운동. 상태 x = [p, v]이고 월드 좌표계입니다. 프레임 사이(dt = 프레임 간격)에는
x ← F·x, P ← F·P·Fᵀ + Q입니다. Q는 이산 백색 잡음 가속도 모델
Q = σ_a²·[[dt⁴/4·I, dt³/2·I], [dt³/2·I, dt²·I]]이고 σ_a =
process_accel_sigma_m_s2(2 m/s²)입니다. - 측정. 타깃의 탐지된 직접 링크가 강한 순서대로 스칼라 두 개씩을 냅니다. 거리 합
measured_range_m(h = |p − TX| + |p − RX|)과 도플러measured_doppler_hz(h = (v_tx · k̂₁ − v_rx · k̂₂ + v · (k̂₂ − k̂₁)) / λ, §4 부호)입니다. 여기서는 기하로 묶지 않습니다. 한 타원체의 두 링크도 수신기 입장에서는 서로 독립인 측정이기 때문입니다. σ_R = (c/B)/√(2·SNR), σ_f = (1/CPI)/√(2·SNR)(각각 최소 1 mm, 1 mHz)로, 잡음을 켰든 껐든measurement_noise규칙을 그대로 씁니다. 움직이는 TX나 RX는 그 프레임의 위치·속도로 들어갑니다. - 갱신. 스칼라를 하나씩, 매번 현재 상태에서 다시 선형화해 반영합니다.
ν = z − h(x), S = H·P·Hᵀ + σ²입니다. ν²/S >
gate_chi2(16 = 4σ, 자유도 1 카이제곱)인 스칼라는 버리고track_gated에 셉니다. 아니면 Joseph 형식으로 갱신하고track_updates에 셉니다. - 시작. 융합이 처음
ok인 프레임에서 융합한 위치·속도(도플러 속도가 없으면 0)로 트랙을 시작합니다. P는 위치 축마다 max(GDOP²·σ̄_R²/3, 0.25 m²)(σ̄_R²는 융합한 링크의 σ_R² 평균, GDOP가 없으면 100 m²), 속도 축마다 25 (m/s)²(속도가 없으면 400)에서 출발합니다. 같은 데이터로 그 프레임에서 또 갱신하지는 않습니다. - 재시작. 두 조건이 함께 맞을 때 그 프레임의 융합에서 트랙을 다시 시작합니다.
게이트가 그 프레임의 스칼라를 하나 이상 버렸고, 융합이
ok인데 예측한 트랙의 3차원 게이트 밖에 있을 때입니다. 이 게이트는 P_pred + 시작 공분산에 대한 마할라노비스 거리²이고, 문턱은gate_chi2와 꼬리 확률이 같은 자유도 3 카이제곱 값입니다(16이면 22.1). 흔한 경우가 경유점 모서리입니다. 예측은 옛 속도를 유지하고, 그 속도를 바로잡을 도플러 스칼라는 게이트에 걸리며, 거리 합만으로는 방향을 틀지 못합니다. 융합 조건만으로는 부족합니다. 융합 오차의 꼬리는 그 공분산이 말하는 것보다 두꺼워서, 직선 경로에서도 가끔 튀는 융합 하나가 좋은 트랙을 갈아 치울 수 있습니다. 그 프레임의 원시 스칼라가 모두 트랙과 맞는데도 말입니다. 그런 프레임은 트랙을 갱신합니다. - 관성 비행과 소실. 받아들인 스칼라가 없는 프레임은
coasting입니다(예측만, P가 커짐). 이런 프레임이coast_max_frames(10)개 넘게 이어지면 트랙을 버립니다(lost). 사후track_position_std_m이max_position_std_m(25 m)을 넘어도 버립니다. 링크 한두 개뿐인 구간이 길면 트랙은tracking인 채로 그 링크가 보지 못하는 방향으로 표류하기 때문입니다. 어느 쪽이든 다음ok융합에서 다시 시작합니다. - 융합 사전값.
use_as_prior(기본)이면 그 프레임의 가우스–뉴턴 융합이 마지막ok추정치를 속도만큼 옮긴 값 대신 트랙의 예측에서 출발합니다.false면 융합 필드는 추적이 없을 때와 정확히 같습니다.
그러면 estimates[]의 각 행에 다음이 붙습니다.
| 필드 | 의미 |
|---|---|
track_status | none(아직 트랙 없음), init(이 프레임의 융합에서 시작 또는 재시작), tracking(스칼라를 하나 이상 받음), coasting(하나도 못 받음), lost(버려짐, 다음 시작까지) |
track_position, track_velocity | 사후 상태(none과 lost에서는 null) |
track_position_error_m, track_velocity_error_m_s | position_true, velocity_true와의 오차 |
track_position_std_m | sqrt(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_hz | 30e3 | 부반송파 간격 Δf [Hz] |
n_fft | 필수 | FFT 크기(n_active_sc 이상) |
n_active_sc | 612 | 활성 부반송파 수. B_occ = n_active_sc · Δf |
cp | normal | normal 또는 extended |
symbol_duration_s | null | CP를 포함한 평균 심볼 길이. null이면 NR 슬롯 시간(Δf = 15 · 2^μ kHz면 (1 ms / 2^μ) / 14, 확장 CP는 / 12), 그 밖에는 (1/Δf)(1 + 144/2048 또는 512/2048) |
symbols_per_cpi | null | null이면 floor(cpi_s / T_sym) |
pilot.pattern | comb | comb 또는 block(block은 subcarrier_step 1) |
pilot.subcarrier_step, pilot.symbol_step | 1, 1 | m번째 심볼마다, 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_s | T_sym | 심볼 | K × N | 이득 | B_occ | c / B_occ | 도플러 셀 | 바닥(NF 7 dB) |
|---|---|---|---|---|---|---|---|---|
| NR 30 kHz 51 PRB, 10 ms | 0.5 ms / 14 | 280 | 612 × 280 = 171,360 | 52.34 dB | 18.36 MHz | 16.33 m | 100 Hz | −94.36 dBm |
| NR 30 kHz 51 PRB, 50 ms | 1400 | 612 × 1400 = 856,800 | 59.33 dB | 20 Hz | ||||
| Grid S, 10 ms | 1/14400 s | 144 | 256 × 16 = 4096 | 36.12 dB | 20.0 MHz | 14.99 m | 100 Hz | −93.99 dBm |
| Grid S, 50 ms | 720 | 256 × 80 = 20,480 | 43.11 dB | 20 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로 둘 다 풉니다.
- H1(
h1_absorber): 타깃 액터 메시를 모두 감추고 타깃을 흡수체로 넣음 (include_comm_paths와 같은 방식). - H0(
h0_removed): 타깃 액터 메시를 모두 감추고 아무것도 넣지 않음.
| 필드 | 기본값 | 의미 |
|---|---|---|
mode | off | off, h1_absorber, h0_removed, both |
diffuse_reflection | true | 확산 클러터(재질의 산란 계수) |
diffraction, edge_diffraction | false | |
max_depth, num_samples | null | null이면 config 값 |
deterministic | true | PathSolver(deterministic=True). 두 번 돌려도 경로가 같음 |
max_paths_per_link | 256 | 솔브마다 링크별로 가장 센 경로 n개만 남김. null이면 전부 남김. 나머지는 dropped_paths로 세고 실행 경고 하나가 합계를 알려 줌 |
cache | static_nodes | static_nodes: 키가 반복되는 동안 한 번만 풂, per_frame: 매 프레임 풂 |
store | h0 | 결과에 남길 가설: 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_ids | null | 평가할 TX(기본: 모든 TX). |
ue_rx_ids / sensing_rx_ids | null | 역할 분담. 아래 역할 참고. |
target_actor_ids | null | 타깃(기본: 켜진 센싱 바인딩 전부). |
ue_association | serving | serving: UE마다 최적 빔 RSS가 가장 강한 TX에 속합니다. all: 모든 TX가 모든 UE를 서비스합니다(TX별 단일 셀 관점). |
tx_rows × tx_cols | 4 × 4 | TX 패널. 소자 간격은 디바이스 안테나 설정을 따릅니다. |
rx_rows × rx_cols | null | 센싱 RX 패널(기본: TX와 같은 크기). UE는 늘 단일 소자입니다. |
use_device_orientation | true | 패널이 자기 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_pulses | 4096 | ρ = 1일 때 코히어런트하게 적분하는 펄스 수 / OFDM 자원 요소 수. |
cpi_s | 0.01 | 메타데이터 전용(분해능 계산). |
threshold_db | 13 | 빔별 detected 판정 기준. |
pfa | 1e-6 | Pd 모델의 오경보 확률. |
pd_target | 0.9 | 파레토 요약의 동작점. |
slot_ratios | 0, .05, .1, .2, .3, .5, .7, 1 | 센싱에 쓰는 슬롯(펄스) 비율 ρ. 정렬하고 중복을 없앱니다. |
sharing_mode | dual_function | time_sharing 또는 dual_function(아래). |
samples_per_sp / max_depth | 1 000 000 / null | 에코 솔브에 그대로 넘깁니다(§3과 같음). |
include_paths | false | 솔브한 경로를 결과에 저장합니다. 에코가 먼저, 통신 경로가 뒤에 옵니다. |
elevation_start_deg / elevation_stop_deg / elevation_step_deg | null | 패널 로컬 좌표계에서 2차원 코드북의 고도 스윕(셋 다 주거나 셋 다 비움. 고도는 최대 361개, 빔은 모두 합쳐 최대 4096개). null이면 방위각 코드북만 씁니다. 고도 코드북 참고. |
interference | false | UE SINR에 TX 간 간섭을 넣습니다(ue_association: serving일 때만). 간섭 참고. |
detector | Swerling 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에 대해 다음과 같습니다.
h_k,u = Σ_l α_l · w_kᴴ a_t(l), t → u 경로에 대한 합입니다(α_l은path_gain_db와phase_rad에서 얻음).SINR_k,u = P_t + 20·log10|h_k,u| − N0이고 N0 = −174 + 10·log10(B) + NF입니다.interference를 켜지 않으면 TX 간 간섭이 없으므로 SINR = SNR입니다(간섭 참고).rate_k,u = log2(1 + SINR_k,u)이고,R(k) = Σ_u rate_k,u는 이 TX가 서비스하는 UE에 대한 합입니다.- 타깃 q의 에코는 TX 빔 k와 RX 빔 m에 대해 t → s → q 에코를 모두 코히어런트하게
더합니다.
E_q[k, m] = Σ_e α_e (w_kᴴ a_t(e)) (w_mᴴ a_s(e)). TX 빔마다 가장 좋은 RX 빔을 고르고SNR_q(k, ρ) = P_t + 20·log10 max_m |E_q[k, m]| − N0 + 10·log10(ρ · cpi_pulses)입니다. 빔의 센싱 SNR은 가장 약한 타깃이 정합니다.
고도 코드북. 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만큼 적분됩니다.
time_sharing: 센싱 슬롯은 데이터를 싣지 않습니다. rate_u = (1 − ρ) · rate_k_c,u.dual_function: 센싱 빔도 닿는 UE에게는 데이터를 실어 보냅니다. rate_u = (1 − ρ) · rate_k_c,u + ρ · rate_k,u.
ρ = 0은 통신만 하는 점 하나입니다(beam_idx null, P_d = P_fa). dual_function에서
ρ = 1인 행은 빔 자체와 같고(빔 하나가 둘 다 함), time_sharing에서는 전송률이 0입니다.
interference를 켜면 슬롯마다 그 슬롯에 다른 TX가 쏘는 빔을 간섭으로 받습니다. S는 간섭
없는 신호 전력입니다.
time_sharing: rate_u = (1 − ρ) · log2(1 + S_k_c,u / (N0 + I_comm)).dual_function: rate_u = (1 − ρ) · log2(1 + S_k_c,u / (N0 + I_comm)) + ρ · log2(1 + S_k,u / (N0 + I_sens)).
이렇게 슬롯을 맞추면 ρ가 커질수록 전송률이 오를 수도 있습니다. 다른 TX의 센싱 빔이 UE에
통신 빔보다 약하게 닿으면(I_sens < I_comm) dual_function 점이 통신만 하는 전송률을
넘고, 통신 전용 대비 손실이 음수로 나옵니다. 모델이 그렇게 정의된 것이지 버그가
아닙니다.
출력 읽기. txs[] 항목마다 다음이 들어 있습니다.
beams[]: 코드북 각도마다 한 행. UE별 SINR, 합 전송률, 타깃별 에코 SNR과 최적 RX 각도, 가장 약한 타깃의 SNR, P_d,detected가 있습니다. 패널의 빔 표는 통신 빔 행을 시안, 센싱 빔 행을 마젠타로 칠하고, ISAC lobes 오버레이는 TX마다 두 빔을 같은 색으로 그립니다(빠지는 부분은 §11 참고). 고도 스윕이 있으면 빔마다elevation_deg와 타깃별target_best_rx_elevation_deg가 붙고, 오버레이는 통신(센싱) 빔이 속한 고도 단면의 방위각 단면을 그립니다.interference를 켜면 빔마다ue_snr_db(간섭 없음)와ue_interference_dbm(I_comm, 다른 TX가 그 UE에 닿지 않으면 null)이 붙습니다.comm_beam_angle_deg,sensing_beam_angle_deg,angle_gap_deg(방위각 차이).angles_deg는beams와 순서가 같습니다(방위각이 고도마다 반복). 고도 스윕이 있으면elevations_deg도 같은 순서로 붙고,comm_beam_elevation_deg,sensing_beam_elevation_deg,elevation_gap_deg가 짝을 채웁니다.interference를 켜면ue_interference_sensing_dbm이 UE별 I_sens를 줍니다.ue_single_element_rss_dbm과target_single_element_snr_db는 손 계산용 1×1 기준값입니다. 타깃에 대한 빔의 배열 이득은target_snr_db − target_single_element_snr_db입니다. UE 기준값은 SINR이 아니라 dBm 단위 RSS이므로, UE에 대한 배열 이득은ue_sinr_db + noise_floor_dbm − ue_single_element_rss_dbm입니다.points[]: (ρ, 센싱 빔) 동작점마다 합 전송률, SNR, P_d,pareto표시가 있습니다. 파레토 차트는 전송률을 P_d에 대해 그립니다. 프런트는 두 축 모두에서 다른 점에 지지 않는 점들입니다(중복은 하나로 셉니다).pareto:comm_only_rate_bps_hz(ρ = 0),max_pd,rate_at_pd_target_bps_hz/rho_at_pd_target/beam_idx_at_pd_target(P_d ≥pd_target인 점 중 가장 높은 전송률, 도달하는 점이 없으면 null),rate_loss_at_pd_target_bps_hz,pd_at_95pct_rate(통신 전용 전송률의 95 %를 지키는 점 중 가장 높은 P_d). 전송률 차이가 10⁻¹² 이내면 같은 값으로 보고 P_d가 높은 점을 고르므로, 요약이 가리키는 점은 늘 프런트 위에 있습니다.dual_function에서는 통신 빔이 모든 ρ에서 전송률을 그대로 유지하므로, 통신 빔이 목표에 닿으면 기준을 넘는 가장 작은 ρ가 아니라 ρ = 1로 표시됩니다. 센싱 전용 TX(전송률 모두 0)는 P_d가 가장 높은 점을 가리킵니다.
최상위에는 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_ids | null | 디바이스(기본: 모든 TX와 같은 위치의 RX). |
rcs_dbsm / object_type | null | 점 타깃의 σ. 값을 직접 주거나 TR 38.901 유형의 평균 σ_M을 씁니다(둘 다는 안 됨). 둘 다 비우면 uav-small-size. |
height_m | 60 | 격자 평면의 높이(타깃 중심). ±100 000 m 이내. |
cell_size_m | 10 | 셀 크기. 최대 10 000 m. 셀이 40 000개를 넘으면 경고와 함께 키웁니다. |
center_xy / size_xy | null | 범위를 직접 지정(둘 다 주거나 둘 다 비움. 중심은 ±10⁷ m 이내, 변 길이는 10⁶ m까지). 기본은 씬 경계(비주얼 메시, 디바이스, 액터)에 min(15 m, max(3 m, 긴 변의 15 %))만큼 여백을 둔 것으로, sionna 라디오맵과 같습니다. |
threshold_db / cpi_pulses / pfa | 13 / 4096 / 1e-6 | 탐지 조건. §6, §7과 같습니다. |
array_gain | none | steered는 tx_rows×tx_cols / rx_rows×rx_cols 패널의 10·log10(N_tx) + 10·log10(N_rx)를 더합니다. 모든 링크가 모든 셀에 빔을 맞춘다는 상한입니다. |
min_links_for_fusion | 3 | 셀이 융합 가능으로 판정되는 데 필요한 서로 다른 기하의 수. |
detector | Swerling 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의 월드 구면 단위 벡터입니다. 여기서 다음이 나옵니다.
- pitch·roll이 0이고 양쪽 편파가 같은
V(또는H)인 소자는 F = ±1이 정확히 성립하므로 L_pol = 0이고, 이런 맵은 v0.1.13과 같습니다(metadata.polarization_model도 붙지 않습니다). - 모노스태틱 링크에서는 두 구간 사이에 φ̂의 부호가 뒤집히므로, 에코는 셀 쪽으로 θ̂에서 ψ만큼 기울어진 전계를 내는 소자 하나의 20·log10|cos 2ψ|만 남깁니다. 아래로 기울인 옥상 TRP는 대부분의 셀 쪽으로 에코 전력을 잃습니다.
- 기울이지 않은
cross(±45°, 첫 번째 포트) 패널은 에코를 교차 편파로 받습니다. 같은 위치에 둔 패널의 자기 에코도 마찬가지입니다. F = 0이므로 그런 링크는 모노스태틱이든 바이스태틱이든 구간이 막힌 것처럼 그 셀에서 에코가 없고(Sionna 에코는 약 150 dB 아래의 float32 잡음), mock 솔브에도 그 에코 경로가 없습니다(no_echo). 센싱 링크에는V(또는H) 소자를 쓰세요.
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]):
best_snr_db: 가장 좋은 링크의 SNR(에코가 있는 링크가 없으면 null).n_links_detected: SNR ≥threshold_db인 링크 수.pd_best: 가장 좋은 링크의detector.model기준 P_d(기본 Swerling 1, §7과 §9).fusion_feasible: 탐지된 링크가 서로 다른 기하를min_links_for_fusion개 이상 이루면 1, 아니면 0. 서로 1 m 안에 있는 디바이스(TRP의 TX와 그 센싱 RX, ISAC의 같은 위치 규칙)는 한 지점으로 보고, 같은 두 지점을 잇는 링크는 방향과 상관없이 하나로 셉니다. 그래서 센싱 패널이 TX에서 1 m까지 떨어져 있어도 서로 뒤바뀐 쌍(A→B, B→A)은 기하 하나입니다. 묶음은 링크별geometry_group에서 볼 수 있습니다.
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_d | P_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 곡선은
[seed, 모델 번호, SNR 번호], ISAC는[seed, TX 번호, 빔 번호]와[seed, TX 번호, 10⁶ + 점 번호], 시나리오는[seed, 프레임 번호, 링크 번호], 커버리지는[seed, 셀 번호]입니다. - 임계값. −ln P_fa이고,
empirical_threshold이면 잡음만 있는 실행의 (1 − P_fa) 분위수입니다. 이 경우 시행 수 ≥ 20 / P_fa(초과 사례 20개 이상)가 필요하므로 P_fa 10⁻⁶ 에서는 시행 상한 2·10⁶으로 닿지 않습니다. Pd 곡선, ISAC, 커버리지는 임계값과 실측 P_fa를 요청마다 잡음 전용 실행 한 쌍(스트림[seed, 999, 0, 1])에서 얻습니다. 1번 실행이 경험적 임계값을 주고, 1번과 독립인 2번 실행이 쓰이는 임계값에 대한 오경보를 셉니다. 키가 네 자리인 까닭은 numpy가 짧은 키 뒤를 0으로 채우기 때문입니다([seed, 999]와[seed, 999, 0]은 같은 스트림). 마지막 자리가 0이 아니므로 어떤 추정의 스트림과도 겹치지 않습니다.empirical_threshold를 켠 시나리오 링크는 자기 임계값을 위한 잡음 전용 실행을 따로 뽑습니다(링크 스트림에서 먼저 뽑고, 그다음 신호 실행). 시나리오 실행은 P_fa를 재지도 보고하지도 않습니다. - 구간은 95 % Wilson 점수 구간입니다. 회귀 테스트에서 점당 200 000 시행은 모델마다 SNR 다섯 곳에서 해석식과 맞습니다(고정 시드 테스트가 흔들리지 않도록 99.9 % 구간으로 확인).
- 상한. 추정 하나에 시행 최대 2·10⁶, 요청 하나에 시행 × 추정 수로 세어 최대 2·10⁸입니다.
Pd 곡선은 점 수 × 모델 수(422), ISAC는 TX 수 × 빔 수 × (1 + 0이 아닌 슬롯 비율 수)
(400), 시나리오는 프레임 수 × TX 수 × 센싱 RX 수 × 타깃 수(400,
empirical_threshold이면 링크마다 잡음 실행이 붙으므로 시행을 두 배로 셈), 커버리지는 5입니다.
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}'
| 필드 | 기본값 | 의미 |
|---|---|---|
pfa | 1e-6 | 오경보 확률. |
snr_min_db / snr_max_db / step_db | −5 / 30 / 0.5 | SNR 축(71점, 최대 1001점). |
models | 세 모델 전부 | 중복은 없애고 순서는 유지합니다. |
monte_carlo_trials | 0 | 점당 시행 수(0이면 해석식만). |
empirical_threshold | false | 잡음 전용 실행에서 임계값을 얻습니다(시행 수 ≥ 20 / P_fa 필요). |
seed | 0 | 몬테카를로 스트림의 기준값. |
cpi_pulses | 1 | 시행당 펄스 수. SNR 축은 적분 후 값이고 몬테카를로는 적분 샘플을 바로 뽑으므로, 어떤 수치도 이 값에 좌우되지 않습니다. |
pd_target | 0.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 | 통계량 s | pfa_cell에서의 해석적 임계값 α |
|---|---|---|
ca | P / 훈련 셀 평균 | α = N_t (P_fa^(−1/N_t) − 1), 즉 P_fa = (1 + α/N_t)^(−N_t) |
os | P / 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가 들어 있습니다.
cell: 검정한 셀(노치 밖이면서 어느 게이트에도 들지 않는 셀) 가운데 오경보 검출의 비율.link_frame: 오경보가 하나라도 있는 맵의 비율.system: 어느 링크에서든 오경보가 하나라도 있는 프레임의 비율.
셋 다 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_bins | null | 검정도 훈련도 하지 않을 거친 도플러 빈 −k…k. |
cfar.type, cfar.guard, cfar.train | "ca", [1, 1], [8, 4] | 검출기와 [지연, 도플러] 반폭. |
cfar.os_rank, cfar.pfa_cell | ⌈0.75 N_t⌉, 1e-4 | OS 순위, 셀당 설계 P_fa. |
threshold, h0_result_id, system_pfa | "analytic", null, null | 보정 임계값의 출처와 프레임당 목표. |
oracle_gate_cells, max_detections_per_link | 1, 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_ids | null | 내보낼 시나리오 결과, 이 순서대로. null이면 센싱 프레임이 있는 저장된 시나리오 결과 전부. |
skip_without_sensing | false | result_ids와 함께: 나열한 결과에 센싱 프레임이 없으면 400 대신 건너뛰고 warnings에 이름을 남깁니다. UI는 일부만 고를 때 이 값을 보냅니다. |
formats | ["npz"] | npz, csv, parquet 중 아무것이나(parquet은 pyarrow, 즉 parquet extra가 필요: pip install "seam-studio[parquet]"). |
include_echo_paths | false | echoes 표도 씁니다. |
split | null | 프레임 단위 분할 {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에는 다음이 들어 있습니다.
links.<fmt>: 결과 × 프레임 × TX → 센싱 RX 링크 × 타깃마다 한 행, 프레임의links[]순서(결과마다 프레임 × TX × 센싱 RX × 타깃 행).echoes.<fmt>(include_echo_paths): 프레임마다 에코 경로 하나에 한 행.manifest.json: 열마다 dtype, 단위, 역할, 설명. 결과마다 레이블, 백엔드, 주파수, 프레임 수, dt, 행 수,scene_hash(와 지금의 해시),kinematics_source, 그리고metadata.sensing의 실행 상수(타깃별 통계 제외). 그 밖에 분할, 분할별 행 수, 레이블 균형(detected_fraction,reason_counts), 규약.README.txt: 파일별 설명, 불러오는 법, 주의할 점.
열은 늘 모두 있어서 스키마가 바뀌지 않습니다. 값이 없으면 float 열은 NaN, 문자열 열은
""입니다. npz는 열마다 1차원 배열 하나입니다(문자열은 <U라
np.load(..., allow_pickle=False)로 읽힙니다). csv는 헤더가 있고, float는 왕복
변환이 정확한 가장 짧은 형식, NaN은 빈 칸, 불리언은 true/false입니다. parquet은
NaN을 NaN 그대로 둡니다.
links 열
| 열 | dtype | 단위 | 역할 | 출처 |
|---|---|---|---|---|
result_id, split | str | meta | ||
frame_index | int64 | meta | frames 안의 인덱스 | |
time_s | float64 | s | meta | frame.time_s |
tx_id, rx_id, target_id | str | meta | 링크 보고 | |
tx_pos_x/y/z, rx_pos_x/y/z | float64 | m | feature | frame.sensing.nodes |
tx_vel_x/y/z, rx_vel_x/y/z | float64 | m/s | feature | frame.sensing.nodes |
bistatic_range_m, doppler_hz, echo_power_dbm, snr_db, measured_range_m, measured_doppler_hz | float64 | m, Hz, dBm, dB | feature | 링크 보고 |
range_bin, doppler_bin | float64(NaN = 없음) | 셀 | feature | 링크 보고 |
num_echoes | int64 | feature | 링크 보고 | |
multipath | bool | feature | 링크 보고 | |
aod_az_deg, aod_el_deg, aoa_az_deg, aoa_el_deg | float64 | deg | feature | 보고된 에코(frame.sensing.echoes에서 path_id로 찾음) |
pd, pd_mc | float64 | feature | 링크 보고(실행에 pfa가 없으면 NaN) | |
detected | bool | label | ||
reason | str | label | detected / no_echo / below_threshold / mti_rejected / cfar_miss(CFAR 실행, §9 참고) | |
position_true_x/y/z, velocity_true_x/y/z | float64 | m, m/s | label | 그 타깃의 estimates[] 행 |
est_status | str | estimate | ok / insufficient_links / diverged | |
n_links_detected, n_links_used | int64 | estimate | ||
position_est_x/y/z, velocity_est_x/y/z, position_error_m, velocity_error_m_s, gdop | float64 | m, m/s | estimate | |
track_status | str | track | 추적을 껐으면 "" | |
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_gated | float64 | m, m/s | track | 없으면 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_paths | false | background.<fmt>도 씁니다. 저장된 H0 / H1 배경 경로마다 한 행입니다(§6의 배경 경로 H0/H1). |
include_channel | null | channel.npz도 씁니다(아래 옵션). |
include_channel.subcarriers | "pilot" | pilot은 K × N 파일럿 격자, active는 CPI의 모든 심볼 × 모든 활성 부반송파. |
include_channel.waveform | null | 텐서의 격자. 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.enabled | false | 열잡음 실현값 noise도 씁니다. |
include_channel.noise.n0_dbm_per_re | null | RE당 N0 [dBm]. null이면 실행 설정의 −174 + 10 log10(scs) + NF. |
include_channel.noise.seed | null | null이면 실행마다 그 실행의 sensing.noise_seed. |
include_channel.pilot_ls | false | LS 파일럿 추정 H_ls = H_total + noise도 씁니다(잡음 필요). |
include_channel.max_elements | 50,000,000 | channel.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] complex64 | noise.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. 한계
- 타깃은 강체 평행 이동만 합니다. 회전이나 마이크로 도플러는 없습니다.
- 직육면체 크기는 액터 박스 크기이므로 메시 액터는
size_m을 직접 지정해야 합니다. - 센싱 솔브 동안 액터 자신의 메시는 씬에서 빠졌다가(산란 모델이 그 액터를 전부 기술하므로) 끝나면 복원됩니다.
- 확산 산란·회절 구간은 없고, 센싱(에코와
include_comm_paths모두)은 항상 내장 sionna-rt 엔진에서 돕니다. - mock은 타깃 중심의 LoS 산란점 하나와 σ_M만 씁니다. 각도별 로브, 다중 산란점 배치, 차폐는 없습니다. Sionna처럼, 같은 위치에 놓인(모노스태틱) TX와 RX 사이에는 LoS 통신 경로가 없습니다. v0.1.14부터 mock 에코에는 디바이스마다 소자 이득과 §8의 편파 항이 들어갑니다(그 전에는 등방성이었습니다).
- 안테나가 섞인 경우: Sionna는 처음 선택한 TX(RX)의 안테나를 모든 TX(RX)에 적용하고, 다르면 경고합니다. mock은 디바이스마다 자기 소자 패턴과 방향(그리고 §8의 편파 항)을 적용합니다. 그래서 안테나가 섞여 있으면 두 백엔드의 결과가 다릅니다.
- 랜덤 성분은 기본으로 꺼져 있습니다. 솔버는 GPU에서 float32,
deterministic=False로 돕니다. 그래서 입력이 같아도 강한 경로에서 약 1e-6 dB, 빔 널 근처에서 최대 약 0.1 dB까지 달라질 수 있고, 실행마다 경로 순서와path_id가 바뀔 수 있습니다. 결과는path_id나 정확한 dB 값이 아니라 기하나 경로 종류로 비교하세요. 같은noise_seed는 측정 잡음을 정확히 재현합니다. 배경 솔브(§6)는 기본으로PathSolver(deterministic=True)(background.deterministic)를 쓰고, RCS 에코 솔버는 여전히 결정적이지 않습니다. - 시간에 따른 센싱(§6)은 오라클 연관을 씁니다. 레이 트레이서가 에코마다 어느 타깃인지, 직접 에코인지 다중 경로인지를 알려 줍니다. 실제 수신기라면 둘 다 추정해야 합니다.
- 산란점이 여러 개인 타깃(
vehicle-multi-sp,agv-multi-sp): 링크마다 가장 강한 직접 에코를 보고하는데, 그 에코는 링크마다 다른 산란점에서 올 수 있습니다. 반면position_true는 직육면체 중심입니다. 그래서position_error_m에 타깃 크기의 절반 (자동차면 약 2.5 m)까지 편향이 섞일 수 있습니다. mock은 중심의 점 하나만 쓰므로 이 편향이 없습니다. - 임계값 탐지(기본
sensing.detection.mode)는 에코 단위의 잡음 한계 판정입니다. CFAR, 거리–도플러 맵, 클러터나 자기 간섭 전력은 없습니다.mode: "cfar"(§9)는 H0/H1 클러터와 열잡음으로 지연–도플러 맵을 만들고 CFAR를 돌립니다. 자기 간섭과 직접파 모델(#25)은 아직 없고, 연관은 오라클 그대로입니다. - 배경(§6): 형상이 액터 하나뿐인 씬에서는 Sionna 로더가 그 액터를 가리킬 수 없어서 H0가
액터를 감추지 못하고, 액터가 경고와 함께 H0에 남습니다. 다른 형상을 하나 더 넣으세요(멀리
둔 0.1 m 정육면체면 충분). TX와 정확히 같은 위치의 센싱 RX에는 TX → RX LoS 배경 경로가
없습니다(길이 0인 경로는 버려지므로 그 링크의
total_power_db는 null). 자기 간섭은 모델링하지 않습니다(#25). - 시간에 따른 센싱(§6)은 안테나를 단일 소자로 계산합니다(배열 이득 없음). 그래서 처리 이득은 적분 이득뿐입니다.
measurement_noise는 경험칙(셀 / sqrt(2·SNR))이지 크라메르–라오 하한이 아닙니다.tracking이 없으면 프레임마다 독립된 스냅샷이고, 이전 추정치는 다음 프레임 솔버의 출발점으로만 쓰입니다.- 추적(§6)은 타깃마다 등속 EKF 하나이고 오라클 연관을 씁니다. 기동 모델이나 IMM은
없습니다(방향 전환은 융합에서 다시 시작하는 것으로 처리하므로, 급한 모서리 뒤 한두
프레임은 융합 정확도입니다). 거짓 트랙도, 트랙 간 융합도 없고, 측정 σ는
measurement_noise의 경험칙입니다. 한 프레임에 링크가 한두 개뿐이면 거리 합이 위치를 타원체 위에서 자유롭게 두므로, 링크 세 개가 없는 구간이 길어지면 표류합니다. 그런 구간에서는track_position_std_m이 오차를 몇 배나 작게 말할 수 있습니다. 그래서max_position_std_m상한은 실제 오차가 요구하는 것보다 늦게 트랙을 버리고,coast_max_frames는 프레임마다 스칼라가 하나라도 받아들여지는 한 트랙을 버리지 않습니다. - 센싱 데이터셋(§10): 레이블은 레이 트레이서가 줍니다(오라클 연관). 분할이 프레임 단위라서 한 실행의 이웃 프레임끼리는 분할을 넘어 상관이 있습니다. 엄밀하게 시험하려면 결과를 통째로 떼어 두세요.
- ISAC 트레이드오프(§7):
- 고도 스윕이 없으면 코드북은 방위각 전용이라 세로 행은 늘 정면을 봅니다. λ/2 간격
4행 패널은 패널 면에서 30° 벗어난 방향에 세로 널이 있고, 25°에서 35° 사이 어디서든
끝마다 정면보다 14 dB 이상 낮습니다(28°와 33°에서는 21–23 dB). 그래서 위로 기울인
옥상 TRP는 거리의 UE나 가파른 각도의 드론 쪽으로 배열 이득보다 더 많이 잃을 수 있고,
4×4 빔이 단일 소자 기준값보다 낮게 나오기도 합니다.
elevation_*스윕을 주면 세로 행도 조향합니다. 다만 여전히 고정 격자여서, 격자 고도 사이로 오는 경로는 격자 간격만큼의 스캘럽 손실을 봅니다. - 기본
iso소자에서는 패널에 거울상 뒤쪽 로브가 있습니다. 빔 θ는 패널 뒤쪽의 180° − θ 방향도 같은 이득으로 비추므로, TRP 뒤에 있는 UE나 타깃도 잘리지 않습니다. 앞뒤 비가 필요하면tr38901소자를 쓰세요. λ/2 간격에서는 스윕 범위 밖의 끝쪽 방향 타깃(예: ±60° 스윕에서 정면 기준 78°)을 양쪽 가장자리 빔이 그레이팅 로브 자락으로 서로 1 dB 안팎의 차이로 보므로, 어느 쪽 가장자리가 이길지는 거의 임의입니다. - ISAC lobes 오버레이는 빔의 방위각 단면을 패널 앞쪽 반구에만 그립니다(고도 스윕이
있으면 통신 또는 센싱 빔의 고도 단면).
iso처럼 앞뒤가 대칭인 소자라면 TX가 뒤쪽 로브로 패널 뒤의 UE를 서비스하면서도 시안 통신 로브는 그 UE 반대쪽을 가리킬 수 있습니다. interference가 없으면 SINR = SNR입니다. 켜면 모든 TX가 늘 데이터가 차 있어 동기 슬롯에서 통신 빔이나 센싱 빔을 항상 쏜다고 보고, 간섭은 UE에만 닿습니다(센싱 수신기는 계속 잡음 제한이며, TX 간 에코나 직접 경로 누설은 없습니다). 통신 빔은 최적 응답 평형이지 공동 최적해가 아닙니다. UE마다 대역 전체를 쓴다고 보고 전송률을 계산합니다.- P_d는 고정 임계값의 닫힌 식(Swerling 0, 1, 3, §9)입니다. CFAR도, 거리·도플러 셀 경계 손실도, 펄스 간 요동(Swerling 2와 4)도 없습니다.
- t = 0 스냅샷 하나입니다. 이중 편파 안테나는 첫 번째 편파 포트만 합성하고, UE는 단일 소자입니다.
- 고도 스윕이 없으면 코드북은 방위각 전용이라 세로 행은 늘 정면을 봅니다. λ/2 간격
4행 패널은 패널 면에서 30° 벗어난 방향에 세로 널이 있고, 25°에서 35° 사이 어디서든
끝마다 정면보다 14 dB 이상 낮습니다(28°와 33°에서는 21–23 dB). 그래서 위로 기울인
옥상 TRP는 거리의 UE나 가파른 각도의 드론 쪽으로 배열 이득보다 더 많이 잃을 수 있고,
4×4 빔이 단일 소자 기준값보다 낮게 나오기도 합니다.
- 센싱 커버리지(§8):
- 타깃은 고정 점 RCS입니다. TR 38.901의 각도별 로브는 없습니다.
- 직접 LOS 구간만 셉니다(다중 경로 에코 없음). LOS 판정에서 액터 메시는 무시합니다.
steered는 모든 링크·셀에 대한 이상적인 전체 배열 이득입니다.- 소자 이득은 디바이스마다 자기 안테나를 씁니다. Sionna 솔브는 처음 선택한 TX(RX)의 안테나를 모든 TX(RX)에 적용하고 경고하므로, 패턴이 섞여 있으면 둘이 달라집니다.
- 편파: 배열마다 첫 번째 포트만 봅니다(Sionna 에코와 같음). 바이스태틱 링크의
TR 38.901 타깃은 이 투영에서 15° 기울기에서 최대 0.2 dB, 45°에서 최대 2.5 dB
벗어납니다.
xpr_db를 정한(편파를 바꾸는) 타깃은 맵에서도 mock에서도 모델링하지 않습니다. - mock은 모든 구간을 LOS로 봅니다.
- 탐지기 모델(§9): 몬테카를로는 닫힌 식이 기술하는 것과 같은 이상화된 모델(CPI 동안 일정한 진폭, 백색 가우스 잡음, 이상적인 코히어런트 적분)을 뽑습니다. 계산을 검증하고 표본 흩어짐을 보여 줄 뿐, 파형 수준의 시뮬레이션은 아닙니다.
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. 필요할 때 뽑습니다. |
seed | 0 | 섭동 추첨과 잡음의 seed. |
perturb.material.eps_r_pct, perturb.material.sigma_pct | 0, 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_m | 0 | 노드 사이트마다 N(0, σ² I₃) 오프셋[m]. 서로 1 m 안에 있는 TX와 센싱 RX는 한 사이트라 같이 움직입니다. |
perturb.cfo_hz | 0 | 바이스태틱 센싱 RX(모든 TX에서 1 m 넘게 떨어진 RX)마다 N(0, cfo²)[Hz]. 합성할 때 모든 경로의 도플러 오프셋으로 들어갑니다(양수 = 접근). 공동 위치 수신기는 TX와 발진기를 공유하므로 받지 않습니다. |
background | null | 배경 솔버 옵션(§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,)))에서 이
순서로 뽑습니다.
- 무선 재질마다(이름순):
normal(0, eps_r_pct/100), 이어서normal(0, sigma_pct/100) - 노드 사이트마다(TX 먼저, 다음 센싱 수신기, 사이트 순서):
normal(0, node_pos_sigma_m, 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.
한계.
- 재질 배율은 솔브 한 번 동안 불러온 솔버 재질에 걸고 끝나면 되돌립니다. 저장된 프로젝트와 RF 프로젝션에는 쓰지 않습니다. mock은 상수 모델 라이브러리 재질만 섭동합니다(ITU 재질은 mock에서 손실이 그대로입니다).
- diffuse 반사를 켜고
background.deterministic: false이면 realization끼리 몬테카를로 광선 재표본 차이도 납니다. 이것은 RT 표본 잡음이지 클러터 realization이 아니며, 앙상블이 경고합니다. 모든 realization은 설정의 솔버 seed를 씁니다. - 섭동은 정적입니다. CFO 말고는 클러터 내부 운동, 위상잡음, 타이밍 오차가 없습니다. 정지한 공칭 트윈의 배경은 정확히 0 Hz입니다.
- prim별 추첨(
scope: "prim")은 지원하지 않습니다.
관련 문서
- simulation.ko.md — 경로, 라디오맵, 빔포밍, 채널 분석
- trajectory_uav.ko.md — 액터 궤적(타깃 기본 속도의 출처)
- datasets_export.ko.md — RFData / AODT / 채널 npz 내보내기
- ../dynamic_scattering.ko.md — 일반 경로 솔브에서 움직이는 디바이스·액터의 도플러