From b3e41c629652341411d1f3bf11cd4a281951012a Mon Sep 17 00:00:00 2001 From: Albert Xu Date: Sat, 10 Jun 2023 16:54:38 -0400 Subject: [PATCH 01/38] Please dont go into master please please --- .../frontend/src/app/Formula/differentiate.ts | 78 ++ .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 366 ++++++++++ .../Tabs/TabUpgradeOpt/artifactQuery.ts | 158 ++++ .../TabUpgradeOpt/artifactUpgradeCrawl.ts | 125 ++++ .../Tabs/TabUpgradeOpt/evalArtifact.ts | 302 ++++++++ .../Tabs/TabUpgradeOpt/index.tsx | 691 ++++++++++++++++++ .../Tabs/TabUpgradeOpt/mathUtil.ts | 64 ++ .../Tabs/TabUpgradeOpt/mvncdf.ts | 76 ++ .../Tabs/TabUpgradeOpt/upOpt.ts | 184 +++++ .../PageCharacter/CharacterDisplay/index.tsx | 20 +- 10 files changed, 2059 insertions(+), 5 deletions(-) create mode 100644 apps/frontend/src/app/Formula/differentiate.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactQuery.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactUpgradeCrawl.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/evalArtifact.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts create mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts diff --git a/apps/frontend/src/app/Formula/differentiate.ts b/apps/frontend/src/app/Formula/differentiate.ts new file mode 100644 index 0000000000..3e3ea4191e --- /dev/null +++ b/apps/frontend/src/app/Formula/differentiate.ts @@ -0,0 +1,78 @@ +import { assertUnreachable } from '../Util/Util'; +import { forEachNodes } from './internal'; +import { constant, sum, prod, threshold, frac, max, min } from './utils'; +import type { ReadNode } from './type'; +import type { OptNode } from './optimization'; + +export function zero_deriv(funct: OptNode, binding: (readNode: ReadNode) => string, diff: string): boolean { + let ret = true; + // eslint-disable-next-line @typescript-eslint/no-empty-function + forEachNodes([funct], (_) => { }, (f) => { + const { operation } = f; + switch (operation) { + case 'read': + if (f.type !== 'number' || (f.accu && f.accu !== 'add')) + throw new Error(`Unsupported [${operation}] node in zero_deriv`); + if (binding(f) === diff) ret = false; + } + }); + return ret; +} + +export function ddx(f: OptNode, binding: (readNode: ReadNode) => string, diff: string): OptNode { + const { operation } = f; + switch (operation) { + case 'read': { + if (f.type !== 'number' || (f.accu && f.accu !== 'add')) + throw new Error(`Unsupported [${operation}] node in d/dx`); + const name = binding(f); + if (name === diff) return constant(1); + return constant(0); + } + case 'const': return constant(0); + case 'res': + if (!zero_deriv(f, binding, diff)) + throw new Error(`[${operation}] node takes only constant inputs. ${f}`); + return constant(0); + + case 'add': + return sum(...f.operands.map((fi) => ddx(fi, binding, diff))); + case 'mul': { + const ops = f.operands.map((fi, i) => prod(ddx(fi, binding, diff), ...f.operands.filter((v, ix) => ix !== i))); + return sum(...ops); + } + case 'sum_frac': { + const a = f.operands[0]; + const da = ddx(a, binding, diff); + const b = sum(...f.operands.slice(1)); + const db = ddx(b, binding, diff); + const denom = prod(sum(...f.operands), sum(...f.operands)); + const numerator = sum(prod(b, da), prod(-1, a, db)); + return frac(numerator, sum(prod(-1, numerator), denom)); + } + + case 'min': case 'max': { + if (f.operands.length === 1) return ddx(f.operands[0], binding, diff); + else if (f.operands.length === 2) { + const [arg1, arg2] = f.operands; + if (operation === 'min') return threshold(arg1, arg2, ddx(arg2, binding, diff), ddx(arg1, binding, diff)); + if (operation === 'max') return threshold(arg1, arg2, ddx(arg1, binding, diff), ddx(arg2, binding, diff)); + assertUnreachable(operation); + break; + } else { + if (operation === 'min') return ddx(min(f.operands[0], min(...f.operands.slice(1))), binding, diff); + if (operation === 'max') return ddx(max(f.operands[0], max(...f.operands.slice(1))), binding, diff); + assertUnreachable(operation); + break; + } + } + case 'threshold': { + const [value, thr, pass, fail] = f.operands; + if (!zero_deriv(value, binding, diff) || !zero_deriv(thr, binding, diff)) + throw new Error(`[${operation}] node must branch on constant inputs. ${f}`); + return threshold(value, thr, ddx(pass, binding, diff), ddx(fail, binding, diff)); + } + default: + assertUnreachable(operation); + } +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx new file mode 100644 index 0000000000..2cfa5bc70e --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -0,0 +1,366 @@ +import { Button, CardContent, Grid, Box } from '@mui/material' +import React, { + useEffect, + useState, + useContext, + useMemo, + useCallback, +} from 'react' +import { DatabaseContext } from '../../../../Database/Database' +import { DataContext } from '../../../../Context/DataContext' +import Assets from '../../../../Assets/Assets' +import type { TooltipProps } from 'recharts' +import { + Line, + Area, + ComposedChart, + Legend, + ReferenceLine, + ReferenceDot, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, + Label, +} from 'recharts' +import CardLight from '../../../../Components/Card/CardLight' +import type { QueryResult } from './artifactQuery' +import { allUpgradeValues } from './artifactUpgradeCrawl' +import { uiInput as input } from '../../../../Formula' +import ArtifactCardPico from '../../../../Components/Artifact/ArtifactCardPico' +import type { ArtifactSlotKey } from '@genshin-optimizer/consts' +import { allArtifactSlotKeys } from '@genshin-optimizer/consts' +import type { ICachedArtifact } from '../../../../Types/artifact' +import { gaussPDF } from './mathUtil' + +type Data = { + upgradeOpt: QueryResult + showTrue?: boolean + objMin: number + objMax: number + ix?: number +} +type ChartData = { + x: number + est?: number + estCons?: number + exact?: number + exactCons?: number + expInc?: number +} + +// linspace with non-inclusive endpoint. +function linspace(lower = 0, upper = 1, steps = 50): number[] { + const arr: number[] = [] + const step = (upper - lower) / steps + for (let i = 0; i < steps; ++i) { + arr.push(lower + i * step) + } + return arr +} + +const nbins = 50 +const plotPoints = 500 +export default function UpgradeOptChartCard({ + upgradeOpt, + objMin, + objMax, +}: Data) { + const [calcExacts, setCalcExacts] = useState(false) + + const { database } = useContext(DatabaseContext) + const bla = database.arts.get(upgradeOpt.id) + if (!bla) { + throw new Error(`artifact ${upgradeOpt.id} not found.`) + } + + const constrained = upgradeOpt.thresholds.length > 1 + + const slot = bla.slotKey + const { data } = useContext(DataContext) + const artifacts = useMemo( + () => + allArtifactSlotKeys.map((k) => [ + k, + database.arts.get(data.get(input.art[k].id).value ?? ''), + ]), + [data, database] + ) as Array<[ArtifactSlotKey, ICachedArtifact | undefined]> + + const gauss = (x: number) => + upgradeOpt.distr.gmm.reduce( + (pv, { phi, mu, sig2 }) => pv + phi * gaussPDF(x, mu, sig2), + 0 + ) + const gaussConstrained = (x: number) => + upgradeOpt.distr.gmm.reduce( + (pv, { phi, cp, mu, sig2 }) => pv + cp * phi * gaussPDF(x, mu, sig2), + 0 + ) + const thresh = upgradeOpt.thresholds + const thr0 = thresh[0] + // const perc = (x: number) => 100 * (x - thr0) / thr0; + const perc = useCallback((x: number) => (100 * (x - thr0)) / thr0, [thr0]) + + const miin = objMin + const maax = objMax + + let ymax = 0 + const dataEst: ChartData[] = linspace(miin, maax, plotPoints).map((v) => { + const est = gauss(v) + ymax = Math.max(ymax, est) + return { x: perc(v), est: est, estCons: gaussConstrained(v) } + }) + if (ymax === 0) ymax = nbins / (maax - miin) + + // go back and add delta distributions. + const deltas: { [key: number]: number } = {} + const deltasConstrained: { [key: number]: number } = {} + upgradeOpt.distr.gmm.forEach(({ phi, mu, sig2, cp }) => { + if (sig2 <= 0) { + deltas[mu] = (deltas[mu] ?? 0) + phi + deltasConstrained[mu] = (deltasConstrained[mu] ?? 0) + phi * cp + } + }) + Object.entries(deltas).forEach(([mu, p]) => + dataEst.push({ + x: perc(parseFloat(mu)), + est: (p * nbins) / (maax - miin), + estCons: (deltasConstrained[mu] * nbins) / (maax - miin), + }) + ) + + dataEst.sort((a, b) => a.x - b.x) + const xpercent = (thr0 - miin) / (maax - miin) + + const [trueData, setTrueData] = useState([]) + const [trueP, setTrueP] = useState(-1) + const [trueE, setTrueE] = useState(-1) + + useEffect(() => { + // When `calcExacts` is pressed, we may want to sink/swim this artifact to its proper spot. + // Or not b/c people only really need a fuzzy ordering anyways. + if (!calcExacts) return + const exactData = allUpgradeValues(upgradeOpt) + let true_p = 0 + let true_e = 0 + + const bins = new Array(nbins).fill(0) + const binsConstrained = new Array(nbins).fill(0) + const binstep = (maax - miin) / nbins + + exactData.forEach(({ p, v }) => { + const whichBin = Math.min(Math.trunc((v[0] - miin) / binstep), nbins - 1) + bins[whichBin] += p + + if (v.every((val, ix) => ix === 0 || val > thresh[ix])) { + binsConstrained[whichBin] += p + if (v[0] > thr0) { + true_p += p + true_e += p * (v[0] - thr0) + } + } + }) + if (true_p > 0) true_e = true_e / true_p + + const dataExact: ChartData[] = bins.map((dens, ix) => ({ + x: perc(miin + ix * binstep), + exact: dens / binstep, + exactCons: binsConstrained[ix] / binstep, + })) + setTrueP(true_p) + setTrueE(true_e) + setTrueData(dataExact) + }, [calcExacts, maax, miin, thr0, thresh, upgradeOpt, perc]) + + if (trueData.length === 0) { + const binstep = (maax - miin) / nbins + for (let i = 0; i < nbins; i++) { + trueData.push({ x: perc(miin + i * binstep), exact: 0, exactCons: 0 }) + } + } + + // if trueP/E have been calculated, otherwise use upgradeOpt's estimate + const reportP = trueP >= 0 ? trueP : upgradeOpt.prob + const reportD = trueE >= 0 ? trueE : upgradeOpt.upAvg + const chartData = dataEst.concat(trueData) + + // console.log('repd', reportD, upgradeOpt.upAvg) + + const CustomTooltip = ({ + active, + }: TooltipProps) => { + if (!active) return null + // I kinda want the [average increase] to only appear when hovering the white dot. + return ( +
+

+

+ prob. upgrade{trueP >= 0 ? '' : ' (est.)'}:{' '} + {(100 * reportP).toFixed(1)}% +

+

+ average increase{trueE >= 0 ? '' : ' (est.)'}:{' '} + {reportD <= 0 ? '' : '+'} + {((100 * reportD) / thr0).toFixed(1)}% +

+
+ ) + } + + return ( + + + + + `${v <= 0 ? '' : '+'}${v}%`} + > + + + + + + + + + + + + + + {constrained && !calcExacts && ( + + )} + {constrained && calcExacts && ( + + )} + + {calcExacts && ( + + )} + + + } + /> + + } cursor={false} /> + + + + + {artifacts.map( + ([sk, art]: [ArtifactSlotKey, ICachedArtifact | undefined]) => { + if (sk !== slot) + return ( + + + + ) + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {numPages > 1 && ( + + + + + + + + + + + + + )} + + + + {noArtifact && ( + + Looks like you haven't added any artifacts yet. If you + want, there are{' '} + + automatic scanners + {' '} + that can speed up the import process! + + )} + + } + > + {/* */} + {artifactsToShow.map((art) => ( + + + + + + + + + ))} + {/* */} + + + + + {numPages > 1 && ( + + + + + + + + + + + + + )} + + + + + )} + + ) +} + +function ShowingArt({ numShowing, total }) { + return ( + + + {/* Showing {{ count: numShowing }} out of {{ value: total }} Artifacts */} + Showing {{ count: numShowing }} out of {{ value: total }} Artifacts + + + ) +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts new file mode 100644 index 0000000000..6344c29ea0 --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts @@ -0,0 +1,64 @@ +// https://oeis.org/A008287 +// step 1: a basic LUT with a few steps of Pascal's triangle +const quadrinomials = [ + [1], + [1, 1, 1, 1], + [1, 2, 3, 4, 3, 2, 1], + [1, 3, 6, 10, 12, 12, 10, 6, 3, 1], + [1, 4, 10, 20, 31, 40, 44, 40, 31, 20, 10, 4, 1], + [1, 5, 15, 35, 65, 101, 135, 155, 155, 135, 101, 65, 35, 15, 5, 1], +] + +// step 2: a function that builds out the LUT if it needs to. +export function quadrinomial(n: number, k: number) { + while (n >= quadrinomials.length) { + const s = quadrinomials.length + + const nextRow: number[] = [] + for (let i = 0, prev = s - 1; i <= 3 * s; i++) { + const a = quadrinomials[prev][i - 3] ?? 0 + const b = quadrinomials[prev][i - 2] ?? 0 + const c = quadrinomials[prev][i - 1] ?? 0 + const d = quadrinomials[prev][i] ?? 0 + + nextRow[i] = a + b + c + d + } + quadrinomials.push(nextRow) + } + return quadrinomials[n][k] ?? 0 +} + +// https://hewgill.com/picomath/javascript/erf.js.html +// very good algebraic approximation of erf function. Maximum deviation below 1.5e-7 +export function erf(x: number) { + // constants + const a1 = 0.254829592, + a2 = -0.284496736, + a3 = 1.421413741 + const a4 = -1.453152027, + a5 = 1.061405429, + p = 0.3275911 + + // Save the sign of x + let sign = 1 + if (x < 0) sign = -1 + x = Math.abs(x) + + // A&S formula 7.1.26 + const t = 1.0 / (1.0 + p * x) + const y = + 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x) + + return sign * y +} + +// Gaussian probability distribution. mean & variance can be omitted for standard Gaussian. +export function gaussPDF(x: number, mu?: number, sig2?: number) { + if (mu === undefined) mu = 0 + if (sig2 === undefined) sig2 = 1 + + if (sig2 <= 0) return 0 + return ( + Math.exp((-(mu - x) * (mu - x)) / sig2 / 2) / Math.sqrt(2 * Math.PI * sig2) + ) +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts new file mode 100644 index 0000000000..92ec3e0657 --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts @@ -0,0 +1,76 @@ +import { erf } from './mathUtil' +// import { Module } from "wasmpack/assembly.js"; + +// From a Gaussian mean & variance, get P(x > mu) and E[x | x > mu] +export function gaussianPE( + mean: number, + variance: number, + x: number +): { p: number; upAvg: number } { + if (variance < 1e-5) { + if (mean > x) return { p: 1, upAvg: mean - x } + return { p: 0, upAvg: 0 } + } + + const z = (x - mean) / Math.sqrt(variance) + const p = (1 - erf(z / Math.sqrt(2))) / 2 + if (z > 5) { + // Z-score large means p will be very small. + // We can use taylor expansion at infinity to evaluate upAvg. + const y = 1 / z, + y2 = y * y + return { + p: p, + upAvg: Math.sqrt(variance) * y * (1 - 2 * y2 * (1 - y2 * (5 + 37 * y2))), + } + } + + const phi = Math.exp((-z * z) / 2) / Math.sqrt(2 * Math.PI) + return { p: p, upAvg: mean - x + (Math.sqrt(variance) * phi) / p } +} + +// From a multivariate Gaussian mean & variance, get P(x > mu) and E[x0 | x > mu] +export function mvnPE_bad(mu: number[], cov: number[][], x: number[]) { + // TODO: an implementation without using the independence assumption + let ptot = 1 + let cptot = 1 + for (let i = 0; i < mu.length; ++i) { + if (cov[i][i] < 1e-5) { + if (mu[i] < x[i]) return { p: 0, upAvg: 0, cp: 0 } + continue + } + + const z = (x[i] - mu[i]) / Math.sqrt(cov[i][i]) + const p = (1 - erf(z / Math.sqrt(2))) / 2 + ptot *= p + + if (i !== 0) cptot *= p + } + + // Naive 1st moment of truncated distribution: assume it's relatively stationary w.r.t. the + // constraints. If the constraints greatly affects the moment, then its associated + // conditional probability should also be small. Therefore in conjunction with the summation + // method in `gmmNd()`, the overall approximation should be fairly good, even if the individual + // upAvg terms may be very bad. + // Appears to work well in practice. + // + // More rigorous methods for estimating 1st moment of truncated multivariate distribution exist. + // https://www.cesarerobotti.com/wp-content/uploads/2019/04/JCGS-KR.pdf + const { upAvg } = gaussianPE(mu[0], cov[0][0], x[0]) + return { p: ptot, upAvg: upAvg, cp: cptot } +} + +// export function mvnPE_good(mu: number[], cov: number[][], x: number[]) { +// let mvn: any = new Module.MVNHandle(mu.length); +// try { +// x.forEach(xi => mvn.pushX(xi)); +// mu.forEach(mui => mvn.pushMu(mui)); +// cov.forEach(arr => arr.forEach(c => mvn.pushCov(c))); + +// mvn.compute() +// return { p: mvn.p, upAvg: mvn.Eup, cp: mvn.cp } +// } +// finally { +// mvn.delete(); +// } +// } diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts new file mode 100644 index 0000000000..a81953cef0 --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -0,0 +1,184 @@ +import type { ArtifactSlotKey, RarityKey } from '@genshin-optimizer/consts' +import { + optimize, + precompute, + type OptNode, +} from '../../../../Formula/optimization' + +import type { ICachedArtifact } from '../../../../Types/artifact' +import { allSubstatKeys } from '../../../../Types/artifact' +import { ddx, zero_deriv } from '../../../../Formula/differentiate' +import type { ArtifactBuildData, DynStat } from '../../../../Solver/common' +import Artifact, { maxArtifactLevel } from '../../../../Data/Artifacts/Artifact' +import type { MainStatKey, SubstatKey } from '@genshin-optimizer/dm' + +import { gaussianPE } from './mvncdf' + +enum ResultType { + Fast, + Slow, + Exact, +} +export type UpOptBuild = { + [key in ArtifactSlotKey]: ArtifactBuildData | undefined +} +export type UpOptResult = { + p: number + upAvg: number + distr: GaussianMixture + + evalMode: ResultType +} +export type UpOptArtifact = { + id: string + rollsLeft: number + subs: SubstatKey[] + values: DynStat + slotKey: ArtifactSlotKey + + result?: UpOptResult +} +type GaussianMixture = { + gmm: { + phi: number // Item weight; must sum to 1. + cp: number // Constraint probability + mu: number + sig2: number + }[] + + // Store estimates of left and right bounds of distribution for visualization. + lower: number + upper: number +} + +function scale(key: SubstatKey, rarity: RarityKey = 5) { + return key.endsWith('_') + ? Artifact.substatValue(key, rarity) / 1000 + : Artifact.substatValue(key, rarity) / 10 +} + +function toDecimal(key: SubstatKey | MainStatKey | '', value: number) { + return key.endsWith('_') ? value / 100 : value +} + +export class UpOptCalculator { + baseBuild: UpOptBuild + nodes: OptNode[] + thresholds: number[] + + skippableDerivatives: boolean[] + eval: ( + stats: DynStat, + slot: ArtifactSlotKey + ) => { v: number; grads: number[] }[] + + artifacts: UpOptArtifact[] = [] + + constructor(nodes: OptNode[], thresholds: number[], build: UpOptBuild) { + this.baseBuild = build + this.nodes = nodes + this.thresholds = thresholds + + const toEval: OptNode[] = [] + nodes.forEach((n) => { + toEval.push( + n, + ...allSubstatKeys.map((sub) => ddx(n, (fo) => fo.path[1], sub)) + ) + }) + const evalOpt = optimize(toEval, {}, ({ path: [p] }) => p !== 'dyn') + + const evalFn = precompute(evalOpt, {}, (f) => f.path[1], 5) + thresholds[0] = evalFn(Object.values(build))[0] // dmg threshold is current objective value + + this.skippableDerivatives = allSubstatKeys.map((sub) => + nodes.every((n) => zero_deriv(n, (f) => f.path[1], sub)) + ) + this.eval = (stats: DynStat, slot: ArtifactSlotKey) => { + const b2 = { ...build, [slot]: { id: '', values: stats } } + const out = evalFn(Object.values(b2)) + return nodes.map((_, i) => { + const ix = i * (1 + allSubstatKeys.length) + return { + v: out[ix], + grads: allSubstatKeys.map((sub, si) => out[ix + 1 + si]), + } + }) + } + } + + addArtifact(art: ICachedArtifact) { + const maxLevel = maxArtifactLevel[art.rarity] + const mainStatVal = Artifact.mainStatValue( + art.mainStatKey, + art.rarity, + maxLevel + ) // 5* only + + this.artifacts.push({ + id: art.id, + rollsLeft: 0, + slotKey: art.slotKey, + subs: art.substats + .map(({ key }) => key) + .filter((v) => v !== '') as SubstatKey[], + values: { + [art.setKey]: 1, + [art.mainStatKey]: toDecimal(art.mainStatKey, mainStatVal), + ...Object.fromEntries( + art.substats.map((substat) => [ + substat.key, + toDecimal(substat.key, substat.accurateValue), + ]) + ), // Assumes substats cannot match main stat key + }, + }) + } + + evalFast(ix: number, subKey4th?: SubstatKey) { + const { subs, slotKey, rollsLeft } = this.artifacts[ix] + const stats = { ...this.artifacts[ix].values } + + // Increment stats to evaluate derivatives at "center" of upgrade distribution + subs.forEach((subKey) => { + stats[subKey] = + (stats[subKey] ?? 0) + (17 / 2) * (rollsLeft / 4) * scale(subKey) + }) + if (subKey4th !== undefined) { + stats[subKey4th] = + (stats[subKey4th] ?? 0) + + (17 / 2) * (rollsLeft / 4 + 1) * scale(subKey4th) + } + + // Compute upgrade estimates. For fast case, loop over probability for each stat + // independently, and take the minimum probability. + const N = rollsLeft + const obj = this.eval(stats, slotKey) + for (let ix = 0; ix < obj.length; ix++) { + const { v, grads } = obj[ix] + const gsum = grads.reduce((a, b) => a + b, 0) + const gsum2 = grads.reduce((a, b) => a + b * b, 0) + + const mean = v + const variance = + ((147 / 8) * gsum2 - (289 / 64) * gsum * gsum) * N + + (subKey4th === undefined ? (5 / 4) * grads[3] * grads[3] : 0) + + const { p, upAvg } = gaussianPE(mean, variance, this.thresholds[ix]) + if (ix === 0) { + // Store fast eval result on first iteration. + this.artifacts[ix].result = { + p, + upAvg, + distr: { + gmm: [{ phi: 1, mu: mean, sig2: variance, cp: 1 }], + lower: mean - 4 * Math.sqrt(variance), + upper: mean + 4 * Math.sqrt(variance), + }, + evalMode: ResultType.Fast, + } + } + this.artifacts[ix].result!.p = Math.min(p, this.artifacts[ix].result!.p) + } + } +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx index 41e4e5c8c4..dc9ee44d56 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx @@ -62,6 +62,7 @@ import CharSelectButton from './CharSelectButton' import FormulaModal from './FormulaModal' import StatModal from './StatModal' import TabBuild from './Tabs/TabOptimize' +import TabUpopt from './Tabs/TabUpgradeOpt' import TabOverview from './Tabs/TabOverview' import TabTalent from './Tabs/TabTalent' import TabTeambuffs from './Tabs/TabTeambuffs' @@ -231,6 +232,7 @@ function CharacterPanel() { } /> } /> } /> + } /> ) @@ -250,7 +252,7 @@ function TabNav({ tab }: { tab: string }) { }} > } @@ -258,7 +260,7 @@ function TabNav({ tab }: { tab: string }) { to="" /> } @@ -266,7 +268,7 @@ function TabNav({ tab }: { tab: string }) { to="talent" /> } @@ -274,7 +276,7 @@ function TabNav({ tab }: { tab: string }) { to="teambuffs" /> } @@ -282,13 +284,21 @@ function TabNav({ tab }: { tab: string }) { to="optimize" /> } component={RouterLink} to="theorycraft" /> + } + component={RouterLink} + to="upopt" + /> ) } From 083ca9f16205399b314e6c846c695664d646c6ac Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 10 Jun 2023 20:08:53 -0400 Subject: [PATCH 02/38] fast eval matches --- .../Tabs/TabUpgradeOpt/upOpt.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index a81953cef0..db4b42d401 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -117,7 +117,7 @@ export class UpOptCalculator { this.artifacts.push({ id: art.id, - rollsLeft: 0, + rollsLeft: Artifact.rollsRemaining(art.level, art.rarity), slotKey: art.slotKey, subs: art.substats .map(({ key }) => key) @@ -126,10 +126,12 @@ export class UpOptCalculator { [art.setKey]: 1, [art.mainStatKey]: toDecimal(art.mainStatKey, mainStatVal), ...Object.fromEntries( - art.substats.map((substat) => [ - substat.key, - toDecimal(substat.key, substat.accurateValue), - ]) + art.substats + .filter(({ key }) => key !== '') + .map((substat) => [ + substat.key, + toDecimal(substat.key, substat.accurateValue), + ]) ), // Assumes substats cannot match main stat key }, }) @@ -138,31 +140,40 @@ export class UpOptCalculator { evalFast(ix: number, subKey4th?: SubstatKey) { const { subs, slotKey, rollsLeft } = this.artifacts[ix] const stats = { ...this.artifacts[ix].values } + const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) // Increment stats to evaluate derivatives at "center" of upgrade distribution subs.forEach((subKey) => { stats[subKey] = - (stats[subKey] ?? 0) + (17 / 2) * (rollsLeft / 4) * scale(subKey) + (stats[subKey] ?? 0) + (17 / 2) * (upgradesLeft / 4) * scale(subKey) }) if (subKey4th !== undefined) { stats[subKey4th] = (stats[subKey4th] ?? 0) + - (17 / 2) * (rollsLeft / 4 + 1) * scale(subKey4th) + (17 / 2) * (upgradesLeft / 4 + 1) * scale(subKey4th) } // Compute upgrade estimates. For fast case, loop over probability for each stat // independently, and take the minimum probability. - const N = rollsLeft + const N = upgradesLeft const obj = this.eval(stats, slotKey) for (let ix = 0; ix < obj.length; ix++) { const { v, grads } = obj[ix] - const gsum = grads.reduce((a, b) => a + b, 0) - const gsum2 = grads.reduce((a, b) => a + b * b, 0) + const ks = grads + .map((g, i) => g * scale(allSubstatKeys[i])) + .filter( + (g, i) => + subs.includes(allSubstatKeys[i]) || allSubstatKeys[i] === subKey4th + ) + const ksum = ks.reduce((a, b) => a + b, 0) + const ksum2 = ks.reduce((a, b) => a + b * b, 0) const mean = v - const variance = - ((147 / 8) * gsum2 - (289 / 64) * gsum * gsum) * N + - (subKey4th === undefined ? (5 / 4) * grads[3] * grads[3] : 0) + let variance = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + if (subKey4th) { + const k4th = grads[allSubstatKeys.indexOf(subKey4th)] * scale(subKey4th) + variance += (5 / 4) * k4th * k4th + } const { p, upAvg } = gaussianPE(mean, variance, this.thresholds[ix]) if (ix === 0) { From 682c51bc171e9a0b889f11b9bcf22140a74e0890 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sun, 11 Jun 2023 01:33:43 -0400 Subject: [PATCH 03/38] fast eval w/ 4th sub working! --- .../Tabs/TabUpgradeOpt/upOpt.ts | 155 ++++++++++++------ 1 file changed, 106 insertions(+), 49 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index db4b42d401..0cd0599010 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -22,22 +22,22 @@ enum ResultType { export type UpOptBuild = { [key in ArtifactSlotKey]: ArtifactBuildData | undefined } -export type UpOptResult = { - p: number - upAvg: number - distr: GaussianMixture - - evalMode: ResultType -} export type UpOptArtifact = { id: string rollsLeft: number + mainStat: MainStatKey subs: SubstatKey[] values: DynStat slotKey: ArtifactSlotKey result?: UpOptResult } +export type UpOptResult = { + p: number + upAvg: number + distr: GaussianMixture + evalMode: ResultType +} type GaussianMixture = { gmm: { phi: number // Item weight; must sum to 1. @@ -51,17 +51,37 @@ type GaussianMixture = { upper: number } +/* substat roll weights */ +const fWeight: StrictDict = { + hp: 6, + atk: 6, + def: 6, + hp_: 4, + atk_: 4, + def_: 4, + eleMas: 4, + enerRech_: 4, + critRate_: 3, + critDMG_: 3, +} + +/* Gets "0.1x" 1 roll value for a stat w/ the given rarity. */ function scale(key: SubstatKey, rarity: RarityKey = 5) { - return key.endsWith('_') - ? Artifact.substatValue(key, rarity) / 1000 - : Artifact.substatValue(key, rarity) / 10 + return toDecimal(key, Artifact.substatValue(key, rarity)) / 10 } +/* Fixes silliness with percents and being multiplied by 100. */ function toDecimal(key: SubstatKey | MainStatKey | '', value: number) { return key.endsWith('_') ? value / 100 : value } export class UpOptCalculator { + /** + * Calculator class to track artifacts and their evaluation status. Method overview: + * constructor(OptNode[], number[], Build): Upgrade problem setup + * addArtifact(ICachedArtifact): Add an artifact to be considered + * evalFast/Slow/Exact(number, bool): Evaluate an artifact with X method and keep track of results + */ baseBuild: UpOptBuild nodes: OptNode[] thresholds: number[] @@ -119,6 +139,7 @@ export class UpOptCalculator { id: art.id, rollsLeft: Artifact.rollsRemaining(art.level, art.rarity), slotKey: art.slotKey, + mainStat: art.mainStatKey, subs: art.substats .map(({ key }) => key) .filter((v) => v !== '') as SubstatKey[], @@ -137,48 +158,60 @@ export class UpOptCalculator { }) } - evalFast(ix: number, subKey4th?: SubstatKey) { - const { subs, slotKey, rollsLeft } = this.artifacts[ix] - const stats = { ...this.artifacts[ix].values } - const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) + evalFast(ix: number, calc4th = false) { + const { mainStat, subs, slotKey, rollsLeft } = this.artifacts[ix] - // Increment stats to evaluate derivatives at "center" of upgrade distribution - subs.forEach((subKey) => { - stats[subKey] = - (stats[subKey] ?? 0) + (17 / 2) * (upgradesLeft / 4) * scale(subKey) - }) - if (subKey4th !== undefined) { - stats[subKey4th] = - (stats[subKey4th] ?? 0) + - (17 / 2) * (upgradesLeft / 4 + 1) * scale(subKey4th) + const sub4thOptions: { prob: number; sub?: SubstatKey }[] = [] + if (!calc4th) { + sub4thOptions.push({ prob: 1 }) + } else { + const subsToConsider = allSubstatKeys.filter( + (s) => !subs.includes(s) && s !== mainStat + ) + + const Z = subsToConsider.reduce((tot, sub) => tot + fWeight[sub], 0) + subsToConsider.forEach((sub) => + sub4thOptions.push({ prob: fWeight[sub] / Z, sub }) + ) } - // Compute upgrade estimates. For fast case, loop over probability for each stat - // independently, and take the minimum probability. - const N = upgradesLeft - const obj = this.eval(stats, slotKey) - for (let ix = 0; ix < obj.length; ix++) { - const { v, grads } = obj[ix] - const ks = grads - .map((g, i) => g * scale(allSubstatKeys[i])) - .filter( - (g, i) => - subs.includes(allSubstatKeys[i]) || allSubstatKeys[i] === subKey4th - ) - const ksum = ks.reduce((a, b) => a + b, 0) - const ksum2 = ks.reduce((a, b) => a + b * b, 0) - - const mean = v - let variance = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N - if (subKey4th) { - const k4th = grads[allSubstatKeys.indexOf(subKey4th)] * scale(subKey4th) - variance += (5 / 4) * k4th * k4th + const evalResults = sub4thOptions.map(({ prob, sub: subKey4 }) => { + const stats = { ...this.artifacts[ix].values } + const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) + + // Increment stats to evaluate derivatives at "center" of upgrade distribution + subs.forEach((subKey) => { + stats[subKey] = + (stats[subKey] ?? 0) + (17 / 2) * (upgradesLeft / 4) * scale(subKey) + }) + if (subKey4 !== undefined) { + stats[subKey4] = + (stats[subKey4] ?? 0) + + (17 / 2) * (upgradesLeft / 4 + 1) * scale(subKey4) } - const { p, upAvg } = gaussianPE(mean, variance, this.thresholds[ix]) - if (ix === 0) { - // Store fast eval result on first iteration. - this.artifacts[ix].result = { + // Compute upgrade estimates. For fast case, loop over probability for each stat + // independently, and take the minimum probability. + const N = upgradesLeft + const obj = this.eval(stats, slotKey) + const results: UpOptResult[] = obj.map(({ v: mean, grads }, fi) => { + const ks = grads + .map((g, i) => g * scale(allSubstatKeys[i])) + .filter( + (g, i) => + subs.includes(allSubstatKeys[i]) || allSubstatKeys[i] === subKey4 + ) + const ksum = ks.reduce((a, b) => a + b, 0) + const ksum2 = ks.reduce((a, b) => a + b * b, 0) + + let variance = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + if (subKey4) { + const k4 = grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4) + variance += (5 / 4) * k4 ** 2 + } + + const { p, upAvg } = gaussianPE(mean, variance, this.thresholds[fi]) + return { p, upAvg, distr: { @@ -188,8 +221,32 @@ export class UpOptCalculator { }, evalMode: ResultType.Fast, } - } - this.artifacts[ix].result!.p = Math.min(p, this.artifacts[ix].result!.p) + }) + + results[0].p = Math.min(...results.map(({ p }) => p)) + return { prob, result: results[0] } + }) + + const ptot = evalResults.reduce( + (ptot, { prob, result: { p } }) => ptot + prob * p, + 0 + ) + const aggResult: UpOptResult = { + p: ptot, + upAvg: 0, + distr: { gmm: [], lower: Infinity, upper: -Infinity }, + evalMode: ResultType.Fast, } + + evalResults.forEach(({ prob, result: { p, upAvg, distr } }) => { + aggResult.upAvg += ptot < 1e-6 ? 0 : (prob * p * upAvg) / ptot + aggResult.distr.gmm.push( + ...distr.gmm.map((g) => ({ ...g, phi: prob * g.phi })) // Scale each component by `prob` + ) + aggResult.distr.lower = Math.min(aggResult.distr.lower, distr.lower) + aggResult.distr.upper = Math.max(aggResult.distr.upper, distr.upper) + }) + + this.artifacts[ix].result = aggResult } } From e27ebde5d86dcb93ee99723cc37488042e391660 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sun, 11 Jun 2023 01:58:28 -0400 Subject: [PATCH 04/38] golf? --- .../Tabs/TabUpgradeOpt/upOpt.ts | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index 0cd0599010..f37bd1beb4 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -75,6 +75,27 @@ function toDecimal(key: SubstatKey | MainStatKey | '', value: number) { return key.endsWith('_') ? value / 100 : value } +/* Aggregates `results`, each weighted by `prob`. Assumes `prob` sums to 1. */ +function aggregateResults(results: { prob: number; result: UpOptResult }[]) { + const ptot = results.reduce((a, { prob, result: { p } }) => a + prob * p, 0) + const aggResult: UpOptResult = { + p: ptot, + upAvg: 0, + distr: { gmm: [], lower: Infinity, upper: -Infinity }, + evalMode: results[0].result.evalMode, + } + + results.forEach(({ prob, result: { p, upAvg, distr } }) => { + aggResult.upAvg += ptot < 1e-6 ? 0 : (prob * p * upAvg) / ptot + aggResult.distr.gmm.push( + ...distr.gmm.map((g) => ({ ...g, phi: prob * g.phi })) // Scale each component by `prob` + ) + aggResult.distr.lower = Math.min(aggResult.distr.lower, distr.lower) + aggResult.distr.upper = Math.max(aggResult.distr.upper, distr.upper) + }) + return aggResult +} + export class UpOptCalculator { /** * Calculator class to track artifacts and their evaluation status. Method overview: @@ -94,6 +115,12 @@ export class UpOptCalculator { artifacts: UpOptArtifact[] = [] + /** + * Constructs UpOptCalculator. + * @param nodes Formulas to find upgrades for. nodes[0] is main objective, the rest are constraints. + * @param thresholds Constraint values. thresholds[0] will be auto-populated with current objective value. + * @param build Build to check 1-swaps against. + */ constructor(nodes: OptNode[], thresholds: number[], build: UpOptBuild) { this.baseBuild = build this.nodes = nodes @@ -127,6 +154,7 @@ export class UpOptCalculator { } } + /** Adds an artifact to be tracked by UpOptCalc. It is initially un-evaluated. */ addArtifact(art: ICachedArtifact) { const maxLevel = maxArtifactLevel[art.rarity] const mainStatVal = Artifact.mainStatValue( @@ -158,8 +186,10 @@ export class UpOptCalculator { }) } - evalFast(ix: number, calc4th = false) { + _calcFast(ix: number, calc4th = false) { const { mainStat, subs, slotKey, rollsLeft } = this.artifacts[ix] + if (subs.length === 4) calc4th = false + const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) const sub4thOptions: { prob: number; sub?: SubstatKey }[] = [] if (!calc4th) { @@ -177,7 +207,6 @@ export class UpOptCalculator { const evalResults = sub4thOptions.map(({ prob, sub: subKey4 }) => { const stats = { ...this.artifacts[ix].values } - const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) // Increment stats to evaluate derivatives at "center" of upgrade distribution subs.forEach((subKey) => { @@ -193,8 +222,8 @@ export class UpOptCalculator { // Compute upgrade estimates. For fast case, loop over probability for each stat // independently, and take the minimum probability. const N = upgradesLeft - const obj = this.eval(stats, slotKey) - const results: UpOptResult[] = obj.map(({ v: mean, grads }, fi) => { + const objective = this.eval(stats, slotKey) + const results: UpOptResult[] = objective.map(({ v: mean, grads }, fi) => { const ks = grads .map((g, i) => g * scale(allSubstatKeys[i])) .filter( @@ -210,10 +239,8 @@ export class UpOptCalculator { variance += (5 / 4) * k4 ** 2 } - const { p, upAvg } = gaussianPE(mean, variance, this.thresholds[fi]) return { - p, - upAvg, + ...gaussianPE(mean, variance, this.thresholds[fi]), distr: { gmm: [{ phi: 1, mu: mean, sig2: variance, cp: 1 }], lower: mean - 4 * Math.sqrt(variance), @@ -226,27 +253,6 @@ export class UpOptCalculator { results[0].p = Math.min(...results.map(({ p }) => p)) return { prob, result: results[0] } }) - - const ptot = evalResults.reduce( - (ptot, { prob, result: { p } }) => ptot + prob * p, - 0 - ) - const aggResult: UpOptResult = { - p: ptot, - upAvg: 0, - distr: { gmm: [], lower: Infinity, upper: -Infinity }, - evalMode: ResultType.Fast, - } - - evalResults.forEach(({ prob, result: { p, upAvg, distr } }) => { - aggResult.upAvg += ptot < 1e-6 ? 0 : (prob * p * upAvg) / ptot - aggResult.distr.gmm.push( - ...distr.gmm.map((g) => ({ ...g, phi: prob * g.phi })) // Scale each component by `prob` - ) - aggResult.distr.lower = Math.min(aggResult.distr.lower, distr.lower) - aggResult.distr.upper = Math.max(aggResult.distr.upper, distr.upper) - }) - - this.artifacts[ix].result = aggResult + this.artifacts[ix].result = aggregateResults(evalResults) } } From 22ade5e4c5f20145253a848a75ed699fbada227a Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sun, 11 Jun 2023 16:27:18 -0400 Subject: [PATCH 05/38] :) --- .../Tabs/TabUpgradeOpt/upOpt.ts | 143 +++++++++++------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index f37bd1beb4..fc28bfc35e 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -186,73 +186,108 @@ export class UpOptCalculator { }) } - _calcFast(ix: number, calc4th = false) { + /* Fast distribution to result. */ + toResult1( + distr: { prob: number; mu: number[]; cov: number[] }[] + ): UpOptResult { + let ptot = 0 + let upAvgtot = 0 + const gmm = distr.map(({ prob, mu, cov }) => { + const z = mu.map((mui, i) => { + return gaussianPE(mui, cov[i], this.thresholds[i]) + }) + const p = Math.min(...z.map(({ p }) => p)) + const cp = Math.min(...z.slice(1).map(({ p }) => p)) + ptot += p * prob + upAvgtot += p * prob * z[0].upAvg + return { phi: prob, cp, mu: mu[0], sig2: cov[0] } + }) + const lowers = gmm.map(({ mu, sig2 }) => mu - 4 * Math.sqrt(sig2)) + const uppers = gmm.map(({ mu, sig2 }) => mu + 4 * Math.sqrt(sig2)) + return { + p: ptot, + upAvg: ptot < 1e-6 ? 0 : upAvgtot / ptot, + distr: { gmm, lower: Math.min(...lowers), upper: Math.max(...uppers) }, + evalMode: ResultType.Fast, + } + } + + calcFast(ix: number, calc4th = true) { + if (this.artifacts[ix].subs.length === 4) calc4th = false + if (calc4th) this._calcFast4th(ix) + else this._calcFast(ix) + } + + _calcFast(ix: number) { + const { subs, slotKey, rollsLeft } = this.artifacts[ix] + const N = rollsLeft - (subs.length < 4 ? 1 : 0) // only for 5* + + const stats = { ...this.artifacts[ix].values } + subs.forEach((subKey) => { + stats[subKey] += (17 / 2) * (N / 4) * scale(subKey) + }) + const objective = this.eval(stats, slotKey) + const results = objective.map(({ v: mu, grads }) => { + const ks = grads + .map((g, i) => g * scale(allSubstatKeys[i])) + .filter((g, i) => subs.includes(allSubstatKeys[i])) + const ksum = ks.reduce((a, b) => a + b, 0) + const ksum2 = ks.reduce((a, b) => a + b * b, 0) + const sig2 = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + + return { mu, sig2 } + }) + + this.artifacts[ix].result = this.toResult1([ + { + prob: 1, + mu: results.map(({ mu }) => mu), + cov: results.map(({ sig2 }) => sig2), + }, + ]) + } + + _calcFast4th(ix: number) { const { mainStat, subs, slotKey, rollsLeft } = this.artifacts[ix] - if (subs.length === 4) calc4th = false - const upgradesLeft = rollsLeft - (subs.length < 4 ? 1 : 0) - - const sub4thOptions: { prob: number; sub?: SubstatKey }[] = [] - if (!calc4th) { - sub4thOptions.push({ prob: 1 }) - } else { - const subsToConsider = allSubstatKeys.filter( - (s) => !subs.includes(s) && s !== mainStat - ) + const N = rollsLeft - 1 // Minus 1 because 4th slot takes 1. - const Z = subsToConsider.reduce((tot, sub) => tot + fWeight[sub], 0) - subsToConsider.forEach((sub) => - sub4thOptions.push({ prob: fWeight[sub] / Z, sub }) - ) - } + const subsToConsider = allSubstatKeys.filter( + (s) => !subs.includes(s) && s !== mainStat + ) - const evalResults = sub4thOptions.map(({ prob, sub: subKey4 }) => { + const Z = subsToConsider.reduce((tot, sub) => tot + fWeight[sub], 0) + const distr = subsToConsider.map((subKey4) => { + const prob = fWeight[subKey4] / Z const stats = { ...this.artifacts[ix].values } - - // Increment stats to evaluate derivatives at "center" of upgrade distribution subs.forEach((subKey) => { - stats[subKey] = - (stats[subKey] ?? 0) + (17 / 2) * (upgradesLeft / 4) * scale(subKey) + stats[subKey] += (17 / 2) * (N / 4) * scale(subKey) }) - if (subKey4 !== undefined) { - stats[subKey4] = - (stats[subKey4] ?? 0) + - (17 / 2) * (upgradesLeft / 4 + 1) * scale(subKey4) - } + stats[subKey4] = + (stats[subKey4] ?? 0) + (17 / 2) * (N / 4 + 1) * scale(subKey4) - // Compute upgrade estimates. For fast case, loop over probability for each stat - // independently, and take the minimum probability. - const N = upgradesLeft const objective = this.eval(stats, slotKey) - const results: UpOptResult[] = objective.map(({ v: mean, grads }, fi) => { + const results = objective.map(({ v: mu, grads }) => { const ks = grads .map((g, i) => g * scale(allSubstatKeys[i])) - .filter( - (g, i) => - subs.includes(allSubstatKeys[i]) || allSubstatKeys[i] === subKey4 - ) - const ksum = ks.reduce((a, b) => a + b, 0) - const ksum2 = ks.reduce((a, b) => a + b * b, 0) - - let variance = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N - if (subKey4) { - const k4 = grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4) - variance += (5 / 4) * k4 ** 2 - } + .filter((g, i) => subs.includes(allSubstatKeys[i])) + const k4 = grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4) + const ksum = ks.reduce((a, b) => a + b, k4) + const ksum2 = ks.reduce((a, b) => a + b * b, k4 * k4) + const sig2 = + ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + (5 / 4) * k4 * k4 - return { - ...gaussianPE(mean, variance, this.thresholds[fi]), - distr: { - gmm: [{ phi: 1, mu: mean, sig2: variance, cp: 1 }], - lower: mean - 4 * Math.sqrt(variance), - upper: mean + 4 * Math.sqrt(variance), - }, - evalMode: ResultType.Fast, - } + return { mu, sig2 } }) - results[0].p = Math.min(...results.map(({ p }) => p)) - return { prob, result: results[0] } + return { + prob, + mu: results.map(({ mu }) => mu), + cov: results.map(({ sig2 }) => sig2), + } }) - this.artifacts[ix].result = aggregateResults(evalResults) + + this.artifacts[ix].result = this.toResult1(distr) } + + } From ec945c542b1fb1f4bfd8dac8c93eb6b130e54e13 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sun, 11 Jun 2023 23:16:04 -0400 Subject: [PATCH 06/38] slowEval working :) --- .../Tabs/TabUpgradeOpt/upOpt.ts | 146 ++++++++++++++---- 1 file changed, 112 insertions(+), 34 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index fc28bfc35e..bcd22e2b35 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -12,7 +12,8 @@ import type { ArtifactBuildData, DynStat } from '../../../../Solver/common' import Artifact, { maxArtifactLevel } from '../../../../Data/Artifacts/Artifact' import type { MainStatKey, SubstatKey } from '@genshin-optimizer/dm' -import { gaussianPE } from './mvncdf' +import { gaussianPE, mvnPE_bad } from './mvncdf' +import { crawlUpgrades } from './artifactUpgradeCrawl' enum ResultType { Fast, @@ -75,27 +76,6 @@ function toDecimal(key: SubstatKey | MainStatKey | '', value: number) { return key.endsWith('_') ? value / 100 : value } -/* Aggregates `results`, each weighted by `prob`. Assumes `prob` sums to 1. */ -function aggregateResults(results: { prob: number; result: UpOptResult }[]) { - const ptot = results.reduce((a, { prob, result: { p } }) => a + prob * p, 0) - const aggResult: UpOptResult = { - p: ptot, - upAvg: 0, - distr: { gmm: [], lower: Infinity, upper: -Infinity }, - evalMode: results[0].result.evalMode, - } - - results.forEach(({ prob, result: { p, upAvg, distr } }) => { - aggResult.upAvg += ptot < 1e-6 ? 0 : (prob * p * upAvg) / ptot - aggResult.distr.gmm.push( - ...distr.gmm.map((g) => ({ ...g, phi: prob * g.phi })) // Scale each component by `prob` - ) - aggResult.distr.lower = Math.min(aggResult.distr.lower, distr.lower) - aggResult.distr.upper = Math.max(aggResult.distr.upper, distr.upper) - }) - return aggResult -} - export class UpOptCalculator { /** * Calculator class to track artifacts and their evaluation status. Method overview: @@ -227,10 +207,10 @@ export class UpOptCalculator { stats[subKey] += (17 / 2) * (N / 4) * scale(subKey) }) const objective = this.eval(stats, slotKey) - const results = objective.map(({ v: mu, grads }) => { - const ks = grads - .map((g, i) => g * scale(allSubstatKeys[i])) - .filter((g, i) => subs.includes(allSubstatKeys[i])) + const gaussians = objective.map(({ v: mu, grads }) => { + const ks = subs.map( + (sub) => grads[allSubstatKeys.indexOf(sub)] * scale(sub) + ) const ksum = ks.reduce((a, b) => a + b, 0) const ksum2 = ks.reduce((a, b) => a + b * b, 0) const sig2 = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N @@ -241,8 +221,8 @@ export class UpOptCalculator { this.artifacts[ix].result = this.toResult1([ { prob: 1, - mu: results.map(({ mu }) => mu), - cov: results.map(({ sig2 }) => sig2), + mu: gaussians.map(({ mu }) => mu), + cov: gaussians.map(({ sig2 }) => sig2), }, ]) } @@ -266,10 +246,10 @@ export class UpOptCalculator { (stats[subKey4] ?? 0) + (17 / 2) * (N / 4 + 1) * scale(subKey4) const objective = this.eval(stats, slotKey) - const results = objective.map(({ v: mu, grads }) => { - const ks = grads - .map((g, i) => g * scale(allSubstatKeys[i])) - .filter((g, i) => subs.includes(allSubstatKeys[i])) + const gaussians = objective.map(({ v: mu, grads }) => { + const ks = subs.map( + (sub) => grads[allSubstatKeys.indexOf(sub)] * scale(sub) + ) const k4 = grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4) const ksum = ks.reduce((a, b) => a + b, k4) const ksum2 = ks.reduce((a, b) => a + b * b, k4 * k4) @@ -281,13 +261,111 @@ export class UpOptCalculator { return { prob, - mu: results.map(({ mu }) => mu), - cov: results.map(({ sig2 }) => sig2), + mu: gaussians.map(({ mu }) => mu), + cov: gaussians.map(({ sig2 }) => sig2), } }) this.artifacts[ix].result = this.toResult1(distr) } + toResult2( + distr: { prob: number; mu: number[]; cov: number[][] }[] + ): UpOptResult { + let ptot = 0 + let upAvgtot = 0 + const gmm = distr.map(({ prob, mu, cov }) => { + const { p, upAvg, cp } = mvnPE_bad(mu, cov, this.thresholds) + ptot += prob * p + upAvgtot += prob * p * upAvg + return { phi: prob, cp, mu: mu[0], sig2: cov[0][0] } + }) + const lowers = gmm.map(({ mu, sig2 }) => mu - 4 * Math.sqrt(sig2)) + const uppers = gmm.map(({ mu, sig2 }) => mu + 4 * Math.sqrt(sig2)) + return { + p: ptot, + upAvg: ptot < 1e-6 ? 0 : upAvgtot / ptot, + distr: { + gmm, + lower: Math.min(...lowers, this.thresholds[0]), + upper: Math.max(...uppers, this.thresholds[0]), + }, + evalMode: ResultType.Slow, + } + } + + calcSlow(ix: number, calc4th = true) { + if (this.artifacts[ix].subs.length === 4) calc4th = false + if (calc4th) this._calcSlow4th(ix) + else this._calcSlow(ix) + } + + _calcSlow(ix: number) { + const { subs, slotKey, rollsLeft } = this.artifacts[ix] + const N = rollsLeft - (subs.length < 4 ? 1 : 0) // only for 5* + + const distrs: { prob: number; mu: number[]; cov: number[][] }[] = [] + crawlUpgrades(N, (ns, prob) => { + const stats = { ...this.artifacts[ix].values } + subs.forEach((subKey, i) => { + stats[subKey] += (17 / 2) * ns[i] * scale(subKey) + }) + + const objective = this.eval(stats, slotKey) + const obj_ks = objective.map(({ grads }) => + subs.map((sub) => grads[allSubstatKeys.indexOf(sub)] * scale(sub)) + ) + const mu = objective.map((o) => o.v) + const cov = obj_ks.map((k1) => + obj_ks.map((k2) => + k1.reduce((pv, cv, j) => pv + k1[j] * k2[j] * ns[j], 0) + ) + ) + distrs.push({ prob, mu, cov }) + }) + + this.artifacts[ix].result = this.toResult2(distrs) + } + _calcSlow4th(ix: number) { + const { mainStat, subs, slotKey, rollsLeft } = this.artifacts[ix] + const N = rollsLeft - 1 // only for 5* + + const subsToConsider = allSubstatKeys.filter( + (s) => !subs.includes(s) && s !== mainStat + ) + + const Z = subsToConsider.reduce((tot, sub) => tot + fWeight[sub], 0) + const distrs: { prob: number; mu: number[]; cov: number[][] }[] = [] + subsToConsider.forEach((subKey4) => { + crawlUpgrades(N, (ns, prob) => { + prob = prob * (fWeight[subKey4] / Z) + const stats = { ...this.artifacts[ix].values } + ns[3] += 1 // last substat has initial roll + subs.forEach((subKey, i) => { + stats[subKey] += (17 / 2) * ns[i] * scale(subKey) + }) + stats[subKey4] = + (stats[subKey4] ?? 0) + (17 / 2) * ns[3] * scale(subKey4) + + const objective = this.eval(stats, slotKey) + const obj_ks = objective.map(({ grads }) => { + const ks = subs.map( + (sub) => grads[allSubstatKeys.indexOf(sub)] * scale(sub) + ) + ks.push(grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4)) + return ks + }) + const mu = objective.map((o) => o.v) + const cov = obj_ks.map((k1) => + obj_ks.map((k2) => + k1.reduce((pv, cv, j) => pv + k1[j] * k2[j] * ns[j], 0) + ) + ) + distrs.push({ prob, mu, cov }) + }) + }) + + this.artifacts[ix].result = this.toResult2(distrs) + } } From db45f4c747d96b70e96ab6aec36f2999d34ce585 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Wed, 14 Jun 2023 03:04:38 -0400 Subject: [PATCH 07/38] big ass comment --- .../Tabs/TabUpgradeOpt/upOpt.ts | 81 ++++++++++++++++--- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index bcd22e2b35..e1fc591cff 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -15,6 +15,60 @@ import type { MainStatKey, SubstatKey } from '@genshin-optimizer/dm' import { gaussianPE, mvnPE_bad } from './mvncdf' import { crawlUpgrades } from './artifactUpgradeCrawl' +/** + * Artifact upgrade distribution math summary. + * + * Let A be the upgrade distribution of some artifact. We write + * art ~ A to say `art` is a random artifact drawn from the + * upgrade distribution `A`. + * With f(art) being damage function of the 1-swap of an artifact, + * we write dmg ~ f(A) to say `dmg` is a random number sampled by + * the process: art ~ A ; dmg = f(art) + * + * To approximate f(A), we take the linear approximation of upgrade + * distribution of an artifact. + * L(art) = f(art0) + ∂f/∂x_i * s_i * RV_i + * L(art) - Linear approximation of damage function + * ∂f/∂x_i - derivative of f wrt substat i (at art0) + * s_i - "scale" of substat i + * RV_i - roll value of substat i + * Let k_i = ∂f/∂x_i * s_i below. + * + * The roll value is the sum of n_i uniform distributions {7, 8, 9, 10}. + * We approximate it with a Normal distribution: + * RV_i = N[ 8.5n_i, 1.25n_i ] + * n_i - number of upgrades for substat i + * Magic numbers µ = 17/2 and ν = 5/4 come from the mean & std.dev of RV. + * + * The substat upgrades are chosen uniform randomly per roll, so + * the n_i follow a multinomial distribution. So the linearized + * dmg distribution `L(A)` can be written: + * L(A) = sum σ(n1, n2, n3, n4) * (f(art0) + sum_i k_i * RV_i(n_i)) + * σ(n1, n2, n3, n4) - Multinomial distribution for upgrade numbers + * with total n1 + n2 + n3 + n4 = N + * where the first sum is over the (n1, n2, n3, n4) of the multinomial + * distribution. + * + * =========== SLOW APPROXIMATION ========== + * Because L(A) is written as the sum of Normal distributions, we + * can write it as a Gaussian Mixture. We can perform probability + * queries following the ordinary Gaussian Mixture methods. + * Note that in the code, `art0` is chosen as a function of n_i. + * + * =========== FAST APPROXIMATION ========== + * Tthe Fast method just matches the 1st and 2nd moment of the Mixture + * distribution. This is not expected to be a good approximation; it's + * only useful for a ball-park estimate of the upgrade probability. + * Note that in here, `art0` is fixed wrt n_i. + * mean of L(A) = f(art0) + sum_i mean(n_i) * µ k_i + * variance of L(A) = sum_i (mean(n_i)/4 * ν k_i^2 + N/4 (µ k_i)^2) + * - N * (sum_i µ/4 k_i)^2 + */ +const up_rv_mean = 17 / 2 +const up_rv_stdev = 5 / 4 +const Q = (up_rv_mean * up_rv_mean + up_rv_stdev) / 4 +const W = (up_rv_mean / 4) ** 2 + enum ResultType { Fast, Slow, @@ -99,9 +153,15 @@ export class UpOptCalculator { * Constructs UpOptCalculator. * @param nodes Formulas to find upgrades for. nodes[0] is main objective, the rest are constraints. * @param thresholds Constraint values. thresholds[0] will be auto-populated with current objective value. + * @param baseStats Character base stats * @param build Build to check 1-swaps against. */ - constructor(nodes: OptNode[], thresholds: number[], build: UpOptBuild) { + constructor( + nodes: OptNode[], + thresholds: number[], + baseStats: DynStat, + build: UpOptBuild + ) { this.baseBuild = build this.nodes = nodes this.thresholds = thresholds @@ -115,7 +175,7 @@ export class UpOptCalculator { }) const evalOpt = optimize(toEval, {}, ({ path: [p] }) => p !== 'dyn') - const evalFn = precompute(evalOpt, {}, (f) => f.path[1], 5) + const evalFn = precompute(evalOpt, baseStats, (f) => f.path[1], 5) thresholds[0] = evalFn(Object.values(build))[0] // dmg threshold is current objective value this.skippableDerivatives = allSubstatKeys.map((sub) => @@ -204,7 +264,7 @@ export class UpOptCalculator { const stats = { ...this.artifacts[ix].values } subs.forEach((subKey) => { - stats[subKey] += (17 / 2) * (N / 4) * scale(subKey) + stats[subKey] += up_rv_mean * (N / 4) * scale(subKey) }) const objective = this.eval(stats, slotKey) const gaussians = objective.map(({ v: mu, grads }) => { @@ -213,7 +273,7 @@ export class UpOptCalculator { ) const ksum = ks.reduce((a, b) => a + b, 0) const ksum2 = ks.reduce((a, b) => a + b * b, 0) - const sig2 = ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + const sig2 = (Q * ksum2 - W * ksum ** 2) * N return { mu, sig2 } }) @@ -240,10 +300,10 @@ export class UpOptCalculator { const prob = fWeight[subKey4] / Z const stats = { ...this.artifacts[ix].values } subs.forEach((subKey) => { - stats[subKey] += (17 / 2) * (N / 4) * scale(subKey) + stats[subKey] += up_rv_mean * (N / 4) * scale(subKey) }) stats[subKey4] = - (stats[subKey4] ?? 0) + (17 / 2) * (N / 4 + 1) * scale(subKey4) + (stats[subKey4] ?? 0) + up_rv_mean * (N / 4 + 1) * scale(subKey4) const objective = this.eval(stats, slotKey) const gaussians = objective.map(({ v: mu, grads }) => { @@ -253,8 +313,7 @@ export class UpOptCalculator { const k4 = grads[allSubstatKeys.indexOf(subKey4)] * scale(subKey4) const ksum = ks.reduce((a, b) => a + b, k4) const ksum2 = ks.reduce((a, b) => a + b * b, k4 * k4) - const sig2 = - ((147 / 8) * ksum2 - (289 / 64) * ksum ** 2) * N + (5 / 4) * k4 * k4 + const sig2 = (Q * ksum2 - W * ksum ** 2) * N + up_rv_stdev * k4 * k4 return { mu, sig2 } }) @@ -308,7 +367,7 @@ export class UpOptCalculator { crawlUpgrades(N, (ns, prob) => { const stats = { ...this.artifacts[ix].values } subs.forEach((subKey, i) => { - stats[subKey] += (17 / 2) * ns[i] * scale(subKey) + stats[subKey] += up_rv_mean * ns[i] * scale(subKey) }) const objective = this.eval(stats, slotKey) @@ -343,10 +402,10 @@ export class UpOptCalculator { const stats = { ...this.artifacts[ix].values } ns[3] += 1 // last substat has initial roll subs.forEach((subKey, i) => { - stats[subKey] += (17 / 2) * ns[i] * scale(subKey) + stats[subKey] += up_rv_mean * ns[i] * scale(subKey) }) stats[subKey4] = - (stats[subKey4] ?? 0) + (17 / 2) * ns[3] * scale(subKey4) + (stats[subKey4] ?? 0) + up_rv_mean * ns[3] * scale(subKey4) const objective = this.eval(stats, slotKey) const obj_ks = objective.map(({ grads }) => { From 60063e7c4735c81daf0533ac7f1a3f3f7d333ffd Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 15:26:14 -0400 Subject: [PATCH 08/38] integrate new class (I think) --- .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 72 +++++++------- .../Tabs/TabUpgradeOpt/index.tsx | 96 +++++++++++++------ .../Tabs/TabUpgradeOpt/upOpt.ts | 58 +++++++++-- 3 files changed, 155 insertions(+), 71 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx index 2cfa5bc70e..6db0094fd6 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -32,12 +32,14 @@ import type { ArtifactSlotKey } from '@genshin-optimizer/consts' import { allArtifactSlotKeys } from '@genshin-optimizer/consts' import type { ICachedArtifact } from '../../../../Types/artifact' import { gaussPDF } from './mathUtil' +import type { UpOptArtifact } from './upOpt' type Data = { - upgradeOpt: QueryResult + upgradeOpt: UpOptArtifact showTrue?: boolean objMin: number objMax: number + thresholds: number[] ix?: number } type ChartData = { @@ -63,6 +65,7 @@ const nbins = 50 const plotPoints = 500 export default function UpgradeOptChartCard({ upgradeOpt, + thresholds, objMin, objMax, }: Data) { @@ -74,7 +77,7 @@ export default function UpgradeOptChartCard({ throw new Error(`artifact ${upgradeOpt.id} not found.`) } - const constrained = upgradeOpt.thresholds.length > 1 + const constrained = thresholds.length > 1 const slot = bla.slotKey const { data } = useContext(DataContext) @@ -88,16 +91,16 @@ export default function UpgradeOptChartCard({ ) as Array<[ArtifactSlotKey, ICachedArtifact | undefined]> const gauss = (x: number) => - upgradeOpt.distr.gmm.reduce( + upgradeOpt.result!.distr.gmm.reduce( (pv, { phi, mu, sig2 }) => pv + phi * gaussPDF(x, mu, sig2), 0 ) const gaussConstrained = (x: number) => - upgradeOpt.distr.gmm.reduce( + upgradeOpt.result!.distr.gmm.reduce( (pv, { phi, cp, mu, sig2 }) => pv + cp * phi * gaussPDF(x, mu, sig2), 0 ) - const thresh = upgradeOpt.thresholds + const thresh = thresholds const thr0 = thresh[0] // const perc = (x: number) => 100 * (x - thr0) / thr0; const perc = useCallback((x: number) => (100 * (x - thr0)) / thr0, [thr0]) @@ -116,7 +119,7 @@ export default function UpgradeOptChartCard({ // go back and add delta distributions. const deltas: { [key: number]: number } = {} const deltasConstrained: { [key: number]: number } = {} - upgradeOpt.distr.gmm.forEach(({ phi, mu, sig2, cp }) => { + upgradeOpt.result!.distr.gmm.forEach(({ phi, mu, sig2, cp }) => { if (sig2 <= 0) { deltas[mu] = (deltas[mu] ?? 0) + phi deltasConstrained[mu] = (deltasConstrained[mu] ?? 0) + phi * cp @@ -141,36 +144,37 @@ export default function UpgradeOptChartCard({ // When `calcExacts` is pressed, we may want to sink/swim this artifact to its proper spot. // Or not b/c people only really need a fuzzy ordering anyways. if (!calcExacts) return - const exactData = allUpgradeValues(upgradeOpt) - let true_p = 0 - let true_e = 0 + throw new Error('Not Implemented!') + // const exactData = allUpgradeValues(upgradeOpt) + // let true_p = 0 + // let true_e = 0 - const bins = new Array(nbins).fill(0) - const binsConstrained = new Array(nbins).fill(0) - const binstep = (maax - miin) / nbins + // const bins = new Array(nbins).fill(0) + // const binsConstrained = new Array(nbins).fill(0) + // const binstep = (maax - miin) / nbins - exactData.forEach(({ p, v }) => { - const whichBin = Math.min(Math.trunc((v[0] - miin) / binstep), nbins - 1) - bins[whichBin] += p + // exactData.forEach(({ p, v }) => { + // const whichBin = Math.min(Math.trunc((v[0] - miin) / binstep), nbins - 1) + // bins[whichBin] += p - if (v.every((val, ix) => ix === 0 || val > thresh[ix])) { - binsConstrained[whichBin] += p - if (v[0] > thr0) { - true_p += p - true_e += p * (v[0] - thr0) - } - } - }) - if (true_p > 0) true_e = true_e / true_p + // if (v.every((val, ix) => ix === 0 || val > thresh[ix])) { + // binsConstrained[whichBin] += p + // if (v[0] > thr0) { + // true_p += p + // true_e += p * (v[0] - thr0) + // } + // } + // }) + // if (true_p > 0) true_e = true_e / true_p - const dataExact: ChartData[] = bins.map((dens, ix) => ({ - x: perc(miin + ix * binstep), - exact: dens / binstep, - exactCons: binsConstrained[ix] / binstep, - })) - setTrueP(true_p) - setTrueE(true_e) - setTrueData(dataExact) + // const dataExact: ChartData[] = bins.map((dens, ix) => ({ + // x: perc(miin + ix * binstep), + // exact: dens / binstep, + // exactCons: binsConstrained[ix] / binstep, + // })) + // setTrueP(true_p) + // setTrueE(true_e) + // setTrueData(dataExact) }, [calcExacts, maax, miin, thr0, thresh, upgradeOpt, perc]) if (trueData.length === 0) { @@ -181,8 +185,8 @@ export default function UpgradeOptChartCard({ } // if trueP/E have been calculated, otherwise use upgradeOpt's estimate - const reportP = trueP >= 0 ? trueP : upgradeOpt.prob - const reportD = trueE >= 0 ? trueE : upgradeOpt.upAvg + const reportP = trueP >= 0 ? trueP : upgradeOpt.result!.p + const reportD = trueE >= 0 ? trueE : upgradeOpt.result!.upAvg const chartData = dataEst.concat(trueData) // console.log('repd', reportD, upgradeOpt.upAvg) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx index 348ec6ab7f..6e9d1f294b 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx @@ -67,6 +67,8 @@ import { } from '@genshin-optimizer/consts' import useForceUpdate from '../../../../ReactHooks/useForceUpdate' +import { UpOptCalculator } from './upOpt' + export default function TabUpopt() { const { character: { key: characterKey }, @@ -139,6 +141,9 @@ export default function TabUpopt() { const [artifactUpgradeOpts, setArtifactUpgradeOpts] = useState( undefined as UpgradeOptResult | undefined ) + const [upOptCalc, setUpOptCalc] = useState( + undefined as UpOptCalculator | undefined + ) const [show20, setShow20] = useState(true) const [check4th, setCheck4th] = useState(true) @@ -192,7 +197,7 @@ export default function TabUpopt() { const artifactsToDisplayPerPage = 5 const { artifactsToShow, numPages, currentPageIndex, minObj0, maxObj0 } = useMemo(() => { - if (!artifactUpgradeOpts) + if (!artifactUpgradeOpts || !upOptCalc) return { artifactsToShow: [], numPages: 0, @@ -204,20 +209,34 @@ export default function TabUpopt() { artifactUpgradeOpts.arts.length / artifactsToDisplayPerPage ) const currentPageIndex = clamp(pageIdex, 0, numPages - 1) - const toShow = artifactUpgradeOpts.arts.slice( + const toShow_old = artifactUpgradeOpts.arts.slice( + currentPageIndex * artifactsToDisplayPerPage, + (currentPageIndex + 1) * artifactsToDisplayPerPage + ) + const toShow = upOptCalc.artifacts.slice( currentPageIndex * artifactsToDisplayPerPage, (currentPageIndex + 1) * artifactsToDisplayPerPage ) - const thr = toShow.length > 0 ? toShow[0].thresholds[0] : 0 + // const thr = toShow.length > 0 ? toShow[0].thresholds[0] : 0 + const thr = upOptCalc.thresholds[0] + + console.log(toShow_old) + console.log(toShow) return { artifactsToShow: toShow, numPages, currentPageIndex, - minObj0: toShow.reduce((a, b) => Math.min(b.distr.lower, a), thr), - maxObj0: toShow.reduce((a, b) => Math.max(b.distr.upper, a), thr), + minObj0: toShow.reduce( + (a, b) => Math.min(b.result!.distr.lower, a), + thr + ), + maxObj0: toShow.reduce( + (a, b) => Math.max(b.result!.distr.upper, a), + thr + ), } - }, [artifactUpgradeOpts, artifactsToDisplayPerPage, pageIdex]) + }, [artifactUpgradeOpts, pageIdex, upOptCalc]) const setPage = useCallback( (e, value) => { @@ -227,17 +246,10 @@ export default function TabUpopt() { const end = value * artifactsToDisplayPerPage const zz = upgradeOptExpandSink(artifactUpgradeOpts, start, end) setArtifactUpgradeOpts(zz) + upOptCalc?.calcSlowToIndex(end) setpageIdex(value - 1) }, - [ - setpageIdex, - setArtifactUpgradeOpts, - invScrollRef, - currentPageIndex, - artifactsToDisplayPerPage, - artifactUpgradeOpts, - upgradeOptExpandSink, - ] + [artifactUpgradeOpts, currentPageIndex, upgradeOptExpandSink, upOptCalc] ) const generateBuilds = useCallback(async () => { @@ -281,10 +293,6 @@ export default function TabUpopt() { return { value: filterNode, minimum } }) ) - // const valueFilter: { value: NumNode, minimum: number }[] = Object.entries(statFilters).map(([key, value]) => { - // if (key.endsWith("_")) value = value / 100 - // return { value: input.total[key], minimum: value } - // }).filter(x => x.value && x.minimum > -Infinity) const equippedArts = database.chars.get(characterKey)?.equippedArtifacts ?? @@ -320,7 +328,7 @@ export default function TabUpopt() { case 5: return !artSetExclusion[setKey].includes(4) default: - throw Error('error in respectSex: num > 5') + throw Error('error in respectSetExclude: num > 5') } }) if (!pass) return false @@ -342,6 +350,41 @@ export default function TabUpopt() { } } + const nodesPreOpt = [ + optimizationTargetNode, + ...valueFilter.map((x) => x.value), + ] + const nodes = optimize( + nodesPreOpt, + workerData, + ({ path: [p] }) => p !== 'dyn' + ) + const upoptCalc = new UpOptCalculator( + nodes, + [-Infinity, ...valueFilter.map((x) => x.minimum)], + curEquip + ) + upoptCalc.calc4th = check4th + database.arts.values + .filter((art) => art.rarity === 5) + .filter(respectSexExclusion) + .filter((art) => show20 || art.level !== 20) + .filter( + (art) => + !useFilters || + !mainStatKeys[art.slotKey]?.length || + mainStatKeys[art.slotKey]?.includes(art.mainStatKey) + ) + .filter( + (art) => + !useFilters || (levelLow <= art.level && art.level <= levelHigh) + ) + .forEach((art) => upoptCalc.addArtifact(art)) + upoptCalc.calcFastAll() + upoptCalc.calcSlowToIndex(5) + setUpOptCalc(upoptCalc) + + // OLD METHOD const queryArts: QueryArtifact[] = database.arts.values .filter((art) => art.rarity === 5) .filter(respectSexExclusion) @@ -357,18 +400,10 @@ export default function TabUpopt() { !useFilters || (levelLow <= art.level && art.level <= levelHigh) ) .map((art) => toQueryArtifact(art, 20)) + const qaLookup: Dict = {} queryArts.forEach((art) => (qaLookup[art.id] = art)) - const nodesPreOpt = [ - optimizationTargetNode, - ...valueFilter.map((x) => x.value), - ] - const nodes = optimize( - nodesPreOpt, - workerData, - ({ path: [p] }) => p !== 'dyn' - ) const query = querySetup( nodes, valueFilter.map((x) => x.minimum), @@ -378,6 +413,7 @@ export default function TabUpopt() { let artUpOpt = queryArts.map((art) => evalArtifact(query, art, false, check4th) ) + artUpOpt = artUpOpt.sort((a, b) => b.prob * b.upAvg - a.prob * a.upAvg) // Re-sort & slow eval @@ -385,6 +421,7 @@ export default function TabUpopt() { upOpt = upgradeOptExpandSink(upOpt, 0, 5) setArtifactUpgradeOpts(upOpt) console.log('result', upOpt) + // OLD METHOD }, [ buildSetting, characterKey, @@ -638,6 +675,7 @@ export default function TabUpopt() { diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index e1fc591cff..ec755f4207 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -140,6 +140,7 @@ export class UpOptCalculator { baseBuild: UpOptBuild nodes: OptNode[] thresholds: number[] + calc4th = false skippableDerivatives: boolean[] eval: ( @@ -148,20 +149,15 @@ export class UpOptCalculator { ) => { v: number; grads: number[] }[] artifacts: UpOptArtifact[] = [] + fixedIx = 0 /** * Constructs UpOptCalculator. * @param nodes Formulas to find upgrades for. nodes[0] is main objective, the rest are constraints. * @param thresholds Constraint values. thresholds[0] will be auto-populated with current objective value. - * @param baseStats Character base stats * @param build Build to check 1-swaps against. */ - constructor( - nodes: OptNode[], - thresholds: number[], - baseStats: DynStat, - build: UpOptBuild - ) { + constructor(nodes: OptNode[], thresholds: number[], build: UpOptBuild) { this.baseBuild = build this.nodes = nodes this.thresholds = thresholds @@ -175,7 +171,7 @@ export class UpOptCalculator { }) const evalOpt = optimize(toEval, {}, ({ path: [p] }) => p !== 'dyn') - const evalFn = precompute(evalOpt, baseStats, (f) => f.path[1], 5) + const evalFn = precompute(evalOpt, {}, (f) => f.path[1], 5) thresholds[0] = evalFn(Object.values(build))[0] // dmg threshold is current objective value this.skippableDerivatives = allSubstatKeys.map((sub) => @@ -226,6 +222,51 @@ export class UpOptCalculator { }) } + /** Calcs all artifacts using Fast method */ + calcFastAll() { + function score(a: UpOptArtifact) { + return a.result!.p * a.result!.upAvg + } + this.artifacts.forEach((_, i) => this.calcFast(i, this.calc4th)) + this.artifacts.sort((a, b) => score(b) - score(a)) + this.fixedIx = 0 + } + + calcSlowToIndex(ix: number, lookahead = 5) { + const fixedList = this.artifacts.slice(0, this.fixedIx) + const arr = this.artifacts.slice(this.fixedIx) + + function score(a: UpOptArtifact) { + return a.result!.p * a.result!.upAvg + } + function compare(a: UpOptArtifact, b: UpOptArtifact) { + if (score(a) > 1e-5 || score(b) > 1e-5) return score(b) - score(a) + + const meanA = a.result!.distr.gmm.reduce( + (pv, { phi, mu }) => pv + phi * mu, + 0 + ) + const meanB = b.result!.distr.gmm.reduce( + (pv, { phi, mu }) => pv + phi * mu, + 0 + ) + return meanB - meanA + } + + // Assume `fixedList` is all slowEval'd. + let i = 0 + const end = Math.min(ix - this.fixedIx + lookahead, arr.length) + do { + for (; i < end; i++) this.calcSlow(this.fixedIx + i, this.calc4th) + + arr.sort(compare) + this.artifacts = [...fixedList, ...arr] + for (i = 0; i < end; i++) { + if (arr[i].result!.evalMode === ResultType.Fast) break + } + } while (i < end) + } + /* Fast distribution to result. */ toResult1( distr: { prob: number; mu: number[]; cov: number[] }[] @@ -354,6 +395,7 @@ export class UpOptCalculator { } calcSlow(ix: number, calc4th = true) { + if (this.artifacts[ix].result?.evalMode === ResultType.Slow) return if (this.artifacts[ix].subs.length === 4) calc4th = false if (calc4th) this._calcSlow4th(ix) else this._calcSlow(ix) From a982481af1345d5d69bc868d71bd3b5196de83c5 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 15:37:57 -0400 Subject: [PATCH 09/38] strip out old code --- .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 9 +- .../Tabs/TabUpgradeOpt/index.tsx | 127 ++---------------- .../Tabs/TabUpgradeOpt/upOpt.ts | 37 +++++ 3 files changed, 54 insertions(+), 119 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx index 6db0094fd6..9c828d00e0 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -24,8 +24,6 @@ import { Label, } from 'recharts' import CardLight from '../../../../Components/Card/CardLight' -import type { QueryResult } from './artifactQuery' -import { allUpgradeValues } from './artifactUpgradeCrawl' import { uiInput as input } from '../../../../Formula' import ArtifactCardPico from '../../../../Components/Artifact/ArtifactCardPico' import type { ArtifactSlotKey } from '@genshin-optimizer/consts' @@ -145,6 +143,9 @@ export default function UpgradeOptChartCard({ // Or not b/c people only really need a fuzzy ordering anyways. if (!calcExacts) return throw new Error('Not Implemented!') + setTrueData([]) + setTrueP(0) + setTrueE(0) // const exactData = allUpgradeValues(upgradeOpt) // let true_p = 0 // let true_e = 0 @@ -191,9 +192,7 @@ export default function UpgradeOptChartCard({ // console.log('repd', reportD, upgradeOpt.upAvg) - const CustomTooltip = ({ - active, - }: TooltipProps) => { + const CustomTooltip = ({ active }: TooltipProps) => { if (!active) return null // I kinda want the [average increase] to only appear when hovering the white dot. return ( diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx index 6e9d1f294b..fc27ac4517 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx @@ -47,13 +47,6 @@ import useBuildSetting from '../TabOptimize/useBuildSetting' import { dynamicData } from '../TabOptimize/foreground' import { clamp, objectKeyMap, objPathValue } from '../../../../Util/Util' import { mergeData, uiDataForTeam } from '../../../../Formula/api' -import { evalArtifact } from './evalArtifact' -import type { - QueryArtifact, - QueryBuild, - UpgradeOptResult, -} from './artifactQuery' -import { querySetup, toQueryArtifact, cmpQuery } from './artifactQuery' import UpgradeOptChartCard from './UpgradeOptChartCard' import MainStatSelectionCard from '../TabOptimize/Components/MainStatSelectionCard' import { CharacterContext } from '../../../../Context/CharacterContext' @@ -67,7 +60,8 @@ import { } from '@genshin-optimizer/consts' import useForceUpdate from '../../../../ReactHooks/useForceUpdate' -import { UpOptCalculator } from './upOpt' +import type { UpOptBuild } from './upOpt' +import { UpOptCalculator, toArtifact } from './upOpt' export default function TabUpopt() { const { @@ -138,9 +132,6 @@ export default function TabUpopt() { [filteredArts] ) - const [artifactUpgradeOpts, setArtifactUpgradeOpts] = useState( - undefined as UpgradeOptResult | undefined - ) const [upOptCalc, setUpOptCalc] = useState( undefined as UpOptCalculator | undefined ) @@ -149,47 +140,6 @@ export default function TabUpopt() { const [check4th, setCheck4th] = useState(true) const [useFilters, setUseMainStatFilter] = useState(false) - // Because upgradeOpt is a two-stage estimation method, we want to expand (slow-estimate) our artifacts lazily as they are needed. - // Lazy method means we need to take care to never 'lift' any artifacts past the current page, since that may cause a user to miss artifacts - // that are lifted in the middle of an expansion. Increase lookahead to mitigate this issue. - const upgradeOptExpandSink = useCallback( - ( - { query, arts }: UpgradeOptResult, - start: number, - expandTo: number - ): UpgradeOptResult => { - const lookahead = 5 - // if (querySaved === undefined) return upOpt - const queryArts: QueryArtifact[] = database.arts.values - .filter((art) => art.rarity === 5) - .map((art) => toQueryArtifact(art, 20)) - - const qaLookup: Dict = {} - queryArts.forEach((art) => (qaLookup[art.id] = art)) - - const fixedList = arts.slice(0, start) - const arr = arts.slice(start) - - let i = 0 - const end = Math.min(expandTo - start + lookahead, arr.length) - do { - for (; i < end; i++) { - const arti = qaLookup[arr[i].id] - if (arti) arr[i] = evalArtifact(query, arti, true, check4th) - } - - // sort on only bottom half to prevent lifting - arr.sort(cmpQuery) - for (i = 0; i < end; i++) { - if (arr[i].evalMode === 'fast') break - } - } while (i < end) - - return { query, arts: [...fixedList, ...arr] } - }, - [database, check4th] - ) - // Paging logic const [pageIdex, setpageIdex] = useState(0) const invScrollRef = useRef(null) @@ -197,7 +147,7 @@ export default function TabUpopt() { const artifactsToDisplayPerPage = 5 const { artifactsToShow, numPages, currentPageIndex, minObj0, maxObj0 } = useMemo(() => { - if (!artifactUpgradeOpts || !upOptCalc) + if (!upOptCalc) return { artifactsToShow: [], numPages: 0, @@ -206,23 +156,15 @@ export default function TabUpopt() { maxObj0: 0, } const numPages = Math.ceil( - artifactUpgradeOpts.arts.length / artifactsToDisplayPerPage + upOptCalc.artifacts.length / artifactsToDisplayPerPage ) const currentPageIndex = clamp(pageIdex, 0, numPages - 1) - const toShow_old = artifactUpgradeOpts.arts.slice( - currentPageIndex * artifactsToDisplayPerPage, - (currentPageIndex + 1) * artifactsToDisplayPerPage - ) const toShow = upOptCalc.artifacts.slice( currentPageIndex * artifactsToDisplayPerPage, (currentPageIndex + 1) * artifactsToDisplayPerPage ) - // const thr = toShow.length > 0 ? toShow[0].thresholds[0] : 0 const thr = upOptCalc.thresholds[0] - console.log(toShow_old) - console.log(toShow) - return { artifactsToShow: toShow, numPages, @@ -236,20 +178,17 @@ export default function TabUpopt() { thr ), } - }, [artifactUpgradeOpts, pageIdex, upOptCalc]) + }, [pageIdex, upOptCalc]) const setPage = useCallback( (e, value) => { - if (!artifactUpgradeOpts) return + if (!upOptCalc) return invScrollRef.current?.scrollIntoView({ behavior: 'smooth' }) - const start = (currentPageIndex + 1) * artifactsToDisplayPerPage const end = value * artifactsToDisplayPerPage - const zz = upgradeOptExpandSink(artifactUpgradeOpts, start, end) - setArtifactUpgradeOpts(zz) - upOptCalc?.calcSlowToIndex(end) + upOptCalc.calcSlowToIndex(end) setpageIdex(value - 1) }, - [artifactUpgradeOpts, currentPageIndex, upgradeOptExpandSink, upOptCalc] + [upOptCalc] ) const generateBuilds = useCallback(async () => { @@ -275,7 +214,7 @@ export default function TabUpopt() { optimizationTarget ) as NumNode | undefined if (!optimizationTargetNode) return - setArtifactUpgradeOpts(undefined) + setUpOptCalc(undefined) setpageIdex(0) const valueFilter: { value: NumNode; minimum: number }[] = Object.entries( @@ -297,11 +236,11 @@ export default function TabUpopt() { const equippedArts = database.chars.get(characterKey)?.equippedArtifacts ?? ({} as StrictDict) - const curEquip: QueryBuild = objectKeyMap( + const curEquip: UpOptBuild = objectKeyMap( allArtifactSlotKeys, (slotKey) => { const art = database.arts.get(equippedArts[slotKey] ?? '') - return art ? toQueryArtifact(art) : undefined + return art ? toArtifact(art) : undefined } ) const curEquipSetKeys = objectKeyMap(allArtifactSlotKeys, (slotKey) => { @@ -383,51 +322,11 @@ export default function TabUpopt() { upoptCalc.calcFastAll() upoptCalc.calcSlowToIndex(5) setUpOptCalc(upoptCalc) - - // OLD METHOD - const queryArts: QueryArtifact[] = database.arts.values - .filter((art) => art.rarity === 5) - .filter(respectSexExclusion) - .filter((art) => show20 || art.level !== 20) - .filter( - (art) => - !useFilters || - !mainStatKeys[art.slotKey]?.length || - mainStatKeys[art.slotKey]?.includes(art.mainStatKey) - ) - .filter( - (art) => - !useFilters || (levelLow <= art.level && art.level <= levelHigh) - ) - .map((art) => toQueryArtifact(art, 20)) - - const qaLookup: Dict = {} - queryArts.forEach((art) => (qaLookup[art.id] = art)) - - const query = querySetup( - nodes, - valueFilter.map((x) => x.minimum), - curEquip - ) - - let artUpOpt = queryArts.map((art) => - evalArtifact(query, art, false, check4th) - ) - - artUpOpt = artUpOpt.sort((a, b) => b.prob * b.upAvg - a.prob * a.upAvg) - - // Re-sort & slow eval - let upOpt = { query: query, arts: artUpOpt } - upOpt = upgradeOptExpandSink(upOpt, 0, 5) - setArtifactUpgradeOpts(upOpt) - console.log('result', upOpt) - // OLD METHOD }, [ buildSetting, characterKey, database, gender, - upgradeOptExpandSink, show20, useFilters, check4th, @@ -631,7 +530,7 @@ export default function TabUpopt() { @@ -701,7 +600,7 @@ export default function TabUpopt() { diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index ec755f4207..d52a970bc5 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -470,3 +470,40 @@ export class UpOptCalculator { this.artifacts[ix].result = this.toResult2(distrs) } } + +/* ICachedArtifact to ArtifactBuildData. Maybe this should go in common? */ +export function toArtifact(art: ICachedArtifact): ArtifactBuildData { + const mainStatVal = Artifact.mainStatValue( + art.mainStatKey, + art.rarity, + art.level + ) // 5* only + const buildData = { + id: art.id, + slot: art.slotKey, + level: art.level, + rarity: art.rarity, + values: { + [art.setKey]: 1, + [art.mainStatKey]: art.mainStatKey.endsWith('_') + ? mainStatVal / 100 + : mainStatVal, + ...Object.fromEntries( + art.substats + .map((substat) => [ + substat.key, + substat.key.endsWith('_') + ? substat.accurateValue / 100 + : substat.accurateValue, + ]) + .filter(([, value]) => value !== 0) + ), + }, + subs: art.substats.reduce((sub: SubstatKey[], x) => { + if (x.key !== '') sub.push(x.key) + return sub + }, []), + } + delete buildData.values[''] + return buildData +} From d90284cee817d87befffb4778b7a8f1eb9671866 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 16:43:11 -0400 Subject: [PATCH 10/38] delet old code --- .../Tabs/TabUpgradeOpt/artifactQuery.ts | 158 --------- .../TabUpgradeOpt/artifactUpgradeCrawl.ts | 125 -------- .../Tabs/TabUpgradeOpt/evalArtifact.ts | 302 ------------------ .../Tabs/TabUpgradeOpt/mathUtil.ts | 47 +++ .../Tabs/TabUpgradeOpt/upOpt.ts | 61 +++- 5 files changed, 107 insertions(+), 586 deletions(-) delete mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactQuery.ts delete mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactUpgradeCrawl.ts delete mode 100644 apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/evalArtifact.ts diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactQuery.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactQuery.ts deleted file mode 100644 index bc0097c581..0000000000 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactQuery.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { ICachedArtifact } from '../../../../Types/artifact' -import { allSubstatKeys } from '../../../../Types/artifact' -import Artifact from '../../../../Data/Artifacts/Artifact' -import type { DynStat } from '../../../../Solver/common' -import type { NumNode } from '../../../../Formula/type' -import type { OptNode } from '../../../../Formula/optimization' -import { precompute, optimize } from '../../../../Formula/optimization' -import { ddx, zero_deriv } from '../../../../Formula/differentiate' - -import type { ArtifactSlotKey, RarityKey } from '@genshin-optimizer/consts' -import type { SubstatKey } from '@genshin-optimizer/dm' - -function toStats(build: QueryBuild): DynStat { - const stats: DynStat = {} - Object.values(build).forEach((a) => { - if (a) - Object.entries(a.values).forEach( - ([key, value]) => (stats[key] = (stats[key] ?? 0) + value) - ) - }) - return stats -} -export function querySetup( - formulas: OptNode[], - thresholds: number[], - curBuild: QueryBuild -): Query { - const toEval: OptNode[] = [] - formulas.forEach((f) => { - toEval.push( - f, - ...allSubstatKeys.map((sub) => ddx(f, (fo) => fo.path[1], sub)) - ) - }) - const evalOpt = optimize(toEval, {}, ({ path: [p] }) => p !== 'dyn') - - const evalFn = precompute(evalOpt, {}, (f) => f.path[1], 1) - const stats = toStats(curBuild) - const dmg0 = evalFn([{ id: '', values: stats }])[0] - - const skippableDerivs = allSubstatKeys.map((sub) => - formulas.every((f) => zero_deriv(f, (f) => f.path[1], sub)) - ) - const structuredEval = (stats: DynStat) => { - const out = evalFn([{ id: '', values: stats }]) - return formulas.map((_, i) => { - const ix = i * (1 + allSubstatKeys.length) - return { - v: out[ix], - grads: allSubstatKeys.map((sub, si) => out[ix + 1 + si]), - } - }) - } - - return { - formulas: formulas, - thresholds: [dmg0, ...thresholds], - curBuild: curBuild, - evalFn: structuredEval, - skippableDerivs: skippableDerivs, - } -} - -export function toQueryArtifact(art: ICachedArtifact, fixedLevel?: number) { - if (fixedLevel === undefined) fixedLevel = art.level - const mainStatVal = Artifact.mainStatValue( - art.mainStatKey, - art.rarity, - fixedLevel - ) // 5* only - const buildData = { - id: art.id, - slot: art.slotKey, - level: art.level, - rarity: art.rarity, - values: { - [art.setKey]: 1, - [art.mainStatKey]: art.mainStatKey.endsWith('_') - ? mainStatVal / 100 - : mainStatVal, - ...Object.fromEntries( - art.substats.map((substat) => [ - substat.key, - substat.key.endsWith('_') - ? substat.accurateValue / 100 - : substat.accurateValue, - ]) - ), - }, - subs: art.substats.reduce((sub: SubstatKey[], x) => { - if (x.key !== '') sub.push(x.key) - return sub - }, []), - } - delete buildData.values[''] - return buildData -} - -export function cmpQuery(a: QueryResult, b: QueryResult) { - if (b.prob > 1e-5 || a.prob > 1e-5) return b.prob * b.upAvg - a.prob * a.upAvg - - const meanA = a.distr.gmm.reduce((pv, { phi, mu }) => pv + phi * mu, 0) - const meanB = b.distr.gmm.reduce((pv, { phi, mu }) => pv + phi * mu, 0) - return meanB - meanA -} - -export type GaussianMixture = { - gmm: { - phi: number // Item weight; must sum to 1. - cp: number // Constraint probability - mu: number - sig2: number - }[] - lower: number - upper: number -} -export type Query = { - formulas: NumNode[] - curBuild: QueryBuild - - thresholds: number[] - evalFn: (values: DynStat) => StructuredNumber[] - skippableDerivs: boolean[] -} -export type QueryResult = { - id: string - rollsLeft: number - subs: SubstatKey[] - statsBase: DynStat - evalFn: (values: DynStat) => StructuredNumber[] - skippableDerivs: boolean[] - - prob: number - upAvg: number - distr: GaussianMixture - thresholds: number[] - fourthsubOpts?: { sub: SubstatKey; subprob: number }[] - - evalMode: 'fast' | 'slow' -} -type StructuredNumber = { - v: number - grads: number[] -} - -export type QueryArtifact = { - id: string - level: number - rarity: RarityKey - slot: ArtifactSlotKey - values: DynStat - subs: SubstatKey[] -} -export type QueryBuild = { [key in ArtifactSlotKey]: QueryArtifact | undefined } -export type UpgradeOptResult = { - query: Query - arts: QueryResult[] -} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactUpgradeCrawl.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactUpgradeCrawl.ts deleted file mode 100644 index 5d70bb7312..0000000000 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/artifactUpgradeCrawl.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { SubstatKey } from '../../../../Types/artifact' -import type { QueryResult } from './artifactQuery' -import Artifact from '../../../../Data/Artifacts/Artifact' -import { allSubstatKeys } from '../../../../Types/artifact' -import { range, cartesian } from '../../../../Util/Util' -import { quadrinomial } from './mathUtil' - -// Manually cached multinomial distribution. -// Example: sigma([2, 3, 0, 0], 5) -// gives the probability (n1=2, n2=3, n3=0, n4=0) given N=5 total rolls. (uniform distribution is assumed for the four bins) -// `sigr` and `sig_arr` constitute a near perfect hash of all combinations for N=1 to N=5. -// This function has undefined behavior for N > 5 and N = 0 -// prettier-ignore -const sig_arr = [270 / 1024, 80 / 1024, 0, 12 / 256, 8 / 256, 120 / 1024, 0, 60 / 1024, 4 / 256, 60 / 1024, 4 / 256, 30 / 1024, 24 / 256, 160 / 1024, 1 / 64, 1 / 64, 24 / 256, 1 / 64, 12 / 256, 0, 6 / 256, 2 / 16, 6 / 256, 0, 81 / 256, 16 / 256, 0, 27 / 64, 12 / 64, 0, 1 / 16, 1 / 16, 12 / 64, 1 / 16, 6 / 64, 3 / 4, 2 / 4, 243 / 1024, 32 / 1024, 0, 108 / 256, 32 / 256, 0, 9 / 64, 6 / 64, 48 / 256, 0, 24 / 256, 3 / 64, 5 / 1024, 3 / 64, 5 / 1024, 0, 405 / 1024, 80 / 1024, 0, 54 / 256, 90 / 1024, 40 / 1024, 0, 1 / 256, 1 / 256, 40 / 1024, 1 / 256, 20 / 1024, 9 / 16, 4 / 16, 0, 1 / 4, 1 / 4, 0, 1 / 4, 27 / 64, 8 / 64, 0, 6 / 16, 4 / 16, 10 / 1024, 0, 10 / 1024, 2 / 16, 0, 0, 0, 15 / 1024, 10 / 1024, 1 / 1024, 1 / 1024, 0, 1 / 1024] -const sigr = [35, 64, 70, 21, 33, 45, 12, 0, 53, 76, 48, 86] -function sigma(ss: number[], N: number) { - const ssum = ss.reduce((a, b) => a + b) - if (ss.length > 4 || ssum > N) return 0 - if (ss.length === 4 && ssum !== N) return 0 - if (ss.length === 3) ss = [...ss, N - ssum] - ss.sort().reverse() - - // t = 12 - // offset = -14 - let v = 13 * N + ss.length - 14 + 16 * ss[0] - if (ss.length > 1) v += 4 * ss[1] - const x = v % 12 - const y = Math.trunc(v / 12) // integer divide - - return sig_arr[x + sigr[y]] -} - -export function crawlUpgrades( - n: number, - fn: (n1234: number[], p: number) => void -) { - if (n === 0) { - fn([0, 0, 0, 0], 1) - return - } - - // Binomial(n+3, 3) branches to crawl. - for (let i1 = n; i1 >= 0; i1--) { - for (let i2 = n - i1; i2 >= 0; i2--) { - for (let i3 = n - i1 - i2; i3 >= 0; i3--) { - const i4 = n - i1 - i2 - i3 - const p_comb = sigma([i1, i2, i3, i4], n) - fn([i1, i2, i3, i4], p_comb) - } - } - } -} - -export function allUpgradeValues({ - statsBase, - rollsLeft, - subs, - skippableDerivs, - fourthsubOpts, - evalFn, -}: QueryResult) { - // TODO: Include non-5* artifacts - const scale = (key: SubstatKey) => - key.endsWith('_') - ? Artifact.substatValue(key, 5) / 1000 - : Artifact.substatValue(key, 5) / 10 - const base = statsBase - - const results: WeightedPoint[] = [] - crawlUpgrades(rollsLeft, (ns, p) => { - if (fourthsubOpts) ns[3] += 1 - const vals = ns.map((ni, i) => { - if (fourthsubOpts && i === 3) return range(7 * ni, 10 * ni) - const sub = subs[i] - if (sub && !skippableDerivs[allSubstatKeys.indexOf(sub)]) - return range(7 * ni, 10 * ni) - return [NaN] - }) - - const allValues: number[][] = cartesian(...vals) - allValues.forEach((upVals) => { - const stats = { ...base } - let p_upVals = 1 - for (let i = 0; i < 3; i++) { - if (isNaN(upVals[i])) continue - - const key = subs[i] - const val = upVals[i] - const ni = ns[i] - stats[key] = (stats[key] ?? 0) + val * scale(key) - const p_val = 4 ** -ni * quadrinomial(ni, val - 7 * ni) - p_upVals *= p_val - } - if (fourthsubOpts !== undefined) { - fourthsubOpts.forEach(({ sub, subprob }) => { - const stats2 = { ...stats } - const key = sub - const val = upVals[3] - const ni = ns[3] - stats2[key] = (stats2[key] ?? 0) + val * scale(key) - const p_val = 4 ** -ni * quadrinomial(ni, val - 7 * ni) * subprob - const p_upVals2 = p_upVals * p_val - results.push({ v: evalFn(stats2).map((n) => n.v), p: p * p_upVals2 }) - }) - return - } - if (!isNaN(upVals[3])) { - const key = subs[3] - const val = upVals[3] - const ni = ns[3] - stats[key] = (stats[key] ?? 0) + val * scale(key) - const p_val = 4 ** -ni * quadrinomial(ni, val - 7 * ni) - p_upVals *= p_val - } - results.push({ v: evalFn(stats).map((n) => n.v), p: p * p_upVals }) - }) - }) - - return results -} - -type WeightedPoint = { - v: number[] - p: number -} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/evalArtifact.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/evalArtifact.ts deleted file mode 100644 index fd4d2cd280..0000000000 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/evalArtifact.ts +++ /dev/null @@ -1,302 +0,0 @@ -import type { SubstatKey } from '../../../../Types/artifact' -import { allSubstatKeys } from '../../../../Types/artifact' -import { allArtifactSetKeys } from '@genshin-optimizer/consts' -import Artifact from '../../../../Data/Artifacts/Artifact' -import type { DynStat } from '../../../../Solver/common' -import type { - GaussianMixture, - Query, - QueryArtifact, - QueryBuild, - QueryResult, -} from './artifactQuery' -import { crawlUpgrades } from './artifactUpgradeCrawl' -import { gaussianPE, mvnPE_bad as mvnPE } from './mvncdf' - -type InternalQuery = { - rollsLeft: number - subs: SubstatKey[] - calc4th: boolean - stats: DynStat - thresholds: number[] - objectiveEval: (values: DynStat) => { v: number; ks: number[] }[] - scale: (key: SubstatKey) => number -} -type InternalResult = { - prob: number - upAvg: number - distr: GaussianMixture -} - -function toStats(build: QueryBuild): DynStat { - const stats: DynStat = {} - Object.values(build).forEach((a) => { - if (a) - Object.entries(a.values).forEach( - ([key, value]) => (stats[key] = (stats[key] ?? 0) + value) - ) - }) - return stats -} -const fWeight: StrictDict = { - hp: 6, - atk: 6, - def: 6, - hp_: 4, - atk_: 4, - def_: 4, - eleMas: 4, - enerRech_: 4, - critRate_: 3, - critDMG_: 3, -} -const fWeightTot = Object.values(fWeight).reduce((a, b) => a + b) -function subProb(sub: SubstatKey, excluded: SubstatKey[]) { - if (excluded.includes(sub)) return 0 - const denom = fWeightTot - excluded.reduce((a, b) => a + (fWeight[b] ?? 0), 0) - return fWeight[sub] / denom -} - -export function evalArtifact( - objective: Query, - art: QueryArtifact, - slow = false, - calc4th = false -): QueryResult { - const newBuild = { ...objective.curBuild } - newBuild[art.slot] = art - const newStats = toStats(newBuild) - const statsBase = { ...newStats } - const scale = (key: SubstatKey) => - key.endsWith('_') - ? Artifact.substatValue(key, art.rarity) / 1000 - : Artifact.substatValue(key, art.rarity) / 10 - - const rollsLeft = - Artifact.rollsRemaining(art.level, art.rarity) - (4 - art.subs.length) - if (art.subs.length === 4) calc4th = false - - if (!calc4th) { - const iq: InternalQuery = { - rollsLeft, - subs: art.subs, - calc4th, - stats: newStats, - thresholds: objective.thresholds, - objectiveEval: (stats) => - objective.evalFn(stats).map(({ v, grads }) => ({ - v: v, - ks: art.subs.map( - (key) => grads[allSubstatKeys.indexOf(key)] * scale(key) - ), - })), - scale, - } - - const out = slow ? slowGMMnd(iq) : fastGaussian(iq) - return { - id: art.id, - subs: art.subs, - rollsLeft: rollsLeft, - statsBase: statsBase, - - ...out, - evalFn: objective.evalFn, - skippableDerivs: objective.skippableDerivs, - thresholds: objective.thresholds, - - evalMode: slow ? 'slow' : 'fast', - } - } else { - const msOption = Object.keys(art.values) - .filter((v) => !(art.subs as string[]).includes(v)) - .filter((v) => !(allArtifactSetKeys as readonly string[]).includes(v)) - if (msOption.length !== 1) - throw Error('Failed to extract artifact main stat') - const mainStat = msOption[0] - - const subsToConsider = allSubstatKeys.filter( - (s) => !art.subs.includes(s) && s !== mainStat - ) - const results = subsToConsider.map((nxtsub) => { - const subs = [...art.subs, nxtsub] - const iq: InternalQuery = { - rollsLeft, - subs, - calc4th, - stats: newStats, - thresholds: objective.thresholds, - objectiveEval: (stats) => - objective.evalFn(stats).map(({ v, grads }) => ({ - v, - ks: subs.map( - (key) => grads[allSubstatKeys.indexOf(key)] * scale(key) - ), - })), - scale, - } - - const out = slow ? slowGMMnd(iq) : fastGaussian(iq) - return { - ...out, - p2: subProb(nxtsub, [...art.subs, mainStat as SubstatKey]), - } - }) - - const ptot = results.reduce((a, { prob: p, p2 }) => a + p * p2, 0) - const upAvgtot = - ptot < 1e-6 - ? 0 - : results.reduce((a, { prob: p, p2, upAvg }) => a + p * p2 * upAvg, 0) / - ptot - const distrtot = results.reduce( - (dtot, { p2, distr }) => { - dtot.lower = Math.min(dtot.lower, distr.lower) - dtot.upper = Math.max(dtot.upper, distr.upper) - dtot.gmm.push( - ...distr.gmm.map(({ phi, cp, mu, sig2 }) => ({ - phi: p2 * phi, - cp, - mu, - sig2, - })) - ) - return dtot - }, - { gmm: [], lower: Infinity, upper: -Infinity } as GaussianMixture - ) - - return { - id: art.id, - subs: art.subs, - fourthsubOpts: subsToConsider.map((sub) => ({ - sub, - subprob: subProb(sub, [...art.subs, mainStat as SubstatKey]), - })), - rollsLeft: rollsLeft, - statsBase: statsBase, - - prob: ptot, - upAvg: upAvgtot, - distr: distrtot, - evalFn: objective.evalFn, - skippableDerivs: objective.skippableDerivs, - thresholds: objective.thresholds, - - evalMode: slow ? 'slow' : 'fast', - } - } -} - -// Estimates an upper bound for summary statistics by approximating each formula/constraint indepenently, -// then taking a min() over all the formulas. The approximations use derivatives to construct a linear -// approximation of the damage formula, which we can use to treat the substats as a weighted sum of random -// variables. Then do some math to get the expected mean & variance of the weighted sum and approximate -// the distribution with a Gaussian. -function fastGaussian({ - rollsLeft, - subs, - stats, - thresholds, - calc4th, - scale, - objectiveEval, -}: InternalQuery): InternalResult { - // Evaluate derivatives at center of 4-D upgrade distribution - const stats2 = { ...stats } - subs.forEach((subKey, i) => { - const b = calc4th && i === 3 ? 1 : 0 - stats2[subKey] = - (stats2[subKey] ?? 0) + (17 / 2) * (rollsLeft / 4 + b) * scale(subKey) - }) - - const N = rollsLeft - const obj = objectiveEval(stats2) - let p_min = 1 - let upAvgUB = -1 - let apxDist: GaussianMixture = { gmm: [], lower: obj[0].v, upper: obj[0].v } - - // Iterate over objectives, aggregate by min() to construct an upper bound. - for (let ix = 0; ix < obj.length; ix++) { - const { v, ks } = obj[ix] - const ksum = ks.reduce((a, b) => a + b) - const ksum2 = ks.reduce((a, b) => a + b * b, 0) - - const mean = v - const variance = - ((147 / 8) * ksum2 - (289 / 64) * ksum * ksum) * N + - (calc4th ? (5 / 4) * ks[3] * ks[3] : 0) - - const { p, upAvg } = gaussianPE(mean, variance, thresholds[ix]) - if (ix === 0) { - upAvgUB = upAvg - apxDist = { - gmm: [{ phi: 1, mu: mean, sig2: variance, cp: 1 }], - lower: mean - 4 * Math.sqrt(variance), - upper: mean + 4 * Math.sqrt(variance), - } - } - p_min = Math.min(p, p_min) - } - - return { prob: p_min, upAvg: upAvgUB, distr: apxDist } -} - -// Accurately estimates the summary statistics by approximating each formula/constraint on the scale of a -// single roll, and iterating across all combinations of roll outcomes. This approximation works much better -// because the linear approximation is more valid on the smaller region. Also the substat upgrade values -// are conditionally independent given the number of rolls in each, giving much better justification for the -// Gaussian approximation. -// The splits across roll combinations means `gmmNd` gives an N-dimensional Gaussian mixture model. -function slowGMMnd({ - rollsLeft, - stats, - subs, - thresholds, - calc4th, - scale, - objectiveEval, -}: InternalQuery): InternalResult { - const appx: GaussianMixture = { - gmm: [], - lower: thresholds[0], - upper: thresholds[0], - } - - const lpe: { l: number; p: number; upAvg: number; cp: number }[] = [] - crawlUpgrades(rollsLeft, (ns, p) => { - const stat2 = { ...stats } - if (calc4th) ns[3] += 1 - subs.forEach((subKey, i) => { - stat2[subKey] = (stat2[subKey] ?? 0) + (17 / 2) * ns[i] * scale(subKey) - }) - - const obj = objectiveEval(stat2) - const mu = obj.map((o) => o.v) - const cov = obj.map((o1) => - obj.map((o2) => - o1.ks.reduce((pv, cv, k) => pv + o1.ks[k] * o2.ks[k] * ns[k], 0) - ) - ) - const res = mvnPE(mu, cov, thresholds) - lpe.push({ l: p, ...res }) - - // Feels a little bad to discard everything but the first axis, but can change later - appx.gmm.push({ phi: p, mu: mu[0], sig2: cov[0][0], cp: res.cp }) - appx.lower = Math.min(appx.lower, mu[0] - 4 * Math.sqrt(cov[0][0])) - appx.upper = Math.max(appx.upper, mu[0] + 4 * Math.sqrt(cov[0][0])) - }) - - // Aggregate gaussian mixture statistics. - let p_ret = 0, - upAvg_ret = 0 - lpe.forEach(({ l, p, upAvg, cp }) => { - // It's quite often that `p` becomes 0 here... should I use log likelihoods instead? - p_ret += l * p * cp - upAvg_ret += l * p * cp * upAvg - }) - - if (p_ret < 1e-10) return { prob: 0, upAvg: 0, distr: appx } - upAvg_ret = upAvg_ret / p_ret - return { prob: p_ret, upAvg: upAvg_ret, distr: appx } -} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts index 6344c29ea0..329d0467ab 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts @@ -62,3 +62,50 @@ export function gaussPDF(x: number, mu?: number, sig2?: number) { Math.exp((-(mu - x) * (mu - x)) / sig2 / 2) / Math.sqrt(2 * Math.PI * sig2) ) } + +// Manually cached multinomial distribution. +// Example: sigma([2, 3, 0, 0], 5) +// gives the probability (n1=2, n2=3, n3=0, n4=0) given N=5 total rolls. (uniform distribution is assumed for the four bins) +// `sigr` and `sig_arr` constitute a near perfect hash of all combinations for N=1 to N=5. +// This function has undefined behavior for N > 5 and N = 0 +// prettier-ignore +const sig_arr = [270 / 1024, 80 / 1024, 0, 12 / 256, 8 / 256, 120 / 1024, 0, 60 / 1024, 4 / 256, 60 / 1024, 4 / 256, 30 / 1024, 24 / 256, 160 / 1024, 1 / 64, 1 / 64, 24 / 256, 1 / 64, 12 / 256, 0, 6 / 256, 2 / 16, 6 / 256, 0, 81 / 256, 16 / 256, 0, 27 / 64, 12 / 64, 0, 1 / 16, 1 / 16, 12 / 64, 1 / 16, 6 / 64, 3 / 4, 2 / 4, 243 / 1024, 32 / 1024, 0, 108 / 256, 32 / 256, 0, 9 / 64, 6 / 64, 48 / 256, 0, 24 / 256, 3 / 64, 5 / 1024, 3 / 64, 5 / 1024, 0, 405 / 1024, 80 / 1024, 0, 54 / 256, 90 / 1024, 40 / 1024, 0, 1 / 256, 1 / 256, 40 / 1024, 1 / 256, 20 / 1024, 9 / 16, 4 / 16, 0, 1 / 4, 1 / 4, 0, 1 / 4, 27 / 64, 8 / 64, 0, 6 / 16, 4 / 16, 10 / 1024, 0, 10 / 1024, 2 / 16, 0, 0, 0, 15 / 1024, 10 / 1024, 1 / 1024, 1 / 1024, 0, 1 / 1024] +const sigr = [35, 64, 70, 21, 33, 45, 12, 0, 53, 76, 48, 86] +function sigma(ss: number[], N: number) { + const ssum = ss.reduce((a, b) => a + b) + if (ss.length > 4 || ssum > N) return 0 + if (ss.length === 4 && ssum !== N) return 0 + if (ss.length === 3) ss = [...ss, N - ssum] + ss.sort().reverse() + + // t = 12 + // offset = -14 + let v = 13 * N + ss.length - 14 + 16 * ss[0] + if (ss.length > 1) v += 4 * ss[1] + const x = v % 12 + const y = Math.trunc(v / 12) // integer divide + + return sig_arr[x + sigr[y]] +} + +/** Crawl the upgrade distribution for `n` upgrades, with a callback function that accepts fn([n1, n2, n3, n4], prob) */ +export function crawlUpgrades( + n: number, + fn: (n1234: number[], p: number) => void +) { + if (n === 0) { + fn([0, 0, 0, 0], 1) + return + } + + // Binomial(n+3, 3) branches to crawl. + for (let i1 = n; i1 >= 0; i1--) { + for (let i2 = n - i1; i2 >= 0; i2--) { + for (let i3 = n - i1 - i2; i3 >= 0; i3--) { + const i4 = n - i1 - i2 - i3 + const p_comb = sigma([i1, i2, i3, i4], n) + fn([i1, i2, i3, i4], p_comb) + } + } + } +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index d52a970bc5..df4ba70ae0 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -13,7 +13,8 @@ import Artifact, { maxArtifactLevel } from '../../../../Data/Artifacts/Artifact' import type { MainStatKey, SubstatKey } from '@genshin-optimizer/dm' import { gaussianPE, mvnPE_bad } from './mvncdf' -import { crawlUpgrades } from './artifactUpgradeCrawl' +import { crawlUpgrades, quadrinomial } from './mathUtil' +import { cartesian, range } from '../../../../Util/Util' /** * Artifact upgrade distribution math summary. @@ -469,6 +470,64 @@ export class UpOptCalculator { this.artifacts[ix].result = this.toResult2(distrs) } + + toResult3(distr: { prob: number; val: number[] }[]): UpOptResult { + let ptot = 0 + let upAvgtot = 0 + const gmm = distr.map(({ prob, val }) => { + if (val.every((vi, i) => vi >= this.thresholds[i])) { + ptot += prob + upAvgtot += prob * (val[0] - this.thresholds[0]) + return { phi: prob, cp: 1, mu: val[0], sig2: 0 } + } + return { phi: prob, cp: 0, mu: val[0], sig2: 0 } + }) + + const vals = gmm.map(({ mu }) => mu) + return { + p: ptot, + upAvg: ptot < 1e-6 ? 0 : upAvgtot / ptot, + distr: { gmm, lower: Math.min(...vals), upper: Math.max(...vals) }, + evalMode: ResultType.Exact, + } + } + + _calcExact(ix: number) { + const { subs, slotKey, rollsLeft } = this.artifacts[ix] + const N = rollsLeft - (subs.length < 4 ? 1 : 0) // only for 5* + + const distrs: { prob: number; val: number[] }[] = [] + crawlUpgrades(N, (ns, prob) => { + const base = { ...this.artifacts[ix].values } + const vals = ns.map((ni, i) => + subs[i] && !this.skippableDerivatives[allSubstatKeys.indexOf(subs[i])] + ? range(7 * ni, 10 * ni) + : [NaN] + ) + + cartesian(...vals).forEach((upVals) => { + const stats = { ...base } + let p_upVals = 1 + for (let i = 0; i < 4; i++) { + if (isNaN(upVals[i])) continue + + const key = subs[i] + const val = upVals[i] + const ni = ns[i] + stats[key] = (stats[key] ?? 0) + val * scale(key) + const p_val = 4 ** -ni * quadrinomial(ni, val - 7 * ni) + p_upVals *= p_val + } + + distrs.push({ + prob: prob * p_upVals, + val: this.eval(stats, slotKey).map((n) => n.v), + }) + }) + }) + + this.artifacts[ix].result = this.toResult3(distrs) + } } /* ICachedArtifact to ArtifactBuildData. Maybe this should go in common? */ From dbfb86b8e98555c0c8e07dde57a99cf4443ef82c Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 17:33:59 -0400 Subject: [PATCH 11/38] explain some magic numbers --- .../Tabs/TabUpgradeOpt/mvncdf.ts | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts index 92ec3e0657..472a9c131a 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts @@ -1,7 +1,7 @@ import { erf } from './mathUtil' // import { Module } from "wasmpack/assembly.js"; -// From a Gaussian mean & variance, get P(x > mu) and E[x | x > mu] +/** From a 1D Gaussian mean & variance, get P(x > mu) and E[x | x > mu] */ export function gaussianPE( mean: number, variance: number, @@ -14,22 +14,21 @@ export function gaussianPE( const z = (x - mean) / Math.sqrt(variance) const p = (1 - erf(z / Math.sqrt(2))) / 2 - if (z > 5) { - // Z-score large means p will be very small. - // We can use taylor expansion at infinity to evaluate upAvg. - const y = 1 / z, - y2 = y * y - return { - p: p, - upAvg: Math.sqrt(variance) * y * (1 - 2 * y2 * (1 - y2 * (5 + 37 * y2))), - } - } - const phi = Math.exp((-z * z) / 2) / Math.sqrt(2 * Math.PI) - return { p: p, upAvg: mean - x + (Math.sqrt(variance) * phi) / p } + + const y2 = 1 / (z * z) + // When z is small, p and phi are both nonzero so (phi/p - z) is ok. + // When p and phi are both small, we can take use the Taylor expansion + // of (phi/p - z) at y=1/z=0. Using 7th order expansion to ensure + // upAvg is continuous at z=5. + const ppz = z < 5 ? phi / p - z : (1 - 2 * y2 * (1 - y2 * (5 + 37 * y2))) / z + return { p, upAvg: Math.sqrt(variance) * ppz } } -// From a multivariate Gaussian mean & variance, get P(x > mu) and E[x0 | x > mu] +/** + * From a multivariate Gaussian mean & covariance, get P(x > mu) and E[x0 | x > mu] + * Javascript implementation ignores the cov off-diagonals (assumes all components are independent). + */ export function mvnPE_bad(mu: number[], cov: number[][], x: number[]) { // TODO: an implementation without using the independence assumption let ptot = 1 @@ -47,19 +46,16 @@ export function mvnPE_bad(mu: number[], cov: number[][], x: number[]) { if (i !== 0) cptot *= p } - // Naive 1st moment of truncated distribution: assume it's relatively stationary w.r.t. the - // constraints. If the constraints greatly affects the moment, then its associated - // conditional probability should also be small. Therefore in conjunction with the summation - // method in `gmmNd()`, the overall approximation should be fairly good, even if the individual - // upAvg terms may be very bad. - // Appears to work well in practice. - // - // More rigorous methods for estimating 1st moment of truncated multivariate distribution exist. - // https://www.cesarerobotti.com/wp-content/uploads/2019/04/JCGS-KR.pdf const { upAvg } = gaussianPE(mu[0], cov[0][0], x[0]) return { p: ptot, upAvg: upAvg, cp: cptot } } +/** + * From a multivariate Gaussian mean & covariance, get P(x > mu) and E[x0 | x > mu] + * This is implemented in Fortran, but it does take into account the off-diagonals. + * + * TODO: re-implement the WebAssembly bridge. + */ // export function mvnPE_good(mu: number[], cov: number[][], x: number[]) { // let mvn: any = new Module.MVNHandle(mu.length); // try { From 853cb771a9cbeef6864461fb7092c3e4f937c996 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 17:58:28 -0400 Subject: [PATCH 12/38] sigma is so scuffed lmao --- .../Tabs/TabUpgradeOpt/mathUtil.ts | 18 ++++++++++-------- .../Tabs/TabUpgradeOpt/mvncdf.ts | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts index 329d0467ab..0d3cb8963c 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mathUtil.ts @@ -1,5 +1,5 @@ -// https://oeis.org/A008287 -// step 1: a basic LUT with a few steps of Pascal's triangle +// `quadronomial` coefficients (See https://oeis.org/A008287) +// step 1: a basic lookup-table with a few steps of Pascal's triangle const quadrinomials = [ [1], [1, 1, 1, 1], @@ -9,7 +9,7 @@ const quadrinomials = [ [1, 5, 15, 35, 65, 101, 135, 155, 155, 135, 101, 65, 35, 15, 5, 1], ] -// step 2: a function that builds out the LUT if it needs to. +// step 2: a function that builds out the lookup-table if it needs to. export function quadrinomial(n: number, k: number) { while (n >= quadrinomials.length) { const s = quadrinomials.length @@ -63,14 +63,16 @@ export function gaussPDF(x: number, mu?: number, sig2?: number) { ) } -// Manually cached multinomial distribution. -// Example: sigma([2, 3, 0, 0], 5) -// gives the probability (n1=2, n2=3, n3=0, n4=0) given N=5 total rolls. (uniform distribution is assumed for the four bins) -// `sigr` and `sig_arr` constitute a near perfect hash of all combinations for N=1 to N=5. -// This function has undefined behavior for N > 5 and N = 0 +// `sigr` and `sig_arr` constitute a near perfect hash (https://en.wikipedia.org/wiki/Perfect_hash_function) of all combinations for N=1 to N=5. // prettier-ignore const sig_arr = [270 / 1024, 80 / 1024, 0, 12 / 256, 8 / 256, 120 / 1024, 0, 60 / 1024, 4 / 256, 60 / 1024, 4 / 256, 30 / 1024, 24 / 256, 160 / 1024, 1 / 64, 1 / 64, 24 / 256, 1 / 64, 12 / 256, 0, 6 / 256, 2 / 16, 6 / 256, 0, 81 / 256, 16 / 256, 0, 27 / 64, 12 / 64, 0, 1 / 16, 1 / 16, 12 / 64, 1 / 16, 6 / 64, 3 / 4, 2 / 4, 243 / 1024, 32 / 1024, 0, 108 / 256, 32 / 256, 0, 9 / 64, 6 / 64, 48 / 256, 0, 24 / 256, 3 / 64, 5 / 1024, 3 / 64, 5 / 1024, 0, 405 / 1024, 80 / 1024, 0, 54 / 256, 90 / 1024, 40 / 1024, 0, 1 / 256, 1 / 256, 40 / 1024, 1 / 256, 20 / 1024, 9 / 16, 4 / 16, 0, 1 / 4, 1 / 4, 0, 1 / 4, 27 / 64, 8 / 64, 0, 6 / 16, 4 / 16, 10 / 1024, 0, 10 / 1024, 2 / 16, 0, 0, 0, 15 / 1024, 10 / 1024, 1 / 1024, 1 / 1024, 0, 1 / 1024] const sigr = [35, 64, 70, 21, 33, 45, 12, 0, 53, 76, 48, 86] +/** + * Manually cached multinomial distribution with uniform bins. Returns probability of (n1, n2, n3, n4) given N total rolls. + * Algebraically = N! / (n1! n2! n3! n4!) * (1/4)^N + * + * WARNING: This function has undefined behavior for N > 5 and N = 0 + */ function sigma(ss: number[], N: number) { const ssum = ss.reduce((a, b) => a + b) if (ss.length > 4 || ssum > N) return 0 diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts index 472a9c131a..2e2797c009 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/mvncdf.ts @@ -1,4 +1,4 @@ -import { erf } from './mathUtil' +import { erf, gaussPDF } from './mathUtil' // import { Module } from "wasmpack/assembly.js"; /** From a 1D Gaussian mean & variance, get P(x > mu) and E[x | x > mu] */ @@ -14,7 +14,7 @@ export function gaussianPE( const z = (x - mean) / Math.sqrt(variance) const p = (1 - erf(z / Math.sqrt(2))) / 2 - const phi = Math.exp((-z * z) / 2) / Math.sqrt(2 * Math.PI) + const phi = gaussPDF(z) const y2 = 1 / (z * z) // When z is small, p and phi are both nonzero so (phi/p - z) is ok. From 40a6d5c8ce2c0760e1c76185edc528f8e7f5cb1b Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sat, 24 Jun 2023 18:18:37 -0400 Subject: [PATCH 13/38] format differentiate --- .../frontend/src/app/Formula/differentiate.ts | 145 ++++++++++++------ 1 file changed, 95 insertions(+), 50 deletions(-) diff --git a/apps/frontend/src/app/Formula/differentiate.ts b/apps/frontend/src/app/Formula/differentiate.ts index 3e3ea4191e..6cca19dbf6 100644 --- a/apps/frontend/src/app/Formula/differentiate.ts +++ b/apps/frontend/src/app/Formula/differentiate.ts @@ -1,78 +1,123 @@ -import { assertUnreachable } from '../Util/Util'; -import { forEachNodes } from './internal'; -import { constant, sum, prod, threshold, frac, max, min } from './utils'; -import type { ReadNode } from './type'; -import type { OptNode } from './optimization'; +import { assertUnreachable } from '../Util/Util' +import { forEachNodes } from './internal' +import { constant, sum, prod, threshold, frac, max, min } from './utils' +import type { ReadNode } from './type' +import type { OptNode } from './optimization' -export function zero_deriv(funct: OptNode, binding: (readNode: ReadNode) => string, diff: string): boolean { - let ret = true; +export function zero_deriv( + funct: OptNode, + binding: (readNode: ReadNode) => string, + diff: string +): boolean { + let ret = true // eslint-disable-next-line @typescript-eslint/no-empty-function - forEachNodes([funct], (_) => { }, (f) => { - const { operation } = f; - switch (operation) { - case 'read': - if (f.type !== 'number' || (f.accu && f.accu !== 'add')) - throw new Error(`Unsupported [${operation}] node in zero_deriv`); - if (binding(f) === diff) ret = false; + forEachNodes( + [funct], + (_) => {}, + (f) => { + const { operation } = f + switch (operation) { + case 'read': + if (f.type !== 'number' || (f.accu && f.accu !== 'add')) + throw new Error(`Unsupported [${operation}] node in zero_deriv`) + if (binding(f) === diff) ret = false + } } - }); - return ret; + ) + return ret } -export function ddx(f: OptNode, binding: (readNode: ReadNode) => string, diff: string): OptNode { - const { operation } = f; +export function ddx( + f: OptNode, + binding: (readNode: ReadNode) => string, + diff: string +): OptNode { + const { operation } = f switch (operation) { case 'read': { if (f.type !== 'number' || (f.accu && f.accu !== 'add')) - throw new Error(`Unsupported [${operation}] node in d/dx`); - const name = binding(f); - if (name === diff) return constant(1); - return constant(0); + throw new Error(`Unsupported [${operation}] node in d/dx`) + const name = binding(f) + if (name === diff) return constant(1) + return constant(0) } - case 'const': return constant(0); + case 'const': + return constant(0) case 'res': if (!zero_deriv(f, binding, diff)) - throw new Error(`[${operation}] node takes only constant inputs. ${f}`); - return constant(0); + throw new Error(`[${operation}] node takes only constant inputs. ${f}`) + return constant(0) case 'add': - return sum(...f.operands.map((fi) => ddx(fi, binding, diff))); + return sum(...f.operands.map((fi) => ddx(fi, binding, diff))) case 'mul': { - const ops = f.operands.map((fi, i) => prod(ddx(fi, binding, diff), ...f.operands.filter((v, ix) => ix !== i))); - return sum(...ops); + const ops = f.operands.map((fi, i) => + prod(ddx(fi, binding, diff), ...f.operands.filter((v, ix) => ix !== i)) + ) + return sum(...ops) } case 'sum_frac': { - const a = f.operands[0]; - const da = ddx(a, binding, diff); - const b = sum(...f.operands.slice(1)); - const db = ddx(b, binding, diff); - const denom = prod(sum(...f.operands), sum(...f.operands)); - const numerator = sum(prod(b, da), prod(-1, a, db)); - return frac(numerator, sum(prod(-1, numerator), denom)); + const a = f.operands[0] + const da = ddx(a, binding, diff) + const b = sum(...f.operands.slice(1)) + const db = ddx(b, binding, diff) + const denom = prod(sum(...f.operands), sum(...f.operands)) + const numerator = sum(prod(b, da), prod(-1, a, db)) + return frac(numerator, sum(prod(-1, numerator), denom)) } - case 'min': case 'max': { - if (f.operands.length === 1) return ddx(f.operands[0], binding, diff); + case 'min': + case 'max': { + if (f.operands.length === 1) return ddx(f.operands[0], binding, diff) else if (f.operands.length === 2) { - const [arg1, arg2] = f.operands; - if (operation === 'min') return threshold(arg1, arg2, ddx(arg2, binding, diff), ddx(arg1, binding, diff)); - if (operation === 'max') return threshold(arg1, arg2, ddx(arg1, binding, diff), ddx(arg2, binding, diff)); - assertUnreachable(operation); - break; + const [arg1, arg2] = f.operands + if (operation === 'min') + return threshold( + arg1, + arg2, + ddx(arg2, binding, diff), + ddx(arg1, binding, diff) + ) + if (operation === 'max') + return threshold( + arg1, + arg2, + ddx(arg1, binding, diff), + ddx(arg2, binding, diff) + ) + assertUnreachable(operation) + break } else { - if (operation === 'min') return ddx(min(f.operands[0], min(...f.operands.slice(1))), binding, diff); - if (operation === 'max') return ddx(max(f.operands[0], max(...f.operands.slice(1))), binding, diff); - assertUnreachable(operation); - break; + if (operation === 'min') + return ddx( + min(f.operands[0], min(...f.operands.slice(1))), + binding, + diff + ) + if (operation === 'max') + return ddx( + max(f.operands[0], max(...f.operands.slice(1))), + binding, + diff + ) + assertUnreachable(operation) + break } } case 'threshold': { - const [value, thr, pass, fail] = f.operands; + const [value, thr, pass, fail] = f.operands if (!zero_deriv(value, binding, diff) || !zero_deriv(thr, binding, diff)) - throw new Error(`[${operation}] node must branch on constant inputs. ${f}`); - return threshold(value, thr, ddx(pass, binding, diff), ddx(fail, binding, diff)); + throw new Error( + `[${operation}] node must branch on constant inputs. ${f}` + ) + return threshold( + value, + thr, + ddx(pass, binding, diff), + ddx(fail, binding, diff) + ) } default: - assertUnreachable(operation); + assertUnreachable(operation) } } From 2533f88240811ebdc7d571096e307c0b949c7e82 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Thu, 29 Jun 2023 00:09:09 -0400 Subject: [PATCH 14/38] fix (?) import errors --- .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 11 +++---- .../Tabs/TabUpgradeOpt/index.tsx | 31 +++++++++---------- .../Tabs/TabUpgradeOpt/upOpt.ts | 25 ++++++++------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx index 9c828d00e0..f46507cefc 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -8,7 +8,7 @@ import React, { } from 'react' import { DatabaseContext } from '../../../../Database/Database' import { DataContext } from '../../../../Context/DataContext' -import Assets from '../../../../Assets/Assets' +import { imgAssets } from '@genshin-optimizer/g-assets' import type { TooltipProps } from 'recharts' import { Line, @@ -81,10 +81,9 @@ export default function UpgradeOptChartCard({ const { data } = useContext(DataContext) const artifacts = useMemo( () => - allArtifactSlotKeys.map((k) => [ - k, - database.arts.get(data.get(input.art[k].id).value ?? ''), - ]), + allArtifactSlotKeys.map((k) => { + return [k, database.arts.get(input.art[k].id)] + }), [data, database] ) as Array<[ArtifactSlotKey, ICachedArtifact | undefined]> @@ -349,7 +348,7 @@ export default function UpgradeOptChartCard({ opacity: 0.7, }} component="img" - src={Assets.slot[sk]} + src={imgAssets.slot[sk]} /> } sx={{ minWidth: 0 }} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx index fc27ac4517..eed1485405 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx @@ -45,7 +45,8 @@ import useCharSelectionCallback from '../../../../ReactHooks/useCharSelectionCal import useTeamData, { getTeamData } from '../../../../ReactHooks/useTeamData' import useBuildSetting from '../TabOptimize/useBuildSetting' import { dynamicData } from '../TabOptimize/foreground' -import { clamp, objectKeyMap, objPathValue } from '../../../../Util/Util' +import { objPathValue } from '../../../../Util/Util' +import { clamp } from '@genshin-optimizer/util' import { mergeData, uiDataForTeam } from '../../../../Formula/api' import UpgradeOptChartCard from './UpgradeOptChartCard' import MainStatSelectionCard from '../TabOptimize/Components/MainStatSelectionCard' @@ -58,7 +59,7 @@ import { allArtifactSlotKeys, charKeyToLocCharKey, } from '@genshin-optimizer/consts' -import useForceUpdate from '../../../../ReactHooks/useForceUpdate' +import { useForceUpdate } from '@genshin-optimizer/react-util' import type { UpOptBuild } from './upOpt' import { UpOptCalculator, toArtifact } from './upOpt' @@ -124,11 +125,7 @@ export default function TabUpopt() { }) }, [database, characterKey, deferredArtsDirty, deferredBuildSetting]) const filteredArtIdMap = useMemo( - () => - objectKeyMap( - filteredArts.map(({ id }) => id), - (_) => true - ), + () => Object.fromEntries(filteredArts.map(({ id }) => [id, true])), [filteredArts] ) @@ -151,6 +148,7 @@ export default function TabUpopt() { return { artifactsToShow: [], numPages: 0, + currentPageIndex: 0, toShow: 0, minObj0: 0, maxObj0: 0, @@ -236,17 +234,18 @@ export default function TabUpopt() { const equippedArts = database.chars.get(characterKey)?.equippedArtifacts ?? ({} as StrictDict) - const curEquip: UpOptBuild = objectKeyMap( - allArtifactSlotKeys, - (slotKey) => { + const curEquip: UpOptBuild = Object.fromEntries( + allArtifactSlotKeys.map((slotKey) => { const art = database.arts.get(equippedArts[slotKey] ?? '') - return art ? toArtifact(art) : undefined - } + return [slotKey, art ? toArtifact(art) : undefined] + }) + ) as UpOptBuild + const curEquipSetKeys = Object.fromEntries( + allArtifactSlotKeys.map((slotKey) => { + const art = database.arts.get(equippedArts[slotKey] ?? '') + return [slotKey, art?.setKey ?? ''] + }) ) - const curEquipSetKeys = objectKeyMap(allArtifactSlotKeys, (slotKey) => { - const art = database.arts.get(equippedArts[slotKey] ?? '') - return art?.setKey ?? '' - }) function respectSexExclusion(art: ICachedArtifact) { const newSK = { ...curEquipSetKeys } newSK[art.slotKey] = art.setKey diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index df4ba70ae0..a24bce5cc9 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -6,15 +6,18 @@ import { } from '../../../../Formula/optimization' import type { ICachedArtifact } from '../../../../Types/artifact' -import { allSubstatKeys } from '../../../../Types/artifact' +import { allSubstatKeys, artMaxLevel } from '@genshin-optimizer/consts' import { ddx, zero_deriv } from '../../../../Formula/differentiate' import type { ArtifactBuildData, DynStat } from '../../../../Solver/common' -import Artifact, { maxArtifactLevel } from '../../../../Data/Artifacts/Artifact' +import { + getSubstatValue, + getRollsRemaining, + getMainStatDisplayValue, +} from '@genshin-optimizer/gi-util' import type { MainStatKey, SubstatKey } from '@genshin-optimizer/dm' - import { gaussianPE, mvnPE_bad } from './mvncdf' import { crawlUpgrades, quadrinomial } from './mathUtil' -import { cartesian, range } from '../../../../Util/Util' +import { cartesian, range } from '@genshin-optimizer/util' /** * Artifact upgrade distribution math summary. @@ -123,7 +126,8 @@ const fWeight: StrictDict = { /* Gets "0.1x" 1 roll value for a stat w/ the given rarity. */ function scale(key: SubstatKey, rarity: RarityKey = 5) { - return toDecimal(key, Artifact.substatValue(key, rarity)) / 10 + return getSubstatValue(key, rarity) / 10 + // return toDecimal(key, Artifact.substatValue(key, rarity)) / 10 } /* Fixes silliness with percents and being multiplied by 100. */ @@ -193,8 +197,8 @@ export class UpOptCalculator { /** Adds an artifact to be tracked by UpOptCalc. It is initially un-evaluated. */ addArtifact(art: ICachedArtifact) { - const maxLevel = maxArtifactLevel[art.rarity] - const mainStatVal = Artifact.mainStatValue( + const maxLevel = artMaxLevel[art.rarity] + const mainStatVal = getMainStatDisplayValue( art.mainStatKey, art.rarity, maxLevel @@ -202,7 +206,7 @@ export class UpOptCalculator { this.artifacts.push({ id: art.id, - rollsLeft: Artifact.rollsRemaining(art.level, art.rarity), + rollsLeft: getRollsRemaining(art.level, art.rarity), slotKey: art.slotKey, mainStat: art.mainStatKey, subs: art.substats @@ -532,10 +536,9 @@ export class UpOptCalculator { /* ICachedArtifact to ArtifactBuildData. Maybe this should go in common? */ export function toArtifact(art: ICachedArtifact): ArtifactBuildData { - const mainStatVal = Artifact.mainStatValue( + const mainStatVal = toDecimal( art.mainStatKey, - art.rarity, - art.level + getMainStatDisplayValue(art.mainStatKey, art.rarity, art.level) ) // 5* only const buildData = { id: art.id, From e3cfdb2dcb64e2920becd46fe1147e8b0bc4977e Mon Sep 17 00:00:00 2001 From: frzyc Date: Sun, 2 Jul 2023 04:50:21 -0400 Subject: [PATCH 15/38] fix and refactor code --- .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 53 +++++++------------ libs/util/src/lib/array.spec.ts | 7 +++ libs/util/src/lib/array.ts | 10 ++++ 3 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 libs/util/src/lib/array.spec.ts diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx index f46507cefc..0c6155eace 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -1,33 +1,28 @@ -import { Button, CardContent, Grid, Box } from '@mui/material' -import React, { - useEffect, - useState, - useContext, - useMemo, - useCallback, -} from 'react' -import { DatabaseContext } from '../../../../Database/Database' -import { DataContext } from '../../../../Context/DataContext' -import { imgAssets } from '@genshin-optimizer/g-assets' +import type { ArtifactSlotKey } from '@genshin-optimizer/consts' +import { allArtifactSlotKeys } from '@genshin-optimizer/consts' +import { imgAssets } from '@genshin-optimizer/gi-assets' +import { linspace } from '@genshin-optimizer/util' +import { Box, Button, CardContent, Grid } from '@mui/material' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import type { TooltipProps } from 'recharts' import { - Line, Area, ComposedChart, + Label, Legend, - ReferenceLine, + Line, ReferenceDot, + ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, - Label, } from 'recharts' +import ArtifactCardPico from '../../../../Components/Artifact/ArtifactCardPico' import CardLight from '../../../../Components/Card/CardLight' +import { DataContext } from '../../../../Context/DataContext' +import { DatabaseContext } from '../../../../Database/Database' import { uiInput as input } from '../../../../Formula' -import ArtifactCardPico from '../../../../Components/Artifact/ArtifactCardPico' -import type { ArtifactSlotKey } from '@genshin-optimizer/consts' -import { allArtifactSlotKeys } from '@genshin-optimizer/consts' import type { ICachedArtifact } from '../../../../Types/artifact' import { gaussPDF } from './mathUtil' import type { UpOptArtifact } from './upOpt' @@ -49,16 +44,6 @@ type ChartData = { expInc?: number } -// linspace with non-inclusive endpoint. -function linspace(lower = 0, upper = 1, steps = 50): number[] { - const arr: number[] = [] - const step = (upper - lower) / steps - for (let i = 0; i < steps; ++i) { - arr.push(lower + i * step) - } - return arr -} - const nbins = 50 const plotPoints = 500 export default function UpgradeOptChartCard({ @@ -82,7 +67,7 @@ export default function UpgradeOptChartCard({ const artifacts = useMemo( () => allArtifactSlotKeys.map((k) => { - return [k, database.arts.get(input.art[k].id)] + return [k, database.arts.get(data.get(input.art[k].id).value)] }), [data, database] ) as Array<[ArtifactSlotKey, ICachedArtifact | undefined]> @@ -106,11 +91,13 @@ export default function UpgradeOptChartCard({ const maax = objMax let ymax = 0 - const dataEst: ChartData[] = linspace(miin, maax, plotPoints).map((v) => { - const est = gauss(v) - ymax = Math.max(ymax, est) - return { x: perc(v), est: est, estCons: gaussConstrained(v) } - }) + const dataEst: ChartData[] = linspace(miin, maax, plotPoints, false).map( + (v) => { + const est = gauss(v) + ymax = Math.max(ymax, est) + return { x: perc(v), est: est, estCons: gaussConstrained(v) } + } + ) if (ymax === 0) ymax = nbins / (maax - miin) // go back and add delta distributions. diff --git a/libs/util/src/lib/array.spec.ts b/libs/util/src/lib/array.spec.ts new file mode 100644 index 0000000000..bcde89b7b4 --- /dev/null +++ b/libs/util/src/lib/array.spec.ts @@ -0,0 +1,7 @@ +import { linspace } from './array' +describe('test @genshin_optimizer/util/array', function () { + it('test linspace', () => { + expect(linspace(1, 2, 3)).toEqual([1, 1.5, 2]) + expect(linspace(1, 5, 4, false)).toEqual([1, 2, 3, 4]) + }) +}) diff --git a/libs/util/src/lib/array.ts b/libs/util/src/lib/array.ts index 5238663b49..b94ad07bd2 100644 --- a/libs/util/src/lib/array.ts +++ b/libs/util/src/lib/array.ts @@ -42,3 +42,13 @@ export function arrayMove(arr: T[], oldIndex: number, newIndex: number) { export function transposeArray(arr: T[][]): T[][] { return arr[0].map((_, i) => arr.map((row) => row[i])) } + +export function linspace( + start: number, + stop: number, + num: number, + inclusiveEnd = true +) { + const step = (stop - start) / (inclusiveEnd ? num - 1 : num) + return range(0, num - 1).map((i) => start + step * i) +} From cdebb8c3fec3c66668a979b24118e196174e6130 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Sun, 2 Jul 2023 12:45:58 -0400 Subject: [PATCH 16/38] finish implementing calcExact --- .../Tabs/TabUpgradeOpt/upOpt.ts | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index a24bce5cc9..12f8017608 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -400,7 +400,11 @@ export class UpOptCalculator { } calcSlow(ix: number, calc4th = true) { - if (this.artifacts[ix].result?.evalMode === ResultType.Slow) return + if ( + this.artifacts[ix].result?.evalMode === ResultType.Slow || + this.artifacts[ix].result?.evalMode === ResultType.Exact + ) + return if (this.artifacts[ix].subs.length === 4) calc4th = false if (calc4th) this._calcSlow4th(ix) else this._calcSlow(ix) @@ -496,6 +500,13 @@ export class UpOptCalculator { } } + calcExact(ix: number, calc4th = true) { + if (this.artifacts[ix].result?.evalMode === ResultType.Exact) return + if (this.artifacts[ix].subs.length === 4) calc4th = false + if (calc4th) this._calcExact4th(ix) + else this._calcExact(ix) + } + _calcExact(ix: number) { const { subs, slotKey, rollsLeft } = this.artifacts[ix] const N = rollsLeft - (subs.length < 4 ? 1 : 0) // only for 5* @@ -532,6 +543,51 @@ export class UpOptCalculator { this.artifacts[ix].result = this.toResult3(distrs) } + + _calcExact4th(ix: number) { + const { mainStat, subs, slotKey, rollsLeft } = this.artifacts[ix] + const N = rollsLeft - 1 // only for 5* + + const subsToConsider = allSubstatKeys.filter( + (s) => !subs.includes(s) && s !== mainStat + ) + const Z = subsToConsider.reduce((tot, sub) => tot + fWeight[sub], 0) + const distrs: { prob: number; val: number[] }[] = [] + subsToConsider.forEach((subKey4) => { + const prob_sub = fWeight[subKey4] / Z + crawlUpgrades(N, (ns, prob) => { + const base = { ...this.artifacts[ix].values } + ns[3] += 1 // last substat has initial roll + const vals = ns.map((ni, i) => + subs[i] && !this.skippableDerivatives[allSubstatKeys.indexOf(subs[i])] + ? range(7 * ni, 10 * ni) + : [NaN] + ) + + cartesian(...vals).forEach((upVals) => { + const stats = { ...base } + let p_upVals = 1 + for (let i = 0; i < 4; i++) { + if (isNaN(upVals[i])) continue + + const key = subs[i] + const val = upVals[i] + const ni = ns[i] + stats[key] = (stats[key] ?? 0) + val * scale(key) + const p_val = 4 ** -ni * quadrinomial(ni, val - 7 * ni) + p_upVals *= p_val + } + + distrs.push({ + prob: prob_sub * prob * p_upVals, + val: this.eval(stats, slotKey).map((n) => n.v), + }) + }) + }) + }) + + this.artifacts[ix].result = this.toResult3(distrs) + } } /* ICachedArtifact to ArtifactBuildData. Maybe this should go in common? */ From 0353bc5a1a53e37c9b46e91052f45a1586d2a279 Mon Sep 17 00:00:00 2001 From: frzyc Date: Mon, 3 Jul 2023 04:07:18 -0400 Subject: [PATCH 17/38] update page layout --- .../src/app/Components/AddArtInfo.tsx | 14 + .../src/app/Components/NoArtWarning.tsx | 15 + apps/frontend/src/app/PageArtifact/index.tsx | 14 +- .../Tabs/TabOptimize/index.tsx | 14 +- .../Tabs/TabUpgradeOpt/index.tsx | 526 ++++++++---------- 5 files changed, 275 insertions(+), 308 deletions(-) create mode 100644 apps/frontend/src/app/Components/AddArtInfo.tsx create mode 100644 apps/frontend/src/app/Components/NoArtWarning.tsx diff --git a/apps/frontend/src/app/Components/AddArtInfo.tsx b/apps/frontend/src/app/Components/AddArtInfo.tsx new file mode 100644 index 0000000000..7d154eccb0 --- /dev/null +++ b/apps/frontend/src/app/Components/AddArtInfo.tsx @@ -0,0 +1,14 @@ +import { Alert, Link } from '@mui/material' +import { Link as RouterLink } from 'react-router-dom' + +export default function AddArtInfo() { + return ( + + Looks like you haven't added any artifacts yet. If you want, there are + + automatic scanners + + that can speed up the import process! + + ) +} diff --git a/apps/frontend/src/app/Components/NoArtWarning.tsx b/apps/frontend/src/app/Components/NoArtWarning.tsx new file mode 100644 index 0000000000..ba7a37eb86 --- /dev/null +++ b/apps/frontend/src/app/Components/NoArtWarning.tsx @@ -0,0 +1,15 @@ +import { Alert, Link } from '@mui/material' +import { Link as RouterLink } from 'react-router-dom' + +export default function NoArtWarning() { + return ( + + Opps! It looks like you haven't added any artifacts to GO yet! You should + go to the{' '} + + Artifacts + {' '} + page and add some! + + ) +} diff --git a/apps/frontend/src/app/PageArtifact/index.tsx b/apps/frontend/src/app/PageArtifact/index.tsx index b1a6d82c6b..da3baa33aa 100644 --- a/apps/frontend/src/app/PageArtifact/index.tsx +++ b/apps/frontend/src/app/PageArtifact/index.tsx @@ -8,12 +8,10 @@ import { clamp, filterFunction, sortFunction } from '@genshin-optimizer/util' import { Add } from '@mui/icons-material' import DifferenceIcon from '@mui/icons-material/Difference' import { - Alert, Box, Button, CardContent, Grid, - Link, Pagination, Skeleton, Typography, @@ -30,7 +28,7 @@ import React, { } from 'react' import ReactGA from 'react-ga4' import { Trans, useTranslation } from 'react-i18next' -import { Link as RouterLink } from 'react-router-dom' +import AddArtInfo from '../Components/AddArtInfo' import SubstatToggle from '../Components/Artifact/SubstatToggle' import BootstrapTooltip from '../Components/BootstrapTooltip' import CardDark from '../Components/Card/CardDark' @@ -197,15 +195,7 @@ export default function PageArtifact() { - {noArtifact && ( - - Looks like you haven't added any artifacts yet. If you want, there are - - automatic scanners - - that can speed up the import process! - - )} + {noArtifact && } `#${index + 1}`, []) return ( - {noArtifact && ( - - - Oops! It looks like you haven't added any artifacts to GO yet! You - should go to the - - Artifacts - - page and add some! - - - )} + {noArtifact && } {/* Build Generator Editor */} {dataContext && ( diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx index eed1485405..229e5cb481 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx @@ -1,30 +1,33 @@ -import { Upgrade, CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material' +import { CheckBox, CheckBoxOutlineBlank, Upgrade } from '@mui/icons-material' import { - Alert, Box, Button, CardContent, Grid, - Link, + Pagination, Skeleton, Typography, - Pagination, } from '@mui/material' -import { Link as RouterLink } from 'react-router-dom' -import CardDark from '../../../../Components/Card/CardDark' import CardLight from '../../../../Components/Card/CardLight' import CharacterCard from '../../../../Components/Character/CharacterCard' -import StatFilterCard from '../TabOptimize/Components/StatFilterCard' -import ArtifactCard from '../../../../PageArtifact/ArtifactCard' -import BonusStatsCard from '../TabOptimize/Components/BonusStatsCard' import { HitModeToggle, ReactionToggle, } from '../../../../Components/HitModeEditor' -import OptimizationTargetSelector from '../TabOptimize/Components/OptimizationTargetSelector' -import ArtifactSetConfig from '../TabOptimize/Components/ArtifactSetConfig' +import ArtifactCard from '../../../../PageArtifact/ArtifactCard' import useDBMeta from '../../../../ReactHooks/useDBMeta' +import ArtifactSetConfig from '../TabOptimize/Components/ArtifactSetConfig' +import BonusStatsCard from '../TabOptimize/Components/BonusStatsCard' +import OptimizationTargetSelector from '../TabOptimize/Components/OptimizationTargetSelector' +import StatFilterCard from '../TabOptimize/Components/StatFilterCard' +import type { ArtifactSlotKey, CharacterKey } from '@genshin-optimizer/consts' +import { + allArtifactSlotKeys, + charKeyToLocCharKey, +} from '@genshin-optimizer/consts' +import { useForceUpdate } from '@genshin-optimizer/react-util' +import { clamp } from '@genshin-optimizer/util' import { Suspense, useCallback, @@ -32,37 +35,32 @@ import { useDeferredValue, useEffect, useMemo, - useRef, useState, } from 'react' import { Trans } from 'react-i18next' +import ArtifactLevelSlider from '../../../../Components/Artifact/ArtifactLevelSlider' +import { CharacterContext } from '../../../../Context/CharacterContext' import type { dataContextObj } from '../../../../Context/DataContext' import { DataContext } from '../../../../Context/DataContext' import { DatabaseContext } from '../../../../Database/Database' +import { mergeData, uiDataForTeam } from '../../../../Formula/api' import { optimize } from '../../../../Formula/optimization' import type { NumNode } from '../../../../Formula/type' import useCharSelectionCallback from '../../../../ReactHooks/useCharSelectionCallback' import useTeamData, { getTeamData } from '../../../../ReactHooks/useTeamData' -import useBuildSetting from '../TabOptimize/useBuildSetting' -import { dynamicData } from '../TabOptimize/foreground' +import type { DynStat } from '../../../../Solver/common' +import type { ICachedArtifact } from '../../../../Types/artifact' import { objPathValue } from '../../../../Util/Util' -import { clamp } from '@genshin-optimizer/util' -import { mergeData, uiDataForTeam } from '../../../../Formula/api' -import UpgradeOptChartCard from './UpgradeOptChartCard' import MainStatSelectionCard from '../TabOptimize/Components/MainStatSelectionCard' -import { CharacterContext } from '../../../../Context/CharacterContext' -import ArtifactLevelSlider from '../../../../Components/Artifact/ArtifactLevelSlider' -import type { ICachedArtifact } from '../../../../Types/artifact' -import type { DynStat } from '../../../../Solver/common' -import type { ArtifactSlotKey, CharacterKey } from '@genshin-optimizer/consts' -import { - allArtifactSlotKeys, - charKeyToLocCharKey, -} from '@genshin-optimizer/consts' -import { useForceUpdate } from '@genshin-optimizer/react-util' +import { dynamicData } from '../TabOptimize/foreground' +import useBuildSetting from '../TabOptimize/useBuildSetting' +import UpgradeOptChartCard from './UpgradeOptChartCard' +import { Stack } from '@mui/system' +import AddArtInfo from '../../../../Components/AddArtInfo' +import NoArtWarning from '../../../../Components/NoArtWarning' import type { UpOptBuild } from './upOpt' -import { UpOptCalculator, toArtifact } from './upOpt' +import { toArtifact, UpOptCalculator } from './upOpt' export default function TabUpopt() { const { @@ -139,7 +137,6 @@ export default function TabUpopt() { // Paging logic const [pageIdex, setpageIdex] = useState(0) - const invScrollRef = useRef(null) const artifactsToDisplayPerPage = 5 const { artifactsToShow, numPages, currentPageIndex, minObj0, maxObj0 } = @@ -181,7 +178,6 @@ export default function TabUpopt() { const setPage = useCallback( (e, value) => { if (!upOptCalc) return - invScrollRef.current?.scrollIntoView({ behavior: 'smooth' }) const end = value * artifactsToDisplayPerPage upOptCalc.calcSlowToIndex(end) setpageIdex(value - 1) @@ -335,280 +331,242 @@ export default function TabUpopt() { return data && teamData && { data, teamData } }, [data, teamData]) + const pagination = numPages > 1 && ( + + + + + + + + + + + + + ) + return ( - {noArtifact && ( - - {' '} - Opps! It looks like you haven't added any artifacts to GO yet! You - should go to the{' '} - - Artifacts - {' '} - page and add some! - - )} + {noArtifact && } {/* Build Generator Editor */} {dataContext && ( - - {/* 1*/} - - {/* character card */} - - - - - + + + {/* 1*/} + + {/* character card */} + + + + + - {/* 2 */} - - - - - - Optimization Target: - { - - buildSettingDispatch({ optimizationTarget: target }) - } - disabled={false} - /> - } - - - - - - - - {useFilters && ( + {/* 2 */} + + + - - Artifact Level Filter - - - buildSettingDispatch({ levelLow }) - } - setHigh={(levelHigh) => - buildSettingDispatch({ levelHigh }) - } - setBoth={(levelLow, levelHigh) => - buildSettingDispatch({ levelLow, levelHigh }) + + Optimization Target: + { + + buildSettingDispatch({ + optimizationTarget: target, + }) + } + disabled={false} + /> } - disabled={false} - /> + + + - + - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {numPages > 1 && ( - - - - - - - - + + Artifact Level Filter + + + buildSettingDispatch({ levelLow }) + } + setHigh={(levelHigh) => + buildSettingDispatch({ levelHigh }) + } + setBoth={(levelLow, levelHigh) => + buildSettingDispatch({ levelLow, levelHigh }) + } + disabled={false} + /> + + - - - - - )} - - - - {noArtifact && ( - - Looks like you haven't added any artifacts yet. If you - want, there are{' '} - - automatic scanners - {' '} - that can speed up the import process! - + + )} - - } - > - {/* */} - {artifactsToShow.map((art) => ( - - - + + + + + + + + + + + + + + + - - + + - ))} - {/* */} - + + - - {numPages > 1 && ( - - - - - - - - - - - - - )} - + + + + + + + + + + + + + + + + {pagination} + {noArtifact && } + + } + > + {artifactsToShow.map((art) => ( + + + + + + + + + + + ))} + + {pagination} + )} From 7a824fe8d89e13f4a0e6a6db81e91dff3558ab95 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Mon, 3 Jul 2023 17:25:58 -0400 Subject: [PATCH 18/38] fix wacky percents --- .../Tabs/TabUpgradeOpt/upOpt.ts | 11 +- libs/gi-stats/Data/Materials/material.json | 167 +++++++++++++++++- 2 files changed, 167 insertions(+), 11 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts index 12f8017608..6a0862425e 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/upOpt.ts @@ -126,8 +126,7 @@ const fWeight: StrictDict = { /* Gets "0.1x" 1 roll value for a stat w/ the given rarity. */ function scale(key: SubstatKey, rarity: RarityKey = 5) { - return getSubstatValue(key, rarity) / 10 - // return toDecimal(key, Artifact.substatValue(key, rarity)) / 10 + return toDecimal(key, getSubstatValue(key, rarity) / 10) } /* Fixes silliness with percents and being multiplied by 100. */ @@ -603,16 +602,12 @@ export function toArtifact(art: ICachedArtifact): ArtifactBuildData { rarity: art.rarity, values: { [art.setKey]: 1, - [art.mainStatKey]: art.mainStatKey.endsWith('_') - ? mainStatVal / 100 - : mainStatVal, + [art.mainStatKey]: mainStatVal, ...Object.fromEntries( art.substats .map((substat) => [ substat.key, - substat.key.endsWith('_') - ? substat.accurateValue / 100 - : substat.accurateValue, + toDecimal(substat.key, substat.accurateValue), ]) .filter(([, value]) => value !== 0) ), diff --git a/libs/gi-stats/Data/Materials/material.json b/libs/gi-stats/Data/Materials/material.json index 9ad5a75b88..483962b7b5 100644 --- a/libs/gi-stats/Data/Materials/material.json +++ b/libs/gi-stats/Data/Materials/material.json @@ -59,6 +59,8 @@ "GalaExcitement": {}, "FestiveTicket": {}, "InvokationCoupons": {}, + "JoyeuxVoucher": {}, + "FascinatingPhenocryst": {}, "Primogem": { "type": "MATERIAL_ADSORBATE" }, @@ -231,10 +233,10 @@ "type": "MATERIAL_GCG_CARD_FACE" }, "KaedeharaKazuha": { - "type": "MATERIAL_AVATAR" + "type": "MATERIAL_GCG_CARD_FACE" }, "Yanfei": { - "type": "MATERIAL_AVATAR" + "type": "MATERIAL_GCG_CARD_FACE" }, "Yoimiya": { "type": "MATERIAL_GCG_CARD_FACE" @@ -306,7 +308,7 @@ "type": "MATERIAL_GCG_CARD_FACE" }, "Candace": { - "type": "MATERIAL_AVATAR" + "type": "MATERIAL_GCG_CARD_FACE" }, "Nahida": { "type": "MATERIAL_GCG_CARD_FACE" @@ -4594,6 +4596,9 @@ "PlumeOfPurifyingLight": { "type": "MATERIAL_CONSUME" }, + "VoucherBox": { + "type": "MATERIAL_CONSUME" + }, "Dish": { "type": "MATERIAL_FOOD" }, @@ -7912,6 +7917,18 @@ "WarriorsSpiritSwordfightingArenaTicket": { "type": "MATERIAL_QUEST" }, + "ScatteredYetWellPreservedPagesI": { + "type": "MATERIAL_QUEST" + }, + "ScatteredYetWellPreservedPagesII": { + "type": "MATERIAL_QUEST" + }, + "ScatteredYetWellPreservedPagesIII": { + "type": "MATERIAL_QUEST" + }, + "ScatteredYetWellPreservedPagesIV": { + "type": "MATERIAL_QUEST" + }, "VentisNote": { "type": "MATERIAL_QUEST" }, @@ -8002,6 +8019,33 @@ "KorybantesScoreVedana": { "type": "MATERIAL_QUEST" }, + "ReturningCuriosMementoI": { + "type": "MATERIAL_QUEST" + }, + "ReturningCuriosMementoII": { + "type": "MATERIAL_QUEST" + }, + "ReturningCuriosMementoIII": { + "type": "MATERIAL_QUEST" + }, + "MementoCollectionBox": { + "type": "MATERIAL_QUEST" + }, + "MysteriousMoraPocketwatch": { + "type": "MATERIAL_QUEST" + }, + "MuralFragmentSwirlingClouds": { + "type": "MATERIAL_QUEST" + }, + "MuralFragmentCentralHub": { + "type": "MATERIAL_QUEST" + }, + "MuralFragmentSkyKeep": { + "type": "MATERIAL_QUEST" + }, + "MuralFragmentFlowerOfTheValley": { + "type": "MATERIAL_QUEST" + }, "JuliensDrawingBoard": { "type": "MATERIAL_QUEST" }, @@ -8011,6 +8055,18 @@ "HaniyyahsGiftOfMarvelousJellies": { "type": "MATERIAL_QUEST" }, + "OldGear": { + "type": "MATERIAL_QUEST" + }, + "MountaineeringRope": { + "type": "MATERIAL_QUEST" + }, + "FamiliarLookingCandyBox": { + "type": "MATERIAL_QUEST" + }, + "TravelLamp": { + "type": "MATERIAL_QUEST" + }, "BoxOfSpecialPotions": { "type": "MATERIAL_QUEST" }, @@ -8029,6 +8085,15 @@ "KingOfInvokations": { "type": "MATERIAL_QUEST" }, + "KeyToTheCastlesSecretChamber": { + "type": "MATERIAL_QUEST" + }, + "AncientHexCleansingRemedy": { + "type": "MATERIAL_QUEST" + }, + "TheGreatEscapePropBook": { + "type": "MATERIAL_QUEST" + }, "NamelessAdventurersNotes": { "type": "MATERIAL_QUEST" }, @@ -8059,6 +8124,12 @@ "EmberglowLeaf": { "type": "MATERIAL_QUEST" }, + "SadifesMarkedMap": { + "type": "MATERIAL_QUEST" + }, + "SadifesCase": { + "type": "MATERIAL_QUEST" + }, "AkademiyaInvestigationTeamsLogsI": { "type": "MATERIAL_QUEST" }, @@ -8110,6 +8181,18 @@ "KhvarenaMonumentInscription": { "type": "MATERIAL_QUEST" }, + "BeautifullyWrappedGiftBox": { + "type": "MATERIAL_QUEST" + }, + "RipeGrapes": { + "type": "MATERIAL_QUEST" + }, + "PrinceQubadsIntaglio": { + "type": "MATERIAL_QUEST" + }, + "AbsolutionSeekersTestament": { + "type": "MATERIAL_QUEST" + }, "SilkFlowerSeed": { "type": "MATERIAL_HOME_SEED" }, @@ -9325,6 +9408,9 @@ "TravelNotesSecrets": { "type": "MATERIAL_NAMECARD" }, + "TravelNotesVividIllumination": { + "type": "MATERIAL_NAMECARD" + }, "AnemoculusResonanceStone": { "type": "MATERIAL_WIDGET" }, @@ -9526,6 +9612,15 @@ "SearchCompass": { "type": "MATERIAL_WIDGET" }, + "FlowingJoyspar": { + "type": "MATERIAL_WIDGET" + }, + "FelicitousJoyspar": { + "type": "MATERIAL_WIDGET" + }, + "InscribedMirror": { + "type": "MATERIAL_WIDGET" + }, "InstructionsAnemoculusResonanceStone": { "type": "MATERIAL_CONSUME" }, @@ -9850,6 +9945,15 @@ "EmbersRekindled": { "type": "MATERIAL_GCG_CARD_FACE" }, + "TheOverflow": { + "type": "MATERIAL_GCG_CARD_FACE" + }, + "RightOfFinalInterpretation": { + "type": "MATERIAL_GCG_CARD_FACE" + }, + "PoeticsOfFuubutsu": { + "type": "MATERIAL_GCG_CARD_FACE" + }, "MagicGuide": { "type": "MATERIAL_GCG_CARD_FACE" }, @@ -10006,6 +10110,9 @@ "ShimenawasReminiscence": { "type": "MATERIAL_GCG_CARD_FACE" }, + "FruitOfFulfillment": { + "type": "MATERIAL_GCG_CARD_FACE" + }, "LiyueHarborWharf": { "type": "MATERIAL_GCG_CARD_FACE" }, @@ -10096,6 +10203,9 @@ "Rana": { "type": "MATERIAL_GCG_CARD_FACE" }, + "MasterZhang": { + "type": "MATERIAL_GCG_CARD_FACE" + }, "ElementalResonanceWovenIce": { "type": "MATERIAL_GCG_CARD_FACE" }, @@ -10210,6 +10320,15 @@ "FriendshipEternal": { "type": "MATERIAL_GCG_CARD_FACE" }, + "AncientCourtyard": { + "type": "MATERIAL_GCG_CARD_FACE" + }, + "CovenantOfRock": { + "type": "MATERIAL_GCG_CARD_FACE" + }, + "RhythmOfTheGreatDream": { + "type": "MATERIAL_GCG_CARD_FACE" + }, "Origin": { "type": "MATERIAL_GCG_CARD_BACK" }, @@ -10288,6 +10407,12 @@ "ASobriquetUnderShade": { "type": "MATERIAL_COSTUME" }, + "BlossomingStarlight": { + "type": "MATERIAL_COSTUME" + }, + "SailwindShadow": { + "type": "MATERIAL_COSTUME" + }, "AdventurerCamp": { "type": "MATERIAL_FURNITURE_SUITE_FORMULA" }, @@ -10621,6 +10746,12 @@ "LightsConstancy": { "type": "MATERIAL_FURNITURE_FORMULA" }, + "ShiningSpotlight": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "GlowingAmusementParkCandyLamp": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, "EfflorescentIllumination": { "type": "MATERIAL_FURNITURE_FORMULA" }, @@ -11488,6 +11619,18 @@ "DodocosSummertime": { "type": "MATERIAL_FURNITURE_FORMULA" }, + "ChooChooCartRestStop": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "SampleAmusementParkEngineering": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "SampleAmusementParkBuilding": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "AmusementParkCelebrationTent": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, "RitualShrine": { "type": "MATERIAL_FURNITURE_FORMULA" }, @@ -12079,6 +12222,18 @@ "LeisureDevicePuffyTubby": { "type": "MATERIAL_FURNITURE_FORMULA" }, + "ScriptsRequirementsAndPrinciples": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "VIPGuestBleachers": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "HeyHaHooSlimeBoxTrio": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, + "WindUpGreatOwlSpiritMechanism": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, "PottedPlantVerdantVastness": { "type": "MATERIAL_FURNITURE_FORMULA" }, @@ -12316,6 +12471,9 @@ "KshahrewarFlag": { "type": "MATERIAL_FURNITURE_FORMULA" }, + "AmusementParkBroadcastTower": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, "WroughtIronCarvedStreetLight": { "type": "MATERIAL_FURNITURE_FORMULA" }, @@ -12421,6 +12579,9 @@ "PearOrchardSeatGatheredInJoy": { "type": "MATERIAL_FURNITURE_FORMULA" }, + "DeadlineWhatDeadlineDirectorsChair": { + "type": "MATERIAL_FURNITURE_FORMULA" + }, "TwoStoryHilichurlSentryTower": { "type": "MATERIAL_FURNITURE_FORMULA" }, From 9703477d47e5571ca88b9571527c9a518a0b1d21 Mon Sep 17 00:00:00 2001 From: tooflesswulf Date: Mon, 3 Jul 2023 21:06:04 -0400 Subject: [PATCH 19/38] clicking button calls calcExact (UI doesnt update tho) --- .../TabUpgradeOpt/UpgradeOptChartCard.tsx | 26 +++++++++++++------ .../Tabs/TabUpgradeOpt/index.tsx | 7 ++++- .../Tabs/TabUpgradeOpt/upOpt.ts | 5 ++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx index 0c6155eace..bb85519803 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -34,6 +34,7 @@ type Data = { objMax: number thresholds: number[] ix?: number + calcExactCallback: () => void } type ChartData = { x: number @@ -51,6 +52,7 @@ export default function UpgradeOptChartCard({ thresholds, objMin, objMax, + calcExactCallback, }: Data) { const [calcExacts, setCalcExacts] = useState(false) @@ -109,13 +111,15 @@ export default function UpgradeOptChartCard({ deltasConstrained[mu] = (deltasConstrained[mu] ?? 0) + phi * cp } }) - Object.entries(deltas).forEach(([mu, p]) => + Object.entries(deltas).forEach(([mu, p]) => { + const mun = parseFloat(mu) dataEst.push({ - x: perc(parseFloat(mu)), - est: (p * nbins) / (maax - miin), - estCons: (deltasConstrained[mu] * nbins) / (maax - miin), + x: perc(mun), + est: (p * nbins) / (maax - miin) + gauss(mun), + estCons: + (deltasConstrained[mu] * nbins) / (maax - miin) + gaussConstrained(mun), }) - ) + }) dataEst.sort((a, b) => a.x - b.x) const xpercent = (thr0 - miin) / (maax - miin) @@ -125,10 +129,13 @@ export default function UpgradeOptChartCard({ const [trueE, setTrueE] = useState(-1) useEffect(() => { + // calcExactCallback() + return + // When `calcExacts` is pressed, we may want to sink/swim this artifact to its proper spot. // Or not b/c people only really need a fuzzy ordering anyways. - if (!calcExacts) return - throw new Error('Not Implemented!') + // if (!calcExacts) return + // throw new Error('Not Implemented!') setTrueData([]) setTrueP(0) setTrueE(0) @@ -322,7 +329,10 @@ export default function UpgradeOptChartCard({