Skip to content

Refactor/extract standalone functions from structure#367

Merged
L-M-Sherlock merged 21 commits intomainfrom
Refactor/extract-standalone-functions-from-structure
Sep 30, 2025
Merged

Refactor/extract standalone functions from structure#367
L-M-Sherlock merged 21 commits intomainfrom
Refactor/extract-standalone-functions-from-structure

Conversation

@L-M-Sherlock
Copy link
Member

@L-M-Sherlock L-M-Sherlock commented Sep 29, 2025

This PR introduces a significant refactoring of the FSRS codebase, making the API more ergonomic and consistent while extracting standalone functions from the FSRS structure. These changes warrant a major version bump from 5.2.0 to 6.0.0.

Key changes:

  • API improvements:

    • Added FSRS::default() for simpler initialization
    • Changed FSRS::new() to take parameters directly instead of Option<&Parameters>
    • Simplified FSRS structure by making the model field non-optional
  • Extracted standalone functions:

    • compute_parameters
    • evaluate_with_time_series_splits
    • optimal_retention
    • current_retrievability
  • Code organization:

    • Test functions renamed with test_ prefix
    • Internal module structure improved with better visibility controls
    • Dependency imports reorganized for clarity

Migration guide:

FSRS Instance Creation

Before:

// With default parameters
let fsrs = FSRS::new(Some(&[])).unwrap();
// or
let fsrs = FSRS::new(None).unwrap();

// With custom parameters
let fsrs = FSRS::new(Some(&parameters)).unwrap();

After:

// With default parameters
let fsrs = FSRS::default();

// With custom parameters
let fsrs = FSRS::new(&parameters).unwrap();

Optimizing Parameters

Before:

let fsrs = FSRS::new(None).unwrap();
let parameters = fsrs.compute_parameters(ComputeParametersInput {
    train_set: items,
    ..Default::default()
}).unwrap();

After:

let parameters = compute_parameters(ComputeParametersInput {
    train_set: items,
    ..Default::default()
}).unwrap();

Evaluating with Time Series Splits

Before:

let fsrs = FSRS::new(None).unwrap();
let evaluation = fsrs.evaluate_with_time_series_splits(input, |progress| true).unwrap();

After:

let evaluation = evaluate_with_time_series_splits(input, |progress| true).unwrap();

Calculating Optimal Retention

Before:

let fsrs = FSRS::new(None).unwrap();
let optimal = fsrs.optimal_retention(&config, &[], |_| true, None, None).unwrap();

After:

let optimal = optimal_retention(&config, &[], |_| true, None, None).unwrap();

Retrievability Calculation

Before:

let fsrs = FSRS::new(None).unwrap();
// Days
let r = fsrs.current_retrievability(state, days_elapsed, decay);
// Seconds
let r = fsrs.current_retrievability_seconds(state, seconds_elapsed, decay);

After:

// Days
let r = current_retrievability(state, days_elapsed as f32, decay);
// Seconds (converted to days)
let r = current_retrievability(state, seconds_elapsed as f32 / 86400.0, decay);

@L-M-Sherlock
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a solid refactoring pull request that significantly improves the modularity and overall quality of the codebase. Extracting current_retrievability into a standalone function is a great move for better API design. The accompanying changes, such as adjusting visibility for better encapsulation, simplifying function signatures, and renaming tests for consistency, are all well-executed and contribute to a cleaner, more maintainable codebase. The major version bump is appropriate for the breaking API changes. I have one minor suggestion to remove a redundant import.

@Luc-Mcgrady
Copy link
Member

Luc-Mcgrady commented Sep 29, 2025

Could you wait for me to implement the per-card DR + (maybe) per-card parameters(?) for the simulator before you publish the next major version?

I might wait for this to merge first so that there are no potential merge conflicts .

@L-M-Sherlock
Copy link
Member Author

I will merge this PR and wait for your PR. The next release will not come soon.

Copy link
Member

@Luc-Mcgrady Luc-Mcgrady left a comment

Choose a reason for hiding this comment

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

Should evaluate_with_time_series_splits and optimal_retention be made standalone?

also should we remove the Option in

pub fn new(parameters: Option<&Parameters>) -> Result<Self> {

if the intention is that there are no functions that don't require parameters?

@L-M-Sherlock
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed refactoring that improves the modularity and API design of the fsrs crate. By extracting standalone functions from the FSRS struct, the API becomes cleaner and more intuitive. The changes are consistently applied across the codebase, including examples, benchmarks, and tests. The renaming of test functions and adjustment of visibility for utility functions are also welcome improvements for code quality. I have one comment regarding device handling which could lead to a runtime panic in some scenarios.

src/model.rs Outdated
None => None,
};
let parameters = check_and_fill_parameters(parameters)?;
let model = parameters_to_model::<B2>(&parameters);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The parameters_to_model function creates tensors on the default device, but new_with_backend can be called with a non-default device. This will lead to a panic if the devices don't match when performing operations.

