fandhe-ai 0.3.0

Facade crate for a from-scratch Rust AI/ML library: tensors, autodiff, and CPU/CUDA/Metal backends behind one composition root
Documentation
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
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
//! Keras `Sequential` 慣習のレイヤー積み上げビルダー(TASK-9.2a・
//! #95。TASK-9.4・#411 で `fandhe_ai_autodiff::compat` から本クレートへ移設)。
//! 数値ロジックは一切持たず、`fandhe_ai_autodiff::nn::Module` 実装
//! (`Linear`・`Relu`・`Sigmoid`・`Tanh`)をメソッドチェーンで積み上げ、
//! `forward`/`predict` で `nn::Module::forward` へ委譲するだけの薄い
//! ビルダー(REQ-9)。対象レイヤーは `docs/compat-api-scope.md` §1 の
//! 3 種限定(Linear・ReLU/Sigmoid/Tanh。Softmax・GELU・Conv 等は範囲
//! 拡張の手続き〈同 §5〉を経ずに追加しない)。
//!
//! **学習(勾配取得・パラメータ更新。#294 で対応済み)**: [`Sequential::bind`]
//! が返す [`SequentialVars`] を経由して `LinearVars`(勾配取得の入口。
//! `Tape::backward` 後に `Gradients::get(&vars.weight)` する経路)へ
//! アクセスできる。[`Sequential::trainable_parameters`]/
//! [`Sequential::apply_parameters`] と組み合わせ、`fandhe_ai_autodiff::optim::Sgd`・
//! `fandhe_ai_autodiff::nn::optim::AdamW` の位置対応契約にそのまま渡せる。
//!
//! **`predict` の既定結線(TASK-9.4・#411)**: `predict` は本クレートの
//! composition root([`crate::tape`]。既定 CPU・`CpuBackendOps`・融合
//! 有効)で `Tape` を構築して forward する。旧 `fandhe_ai_autodiff::compat` 版が
//! 依存していた naive 参照実装(`fandhe_ai_autodiff::default_ops::naive_ops()`。
//! クレート非公開)は facade から到達できないため、この結線先の変更に
//! 伴い旧 `predict_with_ops`(任意 `BackendOps` 注入経路)は公開面から
//! 撤去した(REQ-12「任意 `BackendOps` 実装を注入できる公開 API を
//! 設けない」・`crates/facade/tests/api_surface.rs` の機械検査と整合。
//! `compat/mod.rs` モジュール doc 参照)。ops を明示的に選びたい内部用途
//! は [`Sequential::forward`]([`crate::Tape`] を受け取るだけで
//! `BackendOps` は受け取らない)へ、呼び出し元が任意に構築した `Tape` を
//! 渡せば足りる。
//!
//! **公開シグネチャの型(codex-review PR #424 P1 是正)**: `forward`/
//! `bind`/`predict` 等は `fandhe_ai_autodiff::Tape` を直接引数に取らず、本クレート
//! 所有の newtype [`crate::Tape`] を取る(`crate::lib.rs` モジュール doc
//! 「`Tape`(composition root が構築する値)の扱い」参照)。`Var`・
//! `Gradients`・`AutodiffError`・`LinearVars`・`Tensor` は `crate::`
//! 経由(facade の正式な再エクスポート)で参照する。

use crate::{AutodiffError, Gradients, LinearVars, Tape, Tensor, Var};
use fandhe_ai_autodiff::nn::activation::{Relu, Sigmoid, Tanh};
use fandhe_ai_autodiff::nn::{Linear, Module};

/// Keras `Sequential` 慣習のレイヤー積み上げビルダー。`add_*` はメソッド
/// チェーン(`self` を消費し `Self` を返す)で層を追加し、`predict` で
/// 推論を実行する。層は `nn::Module`(`fandhe_ai_autodiff::nn::module`)実装として
/// `Vec<Box<dyn Module>>` に格納するため、種類の異なる層(`Linear` と
/// 活性化関数)を同じ列で扱える。
pub struct Sequential {
    layers: Vec<Box<dyn Module>>,
}

impl Default for Sequential {
    fn default() -> Self {
        Self::new()
    }
}

impl Sequential {
    pub fn new() -> Self {
        Sequential { layers: Vec::new() }
    }

    /// 全結合層を追加する(bias あり既定。PyTorch `nn.Linear` の既定
    /// `bias=True` と揃える)。`Linear::new` が `Result` を返す
    /// (`in_features == 0` を拒否する。`fandhe_ai_autodiff::nn::linear` 参照)ため、
    /// 本メソッドも `Result<Self, AutodiffError>` を返し `?` で連鎖
    /// できるようにする。
    pub fn add_linear(
        mut self,
        in_features: usize,
        out_features: usize,
        seed: u64,
    ) -> Result<Self, AutodiffError> {
        let linear = Linear::new(in_features, out_features, true, seed)?;
        self.layers.push(Box::new(linear));
        Ok(self)
    }

    /// ReLU 層を追加する(`nn::activation::Relu`。shape 不変の演算のため
    /// 構造的に失敗しえず `Result` を返さない)。
    pub fn add_relu(mut self) -> Self {
        self.layers.push(Box::new(Relu));
        self
    }

    /// シグモイド層を追加する(`nn::activation::Sigmoid`)。
    pub fn add_sigmoid(mut self) -> Self {
        self.layers.push(Box::new(Sigmoid));
        self
    }

    /// 双曲線正接層を追加する(`nn::activation::Tanh`)。
    pub fn add_tanh(mut self) -> Self {
        self.layers.push(Box::new(Tanh));
        self
    }

    /// 積み上げた層を先頭から順に `Module::forward` へ委譲する。
    /// 呼び出し元が用意した `tape` 上で 1 回分の forward を計算する
    /// (`Linear::bind` がステップごとに葉ノードを登録し直す契約に従う。
    /// `fandhe_ai_autodiff::nn::module` 参照)。外部 `Tape` 上で呼ぶことで
    /// `Tape::backward` までグラフ記録がつながる(推論だけでなく
    /// grad check 等の用途にも使える)。
    pub fn forward<'t>(&self, tape: &'t Tape, input: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
        let mut current = *input;
        for layer in &self.layers {
            // `nn::Module::forward` は `&fandhe_ai_autodiff::Tape` を要求する
            // (`autodiff` クレート側の trait 定義のため facade newtype
            // には書き換えられない)。`tape.0`(`pub(crate)` フィールド)
            // 経由で内部の生 `Tape` を取り出す(本ファイル冒頭 doc
            // 「公開シグネチャの型」参照)。
            current = layer.forward(&tape.0, &current)?;
        }
        Ok(current)
    }

    /// 推論の入口(受け入れ条件「Sequential でのモデル構築・推論が
    /// 動作する」を満たす API)。内部で [`crate::tape`](本クレートの
    /// composition root。既定 CPU・`CpuBackendOps`・融合有効)で 1
    /// ステップ分の `Tape` を生成し `forward` を呼んだ後 `to_tensor()`
    /// で追跡を外した `Tensor<f32>` を返す(`Tape` はこの呼び出しの
    /// スコープ内で破棄される。`fandhe_ai_autodiff::nn::linear` の「`Tape` は
    /// ステップごとに生成・破棄される前提」と同じ運用)。
    ///
    /// **TASK-9.4(#411)**: 旧 `fandhe_ai_autodiff::compat` 版が持っていた
    /// `predict_with_ops`(任意 `BackendOps` 注入経路)は本移設で公開面
    /// から撤去した(モジュール doc 参照)。
    pub fn predict(&self, input: &Tensor<f32>) -> Result<Tensor<f32>, AutodiffError> {
        let tape = crate::tape();
        let input_var = tape.var(input);
        let output = self.forward(&tape, &input_var)?;
        Ok(output.to_tensor())
    }

    /// 学習ステップの入口(#294)。このステップの `tape` へ全 `Linear`
    /// 層の `weight`/`bias` を葉ノードとして登録し(`Linear::bind` を
    /// 層順に呼ぶ)、学習用 forward・勾配取得の入口となる
    /// [`SequentialVars`] を返す。
    ///
    /// 返る `SequentialVars` は `&'m Sequential`(`self`)と `&'t Tape`
    /// の両方を借用する。このハンドルが生きている間は
    /// [`Sequential::apply_parameters`](`&mut self` を要求)を呼べない
    /// (ステップ途中でのパラメータ書き換えを借用検査で静的に防ぐ設計。
    /// 呼び出し元は `bind` 以降の一連の処理をブロックスコープで囲み、
    /// スコープを抜けてから `apply_parameters` を呼ぶ運用とする。
    /// `crates/facade/tests/compat_sequential_train.rs` に実例がある)。
    pub fn bind<'m, 't>(&'m self, tape: &'t Tape) -> SequentialVars<'m, 't> {
        // `Linear::bind` も `&fandhe_ai_autodiff::Tape` を要求する(`forward` と
        // 同じ理由。`tape.0` 経由で取り出す)。
        let linears = self
            .layers
            .iter()
            .filter_map(|layer| layer.as_linear())
            .map(|linear| linear.bind(&tape.0))
            .collect();
        SequentialVars {
            model: self,
            linears,
        }
    }

    /// 学習可能パラメータ(`Linear` 層の `weight`/`bias`)への参照列を
    /// 層の追加順・各層内は weight → bias(`Some` の場合のみ)の順で
    /// 返す。`fandhe_ai_autodiff::optim::Sgd::step`/`fandhe_ai_autodiff::nn::optim::AdamW::step`
    /// の位置対応契約にそのまま渡せる。この順序契約は
    /// [`SequentialVars::trainable_vars`]/[`SequentialVars::trainable_grads`]/
    /// [`Sequential::apply_parameters`] と共通(#294 の設計不変条件)。
    pub fn trainable_parameters(&self) -> Vec<&Tensor<f32>> {
        let mut out = Vec::new();
        for layer in &self.layers {
            if let Some(linear) = layer.as_linear() {
                out.push(linear.weight());
                if let Some(bias) = linear.bias() {
                    out.push(bias);
                }
            }
        }
        out
    }

    /// optimizer(`Sgd::step`/`AdamW::step`)が返した更新後テンソル列を
    /// [`Sequential::trainable_parameters`] と同じ順序契約で各 `Linear`
    /// 層へ書き戻す。内部で `Linear::from_parameters`(`fandhe_ai_autodiff::nn::linear`)
    /// により層を再構築するため、compat 層自身は新パラメータ単体の内部
    /// 整合性検証ロジックを重複実装しない(REQ-9「薄いラッパーに徹する」)。
    /// ただし**置換前の層 shape との一致検証**(次段落)は
    /// `Linear::from_parameters` の責務範囲外(層の存在を知らない)なので、
    /// `apply_parameters` 自身の契約として本メソッドが担う(#426)。
    ///
    /// **置換前 shape との完全一致検証(#426。codex-review PR #420 P2 是正)**:
    /// 1 パス目で `Linear::from_parameters` を呼ぶ前に、各層の新
    /// `weight.shape()` が置換前の `weight.shape()` と完全一致することを
    /// 検査する(不一致なら層序数・expected/actual を含む
    /// `AutodiffError::InvalidArgument`)。bias 側の置換前一致は独立検査
    /// せず、`Linear::from_parameters` の bias 検証(`bias.shape() ==
    /// [weight.shape()[1]]`)に委譲する: weight が置換前と完全一致した
    /// 状態でこの bias 検証が通れば、bias の out_features 側も自動的に
    /// 置換前と一致するため(REQ-9「薄いラッパーに徹する」。独立実装は
    /// しない)。
    ///
    /// **層間整合は本検証に内包される**: 層 i の out_features と層 i+1 の
    /// in_features の整合は「置換前モデルが満たしていた水準」を shape 非
    /// 変更の更新が破り得ないことにより、置換前 shape との一致検証へ
    /// 帰着する。したがって隣接層比較を独立実装しない。**この検証により
    /// `apply_parameters` は shape を変えない optimizer 更新専用の契約と
    /// なる**: 層幅を変える意図的な「リサイズ」用途は本メソッドのサポート
    /// 対象外(そのような用途が必要になった場合は別 API として設計する)。
    ///
    /// # エラー
    /// - `updated` の要素数が `trainable_parameters()` の件数と不一致
    ///   → `AutodiffError::InvalidArgument`(fail-closed。位置対応契約が
    ///   崩れたまま一部の層だけ更新して黙って続行しない。`.claude/rules/
    ///   security.md` A03)
    /// - 新しい `weight` の shape が置換前の同一層の `weight` の shape と
    ///   不一致 → `AutodiffError::InvalidArgument`(#426。上記参照)
    /// - 個々のテンソルが `Linear::from_parameters` の内部整合性検証に
    ///   反する(例: bias の shape が対応する weight の out_features と
    ///   食い違う)→ `Linear::from_parameters` 由来の `AutodiffError::Shape`
    ///
    /// **アトミック性(two-pass)**: `updated` は 1 パス目で全層分を検証込みで
    /// `Linear::from_parameters` に通し、実際の代入(`self.layers` への
    /// 書き戻し)は全件の検証が成功した後の 2 パス目でまとめて行う。層ごとに
    /// 検証と代入を同時に行うと、途中の層(例: 2 層目)で shape 不整合エラーが
    /// 起きた際に、既に代入済みの前段の層は新パラメータのまま・未処理の後続層は
    /// 旧パラメータのままという不整合な混在状態がモデルに残ってしまう
    /// (呼び出し側は `Err` を受け取るにもかかわらず一部だけ更新されて
    /// 見える。fail-closed 違反。`.claude/rules/security.md` A03。
    /// review 指摘 #294)。two-pass にすることで、エラー時は呼び出し前の
    /// 状態を完全に維持する。#426 の shape 検証も同じ 1 パス目(代入前)に
    /// 置くため、この不変条件は変わらない。
    pub fn apply_parameters(&mut self, updated: Vec<Tensor<f32>>) -> Result<(), AutodiffError> {
        // trainable_parameters() と同じ順序(層の追加順)で学習可能層への
        // 可変参照を先に集める。まだ何も書き換えない。
        let linears: Vec<&mut Linear> = self
            .layers
            .iter_mut()
            .filter_map(|layer| layer.as_linear_mut())
            .collect();

        let mut updated = updated.into_iter();
        // 1 パス目: 検証込みで新しい Linear を全層分構築する(代入は未実施)。
        let mut rebuilt = Vec::with_capacity(linears.len());
        for (layer_index, linear) in linears.iter().enumerate() {
            let has_bias = linear.bias().is_some();
            let new_weight = updated.next().ok_or_else(|| {
                AutodiffError::InvalidArgument(
                    "Sequential::apply_parameters: updated has fewer elements than \
                     trainable_parameters() (weight missing)"
                        .to_string(),
                )
            })?;
            // #426: 置換前 shape との完全一致検証(層間整合を内包する設計。
            // メソッド doc 参照)。`Linear::from_parameters` 呼び出し前に
            // 行い、shape が変わる更新を fail-closed で拒否する。
            let expected_shape = linear.weight().shape();
            if new_weight.shape() != expected_shape {
                return Err(AutodiffError::InvalidArgument(format!(
                    "Sequential::apply_parameters: layer {layer_index} weight shape changed \
                     from {expected_shape:?} to {:?} (apply_parameters only supports \
                     shape-preserving updates; #426)",
                    new_weight.shape()
                )));
            }
            let new_bias = if has_bias {
                Some(updated.next().ok_or_else(|| {
                    AutodiffError::InvalidArgument(
                        "Sequential::apply_parameters: updated has fewer elements than \
                         trainable_parameters() (bias missing)"
                            .to_string(),
                    )
                })?)
            } else {
                None
            };
            rebuilt.push(Linear::from_parameters(new_weight, new_bias)?);
        }
        if updated.next().is_some() {
            return Err(AutodiffError::InvalidArgument(
                "Sequential::apply_parameters: updated has more elements than \
                 trainable_parameters()"
                    .to_string(),
            ));
        }
        // 2 パス目: ここに到達した時点で件数・shape 検証は全件完了しているため、
        // 代入自体は失敗し得ない。
        for (linear, new_linear) in linears.into_iter().zip(rebuilt) {
            *linear = new_linear;
        }
        Ok(())
    }
}

