@@ -8,10 +8,12 @@ namespace ESPresense.Optimizers;
88public class CombinedOptimizer : IOptimizer
99{
1010 private readonly State _state ;
11+ private readonly Serilog . ILogger _logger ;
1112
1213 public CombinedOptimizer ( State state )
1314 {
1415 _state = state ;
16+ _logger = Log . ForContext < CombinedOptimizer > ( ) ;
1517 }
1618
1719 public string Name => "Two-Step Optimized Combined RxAdjRssi and Absorption" ;
@@ -24,7 +26,11 @@ public OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string,
2426 var allNodes = os . ByRx ( ) . SelectMany ( g => g ) . ToList ( ) ;
2527 var uniqueDeviceIds = allNodes . SelectMany ( n => new [ ] { n . Rx . Id , n . Tx . Id } ) . Distinct ( ) . ToList ( ) ;
2628
27- if ( allNodes . Count < 3 ) return results ;
29+ if ( allNodes . Count < 3 )
30+ {
31+ _logger . Information ( "Not enough nodes for optimization (need at least 3, found {Count})" , allNodes . Count ) ;
32+ return results ;
33+ }
2834
2935 try
3036 {
@@ -39,21 +45,22 @@ public OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string,
3945 // Process and store results
4046 foreach ( var deviceId in uniqueDeviceIds )
4147 {
42- if ( rxAdjRssiDict . TryGetValue ( deviceId , out var rxAdjRssi ) &&
43- nodeAbsorptions . TryGetValue ( deviceId , out var absorption ) )
48+ if ( deviceParams . TryGetValue ( deviceId , out var parameters ) )
4449 {
4550 results . Nodes [ deviceId ] = new ProposedValues
4651 {
47- RxAdjRssi = rxAdjRssi ,
48- Absorption = absorption ,
52+ RxAdjRssi = parameters . RxAdjRssi ,
53+ Absorption = parameters . Absorption ,
4954 Error = error
5055 } ;
5156 }
5257 }
58+
59+ _logger . Information ( "Optimization completed with error: {Error}" , error ) ;
5360 }
5461 catch ( Exception ex )
5562 {
56- Log . Error ( "Error in combined optimization: {0}" , ex . Message ) ;
63+ _logger . Error ( ex , "Error in combined optimization" ) ;
5764 }
5865
5966 return results ;
@@ -130,11 +137,28 @@ public OptimizationResults Optimize(OptimizationSnapshot os, Dictionary<string,
130137 private Dictionary < string , double > OptimizeNodeAbsorptions ( List < Measure > allNodes , List < string > uniqueDeviceIds ,
131138 Dictionary < string , double > rxAdjRssiDict , Dictionary < ( string , string ) , double > pathAbsorptionDict , ConfigOptimization optimization , Dictionary < string , NodeSettings > existingSettings )
132139 {
133- // Fix: Use ObjectiveFunction.Gradient() instead of ValueAndGradient
134- var obj = ObjectiveFunction . Gradient (
135- x => {
136- var nodeAbsorptionDict = new Dictionary < string , double > ( ) ;
137- for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
140+ // Create reasonable initial guesses
141+ var initialGuess = Vector < double > . Build . Dense ( uniqueDeviceIds . Count * 2 ) ;
142+ for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
143+ {
144+ // Include more intelligent initial guesses based on naive distance model
145+ // Attempt to calculate a reasonable starting point based on physics model
146+ double estimatedRxAdjRssi = 0 ;
147+ double estimatedAbsorption = 2.5 ; // Middle of typical range (between 2-3)
148+
149+ // If we have data from existing nodes, try to extract better initial guesses
150+ var existingMeasurements = allNodes . Where ( n =>
151+ n . Rx . Id == uniqueDeviceIds [ i ] || n . Tx . Id == uniqueDeviceIds [ i ] ) . ToList ( ) ;
152+
153+ if ( existingMeasurements . Any ( ) )
154+ {
155+ // Estimate parameters based on known distances and RSSI
156+ // This is a simplified approach, but provides a better starting point
157+ var avgDistance = existingMeasurements . Average ( m => m . Rx . Location . DistanceTo ( m . Tx . Location ) ) ;
158+ var avgRssi = existingMeasurements . Average ( m => m . Rssi ) ;
159+
160+ // Heuristic formula based on RSSI model
161+ if ( avgDistance > 0 && ! double . IsNaN ( avgRssi ) )
138162 {
139163 var absorption = x [ i ] ;
140164 existingSettings . TryGetValue ( uniqueDeviceIds [ i ] , out var nodeSettings ) ;
@@ -148,9 +172,9 @@ private Dictionary<string, double> OptimizeNodeAbsorptions(List<Measure> allNode
148172
149173 return CalculateError ( allNodes , rxAdjRssiDict , nodeAbsorptionDict : nodeAbsorptionDict ) ;
150174 } ,
175+ // Function to compute gradient
151176 x => {
152- var nodeAbsorptionDict = new Dictionary < string , double > ( ) ;
153- for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
177+ try
154178 {
155179 nodeAbsorptionDict [ uniqueDeviceIds [ i ] ] = x [ i ] ;
156180 }
@@ -160,17 +184,36 @@ private Dictionary<string, double> OptimizeNodeAbsorptions(List<Measure> allNode
160184 double epsilon = 1e-5 ;
161185 double baseError = CalculateError ( allNodes , rxAdjRssiDict , nodeAbsorptionDict : nodeAbsorptionDict ) ;
162186
163- for ( int i = 0 ; i < x . Count ; i ++ )
187+ // Compute gradient numerically
188+ var gradient = Vector < double > . Build . Dense ( x . Count ) ;
189+ double h = 1e-5 ; // Step size for finite difference
190+
191+ for ( int i = 0 ; i < x . Count ; i ++ )
192+ {
193+ var xPlus = x . Clone ( ) ;
194+ xPlus [ i ] += h ;
195+
196+ var paramsPlus = CreateDeviceParamsFromVector ( xPlus , uniqueDeviceIds , optimization ) ;
197+ var errorPlus = CalculateError ( allNodes , paramsPlus ) ;
198+
199+ gradient [ i ] = ( errorPlus - baseError ) / h ;
200+ }
201+
202+ return gradient ;
203+ }
204+ catch ( Exception ex )
164205 {
165206 var tempDict = new Dictionary < string , double > ( nodeAbsorptionDict ) ;
166207 tempDict [ uniqueDeviceIds [ i ] ] += epsilon ;
167208
168209 var errorPlusEps = CalculateError ( allNodes , rxAdjRssiDict , nodeAbsorptionDict : tempDict ) ;
169210 gradient [ i ] = ( errorPlusEps - baseError ) / epsilon ;
170211 }
212+ }
213+ ) ;
171214
172- return gradient ;
173- } ) ;
215+ // ConjugateGradientMinimizer only takes 3 tolerance parameters, not a maximum iteration count
216+ var solver = new ConjugateGradientMinimizer ( 1e-3 , 1000 ) ;
174217
175218 // Initial guess uses node setting if available, else global midpoint
176219 var initialGuess = Vector < double > . Build . Dense ( uniqueDeviceIds . Count ) ;
@@ -189,41 +232,117 @@ private Dictionary<string, double> OptimizeNodeAbsorptions(List<Measure> allNode
189232 var nodeAbsorptions = new Dictionary < string , double > ( ) ;
190233 for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
191234 {
192- nodeAbsorptions [ uniqueDeviceIds [ i ] ] = result . MinimizingPoint [ i ] ;
235+ result = solver . FindMinimum ( objGradient , initialGuess ) ;
236+ _logger . Information ( "Optimization completed: Iterations={0}, Status={1}, Error={2}" ,
237+ result . Iterations , result . ReasonForExit , result . FunctionInfoAtMinimum . Value ) ;
238+ }
239+ catch ( Exception ex )
240+ {
241+ _logger . Error ( ex , "Optimization failed" ) ;
242+
243+ // Return default values if optimization fails
244+ var defaultParams = new Dictionary < string , DeviceParameters > ( ) ;
245+ foreach ( var id in uniqueDeviceIds )
246+ {
247+ defaultParams [ id ] = new DeviceParameters
248+ {
249+ RxAdjRssi = 0 ,
250+ Absorption = ( optimization ? . AbsorptionMax + optimization ? . AbsorptionMin ) / 2 ?? 3.0
251+ } ;
252+ }
253+
254+ return ( defaultParams , double . MaxValue ) ;
255+ }
256+
257+ // Extract optimized parameters
258+ var deviceParams = new Dictionary < string , DeviceParameters > ( ) ;
259+ for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
260+ {
261+ deviceParams [ uniqueDeviceIds [ i ] ] = new DeviceParameters
262+ {
263+ RxAdjRssi = result . MinimizingPoint [ i ] ,
264+ Absorption = result . MinimizingPoint [ i + uniqueDeviceIds . Count ]
265+ } ;
193266 }
194267
195- return nodeAbsorptions ;
268+ return ( deviceParams , result . FunctionInfoAtMinimum . Value ) ;
196269 }
197270
198- private double CalculateError ( List < Measure > nodes , Dictionary < string , double > rxAdjRssiDict ,
199- Dictionary < string , double > nodeAbsorptionDict = null , Dictionary < ( string , string ) , double > pathAbsorptionDict = null )
271+ private Dictionary < string , DeviceParameters > CreateDeviceParamsFromVector ( Vector < double > x , List < string > uniqueDeviceIds , ConfigOptimization optimization )
200272 {
201- return nodes . Select ( n =>
273+ var deviceParams = new Dictionary < string , DeviceParameters > ( ) ;
274+
275+ for ( int i = 0 ; i < uniqueDeviceIds . Count ; i ++ )
202276 {
203- var distance = n . Rx . Location . DistanceTo ( n . Tx . Location ) ;
204- var rxAdjRssi = rxAdjRssiDict [ n . Rx . Id ] ;
205- var txAdjRssi = rxAdjRssiDict [ n . Tx . Id ] ;
206- double absorption ;
277+ var rxAdjRssi = x [ i ] ;
278+ var absorption = x [ i + uniqueDeviceIds . Count ] ;
207279
208- if ( pathAbsorptionDict != null )
280+ // Enforce constraints by clamping values to valid ranges
281+ rxAdjRssi = Math . Clamp ( rxAdjRssi ,
282+ optimization ? . RxAdjRssiMin ?? - 20 ,
283+ optimization ? . RxAdjRssiMax ?? 20 ) ;
284+
285+ absorption = Math . Clamp ( absorption ,
286+ optimization ? . AbsorptionMin ?? 1.5 ,
287+ optimization ? . AbsorptionMax ?? 4.5 ) ;
288+
289+ deviceParams [ uniqueDeviceIds [ i ] ] = new DeviceParameters
209290 {
210- var pathKey = ( Min ( n . Rx . Id , n . Tx . Id ) , Max ( n . Rx . Id , n . Tx . Id ) ) ;
211- absorption = pathAbsorptionDict [ pathKey ] ;
212- }
213- else if ( nodeAbsorptionDict != null )
291+ RxAdjRssi = rxAdjRssi ,
292+ Absorption = absorption
293+ } ;
294+ }
295+
296+ return deviceParams ;
297+ }
298+
299+ private double CalculateError ( List < Measure > nodes , Dictionary < string , DeviceParameters > deviceParams )
300+ {
301+ double totalError = 0 ;
302+ int count = 0 ;
303+
304+ foreach ( var node in nodes )
305+ {
306+ try
214307 {
215- absorption = ( nodeAbsorptionDict [ n . Rx . Id ] + nodeAbsorptionDict [ n . Tx . Id ] ) / 2 ;
308+ if ( ! deviceParams . TryGetValue ( node . Rx . Id , out var rxParams ) ||
309+ ! deviceParams . TryGetValue ( node . Tx . Id , out var txParams ) )
310+ {
311+ continue ;
312+ }
313+
314+ var distance = node . Rx . Location . DistanceTo ( node . Tx . Location ) ;
315+ var rxAdjRssi = rxParams . RxAdjRssi ;
316+ var txAdjRssi = txParams . RxAdjRssi ;
317+
318+ // Use average of both device absorptions
319+ var absorption = ( rxParams . Absorption + txParams . Absorption ) / 2 ;
320+
321+ // Safeguard against negative or zero absorption
322+ if ( absorption <= 0.1 )
323+ {
324+ absorption = 0.1 ;
325+ }
326+
327+ // Calculate distance based on RSSI
328+ var calculatedDistance = Math . Pow ( 10 , ( - 59 + rxAdjRssi + txAdjRssi - node . Rssi ) / ( 10.0d * absorption ) ) ;
329+
330+ // Skip invalid calculations
331+ if ( double . IsNaN ( calculatedDistance ) || double . IsInfinity ( calculatedDistance ) )
332+ {
333+ continue ;
334+ }
335+
336+ // Squared error
337+ totalError += Math . Pow ( distance - calculatedDistance , 2 ) ;
338+ count ++ ;
216339 }
217- else
340+ catch ( Exception ex )
218341 {
219- throw new ArgumentException ( "Either nodeAbsorptionDict or pathAbsorptionDict must be provided" ) ;
342+ _logger . Warning ( ex , "Error calculating distance for node {Rx} to {Tx}" , node . Rx . Id , node . Tx . Id ) ;
220343 }
344+ }
221345
222- var calculatedDistance = Math . Pow ( 10 , ( - 59 + rxAdjRssi + txAdjRssi - n . Rssi ) / ( 10.0d * absorption ) ) ;
223- return Math . Pow ( distance - calculatedDistance , 2 ) ;
224- } ) . Average ( ) ;
346+ return count > 0 ? totalError / count : double . MaxValue ;
225347 }
226-
227- private static string Min ( string a , string b ) => string . Compare ( a , b ) < 0 ? a : b ;
228- private static string Max ( string a , string b ) => string . Compare ( a , b ) >= 0 ? a : b ;
229- }
348+ }
0 commit comments