Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/src/dataUtil/classifiersData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { loadJSON } from "../utils";

export type USPSAScoring = "Virginia" | "Comstock" | "Fixed Time";
export type SCSAScoring = "Time Plus";
export type Scoring = USPSAScoring | SCSAScoring;
export type NAScoring = "";
export type Scoring = USPSAScoring | SCSAScoring | NAScoring;

export interface ClassifierJSON {
id: string;
Expand Down Expand Up @@ -35,6 +36,10 @@ export const basicInfoForClassifier = (c: ClassifierJSON): ClassifierBasicInfo =
scoring: c?.scoring,
});

export const basicInfoForClassifierNumber = (
classifierCode: string,
): ClassifierBasicInfo => basicInfoForClassifier(classifiersByNumber[classifierCode]);

export const basicInfoForClassifierCode = (
classifierCode: string,
): ClassifierBasicInfo | undefined => {
Expand Down
64 changes: 48 additions & 16 deletions api/src/db/classifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import mongoose, { Model } from "mongoose";

import { stringSort } from "../../../shared/utils/sort";
import {
basicInfoForClassifier,
classifiers as _classifiers,
classifiersByNumber,
ClassifierJSON,
basicInfoForClassifierNumber,
ClassifierBasicInfo,
Scoring,
} from "../dataUtil/classifiersData";
import { divisionsForScoresAdapter, divShortNames } from "../dataUtil/divisions";
import { hhfsForDivision } from "../dataUtil/hhf";
Expand Down Expand Up @@ -86,16 +87,17 @@ const extendedInfoForClassifier = (
c: ClassifierJSON,
division: string,
hitFactorScores: Score[],
classifiersOnly?: boolean,
) => {
if (!division || !c?.id) {
return {};
}
const divisionHHFs = hhfsForDivision(division);
if (!divisionHHFs) {
if (!divisionHHFs && classifiersOnly) {
return {};
}
const curHHFInfo = divisionHHFs.find(dHHF => dHHF.classifier === c.id);
const hhf = Number(curHHFInfo.hhf);
const curHHFInfo = divisionHHFs?.find(dHHF => dHHF.classifier === c.id);
const hhf = Number(curHHFInfo?.hhf) || -1;

const topXPercentileStats = x => ({
[`top${x}PercentilePercent`]:
Expand Down Expand Up @@ -140,7 +142,7 @@ const extendedInfoForClassifier = (
.sort((a, b) => stringSort(a, b, "id", 1));

const result = {
updated: curHHFInfo.updated, //actualLastUpdate, // before was using curHHFInfo.updated, and it's bs
updated: curHHFInfo?.updated, //actualLastUpdate, // before was using curHHFInfo.updated, and it's bs
hhf,
prevHHF: hhfs.findLast(curHistorical => curHistorical.hhf !== hhf)?.hhf ?? hhf,
hhfs,
Expand Down Expand Up @@ -199,6 +201,7 @@ const ClassifierSchema = new mongoose.Schema<

const WORST_QUALITY_DISTANCE_FROM_TARGET = 100;
const scoresCountOffset = runsCount => {
return 0;
if (runsCount < 200) {
return -40;
} else if (runsCount < 400) {
Expand Down Expand Up @@ -231,11 +234,11 @@ ClassifierSchema.virtual("hqQuality").get(function () {
scoresCountOffset(this.runs) +
Percent(
WORST_QUALITY_DISTANCE_FROM_TARGET -
(10.0 * Math.abs(1 - this.inverse95CurPercentPercentile) +
4.0 * Math.abs(5 - this.inverse85CurPercentPercentile) +
1.0 * Math.abs(15 - this.inverse75CurPercentPercentile) +
0.5 * Math.abs(45 - this.inverse60CurPercentPercentile) +
0.3 * Math.abs(85 - this.inverse40CurPercentPercentile)),
(5.0 * Math.abs(1 - this.inverse95CurPercentPercentile) +
5.0 * Math.abs(5 - this.inverse85CurPercentPercentile) +
2.5 * Math.abs(15 - this.inverse75CurPercentPercentile) +
1.0 * Math.abs(45 - this.inverse60CurPercentPercentile) +
0.5 * Math.abs(85 - this.inverse40CurPercentPercentile)),
WORST_QUALITY_DISTANCE_FROM_TARGET,
)
);
Expand All @@ -244,14 +247,33 @@ ClassifierSchema.index({ classifier: 1, division: 1 }, { unique: true });
ClassifierSchema.index({ division: 1 });
export const Classifiers = mongoose.model("Classifiers", ClassifierSchema);

export interface StageInfo {
code: string; // aka classifierCode/classifierNumber
number: number; // 1, 2, 3, etc
name: string;
scoring: Scoring;
}

const basicInfoForStage = (stageClassifierCode: string): ClassifierBasicInfo => ({
id: stageClassifierCode,
code: stageClassifierCode,
classifier: stageClassifierCode,
name: stageClassifierCode.split(".")[1],
scoring: "",
});

export const singleClassifierExtendedMetaDoc = async (
division: string,
classifier: string,
recHHFReady?: RecHHF,
classifiersOnly?: boolean,
) => {
const c = classifiersByNumber[classifier];
const basicInfo = basicInfoForClassifier(c);
const basicInfo = classifiersOnly
? basicInfoForClassifierNumber(classifier)
: basicInfoForStage(classifier);

if (!basicInfo?.code) {
console.log(classifier);
return null;
}
const [recHHFQuery, hitFactorScoresRaw] = await Promise.all([
Expand Down Expand Up @@ -282,7 +304,7 @@ export const singleClassifierExtendedMetaDoc = async (
return {
division,
...basicInfo,
...extendedInfoForClassifier(c, division, hitFactorScores),
...extendedInfoForClassifier(basicInfo, division, hitFactorScores, classifiersOnly),
recHHF,
...inverseRecPercentileStats(100),
...inverseRecPercentileStats(95),
Expand Down Expand Up @@ -399,8 +421,14 @@ export const rehydrateSingleClassifier = async (
classifier: string,
division: string,
recHHF?: RecHHF,
onlyActualClassifiers: boolean = true,
) => {
const doc = await singleClassifierExtendedMetaDoc(division, classifier, recHHF);
const doc = await singleClassifierExtendedMetaDoc(
division,
classifier,
recHHF,
onlyActualClassifiers,
);
if (doc) {
return Classifiers.updateOne(
{ division, classifier },
Expand All @@ -413,7 +441,10 @@ export const rehydrateSingleClassifier = async (
};

// linear rehydration to prevent OOMs on uploader and mongod
export const rehydrateClassifiers = async (classifiers: ClassifierDivision[]) => {
export const rehydrateClassifiers = async (
classifiers: ClassifierDivision[],
onlyActualClassifiers: boolean = true,
) => {
const recHHFs = await RecHHFs.find({
classifierDivision: {
$in: classifiers.map(c => [c.classifier, c.division].join(":")),
Expand All @@ -432,6 +463,7 @@ export const rehydrateClassifiers = async (classifiers: ClassifierDivision[]) =>
classifier,
division,
recHHFsByClassifierDivision[[classifier, division].join(":")],
onlyActualClassifiers,
);
}
};
5 changes: 3 additions & 2 deletions api/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const connect = async () => {
}
const url = !LOCAL_DEV ? MONGO_URL : MONGO_URL_LOCAL;

const publicLogsDBName = url!.split("@")[1]?.split(".")[0] || "local";
const publicLogsDBHost = url!.split("@")[1]?.split(".")[0] || "local";
const dbName = url!.split("?")[0]?.split("/").reverse()[0] || "root";

const _connect = () => {
console.error("DB: connecting");
Expand All @@ -35,7 +36,7 @@ export const connect = async () => {
await _connect();
});
mongoose.connection.on("connected", () => {
console.error(`DB: connected to ${publicLogsDBName}`);
console.error(`DB: connected to ${publicLogsDBHost} ${dbName}`);
});

mongoose.connection.on("reconnected", () => {
Expand Down
33 changes: 33 additions & 0 deletions api/src/db/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ interface AlgoliaMatchNumericFilters {
timestamp_utc_updated: number;
}

export interface MatchDef {
match_id: string;
match_name: string;
match_type: string;
match_subtype: string;
match_creationdate: string;
match_modifieddate: string;
match_date: string; // 2024-12-31 format
templateName: string;
}

const MatchesSchema = new mongoose.Schema<Match>(
{
updated: Date,
Expand Down Expand Up @@ -96,6 +107,28 @@ const fetchMatchesRange = async (
}));
};

export const matchFromMatchDef = (
h: MatchDef,
forcedTemplateName?: string,
): Match & AlgoliaMatchNumericFilters => {
if (!h) {
return h;
}
const updated = new Date(`${h.match_modifieddate}Z`);
return {
updated,
created: new Date(`${h.match_creationdate}Z`),
id: Number.parseInt(h.match_id.split("-").reverse()[0], 16),
name: h.match_name,
uuid: h.match_id,
date: h.match_date,
timestamp_utc_updated: updated.getTime(),
type: h.match_type,
subType: h.match_subtype,
templateName: forcedTemplateName || h.templateName,
};
};

const fetchMatchesRangeByTimestamp = async (
latestTimestamp: number,
template = "USPSA",
Expand Down
40 changes: 40 additions & 0 deletions api/src/db/recHHF.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import { HF, Percent, PositiveOrMinus1 } from "../dataUtil/numbers";

import { minorHFScoresAdapter, Scores } from "./scores";

const VERY_LOW_SAMPLE_SIZE_DIVISIONS = new Set([
// pcsl2gun
"practical",
"competition",
]);
const LOW_SAMPLE_SIZE_DIVISIONS = new Set([
"rev",
"scsa_ss",
Expand Down Expand Up @@ -416,6 +421,36 @@ const decidedHHFFunctions = {
"23-01": r1,
"23-02": r15,
},

practical: {
"1.GOLDCOUNTRY": r5, // or 15
"2.BELT": r1,
"3.HARDBASS": r5,
"4.CROSSROADS": r5,
"5.TRAPHOUSE": r5,
"6.LEADPEDAL": r5,
"7.BUYHIGHSELLLOW": r5,
"8.WEARESOBACK": r15,
"9.ITSSOOVER": r15,
"10.AREYAWINNINSON": r5,
"11.MOEZAMBIQUE": r5,
"12.WOLVERINES": r15,
},

competition: {
"1.GOLDCOUNTRY": r5,
"2.BELT": r5,
"3.HARDBASS": r5,
"4.CROSSROADS": r15,
"5.TRAPHOUSE": r5,
"6.LEADPEDAL": r5,
"7.BUYHIGHSELLLOW": r5,
"8.WEARESOBACK": r5,
"9.ITSSOOVER": r5,
"10.AREYAWINNINSON": r5,
"11.MOEZAMBIQUE": r5,
"12.WOLVERINES": r5,
},
};

// First manual recHHF Function review notes:
Expand All @@ -430,6 +465,11 @@ const recommendedHHFFunctionFor = ({ division, number }) => {
return decided;
}

// Not enough data for pcsl nats
if (VERY_LOW_SAMPLE_SIZE_DIVISIONS.has(division)) {
return r5;
}

// Not enough data for revolver in ANY of the classifiers, drop to r5 for defaults
if (LOW_SAMPLE_SIZE_DIVISIONS.has(division)) {
return r5;
Expand Down
8 changes: 1 addition & 7 deletions api/src/db/shooters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
hfuDivisionCompatabilityMap,
hfuDivisionsShortNamesThatNeedMinorHF,
mapDivisions,
uspsaDivShortNames,
} from "../dataUtil/divisions";
import { psClassUpdatesByMemberNumber } from "../dataUtil/uspsa";
import { loadJSON, processImportAsyncSeq } from "../utils";
Expand Down Expand Up @@ -483,18 +482,13 @@ export const reclassifyShooters = async shooters => {
]);

const updates = shooters
.filter(
({ memberNumber, division }) =>
// TODO: Implement Reclassify Shooters for SCSA
// https://github.com/CodeHowlerMonkey/hitfactor.info/issues/69
memberNumber && uspsaDivShortNames.find(x => x === division),
)
.map(({ memberNumber, division, name }) => {
if (!memberNumber) {
return [];
}
const recMemberScores = recScoresByMemberNumber[memberNumber];
const curMemberScores = curScoresByMemberNumber[memberNumber];
console.log(`${memberNumber}: ${recMemberScores.length} rec scores`);
const recalcByCurPercent = calculateUSPSAClassification(
curMemberScores,
"curPercent",
Expand Down
6 changes: 3 additions & 3 deletions api/src/db/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import mongoose from "mongoose";

import { ClassificationLetter } from "../../../data/types/USPSA";
import { divShortNames, mapDivisions } from "../dataUtil/divisions";
import { allDivShortNames, mapAllDivisions, mapDivisions } from "../dataUtil/divisions";

import { Shooters } from "./shooters";

Expand Down Expand Up @@ -56,7 +56,7 @@ const addCurClassField = () => ({
});

export const statsByDivision = async (field: string) => {
const byDiv = mapDivisions(() => ({}));
const byDiv = mapAllDivisions(() => ({}));
const dbResults = await Shooters.aggregate([
addCurClassField(),
{
Expand All @@ -77,7 +77,7 @@ export const statsByDivision = async (field: string) => {
]);

dbResults.forEach(({ _id: [classLetter, division], count }) => {
if (!divShortNames.includes(division)) {
if (!allDivShortNames.includes(division)) {
return;
}

Expand Down
Loading