mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
- Clamp ML predictions between physics floor (raw airtime) and ceiling (worst-case formula) so model can never produce unsafe timeouts - Replace hourOfDay feature with secondsSinceLastRx for network activity - Remove unused _ContactStats.stdDev and dead model persistence code - Debounce observation writes (2s) instead of writing on every delivery - Skip recording observations when pathLength is null to avoid corrupting training data - Add comment explaining global (not per-contact) RX time tracking - Remove notifyListeners from retrain to avoid unnecessary widget rebuilds - Run dart format
156 lines
4.2 KiB
Dart
156 lines
4.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:ml_algo/ml_algo.dart';
|
|
import 'package:ml_dataframe/ml_dataframe.dart';
|
|
|
|
void main() {
|
|
test('LinearRegressor basic sanity check', () {
|
|
// Simple: y = 2x + 100
|
|
final data = DataFrame(
|
|
[
|
|
[1.0, 102.0],
|
|
[2.0, 104.0],
|
|
[3.0, 106.0],
|
|
[4.0, 108.0],
|
|
[5.0, 110.0],
|
|
[10.0, 120.0],
|
|
[20.0, 140.0],
|
|
[50.0, 200.0],
|
|
[0.0, 100.0],
|
|
[100.0, 300.0],
|
|
],
|
|
headerExists: false,
|
|
header: ['x', 'y'],
|
|
);
|
|
|
|
debugPrint('Training data columns: ${data.header}');
|
|
debugPrint('Training data rows: ${data.rows.length}');
|
|
|
|
final model = LinearRegressor(data, 'y');
|
|
|
|
final testDf = DataFrame(
|
|
[
|
|
[25.0],
|
|
],
|
|
headerExists: false,
|
|
header: ['x'],
|
|
);
|
|
|
|
final prediction = model.predict(testDf);
|
|
final value = prediction.rows.first.first;
|
|
debugPrint('Predict x=25 → y=$value (expected ~150)');
|
|
expect((value as num).toDouble(), closeTo(150, 5));
|
|
});
|
|
|
|
test('LinearRegressor multi-feature with constant column produces zeros', () {
|
|
// isFlood=0 for all rows → zero-variance column → singular matrix
|
|
final data = DataFrame(
|
|
[
|
|
[0.0, 50.0, 14.0, 0.0, 1900.0],
|
|
[0.0, 80.0, 14.0, 0.0, 2200.0],
|
|
[2.0, 50.0, 14.0, 0.0, 5000.0],
|
|
[4.0, 50.0, 14.0, 0.0, 9500.0],
|
|
],
|
|
headerExists: false,
|
|
header: [
|
|
'pathLength',
|
|
'messageBytes',
|
|
'hourOfDay',
|
|
'isFlood',
|
|
'deliveryMs',
|
|
],
|
|
);
|
|
|
|
final model = LinearRegressor(data, 'deliveryMs');
|
|
final testDf = DataFrame(
|
|
[
|
|
[2.0, 50.0, 14.0, 0.0],
|
|
],
|
|
headerExists: false,
|
|
header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'],
|
|
);
|
|
final pred = model.predict(testDf).rows.first.first;
|
|
debugPrint(
|
|
'With constant isFlood column: hops=2 → ${(pred as num).round()}ms (likely 0)',
|
|
);
|
|
});
|
|
|
|
test('LinearRegressor 2-feature works correctly', () {
|
|
// Just pathLength + messageBytes → deliveryMs
|
|
final data = DataFrame(
|
|
[
|
|
[0.0, 50.0, 1900.0],
|
|
[0.0, 80.0, 2200.0],
|
|
[2.0, 50.0, 5000.0],
|
|
[2.0, 80.0, 5500.0],
|
|
[4.0, 50.0, 9500.0],
|
|
[4.0, 80.0, 10000.0],
|
|
[0.0, 30.0, 1800.0],
|
|
[2.0, 30.0, 4800.0],
|
|
[4.0, 30.0, 9000.0],
|
|
[0.0, 60.0, 2000.0],
|
|
],
|
|
headerExists: false,
|
|
header: ['pathLength', 'messageBytes', 'deliveryMs'],
|
|
);
|
|
|
|
final model = LinearRegressor(data, 'deliveryMs');
|
|
|
|
for (final hops in [0.0, 2.0, 4.0]) {
|
|
final testDf = DataFrame(
|
|
[
|
|
[hops, 50.0],
|
|
],
|
|
headerExists: false,
|
|
header: ['pathLength', 'messageBytes'],
|
|
);
|
|
final pred = model.predict(testDf).rows.first.first;
|
|
debugPrint('2-feature: hops=$hops → ${(pred as num).round()}ms');
|
|
}
|
|
});
|
|
|
|
test('LinearRegressor multi-feature with variance in all columns', () {
|
|
// Mix flood and direct so isFlood has variance
|
|
final data = DataFrame(
|
|
[
|
|
[0.0, 50.0, 14.0, 0.0, 1900.0],
|
|
[0.0, 80.0, 10.0, 0.0, 2200.0],
|
|
[2.0, 50.0, 16.0, 0.0, 5000.0],
|
|
[2.0, 80.0, 20.0, 0.0, 5500.0],
|
|
[4.0, 50.0, 8.0, 0.0, 9500.0],
|
|
[4.0, 80.0, 12.0, 0.0, 10000.0],
|
|
[-1.0, 40.0, 14.0, 1.0, 5000.0],
|
|
[-1.0, 60.0, 18.0, 1.0, 6500.0],
|
|
[-1.0, 30.0, 10.0, 1.0, 4000.0],
|
|
[-1.0, 80.0, 22.0, 1.0, 7000.0],
|
|
],
|
|
headerExists: false,
|
|
header: [
|
|
'pathLength',
|
|
'messageBytes',
|
|
'hourOfDay',
|
|
'isFlood',
|
|
'deliveryMs',
|
|
],
|
|
);
|
|
|
|
final model = LinearRegressor(data, 'deliveryMs');
|
|
|
|
for (final tc in [
|
|
[0.0, 50.0, 14.0, 0.0],
|
|
[2.0, 50.0, 14.0, 0.0],
|
|
[4.0, 50.0, 14.0, 0.0],
|
|
[-1.0, 50.0, 14.0, 1.0],
|
|
]) {
|
|
final testDf = DataFrame(
|
|
[tc],
|
|
headerExists: false,
|
|
header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'],
|
|
);
|
|
final pred = model.predict(testDf).rows.first.first;
|
|
debugPrint(
|
|
'4-feature: hops=${tc[0]} flood=${tc[3]} → ${(pred as num).round()}ms',
|
|
);
|
|
}
|
|
});
|
|
}
|