mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-21 01:45:47 +10:00
fix: address PR #296 code review feedback
- 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
This commit is contained in:
@@ -6,18 +6,22 @@ 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']);
|
||||
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}');
|
||||
@@ -25,7 +29,9 @@ void main() {
|
||||
final model = LinearRegressor(data, 'y');
|
||||
|
||||
final testDf = DataFrame(
|
||||
[[25.0]],
|
||||
[
|
||||
[25.0],
|
||||
],
|
||||
headerExists: false,
|
||||
header: ['x'],
|
||||
);
|
||||
@@ -38,45 +44,63 @@ void main() {
|
||||
|
||||
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 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]],
|
||||
[
|
||||
[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)');
|
||||
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 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]],
|
||||
[
|
||||
[hops, 50.0],
|
||||
],
|
||||
headerExists: false,
|
||||
header: ['pathLength', 'messageBytes'],
|
||||
);
|
||||
@@ -87,20 +111,28 @@ void main() {
|
||||
|
||||
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 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');
|
||||
|
||||
@@ -116,7 +148,9 @@ void main() {
|
||||
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');
|
||||
debugPrint(
|
||||
'4-feature: hops=${tc[0]} flood=${tc[3]} → ${(pred as num).round()}ms',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ void main() {
|
||||
expect(direct4!, greaterThan(direct2!));
|
||||
expect(direct2, greaterThan(direct0!));
|
||||
|
||||
// All should be within the clamp range
|
||||
expect(direct0, greaterThanOrEqualTo(2000));
|
||||
expect(direct4, lessThanOrEqualTo(120000));
|
||||
// All should be positive
|
||||
expect(direct0, greaterThan(0));
|
||||
expect(direct4, greaterThan(0));
|
||||
|
||||
// Print predictions for visibility
|
||||
debugPrint('Predictions (with 1.5x safety margin):');
|
||||
|
||||
Reference in New Issue
Block a user