/// [`Sequential::bind`] が返す、1 学習ステップ分のテープ登録済み
/// ハンドル。`model`(`&'m Sequential`)は layer の走査順(活性化層と
/// `Linear` 層の混在列)を再現するため、`linears`(`Linear` 層のみを
/// 層順に抽出した `LinearVars` 列)は `Vec<Box<dyn Module>>` の添字とは
/// 別に独立して保持する。
pub struct SequentialVars<'m, 't> {
    model: &'m Sequential,
    linears: Vec<LinearVars<'t>>,
}

impl<'m, 't> SequentialVars<'m, 't> {
    /// 学習用 forward。`Linear` 層は [`Sequential::bind`] で既に
    /// 登録済みの `LinearVars`(`self.linears`。層出現順の添字対応)を
    /// 使い、活性化層は `Module::forward` へ委譲する。
    ///
    /// **`Linear::bind` を再度呼ばない理由**: `Module::forward`
    /// (`fandhe_ai_autodiff::nn::module`)の `Linear` 実装は呼び出しのたびに
    /// `self.bind(tape)` して新しい葉ノードを作る(推論用の使い捨て
    /// 契約)。学習用 forward がこの経路を使うと、`bind` 時点で
    /// 取得した `LinearVars`([`SequentialVars::trainable_vars`]/
    /// [`SequentialVars::trainable_grads`] が参照する `Var`)が forward
    /// のテープ記録に含まれず、`Tape::backward` 後の `Gradients::get`
    /// が到達不能(`Ok(None)`)を返してしまう。そのため学習用 forward
    /// は必ず `self.linears` に保持済みの `LinearVars::forward` を使う。
    pub fn forward(&self, tape: &'t Tape, input: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
        let mut current = *input;
        // `self.linears` は `model.layers` から `Linear` 層のみを同じ順序で
        // 抽出したもの(`Sequential::bind` 参照)のため、`Linear` 層に
        // 出会うたびにイテレータから 1 件ずつ消費すれば `model.layers`
        // 側の走査順と対応が取れる。添字アクセス(`self.linears[i]`)では
        // なくイテレータの `next()` を使う理由: 両者の対応は `bind`/
        // `forward` が同じ `as_linear().is_some()` 述語で層を数えている
        // ことに依存する不変条件であり、この不変条件自体は `self.model`
        // が `bind` 時点の `&'m Sequential` を保持し続ける(借用が生きた
        // ままの `SequentialVars` からは `model.layers` を書き換えられない)
        // ため現状は構造的に破れない。したがってこの `ok_or_else` 分岐は
        // 現状のコード構成では到達しない防御コードである。将来この関数・
        // `bind` のいずれかを書き換えて上記の対応関係が崩れた場合に、
        // 添字アクセスが本番経路で panic するのを避け fail-closed
        // (`InvalidArgument`)にするための保険として残す
        // (`.claude/rules/coding-rust.md` 本番経路 panic 禁止方針)。
        let mut linears = self.linears.iter();
        for layer in &self.model.layers {
            if layer.as_linear().is_some() {
                let vars = linears.next().ok_or_else(|| {
                    AutodiffError::InvalidArgument(
                        "SequentialVars::forward: bind 済み LinearVars が model.layers の \
                         Linear 層数より少ない(bind/forward 間の Linear 層数対応が崩れた)"
                            .to_string(),
                    )
                })?;
                current = vars.forward(&current)?;
            } else {
                // 活性化層は `nn::Module::forward` へ委譲する(`&fandhe_ai_autodiff::Tape`
                // が必要。`Sequential::forward` と同じ理由で `tape.0` 経由)。
                current = layer.forward(&tape.0, &current)?;
            }
        }
        Ok(current)
    }

