Skip to main content

rbp_server/analysis/
api.rs

1use rbp_cards::*;
2use rbp_clustering::*;
3use rbp_core::*;
4use rbp_gameplay::*;
5use rbp_mccfr::Decision;
6use rbp_nlhe::*;
7use rbp_database::*;
8use rbp_transport::*;
9use std::sync::Arc;
10use tokio_postgres::Client;
11
12const N_NEIGHBORS: i64 = 6;
13
14// Local conversion functions for database types.
15// These bridge tokio_postgres::Row to our domain types.
16// We use free functions instead of From traits due to orphan rules.
17
18fn api_sample_from_row(row: tokio_postgres::Row) -> ApiSample {
19    ApiSample {
20        obs: Observation::from(row.get::<_, i64>("obs")).to_string(),
21        abs: Abstraction::from(row.get::<_, i16>("abs")).to_string(),
22        equity: row.get::<_, f32>("equity"),
23        density: row.get::<_, f32>("density"),
24        distance: row.try_get::<_, f32>("distance").unwrap_or_default(),
25    }
26}
27
28fn decision_from_row(row: tokio_postgres::Row) -> Decision<NlheEdge> {
29    Decision {
30        edge: NlheEdge::from(row.get::<_, i64>("edge") as u64),
31        mass: Probability::from(row.get::<_, f32>("weight")),
32        counts: row.get::<_, i32>("counts") as u32,
33    }
34}
35
36fn api_strategy_from(strategy: Strategy) -> ApiStrategy {
37    let history = strategy.info().subgame();
38    let present = strategy.info().bucket();
39    let choices = strategy.info().choices();
40    ApiStrategy {
41        history: i64::from(history),
42        present: i16::from(present),
43        choices: i64::from(choices),
44        accumulated: strategy
45            .accumulated()
46            .iter()
47            .map(|(edge, policy)| (edge.to_string(), *policy))
48            .collect(),
49        counts: strategy
50            .counts()
51            .iter()
52            .map(|(edge, count)| (edge.to_string(), *count))
53            .collect(),
54    }
55}
56
57pub struct API(Arc<Client>);
58
59impl From<Arc<Client>> for API {
60    fn from(client: Arc<Client>) -> Self {
61        Self(client)
62    }
63}
64
65impl API {
66    pub fn new(client: Arc<Client>) -> Self {
67        Self(client)
68    }
69    pub fn client(&self) -> &Arc<Client> {
70        &self.0
71    }
72}
73
74// global lookups
75impl API {
76    pub async fn obs_to_abs(&self, obs: Observation) -> anyhow::Result<Abstraction> {
77        let iso = Isomorphism::from(obs);
78        let idx = i64::from(iso);
79        let sql = const_format::concatcp!(
80            "SELECT abs FROM ",
81            ISOMORPHISM,
82            " WHERE obs = $1"
83        );
84        self.0
85            .query_one(sql, &[&idx])
86            .await
87            .map(|row| Abstraction::from(row.get::<_, i16>(0)))
88            .map_err(|e| anyhow::anyhow!("fetch abstraction: {}", e))
89    }
90    pub async fn metric(&self, street: Street) -> anyhow::Result<Metric> {
91        let s = street as i16;
92        let sql = const_format::concatcp!(
93            "SELECT tri, dx FROM ",
94            METRIC,
95            " WHERE street = $1"
96        );
97        let rows = self
98            .0
99            .query(sql, &[&s])
100            .await
101            .map_err(|e| anyhow::anyhow!("fetch metric: {}", e))?;
102        let mut metric = Metric::new(street);
103        for row in rows {
104            let tri = row.get::<_, i32>(0);
105            let dx = row.get::<_, Energy>(1);
106            metric.set(Pair::from(tri), dx);
107        }
108        Ok(metric)
109    }
110}
111
112// equity calculations
113impl API {
114    pub async fn abs_equity(&self, abs: Abstraction) -> anyhow::Result<Probability> {
115        let abs = i16::from(abs);
116        let sql = const_format::concatcp!(
117            "SELECT equity FROM ",
118            ABSTRACTION,
119            " WHERE abs = $1"
120        );
121        self.0
122            .query_one(sql, &[&abs])
123            .await
124            .map(|row| Probability::from(row.get::<_, f32>(0)))
125            .map_err(|e| anyhow::anyhow!("fetch abstraction equity: {}", e))
126    }
127    pub async fn obs_equity(&self, obs: Observation) -> anyhow::Result<Probability> {
128        let iso = i64::from(Isomorphism::from(obs));
129        let river: &str = const_format::concatcp!(
130            "SELECT equity ",
131            "FROM   ",
132            ISOMORPHISM,
133            " ",
134            "WHERE  obs = $1"
135        );
136        let other: &str = const_format::concatcp!(
137            "SELECT SUM(t.dx * a.equity) ",
138            "FROM   ",
139            TRANSITIONS,
140            " t ",
141            "JOIN   ",
142            ISOMORPHISM,
143            " e ON e.abs = t.prev ",
144            "JOIN   ",
145            ABSTRACTION,
146            " a ON a.abs = t.next ",
147            "WHERE  e.obs = $1"
148        );
149        let sql = if obs.street() == Street::Rive {
150            river
151        } else {
152            other
153        };
154        Ok(self
155            .0
156            .query_one(sql, &[&iso])
157            .await
158            .map_err(|e| anyhow::anyhow!("fetch observation equity: {}", e))?
159            .get::<_, f32>(0)
160            .into())
161    }
162}
163
164// distance calculations
165impl API {
166    pub async fn abs_distance(
167        &self,
168        abs1: Abstraction,
169        abs2: Abstraction,
170    ) -> anyhow::Result<Energy> {
171        if abs1.street() != abs2.street() {
172            return Err(anyhow::anyhow!(
173                "abstractions must be from the same street"
174            ));
175        }
176        if abs1 == abs2 {
177            return Ok(0 as Energy);
178        }
179        let pair = Pair::from((&abs1, &abs2));
180        let tri = i32::from(pair);
181        let sql = const_format::concatcp!("SELECT dx FROM ", METRIC, " WHERE tri = $1");
182        self.0
183            .query_one(sql, &[&tri])
184            .await
185            .map(|row| row.get::<_, Energy>(0))
186            .map_err(|e| anyhow::anyhow!("fetch distance: {}", e))
187    }
188    pub async fn obs_distance(
189        &self,
190        obs1: Observation,
191        obs2: Observation,
192    ) -> anyhow::Result<Energy> {
193        if obs1.street() != obs2.street() {
194            return Err(anyhow::anyhow!(
195                "observations must be from the same street"
196            ));
197        }
198        let (ref hx, ref hy, ref metric) = tokio::try_join!(
199            self.obs_histogram(obs1),
200            self.obs_histogram(obs2),
201            self.metric(obs1.street().next())
202        )?;
203        Ok(Sinkhorn::from((hx, hy, metric)).minimize().cost())
204    }
205}
206
207// population lookups
208impl API {
209    pub async fn abs_population(&self, abs: Abstraction) -> anyhow::Result<usize> {
210        let abs = i16::from(abs);
211        let sql = const_format::concatcp!(
212            "SELECT population FROM ",
213            ABSTRACTION,
214            " WHERE abs = $1"
215        );
216        self.0
217            .query_one(sql, &[&abs])
218            .await
219            .map(|row| row.get::<_, i64>(0) as usize)
220            .map_err(|e| anyhow::anyhow!("fetch abstraction population: {}", e))
221    }
222    pub async fn obs_population(&self, obs: Observation) -> anyhow::Result<usize> {
223        let iso = i64::from(Isomorphism::from(obs));
224        let sql: &str = const_format::concatcp!(
225            "SELECT population   ",
226            "FROM   ",
227            ABSTRACTION,
228            " a ",
229            "JOIN   ",
230            ISOMORPHISM,
231            " e ON e.abs = a.abs ",
232            "WHERE  e.obs = $1"
233        );
234        Ok(self
235            .0
236            .query_one(sql, &[&iso])
237            .await
238            .map_err(|e| anyhow::anyhow!("fetch observation population: {}", e))?
239            .get::<_, i64>(0) as usize)
240    }
241}
242
243// histogram aggregation
244impl API {
245    pub async fn abs_histogram(&self, abs: Abstraction) -> anyhow::Result<Histogram> {
246        let abs_i = i16::from(abs);
247        let sql = const_format::concatcp!(
248            "SELECT next, dx FROM ",
249            TRANSITIONS,
250            " WHERE prev = $1"
251        );
252        let street = abs.street().next();
253        let rows = self
254            .0
255            .query(sql, &[&abs_i])
256            .await
257            .map_err(|e| anyhow::anyhow!("fetch abstraction histogram: {}", e))?;
258        Ok(rows
259            .iter()
260            .map(|row| (row.get::<_, i16>(0), row.get::<_, Energy>(1)))
261            .map(|(next, dx)| (Abstraction::from(next), (dx * 1000.0) as usize))
262            .fold(Histogram::empty(street), |mut h, (next, dx)| {
263                h.set(next, dx);
264                h
265            }))
266    }
267    pub async fn obs_histogram(&self, obs: Observation) -> anyhow::Result<Histogram> {
268        let idx = i64::from(Isomorphism::from(obs));
269        let mass = obs.street().n_children() as f32;
270        let sql: &str = const_format::concatcp!(
271            "SELECT next, ",
272            "dx ",
273            "FROM   ",
274            TRANSITIONS,
275            " t ",
276            "JOIN   ",
277            ISOMORPHISM,
278            " e ON e.abs = t.prev ",
279            "WHERE  e.obs = $1"
280        );
281        let street = obs.street().next();
282        Ok(self
283            .0
284            .query(sql, &[&idx])
285            .await
286            .map_err(|e| anyhow::anyhow!("fetch observation histogram: {}", e))?
287            .iter()
288            .map(|row| (row.get::<_, i16>(0), row.get::<_, Energy>(1)))
289            .map(|(next, dx)| (next, (dx * mass).round() as usize))
290            .map(|(next, dx)| (Abstraction::from(next), dx))
291            .fold(Histogram::empty(street), |mut h, (next, dx)| {
292                h.set(next, dx);
293                h
294            }))
295    }
296}
297
298// exploration panel
299impl API {
300    pub async fn exp_wrt_str(&self, str: Street) -> anyhow::Result<ApiSample> {
301        self.exp_wrt_obs(Observation::from(str)).await
302    }
303    pub async fn exp_wrt_obs(&self, obs: Observation) -> anyhow::Result<ApiSample> {
304        let sql: &str = const_format::concatcp!(
305            "SELECT e.obs, ",
306            "a.abs, ",
307            "a.equity::REAL, ",
308            "a.population::REAL / $2 AS density ",
309            "FROM   ",
310            ISOMORPHISM,
311            " e ",
312            "JOIN   ",
313            ABSTRACTION,
314            " a ON a.abs = e.abs ",
315            "WHERE  e.obs = $1"
316        );
317        let n = obs.street().n_observations() as f32;
318        let iso = i64::from(Isomorphism::from(obs));
319        let row = self
320            .0
321            .query_one(sql, &[&iso, &n])
322            .await
323            .map_err(|e| anyhow::anyhow!("explore with respect to observation: {}", e))?;
324        Ok(api_sample_from_row(row))
325    }
326    pub async fn exp_wrt_abs(&self, abs: Abstraction) -> anyhow::Result<ApiSample> {
327        let sql: &str = const_format::concatcp!(
328            "WITH sample AS ( ",
329            "SELECT a.abs, ",
330            "a.population, ",
331            "a.equity, ",
332            "FLOOR(RANDOM() * a.population)::INTEGER AS i ",
333            "FROM   ",
334            ABSTRACTION,
335            " a ",
336            "WHERE  a.abs = $1 ",
337            ") ",
338            "SELECT e.obs, ",
339            "s.abs, ",
340            "s.equity::REAL, ",
341            "s.population::REAL / $2 AS density ",
342            "FROM   sample s ",
343            "JOIN   ",
344            ISOMORPHISM,
345            " e ON e.abs = s.abs AND e.position = s.i ",
346            "LIMIT  1"
347        );
348        let n = abs.street().n_isomorphisms() as f32;
349        let abs = i16::from(abs);
350        let row = self
351            .0
352            .query_one(sql, &[&abs, &n])
353            .await
354            .map_err(|e| anyhow::anyhow!("explore with respect to abstraction: {}", e))?;
355        Ok(api_sample_from_row(row))
356    }
357}
358
359// proximity lookups
360impl API {
361    pub async fn abs_nearby(&self, abs: Abstraction) -> anyhow::Result<Vec<(Abstraction, Energy)>> {
362        let abs = i16::from(abs);
363        let sql: &str = const_format::concatcp!(
364            "SELECT   a.abs, ",
365            "m.dx ",
366            "FROM     ",
367            ABSTRACTION,
368            " a ",
369            "JOIN     ",
370            METRIC,
371            " m ON m.tri = get_pair_tri(a.abs, $1) ",
372            "WHERE    a.abs != $1 ",
373            "ORDER BY m.dx ASC ",
374            "LIMIT    $2"
375        );
376        Ok(self
377            .0
378            .query(sql, &[&abs, &N_NEIGHBORS])
379            .await
380            .map_err(|e| anyhow::anyhow!("fetch nearby abstractions: {}", e))?
381            .iter()
382            .map(|row| (row.get::<_, i16>(0), row.get::<_, Energy>(1)))
383            .map(|(abs, distance)| (Abstraction::from(abs), distance))
384            .collect())
385    }
386    pub async fn obs_nearby(&self, obs: Observation) -> anyhow::Result<Vec<(Abstraction, Energy)>> {
387        let iso = i64::from(Isomorphism::from(obs));
388        let sql: &str = const_format::concatcp!(
389            "SELECT   a.abs, ",
390            "m.dx ",
391            "FROM     ",
392            ISOMORPHISM,
393            " e ",
394            "JOIN     ",
395            ABSTRACTION,
396            " a ON a.street = get_street_abs(e.abs) ",
397            "JOIN     ",
398            METRIC,
399            " m ON m.tri = get_pair_tri(a.abs, e.abs) ",
400            "WHERE    e.obs = $1 ",
401            "AND      a.abs != e.abs ",
402            "ORDER BY m.dx ASC ",
403            "LIMIT    $2"
404        );
405        Ok(self
406            .0
407            .query(sql, &[&iso, &N_NEIGHBORS])
408            .await
409            .map_err(|e| anyhow::anyhow!("fetch nearby abstractions for observation: {}", e))?
410            .iter()
411            .map(|row| (row.get::<_, i16>(0), row.get::<_, Energy>(1)))
412            .map(|(abs, distance)| (Abstraction::from(abs), distance))
413            .collect())
414    }
415}
416
417// similarity lookups
418impl API {
419    pub async fn obs_similar(&self, obs: Observation) -> anyhow::Result<Vec<Observation>> {
420        let iso = i64::from(Isomorphism::from(obs));
421        let sql: &str = const_format::concatcp!(
422            "WITH target AS ( ",
423            "SELECT abs, population ",
424            "FROM   ",
425            ISOMORPHISM,
426            " e ",
427            "JOIN   ",
428            ABSTRACTION,
429            " a ON a.abs = e.abs ",
430            "WHERE  e.obs = $1 ",
431            ") ",
432            "SELECT   e.obs ",
433            "FROM     ",
434            ISOMORPHISM,
435            " e ",
436            "JOIN     target t ON t.abs = e.abs ",
437            "WHERE    e.obs != $1 ",
438            "AND      e.position  < LEAST($2, t.population) ",
439            "AND      e.position >= FLOOR(RANDOM() * GREATEST(t.population - $2, 1)) ",
440            "LIMIT    $2"
441        );
442        Ok(self
443            .0
444            .query(sql, &[&iso, &N_NEIGHBORS])
445            .await
446            .map_err(|e| anyhow::anyhow!("fetch similar observations: {}", e))?
447            .iter()
448            .map(|row| row.get::<_, i64>(0))
449            .map(Observation::from)
450            .collect())
451    }
452    pub async fn abs_similar(&self, abs: Abstraction) -> anyhow::Result<Vec<Observation>> {
453        let abs = i16::from(abs);
454        let sql: &str = const_format::concatcp!(
455            "WITH target AS ( ",
456            "SELECT population ",
457            "FROM   ",
458            ABSTRACTION,
459            " ",
460            "WHERE  abs = $1 ",
461            ") ",
462            "SELECT   obs ",
463            "FROM     ",
464            ISOMORPHISM,
465            " e, target t ",
466            "WHERE    e.abs = $1 ",
467            "AND      e.position  < LEAST($2, t.population) ",
468            "AND      e.position >= FLOOR(RANDOM() * GREATEST(t.population - $2, 1)) ",
469            "LIMIT    $2"
470        );
471        Ok(self
472            .0
473            .query(sql, &[&abs, &N_NEIGHBORS])
474            .await
475            .map_err(|e| anyhow::anyhow!("fetch observations similar to abstraction: {}", e))?
476            .iter()
477            .map(|row| row.get::<_, i64>(0))
478            .map(Observation::from)
479            .collect())
480    }
481    pub async fn replace_obs(&self, obs: Observation) -> anyhow::Result<Observation> {
482        let sql: &str = const_format::concatcp!(
483            "WITH sample AS ( ",
484            "SELECT e.abs, ",
485            "a.population, ",
486            "FLOOR(RANDOM() * a.population)::INTEGER AS i ",
487            "FROM   ",
488            ISOMORPHISM,
489            " e ",
490            "JOIN   ",
491            ABSTRACTION,
492            " a ON a.abs = e.abs ",
493            "WHERE  e.obs = $1 ",
494            ") ",
495            "SELECT e.obs ",
496            "FROM   sample s ",
497            "JOIN   ",
498            ISOMORPHISM,
499            " e ON e.abs = s.abs AND e.position = s.i ",
500            "LIMIT  1"
501        );
502        let iso = i64::from(Isomorphism::from(obs));
503        let row = self
504            .0
505            .query_one(sql, &[&iso])
506            .await
507            .map_err(|e| anyhow::anyhow!("replace observation: {}", e))?;
508        Ok(Observation::from(row.get::<_, i64>(0)))
509    }
510}
511
512// neighborhood lookups
513impl API {
514    pub async fn nbr_any_wrt_abs(&self, wrt: Abstraction) -> anyhow::Result<ApiSample> {
515        use rand::prelude::IndexedRandom;
516        let ref mut rng = rand::rng();
517        let abs = Abstraction::all(wrt.street())
518            .into_iter()
519            .filter(|&x| x != wrt)
520            .collect::<Vec<_>>()
521            .choose(rng)
522            .copied()
523            .expect("more than one abstraction option");
524        self.nbr_abs_wrt_abs(wrt, abs).await
525    }
526    pub async fn nbr_abs_wrt_abs(
527        &self,
528        wrt: Abstraction,
529        abs: Abstraction,
530    ) -> anyhow::Result<ApiSample> {
531        let sql: &str = const_format::concatcp!(
532            "WITH sample AS ( ",
533            "SELECT r.abs, ",
534            "r.population, ",
535            "r.equity, ",
536            "FLOOR(RANDOM() * r.population)::INTEGER AS i, ",
537            "COALESCE(m.dx, 0) AS distance ",
538            "FROM      ",
539            ABSTRACTION,
540            " r ",
541            "LEFT JOIN ",
542            METRIC,
543            " m ON m.tri = get_pair_tri($1, $3) ",
544            "WHERE     r.abs = $1 ",
545            "), ",
546            "random_iso AS ( ",
547            "SELECT e.obs, ",
548            "e.abs, ",
549            "s.equity, ",
550            "s.population, ",
551            "s.distance ",
552            "FROM   sample s ",
553            "JOIN   ",
554            ISOMORPHISM,
555            " e ON e.abs = s.abs AND e.position = s.i ",
556            "LIMIT  1 ",
557            ") ",
558            "SELECT obs, ",
559            "abs, ",
560            "equity::REAL, ",
561            "population::REAL / $2 AS density, ",
562            "distance::REAL ",
563            "FROM   random_iso"
564        );
565        let n = wrt.street().n_isomorphisms() as f32;
566        let abs = i16::from(abs);
567        let wrt = i16::from(wrt);
568        let row = self
569            .0
570            .query_one(sql, &[&abs, &n, &wrt])
571            .await
572            .map_err(|e| anyhow::anyhow!("fetch neighbor abstraction: {}", e))?;
573        Ok(api_sample_from_row(row))
574    }
575    pub async fn nbr_obs_wrt_abs(
576        &self,
577        wrt: Abstraction,
578        obs: Observation,
579    ) -> anyhow::Result<ApiSample> {
580        let sql: &str = const_format::concatcp!(
581            "WITH given AS ( ",
582            "SELECT obs, abs, get_pair_tri(abs, $3) AS tri ",
583            "FROM   ",
584            ISOMORPHISM,
585            " ",
586            "WHERE  obs = $1 ",
587            ") ",
588            "SELECT g.obs, ",
589            "g.abs, ",
590            "a.equity::REAL, ",
591            "a.population::REAL / $2 AS density, ",
592            "COALESCE(m.dx, 0)::REAL AS distance ",
593            "FROM   given g ",
594            "JOIN   ",
595            METRIC,
596            " m ON m.tri = g.tri ",
597            "JOIN   ",
598            ABSTRACTION,
599            " a ON a.abs = g.abs ",
600            "LIMIT  1"
601        );
602        let n = wrt.street().n_isomorphisms() as f32;
603        let iso = i64::from(Isomorphism::from(obs));
604        let wrt = i16::from(wrt);
605        let row = self
606            .0
607            .query_one(sql, &[&iso, &n, &wrt])
608            .await
609            .map_err(|e| anyhow::anyhow!("fetch neighbor observation: {}", e))?;
610        Ok(api_sample_from_row(row))
611    }
612}
613
614// k-nearest neighbors lookups
615impl API {
616    pub async fn kfn_wrt_abs(&self, wrt: Abstraction) -> anyhow::Result<Vec<ApiSample>> {
617        let sql: &str = const_format::concatcp!(
618            "WITH nearest AS ( ",
619            "SELECT   a.abs, ",
620            "a.population, ",
621            "m.dx AS distance, ",
622            "FLOOR(RANDOM() * a.population)::INTEGER AS sample ",
623            "FROM     ",
624            ABSTRACTION,
625            " a ",
626            "JOIN     ",
627            METRIC,
628            " m ON m.tri = get_pair_tri(a.abs, $1) ",
629            "WHERE    a.street = $2 ",
630            "AND      a.abs != $1 ",
631            "ORDER BY m.dx DESC ",
632            "LIMIT    $3 ",
633            ") ",
634            "SELECT   e.obs, ",
635            "n.abs, ",
636            "a.equity::REAL, ",
637            "a.population::REAL / $4 AS density, ",
638            "n.distance::REAL ",
639            "FROM     nearest n ",
640            "JOIN     ",
641            ABSTRACTION,
642            " a ON a.abs = n.abs ",
643            "JOIN     ",
644            ISOMORPHISM,
645            " e ON e.abs = n.abs AND e.position = n.sample ",
646            "ORDER BY n.distance DESC"
647        );
648        let n = wrt.street().n_isomorphisms() as f32;
649        let s = wrt.street() as i16;
650        let wrt = i16::from(wrt);
651        let rows = self
652            .0
653            .query(sql, &[&wrt, &s, &N_NEIGHBORS, &n])
654            .await
655            .map_err(|e| anyhow::anyhow!("fetch k-farthest neighbors: {}", e))?;
656        Ok(rows.into_iter().map(api_sample_from_row).collect())
657    }
658    pub async fn knn_wrt_abs(&self, wrt: Abstraction) -> anyhow::Result<Vec<ApiSample>> {
659        let sql: &str = const_format::concatcp!(
660            "WITH nearest AS ( ",
661            "SELECT   a.abs, ",
662            "a.population, ",
663            "m.dx AS distance, ",
664            "FLOOR(RANDOM() * a.population)::INTEGER AS sample ",
665            "FROM     ",
666            ABSTRACTION,
667            " a ",
668            "JOIN     ",
669            METRIC,
670            " m ON m.tri = get_pair_tri(a.abs, $1) ",
671            "WHERE    a.street = $2 ",
672            "AND      a.abs != $1 ",
673            "ORDER BY m.dx ASC ",
674            "LIMIT    $3 ",
675            ") ",
676            "SELECT   e.obs, ",
677            "n.abs, ",
678            "a.equity::REAL, ",
679            "a.population::REAL / $4 AS density, ",
680            "n.distance::REAL ",
681            "FROM     nearest n ",
682            "JOIN     ",
683            ABSTRACTION,
684            " a ON a.abs = n.abs ",
685            "JOIN     ",
686            ISOMORPHISM,
687            " e ON e.abs = n.abs AND e.position = n.sample ",
688            "ORDER BY n.distance ASC"
689        );
690        let n = wrt.street().n_isomorphisms() as f32;
691        let s = wrt.street() as i16;
692        let wrt = i16::from(wrt);
693        let rows = self
694            .0
695            .query(sql, &[&wrt, &s, &N_NEIGHBORS, &n])
696            .await
697            .map_err(|e| anyhow::anyhow!("fetch k-nearest neighbors: {}", e))?;
698        Ok(rows.into_iter().map(api_sample_from_row).collect())
699    }
700    pub async fn kgn_wrt_abs(
701        &self,
702        wrt: Abstraction,
703        nbr: Vec<Observation>,
704    ) -> anyhow::Result<Vec<ApiSample>> {
705        let sql: &str = const_format::concatcp!(
706            "WITH input(obs, ord) AS ( ",
707            "SELECT unnest($3::BIGINT[]), ",
708            "generate_series(1, array_length($3, 1)) ",
709            ") ",
710            "SELECT   e.obs, ",
711            "e.abs, ",
712            "a.equity::REAL, ",
713            "a.population::REAL / $1 AS density, ",
714            "m.dx::REAL AS distance ",
715            "FROM     input i ",
716            "JOIN     ",
717            ISOMORPHISM,
718            " e ON e.obs = i.obs ",
719            "JOIN     ",
720            ABSTRACTION,
721            " a ON a.abs = e.abs ",
722            "JOIN     ",
723            METRIC,
724            " m ON m.tri = get_pair_tri(a.abs, $2) ",
725            "ORDER BY i.ord ",
726            "LIMIT    $4"
727        );
728        let isos = nbr
729            .into_iter()
730            .map(Isomorphism::from)
731            .map(i64::from)
732            .collect::<Vec<_>>();
733        let n = wrt.street().n_isomorphisms() as f32;
734        let wrt = i16::from(wrt);
735        let rows = self
736            .0
737            .query(sql, &[&n, &wrt, &&isos, &N_NEIGHBORS])
738            .await
739            .map_err(|e| anyhow::anyhow!("fetch given neighbors: {}", e))?;
740        Ok(rows.into_iter().map(api_sample_from_row).collect())
741    }
742}
743
744// histogram lookups
745impl API {
746    pub async fn hst_wrt_obs(&self, obs: Observation) -> anyhow::Result<Vec<ApiSample>> {
747        if obs.street() == Street::Rive {
748            self.hst_wrt_obs_on_river(obs).await
749        } else {
750            self.hst_wrt_obs_on_other(obs).await
751        }
752    }
753    pub async fn hst_wrt_abs(&self, abs: Abstraction) -> anyhow::Result<Vec<ApiSample>> {
754        if abs.street() == Street::Rive {
755            self.hst_wrt_abs_on_river(abs).await
756        } else {
757            self.hst_wrt_abs_on_other(abs).await
758        }
759    }
760    async fn hst_wrt_obs_on_river(&self, obs: Observation) -> anyhow::Result<Vec<ApiSample>> {
761        let sql: &str = const_format::concatcp!(
762            "WITH sample AS ( ",
763            "SELECT e.obs, ",
764            "e.abs, ",
765            "a.equity, ",
766            "a.population, ",
767            "FLOOR(RANDOM() * a.population)::INTEGER AS position ",
768            "FROM   ",
769            ISOMORPHISM,
770            " e ",
771            "JOIN   ",
772            ABSTRACTION,
773            " a ON a.abs = e.abs ",
774            "WHERE  e.abs = (SELECT abs FROM ",
775            ISOMORPHISM,
776            " WHERE obs = $1) ",
777            "LIMIT  1 ",
778            ") ",
779            "SELECT s.obs, ",
780            "s.abs, ",
781            "s.equity::REAL, ",
782            "1::REAL AS density ",
783            "FROM   sample s"
784        );
785        let iso = i64::from(Isomorphism::from(obs));
786        let rows = self
787            .0
788            .query(sql, &[&iso])
789            .await
790            .map_err(|e| anyhow::anyhow!("fetch river observation distribution: {}", e))?;
791        Ok(rows.into_iter().map(api_sample_from_row).collect())
792    }
793    async fn hst_wrt_obs_on_other(&self, obs: Observation) -> anyhow::Result<Vec<ApiSample>> {
794        // Simplified for compilation - full implementation in original code
795        let sql: &str = const_format::concatcp!(
796            "SELECT e.obs, ",
797            "e.abs, ",
798            "a.equity ",
799            "FROM   ",
800            ISOMORPHISM,
801            " e ",
802            "JOIN   ",
803            ABSTRACTION,
804            " a ON a.abs = e.abs ",
805            "WHERE  e.obs = $1"
806        );
807        let iso = i64::from(Isomorphism::from(obs));
808        let rows = self
809            .0
810            .query(sql, &[&iso])
811            .await
812            .map_err(|e| anyhow::anyhow!("fetch observation distribution: {}", e))?;
813        Ok(rows.into_iter().map(api_sample_from_row).collect())
814    }
815    async fn hst_wrt_abs_on_river(&self, abs: Abstraction) -> anyhow::Result<Vec<ApiSample>> {
816        let sql: &str = const_format::concatcp!(
817            "WITH sample AS ( ",
818            "SELECT a.abs, ",
819            "a.population, ",
820            "a.equity, ",
821            "FLOOR(RANDOM() * a.population)::INTEGER AS position ",
822            "FROM   ",
823            ABSTRACTION,
824            " a ",
825            "WHERE  a.abs = $1 ",
826            "LIMIT  1 ",
827            ") ",
828            "SELECT e.obs, ",
829            "e.abs, ",
830            "s.equity::REAL, ",
831            "1::REAL AS density ",
832            "FROM   sample s ",
833            "JOIN   ",
834            ISOMORPHISM,
835            " e ON e.abs = s.abs AND e.position = s.position"
836        );
837        let ref abs = i16::from(abs);
838        let rows = self
839            .0
840            .query(sql, &[abs])
841            .await
842            .map_err(|e| anyhow::anyhow!("fetch river abstraction distribution: {}", e))?;
843        Ok(rows.into_iter().map(api_sample_from_row).collect())
844    }
845    async fn hst_wrt_abs_on_other(&self, abs: Abstraction) -> anyhow::Result<Vec<ApiSample>> {
846        let sql: &str = const_format::concatcp!(
847            "WITH histogram AS ( ",
848            "SELECT p.abs, ",
849            "g.dx AS probability, ",
850            "p.population, ",
851            "p.equity, ",
852            "FLOOR(RANDOM() * p.population)::INTEGER AS i ",
853            "FROM   ",
854            TRANSITIONS,
855            " g ",
856            "JOIN   ",
857            ABSTRACTION,
858            " p ON p.abs = g.next ",
859            "WHERE  g.prev = $1 ",
860            ") ",
861            "SELECT   e.obs, ",
862            "t.abs, ",
863            "t.equity::REAL, ",
864            "t.probability AS density ",
865            "FROM     histogram t ",
866            "JOIN     ",
867            ISOMORPHISM,
868            " e ON e.abs = t.abs AND e.position = t.i ",
869            "ORDER BY t.probability DESC"
870        );
871        let ref abs = i16::from(abs);
872        let rows = self
873            .0
874            .query(sql, &[abs])
875            .await
876            .map_err(|e| anyhow::anyhow!("fetch abstraction distribution: {}", e))?;
877        Ok(rows.into_iter().map(api_sample_from_row).collect())
878    }
879}
880
881// blueprint lookups
882impl API {
883    pub async fn policy(&self, recall: Partial) -> anyhow::Result<Option<ApiStrategy>> {
884        let sql: &str = const_format::concatcp!(
885            "SELECT edge, ",
886            "weight, ",
887            "counts ",
888            "FROM   ",
889            BLUEPRINT,
890            " ",
891            "WHERE  past    = $1 ",
892            "AND    present = $2 ",
893            "AND    choices = $3"
894        );
895        let recall = recall.validate()?;
896        let present = self.obs_to_abs(recall.seen()).await?;
897        let info = NlheInfo::from((&recall, present));
898        let ref history = i64::from(info.subgame());
899        let ref present = i16::from(info.bucket());
900        let ref choices = i64::from(info.choices());
901        let rows = self
902            .0
903            .query(sql, &[history, present, choices])
904            .await
905            .map_err(|e| anyhow::anyhow!("fetch policy: {}", e))?;
906        match rows.len() {
907            0 => Ok(None),
908            _ => Ok(Some(api_strategy_from(Strategy::from((
909                info,
910                rows.into_iter().map(decision_from_row).collect::<Vec<_>>(),
911            ))))),
912        }
913    }
914}