Support rescheduling on weight/retention change

This commit is contained in:
Damien Elmes 2023-10-01 14:59:12 +10:00
parent 0ef28853fd
commit 072cd37b42
14 changed files with 165 additions and 42 deletions

View file

@ -339,6 +339,7 @@ deck-config-fsrs-on-all-clients =
not work correctly if one of your clients is older. not work correctly if one of your clients is older.
deck-config-set-optimal-retention = Set desired retention to { $num } deck-config-set-optimal-retention = Set desired retention to { $num }
deck-config-complete = { $num }% complete. deck-config-complete = { $num }% complete.
deck-config-reschedule-cards-on-change = Reschedule cards on change
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.

View file

@ -139,7 +139,9 @@ message DeckConfig {
bool bury_reviews = 28; bool bury_reviews = 28;
bool bury_interday_learning = 29; bool bury_interday_learning = 29;
float desired_retention = 37; // for fsrs // for fsrs
float desired_retention = 37;
bool reschedule_fsrs_cards = 39;
bytes other = 255; bytes other = 255;
} }

View file

@ -70,6 +70,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
fsrs_weights: vec![], fsrs_weights: vec![],
desired_retention: 0.9, desired_retention: 0.9,
other: Vec::new(), other: Vec::new(),
reschedule_fsrs_cards: false,
}; };
impl Default for DeckConfig { impl Default for DeckConfig {

View file

@ -71,6 +71,8 @@ pub struct DeckConfSchema11 {
desired_retention: f32, desired_retention: f32,
#[serde(default)] #[serde(default)]
stop_timer_on_answer: bool, stop_timer_on_answer: bool,
#[serde(default)]
reschedule_fsrs_cards: bool,
#[serde(flatten)] #[serde(flatten)]
other: HashMap<String, Value>, other: HashMap<String, Value>,
@ -260,6 +262,7 @@ impl Default for DeckConfSchema11 {
bury_interday_learning: false, bury_interday_learning: false,
fsrs_weights: vec![], fsrs_weights: vec![],
desired_retention: 0.9, desired_retention: 0.9,
reschedule_fsrs_cards: false,
} }
} }
} }
@ -331,6 +334,7 @@ impl From<DeckConfSchema11> for DeckConfig {
bury_interday_learning: c.bury_interday_learning, bury_interday_learning: c.bury_interday_learning,
fsrs_weights: c.fsrs_weights, fsrs_weights: c.fsrs_weights,
desired_retention: c.desired_retention, desired_retention: c.desired_retention,
reschedule_fsrs_cards: c.reschedule_fsrs_cards,
other: other_bytes, other: other_bytes,
}, },
} }
@ -425,6 +429,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
bury_interday_learning: i.bury_interday_learning, bury_interday_learning: i.bury_interday_learning,
fsrs_weights: i.fsrs_weights, fsrs_weights: i.fsrs_weights,
desired_retention: i.desired_retention, desired_retention: i.desired_retention,
reschedule_fsrs_cards: i.reschedule_fsrs_cards,
} }
} }
} }
@ -449,6 +454,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"fsrsWeights", "fsrsWeights",
"desiredRetention", "desiredRetention",
"stopTimerOnAnswer", "stopTimerOnAnswer",
"rescheduleFsrsCards"
}; };
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {

View file

@ -16,7 +16,7 @@ use fsrs::DEFAULT_WEIGHTS;
use crate::config::StringKey; use crate::config::StringKey;
use crate::decks::NormalDeck; use crate::decks::NormalDeck;
use crate::prelude::*; use crate::prelude::*;
use crate::scheduler::fsrs::memory_state::WeightsAndDesiredRetention; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest;
use crate::search::JoinSearches; use crate::search::JoinSearches;
use crate::search::SearchNode; use crate::search::SearchNode;
@ -123,19 +123,19 @@ impl Collection {
.collect()) .collect())
} }
fn update_deck_configs_inner(&mut self, mut input: UpdateDeckConfigsRequest) -> Result<()> { fn update_deck_configs_inner(&mut self, mut req: UpdateDeckConfigsRequest) -> Result<()> {
require!(!input.configs.is_empty(), "config not provided"); require!(!req.configs.is_empty(), "config not provided");
let configs_before_update = self.storage.get_deck_config_map()?; let configs_before_update = self.storage.get_deck_config_map()?;
let mut configs_after_update = configs_before_update.clone(); let mut configs_after_update = configs_before_update.clone();
// handle removals first // handle removals first
for dcid in &input.removed_config_ids { for dcid in &req.removed_config_ids {
self.remove_deck_config_inner(*dcid)?; self.remove_deck_config_inner(*dcid)?;
configs_after_update.remove(dcid); configs_after_update.remove(dcid);
} }
// add/update provided configs // add/update provided configs
for conf in &mut input.configs { for conf in &mut req.configs {
let weight_len = conf.inner.fsrs_weights.len(); let weight_len = conf.inner.fsrs_weights.len();
if weight_len != 0 && weight_len != 17 { if weight_len != 0 && weight_len != 17 {
return Err(AnkiError::FsrsWeightsInvalid); return Err(AnkiError::FsrsWeightsInvalid);
@ -145,11 +145,11 @@ impl Collection {
} }
// get selected deck and possibly children // get selected deck and possibly children
let selected_deck_ids: HashSet<_> = if input.apply_to_children { let selected_deck_ids: HashSet<_> = if req.apply_to_children {
let deck = self let deck = self
.storage .storage
.get_deck(input.target_deck_id)? .get_deck(req.target_deck_id)?
.or_not_found(input.target_deck_id)?; .or_not_found(req.target_deck_id)?;
self.storage self.storage
.child_decks(&deck)? .child_decks(&deck)?
.iter() .iter()
@ -157,18 +157,18 @@ impl Collection {
.map(|d| d.id) .map(|d| d.id)
.collect() .collect()
} else { } else {
[input.target_deck_id].iter().cloned().collect() [req.target_deck_id].iter().cloned().collect()
}; };
// loop through all normal decks // loop through all normal decks
let usn = self.usn()?; let usn = self.usn()?;
let today = self.timing_today()?.days_elapsed; let today = self.timing_today()?.days_elapsed;
let selected_config = input.configs.last().unwrap(); let selected_config = req.configs.last().unwrap();
let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> = let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> =
Default::default(); Default::default();
let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != input.fsrs; let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != req.fsrs;
if fsrs_toggled { if fsrs_toggled {
self.set_config_bool_inner(BoolKey::Fsrs, input.fsrs)?; self.set_config_bool_inner(BoolKey::Fsrs, req.fsrs)?;
} }
for deck in self.storage.get_all_decks()? { for deck in self.storage.get_all_decks()? {
if let Ok(normal) = deck.normal() { if let Ok(normal) = deck.normal() {
@ -181,6 +181,7 @@ impl Collection {
.map(|c| c.inner.new_card_insert_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .unwrap_or_default();
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights); let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
let previous_retention = previous_config.map(|c| c.inner.desired_retention);
// if a selected (sub)deck, or its old config was removed, update deck to point // if a selected (sub)deck, or its old config was removed, update deck to point
// to new config // to new config
@ -189,7 +190,7 @@ impl Collection {
{ {
let mut updated = deck.clone(); let mut updated = deck.clone();
updated.normal_mut()?.config_id = selected_config.id.0; updated.normal_mut()?.config_id = selected_config.id.0;
update_deck_limits(updated.normal_mut()?, &input.limits, today); update_deck_limits(updated.normal_mut()?, &req.limits, today);
self.update_deck_inner(&mut updated, deck, usn)?; self.update_deck_inner(&mut updated, deck, usn)?;
selected_config.id selected_config.id
} else { } else {
@ -207,7 +208,11 @@ impl Collection {
// if weights differ, memory state needs to be recomputed // if weights differ, memory state needs to be recomputed
let current_weights = current_config.map(|c| &c.inner.fsrs_weights); let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
if fsrs_toggled || previous_weights != current_weights { let current_retention = current_config.map(|c| c.inner.desired_retention);
if fsrs_toggled
|| previous_weights != current_weights
|| previous_retention != current_retention
{
decks_needing_memory_recompute decks_needing_memory_recompute
.entry(current_config_id) .entry(current_config_id)
.or_default() .or_default()
@ -219,13 +224,18 @@ impl Collection {
} }
if !decks_needing_memory_recompute.is_empty() { if !decks_needing_memory_recompute.is_empty() {
let input: Vec<(Option<WeightsAndDesiredRetention>, Vec<SearchNode>)> = let input: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)> =
decks_needing_memory_recompute decks_needing_memory_recompute
.into_iter() .into_iter()
.map(|(conf_id, search)| { .map(|(conf_id, search)| {
let weights = configs_after_update.get(&conf_id).and_then(|c| { let weights = configs_after_update.get(&conf_id).and_then(|c| {
if input.fsrs { if req.fsrs {
Some((c.inner.fsrs_weights.clone(), c.inner.desired_retention)) Some(UpdateMemoryStateRequest {
weights: c.inner.fsrs_weights.clone(),
desired_retention: c.inner.desired_retention,
max_interval: c.inner.maximum_review_interval,
reschedule: c.inner.reschedule_fsrs_cards,
})
} else { } else {
None None
} }
@ -236,10 +246,10 @@ impl Collection {
self.update_memory_state(input)?; self.update_memory_state(input)?;
} }
self.set_config_string_inner(StringKey::CardStateCustomizer, &input.card_state_customizer)?; self.set_config_string_inner(StringKey::CardStateCustomizer, &req.card_state_customizer)?;
self.set_config_bool_inner( self.set_config_bool_inner(
BoolKey::NewCardsIgnoreReviewLimit, BoolKey::NewCardsIgnoreReviewLimit,
input.new_cards_ignore_review_limit, req.new_cards_ignore_review_limit,
)?; )?;
Ok(()) Ok(())

View file

@ -89,17 +89,23 @@ impl Collection {
pub(crate) fn log_manually_scheduled_review( pub(crate) fn log_manually_scheduled_review(
&mut self, &mut self,
card: &Card, card: &Card,
original: &Card, original_interval: u32,
usn: Usn, usn: Usn,
) -> Result<()> { ) -> Result<()> {
let ease_factor = u32::try_from(
card.memory_state
.map(|s| ((s.difficulty_shifted() * 1000.) as u16))
.unwrap_or(card.ease_factor),
)
.unwrap_or_default();
let entry = RevlogEntry { let entry = RevlogEntry {
id: RevlogId::new(), id: RevlogId::new(),
cid: card.id, cid: card.id,
usn, usn,
button_chosen: 0, button_chosen: 0,
interval: i32::try_from(card.interval).unwrap_or(i32::MAX), interval: i32::try_from(card.interval).unwrap_or(i32::MAX),
last_interval: i32::try_from(original.interval).unwrap_or(i32::MAX), last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
ease_factor: u32::from(card.ease_factor), ease_factor,
taken_millis: 0, taken_millis: 0,
review_kind: RevlogReviewKind::Manual, review_kind: RevlogReviewKind::Manual,
}; };

View file

@ -367,7 +367,7 @@ impl Collection {
let days_elapsed = self let days_elapsed = self
.storage .storage
.time_of_last_review(card.id)? .time_of_last_review(card.id)?
.map(|ts| ts.elapsed_days_since(timing.next_day_at)) .map(|ts| timing.next_day_at.elapsed_days_since(ts))
.unwrap_or_default() as u32; .unwrap_or_default() as u32;
Some(fsrs.next_states( Some(fsrs.next_states(
card.memory_state.map(Into::into), card.memory_state.map(Into::into),
@ -464,6 +464,12 @@ pub mod test_helpers {
} }
} }
impl Card {
pub(crate) fn get_fuzz_factor(&self) -> Option<f32> {
get_fuzz_factor(get_fuzz_seed(self))
}
}
/// Return a consistent seed for a given card at a given number of reps. /// Return a consistent seed for a given card at a given number of reps.
/// If in test environment, disable fuzzing. /// If in test environment, disable fuzzing.
fn get_fuzz_seed(card: &Card) -> Option<u64> { fn get_fuzz_seed(card: &Card) -> Option<u64> {

View file

@ -1,6 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashMap;
use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeMemoryStateResponse;
use fsrs::FSRSItem; use fsrs::FSRSItem;
use fsrs::FSRS; use fsrs::FSRS;
@ -11,6 +13,7 @@ use crate::prelude::*;
use crate::revlog::RevlogEntry; use crate::revlog::RevlogEntry;
use crate::scheduler::fsrs::weights::single_card_revlog_to_items; use crate::scheduler::fsrs::weights::single_card_revlog_to_items;
use crate::scheduler::fsrs::weights::Weights; use crate::scheduler::fsrs::weights::Weights;
use crate::scheduler::states::fuzz::with_review_fuzz;
use crate::search::JoinSearches; use crate::search::JoinSearches;
use crate::search::Negated; use crate::search::Negated;
use crate::search::SearchNode; use crate::search::SearchNode;
@ -22,7 +25,13 @@ pub struct ComputeMemoryProgress {
pub total_cards: u32, pub total_cards: u32,
} }
pub(crate) type WeightsAndDesiredRetention = (Weights, f32); #[derive(Debug)]
pub(crate) struct UpdateMemoryStateRequest {
pub weights: Weights,
pub desired_retention: f32,
pub max_interval: u32,
pub reschedule: bool,
}
impl Collection { impl Collection {
/// For each provided set of weights, locate cards with the provided search, /// For each provided set of weights, locate cards with the provided search,
@ -32,26 +41,71 @@ impl Collection {
/// memory state should be removed. /// memory state should be removed.
pub(crate) fn update_memory_state( pub(crate) fn update_memory_state(
&mut self, &mut self,
entries: Vec<(Option<WeightsAndDesiredRetention>, Vec<SearchNode>)>, entries: Vec<(Option<UpdateMemoryStateRequest>, Vec<SearchNode>)>,
) -> Result<()> { ) -> Result<()> {
let timing = self.timing_today()?; let timing = self.timing_today()?;
let usn = self.usn()?; let usn = self.usn()?;
for (weights_and_desired_retention, search) in entries { for (req, search) in entries {
let search = SearchBuilder::any(search.into_iter()) let search = SearchBuilder::any(search.into_iter())
.and(SearchNode::State(StateKind::New).negated()); .and(SearchNode::State(StateKind::New).negated());
let revlog = self.revlog_for_srs(search)?; let revlog = self.revlog_for_srs(search)?;
let reschedule = req.as_ref().map(|e| e.reschedule).unwrap_or_default();
let last_reviews = if reschedule {
Some(get_last_reviews(&revlog))
} else {
None
};
let items = fsrs_items_for_memory_state(revlog, timing.next_day_at); let items = fsrs_items_for_memory_state(revlog, timing.next_day_at);
let desired_retention = weights_and_desired_retention.as_ref().map(|w| w.1); let desired_retention = req.as_ref().map(|w| w.desired_retention);
let fsrs = FSRS::new(weights_and_desired_retention.as_ref().map(|w| &w.0[..]))?; let fsrs = FSRS::new(req.as_ref().map(|w| &w.weights[..]))?;
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>(); let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
progress.update(false, |s| s.total_cards = items.len() as u32)?; progress.update(false, |s| s.total_cards = items.len() as u32)?;
for (idx, (card_id, item)) in items.into_iter().enumerate() { for (idx, (card_id, item)) in items.into_iter().enumerate() {
progress.update(true, |state| state.current_cards = idx as u32 + 1)?; progress.update(true, |state| state.current_cards = idx as u32 + 1)?;
let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
let original = card.clone(); let original = card.clone();
if weights_and_desired_retention.is_some() { if let Some(req) = &req {
card.set_memory_state(&fsrs, item); card.set_memory_state(&fsrs, item.clone());
card.desired_retention = desired_retention; card.desired_retention = desired_retention;
// if rescheduling
if let Some(reviews) = &last_reviews {
// and we have a last review time for the card
if let Some(last_review) = reviews.get(&card.id) {
let days_elapsed =
timing.next_day_at.elapsed_days_since(*last_review) as i32;
// and the card's not new
if let Some(state) = &card.memory_state {
// or in (re)learning
if card.ctype == CardType::Review {
// reschedule it
let original_interval = card.interval;
let interval = fsrs.next_interval(
Some(state.stability),
card.desired_retention.unwrap(),
0,
) as f32;
card.interval = with_review_fuzz(
card.get_fuzz_factor(),
interval,
1,
req.max_interval,
);
let due = if card.original_due != 0 {
&mut card.original_due
} else {
&mut card.due
};
*due = (timing.days_elapsed as i32) - days_elapsed
+ card.interval as i32;
self.log_manually_scheduled_review(
&card,
original_interval,
usn,
)?;
}
}
}
}
} else { } else {
card.memory_state = None; card.memory_state = None;
card.desired_retention = None; card.desired_retention = None;
@ -117,6 +171,25 @@ pub(crate) fn fsrs_items_for_memory_state(
.collect() .collect()
} }
/// Return a map of cards to the last time they were reviewed.
fn get_last_reviews(revlogs: &[RevlogEntry]) -> HashMap<CardId, TimestampSecs> {
let mut out = HashMap::new();
revlogs
.iter()
.group_by(|r| r.cid)
.into_iter()
.for_each(|(card_id, group)| {
let mut last_ts = TimestampSecs::zero();
for entry in group.into_iter().filter(|r| r.button_chosen >= 1) {
last_ts = entry.id.as_secs();
}
if last_ts != TimestampSecs::zero() {
out.insert(card_id, last_ts);
}
});
out
}
/// When calculating memory state, only the last FSRSItem is required. /// When calculating memory state, only the last FSRSItem is required.
pub(crate) fn single_card_revlog_to_item( pub(crate) fn single_card_revlog_to_item(
entries: Vec<RevlogEntry>, entries: Vec<RevlogEntry>,

View file

@ -168,7 +168,7 @@ impl Collection {
position += 1; position += 1;
} }
if log { if log {
col.log_manually_scheduled_review(&card, &original, usn)?; col.log_manually_scheduled_review(&card, original.interval, usn)?;
} }
col.update_card_inner(&mut card, original, usn)?; col.update_card_inner(&mut card, original, usn)?;
} }

View file

@ -134,7 +134,7 @@ impl Collection {
let original = card.clone(); let original = card.clone();
let days_from_today = distribution.sample(&mut rng); let days_from_today = distribution.sample(&mut rng);
card.set_due_date(today, days_from_today, ease_factor, spec.force_reset); card.set_due_date(today, days_from_today, ease_factor, spec.force_reset);
col.log_manually_scheduled_review(&card, &original, usn)?; col.log_manually_scheduled_review(&card, original.interval, usn)?;
col.update_card_inner(&mut card, original, usn)?; col.update_card_inner(&mut card, original, usn)?;
} }
if let Some(key) = context { if let Some(key) = context {

View file

@ -33,12 +33,21 @@ impl<'a> StateContext<'a> {
/// Apply fuzz, respecting the passed bounds. /// Apply fuzz, respecting the passed bounds.
/// Caller must ensure reasonable bounds. /// Caller must ensure reasonable bounds.
pub(crate) fn with_review_fuzz(&self, interval: f32, minimum: u32, maximum: u32) -> u32 { pub(crate) fn with_review_fuzz(&self, interval: f32, minimum: u32, maximum: u32) -> u32 {
if let Some(fuzz_factor) = self.fuzz_factor { with_review_fuzz(self.fuzz_factor, interval, minimum, maximum)
let (lower, upper) = constrained_fuzz_bounds(interval, minimum, maximum); }
(lower as f32 + fuzz_factor * ((1 + upper - lower) as f32)).floor() as u32 }
} else {
(interval.round() as u32).clamp(minimum, maximum) pub(crate) fn with_review_fuzz(
} fuzz_factor: Option<f32>,
interval: f32,
minimum: u32,
maximum: u32,
) -> u32 {
if let Some(fuzz_factor) = fuzz_factor {
let (lower, upper) = constrained_fuzz_bounds(interval, minimum, maximum);
(lower as f32 + fuzz_factor * ((1 + upper - lower) as f32)).floor() as u32
} else {
(interval.round() as u32).clamp(minimum, maximum)
} }
} }
@ -61,7 +70,7 @@ fn constrained_fuzz_bounds(interval: f32, minimum: u32, maximum: u32) -> (u32, u
(lower, upper) (lower, upper)
} }
fn fuzz_bounds(interval: f32) -> (u32, u32) { pub(crate) fn fuzz_bounds(interval: f32) -> (u32, u32) {
let delta = fuzz_delta(interval); let delta = fuzz_delta(interval);
( (
(interval - delta).round() as u32, (interval - delta).round() as u32,

View file

@ -30,7 +30,7 @@ impl Collection {
let days_elapsed = self let days_elapsed = self
.storage .storage
.time_of_last_review(card.id)? .time_of_last_review(card.id)?
.map(|ts| ts.elapsed_days_since(timing.next_day_at)) .map(|ts| timing.next_day_at.elapsed_days_since(ts))
.unwrap_or_default() as u32; .unwrap_or_default() as u32;
let fsrs_retrievability = card let fsrs_retrievability = card
.memory_state .memory_state

View file

@ -29,7 +29,7 @@ impl TimestampSecs {
} }
pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 { pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 {
(other.0 - self.0).max(0) as u64 / 86_400 (self.0 - other.0).max(0) as u64 / 86_400
} }
pub fn as_millis(self) -> TimestampMillis { pub fn as_millis(self) -> TimestampMillis {

View file

@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} from "@tslib/backend"; } from "@tslib/backend";
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { runWithBackendProgress } from "@tslib/progress"; import { runWithBackendProgress } from "@tslib/progress";
import SwitchRow from "components/SwitchRow.svelte";
import SettingTitle from "../components/SettingTitle.svelte"; import SettingTitle from "../components/SettingTitle.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
@ -223,6 +224,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</WeightsInputRow> </WeightsInputRow>
</div> </div>
<div class="m-2">
<SwitchRow bind:value={$config.rescheduleFsrsCards} defaultValue={false}>
<SettingTitle>
{tr.deckConfigRescheduleCardsOnChange()}
</SettingTitle>
</SwitchRow>
</div>
<div class="m-2"> <div class="m-2">
<details> <details>
<summary>{tr.deckConfigComputeOptimalWeights()}</summary> <summary>{tr.deckConfigComputeOptimalWeights()}</summary>