diff --git a/ardupilot_methodic_configurator/data_model_par_dict.py b/ardupilot_methodic_configurator/data_model_par_dict.py index 6b0171179..04b49a7e6 100644 --- a/ardupilot_methodic_configurator/data_model_par_dict.py +++ b/ardupilot_methodic_configurator/data_model_par_dict.py @@ -261,9 +261,9 @@ def _format_params(self, file_format: str = "missionplanner") -> list[str]: sorted_dict = ParDict(sorted_items) formatted_params = [ ( - f"{key},{format(parameter.value, '.6f').rstrip('0').rstrip('.')} # {parameter.comment}" + f"{key},{format(parameter.value, '.10f').rstrip('0').rstrip('.')} # {parameter.comment}" if isinstance(parameter, Par) and parameter.comment - else f"{key},{format(parameter.value if isinstance(parameter, Par) else parameter, '.6f').rstrip('0').rstrip('.')}" # noqa: E501 # pylint: disable=line-too-long + else f"{key},{format(parameter.value if isinstance(parameter, Par) else parameter, '.10f').rstrip('0').rstrip('.')}" # noqa: E501 # pylint: disable=line-too-long ) for key, parameter in sorted_dict.items() ] @@ -271,9 +271,9 @@ def _format_params(self, file_format: str = "missionplanner") -> list[str]: sorted_dict = ParDict(dict(sorted(self.items()))) formatted_params = [ ( - f"{key:<16} {parameter.value:<8.6f} # {parameter.comment}" + f"{key:<16} {parameter.value:<14.10f} # {parameter.comment}" if isinstance(parameter, Par) and parameter.comment - else f"{key:<16} {parameter.value if isinstance(parameter, Par) else parameter:<8.6f}" + else f"{key:<16} {parameter.value if isinstance(parameter, Par) else parameter:<14.10f}" ) for key, parameter in sorted_dict.items() ] diff --git a/tests/test_data_model_par_dict.py b/tests/test_data_model_par_dict.py index 3ff4ccaaf..1d8918482 100755 --- a/tests/test_data_model_par_dict.py +++ b/tests/test_data_model_par_dict.py @@ -569,8 +569,8 @@ def test_user_can_export_parameters_to_mavproxy_format(self, parameter_dict) -> content = f.read() # MAVProxy format uses fixed-width columns - assert "ACRO_YAW_P 4.500000 # Yaw P gain" in content - assert "GPS_TYPE 1.000000" in content + assert "ACRO_YAW_P 4.5000000000 # Yaw P gain" in content + assert "GPS_TYPE 1.0000000000 " in content finally: os.unlink(output_file) @@ -949,7 +949,7 @@ def test_user_can_format_parameters_for_mavproxy(self, parameter_dict) -> None: # Assert: Correct MAVProxy format (space-separated, fixed width) yaw_line = next((line for line in formatted if "ACRO_YAW_P" in line), "") - assert yaw_line == "ACRO_YAW_P 4.500000 # Yaw P gain" + assert yaw_line == "ACRO_YAW_P 4.5000000000 # Yaw P gain" def test_user_receives_error_for_unsupported_format(self, parameter_dict) -> None: """ @@ -965,6 +965,78 @@ def test_user_receives_error_for_unsupported_format(self, parameter_dict) -> Non with pytest.raises(ParamFileError, match="Unsupported file format"): parameter_dict._format_params("invalid_format") # pylint: disable=protected-access + def test_user_preserves_small_parameter_values_on_export(self) -> None: + """ + User preserves parameter values smaller than 1e-6 during export. + + GIVEN: A user has parameters with very small but nonzero values + WHEN: They format using MissionPlanner or MAVProxy format + THEN: The formatted strings should retain the small values + """ + # Arrange: Parameters with values that would be truncated by .6f formatting + small_value_params = ParDict( + { + "TINY_VAL": Par(0.0000001, "Very small value"), + "MICRO_VAL": Par(0.0000005, "Another small value"), + "BOUNDARY_VAL": Par(0.000001, "Boundary value"), + "NORMAL_VAL": Par(1.5, "Normal value"), + } + ) + + for file_format in ("missionplanner", "mavproxy"): + formatted = small_value_params._format_params(file_format) # pylint: disable=protected-access + + tiny_found = False + micro_found = False + boundary_found = False + for line in formatted: + if "TINY_VAL" in line: + assert "0.0000001" in line, f"Value 1e-7 lost in {file_format} format: {line}" + tiny_found = True + if "MICRO_VAL" in line: + assert "0.0000005" in line, f"Value 5e-7 lost in {file_format} format: {line}" + micro_found = True + if "BOUNDARY_VAL" in line: + assert "0.000001" in line, f"Value 1e-6 lost in {file_format} format: {line}" + boundary_found = True + + assert tiny_found, f"TINY_VAL not found in {file_format} formatted output" + assert micro_found, f"MICRO_VAL not found in {file_format} formatted output" + assert boundary_found, f"BOUNDARY_VAL not found in {file_format} formatted output" + + def test_user_preserves_small_values_on_export_reimport_round_trip(self) -> None: + """ + User preserves small parameter values through a full export→reimport round-trip. + + GIVEN: A user has parameters with very small but nonzero values + WHEN: They export to a file and reimport it + THEN: The numeric values should match within float32 tolerance + """ + small_value_params = ParDict( + { + "TINY_VAL": Par(0.0000001, "Very small value"), + "BOUNDARY_VAL": Par(0.000001, "Boundary value"), + "NORMAL_VAL": Par(1.5, "Normal value"), + } + ) + + for file_format in ("missionplanner", "mavproxy"): + with tempfile.NamedTemporaryFile(mode="w", suffix=".param", delete=False) as f: + output_file = f.name + + try: + small_value_params.export_to_param(output_file, file_format) + reloaded = ParDict.load_param_file_into_dict(output_file) + + for key, original in small_value_params.items(): + original_val = original.value if isinstance(original, Par) else original + reloaded_val = reloaded[key].value if isinstance(reloaded[key], Par) else reloaded[key] + assert abs(reloaded_val - original_val) < 1e-10, ( + f"{key}: value {original_val} became {reloaded_val} after {file_format} round-trip" + ) + finally: + os.unlink(output_file) + def test_user_can_annotate_parameters_with_comments(self, parameter_dict) -> None: """ User can add comments to parameters using a lookup table.