Skip to content

Conversation

@tooflesswulf
Copy link
Collaborator

@tooflesswulf tooflesswulf commented Sep 27, 2025

This PR is intended to merge the math in first without connecting anything to UI yet.

Also, please check my config files. I don't know what I'm doing.

Theoretical Idea

Restructures artifact statistics code to be more general & support requested features like reshaping / defining / unactivated substats. We set up an "artifact statsistic query" as a tree of probability nodes with edge weights describing the probability of a branch & node values being the expected function evaluation at that node.

The artifact upgrade tree is sectioned into 3 "layers" which I'm calling SubstatLevel, RollsLevel, and ValuesLevel. The children of a SubstatLevel node is a sequence of RollsLevel nodes, and the children of the Rolls level are the Values level. Intuitively, we can think of a Substat node as a selection of substats [atk, def%, critRate%, hp] and its children as distributing the a number of rolls into each substat. For example:

[atk,   def%,   critRate%,   hp] @ 5 rolls
   v     v          v          v
[2 atk, 2x def%, 1x critRate%, 0x hp], [0x atk, 2x def%, 3x critRate%, 0x hp], ...

Then the children of the Rolls node assigns actual roll values to each substat depending on how many rolls each stat should have. Example:

[0 atk, 0def%, 3CR, 2hp ] --> [0 atk, 0def%, 24RV=9.58CR, 17RV=507.9hp]

So each node defines a distribution through the distribution of its children, and the distribution of the "root" node is the distribution of the possible upgrade outcomes of an artifact or process. However, because the child distribution of the Substat and Rolls nodes are known, we can estimate the mean and variance of these nodes without having to expand any of their children. This allows us to estimate the statistics of a node directly, shortcutting the need to evaluate its children.

In practice, I intend initial distribution of a "root" node to just be its direct children (a list of Substat nodes). The root node's statistics can be estimated by evaluating at each substat node; think of the initial tree as a 1-depth tree whose leaves are all of the root's children. Then the root's estimated distribution can be refined by expanding a Substat node into a sequence of Rolls nodes or Rolls into Values; generally the "root"s value will be a sum over its leaves.

Description of code

This library is centered on the probability nodes SubstatLevelNode, RollsLevelNode and ValuesLevelNode. The objective function is encoded in an Objective type.

Each node type has supporting code to estimate its stat distribution (mean & covariance). Using this Gaussian approximation, we can evaluate the Objective on each node to shortcut its children. Tests were written to ensure that the Gaussian's mean and covariance do actually match the child nodes' mean and covariance, as well as many intermediate steps along the way.

Exposed API (Not finished)

I intend to expose the following functions that the UI code can call:

Tree construction functions in upOpt.ts, for the different types of artifact statistics queries. levelUp is the current upOpt feature. freshArt considers a new artifact from a Domain or a Strongbox. dustReshape and elixirDefinition are the reshaping and crafting features, respectively.

levelUpArtifact(art: ICachedArtifact, currentBuild: Build): weightedSubstatNode[]
freshArtifact(artSets, prob3sub, rarity, currentBuild): weightedSubstatNode[]
dustReshape(art: ICachedArtifact, currentBuild: build): weightedSubstatNode[]
elixirDefinition(setKey, slotKey, mainStatKey, affixes, currentBuild): weightedSubstatNode[]

Node estimation and expansion:

evaluation.ts - evalMarkovNode(Objective, Node): EvaluatedNode
expandRolls.ts - expandRollsLevel(RollsNode): weightedValuesNode[]
expandSubstat.ts - expandSubstatLevel(SubstatNode): weightedRollsNode[]

Issue or discord link

Testing/validation

Tests written:
makeObjective: Test linear & nonlinear objective functions, ensure the function & its derivatives are computed correctly.
evaluateGaussianDegen: Test EvaluateGaussian on a 0-variance Gaussian (essentially same as previous)
evaluateGaussian: Test EvaluateGaussian with non-zero covariance returns correct values
expandSubstatReshape: Check that expandSubstat when reshape affixes are specified returns the correct distribution of rolls.
expandRolls: expand a Rolls node and ensure that evaluateGaussian exactly matches between the node and its children for a linear objective.
expandSubstat: expand a Substat node and ensure that evaluateGaussian exactly matches between the node and its children for a linear objective.
expandSubstatFancy: Same as above but with baseRolls and reshape affixes
deduplicate: Combine functionally identical child nodes (e.g. when two nodes have different substats, but neither substat appears in objective)
substatProbs: Check that substat probability function matches my spreadsheet values

Checklist before requesting a review (leave this PR as draft if any part of this list is not done.)

  • I have commented my code in hard-to understand areas.
  • I have made corresponding changes to README or wiki.
  • For front-end changes, I have updated the corresponding English translations.
  • I have run yarn run mini-ci locally to validate format and lint.
  • If I have added a new library or app, I have updated the deployment scripts to ignore changes as needed

Summary by CodeRabbit

  • New Features
    • Added comprehensive artifact optimization engine for Genshin Impact, enabling probabilistic analysis of artifact enhancement outcomes.
    • Introduced support for multiple artifact acquisition methods: leveling existing artifacts, obtaining fresh artifacts, reshaping via special materials, and creating fixed-stat artifacts.
    • Added probabilistic calculations for substat combinations and roll distributions to optimize artifact selection strategies.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Caution

Review failed

The head commit changed during the review from b5127c0 to d61c73c.

Walkthrough

Introduces a new @genshin-optimizer/gi/upopt library implementing probabilistic artifact optimization for Genshin Impact. Adds core modules for expanding substat configurations, computing roll distributions, evaluating Gaussian mixture models, and deduplicating Markov nodes. Includes type system, mathematical utilities, and high-level API for artifact leveling scenarios.

Changes

Cohort / File(s) Summary
Project Configuration
.eslintrc.json, project.json, tsconfig*.json, vite.config.ts
Establishes ESLint, TypeScript, Vite, and Nx project configuration for the new gi-upopt library with test runner setup and coverage reporting
Type System
src/upOpt.types.ts, src/markov-tree/markov.types.ts
Defines union types for Markov nodes (Substat/Rolls/Values levels), their evaluated variants, and core types for Gaussian distributions and optimization objectives
Mathematical Utilities
src/markov-tree/mathUtil.ts, src/markov-tree/mvncdf.ts
Implements quadrinomial coefficients, error function approximation, Gaussian PDF, factorial caching, roll probability distributions, and multivariate Gaussian probability estimation
Constants & Weights
src/consts.ts
Exports substat weight mappings and main stat probability distributions per artifact type (Flower, Plume, Sands, Goblet, Circlet)
Core Algorithms
src/expandSubstat.ts, src/expandRolls.ts, src/substatProbs.ts
Implements probabilistic expansion of substat configurations with reshaping support, roll value distributions across substats, and combinatorial substat probability crawling
Post-Processing
src/deduplicate.ts, src/markov-tree/evaluation.ts, src/markov-tree/makeObjective.ts
Deduplicates and simplifies Markov nodes, evaluates Gaussian nodes under optimization objectives, and constructs objective functions with derivative tracking
Public API
src/upOpt.ts
Exports four artifact simulation scenarios: levelUpArtifact, freshArtifact, dustReshape, elixirDefinition, each returning weighted substat node distributions
Test Suite
src/upOpt.test.ts, src/markov-tree/mathUtil.test.ts
Comprehensive tests for expansion, evaluation, deduplication, objective construction, and mathematical utilities with correctness verification across all major components
Module Alias
tsconfig.base.json
Registers @genshin-optimizer/gi/upopt path alias mapping to libs/gi/upopt/src/index.ts

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant upOpt as upOpt.ts
    participant expandSub as expandSubstat
    participant expandRolls as expandRolls
    participant subProbs as substatProbs
    participant dedup as deduplicate
    participant eval as evaluation

    User->>upOpt: levelUpArtifact(artifact, build)
    upOpt->>subProbs: crawlSubstats(prefix, options)
    subProbs-->>upOpt: [{p, subs}, ...]
    upOpt->>expandSub: makeSubstatNode(info)
    expandSub-->>upOpt: SubstatLevelNode
    loop For each SubstatLevelNode
        upOpt->>expandSub: expandSubstatLevel(node)
        expandSub-->>upOpt: [{p, n: RollsLevelNode}, ...]
        upOpt->>expandRolls: expandRollsLevel(rollsNode)
        expandRolls-->>upOpt: [{p, n: ValuesLevelNode}, ...]
    end
    upOpt->>dedup: deduplicate(nodes)
    dedup-->>upOpt: [deduplicated nodes]
    upOpt->>eval: evalMarkovNode(obj, node)
    eval-->>upOpt: EvaluatedMarkovNode[]
    upOpt-->>User: [evaluated, weighted nodes]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Rationale: This introduces a substantial, multi-layered probabilistic optimization framework spanning 20+ files across configuration, type system, mathematical utilities, and algorithm modules. The core logic density is high—roll distribution expansion, Gaussian covariance propagation, Markov node deduplication, and statistical re-weighting each demand careful verification. While individual algorithms follow recognizable patterns (combinatorial crawling, matrix operations, node traversal), their composition and correctness guarantees require understanding the entire pipeline. The comprehensive test suite aids review but heterogeneous mathematical logic, multiple node types, and edge cases (reshaping constraints, zero-derivative pruning, probability normalization) across files elevate complexity.

