///|
priv suberror InputError {
InputError(String)
}
///|
priv suberror FieldError {
FieldError(String, String)
}
///|
/// Prefix nested field paths without parsing human-readable messages.
fn[T] located(path : String, action : () -> T raise) -> T raise {
action() catch {
FieldError(field, message) => raise FieldError(path + "." + field, message)
InputError(message) => raise FieldError(path, message)
e => raise e
}
}
///|
fn obj(value : Json, allowed : Array[String]) -> Map[String, Json] raise {
guard value is Object(fields) else { raise InputError("expected object") }
for key, _ in fields {
if !allowed.contains(key) {
raise FieldError(key, "unknown field: " + key)
}
}
fields
}
///|
fn required(fields : Map[String, Json], key : String) -> Json raise {
match fields.get(key) {
Some(v) => v
None => raise FieldError(key, "missing field: " + key)
}
}
///|
fn num(fields : Map[String, Json], key : String) -> Double raise {
match required(fields, key) {
Number(n, ..) =>
if finite(n) {
n
} else {
raise FieldError(key, "non-finite field: " + key)
}
_ => raise FieldError(key, "expected number: " + key)
}
}
///|
fn integer(
fields : Map[String, Json],
key : String,
low : Int32,
high : Int32,
) -> Int32 raise {
let n = num(fields, key)
if n < low.to_double() || n > high.to_double() || @math.floor(n) != n {
raise FieldError(key, "integer out of range: " + key)
}
n.to_int()
}
///|
fn text(fields : Map[String, Json], key : String) -> String raise {
match required(fields, key) {
String(s) => s
_ => raise FieldError(key, "expected string: " + key)
}
}
///|
fn[T] checked(value : Result[T, String]) -> T raise {
match value {
Ok(v) => v
Err(e) => raise InputError(e)
}
}
///|
fn checked_search(
value : Result[SearchResult, SearchError],
) -> SearchResult raise {
match value {
Ok(v) => v
Err(InvalidInput(e)) => raise InputError(e)
Err(IndeterminateGeometry(e)) => raise RequestError(CheckIndeterminate, e)
Err(NumericalFailure(e)) => raise RequestError(CalculationFailed, e)
Err(NoCandidate(_)) =>
raise RequestError(
NoCandidateFound,
"no sampled-accepted grid candidate found; this is not proof of continuous infeasibility",
)
}
}
///|
fn decode_program(value : Json) -> Program raise {
guard value is Array(items) else {
raise FieldError("segments", "segments must be an array")
}
if items.length() > Segments_max {
raise FieldError("segments", "too many segments")
}
let segments : Array[Segment] = []
let mut height = 0.0
for i = 0; i < items.length(); i = i + 1 {
located("segments[\{i}]", fn() raise {
let f = obj(items[i], ["angle_deg", "lift_mm", "law"])
let law = match text(f, "law") {
"cycloidal" => Cycloidal
"345" => Polynomial345
"4567" => Polynomial4567
_ => raise FieldError("law", "law must be cycloidal, 345 or 4567")
}
let lift = num(f, "lift_mm")
if lift.abs() > Geometry_length_max {
raise FieldError(
"lift_mm",
"lift magnitude exceeds \{Geometry_length_max} mm",
)
}
let angle = num(f, "angle_deg") * @math.PI / 180.0
if !finite(angle) || angle < Segment_angle_min {
raise FieldError(
"angle_deg",
"segment angle must be finite and >= \{Segment_angle_min} rad",
)
}
height = height + lift
if !finite(height) || height < -1.0e-9 {
raise FieldError(
"lift_mm", "cumulative lift must stay nonnegative and finite",
)
}
segments.push({ angle, lift, law, })
})
}
located("segments", fn() raise { checked(program(segments)) })
}
///|
/// Shared wire limit in UTF-8 bytes, exported to CLI hosts.
pub fn input_limit_bytes() -> Int32 {
Input_bytes_max
}
///|
priv suberror RequestError {
RequestError(ErrorCode, String)
}
///|
fn selected_outputs(f : Map[String, Json]) -> Array[String] raise {
let choices : Array[String] = []
let value = f.get("outputs").unwrap_or(Json(["points"]))
guard value is Array(items) else {
raise FieldError("outputs", "outputs must be an array")
}
for item in items {
guard item is String(name) else {
raise FieldError("outputs", "output names must be strings")
}
if !["points", "csv", "svg", "dxf"].contains(name) || choices.contains(name) {
raise FieldError("outputs", "unknown or duplicate output: " + name)
}
choices.push(name)
}
choices
}
///|
fn error_json(code : ErrorCode, message : String) -> String {
field_error_json(code, message, Json(null))
}
///|
fn field_error_json(code : ErrorCode, message : String, field : Json) -> String {
let err : Json = {
"ok": false,
"schema_version": Json(Schema_version),
"generator": generator_json(),
"error_code": Json(code.code()),
"field": field,
"error": message,
}
err.stringify()
}
///|
fn crossing_json(check : CrossingCheck) -> Json {
match check {
Indeterminate(n, pairs) =>
{
"status": Json(check.status()),
"count": Json(null),
"confirmed_crossings": Json(n),
"uncertain_pairs": Json(pairs),
}
Checked(n) => { "status": Json(check.status()), "count": Json(n) }
NotChecked => { "status": Json(check.status()), "count": Json(null) }
Failed(message) =>
{ "status": Json(check.status()), "count": Json(null), "error": message }
}
}
///|
fn location_json(location : Location) -> Json {
{
"sample_index": match location.sample_index {
Some(index) => Json(index)
None => Json(null)
},
"segment_index": Json(location.segment_index),
"theta_rad": Json(location.theta_rad),
"segment_start_rad": Json(location.segment_start_rad),
"segment_end_rad": Json(location.segment_end_rad),
"law": Json(location.law),
"periodic_endpoint": Json(location.periodic_endpoint),
}
}
///|
fn constraint_json(d : ConstraintDiagnostic, pressure : Bool) -> Json {
let scale = if pressure { 180.0 / @math.PI } else { 1.0 }
{
"code": Json(d.code),
"value": Json(d.value * scale),
"limit": Json(d.limit * scale),
"margin": Json(d.margin * scale),
"passed": Json(d.passed),
"unit": Json(if pressure { "deg" } else { "dimensionless" }),
"comparison": Json(
if pressure {
"value <= limit"
} else {
"value > limit"
},
),
"location": location_json(d.location),
}
}
///|
fn reason_codes(a : Analysis, d : Diagnostics) -> Array[String] {
let reasons : Array[String] = []
if !d.pressure.passed {
reasons.push("pressure_limit_exceeded")
}
if !d.offset_factor.passed {
reasons.push("offset_factor_rejected")
}
match a.crossings {
Checked(n) => if n > 0 { reasons.push("profile_crossings") }
Indeterminate(n, _) => {
if n > 0 {
reasons.push("profile_crossings")
}
reasons.push("geometry_indeterminate")
}
Failed(_) => reasons.push("geometry_failed")
NotChecked => reasons.push("geometry_not_checked")
}
reasons
}
///|
fn edge_json(
p : Program,
points : Array[CamPoint],
index : Int32,
) -> Json raise RequestError {
let edge = match locate_edge(p, points, index) {
Ok(edge) => edge
Err(message) =>
raise RequestError(
InternalError,
"cannot encode profile edge \{index}: \{message}",
)
}
{
"edge_index": Json(index),
"segment_index": Json(edge.segment_index),
"start": location_json(edge.start),
"end": location_json(edge.end),
"start_x": Json(points[index].x),
"start_y": Json(points[index].y),
"end_x": Json(points[index + 1].x),
"end_y": Json(points[index + 1].y),
}
}
///|
fn comparison_json(comparison : SamplingComparison) -> Json {
{
"levels": Json(
comparison.levels.map(fn(level) {
{
"nominal_resolution": Json(level.nominal_resolution),
"sample_count": Json(level.sample_count),
"pressure": constraint_json(level.pressure, true),
"offset_factor": constraint_json(level.offset_factor, false),
"crossing_check": crossing_json(level.crossings),
"state": Json(level.state),
}
}),
),
"last_pressure_change_deg": Json(
comparison.pressure_change * 180.0 / @math.PI,
),
"last_offset_factor_change": Json(comparison.offset_factor_change),
"state_consistent": Json(comparison.state_consistent),
"scope": "Observed sampled changes only; no convergence certificate or continuous error bound. Indices belong to each level's own mesh.",
}
}
///|
/// Summaries are always present; heavy outputs are generated only on request.
fn report(
a : Analysis,
g : Geometry,
omega : Double,
outputs : Array[String],
p : Program,
pressure_limit : Double,
) -> Json raise RequestError {
let state = a.state()
let diagnostics = match diagnose(p, a, pressure_limit) {
Ok(value) => value
Err(message) =>
raise RequestError(
InternalError,
"cannot encode report diagnostics: \{message}",
)
}
let witnesses : Array[Json] = []
for w in a.crossing_witnesses {
witnesses.push({
"kind": Json(w.kind),
"edge_a": edge_json(p, a.points, w.edge_a),
"edge_b": edge_json(p, a.points, w.edge_b),
})
}
let fields : Map[String, Json] = {
"schema_version": Json(Schema_version),
"generator": generator_json(),
"units": Json("mm"),
"state": Json(state),
"sampled_local_feasible": Json(a.sampled_local_feasible),
"sampled_feasible": Json(a.sampled_feasible),
"crossing_check": crossing_json(a.crossings),
"diagnostics": {
"pressure": constraint_json(diagnostics.pressure, true),
"offset_factor": constraint_json(diagnostics.offset_factor, false),
"reasons": Json(reason_codes(a, diagnostics)),
},
"segments": Json(
p.segments.mapi(fn(i, _) {
let location = location_at(p, p.starts[i], None)
{
"segment_index": Json(i),
"start_rad": Json(location.segment_start_rad),
"end_rad": Json(location.segment_end_rad),
"law": Json(location.law),
}
}),
),
"crossing_evidence": {
"witnesses": Json(witnesses),
"truncated": Json(a.crossing_witnesses_truncated),
"limit": Json(Crossing_witnesses_max),
},
"base_radius_mm": Json(g.base_radius),
"roller_radius_mm": Json(g.roller_radius),
"offset_mm": Json(g.offset),
"omega_rad_s": Json(omega),
"sample_count": Json(a.points.length()),
"sampling": Json({
"nominal_resolution": Json(a.nominal_resolution),
"actual_intervals": Json(a.points.length() - 1),
"sample_count": Json(a.points.length()),
"preserves_segment_boundaries": true,
}),
"max_pressure_deg": Json(a.max_pressure * 180.0 / @math.PI),
"worst_pressure_angle_deg": Json(a.pressure_angle * 180.0 / @math.PI),
"min_offset_factor": Json(a.min_offset_factor),
"min_offset_factor_angle_rad": Json(a.min_offset_factor_angle),
"max_velocity_mm_s": Json(a.max_velocity),
"max_acceleration_mm_s2": Json(a.max_acceleration),
"max_jerk_mm_s3": Json(a.max_jerk),
"jerk_jumps": Json(a.jerk_jumps),
"scope": Json(
"Sampled local constraints plus filtered polyline crossing check; uncertain contacts never accepted; no continuous or global collision certification.",
),
}
if outputs.contains("points") {
fields.set(
"points",
Json(
a.points.mapi(fn(index, c) {
{
"sample_index": Json(index),
"segment_index": Json(
location_at(p, c.theta, Some(index)).segment_index,
),
"theta_rad": Json(c.theta),
"lift_mm": Json(c.motion.s),
"velocity_mm_s": Json(c.motion.v * omega),
"acceleration_mm_s2": Json(c.motion.a * omega * omega),
"jerk_mm_s3": Json(c.motion.j * omega * omega * omega),
"pitch_x": Json(c.pitch_x),
"pitch_y": Json(c.pitch_y),
"x": Json(c.x),
"y": Json(c.y),
"pressure_deg": Json(c.pressure * 180.0 / @math.PI),
"pitch_curvature": Json(c.curvature),
"offset_factor": Json(c.offset_factor),
}
}),
),
)
}
if outputs.contains("csv") {
fields.set("csv", Json(csv(a.points)))
}
if outputs.contains("svg") {
fields.set("svg", Json(svg(a.points)))
}
if outputs.contains("dxf") {
fields.set("dxf", Json(dxf_mm(a.points)))
}
Json(fields)
}
///|
fn report_with_comparison(
p : Program,
a : Analysis,
g : Geometry,
omega : Double,
pressure : Double,
outputs : Array[String],
compare : Bool,
) -> Json raise {
let result = report(a, g, omega, outputs, p, pressure)
if compare {
let comparison = match
compare_sampling_cached(
p,
g,
comparison_resolutions(a.nominal_resolution),
pressure,
omega,
Some(a),
) {
Ok(c) => c
Err(e) => raise RequestError(CalculationFailed, e)
}
guard result is Object(fields) else {
raise RequestError(InternalError, "invalid report")
}
fields.set("sampling_comparison", comparison_json(comparison))
}
result
}
///|
/// Schema v2: byte-bounded input, selected outputs, typed errors and check states.
fn run_json_cached(input : String, cache : Array[Array[CamPoint]]) -> String {
cache.clear()
try {
if input.length() > input_limit_bytes() ||
@utf8.encode(input).length() > input_limit_bytes() {
raise RequestError(
InputTooLarge,
"input exceeds \{Input_bytes_max} UTF-8 bytes",
)
}
let value = @json.parse(input) catch {
_ => raise RequestError(InvalidJson, "invalid JSON")
}
let f = obj(value, [
"action", "units", "segments", "base_radius_mm", "roller_radius_mm", "offset_mm",
"rpm", "resolution", "pressure_limit_deg", "search", "outputs", "include_input",
"compare_sampling",
])
let outputs = selected_outputs(f)
let include_input = match f.get("include_input") {
None => false
Some(True) => true
Some(False) => false
_ => raise FieldError("include_input", "include_input must be boolean")
}
let compare = match f.get("compare_sampling") {
None | Some(False) => false
Some(True) => true
_ =>
raise FieldError("compare_sampling", "compare_sampling must be boolean")
}
if text(f, "units") != "mm" {
raise FieldError("units", "JSON interface requires mm units")
}
let p = decode_program(required(f, "segments"))
let action = text(f, "action")
if action != "analyze" && action != "search" {
raise FieldError("action", "action must be analyze or search")
}
let roller = num(f, "roller_radius_mm")
if roller <= 0.0 || roller > Geometry_length_max {
raise FieldError(
"roller_radius_mm",
"roller radius must be in (0, \{Geometry_length_max}]",
)
}
let offset = num(f, "offset_mm")
let omega = num(f, "rpm") * (2.0 * @math.PI / 60.0)
if !finite(omega) || omega <= 0.0 {
raise FieldError("rpm", "speed must be finite and positive")
}
let resolution = integer(
f,
"resolution",
Resolution_min,
Json_resolution_max,
)
if action == "search" && resolution > Search_resolution_max {
raise FieldError(
"resolution",
"search resolution must be <= \{Search_resolution_max}",
)
}
let pressure = num(f, "pressure_limit_deg") * (@math.PI / 180.0)
if pressure <= 0.0 || pressure >= @math.PI / 2.0 {
raise FieldError(
"pressure_limit_deg", "pressure limit must be in (0, 90) degrees",
)
}
let result : Json = match action {
"analyze" => {
let radius = num(f, "base_radius_mm")
if radius < Base_radius_min || radius > Geometry_length_max {
raise FieldError("base_radius_mm", "base radius out of range")
}
if offset.abs() >= radius + roller {
raise FieldError(
"offset_mm", "offset magnitude must be smaller than prime-circle radius",
)
}
let g : Geometry = {
base_radius: radius,
roller_radius: roller,
offset,
}
let a = match analyze(p, g, resolution, pressure, omega) {
Ok(a) => a
Err(e) => raise RequestError(CalculationFailed, e)
}
cache.push(a.points)
{
"ok": true,
"schema_version": Json(Schema_version),
"generator": generator_json(),
"result": report_with_comparison(
p, a, g, omega, pressure, outputs, compare,
),
}
}
"search" => {
let search_value = required(f, "search")
let (lower, upper, intervals) = located("search", fn() raise {
let s = obj(search_value, ["lower_mm", "upper_mm", "intervals"])
let lower = num(s, "lower_mm")
let upper = num(s, "upper_mm")
if lower < Base_radius_min || lower > Geometry_length_max {
raise FieldError("lower_mm", "search lower radius out of range")
}
if upper < lower || upper > Geometry_length_max {
raise FieldError(
"upper_mm", "search upper radius out of range or below lower bound",
)
}
(lower, upper, integer(s, "intervals", 1, Search_intervals_max))
})
if offset.abs() >= upper + roller {
raise FieldError(
"offset_mm", "offset magnitude must be smaller than upper prime-circle radius",
)
}
let found = checked_search(
search_base(
p, roller, offset, lower, upper, intervals, resolution, pressure, omega,
),
)
cache.push(found.analysis.points)
{
"ok": true,
"schema_version": Json(Schema_version),
"generator": generator_json(),
"tested": Json(found.tested),
"grid_step_mm": Json(found.grid_step),
"search_scope": "First sampled-accepted grid candidate; topology completed on the fine mesh; not a continuous optimum.",
"result": report_with_comparison(
p,
found.analysis,
found.geometry,
omega,
pressure,
outputs,
compare,
),
}
}
_ => raise FieldError("action", "action must be analyze or search")
}
if include_input {
match result {
Object(fields) => fields.set("input", value)
_ => ()
}
}
result.stringify()
} catch {
FieldError(field, message) => {
cache.clear()
field_error_json(InvalidRequest, message, Json(field))
}
InputError(message) => {
cache.clear()
error_json(InvalidRequest, message)
}
RequestError(code, message) => {
cache.clear()
error_json(code, message)
}
_ => {
cache.clear()
error_json(InternalError, "unexpected calculation failure")
}
}
}
///|
/// Stateless request API. Use a Session to export a previous result without recomputation.
pub fn run_json(input : String) -> String {
run_json_cached(input, [])
}
///|
/// One host-owned calculation result; cache lifetime is explicit, not process-global.
pub struct Session {
points : Array[Array[CamPoint]]
}
///|
pub fn session() -> Session {
{ points: [], }
}
///|
pub fn Session::run(self : Session, input : String) -> String {
run_json_cached(input, self.points)
}
///|
pub fn Session::export_profile(self : Session, format : String) -> String {
if self.points.is_empty() {
return error_json(NoResult, "calculate before exporting")
}
let points = self.points[0]
let content = match checked_export(points, format) {
Ok(value) => value
Err(message) => return error_json(InvalidRequest, message)
}
let result : Json = {
"ok": true,
"schema_version": Json(Schema_version),
"generator": generator_json(),
"format": Json(format),
"content": Json(content),
}
result.stringify()
}
///|
/// Validate a JSON segment array without sampling or changing a Session cache.
/// Totals are present for numeric arrays, including programs which do not close.
pub fn validate_segments_json(input : String) -> String {
let mut total_angle = 0.0
let mut total_lift = 0.0
let mut totals_valid = false
let response = try {
if input.length() > Input_bytes_max ||
@utf8.encode(input).length() > Input_bytes_max {
raise RequestError(
InputTooLarge,
"segment input exceeds UTF-8 byte limit",
)
}
let value = @json.parse(input) catch {
_ => raise RequestError(InvalidJson, "invalid JSON")
}
guard value is Array(items) else {
raise FieldError("segments", "segments must be an array")
}
if items.length() > Segments_max {
raise FieldError("segments", "too many segments")
}
totals_valid = true
for i = 0; i < items.length(); i = i + 1 {
match items[i] {
Object(fields) => {
match fields.get("angle_deg") {
Some(Number(n, ..)) => total_angle = total_angle + n
_ => totals_valid = false
}
match fields.get("lift_mm") {
Some(Number(n, ..)) => total_lift = total_lift + n
_ => totals_valid = false
}
}
_ => totals_valid = false
}
}
let p = decode_program(value)
Json({
"ok": Json(true),
"schema_version": Json(Schema_version),
"generator": generator_json(),
"segment_count": Json(p.segments.length()),
})
} catch {
FieldError(field, message) =>
@json.parse(field_error_json(InvalidRequest, message, Json(field))) catch {
_ => Json(null)
}
InputError(message) =>
@json.parse(error_json(InvalidRequest, message)) catch {
_ => Json(null)
}
RequestError(code, message) =>
@json.parse(error_json(code, message)) catch {
_ => Json(null)
}
_ =>
@json.parse(error_json(InternalError, "unexpected validation failure")) catch {
_ => Json(null)
}
}
match response {
Object(fields) =>
if totals_valid && finite(total_angle) && finite(total_lift) {
fields.set("total_angle_deg", Json(total_angle))
fields.set("total_lift_mm", Json(total_lift))
}
_ => ()
}
response.stringify()
}