Commit pirate JS files
This commit is contained in:
553
js/CandidateFilterByFingerprinting.js
Normal file
553
js/CandidateFilterByFingerprinting.js
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
Copyright (c) 2023-forever Douglas Malnati. All rights reserved.
|
||||
|
||||
See the /faq/tos page for details.
|
||||
|
||||
(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.)
|
||||
*/
|
||||
|
||||
import { CandidateFilterBase } from './CandidateFilterBase.js';
|
||||
import { NonRejectedOnlyFilter } from './WsprMessageCandidate.js';
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Candidate Filter - Fingerprinting
|
||||
//
|
||||
// Identify messages that appear to be yours by matching frequencies
|
||||
// to data you believe in. Reject everything else.
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class CandidateFilterByFingerprinting
|
||||
extends CandidateFilterBase
|
||||
{
|
||||
constructor(t)
|
||||
{
|
||||
super("ByFingerprinting", t);
|
||||
}
|
||||
|
||||
OnFilterStart()
|
||||
{
|
||||
this.t.Event(`CandidateFilterByFingerprinting Start`);
|
||||
this.windowDataSet = new Set();
|
||||
}
|
||||
|
||||
OnFilterEnd()
|
||||
{
|
||||
for (const windowData of this.windowDataSet)
|
||||
{
|
||||
if (windowData?.fingerprintingData)
|
||||
{
|
||||
windowData.fingerprintingData.winFreqDrift = this.EstimateWindowFreqDrift(windowData);
|
||||
}
|
||||
}
|
||||
|
||||
this.t.Event(`CandidateFilterByFingerprinting End`);
|
||||
}
|
||||
|
||||
FilterWindowAlgorithm(msgListList)
|
||||
{
|
||||
this.FingerprintAlgorithm_ByReference(msgListList);
|
||||
}
|
||||
|
||||
|
||||
// private
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// ByReference Algorithm
|
||||
//
|
||||
// If you can find a reference message you believe to be yours, match up
|
||||
// messages in the other slots by frequency to that reference frequency,
|
||||
// then reject all others.
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
FingerprintAlgorithm_ByReference(msgListList)
|
||||
{
|
||||
let windowData = this.GetWindowDataFromMsgListList(msgListList);
|
||||
if (windowData)
|
||||
{
|
||||
this.windowDataSet.add(windowData);
|
||||
|
||||
if (windowData.fingerprintingData)
|
||||
{
|
||||
windowData.fingerprintingData.winFreqDrift = null;
|
||||
}
|
||||
}
|
||||
|
||||
let reference = this.FindNearestReference(msgListList);
|
||||
if (!reference.ok)
|
||||
{
|
||||
for (let msgList of msgListList)
|
||||
{
|
||||
let msgListCandidate = NonRejectedOnlyFilter(msgList);
|
||||
this.RejectAllInList(msgListCandidate, reference.reason);
|
||||
}
|
||||
|
||||
if (windowData?.fingerprintingData)
|
||||
{
|
||||
windowData.fingerprintingData.referenceAudit = this.CreateReferenceAuditData(null, null, reference.source, reference.reason);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let referenceMsg = reference.msg;
|
||||
let referenceSlot = reference.slot;
|
||||
windowData = referenceMsg.windowShortcut;
|
||||
let referenceAudit = this.CreateReferenceAuditData(referenceMsg, referenceSlot, reference.source, reference.reason);
|
||||
if (windowData?.fingerprintingData)
|
||||
{
|
||||
windowData.fingerprintingData.referenceAudit = referenceAudit;
|
||||
}
|
||||
|
||||
for (let slot = 0; slot < 5; ++slot)
|
||||
{
|
||||
if (slot == referenceSlot)
|
||||
{
|
||||
referenceAudit.slotAuditList[slot] = this.CreateSlotAuditData(slot, [referenceMsg], referenceMsg, referenceAudit);
|
||||
referenceAudit.slotAuditList[slot].outcome = "reference";
|
||||
referenceAudit.slotAuditList[slot].msgMatched = referenceMsg;
|
||||
referenceAudit.slotAuditList[slot].msgMatchList = [referenceMsg];
|
||||
continue;
|
||||
}
|
||||
|
||||
let msgCandidateList = NonRejectedOnlyFilter(msgListList[slot]);
|
||||
|
||||
let msgMatchList = [];
|
||||
let freqHzDiffMatch = 0;
|
||||
const FREQ_HZ_PLUS_MINUS_THRESHOLD = 5; // it's +/- this number, so 10Hz
|
||||
let slotAudit = this.CreateSlotAuditData(slot, msgCandidateList, referenceMsg, referenceAudit);
|
||||
referenceAudit.slotAuditList[slot] = slotAudit;
|
||||
if (msgCandidateList.length)
|
||||
{
|
||||
slotAudit.msgAuditList = this.GetMsgFingerprintAuditList(referenceMsg, msgCandidateList, referenceAudit.referenceRxCall__rxRecordListMap);
|
||||
|
||||
let matchResult = this.GetWithinThresholdMatchResult(slotAudit.msgAuditList, FREQ_HZ_PLUS_MINUS_THRESHOLD);
|
||||
msgMatchList = matchResult.msgMatchList;
|
||||
freqHzDiffMatch = matchResult.freqHzDiffMatch;
|
||||
slotAudit.matchThresholdHz = matchResult.matchThresholdHz;
|
||||
}
|
||||
|
||||
if (msgMatchList.length == 0)
|
||||
{
|
||||
this.RejectAllInList(msgCandidateList, `Fingerprint match fail, exceeded ${FREQ_HZ_PLUS_MINUS_THRESHOLD} threshold.`);
|
||||
slotAudit.outcome = "no_match";
|
||||
}
|
||||
else if (msgMatchList.length == 1)
|
||||
{
|
||||
this.RejectAllInListExcept(msgCandidateList, msgMatchList[0], `Fingerprint matched other message`);
|
||||
slotAudit.outcome = "single_match";
|
||||
slotAudit.msgMatched = msgMatchList[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
slotAudit.outcome = "multi_match";
|
||||
}
|
||||
|
||||
slotAudit.msgMatchList = msgMatchList;
|
||||
slotAudit.thresholdHzTriedMax = msgCandidateList.length ? freqHzDiffMatch : null;
|
||||
}
|
||||
}
|
||||
|
||||
FindNearestReference(msgListList)
|
||||
{
|
||||
let slot0List = NonRejectedOnlyFilter(msgListList[0]);
|
||||
if (slot0List.length == 1)
|
||||
{
|
||||
return {
|
||||
ok: true,
|
||||
msg: slot0List[0],
|
||||
slot: 0,
|
||||
source: "slot0",
|
||||
reason: "Using unique slot 0 message as fingerprint reference.",
|
||||
};
|
||||
}
|
||||
|
||||
for (let slot = 0; slot < 5; ++slot)
|
||||
{
|
||||
let msgConfirmedList = NonRejectedOnlyFilter(msgListList[slot]).filter(msg => msg.IsConfirmed());
|
||||
if (msgConfirmedList.length == 1)
|
||||
{
|
||||
return {
|
||||
ok: true,
|
||||
msg: msgConfirmedList[0],
|
||||
slot: slot,
|
||||
source: "borrowed_confirmed_within_window",
|
||||
reason: `Borrowed fingerprint reference from earliest confirmed message in slot ${slot}.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (slot0List.length == 0)
|
||||
{
|
||||
return {
|
||||
ok: false,
|
||||
msg: null,
|
||||
slot: null,
|
||||
source: "none",
|
||||
reason: `No anchor frequency message in slot 0, and no confirmed message available to borrow within the window.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
msg: null,
|
||||
slot: null,
|
||||
source: "none",
|
||||
reason: `Too many candidates (${slot0List.length}) in slot 0, and no unique confirmed message available to borrow within the window.`,
|
||||
};
|
||||
}
|
||||
|
||||
GetWindowDataFromMsgListList(msgListList)
|
||||
{
|
||||
for (const msgList of msgListList)
|
||||
{
|
||||
for (const msg of msgList)
|
||||
{
|
||||
if (msg?.windowShortcut)
|
||||
{
|
||||
return msg.windowShortcut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CreateReferenceAuditData(referenceMsg, referenceSlot, referenceSource, referenceReason)
|
||||
{
|
||||
return {
|
||||
referenceMsg: referenceMsg,
|
||||
referenceSlot: referenceSlot,
|
||||
referenceSource: referenceSource,
|
||||
referenceReason: referenceReason,
|
||||
referenceRxCall__rxRecordListMap: referenceMsg ? this.MakeRxCallToRxRecordListMap(referenceMsg) : new Map(),
|
||||
slotAuditList: [null, null, null, null, null],
|
||||
};
|
||||
}
|
||||
|
||||
CreateSlotAuditData(slot, msgCandidateList, referenceMsg, referenceAudit)
|
||||
{
|
||||
return {
|
||||
slot: slot,
|
||||
referenceMsg: referenceMsg,
|
||||
referenceSlot: referenceAudit.referenceSlot,
|
||||
referenceRxCall__rxRecordListMap: referenceAudit.referenceRxCall__rxRecordListMap,
|
||||
msgCandidateList: msgCandidateList,
|
||||
msgAuditList: [],
|
||||
msgMatchList: [],
|
||||
msgMatched: null,
|
||||
matchThresholdHz: null,
|
||||
thresholdHzTriedMax: null,
|
||||
outcome: msgCandidateList.length ? "pending" : "no_candidates",
|
||||
};
|
||||
}
|
||||
|
||||
GetMsgFingerprintAuditList(msgA, msgBList, rxCall__rxRecordListAMap)
|
||||
{
|
||||
let msgAuditList = [];
|
||||
|
||||
for (let msgB of msgBList)
|
||||
{
|
||||
let diffData = this.GetMinFreqDiffData(msgA, msgB, rxCall__rxRecordListAMap);
|
||||
msgAuditList.push({
|
||||
msg: msgB,
|
||||
minFreqDiff: diffData.minDiff,
|
||||
rxCallMatchList: diffData.rxCallMatchList,
|
||||
candidateRxCall__rxRecordListMap: diffData.rxCall__rxRecordListB,
|
||||
});
|
||||
}
|
||||
|
||||
return msgAuditList;
|
||||
}
|
||||
|
||||
GetWithinThresholdMatchResult(msgAuditList, thresholdMax)
|
||||
{
|
||||
let msgMatchList = [];
|
||||
let freqHzDiffMatch = thresholdMax;
|
||||
let matchThresholdHz = null;
|
||||
|
||||
for (freqHzDiffMatch = 0; freqHzDiffMatch <= thresholdMax; ++freqHzDiffMatch)
|
||||
{
|
||||
msgMatchList = msgAuditList
|
||||
.filter(msgAudit => msgAudit.minFreqDiff != null && msgAudit.minFreqDiff <= freqHzDiffMatch)
|
||||
.map(msgAudit => msgAudit.msg);
|
||||
|
||||
if (msgMatchList.length != 0)
|
||||
{
|
||||
matchThresholdHz = freqHzDiffMatch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
msgMatchList,
|
||||
freqHzDiffMatch,
|
||||
matchThresholdHz,
|
||||
};
|
||||
}
|
||||
|
||||
// Return the set of msgBList elements which fall within the threshold difference
|
||||
// of frequency when compared to msgA.
|
||||
GetWithinThresholdList(msgA, msgBList, threshold)
|
||||
{
|
||||
let msgListWithinThreshold = [];
|
||||
|
||||
// calculate minimum frequency diff between msgA and this
|
||||
// message of this slot
|
||||
let msg__minFreqDiff = new Map();
|
||||
for (let msgB of msgBList)
|
||||
{
|
||||
msg__minFreqDiff.set(msgB, this.GetMinFreqDiff(msgA, msgB));
|
||||
}
|
||||
|
||||
// find out which messages fall within tolerance
|
||||
for (let [msgB, freqDiff] of msg__minFreqDiff)
|
||||
{
|
||||
if (freqDiff != null && freqDiff <= threshold)
|
||||
{
|
||||
msgListWithinThreshold.push(msgB);
|
||||
}
|
||||
}
|
||||
|
||||
return msgListWithinThreshold;
|
||||
}
|
||||
|
||||
MakeRxCallToRxRecordListMap(msg, limitBySet)
|
||||
{
|
||||
limitBySet = limitBySet ?? null;
|
||||
|
||||
let rxCall__recordMap = new Map();
|
||||
|
||||
for (let rxRecord of msg.rxRecordList)
|
||||
{
|
||||
let rxCall = rxRecord.rxCallsign;
|
||||
|
||||
if (limitBySet == null || limitBySet.has(rxCall))
|
||||
{
|
||||
if (rxCall__recordMap.has(rxCall) == false)
|
||||
{
|
||||
rxCall__recordMap.set(rxCall, []);
|
||||
}
|
||||
|
||||
rxCall__recordMap.get(rxCall).push(rxRecord);
|
||||
}
|
||||
}
|
||||
|
||||
return rxCall__recordMap;
|
||||
}
|
||||
|
||||
// Find min diff of entries in B compared to looked up in A.
|
||||
// Only compare equal rxCallsigns.
|
||||
// So, we're looking at:
|
||||
// - for any common rxCallsign
|
||||
// - across all frequencies reported by that rxCallsign
|
||||
// - what is the minimum difference in frequency seen?
|
||||
GetMinFreqDiff(msgA, msgB)
|
||||
{
|
||||
return this.GetMinFreqDiffData(msgA, msgB).minDiff;
|
||||
}
|
||||
|
||||
GetMinFreqDiffData(msgA, msgB, rxCall__rxRecordListAMap)
|
||||
{
|
||||
let rxCall__rxRecordListA = rxCall__rxRecordListAMap ?? this.MakeRxCallToRxRecordListMap(msgA);
|
||||
let rxCall__rxRecordListB = this.MakeRxCallToRxRecordListMap(msgB, rxCall__rxRecordListA);
|
||||
|
||||
let minDiff = null;
|
||||
let rxCallMatchList = [];
|
||||
for (let [rxCall, rxRecordListB] of rxCall__rxRecordListB)
|
||||
{
|
||||
let rxRecordListA = rxCall__rxRecordListA.get(rxCall);
|
||||
|
||||
// unavoidable(?) M*N operation here, hopefully M and N are small
|
||||
let diff = this.GetMinFreqDiffRxRecordList(rxRecordListA, rxRecordListB);
|
||||
|
||||
rxCallMatchList.push({
|
||||
rxCall,
|
||||
rxRecordListA,
|
||||
rxRecordListB,
|
||||
minFreqDiff: diff,
|
||||
});
|
||||
|
||||
if (minDiff == null || diff < minDiff)
|
||||
{
|
||||
minDiff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minDiff,
|
||||
rxCall__rxRecordListA,
|
||||
rxCall__rxRecordListB,
|
||||
rxCallMatchList,
|
||||
};
|
||||
}
|
||||
|
||||
// Returns the smallest absolute difference between frequencies found in the two
|
||||
// supplied record lists. This is an M*N operation.
|
||||
//
|
||||
// This function has no knowledge or assumptions about the contents of the
|
||||
// two lists (ie whether the callsigns are the same).
|
||||
//
|
||||
// This is simply a function broken out to keep calling code simpler.
|
||||
GetMinFreqDiffRxRecordList(rxRecordListA, rxRecordListB)
|
||||
{
|
||||
let minDiff = null;
|
||||
|
||||
for (let rxRecordA of rxRecordListA)
|
||||
{
|
||||
for (let rxRecordB of rxRecordListB)
|
||||
{
|
||||
let diff = Math.abs(rxRecordA.frequency - rxRecordB.frequency);
|
||||
|
||||
if (minDiff == null || diff < minDiff)
|
||||
{
|
||||
minDiff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minDiff;
|
||||
}
|
||||
|
||||
EstimateWindowFreqDrift(windowData)
|
||||
{
|
||||
let referenceAudit = windowData?.fingerprintingData?.referenceAudit;
|
||||
if (!referenceAudit?.referenceMsg)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let slotEstimateList = [];
|
||||
for (let slot = (referenceAudit.referenceSlot + 1); slot < 5; ++slot)
|
||||
{
|
||||
let slotAudit = referenceAudit.slotAuditList?.[slot];
|
||||
let slotDelta = this.EstimateWindowFreqDriftForSlot(slotAudit);
|
||||
if (slotDelta == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
slotEstimateList.push({
|
||||
deltaHz: slotDelta,
|
||||
weight: slot - referenceAudit.referenceSlot,
|
||||
});
|
||||
}
|
||||
|
||||
if (slotEstimateList.length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let weightedSum = 0;
|
||||
let weightSum = 0;
|
||||
for (const estimate of slotEstimateList)
|
||||
{
|
||||
weightedSum += estimate.deltaHz * estimate.weight;
|
||||
weightSum += estimate.weight;
|
||||
}
|
||||
|
||||
if (weightSum == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.round(weightedSum / weightSum);
|
||||
}
|
||||
|
||||
EstimateWindowFreqDriftForSlot(slotAudit)
|
||||
{
|
||||
if (!slotAudit?.msgAuditList?.length || !slotAudit?.msgMatchList?.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let msgMatchSet = new Set(slotAudit.msgMatchList);
|
||||
let rxCall__bestDelta = new Map();
|
||||
|
||||
for (const msgAudit of slotAudit.msgAuditList)
|
||||
{
|
||||
if (!msgMatchSet.has(msgAudit.msg))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rxCallMatch of msgAudit.rxCallMatchList || [])
|
||||
{
|
||||
let bestSignedDelta = this.GetBestSignedFreqDelta(rxCallMatch.rxRecordListA, rxCallMatch.rxRecordListB);
|
||||
if (bestSignedDelta == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let rxCall = rxCallMatch.rxCall;
|
||||
if (!rxCall__bestDelta.has(rxCall))
|
||||
{
|
||||
rxCall__bestDelta.set(rxCall, bestSignedDelta);
|
||||
continue;
|
||||
}
|
||||
|
||||
let cur = rxCall__bestDelta.get(rxCall);
|
||||
if (Math.abs(bestSignedDelta) < Math.abs(cur))
|
||||
{
|
||||
rxCall__bestDelta.set(rxCall, bestSignedDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deltaList = Array.from(rxCall__bestDelta.values());
|
||||
if (deltaList.length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.GetMedian(deltaList);
|
||||
}
|
||||
|
||||
GetBestSignedFreqDelta(rxRecordListA, rxRecordListB)
|
||||
{
|
||||
if (!Array.isArray(rxRecordListA) || !Array.isArray(rxRecordListB))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let bestSignedDelta = null;
|
||||
for (const rxRecordA of rxRecordListA)
|
||||
{
|
||||
for (const rxRecordB of rxRecordListB)
|
||||
{
|
||||
let freqA = Number(rxRecordA?.frequency);
|
||||
let freqB = Number(rxRecordB?.frequency);
|
||||
if (!Number.isFinite(freqA) || !Number.isFinite(freqB))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let signedDelta = freqB - freqA;
|
||||
if (bestSignedDelta == null || Math.abs(signedDelta) < Math.abs(bestSignedDelta))
|
||||
{
|
||||
bestSignedDelta = signedDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestSignedDelta;
|
||||
}
|
||||
|
||||
GetMedian(numList)
|
||||
{
|
||||
if (!numList || numList.length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let list = [...numList].sort((a, b) => a - b);
|
||||
let idxMid = Math.floor(list.length / 2);
|
||||
if (list.length % 2 == 1)
|
||||
{
|
||||
return list[idxMid];
|
||||
}
|
||||
|
||||
return (list[idxMid - 1] + list[idxMid]) / 2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user