diff --git a/calibration/bioacoustic/complex-contour-channel-model-evaluation.json b/calibration/bioacoustic/complex-contour-channel-model-evaluation.json new file mode 100644 index 0000000..5200402 --- /dev/null +++ b/calibration/bioacoustic/complex-contour-channel-model-evaluation.json @@ -0,0 +1,80 @@ +{ + "Schema": "mimir.bioacoustic.complex-contour-channel-model-evaluation.v1", + "CreatedUtc": "2026-05-25T00:38:21.3837184+00:00", + "ReplayReceiptPath": "calibration\\bioacoustic\\complex-contour-replay-panel.json", + "ChannelModelPath": "calibration\\bioacoustic\\complex-contour-channel-model.json", + "CasesEvaluated": 4, + "CasesImproved": 3, + "MeanDeltaAbsolutePredictionErrorMicroseconds": -2.1368517719347526, + "MeanDeltaMeanAbsoluteErrorSamples": -0.7126493463842765, + "Cases": [ + { + "CaseId": "stored-shotgun", + "PathId": "asio-ch2-\u003Easio-ch1@192000", + "BaselineDelayMicroseconds": 2777.180000942597, + "ModeledDelayMicroseconds": 2777.5179506877134, + "BaselineAbsolutePredictionErrorMicroseconds": 5.859608432402889, + "ModeledAbsolutePredictionErrorMicroseconds": 5.521658687286528, + "BaselineMeanAbsoluteErrorSamples": 2.7136531554923558, + "ModeledMeanAbsoluteErrorSamples": 2.340186161946364, + "BaselineMeanAbsolutePhaseErrorRadians": 0.6165969596331552, + "ModeledMeanAbsolutePhaseErrorRadians": 0.47534945201665324, + "BaselineConfidence": 0.00823816403932344, + "ModeledConfidence": 0.008854572805546617, + "DeltaAbsolutePredictionErrorMicroseconds": -0.33794974511636156, + "DeltaMeanAbsoluteErrorSamples": -0.3734669935459918, + "DeltaMeanAbsolutePhaseErrorRadians": -0.14124750761650195 + }, + { + "CaseId": "stored-cardioid", + "PathId": "asio-ch2-\u003Easio-ch0@192000", + "BaselineDelayMicroseconds": 4354.34525443644, + "ModeledDelayMicroseconds": 4352.68559456975, + "BaselineAbsolutePredictionErrorMicroseconds": 15.81663618855996, + "ModeledAbsolutePredictionErrorMicroseconds": 17.476296055249918, + "BaselineMeanAbsoluteErrorSamples": 2.846774242629788, + "ModeledMeanAbsoluteErrorSamples": 1.4773451250014298, + "BaselineMeanAbsolutePhaseErrorRadians": 0.6868608276516036, + "ModeledMeanAbsolutePhaseErrorRadians": 0.46755413883203306, + "BaselineConfidence": 0.003921422554117446, + "ModeledConfidence": 0.008639687591920376, + "DeltaAbsolutePredictionErrorMicroseconds": 1.6596598666899585, + "DeltaMeanAbsoluteErrorSamples": -1.3694291176283584, + "DeltaMeanAbsolutePhaseErrorRadians": -0.21930668881957055 + }, + { + "CaseId": "fresh-shotgun", + "PathId": "asio-ch2-\u003Easio-ch1@192000", + "BaselineDelayMicroseconds": 2841.8565252342623, + "ModeledDelayMicroseconds": 2828.4703486352705, + "BaselineAbsolutePredictionErrorMicroseconds": 7.247155442595243, + "ModeledAbsolutePredictionErrorMicroseconds": 6.139021156396159, + "BaselineMeanAbsoluteErrorSamples": 3.516897994416609, + "ModeledMeanAbsoluteErrorSamples": 2.2414117522659587, + "BaselineMeanAbsolutePhaseErrorRadians": 0.8524654297414582, + "ModeledMeanAbsolutePhaseErrorRadians": 0.9370760478492041, + "BaselineConfidence": 0.0032861669060763625, + "ModeledConfidence": 0.012939018017668592, + "DeltaAbsolutePredictionErrorMicroseconds": -1.1081342861990846, + "DeltaMeanAbsoluteErrorSamples": -1.2754862421506505, + "DeltaMeanAbsolutePhaseErrorRadians": 0.08461061810774584 + }, + { + "CaseId": "fresh-cardioid", + "PathId": "asio-ch2-\u003Easio-ch0@192000", + "BaselineDelayMicroseconds": 4079.2287596066712, + "ModeledDelayMicroseconds": 4070.467776683558, + "BaselineAbsolutePredictionErrorMicroseconds": 8.963384606671454, + "ModeledAbsolutePredictionErrorMicroseconds": 0.20240168355793267, + "BaselineMeanAbsoluteErrorSamples": 2.9884960846911293, + "ModeledMeanAbsoluteErrorSamples": 3.156281052479024, + "BaselineMeanAbsolutePhaseErrorRadians": 0.6570230350323284, + "ModeledMeanAbsolutePhaseErrorRadians": 0.5298336911869612, + "BaselineConfidence": 0.004817015390839313, + "ModeledConfidence": 0.004778906622795285, + "DeltaAbsolutePredictionErrorMicroseconds": -8.760982923113522, + "DeltaMeanAbsoluteErrorSamples": 0.16778496778789487, + "DeltaMeanAbsolutePhaseErrorRadians": -0.12718934384536718 + } + ] +} \ No newline at end of file diff --git a/calibration/bioacoustic/complex-contour-channel-model.json b/calibration/bioacoustic/complex-contour-channel-model.json new file mode 100644 index 0000000..4d04146 --- /dev/null +++ b/calibration/bioacoustic/complex-contour-channel-model.json @@ -0,0 +1,211 @@ +{ + "Schema": "mimir.bioacoustic.complex-contour-channel-model.v1", + "CreatedUtc": "2026-05-25T00:38:15.1446826+00:00", + "SourceReceiptPath": "calibration\\bioacoustic\\complex-contour-replay-panel.json", + "Paths": [ + { + "PathId": "asio-ch2-\u003Easio-ch0@192000", + "SampleRate": 192000, + "ReferenceChannel": 2, + "CandidateChannel": 0, + "CaseIds": [ + "fresh-cardioid", + "stored-cardioid" + ], + "Corrections": [ + { + "CenterHz": 3500, + "DelayCorrectionSamples": -0.7094239775796349, + "PhaseCorrectionRadians": -0.0812280205621396, + "Weight": 2.497079370544234, + "ObservationCount": 2, + "DelayStdDevSamples": 0.970462155866937, + "Reliability": 0.6033138468538993, + "Usable": true + }, + { + "CenterHz": 7250, + "DelayCorrectionSamples": 0.6879842923169613, + "PhaseCorrectionRadians": 0.1470066505771498, + "Weight": 0.6179045188987079, + "ObservationCount": 2, + "DelayStdDevSamples": 1.8460770200116723, + "Reliability": 0.4071827676776666, + "Usable": true + }, + { + "CenterHz": 18250, + "DelayCorrectionSamples": -1.602566536835783, + "PhaseCorrectionRadians": -0.9619693373621746, + "Weight": 0.33563345813168965, + "ObservationCount": 2, + "DelayStdDevSamples": 0.24436247319389057, + "Reliability": 0.8506780619599758, + "Usable": true + } + ], + "ReflectionTaps": [ + { + "RelativeDelaySamples": 8, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 8.12759631542417 + }, + { + "RelativeDelaySamples": 12, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 12.128720151940115 + }, + { + "RelativeDelaySamples": 14, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 14.697493366694744 + }, + { + "RelativeDelaySamples": 20, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 20.38429159887096 + }, + { + "RelativeDelaySamples": 22, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 21.471823084040807 + }, + { + "RelativeDelaySamples": 24, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 23.29111737086498 + }, + { + "RelativeDelaySamples": 28, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 28.067226337488933 + }, + { + "RelativeDelaySamples": 32, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 31.225995403332718 + } + ], + "UsableBandCount": 3, + "Reliability": 0.6203915588305139, + "DelaySpreadSamples": 2.2905508291527443 + }, + { + "PathId": "asio-ch2-\u003Easio-ch1@192000", + "SampleRate": 192000, + "ReferenceChannel": 2, + "CandidateChannel": 1, + "CaseIds": [ + "fresh-shotgun", + "stored-shotgun" + ], + "Corrections": [ + { + "CenterHz": 5000, + "DelayCorrectionSamples": -1.9792070932822021, + "PhaseCorrectionRadians": -0.3228220471324046, + "Weight": 0.30331279362275293, + "ObservationCount": 2, + "DelayStdDevSamples": 0.7521451958632238, + "Reliability": 0.6610794242214837, + "Usable": true + }, + { + "CenterHz": 6250, + "DelayCorrectionSamples": 4.52827682230858, + "PhaseCorrectionRadians": 0.9306787718916169, + "Weight": 0.21954341677414052, + "ObservationCount": 2, + "DelayStdDevSamples": 1.764402642230937, + "Reliability": 0.4317614950896053, + "Usable": true + }, + { + "CenterHz": 6500, + "DelayCorrectionSamples": 3.169130804762752, + "PhaseCorrectionRadians": 0.6746958197422781, + "Weight": 1.256357237939017, + "ObservationCount": 2, + "DelayStdDevSamples": 1.7811349612522396, + "Reliability": 0.42496775534826026, + "Usable": true + }, + { + "CenterHz": 6750, + "DelayCorrectionSamples": 3.061153294692614, + "PhaseCorrectionRadians": 0.6754916638237913, + "Weight": 3.414546739649369, + "ObservationCount": 2, + "DelayStdDevSamples": 0.38001113714073975, + "Reliability": 0.7950343756478847, + "Usable": true + }, + { + "CenterHz": 9500, + "DelayCorrectionSamples": 1.184278598058543, + "PhaseCorrectionRadians": 0.36963988005116966, + "Weight": 1.0576148131987286, + "ObservationCount": 2, + "DelayStdDevSamples": 0.5821406839513044, + "Reliability": 0.3904127110697012, + "Usable": true + }, + { + "CenterHz": 10250, + "DelayCorrectionSamples": 4.993516599117556, + "PhaseCorrectionRadians": 1.6719843951945104, + "Weight": 0.3436057503216003, + "ObservationCount": 2, + "DelayStdDevSamples": 0.3420210053753006, + "Reliability": 0.8104314791275469, + "Usable": true + } + ], + "ReflectionTaps": [ + { + "RelativeDelaySamples": 6, + "ObservationCount": 2, + "MeanRelativeDelaySamples": 6.313098785407533 + }, + { + "RelativeDelaySamples": 20, + "ObservationCount": 2, + "MeanRelativeDelaySamples": 19.76702390859947 + }, + { + "RelativeDelaySamples": 26, + "ObservationCount": 2, + "MeanRelativeDelaySamples": 26.136297412642307 + }, + { + "RelativeDelaySamples": 12, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 11.957723721125149 + }, + { + "RelativeDelaySamples": 14, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 14.43637255997919 + }, + { + "RelativeDelaySamples": 32, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 32.31618934178596 + }, + { + "RelativeDelaySamples": 34, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 33.18148524783078 + }, + { + "RelativeDelaySamples": 38, + "ObservationCount": 1, + "MeanRelativeDelaySamples": 38.20039189612339 + } + ], + "UsableBandCount": 6, + "Reliability": 0.5856145400840803, + "DelaySpreadSamples": 6.972723692399758 + } + ] +} \ No newline at end of file diff --git a/calibration/bioacoustic/complex-contour-replay-panel.json b/calibration/bioacoustic/complex-contour-replay-panel.json index a769b13..2c37f12 100644 --- a/calibration/bioacoustic/complex-contour-replay-panel.json +++ b/calibration/bioacoustic/complex-contour-replay-panel.json @@ -1,6 +1,6 @@ { "Schema": "mimir.bioacoustic.complex-contour-replay.v1", - "CreatedUtc": "2026-05-25T00:23:48.2689725+00:00", + "CreatedUtc": "2026-05-25T00:35:16.0211249+00:00", "Cases": [ { "CaseId": "stored-shotgun", @@ -107,6 +107,116 @@ "DelayResidualSamples": 0.8667391978571636, "PhaseResidualRadians": 0.18091929609672733 } + ], + "BandResiduals": [ + { + "CenterHz": 6750, + "Weight": 1.2699300283963697, + "DelayResidualSamples": 2.5673189716570537, + "PhaseResidualRadians": 0.5658813934187334 + }, + { + "CenterHz": 8750, + "Weight": 1.2568192590391993, + "DelayResidualSamples": -0.4597260852506658, + "PhaseResidualRadians": -0.12999969607089684 + }, + { + "CenterHz": 9500, + "Weight": 1.027566709608703, + "DelayResidualSamples": 1.2838263536332217, + "PhaseResidualRadians": 0.3950579393566654 + }, + { + "CenterHz": 3250, + "Weight": 0.7947593597178807, + "DelayResidualSamples": -3.765228895337047, + "PhaseResidualRadians": -0.3894062673999493 + }, + { + "CenterHz": 3500, + "Weight": 0.7529168073230323, + "DelayResidualSamples": -4.625383934838002, + "PhaseResidualRadians": -0.5149730771327364 + }, + { + "CenterHz": 13250, + "Weight": 0.6737108279691784, + "DelayResidualSamples": -2.4168494062876826, + "PhaseResidualRadians": -1.0485938214652033 + }, + { + "CenterHz": 8500, + "Weight": 0.6015228093118099, + "DelayResidualSamples": 1.1707146663913026, + "PhaseResidualRadians": 0.33160115736680457 + }, + { + "CenterHz": 6500, + "Weight": 0.47037755462147757, + "DelayResidualSamples": 0.8667391978571636, + "PhaseResidualRadians": 0.18091929609672733 + }, + { + "CenterHz": 7000, + "Weight": 0.4180309549411402, + "DelayResidualSamples": 4.206696192841818, + "PhaseResidualRadians": 0.9788491596158178 + }, + { + "CenterHz": 12000, + "Weight": 0.34870963753834217, + "DelayResidualSamples": -0.8416144627432232, + "PhaseResidualRadians": -0.32791802374977946 + }, + { + "CenterHz": 13500, + "Weight": 0.3408557430442181, + "DelayResidualSamples": -0.7583035197561686, + "PhaseResidualRadians": -0.33520285952180584 + }, + { + "CenterHz": 2750, + "Weight": 0.3220228225773948, + "DelayResidualSamples": -1.4643006545231856, + "PhaseResidualRadians": -0.1351458321084486 + }, + { + "CenterHz": 5500, + "Weight": 0.27331162789254393, + "DelayResidualSamples": -2.4449027466386832, + "PhaseResidualRadians": -0.44018835232080966 + }, + { + "CenterHz": 5000, + "Weight": 0.26586058278420227, + "DelayResidualSamples": -2.2615087582128646, + "PhaseResidualRadians": -0.3680700214469181 + }, + { + "CenterHz": 10250, + "Weight": 0.19141307586462278, + "DelayResidualSamples": 5.29849132713116, + "PhaseResidualRadians": 1.7591586790326341 + }, + { + "CenterHz": 7250, + "Weight": 0.12170465360563014, + "DelayResidualSamples": -1.097126941141717, + "PhaseResidualRadians": -0.2595639309850276 + }, + { + "CenterHz": 6250, + "Weight": 0.03498156544145096, + "DelayResidualSamples": 0.4755339728131957, + "PhaseResidualRadians": 0.09839773368213911 + }, + { + "CenterHz": 12750, + "Weight": 0.03408014687697398, + "DelayResidualSamples": -1.7678990270655959, + "PhaseResidualRadians": -0.7409251473608747 + } ] }, { @@ -214,6 +324,92 @@ "DelayResidualSamples": 5.932327618452064, "PhaseResidualRadians": 1.1557056318509606 } + ], + "BandResiduals": [ + { + "CenterHz": 3500, + "Weight": 1.4678635457303353, + "DelayResidualSamples": 0.10319879643340892, + "PhaseResidualRadians": 0.013162251967709516 + }, + { + "CenterHz": 6250, + "Weight": 0.595616635929512, + "DelayResidualSamples": 0.6124036977257589, + "PhaseResidualRadians": 0.1271354391988944 + }, + { + "CenterHz": 13000, + "Weight": 0.5567561239589848, + "DelayResidualSamples": 2.6753802477024133, + "PhaseResidualRadians": 1.1449354974851138 + }, + { + "CenterHz": 7250, + "Weight": 0.44458926425684897, + "DelayResidualSamples": -0.4646425397950185, + "PhaseResidualRadians": -0.11029550531251985 + }, + { + "CenterHz": 9500, + "Weight": 0.4068961583884891, + "DelayResidualSamples": -1.4174391923665974, + "PhaseResidualRadians": -0.43593239097651637 + }, + { + "CenterHz": 6750, + "Weight": 0.3710780968347314, + "DelayResidualSamples": -2.182853898329963, + "PhaseResidualRadians": -0.477976049907922 + }, + { + "CenterHz": 9250, + "Weight": 0.23665017208058042, + "DelayResidualSamples": 2.823144216376767, + "PhaseResidualRadians": 0.8534581210981982 + }, + { + "CenterHz": 6000, + "Weight": 0.20029070346773176, + "DelayResidualSamples": 5.932327618452064, + "PhaseResidualRadians": 1.1557056318509606 + }, + { + "CenterHz": 18500, + "Weight": 0.18094863887187154, + "DelayResidualSamples": -5.545837521129101, + "PhaseResidualRadians": 2.9363613327880596 + }, + { + "CenterHz": 18250, + "Weight": 0.17372862429411026, + "DelayResidualSamples": -1.366666108583786, + "PhaseResidualRadians": -0.8203977725269509 + }, + { + "CenterHz": 6500, + "Weight": 0.12728221912126889, + "DelayResidualSamples": -1.5783140657625836, + "PhaseResidualRadians": -0.33777380372894905 + }, + { + "CenterHz": 4500, + "Weight": 0.12290356057303883, + "DelayResidualSamples": -5.920977797438127, + "PhaseResidualRadians": -0.8480305771202491 + }, + { + "CenterHz": 5000, + "Weight": 0.04489399978915249, + "DelayResidualSamples": -4.06090584120966, + "PhaseResidualRadians": -0.6569598908932412 + }, + { + "CenterHz": 12500, + "Weight": 0.03707313281210611, + "DelayResidualSamples": -4.270345279624507, + "PhaseResidualRadians": -1.7528670201188614 + } ] }, { @@ -321,6 +517,116 @@ "DelayResidualSamples": -1.9652138109995578, "PhaseResidualRadians": -1.29593222952682 } + ], + "BandResiduals": [ + { + "CenterHz": 6750, + "Weight": 2.1446167112529992, + "DelayResidualSamples": 3.3535761936586694, + "PhaseResidualRadians": 0.7403126376512428 + }, + { + "CenterHz": 7000, + "Weight": 0.9863738950334722, + "DelayResidualSamples": -4.3123794157954585, + "PhaseResidualRadians": -0.9788777422159413 + }, + { + "CenterHz": 6500, + "Weight": 0.7859796833175395, + "DelayResidualSamples": 4.54702053502405, + "PhaseResidualRadians": 0.9622868397785674 + }, + { + "CenterHz": 9250, + "Weight": 0.7529325316166473, + "DelayResidualSamples": 1.467213791062176, + "PhaseResidualRadians": 0.440377835686411 + }, + { + "CenterHz": 7500, + "Weight": 0.36006454004405286, + "DelayResidualSamples": -2.2281848131519837, + "PhaseResidualRadians": -0.5389168818164407 + }, + { + "CenterHz": 9750, + "Weight": 0.35135027105121286, + "DelayResidualSamples": 0.5850716901119313, + "PhaseResidualRadians": 0.18792002661100599 + }, + { + "CenterHz": 13750, + "Weight": 0.3341554524774217, + "DelayResidualSamples": -1.965213816233927, + "PhaseResidualRadians": -0.885265020371404 + }, + { + "CenterHz": 20250, + "Weight": 0.31148893481945145, + "DelayResidualSamples": -1.9652138109995578, + "PhaseResidualRadians": -1.29593222952682 + }, + { + "CenterHz": 7250, + "Weight": 0.30458826375419357, + "DelayResidualSamples": 3.0249280786131463, + "PhaseResidualRadians": 0.7125700375874803 + }, + { + "CenterHz": 3000, + "Weight": 0.2951735906464143, + "DelayResidualSamples": -4.864411777261353, + "PhaseResidualRadians": -0.49242496662994284 + }, + { + "CenterHz": 6250, + "Weight": 0.18456185133268957, + "DelayResidualSamples": 5.296427380960267, + "PhaseResidualRadians": 1.0712995334867106 + }, + { + "CenterHz": 10250, + "Weight": 0.1521926744569775, + "DelayResidualSamples": 4.609949183942376, + "PhaseResidualRadians": 1.5622638961382556 + }, + { + "CenterHz": 4750, + "Weight": 0.0499382191959116, + "DelayResidualSamples": 5.786543330448353, + "PhaseResidualRadians": 0.8985126666876189 + }, + { + "CenterHz": 5250, + "Weight": 0.04448134170771759, + "DelayResidualSamples": 1.5336841599477111, + "PhaseResidualRadians": 0.26160820555928704 + }, + { + "CenterHz": 14000, + "Weight": 0.0397224736375682, + "DelayResidualSamples": 3.961355187395611, + "PhaseResidualRadians": 1.8237743276335885 + }, + { + "CenterHz": 12500, + "Weight": 0.03861714250811583, + "DelayResidualSamples": 0.47514367773169397, + "PhaseResidualRadians": 0.19503427193296652 + }, + { + "CenterHz": 5000, + "Weight": 0.037452210838550666, + "DelayResidualSamples": 0.02475698464513698, + "PhaseResidualRadians": 0.004058667040229211 + }, + { + "CenterHz": 9500, + "Weight": 0.030048103590025566, + "DelayResidualSamples": -2.2199947977094325, + "PhaseResidualRadians": -0.6838161961776569 + } ] }, { @@ -428,6 +734,104 @@ "DelayResidualSamples": 4.517622003996394, "PhaseResidualRadians": 2.1315131125108575 } + ], + "BandResiduals": [ + { + "CenterHz": 3500, + "Weight": 1.0292158248138983, + "DelayResidualSamples": -1.8683833694250642, + "PhaseResidualRadians": -0.2160553650846187 + }, + { + "CenterHz": 3250, + "Weight": 0.9689156979351102, + "DelayResidualSamples": 2.367757432999856, + "PhaseResidualRadians": 0.2591088800405311 + }, + { + "CenterHz": 3750, + "Weight": 0.8257896687014397, + "DelayResidualSamples": 0.7737901147281051, + "PhaseResidualRadians": 0.09485471658946629 + }, + { + "CenterHz": 3000, + "Weight": 0.7114835803280036, + "DelayResidualSamples": -3.860572966754169, + "PhaseResidualRadians": -0.3840924303579634 + }, + { + "CenterHz": 2750, + "Weight": 0.6891263688332048, + "DelayResidualSamples": -2.0878846225489225, + "PhaseResidualRadians": -0.1926987492556257 + }, + { + "CenterHz": 13500, + "Weight": 0.4537545008959113, + "DelayResidualSamples": 4.8576686994292855, + "PhaseResidualRadians": 2.1472990646038803 + }, + { + "CenterHz": 9000, + "Weight": 0.3377853536497608, + "DelayResidualSamples": 2.4170798183239413, + "PhaseResidualRadians": 0.7163738239152257 + }, + { + "CenterHz": 14500, + "Weight": 0.2755942546185756, + "DelayResidualSamples": 4.517622003996394, + "PhaseResidualRadians": 2.1315131125108575 + }, + { + "CenterHz": 8500, + "Weight": 0.23984708548916367, + "DelayResidualSamples": 5.5039932772551765, + "PhaseResidualRadians": 1.5378129510757694 + }, + { + "CenterHz": 18500, + "Weight": 0.21524110443799965, + "DelayResidualSamples": 0.9999232623606531, + "PhaseResidualRadians": 0.6034376474734766 + }, + { + "CenterHz": 17500, + "Weight": 0.18772248629934302, + "DelayResidualSamples": 1.7056313444122682, + "PhaseResidualRadians": 0.9728508309007613 + }, + { + "CenterHz": 7250, + "Weight": 0.17331525464185893, + "DelayResidualSamples": 3.644709113281465, + "PhaseResidualRadians": 0.8582473356903002 + }, + { + "CenterHz": 18250, + "Weight": 0.16190483383757942, + "DelayResidualSamples": -1.855694599162348, + "PhaseResidualRadians": -1.1139573199928852 + }, + { + "CenterHz": 4750, + "Weight": 0.1440152051953463, + "DelayResidualSamples": -2.1860839525520532, + "PhaseResidualRadians": -0.3439500292546649 + }, + { + "CenterHz": 13250, + "Weight": 0.10510607734758748, + "DelayResidualSamples": 1.4488027765736726, + "PhaseResidualRadians": 0.6292161948732933 + }, + { + "CenterHz": 9750, + "Weight": 0.07447043699438971, + "DelayResidualSamples": 4.5701124550750825, + "PhaseResidualRadians": 1.44228259520202 + } ] } ] diff --git a/docs/bioacoustic-timeline-watermark.md b/docs/bioacoustic-timeline-watermark.md index 880a2f6..9f7adad 100644 --- a/docs/bioacoustic-timeline-watermark.md +++ b/docs/bioacoustic-timeline-watermark.md @@ -180,3 +180,20 @@ reflection field. `--complex-contour-replay-panel` persists this measurement surface to `calibration/bioacoustic/complex-contour-replay-panel.json` for regression and path-learning work. + +`--learn-complex-contour-channel-model` now turns that replay receipt into a +path-scoped complex contour channel model at +`calibration/bioacoustic/complex-contour-channel-model.json`. The model records +stable per-band delay and phase corrections plus observed reflection tap +spacing. `--complex-contour-asio-f32 --channel-model ...` can apply it +explicitly, and `--evaluate-complex-contour-channel-model` writes +`calibration/bioacoustic/complex-contour-channel-model-evaluation.json`. + +The current model is useful but not yet trusted as silent runtime authority. +Across the four stored/fresh Scarlett replay cases it improves 3/4 absolute +path-seed errors and lowers mean direct-cluster residuals overall. The best +fresh cardioid case reaches about `0.202 us` from the seeded path fit, but the +stored cardioid path still worsens from about `15.817 us` to `17.476 us`. +That means the learned phase/group-delay surface is real evidence, while the +Perfect Machine still needs more captures and a stronger direct-path +hypothesis before calibration can be applied automatically. diff --git a/docs/code-algorithm-map.md b/docs/code-algorithm-map.md index cf709a7..5c101cc 100644 --- a/docs/code-algorithm-map.md +++ b/docs/code-algorithm-map.md @@ -860,19 +860,27 @@ Blocks: score floor, lobe separation, and analysis-window bounds. - `MimirComplexContourAnchorHit`: event, anchor identity, center frequency, canonical timeline seconds, sample offset, score, phase, and rank. +- `MimirDirectPathBandObservation`, `MimirDirectPathBandCorrection`, and + `MimirDirectPathChannelModel`: the per-band delay/phase residual surface used + to learn group-delay correction across replay windows. - `MimirComplexContourMatchedFilterBank`: renders known packet anchors into complex kernels, scans current samples around a predicted source offset, keeps multiple lobe candidates per anchor, and exposes phase/magnitude evidence. - `MimirDirectPathTracker`: matches reference and candidate anchor hits, gates updates around the current path prediction, selects the coherent direct-path - cluster, smooths bounded tracking corrections, and reports later coherent - clusters as `MimirAcousticReflectionTap` records. + cluster, applies explicit channel-model delay/phase correction when supplied, + downweights bands outside the learned usable surface, smooths bounded + tracking corrections, and reports later coherent clusters as + `MimirAcousticReflectionTap` records. Invariant: - Recursive refinement is constrained by a current path hypothesis. A loud reflection may be evidence about the room, but it does not get to become time just because its correlation peak is bigger. +- Learned correction is explicit authority, not ambient magic. A + `MimirDirectPathChannelModel` is applied only when the caller supplies the + path model; otherwise the tracker stays measurement-only. ## Stage 9: Chirp-Bin Reference Timeline diff --git a/notes/current-system-map.md b/notes/current-system-map.md index ea3ea76..282e78a 100644 --- a/notes/current-system-map.md +++ b/notes/current-system-map.md @@ -258,6 +258,19 @@ per-band delay/phase residual observations and has a calibration; one-window self-correction is not allowed to become authority. `calibration/bioacoustic/complex-contour-replay-panel.json` is the current persisted receipt for this surface across stored/fresh shotgun/cardioid cases. +`MimirDirectPathChannelModel` now applies learned per-band delay and phase +correction when explicitly supplied, and downweights bands outside the learned +usable surface. `Mimir.BufferSmoke --learn-complex-contour-channel-model` +persists the current path-scoped model at +`calibration/bioacoustic/complex-contour-channel-model.json`; it learns three +usable cardioid bands and six usable shotgun bands from the four-case replay +receipt. `--evaluate-complex-contour-channel-model` writes +`calibration/bioacoustic/complex-contour-channel-model-evaluation.json`: the +model improves 3/4 absolute path-seed errors and lowers mean cluster MAE, with +the fresh cardioid reaching about 0.202 us from the seed, but the stored +cardioid still worsens to about 17.476 us. Treat the model as explicit +calibration evidence, not default runtime authority, until more captures prove +the path surface stable. The first actuator proof now exists: `faust/mimir_alignment_actuator.dsp` owns six channels of bounded fractional delay/gain controls for Faust/native DSP, and `Mimir.BufferSmoke --bioacoustic-actuator-self-test --sample-rate 48000 diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index 4e9019c..3c44b28 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -187,6 +187,7 @@ if (args.Any(arg => string.Equals(arg, "--complex-contour-asio-f32", StringCompa ParseDoubleOption(args, "--seconds", 3.0), ParseDoubleOption(args, "--schedule-offset-samples", 1623.0), ParseDoubleOption(args, "--predicted-delay-samples", 0.0), + ParseStringOption(args, "--channel-model", ""), ParseStringOption(args, "--song", MimirBioacousticContestants.CanaryPacketTrill.Id)); } @@ -196,6 +197,21 @@ if (args.Any(arg => string.Equals(arg, "--complex-contour-replay-panel", StringC ParseStringOption(args, "--output", "calibration/bioacoustic/complex-contour-replay-panel.json")); } +if (args.Any(arg => string.Equals(arg, "--learn-complex-contour-channel-model", StringComparison.OrdinalIgnoreCase))) +{ + return LearnComplexContourChannelModel( + ParseStringOption(args, "--input", "calibration/bioacoustic/complex-contour-replay-panel.json"), + ParseStringOption(args, "--output", "calibration/bioacoustic/complex-contour-channel-model.json")); +} + +if (args.Any(arg => string.Equals(arg, "--evaluate-complex-contour-channel-model", StringComparison.OrdinalIgnoreCase))) +{ + return EvaluateComplexContourChannelModel( + ParseStringOption(args, "--receipt", "calibration/bioacoustic/complex-contour-replay-panel.json"), + ParseStringOption(args, "--channel-model", "calibration/bioacoustic/complex-contour-channel-model.json"), + ParseStringOption(args, "--output", "calibration/bioacoustic/complex-contour-channel-model-evaluation.json")); +} + if (args.Any(arg => string.Equals(arg, "--calibrate-contestant-asio-f32", StringComparison.OrdinalIgnoreCase))) { return CalibrateContestantAsioFloat32( @@ -1732,8 +1748,10 @@ static int AnalyzeComplexContourAsioFloat32( double seconds, double scheduleOffsetSamples, double predictedDelaySamples, + string channelModelPath, string songId) { + var pathModel = LoadComplexContourPathModel(channelModelPath, sampleRate, referenceChannel, candidateChannel); var result = EstimateComplexContourAsioFloat32( inputPath, sampleRate, @@ -1743,6 +1761,7 @@ static int AnalyzeComplexContourAsioFloat32( seconds, scheduleOffsetSamples, predictedDelaySamples, + pathModel?.ToRuntimeModel(), songId); if (result == null) { @@ -1757,7 +1776,7 @@ static int AnalyzeComplexContourAsioFloat32( $"predictedDelaySamples={result.PredictedDelaySamples:0.000000} predictionErrorSamples={result.PredictionErrorSamples:0.000000} predictionErrorUs={result.PredictionErrorMicroseconds:0.000} " + $"confidence={result.Confidence:0.000} directHits={result.DirectHits} maeSamples={result.MeanAbsoluteErrorSamples:0.000000} phaseMaeRad={result.MeanAbsolutePhaseErrorRadians:0.000} " + $"referenceHits={result.ReferenceHits} candidateHits={result.CandidateHits} reflectionTaps={result.ReflectionTaps.Length} " + - $"firstReflectionSamples={result.FirstReflectionSamples:0.000} bands={bands}"); + $"firstReflectionSamples={result.FirstReflectionSamples:0.000} channelModel={(pathModel?.PathId ?? "none")} bands={bands}"); return 0; } @@ -1770,6 +1789,7 @@ static ComplexContourReplayResult? EstimateComplexContourAsioFloat32( double seconds, double scheduleOffsetSamples, double predictedDelaySamples, + MimirDirectPathChannelModel? channelModel, string songId) { if (!File.Exists(inputPath)) @@ -1810,7 +1830,9 @@ static ComplexContourReplayResult? EstimateComplexContourAsioFloat32( eventIndices, scheduleOffsetSamples + predictedDelaySamples, Math.Max(48, sampleRate / 900)); - var estimate = new MimirDirectPathTracker(sampleRate).Update(referenceHits, candidateHits, predictedDelaySamples); + var estimate = new MimirDirectPathTracker( + sampleRate, + new MimirDirectPathTrackerOptions(ChannelModel: channelModel)).Update(referenceHits, candidateHits, predictedDelaySamples); if (estimate == null) { Console.WriteLine( @@ -1819,7 +1841,7 @@ static ComplexContourReplayResult? EstimateComplexContourAsioFloat32( return null; } - var strongestBands = estimate.BandObservations + var bandResiduals = estimate.BandObservations .GroupBy(observation => Math.Round(observation.CenterHz / 250.0) * 250.0) .Select(group => { @@ -1831,8 +1853,8 @@ static ComplexContourReplayResult? EstimateComplexContourAsioFloat32( group.Sum(observation => observation.PhaseResidualRadians * Math.Max(observation.Weight, 1.0e-9)) / totalWeight); }) .OrderByDescending(group => group.Weight) - .Take(8) .ToArray(); + var strongestBands = bandResiduals.Take(8).ToArray(); var predictionError = estimate.DelaySamples - predictedDelaySamples; return new ComplexContourReplayResult( "", @@ -1853,7 +1875,8 @@ static ComplexContourReplayResult? EstimateComplexContourAsioFloat32( candidateHits.Count, estimate.ReflectionTaps.ToArray(), estimate.ReflectionTaps.FirstOrDefault()?.RelativeDelaySamples ?? double.NaN, - strongestBands); + strongestBands, + bandResiduals); } static int RunComplexContourReplayPanel(string outputPath) @@ -1896,6 +1919,7 @@ static int RunComplexContourReplayPanel(string outputPath) seconds: 4.0, scheduleOffsetSamples: 1623.0, replayCase.PredictedDelaySamples, + channelModel: null, MimirBioacousticContestants.CanaryPacketTrill.Id); if (result == null) { @@ -1928,6 +1952,266 @@ static int RunComplexContourReplayPanel(string outputPath) return failures == 0 ? 0 : 1; } +static int LearnComplexContourChannelModel(string inputPath, string outputPath) +{ + if (!File.Exists(inputPath)) + { + Console.Error.WriteLine($"complex-contour channel learning failed: input not found: {inputPath}"); + return 1; + } + + var receipt = JsonSerializer.Deserialize(File.ReadAllText(inputPath)); + if (receipt == null || receipt.Cases.Length == 0) + { + Console.Error.WriteLine("complex-contour channel learning failed: receipt contains no cases"); + return 1; + } + + var pathModels = receipt.Cases + .GroupBy(result => (result.SampleRate, result.ReferenceChannel, result.CandidateChannel)) + .Select(group => LearnComplexContourPathModel(group.Key.SampleRate, group.Key.ReferenceChannel, group.Key.CandidateChannel, group.ToArray())) + .Where(model => model.Corrections.Length > 0) + .OrderBy(model => model.PathId, StringComparer.Ordinal) + .ToArray(); + var document = new ComplexContourChannelModelDocument( + "mimir.bioacoustic.complex-contour-channel-model.v1", + DateTimeOffset.UtcNow, + inputPath, + pathModels); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? "."); + File.WriteAllText( + outputPath, + JsonSerializer.Serialize(document, new JsonSerializerOptions { WriteIndented = true })); + foreach (var path in pathModels) + { + Console.WriteLine( + $"complex-contour-channel-model path={path.PathId} cases={path.CaseIds.Length} corrections={path.Corrections.Length} " + + $"usableBands={path.UsableBandCount} reliability={path.Reliability:0.000} delaySpreadSamples={path.DelaySpreadSamples:0.000}"); + } + + Console.WriteLine($"complex-contour-channel-model-receipt path={outputPath} paths={pathModels.Length}"); + return pathModels.Length == 0 ? 1 : 0; +} + +static ComplexContourPathChannelModel LearnComplexContourPathModel( + int sampleRate, + int referenceChannel, + int candidateChannel, + ComplexContourReplayResult[] cases) +{ + var corrections = cases + .SelectMany(result => (result.BandResiduals is { Length: > 0 } ? result.BandResiduals : result.StrongestBands) + .Select(band => (Result: result, Band: band))) + .GroupBy(pair => pair.Band.CenterHz) + .Select(group => LearnComplexContourBandCorrection(group.Key, group.Select(pair => pair.Band).ToArray())) + .Where(correction => correction != null) + .Select(correction => correction!) + .OrderBy(correction => correction.CenterHz) + .ToArray(); + var reflectionTaps = cases + .SelectMany(result => result.ReflectionTaps.Select(tap => tap.RelativeDelaySamples)) + .GroupBy(delay => Math.Round(delay / 2.0) * 2.0) + .Select(group => new ComplexContourReflectionCorrection( + group.Key, + group.Count(), + group.Average())) + .OrderByDescending(tap => tap.ObservationCount) + .ThenBy(tap => tap.RelativeDelaySamples) + .Take(8) + .ToArray(); + var reliability = corrections.Length == 0 + ? 0.0 + : corrections.Average(correction => correction.Reliability); + var delaySpread = corrections.Length <= 1 + ? 0.0 + : corrections.Max(correction => correction.DelayCorrectionSamples) - corrections.Min(correction => correction.DelayCorrectionSamples); + return new ComplexContourPathChannelModel( + $"asio-ch{referenceChannel}->asio-ch{candidateChannel}@{sampleRate}", + sampleRate, + referenceChannel, + candidateChannel, + cases.Select(result => result.CaseId).Order(StringComparer.Ordinal).ToArray(), + corrections, + reflectionTaps, + corrections.Count(correction => correction.Usable), + reliability, + delaySpread); +} + +static ComplexContourBandCorrection? LearnComplexContourBandCorrection(double centerHz, ComplexContourBandResidual[] bands) +{ + if (bands.Length < 2) + { + return null; + } + + var totalWeight = Math.Max(1.0e-9, bands.Sum(band => Math.Max(band.Weight, 1.0e-9))); + var delay = bands.Sum(band => band.DelayResidualSamples * Math.Max(band.Weight, 1.0e-9)) / totalWeight; + var phaseVector = bands.Aggregate( + Complex.Zero, + (current, band) => current + Complex.FromPolarCoordinates(Math.Max(band.Weight, 1.0e-9), band.PhaseResidualRadians)); + var phase = Math.Atan2(phaseVector.Imaginary, phaseVector.Real); + var variance = bands.Sum(band => + { + var delta = band.DelayResidualSamples - delay; + return delta * delta * Math.Max(band.Weight, 1.0e-9); + }) / totalWeight; + var stdDev = Math.Sqrt(Math.Max(0.0, variance)); + var signCoherent = bands.All(band => Math.Sign(band.DelayResidualSamples) == Math.Sign(delay) || Math.Abs(band.DelayResidualSamples) < 0.75); + var reliability = Math.Clamp( + (bands.Length / 2.0) * + (1.0 / (1.0 + stdDev / 1.5)) * + (phaseVector.Magnitude / totalWeight) * + (signCoherent ? 1.0 : 0.55), + 0.0, + 1.0); + var usable = reliability >= 0.35 && stdDev <= 3.0; + if (!usable) + { + return null; + } + + return new ComplexContourBandCorrection( + centerHz, + delay, + phase, + totalWeight, + bands.Length, + stdDev, + reliability, + usable); +} + +static ComplexContourPathChannelModel? LoadComplexContourPathModel( + string channelModelPath, + int sampleRate, + int referenceChannel, + int candidateChannel) +{ + if (string.IsNullOrWhiteSpace(channelModelPath)) + { + return null; + } + + if (!File.Exists(channelModelPath)) + { + Console.Error.WriteLine($"complex-contour channel model not found: {channelModelPath}"); + return null; + } + + var document = JsonSerializer.Deserialize(File.ReadAllText(channelModelPath)); + var model = document?.Paths.FirstOrDefault(path => + path.SampleRate == sampleRate && + path.ReferenceChannel == referenceChannel && + path.CandidateChannel == candidateChannel); + if (model == null) + { + Console.Error.WriteLine( + $"complex-contour channel model has no path for asio-ch{referenceChannel}->asio-ch{candidateChannel}@{sampleRate}"); + } + + return model; +} + +static int EvaluateComplexContourChannelModel(string receiptPath, string channelModelPath, string outputPath) +{ + if (!File.Exists(receiptPath)) + { + Console.Error.WriteLine($"complex-contour model evaluation failed: receipt not found: {receiptPath}"); + return 1; + } + + if (!File.Exists(channelModelPath)) + { + Console.Error.WriteLine($"complex-contour model evaluation failed: channel model not found: {channelModelPath}"); + return 1; + } + + var receipt = JsonSerializer.Deserialize(File.ReadAllText(receiptPath)); + var document = JsonSerializer.Deserialize(File.ReadAllText(channelModelPath)); + if (receipt == null || document == null) + { + Console.Error.WriteLine("complex-contour model evaluation failed: invalid JSON input"); + return 1; + } + + var results = new List(); + foreach (var replayCase in receipt.Cases) + { + var pathModel = document.Paths.FirstOrDefault(path => + path.SampleRate == replayCase.SampleRate && + path.ReferenceChannel == replayCase.ReferenceChannel && + path.CandidateChannel == replayCase.CandidateChannel); + var baseline = EstimateComplexContourAsioFloat32( + replayCase.InputPath, + replayCase.SampleRate, + channels: 4, + replayCase.ReferenceChannel, + replayCase.CandidateChannel, + seconds: 4.0, + scheduleOffsetSamples: 1623.0, + replayCase.PredictedDelaySamples, + channelModel: null, + MimirBioacousticContestants.CanaryPacketTrill.Id); + var modeled = EstimateComplexContourAsioFloat32( + replayCase.InputPath, + replayCase.SampleRate, + channels: 4, + replayCase.ReferenceChannel, + replayCase.CandidateChannel, + seconds: 4.0, + scheduleOffsetSamples: 1623.0, + replayCase.PredictedDelaySamples, + pathModel?.ToRuntimeModel(), + MimirBioacousticContestants.CanaryPacketTrill.Id); + if (baseline == null || modeled == null) + { + continue; + } + + var baselineAbsError = Math.Abs(baseline.PredictionErrorMicroseconds); + var modeledAbsError = Math.Abs(modeled.PredictionErrorMicroseconds); + results.Add(new ComplexContourChannelModelEvaluationCase( + replayCase.CaseId, + pathModel?.PathId ?? "", + baseline.DelayMicroseconds, + modeled.DelayMicroseconds, + baselineAbsError, + modeledAbsError, + baseline.MeanAbsoluteErrorSamples, + modeled.MeanAbsoluteErrorSamples, + baseline.MeanAbsolutePhaseErrorRadians, + modeled.MeanAbsolutePhaseErrorRadians, + baseline.Confidence, + modeled.Confidence, + modeledAbsError - baselineAbsError, + modeled.MeanAbsoluteErrorSamples - baseline.MeanAbsoluteErrorSamples, + modeled.MeanAbsolutePhaseErrorRadians - baseline.MeanAbsolutePhaseErrorRadians)); + Console.WriteLine( + $"complex-contour-channel-eval case={replayCase.CaseId} path={pathModel?.PathId ?? "none"} " + + $"baselineAbsUs={baselineAbsError:0.000} modeledAbsUs={modeledAbsError:0.000} deltaAbsUs={modeledAbsError - baselineAbsError:+0.000;-0.000;0.000} " + + $"maeSamples={baseline.MeanAbsoluteErrorSamples:0.000}->{modeled.MeanAbsoluteErrorSamples:0.000} phaseMae={baseline.MeanAbsolutePhaseErrorRadians:0.000}->{modeled.MeanAbsolutePhaseErrorRadians:0.000}"); + } + + var improved = results.Count(result => result.ModeledAbsolutePredictionErrorMicroseconds < result.BaselineAbsolutePredictionErrorMicroseconds); + var documentOut = new ComplexContourChannelModelEvaluationDocument( + "mimir.bioacoustic.complex-contour-channel-model-evaluation.v1", + DateTimeOffset.UtcNow, + receiptPath, + channelModelPath, + results.Count, + improved, + results.Count == 0 ? 0.0 : results.Average(result => result.DeltaAbsolutePredictionErrorMicroseconds), + results.Count == 0 ? 0.0 : results.Average(result => result.DeltaMeanAbsoluteErrorSamples), + results.ToArray()); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? "."); + File.WriteAllText(outputPath, JsonSerializer.Serialize(documentOut, new JsonSerializerOptions { WriteIndented = true })); + Console.WriteLine( + $"complex-contour-channel-evaluation path={outputPath} cases={documentOut.CasesEvaluated} improved={documentOut.CasesImproved} " + + $"meanDeltaAbsUs={documentOut.MeanDeltaAbsolutePredictionErrorMicroseconds:+0.000;-0.000;0.000} meanDeltaMaeSamples={documentOut.MeanDeltaMeanAbsoluteErrorSamples:+0.000;-0.000;0.000}"); + return results.Count == 0 ? 1 : 0; +} + static int CalibrateContestantAsioFloat32( string inputPath, string outputPath, @@ -4981,10 +5265,83 @@ public sealed record ComplexContourReplayResult( int CandidateHits, MimirAcousticReflectionTap[] ReflectionTaps, double FirstReflectionSamples, - ComplexContourBandResidual[] StrongestBands); + ComplexContourBandResidual[] StrongestBands, + ComplexContourBandResidual[] BandResiduals); public sealed record ComplexContourBandResidual( double CenterHz, double Weight, double DelayResidualSamples, double PhaseResidualRadians); + +public sealed record ComplexContourChannelModelDocument( + string Schema, + DateTimeOffset CreatedUtc, + string SourceReceiptPath, + ComplexContourPathChannelModel[] Paths); + +public sealed record ComplexContourPathChannelModel( + string PathId, + int SampleRate, + int ReferenceChannel, + int CandidateChannel, + string[] CaseIds, + ComplexContourBandCorrection[] Corrections, + ComplexContourReflectionCorrection[] ReflectionTaps, + int UsableBandCount, + double Reliability, + double DelaySpreadSamples) +{ + public MimirDirectPathChannelModel ToRuntimeModel() => + new(Corrections + .Where(correction => correction.Usable) + .Select(correction => new MimirDirectPathBandCorrection( + correction.CenterHz, + correction.DelayCorrectionSamples, + correction.PhaseCorrectionRadians, + correction.Weight * correction.Reliability)) + .ToArray()); +} + +public sealed record ComplexContourBandCorrection( + double CenterHz, + double DelayCorrectionSamples, + double PhaseCorrectionRadians, + double Weight, + int ObservationCount, + double DelayStdDevSamples, + double Reliability, + bool Usable); + +public sealed record ComplexContourReflectionCorrection( + double RelativeDelaySamples, + int ObservationCount, + double MeanRelativeDelaySamples); + +public sealed record ComplexContourChannelModelEvaluationDocument( + string Schema, + DateTimeOffset CreatedUtc, + string ReplayReceiptPath, + string ChannelModelPath, + int CasesEvaluated, + int CasesImproved, + double MeanDeltaAbsolutePredictionErrorMicroseconds, + double MeanDeltaMeanAbsoluteErrorSamples, + ComplexContourChannelModelEvaluationCase[] Cases); + +public sealed record ComplexContourChannelModelEvaluationCase( + string CaseId, + string PathId, + double BaselineDelayMicroseconds, + double ModeledDelayMicroseconds, + double BaselineAbsolutePredictionErrorMicroseconds, + double ModeledAbsolutePredictionErrorMicroseconds, + double BaselineMeanAbsoluteErrorSamples, + double ModeledMeanAbsoluteErrorSamples, + double BaselineMeanAbsolutePhaseErrorRadians, + double ModeledMeanAbsolutePhaseErrorRadians, + double BaselineConfidence, + double ModeledConfidence, + double DeltaAbsolutePredictionErrorMicroseconds, + double DeltaMeanAbsoluteErrorSamples, + double DeltaMeanAbsolutePhaseErrorRadians); diff --git a/src/Mimir.Runtime/Synchronization/MimirComplexContourMatchedFilter.cs b/src/Mimir.Runtime/Synchronization/MimirComplexContourMatchedFilter.cs index 9448f3b..b93a86c 100644 --- a/src/Mimir.Runtime/Synchronization/MimirComplexContourMatchedFilter.cs +++ b/src/Mimir.Runtime/Synchronization/MimirComplexContourMatchedFilter.cs @@ -75,10 +75,15 @@ public sealed class MimirDirectPathChannelModel(IReadOnlyList pair.Distance) .First(); return nearest.Distance > maxDistanceHz - ? 0.0 - : nearest.Correction.DelayCorrectionSamples; + ? null + : nearest.Correction; } } @@ -327,14 +332,22 @@ public sealed class MimirDirectPathTracker( var weight = Math.Sqrt(Math.Max(0.0, reference.Score) * Math.Max(0.0, candidate.Score)); var sampleDelay = candidate.SampleOffset - reference.SampleOffset; - var phaseDelta = WrapRadians(candidate.PhaseRadians - reference.PhaseRadians); + var channelModel = options.ChannelModel ?? MimirDirectPathChannelModel.Empty; + var channelCorrection = channelModel.CorrectionFor(candidate.CenterHz); + if (channelModel.Corrections.Count > 0) + { + weight *= channelCorrection == null + ? 0.25 + : Math.Clamp(0.50 + channelCorrection.Weight, 0.25, 1.50); + } + + var phaseDelta = WrapRadians(candidate.PhaseRadians - reference.PhaseRadians - (channelCorrection?.PhaseCorrectionRadians ?? 0.0)); var phaseDelay = candidate.CenterHz <= 1.0 ? 0.0 : phaseDelta * sampleRate / (Math.Tau * candidate.CenterHz); var maxPhaseCorrection = sampleRate / Math.Max(1.0, candidate.CenterHz) * 0.5; - var channelCorrection = (options.ChannelModel ?? MimirDirectPathChannelModel.Empty).DelayCorrectionFor(candidate.CenterHz); observations.Add(new MimirDirectPathObservation( - sampleDelay + Math.Clamp(phaseDelay, -maxPhaseCorrection, maxPhaseCorrection) - channelCorrection, + sampleDelay + Math.Clamp(phaseDelay, -maxPhaseCorrection, maxPhaseCorrection) - (channelCorrection?.DelayCorrectionSamples ?? 0.0), sampleDelay, phaseDelta, candidate.CenterHz, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 88cff36..6b33524 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -104,3 +104,4 @@ {"ts": "2026-05-25T02:24:00+01:00", "type": "implementation", "status": "partial", "note": "Built the first sonar-shaped recursive refinement slice: MimirComplexContourMatchedFilterBank emits complex matched-filter contour-anchor hits with multiple candidate lobes, and MimirDirectPathTracker uses the current predicted path delay as authority, selects the coherent direct-path cluster inside that gate, and records later clusters as reflection taps. Synthetic reflection smoke passes: at 192 kHz with delay 693.5 samples and a louder reflection 116 samples later, estimate is 692.492189 samples (5.249 us error). Stored Scarlett ASIO receipts run through --complex-contour-asio-f32: shotgun path prediction errors are -5.860 us and +7.247 us; cardioid errors are -15.817 us and +8.963 us. This is a better receiver architecture, but low confidence and multi-sample MAE mean the full phase/group-delay channel model is still missing."} {"ts": "2026-05-25T02:42:00+01:00", "type": "implementation", "status": "partial", "note": "Extended the complex contour tracker with per-band phase/delay residual observations and a MimirDirectPathChannelModel correction surface. BufferSmoke replay panel now prints strongest band residuals and phase MAE: stored/fresh shotgun phase MAE is 0.617/0.852 rad, stored/fresh cardioid phase MAE is 0.687/0.657 rad. A same-window correction experiment improved one case but worsened others, so it was not promoted; channel correction must be learned across time/captures before it can influence live timing authority."} {"ts": "2026-05-25T02:53:00+01:00", "type": "verification", "status": "ok", "note": "Added persisted complex contour replay receipt generation. `dotnet run -c Release --no-build --project src/Mimir.BufferSmoke/Mimir.BufferSmoke.csproj -- --complex-contour-replay-panel --output calibration/bioacoustic/complex-contour-replay-panel.json` writes the four stored/fresh shotgun/cardioid cases with delay error, phase MAE, reflection taps, and strongest band residuals. Build passes and synthetic 192 kHz reflection smoke still reports 5.249 us error."} +{"ts": "2026-05-25T03:40:00+01:00", "type": "implementation", "status": "partial", "note": "Built explicit complex contour channel learning and evaluation. `--learn-complex-contour-channel-model` learns path-scoped per-band delay/phase correction and reflection taps from `calibration/bioacoustic/complex-contour-replay-panel.json`, writing `calibration/bioacoustic/complex-contour-channel-model.json`. `MimirDirectPathTracker` now applies supplied channel-model phase/group-delay correction and downweights bands outside the usable learned surface. `--evaluate-complex-contour-channel-model` writes `calibration/bioacoustic/complex-contour-channel-model-evaluation.json`: current model improves 3/4 absolute path-seed errors, mean absolute path error by 2.137 us, and mean cluster MAE by 0.713 samples; fresh cardioid reaches 0.202 us from seed, but stored cardioid worsens from 15.817 us to 17.476 us. Treat the model as explicit calibration evidence, not default runtime authority, until more captures prove stability."} diff --git a/state/map.yaml b/state/map.yaml index 9abbec8..14dbc87 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -228,7 +228,17 @@ accepted_design: Low confidence is expected until phase/group-delay surfaces are learned. The tracker now exposes per-band delay/phase residual observations and a `MimirDirectPathChannelModel` correction surface for multi-window - calibration; one-window self-correction is not live authority. + calibration; one-window self-correction is not live authority. The tracker + now consumes explicit channel models as phase/group-delay correction and + downweights bands outside the learned usable surface. `Mimir.BufferSmoke + --learn-complex-contour-channel-model` persists + `calibration/bioacoustic/complex-contour-channel-model.json` from the + replay panel, and `--evaluate-complex-contour-channel-model` persists + `calibration/bioacoustic/complex-contour-channel-model-evaluation.json`. + Current evaluation improves 3/4 absolute path-seed errors and lowers mean + direct-cluster MAE; the fresh cardioid case reaches about 0.202 us from + seed, while the stored cardioid case still worsens to about 17.476 us. The + model is explicit calibration evidence, not default runtime authority. `calibration/bioacoustic/complex-contour-replay-panel.json` persists the current four-case replay receipt. Acoustic robustness remains open.