Skip to content

Comments

Fancy Optimize#1392

Open
DTTerastar wants to merge 1 commit intomainfrom
fancy-optimize
Open

Fancy Optimize#1392
DTTerastar wants to merge 1 commit intomainfrom
fancy-optimize

Conversation

@DTTerastar
Copy link
Collaborator

@DTTerastar DTTerastar commented Dec 17, 2025

Summary by CodeRabbit

  • New Features

    • Added a new RxTxAdjRssi optimizer for improved joint Rx/Tx parameter tuning.
    • Optimizers now use a configurable TX RSSI reference (runtime value) instead of a fixed constant.
  • Bug Fixes

    • Improved error handling, logging, and early-exit behavior for insufficient data.
    • Results are clamped to configured bounds to ensure stable outputs.
  • Chores

    • Updated default TX RSSI bounds: TxRefRssiMin -80, TxRefRssiMax -40.

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

@DTTerastar DTTerastar had a problem deploying to CI - release environment December 17, 2025 04:43 — with GitHub Actions Failure
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

📝 Walkthrough

Walkthrough

This PR adds a new Rx/Tx RSSI optimizer, refactors CombinedOptimizer with enhanced logging and error handling, replaces hardcoded -59 RSSI constants with a configurable txRefRssi across optimizers, and adjusts TxRefRssiMin/Max defaults in configuration.

Changes

Cohort / File(s) Summary
Configuration Updates
src/Models/Config.cs
Changed defaults for config bounds: TxRefRssiMin from -70-80, TxRefRssiMax from -50-40.
New Optimizer
src/Optimizers/RxTxAdjRssiOptimizer.cs
Added RxTxAdjRssiOptimizer implementing IOptimizer: builds per-node parameters (Rx adj RSSI, absorption; Tx ref RSSI), defines asymmetric distance error + regularization, finite-difference gradient, and solves with bounded BFGS; clamps results and logs errors.
Orchestration & Logging
src/Optimizers/CombinedOptimizer.cs
Introduced logger, early-exit for insufficient nodes, richer error handling and logging, refactored flow into CreateDeviceParamsFromVector/CalculateError, and switched result population to use device-parameter dictionaries.
TxRefRssi Parameterization
src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs, src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
Replaced hardcoded -59 reference with runtime/configurable txRefRssi in distance and RSSI prediction calculations.
Minor
src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs
Added trailing newline only (no functional change).

Sequence Diagram

sequenceDiagram
    participant Caller
    participant CombinedOptimizer
    participant RxTxAdjRssiOptimizer
    participant BfgsBMinimizer

    Caller->>CombinedOptimizer: Optimize(snapshot)
    alt enough nodes
        CombinedOptimizer->>RxTxAdjRssiOptimizer: Optimize(snapshot)
        activate RxTxAdjRssiOptimizer
        Note over RxTxAdjRssiOptimizer: collect measurements<br/>build param vector & bounds
        RxTxAdjRssiOptimizer->>BfgsBMinimizer: Solve(objective, gradient, bounds)
        activate BfgsBMinimizer
        BfgsBMinimizer-->>RxTxAdjRssiOptimizer: optimized vector + error
        deactivate BfgsBMinimizer
        RxTxAdjRssiOptimizer->>CombinedOptimizer: OptimizationResults
        deactivate RxTxAdjRssiOptimizer
        CombinedOptimizer->>CombinedOptimizer: CreateDeviceParamsFromVector()
        CombinedOptimizer->>CombinedOptimizer: CalculateError(deviceParams)
        CombinedOptimizer-->>Caller: final results
    else insufficient nodes
        CombinedOptimizer-->>Caller: early-exit default results (logs)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title "Fancy Optimize" is vague and generic, using non-descriptive language that does not convey meaningful information about the substantial technical changes in the changeset. Replace with a more specific title that describes the primary change, such as 'Add RxTxAdjRssiOptimizer and refactor optimization pipeline' or 'Introduce new RSSI parameter optimization with improved device parameter handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fancy-optimize

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Optimizers/CombinedOptimizer.cs (1)

137-268: Critical: OptimizeNodeAbsorptions method has severe syntax errors preventing compilation.

This method has multiple overlapping code fragments that break the build. The pipeline failures confirm errors at lines 174, 180, 203, 204, 213, and others.

