-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from project-renard/algorithm-sm2
Implement SuperMemo 2 algorithm
- Loading branch information
Showing
5 changed files
with
506 additions
and
0 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
lib/Renard/Incunabula/Flashcard/Model/Card/Role/Scheduler/SuperMemo/SM2.pm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
use Renard::Incunabula::Common::Setup; | ||
package Renard::Incunabula::Flashcard::Model::Card::Role::Scheduler::SuperMemo::SM2; | ||
# ABSTRACT: A role for holding SM2 scheduling data | ||
|
||
use Moo::Role; | ||
|
||
use Renard::Incunabula::Common::Types qw(InstanceOf); | ||
use Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Param; | ||
|
||
=attr sm2_data | ||
The parameters for the SM2 scheduling algorithm. | ||
See L<Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2>. | ||
=cut | ||
has sm2_data => ( | ||
is => 'rw', | ||
isa => InstanceOf['Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Param'], | ||
lazy => 1, | ||
builder => sub { | ||
Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Param->new; | ||
}, | ||
); | ||
|
||
1; |
142 changes: 142 additions & 0 deletions
142
lib/Renard/Incunabula/Flashcard/Scheduler/SuperMemo/SM2.pm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
use Renard::Incunabula::Common::Setup; | ||
package Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2; | ||
# ABSTRACT: Implementation of SM-2 algorithm | ||
|
||
use Moo; | ||
use MooX::HandlesVia; | ||
|
||
use Renard::Incunabula::Common::Types qw(ArrayRef ConsumerOf); | ||
|
||
use Time::Piece; | ||
use Time::Seconds; | ||
use Math::Round; | ||
use List::AllUtils qw(max); | ||
|
||
use constant CARD_ROLE => 'Renard::Incunabula::Flashcard::Model::Card::Role::Scheduler::SuperMemo::SM2'; | ||
|
||
use constant INTERVAL_REPETITION_STEP_1 => 1 * ONE_DAY; | ||
use constant INTERVAL_REPETITION_STEP_2 => 6 * ONE_DAY; | ||
use constant MINIMUM_EASINESS_FACTOR => 1.3; | ||
|
||
has _review_queue => ( | ||
is => 'ro', | ||
default => sub { [] }, | ||
isa => ArrayRef[ConsumerOf[ CARD_ROLE ]], | ||
handles_via => 'Array', | ||
handles => { | ||
_add_to_review_queue => 'push', | ||
_remove_from_review_queue => 'shift', | ||
review_count => 'count', | ||
} | ||
); | ||
|
||
has _done_queue => ( | ||
is => 'ro', | ||
default => sub { [] }, | ||
isa => ArrayRef[ConsumerOf[ CARD_ROLE ]], | ||
handles_via => 'Array', | ||
handles => { | ||
_add_to_done_queue => 'push', | ||
} | ||
); | ||
|
||
|
||
=method get_card | ||
Returns a card from the scheduler. | ||
=cut | ||
method get_card() { | ||
$self->_remove_from_review_queue; | ||
} | ||
|
||
=method add_card | ||
Adds card to review queue. | ||
=cut | ||
method add_card( $card ) { | ||
if( ! $card->does( CARD_ROLE ) ) { | ||
Role::Tiny->apply_roles_to_object( $card, CARD_ROLE ); | ||
} | ||
|
||
$self->_add_to_review_queue( $card ); | ||
} | ||
|
||
=method answer_card | ||
Answers the card and places it on either the review queue or done queue based | ||
on the response. | ||
=cut | ||
method answer_card( $card, $response ) { | ||
# After each repetition session of a given day repeat again all items | ||
# that scored below four in the quality assessment. Continue the | ||
# repetitions until all of these items score at least four. | ||
$card->sm2_data( | ||
$self->process_param( $card->sm2_data, $response ) | ||
); | ||
|
||
if( $response < 4 ) { | ||
$self->_add_to_review_queue( $card ); | ||
} else { | ||
$self->_add_to_done_queue( $card ); | ||
} | ||
} | ||
|
||
=method process_param | ||
Given algorithm parameters and a response, returns the next algorithm parameters. | ||
=cut | ||
method process_param( $param, $quality ) { | ||
my $new_param = Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Param->new; | ||
|
||
$quality = 0 + $quality if( ref $quality && $quality->isa('Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Response') ); | ||
|
||
if( $quality >= 3 ) { | ||
if( $param->repetitions == 0 ) { | ||
# First repetition: | ||
# I(1) = 1. | ||
$new_param->interval( INTERVAL_REPETITION_STEP_1 ); | ||
} elsif( $param->repetitions == 1 ) { | ||
# Second repetition: | ||
# I(2) = 6. | ||
$new_param->interval( INTERVAL_REPETITION_STEP_2 ); | ||
} else { | ||
# Repetitions greater than the second: | ||
# I(n) = I(n-1) * EF where n > 2. | ||
my $interval_raw = $param->interval * $param->easiness_factor; | ||
# Round interval up to the next highest day. | ||
my $interval_round_day = Math::Round::nhimult( ONE_DAY, $interval_raw ); | ||
|
||
$new_param->interval( $interval_round_day ) | ||
} | ||
|
||
# increment repetitions | ||
$new_param->repetitions( $param->repetitions + 1 ); | ||
} elsif( $quality < 3 ) { | ||
$new_param->repetitions( 0 ); | ||
$new_param->interval( INTERVAL_REPETITION_STEP_1 ); | ||
} | ||
|
||
# when $quality == 4, the easiness_factor does not change | ||
$new_param->easiness_factor( $param->easiness_factor + (0.1 - (5 - $quality) * (0.08 + (5 - $quality ) * 0.02)) ); | ||
# easiness_factor must be at least MINIMUM_EASINESS_FACTOR | ||
$new_param->easiness_factor( max(MINIMUM_EASINESS_FACTOR, $new_param->easiness_factor) ); | ||
|
||
$new_param; | ||
} | ||
|
||
1; | ||
=head1 SEE ALSO | ||
=begin :list | ||
* L<SuperMemo 2: Algorithm|https://www.supermemo.com/english/ol/sm2.htm> | ||
* L<SuperMemo 2: Delphi source code|https://www.supermemo.com/english/ol/sm2source.htm> | ||
=end :list | ||
=cut |
66 changes: 66 additions & 0 deletions
66
lib/Renard/Incunabula/Flashcard/Scheduler/SuperMemo/SM2/Param.pm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
use Renard::Incunabula::Common::Setup; | ||
package Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Param; | ||
# ABSTRACT: SM-2 algorithm parameters | ||
|
||
use Moo; | ||
use MooX::StrictConstructor; | ||
|
||
use constant INITIAL_EASINESS_FACTOR => 2.5; | ||
|
||
=attr interval | ||
This is the inter-repetition interval (I(n)). | ||
It is the interval to use from the previous time the card was answered to time | ||
that the next repetition is due. This is in seconds and is usually rounded up | ||
to the closest day. | ||
From SuperMemo 2 description: | ||
I(n) - inter-repetition interval after the n-th repetition (in days) | ||
=cut | ||
has interval => ( | ||
is => 'rw', | ||
default => sub { 0 }, | ||
); | ||
|
||
=attr repetitions | ||
The count of number of repetitions done (n). | ||
=cut | ||
has repetitions => ( | ||
is => 'rw', | ||
default => sub { 0 }, | ||
); | ||
|
||
=attr easiness_factor | ||
The easiness factor (EF) for the item where a lower EF indicates a more | ||
difficult item. | ||
From SuperMemo 2 description: | ||
EF - easiness factor reflecting the easiness of memorizing and retaining a given item in memory (later called the E-Factor). | ||
=cut | ||
has easiness_factor => ( | ||
is => 'rw', | ||
default => sub { INITIAL_EASINESS_FACTOR }, | ||
); | ||
|
||
1; | ||
|
||
=begin :header | ||
=begin stopwords | ||
EF | ||
=end stopwords | ||
=end :header | ||
=cut |
41 changes: 41 additions & 0 deletions
41
lib/Renard/Incunabula/Flashcard/Scheduler/SuperMemo/SM2/Response.pm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
use Renard::Incunabula::Common::Setup; | ||
package Renard::Incunabula::Flashcard::Scheduler::SuperMemo::SM2::Response; | ||
# ABSTRACT: Quality of response | ||
|
||
=head1 DESCRIPTION | ||
=begin :list | ||
* correct_perfect : 5 - perfect response | ||
* correct_hesitation : 4 - correct response after a hesitation | ||
* correct_difficult : 3 - correct response recalled with serious difficulty | ||
* incorrect_easy : 2 - incorrect response; where the correct one seemed easy to recall | ||
* incorrect_remembered : 1 - incorrect response; the correct one remembered | ||
* blackout : 0 - complete blackout. | ||
=end :list | ||
=cut | ||
|
||
=head1 PREDICATES | ||
=method is_blackout | ||
=method is_correct_difficult | ||
=method is_correct_hesitation | ||
=method is_correct_perfect | ||
=method is_incorrect_easy | ||
=method is_incorrect_remembered | ||
=cut | ||
|
||
|
||
use Class::Type::Enum values => { | ||
blackout => 0, | ||
incorrect_remembered => 1, | ||
incorrect_easy => 2, | ||
correct_difficult => 3, | ||
correct_hesitation => 4, | ||
correct_perfect => 5, | ||
}; | ||
|
||
1; |
Oops, something went wrong.