bywind 0.1.1

Sailing route optimisation with a focus on exploiting winds, using PSO over imported GRIB2 data.
Documentation
[workspace]
resolver = "3"
members = [".", "bywind-viz", "bywind-cli", "swarmkit-sailing", "bywind-dev"]
# Diagnostic / dev-only binaries live in `bywind-dev` and aren't part
# of the default build. `cargo build`, `cargo check`, `cargo test`,
# and `cargo clippy` (with no `-p` / no `--workspace`) skip the dev
# crate; opt in via `cargo run -p bywind-dev --release --bin <name>`.
default-members = [".", "bywind-viz", "bywind-cli", "swarmkit-sailing"]

# ----------------------------------------------------------------------------------------
# Shared `[package]` fields. Each member opts in via `field.workspace = true` in
# its own `[package]` so we don't repeat license / edition / etc. four times.

[workspace.package]
version = "0.1.1"
edition = "2024"
rust-version = "1.85"
authors = ["Anvoker <anlogcode@pm.me>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/Anvoker/bywind"
homepage = "https://github.com/Anvoker/bywind"

# ----------------------------------------------------------------------------------------
# Shared external dependencies. Each member declares the dep with
# `serde.workspace = true` (plus feature flags as needed) instead of pinning
# the version inline. Inner-workspace path+version deps stay in the per-crate
# tables since the relative path varies per member.

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
log = "0.4"
rand = "0.10"
rayon = "1.12.0"
swarmkit = "0.1"
# Used by `wind_av1`'s v2 header to round-trip the dataset's UTC
# time-range bounds and by `bywind-cli info` / `bywind-viz`'s Time
# section to format them for display. Default features off because we
# only need `DateTime<Utc>` construction from a Unix timestamp and
# `Display` formatting — not the system-clock or wasm-time bridges.
chrono = { version = "0.4", default-features = false, features = ["std"] }

[package]
name = "bywind"
description = "Sailing route optimisation with a focus on exploiting winds, using PSO over imported GRIB2 data."
keywords = ["sailing", "routing", "pso", "wind", "grib2"]
categories = ["algorithms", "science::geo"]
readme = "README.md"
include = [
    "src/**/*.rs",
    "assets/**",
    "examples/**/*.rs",
    "Cargo.toml",
    "README.md",
    "LICENSE-MIT",
    "LICENSE-APACHE",
]
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation = "https://docs.rs/bywind"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
serde.workspace = true
serde_json.workspace = true
log.workspace = true
rand.workspace = true
rayon.workspace = true
swarmkit.workspace = true
chrono.workspace = true
toml = "1"
grib = { version = "0.15", default-features = false, features = ["png-unpack-with-png-crate"] }
kiddo = "5"
swarmkit-sailing = { path = "swarmkit-sailing", version = "0.1" }
# `baked_codec` payload compression. zstd-rs links a vendored C library
# and at present doesn't cross-compile cleanly to `wasm32-unknown-unknown`
# (it would need a clang/wasm toolchain). The wasm build of `bywind-viz`
# is currently broken for other unrelated reasons (`wind_av1` is also
# native-only and is referenced unconditionally from `bywind-viz`'s app
# state), so we gate the codec module to non-wasm targets via `cfg`
# rather than complicate the Cargo.toml — when wasm support comes back
# online we'll either configure zstd for it or swap in a pure-Rust
# decoder (`ruzstd`) for read-only.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# `zstdmt` enables `Encoder::multithread()` so `baked_codec::encode` can
# parallelise compression across worker threads — without it, level-11
# encoding of multi-GB baked wind grids single-threads on one core.
zstd = { version = "0.13", features = ["zstdmt"] }
# AV1 decoder for the `wind_av1` near-lossless codec. Pure-Rust port of
# dav1d; the no-default-features set drops the NASM-asm path so contributors
# don't need an assembler on PATH. Decode is single-shot at startup so the
# 1.5–2× perf penalty vs C dav1d doesn't matter. Native-only because the
# wasm build of `bywind-viz` is already gated out upstream.
rav1d = { version = "1.1", default-features = false, features = ["bitdepth_8", "bitdepth_16"] }
# Pure-Rust AV1 encoder, paired with rav1d above. Default-features off
# drops the `asm` NASM-SIMD path (same reasoning as rav1d) and the
# `binaries` feature (we only want the library). Encoding is one-shot
# offline so the speed penalty vs libaom is fine.
rav1e = { version = "0.8", default-features = false }
# Used by the rav1d wrapper to source the platform-specific EAGAIN
# value. rav1d's `Rav1dError` enum is pub(crate), so we can't reach the
# canonical constant through it; the libc crate is tiny and zero-trans-
# dep, so depending directly is cheaper than hard-coding a value that
# differs between Linux (11) and macOS (35).
libc = "0.2"
# HTTP client for `bywind::fetch` (pull GRIB2 messages from the NOAA
# GFS S3 bucket via `.idx` sidecars + Range requests). `ureq` is sync
# and has a small dependency footprint vs. `reqwest` / `tokio`. Native
# only — wasm callers will need their own fetch path through the
# browser's `fetch()` API.
ureq = "3"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
# The `fetch_gfs` example uses `chrono`'s `clock` feature for `Utc::now`
# when defaulting the end-of-window. The core `fetch` module itself
# stays clock-agnostic — callers pass explicit `DateTime<Utc>` values.
chrono = { workspace = true, features = ["clock"] }

[features]
# Forwards the `profile-timers` feature down into swarmkit-sailing.
# Build with `cargo build -p bywind-cli --features profile-timers` to
# get a per-stage breakdown of search time on stderr at the end of
# every search.
profile-timers = ["swarmkit-sailing/profile-timers"]

# Apply the workspace lint set (defined below) to the `bywind` crate
# itself, mirroring `bywind-viz` and `bywind-cli`.
[lints]
workspace = true

# The profile that 'dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"

# ----------------------------------------------------------------------------------------
# Workspace lints. Each crate opts in via `[lints] workspace = true` in its own
# Cargo.toml (`bywind`, `bywind-viz`, `bywind-cli`).

[workspace.lints.rust]
unsafe_code = "deny"

elided_lifetimes_in_paths = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
trivial_numeric_casts = "warn"
unsafe_op_in_unsafe_fn = "warn"                         # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"

trivial_casts = "allow"
unused_qualifications = "allow"


[workspace.lints.rustdoc]
all = "warn"
missing_crate_level_docs = "warn"


[workspace.lints.clippy]
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
as_ptr_cast_mut = "warn"
assertions_on_result_states = "warn"
await_holding_lock = "warn"
await_holding_refcell_ref = "warn"
bool_to_int_with_if = "warn"
branches_sharing_code = "warn"
char_lit_as_u8 = "warn"
checked_conversions = "warn"
clear_with_drain = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
debug_assert_with_mut_call = "warn"
default_union_representation = "warn"
derive_partial_eq_without_eq = "warn"
disallowed_macros = "warn"                   # See clippy.toml
disallowed_methods = "warn"                  # See clippy.toml
disallowed_names = "warn"                    # See clippy.toml
disallowed_script_idents = "warn"            # See clippy.toml
disallowed_types = "warn"                    # See clippy.toml
doc_comment_double_space_linebreaks = "warn"
doc_link_with_quotes = "warn"
doc_markdown = "warn"
elidable_lifetime_names = "warn"
empty_enums = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_outer_attr = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
exit = "warn"
expl_impl_clone_on_copy = "warn"
explicit_deref_methods = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
fallible_impl_from = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
float_cmp_const = "warn"
fn_params_excessive_bools = "warn"
fn_to_numeric_cast_any = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
ignore_without_reason = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
inconsistent_struct_constructor = "warn"
index_refutable_slice = "warn"
inefficient_to_string = "warn"
infallible_try_from = "warn"
infinite_loop = "warn"
into_iter_without_iter = "warn"
invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
iter_over_hash_type = "warn"
iter_without_into_iter = "warn"
large_digit_groups = "warn"
large_futures = "warn"
large_include_file = "warn"
large_stack_arrays = "warn"
large_stack_frames = "warn"
large_types_passed_by_value = "warn"
let_underscore_future = "warn"
let_underscore_must_use = "warn"
let_underscore_untyped = "warn"
let_unit_value = "warn"
linkedlist = "warn"
literal_string_with_formatting_args = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
manual_clamp = "warn"
manual_instant_elapsed = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
manual_midpoint = "warn"
manual_ok_or = "warn"
manual_string_new = "warn"
map_err_ignore = "warn"
map_flatten = "warn"
match_bool = "warn"
match_same_arms = "warn"
match_wild_err_arm = "warn"
match_wildcard_for_single_variants = "warn"
mem_forget = "warn"
mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_safety_doc = "warn"
mixed_attributes_style = "warn"
multiple_unsafe_ops_per_block = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_borrow = "warn"
needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
panic = "warn"
panic_in_result_fn = "warn"
path_buf_push_overwrite = "warn"
pathbuf_init_then_push = "warn"
precedence_bits = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
pub_without_shorthand = "warn"
rc_mutex = "warn"
readonly_write_lock = "warn"
redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
set_contains_or_insert = "warn"
should_panic_without_expect = "warn"
single_char_pattern = "warn"
single_match_else = "warn"
single_option_map = "warn"
str_split_at_newline = "warn"
str_to_string = "warn"
string_add = "warn"
string_add_assign = "warn"
string_lit_as_bytes = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
suspicious_command_arg_space = "warn"
suspicious_xor_used_as_pow = "warn"
todo = "warn"
too_long_first_doc_paragraph = "warn"
too_many_lines = "warn"
trailing_empty_array = "warn"
trait_duplication_in_bounds = "warn"
transmute_ptr_to_ptr = "warn"
tuple_array_conversions = "warn"
unchecked_time_subtraction = "warn"
undocumented_unsafe_blocks = "warn"
unimplemented = "warn"
uninhabited_references = "warn"
uninlined_format_args = "warn"
unreachable = "warn"
unnecessary_box_returns = "warn"
unnecessary_debug_formatting = "warn"
unnecessary_literal_bound = "warn"
unnecessary_safety_comment = "warn"
unnecessary_safety_doc = "warn"
unnecessary_self_imports = "warn"
unnecessary_semicolon = "warn"
unnecessary_struct_initialization = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_peekable = "warn"
unused_result_ok = "warn"
unused_rounding = "warn"
unused_self = "warn"
unused_trait_names = "warn"
unwrap_in_result = "warn"
unwrap_used = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
useless_transmute = "warn"
verbose_file_reads = "warn"
wildcard_dependencies = "warn"
wildcard_imports = "warn"
zero_sized_map_values = "warn"

manual_range_contains = "allow" # this is better on 'allow'
map_unwrap_or = "allow"         # this is better on 'allow'
# Noisy in this codebase's grid / numerical paths (`wind_map`, `landmass`,
# `wind_av1`, `baked_codec`) where indices are derived from `0..len` loops or pre-clamped
# helpers and are provably in bounds. `arr[i]` and `arr.get(i).unwrap()`
# emit the same bounds check after LLVM elimination, so there's no perf
# argument either; switching to `.get()` would just trade direct indexing
# for noisier `.unwrap()` / `.expect()` chatter.
indexing_slicing = "allow"
# Same domain as `indexing_slicing`: the grid / coordinate / pixel math
# in `wind_map`, `landmass`, and `bywind-viz/draw` converts floats to
# usize at the integer / float boundary, where every site is either
# preceded by a clamp(0, ...), an explicit non-negative check, or
# operates on a positive-by-construction quantity (dimensions, durations,
# normalised fractions). Modern Rust saturates `f32/f64 as usize`, so
# even an unexpected negative / NaN coerces to 0 rather than UB. The
# lint adds noise without flagging real bugs in this code shape.
cast_sign_loss = "allow"