T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models (Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can map to them in T08 rather than the domain depending on the database. T04 — ride_statistics.dart including ElevationAccumulator, ported structurally faithfully: moving average, reversal hysteresis, gainIncludingPending, and the finish() reconciliation against lastRaw. T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures through the real Kotlin files and the Dart port, then diffs. Result: every value byte-identical, including noisy_gain=38.959594555022136 to the last digit. The sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double with artefacts Dart's binary64 does not reproduce. Documented, not papered over. That harness settled a real question. The ported elevation test failed at 50.9m against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's and Dart's Random(42) are different streams. On a shared LCG fixture both produce 39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed luck rather than on a property of the algorithm. The Dart test now uses the shared LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour (25 seeds spanned 24.7-46.7m). 52 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.5 KiB
Dart
65 lines
2.5 KiB
Dart
// Dart side of the cross-language parity harness (T07).
|
|
// Must stay fixture-for-fixture identical to tool/parity/main.kt.
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:rippr/src/geo/geo.dart';
|
|
import 'package:rippr/src/stats/ride_statistics.dart';
|
|
|
|
/// Same LCG as the Kotlin oracle. Dart ints are 64-bit two's complement on the VM and
|
|
/// multiplication wraps, matching Kotlin's Long.
|
|
class Lcg {
|
|
Lcg(this._s);
|
|
int _s;
|
|
double nextDouble() {
|
|
_s = _s * 6364136223846793005 + 1442695040888963407;
|
|
final bits = (_s >>> 11) & ((1 << 53) - 1);
|
|
return bits / (1 << 53);
|
|
}
|
|
}
|
|
|
|
TrackPoint p(int seg, int ts, double lat, double lon, double sp, double alt, int id) =>
|
|
TrackPoint(id: id, tripId: 1, segmentId: seg, timestamp: ts,
|
|
latitude: lat, longitude: lon, speedKmh: sp, altitudeM: alt);
|
|
|
|
void main() {
|
|
void out(String k, Object v) => print('$k=$v');
|
|
|
|
out('haversine_calgary_edmonton', haversineMeters(51.0447, -114.0719, 53.5461, -113.4938));
|
|
out('haversine_short_hop', haversineMeters(51.0447, -114.0719, 51.04480, -114.0719));
|
|
out('perp_beyond_end', perpendicularDistanceMeters(
|
|
const LatLon(51.003, -114.0), const LatLon(51.000, -114.0), const LatLon(51.002, -114.0)));
|
|
|
|
final ride = List.generate(21600,
|
|
(i) => LatLon(51.0 + i * 0.00001, -114.0 + math.sin(i / 100.0) * 0.001));
|
|
final simplified = simplify(ride, 5.0);
|
|
out('simplify_count', simplified.length);
|
|
out('simplify_last_lat', simplified.last.lat);
|
|
out('path_length_ride', pathLengthMeters(ride));
|
|
|
|
final rng = Lcg(42);
|
|
final noisy = List.generate(600,
|
|
(i) => p(1, i * 1000, 51.0, -114.0, 0.0, 1000.0 + (rng.nextDouble() * 16.0 - 8.0), i + 1));
|
|
out('noisy_gain', computeSummary(noisy).elevationGainM);
|
|
out('noisy_loss', computeSummary(noisy).elevationLossM);
|
|
|
|
final climb = List.generate(101,
|
|
(i) => p(1, i * 1000, 51.0 + i * 0.0001, -114.0, 50.0, 1000.0 + i, i + 1));
|
|
out('climb_gain', computeSummary(climb).elevationGainM);
|
|
|
|
final run = List.generate(100,
|
|
(i) => p(1, i * 1000, 51.0 + i * 0.0001, -114.0, 40.0, 1000.0, i + 1));
|
|
final s = computeSummary(run);
|
|
out('run_distance', s.distanceM);
|
|
out('run_moving_millis', s.movingMillis);
|
|
out('run_avg_speed', s.avgMovingSpeedKmh);
|
|
|
|
final prof = elevationProfile(run, maxSamples: 200);
|
|
out('profile_size', prof.length);
|
|
out('profile_last_distance', prof.last.distanceM);
|
|
|
|
final hist = speedHistogram(run, bucketKmh: 10);
|
|
out('hist_buckets', hist.length);
|
|
out('hist_first_millis', hist.first.millis);
|
|
}
|