RustedSciThe 0.4.20

Rust framework for symbolic and numerical computing:BVP ( Newton-Raphson frozen/damped/with collocations ), IVP( BDF, Radau, Backward Euler, LSODE, LSODA, RK45, DoPri), nonlinear equations ( Levenberg, Gavin) and more
# IVP User Guide (RustedSciThe)

Этот гайд рассчитан на разработчика на Rust, который хочет уверенно решать задачи Коши в RustedSciThe и не тратить время на «археологию» по исходникам. Здесь важна практическая логика: какой метод выбрать для конкретного типа задачи, как backend-маршрут влияет на поведение и производительность, и как собрать конфиг, который сначала корректен, а уже потом оптимален по времени.

Если вы уже используете LSODE2, сразу зафиксируем рамку. У LSODE2 есть отдельное подробное руководство и отдельная алгоритмическая история (mirroring LSODE/LSODA, переключение Adams/BDF, parity-наборы). В этом документе мы сознательно разбираем остальные IVP-пути: Native BDF, Radau, Backward Euler и нежесткую explicit-семью через универсальный фасад.

## 1. Картина методов в этом гайде

В RustedSciThe есть два уровня IVP API.

Первый уровень — специализированные API конкретных солверов. Вы работаете напрямую с `BDF::ODEsolver`, `Radau` или `BE`, детально настраиваете их опции и явно вызываете нужный метод. Это дает максимум контроля и часто удобнее в production-коде, где поведение должно быть максимально прозрачным.

Второй уровень — `UniversalODESolver` из [`ODE_api2.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/ODE_api2.rs). Это единый фасад, который позволяет менять метод без переписывания всей обвязки. Такой путь особенно полезен для сравнительных прогонов, регрессионных тестов и оркестрации, где выбор метода задается данными, а не руками.

В этом гайде обсуждаются:

- жесткие методы: BDF, Radau IIA, Backward Euler;
- нежесткие explicit-методы: RK45, Dormand–Prince (`DOPRI`), Adams–Bashforth 4 (`AB4`).

Комментарий про RK4: отдельной публичной точки входа «классический фиксированный RK4» в `UniversalODESolver` сейчас нет. На практике его нишу закрывают RK45/DOPRI, а в multistep-сценариях — AB4.

## 2. Жесткость задачи и выбор семейства

Во многих реальных задачах самое сложное — не записать уравнения, а правильно выбрать семейство интегратора до того, как начнутся численные проблемы.

Если в модели есть сильно разделенные масштабы времени, быстрые релаксации, химическая кинетика или выраженная диссипация, стартовать разумно со stiff-метода. BDF обычно выступает рабочим «дефолтом» для больших жестких систем, Radau хорош, когда нужна устойчивая высокопорядковая implicit Runge–Kutta схема, а Backward Euler полезен как максимально консервативный baseline.

Если траектория гладкая и stiffness-индикаторы не проявляются, explicit-семейство почти всегда дешевле по запуску и проще в настройке. Для общего случая в этой группе обычно берут RK45, DOPRI используют в той же нише, а AB4 — когда нужен именно явный многошаговый характер.

Если вы не уверены, универсальный API позволяет быстро прогнать одну и ту же постановку разными методами и сравнить финальную ошибку, число вызовов и встроенную статистику.

## 3. Философия backend-маршрутов: Numerical, Lambdify, AOT

Для stiff-солверов в RustedSciThe метод интегрирования и способ вычисления residual/Jacobian — это разные оси настройки. Это ключевая идея.

`Numerical` означает, что вы передаете callbacks напрямую: residual `f(t, y)` и, опционально, Jacobian `df/dy`. Если Jacobian не задан, используется конечная разность. Такой путь удобен, когда модель уже реализована в Rust и символьный этап не нужен.

`Lambdify` означает, что уравнения задаются как `Expr`, а затем при подготовке солвера превращаются в исполняемые closures. Этот путь практически не требует внешней инфраструктуры и обычно удобен как базовый сценарий «сначала корректность».

`AOT` (ahead-of-time) означает, что символьные выражения превращаются в сгенерированный код, который компилируется и линкуется в артефакт. Вы платите upfront-цену за сборку, зато снижаете стоимость вызовов внутри solve-loop. Это инструмент производительности, а не просто другой синтаксис.

Важно понимать, что математика метода при этом не меняется. Меняется только инфраструктура вычислителей.

## 4. UniversalODESolver: один фасад, несколько методов

`UniversalODESolver` позволяет держать единый каркас задачи и менять метод/маршрут с минимальными правками.

### 4.1 Минимальный explicit-пример (нежесткий)

```rust
use nalgebra::DVector;
use RustedSciThe::numerical::ODE_api2::UniversalODESolver;
use RustedSciThe::symbolic::symbolic_engine::Expr;

let eq = vec![Expr::parse_expression("-y")];
let vars = vec!["y".to_string()];

let mut solver = UniversalODESolver::rk45(
    eq,
    vars,
    "t".to_string(),
    0.0,
    DVector::from_vec(vec![1.0]),
    1.0,
    1e-4,
);

solver.solve();
let (t, y) = solver.get_result();
println!("status = {:?}", solver.get_status());
println!("grid points = {}", t.unwrap().len());
println!("y(1) = {}", y.unwrap()[(y.unwrap().nrows() - 1, 0)]);
```

### 4.2 Native stiff-путь с пользовательским Jacobian (BDF)

```rust
use nalgebra::{DMatrix, DVector};
use RustedSciThe::numerical::ODE_api2::UniversalODESolver;
use RustedSciThe::symbolic::symbolic_engine::Expr;

let eq = vec![
    Expr::parse_expression("-1000.0*x0"),
    Expr::parse_expression("-x1"),
];
let vars = vec!["x0".to_string(), "x1".to_string()];

let mut solver = UniversalODESolver::bdf(
    eq,
    vars,
    "t".to_string(),
    0.0,
    DVector::from_vec(vec![1.0, 1.0]),
    0.02,
    1e-4,
    1e-7,
    1e-9,
)
.with_native_ode_callbacks(
    |_t, y| DVector::from_vec(vec![-1000.0 * y[0], -y[1]]),
    Some(|_t, _y| DMatrix::from_row_slice(2, 2, &[-1000.0, 0.0, 0.0, -1.0])),
);

solver.solve();
if let Some(stats) = solver.get_statistics() {
    println!("{}", stats.table_report());
}
```

### 4.3 Native stiff-путь с FD Jacobian fallback

Меняется только аргумент Jacobian:

```rust
.with_native_ode_callbacks(
    |_t, y| DVector::from_vec(vec![-1000.0 * y[0], -y[1]]),
    Option::<fn(f64, &DVector<f64>) -> DMatrix<f64>>::None,
);
```

Это удобно на этапе черновой отладки модели, но для тяжелых stiff-прогонов аналитический Jacobian обычно лучше.

## 5. Собственные API солверов (прямой контроль)

Во многих production-проектах предпочитают прямые API, потому что в ревью и сопровождении сразу видно, какой именно метод используется.

### 5.1 Native BDF API

Основной модуль: [`BDF_api.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/BDF/BDF_api.rs).