Suggested reviewers

  • nguyentvan7
  • frzyc

Poem

🎮 A thousand rolls await the faithful,
Gaussian dreams of perfect stats,
Markov chains guide the artifact quest,
Deduplicate the noise—optimize the best,
Sleep is for the weak, luck is for the blessed 💤✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "UpOpt math code in gi/upopt library" accurately reflects the primary change in this changeset, which introduces a new gi/upopt library containing the mathematical foundation for artifact optimization. The title identifies both the subject matter (UpOpt math code) and the location (gi/upopt library), making it clear to reviewers what this PR fundamentally addresses. While the title could be more specific about the three-layer probability tree model or Gaussian approximation concepts, it conveys sufficient information about the main contribution without being vague or misleading.
Description Check ✅ Passed The PR description is comprehensive and substantially complete, covering the required sections from the template with additional technical depth. It includes a general summary ("This PR is intended to merge the math in first..."), a "Theoretical Idea" section that explains the three-layer probability tree model with clear intuition and examples, a "Description of code" section detailing node types and Gaussian approximations, an "Exposed API" section describing intended functions, a detailed "Testing/validation" section listing test coverage, and the required checklist. The only notable gaps are that the "Issue or discord link" section is empty (no link provided) and the checklist items remain unchecked; however, these are non-critical omissions for a draft PR intended to receive feedback on the implementation before addressing review items.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 27, 2025

[zzz-frontend] [Sat Sep 27 14:29:18 UTC 2025] - Deployed e4ef3ba to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 14:29:27 UTC 2025] - Deployed e4ef3ba to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 14:29:50 UTC 2025] - Deployed e4ef3ba to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 14:30:36 UTC 2025] - Deployed 9acb58d to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 14:30:49 UTC 2025] - Deployed 9acb58d to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 14:30:59 UTC 2025] - Deployed 9acb58d to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 17:09:39 UTC 2025] - Deployed 2975e53 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 17:09:49 UTC 2025] - Deployed 2975e53 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 17:10:11 UTC 2025] - Deployed 2975e53 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 17:14:41 UTC 2025] - Deployed 61c0fb2 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 17:14:58 UTC 2025] - Deployed 61c0fb2 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 17:16:59 UTC 2025] - Deployed 61c0fb2 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 17:37:54 UTC 2025] - Deployed 3a2fc6b to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 17:38:48 UTC 2025] - Deployed 3a2fc6b to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 17:39:03 UTC 2025] - Deployed 3a2fc6b to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 17:50:58 UTC 2025] - Deployed d46ab0e to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 17:51:38 UTC 2025] - Deployed d46ab0e to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 17:51:53 UTC 2025] - Deployed d46ab0e to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 18:07:19 UTC 2025] - Deployed 658ba30 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 18:07:35 UTC 2025] - Deployed 658ba30 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 18:07:39 UTC 2025] - Deployed 658ba30 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 18:10:33 UTC 2025] - Deployed 25d72bd to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 18:10:37 UTC 2025] - Deployed 25d72bd to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 18:10:43 UTC 2025] - Deployed 25d72bd to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Sat Sep 27 19:20:25 UTC 2025] - Deployed 35c69d1 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Sat Sep 27 19:20:34 UTC 2025] - Deployed 35c69d1 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Sat Sep 27 19:20:38 UTC 2025] - Deployed 35c69d1 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Tue Sep 30 05:58:13 UTC 2025] - Deployed 06ee7ba to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Tue Sep 30 05:58:30 UTC 2025] - Deployed 06ee7ba to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Tue Sep 30 05:58:36 UTC 2025] - Deployed 06ee7ba to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Mon Oct 20 16:58:29 UTC 2025] - Deployed 1f2d75d to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Mon Oct 20 16:59:05 UTC 2025] - Deployed 1f2d75d to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Mon Oct 20 17:01:46 UTC 2025] - Deployed 1f2d75d to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Mon Oct 20 17:03:30 UTC 2025] - Deployed a380135 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Mon Oct 20 17:03:56 UTC 2025] - Deployed a380135 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Mon Oct 20 17:04:13 UTC 2025] - Deployed a380135 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Mon Oct 27 19:51:52 UTC 2025] - Deployed b912c64 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Mon Oct 27 19:52:04 UTC 2025] - Deployed b912c64 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Mon Oct 27 19:52:23 UTC 2025] - Deployed b912c64 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Mon Oct 27 19:55:02 UTC 2025] - Deployed 36eff67 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Mon Oct 27 19:55:14 UTC 2025] - Deployed 36eff67 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Mon Oct 27 19:55:26 UTC 2025] - Deployed 36eff67 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[zzz-frontend] [Mon Oct 27 19:56:07 UTC 2025] - Deployed 8aa24e2 to https://genshin-optimizer-prs.github.io/pr/3069/zzz-frontend (Takes 3-5 minutes after this completes to be available)

[frontend] [Mon Oct 27 19:56:16 UTC 2025] - Deployed 8aa24e2 to https://genshin-optimizer-prs.github.io/pr/3069/frontend (Takes 3-5 minutes after this completes to be available)

[sr-frontend] [Mon Oct 27 19:57:51 UTC 2025] - Deployed 8aa24e2 to https://genshin-optimizer-prs.github.io/pr/3069/sr-frontend (Takes 3-5 minutes after this completes to be available)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
libs/gi/upopt/src/mathUtil.test.ts (1)

53-56: Prefer a non-mutating numeric sort for the key.

It's 3 AM, the banner is dry, and seeing in-place lexical sort makes my eyelid twitch—let’s keep the original tuple untouched and make the ordering explicit.

-    const strKey = ns.sort().join('')
+    const strKey = [...ns].sort((a, b) => a - b).join('')
libs/gi/upopt/src/makeObjective.ts (1)

44-46: Reuse the cached nonzero-derivative lookup.

Running zero_deriv again for every substat is like rolling a 10-pull after hitting pity—unnecessary pain. We already have nonzeroDerivs, so lean on it instead.

-    zeroDeriv: allSubstatKeys.filter((s) =>
-      nodes.every((n) => zero_deriv(n, (f) => f.path[1], s))
-    ),
+    zeroDeriv: allSubstatKeys.filter((s) =>
+      nonzeroDerivs.every((subs) => !subs.includes(s))
+    ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9cdba and f46ce4f.

📒 Files selected for processing (21)
  • libs/gi/upopt/.eslintrc.json (1 hunks)
  • libs/gi/upopt/project.json (1 hunks)
  • libs/gi/upopt/src/consts.ts (1 hunks)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/evaluation.ts (1 hunks)
  • libs/gi/upopt/src/expandRolls.ts (1 hunks)
  • libs/gi/upopt/src/expandSubstat.ts (1 hunks)
  • libs/gi/upopt/src/makeObjective.ts (1 hunks)
  • libs/gi/upopt/src/mathUtil.test.ts (1 hunks)
  • libs/gi/upopt/src/mathUtil.ts (1 hunks)
  • libs/gi/upopt/src/mvncdf.ts (1 hunks)
  • libs/gi/upopt/src/substatProbs.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.types.ts (1 hunks)
  • libs/gi/upopt/tsconfig.eslint.json (1 hunks)
  • libs/gi/upopt/tsconfig.json (1 hunks)
  • libs/gi/upopt/tsconfig.lib.json (1 hunks)
  • libs/gi/upopt/tsconfig.spec.json (1 hunks)
  • libs/gi/upopt/vite.config.ts (1 hunks)
  • tsconfig.base.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
libs/gi/upopt/src/upOpt.test.ts (9)
libs/gi/upopt/src/upOpt.types.ts (4)
  • Objective (16-20)
  • MarkovNode (41-41)
  • GaussianNode (22-27)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/evaluation.ts (2)
  • evalMarkovNode (77-85)
  • evaluateGaussian (16-59)
libs/gi/upopt/src/makeObjective.ts (1)
  • makeObjective (13-48)
libs/gi/upopt/src/expandSubstat.ts (3)
  • expandSubstatLevel (8-33)
  • makeRollsNode (98-116)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-37)
libs/gi/upopt/src/deduplicate.ts (1)
  • deduplicate (16-34)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/mvncdf.ts (1)
libs/gi/upopt/src/mathUtil.ts (2)
  • erf (33-53)
  • gaussPDF (56-64)
libs/gi/upopt/src/substatProbs.ts (2)
libs/gi/consts/src/artifact.ts (1)
  • SubstatKey (169-169)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/mathUtil.test.ts (1)
libs/gi/upopt/src/mathUtil.ts (6)
  • quadrinomial (23-27)
  • factorial (68-71)
  • gaussPDF (56-64)
  • erf (33-53)
  • crawlUpgrades (171-186)
  • rollCountProb (87-121)
libs/gi/upopt/src/expandSubstat.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
libs/gi/upopt/src/mathUtil.ts (4)
  • crawlUpgrades (171-186)
  • rollCountProb (87-121)
  • diag (73-75)
  • rollCountMuVar (124-168)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • ArtifactRarity (173-173)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/expandRolls.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/common/util/src/lib/array.ts (2)
  • range (14-16)
  • cartesian (27-31)
libs/gi/upopt/src/mathUtil.ts (1)
  • quadrinomial (23-27)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/upOpt.ts (7)
libs/gi/db/src/Interfaces/ICachedArtifact.ts (1)
  • ICachedArtifact (2-7)
libs/gi/upopt/src/expandSubstat.ts (1)
  • makeSubstatNode (125-175)
libs/gi/consts/src/artifact.ts (8)
  • allSubstatKeys (156-167)
  • ArtifactRarity (173-173)
  • allArtifactSlotKeys (64-70)
  • artSlotMainKeys (128-134)
  • SubstatKey (169-169)
  • ArtifactSlotKey (71-71)
  • MainStatKey (168-168)
  • artMaxLevel (73-79)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/consts.ts (1)
  • allMainStatProbs (60-66)
libs/gi/good/src/IArtifact.ts (1)
  • IArtifact (11-16)
libs/gi/upopt/src/upOpt.types.ts (1)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/evaluation.ts (2)
libs/gi/upopt/src/upOpt.types.ts (11)
  • Objective (16-20)
  • GaussianNode (22-27)
  • EvaluatedGaussian (29-39)
  • SubstatLevelNode (46-56)
  • EvaluatedSubstatNode (57-59)
  • RollsLevelNode (61-69)
  • EvaluatedRollsNode (70-72)
  • ValuesLevelNode (74-77)
  • EvaluatedValuesNode (78-80)
  • MarkovNode (41-41)
  • EvaluatedMarkovNode (42-45)
libs/gi/upopt/src/mvncdf.ts (1)
  • mvnPE_bad (37-57)
libs/gi/upopt/src/makeObjective.ts (3)
libs/gi/wr/src/optimization.ts (2)
  • OptNode (18-22)
  • precompute (72-152)
libs/gi/upopt/src/upOpt.types.ts (1)
  • Objective (16-20)
libs/gi/consts/src/artifact.ts (1)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/upOpt.types.ts (1)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • ArtifactRarity (173-173)
libs/gi/upopt/src/consts.ts (1)
libs/gi/consts/src/artifact.ts (1)
  • MainStatKey (168-168)