Key issues:

  1. Lines 140-173: Incomplete objective function lambda with mixed code
  2. Lines 175-212: Malformed gradient function with duplicate variable declarations (gradient declared at 183 and 188)
  3. Lines 216 and 229: Duplicate solver declarations
  4. Missing/mismatched braces throughout
  5. Method signature says it returns Dictionary<string, double> but the code attempts to return a tuple at line 268

This method needs to be completely rewritten. Here's a structural outline:

private (Dictionary<string, DeviceParameters> DeviceParams, double Error) OptimizeNodeAbsorptions(
    List<Measure> allNodes, 
    List<string> uniqueDeviceIds,
    Dictionary<string, double> rxAdjRssiDict, 
    Dictionary<(string, string), double> pathAbsorptionDict, 
    ConfigOptimization optimization, 
    Dictionary<string, NodeSettings> existingSettings)
{
    // Build initial guess vector
    var initialGuess = Vector<double>.Build.Dense(uniqueDeviceIds.Count * 2);
    for (int i = 0; i < uniqueDeviceIds.Count; i++)
    {
        existingSettings.TryGetValue(uniqueDeviceIds[i], out var nodeSettings);
        initialGuess[i] = Math.Clamp(nodeSettings?.Calibration?.RxAdjRssi ?? 0, 
            optimization.RxAdjRssiMin, optimization.RxAdjRssiMax);
        initialGuess[i + uniqueDeviceIds.Count] = Math.Clamp(
            nodeSettings?.Calibration?.Absorption ?? 3.0,
            optimization.AbsorptionMin, optimization.AbsorptionMax);
    }

    // Define objective with gradient
    var objGradient = ObjectiveFunction.Gradient(
        x => {
            var deviceParams = CreateDeviceParamsFromVector(x, uniqueDeviceIds, optimization);
            return CalculateError(allNodes, deviceParams);
        },
        x => {
            var gradient = Vector<double>.Build.Dense(x.Count);
            double h = 1e-5;
            var deviceParams = CreateDeviceParamsFromVector(x, uniqueDeviceIds, optimization);
            double baseError = CalculateError(allNodes, deviceParams);

            for (int i = 0; i < x.Count; i++)
            {
                var xPlus = x.Clone();
                xPlus[i] += h;
                var paramsPlus = CreateDeviceParamsFromVector(xPlus, uniqueDeviceIds, optimization);
                gradient[i] = (CalculateError(allNodes, paramsPlus) - baseError) / h;
            }
            return gradient;
        }
    );

    try
    {
        var solver = new ConjugateGradientMinimizer(1e-3, 1000);
        var result = solver.FindMinimum(objGradient, initialGuess);
        
        var deviceParams = CreateDeviceParamsFromVector(result.MinimizingPoint, uniqueDeviceIds, optimization);
        return (deviceParams, result.FunctionInfoAtMinimum.Value);
    }
    catch (Exception ex)
    {
        _logger.Error(ex, "Optimization failed");
        // Return defaults...
    }
}
🧹 Nitpick comments (2)
src/Optimizers/RxTxAdjRssiOptimizer.cs (2)

224-227: Hardcoded initial guess may fall outside configured bounds.

The initial txRefRssi is hardcoded to -59, but the bounds are configurable via TxRefRssiMin/TxRefRssiMax. If the bounds don't include -59, this could cause issues with the optimizer.

Apply a consistent approach using bounds midpoint like you did for absorption:

         foreach (var txId in uniqueTxIds)
         {
-            initialGuess[txIndexMap[txId]] = -59;
+            initialGuess[txIndexMap[txId]] = (optimization.TxRefRssiMin + optimization.TxRefRssiMax) / 2.0;
         }

263-266: Use named placeholder for structured logging.

Serilog prefers named placeholders for better structured logging support.

         catch (Exception ex)
         {
-            Log.Error("Error optimizing all nodes: {0}", ex.Message);
+            Log.Error(ex, "Error optimizing all nodes");
         }

Passing the exception as the first argument allows Serilog to capture the full stack trace.

📜 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 129857d.

📒 Files selected for processing (6)
  • src/Models/Config.cs (1 hunks)
  • src/Optimizers/CombinedOptimizer.cs (7 hunks)
  • src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs (1 hunks)
  • src/Optimizers/RxTxAdjRssiOptimizer.cs (1 hunks)
  • src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs (2 hunks)
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
  • src/Optimizers/RxTxAdjRssiOptimizer.cs
  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.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/Optimizers/GlobalAbsorptionRxTxOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
  • src/Optimizers/RxTxAdjRssiOptimizer.cs
  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs
