Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application of fuzz to interval is not deterministic #113

Closed
winghouchan opened this issue Aug 15, 2024 · 3 comments · Fixed by #114
Closed

Application of fuzz to interval is not deterministic #113

winghouchan opened this issue Aug 15, 2024 · 3 comments · Fixed by #114
Assignees
Labels
bug Something isn't working

Comments

@winghouchan
Copy link

Steps to reproduce

  1. Schedule the same card with enable_fuzz: true and the same review date time and rating.

JSFiddle

Code
const {
  createEmptyCard,
  fsrs,
  Grades,
  Rating,
} = window.FSRS

const MOCK_NOW = new Date(2024, 7, 15)
const MOCK_TOMORROW = new Date(2024, 7, 16)
const { card } = fsrs().next(createEmptyCard(), MOCK_NOW, Rating.Good)

for (let count = 0; count < 10; count++) {
  const scheduler = fsrs({ enable_fuzz: true })
  console.log(scheduler.next(card, MOCK_TOMORROW, Rating.Good).card)
}
<script src="https://unpkg.com/ts-fsrs@4.1.1/dist/index.umd.js"></script>

Expected behaviour

All the next cards should be in the same state.

Actual behaviour

The next cards are not in the same state. They have different due dates. An example:

Some logs show the next due date is 2024-08-19 while others show 2024-08-20
@winghouchan
Copy link
Author

winghouchan commented Aug 15, 2024

Investigation notes

  • When enable_fuzz is true, the algorithm applies a fuzz factor to the next interval. The fuzz factor is generated with a pseudo-random number generator. When initialising the generator, a seed is passed to it.
    /**
    * If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
    * @param {number} ivl - The interval to be fuzzed.
    * @param {number} elapsed_days t days since the last review
    * @param {number} enable_fuzz - This adds a small random delay to the new interval time to prevent cards from sticking together and always being reviewed on the same day.
    * @return {number} - The fuzzed interval.
    **/
    apply_fuzz(ivl: number, elapsed_days: number, enable_fuzz?: boolean): int {
    if (!enable_fuzz || ivl < 2.5) return Math.round(ivl) as int
    const generator = alea(this.seed)
    const fuzz_factor = generator()
    const { min_ivl, max_ivl } = get_fuzz_range(
    ivl,
    elapsed_days,
    this.param.maximum_interval
    )
    return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl) as int
    }
  • The seed is generated from the time of the review, number of reps, and the product of card's current difficulty and stability.
    private initSeed() {
    const time = this.review_time.getTime()
    const reps = this.current.reps
    const mul = this.current.difficulty * this.current.stability
    this.algorithm.seed = `${time}_${reps}_${mul}`
    }
  • The inputs to the seed should be the same for all the cards in the test however the output due date can be different across runs.
  • When inspecting the state while apply_fuzz is run, it can be seen that this.seed is undefined. This means the pseudo-random number generator was initialised with no seed. It looks like the seed is available on this._seed. Screenshot of debugger showing this.seed is undefined while this._seed has the seed value
  • The root cause is the FSRSAlgorithm class having a setter for seed which assigns the value to this._seed. As there is no getter, this.seed returns undefined.
    set seed(seed: string) {
    this._seed = seed
    }

Resolution

One of:

  • Remove the setter for seed.
  • Add a getter for seed to return this._seed.
  • Update references to this.seed to this._seed.

@ishiko732 ishiko732 added the bug Something isn't working label Aug 15, 2024
@ishiko732
Copy link
Member

Thank you for identifying the issue; I am now checking and will fix it!

@ishiko732
Copy link
Member

  • Remove the setter for seed.

I cannot remove set seed because it is used in abstract_scheduler.ts :

private initSeed() {
const time = this.review_time.getTime()
const reps = this.current.reps
const mul = this.current.difficulty * this.current.stability
this.algorithm.seed = `${time}_${reps}_${mul}`
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants