use std::cmp::Ordering;
use crate::api::expr::{BoolEx, Ex, ExprType, SetEx, SetValued};
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::base::interval::{Interval, IntervalKind};
use crate::base::node::ExprNode;
use crate::calculus::calculus_util as util;
use crate::calculus::limit::Direction;
fn computation_failed(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::computation_failed(operation, reason)
}
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument(operation, reason)
}
fn require_symbol(operation: &'static str, var: &Ex) -> Result<(), SymplexError> {
if var.expr_type() == ExprType::Symbol {
Ok(())
} else {
Err(invalid(operation, format!("`{var}` is not a symbol")))
}
}
const MAX_FAMILY_MEMBERS: i64 = 10_000;
const MAX_FAMILY_INDEX: f64 = 1e9;
const MAX_SIGN_FACTORS: usize = 4;
fn is_real_finite_point(p: &Ex) -> bool {
let ctx = p.context();
if *p == ctx.infinity()
|| *p == ctx.neg_infinity()
|| *p == ctx.complex_infinity()
|| *p == ctx.nan()
{
return false;
}
if p.is_real() == Some(false) || p.is_finite() == Some(false) {
return false;
}
if p.is_constant() {
if p.contains(&ctx.i_unit()) && p.is_real() != Some(true) {
return false;
}
if let Ok(z) = p.eval_complex64()
&& z.im.abs() > 1e-12 * (1.0 + z.re.abs())
{
return false;
}
}
true
}
fn numeric_bounds(domain: &SetEx) -> Option<Interval<f64>> {
let ctx = domain.context();
let parts = domain.as_intervals()?;
let first = parts.first()?;
let last = parts.last()?;
if first.lower == ctx.neg_infinity() || last.upper == ctx.infinity() {
return None;
}
let lo = first.lower.eval_f64().ok()?;
let hi = last.upper.eval_f64().ok()?;
(lo.is_finite() && hi.is_finite()).then_some(Interval {
lower: lo,
upper: hi,
kind: IntervalKind::from_open_ends(first.kind.lower_open(), last.kind.upper_open()),
})
}
fn condition_set(var: &Ex, cond: &BoolEx) -> SetEx {
let var_id = var.raw_id();
let cond_id = var.checked_id(cond);
let id = var
.inner
.write()
.arena
.intern(ExprNode::ConditionSet(var_id, cond_id));
var.wrap_as::<SetValued>(id)
}
fn enumerate_family(
member: &Ex,
param: &Ex,
bounds: Interval<f64>,
domain: &SetEx,
op: &'static str,
out: &mut Vec<Ex>,
) -> Result<(), SymplexError> {
let (lo, hi) = bounds.into_pair();
let step = member.diff(param).eval();
let offset = member.subs_i64(param, 0).eval();
if step.contains(param) {
return Err(computation_failed(
op,
format!("solution family `{member}` is not linear in `{param}`"),
));
}
if !is_real_finite_point(&offset) {
return Ok(());
}
let (Ok(step_f), Ok(offset_f)) = (step.eval_f64(), offset.eval_f64()) else {
return Err(computation_failed(
op,
format!("cannot locate the members of the family `{member}` numerically"),
));
};
if step_f == 0.0 {
out.push(offset);
return Ok(());
}
let (k1, k2) = ((lo - offset_f) / step_f, (hi - offset_f) / step_f);
let (k_lo, k_hi) = (k1.min(k2), k1.max(k2));
if !k_lo.is_finite()
|| !k_hi.is_finite()
|| k_lo.abs() > MAX_FAMILY_INDEX
|| k_hi.abs() > MAX_FAMILY_INDEX
{
return Err(computation_failed(
op,
format!(
"the members of `{member}` in the domain have indices beyond \
±{MAX_FAMILY_INDEX:e}, where they cannot be located reliably"
),
));
}
if k_hi - k_lo > MAX_FAMILY_MEMBERS as f64 {
return Err(computation_failed(
op,
format!("more than {MAX_FAMILY_MEMBERS} members of `{member}` may lie in the domain"),
));
}
let (k_min, k_max) = (k_lo.floor() as i64 - 1, k_hi.ceil() as i64 + 1);
for k in k_min..=k_max {
let point = member.subs_i64(param, k).eval();
match domain.contains(&point) {
Some(true) => out.push(point),
Some(false) => {}
None => {
return Err(computation_failed(
op,
format!("cannot decide whether `{point}` lies in `{domain}`"),
));
}
}
}
Ok(())
}
fn zeros_in_domain(
g: &Ex,
var: &Ex,
domain: &SetEx,
op: &'static str,
) -> Result<SetEx, SymplexError> {
let ctx = g.context();
let var_id = g.checked_id(var);
let periodic = {
let inner = g.inner.read();
util::has_trig_of(&inner.arena, g.raw_id(), var_id)
};
let mut points: Vec<Ex> = Vec::new();
let mut families: Vec<SetEx> = Vec::new();
let outcome = if periodic {
g.solve_general(var).map(|fam| {
let mut plain = Vec::new();
let mut parametric = Vec::new();
for s in fam.solutions {
if fam.parameters.iter().any(|n| s.contains(n)) {
parametric.push(s);
} else {
plain.push(s);
}
}
(plain, parametric, fam.parameters)
})
} else {
g.solve(var).map(|sols| (sols, Vec::new(), Vec::new()))
};
match outcome {
Ok((plain, parametric, parameters)) => {
points.extend(plain);
if !parametric.is_empty() {
let Some(param) = parameters.first() else {
return Err(computation_failed(
op,
"parametric solution without parameter",
));
};
match numeric_bounds(domain) {
Some(bounds) => {
for member in ¶metric {
enumerate_family(member, param, bounds, domain, op, &mut points)?;
}
}
None => families.push(condition_set(var, &g.eq_expr(&ctx.zero()))),
}
}
}
Err(SymplexError::NoSolution { .. }) => {}
Err(SymplexError::InfiniteSolutions { .. }) => return Ok(domain.clone()),
Err(e) => {
return Err(computation_failed(
op,
format!("the zeros of `{g}` cannot be found exactly ({e})"),
));
}
}
points.retain(is_real_finite_point);
let mut result = ctx.finite_set(&points).intersection(domain);
for fam in families {
result = result.union(&fam.intersection(domain));
}
Ok(result.simplify())
}
struct Candidate {
value: Ex,
attained: bool,
}
fn compare(a: &Ex, b: &Ex) -> Option<Ordering> {
let ctx = a.context();
let (inf, ninf) = (ctx.infinity(), ctx.neg_infinity());
if a == b {
return Some(Ordering::Equal);
}
if *a == inf || *b == ninf {
return Some(Ordering::Greater);
}
if *a == ninf || *b == inf {
return Some(Ordering::Less);
}
if a.equals(b) == Some(true) {
return Some(Ordering::Equal);
}
let d = (a - b).eval();
if d.is_positive() == Some(true) {
return Some(Ordering::Greater);
}
if d.is_negative() == Some(true) {
return Some(Ordering::Less);
}
if a.equals(b) != Some(false) {
return None;
}
let v = d.eval_f64().ok()?;
if v > 0.0 {
Some(Ordering::Greater)
} else if v < 0.0 {
Some(Ordering::Less)
} else {
None
}
}
fn min_max(
cands: Vec<Candidate>,
op: &'static str,
) -> Result<(Candidate, Candidate), SymplexError> {
let mut iter = cands.into_iter();
let Some(first) = iter.next() else {
return Err(computation_failed(op, "no candidate values"));
};
let mut lo = Candidate {
value: first.value.clone(),
attained: first.attained,
};
let mut hi = first;
for c in iter {
let Some(ord) = compare(&c.value, &lo.value) else {
return Err(computation_failed(
op,
format!(
"cannot compare the candidates `{}` and `{}`",
c.value, lo.value
),
));
};
match ord {
Ordering::Less => {
lo = Candidate {
value: c.value.clone(),
attained: c.attained,
}
}
Ordering::Equal => lo.attained |= c.attained,
Ordering::Greater => {}
}
let Some(ord) = compare(&c.value, &hi.value) else {
return Err(computation_failed(
op,
format!(
"cannot compare the candidates `{}` and `{}`",
c.value, hi.value
),
));
};
match ord {
Ordering::Greater => hi = c,
Ordering::Equal => hi.attained |= c.attained,
Ordering::Less => {}
}
}
Ok((lo, hi))
}
fn interior_point(ctx: &crate::api::context::Context, lo: &Ex, hi: &Ex) -> Ex {
match (*lo == ctx.neg_infinity(), *hi == ctx.infinity()) {
(true, true) => ctx.zero(),
(true, false) => (hi - 1).eval(),
(false, true) => (lo + 1).eval(),
(false, false) => ((lo + hi) / 2).eval(),
}
}
fn constant_sign(value: &Ex) -> Option<bool> {
if value.is_positive() == Some(true) {
Some(true)
} else if value.is_negative() == Some(true) {
Some(false)
} else {
None
}
}
fn stationary_set(
d: &Ex,
var: &Ex,
domain: &SetEx,
op: &'static str,
) -> Result<SetEx, SymplexError> {
let signs = d.sign_factors(var);
if signs.is_empty() {
return zeros_in_domain(d, var, domain, op);
}
if signs.len() > MAX_SIGN_FACTORS {
return Err(computation_failed(
op,
format!(
"the derivative `{d}` has {} distinct sign(…) factors; at most \
{MAX_SIGN_FACTORS} are resolved",
signs.len()
),
));
}
let ctx = d.context();
let (one, minus_one) = (ctx.one(), ctx.int(-1));
let mut result = ctx.empty_set();
for (_, h) in &signs {
let kinks = zeros_in_domain(h, var, domain, op)?;
let Some(points) = kinks.as_finite_set() else {
return Err(computation_failed(
op,
format!("the kinks {kinks} of `|{h}|` cannot be enumerated"),
));
};
let stationary: Vec<Ex> = points
.into_iter()
.filter(|p| d.subs(var, p).eval().is_zero_structural())
.collect();
if !stationary.is_empty() {
result = result.union(&ctx.finite_set(&stationary));
}
}
for mask in 0..(1usize << signs.len()) {
let mut spec = d.clone();
let mut pattern: Vec<(&Ex, bool)> = Vec::with_capacity(signs.len());
for (i, (s, h)) in signs.iter().enumerate() {
let positive = mask & (1 << i) != 0;
spec = spec.subs(s, if positive { &one } else { &minus_one });
pattern.push((h, positive));
}
let spec = spec.eval();
let region = || -> Result<SetEx, SymplexError> {
let mut region = domain.clone();
for &(h, positive) in &pattern {
let side = if positive {
h.solve_gt(var)
} else {
h.solve_lt(var)
};
if side.has_unevaluated() {
return Err(computation_failed(
op,
format!("cannot describe the region where `{h}` has a fixed sign"),
));
}
region = region.intersection(&side);
}
Ok(region.simplify())
};
if spec.is_zero_structural() {
result = result.union(®ion()?);
continue;
}
let zeros = zeros_in_domain(&spec, var, domain, op)?;
match zeros.as_finite_set() {
Some(points) => {
let mut kept: Vec<Ex> = Vec::new();
for p in points {
let mut holds = true;
for &(h, positive) in &pattern {
let value = h.subs(var, &p).eval();
match constant_sign(&value) {
Some(sign) if sign == positive => {}
Some(_) => {
holds = false;
break;
}
None if value.is_zero_structural() => {
holds = false;
break;
}
None => {
return Err(computation_failed(
op,
format!("cannot decide the sign of `{h}` at `{var} = {p}`"),
));
}
}
}
if holds {
kept.push(p);
}
}
if !kept.is_empty() {
result = result.union(&ctx.finite_set(&kept));
}
}
None => result = result.union(&zeros.intersection(®ion()?)),
}
}
Ok(result.simplify())
}
impl Ex {
fn continuity_scan(&self, var: &Ex) -> util::ContinuityScan {
let var_id = self.checked_id(var);
let mut inner = self.inner.write();
util::continuity_scan(&mut inner.arena, self.raw_id(), var_id)
}
fn require_continuous(
&self,
var: &Ex,
domain: &SetEx,
op: &'static str,
) -> Result<(), SymplexError> {
if let Some(name) = self.continuity_scan(var).opaque {
return Err(computation_failed(
op,
format!("`{self}` contains {name} of `{var}`, which is not analysed"),
));
}
let sing = self.singularities(var, Some(domain))?;
match sing.is_empty() {
Some(true) => Ok(()),
Some(false) => Err(computation_failed(
op,
format!("`{self}` has singularities {sing} inside the domain"),
)),
None => Err(computation_failed(
op,
format!("cannot decide whether the singularity set {sing} meets the domain"),
)),
}
}
fn sign_factors(&self, var: &Ex) -> Vec<(Ex, Ex)> {
let var_id = self.checked_id(var);
let ids = {
let inner = self.inner.read();
util::sign_nodes_of(&inner.arena, self.raw_id(), var_id)
};
ids.into_iter()
.map(|(s, h)| (self.wrap(s), self.wrap(h)))
.collect()
}
fn value_at(
&self,
var: &Ex,
point: &Ex,
limit: Option<Direction>,
op: &'static str,
) -> Result<Candidate, SymplexError> {
let ctx = self.context();
let (value, attained) = match limit {
None => (self.subs(var, point).eval(), true),
Some(dir) => {
let v = self.try_limit_dir(var, point, dir).map_err(|e| {
computation_failed(op, format!("limit at the endpoint `{point}` failed: {e}"))
})?;
(v, false)
}
};
let real = if attained {
is_real_finite_point(&value)
} else {
value == ctx.infinity() || value == ctx.neg_infinity() || is_real_finite_point(&value)
};
if value.has_unevaluated() || !real {
return Err(computation_failed(
op,
format!("the value `{value}` at `{var} = {point}` is not a real number"),
));
}
Ok(Candidate { value, attained })
}
fn candidates_on(
&self,
var: &Ex,
part: Interval<&Ex>,
op: &'static str,
) -> Result<Vec<Candidate>, SymplexError> {
let ctx = self.context();
let Interval {
lower: lo,
upper: hi,
kind,
} = part;
if lo == hi {
return Ok(vec![self.value_at(var, lo, None, op)?]);
}
let part = ctx.interval(lo, hi, kind);
let mut cands = Vec::new();
let derivative = self.diff(var);
if derivative.has_unevaluated() {
return Err(computation_failed(
op,
format!("the derivative `{derivative}` could not be evaluated"),
));
}
if derivative.is_zero_structural() {
return Ok(vec![Candidate {
value: self.simplify(),
attained: true,
}]);
}
let mut interior: Vec<SetEx> = vec![stationary_set(&derivative, var, &part, op)?];
for g in self.continuity_scan(var).kinks {
interior.push(zeros_in_domain(&self.wrap(g), var, &part, op)?);
}
for set in interior {
if let Some(points) = set.as_finite_set() {
for p in points {
cands.push(self.value_at(var, &p, None, op)?);
}
} else if let Some(pieces) = set.as_intervals() {
for piece in pieces {
let p = if piece.lower == piece.upper {
piece.lower
} else {
interior_point(&ctx, &piece.lower, &piece.upper)
};
cands.push(self.value_at(var, &p, None, op)?);
}
} else {
return Err(computation_failed(
op,
format!("the critical points {set} cannot be enumerated"),
));
}
}
let lo_limit = (kind.lower_open() || *lo == ctx.neg_infinity()).then_some(Direction::Right);
let hi_limit = (kind.upper_open() || *hi == ctx.infinity()).then_some(Direction::Left);
cands.push(self.value_at(var, lo, lo_limit, op)?);
cands.push(self.value_at(var, hi, hi_limit, op)?);
Ok(cands)
}
fn extremum(
&self,
var: &Ex,
domain: &SetEx,
want_max: bool,
op: &'static str,
) -> Result<Ex, SymplexError> {
self.checked_id(var);
self.checked_id(domain);
require_symbol(op, var)?;
let Some(parts) = domain.as_intervals() else {
return Err(invalid(
op,
format!("the domain `{domain}` is not a union of intervals"),
));
};
if parts.is_empty() {
return Err(invalid(op, "the domain is empty"));
}
self.require_continuous(var, domain, op)?;
let mut cands = Vec::new();
for part in &parts {
cands.extend(self.candidates_on(var, part.as_ref(), op)?);
}
let (lo, hi) = min_max(cands, op)?;
Ok(if want_max { hi.value } else { lo.value })
}
}
fn poly_positive_on(p: &Poly, var: &Ex, part: Interval<&Ex>) -> Option<bool> {
let Interval {
lower: lo,
upper: hi,
kind,
} = part;
if p.is_positive_on(lo, hi) == Some(true) {
return Some(true);
}
if !p.is_nonnegative_on(lo, hi)? {
return Some(false);
}
let ctx = var.context();
let mut roots = p.count_real_roots_in(lo, hi)?;
let e = p.to_ex();
let vanishes_at = |pt: &Ex| e.subs(var, pt).eval().is_zero_structural();
if kind.lower_open() && *lo != ctx.neg_infinity() && vanishes_at(lo) {
roots = roots.saturating_sub(1);
}
if kind.upper_open() && *hi != ctx.infinity() && lo != hi && vanishes_at(hi) {
roots = roots.saturating_sub(1);
}
Some(roots == 0)
}
fn rational_nonneg_on(d: &Ex, var: &Ex, parts: &[Interval<Ex>], strict: bool) -> Option<bool> {
let (num, den) = d.as_numer_denom();
let pn = Poly::new(&num, &[var])?;
let pd = Poly::new(&den, &[var])?;
if !pn.has_rational_coeffs() || !pd.has_rational_coeffs() {
return None;
}
let neg_pn = pn.neg();
let neg_pd = pd.neg();
for part in parts {
let signed_num = if poly_positive_on(&pd, var, part.as_ref())? {
&pn
} else if poly_positive_on(&neg_pd, var, part.as_ref())? {
&neg_pn
} else {
return None;
};
if !signed_num.is_nonnegative_on(&part.lower, &part.upper)? {
return Some(false);
}
}
if strict && pn.is_zero() {
return Some(parts.iter().all(|part| part.lower == part.upper));
}
Some(true)
}
fn continuous_at_closed_endpoints(f: &Ex, var: &Ex, parts: &[Interval<Ex>]) -> bool {
let ctx = f.context();
let check = |e: &Ex, dir: Direction| -> bool {
let value = f.subs(var, e).eval();
if value.has_unevaluated() || !is_real_finite_point(&value) {
return false;
}
f.try_limit_dir(var, e, dir)
.is_ok_and(|lim| lim.equals(&value) == Some(true))
};
parts.iter().all(|part| {
let (lo, hi) = (&part.lower, &part.upper);
lo == hi
|| ((part.kind.lower_open()
|| *lo == ctx.neg_infinity()
|| check(lo, Direction::Right))
&& (part.kind.upper_open() || *hi == ctx.infinity() || check(hi, Direction::Left)))
})
}
fn interior_pole_breaks_monotonicity(f: &Ex, var: &Ex, domain: &SetEx) -> Option<bool> {
let ctx = f.context();
let interior = domain.interior()?;
let sing = f.singularities(var, Some(&interior)).ok()?;
if sing.is_empty() == Some(true) {
return Some(false);
}
let points = sing.as_finite_set()?;
let (inf, ninf) = (ctx.infinity(), ctx.neg_infinity());
for p in points {
let left = f.try_limit_dir(var, &p, Direction::Left).ok()?;
let right = f.try_limit_dir(var, &p, Direction::Right).ok()?;
if left == inf || left == ninf || right == inf || right == ninf {
return Some(true);
}
if !(is_real_finite_point(&left)
&& is_real_finite_point(&right)
&& left.equals(&right) == Some(true))
{
return None;
}
}
Some(false)
}
fn monotone_on(f: &Ex, var: &Ex, domain: &SetEx, strict: bool) -> Option<bool> {
if f.continuity_scan(var).opaque.is_some() {
return None;
}
let d = f.diff(var);
if d.has_unevaluated() {
return None;
}
let parts = domain.as_intervals()?;
if parts.is_empty() {
return Some(true);
}
if d.is_zero_structural() {
return Some(!strict || parts.iter().all(|part| part.lower == part.upper));
}
match interior_pole_breaks_monotonicity(f, var, domain) {
Some(true) => return Some(false),
Some(false) => {}
None => return None,
}
if let Some(answer) = rational_nonneg_on(&d, var, &parts, strict) {
return Some(answer);
}
if d.is_negative() == Some(true) {
return Some(false);
}
if (strict && d.is_positive() == Some(true)) || (!strict && d.is_nonnegative() == Some(true)) {
return Some(true);
}
let ge = d.solve_ge(var);
if ge.has_unevaluated() {
return None;
}
let region = match domain.is_subset(&ge) {
Some(true) => domain.clone(),
Some(false) => {
if !continuous_at_closed_endpoints(f, var, &parts) {
return Some(false);
}
let interior = domain.interior()?;
match interior.is_subset(&ge) {
Some(true) => interior,
Some(false) => return Some(false),
None => return None,
}
}
None => return None,
};
if !strict {
return Some(true);
}
let gt = d.solve_gt(var);
if gt.has_unevaluated() {
return None;
}
if region.is_subset(>) == Some(true) {
return Some(true);
}
let zeros = ge.difference(>).intersection(®ion).simplify();
zeros.as_finite_set().map(|_| true)
}
fn or3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
match (a, b) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
_ => None,
}
}
impl Ex {
pub fn singularities(&self, var: &Ex, domain: Option<&SetEx>) -> Result<SetEx, SymplexError> {
const OP: &str = "singularities";
self.checked_id(var);
require_symbol(OP, var)?;
let ctx = self.context();
let domain = match domain {
Some(d) => {
self.checked_id(d);
d.clone()
}
None => ctx.reals(),
};
let sources = self.continuity_scan(var).singular;
let mut result = ctx.empty_set();
for g in sources {
let zeros = zeros_in_domain(&self.wrap(g), var, &domain, OP)?;
result = result.union(&zeros);
}
Ok(result.simplify())
}
pub fn stationary_points(
&self,
var: &Ex,
domain: Option<&SetEx>,
) -> Result<SetEx, SymplexError> {
const OP: &str = "stationary_points";
self.checked_id(var);
require_symbol(OP, var)?;
let ctx = self.context();
let domain = match domain {
Some(d) => {
self.checked_id(d);
d.clone()
}
None => ctx.reals(),
};
let derivative = self.diff(var);
if derivative.has_unevaluated() {
return Err(computation_failed(
OP,
format!("the derivative `{derivative}` could not be evaluated"),
));
}
if derivative.is_zero_structural() {
return Ok(domain);
}
stationary_set(&derivative, var, &domain, OP)
}
pub fn maximum(&self, var: &Ex, domain: &SetEx) -> Result<Ex, SymplexError> {
self.extremum(var, domain, true, "maximum")
}
pub fn minimum(&self, var: &Ex, domain: &SetEx) -> Result<Ex, SymplexError> {
self.extremum(var, domain, false, "minimum")
}
#[must_use]
pub fn is_increasing(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
self.checked_id(var);
self.checked_id(domain);
monotone_on(self, var, domain, false)
}
#[must_use]
pub fn is_decreasing(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
self.checked_id(var);
self.checked_id(domain);
monotone_on(&-self, var, domain, false)
}
#[must_use]
pub fn is_strictly_increasing(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
self.checked_id(var);
self.checked_id(domain);
monotone_on(self, var, domain, true)
}
#[must_use]
pub fn is_strictly_decreasing(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
self.checked_id(var);
self.checked_id(domain);
monotone_on(&-self, var, domain, true)
}
#[must_use]
pub fn is_monotonic(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
or3(
self.is_increasing(var, domain),
self.is_decreasing(var, domain),
)
}
#[must_use]
pub fn is_convex(&self, var: &Ex, domain: &SetEx) -> Option<bool> {
self.checked_id(var);
self.checked_id(domain);
monotone_on(&self.diff(var), var, domain, false)
}
#[must_use]
pub fn periodicity(&self, var: &Ex) -> Option<Ex> {
let var_id = self.checked_id(var);
if !self.contains(var) {
return Some(self.context().zero());
}
let simplified = self.simplify();
for candidate in [simplified.raw_id(), self.raw_id()] {
let period = {
let mut inner = self.inner.write();
util::periodicity(&mut inner.arena, candidate, var_id)
};
if let Some(id) = period {
return Some(self.wrap(id).eval());
}
}
None
}
pub fn function_range(&self, var: &Ex, domain: &SetEx) -> Result<SetEx, SymplexError> {
const OP: &str = "function_range";
self.checked_id(var);
self.checked_id(domain);
require_symbol(OP, var)?;
let ctx = self.context();
let Some(parts) = domain.as_intervals() else {
return Err(invalid(
OP,
format!("the domain `{domain}` is not a union of intervals"),
));
};
if parts.is_empty() {
return Ok(ctx.empty_set());
}
self.require_continuous(var, domain, OP)?;
let (inf, ninf) = (ctx.infinity(), ctx.neg_infinity());
let mut result = ctx.empty_set();
for part in &parts {
let cands = self.candidates_on(var, part.as_ref(), OP)?;
let (min, max) = min_max(cands, OP)?;
let piece = if min.value == max.value {
ctx.finite_set(&[min.value])
} else {
let left_open = !min.attained || min.value == ninf;
let right_open = !max.attained || max.value == inf;
ctx.interval(
&min.value,
&max.value,
IntervalKind::from_open_ends(left_open, right_open),
)
};
result = result.union(&piece);
}
Ok(result.simplify())
}
}