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
//! The SetFit pair objective (ENC-06).
//!
//! Contract: `setfit-encoder-conformance-v1`, equation `pair_cosine_mse`.
//!
//! One function, [`pair_cosine_mse`], composed from exactly two 01-03
//! primitives: `cosine_similarity_rows` and `mse_loss`. It computes
//!
//! ```text
//! L = (1/B) * Σ_b ( cos(za[b], zb[b]) - labels[b] )²
//! ```
//!
//! and returns a **graph-connected `[1]` tensor**, not an `f32`.
//!
//! # Why this is its own equation, and why it is not in `nn/`
//!
//! The `nn/loss.rs` and `nn/self_supervised.rs` helpers return `f32`. An `f32`
//! carries no `grad_fn`, so a training loop built on one reports a falling loss
//! while every encoder weight stays exactly where it started — PF-001, the trap
//! this phase exists to close. Nothing here imports from, calls into, or
//! re-exports either module, and a source assertion in `loss_tests.rs` holds
//! that line.
//!
//! The contract annotation names `pair_cosine_mse`, **not** `mse_loss`. They are
//! different obligations: `mse_loss` relates a prediction vector to a target
//! vector, while this relates two `[B,H]` embedding matrices to a vector of
//! binary pair labels. Annotating this wrapper as raw MSE would misdescribe its
//! inputs and quietly drop the two-embedding-input obligation ENC-06 gates.
use crate;
use SetFitError;
/// Epsilon floor on each cosine norm.
///
/// The same explicit constant the encoder's trailing L2 normalization uses
/// (`encoder::L2_EPS`), so the pair objective and the embeddings it consumes
/// agree about what "degenerate" means. `pair_loss_epsilon_agrees_with_the_
/// encoder_normalize_path` asserts the equality rather than trusting two
/// literals to stay in step.
pub const PAIR_COSINE_EPS: f32 = 1e-12;
/// Mean-squared error between row-wise cosine similarity and binary pair
/// labels, as a graph-connected `[1]` tensor.
///
/// `za` and `zb` are the two siamese branches' `[B, H]` sentence embeddings;
/// `labels[b]` is `1.0` when the pair is a positive and `0.0` when it is a
/// negative. The backward reaches BOTH inputs, so one backward pass updates the
/// shared encoder body through both branches.
///
/// # Validation order
///
/// Shapes first (so nothing is computed on mismatched inputs), then labels:
/// **length**, then **finiteness**, then **binary membership**. The finiteness
/// check is explicit and comes first on purpose. `NaN != 0.0 && NaN != 1.0` is
/// true, so the membership test happens to reject `NaN` today — but only
/// incidentally, and it would report "not in {0,1}" for a value whose real
/// problem is that it is not a number.
///
/// # Errors
///
/// * [`SetFitError::Op`] wrapping [`OpError::ShapeMismatch`] — either input is
/// not rank 2, or the two shapes differ.
/// * [`SetFitError::BatchInvalid`] — `labels.len()` disagrees with the batch, a
/// label is non-finite, or a finite label is outside `{0.0, 1.0}`.
/// * [`SetFitError::Op`] — anything the two composed primitives reject
/// (zero dimension, non-finite embedding).