- BG_GATE_ENABLE: frame peak over its median background (sar3d_background); the GUI scales the frame with bg_gate_gain and its cloud threshold. - SAR_CLEAN_ENABLE: CLEAN of a one-frame aperture on a 4x10x4 cm search grid over the whole scene; objects in their own scale (sar3d_objects). - SAR_MARK_ENABLE: boxes by class around object groups (sar3d_marks), with the legend under the views. - SAR_VOXEL_M as three steps X/Y/Z with the notebook defaults 0.025/0.02/0.02. - GPU backend: image/download/load/project API, per-plane block reduction for the maximum, depth gate without FMA; sar3d host code built with -ffp-contract=off. - References regenerated from notebook cells 1 and 5, with new CLEAN scenes and a synthetic box-rule case; docs updated.
138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
"""Tests for GUI profile persistence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from python_app.models.gui_profile_schema import (
|
|
GuiPassThroughStateModel,
|
|
GuiProcessingStateModel,
|
|
GuiProfileModel,
|
|
GuiSar3dStateModel,
|
|
GuiStateModel,
|
|
)
|
|
|
|
|
|
class GuiProfileCodecTest(unittest.TestCase):
|
|
def test_pass_through_combo_filter_round_trips(self) -> None:
|
|
profile = GuiProfileModel(
|
|
gui=GuiStateModel(
|
|
processing=GuiProcessingStateModel(
|
|
pass_through=GuiPassThroughStateModel(combo_filter="0:0,1:0")
|
|
)
|
|
)
|
|
)
|
|
|
|
encoded = profile.to_dict()
|
|
decoded = GuiProfileModel.from_dict(encoded)
|
|
|
|
self.assertIsNotNone(decoded.gui)
|
|
assert decoded.gui is not None
|
|
self.assertEqual(decoded.gui.processing.pass_through.combo_filter, "0:0,1:0")
|
|
self.assertEqual(encoded["gui"]["processing"]["pass_through"]["combo_filter"], "0:0,1:0")
|
|
|
|
def test_pass_through_unwrap_phase_round_trips(self) -> None:
|
|
profile = GuiProfileModel(
|
|
gui=GuiStateModel(
|
|
processing=GuiProcessingStateModel(
|
|
pass_through=GuiPassThroughStateModel(unwrap_phase=True)
|
|
)
|
|
)
|
|
)
|
|
|
|
encoded = profile.to_dict()
|
|
decoded = GuiProfileModel.from_dict(encoded)
|
|
|
|
assert decoded.gui is not None
|
|
self.assertTrue(decoded.gui.processing.pass_through.unwrap_phase)
|
|
self.assertTrue(encoded["gui"]["processing"]["pass_through"]["unwrap_phase"])
|
|
|
|
def test_default_profile_round_trips_idempotently(self) -> None:
|
|
# Full-subtree idempotence catches field-drop/mis-map regressions across every
|
|
# sub-model, which the single combo_filter round-trip above cannot.
|
|
once = GuiProfileModel().to_dict()
|
|
twice = GuiProfileModel.from_dict(once).to_dict()
|
|
self.assertEqual(once["gui"], twice["gui"])
|
|
|
|
def test_legacy_save_count_is_ignored_without_losing_save_destination(self) -> None:
|
|
decoded = GuiProfileModel.from_dict({
|
|
"gui": {"data_actions": {
|
|
"save_count": 7,
|
|
"save_path": "/tmp/measurements",
|
|
"save_name": "scene",
|
|
"record_count": 250,
|
|
}},
|
|
})
|
|
|
|
encoded = decoded.to_dict()["gui"]["data_actions"]
|
|
self.assertNotIn("save_count", encoded)
|
|
self.assertEqual(encoded, {
|
|
"save_path": "/tmp/measurements", "save_name": "scene", "record_count": 250,
|
|
})
|
|
|
|
def test_sar3d_section_round_trips(self) -> None:
|
|
sar3d = GuiSar3dStateModel(
|
|
input_positions="0,2",
|
|
output_positions="1,3",
|
|
min_depth_m=2.5,
|
|
max_depth_m=9.0,
|
|
start_freq_mhz=2000.0,
|
|
stop_freq_mhz=5500.0,
|
|
speed_m_s=1.42,
|
|
ignore_socket_speed_enabled=True,
|
|
look_angle_deg=6.5,
|
|
frame_radius=3,
|
|
voxel_x_m=0.035,
|
|
voxel_y_m=0.03,
|
|
voxel_z_m=0.025,
|
|
bp_ground_depth_m=1.1,
|
|
comp_range_power=0.5,
|
|
use_compensation=False,
|
|
normalize_each_frame=False,
|
|
)
|
|
profile = GuiProfileModel(gui=GuiStateModel(processing=GuiProcessingStateModel(sar3d=sar3d)))
|
|
|
|
decoded = GuiProfileModel.from_dict(profile.to_dict())
|
|
|
|
assert decoded.gui is not None
|
|
self.assertEqual(decoded.gui.processing.sar3d, sar3d)
|
|
|
|
def test_missing_gui_section_yields_none(self) -> None:
|
|
self.assertIsNone(GuiProfileModel.from_dict({}).gui)
|
|
|
|
def test_unsupported_version_is_rejected(self) -> None:
|
|
with self.assertRaisesRegex(ValueError, "version"):
|
|
GuiProfileModel.from_dict({"gui": {"version": 2}})
|
|
|
|
def test_invalid_combo_mode_is_rejected(self) -> None:
|
|
with self.assertRaisesRegex(ValueError, "combo_mode"):
|
|
GuiProfileModel.from_dict({"gui": {"switches": {"combo_mode": "bogus"}}})
|
|
|
|
def test_wrong_typed_field_is_rejected(self) -> None:
|
|
with self.assertRaises(ValueError): # combos_text must be a JSON string, not a number
|
|
GuiProfileModel.from_dict({"gui": {"switches": {"combos_text": 123}}})
|
|
|
|
def test_obsolete_inactive_settings_are_ignored(self) -> None:
|
|
decoded = GuiProfileModel.from_dict({
|
|
"gui": {"processing": {"selected_mode": "gpr", "legacy_gpr": "obsolete",
|
|
"gpr": {"algorithm": "legacy_point"}}},
|
|
"gpr": {"relative_permittivity": "obsolete", "mode": "point"},
|
|
})
|
|
assert decoded.gui is not None
|
|
self.assertEqual(decoded.gui.processing.selected_mode, "gpr")
|
|
self.assertNotIn("legacy_gpr", decoded.to_dict()["gui"]["processing"])
|
|
self.assertNotIn("relative_permittivity", decoded.to_dict()["gpr"])
|
|
|
|
def test_removed_processing_mode_is_rejected(self) -> None:
|
|
with self.assertRaisesRegex(ValueError, "selected_mode must be one of"):
|
|
GuiProfileModel.from_dict({"gui": {"processing": {"selected_mode": "legacy_gpr"}}})
|
|
|
|
def test_modern_gpr_selection_is_not_migrated(self) -> None:
|
|
decoded = GuiProfileModel.from_dict({"gui": {"processing": {"selected_mode": "gpr"}}})
|
|
assert decoded.gui is not None
|
|
self.assertEqual(decoded.gui.processing.selected_mode, "gpr")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|