///|
fn print_usage() -> Unit {
println("MoonBit GaitLab - Biomechanical Gait Analysis CLI Utility")
println("Usage:")
println(" moon run main -- [options]")
println("")
println("Options:")
println(
" -d, --demo Run analysis on a high-fidelity simulated clinical gait dataset",
)
println(
" -c, --csv-data Analyze raw CSV joint trajectory dataset passed as string",
)
println(
" -j, --json-data Analyze raw JSON joint trajectory dataset passed as string",
)
println(
" -f, --format Output format (default: markdown)",
)
println(" --strict Treat trajectory quality errors as fatal")
println(
" --sampling-rate Override the input sampling-rate metadata",
)
println(" --benchmark Print benchmark entry-point guidance")
println(" -h, --help Display this help information")
println("")
println("Examples:")
println(" moon run main -- --demo")
println(" moon run main -- --demo > report.md")
}
///|
fn option_value(args : Array[String], name : String) -> String? {
for index = 0; index + 1 < args.length(); index = index + 1 {
if args[index] == name {
return Some(args[index + 1])
}
}
None
}
///|
fn analysis_config(args : Array[String]) -> @pipeline.AnalysisConfig {
let defaults = @pipeline.default_config()
let strict = args.contains("--strict")
let mut sampling_rate = defaults.sampling_rate
match option_value(args, "--sampling-rate") {
None => ()
Some(value) =>
match @math.parse_double(value) {
Ok(rate) => if rate > 0.0 { sampling_rate = rate }
Err(_) => ()
}
}
@pipeline.AnalysisConfig::{
strict,
sampling_rate,
filter: defaults.filter,
events: defaults.events,
}
}
///|
fn output_format(args : Array[String]) -> String {
match option_value(args, "--format") {
Some(value) => value
None => "markdown"
}
}
///|
fn parse_cell(row : Array[String], index : Int) -> Result[Double, String] {
if index < 0 || index >= row.length() {
return Err("CSV row is missing column " + index.to_string())
}
match @math.parse_double(row[index]) {
Ok(value) => Ok(value)
Err(_) => Err("CSV column " + index.to_string() + " is not numeric")
}
}
///|
fn parse_csv_frames(input : String) -> Result[Array[@gait.JointFrame], String] {
match @csv.parse_csv(input) {
Err(error) => Err(error)
Ok(rows) => {
if rows.length() < 2 {
return Err("CSV requires a header and at least one row")
}
if rows[0].length() < 13 {
return Err("CSV header must contain time plus 12 coordinate columns")
}
let frames = Array::new()
for row_index = 1; row_index < rows.length(); row_index = row_index + 1 {
let row = rows[row_index]
let values = Array::new()
for column = 0; column < 13; column = column + 1 {
match parse_cell(row, column) {
Err(error) => return Err(error + " at row " + row_index.to_string())
Ok(value) => values.push(value)
}
}
frames.push(@gait.JointFrame::{
time: values[0],
hip: @math.Point3D::{ x: values[1], y: values[2], z: values[3] },
knee: @math.Point3D::{ x: values[4], y: values[5], z: values[6] },
ankle: @math.Point3D::{ x: values[7], y: values[8], z: values[9] },
foot: @math.Point3D::{ x: values[10], y: values[11], z: values[12] },
})
}
Ok(frames)
}
}
}
///|
fn json_number(
value : @json.JsonValue?,
label : String,
) -> Result[Double, String] {
match value {
Some(@json.JsonValue::Number(number)) => Ok(number)
_ => Err("JSON field " + label + " must be numeric")
}
}
///|
fn json_point(
value : @json.JsonValue?,
label : String,
) -> Result[@math.Point3D, String] {
match value {
Some(object) =>
match object {
@json.JsonValue::Object(values) =>
match json_number(values.get("x"), label + ".x") {
Err(error) => Err(error)
Ok(x) =>
match json_number(values.get("y"), label + ".y") {
Err(error) => Err(error)
Ok(y) =>
match json_number(values.get("z"), label + ".z") {
Err(error) => Err(error)
Ok(z_value) => Ok(@math.Point3D::{ x, y, z: z_value })
}
}
}
_ => Err("JSON field " + label + " must be an object")
}
None => Err("JSON field " + label + " is missing")
}
}
///|
fn parse_json_frames(input : String) -> Result[Array[@gait.JointFrame], String] {
match @json.parse_json(input) {
Err(error) => Err(error)
Ok(root) =>
match root.get("frames") {
Some(@json.JsonValue::Array(items)) => {
let frames = Array::new()
for item in items {
match item {
@json.JsonValue::Object(values) =>
match json_number(values.get("time"), "time") {
Err(error) => return Err(error)
Ok(time) =>
match json_point(values.get("left_foot"), "left_foot") {
Err(error) => return Err(error)
Ok(foot) =>
frames.push(@gait.JointFrame::{
time,
hip: foot,
knee: foot,
ankle: foot,
foot,
})
}
}
_ => return Err("JSON frames must contain objects")
}
}
Ok(frames)
}
_ => Err("JSON must contain a frames array")
}
}
}
///|
fn should_print_demo_banner(format : String) -> Bool {
format != "json" && format != "JSON" && format != "csv" && format != "CSV"
}
///|
fn run_demo(format : String, config : @pipeline.AnalysisConfig) -> Unit {
let verbose = should_print_demo_banner(format)
if verbose {
println("Initializing MoonBit GaitLab Demo...")
println("Simulating 3.5 seconds of post-stroke hemiplegic walk at 100Hz...")
}
let left_frames = Array::new()
let right_frames = Array::new()
let fs = 100.0
let dt = 1.0 / fs
let num_frames = 350
for i = 0; i < num_frames; i = i + 1 {
let t = i.to_double() * dt
let speed = 0.7
let x_disp = speed * t
// Left foot (Healthy)
let left_stride_progress = t - (t / 1.0).to_int().to_double() * 1.0
let left_height = if left_stride_progress > 0.60 {
let swing = (left_stride_progress - 0.60) / 0.40
0.12 * @math.sin(swing * @math.PI)
} else {
0.0
}
let left_foot = @math.Point3D::{ x: x_disp, y: left_height, z: 0.0 }
left_frames.push(@gait.JointFrame::{
time: t,
hip: @math.Point3D::{ x: x_disp, y: 0.9, z: 0.0 },
knee: @math.Point3D::{ x: x_disp, y: 0.5, z: 0.0 },
ankle: @math.Point3D::{ x: x_disp, y: 0.1, z: 0.0 },
foot: left_foot,
})
// Right foot (Hemiplegic affected)
let right_t_shifted = t + 0.5
let right_stride_progress = right_t_shifted -
(right_t_shifted / 1.0).to_int().to_double() * 1.0
let right_height = if right_stride_progress > 0.68 {
let swing = (right_stride_progress - 0.68) / 0.32
0.05 * @math.sin(swing * @math.PI)
} else {
0.0
}
let right_x = x_disp - 0.12
let right_foot = @math.Point3D::{ x: right_x, y: right_height, z: 0.14 }
right_frames.push(@gait.JointFrame::{
time: t,
hip: @math.Point3D::{ x: x_disp, y: 0.9, z: 0.1 },
knee: @math.Point3D::{ x: x_disp, y: 0.5, z: 0.1 },
ankle: @math.Point3D::{ x: x_disp, y: 0.1, z: 0.1 },
foot: right_foot,
})
}
let result = @pipeline.analyze_frames(left_frames, right_frames, config)
if verbose {
println(
"\n=================================================================",
)
println(" GAIT ANALYSIS RESULTS ")
println("=================================================================")
}
println(@report.render_report(result.report, format))
if verbose && @pipeline.has_fatal_diagnostics(result) {
println("Diagnostics: " + @pipeline.diagnostics_to_text(result.diagnostics))
}
}
///|
fn main {
let args = @env.args()
if args.length() <= 1 {
print_usage()
return
}
let arg1 = args[1]
if arg1 == "-h" || arg1 == "--help" {
print_usage()
} else if arg1 == "-d" || arg1 == "--demo" {
run_demo(output_format(args), analysis_config(args))
} else if arg1 == "-c" || arg1 == "--csv-data" {
if args.length() < 3 {
println("Error: Missing CSV string argument.")
return
}
let csv_str = args[2]
match parse_csv_frames(csv_str) {
Err(e) => println("CSV Parse Error: " + e)
Ok(frames) => {
let result = @pipeline.analyze_frames(
frames,
frames,
analysis_config(args),
)
println(@report.render_report(result.report, output_format(args)))
}
}
} else if arg1 == "-j" || arg1 == "--json-data" {
if args.length() < 3 {
println("Error: Missing JSON string argument.")
return
}
let json_str = args[2]
match @json.parse_json(json_str) {
Err(e) => println("JSON Parse Error: " + e)
Ok(_) =>
match parse_json_frames(json_str) {
Err(e) => println("JSON Frame Error: " + e)
Ok(frames) => {
let result = @pipeline.analyze_frames(
frames,
frames,
analysis_config(args),
)
println(@report.render_report(result.report, output_format(args)))
}
}
}
} else if arg1 == "--benchmark" {
println(
"Run the native benchmark with: pwsh -File scripts/run_benchmark.ps1",
)
} else {
println("Unknown option: " + arg1)
print_usage()
}
}