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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# Руководство по нелинейным системам

## Рекомендуемый типизированный путь

В новом коде лучше один раз подготовить символьную систему и отделить
численные значения параметров от этой подготовки:

```rust
use nalgebra::DVector;
use RustedSciThe::numerical::Nonlinear_systems::prelude::*;

let prepared = PreparedSymbolicNonlinearProblem::from_strings(
    vec!["a*x + y - 3".into(), "x - y".into()],
    SymbolicProblemOptions::new()
        .with_variables(vec!["x".into(), "y".into()])
        .with_equation_parameters(vec!["a".into()]),
)?;

let bound = prepared.bind_values(DVector::from_vec(vec![2.0]))?;
let options = SolveOptions {
    tolerance: 1e-10,
    max_iterations: 64,
    diagnostics: DiagnosticsOptions {
        collect_history: false,
        collect_statistics: true,
        ..DiagnosticsOptions::default()
    },
    ..SolveOptions::default()
};
let result = NonlinearSolverMethod::DampedNewton(DampedNewtonMethod::default())
    .solve(&bound, DVector::from_vec(vec![1.0, 1.0]), options)?;
```

`PreparedSymbolicNonlinearProblem` владеет разобранными уравнениями, порядком
переменных, подготовкой символьного якобиана и вызываемым backend. Метод
`bind_values` проверяет численный вектор по объявленной схеме параметров и
возвращает заимствованный bound-view. Перебор других значений не повторяет
символьную подготовку: один prepared-объект можно использовать для
continuation-подобного перебора и разных начальных приближений.

`SymbolicProblemOptions::with_lambdify_backend()` является явным выбором
внутреннего символьного пути по умолчанию. Для AOT действует общий жизненный
цикл: артефакт нужно материализовать, собрать, зарегистрировать и связать с
процессом до запуска AOT-only задачи. `BuildIfMissing` и `RequirePrebuilt` это
политики жизненного цикла артефакта, а не два разных численных метода; детали
проверенного контракта приведены в `STORY_TESTS.md`.

### Обновление параметров и структурная пересборка

Порядок имен, переданный в `with_equation_parameters`, является ABI параметров.
Вызов `bind_values` (или совместимый `set_parameter_values`) меняет только
численные значения уже существующей схемы. Он не разбирает уравнения заново,
не дифференцирует и не ламбдифицирует их, не пересобирает AOT-артефакт и не
меняет порядок переменных. Изменение уравнений, переменных, имен или порядка
параметров, а также generated chunking является структурным изменением и
требует подготовки нового объекта.

### Диагностика отдельных попыток

При включенной статистике `SolveStatistics::attempts` содержит по одной
записи на каждую внешнюю нелинейную итерацию после начального вычисления
состояния. Счетчики имеют тот же solver-level смысл, что и агрегированные
поля: одно вычисление невязки или якобиана означает один полный запрос к
провайдеру, даже если AOT-провайдер внутри выполняет несколько generated jobs.
Запись содержит также refresh/reuse якобиана, факторизации, линейные решения,
принятые и отвергнутые trial-шаги и суммарные времена стадий:

```rust
for attempt in &result.statistics.attempts {
    println!(
        "iteration={} R/J={}/{} refresh/reuse={}/{} factor/linear={}/{}",
        attempt.iteration,
        attempt.residual_evaluations,
        attempt.jacobian_evaluations,
        attempt.jacobian_refreshes,
        attempt.jacobian_reuses,
        attempt.linear_factorizations,
        attempt.linear_solves,
    );
}
```

Начальное вычисление невязки и якобиана входит в агрегированную статистику,
но не в запись отдельной попытки. `termination_retries` явно равен `None` в
generic engine, поскольку у него нет отдельной границы повторов завершения,
специфичной для метода. Внутренние generated job/chunk принадлежат
`SymbolicPreparationReport`, а не solver-счетчикам. При отключенной статистике
`attempts` пуст, а поля счетчиков и времени не являются измерениями.

### Подробная телеметрия подготовки

Статистика солвера описывает численные итерации. Если нужно отдельно измерять
стоимость построения подготовленной символьной задачи, включите опциональную
телеметрию подготовки:

