diff --git a/ARCHITECTURE_log_analysis.md b/ARCHITECTURE_log_analysis.md index 7f397b486..f69a6aac2 100644 --- a/ARCHITECTURE_log_analysis.md +++ b/ARCHITECTURE_log_analysis.md @@ -12,17 +12,17 @@ The main components are: 1. **Log Analysis Backend** - Loads and validates ArduPilot `.bin` logs and prepares the data required by the analysis layer. - * [`backend_log_analysis.py`](ardupilot_methodic_configurator/log_analysis/backend_log_analysis.py) - * [`backend_log_extraction.py`](ardupilot_methodic_configurator/log_analysis/backend_log_extraction.py) + * [`backend_log_analysis.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/backend_log_analysis.py) + * [`backend_log_extraction.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/backend_log_extraction.py) 2. **Log Analysis Data Models** - Contains the analysis pipeline, shared context, quality models, analysis models, and result structures. - * [`data_model_log_analysis.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py) - * [`data_model_log_analysis_context.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_analysis_context.py) - * [`data_model_parameter_derivation.py`](ardupilot_methodic_configurator/log_analysis/data_model_parameter_derivation.py) - * [`data_model_log_quality.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_quality.py) - * [`data_model_log_quality_check.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_quality_check.py) - * [`data_model_quality_base.py`](ardupilot_methodic_configurator/log_analysis/data_model_quality_base.py) + * [`data_model_log_analysis.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py) + * [`data_model_log_analysis_context.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis_context.py) + * [`data_model_parameter_derivation.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_parameter_derivation.py) + * [`data_model_log_availability.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_availability.py) + * [`data_model_log_availability_check.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_availability_check.py) + * [`data_model_availability_base.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_availability_base.py) 3. **Quality and Analysis Models** - Performs subsystem-specific data validation and analysis. @@ -82,7 +82,8 @@ are converted explicitly before comparison. ## Log Analysis Backend -[`backend_log_analysis.py`](ardupilot_methodic_configurator/log_analysis/backend_log_analysis.py) acts as the orchestration layer between log extraction, Methodic +[`backend_log_analysis.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/backend_log_analysis.py) acts as +the orchestration layer between log extraction, Methodic Configurator context, and the analysis data models. Its responsibilities are: @@ -117,7 +118,8 @@ This prevents analysis results from being interpreted using the configuration of ## Log Analysis Context -[`data_model_log_analysis_context.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_analysis_context.py) provides the common information required by +[`data_model_log_analysis_context.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis_context.py) + provides the common information required by the quality and analysis models. The context contains: @@ -140,7 +142,8 @@ data; tests can supply a small replacement service without a vehicle directory. ## Analysis Pipeline -[`data_model_log_analysis.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py) contains the main domain-level analysis pipeline. +[`data_model_log_analysis.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py) +contains the main domain-level analysis pipeline. The `analyze_log()` function performs the following operations: @@ -201,7 +204,7 @@ This separation distinguishes between missing or invalid data, a valid analysis ## Quality and Analysis Model Registry -The quality and analysis model registry is defined in [`data_model_log_analysis.py`](ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py). +The quality and analysis model registry is defined in [`data_model_log_analysis.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py). | Quality Model | Analysis Model | | ------------- | -------------- | @@ -277,7 +280,8 @@ The resulting `HardwareReport` is included in `LogSummary`. ## Tuning Report -[`data_model_tuning_report.py`](ardupilot_methodic_configurator/log_analysis/data_model_tuning_report.py) provides the data model for parameter changes across +[`data_model_tuning_report.py`](https://github.com/ArduPilot/MethodicConfigurator/blob/master/ardupilot_methodic_configurator/log_analysis/data_model_tuning_report.py) +provides the data model for parameter changes across Methodic Configurator configuration steps. The tuning report parser reads `tuning_report.csv` and produces a `TuningReport` containing `steps` and `values`. diff --git a/ardupilot_methodic_configurator/backend_filesystem_program_settings.py b/ardupilot_methodic_configurator/backend_filesystem_program_settings.py index 9464cc807..fc141eb17 100644 --- a/ardupilot_methodic_configurator/backend_filesystem_program_settings.py +++ b/ardupilot_methodic_configurator/backend_filesystem_program_settings.py @@ -77,8 +77,8 @@ class UsagePopupWindowDefinition: "only_changed_get_uploaded": UsagePopupWindowDefinition( description=_("Only changed parameters get upload explanation"), ), - "log_quality_report": UsagePopupWindowDefinition( - description=_("Log quality report explanation"), + "log_availability_report": UsagePopupWindowDefinition( + description=_("Log availability report explanation"), ), } diff --git a/ardupilot_methodic_configurator/frontend_tkinter_log_analysis.py b/ardupilot_methodic_configurator/frontend_tkinter_log_analysis.py index d41d48d4b..68ba9feb6 100644 --- a/ardupilot_methodic_configurator/frontend_tkinter_log_analysis.py +++ b/ardupilot_methodic_configurator/frontend_tkinter_log_analysis.py @@ -57,7 +57,7 @@ class Severity(Enum): } -def _collect_links(quality_dict: dict[str, Any] | None, analysis_dict: dict[str, Any] | None) -> list[dict[str, Any]]: +def _collect_links(availability_dict: dict[str, Any] | None, analysis_dict: dict[str, Any] | None) -> list[dict[str, Any]]: seen: set[tuple[str | None, str | None]] = set() links: list[dict[str, Any]] = [] @@ -70,8 +70,8 @@ def _add(step_info: dict[str, Any] | None) -> None: seen.add(key) links.append(step_info) - if quality_dict: - for issue in quality_dict.get("issues", []): + if availability_dict: + for issue in availability_dict.get("issues", []): _add(issue.get("step_info")) if analysis_dict: for outcome in analysis_dict.get("outcomes", []): @@ -135,14 +135,14 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen self.upload_callback = upload_callback self._ai_panel_visible = False - self.pairs = summary.paired_quality_and_analysis_results() + self.pairs = summary.paired_availability_and_analysis_results() self.subsystem_names = [q.name for q, _a in self.pairs] - self._report_quality_by_name: dict[str, dict[str, Any]] = {} + self._report_availability_by_name: dict[str, dict[str, Any]] = {} self._report_analysis_by_name: dict[str, dict[str, Any]] = {} if report is not None: - for entry in report.get("data_quality", []): - self._report_quality_by_name[entry.get("name", "")] = entry + for entry in report.get("data_availability", []): + self._report_availability_by_name[entry.get("name", "")] = entry for entry in report.get("analysis", []): name = entry.get("name", "") self._report_analysis_by_name[name.removesuffix(" Analysis")] = entry @@ -238,13 +238,13 @@ def _render_subsystem(self, name: str) -> None: # pylint: disable=too-many-loca matching = [(q, a) for q, a in self.pairs if q.name == name] if not matching: return - quality_result, analysis_result = matching[0] + availability_result, analysis_result = matching[0] - quality_dict = self._report_quality_by_name.get(name) + availability_dict = self._report_availability_by_name.get(name) analysis_dict = self._report_analysis_by_name.get(name) self._section_heading(_("Links")) - links = _collect_links(quality_dict, analysis_dict) + links = _collect_links(availability_dict, analysis_dict) if not links: self._section_body(_("No linked documentation for this subsystem.")) else: @@ -255,7 +255,7 @@ def _render_subsystem(self, name: str) -> None: # pylint: disable=too-many-loca self._section_link(_("Guide"), link.get("blog_text") or link["blog_url"], link["blog_url"]) vehicle_components = (self.report or {}).get("vehicle_components") or {} - component_keys = self.summary.component_keys_for_subsystem(quality_result.subsystem_key) + component_keys = self.summary.component_keys_for_subsystem(availability_result.subsystem_key) hardware_lines: list[tuple[str, list[str]]] = [] for key in component_keys: component = vehicle_components.get(key) @@ -271,14 +271,14 @@ def _render_subsystem(self, name: str) -> None: # pylint: disable=too-many-loca for line in lines: self._bullet_line(line) - self._section_heading(_("Quality")) - self._section_body(quality_result.reason) - for issue in quality_result.issues: + self._section_heading(_("Availability")) + self._section_body(availability_result.reason) + for issue in availability_result.issues: self._bullet_line(issue.message) self._section_heading(_("Analysis")) if analysis_result is None: - self._section_body(_("Not yet analyzed - {reason}").format(reason=quality_result.reason)) + self._section_body(_("Not yet analyzed - {reason}").format(reason=availability_result.reason)) elif not analysis_result.outcomes: self._section_body(_("No findings.")) else: diff --git a/ardupilot_methodic_configurator/frontend_tkinter_log_quality.py b/ardupilot_methodic_configurator/frontend_tkinter_log_availability.py similarity index 89% rename from ardupilot_methodic_configurator/frontend_tkinter_log_quality.py rename to ardupilot_methodic_configurator/frontend_tkinter_log_availability.py index 7e6b0364d..8663a6abb 100644 --- a/ardupilot_methodic_configurator/frontend_tkinter_log_quality.py +++ b/ardupilot_methodic_configurator/frontend_tkinter_log_availability.py @@ -1,5 +1,5 @@ """ -Log quality report window for the ArduPilot Methodic Configurator. +Log availability report window for the ArduPilot Methodic Configurator. Displays a parsed ArduPilot .bin log analysis. @@ -24,14 +24,14 @@ from ardupilot_methodic_configurator.formatting import format_filesize from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow from ardupilot_methodic_configurator.frontend_tkinter_log_analysis import LogAnalysisReportWindow -from ardupilot_methodic_configurator.frontend_tkinter_log_hardware_quality import build_hardware_tab +from ardupilot_methodic_configurator.frontend_tkinter_log_hardware_availability import build_hardware_tab from ardupilot_methodic_configurator.frontend_tkinter_scroll_frame import ScrollFrame from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis import LogSummary -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import ( - LogQualityResult, - LogQualityState, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import ( + AvailabilityIssue, + LogAvailabilityResult, + LogAvailabilityState, StepValidationResult, ) from ardupilot_methodic_configurator.log_analysis.data_model_log_report import ( @@ -47,7 +47,7 @@ def _format_parameter_value(value: float) -> str: return str(int(value)) if value.is_integer() else str(value) -class LogQualityReportWindow(BaseWindow): # pylint: disable=too-many-instance-attributes +class LogAvailabilityReportWindow(BaseWindow): # pylint: disable=too-many-instance-attributes """Displays log analysis results as a beginner-friendly, detailed dashboard.""" # pylint: disable=duplicate-code @@ -68,7 +68,7 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen self.upload_callback = upload_callback self.navigate_callback = navigate_callback self._parent_root = root_tk - self.root.title(_("Log Quality Report")) + self.root.title(_("Log Availability Report")) self.root.geometry(self.calculate_scaled_geometry(1000, 750)) self.center_window(self.root, root_tk) self.root.resizable(width=True, height=True) @@ -147,8 +147,8 @@ def _build_footer(self) -> None: def _on_continue_to_analysis(self) -> None: pending_names = [ - quality_result.name - for quality_result, analysis_result in self.summary.paired_quality_and_analysis_results() + availability_result.name + for availability_result, analysis_result in self.summary.paired_availability_and_analysis_results() if analysis_result is None ] if pending_names: @@ -223,7 +223,7 @@ def _apply_param_fixes(self, fixes: list[tuple[str, float, float, list[str]]], d dialog.destroy() @staticmethod - def _first_config_step(issues: list[QualityIssue]) -> str: + def _first_config_step(issues: list[AvailabilityIssue]) -> str: for issue in issues: if issue.config_step: return issue.config_step @@ -236,7 +236,7 @@ def _navigate_to_step(self, step: str) -> None: self._parent_root.lift() self._parent_root.focus_force() - def _fixes_for_issues(self, issues: list[QualityIssue]) -> list[tuple[str, float, float, list[str]]]: + def _fixes_for_issues(self, issues: list[AvailabilityIssue]) -> list[tuple[str, float, float, list[str]]]: """ Compute proposed parameter changes for a specific set of issues. @@ -244,7 +244,7 @@ def _fixes_for_issues(self, issues: list[QualityIssue]) -> list[tuple[str, float LOG_BITMASK entries within the given issues are OR-merged; every other parameter takes its first suggested value. """ - by_param: dict[str, list[QualityIssue]] = {} + by_param: dict[str, list[AvailabilityIssue]] = {} for issue in issues: if issue.param_name is not None and issue.suggested_value is not None: by_param.setdefault(issue.param_name, []).append(issue) @@ -326,22 +326,22 @@ def _build_tabs(self) -> None: notebook = ttk.Notebook(self.main_frame) notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=12, pady=(12, 12)) - quality_frame = ttk.Frame(notebook) - notebook.add(quality_frame, text=_(" Quality Report ")) - self._build_quality_tab(quality_frame) + availability_frame = ttk.Frame(notebook) + notebook.add(availability_frame, text=_(" Data Availability Report ")) + self._build_availability_tab(availability_frame) hardware_frame = ttk.Frame(notebook) notebook.add(hardware_frame, text=_(" Hardware Overview ")) build_hardware_tab(hardware_frame, self.summary.hardware_report) - def _build_quality_tab(self, parent: ttk.Frame) -> None: # pylint: disable=too-many-branches + def _build_availability_tab(self, parent: ttk.Frame) -> None: # pylint: disable=too-many-branches scroll_container = ScrollFrame(parent) scroll_container.pack(fill=tk.BOTH, expand=True) inner = scroll_container.view_port absorbed_by_step: dict[str, list[StepValidationResult]] = {} for step_result in self.summary.step_results: - for q in self.summary.quality_results: + for q in self.summary.availability_results: if q.related_step and q.related_step == step_result.step: absorbed_by_step.setdefault(q.related_step, []).append(step_result) break @@ -351,8 +351,8 @@ def _build_quality_tab(self, parent: ttk.Frame) -> None: # pylint: disable=too- needs_attention: list[tuple[str, object]] = [] passed_checks: list[tuple[str, object]] = [] - for q in self.summary.quality_results: - (passed_checks if q.state == LogQualityState.INFO else needs_attention).append(("quality", q)) + for q in self.summary.availability_results: + (passed_checks if q.state == LogAvailabilityState.INFO else needs_attention).append(("availability", q)) for s in self.summary.step_results: if s.step in absorbed_steps: continue @@ -363,10 +363,10 @@ def _build_quality_tab(self, parent: ttk.Frame) -> None: # pylint: disable=too- anchor=tk.W, padx=14, pady=(18, 6) ) for kind, item in needs_attention: - if kind == "quality": - quality_item = cast("LogQualityResult", item) - quality_absorbed_steps = absorbed_by_step.get(quality_item.related_step, []) - self._quality_result_card(inner, quality_item, quality_absorbed_steps) + if kind == "availability": + availability_item = cast("LogAvailabilityResult", item) + availability_absorbed_steps = absorbed_by_step.get(availability_item.related_step, []) + self._availability_result_card(inner, availability_item, availability_absorbed_steps) else: self._step_result_card(inner, item) # type: ignore[arg-type] ttk.Separator(inner, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=14, pady=(14, 14)) @@ -376,15 +376,15 @@ def _build_quality_tab(self, parent: ttk.Frame) -> None: # pylint: disable=too- anchor=tk.W, padx=14, pady=(10, 6) ) for kind, item in passed_checks: - if kind == "quality": - quality_item = cast("LogQualityResult", item) - quality_absorbed_steps = absorbed_by_step.get(quality_item.related_step, []) - self._quality_result_card(inner, quality_item, quality_absorbed_steps) + if kind == "availability": + availability_item = cast("LogAvailabilityResult", item) + availability_absorbed_steps = absorbed_by_step.get(availability_item.related_step, []) + self._availability_result_card(inner, availability_item, availability_absorbed_steps) else: self._step_result_card(inner, item) # type: ignore[arg-type] - def _quality_result_card( - self, parent: ttk.Frame, result: LogQualityResult, absorbed_steps: list[StepValidationResult] + def _availability_result_card( + self, parent: ttk.Frame, result: LogAvailabilityResult, absorbed_steps: list[StepValidationResult] ) -> None: card = ttk.Frame(parent) card.pack(fill=tk.X, padx=14, pady=6) diff --git a/ardupilot_methodic_configurator/frontend_tkinter_log_hardware_quality.py b/ardupilot_methodic_configurator/frontend_tkinter_log_hardware_availability.py similarity index 100% rename from ardupilot_methodic_configurator/frontend_tkinter_log_hardware_quality.py rename to ardupilot_methodic_configurator/frontend_tkinter_log_hardware_availability.py diff --git a/ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py b/ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py index 530925f7b..2a457795a 100755 --- a/ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py +++ b/ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py @@ -56,7 +56,7 @@ from ardupilot_methodic_configurator.frontend_tkinter_directory_selection import VehicleDirectorySelectionWidgets from ardupilot_methodic_configurator.frontend_tkinter_fc_banner_window import FlightControllerBannerWindow from ardupilot_methodic_configurator.frontend_tkinter_font import get_safe_font_config -from ardupilot_methodic_configurator.frontend_tkinter_log_quality import LogQualityReportWindow +from ardupilot_methodic_configurator.frontend_tkinter_log_availability import LogAvailabilityReportWindow from ardupilot_methodic_configurator.frontend_tkinter_parameter_compare_and_upload import ParameterFileUploadWindow from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_documentation_frame import DocumentationFrame from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable @@ -284,7 +284,7 @@ def __init__( self._tempcal_imu_progress_window: ProgressWindow | None = None self.file_upload_progress_window: ProgressWindow | None = None self._param_download_progress_window: ProgressWindow | None = None - self._log_quality_report_window: LogQualityReportWindow | None = None + self._log_availability_report_window: LogAvailabilityReportWindow | None = None self._log_report_return_pending: bool = False self.inline_component_editor: ComponentEditorWindow | None = None self._inline_component_name: str | None = None @@ -587,7 +587,7 @@ def _create_parameter_area_widgets(self) -> None: command=self.on_analyse_log_click, ) analyse_log_button.pack(side=tk.LEFT, padx=(8, 8)) - show_tooltip(analyse_log_button, _("Open a .bin flight log and analyse its quality")) + show_tooltip(analyse_log_button, _("Open a .bin flight log and analyse its availability")) # Create Zip file for forum button zip_vehicle_for_forum_button = ttk.Button( @@ -798,7 +798,7 @@ def check_done() -> None: self.ui.show_error(_("Log Analysis Error"), str(e)) return - report_window = LogQualityReportWindow( + report_window = LogAvailabilityReportWindow( self.root, summary, self.parameter_editor.get_vehicle_directory(), @@ -807,20 +807,20 @@ def check_done() -> None: navigate_callback=self._navigate_to_config_step, report=report, ) - self._log_quality_report_window = report_window + self._log_availability_report_window = report_window - if isinstance(self.root, tk.Tk) and UsagePopupWindow.should_display("log_quality_report"): - display_log_quality_report_usage_popup(report_window.root) + if isinstance(self.root, tk.Tk) and UsagePopupWindow.should_display("log_availability_report"): + display_log_availability_report_usage_popup(report_window.root) thread = threading.Thread(target=run_extraction, daemon=True) thread.start() self.root.after(100, check_done) - def display_log_quality_report_usage_popup(parent: tk.Tk | tk.Toplevel) -> None: + def display_log_availability_report_usage_popup(parent: tk.Tk | tk.Toplevel) -> None: usage_popup_window = BaseWindow(parent) usage_popup_window.root.withdraw() instructions_text = RichText(usage_popup_window.main_frame, height=12, width=80) - instructions_text.insert(tk.END, _("Log Quality Report\n\n"), "title") + instructions_text.insert(tk.END, _("Log availability Report\n\n"), "title") instructions_text.insert( tk.END, _( @@ -836,8 +836,8 @@ def display_log_quality_report_usage_popup(parent: tk.Tk | tk.Toplevel) -> None: UsagePopupWindow.display( cast("tk.Tk", parent), usage_popup_window, - _("Log Quality Report"), - "log_quality_report", + _("Log availability Report"), + "log_availability_report", "520x320", instructions_text, ) @@ -1511,13 +1511,13 @@ def _continue_to_analyse(self) -> None: if not self._log_report_return_pending: return self._log_report_return_pending = False - if self._log_quality_report_window is not None and self.ui.ask_yesno( - _("Continue Log Quality Review"), - _("Parameters uploaded. Return to the log quality report to continue?"), + if self._log_availability_report_window is not None and self.ui.ask_yesno( + _("Continue Log availability Review"), + _("Parameters uploaded. Return to the log availability report to continue?"), ): - self._log_quality_report_window.root.deiconify() - self._log_quality_report_window.root.lift() - self._log_quality_report_window.root.focus_force() + self._log_availability_report_window.root.deiconify() + self._log_availability_report_window.root.lift() + self._log_availability_report_window.root.focus_force() # This function can recurse multiple times if there is an upload error diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_arm.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_arm.py similarity index 59% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_arm.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_arm.py index 79651514b..ed2306cb2 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_arm.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_arm.py @@ -1,5 +1,5 @@ """ -Data model for ARM (arming status change) quality check. +Data model for ARM (arming status change) availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,34 +9,36 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState _NAME = "ARM" -class ArmLogQualityModel(BaseLogQualityModel): +class ArmLogAvailabilityModel(BaseLogAvailabilityModel): """ Checks presence of arming/disarming event data. ARM is an unconditional logging. Absence is treated as an anomaly. """ - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("ARM") if records is None or len(records) == 0: reason = _("ARM messages not found, that is unexpected since arm/disarm events are always logged") - issues = [QualityIssue(_("No ARM messages found"))] - return LogQualityResult(available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=_NAME) + issues = [AvailabilityIssue(_("No ARM messages found"))] + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=_NAME + ) issues = self.check_arm_fields() return self.build_result(issues, _NAME) - def check_arm_fields(self) -> list[QualityIssue]: + def check_arm_fields(self) -> list[AvailabilityIssue]: """Check that ArmState/ArmChecks/Forced/Method fields are present and have readable data.""" return self.check_fields_present("ARM", ("ArmState", "ArmChecks", "Forced", "Method")) diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_base.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_base.py similarity index 83% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_base.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_base.py index 10054475c..1edde9e9f 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_base.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_base.py @@ -1,7 +1,7 @@ """ -Base Quality model for all base classes and combined results. +Base Availability model for all base classes and combined results. -Defines the common result data model and the base class used by all subsystem quality analysis models. +Defines the common result data model and the base class used by all subsystem availability analysis models. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -16,10 +16,10 @@ from ardupilot_methodic_configurator import _ from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import ( - LogQualityResult, - LogQualityState, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import ( + AvailabilityIssue, + LogAvailabilityResult, + LogAvailabilityState, ) from ardupilot_methodic_configurator.log_analysis.data_model_parameter_derivation import ParameterDerivationInputs from ardupilot_methodic_configurator.log_analysis.utils import ( @@ -35,7 +35,7 @@ class BaseLogModel: - """Common log-data services shared by quality and analysis models.""" + """Common log-data services shared by availability and analysis models.""" def __init__( self, @@ -83,20 +83,20 @@ def field_values_or_issue( # pylint: disable=too-many-arguments scaled: bool = True, missing_field_message: str, missing_values_message: str, - ) -> tuple[Any | None, list[QualityIssue]]: + ) -> tuple[Any | None, list[AvailabilityIssue]]: """Return field values or a single issue explaining why values are unavailable.""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] if not self.field_available(message_name, field_name): - issues.append(QualityIssue(missing_field_message)) + issues.append(AvailabilityIssue(missing_field_message)) return None, issues values = self.log_data.get_field(message_name, field_name, scaled=scaled) if len(values) == 0: - issues.append(QualityIssue(missing_values_message)) + issues.append(AvailabilityIssue(missing_values_message)) return None, issues if np.issubdtype(values.dtype, np.number) and not np.isfinite(values).all(): - issues.append(QualityIssue(_("{field} contains non-finite telemetry values").format(field=field_name))) + issues.append(AvailabilityIssue(_("{field} contains non-finite telemetry values").format(field=field_name))) return None, issues return values, issues @@ -108,7 +108,7 @@ def diagnose_bitmask_absence( fallback_name: str, *, not_logged_hint: str, - ) -> tuple[str, list[QualityIssue], bool]: + ) -> tuple[str, list[AvailabilityIssue], bool]: """ Diagnose absence of a message via LOG_BITMASK. @@ -124,7 +124,7 @@ def diagnose_bitmask_absence( reason = _("{message} logging is disabled in LOG_BITMASK").format(message=fallback_name) suggested_value = float(int(bitmask) | (1 << log_bit)) issues = [ - QualityIssue( + AvailabilityIssue( _("Enable {message} logging (LOG_BITMASK bit)").format(message=fallback_name), step, param_name="LOG_BITMASK", @@ -136,7 +136,7 @@ def diagnose_bitmask_absence( reason = _("{message} telemetry not logged but logging enabled; {hint}").format( message=fallback_name, hint=not_logged_hint ) - issues = [QualityIssue(_("No {message} messages found").format(message=message_name), step)] + issues = [AvailabilityIssue(_("No {message} messages found").format(message=message_name), step)] return reason, issues, False def check_fields_present( @@ -145,9 +145,9 @@ def check_fields_present( field_names: tuple[str, ...], *, scaled: bool = True, - ) -> list[QualityIssue]: + ) -> list[AvailabilityIssue]: """Check that each field in field_names exists on message_name and has readable data.""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for field_name in field_names: _values, field_issues = self.field_values_or_issue( message_name, @@ -164,21 +164,21 @@ def check_fields_present( return issues -class BaseLogQualityModel(BaseLogModel): - """Base class for subsystem quality models.""" +class BaseLogAvailabilityModel(BaseLogModel): + """Base class for subsystem availability models.""" - def check(self) -> LogQualityResult: - """Run the model-specific quality analysis and return a result.""" + def check(self) -> LogAvailabilityResult: + """Run the model-specific availability analysis and return a result.""" msg = f"{self.__class__.__name__} must implement check()" raise NotImplementedError(msg) - def build_result(self, issues: list[QualityIssue], name: str, related_step: str = "") -> LogQualityResult: - return LogQualityResult( + def build_result(self, issues: list[AvailabilityIssue], name: str, related_step: str = "") -> LogAvailabilityResult: + return LogAvailabilityResult( available=True, - state=LogQualityState.INFO if not issues else LogQualityState.WARNING, + state=LogAvailabilityState.INFO if not issues else LogAvailabilityState.WARNING, reason=_("{name} data present and good for analysis").format(name=name) if not issues - else _("{name} data has quality issues").format(name=name), + else _("{name} data has availability issues").format(name=name), issues=issues, name=name, related_step=related_step, diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_battery.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_battery.py similarity index 91% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_battery.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_battery.py index 2c160c253..6e70dac8b 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_battery.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_battery.py @@ -1,5 +1,5 @@ """ -Data model for battery quality check and battery analysis. +Data model for battery availability check and battery analysis. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -10,14 +10,14 @@ from ardupilot_methodic_configurator import _ from ardupilot_methodic_configurator.data_model_par_dict import is_within_tolerance -from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, BaseLogAnalysisModel, - BaseLogQualityModel, - LogQualityResult, - QualityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState from ardupilot_methodic_configurator.log_analysis.utils import find_log_bit_in_apm_file @@ -31,15 +31,15 @@ def _configured_battery_instances(parameters: dict[str, float]) -> list[int]: return [instance for instance in range(1, 10) if _battery_parameter(instance, "MONITOR") in parameters] -class BatteryLogQualityModel(BaseLogQualityModel): - """Checks battery telemetry and configuration quality.""" +class BatteryLogAvailabilityModel(BaseLogAvailabilityModel): + """Checks battery telemetry and configuration availability.""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("BAT") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_voltage, self.check_curr_total, self.check_current): issues += check() issues += self.check_parameters() @@ -47,7 +47,7 @@ def check(self) -> LogQualityResult: step, name = self.resolve_message_step("BAT", "Battery") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: step, name = self.resolve_message_step("BAT", "Battery") reason, issues, bitmask_disabled = self.diagnose_bitmask_absence( "BAT", "Battery Monitor", "Battery", not_logged_hint=_("check the battery monitor physical connection") @@ -60,25 +60,27 @@ def _diagnose_absence(self) -> LogQualityResult: if instances == [1] and not enabled_instances: reason = _("Battery logging enabled but BATT_MONITOR is 0 (monitor disabled)") issues = [ - QualityIssue(_("Set BATT_MONITOR to enable the battery monitor"), self.step_for_parameter("BATT_MONITOR")) + AvailabilityIssue( + _("Set BATT_MONITOR to enable the battery monitor"), self.step_for_parameter("BATT_MONITOR") + ) ] elif instances and not enabled_instances: reason = _("Battery logging enabled but all configured battery monitors are disabled") issues = [ - QualityIssue( + AvailabilityIssue( _("Enable a configured battery monitor"), self.step_for_parameter(_battery_parameter(instances[0], "MONITOR")), ) ] else: reason = _("Battery logging enabled but no data, monitor may not be configured properly") - issues = [QualityIssue(_("No BAT messages found"), step)] + issues = [AvailabilityIssue(_("No BAT messages found"), step)] - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_voltage(self) -> list[QualityIssue]: + def check_voltage(self) -> list[AvailabilityIssue]: """Voltage presence check.""" volts, issues = self.field_values_or_issue( "BAT", @@ -90,11 +92,11 @@ def check_voltage(self) -> list[QualityIssue]: return issues if volts.max() == 0: - issues.append(QualityIssue(_("Voltage is zero throughout, sensor may not be reading"))) + issues.append(AvailabilityIssue(_("Voltage is zero throughout, sensor may not be reading"))) return issues - def check_current(self) -> list[QualityIssue]: + def check_current(self) -> list[AvailabilityIssue]: _current, issues = self.field_values_or_issue( "BAT", "Curr", @@ -103,7 +105,7 @@ def check_current(self) -> list[QualityIssue]: ) return issues - def check_curr_total(self) -> list[QualityIssue]: + def check_curr_total(self) -> list[AvailabilityIssue]: _cur_tot, issues = self.field_values_or_issue( "BAT", "CurrTot", @@ -112,9 +114,9 @@ def check_curr_total(self) -> list[QualityIssue]: ) return issues - def check_parameters(self) -> list[QualityIssue]: + def check_parameters(self) -> list[AvailabilityIssue]: """Check failsafe thresholds for every enabled battery monitor instance.""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for instance in _configured_battery_instances(self.parameters): monitor_param = _battery_parameter(instance, "MONITOR") if self.parameters[monitor_param] == 0: @@ -124,7 +126,7 @@ def check_parameters(self) -> list[QualityIssue]: low_volt_param = _battery_parameter(instance, "LOW_VOLT") if self.parameters.get(low_volt_param) == 0: issues.append( - QualityIssue( + AvailabilityIssue( _("{prefix}low-voltage failsafe threshold disabled").format(prefix=prefix), self.step_for_parameter(low_volt_param), ) @@ -132,7 +134,7 @@ def check_parameters(self) -> list[QualityIssue]: crt_volt_param = _battery_parameter(instance, "CRT_VOLT") if self.parameters.get(crt_volt_param) == 0: issues.append( - QualityIssue( + AvailabilityIssue( _("{prefix}critical-voltage failsafe threshold disabled").format(prefix=prefix), self.step_for_parameter(crt_volt_param), ) @@ -149,7 +151,7 @@ class BatteryLogAnalysis(BaseLogAnalysisModel): """ Battery analysis on the data from the log. - Runs after battery quality model passes with the required data. + Runs after battery availability model passes with the required data. """ def analyse(self) -> LogAnalysisResult: diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_err.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_err.py similarity index 59% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_err.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_err.py index bc25ff350..bb5d28636 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_err.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_err.py @@ -1,5 +1,5 @@ """ -Data model for ERR message quality check. +Data model for ERR message availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,38 +9,38 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState -class ErrLogQualityModel(BaseLogQualityModel): +class ErrLogAvailabilityModel(BaseLogAvailabilityModel): """ Checks presence and readability of subsystem error/recovery events. - An empty ERR message is NOT a data-quality problem, it means no + An empty ERR message is NOT a data-availability problem, it means no errors occurred during the flight, which is the good outcome. """ - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("ERR") if records is None or len(records) == 0: - return LogQualityResult( + return LogAvailabilityResult( available=True, - state=LogQualityState.INFO, + state=LogAvailabilityState.INFO, reason=_("No errors logged during this flight"), issues=[], name="ERR", ) - issues: list[QualityIssue] = self.check_err_fields() + issues: list[AvailabilityIssue] = self.check_err_fields() return self.build_result(issues, "ERR") - def check_err_fields(self) -> list[QualityIssue]: + def check_err_fields(self) -> list[AvailabilityIssue]: """Check that Subsys/ECode fields are present and have readable data.""" return self.check_fields_present("ERR", ("Subsys", "ECode")) diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_esc.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_esc.py similarity index 93% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_esc.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_esc.py index c0d2bf435..ced3dac94 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_esc.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_esc.py @@ -1,5 +1,5 @@ """ -Data model for ESC quality check. +Data model for ESC availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -12,14 +12,14 @@ from ardupilot_methodic_configurator import _ from ardupilot_methodic_configurator.data_model_par_dict import is_within_tolerance -from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, BaseLogAnalysisModel, - BaseLogQualityModel, - LogQualityResult, - QualityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview_param_metadata import enum_value_name from ardupilot_methodic_configurator.log_analysis.utils import find_matching_param_values @@ -29,22 +29,22 @@ _DSHOT_OUTPUT_RATE_WARN_THRESHOLD = 1000.0 # Amilcar's stated threshold, Hz -class EscLogQualityModel(BaseLogQualityModel): - """Checks ESC telemetry and configuration quality.""" +class EscLogAvailabilityModel(BaseLogAvailabilityModel): + """Checks ESC telemetry and configuration availability.""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("ESC") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_rpm, self.check_current, self.check_error_rate): issues += check() _, name = self.resolve_message_step("ESC", "ESC") return self.build_result(issues, name) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """Diagnose why ESC data is absent.""" step, name = self.resolve_message_step("ESC", "ESC") @@ -55,7 +55,7 @@ def _diagnose_absence(self) -> LogQualityResult: if pwm_type is not None and str(int(pwm_type)) not in dshot_values: reason = _("ESC telemetry not logged") issues = [ - QualityIssue( + AvailabilityIssue( _("Set MOT_PWM_TYPE to a DShot variant for ESC telemetry support"), self.step_for_parameter("MOT_PWM_TYPE"), ) @@ -63,18 +63,20 @@ def _diagnose_absence(self) -> LogQualityResult: elif scr_enabled == 0: reason = _("ESC telemetry not logged, scripting is disabled") issues = [ - QualityIssue( + AvailabilityIssue( _("Enable SCR_ENABLE if using scripted ESC telemetry"), self.step_for_parameter("SCR_ENABLE"), ) ] else: reason = _("ESC telemetry not logged, check ESC hardware supports telemetry and is wired correctly") - issues = [QualityIssue(_("No ESC messages found"), step)] + issues = [AvailabilityIssue(_("No ESC messages found"), step)] - return LogQualityResult(available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name) + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name + ) - def check_rpm(self) -> list[QualityIssue]: + def check_rpm(self) -> list[AvailabilityIssue]: """Validate logged ESC RPM values.""" _rpm, issues = self.field_values_or_issue( "ESC", @@ -84,7 +86,7 @@ def check_rpm(self) -> list[QualityIssue]: ) return issues - def check_current(self) -> list[QualityIssue]: + def check_current(self) -> list[AvailabilityIssue]: """Validate logged ESC current values.""" _current, issues = self.field_values_or_issue( "ESC", @@ -94,7 +96,7 @@ def check_current(self) -> list[QualityIssue]: ) return issues - def check_error_rate(self) -> list[QualityIssue]: + def check_error_rate(self) -> list[AvailabilityIssue]: """Validate ESC error rate.""" err, issues = self.field_values_or_issue( "ESC", @@ -104,7 +106,7 @@ def check_error_rate(self) -> list[QualityIssue]: ) if err is not None and err.max() > 0: step, _name = self.resolve_message_step("ESC", "ESC") - issues.append(QualityIssue(_("ESC error rate detected on at least one ESC instance"), step)) + issues.append(AvailabilityIssue(_("ESC error rate detected on at least one ESC instance"), step)) return issues @@ -112,7 +114,7 @@ class EscLogAnalysis(BaseLogAnalysisModel): """ ESC analysis on the data from the log. - Runs after ESC quality model passes with the required data. + Runs after ESC availability model passes with the required data. """ def analyse(self) -> LogAnalysisResult: diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_fft.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_fft.py similarity index 68% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_fft.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_fft.py index 95f0429a4..fd7e2a270 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_fft.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_fft.py @@ -1,5 +1,5 @@ """ -Data model for FFT / raw IMU batch logging quality check (ISBH and ISBD). +Data model for FFT / raw IMU batch logging availability check (ISBH and ISBD). SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,30 +9,30 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState -class FftLogQualityModel(BaseLogQualityModel): +class FftLogAvailabilityModel(BaseLogAvailabilityModel): """Checks presence of raw IMU batch logging data (ISBH and ISBD samples).""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: header_records = self.log_data.get_message_columns("ISBH") if header_records is None or len(header_records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_header_fields, self.check_batch_data_present): issues += check() _, name = self.resolve_message_step("ISBH", "FFT") return self.build_result(issues, name) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """Diagnose why ISBH/ISBD data is absent using INS_LOG_BAT_MASK.""" step, name = self.resolve_message_step("ISBH", "FFT") @@ -41,7 +41,7 @@ def _diagnose_absence(self) -> LogQualityResult: if bat_mask is not None and int(bat_mask) == 0: reason = _("Raw IMU batch logging is disabled (INS_LOG_BAT_MASK is 0)") issues = [ - QualityIssue( + AvailabilityIssue( _("Set INS_LOG_BAT_MASK to enable raw IMU batch logging for FFT analysis"), self.step_for_parameter("INS_LOG_BAT_MASK"), param_name="INS_LOG_BAT_MASK", @@ -51,24 +51,29 @@ def _diagnose_absence(self) -> LogQualityResult: result_step = "" else: reason = _("Raw IMU batch logging enabled but no data, check firmware build supports batch logging") - issues = [QualityIssue(_("No ISBH messages found"), step)] + issues = [AvailabilityIssue(_("No ISBH messages found"), step)] result_step = step - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=result_step + return LogAvailabilityResult( + available=False, + state=LogAvailabilityState.WARNING, + reason=reason, + issues=issues, + name=name, + related_step=result_step, ) - def check_header_fields(self) -> list[QualityIssue]: + def check_header_fields(self) -> list[AvailabilityIssue]: """Check that ISBH's key fields are present and have readable data.""" return self.check_fields_present("ISBH", ("type", "instance", "smp_cnt", "smp_rate")) - def check_batch_data_present(self) -> list[QualityIssue]: + def check_batch_data_present(self) -> list[AvailabilityIssue]: """Check that ISBD and ISBH header.""" records = self.log_data.get_message_columns("ISBD") if records is None or len(records) == 0: - return [QualityIssue(_("ISBH header present but ISBD batch samples are missing"))] + return [AvailabilityIssue(_("ISBH header present but ISBD batch samples are missing"))] - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for axis_field in ("x", "y", "z"): _values, field_issues = self.field_values_or_issue( "ISBD", diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_gnss.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_gnss.py similarity index 60% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_gnss.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_gnss.py index a6c4d2c61..63bd855fd 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_gnss.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_gnss.py @@ -1,5 +1,5 @@ """ -Data model for GPS/GNSS quality check. +Data model for GPS/GNSS availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,23 +9,23 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState -class GPSLogQualityModel(BaseLogQualityModel): - """Checks GPS/GNSS telemetry and configuration quality.""" +class GPSLogAvailabilityModel(BaseLogAvailabilityModel): + """Checks GPS/GNSS telemetry and configuration availability.""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("GPS") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_status,): issues += check() issues += self.check_parameters() @@ -33,17 +33,17 @@ def check(self) -> LogQualityResult: step, name = self.resolve_message_step("GPS", "GPS") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """Diagnose why GPS data is absent using LOG_BITMASK.""" step, name = self.resolve_message_step("GPS", "GPS") reason, issues, _bitmask_disabled = self.diagnose_bitmask_absence( "GPS", "GPS", "GPS", not_logged_hint=_("check the GPS physical connection") ) - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_status(self) -> list[QualityIssue]: + def check_status(self) -> list[AvailabilityIssue]: """Validate GPS fix status.""" status, issues = self.field_values_or_issue( "GPS", @@ -52,13 +52,13 @@ def check_status(self) -> list[QualityIssue]: missing_values_message=_("GPS fix status missing from GPS records"), ) if status is not None and max(status) < 3: - issues.append(QualityIssue(_("GPS never achieved a 3D fix"))) + issues.append(AvailabilityIssue(_("GPS never achieved a 3D fix"))) return issues - def check_parameters(self) -> list[QualityIssue]: + def check_parameters(self) -> list[AvailabilityIssue]: """Validate GPS-related parameter configuration.""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] gps_type = self.parameters.get("GPS_TYPE", self.parameters.get("GPS1_TYPE")) if gps_type == 0: - issues.append(QualityIssue(_("GPS type not configured (set to None)"), self.step_for_parameter("GPS_TYPE"))) + issues.append(AvailabilityIssue(_("GPS type not configured (set to None)"), self.step_for_parameter("GPS_TYPE"))) return issues diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_imu.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_imu.py similarity index 87% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_imu.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_imu.py index 7a2f2746b..81bd913bb 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_imu.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_imu.py @@ -1,5 +1,5 @@ """ -Data model for IMU quality check. +Data model for IMU availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,14 +9,14 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, BaseLogAnalysisModel, - BaseLogQualityModel, - LogQualityResult, - QualityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview_instances import ( has_nonzero_parameter, imu_device_id_param, @@ -34,32 +34,32 @@ _INVALID_CALTEMP = -300.0 -class ImuLogQualityModel(BaseLogQualityModel): - """Checks IMU telemetry quality (error counts, sensor health, raw signal presence).""" +class ImuLogAvailabilityModel(BaseLogAvailabilityModel): + """Checks IMU telemetry availability (error counts, sensor health, raw signal presence).""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("IMU") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_gyro_error, self.check_accel_error, self.check_health, self.check_signal_present): issues += check() step, name = self.resolve_message_step("IMU", "IMU") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """Diagnose why IMU data is absent using LOG_BITMASK.""" step, name = self.resolve_message_step("IMU", "IMU") reason, issues, _bitmask_disabled = self.diagnose_bitmask_absence( "IMU", "IMU", "IMU", not_logged_hint=_("check firmware build supports IMU logging") ) - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_gyro_error(self) -> list[QualityIssue]: + def check_gyro_error(self) -> list[AvailabilityIssue]: """Validate gyroscope error count across all IMU instances.""" eg, issues = self.field_values_or_issue( "IMU", @@ -68,10 +68,10 @@ def check_gyro_error(self) -> list[QualityIssue]: missing_values_message=_("Gyroscope error count missing from IMU records"), ) if eg is not None and eg.max() > 0: - issues.append(QualityIssue(_("Gyroscope error count detected on at least one IMU instance"))) + issues.append(AvailabilityIssue(_("Gyroscope error count detected on at least one IMU instance"))) return issues - def check_accel_error(self) -> list[QualityIssue]: + def check_accel_error(self) -> list[AvailabilityIssue]: """Validate accelerometer error count across all IMU instances.""" ea, issues = self.field_values_or_issue( "IMU", @@ -80,12 +80,12 @@ def check_accel_error(self) -> list[QualityIssue]: missing_values_message=_("Accelerometer error count missing from IMU records"), ) if ea is not None and ea.max() > 0: - issues.append(QualityIssue(_("Accelerometer error count detected on at least one IMU instance"))) + issues.append(AvailabilityIssue(_("Accelerometer error count detected on at least one IMU instance"))) return issues - def check_health(self) -> list[QualityIssue]: + def check_health(self) -> list[AvailabilityIssue]: """Validate gyroscope/accelerometer health flags across all IMU instances.""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] gh, gh_issues = self.field_values_or_issue( "IMU", @@ -95,7 +95,7 @@ def check_health(self) -> list[QualityIssue]: ) issues += gh_issues if gh is not None and (gh == 0).any(): - issues.append(QualityIssue(_("Gyroscope reported unhealthy at some point during the flight"))) + issues.append(AvailabilityIssue(_("Gyroscope reported unhealthy at some point during the flight"))) ah, ah_issues = self.field_values_or_issue( "IMU", @@ -105,13 +105,13 @@ def check_health(self) -> list[QualityIssue]: ) issues += ah_issues if ah is not None and (ah == 0).any(): - issues.append(QualityIssue(_("Accelerometer reported unhealthy at some point during the flight"))) + issues.append(AvailabilityIssue(_("Accelerometer reported unhealthy at some point during the flight"))) return issues - def check_signal_present(self) -> list[QualityIssue]: + def check_signal_present(self) -> list[AvailabilityIssue]: """Validate that raw gyro/accel signals are not flat-zero throughout (sensor not reading).""" - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for axis_field in ("GyrX", "GyrY", "GyrZ"): values, field_issues = self.field_values_or_issue( @@ -123,7 +123,7 @@ def check_signal_present(self) -> list[QualityIssue]: issues += field_issues if values is not None and values.max() == 0 and values.min() == 0: issues.append( - QualityIssue(_("{field} is zero throughout, gyroscope may not be reading").format(field=axis_field)) + AvailabilityIssue(_("{field} is zero throughout, gyroscope may not be reading").format(field=axis_field)) ) for axis_field in ("AccX", "AccY", "AccZ"): @@ -136,7 +136,9 @@ def check_signal_present(self) -> list[QualityIssue]: issues += field_issues if values is not None and values.max() == 0 and values.min() == 0: issues.append( - QualityIssue(_("{field} is zero throughout, accelerometer may not be reading").format(field=axis_field)) + AvailabilityIssue( + _("{field} is zero throughout, accelerometer may not be reading").format(field=axis_field) + ) ) return issues @@ -146,7 +148,7 @@ class ImuLogAnalysis(BaseLogAnalysisModel): """ IMU analysis on the data from the log. - Runs after IMU quality model passes with the required data for analysis. + Runs after IMU availability model passes with the required data for analysis. """ def analyse(self) -> LogAnalysisResult: diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_mode.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_mode.py similarity index 62% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_mode.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_mode.py index 1c5485e2b..9dd28ae01 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_mode.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_mode.py @@ -1,5 +1,5 @@ """ -Data model for MODE message quality check. +Data model for MODE message availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,30 +9,30 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState -class ModeLogQualityModel(BaseLogQualityModel): +class ModeLogAvailabilityModel(BaseLogAvailabilityModel): """Checks presence of flight mode change data.""" - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("MODE") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_mode_fields,): issues += check() step, name = self.resolve_message_step("MODE", "MODE") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """ Diagnose why MODE data is absent. @@ -41,11 +41,11 @@ def _diagnose_absence(self) -> LogQualityResult: """ step, name = self.resolve_message_step("MODE", "MODE") reason = _("MODE messages not found, this is unexpected since mode changes are always logged") - issues = [QualityIssue(_("No MODE messages found"), step)] - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + issues = [AvailabilityIssue(_("No MODE messages found"), step)] + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_mode_fields(self) -> list[QualityIssue]: + def check_mode_fields(self) -> list[AvailabilityIssue]: """Check that Mode/ModeNum/Rsn fields are present and have readable data.""" return self.check_fields_present("MODE", ("Mode", "ModeNum", "Rsn")) diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_pm.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_pm.py similarity index 70% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_pm.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_pm.py index a853d48ed..e4f86c972 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_pm.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_pm.py @@ -1,5 +1,5 @@ """ -Data model for PM quality check. +Data model for PM availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -9,22 +9,22 @@ """ from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogQualityModel, - LogQualityResult, - QualityIssue, +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState -class PmLogQualityModel(BaseLogQualityModel): +class PmLogAvailabilityModel(BaseLogAvailabilityModel): """ Checks presence and readability of system performance (PM) data. Gated by LOG_BITMASK bit 3, """ - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("PM") if records is None or len(records) == 0: return self._diagnose_absence() @@ -33,7 +33,7 @@ def check(self) -> LogQualityResult: step, name = self.resolve_message_step("PM", "PM") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """Diagnose why PM data is absent using LOG_BITMASK.""" step, name = self.resolve_message_step("PM", "PM") reason, issues, _bitmask_disabled = self.diagnose_bitmask_absence( @@ -42,11 +42,11 @@ def _diagnose_absence(self) -> LogQualityResult: "PM", not_logged_hint=_("check firmware build supports performance monitor logging"), ) - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_pm_fields(self) -> list[QualityIssue]: + def check_pm_fields(self) -> list[AvailabilityIssue]: """Validate whichever known PM fields are provided by this firmware's schema.""" optional_fields = ("Load", "Mem", "NLon", "InE", "ErC") available_fields = tuple(field_name for field_name in optional_fields if self.field_available("PM", field_name)) diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_quality_vibe.py b/ardupilot_methodic_configurator/log_analysis/data_model_availability_vibe.py similarity index 89% rename from ardupilot_methodic_configurator/log_analysis/data_model_quality_vibe.py rename to ardupilot_methodic_configurator/log_analysis/data_model_availability_vibe.py index f66681135..a6a052375 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_quality_vibe.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_availability_vibe.py @@ -1,5 +1,5 @@ """ -Data model for VIBE quality check. +Data model for VIBE availability check. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -11,14 +11,14 @@ from typing import Any from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + AvailabilityIssue, BaseLogAnalysisModel, - BaseLogQualityModel, - LogQualityResult, - QualityIssue, + BaseLogAvailabilityModel, + LogAvailabilityResult, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState _VIBE_WARNING_THRESHOLD = 30.0 _VIBE_SEVERE_THRESHOLD = 60.0 @@ -26,7 +26,7 @@ _VIBE_AXES = ("VibeX", "VibeY", "VibeZ") -class VibeLogQualityModel(BaseLogQualityModel): +class VibeLogAvailabilityModel(BaseLogAvailabilityModel): """ Checks VIBE data presence and availability for analysis. @@ -35,19 +35,19 @@ class VibeLogQualityModel(BaseLogQualityModel): guidance, or clip-count nuance) is deferred to a future analysis layer. """ - def check(self) -> LogQualityResult: + def check(self) -> LogAvailabilityResult: records = self.log_data.get_message_columns("VIBE") if records is None or len(records) == 0: return self._diagnose_absence() - issues: list[QualityIssue] = [] + issues: list[AvailabilityIssue] = [] for check in (self.check_vibe_levels, self.check_clipping): issues += check() step, name = self.resolve_message_step("VIBE", "VIBE") return self.build_result(issues, name, related_step=step) - def _diagnose_absence(self) -> LogQualityResult: + def _diagnose_absence(self) -> LogAvailabilityResult: """ Diagnose why VIBE data is absent. @@ -62,15 +62,15 @@ def _diagnose_absence(self) -> LogQualityResult: "VIBE", not_logged_hint=_("check that IMU data is being logged, since VIBE is derived from it"), ) - return LogQualityResult( - available=False, state=LogQualityState.WARNING, reason=reason, issues=issues, name=name, related_step=step + return LogAvailabilityResult( + available=False, state=LogAvailabilityState.WARNING, reason=reason, issues=issues, name=name, related_step=step ) - def check_vibe_levels(self) -> list[QualityIssue]: + def check_vibe_levels(self) -> list[AvailabilityIssue]: """Check that VibeX/Y/Z fields are present and have readable data.""" return self.check_fields_present("VIBE", ("VibeX", "VibeY", "VibeZ")) - def check_clipping(self) -> list[QualityIssue]: + def check_clipping(self) -> list[AvailabilityIssue]: """Check that the Clip field is present and has readable data.""" _clip, issues = self.field_values_or_issue( "VIBE", @@ -85,7 +85,7 @@ class VibeLogAnalysis(BaseLogAnalysisModel): """ VIBE analysis on the data from the log. - Runs after VIBE quality model passes with the required data. + Runs after VIBE availability model passes with the required data. """ def analyse(self) -> LogAnalysisResult: diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py b/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py index 729e2808f..53e4668cf 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_log_analysis.py @@ -14,36 +14,39 @@ from dataclasses import dataclass, field from ardupilot_methodic_configurator import _ +from ardupilot_methodic_configurator.log_analysis.data_model_availability_arm import ArmLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + BaseLogAnalysisModel, + BaseLogAvailabilityModel, +) +from ardupilot_methodic_configurator.log_analysis.data_model_availability_battery import ( + BatteryLogAnalysis, + BatteryLogAvailabilityModel, +) +from ardupilot_methodic_configurator.log_analysis.data_model_availability_err import ErrLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_esc import EscLogAnalysis, EscLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_fft import FftLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_gnss import GPSLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_imu import ImuLogAnalysis, ImuLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_mode import ModeLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_pm import PmLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_vibe import VibeLogAnalysis, VibeLogAvailabilityModel from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysis, LogAnalysisResult -from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import ( - LogQualityResult, - LogQualityState, +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import ( + AvailabilityIssue, + LogAvailabilityResult, + LogAvailabilityState, MessageValidation, PMStatus, - QualityIssue, StepValidationResult, ) -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality_check import ( +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability_check import ( check_cpu_performance_message, get_pm_status, validate_configuration_steps_data, ) -from ardupilot_methodic_configurator.log_analysis.data_model_quality_arm import ArmLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogAnalysisModel, - BaseLogQualityModel, -) -from ardupilot_methodic_configurator.log_analysis.data_model_quality_battery import BatteryLogAnalysis, BatteryLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_err import ErrLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_esc import EscLogAnalysis, EscLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_fft import FftLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_gnss import GPSLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_imu import ImuLogAnalysis, ImuLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_mode import ModeLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_pm import PmLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_vibe import VibeLogAnalysis, VibeLogQualityModel +from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview import HardwareReport from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview_report import extract_hardware_report @@ -53,25 +56,25 @@ class LogAnalysisModelSpec: """Registration for one log subsystem and its optional detailed analysis.""" key: str - quality_model: type[BaseLogQualityModel] + availability_model: type[BaseLogAvailabilityModel] analysis_model: type[BaseLogAnalysisModel] | None = None component_keys: tuple[str, ...] = () LOG_ANALYSIS_SUBSYSTEMS: tuple[LogAnalysisModelSpec, ...] = ( - LogAnalysisModelSpec("battery", BatteryLogQualityModel, BatteryLogAnalysis, ("Battery", "Battery Monitor")), - LogAnalysisModelSpec("gps", GPSLogQualityModel, component_keys=("GNSS Receiver",)), - LogAnalysisModelSpec("esc", EscLogQualityModel, EscLogAnalysis, ("ESC", "Motors")), - LogAnalysisModelSpec("imu", ImuLogQualityModel, ImuLogAnalysis, ("Flight Controller",)), - LogAnalysisModelSpec("vibe", VibeLogQualityModel, VibeLogAnalysis, ("Flight Controller",)), - LogAnalysisModelSpec("fft", FftLogQualityModel), - LogAnalysisModelSpec("err", ErrLogQualityModel), - LogAnalysisModelSpec("pm", PmLogQualityModel), - LogAnalysisModelSpec("arm", ArmLogQualityModel), - LogAnalysisModelSpec("mode", ModeLogQualityModel), + LogAnalysisModelSpec("battery", BatteryLogAvailabilityModel, BatteryLogAnalysis, ("Battery", "Battery Monitor")), + LogAnalysisModelSpec("gps", GPSLogAvailabilityModel, component_keys=("GNSS Receiver",)), + LogAnalysisModelSpec("esc", EscLogAvailabilityModel, EscLogAnalysis, ("ESC", "Motors")), + LogAnalysisModelSpec("imu", ImuLogAvailabilityModel, ImuLogAnalysis, ("Flight Controller",)), + LogAnalysisModelSpec("vibe", VibeLogAvailabilityModel, VibeLogAnalysis, ("Flight Controller",)), + LogAnalysisModelSpec("fft", FftLogAvailabilityModel), + LogAnalysisModelSpec("err", ErrLogAvailabilityModel), + LogAnalysisModelSpec("pm", PmLogAvailabilityModel), + LogAnalysisModelSpec("arm", ArmLogAvailabilityModel), + LogAnalysisModelSpec("mode", ModeLogAvailabilityModel), ) -ResolvedModel = tuple[type[BaseLogQualityModel], type[BaseLogAnalysisModel] | None, str] +ResolvedModel = tuple[type[BaseLogAvailabilityModel], type[BaseLogAnalysisModel] | None, str] def parse_firmware_version(version: object) -> tuple[int, int, int] | None: @@ -90,36 +93,36 @@ def parse_firmware_version(version: object) -> tuple[int, int, int] | None: return None -def _pm_validation_as_quality_result(validation: MessageValidation | None) -> LogQualityResult | None: - """Convert PM validation into the common quality-result shape used by the frontend.""" +def _pm_validation_as_availability_result(validation: MessageValidation | None) -> LogAvailabilityResult | None: + """Convert PM validation into the common availability-result shape used by the frontend.""" if validation is None: return None - issues = [QualityIssue(issue) for issue in validation.issues] - return LogQualityResult( + issues = [AvailabilityIssue(issue) for issue in validation.issues] + return LogAvailabilityResult( available=validation.valid, - state=LogQualityState.INFO if validation.valid else LogQualityState.WARNING, + state=LogAvailabilityState.INFO if validation.valid else LogAvailabilityState.WARNING, reason=_("Performance monitor data present and good for analysis") if validation.valid - else _("Performance monitor data has quality issues"), + else _("Performance monitor data has availability issues"), issues=issues, name=_("System Performance"), ) def _resolve_models( - quality_and_analysis_models: list[tuple[type[BaseLogQualityModel], type[BaseLogAnalysisModel] | None]] | None, + availability_and_analysis_models: list[tuple[type[BaseLogAvailabilityModel], type[BaseLogAnalysisModel] | None]] | None, ) -> tuple[list[ResolvedModel], dict[str, tuple[str, ...]]]: """Resolve the default registry or caller-provided model pairs.""" - if quality_and_analysis_models is None: + if availability_and_analysis_models is None: return ( - [(spec.quality_model, spec.analysis_model, spec.key) for spec in LOG_ANALYSIS_SUBSYSTEMS], + [(spec.availability_model, spec.analysis_model, spec.key) for spec in LOG_ANALYSIS_SUBSYSTEMS], {spec.key: spec.component_keys for spec in LOG_ANALYSIS_SUBSYSTEMS}, ) return ( [ - (quality_model, analysis_model, f"custom_{index}") - for index, (quality_model, analysis_model) in enumerate(quality_and_analysis_models) + (availability_model, analysis_model, f"custom_{index}") + for index, (availability_model, analysis_model) in enumerate(availability_and_analysis_models) ], {}, ) @@ -127,10 +130,10 @@ def _resolve_models( def _add_related_parameter_values( related_values: dict[str, float], - findings: list[QualityIssue] | list[LogAnalysis], + findings: list[AvailabilityIssue] | list[LogAnalysis], parameters: dict[str, float], ) -> None: - """Add parameters referenced by quality issues or analysis outcomes.""" + """Add parameters referenced by availability issues or analysis outcomes.""" for finding in findings: if finding.param_name is not None and finding.param_name in parameters: related_values[finding.param_name] = parameters[finding.param_name] @@ -141,24 +144,24 @@ def _run_subsystem_models( log_data: LogData, context: LogAnalysisContext, related_parameter_values: dict[str, float], -) -> tuple[list[LogQualityResult], list[LogAnalysisResult], list[str]]: - """Run registered quality and available detailed-analysis models.""" - quality_results: list[LogQualityResult] = [] +) -> tuple[list[LogAvailabilityResult], list[LogAnalysisResult], list[str]]: + """Run registered availability and available detailed-analysis models.""" + availability_results: list[LogAvailabilityResult] = [] analysis_results: list[LogAnalysisResult] = [] analysis_subsystem_keys: list[str] = [] - for quality_model_cls, analysis_model_cls, subsystem_key in resolved_models: - quality_result = quality_model_cls(log_data, context).check() - quality_result.subsystem_key = subsystem_key - quality_results.append(quality_result) - _add_related_parameter_values(related_parameter_values, quality_result.issues, context.parameters) + for availability_model_cls, analysis_model_cls, subsystem_key in resolved_models: + availability_result = availability_model_cls(log_data, context).check() + availability_result.subsystem_key = subsystem_key + availability_results.append(availability_result) + _add_related_parameter_values(related_parameter_values, availability_result.issues, context.parameters) if analysis_model_cls is not None: analysis_subsystem_keys.append(subsystem_key) - if analysis_model_cls is not None and quality_result.available: + if analysis_model_cls is not None and availability_result.available: analysis_result = analysis_model_cls(log_data, context).analyse() analysis_result.subsystem_key = subsystem_key analysis_results.append(analysis_result) _add_related_parameter_values(related_parameter_values, analysis_result.outcomes, context.parameters) - return quality_results, analysis_results, analysis_subsystem_keys + return availability_results, analysis_results, analysis_subsystem_keys def validate_log_matches_vehicle( @@ -197,7 +200,7 @@ class LogSummary: # pylint: disable=too-many-instance-attributes parameter_count: int pm_status: PMStatus | None pm_validation: MessageValidation | None - quality_results: list[LogQualityResult] + availability_results: list[LogAvailabilityResult] analysis_results: list[LogAnalysisResult] step_results: list[StepValidationResult] hardware_report: HardwareReport @@ -213,24 +216,27 @@ def component_keys_for_subsystem(self, subsystem_key: str | None) -> tuple[str, return self.subsystem_component_keys[subsystem_key] return next((spec.component_keys for spec in LOG_ANALYSIS_SUBSYSTEMS if spec.key == subsystem_key), ()) - def paired_quality_and_analysis_results( + def paired_availability_and_analysis_results( self, - ) -> list[tuple[LogQualityResult, LogAnalysisResult | None]]: + ) -> list[tuple[LogAvailabilityResult, LogAnalysisResult | None]]: """Return analysis-enabled subsystem results matched by stable subsystem key.""" - quality_by_key = {result.subsystem_key: result for result in self.quality_results if result.subsystem_key is not None} + availability_by_key = { + result.subsystem_key: result for result in self.availability_results if result.subsystem_key is not None + } analysis_by_key = { result.subsystem_key: result for result in self.analysis_results if result.subsystem_key is not None } registered_keys = self.analysis_subsystem_keys or tuple( spec.key for spec in LOG_ANALYSIS_SUBSYSTEMS if spec.analysis_model is not None ) - return [(quality_by_key[key], analysis_by_key.get(key)) for key in registered_keys if key in quality_by_key] + return [(availability_by_key[key], analysis_by_key.get(key)) for key in registered_keys if key in availability_by_key] def analyze_log( # pylint: disable=too-many-locals log_data: LogData, context: LogAnalysisContext, - quality_and_analysis_models: list[tuple[type[BaseLogQualityModel], type[BaseLogAnalysisModel] | None]] | None = None, + availability_and_analysis_models: list[tuple[type[BaseLogAvailabilityModel], type[BaseLogAnalysisModel] | None]] + | None = None, ) -> LogSummary: """ Run log analysis over already loaded datasource values. @@ -239,7 +245,7 @@ def analyze_log( # pylint: disable=too-many-locals log_data: Parsed log. context: Typed analysis inputs (parameters, configuration steps, optional component metadata and apm.pdef definitions). - quality_and_analysis_models: Optional (quality_model_cls, analysis_model_cls) pairs to run + availability_and_analysis_models: Optional (availability_model_cls, analysis_model_cls) pairs to run instead of the default registry. Pass None if the second element of a pair for subsystems with no analysis model. @@ -250,20 +256,20 @@ def analyze_log( # pylint: disable=too-many-locals parameters = context.parameters pm_status = get_pm_status(log_data) pm_validation = check_cpu_performance_message(log_data) - resolved_models, subsystem_component_keys = _resolve_models(quality_and_analysis_models) + resolved_models, subsystem_component_keys = _resolve_models(availability_and_analysis_models) - quality_results: list[LogQualityResult] = [] - pm_quality_result = _pm_validation_as_quality_result(pm_validation) - if pm_quality_result is not None: - quality_results.append(pm_quality_result) + availability_results: list[LogAvailabilityResult] = [] + pm_availability_result = _pm_validation_as_availability_result(pm_validation) + if pm_availability_result is not None: + availability_results.append(pm_availability_result) related_parameter_values: dict[str, float] = {} - for result in quality_results: + for result in availability_results: _add_related_parameter_values(related_parameter_values, result.issues, parameters) - subsystem_quality_results, analysis_results, analysis_subsystem_keys = _run_subsystem_models( + subsystem_availability_results, analysis_results, analysis_subsystem_keys = _run_subsystem_models( resolved_models, log_data, context, related_parameter_values ) - quality_results.extend(subsystem_quality_results) + availability_results.extend(subsystem_availability_results) step_results = validate_configuration_steps_data(log_data, context.configuration_steps) hardware_report = extract_hardware_report(log_data, parameters, context.apm_doc) @@ -276,7 +282,7 @@ def analyze_log( # pylint: disable=too-many-locals parameter_count=len(parameters), pm_status=pm_status, pm_validation=pm_validation, - quality_results=quality_results, + availability_results=availability_results, step_results=step_results, hardware_report=hardware_report, analysis_results=analysis_results, diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_log_quality.py b/ardupilot_methodic_configurator/log_analysis/data_model_log_availability.py similarity index 77% rename from ardupilot_methodic_configurator/log_analysis/data_model_log_quality.py rename to ardupilot_methodic_configurator/log_analysis/data_model_log_availability.py index e07da4b73..a0880985e 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_log_quality.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_log_availability.py @@ -1,5 +1,5 @@ """ -Structured result types for log quality analysis. +Structured result types for log availability analysis. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -12,15 +12,15 @@ from enum import Enum -class LogQualityState(str, Enum): - """Semantic state for a quality-analysis result.""" +class LogAvailabilityState(str, Enum): + """Semantic state for a availability-analysis result.""" INFO = "info" WARNING = "warning" @dataclass -class QualityIssue: +class AvailabilityIssue: """One detected issue, paired with the configuration step that would fix it.""" message: str @@ -30,13 +30,13 @@ class QualityIssue: @dataclass -class LogQualityResult: - """Result produced by a subsystem quality model (battery, GPS, etc.).""" +class LogAvailabilityResult: + """Result produced by a subsystem availability model (battery, GPS, etc.).""" available: bool - state: LogQualityState + state: LogAvailabilityState reason: str - issues: list[QualityIssue] + issues: list[AvailabilityIssue] name: str related_step: str = "" subsystem_key: str | None = None diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_log_quality_check.py b/ardupilot_methodic_configurator/log_analysis/data_model_log_availability_check.py similarity index 99% rename from ardupilot_methodic_configurator/log_analysis/data_model_log_quality_check.py rename to ardupilot_methodic_configurator/log_analysis/data_model_log_availability_check.py index 5f4de7a3c..856b44370 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_log_quality_check.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_log_availability_check.py @@ -1,5 +1,5 @@ """ -ArduPilot log quality checker. +ArduPilot log availability checker. Validates that the messages and params required by the Methodic Configurator configuration steps are present, also checks if a specific analysis can be performed and the logged records match their FMT schema. @@ -18,7 +18,7 @@ import numpy as np from ardupilot_methodic_configurator import _ -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import ( +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import ( MessageValidation, PMStatus, StepValidationResult, diff --git a/ardupilot_methodic_configurator/log_analysis/data_model_log_report.py b/ardupilot_methodic_configurator/log_analysis/data_model_log_report.py index 03e696e2c..3d2130774 100644 --- a/ardupilot_methodic_configurator/log_analysis/data_model_log_report.py +++ b/ardupilot_methodic_configurator/log_analysis/data_model_log_report.py @@ -1,5 +1,5 @@ """ -Pure presentation helpers for log quality reports. +Pure presentation helpers for log availability reports. These helpers keep report formatting and display decisions out of Tkinter widgets so they can be tested without a GUI. @@ -20,7 +20,7 @@ @dataclass(frozen=True) class ReportStatus: - """Header status for the log quality report.""" + """Header status for the log availability report.""" text: str color: str @@ -46,8 +46,8 @@ def format_duration(sec: float | None) -> str: def build_report_status(summary: LogSummary) -> ReportStatus: - """Return the report header status from quality and step results.""" - issues_count = sum(len(res.issues) for res in summary.quality_results) + """Return the report header status from availability and step results.""" + issues_count = sum(len(res.issues) for res in summary.availability_results) failed_steps = sum(1 for res in summary.step_results if not res.valid) total_problems = issues_count + failed_steps diff --git a/tests/acceptance_only_changed_get_uploaded_usage.py b/tests/acceptance_only_changed_get_uploaded_usage.py index e52320b0f..19e798ab7 100755 --- a/tests/acceptance_only_changed_get_uploaded_usage.py +++ b/tests/acceptance_only_changed_get_uploaded_usage.py @@ -60,7 +60,7 @@ def _build_parameter_editor_window(root: object) -> tuple[ParameterEditorWindow, cast("Any", window).ui = ui cast("Any", window).upload_selected_params = upload_params cast("Any", window).on_skip_click = on_skip - cast("Any", window)._log_quality_report_window = None + cast("Any", window)._log_availability_report_window = None cast("Any", window)._log_report_return_pending = False mocks = { diff --git a/tests/test_backend_filesystem_program_settings.py b/tests/test_backend_filesystem_program_settings.py index 4e4faf392..362800ce5 100755 --- a/tests/test_backend_filesystem_program_settings.py +++ b/tests/test_backend_filesystem_program_settings.py @@ -423,7 +423,7 @@ def test_user_can_load_existing_settings_file(self, mock_user_config, sample_pro expected_result["display_usage_popup"]["workflow_explanation"] = True # Added by default expected_result["display_usage_popup"]["bitmask_parameter_editor"] = True # Added by default expected_result["display_usage_popup"]["only_changed_get_uploaded"] = True # Added by default - expected_result["display_usage_popup"]["log_quality_report"] = True # Added by default + expected_result["display_usage_popup"]["log_availability_report"] = True # Added by default # Update directory_selection with the defaults that would be merged in expected_result["directory_selection"]["new_base_dir"] = os_path.join(mock_user_config["config_dir"], "vehicles") @@ -494,7 +494,7 @@ def test_user_gets_default_popup_settings_when_missing(self, mock_user_config) - assert result["display_usage_popup"]["parameter_editor"] is True assert result["display_usage_popup"]["bitmask_parameter_editor"] is True assert result["display_usage_popup"]["only_changed_get_uploaded"] is True - assert result["display_usage_popup"]["log_quality_report"] is True + assert result["display_usage_popup"]["log_availability_report"] is True def test_user_can_load_settings_from_file_directly(self, mock_user_config) -> None: # pylint: disable=unused-argument """ @@ -558,7 +558,7 @@ def test_user_can_apply_default_settings_to_incomplete_settings(self) -> None: assert "display_usage_popup" in result # Added assert result["display_usage_popup"]["component_editor"] is True # Added assert result["display_usage_popup"]["parameter_editor"] is True # Added - assert result["display_usage_popup"]["log_quality_report"] is True # Added + assert result["display_usage_popup"]["log_availability_report"] is True # Added assert result["auto_open_doc_in_browser"] is True # Added assert result["annotate_docs_into_param_files"] is False # Added @@ -646,12 +646,12 @@ def test_user_can_set_usage_popup_display_preferences(self) -> None: "display_usage_popup": { "component_editor": True, "parameter_editor": True, - "log_quality_report": True, + "log_availability_report": True, } } # Act: Set valid popup preference - ProgramSettings.set_display_usage_popup("log_quality_report", value=False) + ProgramSettings.set_display_usage_popup("log_availability_report", value=False) # Assert: Settings are updated correctly mock_set_settings.assert_called_with( @@ -659,7 +659,7 @@ def test_user_can_set_usage_popup_display_preferences(self) -> None: "display_usage_popup": { "component_editor": True, "parameter_editor": True, - "log_quality_report": False, + "log_availability_report": False, } } ) diff --git a/tests/test_data_model_quality_models.py b/tests/test_data_model_availability_models.py similarity index 83% rename from tests/test_data_model_quality_models.py rename to tests/test_data_model_availability_models.py index ca1e3d5c0..8f9bce1a0 100755 --- a/tests/test_data_model_quality_models.py +++ b/tests/test_data_model_availability_models.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Tests for log-analysis quality domain models. +Tests for log-analysis availability domain models. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -10,13 +10,16 @@ import numpy as np +from ardupilot_methodic_configurator.log_analysis.data_model_availability_battery import ( + BatteryLogAnalysis, + BatteryLogAvailabilityModel, +) +from ardupilot_methodic_configurator.log_analysis.data_model_availability_esc import EscLogAnalysis, EscLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_gnss import GPSLogAvailabilityModel +from ardupilot_methodic_configurator.log_analysis.data_model_availability_pm import PmLogAvailabilityModel from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_battery import BatteryLogAnalysis, BatteryLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_esc import EscLogAnalysis, EscLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_gnss import GPSLogQualityModel -from ardupilot_methodic_configurator.log_analysis.data_model_quality_pm import PmLogQualityModel def _context( @@ -48,7 +51,7 @@ def _context( def test_battery_model_uses_apm_doc_to_diagnose_disabled_logging() -> None: """Missing BAT data should point to LOG_BITMASK when the bit is disabled.""" - result = BatteryLogQualityModel( + result = BatteryLogAvailabilityModel( LogData(), _context( {"LOG_BITMASK": 0.0, "BATT_MONITOR": 4.0}, @@ -57,14 +60,14 @@ def test_battery_model_uses_apm_doc_to_diagnose_disabled_logging() -> None: ).check() assert result.available is False - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert result.issues[0].config_step == "05_battery.param" assert "LOG_BITMASK" in result.issues[0].message def test_gps_model_uses_apm_doc_to_diagnose_disabled_logging() -> None: """Missing GPS data should point to LOG_BITMASK when the GPS bit is disabled.""" - result = GPSLogQualityModel( + result = GPSLogAvailabilityModel( LogData(), _context( {"LOG_BITMASK": 0.0, "GPS_TYPE": 1.0}, @@ -73,14 +76,14 @@ def test_gps_model_uses_apm_doc_to_diagnose_disabled_logging() -> None: ).check() assert result.available is False - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert result.issues[0].config_step == "06_gnss.param" assert "GPS logging" in result.reason def test_esc_model_uses_apm_doc_to_detect_non_dshot_configuration() -> None: """Missing ESC data should recommend DShot when MOT_PWM_TYPE is not one of the documented DShot values.""" - result = EscLogQualityModel( + result = EscLogAvailabilityModel( LogData(), _context( {"MOT_PWM_TYPE": 3.0, "SCR_ENABLE": 1.0}, @@ -89,7 +92,7 @@ def test_esc_model_uses_apm_doc_to_detect_non_dshot_configuration() -> None: ).check() assert result.available is False - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert result.issues[0].config_step == "07_esc.param" assert "DShot" in result.issues[0].message @@ -105,10 +108,10 @@ def test_battery_model_checks_present_log_data_without_datasource_access() -> No ), ) - result = BatteryLogQualityModel(log_data, _context({"BATT_MONITOR": 4.0})).check() + result = BatteryLogAvailabilityModel(log_data, _context({"BATT_MONITOR": 4.0})).check() assert result.available is True - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert any("Voltage is zero" in issue.message for issue in result.issues) @@ -123,10 +126,10 @@ def test_battery_model_rejects_non_finite_telemetry_values() -> None: ), ) - result = BatteryLogQualityModel(log_data, _context({"BATT_MONITOR": 4.0})).check() + result = BatteryLogAvailabilityModel(log_data, _context({"BATT_MONITOR": 4.0})).check() assert result.available is True - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert len(result.issues) == 3 assert all("non-finite telemetry values" in issue.message for issue in result.issues) @@ -139,12 +142,12 @@ def test_battery_model_checks_failsafes_for_each_enabled_monitor() -> None: np.array([(22.0, 1.0, 0.1)], dtype=[("Volt", "f8"), ("Curr", "f8"), ("CurrTot", "f8")]), ) - result = BatteryLogQualityModel( + result = BatteryLogAvailabilityModel( log_data, _context({"BATT_MONITOR": 4.0, "BATT2_MONITOR": 4.0, "BATT2_LOW_VOLT": 0.0, "BATT2_CRT_VOLT": 0.0}), ).check() - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert [issue.message for issue in result.issues] == [ "Battery 2: low-voltage failsafe threshold disabled", "Battery 2: critical-voltage failsafe threshold disabled", @@ -159,9 +162,9 @@ def test_pm_model_accepts_valid_legacy_schema_without_internal_error_fields() -> np.array([(20.0, 10_000.0, 0.0)], dtype=[("Load", "f8"), ("Mem", "f8"), ("NLon", "f8")]), ) - result = PmLogQualityModel(log_data, _context({})).check() + result = PmLogAvailabilityModel(log_data, _context({})).check() - assert result.state == LogQualityState.INFO + assert result.state == LogAvailabilityState.INFO def test_battery_capacity_uses_canonical_amp_hours() -> None: diff --git a/tests/test_data_model_battery_analysis.py b/tests/test_data_model_battery_analysis.py index 6bf3daf96..13f2de7ad 100755 --- a/tests/test_data_model_battery_analysis.py +++ b/tests/test_data_model_battery_analysis.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Behavior-driven tests for battery log quality and analysis data models. +Behavior-driven tests for battery log availability and analysis data models. -This module contains comprehensive tests for BatteryLogQualityModel and +This module contains comprehensive tests for BatteryLogAvailabilityModel and BatteryLogAnalysis, focusing on user workflows and business value rather than implementation details. @@ -17,12 +17,12 @@ import numpy as np import pytest -from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_battery import ( +from ardupilot_methodic_configurator.log_analysis.data_model_availability_battery import ( BatteryLogAnalysis, - BatteryLogQualityModel, + BatteryLogAvailabilityModel, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState # pylint: disable=redefined-outer-name @@ -124,26 +124,28 @@ def _context( ) -class TestBatteryLogQualityModelPresence: +class TestBatteryLogAvailabilityModelPresence: """Test presence and sensor-reading checks for battery telemetry.""" - def test_user_sees_good_quality_when_bat_data_is_present_and_healthy(self, healthy_bat_log_data, battery_apm_doc) -> None: + def test_user_sees_good_availability_when_bat_data_is_present_and_healthy( + self, healthy_bat_log_data, battery_apm_doc + ) -> None: """ - User sees a clean quality report for a normally logged battery. + User sees a clean availability report for a normally logged battery. GIVEN: A log with BAT data whose voltage and current are non-zero - WHEN: The battery quality model runs + WHEN: The battery availability model runs THEN: It reports available data with no issues """ # Arrange: healthy log data provided by fixture - model = BatteryLogQualityModel(healthy_bat_log_data, _context({"BATT_MONITOR": 4}, apm_doc=battery_apm_doc)) + model = BatteryLogAvailabilityModel(healthy_bat_log_data, _context({"BATT_MONITOR": 4}, apm_doc=battery_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: no issues, informational state assert result.available is True - assert result.state == LogQualityState.INFO + assert result.state == LogAvailabilityState.INFO assert not result.issues def test_user_is_warned_when_voltage_is_stuck_at_zero(self, battery_apm_doc) -> None: @@ -151,22 +153,22 @@ def test_user_is_warned_when_voltage_is_stuck_at_zero(self, battery_apm_doc) -> User is warned when the voltage sensor never reports a real reading. GIVEN: BAT data where Volt never leaves zero - WHEN: The battery quality model runs - THEN: It reports a quality issue about the sensor not reading + WHEN: The battery availability model runs + THEN: It reports a availability issue about the sensor not reading """ # Arrange: log where Volt is flat zero throughout log_data = FakeLogData({"BAT": {"Volt": [0.0, 0.0], "Curr": [1.0, 1.0], "CurrTot": [1.0, 2.0]}}) - model = BatteryLogQualityModel(log_data, _context({"BATT_MONITOR": 4}, apm_doc=battery_apm_doc)) + model = BatteryLogAvailabilityModel(log_data, _context({"BATT_MONITOR": 4}, apm_doc=battery_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: warning state with a zero-reading issue - assert result.state == LogQualityState.WARNING + assert result.state == LogAvailabilityState.WARNING assert any("zero throughout" in issue.message for issue in result.issues) -class TestBatteryLogQualityModelFailsafeParameters: # pylint: disable=too-few-public-methods +class TestBatteryLogAvailabilityModelFailsafeParameters: # pylint: disable=too-few-public-methods """Test detection of disabled battery failsafe thresholds.""" def test_user_is_warned_when_low_and_critical_voltage_failsafes_are_disabled(self, battery_apm_doc) -> None: @@ -174,16 +176,16 @@ def test_user_is_warned_when_low_and_critical_voltage_failsafes_are_disabled(sel User is warned when both battery voltage failsafe thresholds are turned off. GIVEN: BATT_LOW_VOLT and BATT_CRT_VOLT are both 0 (disabled) - WHEN: The battery quality model runs + WHEN: The battery availability model runs THEN: It reports both failsafe thresholds as disabled issues """ # Arrange: log with valid telemetry but both failsafes at 0 log_data = FakeLogData({"BAT": {"Volt": [22.0], "Curr": [1.0], "CurrTot": [1.0]}}) - model = BatteryLogQualityModel( + model = BatteryLogAvailabilityModel( log_data, _context({"BATT_MONITOR": 4, "BATT_LOW_VOLT": 0, "BATT_CRT_VOLT": 0}, apm_doc=battery_apm_doc) ) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: both failsafe-disabled issues present @@ -192,7 +194,7 @@ def test_user_is_warned_when_low_and_critical_voltage_failsafes_are_disabled(sel assert any("critical-voltage failsafe threshold disabled" in m for m in messages) -class TestBatteryLogQualityModelAbsenceDiagnosis: +class TestBatteryLogAvailabilityModelAbsenceDiagnosis: """Test diagnosing why BAT data is absent from a log.""" def test_user_is_told_to_check_log_bitmask_when_battery_monitor_bit_is_disabled(self, battery_apm_doc) -> None: @@ -200,14 +202,16 @@ def test_user_is_told_to_check_log_bitmask_when_battery_monitor_bit_is_disabled( User is pointed at LOG_BITMASK when battery logging was never enabled. GIVEN: No BAT data and LOG_BITMASK with the Battery Monitor bit cleared - WHEN: The battery quality model runs + WHEN: The battery availability model runs THEN: It reports the absence as a LOG_BITMASK configuration issue, not a wiring problem """ # Arrange: no BAT data, bitmask excludes battery monitor logging log_data = FakeLogData({}) - model = BatteryLogQualityModel(log_data, _context({"LOG_BITMASK": 0.0, "BATT_MONITOR": 4.0}, apm_doc=battery_apm_doc)) + model = BatteryLogAvailabilityModel( + log_data, _context({"LOG_BITMASK": 0.0, "BATT_MONITOR": 4.0}, apm_doc=battery_apm_doc) + ) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: absence diagnosed via LOG_BITMASK, not physical connection @@ -219,16 +223,16 @@ def test_user_is_told_batt_monitor_is_disabled_when_bitmask_is_fine(self, batter User is pointed at BATT_MONITOR when logging is enabled but the monitor itself is off. GIVEN: No BAT data, LOG_BITMASK includes Battery Monitor logging, but BATT_MONITOR is 0 - WHEN: The battery quality model runs + WHEN: The battery availability model runs THEN: It reports BATT_MONITOR as the misconfiguration, not the physical connection """ # Arrange: bitmask enabled, but BATT_MONITOR itself is 0 log_data = FakeLogData({}) - model = BatteryLogQualityModel( + model = BatteryLogAvailabilityModel( log_data, _context({"LOG_BITMASK": 512.0, "BATT_MONITOR": 0.0}, apm_doc=battery_apm_doc) ) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: absence diagnosed via BATT_MONITOR, not LOG_BITMASK diff --git a/tests/test_data_model_imu_analysis.py b/tests/test_data_model_imu_analysis.py index d71624a37..abdce68fc 100755 --- a/tests/test_data_model_imu_analysis.py +++ b/tests/test_data_model_imu_analysis.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Behavior-driven tests for IMU log quality and analysis data models. +Behavior-driven tests for IMU log availability and analysis data models. -This module contains comprehensive tests for ImuLogQualityModel and +This module contains comprehensive tests for ImuLogAvailabilityModel and ImuLogAnalysis, focusing on user workflows and business value rather than implementation details. @@ -17,9 +17,9 @@ import numpy as np import pytest +from ardupilot_methodic_configurator.log_analysis.data_model_availability_imu import ImuLogAnalysis, ImuLogAvailabilityModel from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_imu import ImuLogAnalysis, ImuLogQualityModel +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityState # pylint: disable=redefined-outer-name @@ -84,27 +84,27 @@ def _context(parameters: dict[str, float] | None = None, *, apm_doc: dict | None ) -class TestImuLogQualityModelHealth: +class TestImuLogAvailabilityModelHealth: """Test presence and health checks for IMU telemetry.""" - def test_user_sees_good_quality_for_a_healthy_imu(self, healthy_imu_columns, imu_apm_doc) -> None: + def test_user_sees_good_availability_for_a_healthy_imu(self, healthy_imu_columns, imu_apm_doc) -> None: """ - User sees a clean quality report for a healthy IMU. + User sees a clean availability report for a healthy IMU. GIVEN: IMU data with zero error counts, healthy flags, and non-zero signal - WHEN: The IMU quality model runs + WHEN: The IMU availability model runs THEN: It reports available data with no issues """ # Arrange: healthy IMU data provided by fixture log_data = FakeLogData({"IMU": healthy_imu_columns}) - model = ImuLogQualityModel(log_data, _context(apm_doc=imu_apm_doc)) + model = ImuLogAvailabilityModel(log_data, _context(apm_doc=imu_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: no issues, informational state assert result.available is True - assert result.state == LogQualityState.INFO + assert result.state == LogAvailabilityState.INFO assert not result.issues def test_user_is_warned_about_a_nonzero_gyro_error_count(self, healthy_imu_columns, imu_apm_doc) -> None: @@ -112,16 +112,16 @@ def test_user_is_warned_about_a_nonzero_gyro_error_count(self, healthy_imu_colum User is warned when the gyroscope reports internal errors during the flight. GIVEN: EG (gyro error count) is nonzero at some point - WHEN: The IMU quality model runs + WHEN: The IMU availability model runs THEN: It reports a gyroscope error issue """ # Arrange: EG goes nonzero partway through the flight columns = dict(healthy_imu_columns) columns["EG"] = [0.0, 3.0] log_data = FakeLogData({"IMU": columns}) - model = ImuLogQualityModel(log_data, _context(apm_doc=imu_apm_doc)) + model = ImuLogAvailabilityModel(log_data, _context(apm_doc=imu_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: gyroscope error issue reported @@ -132,16 +132,16 @@ def test_user_is_warned_when_accelerometer_health_drops(self, healthy_imu_column User is warned when the accelerometer reports unhealthy during the flight. GIVEN: AH (accelerometer health) drops to 0 at some point - WHEN: The IMU quality model runs + WHEN: The IMU availability model runs THEN: It reports an accelerometer health issue """ # Arrange: AH drops to unhealthy partway through the flight columns = dict(healthy_imu_columns) columns["AH"] = [1.0, 0.0] log_data = FakeLogData({"IMU": columns}) - model = ImuLogQualityModel(log_data, _context(apm_doc=imu_apm_doc)) + model = ImuLogAvailabilityModel(log_data, _context(apm_doc=imu_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: accelerometer health issue reported @@ -152,23 +152,23 @@ def test_user_is_warned_when_a_gyro_axis_is_flat_zero(self, healthy_imu_columns, User is warned when a gyroscope axis never reports a real reading. GIVEN: GyrX is zero throughout the flight - WHEN: The IMU quality model runs + WHEN: The IMU availability model runs THEN: It reports the axis as possibly not reading """ # Arrange: GyrX flat zero throughout columns = dict(healthy_imu_columns) columns["GyrX"] = [0.0, 0.0] log_data = FakeLogData({"IMU": columns}) - model = ImuLogQualityModel(log_data, _context(apm_doc=imu_apm_doc)) + model = ImuLogAvailabilityModel(log_data, _context(apm_doc=imu_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: flat-zero axis flagged assert any("GyrX is zero throughout" in issue.message for issue in result.issues) -class TestImuLogQualityModelAbsenceDiagnosis: # pylint: disable=too-few-public-methods +class TestImuLogAvailabilityModelAbsenceDiagnosis: # pylint: disable=too-few-public-methods """Test diagnosing why IMU data is absent from a log.""" def test_user_is_told_to_check_log_bitmask_when_imu_bit_is_disabled(self, imu_apm_doc) -> None: @@ -176,14 +176,14 @@ def test_user_is_told_to_check_log_bitmask_when_imu_bit_is_disabled(self, imu_ap User is pointed at LOG_BITMASK when IMU logging was never enabled. GIVEN: No IMU data and LOG_BITMASK excludes the IMU bit - WHEN: The IMU quality model runs + WHEN: The IMU availability model runs THEN: It reports the absence as a LOG_BITMASK configuration issue """ # Arrange: no IMU data, bitmask excludes IMU logging log_data = FakeLogData({}) - model = ImuLogQualityModel(log_data, _context({"LOG_BITMASK": 0.0}, apm_doc=imu_apm_doc)) + model = ImuLogAvailabilityModel(log_data, _context({"LOG_BITMASK": 0.0}, apm_doc=imu_apm_doc)) - # Act: run the quality check + # Act: run the availability check result = model.check() # Assert: absence diagnosed via LOG_BITMASK diff --git a/tests/test_data_model_log_analysis.py b/tests/test_data_model_log_analysis.py index a1b2e7047..79a040182 100755 --- a/tests/test_data_model_log_analysis.py +++ b/tests/test_data_model_log_analysis.py @@ -13,22 +13,25 @@ import pytest from ardupilot_methodic_configurator.log_analysis import data_model_log_analysis +from ardupilot_methodic_configurator.log_analysis.data_model_availability_base import ( + BaseLogAnalysisModel, + BaseLogAvailabilityModel, +) from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis import ( analyze_log, parse_firmware_version, validate_log_matches_vehicle, ) from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_context import LogAnalysisContext -from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityResult, LogQualityState -from ardupilot_methodic_configurator.log_analysis.data_model_quality_base import ( - BaseLogAnalysisModel, - BaseLogQualityModel, +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import ( + LogAvailabilityResult, + LogAvailabilityState, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData -class RecordingQualityModel(BaseLogQualityModel): - """Quality model test double that records constructor inputs.""" +class RecordingAvailabilityModel(BaseLogAvailabilityModel): + """Availability model test double that records constructor inputs.""" seen_log_data: ClassVar[LogData | None] = None seen_context: ClassVar[LogAnalysisContext | None] = None @@ -38,23 +41,23 @@ def __init__(self, log_data: LogData, context: LogAnalysisContext) -> None: type(self).seen_log_data = log_data type(self).seen_context = context - def check(self) -> LogQualityResult: - return LogQualityResult( + def check(self) -> LogAvailabilityResult: + return LogAvailabilityResult( available=True, - state=LogQualityState.INFO, + state=LogAvailabilityState.INFO, reason="ok", issues=[], name="Recording", ) -class DummyQualityModel(BaseLogQualityModel): +class DummyAvailabilityModel(BaseLogAvailabilityModel): """Minimal concrete model to exercise base-class context wiring.""" - def check(self) -> LogQualityResult: - return LogQualityResult( + def check(self) -> LogAvailabilityResult: + return LogAvailabilityResult( available=True, - state=LogQualityState.INFO, + state=LogAvailabilityState.INFO, reason="ok", issues=[], name="Dummy", @@ -81,11 +84,11 @@ def derived_and_forced_parameters_matching(self, pattern: str, _inputs: object) return {"TEST_PARAM": "01_test.param"} -def test_analyze_log_passes_context_to_quality_models(monkeypatch: Any) -> None: # noqa: ANN401 +def test_analyze_log_passes_context_to_availability_models(monkeypatch: Any) -> None: # noqa: ANN401 """ - Pass the same context object through to each quality model constructor. + Pass the same context object through to each availability model constructor. - GIVEN analyze_log is called with an explicit context and quality-model list, + GIVEN analyze_log is called with an explicit context and availability-model list, WHEN the analysis runs, THEN each model should receive the original context object. """ @@ -115,22 +118,22 @@ def record_configuration_steps(_log_data: LogData, steps: dict[str, Any]) -> lis lambda _log_data, _params, _apm_doc: sentinel_hardware_report, ) - summary = analyze_log(log_data, context, quality_and_analysis_models=[(RecordingQualityModel, None)]) + summary = analyze_log(log_data, context, availability_and_analysis_models=[(RecordingAvailabilityModel, None)]) - assert RecordingQualityModel.seen_log_data is log_data - assert RecordingQualityModel.seen_context is context + assert RecordingAvailabilityModel.seen_log_data is log_data + assert RecordingAvailabilityModel.seen_context is context assert summary.parameter_count == 1 assert summary.hardware_report is sentinel_hardware_report - assert [result.name for result in summary.quality_results] == ["System Performance", "Recording"] + assert [result.name for result in summary.availability_results] == ["System Performance", "Recording"] assert seen_steps == [context.configuration_steps] -def test_base_quality_model_reads_fields_from_context() -> None: +def test_base_availability_model_reads_fields_from_context() -> None: """ Populate base-model dependencies directly from context. GIVEN a context with parameters, config steps, apm-doc, and components, - WHEN a concrete quality model is created, + WHEN a concrete availability model is created, THEN the base model should expose those values without repacking args. """ log_data = LogData() @@ -141,7 +144,7 @@ def test_base_quality_model_reads_fields_from_context() -> None: apm_doc={"BATT_MONITOR": {"humanName": "Battery monitor"}}, ) - model = DummyQualityModel(log_data, context) + model = DummyAvailabilityModel(log_data, context) assert model.log_data is log_data assert model.parameters is context.parameters @@ -175,12 +178,12 @@ def test_subsystem_component_metadata_is_declared_by_the_registry() -> None: assert component_keys["esc"] == ("ESC", "Motors") -def test_base_quality_model_tolerates_ambiguous_configuration_metadata() -> None: +def test_base_availability_model_tolerates_ambiguous_configuration_metadata() -> None: """ Keep report generation alive when configuration metadata has duplicate references. GIVEN duplicated message and parameter references in configuration steps, - WHEN quality helpers resolve frontend guidance, + WHEN availability helpers resolve frontend guidance, THEN they should fall back instead of raising from the model layer. """ context = LogAnalysisContext( @@ -196,7 +199,7 @@ def test_base_quality_model_tolerates_ambiguous_configuration_metadata() -> None }, }, ) - model = DummyQualityModel(LogData(), context) + model = DummyAvailabilityModel(LogData(), context) assert model.resolve_message_step("BAT", "Battery") == ("", "Battery") assert model.step_for_parameter("BATT_MONITOR") == "" diff --git a/tests/test_data_model_log_quality_check.py b/tests/test_data_model_log_availability_check.py similarity index 99% rename from tests/test_data_model_log_quality_check.py rename to tests/test_data_model_log_availability_check.py index 940e7defc..8ddb3c056 100755 --- a/tests/test_data_model_log_quality_check.py +++ b/tests/test_data_model_log_availability_check.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Tests for ardupilot_methodic_configurator/log_analysis/data_model_log_quality_check.py. +Tests for ardupilot_methodic_configurator/log_analysis/data_model_log_availability_check.py. SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas @@ -11,13 +11,13 @@ import numpy as np import pytest -from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData, MessageSchema -from ardupilot_methodic_configurator.log_analysis.data_model_log_quality_check import ( +from ardupilot_methodic_configurator.log_analysis.data_model_log_availability_check import ( check_cpu_performance_message, get_pm_status, validate_configuration_steps_data, validate_fmt_schema, ) +from ardupilot_methodic_configurator.log_analysis.data_model_log_data import LogData, MessageSchema # pylint: disable=redefined-outer-name diff --git a/tests/test_data_model_log_report.py b/tests/test_data_model_log_report.py index ab2c636c5..eccd1917b 100755 --- a/tests/test_data_model_log_report.py +++ b/tests/test_data_model_log_report.py @@ -21,10 +21,10 @@ from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview import VehicleInfo -def test_report_status_counts_quality_issues_and_failed_steps() -> None: +def test_report_status_counts_availability_issues_and_failed_steps() -> None: """Report status is calculated without building Tk widgets.""" summary = MagicMock() - summary.quality_results = [MagicMock(issues=[object(), object()]), MagicMock(issues=[])] + summary.availability_results = [MagicMock(issues=[object(), object()]), MagicMock(issues=[])] summary.step_results = [MagicMock(valid=True), MagicMock(valid=False)] status = build_report_status(summary) @@ -36,7 +36,7 @@ def test_report_status_counts_quality_issues_and_failed_steps() -> None: def test_report_status_is_healthy_when_no_issues() -> None: summary = MagicMock() - summary.quality_results = [MagicMock(issues=[])] + summary.availability_results = [MagicMock(issues=[])] summary.step_results = [MagicMock(valid=True)] status = build_report_status(summary) diff --git a/tests/test_frontend_tkinter_log_analysis.py b/tests/test_frontend_tkinter_log_analysis.py index 2ddec9f9f..9143f0448 100755 --- a/tests/test_frontend_tkinter_log_analysis.py +++ b/tests/test_frontend_tkinter_log_analysis.py @@ -32,21 +32,21 @@ from pytest_mock import MockerFixture from ardupilot_methodic_configurator.log_analysis.data_model_log_analysis_result import LogAnalysisResult - from ardupilot_methodic_configurator.log_analysis.data_model_log_quality import LogQualityResult + from ardupilot_methodic_configurator.log_analysis.data_model_log_availability import LogAvailabilityResult MODULE = "ardupilot_methodic_configurator.frontend_tkinter_log_analysis" # pylint: disable=protected-access, redefined-outer-name -def _make_quality_issue(message: str = "issue", config_step: str | None = None) -> MagicMock: +def _make_availability_issue(message: str = "issue", config_step: str | None = None) -> MagicMock: issue = MagicMock() issue.message = message issue.config_step = config_step return issue -def _make_quality_result( +def _make_availability_result( *, name: str = "Battery", available: bool = True, @@ -97,7 +97,7 @@ def _make_analysis_result( return result -def _make_summary(quality_results: list[MagicMock], analysis_results: list[MagicMock]) -> LogSummary: +def _make_summary(availability_results: list[MagicMock], analysis_results: list[MagicMock]) -> LogSummary: return LogSummary( flight_duration_sec=None, file_size_bytes=0, @@ -106,7 +106,7 @@ def _make_summary(quality_results: list[MagicMock], analysis_results: list[Magic parameter_count=0, pm_status=None, pm_validation=None, - quality_results=cast("list[LogQualityResult]", quality_results), + availability_results=cast("list[LogAvailabilityResult]", availability_results), analysis_results=cast("list[LogAnalysisResult]", analysis_results), step_results=[], hardware_report=MagicMock(), @@ -115,42 +115,42 @@ def _make_summary(quality_results: list[MagicMock], analysis_results: list[Magic ) -class TestPairedQualityAndAnalysisResults: - """Cover stable-key pairing between quality and analysis results.""" +class TestPairedAvailabilityAndAnalysisResults: + """Cover stable-key pairing between availability and analysis results.""" - def test_pairs_available_quality_with_its_analysis_result(self) -> None: + def test_pairs_available_availability_with_its_analysis_result(self) -> None: """ - Quality models with an analysis counterpart are paired when data is available. + Availability models with an analysis counterpart are paired when data is available. - GIVEN: Battery has an analysis class and its quality check was available + GIVEN: Battery has an analysis class and its availability check was available WHEN: Results are paired - THEN: The quality result is paired with the corresponding analysis result + THEN: The availability result is paired with the corresponding analysis result """ - quality = _make_quality_result(name="Battery", available=True) + availability = _make_availability_result(name="Battery", available=True) analysis = _make_analysis_result(name="Battery Analysis") - summary = _make_summary([quality], [analysis]) + summary = _make_summary([availability], [analysis]) - pairs = summary.paired_quality_and_analysis_results() + pairs = summary.paired_availability_and_analysis_results() - assert pairs == [(quality, analysis)] + assert pairs == [(availability, analysis)] - def test_pairs_unavailable_quality_with_none(self) -> None: + def test_pairs_unavailable_availability_with_none(self) -> None: """ - Quality models whose data was unavailable pair with None instead of an analysis result. + Availability models whose data was unavailable pair with None instead of an analysis result. - GIVEN: Battery has an analysis class but the quality check reported unavailable + GIVEN: Battery has an analysis class but the availability check reported unavailable WHEN: Results are paired THEN: The analysis side of the pair is None AND: No analysis result is consumed from the iterator """ - quality = _make_quality_result(name="ESC telemetry", available=False) - summary = _make_summary([quality], []) + availability = _make_availability_result(name="ESC telemetry", available=False) + summary = _make_summary([availability], []) - pairs = summary.paired_quality_and_analysis_results() + pairs = summary.paired_availability_and_analysis_results() - assert pairs == [(quality, None)] + assert pairs == [(availability, None)] - def test_skips_quality_models_with_no_analysis_class(self) -> None: + def test_skips_availability_models_with_no_analysis_class(self) -> None: """ Subsystems with no analysis model (analysis_cls is None) are excluded entirely. @@ -158,65 +158,65 @@ def test_skips_quality_models_with_no_analysis_class(self) -> None: WHEN: Results are paired THEN: GPS does not appear in the paired output at all """ - quality = _make_quality_result(name="GPS", available=True, subsystem_key="gps") - summary = _make_summary([quality], []) + availability = _make_availability_result(name="GPS", available=True, subsystem_key="gps") + summary = _make_summary([availability], []) - pairs = summary.paired_quality_and_analysis_results() + pairs = summary.paired_availability_and_analysis_results() assert not pairs - def test_ignores_unregistered_prepended_quality_result(self) -> None: + def test_ignores_unregistered_prepended_availability_result(self) -> None: """ - A single extra quality result (e.g. System Performance) prepended by analyze_log(). + A single extra availability result (e.g. System Performance) prepended by analyze_log(). GIVEN: the extra result has no subsystem key WHEN: Results are paired THEN: the keyed Battery entry is paired, independently of list position """ - prepended = _make_quality_result(name="System Performance", available=True, subsystem_key=None) - battery_quality = _make_quality_result(name="Battery", available=True) + prepended = _make_availability_result(name="System Performance", available=True, subsystem_key=None) + battery_availability = _make_availability_result(name="Battery", available=True) analysis = _make_analysis_result(name="Battery Analysis") - summary = _make_summary([prepended, battery_quality], [analysis]) + summary = _make_summary([prepended, battery_availability], [analysis]) - pairs = summary.paired_quality_and_analysis_results() + pairs = summary.paired_availability_and_analysis_results() - assert pairs == [(battery_quality, analysis)] + assert pairs == [(battery_availability, analysis)] def test_ignores_results_without_a_registered_subsystem_key(self) -> None: """ Unknown or legacy result entries do not affect keyed pairing. - GIVEN: quality results have no registered subsystem keys + GIVEN: availability results have no registered subsystem keys WHEN: Results are paired THEN: no positional pairing is attempted """ summary = _make_summary( [ - _make_quality_result(subsystem_key=None), - _make_quality_result(subsystem_key=None), - _make_quality_result(subsystem_key=None), + _make_availability_result(subsystem_key=None), + _make_availability_result(subsystem_key=None), + _make_availability_result(subsystem_key=None), ], [], ) - assert summary.paired_quality_and_analysis_results() == [] + assert summary.paired_availability_and_analysis_results() == [] class TestCollectLinks: - """Cover step_info link de-duplication across quality issues and analysis outcomes.""" + """Cover step_info link de-duplication across availability issues and analysis outcomes.""" - def test_collects_link_from_quality_issue_step_info(self) -> None: + def test_collects_link_from_availability_issue_step_info(self) -> None: """ - A quality issue's step_info with a wiki_url is included in the collected links. + A availability issue's step_info with a wiki_url is included in the collected links. - GIVEN: One quality issue carries step_info with a wiki_url + GIVEN: One availability issue carries step_info with a wiki_url WHEN: Links are collected THEN: That step_info dict is present in the returned list """ step_info = {"wiki_url": "https://ardupilot.org/wiki", "wiki_text": "Wiki"} - quality_dict = {"issues": [{"step_info": step_info}]} + availability_dict = {"issues": [{"step_info": step_info}]} - links = _collect_links(quality_dict, None) + links = _collect_links(availability_dict, None) assert links == [step_info] @@ -239,14 +239,14 @@ def test_deduplicates_identical_links_across_multiple_findings(self) -> None: """ The same (wiki_url, blog_url) pair appearing on multiple findings is only listed once. - GIVEN: Two quality issues reference the same step_info wiki_url and blog_url + GIVEN: Two availability issues reference the same step_info wiki_url and blog_url WHEN: Links are collected THEN: Only one entry appears in the result """ step_info = {"wiki_url": "https://ardupilot.org/wiki", "blog_url": None} - quality_dict = {"issues": [{"step_info": step_info}, {"step_info": dict(step_info)}]} + availability_dict = {"issues": [{"step_info": step_info}, {"step_info": dict(step_info)}]} - links = _collect_links(quality_dict, None) + links = _collect_links(availability_dict, None) assert len(links) == 1 @@ -258,17 +258,17 @@ def test_ignores_step_info_with_no_urls_at_all(self) -> None: WHEN: Links are collected THEN: No links are returned """ - quality_dict = {"issues": [{"step_info": {"wiki_url": None, "blog_url": None}}]} + availability_dict = {"issues": [{"step_info": {"wiki_url": None, "blog_url": None}}]} - links = _collect_links(quality_dict, None) + links = _collect_links(availability_dict, None) assert links == [] - def test_returns_empty_list_when_no_quality_or_analysis_dicts_given(self) -> None: + def test_returns_empty_list_when_no_availability_or_analysis_dicts_given(self) -> None: """ - Missing quality/analysis dicts (e.g. pending subsystem) produce no links, not an error. + Missing availability/analysis dicts (e.g. pending subsystem) produce no links, not an error. - GIVEN: Both quality_dict and analysis_dict are None + GIVEN: Both availability_dict and analysis_dict are None WHEN: Links are collected THEN: An empty list is returned """ @@ -284,9 +284,9 @@ def test_ignores_issues_or_outcomes_missing_step_info_key(self) -> None: WHEN: Links are collected THEN: No exception is raised and no link is added """ - quality_dict = {"issues": [{"message": "no step info here"}]} + availability_dict = {"issues": [{"message": "no step info here"}]} - links = _collect_links(quality_dict, None) + links = _collect_links(availability_dict, None) assert links == [] @@ -424,7 +424,7 @@ def _build_window( self, mocker: MockerFixture, patched_widgets: dict[str, MagicMock], - quality_results: list[MagicMock], + availability_results: list[MagicMock], analysis_results: list[MagicMock], *, report: dict | None = None, @@ -432,7 +432,7 @@ def _build_window( ) -> LogAnalysisReportWindow: mocker.patch.object(LogAnalysisReportWindow, "calculate_scaled_geometry", return_value="1050x800") mocker.patch.object(LogAnalysisReportWindow, "center_window") - summary = _make_summary(quality_results, analysis_results) + summary = _make_summary(availability_results, analysis_results) root = MagicMock() @@ -447,16 +447,18 @@ def test_selector_defaults_to_first_subsystem(self, mocker: MockerFixture, patch """ The subsystem selector is preset to the first available subsystem on open. - GIVEN: Two quality/analysis pairs are present, Battery first + GIVEN: Two availability/analysis pairs are present, Battery first WHEN: The window is constructed THEN: The selector's set() is called with the first subsystem's name """ - quality_battery = _make_quality_result(name="Battery", subsystem_key="battery") - quality_imu = _make_quality_result(name="IMU", subsystem_key="imu") + availability_battery = _make_availability_result(name="Battery", subsystem_key="battery") + availability_imu = _make_availability_result(name="IMU", subsystem_key="imu") analysis_battery = _make_analysis_result(name="Battery Analysis", subsystem_key="battery") analysis_imu = _make_analysis_result(name="IMU Analysis", subsystem_key="imu") - window = self._build_window(mocker, patched_widgets, [quality_battery, quality_imu], [analysis_battery, analysis_imu]) + window = self._build_window( + mocker, patched_widgets, [availability_battery, availability_imu], [analysis_battery, analysis_imu] + ) window.selector.set.assert_called_once_with("Battery") @@ -466,7 +468,7 @@ def test_no_selector_default_when_no_subsystems_present( """ An empty pairing list does not attempt to set a selector value. - GIVEN: No quality/analysis pairs exist (empty registry) + GIVEN: No availability/analysis pairs exist (empty registry) WHEN: The window is constructed THEN: The selector's set() is never called """ @@ -548,17 +550,17 @@ def _window_for_render(self, bare_window: LogAnalysisReportWindow) -> LogAnalysi def test_shows_pending_message_for_unpaired_analysis(self, bare_window: LogAnalysisReportWindow) -> None: """ - A subsystem with no analysis result (pending) shows its quality reason as the analysis text. + A subsystem with no analysis result (pending) shows its availability reason as the analysis text. - GIVEN: A subsystem paired with None (quality gate not yet passed) + GIVEN: A subsystem paired with None (availability gate not yet passed) WHEN: That subsystem is rendered - THEN: The Analysis section body includes the quality result's reason + THEN: The Analysis section body includes the availability result's reason """ window = self._window_for_render(bare_window) - quality = _make_quality_result(name="ESC telemetry", reason="ESC telemetry not logged") - window.pairs = [(quality, None)] + availability = _make_availability_result(name="ESC telemetry", reason="ESC telemetry not logged") + window.pairs = [(availability, None)] window.report = None - window._report_quality_by_name = {} + window._report_availability_by_name = {} window._report_analysis_by_name = {} with ( @@ -579,11 +581,11 @@ def test_shows_no_findings_message_for_clean_subsystem(self, bare_window: LogAna THEN: The Analysis section body includes "No findings." """ window = self._window_for_render(bare_window) - quality = _make_quality_result(name="VIBE", reason="VIBE data present and good for analysis") + availability = _make_availability_result(name="VIBE", reason="VIBE data present and good for analysis") analysis = _make_analysis_result(name="Vibration Analysis", outcomes=[]) - window.pairs = [(quality, analysis)] + window.pairs = [(availability, analysis)] window.report = None - window._report_quality_by_name = {} + window._report_availability_by_name = {} window._report_analysis_by_name = {} with ( @@ -604,11 +606,11 @@ def test_renders_each_outcome_as_an_outcome_line(self, bare_window: LogAnalysisR """ window = self._window_for_render(bare_window) outcomes = [_make_outcome(message="first"), _make_outcome(message="second")] - quality = _make_quality_result(name="Battery") + availability = _make_availability_result(name="Battery") analysis = _make_analysis_result(name="Battery Analysis", outcomes=outcomes) - window.pairs = [(quality, analysis)] + window.pairs = [(availability, analysis)] window.report = None - window._report_quality_by_name = {} + window._report_availability_by_name = {} window._report_analysis_by_name = {} with ( @@ -623,7 +625,7 @@ def test_renders_each_outcome_as_an_outcome_line(self, bare_window: LogAnalysisR mock_outcome_line.assert_any_call(outcomes[1]) def test_actionable_outcome_exposes_a_parameter_fix(self, bare_window: LogAnalysisReportWindow) -> None: - """Analysis recommendations should use the same parameter-fix workflow as quality issues.""" + """Analysis recommendations should use the same parameter-fix workflow as availability issues.""" outcome = _make_outcome(param_name="MOT_SPIN_MIN", suggested_value=0.15) bare_window.summary.related_parameter_values = {"MOT_SPIN_MIN": 0.1} @@ -663,11 +665,11 @@ def test_renders_no_hardware_section_when_no_component_data(self, bare_window: L THEN: "Hardware & Connections" is never passed to _section_heading """ window = self._window_for_render(bare_window) - quality = _make_quality_result(name="ARM") + availability = _make_availability_result(name="ARM") analysis = _make_analysis_result(name="ARM Analysis", outcomes=[]) - window.pairs = [(quality, analysis)] + window.pairs = [(availability, analysis)] window.report = {"vehicle_components": {}} - window._report_quality_by_name = {} + window._report_availability_by_name = {} window._report_analysis_by_name = {} with ( diff --git a/tests/test_frontend_tkinter_log_quality.py b/tests/test_frontend_tkinter_log_quality.py index e46f01901..5b54763b3 100755 --- a/tests/test_frontend_tkinter_log_quality.py +++ b/tests/test_frontend_tkinter_log_quality.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Tests for ardupilot_methodic_configurator/frontend_tkinter_log_quality.py. +Tests for ardupilot_methodic_configurator/frontend_tkinter_log_availability.py. This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator @@ -14,7 +14,10 @@ import pytest -from ardupilot_methodic_configurator.frontend_tkinter_log_quality import LogQualityReportWindow, _format_parameter_value +from ardupilot_methodic_configurator.frontend_tkinter_log_availability import ( + LogAvailabilityReportWindow, + _format_parameter_value, +) @pytest.mark.parametrize( @@ -43,16 +46,16 @@ def test_parameter_value_formatting_preserves_fractional_changes(value: float, e @pytest.mark.parametrize("upload_result", [None, True]) -def test_quality_fix_accepts_non_false_upload_result(upload_result: bool | None) -> None: +def test_availability_fix_accepts_non_false_upload_result(upload_result: bool | None) -> None: """ Treat callbacks without an explicit failure result as successful. GIVEN a parameter-fix callback returning None or True, - WHEN the quality report applies a fix, + WHEN the availability report applies a fix, THEN the displayed parameter state is updated and the dialog closes. """ # Arrange: bypass Tk construction and provide a side-effect callback. - window = LogQualityReportWindow.__new__(LogQualityReportWindow) + window = LogAvailabilityReportWindow.__new__(LogAvailabilityReportWindow) window.summary = MagicMock(related_parameter_values={}) window.upload_callback = MagicMock(return_value=upload_result) dialog = MagicMock() diff --git a/tests/test_frontend_tkinter_log_views.py b/tests/test_frontend_tkinter_log_views.py index 878d532be..2a7b2bb3d 100755 --- a/tests/test_frontend_tkinter_log_views.py +++ b/tests/test_frontend_tkinter_log_views.py @@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch -from ardupilot_methodic_configurator.frontend_tkinter_log_hardware_quality import build_hardware_tab -from ardupilot_methodic_configurator.frontend_tkinter_log_quality import LogQualityReportWindow +from ardupilot_methodic_configurator.frontend_tkinter_log_availability import LogAvailabilityReportWindow +from ardupilot_methodic_configurator.frontend_tkinter_log_hardware_availability import build_hardware_tab from ardupilot_methodic_configurator.log_analysis.data_model_vehicle_overview import HardwareReport, VehicleInfo @@ -40,9 +40,11 @@ def test_hardware_tab_shows_empty_state_when_report_has_no_sensors() -> None: ) with ( - patch("ardupilot_methodic_configurator.frontend_tkinter_log_hardware_quality.ScrollFrame", return_value=scroll_frame), patch( - "ardupilot_methodic_configurator.frontend_tkinter_log_hardware_quality.ttk.Label", + "ardupilot_methodic_configurator.frontend_tkinter_log_hardware_availability.ScrollFrame", return_value=scroll_frame + ), + patch( + "ardupilot_methodic_configurator.frontend_tkinter_log_hardware_availability.ttk.Label", return_value=label, ) as mock_label, ): @@ -54,7 +56,7 @@ def test_hardware_tab_shows_empty_state_when_report_has_no_sensors() -> None: def test_release_link_opens_with_backend_internet_browser_helper() -> None: """Clickable report links should reuse the application's browser helper.""" - report_window = LogQualityReportWindow.__new__(LogQualityReportWindow) + report_window = LogAvailabilityReportWindow.__new__(LogAvailabilityReportWindow) report_window.default_font_size = 11 parent = MagicMock() row = MagicMock() @@ -62,13 +64,13 @@ def test_release_link_opens_with_backend_internet_browser_helper() -> None: link_label = MagicMock() with ( - patch("ardupilot_methodic_configurator.frontend_tkinter_log_quality.ttk.Frame", return_value=row), + patch("ardupilot_methodic_configurator.frontend_tkinter_log_availability.ttk.Frame", return_value=row), patch( - "ardupilot_methodic_configurator.frontend_tkinter_log_quality.ttk.Label", + "ardupilot_methodic_configurator.frontend_tkinter_log_availability.ttk.Label", side_effect=[key_label, link_label], ), - patch("ardupilot_methodic_configurator.frontend_tkinter_log_quality.show_tooltip"), - patch("ardupilot_methodic_configurator.frontend_tkinter_log_quality.webbrowser_open_url") as mock_open, + patch("ardupilot_methodic_configurator.frontend_tkinter_log_availability.show_tooltip"), + patch("ardupilot_methodic_configurator.frontend_tkinter_log_availability.webbrowser_open_url") as mock_open, ): report_window._add_clickable_key_value(parent, "Firmware:", "Copter-4.5.5", "https://example.test/release") # pylint: disable=protected-access diff --git a/tests/test_frontend_tkinter_parameter_editor.py b/tests/test_frontend_tkinter_parameter_editor.py index 66efbfcc6..61f95150d 100755 --- a/tests/test_frontend_tkinter_parameter_editor.py +++ b/tests/test_frontend_tkinter_parameter_editor.py @@ -71,7 +71,7 @@ def _create_editor(parameter_editor: MagicMock) -> ParameterEditorWindow: # noq editor.parameter_editor_table.repopulate = MagicMock() editor.parameter_editor_table.get_upload_selected_params = MagicMock(return_value={}) editor.parameter_editor_table.view_port = MagicMock() - editor._log_quality_report_window = None + editor._log_availability_report_window = None editor._log_report_return_pending = False focus_widget = MagicMock() editor.parameter_editor_table.view_port.focus_get.return_value = focus_widget @@ -739,7 +739,9 @@ def fake_thread(target: object, **_kwargs: object) -> MagicMock: "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.threading.Thread", side_effect=fake_thread, ), - patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogQualityReportWindow") as mock_report, + patch( + "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogAvailabilityReportWindow" + ) as mock_report, patch( "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.UsagePopupWindow.should_display", return_value=False, @@ -807,7 +809,7 @@ def fake_thread(target: object, **_kwargs: object) -> MagicMock: "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.threading.Thread", side_effect=fake_thread, ), - patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogQualityReportWindow"), + patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogAvailabilityReportWindow"), patch( "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.UsagePopupWindow.should_display", return_value=False, @@ -931,7 +933,7 @@ def fake_thread(target: object, **_kwargs: object) -> MagicMock: "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.threading.Thread", side_effect=fake_thread, ) as mock_thread, - patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogQualityReportWindow"), + patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.LogAvailabilityReportWindow"), patch( "ardupilot_methodic_configurator.frontend_tkinter_parameter_editor.UsagePopupWindow.should_display", return_value=False,