🧠 Learnings (1)
📚 Learning: 2025-09-16T00:51:01.127Z
Learnt from: CR
Repo: ESPresense/ESPresense-companion PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T00:51:01.127Z
Learning: Applies to src/**/Controllers/StateController.cs : StateController must populate Device.ConfiguredRefRssi from DeviceSettings during API calls

Applied to files:

  • src/Models/Config.cs
🧬 Code graph analysis (2)
src/Optimizers/RxTxAdjRssiOptimizer.cs (2)
src/Models/Config.cs (1)
  • Config (8-58)
src/Models/OptimizationSnapshot.cs (1)
  • Measure (40-57)
src/Optimizers/CombinedOptimizer.cs (1)
src/Models/OptimizationSnapshot.cs (1)
  • Measure (40-57)
🪛 GitHub Actions: Build and test
src/Optimizers/CombinedOptimizer.cs

[error] 174-174: CS1513: } expected

🪛 GitHub Actions: Deploy to Docker
src/Optimizers/CombinedOptimizer.cs

[error] 174-174: /App/src/Optimizers/CombinedOptimizer.cs(174,14): error CS1513: } expected


[error] 180-180: /App/src/Optimizers/CombinedOptimizer.cs(180,17): error CS1524: Expected catch or finally


[error] 203-203: /App/src/Optimizers/CombinedOptimizer.cs(203,18): error CS1002: ; expected


[error] 204-204: /App/src/Optimizers/CombinedOptimizer.cs(204,34): error CS1026: ) expected


[error] 204-204: /App/src/Optimizers/CombinedOptimizer.cs(204,36): error CS1002: ; expected


[error] 213-213: /App/src/Optimizers/CombinedOptimizer.cs(213,9): error CS1519: Invalid token ')' in class, record, struct, or interface member declaration


[error] 220-220: /App/src/Optimizers/CombinedOptimizer.cs(220,9): error CS1519: Invalid token 'for' in class, record, struct, or interface member declaration


[error] 220-220: /App/src/Optimizers/CombinedOptimizer.cs(220,20): error CS8124: Tuple must contain at least two elements


[error] 220-220: /App/src/Optimizers/CombinedOptimizer.cs(220,34): error CS1026: ) expected


[error] 226-226: /App/src/Optimizers/CombinedOptimizer.cs(226,29): error CS1519: Invalid token '=' in class, record, struct, or interface member declaration


[error] 226-226: /App/src/Optimizers/CombinedOptimizer.cs(226,41): error CS1519: Invalid token '(' in class, record, struct, or interface member declaration


[error] 226-226: /App/src/Optimizers/CombinedOptimizer.cs(226,55): error CS8124: Tuple must contain at least two elements


[error] 226-226: /App/src/Optimizers/CombinedOptimizer.cs(226,98): error CS1026: ) expected


[error] 239-239: /App/src/Optimizers/CombinedOptimizer.cs(239,26): error CS1002: ; expected


[error] 239-239: /App/src/Optimizers/CombinedOptimizer.cs(239,28): error CS1001: Identifier expected


[error] 241-241: /App/src/Optimizers/CombinedOptimizer.cs(241,31): error CS1001: Identifier expected


[error] 255-255: /App/src/Optimizers/CombinedOptimizer.cs(255,9): error CS1022: Type or namespace definition, or end-of-file expected


[error] 269-269: /App/src/Optimizers/CombinedOptimizer.cs(269,5): error CS1022: Type or namespace definition, or end-of-file expected


[error] 271-271: /App/src/Optimizers/CombinedOptimizer.cs(271,5): error CS0106: The modifier 'private' is not valid for this item


[error] 299-299: /App/src/Optimizers/CombinedOptimizer.cs(299,5): error CS0106: The modifier 'private' is not valid for this item


[error] 348-348: /App/src/Optimizers/CombinedOptimizer.cs(348,1): error CS1022: Type or namespace definition, or end-of-file expected

🪛 GitHub Check: build
src/Optimizers/CombinedOptimizer.cs

[failure] 213-213:
Invalid token ')' in a member declaration


[failure] 204-204:
} expected


[failure] 204-204:
; expected


[failure] 204-204:
; expected


[failure] 204-204:
) expected


[failure] 203-203:
} expected


[failure] 203-203:
; expected

⏰ 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). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (4)
src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs (1)

341-341: LGTM!

This is a cosmetic change adding a newline at the end of the file, which is a good practice for version control.

src/Models/Config.cs (1)

137-138: Verify the widened TxRefRssi bounds are intentional.

