Skip to main content

quantrs2_anneal/
qubo.rs

1//! QUBO problem formulation for quantum annealing
2//!
3//! This module provides utilities for formulating optimization problems as
4//! Quadratic Unconstrained Binary Optimization (QUBO) problems.
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9use crate::ising::{IsingError, QuboModel};
10
11/// Errors that can occur when formulating QUBO problems
12#[derive(Error, Debug)]
13#[non_exhaustive]
14pub enum QuboError {
15    /// Error in the underlying Ising model
16    #[error("Ising error: {0}")]
17    IsingError(#[from] IsingError),
18
19    /// Error when formulating a constraint
20    #[error("Constraint error: {0}")]
21    ConstraintError(String),
22
23    /// Error when a variable is already defined
24    #[error("Variable {0} is already defined")]
25    DuplicateVariable(String),
26
27    /// Error when a variable is not found
28    #[error("Variable {0} not found")]
29    VariableNotFound(String),
30}
31
32/// Result type for QUBO problem operations
33pub type QuboResult<T> = Result<T, QuboError>;
34
35/// A variable in a QUBO problem formulation
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct Variable {
38    /// Name of the variable
39    pub name: String,
40    /// Index of the variable in the QUBO model
41    pub index: usize,
42}
43
44impl Variable {
45    /// Create a new variable with the given name and index
46    pub fn new(name: impl Into<String>, index: usize) -> Self {
47        Self {
48            name: name.into(),
49            index,
50        }
51    }
52}
53
54/// A builder for creating QUBO problems.
55///
56/// This provides a more convenient interface for formulating optimization problems
57/// than directly working with the `QuboModel`.
58///
59/// # Examples
60///
61/// ```rust
62/// use quantrs2_anneal::qubo::QuboBuilder;
63///
64/// let mut builder = QuboBuilder::new();
65/// let x0 = builder.add_variable("x0").expect("add x0");
66/// let x1 = builder.add_variable("x1").expect("add x1");
67/// // Penalise x0 + x1 (both set to 1)
68/// builder.set_linear_term(&x0, 1.0).expect("linear x0");
69/// builder.set_linear_term(&x1, 1.0).expect("linear x1");
70/// let model = builder.build();
71/// assert_eq!(model.num_variables, 2);
72/// ```
73#[derive(Debug, Clone)]
74pub struct QuboBuilder {
75    /// Current number of variables
76    num_vars: usize,
77
78    /// Mapping from variable names to indices
79    var_map: HashMap<String, usize>,
80
81    /// The underlying QUBO model
82    model: QuboModel,
83
84    /// Penalty weight for constraint violations
85    constraint_weight: f64,
86}
87
88impl QuboBuilder {
89    /// Create a new empty QUBO builder
90    #[must_use]
91    pub fn new() -> Self {
92        Self {
93            num_vars: 0,
94            var_map: HashMap::new(),
95            model: QuboModel::new(0),
96            constraint_weight: 10.0,
97        }
98    }
99
100    /// Set the penalty weight for constraint violations
101    pub fn set_constraint_weight(&mut self, weight: f64) -> QuboResult<()> {
102        if !weight.is_finite() || weight <= 0.0 {
103            return Err(QuboError::ConstraintError(format!(
104                "Constraint weight must be positive and finite, got {weight}"
105            )));
106        }
107
108        self.constraint_weight = weight;
109        Ok(())
110    }
111
112    /// Add a new binary variable to the problem
113    pub fn add_variable(&mut self, name: impl Into<String>) -> QuboResult<Variable> {
114        let name = name.into();
115
116        // Check if the variable already exists
117        if self.var_map.contains_key(&name) {
118            return Err(QuboError::DuplicateVariable(name));
119        }
120
121        // Add the variable
122        let index = self.num_vars;
123        self.var_map.insert(name.clone(), index);
124        self.num_vars += 1;
125
126        // Update the QUBO model
127        self.model = QuboModel::new(self.num_vars);
128
129        Ok(Variable::new(name, index))
130    }
131
132    /// Add multiple binary variables to the problem
133    pub fn add_variables(
134        &mut self,
135        names: impl IntoIterator<Item = impl Into<String>>,
136    ) -> QuboResult<Vec<Variable>> {
137        let mut variables = Vec::new();
138        for name in names {
139            variables.push(self.add_variable(name)?);
140        }
141        Ok(variables)
142    }
143
144    /// Get a variable by name
145    pub fn get_variable(&self, name: &str) -> QuboResult<Variable> {
146        match self.var_map.get(name) {
147            Some(&index) => Ok(Variable::new(name, index)),
148            None => Err(QuboError::VariableNotFound(name.to_string())),
149        }
150    }
151
152    /// Set the linear coefficient for a variable
153    pub fn set_linear_term(&mut self, var: &Variable, value: f64) -> QuboResult<()> {
154        // Ensure the variable exists in the model
155        if var.index >= self.num_vars {
156            return Err(QuboError::VariableNotFound(var.name.clone()));
157        }
158
159        Ok(self.model.set_linear(var.index, value)?)
160    }
161
162    /// Set the quadratic coefficient for a pair of variables
163    pub fn set_quadratic_term(
164        &mut self,
165        var1: &Variable,
166        var2: &Variable,
167        value: f64,
168    ) -> QuboResult<()> {
169        // Ensure the variables exist in the model
170        if var1.index >= self.num_vars {
171            return Err(QuboError::VariableNotFound(var1.name.clone()));
172        }
173        if var2.index >= self.num_vars {
174            return Err(QuboError::VariableNotFound(var2.name.clone()));
175        }
176
177        // Check if the variables are the same
178        if var1.index == var2.index {
179            return Err(QuboError::ConstraintError(format!(
180                "Cannot set quadratic term for the same variable: {}",
181                var1.name
182            )));
183        }
184
185        Ok(self.model.set_quadratic(var1.index, var2.index, value)?)
186    }
187
188    /// Set the offset term in the QUBO model
189    pub fn set_offset(&mut self, offset: f64) -> QuboResult<()> {
190        if !offset.is_finite() {
191            return Err(QuboError::ConstraintError(format!(
192                "Offset must be finite, got {offset}"
193            )));
194        }
195
196        self.model.offset = offset;
197        Ok(())
198    }
199
200    /// Add a bias term to a variable (linear coefficient)
201    pub fn add_bias(&mut self, var_index: usize, bias: f64) -> QuboResult<()> {
202        if var_index >= self.num_vars {
203            return Err(QuboError::VariableNotFound(format!(
204                "Variable index {var_index}"
205            )));
206        }
207        let current = self.model.get_linear(var_index)?;
208        self.model.set_linear(var_index, current + bias)?;
209        Ok(())
210    }
211
212    /// Add a coupling term between two variables (quadratic coefficient)
213    pub fn add_coupling(
214        &mut self,
215        var1_index: usize,
216        var2_index: usize,
217        coupling: f64,
218    ) -> QuboResult<()> {
219        if var1_index >= self.num_vars {
220            return Err(QuboError::VariableNotFound(format!(
221                "Variable index {var1_index}"
222            )));
223        }
224        if var2_index >= self.num_vars {
225            return Err(QuboError::VariableNotFound(format!(
226                "Variable index {var2_index}"
227            )));
228        }
229        let current = self.model.get_quadratic(var1_index, var2_index)?;
230        self.model
231            .set_quadratic(var1_index, var2_index, current + coupling)?;
232        Ok(())
233    }
234
235    /// Add a linear objective term to minimize
236    pub fn minimize_linear(&mut self, var: &Variable, coeff: f64) -> QuboResult<()> {
237        self.set_linear_term(var, self.model.get_linear(var.index)? + coeff)
238    }
239
240    /// Add a quadratic objective term to minimize
241    pub fn minimize_quadratic(
242        &mut self,
243        var1: &Variable,
244        var2: &Variable,
245        coeff: f64,
246    ) -> QuboResult<()> {
247        let current = self.model.get_quadratic(var1.index, var2.index)?;
248        self.set_quadratic_term(var1, var2, current + coeff)
249    }
250
251    /// Add a constraint that two variables must be equal
252    ///
253    /// This adds a penalty term: weight * (x1 - x2)^2
254    pub fn constrain_equal(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
255        // Penalty term: weight * (x1 - x2)^2 = weight * (x1 + x2 - 2*x1*x2)
256        let weight = self.constraint_weight;
257
258        // Add weight to var1's linear term
259        self.set_linear_term(var1, self.model.get_linear(var1.index)? + weight)?;
260
261        // Add weight to var2's linear term
262        self.set_linear_term(var2, self.model.get_linear(var2.index)? + weight)?;
263
264        // Add -2*weight to the quadratic term
265        let current = self.model.get_quadratic(var1.index, var2.index)?;
266        self.set_quadratic_term(var1, var2, 2.0f64.mul_add(-weight, current))
267    }
268
269    /// Add a constraint that two variables must be different
270    ///
271    /// This adds a penalty term: weight * (1 - (x1 - x2)^2)
272    pub fn constrain_different(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
273        // Penalty term: weight * (1 - (x1 - x2)^2) = weight * (1 - x1 - x2 + 2*x1*x2)
274        let weight = self.constraint_weight;
275
276        // Add -weight to var1's linear term
277        self.set_linear_term(var1, self.model.get_linear(var1.index)? - weight)?;
278
279        // Add -weight to var2's linear term
280        self.set_linear_term(var2, self.model.get_linear(var2.index)? - weight)?;
281
282        // Add 2*weight to the quadratic term
283        let current = self.model.get_quadratic(var1.index, var2.index)?;
284        self.set_quadratic_term(var1, var2, 2.0f64.mul_add(weight, current))?;
285
286        // Add weight to the offset
287        self.model.offset += weight;
288
289        Ok(())
290    }
291
292    /// Add a constraint that exactly one of the variables must be 1
293    ///
294    /// This adds a penalty term: weight * (`sum(x_i)` - 1)^2
295    pub fn constrain_one_hot(&mut self, vars: &[Variable]) -> QuboResult<()> {
296        if vars.is_empty() {
297            return Err(QuboError::ConstraintError(
298                "Empty one-hot constraint".to_string(),
299            ));
300        }
301
302        // Penalty term: weight * (sum(x_i) - 1)^2
303        // = weight * (sum(x_i)^2 - 2*sum(x_i) + 1)
304        // = weight * (sum(x_i) + sum(x_i*x_j for i!=j) - 2*sum(x_i) + 1)
305        // = weight * (sum(x_i*x_j for i!=j) - sum(x_i) + 1)
306        let weight = self.constraint_weight;
307
308        // Add -weight to each variable's linear term
309        for var in vars {
310            self.set_linear_term(var, self.model.get_linear(var.index)? - weight)?;
311        }
312
313        // Add weight to each pair of variables' quadratic term
314        for i in 0..vars.len() {
315            for j in (i + 1)..vars.len() {
316                let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
317                self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
318            }
319        }
320
321        // Add weight to the offset
322        self.model.offset += weight;
323
324        Ok(())
325    }
326
327    /// Add a constraint that at most one of the variables can be 1
328    ///
329    /// This adds a penalty term: weight * max(0, `sum(x_i)` - 1)^2
330    pub fn constrain_at_most_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
331        if vars.is_empty() {
332            return Err(QuboError::ConstraintError(
333                "Empty at-most-one constraint".to_string(),
334            ));
335        }
336
337        // Penalty term: weight * max(0, sum(x_i) - 1)^2
338        // For binary variables, this simplifies to:
339        // weight * sum(x_i*x_j for i!=j)
340        let weight = self.constraint_weight;
341
342        // Add weight to each pair of variables' quadratic term
343        for i in 0..vars.len() {
344            for j in (i + 1)..vars.len() {
345                let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
346                self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
347            }
348        }
349
350        Ok(())
351    }
352
353    /// Add a constraint that at least one of the variables must be 1
354    ///
355    /// This adds a penalty term: weight * (1 - `sum(x_i))^2`
356    pub fn constrain_at_least_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
357        if vars.is_empty() {
358            return Err(QuboError::ConstraintError(
359                "Empty at-least-one constraint".to_string(),
360            ));
361        }
362
363        // Penalty term: weight * (1 - sum(x_i))^2
364        // = weight * (1 - 2*sum(x_i) + sum(x_i)^2)
365        // = weight * (1 - 2*sum(x_i) + sum(x_i) + sum(x_i*x_j for i!=j))
366        // = weight * (1 - sum(x_i) + sum(x_i*x_j for i!=j))
367        let weight = self.constraint_weight;
368
369        // Add -weight to each variable's linear term
370        for var in vars {
371            self.set_linear_term(
372                var,
373                2.0f64.mul_add(-weight, self.model.get_linear(var.index)?),
374            )?;
375        }
376
377        // Add weight to each pair of variables' quadratic term
378        for i in 0..vars.len() {
379            for j in (i + 1)..vars.len() {
380                let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
381                self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
382            }
383        }
384
385        // Add weight to the offset
386        self.model.offset += weight;
387
388        Ok(())
389    }
390
391    /// Add a constraint that the sum of variables equals a target value
392    ///
393    /// This adds a penalty term: weight * (`sum(x_i)` - target)^2
394    pub fn constrain_sum_equal(&mut self, vars: &[Variable], target: f64) -> QuboResult<()> {
395        if vars.is_empty() {
396            return Err(QuboError::ConstraintError(
397                "Empty sum constraint".to_string(),
398            ));
399        }
400
401        // Penalty term: weight * (sum(x_i) - target)^2
402        let weight = self.constraint_weight;
403
404        // Add linear terms: weight * (2*target - 2*sum(x_i))
405        for var in vars {
406            let current = self.model.get_linear(var.index)?;
407            self.set_linear_term(var, weight.mul_add(2.0f64.mul_add(-target, 1.0), current))?;
408        }
409
410        // Add quadratic terms between all pairs: weight * 2*x_i*x_j
411        for i in 0..vars.len() {
412            for j in (i + 1)..vars.len() {
413                let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
414                self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
415            }
416        }
417
418        // Add offset: weight * target^2
419        self.model.offset += weight * target * target;
420
421        Ok(())
422    }
423
424    /// Build the final QUBO model
425    #[must_use]
426    pub fn build(&self) -> QuboModel {
427        self.model.clone()
428    }
429
430    /// Get a map of variable names to indices
431    #[must_use]
432    pub fn variable_map(&self) -> HashMap<String, usize> {
433        self.var_map.clone()
434    }
435
436    /// Get the total number of variables
437    #[must_use]
438    pub const fn num_variables(&self) -> usize {
439        self.num_vars
440    }
441
442    /// Get a list of all variables
443    #[must_use]
444    pub fn variables(&self) -> Vec<Variable> {
445        self.var_map
446            .iter()
447            .map(|(name, &index)| Variable::new(name, index))
448            .collect()
449    }
450}
451
452/// Default implementation for `QuboBuilder`
453impl Default for QuboBuilder {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459/// Trait for problems that can be formulated as QUBO
460pub trait QuboFormulation {
461    /// Formulate the problem as a QUBO
462    fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)>;
463
464    /// Interpret the solution to the QUBO in the context of the original problem
465    fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>>;
466}
467
468/// Implementation of `QuboFormulation` for `QuboModel`
469impl QuboFormulation for QuboModel {
470    fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)> {
471        // QuboModel is already a QUBO, so we just return a clone
472        let mut var_map = HashMap::new();
473        for i in 0..self.num_variables {
474            var_map.insert(format!("x_{i}"), i);
475        }
476        Ok((self.clone(), var_map))
477    }
478
479    fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>> {
480        if binary_vars.len() != self.num_variables {
481            return Err(QuboError::ConstraintError(format!(
482                "Solution length {} does not match number of variables {}",
483                binary_vars.len(),
484                self.num_variables
485            )));
486        }
487
488        let mut result = Vec::new();
489        for (i, &value) in binary_vars.iter().enumerate() {
490            result.push((format!("x_{i}"), value));
491        }
492        Ok(result)
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_qubo_builder_basic() {
502        let mut builder = QuboBuilder::new();
503
504        // Add variables
505        let x1 = builder
506            .add_variable("x1")
507            .expect("failed to add variable x1");
508        let x2 = builder
509            .add_variable("x2")
510            .expect("failed to add variable x2");
511        let x3 = builder
512            .add_variable("x3")
513            .expect("failed to add variable x3");
514
515        // Set coefficients
516        builder
517            .set_linear_term(&x1, 2.0)
518            .expect("failed to set linear term for x1");
519        builder
520            .set_linear_term(&x2, -1.0)
521            .expect("failed to set linear term for x2");
522        builder
523            .set_quadratic_term(&x1, &x2, -4.0)
524            .expect("failed to set quadratic term for x1-x2");
525        builder
526            .set_quadratic_term(&x2, &x3, 2.0)
527            .expect("failed to set quadratic term for x2-x3");
528        builder.set_offset(1.5).expect("failed to set offset");
529
530        // Build the QUBO model
531        let model = builder.build();
532
533        // Check linear terms
534        assert_eq!(
535            model.get_linear(0).expect("failed to get linear term 0"),
536            2.0
537        );
538        assert_eq!(
539            model.get_linear(1).expect("failed to get linear term 1"),
540            -1.0
541        );
542        assert_eq!(
543            model.get_linear(2).expect("failed to get linear term 2"),
544            0.0
545        );
546
547        // Check quadratic terms
548        assert_eq!(
549            model
550                .get_quadratic(0, 1)
551                .expect("failed to get quadratic term 0-1"),
552            -4.0
553        );
554        assert_eq!(
555            model
556                .get_quadratic(1, 2)
557                .expect("failed to get quadratic term 1-2"),
558            2.0
559        );
560
561        // Check offset
562        assert_eq!(model.offset, 1.5);
563    }
564
565    #[test]
566    fn test_qubo_builder_objective() {
567        let mut builder = QuboBuilder::new();
568
569        // Add variables
570        let x1 = builder
571            .add_variable("x1")
572            .expect("failed to add variable x1");
573        let x2 = builder
574            .add_variable("x2")
575            .expect("failed to add variable x2");
576
577        // Add objective terms
578        builder
579            .minimize_linear(&x1, 2.0)
580            .expect("failed to minimize linear x1");
581        builder
582            .minimize_linear(&x2, -1.0)
583            .expect("failed to minimize linear x2");
584        builder
585            .minimize_quadratic(&x1, &x2, -4.0)
586            .expect("failed to minimize quadratic x1-x2");
587
588        // Build the QUBO model
589        let model = builder.build();
590
591        // Check linear terms
592        assert_eq!(
593            model.get_linear(0).expect("failed to get linear term 0"),
594            2.0
595        );
596        assert_eq!(
597            model.get_linear(1).expect("failed to get linear term 1"),
598            -1.0
599        );
600
601        // Check quadratic terms
602        assert_eq!(
603            model
604                .get_quadratic(0, 1)
605                .expect("failed to get quadratic term 0-1"),
606            -4.0
607        );
608    }
609
610    #[test]
611    fn test_qubo_builder_constraints() {
612        let mut builder = QuboBuilder::new();
613
614        // Add variables
615        let x1 = builder
616            .add_variable("x1")
617            .expect("failed to add variable x1");
618        let x2 = builder
619            .add_variable("x2")
620            .expect("failed to add variable x2");
621        let x3 = builder
622            .add_variable("x3")
623            .expect("failed to add variable x3");
624
625        // Set constraint weight
626        builder
627            .set_constraint_weight(5.0)
628            .expect("failed to set constraint weight");
629
630        // Add equality constraint
631        builder
632            .constrain_equal(&x1, &x2)
633            .expect("failed to add equality constraint");
634
635        // Add inequality constraint
636        builder
637            .constrain_different(&x2, &x3)
638            .expect("failed to add inequality constraint");
639
640        // Build the QUBO model
641        let model = builder.build();
642
643        // Check the model
644        // x1 = x2 constraint adds: 5 * (x1 - x2)^2 = 5 * (x1 + x2 - 2*x1*x2)
645        // x2 != x3 constraint adds: 5 * (1 - (x2 - x3)^2) = 5 * (1 - x2 - x3 + 2*x2*x3)
646
647        // Check linear terms
648        assert_eq!(
649            model.get_linear(0).expect("failed to get linear term 0"),
650            5.0
651        ); // x1: +5 from equality
652        assert_eq!(
653            model.get_linear(1).expect("failed to get linear term 1"),
654            5.0 - 5.0
655        ); // x2: +5 from equality, -5 from inequality
656        assert_eq!(
657            model.get_linear(2).expect("failed to get linear term 2"),
658            -5.0
659        ); // x3: -5 from inequality
660
661        // Check quadratic terms
662        assert_eq!(
663            model
664                .get_quadratic(0, 1)
665                .expect("failed to get quadratic term 0-1"),
666            -10.0
667        ); // x1*x2: -2*5 from equality
668        assert_eq!(
669            model
670                .get_quadratic(1, 2)
671                .expect("failed to get quadratic term 1-2"),
672            10.0
673        ); // x2*x3: +2*5 from inequality
674
675        // Check offset
676        assert_eq!(model.offset, 5.0); // +5 from inequality
677    }
678
679    #[test]
680    fn test_qubo_builder_one_hot() {
681        let mut builder = QuboBuilder::new();
682
683        // Add variables
684        let x1 = builder
685            .add_variable("x1")
686            .expect("failed to add variable x1");
687        let x2 = builder
688            .add_variable("x2")
689            .expect("failed to add variable x2");
690        let x3 = builder
691            .add_variable("x3")
692            .expect("failed to add variable x3");
693
694        // Set constraint weight
695        builder
696            .set_constraint_weight(5.0)
697            .expect("failed to set constraint weight");
698
699        // Add one-hot constraint
700        builder
701            .constrain_one_hot(&[x1.clone(), x2.clone(), x3.clone()])
702            .expect("failed to add one-hot constraint");
703
704        // Build the QUBO model
705        let model = builder.build();
706
707        // Check the model
708        // One-hot constraint adds: 5 * (x1 + x2 + x3 - 1)^2
709        // = 5 * (x1 + x2 + x3 + x1*x2 + x1*x3 + x2*x3 - 2*(x1 + x2 + x3) + 1)
710        // = 5 * (x1*x2 + x1*x3 + x2*x3 - x1 - x2 - x3 + 1)
711
712        // Check linear terms
713        assert_eq!(
714            model.get_linear(0).expect("failed to get linear term 0"),
715            -5.0
716        ); // x1: -5 from one-hot
717        assert_eq!(
718            model.get_linear(1).expect("failed to get linear term 1"),
719            -5.0
720        ); // x2: -5 from one-hot
721        assert_eq!(
722            model.get_linear(2).expect("failed to get linear term 2"),
723            -5.0
724        ); // x3: -5 from one-hot
725
726        // Check quadratic terms
727        assert_eq!(
728            model
729                .get_quadratic(0, 1)
730                .expect("failed to get quadratic term 0-1"),
731            10.0
732        ); // x1*x2: +2*5 from one-hot
733        assert_eq!(
734            model
735                .get_quadratic(0, 2)
736                .expect("failed to get quadratic term 0-2"),
737            10.0
738        ); // x1*x3: +2*5 from one-hot
739        assert_eq!(
740            model
741                .get_quadratic(1, 2)
742                .expect("failed to get quadratic term 1-2"),
743            10.0
744        ); // x2*x3: +2*5 from one-hot
745
746        // Check offset
747        assert_eq!(model.offset, 5.0); // +5 from one-hot
748    }
749}