Skip to main content

rust_igraph/algorithms/games/
recent_degree.rs

1//! Recent-degree game (ALGO-GN-019).
2//!
3//! Counterpart of `igraph_recent_degree_game()` from
4//! `references/igraph/src/games/recent_degree.c:32-186`. Models a
5//! growing network where the probability of citing a vertex is
6//! proportional to the number of edges it received in the last
7//! `time_window` time steps, raised to `power`, plus a `zero_appeal`
8//! floor for never-recently-cited vertices.
9//!
10//! ## Algorithm
11//!
12//! Every vertex `v` carries a weight in a prefix-sum tree:
13//!   `weight[v] = degree[v]^power + zero_appeal`
14//! where `degree[v]` is the count of edges in the sliding window that
15//! pointed at `v` (and originated from `v`, when `outpref = true`).
16//!
17//! The window itself is maintained by a deque (`history`). Entries
18//! ordered by time of creation, with a sentinel value separating each
19//! step's contributions. On step `i`:
20//!
21//! 1. **Expire** (only when `i >= time_window`): pop entries from the
22//!    front of `history` until a sentinel is popped. For each popped
23//!    vertex `j`, decrement `degree[j]` and refresh `psumtree[j]`.
24//! 2. **Draw** `m` (or `outseq[i]`) citations, each via
25//!    `psumtree.search(uniform(0, sum))` — with a uniform fallback
26//!    over `[0, i)` when `sum == 0`. For each citation
27//!    `(src=i, dst=to)`: bump `degree[to]`, push `to` to `history`,
28//!    record the edge.
29//! 3. Push a sentinel to `history`.
30//! 4. **Refresh** psumtree entries for each cited vertex with its new
31//!    degree.
32//! 5. If `outpref`: bump `degree[i] += m` and refresh `psumtree[i]`;
33//!    otherwise `psumtree[i] = zero_appeal`.
34//!
35//! The structure is a binary-indexed (Fenwick) tree with binary-lifting
36//! prefix-search, giving O(log n) per update / search.
37//!
38//! ## Self-loops & multi-edges
39//!
40//! * **Self-loops**: impossible. The psumtree only ranges over vertices
41//!   `[0, i)`; vertex `i` is added *after* its outgoing edges are drawn.
42//! * **Multi-edges**: yes, when `m ≥ 2`. Two draws at the same step can
43//!   land on the same target. Caller should `simplify()` if simple
44//!   output is required (matches upstream behaviour).
45//!
46//! ## Determinism
47//!
48//! Reproducible against the shared `SplitMix64`
49//! PRNG. Not bit-portable to upstream igraph's GLIBC RNG; conformance
50//! asserts structural invariants only.
51//!
52//! ## Validation
53//!
54//! * `power` and `zero_appeal` must be finite and non-NaN.
55//! * `zero_appeal >= 0`.
56//! * `outseq` (when present) must have length `nodes`.
57//! * The first entry of `outseq` is conventionally `0` (matching the C
58//!   reference, which drops `outseq[0]` from the edge total — vertex 0
59//!   has no preceding vertices to cite).
60//!
61//! ## References
62//!
63//! * Upstream igraph C `igraph_recent_degree_game`, MIT-licensed
64//!   reference port.
65
66#![allow(
67    unknown_lints,
68    clippy::cast_possible_truncation,
69    clippy::cast_precision_loss,
70    clippy::cast_sign_loss,
71    clippy::cast_lossless,
72    clippy::float_cmp,
73    clippy::too_many_arguments,
74    clippy::similar_names,
75    clippy::many_single_char_names
76)]
77
78use std::collections::VecDeque;
79
80use crate::core::rng::SplitMix64;
81use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
82
83/// Fenwick-tree-based prefix-sum store with O(log n) point set and
84/// O(log n) prefix-target search.
85struct PsumTree {
86    n: usize,
87    bit: Vec<f64>,
88    values: Vec<f64>,
89    total: f64,
90}
91
92impl PsumTree {
93    fn new(n: usize) -> Self {
94        Self {
95            n,
96            bit: vec![0.0; n + 1],
97            values: vec![0.0; n],
98            total: 0.0,
99        }
100    }
101
102    fn set(&mut self, i: usize, v: f64) {
103        let delta = v - self.values[i];
104        self.values[i] = v;
105        self.total += delta;
106        let mut k = i + 1;
107        while k <= self.n {
108            self.bit[k] += delta;
109            k += k & k.wrapping_neg();
110        }
111    }
112
113    fn total(&self) -> f64 {
114        self.total
115    }
116
117    fn search(&self, target: f64) -> usize {
118        if self.n == 0 {
119            return 0;
120        }
121        let mut idx: usize = 0;
122        let mut remaining = target;
123        let mut step = (self.n + 1).next_power_of_two() >> 1;
124        while step > 0 {
125            let next = idx + step;
126            if next <= self.n && self.bit[next] <= remaining {
127                idx = next;
128                remaining -= self.bit[next];
129            }
130            step >>= 1;
131        }
132        idx.min(self.n - 1)
133    }
134}
135
136/// History deque element. Wraps a vertex id; `None` is the
137/// per-step sentinel.
138type HistoryEntry = Option<u32>;
139
140fn weight_from_degree(degree: u32, power: f64, zero_appeal: f64) -> f64 {
141    // Match C `pow(VECTOR(degree)[j], power) + zero_appeal`.
142    // `pow(0, positive) == 0`, `pow(0, 0) == 1`.
143    let d = f64::from(degree);
144    let term = if degree == 0 && power == 0.0 {
145        1.0
146    } else if degree == 0 {
147        0.0
148    } else {
149        d.powf(power)
150    };
151    term + zero_appeal
152}
153
154fn validate(
155    nodes: u32,
156    power: f64,
157    m: u32,
158    outseq: Option<&[u32]>,
159    zero_appeal: f64,
160) -> IgraphResult<()> {
161    if power.is_nan() || !power.is_finite() {
162        return Err(IgraphError::InvalidArgument(format!(
163            "power must be finite (got {power})"
164        )));
165    }
166    if zero_appeal.is_nan() || !zero_appeal.is_finite() {
167        return Err(IgraphError::InvalidArgument(format!(
168            "zero_appeal must be finite (got {zero_appeal})"
169        )));
170    }
171    if zero_appeal < 0.0 {
172        return Err(IgraphError::InvalidArgument(format!(
173            "zero_appeal must be non-negative (got {zero_appeal})"
174        )));
175    }
176    if let Some(seq) = outseq {
177        if seq.len() != nodes as usize {
178            return Err(IgraphError::InvalidArgument(format!(
179                "outseq length ({}) must equal nodes ({nodes})",
180                seq.len()
181            )));
182        }
183    }
184    let _ = m;
185    Ok(())
186}
187
188/// Recent-degree game. See module docs.
189///
190/// # Examples
191///
192/// ```
193/// use rust_igraph::recent_degree_game;
194///
195/// // 10 vertices, linear preference, window of 5, 1 edge per step.
196/// let g = recent_degree_game(10, 1.0, 5, 1, None, false, 1.0, true, 42).unwrap();
197/// assert_eq!(g.vcount(), 10);
198/// assert_eq!(g.ecount(), 9);
199/// ```
200pub fn recent_degree_game(
201    nodes: u32,
202    power: f64,
203    time_window: u32,
204    m: u32,
205    outseq: Option<&[u32]>,
206    outpref: bool,
207    zero_appeal: f64,
208    directed: bool,
209    seed: u64,
210) -> IgraphResult<Graph> {
211    validate(nodes, power, m, outseq, zero_appeal)?;
212
213    let mut graph = Graph::new(nodes, directed)?;
214    if nodes == 0 || nodes < 2 {
215        return Ok(graph);
216    }
217    // If both no outseq AND m == 0, no edges at all.
218    if outseq.is_none() && m == 0 {
219        return Ok(graph);
220    }
221    // If outseq is given and the total minus outseq[0] is zero, no edges.
222    if let Some(seq) = outseq {
223        let total_after_zero: u64 = seq.iter().skip(1).map(|&x| u64::from(x)).sum();
224        if total_after_zero == 0 {
225            return Ok(graph);
226        }
227    }
228
229    let n = nodes as usize;
230    let mut rng = SplitMix64::new(seed);
231    let mut psum = PsumTree::new(n);
232    let mut degree: Vec<u32> = vec![0; n];
233    let mut history: VecDeque<HistoryEntry> = VecDeque::new();
234
235    // Pre-compute edge capacity to avoid reallocation.
236    let edge_capacity = if let Some(seq) = outseq {
237        seq.iter().skip(1).map(|&x| u64::from(x)).sum::<u64>() as usize
238    } else {
239        (n - 1).saturating_mul(m as usize)
240    };
241    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_capacity);
242
243    // First vertex enters with degree 0 — weight = zero_appeal (per C: explicit
244    // `igraph_psumtree_update(&sumtree, 0, zero_appeal)`).
245    psum.set(0, zero_appeal);
246    history.push_back(None); // sentinel for step 0
247
248    for i in 1..n {
249        let no_of_neighbors = if let Some(seq) = outseq {
250            seq[i] as usize
251        } else {
252            m as usize
253        };
254
255        // Expire window: pop entries until a sentinel.
256        if i >= time_window as usize {
257            while let Some(entry) = history.pop_front() {
258                match entry {
259                    None => break,
260                    Some(j) => {
261                        let ju = j as usize;
262                        degree[ju] -= 1;
263                        psum.set(ju, weight_from_degree(degree[ju], power, zero_appeal));
264                    }
265                }
266            }
267        }
268
269        // Draw `no_of_neighbors` citations.
270        for _ in 0..no_of_neighbors {
271            let sum = psum.total();
272            let to = if sum > 0.0 {
273                let target = rng.gen_unit() * sum;
274                psum.search(target)
275            } else {
276                // Fallback: uniform over [0, i).
277                let span = i as u64;
278                (rng.next_u64() % span) as usize
279            };
280            let src = u32::try_from(i)
281                .map_err(|_| IgraphError::InvalidArgument("vertex index overflow".into()))?;
282            let dst = u32::try_from(to)
283                .map_err(|_| IgraphError::InvalidArgument("vertex index overflow".into()))?;
284            degree[to] = degree[to]
285                .checked_add(1)
286                .ok_or_else(|| IgraphError::InvalidArgument("degree overflow".into()))?;
287            edges.push((src, dst));
288            history.push_back(Some(dst));
289        }
290        history.push_back(None); // sentinel for step i
291
292        // Refresh psum for each cited target.
293        // Walk back the last `no_of_neighbors` edges.
294        let start = edges.len() - no_of_neighbors;
295        for edge in &edges[start..] {
296            let nn = edge.1 as usize;
297            psum.set(nn, weight_from_degree(degree[nn], power, zero_appeal));
298        }
299
300        // Update self entry depending on outpref.
301        if outpref {
302            // outpref bumps source degree by no_of_neighbors. Use checked
303            // add to keep our no-unwrap discipline.
304            let bump = u32::try_from(no_of_neighbors)
305                .map_err(|_| IgraphError::InvalidArgument("neighbors count overflow".into()))?;
306            degree[i] = degree[i]
307                .checked_add(bump)
308                .ok_or_else(|| IgraphError::InvalidArgument("degree overflow".into()))?;
309            psum.set(i, weight_from_degree(degree[i], power, zero_appeal));
310        } else {
311            psum.set(i, zero_appeal);
312        }
313    }
314
315    graph.add_edges(edges)?;
316    Ok(graph)
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn assert_edges_match(g: &Graph, expected_count: u64) {
324        assert_eq!(g.ecount() as u64, expected_count);
325    }
326
327    #[test]
328    fn empty_graph_returns_empty() {
329        let g = recent_degree_game(0, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
330        assert_eq!(g.vcount(), 0);
331        assert_eq!(g.ecount(), 0);
332    }
333
334    #[test]
335    fn single_vertex_no_edges() {
336        let g = recent_degree_game(1, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
337        assert_eq!(g.vcount(), 1);
338        assert_eq!(g.ecount(), 0);
339    }
340
341    #[test]
342    fn m_zero_no_edges() {
343        let g = recent_degree_game(10, 1.0, 5, 0, None, false, 1.0, true, 42).unwrap();
344        assert_eq!(g.vcount(), 10);
345        assert_eq!(g.ecount(), 0);
346    }
347
348    #[test]
349    fn ecount_exact_constant_m() {
350        let n = 50u32;
351        let m = 3u32;
352        let g = recent_degree_game(n, 1.0, 10, m, None, false, 1.0, true, 42).unwrap();
353        assert_eq!(g.vcount(), n);
354        assert_edges_match(&g, u64::from(n - 1) * u64::from(m));
355    }
356
357    #[test]
358    fn ecount_exact_outseq() {
359        let n = 20u32;
360        let outseq: Vec<u32> = (0..n).map(|i| if i == 0 { 0 } else { 2 }).collect();
361        let g = recent_degree_game(n, 1.0, 5, 0, Some(&outseq), false, 1.0, true, 42).unwrap();
362        let expected: u64 = outseq.iter().skip(1).map(|&x| u64::from(x)).sum();
363        assert_edges_match(&g, expected);
364    }
365
366    #[test]
367    fn determinism_same_seed() {
368        let g1 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 123).unwrap();
369        let g2 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 123).unwrap();
370        assert_eq!(g1.vcount(), g2.vcount());
371        assert_eq!(g1.ecount(), g2.ecount());
372        let m = u32::try_from(g1.ecount()).unwrap();
373        for eid in 0..m {
374            assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
375        }
376    }
377
378    #[test]
379    fn seed_divergence() {
380        let g1 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 1).unwrap();
381        let g2 = recent_degree_game(40, 1.5, 8, 2, None, false, 1.0, true, 2).unwrap();
382        let m = u32::try_from(g1.ecount()).unwrap();
383        let mut diff = 0;
384        for eid in 0..m {
385            if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
386                diff += 1;
387            }
388        }
389        assert!(diff > 0, "two seeds should yield different graphs");
390    }
391
392    #[test]
393    fn no_self_loops_when_positive_appeal() {
394        let g = recent_degree_game(50, 1.0, 5, 3, None, false, 1.0, true, 42).unwrap();
395        let m = u32::try_from(g.ecount()).unwrap();
396        for eid in 0..m {
397            let (s, d) = g.edge(eid).unwrap();
398            assert_ne!(s, d, "edge {eid} is a self-loop");
399        }
400    }
401
402    #[test]
403    fn source_is_always_step_index() {
404        let n = 30u32;
405        let m = 2u32;
406        let g = recent_degree_game(n, 1.0, 5, m, None, false, 1.0, true, 42).unwrap();
407        let edges_n = u32::try_from(g.ecount()).unwrap();
408        for eid in 0..edges_n {
409            let (s, _) = g.edge(eid).unwrap();
410            let step = eid / m + 1; // step i emits eps edges starting at index (i-1)*m
411            assert_eq!(s, step, "edge {eid} src {s} ≠ step {step}");
412        }
413    }
414
415    #[test]
416    fn target_in_zero_to_step_minus_one() {
417        let g = recent_degree_game(30, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
418        let edges_n = u32::try_from(g.ecount()).unwrap();
419        for eid in 0..edges_n {
420            let (s, d) = g.edge(eid).unwrap();
421            assert!(d < s, "target {d} not in [0, src={s})");
422        }
423    }
424
425    #[test]
426    fn directed_flag_propagates() {
427        let g1 = recent_degree_game(15, 1.0, 5, 2, None, false, 1.0, true, 42).unwrap();
428        let g2 = recent_degree_game(15, 1.0, 5, 2, None, false, 1.0, false, 42).unwrap();
429        assert!(g1.is_directed());
430        assert!(!g2.is_directed());
431    }
432
433    #[test]
434    fn outpref_changes_graph() {
435        let g1 = recent_degree_game(40, 1.5, 10, 2, None, false, 1.0, true, 42).unwrap();
436        let g2 = recent_degree_game(40, 1.5, 10, 2, None, true, 1.0, true, 42).unwrap();
437        // Both should have the same ecount but different edge layout due
438        // to different psumtree weights.
439        assert_eq!(g1.ecount(), g2.ecount());
440        let edges_n = u32::try_from(g1.ecount()).unwrap();
441        let mut diff = 0;
442        for eid in 0..edges_n {
443            if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
444                diff += 1;
445            }
446        }
447        assert!(
448            diff > 0,
449            "outpref=true and outpref=false should yield different graphs"
450        );
451    }
452
453    #[test]
454    fn time_window_zero_uses_zero_appeal_only() {
455        // time_window=0 means edges expire on the very next step, so
456        // every draw past the first only sees zero_appeal weights, which
457        // are equal → uniform across all alive vertices.
458        let g = recent_degree_game(20, 1.0, 0, 2, None, false, 1.0, true, 42).unwrap();
459        // Still exactly (n-1)*m edges.
460        assert_eq!(g.ecount(), 19 * 2);
461        // No self-loops still.
462        let edges_n = u32::try_from(g.ecount()).unwrap();
463        for eid in 0..edges_n {
464            let (s, d) = g.edge(eid).unwrap();
465            assert_ne!(s, d);
466        }
467    }
468
469    #[test]
470    fn large_time_window_concentrates_on_early_vertices() {
471        // With time_window >= n, no edges expire, so the algorithm
472        // becomes plain preferential attachment. Old vertices dominate.
473        let n = 200u32;
474        let m = 3u32;
475        let g = recent_degree_game(n, 1.0, n, m, None, true, 1.0, true, 42).unwrap();
476        let mut indeg = vec![0u32; n as usize];
477        let edges_n = u32::try_from(g.ecount()).unwrap();
478        for eid in 0..edges_n {
479            let (_, d) = g.edge(eid).unwrap();
480            indeg[d as usize] += 1;
481        }
482        // The earliest 10% of vertices should hold > 20% of incoming edges.
483        let early_cut = (n as usize) / 10;
484        let early_sum: u32 = indeg[..early_cut].iter().sum();
485        let total: u32 = indeg.iter().sum();
486        assert!(
487            early_sum * 5 > total,
488            "expected early-vertex concentration (early_sum={early_sum}, total={total})"
489        );
490    }
491
492    #[test]
493    fn validation_outseq_wrong_length() {
494        let outseq = vec![0u32, 1, 2];
495        let err =
496            recent_degree_game(10, 1.0, 5, 0, Some(&outseq), false, 1.0, true, 42).unwrap_err();
497        match err {
498            IgraphError::InvalidArgument(s) => assert!(s.contains("outseq length")),
499            other => panic!("expected InvalidArgument, got {other:?}"),
500        }
501    }
502
503    #[test]
504    fn validation_nan_power() {
505        let err = recent_degree_game(10, f64::NAN, 5, 2, None, false, 1.0, true, 42).unwrap_err();
506        match err {
507            IgraphError::InvalidArgument(s) => assert!(s.contains("power must be finite")),
508            other => panic!("expected InvalidArgument, got {other:?}"),
509        }
510    }
511
512    #[test]
513    fn validation_inf_power() {
514        let err =
515            recent_degree_game(10, f64::INFINITY, 5, 2, None, false, 1.0, true, 42).unwrap_err();
516        match err {
517            IgraphError::InvalidArgument(s) => assert!(s.contains("power must be finite")),
518            other => panic!("expected InvalidArgument, got {other:?}"),
519        }
520    }
521
522    #[test]
523    fn validation_negative_zero_appeal() {
524        let err = recent_degree_game(10, 1.0, 5, 2, None, false, -0.5, true, 42).unwrap_err();
525        match err {
526            IgraphError::InvalidArgument(s) => {
527                assert!(s.contains("zero_appeal must be non-negative"));
528            }
529            other => panic!("expected InvalidArgument, got {other:?}"),
530        }
531    }
532
533    #[test]
534    fn validation_nan_zero_appeal() {
535        let err = recent_degree_game(10, 1.0, 5, 2, None, false, f64::NAN, true, 42).unwrap_err();
536        match err {
537            IgraphError::InvalidArgument(s) => assert!(s.contains("zero_appeal must be finite")),
538            other => panic!("expected InvalidArgument, got {other:?}"),
539        }
540    }
541
542    #[test]
543    fn weight_from_degree_zero_power_zero() {
544        // pow(0, 0) = 1.0
545        let w = weight_from_degree(0, 0.0, 0.5);
546        assert_eq!(w, 1.5);
547    }
548
549    #[test]
550    fn weight_from_degree_zero_positive_power() {
551        let w = weight_from_degree(0, 1.0, 0.5);
552        assert_eq!(w, 0.5);
553    }
554
555    #[test]
556    fn weight_from_degree_nonzero() {
557        let w = weight_from_degree(4, 0.5, 1.0);
558        assert!((w - 3.0).abs() < 1e-12);
559    }
560}
561
562#[cfg(all(test, feature = "proptest-harness"))]
563mod proptests {
564    use super::*;
565    use proptest::prelude::*;
566
567    proptest! {
568        #[test]
569        fn determinism_under_proptest(
570            nodes in 1u32..50,
571            m in 0u32..4,
572            power in 0.0f64..3.0,
573            zero_appeal in 0.1f64..5.0,
574            time_window in 1u32..20,
575            directed in any::<bool>(),
576            outpref in any::<bool>(),
577            seed in any::<u64>(),
578        ) {
579            let g1 = recent_degree_game(nodes, power, time_window, m, None, outpref, zero_appeal, directed, seed).unwrap();
580            let g2 = recent_degree_game(nodes, power, time_window, m, None, outpref, zero_appeal, directed, seed).unwrap();
581            prop_assert_eq!(g1.vcount(), g2.vcount());
582            prop_assert_eq!(g1.ecount(), g2.ecount());
583            let edges_n = u32::try_from(g1.ecount()).unwrap();
584            for eid in 0..edges_n {
585                prop_assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
586            }
587        }
588
589        #[test]
590        fn ecount_exact_constant_m(
591            nodes in 1u32..40,
592            m in 0u32..5,
593            time_window in 1u32..20,
594            seed in any::<u64>(),
595        ) {
596            let g = recent_degree_game(nodes, 1.0, time_window, m, None, false, 1.0, true, seed).unwrap();
597            let expected = if nodes < 2 { 0 } else { u64::from(nodes - 1) * u64::from(m) };
598            prop_assert_eq!(g.ecount() as u64, expected);
599        }
600
601        #[test]
602        fn no_self_loops_when_zero_appeal_positive(
603            nodes in 2u32..40,
604            m in 1u32..4,
605            power in 0.0f64..2.0,
606            zero_appeal in 0.1f64..3.0,
607            time_window in 1u32..20,
608            seed in any::<u64>(),
609        ) {
610            let g = recent_degree_game(nodes, power, time_window, m, None, false, zero_appeal, true, seed).unwrap();
611            let edges_n = u32::try_from(g.ecount()).unwrap();
612            for eid in 0..edges_n {
613                let (s, d) = g.edge(eid).unwrap();
614                prop_assert_ne!(s, d);
615            }
616        }
617
618        #[test]
619        fn source_is_always_step_index(
620            nodes in 2u32..30,
621            m in 1u32..4,
622            seed in any::<u64>(),
623        ) {
624            let g = recent_degree_game(nodes, 1.0, 5, m, None, false, 1.0, true, seed).unwrap();
625            let edges_n = u32::try_from(g.ecount()).unwrap();
626            for eid in 0..edges_n {
627                let (s, _) = g.edge(eid).unwrap();
628                let step = eid / m + 1;
629                prop_assert_eq!(s, step);
630            }
631        }
632
633        #[test]
634        fn target_in_zero_to_step_minus_one(
635            nodes in 2u32..30,
636            m in 1u32..4,
637            seed in any::<u64>(),
638        ) {
639            let g = recent_degree_game(nodes, 1.0, 5, m, None, false, 1.0, true, seed).unwrap();
640            let edges_n = u32::try_from(g.ecount()).unwrap();
641            for eid in 0..edges_n {
642                let (s, d) = g.edge(eid).unwrap();
643                prop_assert!(d < s);
644            }
645        }
646    }
647}