The default range has been expanded from [-70, -50] (20 dBm) to [-80, -40] (40 dBm). This doubles the optimization search space for TxRefRssi.

Ensure this change is intentional and won't cause optimization instability or convergence issues with the wider bounds.

src/Optimizers/RxTxAdjRssiOptimizer.cs (2)

12-21: LGTM!

Clean class structure with proper constructor injection and property implementation.


65-89: LGTM!

Good use of inverse variance weighting with appropriate safeguards against extreme weights and proper normalization.

Comment on lines +271 to +297
private Dictionary<string, DeviceParameters> CreateDeviceParamsFromVector(Vector<double> x, List<string> uniqueDeviceIds, ConfigOptimization optimization)
{
return nodes.Select(n =>
var deviceParams = new Dictionary<string, DeviceParameters>();

for (int i = 0; i < uniqueDeviceIds.Count; i++)
{
var distance = n.Rx.Location.DistanceTo(n.Tx.Location);
var rxAdjRssi = rxAdjRssiDict[n.Rx.Id];
var txAdjRssi = rxAdjRssiDict[n.Tx.Id];
double absorption;
var rxAdjRssi = x[i];
var absorption = x[i + uniqueDeviceIds.Count];

if (pathAbsorptionDict != null)
// Enforce constraints by clamping values to valid ranges
rxAdjRssi = Math.Clamp(rxAdjRssi,
optimization?.RxAdjRssiMin ?? -20,
optimization?.RxAdjRssiMax ?? 20);

absorption = Math.Clamp(absorption,
optimization?.AbsorptionMin ?? 1.5,
optimization?.AbsorptionMax ?? 4.5);

deviceParams[uniqueDeviceIds[i]] = new DeviceParameters
{
var pathKey = (Min(n.Rx.Id, n.Tx.Id), Max(n.Rx.Id, n.Tx.Id));
absorption = pathAbsorptionDict[pathKey];
}
else if (nodeAbsorptionDict != null)
RxAdjRssi = rxAdjRssi,
Absorption = absorption
};
}

return deviceParams;
}
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 | 🔴 Critical

Missing DeviceParameters class definition.

The DeviceParameters class is used throughout but is not defined in this file. Either define it as a nested class or ensure it exists in the Models namespace.

Add the class definition (as a nested class or in a separate file):

private class DeviceParameters
{
    public double RxAdjRssi { get; set; }
    public double Absorption { get; set; }
    public double? TxRefRssi { get; set; }  // Add for txRefRssi support
}
🧰 Tools
🪛 GitHub Actions: Deploy to Docker

[error] 271-271: /App/src/Optimizers/CombinedOptimizer.cs(271,5): error CS0106: The modifier 'private' is not valid for this item

🤖 Prompt for AI Agents
In src/Optimizers/CombinedOptimizer.cs around lines 271 to 297, the
DeviceParameters type used to build deviceParams is missing; add a
DeviceParameters definition (either as a private nested class in this file or as
a public/internal class in the Models namespace) with at least the properties
double RxAdjRssi { get; set; }, double Absorption { get; set; } and a nullable
double? TxRefRssi { get; set; } so callers compile and txRefRssi support is
available.

Comment on lines +327 to +328
// Calculate distance based on RSSI
var calculatedDistance = Math.Pow(10, (-59 + rxAdjRssi + txAdjRssi - node.Rssi) / (10.0d * absorption));
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

Inconsistency: Hardcoded -59 contradicts the PR objective.

The PR aims to replace hardcoded RSSI constants with configurable txRefRssi, but this calculation still uses the hardcoded value -59. For consistency with other optimizers, consider using the transmitter's TxRefRssi parameter.

-                // Calculate distance based on RSSI
-                var calculatedDistance = Math.Pow(10, (-59 + rxAdjRssi + txAdjRssi - node.Rssi) / (10.0d * absorption));
+                // Calculate distance based on RSSI using txRefRssi from tx parameters
+                var txRefRssi = txParams.TxRefRssi ?? -59;
+                var calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi + txAdjRssi - node.Rssi) / (10.0d * absorption));

Note: This requires DeviceParameters to include a TxRefRssi property, which appears to be missing from the current definition.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Optimizers/CombinedOptimizer.cs around lines 327-328, the distance
calculation still uses a hardcoded -59 RSSI constant which contradicts the PR
goal to use configurable transmitter reference RSSI. Replace the -59 literal
with the transmitter reference value (e.g., node.DeviceParameters.TxRefRssi or
the appropriate TxRefRssi field on the transmitter/DeviceParameters object),
add/ensure a TxRefRssi property exists on DeviceParameters, and handle
missing/null values by falling back to a sensible default or by validating and
throwing a clear exception; update any callers/tests that construct
DeviceParameters to provide TxRefRssi.