```rust
let prepared = PreparedSymbolicNonlinearProblem::from_strings(
    equations,
    SymbolicProblemOptions::new()
        .with_variables(variables)
        .with_preparation_telemetry(PreparationTelemetryMode::Collect),
)?;

if let Some(report) = prepared.preparation_report().detailed.as_ref() {
    println!(
        "preparation total={:?}, input={:?}, mode={:?}",
        report.total_wall_time, report.input_kind, report.execution_mode,
    );
    for stage in &report.stages {
        println!("{:?}: {:?}", stage.stage, stage.wall_time);
    }
}
```

Агрегированный `SymbolicPreparationReport` доступен и при отключенной
подробной телеметрии. Время стадии является исключительным, если реализация
может выделить его отдельно; `None` означает, что стадия неприменима или ее
нельзя надежно изолировать, а не нулевое время. Поле
`unattributed_wall_time` содержит остаток времени подготовки. Обновление
значений параметров не повторяет подготовку и не создает новый отчет о
подготовке.

Для диагностического контура, которому нужно сохранить границу ошибки,
используйте подробный конструктор. Он возвращает `SymbolicPreparationFailure`,
содержащий исходный `SolveError` и завершенные к моменту ошибки стадии:

```rust
let attempt = SymbolicNonlinearProblem::from_strings_with_options_detailed(
    equations,
    SymbolicProblemOptions::new()
        .with_variables(variables)
        .with_preparation_telemetry(PreparationTelemetryMode::Collect),
);
if let Err(failure) = attempt {
    eprintln!("{}; telemetry={:?}", failure, failure.telemetry);
}
```

Совместимые конструкторы не меняются и по-прежнему возвращают непосредственно
`SolveError`. Failure telemetry предназначена для отчетов и отладки и не
превращает неудачную подготовку в пригодную для решения задачу.

### Опциональное логирование солвера

Логирование солвера по умолчанию отключено. Настройка действует только в
рамках одного запуска `SolverEngine` и не меняет глобальный logger процесса:

```rust
let quiet = SolveOptions::default();
let verbose = SolveOptions::default().with_logging(EngineLogLevel::Debug);
let quiet_again = verbose.without_logging();
```

Та же политика доступна у `DiagnosticsOptions` через методы
`with_logging(...)` и `without_logging()`. Диагностика runtime-методов
использует этот публичный параметр и не пишет напрямую в stdout. Поэтому
обычный путь не выполняет форматирование и отправку log-записей, а приложение
с установленным фасадом `log` может явно включить уровни `Info`, `Warn` или
`Debug`.

### Жизненный цикл AOT-артефакта

Для холодного production-подобного запуска задайте каталог вывода и
используйте `BuildIfMissing`:

```rust
let cold = SymbolicNonlinearProblem::from_strings_with_generated_backend(
    equations.clone(),
    problem_options.clone(),
    SymbolicGeneratedBackendConfig::build_if_missing_release(&output_dir),
)?;
let resolver = cold.updated_resolver.clone();
println!(
    "cold: backend={:?}, action={:?}, manifest_key={:?}, lifecycle_key={:?}, build={:?}",
    cold.selected_backend,
    cold.preparation_report.artifact_action,
    cold.preparation_report.artifact_key,
    cold.preparation_report.artifact_lifecycle_key,
    cold.preparation_report.build_duration,
);
```

После публикации артефакта строгий теплый запуск использует `RequirePrebuilt`
и ничего не компилирует:

```rust
let warm = SymbolicNonlinearProblem::from_strings_with_generated_backend(
    equations,
    problem_options,
    SymbolicGeneratedBackendConfig::require_prebuilt().with_resolver(resolver),
)?;
assert!(warm.build_result.is_none());
assert_eq!(warm.preparation_report.artifact_action, SymbolicArtifactAction::Reused);
```

`RequirePrebuilt` возвращает типизированную ошибку для отсутствующего,
несовместимого или не связанного артефакта и не делает молчаливый fallback на
Lambdify. `resolver` является снимком внутри процесса; новый процесс должен
найти опубликованный совместимый артефакт в настроенном каталоге. После любой
из этих стадий обновление параметров остается дешевой численной операцией.

## Выбор политики выполнения и backend

### Последовательный и параллельный Lambdify

Политику выполнения callback можно задать при подготовке символьной задачи:

```rust
let sequential = SymbolicProblemOptions::new()
    .with_variables(variables.clone())
    .with_lambdify_execution_policy(LambdifyExecutionPolicy::Sequential);

let parallel = SymbolicProblemOptions::new()
    .with_variables(variables)
    .with_lambdify_execution_policy(LambdifyExecutionPolicy::Parallel {
        min_work: 1_500,
    });
```

`Sequential` является режимом по умолчанию и первым выбором для небольших
или разреженных систем. `Parallel { min_work }` включается явно и запускает
распараллеливание только когда подготовленный объем работы достигает порога.
`min_work` является правилом runtime-диспетчеризации, а не параметром
сходимости или точности. Начните с `Sequential`, затем измерьте настоящую
структуру невязки и якобиана на целевой машине и только после этого выбирайте
`min_work`.

Это подтверждается release-корпусом. При размерности `512` подготовленный
Sequential был самым быстрым полным Newton-маршрутом во всех шести строках
большого корпуса. Parallel был полезен для широкого `band-five` callback и
оказался быстрее legacy-маршрута, но в полном решении все еще уступал
подготовленному Sequential. В sweep по порогу `Parallel { min_work: 1 }`
был примерно на `9-10%` медленнее Sequential, а включение Parallel ровно на
границе structural `nnz` замедляло путь еще сильнее. Доказательства приведены
в Sections 44 и 45 `STORY_TESTS.md`.

### Почему новый якобиан не использует Mutex

Подготовленный якобиан уже знает независимую структуру вычисления ненулевых
элементов. Новый parallel-путь распределяет независимые строки/элементы по
worker-потокам и пишет непосредственно в принадлежащую вызывающей стороне
матрицу `DMatrix`. Поэтому ему не нужны общий аккумулятор, поэлементный
`Mutex` или промежуточная dense-матрица. Legacy compatibility-путь сохраняет
старую семантику allocated-return и mutex/Rayon dispatch, чтобы не ломать
существующих пользователей.

Это одновременно архитектурное и производительное решение. На release
корпусе `band-five`, размерность `512`, legacy Jacobian занимал `1230.7 us`,
подготовленный Sequential `397.15 us`, а новый mutex-free Parallel `426.31 us`.
То есть Parallel был примерно в `2.89x` быстрее legacy, хотя Sequential был
еще быстрее. Parallel не обязан быть быстрее Sequential: для дешевых
элементов стоимость запуска worker-потоков перекрывает выигрыш. Корректность
и детерминированность разреженной раскладки проверяются в Sections 41 и 42.

### Lambdify и AOT: когда наступает break-even

Выбирайте Lambdify, если важны короткий запуск, переносимость и небольшое
число решаемых задач. Выбирайте AOT, если доступен toolchain, артефакт можно
сохранить и переиспользовать, а выигрыш callback подтвержден именно для вашей
задачи. Первый AOT-запуск включает materialization, compilation, linking и
publication; эту стоимость нельзя смешивать с warm solve.

Грубая оценка числа warm-решений до окупаемости:

```text
break_even_solves ~=
    (AOT preparation + build - Lambdify preparation)
    / (Lambdify warm solve - AOT warm solve)
```

Знаменатель должен быть положительным. Если AOT в warm-режиме медленнее,
для данной конфигурации break-even отсутствует. В release large-story при
`n=512` warm total составил `17.242 ms` у Lambdify и `17.835 ms` у AOT;
якобиан AOT занимал `2.913 ms` против `2.296 ms`, а невязка и linear stage
были близки. При `n=128` AOT был быстрее (`0.397 ms` против `0.516 ms`), но
стоимость build около `365 ms` означает тысячи повторных решений до
амортизации. Поэтому текущие данные не дают универсального утверждения
«AOT быстрее». Они подтверждают AOT как deployment/lifecycle-вариант с
зависящим от workload break-even. См. Sections 53, 64 и 65.

### Архитектурные решения и их обоснование

- **Разделение prepared и bound:** parsing, differentiation и построение
  callback выполняются один раз; новая привязка параметров создает дешевое
  представление для solver-а. Поэтому parameter sweep должен переиспользовать
  prepared-объект.
- **Разделение solver-level и generated-job счетчиков:** один счетчик
  residual/Jacobian означает один полный запрос к provider. Внутренние AOT
  chunks/jobs являются backend detail, поэтому Lambdify и AOT сопоставимы.
- **Разделение manifest и lifecycle identity:** математический artifact key
  используется resolver-ом, а profile/compiler settings входят в on-disk
  lifecycle key. Debug-marker не может молча удовлетворить другому build.
- **Fail-closed RequirePrebuilt:** отсутствующий, устаревший, несовместимый или
  не связанный артефакт дает typed error; молчаливого fallback на Lambdify нет.
  Это делает deployment воспроизводимым.
- **Whole для compact dense AOT по умолчанию:** layout-profile при `n=512`
  показал, что `Whole` быстрее row chunks 32 и 64. Caller-side adaptation
  занимала `0.216 ms` из `0.301 ms` полного вызова якобиана. Это вывод для
  измеренного dense-пути, а не правило для sparse или banded задач.

### Стоимость телеметрии

Solver statistics и preparation telemetry отвечают на разные вопросы.
`collect_statistics` дает solver-level counters и stage timers, а
`collect_history` дополнительно сохраняет снимки итераций и может увеличить
расход памяти. Подробная телеметрия подготовки отдельно включается через
`PreparationTelemetryMode::Collect`. Release-аудит подготовки показал лишь
сотые доли миллисекунды разницы между disabled и enabled режимами при
размерностях `40`, `128` и `512`; диапазоны повторов перекрывались. Это не
является универсальным процентом стоимости warm solve. Для чистого hot-path
benchmark отключайте history/statistics, если сами диагностические данные не
являются предметом эксперимента.

## Ограничения и диагностика

Для решения в прямоугольной области используйте `SolveOptions::bounds`:

```rust
let bounds = Bounds::new(vec![(0.0, 3.0), (0.0, 2.0)])?;
let options = SolveOptions {
    bounds: Some(bounds),
    ..SolveOptions::default()
};
```

При `collect_statistics: true` поле `SolveResult::statistics` содержит
счетчики вызовов невязки, якобиана и линейного этапа, принятых и отвергнутых
шагов, а также суммарные времена стадий. `collect_history` включается
отдельно и может увеличивать память и количество аллокаций. Для измерения
горячего пути без истории диагностику следует отключать, если история не
является предметом эксперимента.

Для полностью численных задач реализуйте `NonlinearProblem` и
`JacobianProvider`, используя принадлежащие вызывающей стороне
`DVector`/`DMatrix`. Дополнительные методы `residual_into` и `jacobian_into`
позволяют заполнять буферы солвера и являются правильным путем для задач, где
аллокации важны. Математика метода при этом не меняется.

## Выбор метода

Обычный Newton обычно быстрее всего вблизи хорошо масштабированного решения,
но чувствительнее к плохому начальному приближению и вырожденному якобиану.
Damped Newton удобен, когда нужны backtracking или ограничения. Варианты
Levenberg-Marquardt и trust-region полезны для трудных, плохо масштабированных
или least-squares-подобных систем. Одно число wall-clock не ранжирует все
методы: сравнивайте сходимость, качество невязки, отвергнутые шаги и времена
стадий на том классе задач, который важен именно вам.

### Levenberg-Marquardt с backtracking

`BacktrackingLevenbergMarquardt` является самостоятельным вариантом наравне
с классическим LM, MINPACK, Nielsen и trust-region методами. Он решает
регуляризованную систему с единичным демпфированием:

`(J^T J + lambda I) delta = -J^T residual`

Затем проверяются допустимые пробные точки: сначала `alpha = 1`, после чего
`alpha` уменьшается вдвое, пока норма невязки строго не уменьшится или не
будет достигнуто `alpha_min`. После принятого шага `lambda` умножается на
`lambda_decrease`, а после полностью неудачного backtracking-поиска — на
`lambda_increase`. Значения по умолчанию равны `0.3` и `10.0`. Общий движок
по-прежнему объявляет `Converged` только при достижении заданной точности
невязки: принятый шаг сам по себе не является доказательством сходимости.

```rust
let method = NonlinearSolverMethod::BacktrackingLevenbergMarquardt(
    BacktrackingLevenbergMarquardtMethod::default(),
);
let result = method.solve(&problem, initial_guess, SolveOptions::default())?;
```

Выбирайте этот вариант, когда нужна именно политика единичного демпфирования
и строгого backtracking с учетом границ. Для классической настраиваемой
политики масштабирования используйте `LevenbergMarquardt`; варианты намеренно
разделены и не подменяют друг друга неявно.

## Аудит аллокаций

Запуск release-аудита на уровне процесса:

```text
cargo bench --bench nonlinear_systems_allocation_audit
```

Аудит охватывает размерности 32, 128 и 512, принятые решения без истории,
сравнение с историей на минимальной и максимальной размерностях, reusable
callback-буферы и отдельный rejection-heavy контроль. Он печатает число
аллокаций/деаллокаций и объем байт за полный lifetime результата решения.
Counting allocator меняет поведение процесса, поэтому его времена нужны только
для диагностики и не должны напрямую сравниваться с обычным Criterion или
прикладным wall-clock. Аудит не определяет каждый отдельный copy и не заменяет
измерение peak resident memory; это материал для гипотезы оптимизации, после
которой обязательны correctness-тесты.

Современный executable-пример:

```text
cargo run --example rus_nonlinear_systems_modern_guide
```

Примеры совместимости и AOT lifecycle:

```text
cargo run --example nonlinear_lambdify_legacy_guide
cargo run --example nonlinear_lambdify_prepared_guide
cargo run --example rus_nonlinear_aot_lifecycle_guide
```

Старый `nonlinear_systems_guide` оставлен как compatibility-пример legacy
LM-wrapper.

### Выбор символьного frontend

Плотный Lambdify backend поддерживает два frontend-пути:

```rust
let expr_legacy = SymbolicProblemOptions::new()
    .with_lambdify_frontend(SymbolicLambdifyFrontend::ExprLegacy);
let atom_native = SymbolicProblemOptions::new()
    .with_atom_native_frontend();
```

`ExprLegacy` остается совместимым выбором по умолчанию. `AtomViewNative` один
раз преобразует публичные `Expr` в Atom, выполняет дифференцирование
Якобиана на Atom-представлении и записывает невязку и Якобиан в плотные буферы
вызывающей стороны. Плотный AOT поддерживает оба frontend-а: `ExprLegacy` и
`AtomViewNative`; во втором случае генерация невязки и Якобиана остается на
Atom-представлении и использует тот же dense ABI. Для AOT сейчас требуется
цельная (`Whole`) стратегия chunking.

Этот решатель намеренно работает с плотными матрицами. Для Sparse и Banded
следует использовать ODE-солверы, например LSODE2, где layout-specific
линейные backend-ы являются частью контракта.

## Прямоугольные задачи least-squares

Канонический least-squares-солвер минимизирует вектор невязок `r(x)`. Он
подходит для переопределённых и недоопределённых моделей, шумных наблюдений и
задач, где невозможно одновременно обратить все невязки в ноль. Это другой
контракт, чем поиск корня: root solver ищет `r(x) = 0`, а LM минимизирует
`0.5 * ||r(x)||^2` и возвращает итоговую целевую функцию. Для несогласованной
аппроксимации ненулевая итоговая невязка ожидаема.

Общий селектор `NonlinearSolver` содержит отдельные варианты `Root` и
`LeastSquares`. Least-squares **намеренно не является** вариантом
`NonlinearSolverMethod`: этот enum используется движком поиска корня для
квадратной системы и возвращает `SolveResult`; прямоугольный least-squares
принимает собственный контракт задачи и возвращает `MinimizationReport`.

### Численные callbacks невязки и Якобиана

Реализуйте `LeastSquaresProblem` или используйте
`ClosureLeastSquaresProblem`, задав текущий вектор параметров, невязку и её
Якобиан. Ниже аппроксимируется прямая по трём наблюдениям. Данные
несогласованы, поэтому в оптимуме останется небольшая ненулевая невязка:

```rust
use nalgebra::{DMatrix, DVector};
use RustedSciThe::numerical::Nonlinear_systems::prelude::*;

let problem = ClosureLeastSquaresProblem::new(
    DVector::from_vec(vec![0.0, 0.0]), // [свободный член, наклон]
    |p| DVector::from_vec(vec![p[0] - 1.0, p[0] + p[1] - 2.0, p[0] + 2.0 * p[1] - 2.9]),
    |_| DMatrix::from_row_slice(3, 2, &[1.0, 0.0, 1.0, 1.0, 1.0, 2.0]),
);

let method = NonlinearSolver::LeastSquares(
    LeastSquaresLevenbergMarquardt::new().with_tol(1e-10),
);
let (problem, report) = method.try_minimize_least_squares(problem)?;
assert!(report.termination.was_successful());
println!("fit={:?}, objective={:e}", problem.params(), report.objective_function);
```

`try_minimize_least_squares` сохраняет типизированные ошибки численного
метода и callbacks. Сходимость и другие допустимые исходы записаны в
`report.termination`; проверяйте `was_successful()`, а не считайте любой
возвращённый отчёт успешной подгонкой. Runtime-телеметрия по умолчанию
отключена; её можно включить через `LeastSquaresTelemetryMode::Counters` или
`Detailed` в настройках LM.

### Символьный least-squares builder

Для символьных невязок `SymbolicLeastSquaresSolver` подготавливает невязку и
Якобиан, после чего использует тот же канонический LM core. Builder принимает
начальное приближение, имена неизвестных, параметризованные уравнения,
настройку телеметрии и явное объявление положительных переменных:

```rust
use RustedSciThe::numerical::Nonlinear_systems::prelude::*;

let mut solver = SymbolicLeastSquaresSolver::new()
    .with_equations_str(vec![
        "a - 1".into(),
        "a + b - 2".into(),
        "a + 2*b - 2.9".into(),
    ])
    .with_unknowns(vec!["a".into(), "b".into()])
    .with_initial_guess(vec![0.0, 0.0])
    .with_tolerance(1e-10)
    .with_telemetry(LeastSquaresTelemetryMode::Detailed);

let report = solver.try_solve()?;
if report.termination.was_successful() {
    let coefficients = solver.map_of_solutions.as_ref().expect("успешная аппроксимация");
    println!("{coefficients:?}; objective={:e}", report.objective_function);
}
```

Для типизированной обработки ошибок используйте `try_solve` и
`try_minimize_least_squares`. `set_positive_variables` — явная политика
пользователя для любой задачи, в которой выбранные переменные должны
оставаться строго положительными; солвер сам не выводит ограничения из
предметной области. Это может быть полезно для логарифмов и других выражений
с ограниченной областью определения, но не ограничивается химическими
задачами.

### Повторное использование параметризованной символьной модели

Объявляйте параметры уравнений отдельно от неизвестных. Каждый вызов
`try_solve_with_params` привязывает новые численные значения и повторно
использует подготовленные символьные невязку и Якобиан, не выполняя символьную
подготовку заново. Wrapper использует заданное в нём начальное приближение для
каждого вызова, поэтому это отдельные fresh-решения, а не warm-start
continuation от предыдущего результата:

```rust
let mut solver = SymbolicLeastSquaresSolver::new()
    .with_equations_str(vec!["a - target".into(), "2*a - 2*target".into()])
    .with_unknowns(vec!["a".into()])
    .with_parameters(vec!["target".into()])
    .with_initial_guess(vec![0.0]);

for target in [1.0, 2.0, 3.0] {
    let report = solver.try_solve_with_params(vec![target])?;
    assert!(report.termination.was_successful());
    println!("target={target}, fit={:?}", solver.map_of_solutions);
}
```