problemreductions/models/misc/
expected_retrieval_cost.rs1use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Min;
9use serde::{Deserialize, Serialize};
10
11const FLOAT_TOLERANCE: f64 = 1e-9;
12
13inventory::submit! {
14 ProblemSchemaEntry {
15 name: "ExpectedRetrievalCost",
16 display_name: "Expected Retrieval Cost",
17 aliases: &[],
18 dimensions: &[],
19 category: crate::registry::ProblemCategory::Misc,
20 module_path: module_path!(),
21 description: "Assign records to circular storage sectors to minimize expected retrieval latency",
22 fields: &[
23 FieldInfo { name: "probabilities", type_name: "Vec<f64>", description: "Access probabilities p(r) for each record" },
24 FieldInfo { name: "num_sectors", type_name: "usize", description: "Number of sectors on the drum-like device" },
25 ],
26 }
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct ExpectedRetrievalCost {
31 probabilities: Vec<f64>,
32 num_sectors: usize,
33}
34
35impl ExpectedRetrievalCost {
36 pub fn new(probabilities: Vec<f64>, num_sectors: usize) -> Result<Self, ConstructionError> {
37 if probabilities.is_empty() {
38 return Err(ConstructionError::Conversion(
39 "ExpectedRetrievalCost requires at least one record".into(),
40 ));
41 }
42 if num_sectors < 2 {
43 return Err(ConstructionError::Conversion(
44 "ExpectedRetrievalCost requires at least two sectors".into(),
45 ));
46 }
47 for (index, &probability) in probabilities.iter().enumerate() {
48 if !probability.is_finite() {
49 return Err(ConstructionError::NonFiniteFloat(format!(
50 "probability at index {index} must be finite"
51 )));
52 }
53 if !(0.0..=1.0).contains(&probability) {
54 return Err(ConstructionError::Conversion(format!(
55 "probability at index {index} must lie in [0, 1]"
56 )));
57 }
58 }
59 let total_probability: f64 = probabilities.iter().sum();
60 if !total_probability.is_finite() || (total_probability - 1.0).abs() > FLOAT_TOLERANCE {
61 if !total_probability.is_finite() {
62 return Err(ConstructionError::NonFiniteFloat(
63 "summing probabilities produced a non-finite value".into(),
64 ));
65 }
66 return Err(ConstructionError::Conversion(
67 "probabilities must sum to 1.0".into(),
68 ));
69 }
70 Ok(Self {
71 probabilities,
72 num_sectors,
73 })
74 }
75
76 pub fn probabilities(&self) -> &[f64] {
77 &self.probabilities
78 }
79
80 pub fn num_records(&self) -> usize {
81 self.probabilities.len()
82 }
83
84 pub fn num_sectors(&self) -> usize {
85 self.num_sectors
86 }
87
88 pub fn sector_masses(
89 &self,
90 config: &[usize],
91 ) -> Result<Option<Vec<f64>>, crate::traits::EvaluationError> {
92 if config.len() != self.num_records() {
93 return Ok(None);
94 }
95
96 let mut masses = vec![0.0; self.num_sectors];
97 for (record, §or) in config.iter().enumerate() {
98 if sector >= self.num_sectors {
99 return Ok(None);
100 }
101 let mass = masses[sector] + self.probabilities[record];
102 if !mass.is_finite() {
103 return Err(crate::traits::EvaluationError::NonFiniteResult(
104 "summing expected-retrieval sector probabilities".to_string(),
105 ));
106 }
107 masses[sector] = mass;
108 }
109 Ok(Some(masses))
110 }
111
112 pub fn expected_cost(
113 &self,
114 config: &[usize],
115 ) -> Result<Option<f64>, crate::traits::EvaluationError> {
116 let Some(masses) = self.sector_masses(config)? else {
117 return Ok(None);
118 };
119 let mut total = 0.0;
120 for source in 0..self.num_sectors {
121 for target in 0..self.num_sectors {
122 let latency = i64::try_from(latency_distance(self.num_sectors, source, target))
123 .map_err(|_| {
124 crate::traits::EvaluationError::IntegerOverflow(
125 "converting expected-retrieval latency to i64".to_string(),
126 )
127 })?;
128 let latency = crate::types::i64_to_exact_f64(latency).map_err(|_| {
129 crate::traits::EvaluationError::InexactFloatConversion(
130 "converting expected-retrieval latency to f64".to_string(),
131 )
132 })?;
133 let term = masses[source] * masses[target] * latency;
134 let next = total + term;
135 if !term.is_finite() || !next.is_finite() {
136 return Err(crate::traits::EvaluationError::NonFiniteResult(
137 "computing expected retrieval cost".to_string(),
138 ));
139 }
140 total = next;
141 }
142 }
143 Ok(Some(total))
144 }
145
146 pub fn is_valid_solution(
147 &self,
148 config: &[usize],
149 ) -> Result<bool, crate::traits::EvaluationError> {
150 Ok(self.expected_cost(config)?.is_some())
151 }
152}
153
154#[derive(Deserialize)]
155struct ExpectedRetrievalCostData {
156 probabilities: Vec<f64>,
157 num_sectors: usize,
158}
159
160impl<'de> Deserialize<'de> for ExpectedRetrievalCost {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: serde::Deserializer<'de>,
164 {
165 let data = ExpectedRetrievalCostData::deserialize(deserializer)?;
166 Self::new(data.probabilities, data.num_sectors).map_err(serde::de::Error::custom)
167 }
168}
169
170impl Problem for ExpectedRetrievalCost {
171 const NAME: &'static str = "ExpectedRetrievalCost";
172 type Solution = Vec<usize>;
173 type Value = Min<f64>;
174
175 crate::problem_parameters![("num_records", num_records), ("num_sectors", num_sectors),];
176
177 fn variant() -> Vec<(&'static str, &'static str)> {
178 crate::variant_params![]
179 }
180
181 fn evaluate(
182 &self,
183 config: &Self::Solution,
184 ) -> Result<Min<f64>, crate::traits::EvaluationError> {
185 if config.len() != self.num_records() {
186 return Err(crate::traits::EvaluationError::InvalidConfiguration(
187 "sector assignment length does not match the records".into(),
188 ));
189 }
190 if config.iter().any(|§or| sector >= self.num_sectors) {
191 return Err(crate::traits::EvaluationError::InvalidConfiguration(
192 "sector assignment contains an out-of-range sector".into(),
193 ));
194 }
195 Ok({
196 match self.expected_cost(config)? {
197 Some(cost) => Min(Some(cost)),
198 None => Min(None),
199 }
200 })
201 }
202}
203
204impl crate::solvers::BruteForceProblem for ExpectedRetrievalCost {
205 fn dimensions(&self) -> Vec<usize> {
206 vec![self.num_sectors; self.num_records()]
207 }
208}
209
210fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize {
211 if source < target {
212 target - source - 1
213 } else {
214 num_sectors - source + target - 1
215 }
216}
217
218crate::declare_variants! {
219 default ExpectedRetrievalCost => "num_sectors ^ num_records",
220}
221
222crate::register_brute_force! {
223 ExpectedRetrievalCost,
224}
225
226#[cfg(feature = "example-db")]
227pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
228 vec![crate::example_db::specs::ModelExampleSpec {
229 id: "expected_retrieval_cost",
230 instance: Box::new(
231 ExpectedRetrievalCost::new(vec![0.2, 0.15, 0.15, 0.2, 0.1, 0.2], 3).unwrap(),
232 ),
233 optimal_config: serde_json::json!(vec![0, 1, 2, 1, 0, 2]),
234 optimal_value: serde_json::json!(1.0025),
235 }]
236}
237
238#[cfg(test)]
239#[path = "../../unit_tests/models/misc/expected_retrieval_cost.rs"]
240mod tests;