Skip to content

Rounding ​

RoundingFunction maps a linear polynomial to an integer using the numeric implementation's round operation:

y=round(p).

The direct-evaluation and solver contracts are intentionally documented separately because their half-integer behavior is not identical.

Contract ​

  • Input: x: LinearPolynomial<V>.
  • Output: an IntVar (resultVar) exposed as resultPolynomial.
  • evaluate returns null when the input is not evaluable; otherwise it delegates to converter.fromValue(x).round().
  • bigM controls the fractional-indicator gate and is normalized to at least one; it must be large enough for that gate, although the default is one.

Mathematical definition ​

The solver decomposes the input as

k=⌊p⌋,b=p−k,0≤b<1,

then chooses binary r with

b≥0.5r,b≤0.5−ε+Mr,

and registers

y=k+r.

Consequently, the solver chooses r = 0 below 0.5 and r = 1 at or above 0.5: ties go toward positive infinity.

Domain and boundaries ​

All finite real inputs are accepted by direct evaluation. Solver strictness around integer boundaries uses NONZERO_TOLERANCE, and the fractional gate uses the normalized Big-M. The important semantic boundary is a half-integer:

PathHalf-integer ruleExample
Solver registrationb >= 0.5 rounds up (k + 1)2.5 -> 3, -1.5 -> -1
Flt32 / Flt64 evaluationdelegates to kotlin.math.round, ties-to-even2.5 -> 2, -1.5 -> -2
FltX evaluationBigDecimal HALF_UP2.5 -> 3, -1.5 -> -2

Do not use a half-integer as a cross-check between evaluate and a solver result without accounting for this mismatch.

Current API ​

Kotlin ​

Source: Rounding.kt (constructor, evaluation, and constraints)

The numeric behavior used by evaluate is implemented in Floating.kt (Flt64.round) and FltX.round.

kotlin
RoundingFunction(
    x: LinearPolynomial<V>,
    bigM: V? = null,
    converter: IntoValue<V>,
    name: String,
    displayName: String? = null
)

Rust ​

Rust exposes RoundingFunction with an explicit rounding kind:

rust
RoundingFunction::new(id: u64, name: &str, input: Linear<V>, kind: RoundingKind) -> RoundingFunction<V>
RoundingFunction::round(id: u64, name: &str, input: Linear<V>) -> RoundingFunction<V>

RoundingKind is Floor, Ceil, Round, or Trunc; named_round and the analogous helpers are also provided. Rust keeps an integer helper but exposes a continuous result_variable(), and it has no Kotlin bigM/converter parameter. Its round boundary is implemented by the Rust function's own ROUNDING_EPSILON rules, so do not assume half-integer tie behavior is identical across languages.

Solver mathematical model ​

Kotlin introduces k∈Z, r∈{0,1}, b≥0, and integer result y, then passes

p−k≥0,p−k≤1−ε,b−p+k=0,b−0.5r≥0,b−Mr≤0.5−ε,y−k−r=0.

Rust uses the row family belonging to its selected RoundingKind; the Round variant likewise links a continuous result to an integer helper, but uses Rust's own fixed boundary epsilon.

evaluate versus solver ​

evaluate calls the converter's round; it does not create helpers or use Big-M. Solver registration always uses floor plus the b >= 0.5 gate. The paths agree away from half-integers, but can return different values exactly at ties as shown above.

Examples and tests ​

kotlin
import fuookami.ospf.kotlin.core.solver.value.IntoValue
import fuookami.ospf.kotlin.core.symbol.function.RoundingFunction
import fuookami.ospf.kotlin.core.variable.RealVar
import fuookami.ospf.kotlin.math.algebra.number.Flt64
import fuookami.ospf.kotlin.math.symbol.Symbol
import fuookami.ospf.kotlin.math.symbol.inequality.eq
import fuookami.ospf.kotlin.math.symbol.monomial.LinearMonomial
import fuookami.ospf.kotlin.math.symbol.polynomial.LinearPolynomial

val x = RealVar("x")
val xPoly = LinearPolynomial(listOf(LinearMonomial(Flt64.one, x)), Flt64.zero)
val round = RoundingFunction(
    x = xPoly,
    converter = IntoValue.Identity,
    name = "round"
)
val value = round.evaluate(mapOf<Symbol, Flt64>(x to Flt64(1.2)))
check(value != null && (value eq Flt64.one))
rust
use ospf_rust_core::symbol::flatten::{Linear, LinearMonomial};
use ospf_rust_core::symbol::function::RoundingFunction;

let input = Linear::new(vec![LinearMonomial::new(1.0, 0)], 0.0);
let round = RoundingFunction::round(1, "round", input);
let _result = round.result_variable();
assert!(round.sign_variable().is_some());

Complete example: RoundTest.kt

Core validation: FunctionSymbolDiscreteGenericEvaluateTest.kt and FunctionSymbolRoundingGenericRegistrationTest.kt

Rust source and parity coverage: rounding.rs and gurobi_linear_function_kotlin_parity.rs.

  • floor: the solver's integer decomposition step.
  • ceiling: upper-integer transformation without a divisor.
  • mod: floor-based remainder with d > 0.