Skip to content

Commit

Permalink
Merge pull request #1 from project-renard/algorithm-sm2
Browse files Browse the repository at this point in the history
Implement SuperMemo 2 algorithm
  • Loading branch information
zmughal committed Jun 18, 2018
2 parents ead9be9 + ae8545f commit 5298be9
Show file tree
Hide file tree
Showing 5 changed files with 506 additions and 0 deletions.
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 lib/Renard/Incunabula/Flashcard/Scheduler/SuperMemo/SM2.pm
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 lib/Renard/Incunabula/Flashcard/Scheduler/SuperMemo/SM2/Param.pm
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
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;

0 comments on commit 5298be9

Please sign in to comment.