Skip to content
116 changes: 85 additions & 31 deletions apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
// בס"ד
import type { FuelObject, ShootEvent } from "@repo/scouting_types";
import type { BPS } from "../fuel-object";
import { calculateSum, firstElement, lastElement } from "@repo/array-functions";
import type { BPS, BPSEvent, FuelObject, ShootEvent } from "@repo/scouting_types";
import {
calculateAverage,
calculateSum,
firstElement,
isEmpty,
lastElement,
} from "@repo/array-functions";
import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map";
import { interpolateQuadratic } from "./interpolation";

interface ShotStats {
Comment thread
RJ0907 marked this conversation as resolved.
durationMilliseconds: number;
hubDistanceCentimeters: number;
}

const EMPTY_INTERVAL_DURATION = 0;
const FIRST_INTERVAL_BOUNDARY = 0;
const NO_FUEL_COLLECTED = 0;
const FIRST_SECTION_AMOUNT = 1;
const LAST_SECTION_LENGTH = 1;
const ONE_SECTION_ONLY_LENGTH = 1;

/**
* @param sections consists of sections that contains a list of timestamps in ms
* @returns mean ball amount
*/
const calculateBallAmount = (
sections: number[][],
shotLength: number,
): number => {
const calculateBallAmount = (sections: number[][], shot: ShotStats): number => {
Comment thread
RJ0907 marked this conversation as resolved.
// Base Case 1
if (shotLength <= EMPTY_INTERVAL_DURATION) {
if (shot.durationMilliseconds <= EMPTY_INTERVAL_DURATION) {
return NO_FUEL_COLLECTED;
}
// Base Case 2: Happens if no section is long enough for the shot length
Comment thread
YoniKiriaty marked this conversation as resolved.
if (sections.length === LAST_SECTION_LENGTH) {
if (sections.length === ONE_SECTION_ONLY_LENGTH) {
const onlySection = firstElement(sections);
const ballAmount = onlySection.length;
Comment thread
RJ0907 marked this conversation as resolved.
const sectionDuration = lastElement(onlySection);
Comment thread
RJ0907 marked this conversation as resolved.
return (ballAmount / sectionDuration) * shotLength;
return (ballAmount / sectionDuration) * shot.durationMilliseconds;
}

// finds the average for the first interval, removes it and then recurses
Expand All @@ -36,9 +45,12 @@ const calculateBallAmount = (
const adjustedSections = sections.map((section) =>
section.map((timing) => timing - firstIntervalDuration),
);

const firstIntervalSections = adjustedSections.map((section) =>
section.filter(
(timing) => timing <= FIRST_INTERVAL_BOUNDARY && timing <= shotLength,
(timing) =>
timing <= FIRST_INTERVAL_BOUNDARY &&
timing <= shot.durationMilliseconds,
),
);

Expand All @@ -51,35 +63,76 @@ const calculateBallAmount = (
.map((section) =>
section.filter((timing) => timing > FIRST_INTERVAL_BOUNDARY),
);

return (
avgBallsFirstInterval +
calculateBallAmount(nonFirstSections, shotLength - firstIntervalDuration)
calculateBallAmount(nonFirstSections, {
durationMilliseconds: shot.durationMilliseconds - firstIntervalDuration,
hubDistanceCentimeters: shot.hubDistanceCentimeters,
})
);
};

const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => {
Comment thread
YoniKiriaty marked this conversation as resolved.
const durationedSections = sections.map((section) => ({
shoot: section.shoot.filter((timestamp) => timestamp <= shotDuration),
score: section.score.filter((timestamp) => timestamp <= shotDuration),
positions: section.positions,
}));

const filteredSections = durationedSections.filter(
(section) => !isEmpty(section.shoot),
);

const accuracies = filteredSections.map((section) => ({
distance: calculateAverage(section.positions, (point) =>
distanceFromHub(convertPixelToCentimeters(point)),
),
accuracy: section.score.length / section.shoot.length,
}));
const sortedAccuracies = accuracies.sort(
(accuracy1, accuracy2) => accuracy1.distance - accuracy2.distance,
);

return sortedAccuracies;
};

const compareSections = (a: number[], b: number[]) =>
lastElement(a) - lastElement(b);
const compareSections = (section1: number[], section2: number[]) =>
lastElement(section1) - lastElement(section2);

const correctSectionToTimeFromEnd = (sections: number[]) => {
const endTimestamp = lastElement(sections);
return sections
.filter((timestamp) => timestamp < endTimestamp)
.map((timestamp) => endTimestamp - timestamp);
return sections.map((timestamp) => endTimestamp - timestamp).reverse();
};

const formatSections = (sections: BPS["events"]) =>
sections
.map((section) => ({
...section,
score: correctSectionToTimeFromEnd(section.score),
shoot: correctSectionToTimeFromEnd(section.shoot),
}))
.sort((formattedSection1, formattedSection2) =>
compareSections(formattedSection1.shoot, formattedSection2.shoot),
);

export const calculateFuelByAveraging = (
shot: ShootEvent,
isPass: boolean,
sections: BPS["events"],
sections: BPSEvent[],
): Partial<FuelObject> => {
const shotLength = shot.interval.end - shot.interval.start;
const shotStats: ShotStats = {
durationMilliseconds: shot.interval.end - shot.interval.start,
hubDistanceCentimeters: distanceFromHub(
convertPixelToCentimeters(shot.startPosition),
),
};

const formattedSections = formatSections(sections);

const shotAmount = calculateBallAmount(
sections
.map((section) => section.shoot)
.map(correctSectionToTimeFromEnd)
.sort(compareSections),
shotLength,
formattedSections.map((section) => section.shoot),
shotStats,
);

if (isPass) {
Expand All @@ -89,14 +142,15 @@ export const calculateFuelByAveraging = (
positions: [shot.startPosition],
};
}
const scoredAmount = calculateBallAmount(
sections
.map((section) => section.score)
.map(correctSectionToTimeFromEnd)
.sort(compareSections),
shotLength,
const scoredAccuracy = interpolateQuadratic(
shotStats.hubDistanceCentimeters,
calculateAccuracies(formattedSections, shotStats.durationMilliseconds).map(
({ distance, accuracy }) => ({ x: distance, y: accuracy }),
),
);

const scoredAmount = shotAmount * scoredAccuracy;

return {
scored: scoredAmount,
shot: shotAmount,
Expand Down
4 changes: 2 additions & 2 deletions apps/scouting/backend/src/fuel/calculations/fuel-match.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// בס"ד
import type { FuelObject, ShootEvent } from "@repo/scouting_types";
import type { BPS } from "../fuel-object";
import type { BPS, FuelObject, ShootEvent } from "@repo/scouting_types";

const getIncludedShots = (section: number[], shot: ShootEvent) => {
return section.filter(
Expand Down Expand Up @@ -30,6 +29,7 @@ export const calculateFuelByMatch = (

const scoredAmount = shotBps.flatMap((section) => section.score).length;


return {
shot: shotAmount,
scored: scoredAmount,
Expand Down
35 changes: 35 additions & 0 deletions apps/scouting/backend/src/fuel/calculations/interpolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// בס"ד

import { isEmpty } from "@repo/array-functions";
import type { Point } from "@repo/scouting_types";

const QUADRATIC_EXPONENT = 2;
const interpolateTwoPointQuadratic = (x: number, p1: Point, p2: Point) => {
// Assumes p1 is the "peak" (e.g., distance 0)
const a = (p2.y - p1.y) / p2.x ** QUADRATIC_EXPONENT;
return a * x ** QUADRATIC_EXPONENT + p1.y;
};

const ONE_ITEM = 1;
const DEFAULT_EMPTY_VALUE = 0;
const LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS = 1;
export const interpolateQuadratic = (
x: number,
points: Point[],
emptyValue = DEFAULT_EMPTY_VALUE,
): number => {
if (isEmpty(points)) {
return emptyValue;
}

const [first, second] = points;
Comment thread
YoniKiriaty marked this conversation as resolved.

if (first.x >= x || points.length === LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS) {
return first.y;
}
if (x < second.x) {
return interpolateQuadratic(x, points.slice(ONE_ITEM));
}

return interpolateTwoPointQuadratic(x, first, second);
};
6 changes: 1 addition & 5 deletions apps/scouting/backend/src/fuel/fuel-object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// בס"ד
import type {
BPS,
FuelObject,
Match,
Point,
Expand All @@ -9,11 +10,6 @@ import { calculateFuelByAveraging } from "./calculations/fuel-averaging";
import { calculateFuelByMatch } from "./calculations/fuel-match";
import { ALLIANCE_ZONE_WIDTH_PIXELS } from "@repo/rebuilt_map";

export interface BPS {
events: { shoot: number[]; score: number[] }[];
match: Match;
}

const isShotPass = (positionPixels: Point) =>
positionPixels.x > ALLIANCE_ZONE_WIDTH_PIXELS;

Expand Down
6 changes: 4 additions & 2 deletions apps/scouting/backend/src/routes/teams-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getFormsCollection } from "./forms-router";
import { StatusCodes } from "http-status-codes";
import { castItem } from "@repo/type-utils";
import type {
BPS,
Match,
ScoutingForm,
SectionTeamData,
Expand All @@ -26,7 +27,7 @@ import type {
import { ACCURACY_DISTANCES, teamsProps } from "@repo/scouting_types";
import { groupBy } from "fp-ts/lib/NonEmptyArray";
import { calculateSum, isEmpty, mapObject } from "@repo/array-functions";
import { createFuelObject, type BPS } from "../fuel/fuel-object";
import { createFuelObject } from "../fuel/fuel-object";
import { splitByDistances } from "../fuel/distance-split";
import { calculateFuelStatisticsOfShift } from "../fuel/fuel-general";

Expand Down Expand Up @@ -124,9 +125,10 @@ export const getAllBPS = (): BPS[] => [
score: [1000, 2000, 3000],
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
shoot: [1000, 1400, 2000, 3000],
positions: [{ x: 300, y: 200 }],
},
],
match: { type: "qualification", number: 8 },
match: { type: "qualification", number: 10 },
Comment on lines 125 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is for trial remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its for until the bps gets merged, as this is used in other stuff in the code it shouldnt be removed

},
];

Expand Down
8 changes: 7 additions & 1 deletion packages/scouting_types/rebuilt/fuel/FuelTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ export interface GeneralFuelData {

export type GameTime = keyof GeneralFuelData;

export interface BPSEvent {
shoot: number[];
score: number[];
positions: Point[];
}

export interface BPS {
events: { shoot: number[]; score: number[] }[];
events: BPSEvent[];
match: Match;
}

Expand Down
Loading