fsrs
py-fsrs
Py-FSRS is the official Python implementation of the FSRS scheduler algorithm, which can be used to develop spaced repetition systems.
1""" 2py-fsrs 3------- 4 5Py-FSRS is the official Python implementation of the FSRS scheduler algorithm, which can be used to develop spaced repetition systems. 6""" 7 8from typing import TYPE_CHECKING 9 10from fsrs.card import Card 11from fsrs.rating import Rating 12from fsrs.review_log import ReviewLog 13from fsrs.scheduler import Scheduler 14from fsrs.state import State 15 16if TYPE_CHECKING: 17 from fsrs.optimizer import Optimizer 18 19 20# lazy load the Optimizer module due to heavy dependencies 21def __getattr__(name: str) -> type: 22 if name == "Optimizer": 23 global Optimizer 24 from fsrs.optimizer import Optimizer 25 26 return Optimizer 27 raise AttributeError 28 29 30__all__ = ["Card", "Optimizer", "Rating", "ReviewLog", "Scheduler", "State"]
40@dataclass(init=False) 41class Card: 42 """ 43 Represents a flashcard in the FSRS system. 44 45 Attributes: 46 card_id: The id of the card. Defaults to the epoch milliseconds of when the card was created. 47 state: The card's current learning state. 48 step: The card's current learning or relearning step or None if the card is in the Review state. 49 stability: Core mathematical parameter used for future scheduling. 50 difficulty: Core mathematical parameter used for future scheduling. 51 due: The date and time when the card is due next. 52 last_review: The date and time of the card's last review. 53 """ 54 55 card_id: int 56 state: State 57 step: int | None 58 stability: float | None 59 difficulty: float | None 60 due: datetime 61 last_review: datetime | None 62 63 def __init__( 64 self, 65 card_id: int | None = None, 66 state: State = State.Learning, 67 step: int | None = None, 68 stability: float | None = None, 69 difficulty: float | None = None, 70 due: datetime | None = None, 71 last_review: datetime | None = None, 72 ) -> None: 73 if card_id is None: 74 # epoch milliseconds of when the card was created 75 card_id = int(datetime.now(timezone.utc).timestamp() * 1000) 76 # wait 1ms to prevent potential card_id collision on next Card creation 77 time.sleep(0.001) 78 self.card_id = card_id 79 80 self.state = state 81 82 if self.state == State.Learning and step is None: 83 step = 0 84 self.step = step 85 86 self.stability = stability 87 self.difficulty = difficulty 88 89 if due is None: 90 due = datetime.now(timezone.utc) 91 self.due = due 92 93 self.last_review = last_review 94 95 def to_dict(self) -> CardDict: 96 """ 97 Returns a dictionary representation of the Card object. 98 99 Returns: 100 CardDict: A dictionary representation of the Card object. 101 """ 102 103 return { 104 "card_id": self.card_id, 105 "state": self.state.value, 106 "step": self.step, 107 "stability": self.stability, 108 "difficulty": self.difficulty, 109 "due": self.due.isoformat(), 110 "last_review": self.last_review.isoformat() if self.last_review else None, 111 } 112 113 @classmethod 114 def from_dict(cls, source_dict: CardDict) -> Self: 115 """ 116 Creates a Card object from an existing dictionary. 117 118 Args: 119 source_dict: A dictionary representing an existing Card object. 120 121 Returns: 122 Self: A Card object created from the provided dictionary. 123 """ 124 125 return cls( 126 card_id=int(source_dict["card_id"]), 127 state=State(int(source_dict["state"])), 128 step=source_dict["step"], 129 stability=( 130 float(source_dict["stability"]) if source_dict["stability"] else None 131 ), 132 difficulty=( 133 float(source_dict["difficulty"]) if source_dict["difficulty"] else None 134 ), 135 due=datetime.fromisoformat(source_dict["due"]), 136 last_review=( 137 datetime.fromisoformat(source_dict["last_review"]) 138 if source_dict["last_review"] 139 else None 140 ), 141 ) 142 143 def to_json(self, indent: int | str | None = None) -> str: 144 """ 145 Returns a JSON-serialized string of the Card object. 146 147 Args: 148 indent: Equivalent argument to the indent in json.dumps() 149 150 Returns: 151 str: A JSON-serialized string of the Card object. 152 """ 153 return json.dumps(self.to_dict(), indent=indent) 154 155 @classmethod 156 def from_json(cls, source_json: str) -> Self: 157 """ 158 Creates a Card object from a JSON-serialized string. 159 160 Args: 161 source_json: A JSON-serialized string of an existing Card object. 162 163 Returns: 164 Self: A Card object created from the JSON string. 165 """ 166 167 source_dict: CardDict = json.loads(source_json) 168 return cls.from_dict(source_dict=source_dict)
Represents a flashcard in the FSRS system.
Attributes: card_id: The id of the card. Defaults to the epoch milliseconds of when the card was created. state: The card's current learning state. step: The card's current learning or relearning step or None if the card is in the Review state. stability: Core mathematical parameter used for future scheduling. difficulty: Core mathematical parameter used for future scheduling. due: The date and time when the card is due next. last_review: The date and time of the card's last review.
63 def __init__( 64 self, 65 card_id: int | None = None, 66 state: State = State.Learning, 67 step: int | None = None, 68 stability: float | None = None, 69 difficulty: float | None = None, 70 due: datetime | None = None, 71 last_review: datetime | None = None, 72 ) -> None: 73 if card_id is None: 74 # epoch milliseconds of when the card was created 75 card_id = int(datetime.now(timezone.utc).timestamp() * 1000) 76 # wait 1ms to prevent potential card_id collision on next Card creation 77 time.sleep(0.001) 78 self.card_id = card_id 79 80 self.state = state 81 82 if self.state == State.Learning and step is None: 83 step = 0 84 self.step = step 85 86 self.stability = stability 87 self.difficulty = difficulty 88 89 if due is None: 90 due = datetime.now(timezone.utc) 91 self.due = due 92 93 self.last_review = last_review
95 def to_dict(self) -> CardDict: 96 """ 97 Returns a dictionary representation of the Card object. 98 99 Returns: 100 CardDict: A dictionary representation of the Card object. 101 """ 102 103 return { 104 "card_id": self.card_id, 105 "state": self.state.value, 106 "step": self.step, 107 "stability": self.stability, 108 "difficulty": self.difficulty, 109 "due": self.due.isoformat(), 110 "last_review": self.last_review.isoformat() if self.last_review else None, 111 }
Returns a dictionary representation of the Card object.
Returns: CardDict: A dictionary representation of the Card object.
113 @classmethod 114 def from_dict(cls, source_dict: CardDict) -> Self: 115 """ 116 Creates a Card object from an existing dictionary. 117 118 Args: 119 source_dict: A dictionary representing an existing Card object. 120 121 Returns: 122 Self: A Card object created from the provided dictionary. 123 """ 124 125 return cls( 126 card_id=int(source_dict["card_id"]), 127 state=State(int(source_dict["state"])), 128 step=source_dict["step"], 129 stability=( 130 float(source_dict["stability"]) if source_dict["stability"] else None 131 ), 132 difficulty=( 133 float(source_dict["difficulty"]) if source_dict["difficulty"] else None 134 ), 135 due=datetime.fromisoformat(source_dict["due"]), 136 last_review=( 137 datetime.fromisoformat(source_dict["last_review"]) 138 if source_dict["last_review"] 139 else None 140 ), 141 )
Creates a Card object from an existing dictionary.
Args: source_dict: A dictionary representing an existing Card object.
Returns: Self: A Card object created from the provided dictionary.
143 def to_json(self, indent: int | str | None = None) -> str: 144 """ 145 Returns a JSON-serialized string of the Card object. 146 147 Args: 148 indent: Equivalent argument to the indent in json.dumps() 149 150 Returns: 151 str: A JSON-serialized string of the Card object. 152 """ 153 return json.dumps(self.to_dict(), indent=indent)
Returns a JSON-serialized string of the Card object.
Args: indent: Equivalent argument to the indent in json.dumps()
Returns: str: A JSON-serialized string of the Card object.
155 @classmethod 156 def from_json(cls, source_json: str) -> Self: 157 """ 158 Creates a Card object from a JSON-serialized string. 159 160 Args: 161 source_json: A JSON-serialized string of an existing Card object. 162 163 Returns: 164 Self: A Card object created from the JSON string. 165 """ 166 167 source_dict: CardDict = json.loads(source_json) 168 return cls.from_dict(source_dict=source_dict)
Creates a Card object from a JSON-serialized string.
Args: source_json: A JSON-serialized string of an existing Card object.
Returns: Self: A Card object created from the JSON string.
5class Rating(IntEnum): 6 """ 7 Enum representing the four possible ratings when reviewing a card. 8 """ 9 10 Again = 1 11 Hard = 2 12 Good = 3 13 Easy = 4
Enum representing the four possible ratings when reviewing a card.
36@dataclass 37class ReviewLog: 38 """ 39 Represents the log entry of a Card object that has been reviewed. 40 41 Attributes: 42 card_id: The id of the card being reviewed. 43 rating: The rating given to the card during the review. 44 review_datetime: The date and time of the review. 45 review_duration: The number of milliseconds it took to review the card or None if unspecified. 46 """ 47 48 card_id: int 49 rating: Rating 50 review_datetime: datetime 51 review_duration: int | None 52 53 def to_dict( 54 self, 55 ) -> ReviewLogDict: 56 """ 57 Returns a dictionary representation of the ReviewLog object. 58 59 Returns: 60 ReviewLogDict: A dictionary representation of the ReviewLog object. 61 """ 62 63 return { 64 "card_id": self.card_id, 65 "rating": int(self.rating), 66 "review_datetime": self.review_datetime.isoformat(), 67 "review_duration": self.review_duration, 68 } 69 70 @classmethod 71 def from_dict( 72 cls, 73 source_dict: ReviewLogDict, 74 ) -> Self: 75 """ 76 Creates a ReviewLog object from an existing dictionary. 77 78 Args: 79 source_dict: A dictionary representing an existing ReviewLog object. 80 81 Returns: 82 Self: A ReviewLog object created from the provided dictionary. 83 """ 84 85 return cls( 86 card_id=source_dict["card_id"], 87 rating=Rating(int(source_dict["rating"])), 88 review_datetime=datetime.fromisoformat(source_dict["review_datetime"]), 89 review_duration=source_dict["review_duration"], 90 ) 91 92 def to_json(self, indent: int | str | None = None) -> str: 93 """ 94 Returns a JSON-serialized string of the ReviewLog object. 95 96 Args: 97 indent: Equivalent argument to the indent in json.dumps() 98 99 Returns: 100 str: A JSON-serialized string of the ReviewLog object. 101 """ 102 103 return json.dumps(self.to_dict(), indent=indent) 104 105 @classmethod 106 def from_json(cls, source_json: str) -> Self: 107 """ 108 Creates a ReviewLog object from a JSON-serialized string. 109 110 Args: 111 source_json: A JSON-serialized string of an existing ReviewLog object. 112 113 Returns: 114 Self: A ReviewLog object created from the JSON string. 115 """ 116 117 source_dict: ReviewLogDict = json.loads(source_json) 118 return cls.from_dict(source_dict=source_dict)
Represents the log entry of a Card object that has been reviewed.
Attributes: card_id: The id of the card being reviewed. rating: The rating given to the card during the review. review_datetime: The date and time of the review. review_duration: The number of milliseconds it took to review the card or None if unspecified.
53 def to_dict( 54 self, 55 ) -> ReviewLogDict: 56 """ 57 Returns a dictionary representation of the ReviewLog object. 58 59 Returns: 60 ReviewLogDict: A dictionary representation of the ReviewLog object. 61 """ 62 63 return { 64 "card_id": self.card_id, 65 "rating": int(self.rating), 66 "review_datetime": self.review_datetime.isoformat(), 67 "review_duration": self.review_duration, 68 }
Returns a dictionary representation of the ReviewLog object.
Returns: ReviewLogDict: A dictionary representation of the ReviewLog object.
70 @classmethod 71 def from_dict( 72 cls, 73 source_dict: ReviewLogDict, 74 ) -> Self: 75 """ 76 Creates a ReviewLog object from an existing dictionary. 77 78 Args: 79 source_dict: A dictionary representing an existing ReviewLog object. 80 81 Returns: 82 Self: A ReviewLog object created from the provided dictionary. 83 """ 84 85 return cls( 86 card_id=source_dict["card_id"], 87 rating=Rating(int(source_dict["rating"])), 88 review_datetime=datetime.fromisoformat(source_dict["review_datetime"]), 89 review_duration=source_dict["review_duration"], 90 )
Creates a ReviewLog object from an existing dictionary.
Args: source_dict: A dictionary representing an existing ReviewLog object.
Returns: Self: A ReviewLog object created from the provided dictionary.
92 def to_json(self, indent: int | str | None = None) -> str: 93 """ 94 Returns a JSON-serialized string of the ReviewLog object. 95 96 Args: 97 indent: Equivalent argument to the indent in json.dumps() 98 99 Returns: 100 str: A JSON-serialized string of the ReviewLog object. 101 """ 102 103 return json.dumps(self.to_dict(), indent=indent)
Returns a JSON-serialized string of the ReviewLog object.
Args: indent: Equivalent argument to the indent in json.dumps()
Returns: str: A JSON-serialized string of the ReviewLog object.
105 @classmethod 106 def from_json(cls, source_json: str) -> Self: 107 """ 108 Creates a ReviewLog object from a JSON-serialized string. 109 110 Args: 111 source_json: A JSON-serialized string of an existing ReviewLog object. 112 113 Returns: 114 Self: A ReviewLog object created from the JSON string. 115 """ 116 117 source_dict: ReviewLogDict = json.loads(source_json) 118 return cls.from_dict(source_dict=source_dict)
Creates a ReviewLog object from a JSON-serialized string.
Args: source_json: A JSON-serialized string of an existing ReviewLog object.
Returns: Self: A ReviewLog object created from the JSON string.
142@dataclass(init=False) 143class Scheduler: 144 """ 145 The FSRS scheduler. 146 147 Enables the reviewing and future scheduling of cards according to the FSRS algorithm. 148 149 Attributes: 150 parameters: The model weights of the FSRS scheduler. 151 desired_retention: The desired retention rate of cards scheduled with the scheduler. 152 learning_steps: Small time intervals that schedule cards in the Learning state. 153 relearning_steps: Small time intervals that schedule cards in the Relearning state. 154 maximum_interval: The maximum number of days a Review-state card can be scheduled into the future. 155 enable_fuzzing: Whether to apply a small amount of random 'fuzz' to calculated intervals. 156 """ 157 158 parameters: tuple[float, ...] 159 desired_retention: float 160 learning_steps: tuple[timedelta, ...] 161 relearning_steps: tuple[timedelta, ...] 162 maximum_interval: int 163 enable_fuzzing: bool 164 165 def __init__( 166 self, 167 parameters: Sequence[float] = DEFAULT_PARAMETERS, 168 desired_retention: float = 0.9, 169 learning_steps: tuple[timedelta, ...] | list[timedelta] = ( 170 timedelta(minutes=1), 171 timedelta(minutes=10), 172 ), 173 relearning_steps: tuple[timedelta, ...] | list[timedelta] = ( 174 timedelta(minutes=10), 175 ), 176 maximum_interval: int = 36500, 177 enable_fuzzing: bool = True, 178 ) -> None: 179 self._validate_parameters(parameters=parameters) 180 181 self.parameters = tuple(parameters) 182 self.desired_retention = desired_retention 183 self.learning_steps = tuple(learning_steps) 184 self.relearning_steps = tuple(relearning_steps) 185 self.maximum_interval = maximum_interval 186 self.enable_fuzzing = enable_fuzzing 187 188 self._DECAY = -self.parameters[20] 189 self._FACTOR = 0.9 ** (1 / self._DECAY) - 1 190 191 def _validate_parameters(self, *, parameters: Sequence[float]) -> None: 192 if len(parameters) != len(LOWER_BOUNDS_PARAMETERS): 193 raise ValueError( 194 f"Expected {len(LOWER_BOUNDS_PARAMETERS)} parameters, got {len(parameters)}." 195 ) 196 197 error_messages = [] 198 for index, (parameter, lower_bound, upper_bound) in enumerate( 199 zip(parameters, LOWER_BOUNDS_PARAMETERS, UPPER_BOUNDS_PARAMETERS) 200 ): 201 if not lower_bound <= parameter <= upper_bound: 202 error_message = f"parameters[{index}] = {parameter} is out of bounds: ({lower_bound}, {upper_bound})" 203 error_messages.append(error_message) 204 205 if len(error_messages) > 0: 206 raise ValueError( 207 "One or more parameters are out of bounds:\n" 208 + "\n".join(error_messages) 209 ) 210 211 def get_card_retrievability( 212 self, card: Card, current_datetime: datetime | None = None 213 ) -> float: 214 """ 215 Calculates a Card object's current retrievability for a given date and time. 216 217 The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime. 218 219 Args: 220 card: The card whose retrievability is to be calculated 221 current_datetime: The current date and time 222 223 Returns: 224 float: The retrievability of the Card object. 225 """ 226 227 if card.last_review is None or card.stability is None: 228 return 0 229 230 if current_datetime is None: 231 current_datetime = datetime.now(timezone.utc) 232 233 elapsed_days = max(0, (current_datetime - card.last_review).days) 234 235 return (1 + self._FACTOR * elapsed_days / card.stability) ** self._DECAY 236 237 def review_card( 238 self, 239 card: Card, 240 rating: Rating, 241 review_datetime: datetime | None = None, 242 review_duration: int | None = None, 243 ) -> tuple[Card, ReviewLog]: 244 """ 245 Reviews a card with a given rating at a given time for a specified duration. 246 247 Args: 248 card: The card being reviewed. 249 rating: The chosen rating for the card being reviewed. 250 review_datetime: The date and time of the review. 251 review_duration: The number of miliseconds it took to review the card or None if unspecified. 252 253 Returns: 254 tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log. 255 256 Raises: 257 ValueError: If the `review_datetime` argument is not timezone-aware and set to UTC. 258 """ 259 260 if review_datetime is not None and ( 261 (review_datetime.tzinfo is None) or (review_datetime.tzinfo != timezone.utc) 262 ): 263 raise ValueError("datetime must be timezone-aware and set to UTC") 264 265 card = copy(card) 266 267 if review_datetime is None: 268 review_datetime = datetime.now(timezone.utc) 269 270 days_since_last_review = ( 271 (review_datetime - card.last_review).days if card.last_review else None 272 ) 273 274 match card.state: 275 case State.Learning: 276 assert card.step is not None 277 278 # update the card's stability and difficulty 279 if card.stability is None or card.difficulty is None: 280 card.stability = self._initial_stability(rating=rating) 281 card.difficulty = self._initial_difficulty( 282 rating=rating, clamp=True 283 ) 284 285 elif days_since_last_review is not None and days_since_last_review < 1: 286 card.stability = self._short_term_stability( 287 stability=card.stability, rating=rating 288 ) 289 card.difficulty = self._next_difficulty( 290 difficulty=card.difficulty, rating=rating 291 ) 292 293 else: 294 card.stability = self._next_stability( 295 difficulty=card.difficulty, 296 stability=card.stability, 297 retrievability=self.get_card_retrievability( 298 card, 299 current_datetime=review_datetime, 300 ), 301 rating=rating, 302 ) 303 card.difficulty = self._next_difficulty( 304 difficulty=card.difficulty, rating=rating 305 ) 306 307 # calculate the card's next interval 308 ## first if-clause handles edge case where the Card in the Learning state was previously 309 ## scheduled with a Scheduler with more learning_steps than the current Scheduler 310 if len(self.learning_steps) == 0 or ( 311 card.step >= len(self.learning_steps) 312 and rating in (Rating.Hard, Rating.Good, Rating.Easy) 313 ): 314 card.state = State.Review 315 card.step = None 316 317 next_interval_days = self._next_interval(stability=card.stability) 318 next_interval = timedelta(days=next_interval_days) 319 320 else: 321 match rating: 322 case Rating.Again: 323 card.step = 0 324 next_interval = self.learning_steps[card.step] 325 326 case Rating.Hard: 327 # card step stays the same 328 329 if card.step == 0 and len(self.learning_steps) == 1: 330 next_interval = self.learning_steps[0] * 1.5 331 elif card.step == 0 and len(self.learning_steps) >= 2: 332 next_interval = ( 333 self.learning_steps[0] + self.learning_steps[1] 334 ) / 2.0 335 else: 336 next_interval = self.learning_steps[card.step] 337 338 case Rating.Good: 339 if card.step + 1 == len( 340 self.learning_steps 341 ): # the last step 342 card.state = State.Review 343 card.step = None 344 345 next_interval_days = self._next_interval( 346 stability=card.stability 347 ) 348 next_interval = timedelta(days=next_interval_days) 349 350 else: 351 card.step += 1 352 next_interval = self.learning_steps[card.step] 353 354 case Rating.Easy: 355 card.state = State.Review 356 card.step = None 357 358 next_interval_days = self._next_interval( 359 stability=card.stability 360 ) 361 next_interval = timedelta(days=next_interval_days) 362 363 case _: 364 raise ValueError(f"Unknown rating: {rating}") 365 366 case State.Review: 367 assert card.stability is not None 368 assert card.difficulty is not None 369 370 # update the card's stability and difficulty 371 if days_since_last_review is not None and days_since_last_review < 1: 372 card.stability = self._short_term_stability( 373 stability=card.stability, rating=rating 374 ) 375 else: 376 card.stability = self._next_stability( 377 difficulty=card.difficulty, 378 stability=card.stability, 379 retrievability=self.get_card_retrievability( 380 card, 381 current_datetime=review_datetime, 382 ), 383 rating=rating, 384 ) 385 386 card.difficulty = self._next_difficulty( 387 difficulty=card.difficulty, rating=rating 388 ) 389 390 # calculate the card's next interval 391 match rating: 392 case Rating.Again: 393 # if there are no relearning steps (they were left blank) 394 if len(self.relearning_steps) == 0: 395 next_interval_days = self._next_interval( 396 stability=card.stability 397 ) 398 next_interval = timedelta(days=next_interval_days) 399 400 else: 401 card.state = State.Relearning 402 card.step = 0 403 404 next_interval = self.relearning_steps[card.step] 405 406 case Rating.Hard | Rating.Good | Rating.Easy: 407 next_interval_days = self._next_interval( 408 stability=card.stability 409 ) 410 next_interval = timedelta(days=next_interval_days) 411 412 case _: 413 raise ValueError(f"Unknown rating: {rating}") 414 415 case State.Relearning: 416 assert card.stability is not None 417 assert card.difficulty is not None 418 assert card.step is not None 419 420 # update the card's stability and difficulty 421 if days_since_last_review is not None and days_since_last_review < 1: 422 card.stability = self._short_term_stability( 423 stability=card.stability, rating=rating 424 ) 425 card.difficulty = self._next_difficulty( 426 difficulty=card.difficulty, rating=rating 427 ) 428 429 else: 430 card.stability = self._next_stability( 431 difficulty=card.difficulty, 432 stability=card.stability, 433 retrievability=self.get_card_retrievability( 434 card, 435 current_datetime=review_datetime, 436 ), 437 rating=rating, 438 ) 439 card.difficulty = self._next_difficulty( 440 difficulty=card.difficulty, rating=rating 441 ) 442 443 # calculate the card's next interval 444 ## first if-clause handles edge case where the Card in the Relearning state was previously 445 ## scheduled with a Scheduler with more relearning_steps than the current Scheduler 446 if len(self.relearning_steps) == 0 or ( 447 card.step >= len(self.relearning_steps) 448 and rating in (Rating.Hard, Rating.Good, Rating.Easy) 449 ): 450 card.state = State.Review 451 card.step = None 452 453 next_interval_days = self._next_interval(stability=card.stability) 454 next_interval = timedelta(days=next_interval_days) 455 456 else: 457 match rating: 458 case Rating.Again: 459 card.step = 0 460 next_interval = self.relearning_steps[card.step] 461 462 case Rating.Hard: 463 # card step stays the same 464 465 if card.step == 0 and len(self.relearning_steps) == 1: 466 next_interval = self.relearning_steps[0] * 1.5 467 elif card.step == 0 and len(self.relearning_steps) >= 2: 468 next_interval = ( 469 self.relearning_steps[0] + self.relearning_steps[1] 470 ) / 2.0 471 else: 472 next_interval = self.relearning_steps[card.step] 473 474 case Rating.Good: 475 if card.step + 1 == len( 476 self.relearning_steps 477 ): # the last step 478 card.state = State.Review 479 card.step = None 480 481 next_interval_days = self._next_interval( 482 stability=card.stability 483 ) 484 next_interval = timedelta(days=next_interval_days) 485 486 else: 487 card.step += 1 488 next_interval = self.relearning_steps[card.step] 489 490 case Rating.Easy: 491 card.state = State.Review 492 card.step = None 493 494 next_interval_days = self._next_interval( 495 stability=card.stability 496 ) 497 next_interval = timedelta(days=next_interval_days) 498 499 case _: 500 raise ValueError(f"Unknown rating: {rating}") 501 502 case _: 503 raise ValueError(f"Unknown card state: {card.state}") 504 505 if self.enable_fuzzing and card.state == State.Review: 506 next_interval = self._get_fuzzed_interval(interval=next_interval) 507 508 card.due = review_datetime + next_interval 509 card.last_review = review_datetime 510 511 review_log = ReviewLog( 512 card_id=card.card_id, 513 rating=rating, 514 review_datetime=review_datetime, 515 review_duration=review_duration, 516 ) 517 518 return card, review_log 519 520 def reschedule_card(self, card: Card, review_logs: list[ReviewLog]) -> Card: 521 """ 522 Reschedules/updates the given card with the current scheduler provided that card's review logs. 523 524 If the current card was previously scheduled with a different scheduler, you may want to reschedule/update 525 it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule 526 each of your cards with a new scheduler after computing the optimal parameters with the Optimizer. 527 528 Args: 529 card: The card to be rescheduled/updated. 530 review_logs: A list of that card's review logs (order doesn't matter). 531 532 Returns: 533 Card: A new card that has been rescheduled/updated with this current scheduler. 534 535 Raises: 536 ValueError: If any of the review logs are for a card other than the one specified, this will raise an error. 537 538 """ 539 540 for review_log in review_logs: 541 if review_log.card_id != card.card_id: 542 raise ValueError( 543 f"ReviewLog card_id {review_log.card_id} does not match Card card_id {card.card_id}" 544 ) 545 546 review_logs = sorted(review_logs, key=lambda log: log.review_datetime) 547 548 rescheduled_card = Card(card_id=card.card_id, due=card.due) 549 550 for review_log in review_logs: 551 rescheduled_card, _ = self.review_card( 552 card=rescheduled_card, 553 rating=review_log.rating, 554 review_datetime=review_log.review_datetime, 555 ) 556 557 return rescheduled_card 558 559 def to_dict( 560 self, 561 ) -> SchedulerDict: 562 """ 563 Returns a dictionary representation of the Scheduler object. 564 565 Returns: 566 SchedulerDict: A dictionary representation of the Scheduler object. 567 """ 568 569 return { 570 "parameters": list(self.parameters), 571 "desired_retention": self.desired_retention, 572 "learning_steps": [ 573 int(learning_step.total_seconds()) 574 for learning_step in self.learning_steps 575 ], 576 "relearning_steps": [ 577 int(relearning_step.total_seconds()) 578 for relearning_step in self.relearning_steps 579 ], 580 "maximum_interval": self.maximum_interval, 581 "enable_fuzzing": self.enable_fuzzing, 582 } 583 584 @classmethod 585 def from_dict(cls, source_dict: SchedulerDict) -> Self: 586 """ 587 Creates a Scheduler object from an existing dictionary. 588 589 Args: 590 source_dict: A dictionary representing an existing Scheduler object. 591 592 Returns: 593 Self: A Scheduler object created from the provided dictionary. 594 """ 595 596 return cls( 597 parameters=source_dict["parameters"], 598 desired_retention=source_dict["desired_retention"], 599 learning_steps=[ 600 timedelta(seconds=learning_step) 601 for learning_step in source_dict["learning_steps"] 602 ], 603 relearning_steps=[ 604 timedelta(seconds=relearning_step) 605 for relearning_step in source_dict["relearning_steps"] 606 ], 607 maximum_interval=source_dict["maximum_interval"], 608 enable_fuzzing=source_dict["enable_fuzzing"], 609 ) 610 611 def to_json(self, indent: int | str | None = None) -> str: 612 """ 613 Returns a JSON-serialized string of the Scheduler object. 614 615 Args: 616 indent: Equivalent argument to the indent in json.dumps() 617 618 Returns: 619 str: A JSON-serialized string of the Scheduler object. 620 """ 621 622 return json.dumps(self.to_dict(), indent=indent) 623 624 @classmethod 625 def from_json(cls, source_json: str) -> Self: 626 """ 627 Creates a Scheduler object from a JSON-serialized string. 628 629 Args: 630 source_json: A JSON-serialized string of an existing Scheduler object. 631 632 Returns: 633 Self: A Scheduler object created from the JSON string. 634 """ 635 636 source_dict: SchedulerDict = json.loads(source_json) 637 return cls.from_dict(source_dict=source_dict) 638 639 @overload 640 def _clamp_difficulty(self, *, difficulty: float) -> float: ... 641 @overload 642 def _clamp_difficulty(self, *, difficulty: Tensor) -> Tensor: ... 643 def _clamp_difficulty(self, *, difficulty: float | Tensor) -> float | Tensor: 644 if isinstance(difficulty, (int, float)): 645 difficulty = min(max(difficulty, MIN_DIFFICULTY), MAX_DIFFICULTY) 646 else: 647 difficulty = difficulty.clamp(min=MIN_DIFFICULTY, max=MAX_DIFFICULTY) 648 649 return difficulty 650 651 @overload 652 def _clamp_stability(self, *, stability: float) -> float: ... 653 @overload 654 def _clamp_stability(self, *, stability: Tensor) -> Tensor: ... 655 def _clamp_stability(self, *, stability: float | Tensor) -> float | Tensor: 656 if isinstance(stability, (int, float)): 657 stability = max(stability, STABILITY_MIN) 658 else: 659 stability = stability.clamp(min=STABILITY_MIN) 660 661 return stability 662 663 def _initial_stability(self, *, rating: Rating) -> float: 664 initial_stability = self.parameters[rating - 1] 665 666 initial_stability = self._clamp_stability(stability=initial_stability) 667 668 return initial_stability 669 670 def _initial_difficulty(self, *, rating: Rating, clamp: bool) -> float: 671 initial_difficulty = ( 672 self.parameters[4] - (math.e ** (self.parameters[5] * (rating - 1))) + 1 673 ) 674 675 if clamp: 676 initial_difficulty = self._clamp_difficulty(difficulty=initial_difficulty) 677 678 return initial_difficulty 679 680 def _next_interval(self, *, stability: float) -> int: 681 next_interval = (stability / self._FACTOR) * ( 682 (self.desired_retention ** (1 / self._DECAY)) - 1 683 ) 684 685 if not isinstance(next_interval, (int, float)): 686 next_interval = next_interval.detach().item() 687 688 next_interval = round(next_interval) # intervals are full days 689 690 # must be at least 1 day long 691 next_interval = max(next_interval, 1) 692 693 # can not be longer than the maximum interval 694 next_interval = min(next_interval, self.maximum_interval) 695 696 return next_interval 697 698 def _short_term_stability(self, *, stability: float, rating: Rating) -> float: 699 short_term_stability_increase = ( 700 math.e ** (self.parameters[17] * (rating - 3 + self.parameters[18])) 701 ) * (stability ** -self.parameters[19]) 702 703 if rating in (Rating.Hard, Rating.Good, Rating.Easy): 704 if isinstance(short_term_stability_increase, (int, float)): 705 short_term_stability_increase = max(short_term_stability_increase, 1.0) 706 else: 707 short_term_stability_increase = short_term_stability_increase.clamp( 708 min=1.0 709 ) 710 711 short_term_stability = stability * short_term_stability_increase 712 713 short_term_stability = self._clamp_stability(stability=short_term_stability) 714 715 return short_term_stability 716 717 def _next_difficulty(self, *, difficulty: float, rating: Rating) -> float: 718 def _linear_damping(*, delta_difficulty: float, difficulty: float) -> float: 719 return (10.0 - difficulty) * delta_difficulty / 9.0 720 721 def _mean_reversion(*, arg_1: float, arg_2: float) -> float: 722 return self.parameters[7] * arg_1 + (1 - self.parameters[7]) * arg_2 723 724 arg_1 = self._initial_difficulty(rating=Rating.Easy, clamp=False) 725 726 delta_difficulty = -(self.parameters[6] * (rating - 3)) 727 arg_2 = difficulty + _linear_damping( 728 delta_difficulty=delta_difficulty, difficulty=difficulty 729 ) 730 731 next_difficulty = _mean_reversion(arg_1=arg_1, arg_2=arg_2) 732 733 next_difficulty = self._clamp_difficulty(difficulty=next_difficulty) 734 735 return next_difficulty 736 737 def _next_stability( 738 self, 739 *, 740 difficulty: float, 741 stability: float, 742 retrievability: float, 743 rating: Rating, 744 ) -> float: 745 if rating == Rating.Again: 746 next_stability = self._next_forget_stability( 747 difficulty=difficulty, 748 stability=stability, 749 retrievability=retrievability, 750 ) 751 752 elif rating in (Rating.Hard, Rating.Good, Rating.Easy): 753 next_stability = self._next_recall_stability( 754 difficulty=difficulty, 755 stability=stability, 756 retrievability=retrievability, 757 rating=rating, 758 ) 759 760 else: 761 raise ValueError(f"Unknown rating: {rating}") 762 763 next_stability = self._clamp_stability(stability=next_stability) 764 765 return next_stability 766 767 def _next_forget_stability( 768 self, *, difficulty: float, stability: float, retrievability: float 769 ) -> float: 770 next_forget_stability_long_term_params = ( 771 self.parameters[11] 772 * (difficulty ** -self.parameters[12]) 773 * (((stability + 1) ** (self.parameters[13])) - 1) 774 * (math.e ** ((1 - retrievability) * self.parameters[14])) 775 ) 776 777 next_forget_stability_short_term_params = stability / ( 778 math.e ** (self.parameters[17] * self.parameters[18]) 779 ) 780 781 return min( 782 next_forget_stability_long_term_params, 783 next_forget_stability_short_term_params, 784 ) 785 786 def _next_recall_stability( 787 self, 788 *, 789 difficulty: float, 790 stability: float, 791 retrievability: float, 792 rating: Rating, 793 ) -> float: 794 hard_penalty = self.parameters[15] if rating == Rating.Hard else 1 795 easy_bonus = self.parameters[16] if rating == Rating.Easy else 1 796 797 return stability * ( 798 1 799 + (math.e ** (self.parameters[8])) 800 * (11 - difficulty) 801 * (stability ** -self.parameters[9]) 802 * ((math.e ** ((1 - retrievability) * self.parameters[10])) - 1) 803 * hard_penalty 804 * easy_bonus 805 ) 806 807 def _get_fuzzed_interval(self, *, interval: timedelta) -> timedelta: 808 """ 809 Takes the current calculated interval and adds a small amount of random fuzz to it. 810 For example, a card that would've been due in 50 days, after fuzzing, might be due in 49, or 51 days. 811 812 Args: 813 interval: The calculated next interval, before fuzzing. 814 815 Returns: 816 timedelta: The new interval, after fuzzing. 817 """ 818 819 interval_days = interval.days 820 821 if interval_days < 2.5: # fuzz is not applied to intervals less than 2.5 822 return interval 823 824 def _get_fuzz_range(*, interval_days: int) -> tuple[int, int]: 825 """ 826 Helper function that computes the possible upper and lower bounds of the interval after fuzzing. 827 """ 828 829 delta = 1.0 830 for fuzz_range in FUZZ_RANGES: 831 delta += fuzz_range["factor"] * max( 832 min(float(interval_days), fuzz_range["end"]) - fuzz_range["start"], 833 0.0, 834 ) 835 836 min_ivl = round(interval_days - delta) 837 max_ivl = round(interval_days + delta) 838 839 # make sure the min_ivl and max_ivl fall into a valid range 840 min_ivl = max(2, min_ivl) 841 max_ivl = min(max_ivl, self.maximum_interval) 842 min_ivl = min(min_ivl, max_ivl) 843 844 return min_ivl, max_ivl 845 846 min_ivl, max_ivl = _get_fuzz_range(interval_days=interval_days) 847 848 fuzzed_interval_days = ( 849 random() * (max_ivl - min_ivl + 1) 850 ) + min_ivl # the next interval is a random value between min_ivl and max_ivl 851 852 fuzzed_interval_days = min(round(fuzzed_interval_days), self.maximum_interval) 853 854 fuzzed_interval = timedelta(days=fuzzed_interval_days) 855 856 return fuzzed_interval
The FSRS scheduler.
Enables the reviewing and future scheduling of cards according to the FSRS algorithm.
Attributes: parameters: The model weights of the FSRS scheduler. desired_retention: The desired retention rate of cards scheduled with the scheduler. learning_steps: Small time intervals that schedule cards in the Learning state. relearning_steps: Small time intervals that schedule cards in the Relearning state. maximum_interval: The maximum number of days a Review-state card can be scheduled into the future. enable_fuzzing: Whether to apply a small amount of random 'fuzz' to calculated intervals.
165 def __init__( 166 self, 167 parameters: Sequence[float] = DEFAULT_PARAMETERS, 168 desired_retention: float = 0.9, 169 learning_steps: tuple[timedelta, ...] | list[timedelta] = ( 170 timedelta(minutes=1), 171 timedelta(minutes=10), 172 ), 173 relearning_steps: tuple[timedelta, ...] | list[timedelta] = ( 174 timedelta(minutes=10), 175 ), 176 maximum_interval: int = 36500, 177 enable_fuzzing: bool = True, 178 ) -> None: 179 self._validate_parameters(parameters=parameters) 180 181 self.parameters = tuple(parameters) 182 self.desired_retention = desired_retention 183 self.learning_steps = tuple(learning_steps) 184 self.relearning_steps = tuple(relearning_steps) 185 self.maximum_interval = maximum_interval 186 self.enable_fuzzing = enable_fuzzing 187 188 self._DECAY = -self.parameters[20] 189 self._FACTOR = 0.9 ** (1 / self._DECAY) - 1
211 def get_card_retrievability( 212 self, card: Card, current_datetime: datetime | None = None 213 ) -> float: 214 """ 215 Calculates a Card object's current retrievability for a given date and time. 216 217 The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime. 218 219 Args: 220 card: The card whose retrievability is to be calculated 221 current_datetime: The current date and time 222 223 Returns: 224 float: The retrievability of the Card object. 225 """ 226 227 if card.last_review is None or card.stability is None: 228 return 0 229 230 if current_datetime is None: 231 current_datetime = datetime.now(timezone.utc) 232 233 elapsed_days = max(0, (current_datetime - card.last_review).days) 234 235 return (1 + self._FACTOR * elapsed_days / card.stability) ** self._DECAY
Calculates a Card object's current retrievability for a given date and time.
The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.
Args: card: The card whose retrievability is to be calculated current_datetime: The current date and time
Returns: float: The retrievability of the Card object.
237 def review_card( 238 self, 239 card: Card, 240 rating: Rating, 241 review_datetime: datetime | None = None, 242 review_duration: int | None = None, 243 ) -> tuple[Card, ReviewLog]: 244 """ 245 Reviews a card with a given rating at a given time for a specified duration. 246 247 Args: 248 card: The card being reviewed. 249 rating: The chosen rating for the card being reviewed. 250 review_datetime: The date and time of the review. 251 review_duration: The number of miliseconds it took to review the card or None if unspecified. 252 253 Returns: 254 tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log. 255 256 Raises: 257 ValueError: If the `review_datetime` argument is not timezone-aware and set to UTC. 258 """ 259 260 if review_datetime is not None and ( 261 (review_datetime.tzinfo is None) or (review_datetime.tzinfo != timezone.utc) 262 ): 263 raise ValueError("datetime must be timezone-aware and set to UTC") 264 265 card = copy(card) 266 267 if review_datetime is None: 268 review_datetime = datetime.now(timezone.utc) 269 270 days_since_last_review = ( 271 (review_datetime - card.last_review).days if card.last_review else None 272 ) 273 274 match card.state: 275 case State.Learning: 276 assert card.step is not None 277 278 # update the card's stability and difficulty 279 if card.stability is None or card.difficulty is None: 280 card.stability = self._initial_stability(rating=rating) 281 card.difficulty = self._initial_difficulty( 282 rating=rating, clamp=True 283 ) 284 285 elif days_since_last_review is not None and days_since_last_review < 1: 286 card.stability = self._short_term_stability( 287 stability=card.stability, rating=rating 288 ) 289 card.difficulty = self._next_difficulty( 290 difficulty=card.difficulty, rating=rating 291 ) 292 293 else: 294 card.stability = self._next_stability( 295 difficulty=card.difficulty, 296 stability=card.stability, 297 retrievability=self.get_card_retrievability( 298 card, 299 current_datetime=review_datetime, 300 ), 301 rating=rating, 302 ) 303 card.difficulty = self._next_difficulty( 304 difficulty=card.difficulty, rating=rating 305 ) 306 307 # calculate the card's next interval 308 ## first if-clause handles edge case where the Card in the Learning state was previously 309 ## scheduled with a Scheduler with more learning_steps than the current Scheduler 310 if len(self.learning_steps) == 0 or ( 311 card.step >= len(self.learning_steps) 312 and rating in (Rating.Hard, Rating.Good, Rating.Easy) 313 ): 314 card.state = State.Review 315 card.step = None 316 317 next_interval_days = self._next_interval(stability=card.stability) 318 next_interval = timedelta(days=next_interval_days) 319 320 else: 321 match rating: 322 case Rating.Again: 323 card.step = 0 324 next_interval = self.learning_steps[card.step] 325 326 case Rating.Hard: 327 # card step stays the same 328 329 if card.step == 0 and len(self.learning_steps) == 1: 330 next_interval = self.learning_steps[0] * 1.5 331 elif card.step == 0 and len(self.learning_steps) >= 2: 332 next_interval = ( 333 self.learning_steps[0] + self.learning_steps[1] 334 ) / 2.0 335 else: 336 next_interval = self.learning_steps[card.step] 337 338 case Rating.Good: 339 if card.step + 1 == len( 340 self.learning_steps 341 ): # the last step 342 card.state = State.Review 343 card.step = None 344 345 next_interval_days = self._next_interval( 346 stability=card.stability 347 ) 348 next_interval = timedelta(days=next_interval_days) 349 350 else: 351 card.step += 1 352 next_interval = self.learning_steps[card.step] 353 354 case Rating.Easy: 355 card.state = State.Review 356 card.step = None 357 358 next_interval_days = self._next_interval( 359 stability=card.stability 360 ) 361 next_interval = timedelta(days=next_interval_days) 362 363 case _: 364 raise ValueError(f"Unknown rating: {rating}") 365 366 case State.Review: 367 assert card.stability is not None 368 assert card.difficulty is not None 369 370 # update the card's stability and difficulty 371 if days_since_last_review is not None and days_since_last_review < 1: 372 card.stability = self._short_term_stability( 373 stability=card.stability, rating=rating 374 ) 375 else: 376 card.stability = self._next_stability( 377 difficulty=card.difficulty, 378 stability=card.stability, 379 retrievability=self.get_card_retrievability( 380 card, 381 current_datetime=review_datetime, 382 ), 383 rating=rating, 384 ) 385 386 card.difficulty = self._next_difficulty( 387 difficulty=card.difficulty, rating=rating 388 ) 389 390 # calculate the card's next interval 391 match rating: 392 case Rating.Again: 393 # if there are no relearning steps (they were left blank) 394 if len(self.relearning_steps) == 0: 395 next_interval_days = self._next_interval( 396 stability=card.stability 397 ) 398 next_interval = timedelta(days=next_interval_days) 399 400 else: 401 card.state = State.Relearning 402 card.step = 0 403 404 next_interval = self.relearning_steps[card.step] 405 406 case Rating.Hard | Rating.Good | Rating.Easy: 407 next_interval_days = self._next_interval( 408 stability=card.stability 409 ) 410 next_interval = timedelta(days=next_interval_days) 411 412 case _: 413 raise ValueError(f"Unknown rating: {rating}") 414 415 case State.Relearning: 416 assert card.stability is not None 417 assert card.difficulty is not None 418 assert card.step is not None 419 420 # update the card's stability and difficulty 421 if days_since_last_review is not None and days_since_last_review < 1: 422 card.stability = self._short_term_stability( 423 stability=card.stability, rating=rating 424 ) 425 card.difficulty = self._next_difficulty( 426 difficulty=card.difficulty, rating=rating 427 ) 428 429 else: 430 card.stability = self._next_stability( 431 difficulty=card.difficulty, 432 stability=card.stability, 433 retrievability=self.get_card_retrievability( 434 card, 435 current_datetime=review_datetime, 436 ), 437 rating=rating, 438 ) 439 card.difficulty = self._next_difficulty( 440 difficulty=card.difficulty, rating=rating 441 ) 442 443 # calculate the card's next interval 444 ## first if-clause handles edge case where the Card in the Relearning state was previously 445 ## scheduled with a Scheduler with more relearning_steps than the current Scheduler 446 if len(self.relearning_steps) == 0 or ( 447 card.step >= len(self.relearning_steps) 448 and rating in (Rating.Hard, Rating.Good, Rating.Easy) 449 ): 450 card.state = State.Review 451 card.step = None 452 453 next_interval_days = self._next_interval(stability=card.stability) 454 next_interval = timedelta(days=next_interval_days) 455 456 else: 457 match rating: 458 case Rating.Again: 459 card.step = 0 460 next_interval = self.relearning_steps[card.step] 461 462 case Rating.Hard: 463 # card step stays the same 464 465 if card.step == 0 and len(self.relearning_steps) == 1: 466 next_interval = self.relearning_steps[0] * 1.5 467 elif card.step == 0 and len(self.relearning_steps) >= 2: 468 next_interval = ( 469 self.relearning_steps[0] + self.relearning_steps[1] 470 ) / 2.0 471 else: 472 next_interval = self.relearning_steps[card.step] 473 474 case Rating.Good: 475 if card.step + 1 == len( 476 self.relearning_steps 477 ): # the last step 478 card.state = State.Review 479 card.step = None 480 481 next_interval_days = self._next_interval( 482 stability=card.stability 483 ) 484 next_interval = timedelta(days=next_interval_days) 485 486 else: 487 card.step += 1 488 next_interval = self.relearning_steps[card.step] 489 490 case Rating.Easy: 491 card.state = State.Review 492 card.step = None 493 494 next_interval_days = self._next_interval( 495 stability=card.stability 496 ) 497 next_interval = timedelta(days=next_interval_days) 498 499 case _: 500 raise ValueError(f"Unknown rating: {rating}") 501 502 case _: 503 raise ValueError(f"Unknown card state: {card.state}") 504 505 if self.enable_fuzzing and card.state == State.Review: 506 next_interval = self._get_fuzzed_interval(interval=next_interval) 507 508 card.due = review_datetime + next_interval 509 card.last_review = review_datetime 510 511 review_log = ReviewLog( 512 card_id=card.card_id, 513 rating=rating, 514 review_datetime=review_datetime, 515 review_duration=review_duration, 516 ) 517 518 return card, review_log
Reviews a card with a given rating at a given time for a specified duration.
Args: card: The card being reviewed. rating: The chosen rating for the card being reviewed. review_datetime: The date and time of the review. review_duration: The number of miliseconds it took to review the card or None if unspecified.
Returns: tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log.
Raises:
ValueError: If the review_datetime argument is not timezone-aware and set to UTC.
520 def reschedule_card(self, card: Card, review_logs: list[ReviewLog]) -> Card: 521 """ 522 Reschedules/updates the given card with the current scheduler provided that card's review logs. 523 524 If the current card was previously scheduled with a different scheduler, you may want to reschedule/update 525 it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule 526 each of your cards with a new scheduler after computing the optimal parameters with the Optimizer. 527 528 Args: 529 card: The card to be rescheduled/updated. 530 review_logs: A list of that card's review logs (order doesn't matter). 531 532 Returns: 533 Card: A new card that has been rescheduled/updated with this current scheduler. 534 535 Raises: 536 ValueError: If any of the review logs are for a card other than the one specified, this will raise an error. 537 538 """ 539 540 for review_log in review_logs: 541 if review_log.card_id != card.card_id: 542 raise ValueError( 543 f"ReviewLog card_id {review_log.card_id} does not match Card card_id {card.card_id}" 544 ) 545 546 review_logs = sorted(review_logs, key=lambda log: log.review_datetime) 547 548 rescheduled_card = Card(card_id=card.card_id, due=card.due) 549 550 for review_log in review_logs: 551 rescheduled_card, _ = self.review_card( 552 card=rescheduled_card, 553 rating=review_log.rating, 554 review_datetime=review_log.review_datetime, 555 ) 556 557 return rescheduled_card
Reschedules/updates the given card with the current scheduler provided that card's review logs.
If the current card was previously scheduled with a different scheduler, you may want to reschedule/update it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule each of your cards with a new scheduler after computing the optimal parameters with the Optimizer.
Args: card: The card to be rescheduled/updated. review_logs: A list of that card's review logs (order doesn't matter).
Returns: Card: A new card that has been rescheduled/updated with this current scheduler.
Raises: ValueError: If any of the review logs are for a card other than the one specified, this will raise an error.
559 def to_dict( 560 self, 561 ) -> SchedulerDict: 562 """ 563 Returns a dictionary representation of the Scheduler object. 564 565 Returns: 566 SchedulerDict: A dictionary representation of the Scheduler object. 567 """ 568 569 return { 570 "parameters": list(self.parameters), 571 "desired_retention": self.desired_retention, 572 "learning_steps": [ 573 int(learning_step.total_seconds()) 574 for learning_step in self.learning_steps 575 ], 576 "relearning_steps": [ 577 int(relearning_step.total_seconds()) 578 for relearning_step in self.relearning_steps 579 ], 580 "maximum_interval": self.maximum_interval, 581 "enable_fuzzing": self.enable_fuzzing, 582 }
Returns a dictionary representation of the Scheduler object.
Returns: SchedulerDict: A dictionary representation of the Scheduler object.
584 @classmethod 585 def from_dict(cls, source_dict: SchedulerDict) -> Self: 586 """ 587 Creates a Scheduler object from an existing dictionary. 588 589 Args: 590 source_dict: A dictionary representing an existing Scheduler object. 591 592 Returns: 593 Self: A Scheduler object created from the provided dictionary. 594 """ 595 596 return cls( 597 parameters=source_dict["parameters"], 598 desired_retention=source_dict["desired_retention"], 599 learning_steps=[ 600 timedelta(seconds=learning_step) 601 for learning_step in source_dict["learning_steps"] 602 ], 603 relearning_steps=[ 604 timedelta(seconds=relearning_step) 605 for relearning_step in source_dict["relearning_steps"] 606 ], 607 maximum_interval=source_dict["maximum_interval"], 608 enable_fuzzing=source_dict["enable_fuzzing"], 609 )
Creates a Scheduler object from an existing dictionary.
Args: source_dict: A dictionary representing an existing Scheduler object.
Returns: Self: A Scheduler object created from the provided dictionary.
611 def to_json(self, indent: int | str | None = None) -> str: 612 """ 613 Returns a JSON-serialized string of the Scheduler object. 614 615 Args: 616 indent: Equivalent argument to the indent in json.dumps() 617 618 Returns: 619 str: A JSON-serialized string of the Scheduler object. 620 """ 621 622 return json.dumps(self.to_dict(), indent=indent)
Returns a JSON-serialized string of the Scheduler object.
Args: indent: Equivalent argument to the indent in json.dumps()
Returns: str: A JSON-serialized string of the Scheduler object.
624 @classmethod 625 def from_json(cls, source_json: str) -> Self: 626 """ 627 Creates a Scheduler object from a JSON-serialized string. 628 629 Args: 630 source_json: A JSON-serialized string of an existing Scheduler object. 631 632 Returns: 633 Self: A Scheduler object created from the JSON string. 634 """ 635 636 source_dict: SchedulerDict = json.loads(source_json) 637 return cls.from_dict(source_dict=source_dict)
Creates a Scheduler object from a JSON-serialized string.
Args: source_json: A JSON-serialized string of an existing Scheduler object.
Returns: Self: A Scheduler object created from the JSON string.
5class State(IntEnum): 6 """ 7 Enum representing the learning state of a Card object. 8 """ 9 10 Learning = 1 11 Review = 2 12 Relearning = 3
Enum representing the learning state of a Card object.