    /// bind 済み `LinearVars` 列(層順)。受け入れ条件 1
    /// (「Sequential から学習可能パラメータを取得する API」)の本体。
    pub fn linears(&self) -> &[LinearVars<'t>] {
        &self.linears
    }

    /// [`Sequential::trainable_parameters`] と同一の順序契約(層順に
    /// weight → bias〈`Some` の場合のみ〉)で `Var` 参照列を返す。
    pub fn trainable_vars(&self) -> Vec<&Var<'t>> {
        let mut out = Vec::new();
        for vars in &self.linears {
            out.push(&vars.weight);
            if let Some(bias) = &vars.bias {
                out.push(bias);
            }
        }
        out
    }

    /// `Tape::backward` の結果 `grads` から、同じ順序契約
    /// (`trainable_vars`/`trainable_parameters` と共通)で勾配参照列を
    /// 収集する。
    ///
    /// 勾配が存在しないパラメータ(loss へ未到達。`Gradients::get` が
    /// `Ok(None)` を返す場合)は黙って除外せず `InvalidArgument` にする
    /// (fail-closed): 除外してしまうと戻り値の件数が
    /// `trainable_parameters()` の件数より少なくなり、
    /// `Sgd::step`/`AdamW::step` の位置対応契約(`params[i]` ↔ `grads[i]`)
    /// が呼び出し元の意図と無関係にずれて、誤ったパラメータへ誤った
    /// 勾配を適用しかねないため(`.claude/rules/security.md` A03)。
    pub fn trainable_grads<'g>(
        &self,
        grads: &'g Gradients,
    ) -> Result<Vec<&'g Tensor<f32>>, AutodiffError> {
        let mut out = Vec::new();
        for vars in &self.linears {
            let weight_grad = grads.get(&vars.weight)?.ok_or_else(|| {
                AutodiffError::InvalidArgument(
                    "SequentialVars::trainable_grads: weight に到達する勾配がない \
                     (loss へ未到達)"
                        .to_string(),
                )
            })?;
            out.push(weight_grad);
            if let Some(bias) = &vars.bias {
                let bias_grad = grads.get(bias)?.ok_or_else(|| {
                    AutodiffError::InvalidArgument(
                        "SequentialVars::trainable_grads: bias に到達する勾配がない \
                         (loss へ未到達)"
                            .to_string(),
                    )
                })?;
                out.push(bias_grad);
            }
        }
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const SEED1: u64 = 1001;
    const SEED2: u64 = 2002;

    /// テスト専用の連続化ヘルパー(`fandhe_ai_tensor_core::Tensor` の `pub` API
    /// のみを使用。旧 `fandhe_ai_autodiff::eval::dense_vec`〈クレート非公開〉の
    /// 代替。`compat/array.rs` のテストヘルパーと同型)。
    fn dense_vec(t: &Tensor<f32>) -> Vec<f32> {
        t.contiguous()
            .as_slice()
            .expect("contiguous() 直後は必ず as_slice() が Some を返す")
            .to_vec()
    }

    #[test]
    fn sequential_builds_and_predicts_two_layer_mlp() {
        // 受け入れ条件(#95): Sequential でのモデル構築・推論が動作する。
        let model = Sequential::new()
            .add_linear(8, 16, SEED1)
            .unwrap()
            .add_relu()
            .add_linear(16, 4, SEED2)
            .unwrap();

        let batch = 3;
        let input = Tensor::new(vec![0.1_f32; batch * 8], &[batch, 8]).unwrap();
        let output = model.predict(&input).unwrap();

        assert_eq!(output.shape(), &[batch, 4]);
    }

    #[test]
    fn sequential_forward_matches_manual_forward_bit_exact() {
        // 同一シード・同一演算列で組んだ手動 forward(nn::Linear +
        // nn::activation を直接呼ぶ経路)と Sequential::forward の出力が
        // ビット一致することを確認する(同一演算列のため tolerance は
        // 新設しない。coding-rust.md「許容誤差を単独で緩和しない」の
        // 趣旨に沿い、ここでは緩和ではなく完全一致で判定する)。
        let linear1 = Linear::new(8, 16, true, SEED1).unwrap();
        let linear2 = Linear::new(16, 4, true, SEED2).unwrap();

        let batch = 2;
        let input_data: Vec<f32> = (0..batch * 8).map(|i| i as f32 * 0.05).collect();
        let input_tensor = Tensor::new(input_data, &[batch, 8]).unwrap();

        // 手動経路: Linear -> ReLU -> Linear を直接組み立てる。
        // `Linear::bind` は `&fandhe_ai_autodiff::Tape` を要求するため `manual_tape.0`
        // (同一クレート内なので `pub(crate)` フィールドへアクセス可能)
        // 経由で内部の生 Tape を渡す。
        let manual_tape = crate::tape();
        let manual_input = manual_tape.var(&input_tensor);
        let h = linear1.bind(&manual_tape.0).forward(&manual_input).unwrap();
        let h = h.relu();
        let manual_output = linear2.bind(&manual_tape.0).forward(&h).unwrap();

        // Sequential 経路: 同じ Linear インスタンスを Module として積む。
        let model = Sequential {
            layers: vec![Box::new(linear1), Box::new(Relu), Box::new(linear2)],
        };
        let seq_tape = crate::tape();
        let seq_input = seq_tape.var(&input_tensor);
        let seq_output = model.forward(&seq_tape, &seq_input).unwrap();

        assert_eq!(
            dense_vec(&manual_output.to_tensor()),
            dense_vec(&seq_output.to_tensor())
        );
    }

    #[test]
    fn sequential_forward_on_external_tape_supports_backward() {
        // グラフ記録の整合確認: Sequential::forward を外部 Tape 上で
        // 実行し、Tape::backward まで通ることを検証する。
        let model = Sequential::new()
            .add_linear(4, 3, SEED1)
            .unwrap()
            .add_sigmoid();

        let tape = crate::tape();
        let input = tape.var(&Tensor::new(vec![0.1_f32, 0.2, 0.3, 0.4], &[1, 4]).unwrap());
        let output = model.forward(&tape, &input).unwrap();
        let loss = output.sum(None).unwrap();

        let grads = tape.backward(&loss).unwrap();
        // 入力自身の勾配が取得できること(グラフが入力ノードまで
        // つながっていることの確認)。
        let input_grad = grads
            .get(&input)
            .unwrap()
            .expect("入力は loss に寄与している");
        assert_eq!(input_grad.shape(), input.to_tensor().shape());
    }

    #[test]
    fn add_linear_propagates_invalid_argument() {
        // in_features == 0 は Linear::new が拒否する
        // (1/sqrt(in_features) が非有限になるため。fandhe_ai_autodiff::nn::linear 参照)。
        // add_linear はこれをそのまま Result で伝播する。
        // `Sequential` は `Box<dyn Module>` を保持し `Debug` を実装しない
        // ため、`unwrap_err()`(`Ok` 側にも `Debug` を要求する)は使わず
        // `match` で値を取り出す。
        let result = Sequential::new().add_linear(0, 4, SEED1);
        match result {
            Err(err) => assert!(matches!(err, AutodiffError::InvalidArgument(_))),
            Ok(_) => panic!("in_features == 0 は拒否されるはず"),
        }
    }

    // =================================================================
    // #294: 学習 API(optimizer 接続)
    // =================================================================

    #[test]
    fn bind_returns_linear_vars_in_layer_order_with_mixed_bias() {
        // bias 有無混在(第 1 層 bias あり・第 2 層 bias なし)で bind が
        // 層順どおり LinearVars を返すことを確認する。
        let l1 = Linear::new(4, 3, true, SEED1).unwrap();
        let l2 = Linear::new(3, 2, false, SEED2).unwrap();
        let model = Sequential {
            layers: vec![Box::new(l1), Box::new(Relu), Box::new(l2)],
        };

        let tape = crate::tape();
        let bound = model.bind(&tape);

        assert_eq!(bound.linears().len(), 2);
        assert!(bound.linears()[0].bias.is_some());
        assert!(bound.linears()[1].bias.is_none());
    }

    #[test]
    fn sequential_vars_forward_matches_manual_forward_bit_exact() {
        // SequentialVars::forward の出力が既存 Sequential::forward
        // (Module 経路)とビット一致することを検証する(tolerance は
        // 新設しない。既存 `sequential_forward_matches_manual_forward_bit_exact`
        // と同じ判定様式)。
        let batch = 2;
        let input_data: Vec<f32> = (0..batch * 8).map(|i| i as f32 * 0.05).collect();
        let input_tensor = Tensor::new(input_data, &[batch, 8]).unwrap();

        // Module 経路(既存の推論用 forward)。
        let module_model = Sequential {
            layers: vec![
                Box::new(Linear::new(8, 16, true, SEED1).unwrap()),
                Box::new(Relu),
                Box::new(Linear::new(16, 4, true, SEED2).unwrap()),
            ],
        };
        let module_tape = crate::tape();
        let module_input = module_tape.var(&input_tensor);
        let module_output = module_model.forward(&module_tape, &module_input).unwrap();

        // SequentialVars 経路(学習用 forward)。
        let train_model = Sequential {
            layers: vec![
                Box::new(Linear::new(8, 16, true, SEED1).unwrap()),
                Box::new(Relu),
                Box::new(Linear::new(16, 4, true, SEED2).unwrap()),
            ],
        };
        let train_tape = crate::tape();
        let bound = train_model.bind(&train_tape);
        let train_input = train_tape.var(&input_tensor);
        let train_output = bound.forward(&train_tape, &train_input).unwrap();

        assert_eq!(
            dense_vec(&module_output.to_tensor()),
            dense_vec(&train_output.to_tensor())
        );
    }

    #[test]
    fn trainable_parameters_order_contract_weight_then_bias_per_layer() {
        let model = Sequential::new()
            .add_linear(4, 3, SEED1) // bias あり
            .unwrap()
            .add_relu()
            .add_linear(3, 2, SEED2) // bias あり
            .unwrap();

        let params = model.trainable_parameters();
        // 層 1: weight, bias / 層 2: weight, bias の 4 件。
        assert_eq!(params.len(), 4);
        assert_eq!(params[0].shape(), &[4, 3]); // l1.weight
        assert_eq!(params[1].shape(), &[3]); // l1.bias
        assert_eq!(params[2].shape(), &[3, 2]); // l2.weight
        assert_eq!(params[3].shape(), &[2]); // l2.bias
    }

    #[test]
    fn apply_parameters_rejects_element_count_mismatch() {
        let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
        // trainable_parameters() は [weight, bias] の 2 件を要求するが
        // 1 件しか渡さない。
        let updated = vec![Tensor::new(vec![0.0f32; 12], &[4, 3]).unwrap()];
        let err = model.apply_parameters(updated).unwrap_err();
        assert!(matches!(err, AutodiffError::InvalidArgument(_)));
    }

    #[test]
    fn apply_parameters_propagates_bias_shape_mismatch() {
        let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
        // weight は rank 2 の妥当な shape([4, 3])だが、bias の shape が
        // weight.shape()[1](out_features=3)と食い違う([5])ケース。
        // `Linear::from_parameters` の bias 検証(`fandhe_ai_autodiff::nn::linear`)
        // がこれを拒否することを確認する(`apply_parameters` は新パラメータ
        // 単体の内部整合性検証を重複実装せず委譲する、という設計の裏付け。
        // 置換前 shape との一致検証〈#426〉は本テストの weight 側では
        // 通過するため、ここで拒否されるのは委譲先の bias 検証由来)。
        let weight = Tensor::new(vec![0.0f32; 12], &[4, 3]).unwrap();
        let bad_bias = Tensor::new(vec![0.0f32; 5], &[5]).unwrap();
        let err = model.apply_parameters(vec![weight, bad_bias]).unwrap_err();
        assert!(matches!(err, AutodiffError::Shape(_)));
    }

    #[test]
    fn apply_parameters_leaves_model_unchanged_on_excess_element_count() {
        // two-pass 化の回帰テスト(review 指摘 #294 の Medium 項目、後半):
        // 修正前は `if updated.next().is_some()` の超過件数チェックが
        // 全層への代入完了「後」に走っていたため、この分岐で `Err` を
        // 返すケースでも実際には全層が新パラメータへ更新済みという
        // 不整合(呼び出し側はエラー扱いなのにモデルは書き換わっている)
        // があった。two-pass では代入前に検証(超過チェックを含む)が
        // 完了するため、この場合もモデルは元の値のまま残ることを確認する。
        let mut model = Sequential::new().add_linear(2, 1, SEED1).unwrap();
        let original_params: Vec<Tensor<f32>> =
            model.trainable_parameters().into_iter().cloned().collect();

        // trainable_parameters() は [weight, bias] の 2 件を要求するが
        // 3 件目(余剰)を渡す。
        let weight = Tensor::new(vec![9.0f32, 9.0], &[2, 1]).unwrap();
        let bias = Tensor::new(vec![9.0f32], &[1]).unwrap();
        let extra = Tensor::new(vec![9.0f32], &[1]).unwrap();
        let err = model
            .apply_parameters(vec![weight, bias, extra])
            .unwrap_err();
        assert!(matches!(err, AutodiffError::InvalidArgument(_)));

        let after_params = model.trainable_parameters();
        assert_eq!(original_params.len(), after_params.len());
        for (before, after) in original_params.iter().zip(after_params.iter()) {
            assert_eq!(dense_vec(before), dense_vec(after));
        }
    }

    #[test]
    fn apply_parameters_updates_weights_used_by_subsequent_predict() {
        // apply_parameters で書き戻した値が実際に以後の forward に
        // 反映されることを確認する(薄いラッパー性の実証: compat 層は
        // Linear::from_parameters への委譲のみで独自状態を持たない)。
        let mut model = Sequential::new().add_linear(2, 1, SEED1).unwrap();
        let zero_weight = Tensor::new(vec![0.0f32, 0.0], &[2, 1]).unwrap();
        let zero_bias = Tensor::new(vec![7.0f32], &[1]).unwrap();
        model
            .apply_parameters(vec![zero_weight, zero_bias])
            .unwrap();

        let input = Tensor::new(vec![1.0f32, 2.0], &[1, 2]).unwrap();
        let output = model.predict(&input).unwrap();
        // weight=0 のため matmul 項は 0、bias=7.0 のみが出力される。
        assert!((output.get(&[0, 0]).unwrap() - 7.0).abs() < 1e-6);
    }

    #[test]
    fn apply_parameters_updates_bias_less_layer() {
        // `apply_parameters` の has_bias == false 分岐(bias を渡さず
        // weight のみ消費する経路)を直接カバーする。`Sequential::add_linear`
        // は常に bias ありの層しか組み立てられないため(公開 API からは
        // 到達不能。review 指摘 #294)、bias なし `Linear::new` を内部
        // コンストラクタ(`Sequential { layers: ... }`。同一クレート内の
        // テストのみ使用可能)で直接組み込んで検証する。
        let linear = Linear::new(2, 1, false, SEED1).unwrap();
        let mut model = Sequential {
            layers: vec![Box::new(linear)],
        };

        // has_bias == false のため trainable_parameters()/apply_parameters()
        // は weight 1 件のみを要求する。
        assert_eq!(model.trainable_parameters().len(), 1);

        let zero_weight = Tensor::new(vec![0.0f32, 0.0], &[2, 1]).unwrap();
        model.apply_parameters(vec![zero_weight]).unwrap();

        let input = Tensor::new(vec![1.0f32, 2.0], &[1, 2]).unwrap();
        let output = model.predict(&input).unwrap();
        // weight=0・bias なしのため出力は 0.0。
        assert!((output.get(&[0, 0]).unwrap()).abs() < 1e-6);
    }

    #[test]
    fn apply_parameters_leaves_model_unchanged_on_mid_sequence_shape_error() {
        // two-pass 化(review 指摘 #294 の Medium 項目)の回帰テスト:
        // 2 層目で shape 不整合エラーが起きても、検証は代入前に全件完了する
        // ため 1 層目(先に走査される層)はエラー前の値のまま保持される
        // (部分適用によるモデルの不整合な混在状態を防ぐ。
        // `.claude/rules/security.md` A03)。
        let mut model = Sequential::new()
            .add_linear(2, 3, SEED1)
            .unwrap()
            .add_relu()
            .add_linear(3, 1, SEED2)
            .unwrap();

        let original_params: Vec<Tensor<f32>> =
            model.trainable_parameters().into_iter().cloned().collect();

        // 1 層目(weight, bias)は妥当な shape、2 層目の bias は
        // out_features=1 と食い違う shape([5])にして拒否させる。
        let l1_weight = Tensor::new(vec![9.0f32; 6], &[2, 3]).unwrap();
        let l1_bias = Tensor::new(vec![9.0f32; 3], &[3]).unwrap();
        let l2_weight = Tensor::new(vec![9.0f32; 3], &[3, 1]).unwrap();
        let l2_bad_bias = Tensor::new(vec![9.0f32; 5], &[5]).unwrap();

        let err = model
            .apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bad_bias])
            .unwrap_err();
        assert!(matches!(err, AutodiffError::Shape(_)));

        // 1 層目を含め全パラメータがエラー前の値のまま残っていること。
        let after_params = model.trainable_parameters();
        assert_eq!(original_params.len(), after_params.len());
        for (before, after) in original_params.iter().zip(after_params.iter()) {
            assert_eq!(dense_vec(before), dense_vec(after));
        }
    }

    // =================================================================
    // #426: apply_parameters の置換前 shape 一致検証
    // =================================================================

    #[test]
    fn apply_parameters_rejects_weight_shape_change() {
        // 新 weight([3, 3])は `Linear::from_parameters` の単体検証
        // (rank 2・in_features > 0・bias 整合)は通過するが、置換前の
        // weight shape([4, 3])とは一致しない。#426 の検証がこれを
        // 拒否し、モデルは不変のまま残ることを確認する。
        let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
        let original_params: Vec<Tensor<f32>> =
            model.trainable_parameters().into_iter().cloned().collect();

        let new_weight = Tensor::new(vec![0.0f32; 9], &[3, 3]).unwrap();
        let new_bias = Tensor::new(vec![0.0f32; 3], &[3]).unwrap();
        let err = model
            .apply_parameters(vec![new_weight, new_bias])
            .unwrap_err();
        assert!(matches!(err, AutodiffError::InvalidArgument(_)));

        let after_params = model.trainable_parameters();
        assert_eq!(original_params.len(), after_params.len());
        for (before, after) in original_params.iter().zip(after_params.iter()) {
            assert_eq!(dense_vec(before), dense_vec(after));
        }
    }

    #[test]
    fn apply_parameters_rejects_interlayer_consistent_resize() {
        // 層間整合(層 1 の out_features == 層 2 の in_features)は
        // 保たれているが、置換前の層幅(2→3→1)自体を変える更新
        // (2→4→1)を渡すケース。#426 の設計判断
        // (「層間整合は置換前 shape 一致に内包される。リサイズは非
        // サポート」)どおり拒否されることを固定する。
        let mut model = Sequential::new()
            .add_linear(2, 3, SEED1)
            .unwrap()
            .add_relu()
            .add_linear(3, 1, SEED2)
            .unwrap();

        let l1_weight = Tensor::new(vec![0.0f32; 8], &[2, 4]).unwrap();
        let l1_bias = Tensor::new(vec![0.0f32; 4], &[4]).unwrap();
        let l2_weight = Tensor::new(vec![0.0f32; 4], &[4, 1]).unwrap();
        let l2_bias = Tensor::new(vec![0.0f32; 1], &[1]).unwrap();

        let err = model
            .apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bias])
            .unwrap_err();
        assert!(matches!(err, AutodiffError::InvalidArgument(_)));
    }

    #[test]
    fn apply_parameters_leaves_model_unchanged_on_mid_sequence_weight_shape_error() {
        // #426 の two-pass 非退行確認: 2 層目の weight shape のみ不一致に
        // した場合でも、1 層目(先に走査される層)を含む全パラメータが
        // エラー前の値のまま保持されることを確認する
        // (既存 `..._on_mid_sequence_shape_error` と同型。#294 の
        // 設計不変条件が #426 の検証にも及ぶことの回帰テスト)。
        let mut model = Sequential::new()
            .add_linear(2, 3, SEED1)
            .unwrap()
            .add_relu()
            .add_linear(3, 1, SEED2)
            .unwrap();

        let original_params: Vec<Tensor<f32>> =
            model.trainable_parameters().into_iter().cloned().collect();

        // 1 層目(weight, bias)は妥当な shape、2 層目の weight のみ
        // 置換前 shape([3, 1])と食い違う shape([3, 2])にする。
        let l1_weight = Tensor::new(vec![9.0f32; 6], &[2, 3]).unwrap();
        let l1_bias = Tensor::new(vec![9.0f32; 3], &[3]).unwrap();
        let l2_weight = Tensor::new(vec![9.0f32; 6], &[3, 2]).unwrap();
        let l2_bias = Tensor::new(vec![9.0f32; 2], &[2]).unwrap();

        let err = model
            .apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bias])
            .unwrap_err();
        assert!(matches!(err, AutodiffError::InvalidArgument(_)));

        let after_params = model.trainable_parameters();
        assert_eq!(original_params.len(), after_params.len());
        for (before, after) in original_params.iter().zip(after_params.iter()) {
            assert_eq!(dense_vec(before), dense_vec(after));
        }
    }
}