From dc25b68cda20e77f091cef22a09cf645a17ed0e5 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 15:58:45 +0300 Subject: [PATCH 01/11] Docs: Add developer guide with architecture and decompression engine reference Adds a Building from source and Architecture section to the readme, plus docs/ARCHITECTURE.md (codebase map for newcomers) and docs/DECOMPRESSION.md (in-depth walkthrough of the Buhlmann decompression engine). Renames readme.md to README.md for standard discovery. Co-Authored-By: Claude Opus 4.8 --- readme.md => README.md | 83 +++++- docs/ARCHITECTURE.md | 253 ++++++++++++++++++ docs/DECOMPRESSION.md | 570 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 905 insertions(+), 1 deletion(-) rename readme.md => README.md (91%) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DECOMPRESSION.md diff --git a/readme.md b/README.md similarity index 91% rename from readme.md rename to README.md index d8f8d2d..6dddaf0 100644 --- a/readme.md +++ b/README.md @@ -29,6 +29,11 @@ mobile solution to date), available on both Android and iOS, and free to inspect > affiliates) can be held responsible for the outcomes of your use of the information provided by > this application. The use of this application is entirely at your own risk. +Want to build Abysner yourself, contribute, or understand how it works under the hood? Start at +[Building from source](#building-from-source) and the [`docs/`](docs) folder. The +[architecture overview](docs/ARCHITECTURE.md) explains how the code is laid out, and the +[decompression engine deep-dive](docs/DECOMPRESSION.md) documents the deco algorithm in detail. + # Philosophy Abysner is built with simplicity in mind. Other planners may offer more data or options, but Abysner @@ -908,11 +913,87 @@ calculating the reserve gas requirements. +# Building from source +Abysner is a [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) project that +runs on Android and iOS, with a JVM/desktop target that exists mainly to render Compose previews. +Almost all of the code is shared, so most work happens once and runs everywhere. + +**Prerequisites:** + +- A JDK to run Gradle. The build uses Gradle toolchains to provision JDK 21 (Temurin) automatically, + so the exact JDK you launch Gradle with is not critical, but JDK 21 is recommended. +- The **Android SDK** (compile and target SDK 37, minimum SDK 26) for the Android app. Android + Studio is the easiest way to get this. +- **Xcode** for the iOS app (macOS only). + +You do not need to install Gradle itself, use the included wrapper (`./gradlew`), which pins the +Gradle version for you. + +**Quickstart:** + +```sh +# Clone +git clone https://github.com/NeoTech-Software/abysner.git +cd abysner + +# Run the decompression engine tests (the domain module is pure Kotlin, no Android/iOS SDK needed) +./gradlew :domain:jvmTest + +# Run everything CI runs (JVM tests, coverage and screenshot validation) +./gradlew :koverXmlReportDomain :koverXmlReportPresentation +``` + +**Running the app:** + +```sh +# Android: build a debug APK, or install it on a connected device/emulator +./gradlew :androidApp:assembleDebug +./gradlew :androidApp:installDebug + +# Desktop (JVM): the fastest way to see the UI without a device or simulator +./gradlew :composeApp:run +``` + +For iOS, open `iosApp/iosApp.xcodeproj` in Xcode and run it on a simulator or device. Gradle builds +the shared `ComposeApp` framework as part of the Xcode build. + +**Common first-run issues:** + +- *Gradle cannot find a JDK*: install JDK 21 (Temurin) and make sure `java -version` works, or point + Gradle at it. +- *Android SDK not found*: open the project once in Android Studio, or create a `local.properties` + file with `sdk.dir=/path/to/Android/sdk`. + +The codebase is split into four Gradle modules (`domain`, `data`, `composeApp`, `androidApp`). See +[Architecture](#architecture) below for what each one does. + + +# Architecture +Abysner uses a layered architecture split across Gradle modules, with dependencies pointing in one +direction only (`composeApp` -> `data` -> `domain`): + +| Module | What it contains | +|--------------|-------------------------------------------------------------------------------------| +| `domain` | Pure Kotlin business logic: the decompression engine, gas planning, and the models. | +| `data` | Persistence: repositories, serialization, and platform file access. | +| `composeApp` | The shared Compose Multiplatform UI: screens, view models, navigation, theming. | +| `androidApp` | The Android application wrapper (the iOS wrapper lives in `iosApp`). | + +The `domain` module has no Android, iOS, or UI dependencies, so the decompression math can be read, +tested, and verified in isolation. + +For a full tour of the codebase (how it is divided, the design patterns used, where to find things, +and how the UI talks to the decompression engine) see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +For an in-depth explanation of the decompression algorithm itself, which is the heart of the app and +the part most worth verifying, see [docs/DECOMPRESSION.md](docs/DECOMPRESSION.md). + + # Contributing If you'd like to contribute, please open an issue or start a discussion before putting significant effort into a feature or refactor. This project has a specific scope and direction, and not all pull requests will be accepted. A conversation upfront is the best way to make sure your time is -well spent. Detailed contribution guidelines are not yet available, but the basics are covered here. +well spent. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guidelines, including how to report +bugs, the development workflow, and commit conventions. All contributors are required to sign a [Contributor License Agreement (CLA)](cla.txt) before their pull request can be merged. The CLA process is automated via diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..3486a53 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,253 @@ +# Architecture + +This document explains how the Abysner codebase is laid out, the patterns it uses, and where to +find things. It is aimed at developers who want to contribute, including those who have never worked +on a Kotlin Multiplatform project before. If you are mainly interested in the decompression +algorithm, read this first for the lay of the land, then move on to +[DECOMPRESSION.md](DECOMPRESSION.md). + +For build and run instructions see [Building from source](../README.md#building-from-source) in the +main readme. + + +## A short Kotlin Multiplatform primer + +If you already know Kotlin Multiplatform (KMP) you can skip this section. + +Abysner is one codebase that compiles to Android, iOS, and a JVM/desktop build. The way KMP makes +that work is through **source sets**. Each module has a `commonMain` source set for code that is +shared across every platform, and optional platform-specific source sets that fill in the gaps: + +- `commonMain`: shared code, the large majority of the project. It cannot use Android or iOS APIs + directly, only Kotlin and multiplatform libraries. +- `androidMain`, `iosMain`, `jvmMain`: platform-specific code for one target. +- `commonTest`: shared tests, which run on the JVM during CI. + +When shared code needs something that only a platform can provide (for example, the path to the +app's data folder), KMP uses the `expect`/`actual` mechanism: `commonMain` declares an `expect` +function or class, and each platform source set provides the matching `actual` implementation. You +will see this in the `data` module, where file access differs per platform. + +The `domain` module is special: it only has `commonMain` (plus tests) and no platform code at all. +That is deliberate. All the decompression and planning logic is pure Kotlin, so it behaves +identically on every platform and can be tested without any device or simulator. + + +## Modules + +Abysner is split into four Gradle modules (see [`settings.gradle.kts`](../settings.gradle.kts)), +plus the iOS app which is an Xcode project rather than a Gradle module. Dependencies point in one +direction only, so the lower layers never know about the higher ones: + +```mermaid +graph TD + androidApp["androidApp
(Android wrapper)"] + iosApp["iosApp
(Xcode project)"] + composeApp["composeApp
(shared Compose UI)"] + data["data
(persistence)"] + domain["domain
(business logic + deco engine)"] + + androidApp --> composeApp + iosApp --> composeApp + composeApp --> data + composeApp --> domain + data --> domain +``` + +| Module | Path | Responsibility | +|--------------|----------------|------------------------------------------------------------------------| +| `domain` | [`domain/`](../domain) | Pure Kotlin: decompression engine, gas planning, physics, models. | +| `data` | [`data/`](../data) | Repositories, serialization (DTOs), DataStore persistence, file access. | +| `composeApp` | [`composeApp/`](../composeApp) | Shared Compose Multiplatform UI: screens, view models, navigation, theme. | +| `androidApp` | [`androidApp/`](../androidApp) | Android `Application` and `Activity` entry points. | +| iOS app | [`iosApp/`](../iosApp) | SwiftUI wrapper that hosts the shared Compose UI. | + +All Kotlin code lives under the reverse-domain package `org.neotech.app.abysner` (note: the app's +build identifier is `nl.neotech.app.abysner`, the Kotlin package uses `org`). + + +### Where do I find...? + +| I'm looking for... | Look in... | +|----------------------------------------|------------------------------------------------------------------| +| The decompression algorithm | `domain/.../decompression/` (start at [DECOMPRESSION.md](DECOMPRESSION.md)) | +| Gas mixes, cylinders, configuration | `domain/.../core/model/` | +| Physics (pressure, depth, gas laws) | `domain/.../core/physics/` | +| Gas and oxygen-toxicity planning | `domain/.../gasplanning/` | +| The public planning entry point | `domain/.../diveplanning/DivePlanner.kt` | +| A screen or UI component | `composeApp/.../presentation/` | +| A view model | `composeApp/.../presentation/screens/` | +| How settings and dives are saved | `data/.../` and `domain/.../persistence/` | +| Dependency injection wiring | `composeApp/.../di/AppComponent.kt` | +| Platform entry points | `androidApp/`, `composeApp/src/iosMain/`, `composeApp/src/jvmMain/` | + + +## Design patterns + +The project follows a small set of well-known patterns. Each is described below with a real file to +look at. + +**Layered (clean) architecture.** Business logic in `domain` knows nothing about persistence or UI. +Persistence in `data` depends on `domain` (it implements interfaces declared there) but not on the +UI. The UI in `composeApp` depends on both. Because the dependency graph is acyclic, you can change +the UI without touching the engine, and verify the engine without building the app. + +**Repository pattern.** The `domain` module declares repository interfaces, and `data` provides the +implementations. For example +[`PlanningRepository`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/PlanningRepository.kt) +is the interface, and +[`PlanningRepositoryImpl`](../data/src/commonMain/kotlin/org/neotech/app/abysner/data/diveplanning/PlanningRepositoryImpl.kt) +is the implementation. This keeps storage details out of the domain and UI. + +**MVVM with Compose.** Screens are Composables that observe state from a view model. View models +extend the multiplatform `ViewModel` and expose state via `StateFlow`. See +[`PlanScreenViewModel`](../composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenViewModel.kt), +which holds the dive input, runs the planner, and exposes the resulting plan as state. Heavy +calculations run on `Dispatchers.Default` so the UI thread stays responsive. + +**Dependency injection.** A single object graph wires the repositories and navigation together (see +the next section). + + +## Dependency injection (Metro) + +Abysner uses [Metro](https://github.com/ZacSweers/metro) for dependency injection. The whole graph +is defined in one place, +[`AppComponent`](../composeApp/src/commonMain/kotlin/org/neotech/app/abysner/di/AppComponent.kt): + +- It is annotated `@DependencyGraph` and scoped with `@SingleIn(AppScope::class)`, so the + repositories are effectively singletons for the app's lifetime. +- `@Provides` functions bind each repository interface to its implementation (for example + `PlanningRepository` to `PlanningRepositoryImpl`). +- A nested `@DependencyGraph.Factory` accepts the one dependency that has to be built per platform, + a `PlatformFileDataSource`, and returns the graph. + +The factory exists because a Metro graph cannot take constructor parameters, and platform source +sets cannot extend the shared graph. So each platform constructs its own `PlatformFileDataSourceImpl` +and passes it in. The reasoning is documented in a comment in +[`AbysnerApplication`](../androidApp/src/main/kotlin/org/neotech/app/abysner/AbysnerApplication.kt). + +Each platform creates the graph at startup and hands it to the shared `App` composable: + +| Platform | Entry point | Creates the graph in | +|----------|------------------------------------------------------------------------------|--------------------------------| +| Android | [`MainActivity`](../androidApp/src/main/kotlin/org/neotech/app/abysner/MainActivity.kt) / [`AbysnerApplication`](../androidApp/src/main/kotlin/org/neotech/app/abysner/AbysnerApplication.kt) | `AbysnerApplication.onCreate()` | +| iOS | [`MainViewController`](../composeApp/src/iosMain/kotlin/MainViewController.kt) | lazily, in `iosMain` | +| Desktop | [`main.kt`](../composeApp/src/jvmMain/kotlin/main.kt) | `main()` | + + +## Persistence + +Persistence is built on [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) +(the preferences flavor) plus JSON serialization. The single DataStore file holds everything: the +dive configuration, the multi-dive plan input, and the app settings. + +The key pattern here is **versioned resource DTOs**. Domain models (like `Configuration`) are not +serialized directly. Instead the `data` module has separate serializable classes such as +[`ConfigurationResourceV1`](../data/src/commonMain/kotlin/org/neotech/app/abysner/data/diveplanning/resources/ConfigurationResourceV1.kt), +[`DivePlanInputResourceV1`](../data/src/commonMain/kotlin/org/neotech/app/abysner/data/diveplanning/resources/DivePlanInputResourceV1.kt), +and +[`SettingsResourceV1`](../data/src/commonMain/kotlin/org/neotech/app/abysner/data/settings/resources/SettingsResourceV1.kt). +Keeping the storage format separate from the domain model means the domain can evolve without +breaking saved data, and the `V1` suffix leaves room to add `V2` with a migration later. The +repository implementations also include migrations from older preference keys so updates do not lose +user data. + +File access itself is the one piece that differs per platform, handled through `PlatformFileDataSource` +and its platform implementations in the `data` module. + + +## Core domain models + +These are the data types that flow through the app. They all live under `domain/.../core/model/` +and `domain/.../diveplanning/model/`. + +| Model | Purpose | +|------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| [`Gas`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Gas.kt) | A breathing gas, defined by its oxygen and helium fractions. | +| [`Cylinder`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Cylinder.kt) | A gas plus a tank size and fill pressure. | +| [`Configuration`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Configuration.kt) | All the dive settings: gradient factors, rates, limits, algorithm, etc. | +| [`Environment`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Environment.kt) | Salinity and atmospheric pressure for the dive. | +| [`DiveProfileSection`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/model/DiveProfileSection.kt) | One user-planned bottom section: a depth, a duration, and a cylinder. | +| [`DivePlanInputModel`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/model/DivePlanInputModel.kt) | The full user input for one dive (sections, cylinders, dive mode). | +| [`DivePlan`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/model/DivePlan.kt) | The calculated result: segments, alternative ascents, CNS/OTU totals. | +| [`DiveSegment`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/model/DiveSegment.kt) | The smallest unit of a plan: a descent, stop, ascent, or gas switch. | +| [`UnitSystem`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/UnitSystem.kt) | Metric or imperial, with display conversions. | + + +## From the UI to the decompression engine + +This is the seam most contributors care about: how a tap in the UI turns into a calculated dive +plan. It all funnels through `PlanScreenViewModel.calculateMultiDivePlan()`, which constructs a +[`DivePlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt) +and a +[`GasPlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/GasPlanner.kt) +and asks them to do the work. + +```mermaid +flowchart TD + UI["PlannerScreen (Composable)"] + VM["PlanScreenViewModel
calculateMultiDivePlan()"] + DP["DivePlanner.addDive()"] + DCP["DecompressionPlanner"] + MODEL["Buhlmann (DecompressionModel)"] + GP["GasPlanner.calculateGasPlan()"] + STATE["StateFlow<Result<MultiDivePlanSet>>"] + + UI -->|user edits dive| VM + VM --> DP + DP --> DCP + DCP --> MODEL + VM --> GP + DP -->|DivePlan| GP + VM -->|emits| STATE + STATE -->|observed by| UI +``` + +`DivePlanner` turns user sections into pressure changes, hands those to `DecompressionPlanner` for +the stop and gas-switch logic, which in turn drives the `Buhlmann` tissue model. The result, a +`DivePlan`, is then passed to `GasPlanner` for the gas requirements. The view model wraps everything +in a `Result` and emits it as state, so calculation failures (for example, not enough time to +decompress) surface as UI state rather than crashes. + +The internals of that chain are the subject of [DECOMPRESSION.md](DECOMPRESSION.md). + + +## Testing and CI + +The decompression engine is covered by a thorough test suite in +[`domain/src/commonTest/`](../domain/src/commonTest). Notable files: + +| Test | Covers | +|----------------------------------------|----------------------------------------------------------------| +| `BuhlmannTest` | Tissue model, no-decompression limits, ceilings, snapshots. | +| `BuhlmannCcrTest` | Closed-circuit tissue loading and setpoint handling. | +| `BuhlmannUtilitiesTest` | The Schreiner equation, water vapour, CCR inputs. | +| `DecompressionPlannerTest` | Stop and ascent logic. | +| `DecoGridTest` | Snapping ceilings to deco stops. | +| `DivePlannerTest` | Full reference plans end to end (matching the readme tables). | +| `OxygenToxicityCalculatorTest` | CNS and OTU calculations. | + +CI is defined in [`.github/workflows/build.yml`](../.github/workflows/build.yml) and runs three +jobs: JVM tests with coverage and screenshot validation, an Android debug build, and an iOS build. +Coverage is collected with [Kover](https://github.com/Kotlin/kotlinx-kover) and reported separately +for the `domain` (core) and `presentation` (UI) layers. The UI also has screenshot tests, with +reference images stored via Git LFS. + + +## Map of the decompression engine + +The files below make up the decompression engine. Each is explained in detail in +[DECOMPRESSION.md](DECOMPRESSION.md); this table is a quick index. + +| File | Role | +|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| +| [`DivePlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt) | Public entry point. Turns user sections into a `DivePlan`. | +| [`DecompressionPlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecompressionPlanner.kt) | Stop times, ascents, gas switches. | +| [`DecoGrid.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecoGrid.kt) | Snapping ceilings to whole deco-stop depths. | +| [`DecompressionModel.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/DecompressionModel.kt) | The interface a decompression model must satisfy. | +| [`Buhlmann.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt) | Bühlmann ZHL-16 tissue model with gradient factors. | +| [`BuhlmannUtilities.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/BuhlmannUtilities.kt) | Schreiner equation, water vapour, CCR inputs. | +| [`DiveSegment.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/model/DiveSegment.kt) | The segment model and segment compaction. | +| [`OxygenToxicityCalculator.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/OxygenToxicityCalculator.kt) | CNS and OTU oxygen toxicity. | +| [`GasPlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/GasPlanner.kt) | Gas requirements (used/reserve, loop/bailout). | diff --git a/docs/DECOMPRESSION.md b/docs/DECOMPRESSION.md new file mode 100644 index 0000000..6fa6da1 --- /dev/null +++ b/docs/DECOMPRESSION.md @@ -0,0 +1,570 @@ +# The decompression engine + +This document explains how Abysner plans a dive, in detail. It is the most important document in +this repository, because the decompression engine is the part of the app most worth reading, +checking, and trusting. It is written to be followed by a developer who is comfortable with code but +new to decompression theory, so it starts with a short primer and then works through the actual +implementation, file by file. + +Everything here is tied to specific source files. Where a number, formula, or coefficient is quoted, +the source file it comes from is named so you can confirm it yourself. + +> **Safety note:** Abysner is a planning aid, not a substitute for training. The explanations below +> describe how the software works, not how to dive. Read the disclaimer in the +> [readme](../README.md) before relying on anything here. + + +## How to keep this document accurate + +This document is maintained by hand, so a few rules keep it from drifting out of sync with the code: + +- **One source of truth per fact.** A specific value or table is quoted in one place only, and the + file it came from is named right next to it. If you change that value in code, update the one spot + here that quotes it. +- **Formulas and structure are written out in full**, because they rarely change. Long constant + tables and tunable values are quoted but always point back to their source file as the canonical + copy. The full 16-row compartment tables, for example, live in `Buhlmann.kt`, not here. +- **Default limits and thresholds** (gradient factors, max ppO2, ascent rates, deco step, and so on) + all come from + [`Configuration.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Configuration.kt). + That file is the single source of truth for them. + +There is a [maintenance table](#where-to-change-what) at the end mapping each concept to the file +that owns it. + + +## Decompression in five minutes + +When you breathe gas under pressure, the inert part of that gas (nitrogen, and helium if you use it) +dissolves into your body. The deeper you go and the longer you stay, the more dissolves in. This is +called **on-gassing**. When you ascend, the surrounding pressure drops and that dissolved gas comes +back out, which is **off-gassing**. + +The danger is ascending too fast. If the pressure drops faster than your body can release the gas, +the dissolved gas can form bubbles, which causes decompression sickness. To avoid that, a dive +planner works out how much inert gas your body has taken on, and how slowly you must ascend +(including pauses, called **decompression stops**) to let it back out safely. + +**The Bühlmann ZHL-16 model** is the standard way to estimate this. It models the body as 16 +theoretical **tissue compartments**, each on-gassing and off-gassing at a different speed (a +different "half-time"). Fast compartments load and unload quickly (think blood), slow ones take +hours (think bone and fat). At any moment, each compartment has a tolerated ceiling: the shallowest +depth (lowest pressure) it can be exposed to without exceeding its limit. The overall **ceiling** is +the shallowest of all 16. You may not ascend above it. + +**Gradient factors** make the model more conservative. The raw Bühlmann limits (the "M-values") +represent the most a compartment can supposedly tolerate. Many divers do not want to ride right up +against that limit, so a gradient factor expresses what fraction of it you allow. Abysner uses two: +`gfLow`, applied at depth (at the deepest stop), and `gfHigh`, applied at the surface, with a linear +interpolation in between. A lower number is more conservative. The defaults are `gfLow = 0.6` and +`gfHigh = 0.7` (from +[`Configuration.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Configuration.kt)), +usually written as "60/70". + +That is the whole idea: track gas in 16 compartments, compute a ceiling from their tolerances, +apply gradient factors, and ascend no faster than the ceiling allows. + + +## The big picture + +The engine is built in layers. Each layer has one job and hands off to the next. + +```mermaid +flowchart TD + DP["DivePlanner
turns user sections into pressure changes,
handles multi-level and multi-dive"] + DCP["DecompressionPlanner
stop times, ascents, gas switches"] + GRID["DecoGrid
snaps ceilings to whole stop depths"] + MODEL["Buhlmann (DecompressionModel)
tissue loading and the raw ceiling"] + OXTOX["OxygenToxicityCalculator
CNS and OTU"] + + DP --> DCP + DCP --> GRID + DCP --> MODEL + DP --> OXTOX +``` + +- [`DivePlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt) + is the public entry point. You give it a list of bottom sections and cylinders, it gives you back a + `DivePlan`. +- [`DecompressionPlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecompressionPlanner.kt) + implements the diving procedures: where to stop, for how long, and when to switch gas. It works + entirely in absolute pressure (bar). +- [`DecoGrid`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecoGrid.kt) + knows that divers stop on a grid (every 3 m or 10 ft), and rounds the model's continuous ceiling + onto that grid. +- [`Buhlmann`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt) + is the model itself. It only does tissue loading and ceiling calculation, deliberately with no + planning logic. It implements the + [`DecompressionModel`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/DecompressionModel.kt) + interface, so a different model could be dropped in. +- [`OxygenToxicityCalculator`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/OxygenToxicityCalculator.kt) + computes oxygen exposure (CNS and OTU) after the plan is built. + +A note on units that matters throughout: the planner and model work in **absolute ambient pressure +in bar** (depth pressure plus atmospheric pressure). Conversions to and from meters or feet happen +at the edges, via the helpers in +[`Pressure.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/physics/Pressure.kt). +Atmospheric pressure at sea level is `1.01325` bar (`ATMOSPHERIC_PRESSURE_AT_SEA_LEVEL` in that +file), adjusted for altitude with the barometric formula. + + +## Tissue compartments + +The model is 16 compartments, defined as `CompartmentParameters` in +[`Buhlmann.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt). +Each compartment carries six numbers: a half-time and two coefficients (`a` and `b`) for nitrogen, +and the same three for helium. + +```kotlin +data class CompartmentParameters( + val n2HalfTime: Double, + val n2ValueA: Double, + val n2ValueB: Double, + val heHalfTime: Double, + val heValueA: Double, + val heValueB: Double, +) +``` + +The nitrogen half-times run from 5 minutes (the fastest compartment) to 635 minutes (the slowest). +There are three versions of the table, `ZH16A`, `ZH16B`, and `ZH16C`, selected by the +`algorithm` setting. As the comment in the source notes, the N2 half-times and the `b` coefficients +are identical across all three versions; **only the nitrogen `a` coefficients differ**. ZHL-16C is +the most conservative of the three and is the default. + +The full tables (all 16 rows for each version, with the helium values too) are the canonical copy in +`Buhlmann.kt`, in the `ZH16A_COMPARTMENTS`, `ZH16B_COMPARTMENTS`, and `ZH16C_COMPARTMENTS` lists. +They are not reproduced here to avoid two copies drifting apart. As a representative sample, the +first and last ZHL-16C nitrogen rows are: + +| Compartment | N2 half-time (min) | N2 `a` (ZHL-16C) | N2 `b` | +|-------------|--------------------|------------------|--------| +| 1 (fastest) | 5.0 | 1.1696 | 0.5578 | +| 16 (slowest)| 635.0 | 0.2327 | 0.9653 | + +The source also documents how these numbers were cross-checked, against Subsurface, DecoTengu, and +dipplanner, with links in the comment above the tables. + +At runtime each compartment is a `TissueCompartment` that tracks its current nitrogen and helium +partial pressures (`pNitrogen`, `pHelium`) and their sum (`pTotal`). A fresh compartment starts +fully saturated with nitrogen at the surface: + +```kotlin +private var pNitrogen: Double = partialPressure(environment.atmosphericPressure - waterVapourPressure, 0.79), +private var pHelium: Double = 0.0, +``` + +That is, it assumes you have been breathing air (79% nitrogen) at the surface long enough to be in +equilibrium, with the alveolar water vapour already subtracted (see the next sections). + + +## The Schreiner equation + +When the diver spends time at a depth, or changes depth, each compartment's gas pressure has to be +updated. Abysner uses the **Schreiner equation** for this, implemented in +[`BuhlmannUtilities.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/BuhlmannUtilities.kt): + +```kotlin +fun schreinerEquation(initialTissuePressure: Double, inspiredGasPressure: Double, time: Double, halfTime: Double, inspiredGasRate: Double): Double { + val timeConstant = ln(2.0) / halfTime + return (inspiredGasPressure + (inspiredGasRate * (time - (1.0 / timeConstant))) - ((inspiredGasPressure - initialTissuePressure - (inspiredGasRate / timeConstant)) * exp(-timeConstant * time))) +} +``` + +In words, this is the standard Schreiner form: + +``` +P(t) = Pio + R * (t - 1/k) - (Pio - Po - R/k) * e^(-k*t) +``` + +where: + +- `Po` is the compartment's current inert gas pressure (`initialTissuePressure`). +- `Pio` is the inspired inert gas pressure at the start of the segment (`inspiredGasPressure`). +- `R` is the rate at which the inspired inert gas pressure changes per minute, which is non-zero + during a depth change (`inspiredGasRate`). +- `k` is the compartment's time constant, `ln(2) / halfTime`. +- `t` is the time in minutes. + +The reason for Schreiner rather than the simpler Haldane equation is the `R` term. Haldane assumes a +constant inspired pressure, which is fine for a flat segment but wrong during a descent or ascent, +where the inspired pressure changes the whole time. Schreiner handles the changing-depth case +directly, so the same function works for flat, descending, and ascending segments. Note the function +itself knows nothing about water vapour; the caller is responsible for subtracting it before +computing `Pio` and `R`. + + +## Water vapour and inspired pressure + +The gas in your lungs is not the same pressure as the gas in your tank. Your alveoli are saturated +with water vapour at body temperature, and that vapour takes up part of the pressure, leaving less +for the gas you actually breathe. So before applying gas fractions, the model subtracts the alveolar +water vapour pressure from the ambient pressure. + +The water vapour pressure is computed with the +[Antoine equation](https://en.wikipedia.org/wiki/Antoine_equation) in `BuhlmannUtilities.kt` +(`waterVapourPressure` / `waterVapourPressureInBars`), assuming a body temperature of 37 degrees +Celsius. That works out to roughly 0.063 bar. The single source of truth for the temperature is the +`waterVapourPressure` constant in `Buhlmann.kt`: + +```kotlin +private val waterVapourPressure: Double = waterVapourPressureInBars(37.0) +``` + +So the inspired inert gas pressure on open circuit is `(ambient - waterVapour) * inertFraction`, +using the `partialPressure` helper from +[`Pressure.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/physics/Pressure.kt) +(which is just Dalton's law: `totalPressure * fraction`). + + +## Loading the tissues + +Tissue loading happens in `TissueCompartment.addPressureChange()` in `Buhlmann.kt`. It calculates +the rate of pressure change per minute, then branches on whether this is an open-circuit or +closed-circuit segment. Nitrogen and helium are always tracked independently, each with its own +half-time and its own Schreiner call. + +A guard rejects zero or negative durations, both because they make no physical sense and because +they would divide by zero when computing the rate. + +### Open circuit + +On open circuit the breathing gas has fixed fractions, so loading is direct. From +`addPressureChangeOc`: + +```kotlin +val inspiredPressure = startPressure - waterVapourPressure + +this.pNitrogen = schreinerEquation( + initialTissuePressure = pNitrogen, + inspiredGasPressure = partialPressure(inspiredPressure, fN2), + time = timeInMinutes, + halfTime = parameters.n2HalfTime, + inspiredGasRate = partialPressure(depthChangeInBarsPerMinute, fN2), +) +``` + +The same is done for helium with the helium fraction and helium half-time. The oxygen fraction does +not appear, because oxygen is metabolized and is not an inert gas to track for decompression. + +### Closed circuit + +On a closed-circuit rebreather (CCR), the loop holds the oxygen partial pressure at a constant +**setpoint**, so the inert gas pressure does not follow ambient pressure in the simple open-circuit +way. The trick Abysner uses is `ccrSchreinerInputs` (in `BuhlmannUtilities.kt`), which computes an +effective inspired pressure and rate so that the *same* Schreiner equation still applies. The inert +gas partial pressure on CCR works out to: + +``` +(ambient - setpoint) * inertFraction / (1 - oxygenFractionDiluent) +``` + +`(ambient - setpoint)` is the pressure left over once the oxygen setpoint is accounted for, and +dividing by `(1 - oxygenFractionDiluent)` rescales the diluent's inert fraction to exclude the +diluent oxygen the setpoint already covers. This stays linear in ambient pressure, which is exactly +what Schreiner needs. + +One complication is handled explicitly in `addPressureChangeCcr`: the setpoint cannot be held once +ambient pressure drops below it (the loop maxes out on pure oxygen). So when a segment crosses the +setpoint pressure during an ascent or descent, the segment is split at the exact crossing point. On +the deep side normal CCR loading applies; on the shallow side there is no inert gas being inspired at +all (inspired pressure and rate are both zero). The setpoint passed in is first corrected for water +vapour (`ccrSetpoint + waterVapourPressure`). + +The CCR inputs are verified in `BuhlmannUtilitiesTest` against both the Helling CCR Schreiner +formulation and a brute-force Haldane simulation. + + +## M-values, ceilings, and gradient factors + +The ceiling is where decompression theory becomes a number. There are two related calculations in +`Buhlmann.kt`. + +### The raw per-compartment ceiling + +`TissueCompartment.calculateCeiling(gf)` returns the shallowest pressure a single compartment +tolerates, for a given gradient factor. Because both nitrogen and helium are present, their `a` and +`b` coefficients are first combined, weighted by their partial pressures: + +```kotlin +val a = ((parameters.n2ValueA * this.pNitrogen) + (parameters.heValueA * this.pHelium)) / (this.pTotal) +val b = ((parameters.n2ValueB * this.pNitrogen) + (parameters.heValueB * this.pHelium)) / (this.pTotal) + +val ceiling = (this.pTotal - (a * gf)) / ((gf / b) + 1.0 - gf) +``` + +With `gf = 1.0` this is the raw Bühlmann M-value limit. A smaller `gf` pulls the tolerated pressure +deeper (more conservative). The result is clamped so it never goes above the surface pressure. + +### Applying gradient factors across the dive + +A single `gf` is not the whole story, because Abysner interpolates between `gfLow` at the deepest +ceiling and `gfHigh` at the surface. This is `toleratedInertGasPressure()`, the most involved piece +of math in the engine. It: + +1. Computes the tolerated inert gas pressure at the surface using `gfHigh`. +2. Computes the tolerated inert gas pressure at the lowest ceiling reached so far using `gfLow`. +3. Treats those as two points on a line, solves for that line's slope and intercept, and inverts it + to get the tolerated ambient pressure for the compartment's current loading. + +The "lowest ceiling reached so far" is tracked across the whole dive in the `Buhlmann.lowestCeiling` +field, and updated in `getMinimumToleratedAmbientPressure()`. That history matters: the gradient +factor line is anchored to the deepest stop the diver actually needed, not just the current depth. +The source has an extended comment deriving the slope and intercept, along with the references it was +built from (Subsurface, the OSTC gradient factor document, dive-tech's M-values paper, and +DecoTengu). + +The overall ceiling for the whole model is then the deepest (highest pressure) ceiling across all 16 +compartments, exposed via `Buhlmann.getCeiling()`. + + +## The deco-stop grid + +The model produces a continuous ceiling in bar, but divers stop at whole depths on a fixed grid +(every 3 m, or 10 ft in imperial), and there is a configured last stop depth (3 m by default). That +grid logic is isolated in +[`DecoGrid`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecoGrid.kt), +which keeps it out of both the model and the planner. + +It has three jobs: + +- `snapCeilingToDecoGrid(rawCeiling)` rounds a raw ceiling **up** (deeper) to the next grid stop. If + the result would land between the surface and the configured last stop, it clamps to the last stop. +- `findNextDecoStopPressure(from)` returns the next shallower stop. If already exactly on a grid + point, it goes one step shallower. +- `isAtDecoStop(pressure)` reports whether a pressure sits on a grid point, which is used to align + gas switches to stops. + +The grid is configured in pressure, not depth, but it is built so stops always land on whole display +units. `DivePlanner.createDecompressionPlanner()` constructs it from the configured `decoStepSize`, +`lastDecoStopDepth`, and the display unit (meter or foot), converting each to a pressure delta. A +constructor check enforces that the deco step is an exact multiple of the display unit, so you never +get a stop at, say, 2.9 m. + + +## The planner loop + +[`DecompressionPlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecompressionPlanner.kt) +ties the model and the grid together into an actual ascent. The two building blocks are `addFlat()` +and `addDepthChange()`, which apply a segment to the model and record a `DiveSegment`. Both run the +model minute by minute (except in lookahead mode, see below), so tissue loading is always evaluated +in whole-minute steps. A central detail: **the whole engine plans in whole minutes**, which is a +deliberate design choice explained in [Design decisions](#design-decisions-and-known-divergences). + +The heart of it is `calculateDecompression(toAmbientPressure, breathingMode)`. Roughly, it does +this: + +1. Find the first deco ceiling from the current depth, via `findFirstDecoCeiling()`. +2. On open circuit, check whether a better deco gas is available right now and switch if so. +3. Ascend to that first ceiling (the gas-switch-aware ascent is `addDecoDepthChange()`). +4. Loop while the ceiling is still below the surface: + - work out the next shallower stop, + - add one-minute stops at the current depth until the ceiling clears enough to move up, + - ascend to the next ceiling. +5. Stop once the ceiling reaches the surface (or the requested target pressure). + +There is a safety valve inside the stop loop: if a compartment cannot off-gas enough to ever reach +the next stop (for example, a last stop set too shallow with extremely conservative gradient +factors), the loop gives up after 1000 minutes and throws a `PlanningException` with an explanation, +rather than spinning forever. + +### Lookahead and the ceiling-skip optimization + +Because the engine plans in whole minutes, a ceiling that is only a hair deeper than a stop would +otherwise cost a full extra minute at the deeper stop, even if a few seconds of off-gassing during +the ascent would clear it. To avoid that penalty, the planner simulates the ascent before committing +to it. `isCeilingClearedDuringAscent()` runs the ascent, checks whether the ceiling cleared, and +then rolls the model back. + +That rollback is the `lookahead {}` helper. It snapshots the planner state and the model, runs the +block, and restores everything afterward, so these "what if" calculations never affect the real +plan. The same mechanism powers time-to-surface (TTS) calculations, see below. The model side of the +rollback is `DecompressionModel.resetAfter {}`, which snapshots and restores the tissue state. + +### Gas switching + +On open circuit, the planner picks the best available gas at each stop. `addDecoDepthChange()` walks +up from the current pressure, and at each grid stop asks `decoGases.findBetterGasOrFallback(...)` +whether a better gas (higher oxygen, within the ppO2 and END limits) is now breathable. If so it +emits a `GAS_SWITCH` segment and continues on the new gas. The gas-switch time (1 minute by default) +is spent on the *old* gas, modeling the diver still breathing the previous gas while preparing to +switch. Gas switching is skipped entirely on CCR, since the diver stays on the loop. A CCR-to-OC +bailout is handled as a special case at the top of `calculateDecompression()`. + +The limits used here come from the configuration: `maxPPO2Deco` (1.6 bar by default) for the deco +ppO2 ceiling, and `maxEND` (30 m) for the equivalent narcotic depth. END is computed in +[`Gas.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Gas.kt), +treating both oxygen and nitrogen as narcotic. + + +## Oxygen toxicity (CNS and OTU) + +Breathing oxygen at raised partial pressure is itself a hazard, tracked two ways in +[`OxygenToxicityCalculator.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/OxygenToxicityCalculator.kt). +Both are computed from the finished segment list, after the plan is built (in `DivePlanner.addDive`). +The work is based on Erik Baker's papers and Robert Helling's write-ups, with the references listed +in the file header. + +The shared input is the effective oxygen partial pressure, `effectivePartialOxygenPressure()`. On +open circuit it is simply `oxygenFraction * ambientPressure`. On CCR it is +`min(max(setpoint, diluentPpO2), ambient)`, so the ppO2 follows the setpoint but can never exceed +ambient pressure at shallow depths. + +### CNS + +CNS (central nervous system) toxicity is accumulated per segment, using the average pressure of the +segment. Below a ppO2 of 0.5 bar it contributes nothing. Above that, the per-minute rate is an +exponential fit to the NOAA exposure table, with two line segments (one up to ppO2 1.5, one above): + +```kotlin +private fun getCnsPpo2Slope(ppO2: Double): Double { + if(ppO2 <= 1.5) { + return -11.7853 + (1.93873 * ppO2) + } + return -23.6349 + (9.80829 * ppO2) +} +``` + +and the contribution of a segment is `(duration * 60) * exp(slope) * 100`, giving a percentage. The +source notes a 2025 proposal to relax the 1.3 bar limit, with a pointer to re-fit the curve if the +NOAA table is updated, a good example of a value to keep an eye on. + +### OTU + +OTU (oxygen toxicity units, the "whole body" measure) uses the improved Baker/Helling formula that +works for flat, ascending, and descending segments without dividing by zero. With `Pm` defined as +`(ppO2Start + ppO2End) - 1.0`: + +```kotlin +val pm = (ppo2Start + ppo2End) - 1.0 +val rate = pm.pow(5.0 / 6.0) * (1.0 - 5.0 * (ppo2End - ppo2Start).pow(2) / 216 / (pm * pm)) +return rate * durationInMinutes +``` + +As with CNS, exposure below 0.5 bar is ignored, and when only part of a segment is above 0.5 bar the +duration is scaled to just that part. + + +## Gas planning + +Once the dive profile exists, [`GasPlanner`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/GasPlanner.kt) +works out how much gas it needs. This is separate from decompression but uses the same segments. + +For **open circuit** it reports two numbers per cylinder: + +- *Used gas*: the gas to complete the planned profile, at the normal SAC rate. Per segment this is + `duration * sacRate * averagePressure`, summed per cylinder. +- *Reserve gas*: enough to bring an out-of-air diver up from the worst point of the dive, at the + emergency SAC rate (`sacRateOutOfAir`, double the normal rate by default). + +The "worst point" is not simply the deepest point. Abysner uses time-to-surface (TTS) to find it. +During planning, the `DecompressionPlanner` records an alternative ascent (and its TTS) at the end of +each section, using the `lookahead` mechanism. `findWorstCaseAscentCandidates()` then narrows these +down to the segments that could plausibly demand the most gas, and the actual gas usage is computed +for each candidate, taking the maximum per mix. The reasoning, including why a shallower section can +be worse than a deeper one, is explained in FAQ 5 of the [readme](../README.md#faq). + +For **closed circuit** the numbers are different. There is no buddy reserve; instead the plan reports +loop gas (oxygen consumed metabolically at `ccrMetabolicO2LitersPerMinute`, plus diluent added to +the loop on descent based on `ccrLoopVolumeLiters`) and bailout gas (the worst-case open-circuit +ascent at the normal SAC rate). + +Cylinder capacity itself is not a simple pressure-times-volume product. It uses a real-gas model +([`GasEquationOfStateModel`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/physics/GasEquationOfStateModel.kt), +defaulting to `PolynomialRealGasModel`), because real gases deviate from the ideal gas law at the +high pressures used in scuba cylinders. + + +## A worked example, end to end + +Here is the simplest reference plan from `DivePlannerTest`, walked through the engine. The exact +expected output is asserted by `referencePlan1_producesExpectedSegments()`. + +Input: + +- One section: 20 meters for 20 minutes, on air (21/0), in a 12 L steel cylinder. +- Configuration: ascent and descent rate 5 m/min, gradient factors 30/70, fresh water, sea level, + ZHL-16C, deco step 3 m, last stop 3 m. (Note this example uses 30/70 to match the readme reference + table; the app default is 60/70.) + +What happens: + +1. `DivePlanner.addDive()` creates a `Buhlmann` model (ZHL-16C, GF 30/70) and a + `DecompressionPlanner` with a 3 m grid. +2. The single section needs a descent first. At 5 m/min, reaching 20 m takes 4 minutes, so + `addDepthChange` loads the tissues over a 4-minute descent from the surface to 20 m. +3. The remaining bottom time is `20 - 4 = 16` minutes, applied with `addFlat` at 20 m. +4. The final ascent calls `calculateDecompression(toAmbientPressure = surface)`. At 20 m for this + short a time the ceiling never drops below the surface, so no stops are required. The ascent from + 20 m to the surface at 5 m/min takes 4 minutes. +5. `OxygenToxicityCalculator` runs over the finished segments. + +The result is three segments, matching the test and the readme's reference plan 1: + +| Type | Depth | Duration | Runtime | +|---------|--------------|----------|---------| +| Descent | 0 -> 20 m | 4 min | 4 min | +| Flat | 20 m | 16 min | 20 min | +| Ascent | 20 -> 0 m | 4 min | 24 min | + +with `totalCns = 2.731` and `totalOtu = 5.443` (both asserted to three decimals in the test). For a +plan that does involve stops and gas switches, see reference plans 2 and onward in the +[readme](../README.md#compared-to-other-planners), all of which are also covered by `DivePlannerTest`. + + +## Design decisions and known divergences + +A few choices are worth calling out, because they explain why Abysner's plans can differ from other +tools. + +- **Whole-minute planning.** Abysner calculates the whole dive in whole minutes from the start, + rather than computing in seconds and rounding at the end. Divers write plans in minutes, and + planning in minutes from the start means the plan needs no rounding that would otherwise leave you + slightly under- or over-decompressed. The trade-off is that ascent and descent speeds are rounded + instead. This is discussed in FAQ 1 of the [readme](../README.md#faq). +- **Why plans differ from other planners.** Even within "Bühlmann with gradient factors" there are + many small implementation details that are simply undefined, so two correct planners can produce + different stops. Robert Helling's article + ["Why is Bühlmann not like Bühlmann"](https://thetheoreticaldiver.org/wordpress/index.php/2017/11/02/why-is-buhlmann-not-like-buhlmann/) + is the best explanation. The readme's "Compared to other planners" section documents specific + differences against Subsurface and DIVESOFT.APP. +- **Determinism.** A dive planner must be deterministic and reproducible. The engine is pure Kotlin + with no randomness and no platform dependencies, which is also what makes it straightforward to + cover with the reference-plan tests. + + +## Where to change what + +This table maps each concept to the file that owns it, so a change in the code maps to one place to +update here. + +| Concept | Source of truth | +|------------------------------------------------|---------------------------------------------------------------------------------| +| Default GF, ppO2, rates, deco step, SAC, CCR setpoints | [`Configuration.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Configuration.kt) | +| Compartment tables (half-times, a/b) | [`Buhlmann.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt) (`ZH16*_COMPARTMENTS`) | +| Ceiling and gradient-factor math | [`Buhlmann.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt) (`calculateCeiling`, `toleratedInertGasPressure`) | +| Schreiner equation, water vapour, CCR inputs | [`BuhlmannUtilities.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/BuhlmannUtilities.kt) | +| Body temperature for water vapour | [`Buhlmann.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/algorithm/buhlmann/Buhlmann.kt) (`waterVapourPressure`) | +| Stop grid and last-stop logic | [`DecoGrid.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecoGrid.kt) | +| Stop times, ascents, gas switching, TTS | [`DecompressionPlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/decompression/DecompressionPlanner.kt) | +| Multi-level and multi-dive orchestration | [`DivePlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt) | +| CNS and OTU formulas | [`OxygenToxicityCalculator.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/OxygenToxicityCalculator.kt) | +| Gas requirements (used/reserve, loop/bailout) | [`GasPlanner.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/gasplanning/GasPlanner.kt) | +| Gas mixes, MOD, END, density | [`Gas.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/model/Gas.kt) | +| Pressure, depth, altitude conversions | [`Pressure.kt`](../domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/core/physics/Pressure.kt) | + + +## References + +These are the main sources the engine is built on. They are also credited in the +[readme](../README.md#credits). + +- [The Theoretical Diver](https://thetheoreticaldiver.org) (Robert Helling), especially the articles + on gradient factors, oxygen toxicity, and the Schreiner equations for CCR. +- Erik C. Baker: "Understanding M-Values", "Clearing Up The Confusion About Deep Stops", and + "Oxygen Toxicity Calculations". +- Open-source planners used for cross-checking the compartment data and behavior: + [Subsurface](https://github.com/subsurface/subsurface), + [DecoTengu](https://wrobell.dcmod.org/decotengu/index.html), + [GasPlanner](https://github.com/jirkapok/GasPlanner), and + [nyxtom/dive](https://github.com/nyxtom/dive). From 7b73a6607cb1ace9a44709f3d5c46d6b33c561c5 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 15:58:45 +0300 Subject: [PATCH 02/11] Docs: Add contributing guidelines, code of conduct and issue/PR templates Adds CONTRIBUTING.md (scope policy, bug reporting, dev workflow, commit conventions), a code of conduct, structured bug report and feature request issue templates, an issue template config routing questions to Discussions, and a pull request template. Co-Authored-By: Claude Opus 4.8 --- .github/ISSUE_TEMPLATE/bug_report.md | 33 +++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.md | 23 +++++ .github/PULL_REQUEST_TEMPLATE.md | 17 ++++ CODE_OF_CONDUCT.md | 82 +++++++++++++++++ CONTRIBUTING.md | 105 ++++++++++++++++++++++ 6 files changed, 265 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d0f0895 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Report something that is wrong or not working +title: '' +labels: bug +assignees: '' +--- + +**What is wrong?** +A clear description of the problem, what you expected, and what actually happened. + +**Is this about a dive plan or calculation?** +If so, please include enough detail to reproduce it exactly. The reference plan tables in the +[readme](../../README.md#compared-to-other-planners) are a good template for how to present this. + +- Algorithm (e.g. ZHL-16C) and gradient factors: +- Salinity, altitude, ascent/descent rates, max ppO2, last deco stop: +- Unit system (metric or imperial): +- The dive (each section's depth and duration, gases, cylinders): +- What you expected vs. what Abysner produced: +- If comparing to another planner, which one and which version: + +**Steps to reproduce (for non-calculation bugs)** +1. +2. +3. + +**Platform** +- Device and OS (Android or iOS, version): +- App version: + +**Anything else?** +Screenshots, logs, or other context. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e9b828c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question or discussion + url: https://github.com/NeoTech-Software/abysner/discussions + about: For questions, ideas, and general discussion, please use Discussions instead of opening an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f68582f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea or improvement +title: '' +labels: enhancement +assignees: '' +--- + +> Please note: Abysner has a specific scope and direction, and not all requests fit it. Starting a +> discussion before a large request is often the better first step. See +> [CONTRIBUTING.md](../../CONTRIBUTING.md). + +**What problem would this solve?** +Describe the need or the gap you are running into. + +**What would you like to see?** +A clear description of the feature or change. + +**Alternatives you have considered** +Other ways to solve the same problem, if any. + +**Anything else?** +Context, examples, or references. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ead5153 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ + + +**What does this change?** +A short description of the change and why it is needed. Link any related issue or discussion. + +**Checklist** +- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md). +- [ ] For anything non-trivial, this was discussed in an issue or discussion first. +- [ ] Tests pass locally (`./gradlew :domain:jvmTest`, or the full CI command for wider changes). +- [ ] New or changed behavior in the `domain` module is covered by tests. +- [ ] If this changes calculations, a reference-plan style test was added or updated. +- [ ] If this changes the UI, screenshot references were updated if needed. +- [ ] I will sign the CLA when prompted. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7023dd6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,82 @@ +# Code of Conduct + +## Our Pledge + +Abysner is built with simplicity and correctness in mind, and we want the community around it to +reflect those same values. We pledge to make participation in this project a welcoming and +respectful experience for everyone, regardless of background, experience level, or diving +certification. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Being respectful and constructive in discussions, issues, and pull requests +- Giving and receiving technical feedback graciously and without making it personal +- Being honest about the limits of your expertise, especially on safety-relevant topics +- Backing technical claims with references, test results, or validated sources where possible +- Properly crediting the sources of content you contribute +- Respecting the project's scope and direction, even when a contribution is not accepted +- Taking responsibility for your actions, and committing to repairing harm when it occurs + +Examples of behavior that is not acceptable: + +- Harassment, discrimination, or personal attacks of any kind +- Dismissing safety concerns without justification +- Submitting unverified changes to decompression logic or other safety-critical algorithms +- Misrepresenting test results or reference plan comparisons +- Spam, off-topic self-promotion, or low-effort contributions with no intent to improve + +## Safety and Correctness + +Divers use Abysner to plan real dives. That means correctness is not optional. Issues that affect +decompression calculations, gas planning, or ascent and descent modeling are treated with extra +care. + +If you think you have found a bug that could lead to an unsafe dive plan, please **open an issue +right away** and describe it clearly. Do not wait until you have a fix ready. The community will +treat it as a priority. + +Algorithmic contributions must be validated against the reference plans in the README, and ideally +reviewed by someone with relevant technical diving knowledge. Unverified or speculative changes to +core algorithms will not be merged, no matter how well-intentioned. + +## AI Tools + +As noted in the README, AI assistance has a place in this project: writing boilerplate, surfacing +bugs, exploring ideas. But it cannot replace correctness, and a safety-relevant application like a +dive planner must be deterministic. + +Contributors are welcome to use AI tools, but: + +- Do not submit AI-generated decompression logic or algorithm changes without manually verifying them +- Do not present AI-generated safety-critical content as your own verified work +- Every line of code that affects a dive calculation must be understood and checked by a person + +## Enforcement + +Project maintainers are responsible for clarifying and enforcing this Code of Conduct. You can +report unacceptable behavior by opening a private discussion or contacting the maintainers directly +through GitHub. + +When a violation occurs, maintainers will do their best to respond promptly and handle it fairly. +Depending on the severity and whether it is a first or repeated incident, responses may include: + +1. **Warning.** A private written note explaining the issue and what is expected going forward. +2. **Temporary cooldown.** A time-limited restriction on participation, giving everyone involved + time to process the situation. +3. **Temporary suspension.** A pause from the project with conditions for return. +4. **Permanent ban.** Reserved for serious or repeated violations where other steps have not worked. + +Maintainers may also remove, edit, or reject comments, commits, issues, and other contributions +that violate this Code of Conduct. + +## Scope + +This Code of Conduct applies in all project spaces, including GitHub issues, pull requests, +discussions, and any other official project channels. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant, version 3.0](https://www.contributor-covenant.org/version/3/0/code_of_conduct/), +with additions specific to the safety-critical nature of Abysner. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d4efea1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,105 @@ +# Contributing to Abysner + +Thanks for your interest in contributing. Abysner is an open-source dive planner, and contributions +are welcome, but please read this first. Abysner has a specific scope and direction, and it is a +safety-relevant application, so the bar for changes is higher than for a typical app. + +## Before you start + +**Open an issue or a discussion before writing significant code.** This project has a clear scope, +and not every change fits it. A short conversation up front is the best way to make sure your time is +well spent, and to avoid a pull request that cannot be merged. Small, obvious fixes (typos, clear +bugs) are fine to send directly. + +By contributing you agree to license your work under the project's +[AGPLv3 license](LICENSE), and you must sign the +[Contributor License Agreement (CLA)](cla.txt). The CLA is automated: when you open a pull request, +the [CLA Assistant](https://github.com/contributor-assistant/github-action) bot will prompt you to +sign if you have not already. A pull request cannot be merged until the CLA is signed. + +## Reporting bugs + +Good bug reports are especially valuable for a dive planner, where a difference of one minute can +matter. When the bug is about a dive plan or a calculation, please include enough detail to +reproduce it exactly: + +- The full configuration: algorithm (for example ZHL-16C), gradient factors, salinity, altitude, + ascent/descent rates, max ppO2, last deco stop, and unit system (metric or imperial). +- The dive itself: each section's depth and duration, the gas mixes, and the cylinders. +- What you expected, and what Abysner produced. If you can, compare against another planner and say + which one and which version. + +The reference plan tables in the [readme](README.md#compared-to-other-planners) are a good template +for how to present a plan clearly. The more your report looks like those, the faster it can be +checked. + +For anything that is not a calculation bug, just describe the steps to reproduce, what you expected, +and what happened, along with your platform (Android or iOS) and app version. + +## Setting up your environment + +See [Building from source](README.md#building-from-source) for prerequisites and build commands, and +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for a tour of how the codebase is organized. If you are +working on the decompression engine, [docs/DECOMPRESSION.md](docs/DECOMPRESSION.md) explains it in +detail. + +## Making changes + +**Code style.** The project uses the official Kotlin code style. An [`.editorconfig`](.editorconfig) +is included and is respected by IntelliJ IDEA and Android Studio, so in most cases you do not need to +configure anything. Match the style of the surrounding code. + +**Tests.** Changes to the `domain` module (the decompression engine, gas planning, physics) must be +covered by tests, and all tests must pass. The engine is pure Kotlin, so its tests run quickly +without any device or simulator: + +```sh +# Run the domain (engine) tests +./gradlew :domain:jvmTest + +# Run everything CI runs (tests, coverage, screenshot validation) +./gradlew :koverXmlReportDomain :koverXmlReportPresentation +``` + +If you change calculations, prefer adding or extending a reference-plan style test in +`DivePlannerTest`, so the new behavior is pinned down and stays correct. + +**UI changes.** The UI has screenshot tests with reference images stored in Git LFS. If your change +affects the UI, the references may need updating. There is a dedicated GitHub Actions workflow +([`update-screenshots.yml`](.github/workflows/update-screenshots.yml)) for regenerating them. + +**Commits.** Commit messages in this project follow a simple `Category: Short description` format. +The categories in use are: + +| Category | For | +|-------------|------------------------------------------------| +| `Add:` | New features or capabilities | +| `Fix:` | Bug fixes | +| `Refactor:` | Code changes that do not change behavior | +| `Update:` | Dependency or maintenance updates | +| `Docs:` | Documentation only | +| `CI:` | Build, CI, or release tooling | +| `Release:` | Version bumps (maintainer only) | + +Keep each commit focused, and write the description in plain language (see the project's git history +for examples). + +## Opening a pull request + +- Base your work on `main` and open the pull request against `main`. +- Keep pull requests focused on a single change. Smaller is easier to review and more likely to be + merged. +- Make sure the build and tests pass locally before opening it. CI will run the JVM tests, an Android + build, and an iOS build. +- Sign the CLA when prompted. + +## A note on AI + +This is not a vibe-coded project. AI assistance is used where it helps (boilerplate, surfacing bugs, +exploring ideas), but every architectural decision is made deliberately, every algorithm is +validated against known references and diving standards, and every line of code is reviewed by a +person. Decompression is safety-relevant and must be deterministic, so correctness is never +delegated to a probabilistic tool. Contributions are held to the same standard: if you used AI to +help write something, that is fine, but you are responsible for understanding and verifying it. + +Thanks again, and dive safe. From cb20650ab212212a5ac513247eeb69ff80dc4e9c Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 15:58:45 +0300 Subject: [PATCH 03/11] Docs: Rename license.txt to LICENSE for standard discovery Co-Authored-By: Claude Opus 4.8 --- license.txt => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename license.txt => LICENSE (100%) diff --git a/license.txt b/LICENSE similarity index 100% rename from license.txt rename to LICENSE From e9757a76e7dae41683fe6670487764293d78760c Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 16:01:14 +0300 Subject: [PATCH 04/11] Docs: Fix ceiling description and clarify wording in decompression doc Corrects the primer, which said the overall ceiling is the shallowest of the 16 compartments; it is the deepest (most restrictive) one, matching getCeiling. Also clarifies the per-compartment ceiling wording and the CNS rate description. Co-Authored-By: Claude Opus 4.8 --- docs/DECOMPRESSION.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/DECOMPRESSION.md b/docs/DECOMPRESSION.md index 6fa6da1..8ad4a20 100644 --- a/docs/DECOMPRESSION.md +++ b/docs/DECOMPRESSION.md @@ -48,9 +48,10 @@ planner works out how much inert gas your body has taken on, and how slowly you **The Bühlmann ZHL-16 model** is the standard way to estimate this. It models the body as 16 theoretical **tissue compartments**, each on-gassing and off-gassing at a different speed (a different "half-time"). Fast compartments load and unload quickly (think blood), slow ones take -hours (think bone and fat). At any moment, each compartment has a tolerated ceiling: the shallowest -depth (lowest pressure) it can be exposed to without exceeding its limit. The overall **ceiling** is -the shallowest of all 16. You may not ascend above it. +hours (think bone and fat). At any moment, each compartment has a ceiling: the shallowest depth (the +lowest pressure) it can be exposed to without exceeding its limit. The overall **ceiling** is the +deepest of those 16, because the diver has to obey the most restrictive compartment. You may not +ascend above it. **Gradient factors** make the model more conservative. The raw Bühlmann limits (the "M-values") represent the most a compartment can supposedly tolerate. Many divers do not want to ride right up @@ -282,9 +283,10 @@ The ceiling is where decompression theory becomes a number. There are two relate ### The raw per-compartment ceiling -`TissueCompartment.calculateCeiling(gf)` returns the shallowest pressure a single compartment -tolerates, for a given gradient factor. Because both nitrogen and helium are present, their `a` and -`b` coefficients are first combined, weighted by their partial pressures: +`TissueCompartment.calculateCeiling(gf)` returns the ceiling for a single compartment (the shallowest +depth, which is the lowest ambient pressure, it can tolerate) for a given gradient factor. Because +both nitrogen and helium are present, their `a` and `b` coefficients are first combined, weighted by +their partial pressures: ```kotlin val a = ((parameters.n2ValueA * this.pNitrogen) + (parameters.heValueA * this.pHelium)) / (this.pTotal) @@ -413,7 +415,7 @@ ambient pressure at shallow depths. ### CNS CNS (central nervous system) toxicity is accumulated per segment, using the average pressure of the -segment. Below a ppO2 of 0.5 bar it contributes nothing. Above that, the per-minute rate is an +segment. Below a ppO2 of 0.5 bar it contributes nothing. Above that, the rate comes from an exponential fit to the NOAA exposure table, with two line segments (one up to ppO2 1.5, one above): ```kotlin From e84bad755933811972c8b00b7ce68bde73c12561 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 16:30:05 +0300 Subject: [PATCH 05/11] Docs: Correct architecture details and tighten wording after review Architecture fixes verified against source: - domain is not pure commonMain; it has jvmMain/iosMain Polyfills (UUID, number formatting) via expect/actual, and targets JVM (used by Android) + iOS. - Most code is under org.neotech.app.abysner, but the desktop and iOS entry files sit in the default package; build ids use nl.neotech.app.abysner. - iOS and desktop create the DI graph in a top-level property, not lazily or inside main(); add the androidApp -> data edge to the module graph. - README: domain has no UI deps and is almost entirely shared Kotlin (it does depend on multiplatform libraries), rather than no Android/iOS deps. Decompression doc clarity: state that only the nitrogen a coefficients differ across ZHL-16 versions (half-times, b, and all helium values are identical), and reword the gradient-factor direction (smaller gf raises the ceiling). Co-Authored-By: Claude Opus 4.8 --- README.md | 4 ++-- docs/ARCHITECTURE.md | 20 +++++++++++++------- docs/DECOMPRESSION.md | 11 ++++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6dddaf0..6eb7fc1 100644 --- a/README.md +++ b/README.md @@ -979,8 +979,8 @@ direction only (`composeApp` -> `data` -> `domain`): | `composeApp` | The shared Compose Multiplatform UI: screens, view models, navigation, theming. | | `androidApp` | The Android application wrapper (the iOS wrapper lives in `iosApp`). | -The `domain` module has no Android, iOS, or UI dependencies, so the decompression math can be read, -tested, and verified in isolation. +The `domain` module has no UI dependencies and is almost entirely shared Kotlin code, so the +decompression math can be read, tested, and verified in isolation. For a full tour of the codebase (how it is divided, the design patterns used, where to find things, and how the UI talks to the decompression engine) see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3486a53..94e1513 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -28,9 +28,12 @@ app's data folder), KMP uses the `expect`/`actual` mechanism: `commonMain` decla function or class, and each platform source set provides the matching `actual` implementation. You will see this in the `data` module, where file access differs per platform. -The `domain` module is special: it only has `commonMain` (plus tests) and no platform code at all. -That is deliberate. All the decompression and planning logic is pure Kotlin, so it behaves -identically on every platform and can be tested without any device or simulator. +The `domain` module is almost entirely `commonMain`. Its only platform-specific code is a pair of +small `Polyfills` files in `jvmMain` and `iosMain` that provide helpers common Kotlin lacks +(generating a UUID, formatting numbers) through `expect`/`actual`. None of the decompression or +planning logic is platform-specific, so it is plain Kotlin that behaves identically everywhere and +can be tested without any device or simulator. (The module targets the JVM, which Android also uses, +and iOS; it has no separate Android target.) ## Modules @@ -48,6 +51,7 @@ graph TD domain["domain
(business logic + deco engine)"] androidApp --> composeApp + androidApp --> data iosApp --> composeApp composeApp --> data composeApp --> domain @@ -62,8 +66,10 @@ graph TD | `androidApp` | [`androidApp/`](../androidApp) | Android `Application` and `Activity` entry points. | | iOS app | [`iosApp/`](../iosApp) | SwiftUI wrapper that hosts the shared Compose UI. | -All Kotlin code lives under the reverse-domain package `org.neotech.app.abysner` (note: the app's -build identifier is `nl.neotech.app.abysner`, the Kotlin package uses `org`). +Most Kotlin code lives under the package `org.neotech.app.abysner`. The exceptions are the two +platform entry-point files (`main.kt` for desktop and `MainViewController.kt` for iOS), which sit in +the default package. Note also that the build identifiers (the Android `applicationId` and the iOS +bundle id) use `nl.neotech.app.abysner`, while the Kotlin packages use `org.neotech.app.abysner`. ### Where do I find...? @@ -132,8 +138,8 @@ Each platform creates the graph at startup and hands it to the shared `App` comp | Platform | Entry point | Creates the graph in | |----------|------------------------------------------------------------------------------|--------------------------------| | Android | [`MainActivity`](../androidApp/src/main/kotlin/org/neotech/app/abysner/MainActivity.kt) / [`AbysnerApplication`](../androidApp/src/main/kotlin/org/neotech/app/abysner/AbysnerApplication.kt) | `AbysnerApplication.onCreate()` | -| iOS | [`MainViewController`](../composeApp/src/iosMain/kotlin/MainViewController.kt) | lazily, in `iosMain` | -| Desktop | [`main.kt`](../composeApp/src/jvmMain/kotlin/main.kt) | `main()` | +| iOS | [`MainViewController`](../composeApp/src/iosMain/kotlin/MainViewController.kt) | a top-level property (`iosMain`) | +| Desktop | [`main.kt`](../composeApp/src/jvmMain/kotlin/main.kt) | a top-level property (`jvmMain`) | ## Persistence diff --git a/docs/DECOMPRESSION.md b/docs/DECOMPRESSION.md index 8ad4a20..9259155 100644 --- a/docs/DECOMPRESSION.md +++ b/docs/DECOMPRESSION.md @@ -129,9 +129,9 @@ data class CompartmentParameters( The nitrogen half-times run from 5 minutes (the fastest compartment) to 635 minutes (the slowest). There are three versions of the table, `ZH16A`, `ZH16B`, and `ZH16C`, selected by the -`algorithm` setting. As the comment in the source notes, the N2 half-times and the `b` coefficients -are identical across all three versions; **only the nitrogen `a` coefficients differ**. ZHL-16C is -the most conservative of the three and is the default. +`algorithm` setting. As the comment in the source notes, **across the three versions only the +nitrogen `a` coefficients differ**. The half-times, the `b` coefficients, and all of the helium +values are identical. ZHL-16C is the most conservative of the three and is the default. The full tables (all 16 rows for each version, with the helium values too) are the canonical copy in `Buhlmann.kt`, in the `ZH16A_COMPARTMENTS`, `ZH16B_COMPARTMENTS`, and `ZH16C_COMPARTMENTS` lists. @@ -295,8 +295,9 @@ val b = ((parameters.n2ValueB * this.pNitrogen) + (parameters.heValueB * this.pH val ceiling = (this.pTotal - (a * gf)) / ((gf / b) + 1.0 - gf) ``` -With `gf = 1.0` this is the raw Bühlmann M-value limit. A smaller `gf` pulls the tolerated pressure -deeper (more conservative). The result is clamped so it never goes above the surface pressure. +With `gf = 1.0` this is the raw Bühlmann M-value limit. A smaller `gf` raises the ceiling (forces a +deeper stop), which is more conservative. The result is clamped so it never goes above the surface +pressure. ### Applying gradient factors across the dive From 936b2beffa47eef8dee0b7595ffc6dbeb5f3b41c Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 16:44:15 +0300 Subject: [PATCH 06/11] Docs: Tighten decompression wording for clarity after deep review Standardizes on 'deepest ceiling' instead of the ambiguous 'lowest ceiling' phrasing for the gfLow anchor, and smooths the per-compartment ceiling sentence. No behavioral claims changed; verified against source, the test suite, and independent computation of the water-vapour value and ceiling formula. Co-Authored-By: Claude Opus 4.8 --- docs/DECOMPRESSION.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/DECOMPRESSION.md b/docs/DECOMPRESSION.md index 9259155..1ae25bc 100644 --- a/docs/DECOMPRESSION.md +++ b/docs/DECOMPRESSION.md @@ -283,8 +283,8 @@ The ceiling is where decompression theory becomes a number. There are two relate ### The raw per-compartment ceiling -`TissueCompartment.calculateCeiling(gf)` returns the ceiling for a single compartment (the shallowest -depth, which is the lowest ambient pressure, it can tolerate) for a given gradient factor. Because +`TissueCompartment.calculateCeiling(gf)` returns the ceiling for a single compartment, that is, the +shallowest depth (the lowest ambient pressure) it can tolerate, for a given gradient factor. Because both nitrogen and helium are present, their `a` and `b` coefficients are first combined, weighted by their partial pressures: @@ -306,13 +306,13 @@ ceiling and `gfHigh` at the surface. This is `toleratedInertGasPressure()`, the of math in the engine. It: 1. Computes the tolerated inert gas pressure at the surface using `gfHigh`. -2. Computes the tolerated inert gas pressure at the lowest ceiling reached so far using `gfLow`. +2. Computes the tolerated inert gas pressure at the deepest ceiling reached so far using `gfLow`. 3. Treats those as two points on a line, solves for that line's slope and intercept, and inverts it to get the tolerated ambient pressure for the compartment's current loading. -The "lowest ceiling reached so far" is tracked across the whole dive in the `Buhlmann.lowestCeiling` +The deepest ceiling encountered so far is tracked across the whole dive in the `Buhlmann.lowestCeiling` field, and updated in `getMinimumToleratedAmbientPressure()`. That history matters: the gradient -factor line is anchored to the deepest stop the diver actually needed, not just the current depth. +factor line is anchored to the deepest ceiling reached during the dive, not just the current depth. The source has an extended comment deriving the slope and intercept, along with the references it was built from (Subsurface, the OSTC gradient factor document, dive-tech's M-values paper, and DecoTengu). From 4c6a5d440d9e52d6e9cb9da03c51c924fbd810a6 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 16:47:14 +0300 Subject: [PATCH 07/11] Docs: Fix build prerequisites, JDK 21 is required to run Gradle Verified by running the build: with the Metro plugin applied, Gradle must run on Java 21+ or configuration fails (Could not resolve metro:gradle-plugin, requires JVM 21). The previous text wrongly implied the launching JDK did not matter because toolchains would provision it; daemon-toolchain auto-provisioning did not happen on a JDK-17-only machine. Also drop the inaccurate claim that the domain tests need no Android SDK, since a normal gradlew run configures the androidApp module too. Co-Authored-By: Claude Opus 4.8 --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6eb7fc1..ff47565 100644 --- a/README.md +++ b/README.md @@ -920,8 +920,8 @@ Almost all of the code is shared, so most work happens once and runs everywhere. **Prerequisites:** -- A JDK to run Gradle. The build uses Gradle toolchains to provision JDK 21 (Temurin) automatically, - so the exact JDK you launch Gradle with is not critical, but JDK 21 is recommended. +- **JDK 21 or newer** to run Gradle. The Metro dependency-injection plugin requires the build to run + on Java 21+, so an older JDK fails at configuration. CI uses Temurin (Adoptium) 21. - The **Android SDK** (compile and target SDK 37, minimum SDK 26) for the Android app. Android Studio is the easiest way to get this. - **Xcode** for the iOS app (macOS only). @@ -936,7 +936,7 @@ Gradle version for you. git clone https://github.com/NeoTech-Software/abysner.git cd abysner -# Run the decompression engine tests (the domain module is pure Kotlin, no Android/iOS SDK needed) +# Run the decompression engine tests ./gradlew :domain:jvmTest # Run everything CI runs (JVM tests, coverage and screenshot validation) @@ -959,8 +959,9 @@ the shared `ComposeApp` framework as part of the Xcode build. **Common first-run issues:** -- *Gradle cannot find a JDK*: install JDK 21 (Temurin) and make sure `java -version` works, or point - Gradle at it. +- *Build fails with "requires at least JVM runtime version 21", or cannot resolve the Metro plugin*: + Gradle is running on too old a JDK. Install JDK 21 and make sure Gradle uses it (set `JAVA_HOME` to + your JDK 21, or select it in your IDE's Gradle settings). - *Android SDK not found*: open the project once in Android Studio, or create a `local.properties` file with `sdk.dir=/path/to/Android/sdk`. From 69f3ed3931675b6012bce83cd58908dbeaaeb678 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 17:24:13 +0300 Subject: [PATCH 08/11] Docs: Move reference plans to docs/REFERENCE_PLANS.md The reference plan comparisons took up most of the README. Move them into a dedicated doc and leave a short paragraph and link in the README. Repoint the cross-links in CONTRIBUTING.md and DECOMPRESSION.md to the new doc. Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 2 +- README.md | 707 +--------------------------------------- docs/DECOMPRESSION.md | 4 +- docs/REFERENCE_PLANS.md | 705 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 713 insertions(+), 705 deletions(-) create mode 100644 docs/REFERENCE_PLANS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4efea1..5786718 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ reproduce it exactly: - What you expected, and what Abysner produced. If you can, compare against another planner and say which one and which version. -The reference plan tables in the [readme](README.md#compared-to-other-planners) are a good template +The reference plan tables in [docs/REFERENCE_PLANS.md](docs/REFERENCE_PLANS.md) are a good template for how to present a plan clearly. The more your report looks like those, the faster it can be checked. diff --git a/README.md b/README.md index ff47565..dad3c8d 100644 --- a/README.md +++ b/README.md @@ -111,708 +111,11 @@ setting to account for the stress of a bailout scenario as they see fit. # Compared to other planners -How do Abysner dive plans compare to dive plans created by other dive planners? Below are some -reference plans. If you want to recreate a reference plan, these are the settings used by all of -them, some settings (Gradient factors, Salinity, Altitude and Last-deco stop) are specific for each -plan, see the plan specific tables for those. - - -| **Setting** | **Value** | -|--------------------|------------------| -| Ascent | 5 m/min | -| Descent | 5 m/min | -| Algorithm | Bühlmann ZHL-16C | -| Deco PPO2 | 1.6 | -| Bottom/travel PPO2 | 1.4 | -| END | 30 meter | -| O2 Narcotic | true | - -## Reference plan 1 -**20 meter, 20 minutes, single-gas (21/0)** - -| GF | Salinity | Altitude | Last-deco stop | -|-------|----------|----------|----------------| -| 30/70 | Fresh | 0 meters | 3 meter | - -
-Abysner - -| Depth | Duration | Runtime | Gas | -|-------|----------|---------|------| -| 20m | 4min | 4min | 21/0 | -| 20m | 16min | 20min | 21/0 | -| 0m | 4min | 24min | 21/0 | -**CNS**: 3% -**OTU**: 6 -
- -
-Subsurface - -| Depth | Duration | Runtime | Gas | -|-------|----------|---------|------| -| 20m | 4min | 4min | 21/0 | -| 20m | 16min | 20min | 21/0 | -| 0m | 4min | 24min | 21/0 | -**CNS**: 3% -**OTU**: 6 -*Subsurface (6.0.5214-CICD-release)* -
- -
-DIVESOFT.APP - -| Depth | Duration | Runtime | Gas | -|-------|----------|---------|------| -| 20m | 4min | 4min | 21/0 | -| 20m | 16min | 20min | 21/0 | -| 0m | 4min | 24min | 21/0 | -**CNS**: 2% -**OTU**: 5 -*DIVESOFT.APP (Android 1.8.4)* -
- -## Reference plan 2 -**30 meter, 30 minutes, multi-gas** - -| GF | Salinity | Altitude | Last-deco stop | -|-------|----------|----------|----------------| -| 30/70 | Salt | 0 meters | 6 meter | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 21m | 2min | 32min | 21/0 | -| - | 21m | 1min | 33min | 50/0 | -| ➚ | 9m | 3min | 36min | 50/0 | -| ⏹ | 9m | 1min | 37min | 50/0 | -| ⏹ | 6m | 11min | 48min | 50/0 | -| ➚ | 0m | 2min | 50min | 50/0 | -**CNS**: 12% -**OTU**: 34 -
- -
-Subsurface - -> **Observations:** -> - Subsurface merges the ascent from 21 meter to 9 meter into the 9 meter stop, while Abysner shows -> the ascent as a separate segment followed by the stop. -> - Subsurface rounds the final ascent (6 to 0 meter) down to 1 minute while the ascent actually -> takes longer at 5 meter per min. Abysner uses 2 min, adjusting the ascent slope to fit exactly. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 21m | 2min | 32min | 21/0 | -| - | 21m | 1min | 33min | 50/0 | -| ⏹ | 9m | 3min | 36min | 50/0 | -| ⏹ | 6m | 11min | 47min | 50/0 | -| ➚ | 0m | 1min | 48min | 50/0 | -**CNS**: 13% -**OTU**: 34 -*Subsurface (6.0.5214-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). -> -> DIVESOFT.APP does not appear to include a gas switch time. With gas switch time set to zero -> Abysner produces the same total runtime (50min), though the individual stop distributions differ -> ever so slightly. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 21m | 2min | 32min | 21/0 | -| ➚ | 9m | 2min | 34min | 50/0 | -| ⏹ | 9m | 1min | 35min | 50/0 | -| ⏹ | 6m | 13min | 48min | 50/0 | -| ➚ | 0m | 2min | 50min | 50/0 | -**CNS**: 11% -**OTU**: 32 -*DIVESOFT.APP (Android 1.8.4)* -
- - -## Reference plan 3 -**45 meter, 15 minutes, multi-gas, trimix** - -| GF | Salinity | Altitude | Last-deco stop | -|-------|----------|----------|----------------| -| 30/70 | Salt | 0 meters | 3 meter | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 45m | 9min | 9min | 21/35 | -| ➙ | 45m | 6min | 15min | 21/35 | -| ➚ | 21m | 5min | 20min | 21/35 | -| - | 21m | 1min | 21min | 50/0 | -| ➚ | 6m | 3min | 24min | 50/0 | -| ⏹ | 6m | 2min | 26min | 50/0 | -| ⏹ | 3m | 6min | 32min | 50/0 | -| ➚ | 0m | 1min | 33min | 50/0 | -**CNS**: 9% -**OTU**: 24 -
- -
-Subsurface - -> **Observations:** -> Subsurface merges the ascent from 21 meter to 6 meter into the 6 meter stop, while Abysner shows -> the ascent as a separate segment followed by the stop. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 45m | 9min | 9min | 21/35 | -| ➙ | 45m | 6min | 15min | 21/35 | -| ➚ | 21m | 5min | 20min | 21/35 | -| - | 21m | 1min | 21min | 50/0 | -| ⏹ | 6m | 5min | 26min | 50/0 | -| ⏹ | 3m | 5min | 31min | 50/0 | -| ➚ | 0m | 1min | 32min | 50/0 | -**CNS**: 10% -**OTU**: 26 -*Subsurface (6.0.5214-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). -> -> - DIVESOFT.APP does not appear to include a gas switch duration. With the gas switch duration set -> to one minute Abysner produces the same total runtime of 33 minutes. The individual stop -> distributions differ ever so slightly. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 45m | 9min | 9min | 21/35 | -| ➙ | 45m | 6min | 15min | 21/35 | -| ➚ | 21m | 5min | 20min | 21/35 | -| ➚ | 9m | 2min | 22min | 50/0 | -| ⏹ | 9m | 1min | 23min | 50/0 | -| ⏹ | 6m | 2min | 25min | 50/0 | -| ⏹ | 3m | 7min | 32min | 50/0 | -| ➚ | 0m | 1min | 33min | 50/0 | -**CNS**: 9% -**OTU**: 23 -*DIVESOFT.APP (Android 1.8.4)* -
- - -## Reference plan 4 -**60 meter, 20 minutes, multi-gas, trimix, altitude** - -| GF | Salinity | Altitude | Last-deco stop | -|-------|----------|-------------|----------------| -| 40/85 | Fresh | 1000 meters | 3 meter | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 60m | 12min | 12min | 18/45 | -| ➙ | 60m | 8min | 20min | 18/45 | -| ➚ | 21m | 8min | 28min | 18/45 | -| - | 21m | 1min | 29min | 50/0 | -| ➚ | 15m | 2min | 31min | 50/0 | -| ⏹ | 15m | 1min | 32min | 50/0 | -| ⏹ | 12m | 2min | 34min | 50/0 | -| ⏹ | 9m | 4min | 38min | 50/0 | -| ⏹ | 6m | 7min | 45min | 50/0 | -| ⏹ | 3m | 13min | 58min | 50/0 | -| ➚ | 0m | 1min | 59min | 50/0 | -**CNS**: 15% -**OTU**: 41 -
- -
-Subsurface - -> **Observations:** -> - Atmospheric pressure was set to 900 mbar directly in Subsurface to match Abysner's barometric -> formula result for 1000 meter altitude, eliminating it as a variable in the comparison. -> - The remaining stop-time differences (12, 6 and 3 meter) seem to be algorithmic. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 60m | 12min | 12min | 18/45 | -| ➙ | 60m | 8min | 20min | 18/45 | -| ➚ | 21m | 8min | 28min | 18/45 | -| - | 21m | 1min | 29min | 50/0 | -| ⏹ | 15m | 3min | 32min | 50/0 | -| ⏹ | 12m | 3min | 35min | 50/0 | -| ⏹ | 9m | 4min | 39min | 50/0 | -| ⏹ | 6m | 8min | 47min | 50/0 | -| ⏹ | 3m | 15min | 62min | 50/0 | -| ➚ | 0m | 1min | 63min | 50/0 | -**CNS**: 17% -**OTU**: 46 -*Subsurface (6.0.5576-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not support setting an altitude. This plan has been based on 0 meters instead -> of 1000 meters used in the other planners. -> -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 60m | 12min | 12min | 18/45 | -| ➙ | 60m | 8min | 20min | 18/45 | -| ➚ | 21m | 8min | 28min | 18/45 | -| ➚ | 18m | 0min | 28min | 18/45 | -| ⏹ | 18m | 1min | 29min | 50/0 | -| ⏹ | 15m | 3min | 32min | 50/0 | -| ⏹ | 12m | 2min | 34min | 50/0 | -| ⏹ | 9m | 5min | 39min | 50/0 | -| ⏹ | 6m | 8min | 47min | 50/0 | -| ⏹ | 3m | 17min | 64min | 50/0 | -| ➚ | 0m | 1min | 65min | 50/0 | -**CNS**: 17% -**OTU**: 47 -*DIVESOFT.APP (Android 1.8.4)* -
- - -## Reference plan 5 -**40 meter max, multi-level (cave-profile) dive, single-gas trimix** - -*Note: this is not meant to be a realistic scenario.* - -
-Plan details - -``` -In: -- Descent: 40 meter, 8 minutes -- Flat: 40 meter, 2 minutes -- Ascent: 30 meter, 2 minutes -- Flat: 30 meter, 8 minutes -Out: -- Flat: 30 meter, 8 minutes -- Descent: 40 meter, 2 minutes -- Flat: 40 meter, 2 minutes -- Ascent: at 5 m/min max (as planned by planner) -``` -
- -| GF | Salinity | Altitude | Last-deco stop | -|-------|----------|----------|----------------| -| 50/80 | Fresh | 0 meters | 3 meter | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 40m | 8min | 8min | 21/20 | -| ➙ | 40m | 2min | 10min | 21/20 | -| ➚ | 30m | 2min | 12min | 21/20 | -| ➙ | 30m | 16min | 28min | 21/20 | -| ➘ | 40m | 2min | 30min | 21/20 | -| ➙ | 40m | 2min | 32min | 21/20 | -| ➚ | 9m | 7min | 39min | 21/20 | -| ⏹ | 9m | 3min | 42min | 21/20 | -| ⏹ | 6m | 7min | 49min | 21/20 | -| ⏹ | 3m | 17min | 66min | 21/20 | -| ➚ | 0m | 1min | 67min | 21/20 | -**CNS**: 8% -**OTU**: 26 -
- -
-Subsurface - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 40m | 8min | 8min | 21/20 | -| ➙ | 40m | 2min | 10min | 21/20 | -| ➚ | 30m | 2min | 12min | 21/20 | -| ➙ | 30m | 16min | 28min | 21/20 | -| ➘ | 40m | 2min | 30min | 21/20 | -| ➙ | 40m | 2min | 32min | 21/20 | -| ➚ | 9m | 7min | 39min | 21/20 | -| ⏹ | 9m | 3min | 42min | 21/20 | -| ⏹ | 6m | 8min | 50min | 21/20 | -| ⏹ | 3m | 16min | 66min | 21/20 | -| ➚ | 0m | 1min | 67min | 21/20 | -**CNS**: 9% -**OTU**: 25 -*Subsurface (6.0.5214-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|-------| -| ➘ | 40m | 8min | 8min | 21/20 | -| ➙ | 40m | 2min | 10min | 21/20 | -| ➚ | 30m | 2min | 12min | 21/20 | -| ➙ | 30m | 16min | 28min | 21/20 | -| ➘ | 40m | 2min | 30min | 21/20 | -| ➙ | 40m | 2min | 32min | 21/20 | -| ➚ | 9m | 6min | 38min | 21/20 | -| ⏹ | 9m | 3min | 41min | 21/20 | -| ⏹ | 6m | 7min | 48min | 21/20 | -| ⏹ | 3m | 16min | 64min | 21/20 | -| ➚ | 0m | 1min | 65min | 21/20 | -**CNS**: 8% -**OTU**: 24 -*DIVESOFT.APP (Android 1.8.4)* -
- - -## Reference plan 6 (CCR) -**30 meter, 30 minutes, CCR with air diluent, setpoints 0.7 low / 1.2 high** - -| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | -|-------|----------|----------|----------------|--------|---------| -| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| ➚ | 3m | 6min | 36min | 21/0 | CCR (SP 1.2) | -| ⏹ | 3m | 2min | 38min | 21/0 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 39min | 21/0 | CCR (SP 1.2) | -**CNS**: 17% -**OTU**: 47 -
- -
-Subsurface - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| ➚ | 3m | 6min | 36min | 21/0 | CCR (SP 1.2) | -| - | 3m | 2min | 38min | 21/0 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 39min | 21/0 | CCR (SP 1.2) | -**CNS**: 16% -**OTU**: 46 -*Subsurface (6.0.5576-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). -> -> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for -> descents. It is unclear whether this is just a display choice (labeling the descent with the -> bottom setpoint) or whether the low setpoint is not applied during descent. - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 1.2) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| ➚ | 6m | 5min | 35min | 21/0 | CCR (SP 1.2) | -| ⏹ | 6m | 1min | 36min | 21/0 | CCR (SP 1.2) | -| ⏹ | 3m | 3min | 39min | 21/0 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 40min | 21/0 | CCR (SP 1.2) | -**CNS**: 17% -**OTU**: 47 -*DIVESOFT.APP (Android 2.5.1)* -
- - -## Reference plan 7 (CCR bailout) -**30 meter, 30 minutes, CCR with air diluent (setpoints 0.7 low / 1.2 high), bailout to open -circuit** - -Same configuration as reference plan 6, but the diver bails out to open circuit at the end of -the bottom section. - -| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | -|-------|----------|----------|----------------|--------|---------| -| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| - | 30m | 1min | 31min | 21/0 | Bailout to OC | -| ➚ | 9m | 5min | 36min | 21/0 | OC | -| ⏹ | 9m | 1min | 37min | 21/0 | OC | -| ⏹ | 6m | 5min | 42min | 21/0 | OC | -| ⏹ | 3m | 9min | 51min | 21/0 | OC | -| ➚ | 0m | 1min | 52min | 21/0 | OC | -**CNS**: 13% -**OTU**: 37 -
- -
-Subsurface - -> **Observations:** -> Subsurface produces a 1 minute shorter runtime (51 vs 52 minutes) with slightly different stop -> distributions. - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| - | 30m | 1min | 31min | 21/0 | Bailout to OC | -| ➚ | 9m | 4min | 35min | 21/0 | OC | -| ⏹ | 9m | 2min | 37min | 21/0 | OC | -| ⏹ | 6m | 5min | 42min | 21/0 | OC | -| ⏹ | 3m | 8min | 50min | 21/0 | OC | -| ➚ | 0m | 1min | 51min | 21/0 | OC | -**CNS**: 13% -**OTU**: 37 -*Subsurface (6.0.5576-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). -> -> - DIVESOFT.APP produces a significantly longer runtime (60 min vs 52/51 min for Abysner and -> Subsurface). This is a much larger difference than seen in the OC reference plans. It is unclear -> why this is, but the DIVESOFT.APP versions between OC and CCR plans differ, could that be a -> cause? Or is it a difference in how the bailout is handled? -> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for -> descents. It is unclear whether this is just a display choice (labeling the descent with the -> bottom setpoint) or whether the low setpoint is not applied during descent. - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|------|---------------| -| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 1.2) | -| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | -| - | 30m | 3min | 33min | 21/0 | Bailout to OC | -| ➚ | 9m | 4min | 37min | 21/0 | OC | -| ⏹ | 9m | 3min | 40min | 21/0 | OC | -| ⏹ | 6m | 5min | 45min | 21/0 | OC | -| ⏹ | 3m | 14min | 59min | 21/0 | OC | -| ➚ | 0m | 1min | 60min | 21/0 | OC | -**CNS**: 12% -**OTU**: 35 -*DIVESOFT.APP (Android 2.5.1)* -
- - -## Reference plan 8 (CCR) -**60 meter, 20 minutes, CCR with 10/70 trimix diluent, setpoints 0.7 low / 1.2 high** - -| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | -|-------|----------|----------|----------------|--------|---------| -| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | - -
-Abysner - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|-------|--------------| -| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 0.7) | -| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | -| ➚ | 24m | 8min | 28min | 10/70 | CCR (SP 1.2) | -| ⏹ | 24m | 1min | 29min | 10/70 | CCR (SP 1.2) | -| ⏹ | 21m | 2min | 31min | 10/70 | CCR (SP 1.2) | -| ⏹ | 18m | 2min | 33min | 10/70 | CCR (SP 1.2) | -| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | -| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | -| ⏹ | 9m | 6min | 46min | 10/70 | CCR (SP 1.2) | -| ⏹ | 6m | 9min | 55min | 10/70 | CCR (SP 1.2) | -| ⏹ | 3m | 14min | 69min | 10/70 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 70min | 10/70 | CCR (SP 1.2) | -**CNS**: 29% -**OTU**: 82 -
- -
-Subsurface - -> **Observations:** -> Subsurface does not require a 24 meter stop, while Abysner barely does (1 minute). This is likely -> due to minor algorithmic differences in tissue loading precision. - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|-------|--------------| -| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 0.7) | -| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | -| ➚ | 24m | 8min | 28min | 10/70 | CCR (SP 1.2) | -| ⏹ | 21m | 2min | 30min | 10/70 | CCR (SP 1.2) | -| ⏹ | 18m | 3min | 33min | 10/70 | CCR (SP 1.2) | -| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | -| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | -| ⏹ | 9m | 6min | 46min | 10/70 | CCR (SP 1.2) | -| ⏹ | 6m | 9min | 55min | 10/70 | CCR (SP 1.2) | -| ⏹ | 3m | 13min | 68min | 10/70 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 69min | 10/70 | CCR (SP 1.2) | -**CNS**: 29% -**OTU**: 80 -*Subsurface (6.0.5576-CICD-release)* -
- -
-DIVESOFT.APP - -> **Observations:** -> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time -> to the next stop in the total stop duration. This means the displayed stop time does not always -> match up to the runtime. Duration values below were derived by subtracting runtimes and the -> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). -> -> - DIVESOFT.APP produces a longer runtime (74 min vs 70 min for Abysner and 69 min for Subsurface). -> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for -> descents. It is unclear whether this is just a display choice (labeling the descent with the -> bottom setpoint) or whether the low setpoint is not applied during descent. - -| | Depth | Duration | Runtime | Gas | Mode | -|---|-------|----------|---------|-------|--------------| -| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 1.2) | -| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | -| ➚ | 24m | 7min | 27min | 10/70 | CCR (SP 1.2) | -| ⏹ | 24m | 1min | 28min | 10/70 | CCR (SP 1.2) | -| ⏹ | 21m | 2min | 30min | 10/70 | CCR (SP 1.2) | -| ⏹ | 18m | 3min | 33min | 10/70 | CCR (SP 1.2) | -| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | -| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | -| ⏹ | 9m | 7min | 47min | 10/70 | CCR (SP 1.2) | -| ⏹ | 6m | 9min | 56min | 10/70 | CCR (SP 1.2) | -| ⏹ | 3m | 17min | 73min | 10/70 | CCR (SP 1.2) | -| ➚ | 0m | 1min | 74min | 10/70 | CCR (SP 1.2) | -**CNS**: 31% -**OTU**: 85 -*DIVESOFT.APP (Android 2.5.1)* -
- - -## Reference plan 9 (surface interval) -**30 meter, 30 minutes, repeated after 30-minute surface interval** - -Both dives are identical: 30 meters for 30 minutes on air, with a 30-minute surface interval between -them. The second dive should produce noticeably longer decompression due to residual tissue loading -from the first dive. - -| GF | Salinity | Altitude | Last-deco stop | Surface interval | -|-------|----------|----------|----------------|------------------| -| 85/85 | Fresh | 0 meters | 3 meter | 30 minutes | - -### Dive 1 - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 3m | 6min | 36min | 21/0 | -| ⏹ | 3m | 8min | 44min | 21/0 | -| ➚ | 0m | 1min | 45min | 21/0 | -**CNS**: 7% -**OTU**: 20 -
- -
-Subsurface - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 3m | 6min | 36min | 21/0 | -| ⏹ | 3m | 8min | 44min | 21/0 | -| ➚ | 0m | 1min | 45min | 21/0 | -**CNS**: 7% -**OTU**: 19 -*Subsurface (6.0.5576-CICD-release)* -
- -### Dive 2 (after 30-minute surface interval) - -
-Abysner - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 6m | 5min | 35min | 21/0 | -| ⏹ | 6m | 1min | 36min | 21/0 | -| ➚ | 3m | 1min | 37min | 21/0 | -| ⏹ | 3m | 27min | 64min | 21/0 | -| ➚ | 0m | 1min | 65min | 21/0 | -**CNS**: 7% -**OTU**: 20 -
- -
-Subsurface - -> **Observations:** -> Subsurface produces a 1 minute longer runtime (66 vs 65 minutes) with slightly different stop -> distributions. The stop structure is the same: both planners require a 6 meter stop on the -> repetitive dive that was not needed on the first dive. - -| | Depth | Duration | Runtime | Gas | -|---|-------|----------|---------|------| -| ➘ | 30m | 6min | 6min | 21/0 | -| ➙ | 30m | 24min | 30min | 21/0 | -| ➚ | 6m | 5min | 35min | 21/0 | -| ⏹ | 6m | 2min | 37min | 21/0 | -| ⏹ | 3m | 28min | 65min | 21/0 | -| ➚ | 0m | 1min | 66min | 21/0 | -**CNS**: 12% -**OTU**: 19 -*Subsurface (6.0.5576-CICD-release)* -
- +Abysner's dive plans are validated against other planners across a range of scenarios: open-circuit, +closed-circuit (CCR) and bailout, multi-level, multi-dive with surface intervals, trimix, and +altitude. The full set of reference plans, with side-by-side comparisons against Subsurface and +DIVESOFT.APP and notes on where and why they differ, lives in +[docs/REFERENCE_PLANS.md](docs/REFERENCE_PLANS.md). # FAQ diff --git a/docs/DECOMPRESSION.md b/docs/DECOMPRESSION.md index 1ae25bc..03d609c 100644 --- a/docs/DECOMPRESSION.md +++ b/docs/DECOMPRESSION.md @@ -511,8 +511,8 @@ The result is three segments, matching the test and the readme's reference plan | Ascent | 20 -> 0 m | 4 min | 24 min | with `totalCns = 2.731` and `totalOtu = 5.443` (both asserted to three decimals in the test). For a -plan that does involve stops and gas switches, see reference plans 2 and onward in the -[readme](../README.md#compared-to-other-planners), all of which are also covered by `DivePlannerTest`. +plan that does involve stops and gas switches, see reference plans 2 and onward in +[REFERENCE_PLANS.md](REFERENCE_PLANS.md), all of which are also covered by `DivePlannerTest`. ## Design decisions and known divergences diff --git a/docs/REFERENCE_PLANS.md b/docs/REFERENCE_PLANS.md new file mode 100644 index 0000000..4f17224 --- /dev/null +++ b/docs/REFERENCE_PLANS.md @@ -0,0 +1,705 @@ +# Reference plans + +How do Abysner dive plans compare to dive plans created by other dive planners? Below are some +reference plans. If you want to recreate a reference plan, these are the settings used by all of +them, some settings (Gradient factors, Salinity, Altitude and Last-deco stop) are specific for each +plan, see the plan specific tables for those. + + +| **Setting** | **Value** | +|--------------------|------------------| +| Ascent | 5 m/min | +| Descent | 5 m/min | +| Algorithm | Bühlmann ZHL-16C | +| Deco PPO2 | 1.6 | +| Bottom/travel PPO2 | 1.4 | +| END | 30 meter | +| O2 Narcotic | true | + +## Reference plan 1 +**20 meter, 20 minutes, single-gas (21/0)** + +| GF | Salinity | Altitude | Last-deco stop | +|-------|----------|----------|----------------| +| 30/70 | Fresh | 0 meters | 3 meter | + +
+Abysner + +| Depth | Duration | Runtime | Gas | +|-------|----------|---------|------| +| 20m | 4min | 4min | 21/0 | +| 20m | 16min | 20min | 21/0 | +| 0m | 4min | 24min | 21/0 | +**CNS**: 3% +**OTU**: 6 +
+ +
+Subsurface + +| Depth | Duration | Runtime | Gas | +|-------|----------|---------|------| +| 20m | 4min | 4min | 21/0 | +| 20m | 16min | 20min | 21/0 | +| 0m | 4min | 24min | 21/0 | +**CNS**: 3% +**OTU**: 6 +*Subsurface (6.0.5214-CICD-release)* +
+ +
+DIVESOFT.APP + +| Depth | Duration | Runtime | Gas | +|-------|----------|---------|------| +| 20m | 4min | 4min | 21/0 | +| 20m | 16min | 20min | 21/0 | +| 0m | 4min | 24min | 21/0 | +**CNS**: 2% +**OTU**: 5 +*DIVESOFT.APP (Android 1.8.4)* +
+ +## Reference plan 2 +**30 meter, 30 minutes, multi-gas** + +| GF | Salinity | Altitude | Last-deco stop | +|-------|----------|----------|----------------| +| 30/70 | Salt | 0 meters | 6 meter | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 21m | 2min | 32min | 21/0 | +| - | 21m | 1min | 33min | 50/0 | +| ➚ | 9m | 3min | 36min | 50/0 | +| ⏹ | 9m | 1min | 37min | 50/0 | +| ⏹ | 6m | 11min | 48min | 50/0 | +| ➚ | 0m | 2min | 50min | 50/0 | +**CNS**: 12% +**OTU**: 34 +
+ +
+Subsurface + +> **Observations:** +> - Subsurface merges the ascent from 21 meter to 9 meter into the 9 meter stop, while Abysner shows +> the ascent as a separate segment followed by the stop. +> - Subsurface rounds the final ascent (6 to 0 meter) down to 1 minute while the ascent actually +> takes longer at 5 meter per min. Abysner uses 2 min, adjusting the ascent slope to fit exactly. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 21m | 2min | 32min | 21/0 | +| - | 21m | 1min | 33min | 50/0 | +| ⏹ | 9m | 3min | 36min | 50/0 | +| ⏹ | 6m | 11min | 47min | 50/0 | +| ➚ | 0m | 1min | 48min | 50/0 | +**CNS**: 13% +**OTU**: 34 +*Subsurface (6.0.5214-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). +> +> DIVESOFT.APP does not appear to include a gas switch time. With gas switch time set to zero +> Abysner produces the same total runtime (50min), though the individual stop distributions differ +> ever so slightly. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 21m | 2min | 32min | 21/0 | +| ➚ | 9m | 2min | 34min | 50/0 | +| ⏹ | 9m | 1min | 35min | 50/0 | +| ⏹ | 6m | 13min | 48min | 50/0 | +| ➚ | 0m | 2min | 50min | 50/0 | +**CNS**: 11% +**OTU**: 32 +*DIVESOFT.APP (Android 1.8.4)* +
+ + +## Reference plan 3 +**45 meter, 15 minutes, multi-gas, trimix** + +| GF | Salinity | Altitude | Last-deco stop | +|-------|----------|----------|----------------| +| 30/70 | Salt | 0 meters | 3 meter | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 45m | 9min | 9min | 21/35 | +| ➙ | 45m | 6min | 15min | 21/35 | +| ➚ | 21m | 5min | 20min | 21/35 | +| - | 21m | 1min | 21min | 50/0 | +| ➚ | 6m | 3min | 24min | 50/0 | +| ⏹ | 6m | 2min | 26min | 50/0 | +| ⏹ | 3m | 6min | 32min | 50/0 | +| ➚ | 0m | 1min | 33min | 50/0 | +**CNS**: 9% +**OTU**: 24 +
+ +
+Subsurface + +> **Observations:** +> Subsurface merges the ascent from 21 meter to 6 meter into the 6 meter stop, while Abysner shows +> the ascent as a separate segment followed by the stop. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 45m | 9min | 9min | 21/35 | +| ➙ | 45m | 6min | 15min | 21/35 | +| ➚ | 21m | 5min | 20min | 21/35 | +| - | 21m | 1min | 21min | 50/0 | +| ⏹ | 6m | 5min | 26min | 50/0 | +| ⏹ | 3m | 5min | 31min | 50/0 | +| ➚ | 0m | 1min | 32min | 50/0 | +**CNS**: 10% +**OTU**: 26 +*Subsurface (6.0.5214-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). +> +> - DIVESOFT.APP does not appear to include a gas switch duration. With the gas switch duration set +> to one minute Abysner produces the same total runtime of 33 minutes. The individual stop +> distributions differ ever so slightly. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 45m | 9min | 9min | 21/35 | +| ➙ | 45m | 6min | 15min | 21/35 | +| ➚ | 21m | 5min | 20min | 21/35 | +| ➚ | 9m | 2min | 22min | 50/0 | +| ⏹ | 9m | 1min | 23min | 50/0 | +| ⏹ | 6m | 2min | 25min | 50/0 | +| ⏹ | 3m | 7min | 32min | 50/0 | +| ➚ | 0m | 1min | 33min | 50/0 | +**CNS**: 9% +**OTU**: 23 +*DIVESOFT.APP (Android 1.8.4)* +
+ + +## Reference plan 4 +**60 meter, 20 minutes, multi-gas, trimix, altitude** + +| GF | Salinity | Altitude | Last-deco stop | +|-------|----------|-------------|----------------| +| 40/85 | Fresh | 1000 meters | 3 meter | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 60m | 12min | 12min | 18/45 | +| ➙ | 60m | 8min | 20min | 18/45 | +| ➚ | 21m | 8min | 28min | 18/45 | +| - | 21m | 1min | 29min | 50/0 | +| ➚ | 15m | 2min | 31min | 50/0 | +| ⏹ | 15m | 1min | 32min | 50/0 | +| ⏹ | 12m | 2min | 34min | 50/0 | +| ⏹ | 9m | 4min | 38min | 50/0 | +| ⏹ | 6m | 7min | 45min | 50/0 | +| ⏹ | 3m | 13min | 58min | 50/0 | +| ➚ | 0m | 1min | 59min | 50/0 | +**CNS**: 15% +**OTU**: 41 +
+ +
+Subsurface + +> **Observations:** +> - Atmospheric pressure was set to 900 mbar directly in Subsurface to match Abysner's barometric +> formula result for 1000 meter altitude, eliminating it as a variable in the comparison. +> - The remaining stop-time differences (12, 6 and 3 meter) seem to be algorithmic. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 60m | 12min | 12min | 18/45 | +| ➙ | 60m | 8min | 20min | 18/45 | +| ➚ | 21m | 8min | 28min | 18/45 | +| - | 21m | 1min | 29min | 50/0 | +| ⏹ | 15m | 3min | 32min | 50/0 | +| ⏹ | 12m | 3min | 35min | 50/0 | +| ⏹ | 9m | 4min | 39min | 50/0 | +| ⏹ | 6m | 8min | 47min | 50/0 | +| ⏹ | 3m | 15min | 62min | 50/0 | +| ➚ | 0m | 1min | 63min | 50/0 | +**CNS**: 17% +**OTU**: 46 +*Subsurface (6.0.5576-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not support setting an altitude. This plan has been based on 0 meters instead +> of 1000 meters used in the other planners. +> +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 60m | 12min | 12min | 18/45 | +| ➙ | 60m | 8min | 20min | 18/45 | +| ➚ | 21m | 8min | 28min | 18/45 | +| ➚ | 18m | 0min | 28min | 18/45 | +| ⏹ | 18m | 1min | 29min | 50/0 | +| ⏹ | 15m | 3min | 32min | 50/0 | +| ⏹ | 12m | 2min | 34min | 50/0 | +| ⏹ | 9m | 5min | 39min | 50/0 | +| ⏹ | 6m | 8min | 47min | 50/0 | +| ⏹ | 3m | 17min | 64min | 50/0 | +| ➚ | 0m | 1min | 65min | 50/0 | +**CNS**: 17% +**OTU**: 47 +*DIVESOFT.APP (Android 1.8.4)* +
+ + +## Reference plan 5 +**40 meter max, multi-level (cave-profile) dive, single-gas trimix** + +*Note: this is not meant to be a realistic scenario.* + +
+Plan details + +``` +In: +- Descent: 40 meter, 8 minutes +- Flat: 40 meter, 2 minutes +- Ascent: 30 meter, 2 minutes +- Flat: 30 meter, 8 minutes +Out: +- Flat: 30 meter, 8 minutes +- Descent: 40 meter, 2 minutes +- Flat: 40 meter, 2 minutes +- Ascent: at 5 m/min max (as planned by planner) +``` +
+ +| GF | Salinity | Altitude | Last-deco stop | +|-------|----------|----------|----------------| +| 50/80 | Fresh | 0 meters | 3 meter | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 40m | 8min | 8min | 21/20 | +| ➙ | 40m | 2min | 10min | 21/20 | +| ➚ | 30m | 2min | 12min | 21/20 | +| ➙ | 30m | 16min | 28min | 21/20 | +| ➘ | 40m | 2min | 30min | 21/20 | +| ➙ | 40m | 2min | 32min | 21/20 | +| ➚ | 9m | 7min | 39min | 21/20 | +| ⏹ | 9m | 3min | 42min | 21/20 | +| ⏹ | 6m | 7min | 49min | 21/20 | +| ⏹ | 3m | 17min | 66min | 21/20 | +| ➚ | 0m | 1min | 67min | 21/20 | +**CNS**: 8% +**OTU**: 26 +
+ +
+Subsurface + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 40m | 8min | 8min | 21/20 | +| ➙ | 40m | 2min | 10min | 21/20 | +| ➚ | 30m | 2min | 12min | 21/20 | +| ➙ | 30m | 16min | 28min | 21/20 | +| ➘ | 40m | 2min | 30min | 21/20 | +| ➙ | 40m | 2min | 32min | 21/20 | +| ➚ | 9m | 7min | 39min | 21/20 | +| ⏹ | 9m | 3min | 42min | 21/20 | +| ⏹ | 6m | 8min | 50min | 21/20 | +| ⏹ | 3m | 16min | 66min | 21/20 | +| ➚ | 0m | 1min | 67min | 21/20 | +**CNS**: 9% +**OTU**: 25 +*Subsurface (6.0.5214-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|-------| +| ➘ | 40m | 8min | 8min | 21/20 | +| ➙ | 40m | 2min | 10min | 21/20 | +| ➚ | 30m | 2min | 12min | 21/20 | +| ➙ | 30m | 16min | 28min | 21/20 | +| ➘ | 40m | 2min | 30min | 21/20 | +| ➙ | 40m | 2min | 32min | 21/20 | +| ➚ | 9m | 6min | 38min | 21/20 | +| ⏹ | 9m | 3min | 41min | 21/20 | +| ⏹ | 6m | 7min | 48min | 21/20 | +| ⏹ | 3m | 16min | 64min | 21/20 | +| ➚ | 0m | 1min | 65min | 21/20 | +**CNS**: 8% +**OTU**: 24 +*DIVESOFT.APP (Android 1.8.4)* +
+ + +## Reference plan 6 (CCR) +**30 meter, 30 minutes, CCR with air diluent, setpoints 0.7 low / 1.2 high** + +| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | +|-------|----------|----------|----------------|--------|---------| +| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| ➚ | 3m | 6min | 36min | 21/0 | CCR (SP 1.2) | +| ⏹ | 3m | 2min | 38min | 21/0 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 39min | 21/0 | CCR (SP 1.2) | +**CNS**: 17% +**OTU**: 47 +
+ +
+Subsurface + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| ➚ | 3m | 6min | 36min | 21/0 | CCR (SP 1.2) | +| - | 3m | 2min | 38min | 21/0 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 39min | 21/0 | CCR (SP 1.2) | +**CNS**: 16% +**OTU**: 46 +*Subsurface (6.0.5576-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). +> +> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for +> descents. It is unclear whether this is just a display choice (labeling the descent with the +> bottom setpoint) or whether the low setpoint is not applied during descent. + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 1.2) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| ➚ | 6m | 5min | 35min | 21/0 | CCR (SP 1.2) | +| ⏹ | 6m | 1min | 36min | 21/0 | CCR (SP 1.2) | +| ⏹ | 3m | 3min | 39min | 21/0 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 40min | 21/0 | CCR (SP 1.2) | +**CNS**: 17% +**OTU**: 47 +*DIVESOFT.APP (Android 2.5.1)* +
+ + +## Reference plan 7 (CCR bailout) +**30 meter, 30 minutes, CCR with air diluent (setpoints 0.7 low / 1.2 high), bailout to open +circuit** + +Same configuration as reference plan 6, but the diver bails out to open circuit at the end of +the bottom section. + +| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | +|-------|----------|----------|----------------|--------|---------| +| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| - | 30m | 1min | 31min | 21/0 | Bailout to OC | +| ➚ | 9m | 5min | 36min | 21/0 | OC | +| ⏹ | 9m | 1min | 37min | 21/0 | OC | +| ⏹ | 6m | 5min | 42min | 21/0 | OC | +| ⏹ | 3m | 9min | 51min | 21/0 | OC | +| ➚ | 0m | 1min | 52min | 21/0 | OC | +**CNS**: 13% +**OTU**: 37 +
+ +
+Subsurface + +> **Observations:** +> Subsurface produces a 1 minute shorter runtime (51 vs 52 minutes) with slightly different stop +> distributions. + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 0.7) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| - | 30m | 1min | 31min | 21/0 | Bailout to OC | +| ➚ | 9m | 4min | 35min | 21/0 | OC | +| ⏹ | 9m | 2min | 37min | 21/0 | OC | +| ⏹ | 6m | 5min | 42min | 21/0 | OC | +| ⏹ | 3m | 8min | 50min | 21/0 | OC | +| ➚ | 0m | 1min | 51min | 21/0 | OC | +**CNS**: 13% +**OTU**: 37 +*Subsurface (6.0.5576-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). +> +> - DIVESOFT.APP produces a significantly longer runtime (60 min vs 52/51 min for Abysner and +> Subsurface). This is a much larger difference than seen in the OC reference plans. It is unclear +> why this is, but the DIVESOFT.APP versions between OC and CCR plans differ, could that be a +> cause? Or is it a difference in how the bailout is handled? +> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for +> descents. It is unclear whether this is just a display choice (labeling the descent with the +> bottom setpoint) or whether the low setpoint is not applied during descent. + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|------|---------------| +| ➘ | 30m | 6min | 6min | 21/0 | CCR (SP 1.2) | +| ➙ | 30m | 24min | 30min | 21/0 | CCR (SP 1.2) | +| - | 30m | 3min | 33min | 21/0 | Bailout to OC | +| ➚ | 9m | 4min | 37min | 21/0 | OC | +| ⏹ | 9m | 3min | 40min | 21/0 | OC | +| ⏹ | 6m | 5min | 45min | 21/0 | OC | +| ⏹ | 3m | 14min | 59min | 21/0 | OC | +| ➚ | 0m | 1min | 60min | 21/0 | OC | +**CNS**: 12% +**OTU**: 35 +*DIVESOFT.APP (Android 2.5.1)* +
+ + +## Reference plan 8 (CCR) +**60 meter, 20 minutes, CCR with 10/70 trimix diluent, setpoints 0.7 low / 1.2 high** + +| GF | Salinity | Altitude | Last-deco stop | Low SP | High SP | +|-------|----------|----------|----------------|--------|---------| +| 30/70 | Salt | 0 meters | 3 meter | 0.7 | 1.2 | + +
+Abysner + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|-------|--------------| +| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 0.7) | +| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | +| ➚ | 24m | 8min | 28min | 10/70 | CCR (SP 1.2) | +| ⏹ | 24m | 1min | 29min | 10/70 | CCR (SP 1.2) | +| ⏹ | 21m | 2min | 31min | 10/70 | CCR (SP 1.2) | +| ⏹ | 18m | 2min | 33min | 10/70 | CCR (SP 1.2) | +| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | +| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | +| ⏹ | 9m | 6min | 46min | 10/70 | CCR (SP 1.2) | +| ⏹ | 6m | 9min | 55min | 10/70 | CCR (SP 1.2) | +| ⏹ | 3m | 14min | 69min | 10/70 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 70min | 10/70 | CCR (SP 1.2) | +**CNS**: 29% +**OTU**: 82 +
+ +
+Subsurface + +> **Observations:** +> Subsurface does not require a 24 meter stop, while Abysner barely does (1 minute). This is likely +> due to minor algorithmic differences in tissue loading precision. + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|-------|--------------| +| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 0.7) | +| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | +| ➚ | 24m | 8min | 28min | 10/70 | CCR (SP 1.2) | +| ⏹ | 21m | 2min | 30min | 10/70 | CCR (SP 1.2) | +| ⏹ | 18m | 3min | 33min | 10/70 | CCR (SP 1.2) | +| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | +| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | +| ⏹ | 9m | 6min | 46min | 10/70 | CCR (SP 1.2) | +| ⏹ | 6m | 9min | 55min | 10/70 | CCR (SP 1.2) | +| ⏹ | 3m | 13min | 68min | 10/70 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 69min | 10/70 | CCR (SP 1.2) | +**CNS**: 29% +**OTU**: 80 +*Subsurface (6.0.5576-CICD-release)* +
+ +
+DIVESOFT.APP + +> **Observations:** +> DIVESOFT.APP does not display ascents between deco stops, and does not include the ascent time +> to the next stop in the total stop duration. This means the displayed stop time does not always +> match up to the runtime. Duration values below were derived by subtracting runtimes and the +> final ascent using the leftover runtime (which is displayed by DIVESOFT.APP). +> +> - DIVESOFT.APP produces a longer runtime (74 min vs 70 min for Abysner and 69 min for Subsurface). +> - DIVESOFT.APP displays 1.2 for the setpoint on the initial descent, while 0.7 was configured for +> descents. It is unclear whether this is just a display choice (labeling the descent with the +> bottom setpoint) or whether the low setpoint is not applied during descent. + +| | Depth | Duration | Runtime | Gas | Mode | +|---|-------|----------|---------|-------|--------------| +| ➘ | 60m | 12min | 12min | 10/70 | CCR (SP 1.2) | +| ➙ | 60m | 8min | 20min | 10/70 | CCR (SP 1.2) | +| ➚ | 24m | 7min | 27min | 10/70 | CCR (SP 1.2) | +| ⏹ | 24m | 1min | 28min | 10/70 | CCR (SP 1.2) | +| ⏹ | 21m | 2min | 30min | 10/70 | CCR (SP 1.2) | +| ⏹ | 18m | 3min | 33min | 10/70 | CCR (SP 1.2) | +| ⏹ | 15m | 3min | 36min | 10/70 | CCR (SP 1.2) | +| ⏹ | 12m | 4min | 40min | 10/70 | CCR (SP 1.2) | +| ⏹ | 9m | 7min | 47min | 10/70 | CCR (SP 1.2) | +| ⏹ | 6m | 9min | 56min | 10/70 | CCR (SP 1.2) | +| ⏹ | 3m | 17min | 73min | 10/70 | CCR (SP 1.2) | +| ➚ | 0m | 1min | 74min | 10/70 | CCR (SP 1.2) | +**CNS**: 31% +**OTU**: 85 +*DIVESOFT.APP (Android 2.5.1)* +
+ + +## Reference plan 9 (surface interval) +**30 meter, 30 minutes, repeated after 30-minute surface interval** + +Both dives are identical: 30 meters for 30 minutes on air, with a 30-minute surface interval between +them. The second dive should produce noticeably longer decompression due to residual tissue loading +from the first dive. + +| GF | Salinity | Altitude | Last-deco stop | Surface interval | +|-------|----------|----------|----------------|------------------| +| 85/85 | Fresh | 0 meters | 3 meter | 30 minutes | + +### Dive 1 + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 3m | 6min | 36min | 21/0 | +| ⏹ | 3m | 8min | 44min | 21/0 | +| ➚ | 0m | 1min | 45min | 21/0 | +**CNS**: 7% +**OTU**: 20 +
+ +
+Subsurface + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 3m | 6min | 36min | 21/0 | +| ⏹ | 3m | 8min | 44min | 21/0 | +| ➚ | 0m | 1min | 45min | 21/0 | +**CNS**: 7% +**OTU**: 19 +*Subsurface (6.0.5576-CICD-release)* +
+ +### Dive 2 (after 30-minute surface interval) + +
+Abysner + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 6m | 5min | 35min | 21/0 | +| ⏹ | 6m | 1min | 36min | 21/0 | +| ➚ | 3m | 1min | 37min | 21/0 | +| ⏹ | 3m | 27min | 64min | 21/0 | +| ➚ | 0m | 1min | 65min | 21/0 | +**CNS**: 7% +**OTU**: 20 +
+ +
+Subsurface + +> **Observations:** +> Subsurface produces a 1 minute longer runtime (66 vs 65 minutes) with slightly different stop +> distributions. The stop structure is the same: both planners require a 6 meter stop on the +> repetitive dive that was not needed on the first dive. + +| | Depth | Duration | Runtime | Gas | +|---|-------|----------|---------|------| +| ➘ | 30m | 6min | 6min | 21/0 | +| ➙ | 30m | 24min | 30min | 21/0 | +| ➚ | 6m | 5min | 35min | 21/0 | +| ⏹ | 6m | 2min | 37min | 21/0 | +| ⏹ | 3m | 28min | 65min | 21/0 | +| ➚ | 0m | 1min | 66min | 21/0 | +**CNS**: 12% +**OTU**: 19 +*Subsurface (6.0.5576-CICD-release)* +
+ + From c5c1a5123d656d865502a26ebc1d8085eb42bfa3 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 17:34:27 +0300 Subject: [PATCH 09/11] Docs: Add repository layout section to ARCHITECTURE.md Map the supporting top-level folders (gradle, buildSrc, resources, store-art, docs, iosApp) and root files, explain what buildSrc provides, and document the generated/git-ignored folders (build/, .gradle/, iosApp/build/) and where build artifacts land. Co-Authored-By: Claude Opus 4.8 --- docs/ARCHITECTURE.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 94e1513..c431c98 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -88,6 +88,51 @@ bundle id) use `nl.neotech.app.abysner`, while the Kotlin packages use `org.neot | Platform entry points | `androidApp/`, `composeApp/src/iosMain/`, `composeApp/src/jvmMain/` | +## Repository layout + +Beyond the Gradle modules above, the repository has a number of supporting folders and files: + +| Path | What it is | +|-------------------------------|-------------------------------------------------------------------------------------| +| `iosApp/` | The Xcode project (SwiftUI wrapper and iOS config) that hosts the shared Compose UI. | +| `docs/` | This documentation. | +| `gradle/` | The Gradle wrapper (`wrapper/`) and the version catalog (`libs.versions.toml`) that pins every dependency and plugin version. | +| `buildSrc/` | Custom Gradle build logic (see below). | +| `resources/` | Images used by this README (header, demo, store badges). | +| `store-art/` | Marketing and store assets: app icon, feature images, and framed device screenshots for the Play Store and App Store. | +| `build.gradle.kts` | Root build script (also defines the iOS archive/export and screenshot-framing tasks). | +| `settings.gradle.kts` | Declares the included modules. | +| `gradle.properties` | Versions plus JVM and Gradle daemon-toolchain config. | +| `gradlew`, `gradlew.bat` | Gradle wrapper scripts. Use these instead of a local Gradle install. | +| `Dockerfile`, `.dockerignore` | A `temurin:21-jdk` container for a reproducible build environment. | +| `.gitattributes` | Git LFS rules for binary assets (`.psd`, `.ai`, screenshot reference PNGs, store art). | +| `LICENSE`, `cla.txt` | The AGPLv3 license and the contributor license agreement. | + +### buildSrc + +`buildSrc` is Gradle's convention for build logic that is compiled and applied to the build itself. +Here it provides the project's own plugins and task types, all under +`buildSrc/src/main/kotlin/org/neotech/plugin/`: + +- `ScreenshotReferenceCleanupPlugin` and `ScreenshotTestCoveragePlugin`, applied by `androidApp`. +- `IosArchiveTask` and `IosExportTask`, used by the root build to archive the iOS app and upload it + to App Store Connect. +- `FrameScreenshotsTask`, used to frame screenshots for the store listings. + +### Generated folders (not in version control) + +These appear after building and are git-ignored, so you will see them locally but not in the repo: + +- `build/` (in the root and in each module): build outputs. Compiled code, test results, the Android + APKs, Kover coverage reports under `build/reports/kover/`, and screenshot diffs under + `androidApp/build/reports/screenshotTest/`. +- `.gradle/` (root and `buildSrc/.gradle/`): Gradle's per-project cache and daemon state. +- `iosApp/build/`: Xcode and iOS build output. + +All of these are safe to delete and will regenerate. `./gradlew clean` removes the modules' `build/` +folders. + + ## Design patterns The project follows a small set of well-known patterns. Each is described below with a real file to From b99b2a69c9292b6dd6b148eb795a2171c8b768c2 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 17:36:16 +0300 Subject: [PATCH 10/11] Docs: Mention the Docker build environment and refine its description Add a Docker section to the README Building from source instructions, and note in ARCHITECTURE.md that the image pre-fetches dependencies for a JVM build only. Co-Authored-By: Claude Opus 4.8 --- README.md | 12 ++++++++++++ docs/ARCHITECTURE.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dad3c8d..20ff63d 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,18 @@ the shared `ComposeApp` framework as part of the Xcode build. - *Android SDK not found*: open the project once in Android Studio, or create a `local.properties` file with `sdk.dir=/path/to/Android/sdk`. +**Reproducible build environment (Docker):** + +If you would rather not set up a local JDK, the included `Dockerfile` provides a `temurin:21-jdk` +environment with the project's dependencies pre-fetched. It targets the JVM build (the decompression +engine and its tests), and does not include the Android SDK or Xcode, so it is not meant for full +Android or iOS app builds. + +```sh +docker build -t abysner-build . +docker run --rm abysner-build ./gradlew :domain:jvmTest +``` + The codebase is split into four Gradle modules (`domain`, `data`, `composeApp`, `androidApp`). See [Architecture](#architecture) below for what each one does. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c431c98..c9f9000 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -104,7 +104,7 @@ Beyond the Gradle modules above, the repository has a number of supporting folde | `settings.gradle.kts` | Declares the included modules. | | `gradle.properties` | Versions plus JVM and Gradle daemon-toolchain config. | | `gradlew`, `gradlew.bat` | Gradle wrapper scripts. Use these instead of a local Gradle install. | -| `Dockerfile`, `.dockerignore` | A `temurin:21-jdk` container for a reproducible build environment. | +| `Dockerfile`, `.dockerignore` | A `temurin:21-jdk` container that pre-fetches dependencies for a reproducible JVM build (no Android SDK or Xcode). | | `.gitattributes` | Git LFS rules for binary assets (`.psd`, `.ai`, screenshot reference PNGs, store art). | | `LICENSE`, `cla.txt` | The AGPLv3 license and the contributor license agreement. | From a9b6f1389dec024899de2de0445de19af08a9798 Mon Sep 17 00:00:00 2001 From: Ronny Majani Date: Sun, 7 Jun 2026 17:37:36 +0300 Subject: [PATCH 11/11] Docs: Clarify that buildSrc itself is not safe to delete Disambiguate the generated-folders note: only build/, .gradle/ and iosApp/build/ are safe to delete. buildSrc/src and its build scripts are version-controlled build logic the project needs. Co-Authored-By: Claude Opus 4.8 --- docs/ARCHITECTURE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c9f9000..514f8f8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -123,14 +123,16 @@ Here it provides the project's own plugins and task types, all under These appear after building and are git-ignored, so you will see them locally but not in the repo: -- `build/` (in the root and in each module): build outputs. Compiled code, test results, the Android - APKs, Kover coverage reports under `build/reports/kover/`, and screenshot diffs under - `androidApp/build/reports/screenshotTest/`. +- `build/` (in the root, in each module, and in `buildSrc/`): build outputs. Compiled code, test + results, the Android APKs, Kover coverage reports under `build/reports/kover/`, and screenshot + diffs under `androidApp/build/reports/screenshotTest/`. - `.gradle/` (root and `buildSrc/.gradle/`): Gradle's per-project cache and daemon state. - `iosApp/build/`: Xcode and iOS build output. -All of these are safe to delete and will regenerate. `./gradlew clean` removes the modules' `build/` -folders. +Only these generated folders are safe to delete (they regenerate); `./gradlew clean` removes the +modules' `build/` folders. This does not include `buildSrc/` itself: its `src/` and build scripts are +version-controlled build logic the project needs, so deleting `buildSrc/` (as opposed to its +generated `build/` and `.gradle/` subfolders) would break the build. ## Design patterns