Comment on lines +25 to +35
var or = new OptimizationResults();
var optimization = _state.Config?.Optimization;

// Group all valid measurements
var allRxNodes = os.ByRx().SelectMany(g => g).ToList();

if (allRxNodes.Count < 3)
{
Log.Warning("Not enough valid measurements for optimization");
return or;
}
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

Potential NullReferenceException if Config is null.

The optimization variable is accessed throughout the method (lines 62, 203-206, 212-213, 243-244, 256) without null checks. If _state.Config is null, this will throw.

Apply this diff to add null safety:

         var or = new OptimizationResults();
         var optimization = _state.Config?.Optimization;
+        if (optimization == null)
+        {
+            Log.Warning("Configuration not available for optimization");
+            return or;
+        }

         // Group all valid measurements
-        var allRxNodes = os.ByRx().SelectMany(g => g).ToList();
+        var allMeasurements = os.ByRx().SelectMany(g => g).ToList();

-        if (allRxNodes.Count < 3)
+        if (allMeasurements.Count < 3)
         {
             Log.Warning("Not enough valid measurements for optimization");
             return or;
         }

Also consider renaming allRxNodes to allMeasurements throughout the file for clarity, as it contains all measurements, not just Rx nodes.

🤖 Prompt for AI Agents
In src/Optimizers/RxTxAdjRssiOptimizer.cs around lines 25-35, the code grabs
_state.Config?.Optimization into a local but doesn't handle a null
Config/Optimization anywhere else (used later at lines 62, 203-206, 212-213,
243-244, 256) which can cause NullReferenceExceptions; add an explicit
null-safety check right after retrieving optimization (e.g., if
(_state?.Config?.Optimization == null) { Log.Warning("Optimization config
missing"); return or set default Optimization values }) and update all
subsequent uses to rely on this guarded variable; also rename the local
allRxNodes to allMeasurements and replace that identifier throughout the file to
better reflect it contains all measurements, not just Rx nodes.

Comment on lines +120 to +125
double rxAdjRssi = x[rxBaseIndex];
double absorption = x[rxBaseIndex + 1];
double txRefRssi = x[txBaseIndex];

double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * absorption));
double mapDistance = node.Rx.Location.DistanceTo(node.Tx.Location);
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

Potential division by zero if absorption approaches zero.

The distance calculation (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * absorption) will fail if absorption is zero. While bounds should prevent this, consider adding a defensive check.

                     double absorption = x[rxBaseIndex + 1];
                     double txRefRssi = x[txBaseIndex];

-                    double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * absorption));
+                    double safeAbsorption = Math.Max(absorption, 0.1);
+                    double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * safeAbsorption));

The same pattern applies in the gradient calculation (lines 166, 177).

📝 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
double rxAdjRssi = x[rxBaseIndex];
double absorption = x[rxBaseIndex + 1];
double txRefRssi = x[txBaseIndex];
double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * absorption));
double mapDistance = node.Rx.Location.DistanceTo(node.Tx.Location);
double rxAdjRssi = x[rxBaseIndex];
double absorption = x[rxBaseIndex + 1];
double txRefRssi = x[txBaseIndex];
double safeAbsorption = Math.Max(absorption, 0.1);
double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * safeAbsorption));
double mapDistance = node.Rx.Location.DistanceTo(node.Tx.Location);

