Skip to content

Comments

Pluggable kernels#1405

Open
DTTerastar wants to merge 1 commit intomainfrom
pluggable-kernels
Open

Pluggable kernels#1405
DTTerastar wants to merge 1 commit intomainfrom
pluggable-kernels

Conversation

@DTTerastar
Copy link
Collaborator

@DTTerastar DTTerastar commented Jan 4, 2026

Summary by CodeRabbit

  • New Features
    • Added signal filtering with adaptive smoothing for 1D and 3D sensor data.
    • Introduced iterative location estimation algorithm for improved positioning accuracy.
    • Added configurable kernel-based weighting options (Gaussian, Epanechnikov, Inverse Square) for location calculations.

✏️ Tip: You can customize this high-level summary in your review settings.

@DTTerastar DTTerastar had a problem deploying to CI - release environment January 4, 2026 18:49 — with GitHub Actions Failure
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 4, 2026

📝 Walkthrough

Walkthrough

The PR introduces one-euro signal filtering for 1D and 3D points, implements kernel-based weighting (Epanechnikov, Gaussian, inverse-square), adds an iterative Nadaraya-Watson localization algorithm, and refactors the existing Nadaraya-Watson locator to use configurable kernels. Configuration structures and state wiring are updated to support these new components.

Changes

Cohort / File(s) Summary
Filtering utilities
src/Filtering/OneEuro.cs, src/Filtering/OneEuro3D.cs
New 1€ filter for 1D signals with derivative estimation and exponential smoothing; 3D wrapper applies independent filters to each axis.
Kernel interface and implementations
src/Weighting/IKernel.cs, src/Weighting/EpanechnikovKernel.cs, src/Weighting/GaussianKernel.cs, src/Weighting/InverseSquareKernel.cs
New IKernel interface and three concrete kernel implementations with configurable bandwidth/epsilon and Evaluate methods for distance-based weighting.
New iterative locator
src/Locators/IterativeNadarayaWatsonMultilateralizer.cs
New locator implementing multi-step refinement: initial Nadaraya-Watson estimate followed by iterative location and scale adjustment using kernel-based weighting; computes confidence score and room determination.
Refactored locator
src/Locators/NadarayaWatsonMultilateralizer.cs
Made public; converted to field-based design; added kernel support via constructor parameter; replaced fixed distance weighting with kernel-based weighting; updated to use private fields for device, floor, and state.
Configuration models
src/Models/Config.cs, src/Models/Config.Clone.cs
New IterativeNadarayaWatsonConfig and ConfigKernel classes with YAML support; refactored NadarayaWatsonConfig.Kernel from string to ConfigKernel; added cloning logic for new types.
Integration and wiring
src/Models/State.cs
Updated locator instantiation to pass kernel configuration to NadarayaWatsonMultilateralizer; added new code path for IterativeNadarayaWatsonMultilateralizer construction and yielding when enabled.
Configuration example
src/config.example.yaml
Updated Nadaraya-Watson kernel to inverse_square algorithm; added new iterative_nadaraya_watson locator section with Gaussian kernel and configurable iterations.
Service file
src/Services/MultiScenarioLocator.cs
Minor whitespace/encoding change in using directive (no semantic impact).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Caller
    participant Iter as IterativeNadarayaWatsonMultilateralizer
    participant NW as Nadaraya-Watson<br/>(internal)
    participant Kernel as Kernel<br/>(weighting)
    participant Scenario as Scenario<br/>(state)

    Client->>Iter: Locate(scenario)
    
    rect rgb(200, 220, 255)
    Note over Iter: Setup & Validation
    Iter->>Iter: Collect active device nodes
    alt Fewer than 2 nodes
        Iter-->>Client: return false
    end
    end
    
    rect rgb(220, 240, 220)
    Note over Iter: Initial Estimate (alpha = 1.0)
    Iter->>NW: ComputeNadarayaWatsonLocation<br/>(nodes, alpha=1.0)
    NW->>Kernel: Evaluate(distance) for each node
    Kernel-->>NW: weight values
    NW-->>Iter: initial location, error
    Iter->>Scenario: Update with initial location
    end
    
    rect rgb(240, 220, 220)
    Note over Iter: Iterative Refinement Loop
    loop for each iteration (maxIterations)
        Iter->>Iter: ComputeScaleFactor(alpha)<br/>from measured vs. geometric distances
        Iter->>NW: ComputeNadarayaWatsonLocation<br/>(nodes, new alpha)
        NW->>Kernel: Evaluate(distance) with updated weights
        Kernel-->>NW: refined weight values
        NW-->>Iter: refined location, error
        Iter->>Scenario: Update location & alpha
    end
    end
    
    rect rgb(240, 240, 200)
    Note over Iter: Final Processing
    Iter->>Iter: Compute confidence score<br/>(fixes, error, scale)
    Iter->>Scenario: Determine containing room
    Iter->>Scenario: Log diagnostics if Android device
    end
    
    alt Confidence > 0
        Iter-->>Client: return true
    else Confidence ≤ 0
        Iter-->>Client: return false
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add NadarayaWatsonMultilateralizer #754: Directly introduces the base NadarayaWatsonMultilateralizer class and related locator refactoring that this PR extends with kernel support and an iterative variant.
  • Locator configuration added #794: Modifies locator configuration and state wiring (src/Models/Config.cs and src/Models/State.cs) to integrate new locators into the overall system.
  • Review codex-anchor branch changes #1330: Refactors the kernel and weighting system infrastructure, overlapping with the IKernel interface and kernel implementation additions in this PR.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Pluggable kernels' accurately captures the main objective of this pull request, which introduces a configurable kernel system with multiple implementations (Epanechnikov, Gaussian, InverseSquare) that can be selected via configuration.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pluggable-kernels

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (4)
src/Weighting/IKernel.cs (1)

