# BVP Damped/Frozen в RustedSciThe: руководство пользователя
Этот текст написан для разработчика на Rust, который впервые открывает BVP-часть RustedSciThe и хочет не гадать по тестам, а сразу понять рабочую модель: какие данные передаются в солвер, чем отличаются Damped и Frozen, когда выбирать Sparse или Banded, что реально означают Lambdify, AOT и Numerical path, и какие выводы дают наши story/performance-прогоны.
В RustedSciThe BVP-солверы решают краевые задачи (boundary value problems) для систем обыкновенных дифференциальных уравнений. Пользователь задает непрерывную систему первого порядка, сетку, начальное приближение и граничные условия. Внутри система дискретизуется, превращается в большую нелинейную систему алгебраических уравнений, а затем решается модифицированным методом Ньютона. Это важно держать в голове: вы настраиваете не только формулы правых частей, но и весь путь от дискретизации до сборки якобиана и решения линейной системы на каждом шаге Ньютона.
## 1. Damped и Frozen: две стратегии Ньютона
Основной production-путь - `Damped` Newton, реализованный в [`NR_Damp_solver_damped.rs`](NR_Damp_solver_damped.rs). Он использует демпфирование шага Ньютона, bounds, повторную сборку якобиана, диагностику и современный backend-handoff. Если вы не знаете заранее, что вам нужна Frozen-стратегия, начинайте с Damped.
`Frozen` Newton, реализованный в [`NR_Damp_solver_frozen.rs`](NR_Damp_solver_frozen.rs), оставлен как специализированная стратегия с "замороженной" линеаризацией. Он полезен, когда вы намеренно хотите переиспользовать структуру/якобиан или сравнить поведение с более простым frozen-процессом. Это не более "новый" и не более "быстрый" путь по умолчанию. Практическое правило простое: сначала добейтесь сходимости и правильности на Damped, затем используйте Frozen как осознанную оптимизацию или исследовательский режим.
Оба солвера поддерживают символическую постановку через `Expr` и generated backend configuration. Чистый численный путь с Rust-замыканием RHS сейчас полноценно реализован для Damped.
## 2. Что именно передается в BVP-солвер
Предпочтительный конструктор для нового кода - `NRBVP::new_with_options(...)`. Его сигнатура для Damped выглядит так:
```rust
pub fn new_with_options(
eq_system: Vec<Expr>,
initial_guess: DMatrix<f64>,
values: Vec<String>,
arg: String,
border_conditions: HashMap<String, Vec<(usize, f64)>>,
t0: f64,
t_end: f64,
n_steps: usize,
options: DampedSolverOptions,
) -> NRBVP
```
`eq_system` - это правая часть системы первого порядка. Например, уравнение `y'' + y = 0` обычно записывают как `y' = z`, `z' = -y`, значит `eq_system = ["z", "-y"]`, а `values = ["y", "z"]`. Порядок в `values` задает порядок компонент состояния и должен совпадать с порядком правых частей.
`arg` - имя независимой переменной, обычно `"x"`. Пара `t0`, `t_end` задает отрезок интегрирования/краевой задачи, а `n_steps` - число интервалов сетки.
`initial_guess` - начальное приближение для неизвестных значений на сетке. Важная синтаксическая особенность текущего BVP Damped/Frozen API: начальное приближение обычно имеет размер `values.len() x n_steps`, а не `values.len() x (n_steps + 1)`. Граничные значения задаются отдельно в `border_conditions` и затем встраиваются в полное сеточное решение. В тестах это обычно выглядит так:
```rust
let initial_guess = DMatrix::from_vec(values.len(), n_steps, guess_values);
```
`border_conditions` задаются как `HashMap<String, Vec<(usize, f64)>>`. В текущем соглашении индекс `0` означает левую границу, индекс `1` - правую границу. Например, `y(0)=0` и `z(L)=1` записываются так:
```rust
let border_conditions = HashMap::from([
("y".to_string(), vec![(0usize, 0.0)]),
("z".to_string(), vec![(1usize, 1.0)]),
]);
```
Damped numeric route (`NumericOnly`) ожидает корректно поставленную систему первого порядка: общее число фиксированных граничных условий должно равняться `values.len()`. Эти условия не обязаны распределяться строго "одно на переменную". Например, осциллятор `y' = z`, `z' = -y` можно задать как численную краевую задачу с условиями `y(0)=0` и `y(pi/2)=1`, оставляя производную переменную `z` свободной на обеих границах. Frozen численный route не использует.
## 3. `DampedSolverOptions`: основной синтаксис настройки
`DampedSolverOptions` группирует настройки, которые раньше передавались длинным списком позиционных аргументов. Его полный конструктор:
`new(...)` constructor is retained for compatibility, but new code should prefer presets plus builder methods. In particular, prefer typed derivative-scheme builders over raw `scheme: String`.
На практике чаще используются пресеты:
```rust
let options = DampedSolverOptions::sparse_damped();
let options = DampedSolverOptions::banded_damped();
let options = DampedSolverOptions::dense_damped();
```
После пресета обычно уточняют tolerance, bounds, nonlinear strategy и backend:
```rust
let strategy = SolverParams {
max_jac: Some(6),
max_damp_iter: Some(6),
damp_factor: Some(0.5),
adaptive: None,
};
let options = DampedSolverOptions::banded_damped()
.trapezoid_derivative()
.with_strategy_params(Some(strategy))
.with_abs_tolerance(1e-8)
.with_rel_tolerance(HashMap::from([
("y".to_string(), 1e-8),
("z".to_string(), 1e-8),
]))
.with_bounds(HashMap::from([
("y".to_string(), (-2.0, 2.0)),
("z".to_string(), (-2.0, 2.0)),
]))
.with_max_iterations(40)
.with_loglevel(Some("none".to_string()));
```
Схема дискретизации выбирается через typed builder (`forward_derivative`, `trapezoid_derivative`, `with_scheme`). `strategy` обычно `"Damped"`. `linear_sys_method` лучше оставлять `None`, если вы выбираете Sparse/Banded через `method` и generated backend config. `method` - это крупный выбор матричного пути: `"Dense"`, `"Sparse"` или `"Banded"`.
`FrozenSolverOptions` устроен похожим образом, но проще: там нет bounds и per-variable relative tolerance в том же виде, а главный пресет выглядит так:
```rust
let options = FrozenSolverOptions::banded_frozen()
.with_tolerance(1e-8)
.with_max_iterations(40);
```
Typed API для схемы дискретизации доступен и для Damped, и для Frozen:
```rust
use RustedSciThe::numerical::BVP_Damp::BvpDerivativeScheme;
let options = DampedSolverOptions::sparse_damped()
.with_scheme(BvpDerivativeScheme::Trapezoid);
let options = DampedSolverOptions::sparse_damped()
.forward_derivative();
let options = FrozenSolverOptions::banded_frozen()
.trapezoid_derivative();
```
`with_scheme_name("...")` оставлен только как compatibility escape hatch для legacy или experimental scheme names.
## 4. Dense, Sparse, Banded: это не стиль, а модель стоимости
`Dense` - проверочный и малый путь. Он удобен для маленьких систем, отладки и случаев, когда структура матрицы не важна. Для больших BVP Dense быстро становится дорогим по памяти и времени.
`Sparse` - универсальный production-путь для больших задач, где якобиан разреженный, но структура не обязательно узкополосная. Он хорошо подходит для химических сеток, систем с нерегулярной связностью и задач, где вы не хотите заранее доказывать banded-структуру.
`Banded` - лучший выбор для задач, где ненулевые элементы якобиана лежат около диагонали. Это типично для BVP после конечно-разностной дискретизации систем первого порядка: соседние узлы связаны, дальние узлы почти никогда не связаны напрямую. В RustedSciThe Banded route использует LAPACK-style banded LU, то есть матрица хранится и факторизуется в полосовом виде. На наших combustion BVP story-тестах Banded обычно дает заметно меньшее время линейного solve, чем Sparse, особенно когда система действительно имеет узкую полосу.
Главное предупреждение: Banded хорош тогда, когда структура правда banded. Если задача не полосовая, принудительный Banded route может ухудшить устойчивость и производительность. В этом смысле Banded - как спортивная резина: прекрасна на своем покрытии и бессмысленна на болоте.
## 5. Три backend-пути: Numerical, Lambdify, AOT
В BVP Damped/Frozen полезно разделять математический метод и backend. Метод Ньютона и дискретизация остаются теми же, но backend определяет, как вычисляется невязка, как собирается якобиан и какой линейный solve используется.
### Numerical path
Numerical path в Damped - это путь без символической машинерии. Вы передаете Rust-замыкание правой части `f(x, y, params)`, а отдельный модуль [`numeric_discretization.rs`](numeric_discretization.rs) строит дискретизованную невязку напрямую из этого замыкания. Это путь для моделей, где source of truth уже является Rust-кодом, а не `Expr`.
Теперь у Damped есть два явных численных конструктора. `NRBVP::new_numeric_fd_with_options(...)` принимает RHS-замыкание и явно выбирает конечно-разностный якобиан Ньютона. `NRBVP::new_numeric_with_jacobian_options(...)` принимает RHS-замыкание и маленькое непрерывное замыкание якобиана `df/dy`; большой дискретизованный BVP-якобиан строится внутри солвера с учетом сетки, схемы дискретизации и граничных условий. Это важное отличие от старого низкоуровневого паттерна: пользователю больше не нужно передавать пустой `eq_system`, чтобы сказать "это чисто численная задача".
Граничные условия в численном пути следуют тому же правилу для системы первого порядка: нужно задать ровно столько фиксированных значений на концах отрезка, сколько переменных состояния находится в `values`. Они могут относиться к разным переменным или к одной переменной на двух концах, если итоговая редуцированная система остается квадратной. Типичный двухточечный синтаксис выглядит так:
```rust
let border_conditions = HashMap::from([(
"y".to_string(),
vec![(0usize, 0.0), (1usize, 1.0)],
)]);
```
Здесь `z = y'` остается частью вектора состояния и по-прежнему участвует в RHS-замыкании и замыкании якобиана, но прямого граничного значения для `z` нет.
Для типичного sparse-сценария есть более короткие обертки: `NRBVP::new_numeric_fd(...)` и `NRBVP::new_numeric_with_jacobian(...)`. Они все равно принимают `bounds` и per-variable `rel_tolerance`, потому что в демпфированном Ньютоне это не декоративные параметры: bounds ограничивают допустимый шаг, а tolerances задают взвешенный критерий сходимости. Если нужен Banded, особые параметры нелинейной стратегии или логирование, используйте конструкторы `*_with_options`.
Frozen устроен строже. `BackendSelectionPolicy::NumericOnly` для Frozen намеренно отвергается, а не превращается молча в Lambdify или FD. Frozen сейчас ожидает symbolic Lambdify/AOT маршрут с подготовленным callback-якобианом.
### Lambdify
Lambdify означает: вы задаете систему через `Expr`, RustedSciThe выполняет символическую сборку дискретизованной BVP-системы и превращает выражения невязки/якобиана в исполняемые Rust callbacks без внешней компиляции. Это лучший baseline для correctness. У него низкий bootstrap-риск: не нужны `gcc`, `tcc`, `zig` или загрузка динамических библиотек.
Lambdify остается лучшим первым запуском новой постановки: в этот момент важнее всего проверить уравнения, граничные условия и начальное приближение без зависимости от внешнего toolchain. Для малых одноразовых расчетов он часто выигрывает end-to-end. Однако после оптимизации `AtomView` это уже не следует переносить на большие задачи: на measured combustion BVP с `n_steps = 1000` cold `tcc` AOT оказался быстрее Lambdify и в Sparse, и в Banded маршруте.
#### Параллелизм callback-ов Lambdify
Для вычисления невязки и якобиана используется явная политика
`BvpLambdifyExecutionPolicy`:
```rust
use RustedSciThe::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy;
let sequential = BvpLambdifyExecutionPolicy::Sequential;
let forced_parallel = BvpLambdifyExecutionPolicy::Parallel { min_work: 0 };
let automatic = BvpLambdifyExecutionPolicy::Auto { min_work: 0 };
```
`Sequential` удобен для маленьких систем и воспроизводимой диагностики.
`Parallel` является явным экспериментальным override. `Auto` -- осторожный
production-режим: кроме заданного пользователем порога требуется не менее
восьми скалярных вычислений на worker Rayon, поэтому маленькая задача остается
последовательной, а большая может использовать раздельные буферы worker-ов.
Решение детерминировано и не выполняет скрытую калибровку запуска потоков
внутри первого callback. Точку break-even следует проверять story/performance
тестами на конкретной машине.
### AOT
AOT (ahead-of-time) - это compiled backend. Символические выражения проходят через intermediate representation, затем генерируется код, собирается отдельный артефакт и подключается как runtime callback. Философия простая: заплатить за подготовку заранее, чтобы последующие вызовы невязки и якобиана были дешевле.
RustedSciThe поддерживает несколько codegen/toolchain-вариантов: C через `gcc` или `tcc`, Zig, а также Rust backend в codegen-слое. Для C/Zig путей соответствующие компиляторы должны быть установлены и доступны в `PATH`. По текущим Windows release-прогонам `tcc` оказался не просто самым дешевым способом собрать артефакт: в больших `AtomView`-задачах он вполне конкурентен Lambdify по полному времени от старта до результата и в отдельных опытах превосходит его уже при cold build. `gcc` может давать сильный runtime-код, но стартовая цена обычно выше, а Zig иногда имеет заметную и нестабильную стоимость сборки. Это измеренная рекомендация, а не жесткое правило: на другой машине и для другой структуры уравнений ranking следует перепроверить.
AOT безусловно имеет смысл, когда вы решаете одну и ту же большую задачу много раз, меняете параметры, прогоняете continuation, sensitivity-like сценарии или запускаете тяжелый production batch. Но граница оказалась приятнее, чем ожидалось: с `AtomView` и `tcc` AOT может быть разумным выбором уже для одного крупного расчета, особенно в Banded-маршруте. Для маленького BVP цена подготовки артефакта по-прежнему обычно не окупается.
## 6. ExprLegacy и AtomView
В символическом BVP pipeline есть два пути сборки выражений:
`ExprLegacy` - старый, проверенный путь через классические `Expr`-структуры. Он полезен как compatibility baseline и страховка, но больше не является production-default для Sparse/Banded Lambdify.
`AtomView` - более современный путь, рассчитанный на эффективную сборку и codegen. Отдельные process-isolated release-измерения на семействе combustion BVP показали, что он снимает особенно дорогую стоимость построения символьного якобиана и в разреженном (Sparse), и в полосовом (Banded) пути, сохраняя решение в ожидаемой численной точности. Поэтому production-пресеты Sparse и Banded и для Damped, и для Frozen солвера теперь выбирают `AtomView` по умолчанию.
Для полосового решения через Lambdify production-синтаксис теперь намеренно короткий:
```rust
use RustedSciThe::symbolic::symbolic_functions_BVP::BvpSymbolicAssemblyBackend;
let options = DampedSolverOptions::banded_damped()
.with_banded_lambdify();
// Compatibility/control route для воспроизведения ExprLegacy-прогона:
let legacy_options = DampedSolverOptions::banded_damped()
.with_banded_lambdify()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::ExprLegacy);
```
Тот же default наследуется в `FrozenSolverOptions::{sparse_frozen, banded_frozen}()` и в Sparse/Banded AOT-пресетах. `ExprLegacy` остается поддерживаемым явным override для проверки совместимости, контрольных сравнений и исследования frontend-зависимого поведения.
## 7. Generated backend config: как выбирать Lambdify, AOT и build policy
`GeneratedBackendConfig` - центральный объект для generated backend route. Он хранит policy выбора backend, build policy, codegen backend, C-компилятор, symbolic assembly backend, matrix override и chunking policy.
Высокоуровневые режимы:
```rust
SparseGeneratedBackendMode::Defaults
SparseGeneratedBackendMode::RequirePrebuilt
SparseGeneratedBackendMode::BuildIfMissingRelease
BandedGeneratedBackendMode::Defaults
BandedGeneratedBackendMode::Lambdify
BandedGeneratedBackendMode::BuildIfMissingRelease
```
`Defaults` обычно означает "предпочесть AOT, если он уже доступен, иначе fallback на Lambdify". Для production-режимов Sparse и Banded это теперь дополнительно означает символьную сборку через `AtomView`; Banded сверх того выбирает faithful LAPACK-style полосовой линейный решатель. `RequirePrebuilt` полезен в production, где отсутствие артефакта должно быть ошибкой, а не скрытой компиляцией. `BuildIfMissingRelease` удобен в интерактивном workflow: если артефакта нет, RustedSciThe соберет его на первом запуске.
Пресеты для repeated solves:
```rust
let sparse_aot = GeneratedBackendConfig::sparse_atomview_for_repeated_solves();
let banded_aot = GeneratedBackendConfig::banded_atomview_for_repeated_solves();
```
Эти пресеты сейчас используют AtomView и `tcc`-compiled C AOT как практический компромисс между скоростью bootstrap и runtime. Для явного выбора toolchain:
```rust
let cfg = GeneratedBackendConfig::banded_atomview_build_if_missing_release_gcc();
let cfg = GeneratedBackendConfig::banded_atomview_build_if_missing_release_tcc();
let cfg = GeneratedBackendConfig::banded_atomview_build_if_missing_release_zig();
```
Скомпилированные артефакты можно использовать повторно. В этом и состоит смысл `BuildIfMissing`: первый запуск платит цену генерации/сборки, последующие запуски могут идти через уже собранный runtime backend. Если вам нужно жестко гарантировать, что production-ран не начнет компиляцию, используйте `RequirePrebuilt`.
Выбор для новой большой символической задачи удобно делать в два хода. Сначала запустите `AtomView + Lambdify`: это простой контроль решения и структуры матрицы. Затем сравните его с `AtomView + tcc AOT` на том же `Sparse` или `Banded` маршруте. В сохраненном честном cold-прогоне `combustion-1000` AOT `tcc/whole` дал около `2.46 s` против `2.84 s` у Banded Lambdify и около `2.68 s` против `4.33 s` у Sparse Lambdify. Это достаточно сильный сигнал, чтобы тестировать TCC AOT рано, а не оставлять его только для поздней оптимизации repeated solves.
Теперь есть и контролируемое warm-измерение production lifecycle. В story-тесте
Damped Banded AtomView `combustion-1000` один setup-запуск собрал `tcc`-артефакт,
после чего пять строгих решений `RequirePrebuilt` сравнивались с пятью решениями
Lambdify при чередовании порядка и одинаковой пятисекундной паузе. Warm AOT дал
`431.4 +/- 10.9 ms` против `468.3 +/- 12.5 ms` у Lambdify, то есть небольшой,
но устойчивый выигрыш примерно `7.9%`. С учетом цены первой сборки этот выигрыш
в зафиксированном опыте окупается приблизительно после восьми последующих
решений. Именно так следует понимать repeated-solve preset: не как обещание
победы в одиночном запуске, а как compiled route для повторного решения одной
крупной модели.
### Lifecycle AOT-артефактов и cleanup
AOT-артефакты создаются внутри crate `target` и отслеживаются через
`AotResolver`: ключ задачи (`problem_key`) и manifest metadata. Это и есть
source of truth. Не редактируйте generated-папки вручную и не делайте вывод о
корректности артефакта только по имени файла. Если меняется сетка или
дискретизованная задача, symbolic frontend, matrix route, toolchain или
chunking policy, рассматривайте это как отдельный artifact lifecycle и
позвольте manifest/resolver contract решить, можно ли переиспользовать старый
артефакт.
Рекомендуемый lifecycle явный:
1. `BuildIfMissing` - setup/warmup-запуск, в котором разрешено создать
артефакт.
2. `RequirePrebuilt` - измеряемый или production-цикл, где отсутствующий
артефакт должен быть typed error, а не скрытая компиляция.
3. `cleanup_registered_aot_artifacts()` - явная очистка для
story/debug/cold-build workflow. Вызывайте ее только после того, как compiled
callbacks этого солвера больше не нужны.
```rust
use RustedSciThe::numerical::BVP_Damp::generated_solver_handoff::AotBuildPolicy;
// Первый проход: BuildIfMissing preset, например
// GeneratedBackendConfig::banded_atomview_build_if_missing_release_tcc().
solver.try_eq_generate()?;
solver.solve();
// Строгий repeated-solve проход: используем уже собранный артефакт
// без скрытой компиляции.
let strict_cfg = solver
.generated_backend_config()
.clone()
.with_aot_build_policy(AotBuildPolicy::RequirePrebuilt);
solver.set_generated_backend_config(strict_cfg);
solver.try_eq_generate()?;
solver.solve();
// Необязательный cleanup для story/debug/cold-build workflow.
// Не делайте это, пока нужны live callbacks этого solver.
let removed = solver.cleanup_registered_aot_artifacts()?;
println!("removed {removed} generated AOT artifact trees");
```
Cleanup намеренно не выполняется автоматически в `drop`. Compiled callbacks
могут еще жить в процессе, а на Windows загруженная dynamic library может
держать file handles. Метод чистит только registered generated trees из
текущего resolver snapshot; он не unregister linked callbacks и не unload
dynamic libraries. Это менее магично, зато безопаснее как production-контракт.
## 8. Chunking и parallel execution: честный вывод из измерений
AOT route умеет разбивать вычисление невязки и sparse Jacobian values на чанки. В терминах API это `AotChunkingPolicy`, `ResidualChunkingStrategy` и `SparseChunkingStrategy`. Runtime execution дополнительно управляется `AotExecutionPolicy` и `ParallelExecutorConfig`.
Пример явной настройки:
```rust
use RustedSciThe::numerical::BVP_Damp::generated_solver_handoff::{
AotChunkingPolicy, AotExecutionPolicy, GeneratedBackendConfig,
};
use RustedSciThe::symbolic::codegen::codegen_orchestrator::{
ParallelExecutorConfig, ParallelFallbackPolicy,
};
use RustedSciThe::symbolic::codegen::codegen_runtime_api::ResidualChunkingStrategy;
use RustedSciThe::symbolic::codegen::codegen_tasks::SparseChunkingStrategy;
let chunking = AotChunkingPolicy::with_parts(
Some(ResidualChunkingStrategy::ByTargetChunkCount { target_chunks: 4 }),
Some(SparseChunkingStrategy::ByTargetChunkCount { target_chunks: 4 }),
);
let exec = ParallelExecutorConfig {
jobs_per_worker: 1,
max_residual_jobs: None,
max_sparse_jobs: None,
fallback_policy: ParallelFallbackPolicy::Auto,
};
let cfg = GeneratedBackendConfig::sparse_atomview_build_if_missing_release_tcc()
.with_aot_chunking_policy(chunking)
.with_aot_execution_policy(AotExecutionPolicy::Parallel(exec));
```
По нашим текущим BVP combustion story-тестам вывод такой: chunking корректен и реальный parallel path существует, но его польза зависит от масштаба и от того, какая фаза доминирует. На средних задачах `n_steps = 200` и `n_steps = 1000` вычисление callback values часто занимает лишь миллисекунды, поэтому распараллеливание мало двигает полный wall clock. На большой Banded `AtomView` задаче с `n_steps = 3000` путь `tcc/chunk4` действительно выполнил четыре residual и четыре Jacobian jobs без fallback и сократил hot callback intervals: residual values примерно с `13.8` до `7.1 ms`, Jacobian values примерно с `6.7` до `1.7 ms`.
Даже этот большой прогон не делает forced chunking универсальным default: полное cold-время `tcc/whole` и `tcc/chunk4` оказалось статистически близким, потому что hot callback - только часть расчета. Именно поэтому policy `Auto` важнее, чем "включить parallel всегда". Auto может fallback-нуться в sequential path, если chunk/job workload слишком мал; это защита от overhead, а не скрытая неисправность. Если вы исследуете break-even на своей машине, используйте diagnostic tests и печать `actual_jobs`, `fallback`, `residual_ms`, `jacobian_ms`, `linear_ms`. Для production оставляйте `Auto`, а forced parallel используйте как измеряемую настройку конкретной тяжелой модели.
Самый прямой production-синтаксис - оставить chunking policy по умолчанию и
попросить compiled backend самому выбрать runtime execution:
```rust
let generated = GeneratedBackendConfig::banded_atomview_build_if_missing_release_tcc()
.with_aot_execution_policy(AotExecutionPolicy::Auto);
```
После `solver.try_solver()?` смотрите не на то, что вы попросили в конфиге, а
на то, что реально попало в diagnostics:
```rust
let stats = solver.get_statistics();
let d = &stats.diagnostics;
let auto_mode = d.get("aot.auto.execution_mode");
let auto_residual_reason = d.get("aot.auto.residual.reason");
let auto_jacobian_reason = d.get("aot.auto.sparse_jacobian.reason");
let residual_jobs = d.get("aot.runtime.residual.actual_jobs");
let jacobian_jobs = d.get("aot.runtime.sparse_jacobian.actual_jobs");
let residual_fallback = d.get("aot.runtime.residual.fallback_reason");
let jacobian_fallback = d.get("aot.runtime.sparse_jacobian.fallback_reason");
let parallel_requested = d.get("aot.runtime.parallel_requested");
println!(
"Auto={auto_mode:?}, residual={auto_residual_reason:?}/{residual_jobs:?}/{residual_fallback:?}, \
jacobian={auto_jacobian_reason:?}/{jacobian_jobs:?}/{jacobian_fallback:?}, \
parallel_requested={parallel_requested:?}"
);
```
Смысл разделения такой: `aot.auto.*` описывает план, а `aot.runtime.*` описывает
фактическое поведение уже связанного callback. В 12 Core story-тесте
`combustion-1000` для AtomView+tcc Auto выбрал `Sequential` и для Sparse, и для
Banded. План объяснил это как `work_per_chunk_too_small` для стадии Jacobian и
`single_chunk_or_job` для residual; runtime diagnostics подтвердили это через
`actual_jobs = 1`, `parallel_requested = false` и fallback reason
`single_requested_job`. Это успешное решение Auto, а не отказ параллелизма.
Forced chunking имеет смысл включать только тогда, когда большая модель или
break-even тест показывает достаточно callback work per job, чтобы окупить
распределение работы по потокам.
### Как сравнивать cold и warm расчеты
Число `total_ms` имеет смысл только если ясно, что именно было включено в запуск. В cold wall-clock тесте новый процесс начинает с построения symbolic frontend, генерации кода, компиляции, линковки и Newton solve; такой опыт отвечает на вопрос "сколько ждать от нажатия кнопки до результата". В warm или `RequirePrebuilt` режиме готовый артефакт уже существует, и сравнение отвечает на другой вопрос: "сколько стоит очередное решение в серии".
Для выбора пользовательского маршрута нужны оба взгляда. Cold-проверка защищает от AOT pipeline, который великолепен в callback, но слишком дорог до первого ответа. Warm/prebuilt-проверка показывает отдачу в parameter sweep, continuation и batch workflow. Не сравнивайте cold Lambdify с warm AOT и не делайте вывод по таблице, где одна строка могла переиспользовать artifact, а другая его строила.
`RequirePrebuilt` при этом является еще и договором о корректности lifecycle,
а не просто performance-флагом. В текущих story-тестах Damped Sparse/Banded и
Frozen Sparse/Banded строгий reuse сохраняет `AotCompiled`, не показывает
стадию compiler/linker и совпадает с Lambdify на уровне округления. Если
prebuilt-строка вновь начнет компиляцию или молча перейдет на Lambdify, это
регрессия. В частности, lifecycle Frozen combustion-1000 теперь подтвержден в
release для обоих матричных маршрутов; на Sparse пути reuse-строки совпали с
Lambdify до `4.44e-16`.
## 9. End-to-end пример: Symbolic Lambdify + Banded
Ниже пример гармонического осциллятора как BVP: `y' = z`, `z' = -y`, `y(0)=0`, `z(0)=1`. Решение `y=sin(x)`, `z=cos(x)`, что удобно для sanity check.
```rust
use std::collections::HashMap;
use nalgebra::{DMatrix, DVector};
use RustedSciThe::numerical::BVP_Damp::NR_Damp_solver_damped::{
DampedSolverOptions, NRBVP,
};
use RustedSciThe::symbolic::symbolic_engine::Expr;
let n_steps = 80;
let values = vec!["y".to_string(), "z".to_string()];
let eq_system = vec![
Expr::parse_expression("z"),
Expr::parse_expression("-y"),
];
let t0 = 0.0;
let t_end = std::f64::consts::FRAC_PI_2;
let h = (t_end - t0) / n_steps as f64;
let mut guess = Vec::with_capacity(values.len() * n_steps);
for i in 0..n_steps {
let x = t0 + i as f64 * h;
guess.push(x.sin());
guess.push(x.cos());
}
let initial_guess = DMatrix::from_column_slice(
values.len(),
n_steps,
DVector::from_vec(guess).as_slice(),
);
let border_conditions = HashMap::from([
("y".to_string(), vec![(0usize, 0.0)]),
("z".to_string(), vec![(0usize, 1.0)]),
]);
let bounds = HashMap::from([
("y".to_string(), (-2.0, 2.0)),
("z".to_string(), (-2.0, 2.0)),
]);
let rel_tol = HashMap::from([
("y".to_string(), 1e-8),
("z".to_string(), 1e-8),
]);
let options = DampedSolverOptions::banded_damped()
.with_banded_lambdify()
.with_abs_tolerance(1e-8)
.with_rel_tolerance(rel_tol)
.with_bounds(bounds)
.with_max_iterations(40)
.with_loglevel(Some("none".to_string()));
let mut solver = NRBVP::new_with_options(
eq_system,
initial_guess,
values,
"x".to_string(),
border_conditions,
t0,
t_end,
n_steps,
options,
);
solver.dont_save_log(true);
solver.try_solve()?;
let result = solver.get_result().expect("BVP result should be available");
let stats = solver.get_statistics();
println!("solution shape = {} x {}", result.nrows(), result.ncols());
println!("timers = {:?}", stats.timers);
# Ok::<(), Box<dyn std::error::Error>>(())
```
Этот пример хорош как стартовый: он использует символические уравнения, но не требует внешнего компилятора.
## 10. End-to-end пример: Numerical path
Pure Numerical path полезен, когда source of truth - ваше Rust-замыкание, а не `Expr`. Конструктор ниже вообще не принимает символические уравнения. Невязка строится численной дискретизацией RHS-замыкания, а выбор якобиана явный: либо конечно-разностный путь через `new_numeric_fd_with_options`, либо пользовательский непрерывный якобиан через `new_numeric_with_jacobian_options`.
```rust
use std::collections::HashMap;
use nalgebra::{DMatrix, DVector};
use RustedSciThe::numerical::BVP_Damp::NR_Damp_solver_damped::{
DampedSolverOptions, NRBVP,
};
let n_steps = 80;
let values = vec!["y".to_string(), "z".to_string()];
let t0 = 0.0;
let t_end = std::f64::consts::FRAC_PI_2;
let h = (t_end - t0) / n_steps as f64;
let mut guess = Vec::with_capacity(values.len() * n_steps);
for i in 0..n_steps {
let x = t0 + i as f64 * h;
guess.push(x.sin());
guess.push(x.cos());
}
let initial_guess = DMatrix::from_column_slice(
values.len(),
n_steps,
DVector::from_vec(guess).as_slice(),
);
let border_conditions = HashMap::from([
("y".to_string(), vec![(0usize, 0.0)]),
("z".to_string(), vec![(0usize, 1.0)]),
]);
let bounds = HashMap::from([
("y".to_string(), (-2.0, 2.0)),
("z".to_string(), (-2.0, 2.0)),
]);
let rel_tol = HashMap::from([
("y".to_string(), 1e-8),
("z".to_string(), 1e-8),
]);
let options = DampedSolverOptions::sparse_damped()
.with_abs_tolerance(1e-8)
.with_rel_tolerance(rel_tol)
.with_bounds(bounds)
.with_loglevel(Some("none".to_string()));
let mut solver = NRBVP::new_numeric_fd_with_options(
initial_guess,
values,
"x".to_string(),
border_conditions,
t0,
t_end,
n_steps,
options,
|_x, y, _params| {
DVector::from_vec(vec![y[1], -y[0]])
},
);
solver.dont_save_log(true);
solver.try_solve()?;
# Ok::<(), Box<dyn std::error::Error>>(())
```
Если у вас есть аналитический локальный якобиан, используйте второй численный конструктор. Замыкание возвращает непрерывный `2 x 2` якобиан правой части в одной точке сетки; глобальную BVP-матрицу солвер соберет сам.
```rust
let mut solver = NRBVP::new_numeric_with_jacobian_options(
initial_guess,
values,
"x".to_string(),
border_conditions,
t0,
t_end,
n_steps,
options,
|_x, y, _params| DVector::from_vec(vec![y[1], -y[0]]),
|_x, _y, _params| DMatrix::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0]),
);
```
Практический смысл этого пути - исключить символический слой. Если такой тест падает, вы локализуете проблему в численной дискретизации, Newton loop или линейной алгебре, а не в symbolic/AOT.
## 11. End-to-end пример: AtomView + AOT + Banded
Для большой структурированной BVP-задачи, которую вы будете решать много раз, типичный production-кандидат - Banded + AtomView + AOT. Первый запуск может быть дорогим из-за сборки артефакта, но повторные solve получают compiled callbacks и быстрый banded linear solve.
```rust
use RustedSciThe::numerical::BVP_Damp::generated_solver_handoff::GeneratedBackendConfig;
use RustedSciThe::numerical::BVP_Damp::NR_Damp_solver_damped::DampedSolverOptions;
let generated = GeneratedBackendConfig::banded_atomview_for_repeated_solves();
let options = DampedSolverOptions::banded_damped()
.with_generated_backend_config(generated)
.with_abs_tolerance(1e-8)
.with_max_iterations(60)
.with_loglevel(Some("none".to_string()));
```
Если вы хотите явно выбрать компилятор:
```rust
let generated = GeneratedBackendConfig::banded_atomview_build_if_missing_release_tcc();
let generated = GeneratedBackendConfig::banded_atomview_build_if_missing_release_gcc();
let generated = GeneratedBackendConfig::banded_atomview_build_if_missing_release_zig();
```
Для CI или production, где артефакт должен быть подготовлен заранее:
```rust
use RustedSciThe::numerical::BVP_Damp::generated_solver_handoff::{
AotBuildPolicy, GeneratedBackendConfig,
};
let generated = GeneratedBackendConfig::banded_atomview_for_repeated_solves()
.with_aot_build_policy(AotBuildPolicy::RequirePrebuilt);
```
`RequirePrebuilt` намеренно падает typed error, если артефакт отсутствует. Это лучше, чем неожиданно начать компиляцию внутри production run.
## 12. Практические рекомендации по выбору
Если вы пишете новую постановку BVP, начните с Lambdify. Он проще, прозрачнее и быстрее дает ответ на вопрос "правильны ли уравнения и граничные условия?".
Если у вас есть готовая Rust-модель в виде RHS-замыкания и вы не хотите использовать символику, выбирайте Numerical path. Используйте `new_numeric_fd_with_options`, когда поддерживать аналитический якобиан невыгодно, и `new_numeric_with_jacobian_options`, когда непрерывный `df/dy` доступен и хочется избежать шума конечных разностей. Для очень жестких или плохо масштабированных символических моделей Lambdify/AOT может быть предпочтительнее, потому что якобиан механически выводится из `Expr`-источника истины.
Если задача большая и символическая, проверяйте AOT с `tcc` одновременно с Lambdify, а не только после того, как возникнет repeated-solve сценарий. В исправленном `AtomView` пути TCC AOT уже может не уступать или превосходить Lambdify в честном cold end-to-end расчете. Для BVP/PDE-like задач с локальным stencil почти всегда стоит проверить Banded; для неструктурированных sparse-задач выбирайте Sparse. Для маленького solve по-прежнему начинайте с Lambdify, потому что build/bootstrap легко съедает весь runtime-выигрыш.
Если одну и ту же модель предстоит решать многократно, разделяйте создание и
использование артефакта явно: создавайте либо проверяйте его через
`BuildIfMissing`, а в измеряемом или production-цикле используйте
`RequirePrebuilt`. В измеренном Damped Banded случае combustion-1000 такой
warm-путь оказался примерно в `1.09x` быстрее Lambdify на одно решение, однако
потребовал нескольких повторений для компенсации первой сборки.
Если вы выбираете между Sparse и Banded, сначала спросите себя, откуда берется якобиан. Если переменная на узле связана только с соседними узлами, Banded почти наверняка кандидат номер один. Если связность дальняя или нерегулярная, Sparse безопаснее.
Если вы выбираете toolchain для AOT, первым кандидатом сейчас разумно делать `tcc`: он дал лучший practical cold profile в наших больших BVP-прогонах. `gcc`, Zig и Rust backend остаются важными контрольными и платформенно-зависимыми альтернативами, но их не следует молча считать default для performance.
Если вы выбираете chunking, не включайте parallel "на веру". Используйте story/perf tests, смотрите `callback residual values`, `callback jacobian values`, `linear_ms`, `solver_total_ms`, `actual_jobs` и `fallback`. Наши текущие измерения показывают одновременно две вещи: реальный параллелизм доказан, и на Banded `n_steps = 3000` он заметно ускоряет callback values; однако полный cold wall clock может остаться почти тем же. Поэтому для библиотечного default подходит `Auto`, а не forced `chunk4`.
## 13. Как читать statistics и story-таблицы
`solver.get_statistics()` возвращает счетчики и таймеры и для Damped, и для Frozen солвера. Frozen остается другой нелинейной стратегией, однако теперь показывает те же факты о generated backend, которые нужны для честного сравнения Lambdify/AOT: выбранный backend, символьный frontend, стадии handoff-подготовки, callback-диагностику, итерации Ньютона, пересборки якобиана и решения линейной системы. В BVP story-тестах особенно полезны:
`linear_ms` - время решения линейных систем Ньютона.
`jac_ms` - время подготовки/вычисления якобиана на уровне solver statistics.
`fun_ms` - время вычисления невязки.
`Callback Residual Values` и `Callback Jacobian Values` - более низкоуровневые таймеры compiled/lambdified callbacks.
`Callback Jacobian Matrix Assembly` - сборка матрицы из values, часто важнее самой арифметики values.
`iters`, `linsys`, `jac_re` - число итераций Ньютона, линейных solve и пересборок якобиана.
Для generated AOT route тот же объект statistics содержит карту `diagnostics`. Самые полезные ключи: `generated.selected_backend`, `generated.handoff.initial_generate_wall_ms`, `generated.handoff.build_policy_wall_ms`, `generated.handoff.post_build_rebind_wall_ms`, `aot.runtime.execution_policy`, `aot.runtime.parallel_requested`, `aot.runtime.residual.actual_jobs`, `aot.runtime.residual.fallback_reason`, `aot.runtime.sparse_jacobian.actual_jobs`, `aot.runtime.sparse_jacobian.fallback_reason`, а также соответствующие `work_per_job` и `work_per_chunk`. Это не просто повтор пользовательской настройки, а фактическая runtime-диагностика: если `Auto` или threshold policy ушли в sequential fallback, statistics покажет это напрямую.
В старых таблицах этот подготовительный этап может называться `Symbolic Operations`; новый код дополнительно экспортирует тот же таймер как `Backend Preparation`. Alias оставлен намеренно. В Lambdify/AOT это действительно символическая/codegen подготовка, а в Numerical path - численная дискретизация и backend handoff, а не символическая работа.
По результатам текущих story-тестов для combustion BVP складывается следующая рабочая картина. Для `n_steps = 200` cold AOT еще легко теряет время на подготовке. Для `n_steps = 1000` после перехода на `AtomView` cold TCC AOT уже обогнал Lambdify в зафиксированном сравнении и на Sparse, и на Banded пути; в отдельном warm Damped Banded прогоне со строгим `RequirePrebuilt` и паузами TCC также оказался быстрее Lambdify примерно на `7.9%`. Для Banded `n_steps = 3000` production-frontend `AtomView` удержал решение на уровне roundoff и показал TCC AOT примерно на одном или лучшем уровне полного cold wall clock, хотя Lambdify-строка в этом опыте была шумной. Banded остается естественным выбором для узкой полосы, а chunking следует включать через `Auto` либо после отдельного измерения: он ускоряет hot callback work, но не гарантирует пропорционального ускорения всего solve.
Это не маркетинговые обещания, а engineering conclusions. Запускайте story-тесты на своей машине, особенно если меняете toolchain, число потоков, размер сетки или структуру уравнений.
## 14. Запускаемые guide-examples
В каталоге `examples/` теперь есть небольшие исполняемые приложения к этой главе. Каждое решает краевую задачу с известным аналитическим решением, поэтому показывает не только синтаксис настройки, но и немедленную проверку корректности. Для Damped представлены чисто численный путь (pure numerical path), символьная ламбдификация (Lambdify) и AOT; для Frozen представлены поддерживаемые символьные пути Lambdify и AOT. Отдельный пример про numerical route для Frozen поясняет, почему задачу, заданную замыканиями, следует отправлять в Damped, а не маскировать пустой символьной заглушкой.
```powershell
cargo run --example bvp_damped_numerical_guide
cargo run --example bvp_damped_lambdify_guide
cargo run --example bvp_damped_aot_guide
cargo run --example bvp_frozen_lambdify_guide
cargo run --example bvp_frozen_aot_guide
cargo run --example bvp_frozen_numerical_route_guide
```
В AOT-примерах намеренно показаны две фазы жизни артефакта: первое решение с `BuildIfMissing`, а затем решение с `RequirePrebuilt`. Для их запуска нужен `tcc` в `PATH`; если компилятор не установлен, пример сообщает о пропуске AOT-демонстрации, а не выдает отсутствие toolchain за ошибку солвера.
## 15. Где смотреть примеры и регрессии
Suite разделен по назначению в `src/numerical/BVP_Damp/tests/`:
- `basic_correctness.rs` и `classic_examples.rs` - быстрые correctness-проверки
в debug.
- `lambdify_acceptance.rs`, `lambdify_cross_product.rs`,
`lambdify_lifecycle.rs`, `parity_corpus.rs` и `parameter_rebind.rs` -
AtomView/ExprLegacy, matrix backends, execution policies и invalidation.
- `aot_diagnostics.rs`, `aot_runtime_contract.rs` и `aot_race_stress.rs` -
архитектурные и lifecycle story-тесты; дорогие части обычно помечены
`#[ignore]` и запускаются явно.
- `telemetry_story.rs`, `backend_compare.rs` и `frozen_runtime_story.rs` -
отдельные диагностические и performance-прогоны.
Например:
```powershell
cargo test --lib --no-default-features numerical::BVP_Damp::test_correctness -- --nocapture --test-threads=1
cargo test --lib --no-default-features numerical::BVP_Damp::test_lambdify_lifecycle -- --nocapture --test-threads=1
cargo test --lib --no-default-features numerical::BVP_Damp::test_parity_corpus -- --nocapture --test-threads=1
cargo test --lib --no-default-features numerical::BVP_Damp::test_aot_runtime_contract -- --nocapture --test-threads=1
```
Ignored release story-тесты отделены от debug correctness:
```powershell
cargo test --release --lib --no-default-features numerical::BVP_Damp::test_aot_diagnostics -- --ignored --nocapture --test-threads=1
cargo test --release --lib --no-default-features numerical::BVP_Damp::test_aot_race_stress -- --ignored --nocapture --test-threads=1
```
Подробные тесты дублируют захваченный вывод в `test_reports/BVP_Damp*`.
Это generated output, а не вход Cargo; в нем есть имя теста, timestamp и
статус `passed/failed`. Сводка story-тестов ведется в
[`BVP_DAMP_STORY_TESTS.md`](BVP_DAMP_STORY_TESTS.md), а исторические
performance-, correctness- и AOT-сравнения остаются в отдельных story-файлах.
Низкоуровневый codegen/performance слой дополнительно документируется в [`../../symbolic/codegen/tests/BVP_CODEGEN_STORY_TESTS.md`](../../symbolic/codegen/tests/BVP_CODEGEN_STORY_TESTS.md). Это полезно, когда нужно понять, проблема в самом BVP solver loop или в generated callback backend.
В рабочем коде лучше использовать `try_solve()` вместо `solve()`: первый возвращает typed errors, второй оставлен как compatibility wrapper и паникует при ошибках. Для production BVP-задач typed error почти всегда лучше, чем красивый panic с грустной музыкой.
## 16. Типизированное логирование решений
Числовая телеметрия и логирование решений разделены. Typed-снимок
`telemetry` содержит счетчики и таймеры стадий, а опциональный trace объясняет
выбор или fallback backend, обновление якобиана, отклонение damping-шага,
уточнение сетки, ограничение шага bounds, non-finite значения и завершение.
```rust
use RustedSciThe::numerical::BVP_Damp::{
BvpLoggingConfig, BvpLoggingMode, BvpTelemetryMode,
};
use RustedSciThe::numerical::BVP_Damp::generated_solver_handoff::GeneratedBackendConfig;
let config = GeneratedBackendConfig::banded_lambdify_defaults()
// Counters — совместимый режим; Off отключает сбор счетчиков и таймеров.
.with_bvp_telemetry_mode(BvpTelemetryMode::Off)
.with_bvp_logging_config(
BvpLoggingConfig::new(BvpLoggingMode::Warnings).with_max_events(128),
);
// Передайте config через builder солвера или set_bvp_logging_config.
let trace = &solver.get_statistics().telemetry;
println!("solve_id={} dropped={}", trace.solve_id, trace.log_events_dropped);
```
`BvpTelemetryMode::Off` отключает счетчики solver-а, stage timers и timing
подстадий callback без изменения численного алгоритма. Режим telemetry
независим от `BvpLoggingMode`: можно оставить ограниченный decision trace,
не включая подробный сбор счетчиков и времени. `Counters` остается режимом
по умолчанию для совместимости, а `Detailed` дополнительно собирает timing
подстадий callback.
По умолчанию действует `Off`, и события не сохраняются. `Warnings` оставляет
восстанавливаемые и неуспешные решения, `Detailed` также сохраняет обычные
адаптивные решения. Буфер ограничен на каждый solve, поэтому большая задача не
может бесконечно наращивать память диагностики. При достижении лимита проверяйте
`log_events_dropped`.
## Запуск BVP-задач из текстовых документов
Текстовый task-document — это удобный слой над нативным API BVP_Damp для
воспроизводимых экспериментов и пакетных запусков. В прикладном коде лучше
использовать типизированный builder API из предыдущих разделов. Документ может
задать солвер, layout матрицы, символьный маршрут, режим исполнения,
допуски, continuation и политику вывода.
```text
# BVP_Damp task: комментарии игнорируются парсером.
solver: BVP
strategy: Damped
method: Sparse
frontend: AtomViewNative
execution: Lambdify
tolerance: 1e-6
max_iterations: 100
parameters:
k: 2.5
continuation:
parameter: k
values: 1.0, 1.5, 2.0, 2.5
mode: prepared
restart_each: false
output_policy: none
```
`solver: BVP` выбирает адаптер BVP_Damp. `strategy` задаёт нелинейную политику
(`Damped`, `Frozen` или `Naive`), а `method` — layout линейной алгебры
(`Dense`, `Sparse` или `Banded`). Это независимые измерения: layout не является
символьным frontend, а strategy не является execution backend.
`frontend` выбирает символьное представление. `ExprLegacy` использует
expression-oriented маршрут; `AtomViewNative` переводит residual и Jacobian на
нативный AtomView и не возвращает подготовленный граф обратно в Expr. Параметр
`execution` выбирает `Lambdify` или `AOT`; `Lambdify` — режим исполнения, а не
ещё один frontend. Для большой разреженной задачи обычно разумно начать с
`AtomViewNative` и `Lambdify`, а для маленькой полностью связанной задачи — с
`ExprLegacy` и `Dense`. Универсально быстрого выбора нет: сравнивайте одинаковые
задачи и выбирайте маршрут, который быстрее и устойчивее на реальной модели.
Для AOT можно дополнительно задать `aot_build_policy`
(`BuildIfMissing`, `RequirePrebuilt` или `RebuildAlways`), `output_dir`,
`compiler`, `profile` и каталог publication/handoff. `RequirePrebuilt` ничего
не компилирует; отсутствие или устаревание артефакта, неверная provenance,
ошибка compiler/link или timeout должны возвращаться типизированной ошибкой,
а не скрытым переходом на другой маршрут. В lifecycle-отчёте отдельно видны
cache hit/miss, build/link attempts, publication и runtime readiness.
Блок `continuation` описывает семейство BVP-задач. `mode: fresh` заново
подготавливает каждый сегмент, `warm` переиспользует подготовленную модель с
новыми параметрами, а `prepared` запрашивает максимально сильное повторное
использование. `restart_each` определяет, начинается ли каждый сегмент с
исходной сетки и начального приближения. Для каждого сегмента можно задать
собственные `y0`, `t0` и `t_end`; это настоящий restart-контракт, а не просто
изменение параметра. Для обычного continuation используйте монотонные значения,
если немонотонный sweep не является намеренной частью эксперимента.
`output_policy` принимает `none`, `terminal`, `plotters` или `gnuplot`; старый
ключ `plot` поддерживается как compatibility alias. Комментарии, начинающиеся
с `#`, игнорируются, поэтому пояснения можно оставлять прямо в task-файле.
Эталонные документы находятся в `examples/task_docs/`. Общая грамматика,
типизированные diagnostics и правила batch-runner описаны в
[`TASK_DOCS_GUIDE_EN.md`](../../command_interpreter/TASK_DOCS_GUIDE_EN.md).