#!/usr/bin/env python3 """Фазовая калибровка MIMO-радара по точечному отражателю (шару) и применение её к данным. Пример использует общий расчёт с интерфейсом программы. Формат файлов калибровки (cal_*.npy, cal_meta.json) и папки postprocessed сохранён. Запуск целиком: python mimo_calibration.py (работает по параметрам из блока ниже) Из другого кода: import mimo_calibration as mc cal = mc.compute_calibration(cal_sets, empty_scene_path, mc.Settings(...)) mc.save_calibration(cal, cal_dir) cal = mc.load_calibration(cal_dir) s21_corr = mc.apply_calibration(s21, freq, cal) # массив (TX, RX, Nf) в памяти mc.apply_to_measurement(meas_dir, cal) # preprocessed -> postprocessed """ from pathlib import Path from statistics import mean from python_app.workflows.mimo_calibration import ( Settings, apply_calibration, apply_to_measurement, calibration_on_grid, compute_calibration, find_config_profile, load_calibration, load_geometry, load_s21, save_calibration, ) # --- что делать ------------------------------------------------------------------------- RUN_CALIBRATION = True # True -> посчитать калибровку по CAL_SETS и записать в CAL_DIR; # False -> взять готовую калибровку из CAL_DIR APPLY_TO_DATA = False # True -> записать исправленные данные в <измерение>/postprocessed # --- калибровочные измерения ------------------------------------------------------------ # Каждая запись — одно положение отражателя. snapshots: список префиксов снимков ('0003') # или None (все снимки папки). Несколько снимков одной записи усредняются. # target_scene_m = (X, Y, Z) в осях сцены, метры; для шара — координаты его ЦЕНТРА, # измеренные физически (а не взятые с некалиброванного изображения): # X — поперёк, вправо, от начала координат антенн (config_profile, x = 0); # Y — высота центра отражателя над землёй; # Z — расстояние вдоль земли от точки под началом координат антенн до отражателя. CAL_ROOT = Path('/Users/ivan_root/Downloads/Telegram_dwnld/2026-09-31 balls calib/' 'librevna_multi_0.5-6GHz_1031pts_-3dBm_big_ball_h_0cm_z_5m_x_0cm/preprocessed') CAL_SETS = [ dict(path=CAL_ROOT, snapshots=[f'{index:04d}'], target_scene_m=(0.0, 0.16, 5.20)) for index in range(5) ] SPHERE_RADIUS_M = 0.16 # радиус шара-отражателя [м]; 0 -> точечная цель EMPTY_SCENE_PATH = Path('/Users/ivan_root/Downloads/Telegram_dwnld/2026-09-31 balls calib/' 'librevna_multi_0.5-6GHz_1031pts_-3dBm_bkgd_h_0cm/preprocessed') # Дополнительная пустая сцена для архивного примера. # Если референс уже удалил фон, задайте None. # --- геометрия (как в софте построения изображений) --------------------------------------- INPUT_IDX = [0, 1, 2, 3] # физические входы (Rx), input_pos OUTPUT_IDX = [0, 1, 2, 3] # физические выходы (Tx), output_pos ANTENNA_HEIGHT_M = 0.92 # высота начала координат антенн (нижний ряд) над землёй [м] PITCH_DEG = 10.0 # наклон оси радара ниже горизонта [град] # --- оценка ----------------------------------------------------------------------------- CAL_F_MIN = 2.0e9 # полоса, по которой оценивается калибровка [Гц] CAL_F_MAX = 6.0e9 GATE_HALF_M = 0.15 # окно вокруг эха цели, ± по бистатическому пути [м] GATE_TAPER_M = 0.03 # плавные края окна [м] COARSE_RANGE_SEARCH_M = 0.6 # грубый поиск эха по дальности перед точной подгонкой, ± по пути [м]; # 0 -> выключен REFINE_AXES = ('x', 'y', 'z') # какие координаты цели уточнять по данным; если X и Y измерены # точно — ('z',) REFINE_RANGE_M = 0.10 # уточнение не дальше этого от найденной точки [м] APPLY_AMPLITUDE = False # True -> поправка выравнивает и амплитуды антенн PHASE_MODEL = 'linear' # модель фазы антенны по частоте: 'const' | 'linear' | 'poly3' | 'raw' # --- куда писать калибровку --------------------------------------------------------------- CAL_NAME = 'cal_20260931_big_ball_h0_script' CAL_DIR = Path(__file__).resolve().parent / 'results' / 'calibration' / CAL_NAME OVERWRITE = True # True -> перезаписать файлы калибровки в CAL_DIR # --- к каким измерениям применять --------------------------------------------------------- # Список папок измерений (тех, где лежат preprocessed, raw, results). APPLY_TO_MEASUREMENTS = [ # '/путь/к/измерению', ] OVERWRITE_POSTPROCESSED = False # True -> пересоздать postprocessed (только созданный этой калибровкой: # папка без calibration_applied.json не удаляется никогда) def main() -> None: if RUN_CALIBRATION: cal = compute_calibration(CAL_SETS, EMPTY_SCENE_PATH, Settings( sphere_radius_m=SPHERE_RADIUS_M, input_idx=tuple(INPUT_IDX), output_idx=tuple(OUTPUT_IDX), antenna_height_m=ANTENNA_HEIGHT_M, pitch_deg=PITCH_DEG, f_min=CAL_F_MIN, f_max=CAL_F_MAX, gate_half_m=GATE_HALF_M, gate_taper_m=GATE_TAPER_M, coarse_range_search_m=COARSE_RANGE_SEARCH_M, refine_axes=tuple(REFINE_AXES), refine_range_m=REFINE_RANGE_M, apply_amplitude=APPLY_AMPLITUDE, phase_model=PHASE_MODEL)) save_calibration(cal, CAL_DIR, overwrite=OVERWRITE) q = cal['meta']['quality'] print(f'Калибровка записана в {CAL_DIR}: сфазированность пар на отражателе ' f'{mean(q["coherence_before"]):.2f} -> {mean(q["coherence_final"]):.2f}') if APPLY_TO_DATA: cal = load_calibration(CAL_DIR) targets = APPLY_TO_MEASUREMENTS if isinstance(targets, (str, Path)): # один путь вместо списка targets = [targets] for meas in targets: post, n_snap, n_fix, _ = apply_to_measurement(meas, cal, OVERWRITE_POSTPROCESSED) print(f'{Path(meas).name}: {n_snap} снимков, исправлено S21 {n_fix} -> {post}') if __name__ == '__main__': main()