///|
pub(all) struct Constraint {
id : String
from : String
to : String
lower : Int32?
upper : Int32?
} derive(Eq, Debug)
///|
pub(all) suberror StnError {
StnError(String, String)
}
///|
pub(all) struct Edge {
id : String
from : String
to : String
bound : Int64
side : String
} derive(Eq, Debug)
///|
pub struct Network {
names : Array[String]
edges : Array[Edge]
}
///|
pub struct Solution {
names : Array[String]
values : Array[Int64]
distances : Array[Array[Int64?]]
}
///|
pub(all) enum Outcome {
Feasible(Solution)
Inconsistent(Array[Edge])
}
///|
priv struct Budget {
mut remaining : Int32
}
///|
fn Budget::tick(self : Budget) -> Unit raise StnError {
self.charge(1)
}
///|
fn Budget::charge(self : Budget, steps : Int32) -> Unit raise StnError {
if self.remaining < steps {
raise StnError(
"limit_exceeded", "work budget exhausted; no feasibility conclusion",
)
}
self.remaining -= steps
}
///|
fn valid_name(name : String) -> Bool {
if name.length() == 0 || name.length() > 64 {
return false
}
for c in name.iter() {
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '-' ||
c == '.') {
return false
}
}
true
}
///|
fn find_name(names : Array[String], name : String) -> Int32 raise StnError {
for i = 0; i < names.length(); i = i + 1 {
if names[i] == name {
return i
}
}
raise StnError("invalid_input", "unknown timepoint: " + name)
}
///|
pub fn network(
names : Array[String],
constraints : Array[Constraint],
) -> Network raise StnError {
if names.length() == 0 || names.length() > 64 || constraints.length() > 512 {
raise StnError(
"limit_exceeded", "require 1..64 timepoints and at most 512 constraints",
)
}
let seen : Map[String, Bool] = Map([])
for name in names {
if !valid_name(name) || seen.contains(name) {
raise StnError("invalid_input", "invalid or duplicate timepoint name")
}
seen[name] = true
}
let ids : Map[String, Bool] = Map([])
let edges : Array[Edge] = []
for c in constraints {
if !valid_name(c.id) || ids.contains(c.id) {
raise StnError("invalid_input", "invalid or duplicate constraint id")
}
ids[c.id] = true
find_name(names, c.from) |> ignore
find_name(names, c.to) |> ignore
if c.lower is None && c.upper is None {
raise StnError(
"invalid_input", "constraint must have a lower or upper bound",
)
}
for bound in [c.lower, c.upper] {
if bound is Some(v) && (v < -1000000 || v > 1000000) {
raise StnError(
"invalid_input", "bounds must be integers in -1000000..1000000",
)
}
}
// Reversed intervals are legitimate inconsistent constraints, not syntax errors.
if c.upper is Some(v) {
edges.push({
id: c.id,
from: c.from,
to: c.to,
bound: v.to_int64(),
side: "upper",
})
}
if c.lower is Some(v) {
edges.push({
id: c.id,
from: c.to,
to: c.from,
bound: -v.to_int64(),
side: "lower",
})
}
}
{ names: names.copy(), edges, }
}
///|
pub fn Network::timepoints(self : Network) -> Array[String] {
self.names.copy()
}
///|
pub fn Solution::assignment(self : Solution) -> Array[(String, Int64)] {
self.names.mapi((i, name) => (name, self.values[i]))
}
///|
/// Returns the tight interval for to - from; None means unbounded in that direction.
pub fn Solution::bounds(
self : Solution,
from : String,
to : String,
) -> (Int64?, Int64?) raise StnError {
let a = find_name(self.names, from)
let b = find_name(self.names, to)
(self.distances[b][a].map(v => -v), self.distances[a][b])
}
///|
pub fn version() -> String {
"0.1.1"
}
///|
pub extend Constraint with Eq::{equal, not_equal}
///|
pub extend Constraint with @debug.Debug::{to_repr}
///|
pub extend Edge with Eq::{equal, not_equal}
///|
pub extend Edge with @debug.Debug::{to_repr}