use std::any::Any;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use crate::datatype::{Buffer, BufferMut, DatatypeRef};
use crate::point_to_point::{AnyProcess, Process};
use crate::transport;
use crate::{Count, Rank, Tag};
pub type Key = i32;
pub(crate) const COLL_CONTEXT_BIT: u32 = 0x8000_0000;
pub struct CommData {
pub(crate) context: u32,
pub(crate) rank: Rank,
pub(crate) size: Rank,
pub(crate) world_ranks: Vec<i32>,
pub(crate) child_seq: AtomicU32,
pub(crate) name: Mutex<Option<String>>,
pub(crate) attributes: Mutex<HashMap<i32, Box<dyn Any + Send + Sync>>>,
}
impl CommData {
fn world() -> CommData {
let rt = transport::runtime();
let size = rt.size;
CommData {
context: 0,
rank: rt.rank,
size,
world_ranks: (0..size).collect(),
child_seq: AtomicU32::new(0),
name: Mutex::new(Some("MPI_COMM_WORLD".to_string())),
attributes: Mutex::new(HashMap::new()),
}
}
pub(crate) fn coll_context(&self) -> u32 {
self.context | COLL_CONTEXT_BIT
}
pub(crate) fn world_rank(&self, comm_rank: Rank) -> i32 {
self.world_ranks[comm_rank as usize]
}
pub(crate) fn async_clone(&self, context: u32) -> CommData {
CommData {
context,
rank: self.rank,
size: self.size,
world_ranks: self.world_ranks.clone(),
child_seq: AtomicU32::new(0),
name: Mutex::new(None),
attributes: Mutex::new(HashMap::new()),
}
}
pub(crate) fn derive_context(&self, disc: u32) -> u32 {
let seq = self.child_seq.fetch_add(1, Ordering::SeqCst);
let mut h = self
.context
.wrapping_mul(0x9E37_79B1)
.wrapping_add(seq.wrapping_mul(0x85EB_CA77))
.wrapping_add(disc.wrapping_mul(0xC2B2_AE3D));
h ^= h >> 15;
h = h.wrapping_mul(0x2545_F491);
h ^= h >> 13;
h & !COLL_CONTEXT_BIT
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CommunicatorRelation {
Identical,
Congruent,
Similar,
Unequal,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GroupRelation {
Identical,
Similar,
Unequal,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Color(Option<i32>);
impl Color {
pub fn with_value(value: Rank) -> Color {
Color(Some(value))
}
pub fn undefined() -> Color {
Color(None)
}
fn value(&self) -> Option<i32> {
self.0
}
}
#[derive(Clone, Debug)]
pub struct Group {
members: Vec<i32>,
}
pub type UserGroup = Group;
impl Group {
pub(crate) fn from_world_ranks(mut members: Vec<i32>) -> Group {
members.dedup();
Group { members }
}
pub fn empty() -> Group {
Group {
members: Vec::new(),
}
}
pub fn size(&self) -> Rank {
self.members.len() as Rank
}
pub fn rank(&self) -> Option<Rank> {
let me = transport::runtime().rank;
self.members
.iter()
.position(|&w| w == me)
.map(|p| p as Rank)
}
pub fn translate_ranks(&self, ranks: &[Rank], other: &Group) -> Vec<Option<Rank>> {
ranks
.iter()
.map(|&r| {
let w = self.members.get(r as usize).copied()?;
other
.members
.iter()
.position(|&o| o == w)
.map(|p| p as Rank)
})
.collect()
}
pub fn union(&self, other: &Group) -> Group {
let mut m = self.members.clone();
for &w in &other.members {
if !m.contains(&w) {
m.push(w);
}
}
Group { members: m }
}
pub fn intersection(&self, other: &Group) -> Group {
let m = self
.members
.iter()
.copied()
.filter(|w| other.members.contains(w))
.collect();
Group { members: m }
}
pub fn difference(&self, other: &Group) -> Group {
let m = self
.members
.iter()
.copied()
.filter(|w| !other.members.contains(w))
.collect();
Group { members: m }
}
pub fn include(&self, ranks: &[Rank]) -> Group {
let m = ranks
.iter()
.filter_map(|&r| self.members.get(r as usize).copied())
.collect();
Group { members: m }
}
pub fn exclude(&self, ranks: &[Rank]) -> Group {
let drop: Vec<i32> = ranks
.iter()
.filter_map(|&r| self.members.get(r as usize).copied())
.collect();
let m = self
.members
.iter()
.copied()
.filter(|w| !drop.contains(w))
.collect();
Group { members: m }
}
pub fn compare(&self, other: &Group) -> GroupRelation {
if self.members == other.members {
GroupRelation::Identical
} else {
let mut a = self.members.clone();
let mut b = other.members.clone();
a.sort_unstable();
b.sort_unstable();
if a == b {
GroupRelation::Similar
} else {
GroupRelation::Unequal
}
}
}
pub(crate) fn members(&self) -> &[i32] {
&self.members
}
}
pub trait Communicator {
#[doc(hidden)]
fn comm_data(&self) -> &CommData;
fn size(&self) -> Rank {
self.comm_data().size
}
fn rank(&self) -> Rank {
self.comm_data().rank
}
fn target_size(&self) -> Rank {
self.comm_data().size
}
fn process_at_rank(&self, r: Rank) -> Process<'_> {
Process::new(self.comm_data(), r)
}
fn this_process(&self) -> Process<'_> {
Process::new(self.comm_data(), self.comm_data().rank)
}
fn any_process(&self) -> AnyProcess<'_> {
AnyProcess::new(self.comm_data())
}
fn group(&self) -> Group {
Group::from_world_ranks(self.comm_data().world_ranks.clone())
}
fn compare(&self, other: &dyn Communicator) -> CommunicatorRelation {
let a = self.comm_data();
let b = other.comm_data();
if a.context == b.context {
CommunicatorRelation::Identical
} else if a.world_ranks == b.world_ranks {
CommunicatorRelation::Congruent
} else {
let mut sa = a.world_ranks.clone();
let mut sb = b.world_ranks.clone();
sa.sort_unstable();
sb.sort_unstable();
if sa == sb {
CommunicatorRelation::Similar
} else {
CommunicatorRelation::Unequal
}
}
}
fn duplicate(&self) -> SimpleCommunicator {
let data = self.comm_data();
let ctx = data.derive_context(0xD00Du32);
SimpleCommunicator::from_parts(ctx, data.rank, data.size, data.world_ranks.clone())
}
fn split_by_color(&self, color: Color) -> Option<SimpleCommunicator> {
self.split_by_color_with_key(color, self.rank())
}
fn split_by_color_with_key(&self, color: Color, key: Key) -> Option<SimpleCommunicator> {
split_impl(self.comm_data(), color, key)
}
fn split_by_subgroup(&self, group: &Group) -> Option<SimpleCommunicator> {
let data = self.comm_data();
let me = transport::runtime().rank;
if !group.members().contains(&me) {
return None;
}
let disc = group
.members()
.iter()
.fold(0u32, |a, &w| a.wrapping_mul(31).wrapping_add(w as u32));
let ctx = data.derive_context(disc ^ 0x5EED);
let world_ranks: Vec<i32> = group.members().to_vec();
let rank = world_ranks.iter().position(|&w| w == me).unwrap() as Rank;
let size = world_ranks.len() as Rank;
Some(SimpleCommunicator::from_parts(ctx, rank, size, world_ranks))
}
fn abort(&self, errorcode: i32) -> ! {
eprintln!(
"MPI_Abort called on rank {} (code {})",
self.rank(),
errorcode
);
transport::abort_job(errorcode);
}
fn set_name(&self, name: &str) {
*self.comm_data().name.lock().unwrap() = Some(name.to_string());
}
fn get_name(&self) -> String {
self.comm_data()
.name
.lock()
.unwrap()
.clone()
.unwrap_or_default()
}
fn pack_size(&self, incount: Count, dt: DatatypeRef) -> Count {
incount * dt.size as Count
}
fn pack<Buf: Buffer + ?Sized>(&self, inbuf: &Buf) -> Vec<u8>
where
Self: Sized,
{
inbuf.as_bytes().to_vec()
}
fn pack_into<Buf: Buffer + ?Sized>(
&self,
inbuf: &Buf,
outbuf: &mut [u8],
position: Count,
) -> Count
where
Self: Sized,
{
let bytes = inbuf.as_bytes();
let start = position as usize;
outbuf[start..start + bytes.len()].copy_from_slice(bytes);
position + bytes.len() as Count
}
unsafe fn unpack_into<Buf: BufferMut + ?Sized>(
&self,
inbuf: &[u8],
outbuf: &mut Buf,
position: Count,
) -> Count
where
Self: Sized,
{
let dst = outbuf.as_bytes_mut();
let start = position as usize;
let n = dst.len().min(inbuf.len() - start);
dst[..n].copy_from_slice(&inbuf[start..start + n]);
position + n as Count
}
fn create_graph_communicator(
&self,
index: &[Count],
edges: &[Count],
) -> Option<GraphCommunicator> {
let nnodes = index.len() as Count;
let color = if self.rank() < nnodes {
Color::with_value(0)
} else {
Color::undefined()
};
let sub = self.split_by_color(color)?;
Some(GraphCommunicator {
comm: sub,
index: index.to_vec(),
edges: edges.to_vec(),
})
}
fn create_dist_graph_adjacent(
&self,
sources: &[Rank],
destinations: &[Rank],
) -> DistGraphCommunicator {
DistGraphCommunicator {
comm: self.duplicate(),
sources: sources.to_vec(),
destinations: destinations.to_vec(),
}
}
fn split_intercommunicator(&self, in_group_a: bool) -> InterCommunicator {
let data = self.comm_data();
let me = transport::runtime().rank;
let mut rec = Vec::with_capacity(5);
rec.push(in_group_a as u8);
rec.extend_from_slice(&me.to_le_bytes());
let table = allgather_bytes(data, &rec);
let mut group_a = Vec::new();
let mut group_b = Vec::new();
for r in &table {
let a = r[0] != 0;
let w = i32::from_le_bytes(r[1..5].try_into().unwrap());
if a {
group_a.push(w);
} else {
group_b.push(w);
}
}
let (local, remote) = if in_group_a {
(group_a, group_b)
} else {
(group_b, group_a)
};
let ctx = data.derive_context(0x1E7E_1C0D);
let my_local_rank = local.iter().position(|&w| w == me).unwrap() as Rank;
InterCommunicator::new(ctx, my_local_rank, local, remote)
}
fn create_cartesian_communicator(
&self,
dims: &[Count],
periods: &[bool],
_reorder: bool,
) -> Option<CartesianCommunicator> {
assert_eq!(
dims.len(),
periods.len(),
"dims and periods length mismatch"
);
let total: Count = dims.iter().product();
let color = if self.rank() < total {
Color::with_value(0)
} else {
Color::undefined()
};
let sub = self.split_by_color(color)?;
Some(CartesianCommunicator {
comm: sub,
dims: dims.to_vec(),
periods: periods.to_vec(),
})
}
fn parent(&self) -> Option<InterCommunicator> {
let (ictx, paddrs) = transport::spawn_parent()?;
transport::runtime().register_context_peers(ictx, paddrs.clone());
let data = self.comm_data();
Some(InterCommunicator::new_spawned(
ictx,
data.rank,
data.world_ranks.clone(),
paddrs.len(),
))
}
}
pub struct SimpleCommunicator {
inner: Arc<CommData>,
}
pub type SystemCommunicator = SimpleCommunicator;
pub type UserCommunicator = SimpleCommunicator;
impl SimpleCommunicator {
pub fn world() -> SimpleCommunicator {
SimpleCommunicator {
inner: Arc::new(CommData::world()),
}
}
fn from_parts(
context: u32,
rank: Rank,
size: Rank,
world_ranks: Vec<i32>,
) -> SimpleCommunicator {
SimpleCommunicator {
inner: Arc::new(CommData {
context,
rank,
size,
world_ranks,
child_seq: AtomicU32::new(0),
name: Mutex::new(None),
attributes: Mutex::new(HashMap::new()),
}),
}
}
}
impl Communicator for SimpleCommunicator {
fn comm_data(&self) -> &CommData {
&self.inner
}
}
impl Clone for SimpleCommunicator {
fn clone(&self) -> Self {
SimpleCommunicator {
inner: Arc::clone(&self.inner),
}
}
}
fn split_impl(parent: &CommData, color: Color, key: Key) -> Option<SimpleCommunicator> {
let mut rec = Vec::with_capacity(16);
let c = color.value();
rec.extend_from_slice(&(c.is_some() as i32).to_le_bytes());
rec.extend_from_slice(&c.unwrap_or(0).to_le_bytes());
rec.extend_from_slice(&key.to_le_bytes());
rec.extend_from_slice(&transport::runtime().rank.to_le_bytes());
let table = allgather_bytes(parent, &rec);
struct Entry {
defined: bool,
color: i32,
key: i32,
world: i32,
}
let entries: Vec<Entry> = table
.iter()
.map(|r| Entry {
defined: i32::from_le_bytes(r[0..4].try_into().unwrap()) != 0,
color: i32::from_le_bytes(r[4..8].try_into().unwrap()),
key: i32::from_le_bytes(r[8..12].try_into().unwrap()),
world: i32::from_le_bytes(r[12..16].try_into().unwrap()),
})
.collect();
let my_world = transport::runtime().rank;
let my_color = color.value()?;
let mut members: Vec<(&Entry, usize)> = entries
.iter()
.enumerate()
.filter(|(_, e)| e.defined && e.color == my_color)
.map(|(i, e)| (e, i))
.collect();
members.sort_by(|a, b| a.0.key.cmp(&b.0.key).then(a.1.cmp(&b.1)));
let world_ranks: Vec<i32> = members.iter().map(|(e, _)| e.world).collect();
let rank = world_ranks.iter().position(|&w| w == my_world).unwrap() as Rank;
let size = world_ranks.len() as Rank;
let ctx = parent.derive_context(my_color as u32);
Some(SimpleCommunicator::from_parts(ctx, rank, size, world_ranks))
}
const SPLIT_GATHER_TAG: Tag = 1;
const SPLIT_BCAST_TAG: Tag = 2;
pub(crate) fn allgather_bytes(comm: &CommData, mine: &[u8]) -> Vec<Vec<u8>> {
let rt = transport::runtime();
let ctx = comm.coll_context();
let n = comm.size;
let me = comm.rank;
let dt = crate::datatype::ids::U8;
if me == 0 {
let mut table: Vec<Vec<u8>> = vec![Vec::new(); n as usize];
table[0] = mine.to_vec();
for src in 1..n {
let (_s, _t, _c, _d, payload) = rt.recv(ctx, src, SPLIT_GATHER_TAG);
table[src as usize] = payload;
}
let mut blob = Vec::new();
blob.extend_from_slice(&(n as u32).to_le_bytes());
for rec in &table {
blob.extend_from_slice(&(rec.len() as u32).to_le_bytes());
blob.extend_from_slice(rec);
}
for dst in 1..n {
rt.send(
ctx,
0,
comm.world_rank(dst),
SPLIT_BCAST_TAG,
blob.len() as u64,
dt,
&blob,
)
.expect("split broadcast failed");
}
table
} else {
rt.send(
ctx,
me,
comm.world_rank(0),
SPLIT_GATHER_TAG,
mine.len() as u64,
dt,
mine,
)
.expect("split gather failed");
let (_s, _t, _c, _d, blob) = rt.recv(ctx, 0, SPLIT_BCAST_TAG);
decode_table(&blob)
}
}
fn decode_table(blob: &[u8]) -> Vec<Vec<u8>> {
let mut pos = 0;
let n = u32::from_le_bytes(blob[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let mut out = Vec::with_capacity(n);
for _ in 0..n {
let len = u32::from_le_bytes(blob[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
out.push(blob[pos..pos + len].to_vec());
pos += len;
}
out
}
#[derive(Clone)]
pub struct CartesianCommunicator {
comm: SimpleCommunicator,
dims: Vec<Count>,
periods: Vec<bool>,
}
impl Communicator for CartesianCommunicator {
fn comm_data(&self) -> &CommData {
self.comm.comm_data()
}
}
impl CartesianCommunicator {
pub fn num_dimensions(&self) -> usize {
self.dims.len()
}
pub fn dimensions(&self) -> &[Count] {
&self.dims
}
pub fn periods(&self) -> &[bool] {
&self.periods
}
pub fn coordinates(&self, rank: Rank) -> Vec<Count> {
let mut coords = vec![0; self.dims.len()];
let mut r = rank;
for i in (0..self.dims.len()).rev() {
coords[i] = r % self.dims[i];
r /= self.dims[i];
}
coords
}
pub fn my_coordinates(&self) -> Vec<Count> {
self.coordinates(self.rank())
}
pub fn rank_from_coordinates(&self, coords: &[Count]) -> Option<Rank> {
let mut rank = 0;
for ((&dim, &periodic), &coord) in self.dims.iter().zip(&self.periods).zip(coords) {
let mut c = coord;
if periodic {
c = c.rem_euclid(dim);
} else if c < 0 || c >= dim {
return None;
}
rank = rank * dim + c;
}
Some(rank)
}
pub fn shift(&self, direction: usize, disp: Count) -> (Option<Rank>, Option<Rank>) {
let coords = self.my_coordinates();
let mut dest = coords.clone();
dest[direction] += disp;
let mut source = coords;
source[direction] -= disp;
(
self.rank_from_coordinates(&source),
self.rank_from_coordinates(&dest),
)
}
}
#[derive(Clone)]
pub struct GraphCommunicator {
comm: SimpleCommunicator,
index: Vec<Count>,
edges: Vec<Count>,
}
impl Communicator for GraphCommunicator {
fn comm_data(&self) -> &CommData {
self.comm.comm_data()
}
}
impl GraphCommunicator {
pub fn num_nodes(&self) -> Count {
self.index.len() as Count
}
pub fn num_edges(&self) -> Count {
self.edges.len() as Count
}
pub fn neighbor_count(&self, rank: Rank) -> Count {
let (s, e) = self.range(rank);
(e - s) as Count
}
pub fn neighbors(&self, rank: Rank) -> Vec<Rank> {
let (s, e) = self.range(rank);
self.edges[s..e].to_vec()
}
pub fn my_neighbors(&self) -> Vec<Rank> {
self.neighbors(self.rank())
}
pub fn neighbor_all_gather_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
where
S: Buffer + ?Sized,
R: BufferMut + ?Sized,
{
const TAG: Tag = 40;
let rt = transport::runtime();
let ctx = self.comm.comm_data().coll_context();
let me = self.comm.comm_data().rank;
let nbrs = self.my_neighbors();
let send = sendbuf.as_bytes();
let dt = sendbuf.as_datatype().id;
for &nb in &nbrs {
rt.send(
ctx,
me,
self.comm.comm_data().world_rank(nb),
TAG,
sendbuf.count() as u64,
dt,
send,
)
.expect("neighbor_all_gather send");
}
let out = recvbuf.as_bytes_mut();
let blk = send.len();
for (k, &nb) in nbrs.iter().enumerate() {
let (_s, _t, _c, _d, payload) = rt.recv(ctx, nb, TAG);
out[k * blk..k * blk + payload.len()].copy_from_slice(&payload);
}
}
pub fn neighbor_all_to_all_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
where
S: Buffer + ?Sized,
R: BufferMut + ?Sized,
{
const TAG: Tag = 41;
let rt = transport::runtime();
let ctx = self.comm.comm_data().coll_context();
let me = self.comm.comm_data().rank;
let nbrs = self.my_neighbors();
let send = sendbuf.as_bytes();
let dt = sendbuf.as_datatype().id;
let esize = sendbuf.as_datatype().size.max(1);
let blk = if nbrs.is_empty() {
0
} else {
send.len() / nbrs.len()
};
for (k, &nb) in nbrs.iter().enumerate() {
rt.send(
ctx,
me,
self.comm.comm_data().world_rank(nb),
TAG,
(blk / esize) as u64,
dt,
&send[k * blk..(k + 1) * blk],
)
.expect("neighbor_all_to_all send");
}
let out = recvbuf.as_bytes_mut();
for (k, &nb) in nbrs.iter().enumerate() {
let (_s, _t, _c, _d, payload) = rt.recv(ctx, nb, TAG);
out[k * blk..k * blk + payload.len()].copy_from_slice(&payload);
}
}
fn range(&self, rank: Rank) -> (usize, usize) {
let r = rank as usize;
let start = if r == 0 {
0
} else {
self.index[r - 1] as usize
};
let end = self.index[r] as usize;
(start, end)
}
}
pub struct InterCommunicator {
data: CommData,
local: Vec<i32>,
remote: Vec<i32>,
}
impl InterCommunicator {
fn new(context: u32, local_rank: Rank, local: Vec<i32>, remote: Vec<i32>) -> InterCommunicator {
let data = CommData {
context,
rank: local_rank,
size: local.len() as Rank,
world_ranks: remote.clone(),
child_seq: AtomicU32::new(0),
name: Mutex::new(None),
attributes: Mutex::new(HashMap::new()),
};
InterCommunicator {
data,
local,
remote,
}
}
pub(crate) fn new_spawned(
context: u32,
local_rank: Rank,
local: Vec<i32>,
remote_count: usize,
) -> InterCommunicator {
let remote: Vec<i32> = (0..remote_count as i32).collect();
let data = CommData {
context,
rank: local_rank,
size: local.len() as Rank,
world_ranks: remote.clone(),
child_seq: AtomicU32::new(0),
name: Mutex::new(None),
attributes: Mutex::new(HashMap::new()),
};
InterCommunicator {
data,
local,
remote,
}
}
pub fn local_size(&self) -> Rank {
self.local.len() as Rank
}
pub fn remote_size(&self) -> Rank {
self.remote.len() as Rank
}
pub fn rank(&self) -> Rank {
self.data.rank
}
pub fn local_group(&self) -> Group {
Group::from_world_ranks(self.local.clone())
}
pub fn remote_group(&self) -> Group {
Group::from_world_ranks(self.remote.clone())
}
pub fn merge(&self) -> SimpleCommunicator {
let mut all = self.local.clone();
all.extend_from_slice(&self.remote);
all.sort_unstable();
all.dedup();
let mut ctx = 0x4D_4552u32; for &w in &all {
ctx = ctx.wrapping_mul(31).wrapping_add(w as u32);
}
ctx &= !COLL_CONTEXT_BIT;
let me = transport::runtime().rank;
let rank = all.iter().position(|&w| w == me).unwrap() as Rank;
let size = all.len() as Rank;
SimpleCommunicator::from_parts(ctx, rank, size, all)
}
}
impl Communicator for InterCommunicator {
fn comm_data(&self) -> &CommData {
&self.data
}
fn size(&self) -> Rank {
self.local.len() as Rank
}
}
#[derive(Clone)]
pub struct DistGraphCommunicator {
comm: SimpleCommunicator,
sources: Vec<Rank>,
destinations: Vec<Rank>,
}
impl Communicator for DistGraphCommunicator {
fn comm_data(&self) -> &CommData {
self.comm.comm_data()
}
}
impl DistGraphCommunicator {
pub fn in_degree(&self) -> usize {
self.sources.len()
}
pub fn out_degree(&self) -> usize {
self.destinations.len()
}
pub fn sources(&self) -> &[Rank] {
&self.sources
}
pub fn destinations(&self) -> &[Rank] {
&self.destinations
}
pub fn neighbor_all_gather_into<S, R>(&self, sendbuf: &S, recvbuf: &mut R)
where
S: Buffer + ?Sized,
R: BufferMut + ?Sized,
{
const TAG: Tag = 42;
let comm = self.comm.comm_data();
let ctx = comm.coll_context();
let me = comm.rank;
let send = sendbuf.as_bytes();
let dt = sendbuf.as_datatype().id;
for &d in &self.destinations {
transport::runtime()
.send(
ctx,
me,
comm.world_rank(d),
TAG,
sendbuf.count() as u64,
dt,
send,
)
.expect("dist-graph neighbor_all_gather send");
}
let out = recvbuf.as_bytes_mut();
let blk = send.len();
for (k, &s) in self.sources.iter().enumerate() {
let (_s, _t, _c, _d, payload) = transport::runtime().recv(ctx, s, TAG);
out[k * blk..k * blk + payload.len()].copy_from_slice(&payload);
}
}
}