research(R6): Fresnel-zone forward model — bedrock physics for CSI sensitivity (#710)

The workspace DSP (vital_signs, multistatic, pose_tracker, tomography)
implicitly assumes a forward model that maps scatterer geometry to
per-subcarrier phase shifts. Nobody had written it down. This tick
makes it explicit.

Closed-form first-Fresnel-zone radius + point-scatterer path-delta +
per-subcarrier phase prediction over 802.11n/ac 20 MHz channels (52
subcarriers, 312.5 kHz spacing). Pure NumPy demo + JSON output for
downstream consumers.

Headline numbers:
- 5 m link first-Fresnel radius @ midpoint: 40 cm (2.4 GHz), 27 cm (5 GHz)
- Inside zone-1: phase spread <0.5 deg across 52 subcarriers (band-flat)
- Outside zone-1: phase spread up to 16 deg (band-dispersed)

This unifies R5 + R6: R5's experimentally measured band-spread top
subcarriers is exactly what the Fresnel forward model predicts for
zone-1 occupancy.

Closes the loop on three earlier threads:
- R7 (mincut adversarial) gets a precise definition of 'physically
  inconsistent' instead of a learned classifier
- R10 (foliage range) needs to retract 100 m sparse estimate to ~70 m
  to account for Fresnel-zone obstruction
- R12 (eigenshift negative result) gets its revision basis: PABS over
  Fresnel-grounded forward operator

Honest scope: point-scatterer only, first Fresnel only, frequency-flat
reflectivity, LOS-only (no multipath). The scalar version is the right
first-order approximation; volume-integral / multi-zone / multipath
extensions catalogued as R6.1+R6.2 follow-ups.

Coordination: ticks/tick-8.md, no PROGRESS.md edit.
This commit is contained in:
rUv
2026-05-22 01:31:09 -04:00
committed by GitHub
parent 7bd188ab60
commit 650612e5a2
4 changed files with 947 additions and 0 deletions
@@ -0,0 +1,586 @@
{
"model": "first-Fresnel-zone ellipsoid + per-subcarrier path-delta forward model",
"constants": {
"c_mps": 299800000.0
},
"scenarios": [
{
"name": "human-standing-at-midpoint",
"link_m": 5.0,
"scatterer_offset_m": 0.1,
"scatterer_position_m": 2.5,
"freq_2.4_GHz": {
"first_fresnel_radius_m": 0.39515292398428903,
"zone": "zone-1",
"path_delta_m": 0.003998401278721531,
"phase_rad_per_subcarrier": [
0.20043478616963525,
0.2004609731027643,
0.20048716003589334,
0.20051334696902237,
0.2005395339021514,
0.20056572083528046,
0.2005919077684095,
0.20061809470153852,
0.20064428163466755,
0.2006704685677966,
0.2006966555009256,
0.20072284243405467,
0.2007490293671837,
0.2007752163003127,
0.20080140323344178,
0.2008275901665708,
0.20085377709969982,
0.20087996403282884,
0.20090615096595793,
0.20093233789908693,
0.20095852483221596,
0.20098471176534502,
0.20101089869847405,
0.20103708563160308,
0.20106327256473214,
0.20108945949786114,
0.2011156464309902,
0.20114183336411923,
0.20116802029724826,
0.2011942072303773,
0.20122039416350632,
0.20124658109663537,
0.2012727680297644,
0.20129895496289343,
0.20132514189602246,
0.20135132882915152,
0.20137751576228052,
0.20140370269540958,
0.2014298896285386,
0.20145607656166764,
0.20148226349479667,
0.20150845042792573,
0.20153463736105473,
0.20156082429418376,
0.20158701122731285,
0.20161319816044185,
0.20163938509357088,
0.20166557202669994,
0.20169175895982897,
0.201717945892958,
0.20174413282608702,
0.20177031975921605
],
"phase_rad_min": 0.20043478616963525,
"phase_rad_max": 0.20177031975921605,
"phase_rad_spread": 0.0013355335895808007,
"phase_wraps": 0
},
"freq_5.0_GHz": {
"first_fresnel_radius_m": 0.27376997644007645,
"zone": "zone-1",
"path_delta_m": 0.003998401278721531,
"phase_rad_per_subcarrier": [
0.41831006980320795,
0.41833625673633695,
0.41836244366946607,
0.41838863060259507,
0.4184148175357241,
0.4184410044688532,
0.4184671914019822,
0.4184933783351112,
0.4185195652682403,
0.4185457522013693,
0.4185719391344983,
0.41859812606762736,
0.41862431300075637,
0.4186504999338854,
0.4186766868670145,
0.41870287380014354,
0.41872906073327254,
0.4187552476664016,
0.4187814345995306,
0.4188076215326596,
0.41883380846578866,
0.4188599953989178,
0.4188861823320468,
0.4189123692651758,
0.41893855619830483,
0.41896474313143384,
0.4189909300645629,
0.4190171169976919,
0.41904330393082095,
0.41906949086395,
0.41909567779707907,
0.41912186473020807,
0.4191480516633371,
0.41917423859646613,
0.41920042552959513,
0.4192266124627242,
0.41925279939585325,
0.41927898632898225,
0.4193051732621113,
0.41933136019524037,
0.41935754712836937,
0.4193837340614984,
0.4194099209946275,
0.4194361079277565,
0.4194622948608855,
0.41948848179401454,
0.41951466872714355,
0.4195408556602726,
0.4195670425934017,
0.4195932295265307,
0.4196194164596597,
0.4196456033927888
],
"phase_rad_min": 0.41831006980320795,
"phase_rad_max": 0.4196456033927888,
"phase_rad_spread": 0.0013355335895808285,
"phase_wraps": 0
}
},
{
"name": "human-walking-into-fresnel",
"link_m": 5.0,
"scatterer_offset_m": 0.25,
"scatterer_position_m": 2.5,
"freq_2.4_GHz": {
"first_fresnel_radius_m": 0.39515292398428903,
"zone": "zone-1",
"path_delta_m": 0.024937810560444973,
"phase_rad_per_subcarrier": [
1.2501008225017065,
1.2502641489744661,
1.2504274754472258,
1.2505908019199852,
1.250754128392745,
1.2509174548655044,
1.2510807813382638,
1.2512441078110232,
1.251407434283783,
1.2515707607565425,
1.2517340872293021,
1.2518974137020618,
1.2520607401748214,
1.2522240666475808,
1.2523873931203406,
1.2525507195930998,
1.2527140460658595,
1.2528773725386189,
1.2530406990113787,
1.2532040254841381,
1.2533673519568977,
1.2535306784296574,
1.253694004902417,
1.2538573313751764,
1.254020657847936,
1.2541839843206957,
1.254347310793455,
1.2545106372662147,
1.2546739637389743,
1.254837290211734,
1.2550006166844934,
1.2551639431572532,
1.2553272696300126,
1.2554905961027722,
1.2556539225755317,
1.2558172490482913,
1.2559805755210507,
1.2561439019938105,
1.25630722846657,
1.2564705549393296,
1.256633881412089,
1.2567972078848488,
1.2569605343576082,
1.2571238608303679,
1.2572871873031273,
1.257450513775887,
1.2576138402486463,
1.2577771667214062,
1.2579404931941656,
1.2581038196669252,
1.2582671461396846,
1.2584304726124445
],
"phase_rad_min": 1.2501008225017065,
"phase_rad_max": 1.2584304726124445,
"phase_rad_spread": 0.00832965011073794,
"phase_wraps": 0
},
"freq_5.0_GHz": {
"first_fresnel_radius_m": 0.27376997644007645,
"zone": "zone-1",
"path_delta_m": 0.024937810560444973,
"phase_rad_per_subcarrier": [
2.608977075861283,
2.609140402334042,
2.609303728806802,
2.609467055279562,
2.6096303817523214,
2.6097937082250806,
2.6099570346978402,
2.6101203611706,
2.6102836876433595,
2.610447014116119,
2.6106103405888783,
2.6107736670616384,
2.6109369935343976,
2.611100320007157,
2.611263646479917,
2.611426972952677,
2.611590299425436,
2.6117536258981953,
2.6119169523709553,
2.6120802788437145,
2.612243605316474,
2.6124069317892338,
2.6125702582619934,
2.612733584734753,
2.6128969112075127,
2.613060237680272,
2.613223564153032,
2.613386890625791,
2.6135502170985507,
2.6137135435713104,
2.61387687004407,
2.6140401965168296,
2.614203522989589,
2.6143668494623484,
2.614530175935108,
2.6146935024078677,
2.6148568288806273,
2.6150201553533865,
2.6151834818261466,
2.6153468082989058,
2.6155101347716654,
2.615673461244425,
2.615836787717185,
2.6160001141899443,
2.616163440662704,
2.616326767135463,
2.616490093608223,
2.6166534200809823,
2.616816746553742,
2.6169800730265016,
2.6171433994992612,
2.617306725972021
],
"phase_rad_min": 2.608977075861283,
"phase_rad_max": 2.617306725972021,
"phase_rad_spread": 0.00832965011073794,
"phase_wraps": 0
}
},
{
"name": "scatterer-outside-fresnel",
"link_m": 5.0,
"scatterer_offset_m": 1.5,
"scatterer_position_m": 2.5,
"freq_2.4_GHz": {
"first_fresnel_radius_m": 0.39515292398428903,
"zone": "far-field",
"path_delta_m": 0.8309518948453007,
"phase_rad_per_subcarrier": [
41.65456484993552,
41.660007045499924,
41.66544924106432,
41.67089143662873,
41.676333632193135,
41.681775827757534,
41.68721802332193,
41.69266021888634,
41.69810241445074,
41.703544610015136,
41.70898680557954,
41.71442900114395,
41.71987119670835,
41.72531339227275,
41.73075558783716,
41.73619778340156,
41.74163997896596,
41.74708217453036,
41.75252437009476,
41.75796656565916,
41.763408761223566,
41.76885095678797,
41.77429315235237,
41.77973534791677,
41.78517754348118,
41.79061973904558,
41.79606193460998,
41.801504130174386,
41.806946325738785,
41.812388521303184,
41.81783071686759,
41.823272912431996,
41.828715107996395,
41.834157303560794,
41.83959949912521,
41.845041694689606,
41.850483890254004,
41.85592608581841,
41.86136828138281,
41.86681047694721,
41.87225267251161,
41.87769486807602,
41.88313706364042,
41.88857925920482,
41.89402145476923,
41.89946365033363,
41.90490584589803,
41.91034804146243,
41.91579023702683,
41.92123243259123,
41.92667462815563,
41.932116823720044
],
"phase_rad_min": 41.65456484993552,
"phase_rad_max": 41.932116823720044,
"phase_rad_spread": 0.2775519737845258,
"phase_wraps": 0
},
"freq_5.0_GHz": {
"first_fresnel_radius_m": 0.27376997644007645,
"zone": "far-field",
"path_delta_m": 0.8309518948453007,
"phase_rad_per_subcarrier": [
86.933631945763,
86.9390741413274,
86.94451633689181,
86.94995853245621,
86.9554007280206,
86.96084292358502,
86.9662851191494,
86.97172731471382,
86.97716951027823,
86.98261170584261,
86.98805390140703,
86.99349609697143,
86.99893829253583,
87.00438048810022,
87.00982268366464,
87.01526487922904,
87.02070707479345,
87.02614927035783,
87.03159146592225,
87.03703366148665,
87.04247585705103,
87.04791805261546,
87.05336024817986,
87.05880244374426,
87.06424463930865,
87.06968683487307,
87.07512903043745,
87.08057122600187,
87.08601342156628,
87.09145561713066,
87.09689781269508,
87.10234000825947,
87.10778220382387,
87.11322439938827,
87.11866659495267,
87.12410879051708,
87.1295509860815,
87.13499318164588,
87.14043537721028,
87.1458775727747,
87.15131976833908,
87.1567619639035,
87.1622041594679,
87.1676463550323,
87.1730885505967,
87.17853074616112,
87.1839729417255,
87.18941513728991,
87.19485733285431,
87.20029952841871,
87.20574172398312,
87.21118391954751
],
"phase_rad_min": 86.933631945763,
"phase_rad_max": 87.21118391954751,
"phase_rad_spread": 0.2775519737845116,
"phase_wraps": 0
}
},
{
"name": "scatterer-near-Tx",
"link_m": 5.0,
"scatterer_offset_m": 0.05,
"scatterer_position_m": 0.5,
"freq_2.4_GHz": {
"first_fresnel_radius_m": 0.23709175439057345,
"zone": "zone-1",
"path_delta_m": 0.002771550260963096,
"phase_rad_per_subcarrier": [
0.13893430028417714,
0.13895245213945337,
0.13897060399472957,
0.13898875585000578,
0.139006907705282,
0.13902505956055825,
0.13904321141583445,
0.13906136327111066,
0.1390795151263869,
0.1390976669816631,
0.13911581883693933,
0.13913397069221556,
0.13915212254749174,
0.13917027440276797,
0.1391884262580442,
0.1392065781133204,
0.13922472996859664,
0.13924288182387284,
0.13926103367914908,
0.13927918553442528,
0.13929733738970151,
0.13931548924497772,
0.13933364110025395,
0.13935179295553016,
0.1393699448108064,
0.1393880966660826,
0.13940624852135883,
0.13942440037663503,
0.13944255223191127,
0.13946070408718747,
0.13947885594246368,
0.1394970077977399,
0.13951515965301614,
0.13953331150829235,
0.13955146336356858,
0.1395696152188448,
0.139587767074121,
0.13960591892939725,
0.13962407078467345,
0.13964222263994966,
0.13966037449522586,
0.13967852635050212,
0.1396966782057783,
0.13971483006105453,
0.13973298191633077,
0.13975113377160697,
0.13976928562688318,
0.1397874374821594,
0.13980558933743562,
0.13982374119271185,
0.13984189304798808,
0.13986004490326429
],
"phase_rad_min": 0.13893430028417714,
"phase_rad_max": 0.13986004490326429,
"phase_rad_spread": 0.0009257446190871488,
"phase_wraps": 0
},
"freq_5.0_GHz": {
"first_fresnel_radius_m": 0.16426198586404586,
"zone": "zone-1",
"path_delta_m": 0.002771550260963096,
"phase_rad_per_subcarrier": [
0.28995773618231585,
0.28997588803759206,
0.2899940398928683,
0.2900121917481445,
0.2900303436034208,
0.29004849545869693,
0.29006664731397314,
0.29008479916924934,
0.29010295102452566,
0.29012110287980186,
0.29013925473507807,
0.2901574065903542,
0.2901755584456305,
0.2901937103009067,
0.2902118621561829,
0.29023001401145915,
0.2902481658667354,
0.29026631772201156,
0.29028446957728776,
0.290302621432564,
0.29032077328784023,
0.2903389251431165,
0.2903570769983927,
0.2903752288536689,
0.2903933807089451,
0.2904115325642213,
0.2904296844194975,
0.2904478362747738,
0.29046598813005,
0.2904841399853262,
0.2905022918406024,
0.29052044369587865,
0.29053859555115485,
0.29055674740643106,
0.29057489926170726,
0.2905930511169835,
0.29061120297225973,
0.29062935482753594,
0.2906475066828122,
0.2906656585380884,
0.2906838103933646,
0.2907019622486408,
0.29072011410391707,
0.2907382659591933,
0.2907564178144695,
0.2907745696697457,
0.2907927215250219,
0.2908108733802981,
0.29082902523557436,
0.29084717709085056,
0.2908653289461268,
0.290883480801403
],
"phase_rad_min": 0.28995773618231585,
"phase_rad_max": 0.290883480801403,
"phase_rad_spread": 0.0009257446190871765,
"phase_wraps": 0
}
}
],
"first_fresnel_radii_m": {
"2.4": {
"wavelength_mm": 124.91666666666666,
"link_2.0m": {
"p=0.10": 0.14994999166388773,
"p=0.25": 0.2164341701303193,
"p=0.50": 0.2499166527731462,
"p=0.75": 0.2164341701303193,
"p=0.90": 0.1499499916638877
},
"link_5.0m": {
"p=0.10": 0.23709175439057345,
"p=0.25": 0.3422124705500955,
"p=0.50": 0.39515292398428903,
"p=0.75": 0.3422124705500955,
"p=0.90": 0.2370917543905734
},
"link_10.0m": {
"p=0.10": 0.3352983745859798,
"p=0.25": 0.48396151706514845,
"p=0.50": 0.5588306243099662,
"p=0.75": 0.48396151706514845,
"p=0.90": 0.3352983745859797
}
},
"5.0": {
"wavelength_mm": 59.96,
"link_2.0m": {
"p=0.10": 0.10388840166255327,
"p=0.25": 0.14994999166388773,
"p=0.50": 0.17314733610425545,
"p=0.75": 0.14994999166388773,
"p=0.90": 0.10388840166255325
},
"link_5.0m": {
"p=0.10": 0.16426198586404586,
"p=0.25": 0.23709175439057345,
"p=0.50": 0.27376997644007645,
"p=0.75": 0.23709175439057345,
"p=0.90": 0.16426198586404583
},
"link_10.0m": {
"p=0.10": 0.23230152819127128,
"p=0.25": 0.3352983745859798,
"p=0.50": 0.3871692136521188,
"p=0.75": 0.3352983745859798,
"p=0.90": 0.23230152819127126
}
}
}
}
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""R6 — Fresnel-zone forward model for CSI sensitivity.
See docs/research/sota-2026-05-22/R6-fresnel-forward-model.md.
For a Tx-Rx link, the first Fresnel zone is a prolate ellipsoid whose
radius at fractional position p (0..1) along the LOS path is:
r_n(p) = sqrt(n * lambda * d * p * (1-p)) (for n=1)
A point scatterer that crosses the first Fresnel zone perpendicular to
the LOS introduces a path-length delta:
delta_l(x) = sqrt(d1^2 + x^2) + sqrt(d2^2 + x^2) - d1 - d2
where x is the perpendicular offset. Phase shift on subcarrier k:
phi_k = 2 * pi * f_k * delta_l / c
This is the bedrock forward model that the existing `wifi-densepose-signal`
DSP implicitly assumes. We make it explicit so:
1. R12's revision path (PABS basis grounded in Fresnel geometry) has
somewhere to start.
2. R10's foliage-range estimates can be sanity-checked against Fresnel-
ellipsoid clearance, not just FSPL + foliage attenuation.
3. Multi-subcarrier interference patterns from real scatterers become
predictable rather than mysterious.
Pure NumPy — emits a JSON file with the predictions.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
C = 2.998e8 # speed of light, m/s
def wavelength_m(freq_ghz: float) -> float:
return C / (freq_ghz * 1e9)
def fresnel_radius_m(freq_ghz: float, link_length_m: float, p: float, n: int = 1) -> float:
"""Radius of the n-th Fresnel zone at fractional link position p.
p=0 is at Tx, p=1 is at Rx. r is maximum at p=0.5 (midpoint).
"""
lam = wavelength_m(freq_ghz)
return float(np.sqrt(n * lam * link_length_m * p * (1.0 - p)))
def path_delta_m(d1: float, d2: float, perpendicular_offset_m: float) -> float:
"""Extra path length introduced by a point scatterer at perpendicular
offset x from the LOS, with d1 / d2 the Tx- and Rx-side LOS distances."""
x = perpendicular_offset_m
return float(np.sqrt(d1**2 + x**2) + np.sqrt(d2**2 + x**2) - (d1 + d2))
def csi_phase_shift_rad(freq_ghz: float, path_delta: float) -> float:
"""Phase shift on a single subcarrier given the path-length delta."""
return 2 * np.pi * freq_ghz * 1e9 * path_delta / C
def fresnel_zone_classification(freq_ghz: float, link_length_m: float,
scatterer_offset_m: float,
scatterer_position_m: float) -> str:
"""Is the scatterer inside the n-th Fresnel zone?
Zone n is the volume where r_{n-1} < |offset| <= r_n.
"""
p = scatterer_position_m / link_length_m
if not (0 <= p <= 1):
return "outside-link"
abs_off = abs(scatterer_offset_m)
for n in range(1, 10):
r = fresnel_radius_m(freq_ghz, link_length_m, p, n)
if abs_off <= r:
return f"zone-{n}"
return "far-field"
def subcarrier_phase_sweep(freq_ghz: float, link_length_m: float,
scatterer_offset_m: float,
scatterer_position_m: float,
n_subcarriers: int = 52,
subcarrier_spacing_khz: float = 312.5) -> dict:
"""Predict per-subcarrier phase shift from a single scatterer.
Uses 802.11n/ac 20 MHz channels: 52 used subcarriers, spaced 312.5 kHz.
Subcarrier indices -26..26 excluding DC/pilot tones (we don't bother
excluding here — pure sweep).
"""
d1 = scatterer_position_m
d2 = link_length_m - scatterer_position_m
if d1 <= 0 or d2 <= 0:
raise ValueError("scatterer_position_m must be strictly inside [0, link_length_m]")
delta = path_delta_m(d1, d2, scatterer_offset_m)
# subcarrier frequencies
sub_offsets_hz = (np.arange(n_subcarriers) - n_subcarriers // 2) * subcarrier_spacing_khz * 1e3
f_per_sub = freq_ghz * 1e9 + sub_offsets_hz
phases_rad = 2 * np.pi * f_per_sub * delta / C
return {
"path_delta_m": delta,
"phase_rad_per_subcarrier": phases_rad.tolist(),
"phase_rad_min": float(phases_rad.min()),
"phase_rad_max": float(phases_rad.max()),
"phase_rad_spread": float(phases_rad.max() - phases_rad.min()),
"phase_wraps": int(np.floor((phases_rad.max() - phases_rad.min()) / (2 * np.pi))),
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--out", default="examples/research-sota/r6_fresnel_results.json")
args = parser.parse_args()
# Scenario: 5-metre indoor link (typical bedroom/lab setup)
link_lengths = [2.0, 5.0, 10.0]
freqs = [2.4, 5.0]
p_grid = [0.1, 0.25, 0.5, 0.75, 0.9] # link position fractions
out = {
"model": "first-Fresnel-zone ellipsoid + per-subcarrier path-delta forward model",
"constants": {"c_mps": C},
"scenarios": [],
}
# 1. First Fresnel radii (the basic envelope)
fresnel = {}
for f in freqs:
fresnel[str(f)] = {}
lam = wavelength_m(f)
fresnel[str(f)]["wavelength_mm"] = lam * 1000
for L in link_lengths:
radii = {f"p={p:.2f}": fresnel_radius_m(f, L, p, n=1) for p in p_grid}
fresnel[str(f)][f"link_{L}m"] = radii
out["first_fresnel_radii_m"] = fresnel
# 2. Single-scatterer per-subcarrier sweep
# Scatterer at midpoint, 10 cm off LOS (human standing near link)
scenarios = [
("human-standing-at-midpoint", 5.0, 0.10, 2.5),
("human-walking-into-fresnel", 5.0, 0.25, 2.5),
("scatterer-outside-fresnel", 5.0, 1.50, 2.5),
("scatterer-near-Tx", 5.0, 0.05, 0.5),
]
for name, L, x_off, x_pos in scenarios:
case = {"name": name, "link_m": L, "scatterer_offset_m": x_off,
"scatterer_position_m": x_pos}
for f in freqs:
r1 = fresnel_radius_m(f, L, x_pos / L, n=1)
zone = fresnel_zone_classification(f, L, x_off, x_pos)
sweep = subcarrier_phase_sweep(f, L, x_off, x_pos)
case[f"freq_{f}_GHz"] = {
"first_fresnel_radius_m": r1,
"zone": zone,
**sweep,
}
out["scenarios"].append(case)
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
Path(args.out).write_text(json.dumps(out, indent=2))
print("=== First Fresnel zone radii (m) ===")
print(f"{'freq':>5} {'lambda':>8} {'link':>5} " + " ".join(f"p={p:.2f}" for p in p_grid))
for f in freqs:
lam_mm = wavelength_m(f) * 1000
for L in link_lengths:
radii = [fresnel_radius_m(f, L, p, n=1) for p in p_grid]
row = f"{f:>5.1f} {lam_mm:>5.1f}mm {L:>4.1f}m " + " ".join(f"{r:>6.3f}" for r in radii)
print(row)
print()
print("=== Single-scatterer per-subcarrier predictions ===")
for case in out["scenarios"]:
print(f"{case['name']:>32} ", end="")
for f in freqs:
k = f"freq_{f}_GHz"
v = case[k]
print(f"{f:.1f}GHz: r1={v['first_fresnel_radius_m']*100:.1f}cm "
f"zone={v['zone']:<8} "
f"phase-spread={np.degrees(v['phase_rad_spread']):.3f} deg "
f"wraps={v['phase_wraps']}", end=" ")
print()
print()
print(f"Wrote {args.out}")
if __name__ == "__main__":
main()