for (int i = 0; i < nodesToUse.Length; i++)
{
double dist = Math.Pow(10, (-59 + x[0] - nodesToUse[i].Node.Rssi) / (10.0 * fixedAbsorption));
double dist = Math.Pow(10, (txRefRssi + x[0] - nodesToUse[i].Node.Rssi) / (10.0 * fixedAbsorption));
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 | 🔴 Critical

Critical: txRefRssi is undefined and will cause a compilation error.

The variable txRefRssi is used in the Stage 1 distance calculation but is never declared in this file. This applies to all three usages (lines 73, 100, and 102).

Define txRefRssi before the Stage 1 optimization block, retrieving it from the node settings or using a default:

             Log.Information("Bounds: RxAdj [{0}, {1}], Absorption [{2}, {3}]", rxAdjMin, rxAdjMax, absorptionMin, absorptionMax);

             try
             {
+                // Get txRefRssi from node settings or use default
+                double txRefRssi = nodeSettings?.Calibration?.TxRefRssi ?? -59;
+
                 // Stage 1: Optimize RxAdjRssi with fixed absorption, close nodes only
                 double fixedAbsorption = absorptionMiddle;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs around lines 73, 100 and
102, txRefRssi is referenced but not declared which causes a compilation error;
declare and initialize txRefRssi before the Stage 1 optimization block by
reading it from the relevant node/settings (e.g., nodesToUse[i].Node or a
config/settings object) and fall back to a sensible default value if not
present, then use that variable in the existing distance calculations so the
references at lines 73, 100 and 102 compile and behave predictably.

Comment on lines 58 to 62
double Distance(Vector<double> x, Measure dn)
{
double exponent = (-59 + x[0] - dn.Rssi) / (10.0d * x[1]);
double exponent = (txRefRssi + x[0] - dn.Rssi) / (10.0d * x[1]);
return (x[1] > 0 && !double.IsInfinity(exponent)) ? Math.Pow(10, exponent) : double.MaxValue;
}
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 | 🔴 Critical

Critical: txRefRssi is undefined and will cause a compilation error.

The variable txRefRssi is used in the exponent calculation but is never declared or initialized in this scope. This will fail to compile.

Based on the PR context (replacing hardcoded -59 with configurable txRefRssi), you likely need to retrieve this from the node settings or optimization config:

+            double txRefRssi = nodeSettings?.Calibration?.TxRefRssi ?? -59;
+
             double Distance(Vector<double> x, Measure dn)
             {
                 double exponent = (txRefRssi + x[0] - dn.Rssi) / (10.0d * x[1]);
                 return (x[1] > 0 && !double.IsInfinity(exponent)) ? Math.Pow(10, exponent) : double.MaxValue;
             }

Alternatively, if txRefRssi should come from the transmitter device, you may need to retrieve it per-measurement from the Measure object (e.g., dn.RefRssi or similar).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs around lines 58 to
62, the local function uses txRefRssi which is undefined and causes a compile
error; fix it by obtaining txRefRssi from the correct source and wiring it into
this scope — either read it from the optimizer/config (add a field or method
parameter and pass the configured txRefRssi into the function) or pull it from
the Measure if that object contains the reference RSSI (e.g., use dn.RefRssi
per-measurement); update the Distance signature/closure accordingly so txRefRssi
is defined and used instead of the missing identifier.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Optimizers/CombinedOptimizer.cs (1)

140-213: Critical: Severe syntax errors causing build failure.

The OptimizeNodeAbsorptions method has catastrophic syntax issues that prevent compilation. The code appears to have overlapping/interleaved blocks from an incomplete refactor:

  1. Lines 140-161: Initial guess calculation starts but never completes properly
  2. Lines 162-174: An objective function lambda begins but structure is broken
  3. Lines 175-213: A gradient function is partially defined with mismatched braces
  4. Line 174: Pipeline error reports CS1513: } expected

The method signature returns Dictionary<string, double> but the code attempts to return tuples and has multiple var solver declarations.

This file needs to be completely restructured. The intended flow appears to be:

  1. Create device parameters from optimization vector
  2. Calculate error using those parameters
  3. Run gradient-based optimization
  4. Return results

Consider reverting to a working state and applying changes incrementally.

♻️ Duplicate comments (5)
src/Optimizers/RxTxAdjRssiOptimizer.cs (1)

120-125: Potential division by zero if absorption approaches zero.

While bounds should prevent this, add a defensive check to avoid runtime errors:

Proposed fix
                     double rxAdjRssi = x[rxBaseIndex];
                     double absorption = x[rxBaseIndex + 1];
                     double txRefRssi = x[txBaseIndex];

-                    double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * absorption));
+                    double safeAbsorption = Math.Max(absorption, 0.1);
+                    double calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi - node.Rssi) / (10.0 * safeAbsorption));

Apply the same pattern in the gradient calculation at lines 164-166 and 174-177.

src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs (1)

58-62: Critical: txRefRssi is undefined and will cause a compilation error.

The variable txRefRssi is used in the Distance function but is never declared in this scope. This will fail to compile.

Based on the PR context, you need to retrieve txRefRssi from the node settings or use a default value:

Proposed fix
+            double txRefRssi = nodeSettings?.Calibration?.TxRefRssi ?? -59;
+
             double Distance(Vector<double> x, Measure dn)
             {
                 double exponent = (txRefRssi + x[0] - dn.Rssi) / (10.0d * x[1]);
                 return (x[1] > 0 && !double.IsInfinity(exponent)) ? Math.Pow(10, exponent) : double.MaxValue;
             }
src/Optimizers/CombinedOptimizer.cs (3)

48-56: Critical: deviceParams is undefined in this scope.

The variable deviceParams is referenced at line 48 but is never declared. The method OptimizeNodeAbsorptions returns Dictionary<string, double> (node absorptions), not a device parameters dictionary.

This was flagged in a previous review. The optimization flow needs restructuring to properly capture and use the device parameters.


271-297: Missing DeviceParameters class definition.

The DeviceParameters class is used throughout but is not defined. Add a nested class or create it in the Models namespace:

Proposed fix
private class DeviceParameters
{
    public double RxAdjRssi { get; set; }
    public double Absorption { get; set; }
    public double? TxRefRssi { get; set; }
}

327-328: Hardcoded -59 contradicts the PR objective.

The PR aims to replace hardcoded RSSI constants with configurable txRefRssi, but this calculation still uses -59. For consistency with other optimizers, use the transmitter's TxRefRssi parameter.

Proposed fix
-                var calculatedDistance = Math.Pow(10, (-59 + rxAdjRssi + txAdjRssi - node.Rssi) / (10.0d * absorption));
+                var txRefRssi = txParams.TxRefRssi ?? -59;
+                var calculatedDistance = Math.Pow(10, (txRefRssi + rxAdjRssi + txAdjRssi - node.Rssi) / (10.0d * absorption));

Note: This requires DeviceParameters to include a TxRefRssi property.

🧹 Nitpick comments (1)
src/Optimizers/RxTxAdjRssiOptimizer.cs (1)

216-227: Consider using existing settings for initial guesses.

The initial txRefRssi guess is hardcoded to -59. Since the method should accept existingSettings, leverage it for better convergence:

Proposed fix
         // Initialize with a reasonable guess (ensure within bounds)
         var initialGuess = Vector<double>.Build.Dense(totalParams);
         foreach (var rxId in uniqueRxIds)
         {
             int baseIndex = rxIndexMap[rxId];
-            initialGuess[baseIndex] = 0; // initial rxAdjRssi
+            existingSettings.TryGetValue(rxId, out var rxSettings);
+            initialGuess[baseIndex] = Math.Clamp(
+                rxSettings?.Calibration?.RxAdjRssi ?? 0,
+                optimization.RxAdjRssiMin,
+                optimization.RxAdjRssiMax);
             initialGuess[baseIndex + 1] = ((optimization.AbsorptionMax - optimization.AbsorptionMin) / 2.0) + optimization.AbsorptionMin;
         }
         foreach (var txId in uniqueTxIds)
         {
-            initialGuess[txIndexMap[txId]] = -59;
+            existingSettings.TryGetValue(txId, out var txSettings);
+            initialGuess[txIndexMap[txId]] = Math.Clamp(
+                txSettings?.Calibration?.TxRefRssi ?? -59,
+                optimization.TxRefRssiMin,
+                optimization.TxRefRssiMax);
         }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 129857d and f1a2713.

📒 Files selected for processing (6)
  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs
  • src/Optimizers/RxTxAdjRssiOptimizer.cs
  • src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Optimizers/TwoStageRxAdjAbsorptionOptimizer.cs
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • src/Optimizers/RxTxAdjRssiOptimizer.cs
  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
  • src/Optimizers/GlobalAbsorptionRxTxOptimizer.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/Optimizers/RxTxAdjRssiOptimizer.cs
  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
  • src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs
🧠 Learnings (2)
📚 Learning: 2025-09-16T00:51:01.127Z
Learnt from: CR
Repo: ESPresense/ESPresense-companion PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T00:51:01.127Z
Learning: Applies to src/**/Controllers/StateController.cs : StateController must populate Device.ConfiguredRefRssi from DeviceSettings during API calls

Applied to files:

  • src/Models/Config.cs
  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
📚 Learning: 2025-09-16T00:51:01.127Z
Learnt from: CR
Repo: ESPresense/ESPresense-companion PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T00:51:01.127Z
Learning: Applies to src/ui/src/routes/calibration/**/*.svelte : Calibration UI should determine "Calibrated" status only from configured values (Device.ConfiguredRefRssi), not measured values

