Skip to main content

xlog_neural/
registry.rs

1//! Network registry for managing registered neural networks.
2//!
3//! The registry is the central point for managing all neural networks used
4//! in a probabilistic logic program. It handles:
5//!
6//! - Registration of networks with their configurations
7//! - Train/eval mode switching for all networks
8//! - Network lookup by name
9
10use crate::handle::{EmbeddingHandle, NetworkHandle};
11use std::collections::HashMap;
12
13/// Configuration for registering a neural network.
14///
15/// This mirrors the DeepProbLog `register_network` options.
16#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct NetworkConfig {
19    /// Unique name identifying this network (must match nn() declarations)
20    pub name: String,
21
22    /// Whether to batch inputs for efficient GPU processing.
23    /// When true, multiple queries are grouped into a single forward pass.
24    pub batching: bool,
25
26    /// Top-k sampling: if Some(k), only consider the top k outputs.
27    /// Useful for large output spaces where most classes have near-zero probability.
28    pub k: Option<usize>,
29
30    /// Deterministic mode: use argmax instead of probabilistic sampling.
31    /// Useful for debugging and when you want reproducible results.
32    pub det: bool,
33
34    /// Whether to cache network outputs.
35    /// Caching avoids redundant forward passes for repeated inputs.
36    pub cache_enabled: bool,
37
38    /// Maximum number of entries in the output cache.
39    pub cache_size: usize,
40
41    /// Number of arguments the network's declared predicate(s) take, as
42    /// stated by the caller at registration time. `None` means unstated.
43    /// When present, this is validated against every `nn/4` declaration
44    /// bound to this network name (see `register_network`), so a mismatch
45    /// between what the caller claims and what the program actually
46    /// declared is rejected rather than trusted.
47    pub arity: Option<usize>,
48
49    /// Per-argument catalog sort ids, in declared-argument order. Carried
50    /// opaquely: the registry does not interpret or validate these against
51    /// the program, it only stores them so callers (e.g. slot-matching
52    /// enumeration) can sort-match candidate slots by id. Validated at the
53    /// pyo3 boundary (`register_network`) to be Python ints, with `bool`
54    /// explicitly excluded even though `isinstance(True, int)` holds.
55    pub arg_sorts: Option<Vec<i64>>,
56
57    /// Content hash of the registered artifact (module weights). Carried
58    /// opaquely, like `arg_sorts`: a retrained network is expected to mint a
59    /// new hash, so this field is the identity of *this* registration, not
60    /// a claim the registry checks.
61    pub artifact_hash: Option<String>,
62}
63
64impl NetworkConfig {
65    /// Create a default configuration for a network with the given name.
66    ///
67    /// Default settings:
68    /// - batching: true
69    /// - k: None (consider all outputs)
70    /// - det: false (probabilistic mode)
71    /// - cache_enabled: true
72    /// - cache_size: 10000
73    pub fn default(name: &str) -> Self {
74        Self {
75            name: name.to_string(),
76            batching: true,
77            k: None,
78            det: false,
79            cache_enabled: true,
80            cache_size: 10000,
81            arity: None,
82            arg_sorts: None,
83            artifact_hash: None,
84        }
85    }
86
87    /// Create a configuration for a deterministic network.
88    pub fn deterministic(name: &str) -> Self {
89        Self {
90            name: name.to_string(),
91            batching: true,
92            k: None,
93            det: true,
94            cache_enabled: true,
95            cache_size: 10000,
96            arity: None,
97            arg_sorts: None,
98            artifact_hash: None,
99        }
100    }
101
102    /// Create a configuration with top-k sampling.
103    pub fn with_top_k(name: &str, k: usize) -> Self {
104        Self {
105            name: name.to_string(),
106            batching: true,
107            k: Some(k),
108            det: false,
109            cache_enabled: true,
110            cache_size: 10000,
111            arity: None,
112            arg_sorts: None,
113            artifact_hash: None,
114        }
115    }
116
117    /// Builder method to set batching.
118    pub fn batching(mut self, enabled: bool) -> Self {
119        self.batching = enabled;
120        self
121    }
122
123    /// Builder method to set top-k.
124    pub fn k(mut self, k: Option<usize>) -> Self {
125        self.k = k;
126        self
127    }
128
129    /// Builder method to set deterministic mode.
130    pub fn det(mut self, det: bool) -> Self {
131        self.det = det;
132        self
133    }
134
135    /// Builder method to set cache.
136    pub fn cache(mut self, enabled: bool, size: usize) -> Self {
137        self.cache_enabled = enabled;
138        self.cache_size = size;
139        self
140    }
141}
142
143/// Registry for managing neural networks.
144///
145/// The registry maintains a collection of `NetworkHandle` instances,
146/// each identified by a unique name. Networks are registered with
147/// configurations and then have their PyTorch modules attached via
148/// the Python API.
149pub struct NetworkRegistry {
150    /// Map from network name to handle
151    networks: HashMap<String, NetworkHandle>,
152    /// Map from embedding name to handle
153    embeddings: HashMap<String, EmbeddingHandle>,
154}
155
156impl NetworkRegistry {
157    /// Create a new empty registry.
158    pub fn new() -> Self {
159        Self {
160            networks: HashMap::new(),
161            embeddings: HashMap::new(),
162        }
163    }
164
165    /// Register a network with the given configuration.
166    ///
167    /// If a network with the same name already exists, it will be replaced.
168    pub fn register(&mut self, config: NetworkConfig) {
169        let handle = NetworkHandle::from_config(&config);
170        self.networks.insert(config.name, handle);
171    }
172
173    /// Get a reference to a network handle by name.
174    pub fn get(&self, name: &str) -> Option<&NetworkHandle> {
175        self.networks.get(name)
176    }
177
178    /// Get a mutable reference to a network handle by name.
179    pub fn get_mut(&mut self, name: &str) -> Option<&mut NetworkHandle> {
180        self.networks.get_mut(name)
181    }
182
183    /// Check if a network is registered.
184    pub fn contains(&self, name: &str) -> bool {
185        self.networks.contains_key(name)
186    }
187
188    /// Remove a network from the registry.
189    pub fn unregister(&mut self, name: &str) -> Option<NetworkHandle> {
190        self.networks.remove(name)
191    }
192
193    /// Set train mode for all registered networks.
194    ///
195    /// This affects both the `train_mode` flag on handles and should
196    /// be used to call `.train()` or `.eval()` on PyTorch modules.
197    pub fn set_train_mode(&mut self, train: bool) {
198        for handle in self.networks.values_mut() {
199            handle.train_mode = train;
200        }
201    }
202
203    /// Get the names of all registered networks.
204    pub fn names(&self) -> Vec<&str> {
205        self.networks.keys().map(|s| s.as_str()).collect()
206    }
207
208    /// Get the number of registered networks.
209    pub fn len(&self) -> usize {
210        self.networks.len()
211    }
212
213    /// Check if the registry is empty.
214    pub fn is_empty(&self) -> bool {
215        self.networks.is_empty()
216    }
217
218    /// Remove all networks from the registry.
219    pub fn clear(&mut self) {
220        self.networks.clear();
221    }
222
223    /// Iterate over all network handles.
224    pub fn iter(&self) -> impl Iterator<Item = (&str, &NetworkHandle)> {
225        self.networks.iter().map(|(k, v)| (k.as_str(), v))
226    }
227
228    /// Iterate mutably over all network handles.
229    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut NetworkHandle)> {
230        self.networks.iter_mut().map(|(k, v)| (k.as_str(), v))
231    }
232
233    /// Register an embedding with the given handle.
234    pub fn register_embedding(&mut self, handle: EmbeddingHandle) {
235        self.embeddings.insert(handle.name.clone(), handle);
236    }
237
238    /// Get a reference to an embedding handle by name.
239    pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingHandle> {
240        self.embeddings.get(name)
241    }
242
243    /// Get a mutable reference to an embedding handle by name.
244    pub fn get_embedding_mut(&mut self, name: &str) -> Option<&mut EmbeddingHandle> {
245        self.embeddings.get_mut(name)
246    }
247
248    /// Check if an embedding is registered.
249    pub fn contains_embedding(&self, name: &str) -> bool {
250        self.embeddings.contains_key(name)
251    }
252}
253
254impl Default for NetworkRegistry {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_config_default() {
266        let config = NetworkConfig::default("test");
267        assert_eq!(config.name, "test");
268        assert!(config.batching);
269        assert!(config.k.is_none());
270        assert!(!config.det);
271        assert!(config.cache_enabled);
272        assert_eq!(config.cache_size, 10000);
273        assert!(config.arity.is_none());
274        assert!(config.arg_sorts.is_none());
275        assert!(config.artifact_hash.is_none());
276    }
277
278    #[test]
279    fn test_config_registration_metadata_none_across_constructors() {
280        // Registration metadata (arity/arg_sorts/artifact_hash) is not part of
281        // DeepProbLog-style tuning knobs, so every constructor must default it
282        // to None the same way, not just `default()`.
283        let det = NetworkConfig::deterministic("det_test");
284        assert!(det.arity.is_none());
285        assert!(det.arg_sorts.is_none());
286        assert!(det.artifact_hash.is_none());
287
288        let top_k = NetworkConfig::with_top_k("top_k_test", 5);
289        assert!(top_k.arity.is_none());
290        assert!(top_k.arg_sorts.is_none());
291        assert!(top_k.artifact_hash.is_none());
292    }
293
294    #[test]
295    fn test_config_registration_metadata_survives_clone() {
296        let mut config = NetworkConfig::default("meta_test");
297        config.arity = Some(2);
298        config.arg_sorts = Some(vec![0, 1]);
299        config.artifact_hash = Some("deadbeef".to_string());
300
301        let cloned = config.clone();
302        assert_eq!(cloned.arity, Some(2));
303        assert_eq!(cloned.arg_sorts, Some(vec![0, 1]));
304        assert_eq!(cloned.artifact_hash, Some("deadbeef".to_string()));
305    }
306
307    #[test]
308    fn test_config_deterministic() {
309        let config = NetworkConfig::deterministic("det_test");
310        assert!(config.det);
311    }
312
313    #[test]
314    fn test_config_with_top_k() {
315        let config = NetworkConfig::with_top_k("top_k_test", 5);
316        assert_eq!(config.k, Some(5));
317    }
318
319    #[test]
320    fn test_config_builder() {
321        let config = NetworkConfig::default("builder_test")
322            .batching(false)
323            .k(Some(3))
324            .det(true)
325            .cache(false, 0);
326
327        assert!(!config.batching);
328        assert_eq!(config.k, Some(3));
329        assert!(config.det);
330        assert!(!config.cache_enabled);
331        assert_eq!(config.cache_size, 0);
332    }
333
334    #[test]
335    fn test_registry_new() {
336        let registry = NetworkRegistry::new();
337        assert!(registry.is_empty());
338        assert_eq!(registry.len(), 0);
339    }
340
341    #[test]
342    fn test_registry_register_get() {
343        let mut registry = NetworkRegistry::new();
344        registry.register(NetworkConfig::default("net1"));
345
346        assert!(registry.contains("net1"));
347        assert!(registry.get("net1").is_some());
348        assert!(registry.get("nonexistent").is_none());
349    }
350
351    #[test]
352    fn test_registry_iter() {
353        let mut registry = NetworkRegistry::new();
354        registry.register(NetworkConfig::default("a"));
355        registry.register(NetworkConfig::default("b"));
356
357        let names: Vec<&str> = registry.iter().map(|(name, _)| name).collect();
358        assert_eq!(names.len(), 2);
359    }
360
361    use crate::handle::EmbeddingHandle;
362
363    #[test]
364    fn test_registry_embedding_register_get() {
365        let mut registry = NetworkRegistry::new();
366        let handle = EmbeddingHandle::new("embed1".to_string(), true, 64, 100);
367        registry.register_embedding(handle);
368
369        assert!(registry.contains_embedding("embed1"));
370        assert!(!registry.contains_embedding("nonexistent"));
371
372        let h = registry.get_embedding("embed1").unwrap();
373        assert_eq!(h.dim, 64);
374        assert_eq!(h.vocab_size, 100);
375    }
376
377    #[test]
378    fn test_registry_embedding_get_mut() {
379        let mut registry = NetworkRegistry::new();
380        let handle = EmbeddingHandle::new("embed1".to_string(), true, 64, 100);
381        registry.register_embedding(handle);
382
383        let h = registry.get_embedding_mut("embed1").unwrap();
384        h.trainable = false;
385        assert!(!registry.get_embedding("embed1").unwrap().trainable);
386    }
387}