-
Notifications
You must be signed in to change notification settings - Fork 113
/
confirmHelper.ts
200 lines (176 loc) · 7.79 KB
/
confirmHelper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import { fetchUrlCached } from '../../services/commHelper';
import i18next from 'i18next';
import { logDebug } from '../../plugin/logger';
import { LabelOption, LabelOptions, MultilabelKey, InputDetails } from '../../types/labelTypes';
import { CompositeTrip, InferredLabels, TimelineEntry } from '../../types/diaryTypes';
import { UserInputMap } from '../../TimelineContext';
import DEFAULT_LABEL_OPTIONS from 'e-mission-common/src/emcommon/resources/label-options.default.json';
let appConfig;
export let labelOptions: LabelOptions;
export let inputDetails: InputDetails<MultilabelKey>;
export async function getLabelOptions(appConfigParam?) {
if (appConfigParam) appConfig = appConfigParam;
if (labelOptions) return labelOptions;
if (appConfig.label_options) {
const labelOptionsJson = await fetchUrlCached(appConfig.label_options);
if (labelOptionsJson) {
logDebug(`label_options found in config, using dynamic label options
at ${appConfig.label_options}`);
labelOptions = JSON.parse(labelOptionsJson) as LabelOptions;
} else {
throw new Error('Label options were falsy from ' + appConfig.label_options);
}
} else {
labelOptions = DEFAULT_LABEL_OPTIONS;
}
return labelOptions;
}
export const labelOptionByValue = (value: string, labelType: string): LabelOption | undefined =>
labelOptions[labelType]?.find((o) => o.value == value) || getFakeEntry(value);
export const baseLabelInputDetails = {
MODE: {
name: 'MODE',
labeltext: 'diary.mode',
choosetext: 'diary.choose-mode',
key: 'manual/mode_confirm',
},
PURPOSE: {
name: 'PURPOSE',
labeltext: 'diary.purpose',
choosetext: 'diary.choose-purpose',
key: 'manual/purpose_confirm',
},
};
export function getLabelInputDetails(appConfigParam?) {
if (appConfigParam) appConfig = appConfigParam;
if (inputDetails) return inputDetails;
if (!appConfig.intro.mode_studied) {
/* If there is no mode of interest, we don't need REPLACED_MODE.
So just return the base input details. */
return baseLabelInputDetails;
}
// else this is a program, so add the REPLACED_MODE
inputDetails = {
...baseLabelInputDetails,
REPLACED_MODE: {
name: 'REPLACED_MODE',
labeltext: 'diary.replaces',
choosetext: 'diary.choose-replaced-mode',
key: 'manual/replaced_mode',
},
};
return inputDetails;
}
export function labelInputDetailsForTrip(userInputForTrip, appConfigParam?) {
if (appConfigParam) appConfig = appConfigParam;
if (appConfig.intro.mode_studied) {
if (userInputForTrip?.['MODE']?.data?.label == appConfig.intro.mode_studied) {
logDebug(`Found trip labeled with ${userInputForTrip?.['MODE']?.data?.label}, mode of study = ${appConfig.intro.mode_studied}.
Needs REPLACED_MODE`);
return getLabelInputDetails();
} else {
return baseLabelInputDetails;
}
} else {
return getLabelInputDetails();
}
}
export const getLabelInputs = () => Object.keys(getLabelInputDetails()) as MultilabelKey[];
export const getBaseLabelInputs = () => Object.keys(baseLabelInputDetails) as MultilabelKey[];
/** @description replace all underscores with spaces, and capitalizes the first letter of each word */
export function labelKeyToReadable(otherValue: string) {
if (otherValue == otherValue.toUpperCase()) {
// if all caps, make lowercase
otherValue = otherValue.toLowerCase();
}
const words = otherValue.replace(/_/g, ' ').trim().split(' ');
if (words.length == 0) return '';
return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
}
/** @description replaces all spaces with underscores, and lowercases the string */
export const readableLabelToKey = (otherText: string) =>
otherText.trim().replace(/ /g, '_').toLowerCase();
export function getFakeEntry(otherValue): Partial<LabelOption> | undefined {
if (!otherValue) return undefined;
return {
value: otherValue,
};
}
export let labelTextToKeyMap: { [key: string]: string } = {};
export const labelKeyToText = (labelKey: string) => {
const lang = i18next.resolvedLanguage || 'en';
const text =
labelOptions?.translations?.[lang]?.[labelKey] ||
labelOptions?.translations?.[lang]?.[labelKey] ||
labelKeyToReadable(labelKey);
labelTextToKeyMap[text] = labelKey;
return text;
};
export const textToLabelKey = (text: string) => labelTextToKeyMap[text] || readableLabelToKey(text);
/** @description e.g. manual/mode_confirm becomes mode_confirm */
export const removeManualPrefix = (key: string) => key.split('/')[1];
/** @description e.g. 'MODE' gets looked up, its key is 'manual/mode_confirm'. Returns without prefix as 'mode_confirm' */
export const inputType2retKey = (inputType: string) =>
removeManualPrefix(getLabelInputDetails()[inputType].key);
export function verifiabilityForTrip(trip: CompositeTrip, userInputForTrip?: UserInputMap) {
let allConfirmed = true;
let someInferred = false;
const inputsForTrip = Object.keys(labelInputDetailsForTrip(userInputForTrip));
for (const inputType of inputsForTrip) {
const finalInference = inferFinalLabels(trip, userInputForTrip)[inputType];
const confirmed = userInputForTrip?.[inputType];
const inferred = finalInference && Object.values(finalInference).some((o) => o);
if (inferred && !confirmed) someInferred = true;
if (!confirmed) allConfirmed = false;
}
return someInferred ? 'can-verify' : allConfirmed ? 'already-verified' : 'cannot-verify';
}
export function inferFinalLabels(trip: CompositeTrip, userInputForTrip?: UserInputMap) {
// Deep copy the possibility tuples
let labelsList: InferredLabels = [];
if (trip.inferred_labels) {
labelsList = JSON.parse(JSON.stringify(trip.inferred_labels));
}
// Capture the level of certainty so we can reconstruct it later
const totalCertainty = labelsList.map((item) => item.p).reduce((item, rest) => item + rest, 0);
// Filter out the tuples that are inconsistent with existing green labels
for (const inputType of getLabelInputs()) {
const userInput = userInputForTrip?.[inputType];
if (userInput) {
const retKey = inputType2retKey(inputType);
labelsList = labelsList.filter((item) => item.labels[retKey] == userInput.data.label);
}
}
const finalInference: { [k in MultilabelKey]?: LabelOption } = {};
// Return early with (empty obj) if there are no possibilities left
if (labelsList.length == 0) {
return finalInference;
} else {
// Normalize probabilities to previous level of certainty
const certaintyScalar =
totalCertainty / labelsList.map((item) => item.p).reduce((item, rest) => item + rest);
labelsList.forEach((item) => (item.p *= certaintyScalar));
for (const inputType of getLabelInputs()) {
// For each label type, find the most probable value by binning by label value and summing
const retKey = inputType2retKey(inputType);
let valueProbs = new Map();
for (const tuple of labelsList) {
const labelValue = tuple.labels[retKey];
if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0);
valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p);
}
let max = { p: 0, labelValue: undefined };
for (const [thisLabelValue, thisP] of valueProbs) {
// In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order)
if (thisP > max.p) max = { p: thisP, labelValue: thisLabelValue };
}
// Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold
// Fails safe if confidence_threshold doesn't exist
if (max.p <= trip.confidence_threshold) max.labelValue = undefined;
if (max.labelValue) {
finalInference[inputType] = labelOptionByValue(max.labelValue, inputType);
}
}
return finalInference;
}
}