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
use crate::solvers::{
find_all_ground_states, simulated_annealing, Epoch, SimulatedAnnealingConfiguration,
};
use crate::types::{
BinaryNode, Energy, ExternalMagneticField, Interactions, MagneticFieldStrength, SpinIndex,
State, UnaryNode
};
/// A SpinNetwork is meant to represent a 2D Spin Glass.
/// It provides methods to add any number of nodes with one, two, or n inputs, and one output.
#[derive(Default)]
pub struct SpinNetwork {
input_nodes: Vec<SpinIndex>,
auxiliary_nodes: Vec<SpinIndex>,
output_nodes: Vec<SpinIndex>,
pub interactions: Interactions,
pub external_magnetic_field: ExternalMagneticField,
}
impl SpinNetwork {
/// Creates a new SpinNetwork with no nodes, interactions or external magnetic field
pub fn new() -> Self {
return Default::default();
}
fn add_free_node(&mut self) -> usize {
self.external_magnetic_field.push(0.0);
self.external_magnetic_field.len() - 1
}
/// Adds an output node. You will most likely only use this method if you want to implement your own [Node].
pub fn add_output_node(&mut self, magnetic_field_strength: MagneticFieldStrength) -> usize {
let node_index = self.add_free_node();
self.output_nodes.push(node_index);
*self.external_magnetic_field.get_mut(node_index).unwrap() = magnetic_field_strength;
node_index
}
/// Adds an auxiliary node. You will most likely only use this method if you want to implement your own [Node].
pub fn add_auxiliary_node(&mut self, magnetic_field_strength: MagneticFieldStrength) -> usize {
let node_index = self.add_free_node();
self.auxiliary_nodes.push(node_index);
*self.external_magnetic_field.get_mut(node_index).unwrap() = magnetic_field_strength;
node_index
}
/// Adds an input node. If you add a positive magnetic field to the input node, it will act like a classical circuit
/// with an input set to 1. You need input nodelib.
///
/// ### Example
///
/// ```
/// use ernst::spin_network::SpinNetwork;
///
/// let mut spin_network = SpinNetwork::new();
/// let s0 = spin_network.add_input_node(2.0);
///
/// assert_eq!(spin_network.external_magnetic_field[0], 2.0)
/// ```
pub fn add_input_node(&mut self, magnetic_field_strength: MagneticFieldStrength) -> usize {
let node_index = self.add_free_node();
self.input_nodes.push(node_index);
*self.external_magnetic_field.get_mut(node_index).unwrap() = magnetic_field_strength;
node_index
}
/// Adds a Node with a single input and output. It returns the index of the output node.
///
/// ### Example
///
/// ```
/// use ernst::spin_network::SpinNetwork;
/// use ernst::nodelib::logic_gates::COPY;
///
/// let mut spin_network = SpinNetwork::new();
/// let s0 = spin_network.add_input_node(0.0);
/// let copy_gate = COPY::default();
///
/// spin_network.add_unary_node(s0, ©_gate);
/// ```
pub fn add_unary_node(&mut self, input: usize, unary_node: &impl UnaryNode) -> usize {
return UnaryNode::connect_to_one(unary_node, self, input);
}
/// Adds a Node with two inputs and one output. It returns the index of the output node.
///
/// ### Example
///
/// ```
/// use ernst::spin_network::SpinNetwork;
/// use ernst::nodelib::logic_gates::AND;
///
/// let mut spin_network = SpinNetwork::new();
/// let s0 = spin_network.add_input_node(0.0);
/// let s1 = spin_network.add_input_node(0.0);
/// let and_gate = AND::default();
///
/// spin_network.add_binary_node(s0, s1, &and_gate);
/// ```
pub fn add_binary_node(
&mut self,
left_input: usize,
right_input: usize,
binary_node: &impl BinaryNode,
) -> usize {
return BinaryNode::connect_to_two(binary_node, self, left_input, right_input);
}
/// Finds all ground states of the spin glass represented by the SpinNetwork. The argument `spin_ordering`, when
/// given, will ensure that the `State`s will be projected according to it.
///
/// ### Example
///
/// ```
/// use ernst::spin_network::SpinNetwork;
/// use ernst::nodelib::logic_gates::OR;
///
/// let mut spin_network = SpinNetwork::new();
/// let s0 = spin_network.add_input_node(0.0);
/// let s1 = spin_network.add_input_node(0.0);
/// let s2 = spin_network.add_input_node(0.0);
///
/// let or_gate = OR::default();
/// let z_aux = spin_network.add_binary_node(s0, s1, &or_gate);
/// let z = spin_network.add_binary_node(z_aux, s2, &or_gate);
///
/// // Note how we only ask for ground states to be ordered according to the "interesting" spins i.e
/// // the ones that are able to
/// let actual_ground_states = spin_network.find_all_ground_states(Some(vec![s0, s1, s2, z]));
/// let expected_ground_states = vec![
/// (-7.0, vec![false, false, false, false]),
/// (-7.0, vec![true, false, false, true]),
/// (-7.0, vec![true, true, false, true]),
/// (-7.0, vec![false, true, false, true]),
/// (-7.0, vec![false, true, true, true]),
/// (-7.0, vec![true, true, true, true]),
/// (-7.0, vec![true, false, true, true]),
/// (-7.0, vec![false, false, true, true]),
/// ];
///
/// assert_eq!(expected_ground_states, actual_ground_states)
/// ```
pub fn find_all_ground_states(
&self,
spin_ordering: Option<Vec<SpinIndex>>,
) -> Vec<(Energy, State)> {
return find_all_ground_states(&self.interactions, &self.external_magnetic_field)
.into_iter()
.map(|(energy, state)| {
if let Some(spin_ordering) = &spin_ordering {
return (
energy,
spin_ordering
.iter()
.map(|spin_index| state[*spin_index])
.collect(),
);
}
return (energy, state);
})
.collect();
}
/// Explores the energy landscape of the spin glass represented by the SpinNetwork. The argument `spin_ordering`, when
/// given, will ensure that the `State`s will be projected according
/// to it.
///
/// ### Example
///
/// ```
/// use ernst::spin_network::SpinNetwork;
/// use ernst::nodelib::logic_gates::OR;
///
/// let mut spin_network = SpinNetwork::new();
/// let s0 = spin_network.add_input_node(0.0);
/// let s1 = spin_network.add_input_node(0.0);
/// let s2 = spin_network.add_input_node(0.0);
///
/// let or_gate = OR::default();
/// let z_aux = spin_network.add_binary_node(s0, s1, &or_gate);
/// let z = spin_network.add_binary_node(z_aux, s2, &or_gate);
///
/// // Note how we only ask for ground states to be ordered according to the "interesting" spins i.e
/// // the ones that are able to
/// let actual_ground_states = spin_network.run_simulated_annealing(None, Some(vec![s0, s1, s2, z]));
/// let expected_ground_states = vec![
/// (-7.0, vec![false, false, false, false]),
/// (-7.0, vec![true, false, false, true]),
/// (-7.0, vec![true, true, false, true]),
/// (-7.0, vec![false, true, false, true]),
/// (-7.0, vec![false, true, true, true]),
/// (-7.0, vec![true, true, true, true]),
/// (-7.0, vec![true, false, true, true]),
/// (-7.0, vec![false, false, true, true]),
/// ];
///
/// assert_eq!(expected_ground_states, actual_ground_states)
/// ```
pub fn run_simulated_annealing(
&self,
configuration_override: Option<&SimulatedAnnealingConfiguration>,
spin_ordering: Option<Vec<SpinIndex>>,
) -> Vec<(Energy, State, Epoch)> {
return simulated_annealing(
&self.interactions,
&self.external_magnetic_field,
configuration_override,
)
.into_iter()
.map(|(energy, state, epoch)| {
if let Some(spin_ordering) = &spin_ordering {
return (
energy,
spin_ordering
.iter()
.map(|spin_index| state[*spin_index])
.collect(),
epoch,
);
}
(energy, state, epoch)
})
.collect();
}
/// Returns the external magnetic field with flipped signs. The output of this function alongside `inverted_interactions`
/// should be all that you need to find the ground state of this Spin Glass on a real quantum annealer.
pub fn inverted_external_magnetic_field(&self) -> ExternalMagneticField {
return self.external_magnetic_field.iter().map(|&energy| -energy).collect();
}
/// Returns the interaction terms flipped signs. The output of this function alongside `inverted_external_magnetic_field`
/// should be all that you need to find the ground state of this Spin Glass on a real quantum annealer.
pub fn inverted_interactions(&self) -> Interactions {
return self.interactions.iter().map(|(left_spin_index, right_spin_index, energy)| (*left_spin_index, *right_spin_index, -(*energy))).collect();
}
}