3-6: Add XML documentation to the interface.

The interface lacks XML documentation. While the implementing classes have good documentation, the interface should document the contract, expected parameter behavior (e.g., distance should be non-negative), and return value semantics.

🔎 Proposed documentation
+/// <summary>
+/// Represents a kernel function for distance-based weighting in localization algorithms.
+/// </summary>
 public interface IKernel
 {
+    /// <summary>
+    /// Evaluates the kernel function at the given distance.
+    /// </summary>
+    /// <param name="distance">The distance from the center point. Must be non-negative and finite.</param>
+    /// <returns>The kernel weight for the given distance.</returns>
+    /// <exception cref="ArgumentException">Thrown when distance is negative, NaN, or infinite.</exception>
     double Evaluate(double distance);
 }
src/Weighting/InverseSquareKernel.cs (1)

30-38: Consider handling NaN/Infinity for consistency with other kernels.

GaussianKernel.Evaluate throws ArgumentException for NaN and Infinity values (see src/Weighting/GaussianKernel.cs lines 39-43), but this kernel only checks for negative values. For consistent behavior across all kernel implementations, consider adding the same validation.

Also, distance * distance is slightly more efficient than Math.Pow(distance, 2) for squaring.

🔎 Proposed improvement
 public double Evaluate(double distance)
 {
-    if (distance < 0)
+    if (double.IsNaN(distance) || double.IsInfinity(distance))
+    {
+        throw new ArgumentException("Distance must be a finite number.", nameof(distance));
+    }
+
+    if (distance < 0)
     {
         throw new ArgumentException("Distance must be non-negative.", nameof(distance));
     }

-    return 1.0 / (Math.Pow(distance, 2) + _epsilon);
+    return 1.0 / (distance * distance + _epsilon);
 }
src/Locators/IterativeNadarayaWatsonMultilateralizer.cs (2)

110-111: Consider using a constant for the device type check and case-insensitive comparison.

The hard-coded "android" string and case-sensitive comparison could be improved for maintainability and robustness.

🔎 Suggested improvements
+        private const string AndroidDeviceType = "android";
+
         // ... (in Locate method)
-            if (_device.Id.Contains("android"))
+            if (_device.Id.Contains(AndroidDeviceType, StringComparison.OrdinalIgnoreCase))
                 Log.Information("Scenario {Scenario} confidence {Confidence}% fixes {fixes:0} error {Error:00.0} scale {Scale:0.0}", scenario.Name, scenario.Confidence,scenario.Fixes, scenario.Error, scenario.Scale);

Note: Also added a missing space after the comma in scenario.Confidence,scenario.Fixes.


67-88: Consider enabling the convergence check for early termination.

The iterative loop currently runs for exactly _maxIterations iterations, even if the algorithm converges earlier. Uncommenting the convergence check (lines 76-84) could improve performance by avoiding unnecessary iterations when alpha and location have stabilized.

Since the default is only 2 iterations, the performance gain is minimal, but it could be beneficial if users configure higher iteration counts.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1185b3 and 6c18672.

📒 Files selected for processing (13)
  • src/Filtering/OneEuro.cs
  • src/Filtering/OneEuro3D.cs
  • src/Locators/IterativeNadarayaWatsonMultilateralizer.cs
  • src/Locators/NadarayaWatsonMultilateralizer.cs
  • src/Models/Config.Clone.cs
  • src/Models/Config.cs
  • src/Models/State.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Weighting/EpanechnikovKernel.cs
  • src/Weighting/GaussianKernel.cs
  • src/Weighting/IKernel.cs
  • src/Weighting/InverseSquareKernel.cs
  • src/config.example.yaml
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Services/MultiScenarioLocator.cs
  • src/Weighting/IKernel.cs
  • src/Weighting/GaussianKernel.cs
  • src/Weighting/EpanechnikovKernel.cs
  • src/Weighting/InverseSquareKernel.cs
  • src/Locators/NadarayaWatsonMultilateralizer.cs
  • src/Filtering/OneEuro3D.cs
  • src/config.example.yaml
  • src/Locators/IterativeNadarayaWatsonMultilateralizer.cs
  • src/Models/State.cs
  • src/Models/Config.cs
  • src/Filtering/OneEuro.cs
  • src/Models/Config.Clone.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • src/Services/MultiScenarioLocator.cs
  • src/Weighting/IKernel.cs
  • src/Weighting/GaussianKernel.cs
  • src/Weighting/EpanechnikovKernel.cs
  • src/Weighting/InverseSquareKernel.cs
  • src/Locators/NadarayaWatsonMultilateralizer.cs
  • src/Filtering/OneEuro3D.cs
  • src/Locators/IterativeNadarayaWatsonMultilateralizer.cs
  • src/Models/State.cs
  • src/Models/Config.cs
  • src/Filtering/OneEuro.cs
  • src/Models/Config.Clone.cs
