I’m not sure I follow. In the example I gave, I have 3 Rating every time after 2022-12-24 @ 00:11 within the Review state. During this period where the interval increases then decreases, I never used the Hard button (Rating 2).
Reading through the code a bit again, I think I found a few possible hypotheses. In the review.rs there appear to be two possible locations that the interval is updated in response to a Good (Rating 3) response.
The first being when self.days_late() < 0
within the passing_early_review_intervals()
function:
let good_interval = constrain_passing_interval(
ctx,
(elapsed * self.ease_factor).max(scheduled),
0,
false);
and the second being self.days_late() >= 0
within the passing_nonearly_review_intervals()
function.
let good_interval = constrain_passing_interval(
ctx,
(current_interval + days_late / 2.0) * self.ease_factor,
hard_interval + 1,
true,
);
The definition of the constrain_passing_interval()
function is also of importance here:
/// Transform the provided hard/good/easy interval.
/// - Apply configured interval multiplier.
/// - Apply fuzz.
/// - Ensure it is at least `minimum`, and at least 1.
/// - Ensure it is at or below the configured maximum interval.
fn constrain_passing_interval(ctx: &StateContext, interval: f32, minimum: u32, fuzz: bool) -> u32 {
let interval = interval * ctx.interval_multiplier;
let (minimum, maximum) = ctx.min_and_max_review_intervals(minimum);
if fuzz {
ctx.with_review_fuzz(interval, minimum, maximum)
} else {
(interval.round() as u32).clamp(minimum, maximum)
}
}
So I can expand the assignments to interval
and good_interval
and summarize them below for easier comparison:
interval = (elapsed * self.ease_factor).max(scheduled) * ctx.interval_multiplier;
good_interval = (interval.round() as u32).clamp(minimum, maximum)
and
interval = (current_interval + days_late / 2.0) * self.ease_factor * ctx.interval_multiplier;
good_interval = ctx.with_review_fuzz(interval, minimum, maximum)
I need to confirm still, but I assume ctx.interval_multiplier
refers to the interval modifier in the deck settings. Also I’m not sure which gets updated first scheduled
(aka self.scheduled_days
) or interval
but based on what I see here I’d guess interval is updated, then scheduled is set to that new updated interval sometime later.
In the first case of passing_early_review_intervals()
, interval
cannot decrease before jumping into constrain_passing_interval
, even if self_ease_factor
< 100% or elapsed
being potentially negative, since the max(scheduled)
will hold it to its previously scheduled value (or previous interval) rather than decrease it.
On the other hand, the direct multiplication with ctx.interval_multiplier
after jumping into constrain_passing_interval
doesn’t seem appropriate here, as anything that satisfies self.ease_factor * ctx.interval_multiplier < 1
would lead to either a decrease in interval or at best a rounding up to the previous interval. You might want to either do the .max(scheduled)
operation after multiplying with ctx.interval_modifier
or change the minimum
from 0 to scheduled
as I would never expect to press Good and have the interval decrease.
Also the application of round
could potentially cause the interval to get stuck forever on a single interval (in the case where interval does not increase beyond 0.5 above the previous interval). Why don’t you use a ceiling function (always round up) rather than round()
? That way it will be guaranteed to increase by 1 without relaying on the hard_interval
(as you do in the passing_nonearly_review_intervals
version of the update). But maybe you want it to get stuck when reviewing early so as not to have the potential to indefinitely increase the interval when reviewing ahead.
Here the days_late is guaranteed to be >= 0, thus the only thing that could cause interval
to decrease would be if self.ease_factor
were less than 100%. This was not the case based on the info provided in the screenshot as ease_factor appears to be a constant 145% the whole time, so I don’t think this had anything to do with my issue.
Again though, after jumping into constrain_passing_interval
you multiply by ctx.interval_multiplier
which can cause interval to decrease as per the above comments. This time around it is not so straight forward as we have to consider fuzz, and you also change the minimum
to be hard_interval +1
(which I think is what you previous comment was regarding now that I reread it).
So entering into ctx.with_review_fuzz(interval, minimum, maximum)
there appears to be another function call to constrained_fuzz_bound
but after that there is a line that doesn’t appear to modify interval
before returning a u32
value. Within constrained_fuzz_bound
I also don’t see any modification of interval
so I am a bit confused as to where fuzz is actually applied to the interval. So I’m not sure if fuzz has any potential contribution to this issue or not.
Summary
The direct multiplication of ctx.interval_multiplier
definitely did if I am assuming correctly that this is the interval modifier
from the deck settings. If self.ease_factor * ctx.interval_multiplier < 1
my interval would decrease. In the example screenshot from the last posts, I had ease_factor=1.45
and I believe I had a interval modifier
of 0.65. Assuming I was within the passing_nonearly_review_intervals()
that would lead to an update of:
interval = (15 + 0 / 2.0) * 1.45 * 0.65; // 14.1375, a decrease of almost 1 day.
good_interval = ctx.with_review_fuzz(interval, minimum, maximum)
Now I’m not sure how fuzz actually modifies the value of interval, but assuming it adds some potential range of values and rounds. If that fuzz were near zero (or potentially even negative) then one can easily see an issue.
Continuing down my screenshot data for another example:
interval = (11 + 0 / 2.0) * 1.45 * 0.65; // 10.3675, a decrease of almost 1 day.
good_interval = ctx.with_review_fuzz(interval, minimum, maximum)
Essentially I will have a linear slope downward of 1.45*0.65=0.9425. Eventually the interval gets stuck at some low value because between the rounding/fuzz and the decrease of 5.75%, it will never decrease enough to get below 4.5 (in this example) and continue to round back up to 5.
Then, after I increased the interval modifier
back to 1.00, the combined value of the ease_factor
and interval modifier
became positive and began to increase again.
I’d say this is very opposed to what is written in the documentation regarding the interval modifier in that anything below 1.0 has the potential to have this issue occur.