Типичный сценарий:

1. собрать `BdfSolverOptions`;
2. создать солвер через `ODEsolver::new_with_options(...)`;
3. при необходимости поставить native callbacks через `set_native_ode_callbacks(...)`;
4. запустить solve и снять статистику.

`BDF` поддерживает и symbolic-маршруты, и pure numerical callbacks. Если native Jacobian не задан, автоматически используется конечная разность.

Самостоятельный солвер `BDF` намеренно поддерживает только плотные матрицы:
любой Jacobian материализуется для плотной линейной алгебры. Выбор
ExprLegacy/AtomView и Lambdify/AOT определяет подготовку и выполнение evaluator,
но не включает sparse-маршрут. Для BDF с разреженной или ленточной матрицей
используйте LSODE2, явно выбрав семейство BDF и структуру Sparse или Banded.

### 5.2 Radau API

Основной модуль: [`Radau_main.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/Radau/Radau_main.rs).

Radau в новой архитектуре использует поддерживаемый порядок 5 (`Radau5` в
текстовом IVP task DSL); выбор `Order3/Order5/Order7` не является частью
этого API. Native callbacks включаются через `set_native_ode_callbacks(...)`;
при отсутствии Jacobian применяется FD fallback.

У Radau подробная встроенная статистика (Newton solves, Jacobian calls, LU usage), поэтому метод удобен не только как интегратор, но и как диагностический инструмент.

### 5.3 Backward Euler API

Основной модуль: [`BE.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/BE.rs).

`BE` — самый консервативный stiff baseline в этой группе. Он поддерживает generated backends и pure numerical callbacks, включая FD fallback для Jacobian. Это полезный вариант, когда важны предсказуемость и прозрачность Newton-итераций.

## 6. Как выбирать backend в stiff-методах

### 6.1 Lambdify как baseline

Lambdify разумно брать первым шагом, когда вы валидируете новую модель, хотите минимум инфраструктурных переменных и хотите быстро получить корректный референс.

### 6.2 AOT как инженерный ускоритель

AOT имеет смысл, когда модель решается многократно или solve-loop очень длинный, так что compile/link-затраты окупаются. Для dense IVP-маршрутов в RustedSciThe сейчас практически доступны четыре цели: `C+tcc`, `C+gcc`, `Zig` и `Rust`. Для быстрой локальной итерации часто удобнее `C+tcc`, а для повторяемых production-прогонов нередко выгоднее `C+gcc` из-за лучшей скорости в steady-state. `Zig` и `Rust` — полноценные рабочие варианты, когда они лучше подходят вашей инфраструктуре.

Жизненный цикл артефакта задается через `DenseIvpGeneratedBackendMode` и нижележащий `SymbolicIvpAotBuildPolicy`. На практике чаще всего используют:

- `BuildIfMissingRelease`: собрать при отсутствии, затем переиспользовать;
- `RequirePrebuilt`: требовать заранее собранный артефакт и падать при его отсутствии;
- `Defaults`/`UseIfAvailable`: взять готовый артефакт, а при его отсутствии остаться на lambdify-пути.

Быстрый вариант настройки — через fluent API солвера:

```rust
let solver = ODEsolver::new_with_options(opts)
    .with_dense_generated_backend_c_tcc("target/generated-ivp-tests")
    .with_dense_generated_backend_mode(DenseIvpGeneratedBackendMode::BuildIfMissingRelease);
```

Если нужен явный контроль над политикой сборки, backend-типом и директорией артефактов, лучше задавать `SymbolicIvpGeneratedBackendConfig` напрямую:

```rust
use RustedSciThe::symbolic::symbolic_ivp_generated::{
    DenseIvpGeneratedBackendMode, SymbolicIvpGeneratedBackendConfig,
};

let gen_cfg = SymbolicIvpGeneratedBackendConfig::from_mode(
    DenseIvpGeneratedBackendMode::BuildIfMissingRelease
)
.with_c_gcc()
.with_output_parent_dir(Some("target/generated-ivp-tests".into()));
```

Параллельная декомпозиция в AOT задается через `SymbolicIvpAotOptions`: отдельно для residual и для dense Jacobian. Это тот слой, где вы управляете chunking-стратегией:

```rust
use RustedSciThe::symbolic::symbolic_ivp::SymbolicIvpAotOptions;
use RustedSciThe::symbolic::codegen::codegen_runtime_api::{
    DenseJacobianChunkingStrategy, ResidualChunkingStrategy,
};

let chunked = SymbolicIvpAotOptions {
    residual_strategy: ResidualChunkingStrategy::ByTargetChunkCount { target_chunks: 2 },
    jacobian_strategy: DenseJacobianChunkingStrategy::ByTargetChunkCount { target_chunks: 2 },
};

let gen_cfg = gen_cfg.with_aot_options(chunked);
```

Практическая рекомендация: разделяйте warm-up и чистый solve-benchmark. Сначала прогоните build/prepare, затем отдельно измеряйте steady-state solve, иначе шум сборки скроет реальную разницу между backend-маршрутами.

### 6.3 Pure numerical путь

Native callbacks выбирают, когда модель уже реализована в Rust, символьная подготовка не нужна, и требуется полный контроль над Jacobian-логикой. Для честных бенчмарков против внешних реализаций этот путь обычно самый чистый.

## 7. Статистика — это часть результата, а не «опция»

Сильная сторона текущей архитектуры — встроенная статистика этапов и вызовов. Вместо обвязки кустарными таймерами лучше использовать нативный инструмент.

Для `UniversalODESolver`:

```rust
if let Some(stats) = solver.get_statistics() {
    println!("{}", stats.table_report());
}
```

Для прямых API солверов используйте их собственные `get_statistics()` (где доступны). В stiff-анализе обычно важны как минимум residual calls, Jacobian calls, LU counts, nonlinear iterations и суммарные времена setup/solve.

## 8. End-to-end примеры в репозитории

Если нужен не сниппет, а готовый исполняемый сценарий, начинайте с:

- [`bdf_solver_examples.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/examples/bdf_solver_examples.rs): набор BDF-примеров, включая stiff и нелинейные системы;
- [`universal_ode_example.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/examples/universal_ode_example.rs): использование RK45, DOPRI, Radau, BDF и BE через единый фасад;
- [`radau_backends_guide.rs`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/examples/radau_backends_guide.rs): практические паттерны backend-настройки Radau.

Для LSODE2-сценариев используйте отдельные руководства:

- [`LSODE2_USER_GUIDE_EN.md`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/LSODE2/LSODE2_USER_GUIDE_EN.md)
- [`LSODE2_USER_GUIDE_RU.md`](/f:/RUST/RustProjects_/RustedSciThe_experimental4/src/numerical/LSODE2/LSODE2_USER_GUIDE_RU.md)

## 9. Рабочая последовательность выбора

Практический устойчивый сценарий обычно выглядит так. Сначала вы добиваетесь корректности на Lambdify с аккуратными tolerances. Затем проверяете, действительно ли задача stiff/non-stiff в ваших данных, и читаете статистику. Потом, если есть реальный bottleneck, переходите к native callbacks или AOT. После этого фиксируете выбранную политику в тестах, чтобы будущие изменения не сдвигали поведение молча.

Эта последовательность может показаться «слишком дисциплинированной», но именно она экономит больше всего времени на длинной дистанции.