///|
fn p(x : Int, y : Int) -> @core.Point {
@core.point(x, y)
}
///|
#cfg(target="native")
#external
type FileHandle
///|
#cfg(target="native")
#borrow(path, mode)
extern "C" fn file_open(path : Bytes, mode : Bytes) -> FileHandle = "moonbit_fopen_ffi"
///|
#cfg(target="native")
extern "C" fn is_null_file(file : FileHandle) -> Bool = "moonbit_is_null"
///|
#cfg(target="native")
extern "C" fn file_seek(file : FileHandle, offset : Int, whence : Int) -> Int = "moonbit_fseek_ffi"
///|
#cfg(target="native")
extern "C" fn file_tell(file : FileHandle) -> Int = "moonbit_ftell_ffi"
///|
#cfg(target="native")
#borrow(buffer, file)
extern "C" fn file_read(
buffer : Bytes,
size : Int,
nitems : Int,
file : FileHandle,
) -> Int = "moonbit_fread_ffi"
///|
#cfg(target="native")
extern "C" fn file_close(file : FileHandle) -> Int = "moonbit_fclose_ffi"
///|
#cfg(target="native")
fn c_string(text : String) -> Bytes {
let bytes = @utf8.encode(text).to_array()
bytes.push(b'\x00')
Bytes::from_array(bytes)
}
///|
#cfg(target="native")
fn read_text_file(path : String) -> String? {
let file = file_open(c_string(path), c_string("rb"))
if is_null_file(file) {
return None
}
if file_seek(file, 0, 2) != 0 {
ignore(file_close(file))
return None
}
let size = file_tell(file)
if size < 0 || file_seek(file, 0, 0) != 0 {
ignore(file_close(file))
return None
}
let buffer = Bytes::new(size)
let read = file_read(buffer, 1, size, file)
ignore(file_close(file))
if read == size {
Some(@utf8.decode_lossy(buffer, ignore_bom=true))
} else {
None
}
}
///|
#cfg(not(target="native"))
fn read_text_file(path : String) -> String? {
let _ = path
None
}
///|
fn simple_map() -> @grid.GridMap {
@grid.new_grid(20, 20, p(0, 0), p(19, 19)).with_obstacles([
p(4, 2),
p(4, 3),
p(4, 4),
p(4, 5),
p(4, 6),
p(4, 7),
p(8, 5),
p(9, 5),
p(10, 5),
p(11, 5),
p(12, 5),
p(13, 5),
p(15, 3),
p(15, 4),
p(15, 5),
p(12, 9),
p(12, 10),
p(12, 11),
p(12, 12),
p(12, 13),
p(12, 14),
p(16, 10),
p(17, 10),
p(18, 10),
p(3, 13),
p(4, 13),
p(5, 13),
p(6, 13),
p(7, 13),
p(9, 16),
p(10, 16),
p(11, 16),
p(12, 16),
p(13, 16),
])
}
///|
fn complex_map() -> @grid.GridMap {
@grid.new_grid(20, 20, p(0, 0), p(19, 19))
.with_obstacles([
p(3, 1),
p(3, 2),
p(3, 3),
p(3, 5),
p(3, 6),
p(3, 7),
p(3, 8),
p(3, 9),
p(3, 11),
p(3, 12),
p(3, 13),
p(3, 14),
p(3, 15),
p(7, 4),
p(7, 5),
p(7, 6),
p(7, 7),
p(7, 9),
p(7, 10),
p(7, 11),
p(7, 12),
p(7, 13),
p(7, 14),
p(7, 16),
p(7, 17),
p(7, 18),
p(7, 19),
p(11, 0),
p(11, 1),
p(11, 2),
p(11, 3),
p(11, 4),
p(11, 5),
p(11, 7),
p(11, 8),
p(11, 9),
p(11, 10),
p(11, 11),
p(11, 13),
p(11, 14),
p(15, 5),
p(15, 6),
p(15, 7),
p(15, 8),
p(15, 10),
p(15, 11),
p(15, 12),
p(15, 13),
p(15, 14),
p(15, 15),
p(15, 17),
p(15, 18),
p(15, 19),
p(5, 3),
p(6, 3),
p(7, 3),
p(8, 3),
p(10, 3),
p(12, 3),
p(13, 3),
p(15, 3),
p(16, 3),
p(17, 3),
p(0, 8),
p(1, 8),
p(4, 8),
p(5, 8),
p(7, 8),
p(8, 8),
p(9, 8),
p(12, 8),
p(13, 8),
p(6, 13),
p(8, 13),
p(9, 13),
p(10, 13),
p(12, 13),
p(13, 13),
p(14, 13),
p(16, 13),
p(18, 13),
p(19, 13),
])
.with_terrain(p(5, 5), 3.0)
.with_terrain(p(6, 6), 3.0)
.with_terrain(p(10, 10), 5.0)
.with_terrain(p(14, 14), 4.0)
.with_terrain(p(17, 16), 2.5)
}
///|
fn dynamic_map() -> @grid.GridMap {
@grid.new_grid(5, 1, p(0, 0), p(4, 0))
}
///|
fn dynamic_crossing_map() -> @grid.GridMap {
@grid.new_grid(10, 10, p(0, 0), p(9, 9))
}
///|
fn dynamic_mixed_map() -> @grid.GridMap {
@grid.new_grid(12, 12, p(0, 0), p(11, 11)).with_obstacles([
p(2, 4),
p(3, 4),
p(4, 2),
p(4, 3),
p(5, 9),
p(7, 8),
p(8, 7),
p(9, 5),
])
}
///|
fn status_name(result : @core.PathResult) -> String {
if result.status is @core.Found {
"found"
} else if result.status is @core.NoPath {
"no_path"
} else {
"invalid_input"
}
}
///|
fn default_repeats() -> Int {
5
}
///|
fn heuristic_for(movement : @grid.MovementMode) -> @heuristics.HeuristicKind {
match movement {
@grid.FourWay => @heuristics.Manhattan
@grid.EightWay => @heuristics.Octile
}
}
///|
struct SafetyMetrics {
evaluated : Int
safe : Int
min_clearance : Double
} derive(Debug)
///|
fn no_safety_metrics() -> SafetyMetrics {
{ evaluated: 0, safe: 0, min_clearance: 0.0 }
}
///|
fn bool_int(value : Bool) -> Int {
if value {
1
} else {
0
}
}
///|
fn continuous_safety_metrics(
path : Array[@core.Point],
obstacles : Array[@dynamic.ContinuousMovingObstacle],
options : @dynamic.CollisionOptions,
step_time : Double,
samples_per_segment : Int,
) -> SafetyMetrics {
if path.length() < 2 || obstacles.is_empty() {
return no_safety_metrics()
}
let segments = @dynamic.continuous_segments_from_grid_path(path, step_time)
let safe = @dynamic.continuous_path_is_safe(
segments, obstacles, options, samples_per_segment,
)
let min_clearance = match
@dynamic.continuous_path_min_clearance(
segments, obstacles, options, samples_per_segment,
) {
Some(clearance) => clearance.clearance
None => 0.0
}
{ evaluated: 1, safe: bool_int(safe), min_clearance }
}
///|
fn print_result_with_safety(
scenario : String,
algorithm : String,
result : @core.PathResult,
iterations : Int,
candidates : Int,
best_fitness : Double,
seed : Int,
population_size : Int,
max_iterations : Int,
repeats : Int,
elapsed_us_total : Double,
safety : SafetyMetrics,
) -> Unit {
let elapsed_us_avg = elapsed_us_total / repeats.to_double()
println(
"\{scenario},\{algorithm},\{status_name(result)},\{result.path.length()}," +
"\{result.total_cost},\{@swarm.path_length(result.path)}," +
"\{@swarm.path_smoothness(result.path)},\{result.visited_nodes}," +
"\{result.expanded_nodes},\{iterations},\{candidates},\{best_fitness}," +
"\{seed},\{population_size},\{max_iterations},\{repeats}," +
"\{elapsed_us_total},\{elapsed_us_avg},\{safety.evaluated}," +
"\{safety.safe},\{safety.min_clearance}",
)
}
///|
fn print_result(
scenario : String,
algorithm : String,
result : @core.PathResult,
iterations : Int,
candidates : Int,
best_fitness : Double,
seed : Int,
population_size : Int,
max_iterations : Int,
repeats : Int,
elapsed_us_total : Double,
) -> Unit {
print_result_with_safety(
scenario,
algorithm,
result,
iterations,
candidates,
best_fitness,
seed,
population_size,
max_iterations,
repeats,
elapsed_us_total,
no_safety_metrics(),
)
}
///|
fn adjusted_result(
base : @core.PathResult,
adjusted : Array[@core.Point],
) -> @core.PathResult {
if base.status is @core.Found {
@core.found_result(
adjusted,
@swarm.path_length(adjusted),
base.visited_nodes,
base.expanded_nodes,
)
} else {
base
}
}
///|
fn run_rs_apso_variant(
name : String,
map : @grid.GridMap,
region : @region.SearchRegion,
movement : @grid.MovementMode,
population_size : Int,
max_iterations : Int,
stagnation_limit : Int,
repeats : Int,
) -> Unit {
let options = @swarm.options(
population_size,
max_iterations,
42,
stagnation_limit,
movement,
@swarm.default_fitness_weights(),
)
let start = @clock.monotonic_clock_start()
let mut rs_apso = @swarm.rs_apso_plan(map, region, options)
for _ in 1.. Unit {
let repeats = default_repeats()
let swarm_options = @swarm.options(
60,
100,
42,
3,
movement,
@swarm.default_fitness_weights(),
)
let rrt_options = @continuous.rrt_options(600, 42, 0.2)
let rrt_star_options = @continuous.rrt_star_options(600, 42, 0.2, 3)
let region = @region.build_search_region(map, @region.default_options())
let heuristic = heuristic_for(movement)
let astar_start = @clock.monotonic_clock_start()
let mut astar = @algorithms.astar(map, movement, heuristic)
for _ in 1.. Unit {
let repeats = default_repeats()
let map = dynamic_map()
let movement = @grid.FourWay
let options = @dynamic.collision_options(2.0, 0.5, 0.1)
let base = @algorithms.astar(map, movement, @heuristics.Manhattan)
let baseline_start = @clock.monotonic_clock_start()
let mut baseline = base
for _ in 1.. Unit {
let repeats = default_repeats()
let map = dynamic_crossing_map()
let movement = @grid.EightWay
let options = @dynamic.collision_options(2.0, 0.5, 0.1)
let base = @algorithms.astar(map, movement, @heuristics.Octile)
let baseline_start = @clock.monotonic_clock_start()
let mut baseline = base
for _ in 1.. Unit {
let repeats = default_repeats()
let map = dynamic_mixed_map()
let movement = @grid.EightWay
let options = @dynamic.collision_options(2.0, 0.5, 0.1)
let base = @algorithms.astar(map, movement, @heuristics.Octile)
let baseline_start = @clock.monotonic_clock_start()
let mut baseline = base
for _ in 1.. Unit {
run_scenario("rs_apso_20x20_simple", simple_map(), @grid.EightWay)
run_scenario("rs_apso_20x20_complex", complex_map(), @grid.EightWay)
run_dynamic_scenarios()
}
///|
fn run_json_text(source : String, text : String) -> Bool {
match @json_map.grid_from_json(text) {
Some(data) => {
run_scenario(source, data.map, data.movement)
true
}
None => {
println("Invalid benchmark grid JSON: \{source}")
false
}
}
}
///|
fn run_json_file(path : String) -> Bool {
match read_text_file(path) {
Some(text) => run_json_text(path, text)
None => {
println("Could not read benchmark JSON file: \{path}")
false
}
}
}
///|
fn run_example(name : String) -> Bool {
match @json_map.example_json(name) {
Some(text) => run_json_text("example:\{name}", text)
None => {
println("Unknown benchmark example map: \{name}")
false
}
}
}
///|
fn usage() -> Unit {
println("Usage:")
println(" moon run ./bench")
println(" moon run ./bench -- --json ")
println(" moon run ./bench -- --example [--example ]")
println(" moon run ./bench --target native -- [more.json]")
println(" moon run ./bench --target native -- --map ")
}
///|
fn json_args(args : Array[String]) -> Array[String]? {
let texts = []
let mut index = 1
while index < args.length() {
match args[index] {
"--help" | "-h" => return Some([])
"--json" | "-j" =>
if index + 1 < args.length() {
texts.push(args[index + 1])
index = index + 1
} else {
return Some([])
}
_ => ()
}
index = index + 1
}
if texts.length() == 0 {
None
} else {
Some(texts)
}
}
///|
fn example_args(args : Array[String]) -> Array[String]? {
let names = []
let mut index = 1
while index < args.length() {
match args[index] {
"--help" | "-h" => return Some([])
"--example" | "-e" =>
if index + 1 < args.length() {
names.push(args[index + 1])
index = index + 1
} else {
return Some([])
}
_ => ()
}
index = index + 1
}
if names.length() == 0 {
None
} else {
Some(names)
}
}
///|
fn map_args(args : Array[String]) -> Array[String]? {
let paths = []
let mut index = 1
while index < args.length() {
match args[index] {
"--help" | "-h" => return Some([])
"--map" | "-m" =>
if index + 1 < args.length() {
paths.push(args[index + 1])
index = index + 1
} else {
return Some([])
}
value => paths.push(value)
}
index = index + 1
}
if paths.length() == 0 {
None
} else {
Some(paths)
}
}
///|
fn run_json_files(paths : Array[String]) -> Bool {
let mut ok = true
for path in paths {
if !run_json_file(path) {
ok = false
}
}
ok
}
///|
fn run_examples(names : Array[String]) -> Bool {
let mut ok = true
for name in names {
if !run_example(name) {
ok = false
}
}
ok
}
///|
fn run_json_texts(texts : Array[String]) -> Bool {
let mut ok = true
for index, text in texts {
if !run_json_text("inline-json-\{index + 1}", text) {
ok = false
}
}
ok
}
///|
fn main {
println(
"scenario,algorithm,status,path_nodes,total_cost,path_length,smoothness," +
"visited_nodes,expanded_nodes,iterations,candidates,best_fitness,seed," +
"population_size,max_iterations,repeats,elapsed_us_total,elapsed_us_avg," +
"safety_evaluated,continuous_safe,min_clearance",
)
let args = @env.args()
match json_args(args) {
Some(texts) =>
if texts.length() == 0 {
usage()
} else {
ignore(run_json_texts(texts))
}
None =>
match example_args(args) {
Some(names) =>
if names.length() == 0 {
usage()
} else {
ignore(run_examples(names))
}
None =>
match map_args(args) {
Some(paths) =>
if paths.length() == 0 {
usage()
} else {
ignore(run_json_files(paths))
}
None => run_default_scenarios()
}
}
}
}