The device should be passed to parameters_to_model and used when creating tensors to ensure they are on the correct device.

You'll need to update parameters_to_model to accept a device parameter. This will likely require a few knock-on changes, for example:

// in src/model.rs
pub(crate) fn parameters_to_model<B: Backend>(parameters: &Parameters, device: &B::Device) -> Model<B> {
    let config = ModelConfig::default();
    // Model::new also needs to be updated to accept a device
    let mut model = Model::new(config.clone(), device);
    model.w = Param::from_tensor(Tensor::from_floats(
        TensorData::new(
            clip_parameters(parameters, config.num_relearning_steps, Default::default()),
            Shape { dims: vec![21] },
        ),
        device,
    ));
    model
}
Suggested change
let model = parameters_to_model::<B2>(&parameters);
let model = parameters_to_model::<B2>(&parameters, &device);

Copy link
Member Author

Choose a reason for hiding this comment

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

@AlexErrant, does it affect fsrs-browser?


// evaluate
let model = FSRS::new(Some(&parameters)).unwrap();
let model = FSRS::new(&parameters).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good change here.

…nsor creation, enhancing device management and consistency across the codebase.
@L-M-Sherlock
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-executed refactoring of the FSRS codebase. The key changes, such as extracting standalone functions and simplifying the FSRS struct and its API, make the library more ergonomic and improve its overall structure. The move to a non-optional model field in FSRS and the introduction of FSRS::default() are excellent improvements. The code changes are consistent with the goals outlined in the description. My review includes a few minor suggestions to improve code consistency and clean up test code.

.unwrap();
dbg!(optimal_retention);
let retention_value = optimal_retention(&config, &[], |_| true, None, None).unwrap();
dbg!(retention_value);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This dbg! macro appears to be for debugging purposes. It should be removed from the test code to keep the test output clean, especially since there's already an assertion for retention_value on line 2036.

num_relearning_steps: None,
})
.unwrap();
dbg!(&parameters);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This dbg! macro seems to be for debugging. It should be removed from the test code to keep the output clean. If specific values need to be checked, assertions should be used instead.

let model = FSRS::new(Some(&parameters)).unwrap();
let model = FSRS::new(&parameters).unwrap();
let metrics = model.evaluate(items.clone(), |_| true).unwrap();
dbg!(&metrics);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This dbg! macro seems to be for debugging. It should be removed from the test code to keep the output clean. If specific values need to be checked, assertions should be used instead.

L-M-Sherlock and others added 2 commits September 30, 2025 16:28
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…cing code consistency across multiple files.
…e parameter, improving code consistency and simplifying tensor creation across multiple files.
@L-M-Sherlock
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-executed refactoring of the FSRS codebase. The primary changes involve extracting several key functions (compute_parameters, evaluate_with_time_series_splits, optimal_retention, current_retrievability) into standalone functions, which improves modularity and API ergonomics. The introduction of FSRS::default() and the simplification of FSRS::new are welcome improvements. The code organization has also been enhanced by reducing visibility of internal modules and standardizing test function names.

My review has identified one high-severity issue related to device handling. The refactoring removed the device field from the FSRS struct, but the new device() method incorrectly returns the default device, which could lead to panics if a non-default device is used. I have provided a specific suggestion to fix this.

L-M-Sherlock and others added 2 commits September 30, 2025 17:25
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…iple files for improved consistency and clarity.
@L-M-Sherlock
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-executed refactoring, extracting standalone functions like compute_parameters, evaluate_with_time_series_splits, and optimal_retention from the FSRS struct. The API is now more ergonomic, with the introduction of FSRS::default() and a simplified FSRS::new(). The internal structure of FSRS is also improved by making the model field non-optional and removing the redundant device field. The code organization is enhanced through consistent test naming and better visibility control for internal modules. Overall, these changes are a great improvement to the codebase, making it cleaner, safer, and easier to use for the common cases, justifying the major version bump.

@L-M-Sherlock L-M-Sherlock merged commit 163f031 into main Sep 30, 2025
4 checks passed
@L-M-Sherlock L-M-Sherlock deleted the Refactor/extract-standalone-functions-from-structure branch September 30, 2025 09:36
@L-M-Sherlock
Copy link
Member Author

@Luc-Mcgrady, the PR is merged now.

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