src/config.example.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Use src/config.example.yaml as a template and do not commit real secrets

Files:

  • src/config.example.yaml
🧬 Code graph analysis (9)
src/Weighting/IKernel.cs (3)
src/Weighting/EpanechnikovKernel.cs (1)
  • Evaluate (40-52)
src/Weighting/GaussianKernel.cs (1)
  • Evaluate (40-49)
src/Weighting/InverseSquareKernel.cs (1)
  • Evaluate (30-38)
src/Weighting/GaussianKernel.cs (3)
src/Weighting/EpanechnikovKernel.cs (1)
  • Evaluate (40-52)
src/Weighting/IKernel.cs (1)
  • Evaluate (5-5)
src/Weighting/InverseSquareKernel.cs (1)
  • Evaluate (30-38)
src/Weighting/EpanechnikovKernel.cs (3)
src/Weighting/GaussianKernel.cs (1)
  • Evaluate (40-49)
src/Weighting/IKernel.cs (1)
  • Evaluate (5-5)
src/Weighting/InverseSquareKernel.cs (1)
  • Evaluate (30-38)
src/Weighting/InverseSquareKernel.cs (3)
src/Weighting/EpanechnikovKernel.cs (1)
  • Evaluate (40-52)
src/Weighting/GaussianKernel.cs (1)
  • Evaluate (40-49)
src/Weighting/IKernel.cs (1)
  • Evaluate (5-5)
src/Filtering/OneEuro3D.cs (1)
src/Filtering/OneEuro.cs (3)
  • OneEuroFilter (3-86)
  • OneEuroFilter (20-25)
  • Filter (31-70)
src/Models/State.cs (1)
src/Locators/IterativeNadarayaWatsonMultilateralizer.cs (2)
  • IterativeNadarayaWatsonMultilateralizer (9-172)
  • IterativeNadarayaWatsonMultilateralizer (19-38)
src/Models/Config.cs (1)
src/Models/Config.Clone.cs (6)
  • IterativeNadarayaWatsonConfig (46-58)
  • IterativeNadarayaWatsonConfig (48-57)
  • ConfigKernel (73-83)
  • ConfigKernel (75-82)
  • NadarayaWatsonConfig (60-71)
  • NadarayaWatsonConfig (62-70)
src/Filtering/OneEuro.cs (1)
src/Services/DeviceService.cs (1)
  • DateTime (152-162)
src/Models/Config.Clone.cs (1)
src/Models/Config.cs (2)
  • IterativeNadarayaWatsonConfig (75-88)
  • ConfigKernel (102-109)
🪛 GitHub Actions: Build and test
src/Models/State.cs

[warning] 231-231: dotnet build -c Release --no-restore: Dereference of a possibly null reference. CS8602.


[warning] 246-246: dotnet build -c Release --no-restore: Dereference of a possibly null reference. CS8602.

src/Models/Config.Clone.cs

[warning] 15-15: dotnet build -c Release --no-restore: Possible null reference assignment. CS8601.


[warning] 16-16: dotnet build -c Release --no-restore: Possible null reference assignment. CS8601.

🪛 GitHub Check: build
src/Models/State.cs

[warning] 246-246:
Dereference of a possibly null reference.


[warning] 231-231:
Dereference of a possibly null reference.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Deploy to Docker add-on with tag
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (11)
src/Weighting/GaussianKernel.cs (1)

22-34: LGTM! Constructor correctly handles optional bandwidth configuration.

The constructor properly validates the bandwidth parameter and falls back to a reasonable default of 0.5. The normalization factor is efficiently pre-computed.

src/Weighting/EpanechnikovKernel.cs (1)

19-31: Note: Different default bandwidth from GaussianKernel.

The default bandwidth is 1.0, while GaussianKernel uses 0.5. This difference may be intentional based on the kernel characteristics, but ensure this is documented or justified.

Are these default bandwidth values intentionally different, or should they be unified for consistency?

src/config.example.yaml (1)

54-69: LGTM! Kernel configuration structure is well-designed.

The new kernel-based configuration for both nadaraya_watson and iterative_nadaraya_watson locators is clear and follows a consistent pattern. The default values (epsilon: 1e-6 for inverse_square and bandwidth: 0.5 for gaussian) align with the code defaults in the kernel implementations.

src/Models/Config.Clone.cs (3)

40-42: LGTM: New clone paths for NearestNode and IterativeNadarayaWatson.

The cloning logic correctly handles the new IterativeNadarayaWatson configuration. Since both NearestNode and IterativeNadarayaWatson properties are initialized with new() in Config.cs, they will never be null at runtime.


46-57: LGTM: Deep clone implementation for IterativeNadarayaWatsonConfig.

The clone method correctly copies all properties including the nested Kernel object via Kernel.Clone().


73-83: LGTM: ConfigKernel clone implementation.

Correctly creates a new dictionary copy of Props to ensure deep cloning.

src/Models/Config.cs (2)

70-88: LGTM: New IterativeNadarayaWatson configuration.

The new IterativeNadarayaWatsonConfig class follows the established patterns with proper YAML member aliases and sensible defaults (MaxIterations = 2, kernel initialized with new()).


90-109: LGTM: Refactored NadarayaWatsonConfig and new ConfigKernel type.

Good refactoring to extract ConfigKernel as a reusable type shared between NadarayaWatsonConfig and IterativeNadarayaWatsonConfig. The default algorithm of "gaussian" is appropriate.

src/Locators/NadarayaWatsonMultilateralizer.cs (2)

21-34: LGTM: Kernel-based weighting integration.

The constructor correctly initializes the kernel based on the configuration. The switch expression with ToLowerInvariant() ensures case-insensitive matching, and the null-conditional on kernelConfig?.Props handles the case when no configuration is provided.


68-93: LGTM: Kernel-weighted location estimation.

Good implementation of the Nadaraya-Watson estimator using the pluggable kernel. The fallback to midpoint when wSum <= 0 (which can occur with Epanechnikov kernel when all nodes are outside the bandwidth) is a sensible safeguard.

src/Locators/IterativeNadarayaWatsonMultilateralizer.cs (1)

127-146: The Node property is guaranteed non-null by type declaration, not by filtering logic.

The Node property in DeviceToNode is declared as non-nullable (public Node Node { get; }), which the type system enforces at construction. All instantiations pass non-null node values. The filter at line 44 uses null-conditional ?. defensively, but the null-forgiving operators (!) at lines 136–138 and 161 are safe because the type system—not the filter—guarantees Node cannot be null. No changes needed.

Likely an incorrect or invalid review comment.

Comment on lines +1 to +12
using System;

public class OneEuroFilter
{
private double _minCutoff; // Minimum cutoff frequency
private double _beta; // Speed coefficient for dynamic cutoff
private double _dCutoff; // Derivative (d) cutoff frequency

private bool _initialized;
private double _prevValue; // Previous filtered value
private double _prevDerivative;// Previous filtered derivative
private DateTime _lastTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing namespace declaration.

This class is in the global namespace. Add a namespace (e.g., ESPresense.Filtering) for consistency with the project structure.

The filter parameter fields should also be marked readonly since they're only set in the constructor.

🔎 Proposed fix
 using System;

+namespace ESPresense.Filtering;
+
 public class OneEuroFilter
 {
-    private double _minCutoff;     // Minimum cutoff frequency
-    private double _beta;          // Speed coefficient for dynamic cutoff
-    private double _dCutoff;       // Derivative (d) cutoff frequency
+    private readonly double _minCutoff;     // Minimum cutoff frequency
+    private readonly double _beta;          // Speed coefficient for dynamic cutoff
+    private readonly double _dCutoff;       // Derivative (d) cutoff frequency
🤖 Prompt for AI Agents
In src/Filtering/OneEuro.cs around lines 1 to 12, the OneEuroFilter class is
declared in the global namespace and several filter-parameter fields are
mutable; add a namespace declaration (ESPresense.Filtering) wrapping the class
to match project structure and change the parameter fields (_minCutoff, _beta,
_dCutoff) to readonly since they are only assigned in the constructor; keep
other members unchanged and ensure using directives and indentation remain valid
inside the new namespace.

Comment on lines +81 to +85
private static double ComputeAlpha(double cutoffFreq, double dt)
{
double tau = 1.0 / (2.0 * Math.PI * cutoffFreq);
return (float)(1.0 / (1.0 + tau / dt));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Precision loss due to incorrect cast to float.

ComputeAlpha returns a double but casts the result to float before returning, which unnecessarily loses precision. This appears to be a typo.

🔎 Proposed fix
 private static double ComputeAlpha(double cutoffFreq, double dt)
 {
     double tau = 1.0 / (2.0 * Math.PI * cutoffFreq);
-    return (float)(1.0 / (1.0 + tau / dt));
+    return 1.0 / (1.0 + tau / dt);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static double ComputeAlpha(double cutoffFreq, double dt)
{
double tau = 1.0 / (2.0 * Math.PI * cutoffFreq);
return (float)(1.0 / (1.0 + tau / dt));
}
private static double ComputeAlpha(double cutoffFreq, double dt)
{
double tau = 1.0 / (2.0 * Math.PI * cutoffFreq);
return 1.0 / (1.0 + tau / dt);
}
🤖 Prompt for AI Agents
In src/Filtering/OneEuro.cs around lines 81 to 85, the method ComputeAlpha
returns a double but currently casts the computed value to float which causes
unnecessary precision loss; remove the "(float)" cast and return the double
expression directly (e.g., return 1.0 / (1.0 + tau / dt)); ensure the method
stays typed as double and all intermediate computations remain double to
preserve precision.

Comment on lines +1 to +8
using MathNet.Spatial.Euclidean;
using System;

public class OneEuroFilter3D
{
private OneEuroFilter _filterX;
private OneEuroFilter _filterY;
private OneEuroFilter _filterZ;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing namespace declaration.

This class is defined in the global namespace, while other classes in this project (including OneEuroFilter in src/Filtering/OneEuro.cs) use the ESPresense.Filtering or similar namespaces. Add a namespace for consistency and to avoid potential naming conflicts.

Also, the private fields should be marked readonly since they're only assigned in the constructor.

🔎 Proposed fix
 using MathNet.Spatial.Euclidean;
 using System;

+namespace ESPresense.Filtering;
+
 public class OneEuroFilter3D
 {
-    private OneEuroFilter _filterX;
-    private OneEuroFilter _filterY;
-    private OneEuroFilter _filterZ;
+    private readonly OneEuroFilter _filterX;
+    private readonly OneEuroFilter _filterY;
+    private readonly OneEuroFilter _filterZ;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using MathNet.Spatial.Euclidean;
using System;
public class OneEuroFilter3D
{
private OneEuroFilter _filterX;
private OneEuroFilter _filterY;
private OneEuroFilter _filterZ;
using MathNet.Spatial.Euclidean;
using System;
namespace ESPresense.Filtering;
public class OneEuroFilter3D
{
private readonly OneEuroFilter _filterX;
private readonly OneEuroFilter _filterY;
private readonly OneEuroFilter _filterZ;
🤖 Prompt for AI Agents
In src/Filtering/OneEuro3D.cs around lines 1-8, the class is declared in the
global namespace and its private filter fields are mutable; move the class into
the appropriate namespace (e.g., ESPresense.Filtering or match existing project
namespace) by wrapping the class definition in a namespace block, and mark the
private fields _filterX, _filterY, and _filterZ as readonly since they are only
assigned in the constructor; keep existing using statements at the top.

double weightedError = (totalWeight > 0) ? (weightedSumErrors / totalWeight) : 0;
scenario.Error = weightedError;

scenario.Confidence = (int)Math.Clamp((((double)scenario.Fixes*3)) +( 100 - (Math.Pow(scenario.Error/10 ?? 1, 2) + Math.Pow(5*(1 - (scenario.Scale ?? 1)), 2))), 10, 100);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Break down the confidence calculation for readability and maintainability.

This line contains a highly complex expression with excessive parentheses, inconsistent spacing, and no explanatory comments. The formula logic is opaque, making it difficult to verify correctness or modify in the future.

🔎 Proposed refactor to improve readability
-            scenario.Confidence = (int)Math.Clamp((((double)scenario.Fixes*3)) +( 100 - (Math.Pow(scenario.Error/10 ?? 1, 2) + Math.Pow(5*(1 - (scenario.Scale ?? 1)), 2))), 10, 100);
+            // Confidence formula components:
+            // - Base score from number of fixes (more fixes = higher confidence)
+            // - Penalty for error (normalized by dividing by 10)
+            // - Penalty for scale deviation from 1.0 (ideal scale)
+            double fixesScore = (double)scenario.Fixes * 3;
+            double normalizedError = (scenario.Error ?? 0) / 10.0;
+            double scaleDeviation = 1.0 - (scenario.Scale ?? 1.0);
+            double errorPenalty = Math.Pow(normalizedError, 2) + Math.Pow(5 * scaleDeviation, 2);
+            double rawConfidence = fixesScore + (100 - errorPenalty);
+            scenario.Confidence = (int)Math.Clamp(rawConfidence, 10, 100);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scenario.Confidence = (int)Math.Clamp((((double)scenario.Fixes*3)) +( 100 - (Math.Pow(scenario.Error/10 ?? 1, 2) + Math.Pow(5*(1 - (scenario.Scale ?? 1)), 2))), 10, 100);
// Confidence formula components:
// - Base score from number of fixes (more fixes = higher confidence)
// - Penalty for error (normalized by dividing by 10)
// - Penalty for scale deviation from 1.0 (ideal scale)
double fixesScore = (double)scenario.Fixes * 3;
double normalizedError = scenario.Error / 10 ?? 1;
double scaleDeviation = 1.0 - (scenario.Scale ?? 1.0);
double errorPenalty = Math.Pow(normalizedError, 2) + Math.Pow(5 * scaleDeviation, 2);
double rawConfidence = fixesScore + (100 - errorPenalty);
scenario.Confidence = (int)Math.Clamp(rawConfidence, 10, 100);
🤖 Prompt for AI Agents
In src/Locators/IterativeNadarayaWatsonMultilateralizer.cs around line 109, the
single complex assignment to scenario.Confidence should be refactored into a
sequence of well-named intermediate variables (e.g., fixesScore, errorComponent,
scaleComponent, rawScore) with null-coalescing handled explicitly (store
scenario.Error ?? defaultError and scenario.Scale ?? 1 into locals), remove
redundant parentheses, add a brief comment explaining the formula, compute
rawScore by combining the parts, clamp the value with Math.Clamp(rawScore, 10,
100) and then cast to int for scenario.Confidence; this improves readability,
testability and makes future tweaks to each component trivial.

Log.Information("Scenario {Scenario} confidence {Confidence}% fixes {fixes:0} error {Error:00.0} scale {Scale:0.0}", scenario.Name, scenario.Confidence,scenario.Fixes, scenario.Error, scenario.Scale);
// 7. Decide whether to accept or reject
if (scenario.Confidence <= 0) return false;
if (Math.Abs(scenario.Location.DistanceTo(scenario.LastLocation)) < 0.1) return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the Scenario class definition
fd -e cs | head -20

Repository: ESPresense/ESPresense-companion

Length of output: 816


🏁 Script executed:

# Search for Scenario class definition and LastLocation property
rg -l "class Scenario" --type=cs

Repository: ESPresense/ESPresense-companion

Length of output: 97


🏁 Script executed:

# Search for LastLocation property definition and initialization patterns
rg "LastLocation" --type=cs -B 2 -A 2

Repository: ESPresense/ESPresense-companion

Length of output: 5428


🏁 Script executed:

# Read the full Scenario class definition
cat -n src/Models/Scenario.cs

Repository: ESPresense/ESPresense-companion

Length of output: 2305


🏁 Script executed:

# Read the locator method that contains the distance check in IterativeNadarayaWatsonMultilateralizer
cat -n src/Locators/IterativeNadarayaWatsonMultilateralizer.cs | head -150

Repository: ESPresense/ESPresense-companion

Length of output: 7207


🏁 Script executed:

# Find where Scenario objects are created
rg "new Scenario" --type=cs -B 2 -A 2

Repository: ESPresense/ESPresense-companion

Length of output: 9594


🏁 Script executed:

# Check KalmanLocation initialization to understand default Location
rg "class KalmanLocation" --type=cs -A 20

Repository: ESPresense/ESPresense-companion

Length of output: 3072


🏁 Script executed:

# See how Locate is typically called - check Device class to understand the flow
rg "Locate()" --type=cs -B 3 -A 3

Repository: ESPresense/ESPresense-companion

Length of output: 26011


🏁 Script executed:

# Look at MultiScenarioLocator to see how Locate() is called
cat -n src/Services/MultiScenarioLocator.cs | head -50

Repository: ESPresense/ESPresense-companion

Length of output: 2524


🏁 Script executed:

# Check if there's any first-location initialization logic
rg "UpdateLocation|ResetLocation" --type=cs -B 2 -A 2

Repository: ESPresense/ESPresense-companion

Length of output: 12234


🏁 Script executed:

# Look at the complete KalmanLocation.Location property to understand default initialization
cat -n src/Models/KalmanLocation.cs | head -50

Repository: ESPresense/ESPresense-companion

Length of output: 2332


🏁 Script executed:

# Check the AnchorLocatorTests to see how first locations are handled
cat -n tests/Locators/AnchorLocatorTests.cs | grep -A 15 "Locate_ReturnsTrueWhenLocationIsNull"

Repository: ESPresense/ESPresense-companion

Length of output: 775


🏁 Script executed:

# Check if there's any initialization pattern in scenario usage before Locate()
rg "new Scenario" --type=cs -A 5 | head -80

Repository: ESPresense/ESPresense-companion

Length of output: 4691


🏁 Script executed:

# Verify that the distance check appears in multiple locators as mentioned
rg "DistanceTo.*LastLocation.*< 0.1" --type=cs

Repository: ESPresense/ESPresense-companion

Length of output: 631


Add a guard against uninitialized LastLocation on first location attempt.

On the first Locate() call, LastLocation is uninitialized (defaults to Point3D(0,0,0)). The distance check on line 114 will reject any calculated location within 0.1 units of the origin, preventing valid first-location updates if the device happens to be localized near the origin. Consider adding a check for whether this is the first location update (e.g., LastLocation == Point3D(0,0,0) && Location == Point3D(0,0,0)) or tracking an "is initialized" flag to skip this threshold check initially.

This pattern appears across multiple locators (BaseMultilateralizer, NadarayaWatsonMultilateralizer, MultiFloorMultilateralizer).

🤖 Prompt for AI Agents
In src/Locators/IterativeNadarayaWatsonMultilateralizer.cs around line 114, the
distance check uses LastLocation which is uninitialized on first Locate()
(defaults to origin) and will incorrectly reject valid first-location updates
near the origin; add a guard that detects the first update (either by checking
if LastLocation is effectively Point3D(0,0,0) within a tiny epsilon OR by
introducing an explicit boolean initialized flag) and skip the
"too-close-to-last" threshold on that first run, then proceed to apply the
existing threshold on subsequent updates; apply the same fix pattern to the
other affected locators (BaseMultilateralizer, NadarayaWatsonMultilateralizer,
MultiFloorMultilateralizer).

Comment on lines +229 to +233
yield return new Scenario(
Config,
new NadarayaWatsonMultilateralizer(device, floor, this, _nts, nadarayaWatson.Kernel),
floor.Name
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address null reference warning (CS8602).

The compiler warns about possible null dereference of nadarayaWatson.Kernel on line 231. While the runtime logic is safe (the if condition ensures nadarayaWatson is not null when Enabled is true), the nullable flow analysis doesn't track this pattern. Use the null-forgiving operator to suppress the warning.

🔎 Proposed fix
             if (nadarayaWatson?.Enabled ?? false)
                 foreach (var floor in GetFloorsByIds(nadarayaWatson?.Floors))
                     yield return new Scenario(
                         Config,
-                        new NadarayaWatsonMultilateralizer(device, floor, this, _nts, nadarayaWatson.Kernel),
+                        new NadarayaWatsonMultilateralizer(device, floor, this, _nts, nadarayaWatson!.Kernel),
                         floor.Name
                     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
yield return new Scenario(
Config,
new NadarayaWatsonMultilateralizer(device, floor, this, _nts, nadarayaWatson.Kernel),
floor.Name
);
yield return new Scenario(
Config,
new NadarayaWatsonMultilateralizer(device, floor, this, _nts, nadarayaWatson!.Kernel),
floor.Name
);
🧰 Tools
🪛 GitHub Actions: Build and test

[warning] 231-231: dotnet build -c Release --no-restore: Dereference of a possibly null reference. CS8602.

🪛 GitHub Check: build

[warning] 231-231:
Dereference of a possibly null reference.

🤖 Prompt for AI Agents
In src/Models/State.cs around lines 229 to 233, the nullable analyzer warns
about possible null dereference of nadarayaWatson.Kernel when constructing the
Scenario; suppress the warning by using the null-forgiving operator on Kernel
(i.e., replace nadarayaWatson.Kernel with nadarayaWatson.Kernel! in the
constructor call) so the compiler knows you assert it is non-null in this
control flow.

Comment on lines +238 to +250
if (iterativeNadarayaWatson?.Enabled ?? false)
foreach (var floor in GetFloorsByIds(iterativeNadarayaWatson?.Floors))
yield return new Scenario(
Config,
new IterativeNadarayaWatsonMultilateralizer(
device,
floor,
this,
iterativeNadarayaWatson.Kernel,
iterativeNadarayaWatson.MaxIterations
),
floor.Name
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address null reference warning (CS8602).

Same issue as above—iterativeNadarayaWatson.Kernel and iterativeNadarayaWatson.MaxIterations on line 246-247 trigger null reference warnings despite the guard clause.

🔎 Proposed fix
             if (iterativeNadarayaWatson?.Enabled ?? false)
                 foreach (var floor in GetFloorsByIds(iterativeNadarayaWatson?.Floors))
                     yield return new Scenario(
                         Config,
                         new IterativeNadarayaWatsonMultilateralizer(
                             device,
                             floor,
                             this,
-                            iterativeNadarayaWatson.Kernel,
-                            iterativeNadarayaWatson.MaxIterations
+                            iterativeNadarayaWatson!.Kernel,
+                            iterativeNadarayaWatson.MaxIterations
                         ),
                         floor.Name
                     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (iterativeNadarayaWatson?.Enabled ?? false)
foreach (var floor in GetFloorsByIds(iterativeNadarayaWatson?.Floors))
yield return new Scenario(
Config,
new IterativeNadarayaWatsonMultilateralizer(
device,
floor,
this,
iterativeNadarayaWatson.Kernel,
iterativeNadarayaWatson.MaxIterations
),
floor.Name
);
if (iterativeNadarayaWatson?.Enabled ?? false)
foreach (var floor in GetFloorsByIds(iterativeNadarayaWatson?.Floors))
yield return new Scenario(
Config,
new IterativeNadarayaWatsonMultilateralizer(
device,
floor,
this,
iterativeNadarayaWatson!.Kernel,
iterativeNadarayaWatson.MaxIterations
),
floor.Name
);
🧰 Tools
🪛 GitHub Actions: Build and test

[warning] 246-246: dotnet build -c Release --no-restore: Dereference of a possibly null reference. CS8602.

🪛 GitHub Check: build

[warning] 246-246:
Dereference of a possibly null reference.

🤖 Prompt for AI Agents
In src/Models/State.cs around lines 238-250, the null-check using the
conditional operator doesn’t convince the compiler that iterativeNadarayaWatson
is non-null when accessing Kernel and MaxIterations; replace the current guard
with a local non-null variable and an explicit check (e.g. var cfg =
iterativeNadarayaWatson; if (cfg != null && cfg.Enabled) foreach (...) yield
return new Scenario(... cfg.Kernel, cfg.MaxIterations ...)); this ensures the
compiler knows cfg is not null and removes the CS8602 warning.

@@ -1,4 +1,4 @@
using ESPresense.Controllers;
using ESPresense.Controllers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate BOM characters.

Line 1 contains duplicate UTF-8 BOM (Byte Order Mark) characters () before the using directive. This is likely an encoding artifact that should be cleaned up.

🔎 Proposed fix
-using ESPresense.Controllers;
+using ESPresense.Controllers;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using ESPresense.Controllers;
using ESPresense.Controllers;
🤖 Prompt for AI Agents
In src/Services/MultiScenarioLocator.cs around line 1, the file contains
duplicate UTF-8 BOM characters (visible as leading ``) before the `using`
directive; remove the extraneous BOM bytes so the file starts directly with the
`using` statement and ensure the file is saved in UTF-8 without a BOM (or with a
single BOM if your convention requires it) to eliminate the encoding artifact.

Comment on lines +40 to +52
public double Evaluate(double distance)
{
double u = distance / _bandwidth;

// Outside the range [-1, 1], return 0
if (Math.Abs(u) >= 1.0)
return 0.0;

// Standard Epanechnikov shape
// If you want to maintain integral=1, also divide by _bandwidth
double value = 0.75 * (1.0 - (u * u)) / _bandwidth;
return value;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation to prevent invalid distance values.

The Evaluate method performs no input validation, allowing NaN, Infinity, or negative distances to pass through. This is inconsistent with the other kernel implementations:

  • GaussianKernel validates for NaN/Infinity (lines 42-45)
  • InverseSquareKernel validates for negative distances (lines 29-32)

Without validation, invalid inputs could produce incorrect results or propagate errors silently through the localization pipeline.

🔎 Proposed fix for input validation
 public double Evaluate(double distance)
 {
+    if (distance < 0)
+    {
+        throw new ArgumentException("Distance must be non-negative.", nameof(distance));
+    }
+
+    if (double.IsNaN(distance) || double.IsInfinity(distance))
+    {
+        throw new ArgumentException("Distance must be a finite number.", nameof(distance));
+    }
+
     double u = distance / _bandwidth;

     // Outside the range [-1, 1], return 0
     if (Math.Abs(u) >= 1.0)
         return 0.0;

     // Standard Epanechnikov shape
     // If you want to maintain integral=1, also divide by _bandwidth
     double value = 0.75 * (1.0 - (u * u)) / _bandwidth;
     return value;
 }
🤖 Prompt for AI Agents
In src/Weighting/EpanechnikovKernel.cs around lines 40 to 52, add input
validation at the top of Evaluate to match other kernels: check if distance is
NaN or Infinity and throw an ArgumentException (or similar) and check if
distance is negative and throw an ArgumentOutOfRangeException (or similar); keep
these checks before computing u and retain the existing boundary check and
kernel formula so invalid values are rejected early and valid calculations
remain unchanged.

Comment on lines +40 to +49
public double Evaluate(double distance)
{
if (double.IsNaN(distance) || double.IsInfinity(distance))
{
throw new ArgumentException("Distance must be a finite number.", nameof(distance));
}

double u = distance / _bandwidth;
return Math.Exp(-0.5 * u * u) / _normalizationFactor;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add negative distance validation for consistency.

The method validates for NaN and Infinity but doesn't check for negative distances. InverseSquareKernel (lines 29-32) explicitly validates that distance is non-negative, and distances should semantically be non-negative across all kernel implementations.

🔎 Proposed fix for consistent input validation
 public double Evaluate(double distance)
 {
+    if (distance < 0)
+    {
+        throw new ArgumentException("Distance must be non-negative.", nameof(distance));
+    }
+
     if (double.IsNaN(distance) || double.IsInfinity(distance))
     {
         throw new ArgumentException("Distance must be a finite number.", nameof(distance));
     }

     double u = distance / _bandwidth;
     return Math.Exp(-0.5 * u * u) / _normalizationFactor;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public double Evaluate(double distance)
{
if (double.IsNaN(distance) || double.IsInfinity(distance))
{
throw new ArgumentException("Distance must be a finite number.", nameof(distance));
}
double u = distance / _bandwidth;
return Math.Exp(-0.5 * u * u) / _normalizationFactor;
}
public double Evaluate(double distance)
{
if (distance < 0)
{
throw new ArgumentException("Distance must be non-negative.", nameof(distance));
}
if (double.IsNaN(distance) || double.IsInfinity(distance))
{
throw new ArgumentException("Distance must be a finite number.", nameof(distance));
}
double u = distance / _bandwidth;
return Math.Exp(-0.5 * u * u) / _normalizationFactor;
}
🤖 Prompt for AI Agents
In src/Weighting/GaussianKernel.cs around lines 40 to 49, add validation that
distance is non-negative to match other kernels: if distance < 0, throw an
ArgumentOutOfRangeException (include a clear message and the parameter name
"distance"); keep the existing NaN/Infinity checks and then compute u and return
the kernel value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant