use crate::lane::{F32Full, Layout, RegretLane, StrategyLane};
use crate::storage::{AccumCell, CounterCell, StorageBackend};
use crate::update_rule::UpdateRule;
use std::marker::PhantomData;
pub struct Scratch {
regret: Vec<f32>,
last_inst: Vec<f32>,
strategy: Vec<f32>,
reward: Vec<f32>,
}
impl Scratch {
#[must_use]
pub fn new(num_actions: usize) -> Self {
Self {
regret: vec![0.0; num_actions],
last_inst: vec![0.0; num_actions],
strategy: vec![0.0; num_actions],
reward: vec![0.0; num_actions],
}
}
#[must_use]
pub fn num_actions(&self) -> usize {
self.regret.len()
}
}
const INLINE_ACTIONS: usize = 8;
enum RowBuf {
Inline([f32; INLINE_ACTIONS]),
Heap(Vec<f32>),
}
impl RowBuf {
fn new(len: usize) -> Self {
if len <= INLINE_ACTIONS {
RowBuf::Inline([0.0; INLINE_ACTIONS])
} else {
RowBuf::Heap(vec![0.0; len])
}
}
fn slice_mut(&mut self, len: usize) -> &mut [f32] {
match self {
RowBuf::Inline(a) => &mut a[..len],
RowBuf::Heap(v) => &mut v[..len],
}
}
}
pub struct BatchedMatcher<R: UpdateRule, B: StorageBackend, L: Layout<R, B> = F32Full> {
params: R::Params,
num_rows: usize,
num_actions: usize,
regret: L::Regret,
strategy: L::Strategy,
last_inst: Vec<B::Cell<u32>>,
counter: B::Counter,
regret_weight: B::Cell<u32>,
_rule: PhantomData<R>,
}
impl<R: UpdateRule, B: StorageBackend, L: Layout<R, B>> BatchedMatcher<R, B, L> {
#[must_use]
pub fn new(num_rows: usize, num_actions: usize, params: R::Params) -> Self {
assert!(num_rows > 0, "num_rows must be > 0");
assert!(num_actions > 0, "num_actions must be > 0");
let last_inst = if R::LANES > 2 {
(0..num_rows * num_actions)
.map(|_| B::Cell::<u32>::default())
.collect()
} else {
Vec::new()
};
Self {
params,
num_rows,
num_actions,
regret: L::Regret::new(num_rows, num_actions),
strategy: L::Strategy::new(num_rows, num_actions),
last_inst,
counter: B::Counter::default(),
regret_weight: B::Cell::<u32>::default(),
_rule: PhantomData,
}
}
#[inline]
fn li_load(&self, idx: usize) -> f32 {
f32::from_bits(self.last_inst[idx].load())
}
#[inline]
fn li_store(&self, idx: usize, v: f32) {
self.last_inst[idx].store(v.to_bits());
}
#[inline]
fn rw_load(&self) -> f32 {
f32::from_bits(self.regret_weight.load())
}
#[inline]
fn rw_store(&self, v: f32) {
self.regret_weight.store(v.to_bits());
}
#[must_use]
pub fn num_rows(&self) -> usize {
self.num_rows
}
#[must_use]
pub fn num_actions(&self) -> usize {
self.num_actions
}
#[must_use]
pub fn num_updates(&self) -> usize {
self.counter.load()
}
fn tick(&self) -> R::Step {
let t = self.counter.fetch_incr() + 1;
R::step(&self.params, t)
}
fn advance_weight(&self, step: &R::Step) {
self.rw_store(R::regret_weight_step(step, self.rw_load()));
}
fn update_one(
&self,
row: usize,
step: &R::Step,
value: impl Fn(usize) -> f32,
s: &mut Scratch,
) -> f32 {
let a = self.num_actions;
let predictive = R::LANES > 2;
self.regret.read_row(row, a, &mut s.regret[..a]);
for i in 0..a {
if predictive {
s.last_inst[i] = self.li_load(row * a + i);
}
s.reward[i] = value(i);
}
R::strategy_from_lanes(
&self.params,
&s.regret[..a],
&s.last_inst[..a],
R::pre_discount(step),
&mut s.strategy[..a],
);
let expected = crate::vector_ops::dot(&s.strategy[..a], &s.reward[..a]);
for i in 0..a {
s.regret[i] = R::accumulate_regret(step, s.regret[i], s.reward[i], expected);
if predictive {
let inst = s.reward[i] - expected;
self.li_store(row * a + i, inst);
s.last_inst[i] = inst;
}
}
self.regret.write_row(row, a, &s.regret[..a]);
R::strategy_from_lanes(
&self.params,
&s.regret[..a],
&s.last_inst[..a],
R::post_discount(step),
&mut s.strategy[..a],
);
self.strategy
.accumulate(row, a, step, &s.strategy[..a], self.num_updates());
expected
}
pub fn update_batch(&self, value: impl Fn(usize, usize) -> f32, expected_out: &mut [f32]) {
self.update_batch_with(&mut Scratch::new(self.num_actions), value, expected_out);
}
pub fn update_batch_with(
&self,
scratch: &mut Scratch,
value: impl Fn(usize, usize) -> f32,
expected_out: &mut [f32],
) {
assert!(
expected_out.len() >= self.num_rows,
"expected_out too short"
);
assert!(
scratch.num_actions() >= self.num_actions,
"scratch too small"
);
let step = self.tick();
for (row, ev) in expected_out.iter_mut().enumerate().take(self.num_rows) {
*ev = self.update_one(row, &step, |a| value(a, row), scratch);
}
self.advance_weight(&step);
}
pub fn update_row(&self, row: usize, value: impl Fn(usize) -> f32) -> f32 {
self.update_row_with(&mut Scratch::new(self.num_actions), row, value)
}
pub fn update_row_with(
&self,
scratch: &mut Scratch,
row: usize,
value: impl Fn(usize) -> f32,
) -> f32 {
assert!(row < self.num_rows, "row out of range");
assert!(
scratch.num_actions() >= self.num_actions,
"scratch too small"
);
let step = self.tick();
let ev = self.update_one(row, &step, value, scratch);
self.advance_weight(&step);
ev
}
pub fn reset_average(&self) {
self.strategy.reset();
}
pub fn seed(&self, regret: impl Fn(usize, usize) -> f32, t0: usize) {
let a = self.num_actions;
let mut row_buf = vec![0.0f32; a];
for row in 0..self.num_rows {
for (i, slot) in row_buf.iter_mut().enumerate() {
*slot = regret(i, row);
}
self.regret.write_row(row, a, &row_buf);
}
self.counter.store(t0);
}
pub fn current_into(&self, row: usize, out: &mut [f32]) {
assert!(row < self.num_rows, "row out of range");
assert!(out.len() >= self.num_actions, "out too short");
let n = self.num_actions;
let out = &mut out[..n];
let t = self.num_updates();
let step = R::step(&self.params, t);
let predictive = R::LANES > 2;
let mut regret_buf = RowBuf::new(n);
let regret = regret_buf.slice_mut(n);
self.regret.read_row(row, n, regret);
let last_n = if predictive { n } else { 0 };
let mut last_buf = RowBuf::new(last_n);
let last_inst = last_buf.slice_mut(last_n);
for (i, slot) in last_inst.iter_mut().enumerate() {
*slot = self.li_load(row * n + i);
}
R::strategy_from_lanes(
&self.params,
regret,
last_inst,
R::post_discount(&step),
out,
);
}
pub fn average_into(&self, row: usize, out: &mut [f32]) {
assert!(row < self.num_rows, "row out of range");
assert!(out.len() >= self.num_actions, "out too short");
self.strategy.average_into(row, self.num_actions, out);
}
pub fn regret_into(&self, row: usize, out: &mut [f32]) {
assert!(row < self.num_rows, "row out of range");
assert!(out.len() >= self.num_actions, "out too short");
self.regret
.read_row(row, self.num_actions, &mut out[..self.num_actions]);
}
#[must_use]
pub fn average_regret(&self, row: usize) -> f32 {
assert!(row < self.num_rows, "row out of range");
let w = R::regret_weight_total(&self.params, self.num_updates(), self.rw_load());
if w <= 0.0 {
return 0.0;
}
let mut regret = vec![0.0; self.num_actions];
self.regret.read_row(row, self.num_actions, &mut regret);
let max_pos = regret.iter().fold(0.0_f32, |m, &r| m.max(r.max(0.0)));
max_pos / w
}
}
#[cfg(test)]
impl<R: UpdateRule, B: StorageBackend> BatchedMatcher<R, B> {
fn raw_regret(&self, row: usize) -> Vec<f32> {
let mut v = vec![0.0; self.num_actions];
self.regret.read_row(row, self.num_actions, &mut v);
v
}
fn raw_strategy(&self, row: usize) -> Vec<f32> {
(0..self.num_actions)
.map(|i| self.strategy.strategy_raw_cell(row, i, self.num_actions))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discount::DiscountParams;
use crate::lane::HalfRegret;
use crate::rules::{Dcfr, PdcfrPlus};
use crate::storage::Local;
#[test]
fn fresh_matcher_reads_uniform() {
let m = BatchedMatcher::<Dcfr, Local>::new(2, 3, DiscountParams::RECOMMENDED);
let mut out = [0.0f32; 3];
m.average_into(0, &mut out);
assert!(
out.iter().all(|&v| (v - 1.0 / 3.0).abs() < 1e-6),
"avg {out:?}"
);
m.current_into(1, &mut out);
assert!(
out.iter().all(|&v| (v - 1.0 / 3.0).abs() < 1e-6),
"cur {out:?}"
);
assert_eq!(m.num_updates(), 0);
}
#[test]
fn symmetric_reward_from_uniform_has_zero_expected_value() {
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
let ev = m.update_row(0, |a| [1.0, -1.0, 0.0][a]);
assert!(ev.abs() < 1e-6, "ev {ev}");
assert_eq!(m.num_updates(), 1);
}
#[test]
fn update_batch_fills_expected_value_per_row() {
let m = BatchedMatcher::<Dcfr, Local>::new(2, 2, DiscountParams::RECOMMENDED);
let mut ev = [0.0f32; 2];
m.update_batch(
|a, row| {
if row == 0 {
[2.0, 0.0][a]
} else {
[0.0, 4.0][a]
}
},
&mut ev,
);
assert!((ev[0] - 1.0).abs() < 1e-6, "{ev:?}");
assert!((ev[1] - 2.0).abs() < 1e-6, "{ev:?}");
assert_eq!(m.num_updates(), 1); }
#[test]
fn predictive_rule_uses_three_lanes() {
let m = BatchedMatcher::<PdcfrPlus, Local>::new(1, 3, PdcfrPlus::RECOMMENDED);
let mut out = [0.0f32; 3];
for _ in 0..5 {
m.update_row(0, |a| [1.0, 0.0, -1.0][a]);
}
m.average_into(0, &mut out);
assert!((out.iter().sum::<f32>() - 1.0).abs() < 1e-5);
}
use crate::regret_minimizer::RegretMinimizer;
use crate::rules::{DcfrPlus, LinearCfr, PcfrPlus};
fn next_reward(state: &mut u64) -> f32 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let unit = (*state >> 40) as f32 / (1u64 << 24) as f32; 2.0 * unit - 1.0
}
fn assert_bits(label: &str, got: &[f32], want: &[f32]) {
assert_eq!(got.len(), want.len(), "{label}: length");
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
assert_eq!(g.to_bits(), w.to_bits(), "{label}[{i}]: {g} != {w}");
}
}
fn golden<R, M>(params: R::Params, mut scalar: M, num_actions: usize, iters: usize)
where
R: UpdateRule,
M: RegretMinimizer,
{
let batched = BatchedMatcher::<R, Local>::new(1, num_actions, params);
let mut state = 0x1234_5678_9abc_def0;
let mut current = vec![0.0f32; num_actions];
for _ in 0..iters {
let rewards: Vec<f32> = (0..num_actions).map(|_| next_reward(&mut state)).collect();
scalar.update_regret(&rewards);
batched.update_row(0, |a| rewards[a]);
assert_bits("regret", &batched.raw_regret(0), scalar.cumulative_regret());
assert_bits(
"strategy",
&batched.raw_strategy(0),
scalar.cumulative_strategy(),
);
batched.current_into(0, &mut current);
assert_bits("current", ¤t, scalar.current_strategy());
}
let mut average = vec![0.0f32; num_actions];
batched.average_into(0, &mut average);
assert_bits("average", &average, &scalar.best_weight());
}
#[test]
fn golden_dcfr() {
golden::<Dcfr, _>(
DiscountParams::RECOMMENDED,
DiscountedRegretMatcher::recommended(4),
4,
200,
);
}
#[test]
fn golden_dcfr_plus() {
golden::<DcfrPlus, _>(
DcfrPlus::RECOMMENDED,
DcfrPlusRegretMatcher::recommended(4),
4,
200,
);
}
#[test]
fn golden_linear_cfr() {
golden::<LinearCfr, _>((), LinearCfrRegretMatcher::new(4), 4, 200);
}
#[test]
fn golden_pcfr_plus() {
golden::<PcfrPlus, _>((), PcfrPlusRegretMatcher::new(4), 4, 200);
}
#[test]
fn golden_pdcfr_plus() {
golden::<PdcfrPlus, _>(
PdcfrPlus::RECOMMENDED,
PdcfrPlusRegretMatcher::recommended(4),
4,
200,
);
}
use crate::{
DcfrPlusRegretMatcher, DiscountedRegretMatcher, LinearCfrRegretMatcher,
PcfrPlusRegretMatcher, PdcfrPlusRegretMatcher,
};
#[test]
fn seed_from_own_regret_is_noop_on_current() {
use crate::storage::Local;
let m = BatchedMatcher::<Dcfr, Local>::new(2, 3, DiscountParams::RECOMMENDED);
let mut ev = [0.0f32; 2];
for _ in 0..10 {
m.update_batch(|a, _| [1.0, -0.5, 0.2][a], &mut ev);
}
let mut before = [0.0f32; 3];
m.current_into(1, &mut before);
let t = m.num_updates();
let snaps: Vec<Vec<f32>> = (0..2).map(|r| m.raw_regret(r)).collect();
m.seed(|a, row| snaps[row][a], t);
let mut after = [0.0f32; 3];
m.current_into(1, &mut after);
for (x, y) in before.iter().zip(&after) {
assert_eq!(
x.to_bits(),
y.to_bits(),
"seed from own regret must be a no-op"
);
}
assert_eq!(m.num_updates(), t);
}
#[test]
fn seed_positive_regret_reproduces_target_current_strategy() {
use crate::storage::Local;
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
let target = [0.2f32, 0.3, 0.5];
m.seed(|a, _row| 100.0 * target[a], 50);
let mut out = [0.0f32; 3];
m.current_into(0, &mut out);
for (a, b) in target.iter().zip(&out) {
assert!((a - b).abs() < 1e-5, "{a} vs {b}");
}
assert_eq!(m.num_updates(), 50);
}
#[test]
fn reset_average_makes_average_uniform() {
let m = BatchedMatcher::<Dcfr, Local>::new(2, 3, DiscountParams::RECOMMENDED);
let mut ev = [0.0f32; 2];
for _ in 0..20 {
m.update_batch(|a, _| [1.0, -0.5, 0.2][a], &mut ev);
}
let mut current_before = [0.0f32; 3];
m.current_into(1, &mut current_before);
let updates_before = m.num_updates();
m.reset_average();
let mut avg = [0.0f32; 3];
for row in 0..2 {
m.average_into(row, &mut avg);
for &v in &avg {
assert!(
(v - 1.0 / 3.0).abs() < 1e-6,
"row {row}: expected uniform after reset, got {avg:?}"
);
}
}
let mut current_after = [0.0f32; 3];
m.current_into(1, &mut current_after);
assert_eq!(m.num_updates(), updates_before, "clock must not change");
for (b, a) in current_before.iter().zip(¤t_after) {
assert_eq!(
b.to_bits(),
a.to_bits(),
"current strategy must be unchanged after reset_average"
);
}
}
#[test]
fn seed_under_i16_reproduces_target_within_tolerance() {
use crate::lane::HalfBoth;
use crate::storage::Local;
let m = BatchedMatcher::<Dcfr, Local, HalfBoth>::new(1, 3, DiscountParams::RECOMMENDED);
let target = [0.2f32, 0.3, 0.5];
m.seed(|a, _| 100.0 * target[a], 50);
let mut out = [0.0f32; 3];
m.current_into(0, &mut out);
for (a, b) in target.iter().zip(&out) {
assert!(
(a - b).abs() < 2e-3,
"i16 seed within tolerance: {a} vs {b}"
);
}
assert_eq!(m.num_updates(), 50);
}
#[test]
fn regret_into_matches_raw_regret_helper() {
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
for _ in 0..7 {
m.update_row(0, |a| [1.0, -0.5, 0.2][a]);
}
let mut out = [0.0f32; 3];
m.regret_into(0, &mut out);
let raw = m.raw_regret(0);
for (i, (&g, &w)) in out.iter().zip(&raw).enumerate() {
assert_eq!(g.to_bits(), w.to_bits(), "regret_into[{i}] {g} != raw {w}");
}
}
#[test]
fn regret_into_then_seed_is_noop_on_current() {
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
for _ in 0..10 {
m.update_row(0, |a| [0.8, -0.3, 0.1][a]);
}
let mut before = [0.0f32; 3];
m.current_into(0, &mut before);
let mut r = [0.0f32; 3];
m.regret_into(0, &mut r);
m.seed(|a, _row| r[a], m.num_updates());
let mut after = [0.0f32; 3];
m.current_into(0, &mut after);
for (i, (&b, &a)) in before.iter().zip(&after).enumerate() {
assert_eq!(b.to_bits(), a.to_bits(), "current[{i}] changed: {b} != {a}");
}
}
#[test]
fn regret_into_decodes_int16_layout_within_quantum() {
let m = BatchedMatcher::<Dcfr, Local, HalfRegret>::new(1, 3, DiscountParams::RECOMMENDED);
let seeded = [1000.0f32, -250.0, 30.0];
m.seed(|a, _row| seeded[a], 5);
let mut out = [0.0f32; 3];
m.regret_into(0, &mut out);
let quantum = 1000.0 / i16::MAX as f32;
for (i, (&g, &w)) in out.iter().zip(&seeded).enumerate() {
assert!(
(g - w).abs() <= quantum + 1e-2,
"regret_into[{i}] {g} vs seeded {w}"
);
}
}
#[test]
#[should_panic(expected = "row out of range")]
fn regret_into_panics_on_bad_row() {
let m = BatchedMatcher::<Dcfr, Local>::new(2, 3, DiscountParams::RECOMMENDED);
let mut out = [0.0f32; 3];
m.regret_into(2, &mut out);
}
#[test]
#[should_panic(expected = "out too short")]
fn regret_into_panics_on_short_out() {
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
let mut out = [0.0f32; 2];
m.regret_into(0, &mut out);
}
#[test]
fn update_batch_with_matches_update_batch_bit_for_bit() {
let params = DiscountParams::RECOMMENDED;
let a = BatchedMatcher::<Dcfr, Local>::new(3, 3, params);
let b = BatchedMatcher::<Dcfr, Local>::new(3, 3, params);
let mut scratch = Scratch::new(5); let mut ev_a = [0.0f32; 3];
let mut ev_b = [0.0f32; 3];
let mut state = 0x9876_5432_10ab_cdefu64;
for _ in 0..100 {
let rewards: Vec<f32> = (0..9).map(|_| next_reward(&mut state)).collect();
a.update_batch(|act, row| rewards[row * 3 + act], &mut ev_a);
b.update_batch_with(&mut scratch, |act, row| rewards[row * 3 + act], &mut ev_b);
for (x, y) in ev_a.iter().zip(&ev_b) {
assert_eq!(x.to_bits(), y.to_bits(), "expected values diverged");
}
for row in 0..3 {
assert_bits("regret", &b.raw_regret(row), &a.raw_regret(row));
assert_bits("strategy", &b.raw_strategy(row), &a.raw_strategy(row));
}
}
}
#[test]
fn update_row_with_matches_update_row_bit_for_bit() {
let params = DiscountParams::RECOMMENDED;
let a = BatchedMatcher::<Dcfr, Local>::new(1, 3, params);
let b = BatchedMatcher::<Dcfr, Local>::new(1, 3, params);
let mut scratch = Scratch::new(3);
let mut state = 0x0f0f_1234_dead_beefu64;
for _ in 0..100 {
let rewards: Vec<f32> = (0..3).map(|_| next_reward(&mut state)).collect();
let ev_a = a.update_row(0, |act| rewards[act]);
let ev_b = b.update_row_with(&mut scratch, 0, |act| rewards[act]);
assert_eq!(ev_a.to_bits(), ev_b.to_bits(), "expected values diverged");
assert_bits("regret", &b.raw_regret(0), &a.raw_regret(0));
assert_bits("strategy", &b.raw_strategy(0), &a.raw_strategy(0));
}
}
#[test]
#[should_panic(expected = "scratch too small")]
fn update_batch_with_panics_on_small_scratch() {
let m = BatchedMatcher::<Dcfr, Local>::new(1, 3, DiscountParams::RECOMMENDED);
let mut scratch = Scratch::new(2);
let mut ev = [0.0f32; 1];
m.update_batch_with(&mut scratch, |a, _| a as f32, &mut ev);
}
}
#[cfg(test)]
mod alloc_tests {
use super::*;
use crate::discount::DiscountParams;
use crate::lane::HalfStrategyShared;
use crate::rules::Dcfr;
use crate::storage::Atomic;
use std::alloc::{GlobalAlloc, Layout as AllocLayout, System};
use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
struct CountingAlloc;
fn note_alloc() {
let _ = COUNTING.try_with(|c| {
if c.get() {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
}
});
}
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, l: AllocLayout) -> *mut u8 {
note_alloc();
unsafe { System.alloc(l) }
}
unsafe fn alloc_zeroed(&self, l: AllocLayout) -> *mut u8 {
note_alloc();
unsafe { System.alloc_zeroed(l) }
}
unsafe fn realloc(&self, p: *mut u8, l: AllocLayout, n: usize) -> *mut u8 {
note_alloc();
unsafe { System.realloc(p, l, n) }
}
unsafe fn dealloc(&self, p: *mut u8, l: AllocLayout) {
unsafe { System.dealloc(p, l) }
}
}
#[global_allocator]
static COUNTING_ALLOC: CountingAlloc = CountingAlloc;
fn allocations_in(f: impl FnOnce()) -> usize {
COUNTING.with(|c| c.set(true));
let before = ALLOC_COUNT.load(Ordering::Relaxed);
f();
let after = ALLOC_COUNT.load(Ordering::Relaxed);
COUNTING.with(|c| c.set(false));
after - before
}
#[test]
fn counting_harness_detects_allocations() {
let n = allocations_in(|| {
let v = vec![0u8; 128];
std::hint::black_box(&v);
});
assert!(n > 0, "counting allocator failed to observe an allocation");
}
#[test]
fn hot_path_is_allocation_free() {
let m = BatchedMatcher::<Dcfr, Atomic, HalfStrategyShared>::new(
169,
3,
DiscountParams::new(3.0, 0.0, 20.0),
);
let mut scratch = Scratch::new(4);
let mut expected = vec![0.0f32; 169];
let mut out = [0.0f32; 3];
m.update_batch_with(&mut scratch, |a, _| [1.0, -0.5, 0.25][a], &mut expected);
m.current_into(0, &mut out);
let n = allocations_in(|| {
for _ in 0..10 {
m.update_batch_with(&mut scratch, |a, _| [1.0, -0.5, 0.25][a], &mut expected);
for row in 0..169 {
m.current_into(row, &mut out);
}
}
});
assert_eq!(n, 0, "hot path performed {n} heap allocations");
}
}