1use modewise_method::MethodError;
2
3#[derive(Debug, Clone, PartialEq)]
5pub struct VlmdParams {
6 pub num_modes: usize,
8 pub num_latents: Option<usize>,
10 pub alpha: f64,
12 pub reg_lambda: f64,
14 pub reg_rho: f64,
16 pub tau: f64,
18 pub tolerance: f64,
20 pub max_iterations: usize,
21}
22
23impl VlmdParams {
24 pub fn new(num_modes: usize) -> Self {
25 Self {
26 num_modes,
27 num_latents: None,
28 alpha: 100.0,
29 reg_lambda: 0.01,
30 reg_rho: 1.0,
31 tau: 0.9,
32 tolerance: 1e-3,
33 max_iterations: 1000,
34 }
35 }
36
37 pub(crate) fn validate(&self) -> Result<(), MethodError> {
38 let positive = |v: f64| v.is_finite() && v > 0.0;
39 if self.num_modes == 0 {
40 return Err(MethodError::invalid("num_modes", "must be at least 1"));
41 }
42 if self.num_latents == Some(0) {
43 return Err(MethodError::invalid("num_latents", "must be at least 1"));
44 }
45 if !positive(self.alpha) {
46 return Err(MethodError::invalid("alpha", "must be finite and positive"));
47 }
48 if !(self.reg_lambda.is_finite() && self.reg_lambda >= 0.0) {
49 return Err(MethodError::invalid(
50 "reg_lambda",
51 "must be finite and non-negative",
52 ));
53 }
54 if !positive(self.reg_rho) {
55 return Err(MethodError::invalid(
56 "reg_rho",
57 "must be finite and positive",
58 ));
59 }
60 if !(0.0..=1.0).contains(&self.tau) {
61 return Err(MethodError::invalid("tau", "must be between 0 and 1"));
62 }
63 if !positive(self.tolerance) {
64 return Err(MethodError::invalid(
65 "tolerance",
66 "must be finite and positive",
67 ));
68 }
69 if self.max_iterations == 0 {
70 return Err(MethodError::invalid("max_iterations", "must be at least 1"));
71 }
72 Ok(())
73 }
74
75 pub(crate) fn latents_for(&self, n_channels: usize) -> Result<usize, MethodError> {
76 let latents = self.num_latents.unwrap_or(n_channels);
77 if latents > n_channels {
78 return Err(MethodError::invalid(
79 "num_latents",
80 format!(
81 "must be at most the number of channels ({n_channels}); the model assumes L ≤ C"
82 ),
83 ));
84 }
85 Ok(latents)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn rejects_invalid_values() {
95 let base = VlmdParams::new(3);
96 assert!(base.validate().is_ok());
97 let cases = [
98 VlmdParams {
99 num_modes: 0,
100 ..base.clone()
101 },
102 VlmdParams {
103 num_latents: Some(0),
104 ..base.clone()
105 },
106 VlmdParams {
107 alpha: 0.0,
108 ..base.clone()
109 },
110 VlmdParams {
111 alpha: f64::INFINITY,
112 ..base.clone()
113 },
114 VlmdParams {
115 reg_lambda: -1e-9,
116 ..base.clone()
117 },
118 VlmdParams {
119 reg_lambda: f64::NAN,
120 ..base.clone()
121 },
122 VlmdParams {
123 reg_rho: 0.0,
124 ..base.clone()
125 },
126 VlmdParams {
127 reg_rho: f64::NAN,
128 ..base.clone()
129 },
130 VlmdParams {
131 tau: 1.0 + 1e-9,
132 ..base.clone()
133 },
134 VlmdParams {
135 tau: -1e-9,
136 ..base.clone()
137 },
138 VlmdParams {
139 tolerance: 0.0,
140 ..base.clone()
141 },
142 VlmdParams {
143 max_iterations: 0,
144 ..base.clone()
145 },
146 ];
147 for case in cases {
148 assert!(case.validate().is_err(), "{case:?}");
149 }
150 for ok in [
151 VlmdParams {
152 reg_lambda: 0.0,
153 ..base.clone()
154 },
155 VlmdParams {
156 tau: 0.0,
157 ..base.clone()
158 },
159 VlmdParams {
160 tau: 1.0,
161 ..base.clone()
162 },
163 ] {
164 assert!(ok.validate().is_ok(), "{ok:?}");
165 }
166 }
167
168 #[test]
169 fn latents_default_to_channels_and_cannot_exceed_them() {
170 let p = VlmdParams::new(2);
171 assert_eq!(p.latents_for(5).unwrap(), 5);
172 let three = VlmdParams {
173 num_latents: Some(3),
174 ..p
175 };
176 assert_eq!(three.latents_for(3).unwrap(), 3);
177 assert!(three.latents_for(2).is_err());
178 }
179}