use std::fmt;
use std::time::{Duration, Instant};
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Budget {
pub deadline: Option<Instant>,
pub time_limit: Option<Duration>,
pub max_pivots: Option<usize>,
}
impl Budget {
pub const fn unlimited() -> Self {
Budget {
deadline: None,
time_limit: None,
max_pivots: None,
}
}
pub const fn deadline(at: Instant) -> Self {
Budget {
deadline: Some(at),
time_limit: None,
max_pivots: None,
}
}
pub const fn max_pivots(n: usize) -> Self {
Budget {
deadline: None,
time_limit: None,
max_pivots: Some(n),
}
}
pub fn within(duration: Duration) -> Self {
Budget {
deadline: Instant::now().checked_add(duration),
time_limit: None,
max_pivots: None,
}
}
pub const fn time_limit(duration: Duration) -> Self {
Budget {
deadline: None,
time_limit: Some(duration),
max_pivots: None,
}
}
#[must_use]
pub fn with_deadline(mut self, at: Instant) -> Self {
self.deadline = Some(at);
self
}
#[must_use]
pub fn with_time_limit(mut self, limit: Duration) -> Self {
self.time_limit = Some(limit);
self
}
#[must_use]
pub fn with_max_pivots(mut self, n: usize) -> Self {
self.max_pivots = Some(n);
self
}
#[must_use]
pub fn with_max_steps(self, n: usize) -> Self {
self.with_max_pivots(n)
}
pub const fn is_unlimited(&self) -> bool {
self.deadline.is_none() && self.time_limit.is_none() && self.max_pivots.is_none()
}
pub fn deadline_from_now(&self) -> Option<Instant> {
deadline_from(self.deadline, self.time_limit)
}
#[must_use]
pub fn start(&self) -> Self {
Budget {
deadline: self.deadline_from_now(),
time_limit: None,
max_pivots: self.max_pivots,
}
}
pub fn deadline_passed(&self) -> bool {
deadline_passed(self.deadline)
}
pub fn exhausted(&self, spent: usize) -> Option<BudgetHit> {
if self.deadline_passed() {
return Some(BudgetHit::Deadline);
}
if self.max_pivots.is_some_and(|cap| spent >= cap) {
return Some(BudgetHit::MaxPivots);
}
None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BudgetHit {
Deadline,
MaxPivots,
}
impl fmt::Display for BudgetHit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
BudgetHit::Deadline => "deadline",
BudgetHit::MaxPivots => "max_pivots",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BudgetKind {
SeriesExpansion,
Integration,
QuadratureEvaluations,
}
impl BudgetKind {
pub(crate) const fn reason(self) -> &'static str {
match self {
BudgetKind::SeriesExpansion => "the series expansion exceeded its work budget",
BudgetKind::Integration => "the integration exceeded its work budget",
BudgetKind::QuadratureEvaluations => {
"quadrature of the definite integral did not converge: the evaluation budget was \
exhausted"
}
}
}
}
impl fmt::Display for BudgetKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
BudgetKind::SeriesExpansion => "series expansion",
BudgetKind::Integration => "integration",
BudgetKind::QuadratureEvaluations => "quadrature evaluations",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Work {
spent: u64,
cap: u64,
kind: BudgetKind,
}
impl Work {
pub(crate) const fn new(kind: BudgetKind, cap: u64) -> Self {
Work {
spent: 0,
cap,
kind,
}
}
pub(crate) const fn spent(&self) -> u64 {
self.spent
}
pub(crate) const fn cap(&self) -> u64 {
self.cap
}
pub(crate) const fn left(&self) -> u64 {
self.cap.saturating_sub(self.spent)
}
pub(crate) fn charge(
&mut self,
units: u64,
operation: &'static str,
) -> Result<(), SymplexError> {
if units > self.left() {
return Err(SymplexError::BudgetExhausted {
operation,
kind: self.kind,
});
}
self.spent += units;
Ok(())
}
}
pub(crate) struct WorkScope<'a> {
arena: &'a mut Arena,
}
impl<'a> WorkScope<'a> {
pub(crate) fn enter(arena: &'a mut Arena, kind: BudgetKind, cap: u64) -> Self {
arena.open_work_scope(kind, cap);
WorkScope { arena }
}
}
impl std::ops::Deref for WorkScope<'_> {
type Target = Arena;
fn deref(&self) -> &Arena {
self.arena
}
}
impl std::ops::DerefMut for WorkScope<'_> {
fn deref_mut(&mut self) -> &mut Arena {
self.arena
}
}
impl Drop for WorkScope<'_> {
fn drop(&mut self) {
self.arena.close_work_scope();
}
}
pub fn deadline_from(deadline: Option<Instant>, time_limit: Option<Duration>) -> Option<Instant> {
let from_limit = time_limit.and_then(|t| Instant::now().checked_add(t));
match (deadline, from_limit) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub fn deadline_passed(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|d| Instant::now() >= d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builders_and_defaults() {
assert!(Budget::default().is_unlimited());
assert_eq!(Budget::default(), Budget::unlimited());
let b = Budget::max_pivots(3).with_time_limit(Duration::from_secs(1));
assert_eq!(b.max_pivots, Some(3));
assert_eq!(b.time_limit, Some(Duration::from_secs(1)));
assert!(b.deadline.is_none() && !b.is_unlimited());
assert_eq!(Budget::max_pivots(3), Budget::default().with_max_steps(3));
}
#[test]
fn deadlines() {
let past = Instant::now() - Duration::from_secs(1);
assert!(Budget::deadline(past).deadline_passed());
assert_eq!(
Budget::deadline(past).exhausted(0),
Some(BudgetHit::Deadline)
);
assert!(!Budget::default().deadline_passed());
assert!(Budget::within(Duration::ZERO).deadline_passed());
let b = Budget::time_limit(Duration::ZERO);
assert!(!b.deadline_passed());
let started = b.start();
assert!(started.time_limit.is_none() && started.deadline_passed());
let far = Instant::now() + Duration::from_secs(3600);
let d = deadline_from(Some(far), Some(Duration::ZERO)).expect("some deadline");
assert!(d < far);
assert_eq!(deadline_from(Some(far), None), Some(far));
assert!(deadline_from(None, None).is_none());
assert!(!deadline_passed(None));
assert!(deadline_from(None, Some(Duration::MAX)).is_none());
}
#[test]
fn work_charges_until_the_cap() {
let mut w = Work::new(BudgetKind::QuadratureEvaluations, 3);
assert_eq!((w.spent(), w.left(), w.cap()), (0, 3, 3));
assert!(w.charge(2, "evalf").is_ok());
let err = w.charge(2, "evalf").unwrap_err();
assert!(matches!(
err,
SymplexError::BudgetExhausted {
operation: "evalf",
kind: BudgetKind::QuadratureEvaluations
}
));
assert_eq!(w.left(), 1);
assert!(w.charge(1, "evalf").is_ok());
assert!(w.charge(1, "evalf").is_err());
}
#[test]
fn work_scopes_count_interns_and_nest() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let two = arena.int(2);
let warm = arena.pow(x, two);
assert!(!arena.work_exhausted(BudgetKind::SeriesExpansion));
{
let mut outer = WorkScope::enter(&mut arena, BudgetKind::SeriesExpansion, 3);
let mut steps = 0;
while !outer.work_exhausted(BudgetKind::SeriesExpansion) {
assert_eq!(outer.pow(x, two), warm);
steps += 1;
}
assert!((1..=4).contains(&steps), "{steps}");
{
let mut inner = WorkScope::enter(&mut outer, BudgetKind::Integration, 100);
assert!(inner.work_exhausted(BudgetKind::SeriesExpansion));
assert!(!inner.work_exhausted(BudgetKind::Integration));
let mut same = WorkScope::enter(&mut inner, BudgetKind::SeriesExpansion, 100);
assert!(same.work_exhausted(BudgetKind::SeriesExpansion));
same.pow(x, two);
}
assert!(!outer.work_exhausted(BudgetKind::Integration));
}
assert!(!arena.work_exhausted(BudgetKind::SeriesExpansion));
}
#[test]
fn step_cap() {
let b = Budget::max_pivots(2);
assert_eq!(b.exhausted(0), None);
assert_eq!(b.exhausted(1), None);
assert_eq!(b.exhausted(2), Some(BudgetHit::MaxPivots));
assert_eq!(BudgetHit::MaxPivots.to_string(), "max_pivots");
assert_eq!(BudgetHit::Deadline.to_string(), "deadline");
}
}