Applied to files:

  • src/Optimizers/CombinedOptimizer.cs
  • src/Optimizers/WeightedJointRxAdjAbsorptionOptimizer.cs
🧬 Code graph analysis (1)
src/Optimizers/CombinedOptimizer.cs (2)
src/Models/State.cs (2)
  • State (13-272)
  • State (25-88)
src/Models/OptimizationSnapshot.cs (1)
  • Measure (40-57)
🪛 GitHub Actions: Build and test
src/Optimizers/CombinedOptimizer.cs

[error] 174-174: CS1513: } expected. Compilation failed at CombinedOptimizer.cs(174,14) while running 'dotnet build -c Release --no-restore'.

🪛 GitHub Actions: Deploy to Docker
src/Optimizers/CombinedOptimizer.cs

[error] 174-239: dotnet publish failed. Compilation errors in CombinedOptimizer.cs: multiple CS1002/CS1003/CS1513/CS1524/CS1026/CS1519 errors indicating syntax issues (e.g., missing braces, invalid tokens) leading to build failure during publishing.

🪛 GitHub Check: build
src/Optimizers/CombinedOptimizer.cs

[failure] 213-213:
Invalid token ')' in a member declaration


[failure] 204-204:
} expected


[failure] 204-204:
; expected


[failure] 204-204:
; expected


[failure] 204-204:
) expected


[failure] 203-203:
} expected


[failure] 203-203:
; expected

⏰ 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). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (3)
src/Optimizers/GlobalAbsorptionRxTxOptimizer.cs (1)

341-341: LGTM!

The trailing newline addition is a minor formatting improvement with no functional impact. The optimizer implementation is well-structured with proper bounds validation, error handling, and logging.

src/Models/Config.cs (1)

137-138: Verify the impact of widened TxRefRssi bounds on existing deployments.

The default bounds have been significantly widened from [-70, -50] to [-80, -40] (40dB range vs previous 20dB range). This gives the optimizer more flexibility but may affect existing configurations that relied on the narrower defaults.

Consider documenting this change for users who may need to adjust their limits configuration.

src/Optimizers/RxTxAdjRssiOptimizer.cs (1)

91-107: Asymmetric error function uses fourth power for penalties.

The calculateDistanceError function uses Math.Pow(map - calculated, 4) for "impossible" cases (calculated < map) but Math.Pow(map - calculated, 2) for normal cases. This is a valid approach for asymmetric penalization, but be aware:

  1. Fourth-power penalties can dominate optimization and cause numerical instability with large distance errors
  2. Consider adding a cap similar to GlobalAbsorptionRxTxOptimizer's CappedError = 2560 to prevent outliers from skewing results

Comment on lines +23 to +35
public OptimizationResults Optimize(OptimizationSnapshot os)
{
var or = new OptimizationResults();
var optimization = _state.Config?.Optimization;

// Group all valid measurements
var allRxNodes = os.ByRx().SelectMany(g => g).ToList();

if (allRxNodes.Count < 3)
{
Log.Warning("Not enough valid measurements for optimization");
return or;
}
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 | 🔴 Critical

Method signature doesn't match IOptimizer interface.

The Optimize method takes only OptimizationSnapshot os, but based on other optimizers in this PR (e.g., GlobalAbsorptionRxTxOptimizer, WeightedJointRxAdjAbsorptionOptimizer), the IOptimizer interface requires:

OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string, NodeSettings> existingSettings)

This will cause a compilation error. Additionally, add a null check for optimization before accessing its properties.

Proposed fix
-    public OptimizationResults Optimize(OptimizationSnapshot os)
+    public OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string, NodeSettings> existingSettings)
     {
         var or = new OptimizationResults();
         var optimization = _state.Config?.Optimization;
+        
+        if (optimization == null)
+        {
+            Log.Warning("Configuration not available for optimization");
+            return or;
+        }

         // Group all valid measurements
         var allRxNodes = os.ByRx().SelectMany(g => g).ToList();
🤖 Prompt for AI Agents
In src/Optimizers/RxTxAdjRssiOptimizer.cs around lines 23 to 35, the Optimize
method currently has the wrong signature and may dereference a null
configuration: change the method signature to match the IOptimizer interface:
OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string,
NodeSettings> existingSettings), update all local usages/call sites accordingly,
and add a null check for var optimization = _state.Config?.Optimization before
accessing its members (return empty OptimizationResults or log and exit early if
optimization is null).

@DTTerastar DTTerastar changed the title WIP Fancy Optimize Feb 3, 2026
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