libs/gi/upopt/src/mathUtil.ts (1)
libs/common/util/src/lib/array.ts (1)
  • range (14-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint

{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix the test outDir path before it dumps artifacts in the wrong place

It's 3 a.m., the gacha banners reset in two hours, and this ../../dist/out-tsc is sending the compiled junk into libs/dist/out-tsc instead of the repo’s shared dist/out-tsc. Every other config in this library climbs three directories for a reason—please align this one or the build cache will never crit the way we need.

Apply this patch to point the outDir back to the workspace root:

-    "outDir": "../../dist/out-tsc",
+    "outDir": "../../../dist/out-tsc",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"outDir": "../../dist/out-tsc",
"outDir": "../../../dist/out-tsc",
🤖 Prompt for AI Agents
In libs/gi/upopt/tsconfig.spec.json around line 4, the "outDir" is set to
"../../dist/out-tsc" which places test build artifacts under libs/dist/out-tsc;
update the path to climb one more directory so artifacts go to the repo root
shared dist (change to "../../../dist/out-tsc"). Edit the JSON value for
"outDir" accordingly and save to ensure test outputs and build cache target the
workspace-level dist/out-tsc.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f46ce4f and f7598f4.

📒 Files selected for processing (4)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/expandRolls.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/gi/upopt/src/expandRolls.ts
🧰 Additional context used
🧬 Code graph analysis (3)
libs/gi/upopt/src/upOpt.test.ts (10)
libs/gi/upopt/src/upOpt.types.ts (4)
  • Objective (16-20)
  • MarkovNode (41-41)
  • GaussianNode (22-27)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/evaluation.ts (2)
  • evalMarkovNode (77-85)
  • evaluateGaussian (16-59)
libs/gi/upopt/src/makeObjective.ts (1)
  • makeObjective (13-48)
libs/gi/upopt/src/expandSubstat.ts (3)
  • expandSubstatLevel (8-33)
  • makeRollsNode (98-116)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/upopt/src/deduplicate.ts (1)
  • deduplicate (16-34)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/upOpt.ts (1)
  • expandNode (25-29)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/upOpt.ts (8)
libs/gi/upopt/src/upOpt.types.ts (2)
  • MarkovNode (41-41)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/expandSubstat.ts (2)
  • expandSubstatLevel (8-33)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/db/src/Interfaces/ICachedArtifact.ts (1)
  • ICachedArtifact (2-7)
libs/gi/consts/src/artifact.ts (8)
  • allSubstatKeys (156-167)
  • ArtifactRarity (173-173)
  • allArtifactSlotKeys (64-70)
  • artSlotMainKeys (128-134)
  • SubstatKey (169-169)
  • ArtifactSlotKey (71-71)
  • MainStatKey (168-168)
  • artMaxLevel (73-79)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/consts.ts (1)
  • allMainStatProbs (60-66)
libs/gi/good/src/IArtifact.ts (1)
  • IArtifact (11-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: call-build / build
  • GitHub Check: call-build / build
  • GitHub Check: typecheck
  • GitHub Check: gen-file
  • GitHub Check: test
  • GitHub Check: lint
🔇 Additional comments (2)
libs/gi/upopt/src/upOpt.ts (2)

38-48: Re-add the leveled artifact’s set piece to the base stats.

Running on three hours of sleep, I still see objectives mis-evaluating because the node we return never increments base[art.setKey]. That drops the current artifact’s set membership, so any 2pc/4pc bonus math goes out the window—exactly what I flagged earlier.

   const base = toStats(currentBuild, art)
   const { subkeys, stats } = getSubkeys(art)
   Object.entries(stats).forEach(([k, v]) => (base[k] = (base[k] ?? 0) + v))
   const rollsLeft = getRollsRemaining(art.level, art.rarity)
   const { rarity } = art
+  base[art.setKey] = (base[art.setKey] ?? 0) + 1

100-110: Fresh drops still forget their set tags.

Even my gacha-addled brain remembers the earlier bug: we ignore info.sets, so every generated node behaves like it came from no set at all. Set-bonus objectives will be catastrophically wrong. Please loop the offered sets, stamp the base with the set key, and split the probability.

-      crawlSubstats([], subsToConsider).forEach(({ p, subs }) => {
-        const nodeInfo = {
-          base,
-          rarity,
-          subkeys: subs.map((key) => ({ key, baseRolls: 1 })),
-          rollsLeft: getRollsRemaining(0, rarity),
-        }
-        const n = makeSubstatNode(nodeInfo)
-        out.push({ p: ((p * pMain) / 5) * (1 - p3sub), n })
+      crawlSubstats([], subsToConsider).forEach(({ p, subs }) => {
+        info.sets.forEach((setKey) => {
+          const baseWithSet = {
+            ...base,
+            [setKey]: (base[setKey] ?? 0) + 1,
+          }
+          const nodeInfo = {
+            base: baseWithSet,
+            rarity,
+            subkeys: subs.map((key) => ({ key, baseRolls: 1 })),
+            rollsLeft: getRollsRemaining(0, rarity),
+          }
+          const n = makeSubstatNode(nodeInfo)
+          out.push({
+            p: ((p * pMain) / 5) * (1 - p3sub) * (1 / info.sets.length),
+            n,
+          })
+        })
       })

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
libs/gi/upopt/src/deduplicate.ts (2)

24-34: Non-idiomatic descending sort; drop the negation for clarity

nodes.sort((a, b) => -cmpNodes(...)) is harder to reason about and risks future comparator changes. Ascending works the same for adjacency-based merging.

-  nodes.sort((a, b) => -cmpNodes(a.n, b.n))
+  nodes.sort((a, b) => cmpNodes(a.n, b.n))

20-23: Optional: clone input nodes in deduplicate before simplifying
Burned the midnight oil verifying call sites—none reuse the original objects, so you can safely prepend

nodes = nodes.map(({ p, n }) => ({ p, n: structuredClone(n) }))

to isolate in-place side-effects.

libs/gi/upopt/src/upOpt.test.ts (2)

95-96: Zero-dim covariance: prefer [] over [[]] for clarity

With no substats, cov is unused, but [] communicates 0×0 better than a jagged [[]].

-      cov: [[]],
+      cov: [],

Also applies to: 118-119


296-314: Good coverage on simplify-before-dedup path

This exercises the prune-then-compare logic. Once reshape-affix pruning is fixed in the deduper, consider adding a case where a reshaped affix is pruned to avoid regressions.

Happy to add a test that prunes a reshaped affix and asserts non-zero expansion mass. Want me to draft it?

libs/gi/upopt/src/upOpt.ts (1)

85-119: Proposed patch for 3-sub start (if you want it now)

Here’s a concrete approach assuming we expose crawlSubstatsK from substatProbs:

-import { crawlSubstats } from './substatProbs'
+import { crawlSubstats, crawlSubstatsK } from './substatProbs'
@@
-        crawlSubstats([], subsToConsider).forEach(({ p, subs }) => {
+        // 4-line starts
+        crawlSubstats([], subsToConsider).forEach(({ p, subs }) => {
           const nodeInfo = {
             base,
             rarity,
             subkeys: subs.map((key) => ({ key, baseRolls: 1 })),
             rollsLeft: getRollsRemaining(0, rarity),
           }
           const n = makeSubstatNode(nodeInfo)
           out.push({ p: p * pMain * pSet * pSlot * (1 - p3sub), n })
         })
+        // 3-line starts
+        crawlSubstatsK([], subsToConsider, 3).forEach(({ p, subs }) => {
+          const nodeInfo = {
+            base,
+            rarity,
+            subkeys: subs.map((key) => ({ key, baseRolls: 1 })), // 3 lines
+            rollsLeft: getRollsRemaining(0, rarity), // reserve one roll for the 4th on level-up
+          }
+          const n = makeSubstatNode(nodeInfo)
+          out.push({ p: p * pMain * pSet * pSlot * p3sub, n })
+        })

And add in libs/gi/upopt/src/substatProbs.ts:

export function crawlSubstatsK(
  prefix: readonly SubstatKey[],
  options: readonly SubstatKey[],
  k: number,
  insertPrefix = true
): { p: number; subs: SubstatKey[] }[] {
  const optW = options.map((k) => substatWeights[k]).sort((a, b) => a - b)
  return combinations(options, k - prefix.length)
    .map((combo) => {
      const comboW = combo.map((k) => substatWeights[k]).sort((a, b) => a - b)
      const p = substatProb(comboW, optW, k - prefix.length)
      return insertPrefix ? { p, subs: [...prefix, ...combo] } : { p, subs: combo }
    })
    .filter(({ p }) => p > 0)
}

I can PR this helper + tests if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 418cec8 and 305cb69.

📒 Files selected for processing (3)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
libs/gi/upopt/src/upOpt.test.ts (10)
libs/gi/upopt/src/upOpt.types.ts (4)
  • Objective (16-20)
  • MarkovNode (41-41)
  • GaussianNode (22-27)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/evaluation.ts (2)
  • evalMarkovNode (77-85)
  • evaluateGaussian (16-59)
libs/gi/upopt/src/makeObjective.ts (1)
  • makeObjective (13-48)
libs/gi/upopt/src/expandSubstat.ts (3)
  • expandSubstatLevel (8-33)
  • makeRollsNode (98-116)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/upopt/src/deduplicate.ts (1)
  • deduplicate (16-34)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/upOpt.ts (1)
  • expandNode (25-29)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/upOpt.ts (8)
libs/gi/upopt/src/upOpt.types.ts (2)
  • MarkovNode (41-41)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/expandSubstat.ts (2)
  • expandSubstatLevel (8-33)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/db/src/Interfaces/ICachedArtifact.ts (1)
  • ICachedArtifact (2-7)
libs/gi/consts/src/artifact.ts (8)
  • allSubstatKeys (156-167)
  • ArtifactRarity (173-173)
  • allArtifactSlotKeys (64-70)
  • artSlotMainKeys (128-134)
  • SubstatKey (169-169)
  • ArtifactSlotKey (71-71)
  • MainStatKey (168-168)
  • artMaxLevel (73-79)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/consts.ts (1)
  • allMainStatProbs (60-66)
libs/gi/good/src/IArtifact.ts (1)
  • IArtifact (11-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint
🔇 Additional comments (5)
libs/gi/upopt/src/upOpt.test.ts (2)

225-256: Nice GMM vs Gaussian consistency checks

The linear-objective equivalence checks across expandRolls are tight. The additional post-dedup check is a good guard.


384-401: Deterministic probabilities with manual combos: solid

Precomputed fractions look gnarly but effective for catching drift in crawlSubstats. Keep these.

libs/gi/upopt/src/upOpt.ts (3)

194-239: toStats set accounting looks correct now

Counts existing build sets and adds +1 for the generated artifact; avoids the missing-set bug flagged earlier. Sleep-deprived me is relieved.


127-155: Reshape: enforce initialValue presence and slot set count — good

Throwing on undefined initialValue prevents silent math drift. Base stats include set membership via toStats; this fixes prior set-bonus omissions in rerolls.


31-79: Level-up flow: unactivated substat handling is sensible

Subtracting a roll per activation and folding initialValue into base is aligned with roll accounting. No issues spotted.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
libs/gi/upopt/src/deduplicate.ts (1)

24-34: Comparator sign flip is confusing; prefer direct order

The leading minus makes the sort harder to reason about and invites bugs when cmpNodes evolves. Sort ascending directly for clarity.

-  nodes.sort((a, b) => -cmpNodes(a.n, b.n))
+  nodes.sort((a, b) => cmpNodes(a.n, b.n))
libs/gi/upopt/src/expandSubstat.ts (1)

15-31: Use forEach instead of map for side effects

Tiny nit: map’s return is unused and this confused my half-asleep eyes. forEach reads cleaner.

-  crawlUpgrades(rollsLeft).map((counts) => {
+  crawlUpgrades(rollsLeft).forEach((counts) => {
     ...
-  })
+  })
libs/gi/upopt/src/upOpt.ts (2)

85-119: Model 3‑line starts instead of dropping them

freshArtifact multiplies by (1 − p3sub) and only emits 4‑line starts. We should branch a 3‑line path that adds the 4th stat at first upgrade (consuming 1 roll) with proper probabilities, then feed into makeSubstatNode.

I can wire a small helper that mirrors levelUpArtifact’s “sum over possible 4th stat” and integrate it here.


64-79: Complete the “sum over possible 4th stat” path

levelUpArtifact’s TODO is exactly what freshArtifact needs too. Merging these into a reusable helper reduces cognitive load (and saves me a pity pull’s worth of braincells).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 418cec8 and 5f39c69.

📒 Files selected for processing (6)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/expandRolls.ts (1 hunks)
  • libs/gi/upopt/src/expandSubstat.ts (1 hunks)
  • libs/gi/upopt/src/makeObjective.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
libs/gi/upopt/src/upOpt.test.ts (10)
libs/gi/upopt/src/upOpt.types.ts (4)
  • Objective (16-20)
  • MarkovNode (41-41)
  • GaussianNode (22-27)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/evaluation.ts (2)
  • evalMarkovNode (77-85)
  • evaluateGaussian (16-59)
libs/gi/upopt/src/makeObjective.ts (1)
  • makeObjective (13-48)
libs/gi/upopt/src/expandSubstat.ts (3)
  • expandSubstatLevel (8-33)
  • makeRollsNode (98-116)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/upopt/src/deduplicate.ts (1)
  • deduplicate (16-34)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/upOpt.ts (1)
  • expandNode (25-29)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/expandSubstat.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
libs/gi/upopt/src/mathUtil.ts (4)
  • crawlUpgrades (170-185)
  • rollCountProb (87-120)
  • diag (73-75)
  • rollCountMuVar (123-167)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • ArtifactRarity (173-173)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/expandRolls.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/common/util/src/lib/array.ts (2)
  • range (14-16)
  • cartesian (27-31)
libs/gi/upopt/src/mathUtil.ts (1)
  • quadrinomial (23-27)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/upOpt.ts (8)
libs/gi/upopt/src/upOpt.types.ts (2)
  • MarkovNode (41-41)
  • SubstatLevelNode (46-56)
libs/gi/upopt/src/expandSubstat.ts (2)
  • expandSubstatLevel (8-33)
  • makeSubstatNode (125-175)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/db/src/Interfaces/ICachedArtifact.ts (1)
  • ICachedArtifact (2-7)
libs/gi/consts/src/artifact.ts (8)
  • allSubstatKeys (156-167)
  • ArtifactRarity (173-173)
  • allArtifactSlotKeys (64-70)
  • artSlotMainKeys (128-134)
  • SubstatKey (169-169)
  • ArtifactSlotKey (71-71)
  • MainStatKey (168-168)
  • artMaxLevel (73-79)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/consts.ts (1)
  • allMainStatProbs (60-66)
libs/gi/good/src/IArtifact.ts (1)
  • IArtifact (11-16)
libs/gi/upopt/src/makeObjective.ts (3)
libs/gi/wr/src/optimization.ts (2)
  • OptNode (18-22)
  • precompute (72-152)
libs/gi/upopt/src/upOpt.types.ts (1)
  • Objective (16-20)
libs/gi/consts/src/artifact.ts (1)
  • allSubstatKeys (156-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint
🔇 Additional comments (4)
libs/gi/upopt/src/deduplicate.ts (2)

85-87: Values-level equality on base only — confirm intent

cmpValuesNode ignores mu/cov. If two different roll paths yield identical base stats but different Gaussian params, they’ll merge. If that’s intentional (values nodes as deterministic leaves), all good; otherwise include mu/cov/subs in the compare.


36-45: Zero-derivative pruning looks fixed and aligned

The inverted mask is correct now; subs/mu/cov stay aligned after row/col filtering. My sleep-deprived gacha brain approves.

libs/gi/upopt/src/expandSubstat.ts (1)

35-42: Confirm substat tiering assumptions

Using [0.7, 0.8, 0.9, 1] uniformly times 'max' per roll is a strong assumption. If GI changes tier weights per rarity, mu/var skew. Please confirm these are uniform for 3★/4★/5★ in our data tables.

libs/gi/upopt/src/upOpt.ts (1)

224-239: Probabilities sanity-check

Between pSlot, pSet, pMain and crawlSubstats p, total mass should be ≤1 (exactly 1 if the universe is complete). Worth a quick check in tests to avoid silent drift.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
libs/gi/upopt/src/deduplicate.ts (3)

16-34: Be explicit about mutation or return fresh nodes.

deduplicate simplifies in place, which can surprise callers holding references. Either document the mutation or defensively clone nodes before simplification.


24-24: Drop the negation for clarity.

nodes.sort(cmpNodes) is easier to read than sorting by -cmpNodes and then re‑calling cmpNodes for equality. If you want reverse order, flip type ordering inside cmpNodes.


15-15: Consider exporting the weightedNode type.

Public API exposes it via the function signature; exporting improves DX and avoids anonymous types in d.ts.

libs/gi/upopt/src/makeObjective.ts (1)

38-41: Slightly clearer split of f/df without mutating values

Nit: using indices avoids splice/shift churn in hot paths.

-      const f = values.splice(0, nodes.length)
-      const df = nonzeroDerivs.map((subs) =>
-        Object.fromEntries(subs.map((s) => [s, values.shift()!]))
-      ) as DynStat[]
+      const f = values.slice(0, nodes.length)
+      let k = nodes.length
+      const df = nonzeroDerivs.map((subs) =>
+        Object.fromEntries(subs.map((s) => [s, values[k++]]))
+      ) as DynStat[]
libs/gi/upopt/src/expandSubstat.ts (2)

15-31: Use forEach (or a reducer) instead of map for side effects

Tiny nit, but .map allocates an array you throw away on every branch crawl.

-  crawlUpgrades(rollsLeft).map((counts) => {
+  crawlUpgrades(rollsLeft).forEach((counts) => {
     ...
-  })
+  })

126-135: Avoid mutating inputs (info.subkeys, reshape.affixes) in constructor

Sorting the arrays on info mutates caller-owned data and can surprise upstream code if info is reused.

-export function makeSubstatNode(info: SubstatLevelInfo): SubstatLevelNode {
-  const { rollsLeft, subkeys, reshape } = info
-  subkeys.sort((a, b) => a.key.localeCompare(b.key)) // Ensure consistent ordering
-  reshape?.affixes.sort((a, b) => a.localeCompare(b)) // Ensure consistent ordering
+export function makeSubstatNode(info: SubstatLevelInfo): SubstatLevelNode {
+  const { rollsLeft } = info
+  const subkeys = [...info.subkeys].sort((a, b) => a.key.localeCompare(b.key))
+  const reshape = info.reshape
+    ? { ...info.reshape, affixes: [...info.reshape.affixes].sort((a, b) => a.localeCompare(b)) }
+    : undefined

My brain’s already juggling pity counters; let’s not juggle object identity too.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f39c69 and 1dafe5f.

📒 Files selected for processing (5)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/expandSubstat.ts (1 hunks)
  • libs/gi/upopt/src/makeObjective.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • libs/gi/upopt/src/upOpt.test.ts
  • libs/gi/upopt/src/upOpt.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-27T18:05:10.497Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.497Z
Learning: In gi/upopt's expandSubstatLevel, the getReshapeIxs function handles missing reshape affixes by assigning them virtual indices beyond subkeys.length when there's room (< 4 substats). This allows the reshape constraint calculation to work correctly even after deduplication removes zero-derivative substats, as missing affixes are treated as "omitted substats" with 0 rolls.

Applied to files:

  • libs/gi/upopt/src/expandSubstat.ts
  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T18:05:10.497Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.497Z
Learning: In the gi/upopt library, expandSubstatLevel's getReshapeIxs function has built-in logic to handle missing substats from reshape.affixes, treating omitted substats as pruned affixes. The deduplication simplification of nodes doesn't break reshape constraints because the expansion logic is designed to work with missing reshape affixes.

Applied to files:

  • libs/gi/upopt/src/expandSubstat.ts
  • libs/gi/upopt/src/deduplicate.ts
🧬 Code graph analysis (3)
libs/gi/upopt/src/expandSubstat.ts (3)
libs/gi/upopt/src/upOpt.types.ts (2)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
libs/gi/upopt/src/mathUtil.ts (4)
  • crawlUpgrades (170-185)
  • rollCountProb (87-120)
  • diag (73-75)
  • rollCountMuVar (123-167)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/makeObjective.ts (3)
libs/gi/wr/src/optimization.ts (2)
  • OptNode (18-22)
  • precompute (72-152)
libs/gi/upopt/src/upOpt.types.ts (1)
  • Objective (16-20)
libs/gi/consts/src/artifact.ts (1)
  • allSubstatKeys (156-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: call-build / build
  • GitHub Check: typecheck
  • GitHub Check: lint
  • GitHub Check: gen-file
  • GitHub Check: test
🔇 Additional comments (9)
libs/gi/upopt/src/deduplicate.ts (5)

36-45: Zero‑derivative pruning looks correct; reshape omission is fine.

Masks stay aligned across subkeys/mu/cov and column pruning is done after row pruning. Not mirroring reshape here is OK since expansion handles missing affixes via getReshapeIxs.

Based on learnings


47-58: Rolls simplification is sane and aligned.

Pruning zero‑deriv and zero‑roll subs while keeping subDistr arrays/cov in sync looks good.


74-83: Base comparator fix LGTM.

Key‑set check prevents NaN diffs; deterministic order preserved.


89-113: Substat comparator covers missing ties now.

Including rarity, rollsLeft, and robust reshape checks closes prior gaps.


115-124: Rolls comparator includes rarity — good catch.

Prevents cross‑rarity coalescing.

libs/gi/upopt/src/makeObjective.ts (1)

17-24: LGTM: sparse Jacobian build respects zero-deriv pruning

Nice. Building ddx only for nonzero substats keeps eval fast and memory light—good for my sleep debt and your perf budget.

libs/gi/upopt/src/expandSubstat.ts (3)

149-165: Covariance construction looks correct (counts correlation + per‑roll variance)

Good decomposition: Cov = M_i M_j Cov(N_i, N_j) + 1{i=j} Var_roll * E[N_i]. This matches the model.


79-87: Swap-tracker update is incorrect for j>1; produces wrong swaps downstream

tracker maps sourceIndex → currentPos, but the code swaps by positions (i, j). This corrupts the mapping once we move past the first index, causing wrong mu/cov reordering under reshape.

Apply this fix to maintain the mapping correctly:

 function ixsToSwaps(ixs: number[], nmax: number): number[][] {
   const tracker = Array.from({ length: nmax }, (_, i) => i)

   const out: number[][] = []
   ixs.forEach((i, j) => {
-    if (i === tracker[j]) return
-    out.push([i, tracker[j]])
-    swap(tracker, i, j)
+    const curPos = tracker[j]
+    if (i === curPos) return
+    out.push([i, curPos])
+    // Maintain sourceIndex -> position mapping
+    const srcAtI = tracker.findIndex((pos) => pos === i)
+    tracker[j] = i
+    tracker[srcAtI] = curPos
   })
   return out
 }

Based on learnings.


35-42: Verify getSubstatValue signature matches arguments
Burning the midnight oil here—make sure getSubstatValue in libs/gi/util/src/artifact/artifact.ts actually accepts (substatKey, rarity, tier, roundUp) so that using 'max', false returns the intended max roll, not a truncated value.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45d3b6f and ab2402a.

📒 Files selected for processing (4)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/expandSubstat.ts (1 hunks)
  • libs/gi/upopt/src/makeObjective.ts (1 hunks)
  • libs/gi/upopt/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/gi/upopt/tsconfig.json
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-27T18:05:10.497Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.497Z
Learning: In the gi/upopt library, expandSubstatLevel's getReshapeIxs function has built-in logic to handle missing substats from reshape.affixes, treating omitted substats as pruned affixes. The deduplication simplification of nodes doesn't break reshape constraints because the expansion logic is designed to work with missing reshape affixes.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
  • libs/gi/upopt/src/expandSubstat.ts
📚 Learning: 2025-09-27T18:05:10.497Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.497Z
Learning: In gi/upopt's expandSubstatLevel, the getReshapeIxs function handles missing reshape affixes by assigning them virtual indices beyond subkeys.length when there's room (< 4 substats). This allows the reshape constraint calculation to work correctly even after deduplication removes zero-derivative substats, as missing affixes are treated as "omitted substats" with 0 rolls.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
  • libs/gi/upopt/src/expandSubstat.ts
📚 Learning: 2025-09-27T19:22:20.632Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:85-87
Timestamp: 2025-09-27T19:22:20.632Z
Learning: In the gi/upopt library, ValuesLevelNode instances always have empty mu (mean) and cov (covariance) arrays in their subDistr. Only the base DynStat contains actual values, making base-only comparison in cmpValuesNode sufficient for deduplication.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T19:22:20.632Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:85-87
Timestamp: 2025-09-27T19:22:20.632Z
Learning: In the gi/upopt library, ValuesLevelNode instances always have empty mu (mean) and cov (covariance) arrays in their subDistr. Only the base DynStat contains actual values, making base-only comparison in cmpValuesNode sufficient for deduplication. Values nodes represent deterministic leaf nodes where all probabilistic uncertainty has been resolved.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T19:19:31.838Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/makeObjective.ts:25-31
Timestamp: 2025-09-27T19:19:31.838Z
Learning: The `optimize` function from genshin-optimizer/gi/wr preserves the order of input nodes and does not reorder them, even though it internally calls `deduplicate`. This means that index-based alignment between original nodes/jacobian entries and the optimized `allNodes` array is safe to rely on.

Applied to files:

  • libs/gi/upopt/src/makeObjective.ts
🧬 Code graph analysis (3)
libs/gi/upopt/src/deduplicate.ts (1)
libs/gi/upopt/src/upOpt.types.ts (5)
  • MarkovNode (41-41)
  • Objective (16-20)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
  • ValuesLevelNode (74-77)
libs/gi/upopt/src/makeObjective.ts (3)
libs/gi/wr/src/optimization.ts (2)
  • OptNode (18-22)
  • precompute (72-152)
libs/gi/upopt/src/upOpt.types.ts (1)
  • Objective (16-20)
libs/gi/consts/src/artifact.ts (1)
  • allSubstatKeys (156-167)
libs/gi/upopt/src/expandSubstat.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • SubstatLevelNode (46-56)
  • RollsLevelNode (61-69)
libs/gi/upopt/src/mathUtil.ts (4)
  • crawlUpgrades (177-192)
  • rollCountProb (94-127)
  • diag (80-82)
  • rollCountMuVar (130-174)
libs/gi/consts/src/artifact.ts (2)
  • SubstatKey (169-169)
  • ArtifactRarity (173-173)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: call-build / build
  • GitHub Check: typecheck
  • GitHub Check: lint
🔇 Additional comments (12)
libs/gi/upopt/src/makeObjective.ts (2)

34-43: LGTM on the derivative construction!

takes another sip of energy drink while checking pity counter

The slicing and indexing logic for f and df looks solid. You're correctly starting k at nodes.length and incrementing for each nonzero derivative, which aligns with how the Jacobian was flattened in line 26. The sparse DynStat[] construction using Object.fromEntries is clean and matches the nonzeroDerivs structure.

Based on learnings, optimize preserves order, so this index-based approach is safe.


25-29: Exclude predicate correctly filters dynamic nodes
Dynamic nodes (path[0]=='dyn') are excluded by the predicate, while static nodes (empty path → p=undefined) are retained. No changes needed.

libs/gi/upopt/src/deduplicate.ts (4)

28-28: Clarify the negation in sort comparator.

rubs tired eyes after another all-nighter farming artifacts

Line 28 sorts with -cmpNodes(a.n, b.n). This negates the comparator, effectively reversing the order. Just want to confirm this is intentional. If cmpNodes already defines the desired order, the negation would flip it. Make sure this aligns with your merging logic expectations.


78-87: LGTM on cmpBase—no more NaN leakage!

crashes into chair after another gacha session that drained all my primogems

The updated cmpBase properly compares sorted key arrays first, then does numeric subtraction only when keys are identical. This prevents the NaN-from-undefined bug that was lurking in the original version. Solid fix!


93-117: LGTM on the comprehensive SubstatNode comparator!

barely keeping eyes open while min-maxing artifact substats at 4 AM

The comparator now covers all the critical fields: rarity, rollsLeft, reshape (with both mintotal and affixes.length), subkeys, and base. This should prevent the false-positive merges that the earlier version would've caused. Nice total ordering!


119-129: LGTM on RollsNode comparator with rarity!

zones out momentarily thinking about that 5-star artifact that rolled all into DEF

The rarity field is now properly compared at line 120, preventing the coalescing of 4★ and 5★ distributions. The rest of the comparator looks solid with length checks and element-wise comparisons. Good stuff!

libs/gi/upopt/src/expandSubstat.ts (6)

8-33: LGTM on expandSubstatLevel!

slumps over keyboard after yet another resin refresh wasted on bad artifacts

The expansion logic looks solid. You're computing reshapeIxs once upfront, then for each roll distribution from crawlUpgrades, you build the roll counts, compute the probability with reshape constraints factored in via reshapeIxs.reduce((a, i) => a + counts[i], 0), and filter out impossible configurations. The structure is clean and the early return for p === 0 saves unnecessary node construction.

Based on learnings, the reshape handling with getReshapeIxs is designed to work correctly even with missing affixes.


44-54: Variance formula assumes uniform distribution—confirm this matches your model.

rubs eyes after staring at probability distributions for 6 hours straight

Line 51 computes variance as Σ(b - μ)² / N, which is correct for a uniform distribution over discrete values. Just want to confirm that your artifact roll model indeed treats the 4 possible values [0.7, 0.8, 0.9, 1.0] as equally likely (25% each). If the game has different probabilities for low vs high rolls, this variance would be off.


56-73: LGTM on getReshapeIxs—handles missing affixes elegantly!

finally understanding the reshape logic after 3 cups of coffee

This function correctly handles the case where a reshape affix isn't present in subkeys by assigning virtual indices beyond the current length (lines 65-67). Based on learnings, this design allows the reshape constraint calculation to work correctly even after deduplication removes zero-derivative substats. The error case at line 69 is a proper safeguard for truly impossible states (all 4 slots filled with none matching).


79-96: LGTM on ixsToSwaps—confirmed correct per past discussion!

finally getting some rest after that exhausting debugging session in the past review

The swap-tracking logic here is solid. After the extensive discussion in past reviews, it's confirmed that tracker[j] correctly tracks where the original arr[j] has moved to, and swap(tracker, i, j) properly updates the mapping after each swap. No issues here!


98-117: Verify independence assumption in diagonal covariance.

zones out momentarily thinking about correlated artifact rolls

Line 112-114 constructs a diagonal covariance matrix using diag(), which assumes independence between different substats. This means you're modeling each substat's roll outcomes as independent random variables. Just confirm this aligns with your probabilistic model—if there's any correlation structure between substats (e.g., constraint on total stat points), a diagonal cov would miss that.


35-42: Exhaustive rarity coverage confirmed
ArtifactRarity only includes 3, 4, and 5 stars, so the explicit check covers all possible values and the error throw is dead code.

@nguyentvan7 nguyentvan7 requested a review from lantua October 3, 2025 17:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (12)
libs/gi/upopt/src/markov-tree/markov.types.ts (2)

4-9: Make GaussianNode arrays readonly + document length invariants.

Right now mu/cov/subs are mutable. Lock them down for safety and clarify invariants (mu.length === subs.length, cov is square of that size). Low-effort guardrails that save late-night bug hunts.

-export type GaussianNode = {
-  base: DynStat
-  subs: SubstatKey[]
-  mu: number[] // Mean stats vector
-  cov: number[][] // Covariance of stats matrix
-}
+export type GaussianNode = {
+  base: DynStat
+  subs: readonly SubstatKey[]
+  mu: readonly number[] // length === subs.length
+  cov: readonly (readonly number[])[] // square matrix, size === subs.length
+}

23-27: Narrow derivative typing to reflect sparsity.

computeWithDerivs returns per-node sparse derivatives; advertise that in the type to prevent accidental assumptions of full DynStat.

-export type Objective = {
-  computeWithDerivs: (x: DynStat) => [number[], DynStat[]]
+export type Objective = {
+  computeWithDerivs: (
+    x: DynStat
+  ) => [readonly number[], ReadonlyArray<Partial<DynStat>>]
   threshold: number[]
   zeroDeriv: SubstatKey[] // Substats that have zero derivative for all objective functions.
 }
libs/gi/upopt/src/upOpt.test.ts (1)

434-439: Convert empty tests to test.todo to avoid false greens.

These no-op tests pass without assertions and inflate confidence. Mark as todo or remove.

-describe('upOpt makeSubstatNode(s)', () => {
-  test('levelArtifact', () => {})
-  test('fresh/domain/strongbox', () => {})
-  test('reshape/dust', () => {})
-  test('define/elixir', () => {})
-})
+describe('upOpt makeSubstatNode(s)', () => {
+  test.todo('levelArtifact')
+  test.todo('fresh/domain/strongbox')
+  test.todo('reshape/dust')
+  test.todo('define/elixir')
+})
libs/gi/upopt/src/markov-tree/makeObjective.ts (1)

25-31: Document the order-preservation invariant for optimize.

computeWithDerivs relies on allNodes keeping [nodes, jac] order. Add a short comment referencing the contract so sleepy future devs don’t “optimize” this away. Based on learnings.

-  const allNodes = optimize(
+  // IMPORTANT: optimize preserves input order; f/df slicing below depends on this.
+  const allNodes = optimize(
     [...nodes, ...jac.flat()],
libs/gi/upopt/src/deduplicate.ts (2)

35-44: Add a quick shape assert after pruning to avoid skewed cov.

If a mismatch ever slips in, covariance rows/cols can desync. A tiny runtime guard here prevents silent comparator chaos later.

   node.subDistr.cov = node.subDistr.cov.map((row) =>
     row.filter((_, i) => !toRemove[i])
   )
+  // sanity: cov must remain square and match mu/subs length
+  if (
+    node.subDistr.mu.length !== node.subDistr.subs.length ||
+    node.subDistr.cov.length !== node.subDistr.subs.length ||
+    node.subDistr.cov.some((r) => r.length !== node.subDistr.subs.length)
+  ) {
+    throw new Error('simplifySubstatNode: covariance shape mismatch')
+  }

59-71: Deterministic total order — looks solid; tiny nit: avoid negating comparator in sort.

Style nit from a gacha-addled brain: prefer defining cmpNodes for ascending and call nodes.sort(cmp) to reduce mental flips. No behavior change required.

-  nodes.sort((a, b) => -cmpNodes(a.n, b.n))
+  nodes.sort((a, b) => cmpNodes(a.n, b.n))
   let prev: weightedNode | undefined = undefined

(If you keep the negation, maybe drop a comment that the order is intentionally descending.)

Also applies to: 88-124

libs/gi/upopt/src/markov-tree/evaluation.ts (2)

54-56: Clamp variance before sqrt to avoid NaN bounds at ~zero variance.

f_cov[0][0] can be slightly negative from FP noise. Clamp before sqrt.

-    lower: upAvg - 4 * Math.sqrt(f_cov[0][0]),
-    upper: upAvg + 4 * Math.sqrt(f_cov[0][0]),
+    lower: upAvg - 4 * Math.sqrt(Math.max(f_cov[0]?.[0] ?? 0, 0)),
+    upper: upAvg + 4 * Math.sqrt(Math.max(f_cov[0]?.[0] ?? 0, 0)),

23-45: Optional: assert objective dimensionality matches thresholds.

Quick guard helps catch miswired Objectives early (length mismatch).

   const [f_mu, df] = obj.computeWithDerivs(stats)
+  if (obj.threshold.length !== f_mu.length)
+    throw new Error(`Objective threshold length ${obj.threshold.length} != f_mu length ${f_mu.length}`)
libs/gi/upopt/src/expandRolls.ts (1)

42-49: Make covariance truly 0×0 for values nodes.

cov: [[]] is a ragged 1×0; prefer empty 0×0 to reflect no random substats.

   const subDistr = {
     base,
     subs: [],
     mu: [],
-    cov: [[]],
+    cov: [],
   }
libs/gi/upopt/src/substatProbs.ts (2)

28-57: Caching via JSON is fine; Map can be cleaner and faster.

Low‑stakes, but Map with tuple keys avoids stringify costs and GC churn in hot paths.

-const substatProbCache: Record<string, number> = {}
+const substatProbCache = new Map<string, number>()
 ...
-  if (substatProbCache[key] !== undefined) return substatProbCache[key]
+  if (substatProbCache.has(key)) return substatProbCache.get(key)!
 ...
-  substatProbCache[key] = p
+  substatProbCache.set(key, p)

64-73: Doc nit: probability depends only on weight multiset.

Multiple combos with identical weight patterns will share the same p; that's expected. Consider noting this for consumers to avoid surprise when identical p’s repeat across keys.

libs/gi/upopt/src/upOpt.types.ts (1)

17-23: Reshape type mismatch with mathUtil.

Here: { affixes: SubstatKey[]; mintotal: number }. In mathUtil.rollCountProb/rollCountMuVar the shape is { n, min, total }. Please normalize or add a shared type + adapter to prevent drift.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab2402a and 5dd986b.

📒 Files selected for processing (12)
  • libs/gi/upopt/src/deduplicate.ts (1 hunks)
  • libs/gi/upopt/src/expandRolls.ts (1 hunks)
  • libs/gi/upopt/src/expandSubstat.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/evaluation.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/makeObjective.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/markov.types.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/mathUtil.test.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/mathUtil.ts (1 hunks)
  • libs/gi/upopt/src/markov-tree/mvncdf.ts (1 hunks)
  • libs/gi/upopt/src/substatProbs.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.test.ts (1 hunks)
  • libs/gi/upopt/src/upOpt.types.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • libs/gi/upopt/src/markov-tree/mathUtil.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/gi/upopt/src/expandSubstat.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-09-27T17:26:02.090Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/mathUtil.ts:106-118
Timestamp: 2025-09-27T17:26:02.090Z
Learning: The range(from, to) function in genshin-optimizer/common/util is inclusive on both endpoints, meaning range(0, reshape.total) includes reshape.total as the final value. This differs from typical JavaScript range implementations which are usually half-open.

Applied to files:

  • libs/gi/upopt/src/markov-tree/mathUtil.ts
📚 Learning: 2025-09-27T18:05:10.516Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.516Z
Learning: In the gi/upopt library, expandSubstatLevel's getReshapeIxs function has built-in logic to handle missing substats from reshape.affixes, treating omitted substats as pruned affixes. The deduplication simplification of nodes doesn't break reshape constraints because the expansion logic is designed to work with missing reshape affixes.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T18:05:10.516Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:36-45
Timestamp: 2025-09-27T18:05:10.516Z
Learning: In gi/upopt's expandSubstatLevel, the getReshapeIxs function handles missing reshape affixes by assigning them virtual indices beyond subkeys.length when there's room (< 4 substats). This allows the reshape constraint calculation to work correctly even after deduplication removes zero-derivative substats, as missing affixes are treated as "omitted substats" with 0 rolls.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T19:22:20.666Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:85-87
Timestamp: 2025-09-27T19:22:20.666Z
Learning: In the gi/upopt library, ValuesLevelNode instances always have empty mu (mean) and cov (covariance) arrays in their subDistr. Only the base DynStat contains actual values, making base-only comparison in cmpValuesNode sufficient for deduplication.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T19:22:20.666Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/deduplicate.ts:85-87
Timestamp: 2025-09-27T19:22:20.666Z
Learning: In the gi/upopt library, ValuesLevelNode instances always have empty mu (mean) and cov (covariance) arrays in their subDistr. Only the base DynStat contains actual values, making base-only comparison in cmpValuesNode sufficient for deduplication. Values nodes represent deterministic leaf nodes where all probabilistic uncertainty has been resolved.

Applied to files:

  • libs/gi/upopt/src/deduplicate.ts
📚 Learning: 2025-09-27T19:19:31.846Z
Learnt from: tooflesswulf
PR: frzyc/genshin-optimizer#3069
File: libs/gi/upopt/src/makeObjective.ts:25-31
Timestamp: 2025-09-27T19:19:31.846Z
Learning: The `optimize` function from genshin-optimizer/gi/wr preserves the order of input nodes and does not reorder them, even though it internally calls `deduplicate`. This means that index-based alignment between original nodes/jacobian entries and the optimized `allNodes` array is safe to rely on.

Applied to files:

  • libs/gi/upopt/src/markov-tree/makeObjective.ts
🧬 Code graph analysis (10)
libs/gi/upopt/src/markov-tree/mvncdf.ts (1)
libs/gi/upopt/src/markov-tree/mathUtil.ts (2)
  • erf (40-60)
  • gaussPDF (63-71)
libs/gi/upopt/src/substatProbs.ts (2)
libs/gi/consts/src/artifact.ts (1)
  • SubstatKey (169-169)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/markov-tree/markov.types.ts (1)
libs/gi/consts/src/artifact.ts (1)
  • SubstatKey (169-169)
libs/gi/upopt/src/markov-tree/mathUtil.ts (1)
libs/common/util/src/lib/array.ts (1)
  • range (14-16)
libs/gi/upopt/src/upOpt.test.ts (10)
libs/gi/upopt/src/markov-tree/markov.types.ts (2)
  • Objective (23-27)
  • GaussianNode (4-9)
libs/gi/upopt/src/upOpt.types.ts (2)
  • MarkovNode (8-8)
  • SubstatLevelNode (13-23)
libs/gi/upopt/src/markov-tree/evaluation.ts (2)
  • evalMarkovNode (75-83)
  • evaluateGaussian (14-57)
libs/gi/upopt/src/markov-tree/makeObjective.ts (1)
  • makeObjective (13-49)
libs/gi/upopt/src/expandSubstat.ts (3)
  • expandSubstatLevel (13-38)
  • makeRollsNode (103-122)
  • makeSubstatNode (131-181)
libs/gi/upopt/src/expandRolls.ts (1)
  • expandRollsLevel (8-40)
libs/gi/upopt/src/deduplicate.ts (1)
  • deduplicate (15-33)
libs/gi/upopt/src/upOpt.ts (1)
  • expandNode (25-29)
libs/gi/upopt/src/consts.ts (1)
  • substatWeights (6-17)
libs/gi/upopt/src/substatProbs.ts (1)
  • crawlSubstats (59-73)
libs/gi/upopt/src/upOpt.types.ts (2)
libs/gi/upopt/src/markov-tree/markov.types.ts (2)
  • GaussianNode (4-9)
  • EvaluatedGaussian (11-21)
libs/gi/consts/src/artifact.ts (2)
  • ArtifactRarity (173-173)
  • SubstatKey (169-169)
libs/gi/upopt/src/expandRolls.ts (4)
libs/gi/upopt/src/upOpt.types.ts (2)
  • RollsLevelNode (28-36)
  • ValuesLevelNode (41-44)
libs/common/util/src/lib/array.ts (2)
  • range (14-16)
  • cartesian (27-31)
libs/gi/upopt/src/markov-tree/mathUtil.ts (1)
  • quadrinomial (19-34)
libs/sr/util/src/relic.ts (1)
  • getSubstatValue (61-75)
libs/gi/upopt/src/deduplicate.ts (2)
libs/gi/upopt/src/upOpt.types.ts (4)
  • MarkovNode (8-8)
  • SubstatLevelNode (13-23)
  • RollsLevelNode (28-36)
  • ValuesLevelNode (41-44)
libs/gi/upopt/src/markov-tree/markov.types.ts (1)
  • Objective (23-27)
libs/gi/upopt/src/markov-tree/evaluation.ts (3)
libs/gi/upopt/src/markov-tree/markov.types.ts (3)
  • Objective (23-27)
  • GaussianNode (4-9)
  • EvaluatedGaussian (11-21)
libs/gi/upopt/src/markov-tree/mvncdf.ts (1)
  • mvnPE_bad (37-57)
libs/gi/upopt/src/upOpt.types.ts (8)
  • SubstatLevelNode (13-23)
  • EvaluatedSubstatNode (24-26)
  • RollsLevelNode (28-36)
  • EvaluatedRollsNode (37-39)
  • ValuesLevelNode (41-44)
  • EvaluatedValuesNode (45-47)
  • MarkovNode (8-8)
  • EvaluatedMarkovNode (9-12)
libs/gi/upopt/src/markov-tree/makeObjective.ts (3)
libs/gi/wr/src/optimization.ts (2)
  • OptNode (18-22)
  • precompute (72-152)
libs/gi/upopt/src/markov-tree/markov.types.ts (1)
  • Objective (23-27)
libs/gi/consts/src/artifact.ts (1)
  • allSubstatKeys (156-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: call-build / build
  • GitHub Check: typecheck
  • GitHub Check: lint
🔇 Additional comments (4)
libs/gi/upopt/src/upOpt.test.ts (1)

31-56: Mixture mean/variance check is sound.

The E and Var for a mixture are computed correctly (Σ p μ, Σ p(σ²+μ²)−μ_mix²). Nice sanity net for expansions.

libs/gi/upopt/src/markov-tree/makeObjective.ts (1)

32-48: LGTM — derivative stacking and zeroDeriv look correct.

Index math aligns with nonzeroDerivs; zeroDeriv filters substats absent from all rows. Sweet.

libs/gi/upopt/src/markov-tree/mathUtil.ts (1)

112-120: Range endpoint is already inclusive — good.

No change; keeping for posterity so future me doesn’t “fix” it at 3 AM again.

libs/gi/upopt/src/expandRolls.ts (1)

19-20: <!-- [scratchpad_start]
[task verification_getSubstatValue_signature COMPLETE]
[observations]

  • Full GI getSubstatValue signature found (lines 77-82):
    • params: (substatKey, rarity = 5, type: 'max'|'min'|'mid' = 'max', percent = true)
  • Call in expandRolls.ts:19 is: getSubstatValue(key, rarity, 'max', false)
  • Arg mapping:
    • key → substatKey ✓
    • rarity → rarity ✓
    • 'max' → type: valid value ('max'|'min'|'mid') ✓
    • false → percent: valid, sets percent=false (returns raw value not percent) ✓
      [analysis]
  • ARG ORDER: Correct (matches function signature)
  • TIER VALIDITY: 'max' IS the correct tier for GI (not 'high'/'med'/'low' which are for SR)
  • 4TH PARAMETER: Valid - percent=false means return raw substat value, not percentage
  • REVIEW ASSUMPTION: Reviewer may have confused GI and SR signatures, or assumed 'max' wasn't valid
  • CONCLUSION: Original review comment is INCORRECT - the code is fine
    [done]
    All concerns verified and resolved. Code is correct.
    [scratchpad_start] -->

cracks knuckles while bleary-eyed at 3 AM, gacha ad banner loading in another tab

Yo, so I actually dove into this and... the original review comment is barking up the wrong tree here.

The complete signature for GI's getSubstatValue is:

export function getSubstatValue(
  substatKey: SubstatKey,
  rarity: ArtifactRarity = 5,
  type: 'max' | 'min' | 'mid' = 'max',
  percent = true
): number

Your call at line 19:

getSubstatValue(key, rarity, 'max', false)

Is 100% correct:

  • key maps to substatKey
  • rarity maps to rarity
  • 'max' is a valid tier for GI (not SR which uses 'low'|'med'|'high') ✓
  • false sets percent=false to return raw values instead of percentages ✓

Argument order is solid, tier is right for Genshin, and that fourth parameter is doing exactly what it should. The review concern seems to have mixed up the GI and SR function signatures or made wrong assumptions about what 'max' represents.

Likely an incorrect or invalid review comment.

lantua
lantua previously approved these changes Oct 24, 2025
Copy link
Collaborator

@lantua lantua left a comment

Choose a reason for hiding this comment

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

We will fix it in post XD.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants