#[allow(unused_imports)]
use super::functions::*;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Default)]
pub struct DbRow {
pub columns: HashMap<String, DbValue>,
}
impl DbRow {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, val: impl Into<DbValue>) {
self.columns.insert(key.into(), val.into());
}
pub fn get(&self, key: &str) -> Option<&DbValue> {
self.columns.get(key)
}
}
#[derive(Debug, Clone)]
pub struct SimulationMetadata {
pub sim_id: String,
pub description: String,
pub created_at: f64,
pub parameters: HashMap<String, String>,
pub artifacts: Vec<String>,
}
impl SimulationMetadata {
pub fn new(sim_id: impl Into<String>, description: impl Into<String>, created_at: f64) -> Self {
Self {
sim_id: sim_id.into(),
description: description.into(),
created_at,
parameters: HashMap::new(),
artifacts: Vec::new(),
}
}
pub fn set_param(&mut self, key: impl Into<String>, val: impl Into<String>) {
self.parameters.insert(key.into(), val.into());
}
pub fn add_artifact(&mut self, path: impl Into<String>) {
self.artifacts.push(path.into());
}
}
#[derive(Debug, Default)]
pub struct DataCatalog {
pub(super) entries: HashMap<String, SimulationMetadata>,
}
impl DataCatalog {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, meta: SimulationMetadata) {
self.entries.insert(meta.sim_id.clone(), meta);
}
pub fn lookup(&self, sim_id: &str) -> Option<&SimulationMetadata> {
self.entries.get(sim_id)
}
pub fn lookup_mut(&mut self, sim_id: &str) -> Option<&mut SimulationMetadata> {
self.entries.get_mut(sim_id)
}
pub fn remove(&mut self, sim_id: &str) -> bool {
self.entries.remove(sim_id).is_some()
}
pub fn search_description(&self, query: &str) -> Vec<&SimulationMetadata> {
let q = query.to_lowercase();
self.entries
.values()
.filter(|m| m.description.to_lowercase().contains(&q))
.collect()
}
pub fn all_ids(&self) -> Vec<&str> {
self.entries.keys().map(|s| s.as_str()).collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Default)]
pub struct TimeSeriesStore {
pub(super) samples: Vec<TsSample>,
pub label: String,
}
impl TimeSeriesStore {
pub fn new(label: impl Into<String>) -> Self {
Self {
samples: Vec::new(),
label: label.into(),
}
}
pub fn append(&mut self, time: f64, value: f64) {
self.samples.push(TsSample::new(time, value));
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn range_query(&self, t_start: f64, t_end: f64) -> Vec<&TsSample> {
self.samples
.iter()
.filter(|s| s.time >= t_start && s.time <= t_end)
.collect()
}
pub fn decimate(&self, n: usize) -> TimeSeriesStore {
let mut out = TimeSeriesStore::new(format!("{}_dec{n}", self.label));
if n == 0 {
return out;
}
for (i, s) in self.samples.iter().enumerate() {
if i % n == 0 {
out.append(s.time, s.value);
}
}
out
}
pub fn mean_in_range(&self, t_start: f64, t_end: f64) -> Option<f64> {
let vals: Vec<f64> = self
.range_query(t_start, t_end)
.iter()
.map(|s| s.value)
.collect();
if vals.is_empty() {
None
} else {
Some(vals.iter().sum::<f64>() / vals.len() as f64)
}
}
pub fn max_value(&self) -> Option<f64> {
self.samples.iter().map(|s| s.value).reduce(f64::max)
}
pub fn min_value(&self) -> Option<f64> {
self.samples.iter().map(|s| s.value).reduce(f64::min)
}
pub fn to_csv(&self) -> String {
let mut out = String::from("time,value\n");
for s in &self.samples {
out.push_str(&format!("{},{}\n", s.time, s.value));
}
out
}
pub fn interpolate(&self, t: f64) -> Option<f64> {
if self.samples.is_empty() {
return None;
}
let pos = self.samples.partition_point(|s| s.time <= t);
if pos == 0 {
return Some(self.samples[0].value);
}
if pos >= self.samples.len() {
return Some(
self.samples
.last()
.expect("collection should not be empty")
.value,
);
}
let lo = &self.samples[pos - 1];
let hi = &self.samples[pos];
let dt = hi.time - lo.time;
if dt < 1e-15 {
return Some(lo.value);
}
let frac = (t - lo.time) / dt;
Some(lo.value + frac * (hi.value - lo.value))
}
}
#[derive(Debug, Default)]
pub struct SnapshotCatalog {
pub(super) snapshots: HashMap<String, SnapshotEntry>,
pub(super) next_id: u64,
}
impl SnapshotCatalog {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, entry: SnapshotEntry) -> &str {
let id = entry.id.clone();
self.snapshots.insert(id.clone(), entry);
self.snapshots[&id].id.as_str()
}
pub fn register_auto(&mut self, sim_time: f64, path: impl Into<String>) -> String {
let id = format!("snap_{:06}", self.next_id);
self.next_id += 1;
self.register(SnapshotEntry::new(id.clone(), sim_time, path));
id
}
pub fn get(&self, id: &str) -> Option<&SnapshotEntry> {
self.snapshots.get(id)
}
pub fn get_mut(&mut self, id: &str) -> Option<&mut SnapshotEntry> {
self.snapshots.get_mut(id)
}
pub fn remove(&mut self, id: &str) -> bool {
self.snapshots.remove(id).is_some()
}
pub fn len(&self) -> usize {
self.snapshots.len()
}
pub fn is_empty(&self) -> bool {
self.snapshots.is_empty()
}
pub fn range_query(&self, t_start: f64, t_end: f64) -> Vec<&SnapshotEntry> {
self.snapshots
.values()
.filter(|s| s.sim_time >= t_start && s.sim_time <= t_end)
.collect()
}
pub fn query_by_tag(&self, tag: &str) -> Vec<&SnapshotEntry> {
self.snapshots
.values()
.filter(|s| s.tags.iter().any(|t| t == tag))
.collect()
}
pub fn unloaded_ids(&self) -> Vec<&str> {
self.snapshots
.values()
.filter(|s| !s.loaded)
.map(|s| s.id.as_str())
.collect()
}
pub fn mark_range_loaded(&mut self, t_start: f64, t_end: f64) {
for s in self.snapshots.values_mut() {
if s.sim_time >= t_start && s.sim_time <= t_end {
s.loaded = true;
}
}
}
pub fn total_file_size(&self) -> usize {
self.snapshots.values().map(|s| s.file_size).sum()
}
pub fn sorted_by_time(&self) -> Vec<&SnapshotEntry> {
let mut v: Vec<&SnapshotEntry> = self.snapshots.values().collect();
v.sort_by(|a, b| {
a.sim_time
.partial_cmp(&b.sim_time)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub key: String,
pub data: Vec<f64>,
pub sim_time: f64,
pub version: u64,
}
impl CacheEntry {
pub fn new(key: impl Into<String>, data: Vec<f64>, sim_time: f64, version: u64) -> Self {
Self {
key: key.into(),
data,
sim_time,
version,
}
}
}
#[derive(Debug, Clone)]
pub struct SimulationRecord {
pub name: String,
pub timestamp: f64,
pub params: HashMap<String, String>,
pub output_path: Option<String>,
}
impl SimulationRecord {
pub fn new(name: impl Into<String>, timestamp: f64) -> Self {
Self {
name: name.into(),
timestamp,
params: HashMap::new(),
output_path: None,
}
}
pub fn set_param(&mut self, key: impl Into<String>, val: impl Into<String>) {
self.params.insert(key.into(), val.into());
}
pub fn get_param(&self, key: &str) -> Option<&str> {
self.params.get(key).map(|s| s.as_str())
}
pub fn set_output(&mut self, path: impl Into<String>) {
self.output_path = Some(path.into());
}
}
#[derive(Debug, Clone, Default)]
pub struct DatabaseSerializer;
impl DatabaseSerializer {
pub fn new() -> Self {
Self
}
pub fn serialize(&self, rec: &SimulationRecord) -> String {
let params_str: Vec<String> = rec
.params
.iter()
.map(|(k, v)| format!(r#""{k}":"{v}""#))
.collect();
let params_json = format!("{{{}}}", params_str.join(","));
let output_str = match &rec.output_path {
Some(p) => format!(r#""{p}""#),
None => "null".to_string(),
};
format!(
r#"{{"name":"{name}","timestamp":{ts},"params":{params},"output":{out}}}"#,
name = rec.name,
ts = rec.timestamp,
params = params_json,
out = output_str,
)
}
pub fn serialize_all(&self, recs: &[&SimulationRecord]) -> String {
let items: Vec<String> = recs.iter().map(|r| self.serialize(r)).collect();
format!("[{}]", items.join(","))
}
pub fn deserialize(&self, s: &str) -> Option<SimulationRecord> {
let name = self.extract_string_field(s, "name")?;
let ts_str = self.extract_raw_field(s, "timestamp")?;
let timestamp: f64 = ts_str.trim().parse().ok()?;
let mut rec = SimulationRecord::new(name, timestamp);
let out_raw = self.extract_raw_field(s, "output").unwrap_or_default();
let out_raw = out_raw.trim();
if out_raw != "null" && out_raw.starts_with('"') {
let cleaned = out_raw.trim_matches('"').to_string();
rec.set_output(cleaned);
}
Some(rec)
}
fn extract_string_field(&self, s: &str, field: &str) -> Option<String> {
let key = format!(r#""{field}":""#);
let start = s.find(key.as_str())? + key.len();
let rest = &s[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn extract_raw_field(&self, s: &str, field: &str) -> Option<String> {
let key = format!(r#""{field}":"#);
let start = s.find(key.as_str())? + key.len();
let rest = &s[start..];
let end = rest.find([',', '}']).unwrap_or(rest.len());
Some(rest[..end].to_string())
}
}
#[derive(Debug, Clone)]
pub struct TsSample {
pub time: f64,
pub value: f64,
}
impl TsSample {
pub fn new(time: f64, value: f64) -> Self {
Self { time, value }
}
}
#[derive(Debug, Clone)]
pub struct MaterialRecord {
pub name: String,
pub density: f64,
pub youngs_modulus: f64,
pub poisson_ratio: f64,
pub thermal_conductivity: f64,
pub yield_strength: f64,
pub tags: Vec<String>,
}
impl MaterialRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
name: impl Into<String>,
density: f64,
youngs_modulus: f64,
poisson_ratio: f64,
thermal_conductivity: f64,
yield_strength: f64,
tags: Vec<String>,
) -> Self {
Self {
name: name.into(),
density,
youngs_modulus,
poisson_ratio,
thermal_conductivity,
yield_strength,
tags,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExportFilter {
pub columns: Vec<String>,
pub min_value: Option<(String, f64)>,
pub max_value: Option<(String, f64)>,
}
impl ExportFilter {
pub fn new() -> Self {
Self::default()
}
pub fn row_passes(&self, row: &DbRow) -> bool {
if let Some((col, lo)) = &self.min_value {
let v = row
.get(col)
.and_then(|v| v.as_f64())
.unwrap_or(f64::NEG_INFINITY);
if v < *lo {
return false;
}
}
if let Some((col, hi)) = &self.max_value {
let v = row
.get(col)
.and_then(|v| v.as_f64())
.unwrap_or(f64::INFINITY);
if v > *hi {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
Csv,
Json,
Hdf5Text,
}
#[derive(Debug)]
pub struct ExportPipeline {
pub format: ExportFormat,
pub filter: ExportFilter,
}
impl ExportPipeline {
pub fn new(format: ExportFormat) -> Self {
Self {
format,
filter: ExportFilter::new(),
}
}
pub fn with_filter(mut self, filter: ExportFilter) -> Self {
self.filter = filter;
self
}
pub fn export(&self, db: &SimulationDatabase) -> String {
let cols = &self.filter.columns;
let rows: Vec<&DbRow> = db
.rows
.iter()
.filter(|r| self.filter.row_passes(r))
.collect();
match self.format {
ExportFormat::Csv => self.to_csv(&rows, cols),
ExportFormat::Json => self.to_json(&rows, cols),
ExportFormat::Hdf5Text => self.to_hdf5_text(&rows, cols, &db.name),
}
}
fn effective_cols(&self, rows: &[&DbRow], cols: &[String]) -> Vec<String> {
if cols.is_empty() {
let mut seen: Vec<String> = Vec::new();
for row in rows {
for k in row.columns.keys() {
if !seen.contains(k) {
seen.push(k.clone());
}
}
}
seen.sort();
seen
} else {
cols.to_vec()
}
}
fn value_to_str(v: &DbValue) -> String {
match v {
DbValue::Int(i) => i.to_string(),
DbValue::Float(f) => format!("{f}"),
DbValue::Text(s) => s.clone(),
DbValue::Bool(b) => b.to_string(),
DbValue::Null => "".to_string(),
}
}
fn to_csv(&self, rows: &[&DbRow], cols: &[String]) -> String {
let headers = self.effective_cols(rows, cols);
let mut out = headers.join(",");
out.push('\n');
for row in rows {
let line: Vec<String> = headers
.iter()
.map(|c| row.get(c).map(Self::value_to_str).unwrap_or_default())
.collect();
out.push_str(&line.join(","));
out.push('\n');
}
out
}
fn to_json(&self, rows: &[&DbRow], cols: &[String]) -> String {
let headers = self.effective_cols(rows, cols);
let mut out = String::from("[\n");
for (ri, row) in rows.iter().enumerate() {
out.push_str(" {");
let fields: Vec<String> = headers
.iter()
.map(|c| {
let val = row
.get(c)
.map(|v| match v {
DbValue::Text(s) => format!(r#""{s}""#),
DbValue::Null => "null".to_string(),
other => Self::value_to_str(other),
})
.unwrap_or_else(|| "null".to_string());
format!(r#""{c}":{val}"#)
})
.collect();
out.push_str(&fields.join(","));
out.push('}');
if ri + 1 < rows.len() {
out.push(',');
}
out.push('\n');
}
out.push(']');
out
}
fn to_hdf5_text(&self, rows: &[&DbRow], cols: &[String], table_name: &str) -> String {
let headers = self.effective_cols(rows, cols);
let mut out = format!("# HDF5-like text dump of table '{table_name}'\n");
out.push_str(&format!("# columns: {}\n", headers.join(",")));
out.push_str(&format!("# rows: {}\n", rows.len()));
for row in rows {
let line: Vec<String> = headers
.iter()
.map(|c| row.get(c).map(Self::value_to_str).unwrap_or_default())
.collect();
out.push_str(&line.join(" "));
out.push('\n');
}
out
}
}
#[derive(Debug, Default)]
pub struct MaterialDatabase {
pub(super) records: Vec<MaterialRecord>,
}
impl MaterialDatabase {
pub fn new() -> Self {
Self::default()
}
pub fn with_defaults() -> Self {
let mut db = Self::new();
db.insert(MaterialRecord::new(
"steel_1020",
7850.0,
210e9,
0.29,
50.0,
250e6,
vec!["metal".into(), "steel".into(), "ferrous".into()],
));
db.insert(MaterialRecord::new(
"aluminium_6061",
2700.0,
69e9,
0.33,
167.0,
276e6,
vec!["metal".into(), "aluminium".into(), "light".into()],
));
db.insert(MaterialRecord::new(
"copper",
8960.0,
110e9,
0.34,
385.0,
70e6,
vec!["metal".into(), "copper".into(), "conductor".into()],
));
db.insert(MaterialRecord::new(
"polycarbonate",
1200.0,
2.4e9,
0.37,
0.2,
60e6,
vec!["polymer".into(), "plastic".into(), "transparent".into()],
));
db.insert(MaterialRecord::new(
"concrete",
2300.0,
30e9,
0.20,
1.7,
3e6,
vec!["composite".into(), "concrete".into(), "brittle".into()],
));
db
}
pub fn insert(&mut self, record: MaterialRecord) {
self.records.push(record);
}
pub fn lookup(&self, name: &str) -> Option<&MaterialRecord> {
self.records.iter().find(|r| r.name == name)
}
pub fn fuzzy_search(&self, query: &str) -> Vec<&MaterialRecord> {
let q = query.to_lowercase();
self.records
.iter()
.filter(|r| {
r.name.to_lowercase().contains(&q)
|| r.tags.iter().any(|t| t.to_lowercase().contains(&q))
})
.collect()
}
pub fn interpolate(&self, name_a: &str, name_b: &str, t: f64) -> Option<MaterialRecord> {
let a = self.lookup(name_a)?;
let b = self.lookup(name_b)?;
let lerp = |va: f64, vb: f64| va + t * (vb - va);
Some(MaterialRecord::new(
format!("{name_a}_to_{name_b}_{t:.2}"),
lerp(a.density, b.density),
lerp(a.youngs_modulus, b.youngs_modulus),
lerp(a.poisson_ratio, b.poisson_ratio),
lerp(a.thermal_conductivity, b.thermal_conductivity),
lerp(a.yield_strength, b.yield_strength),
vec![],
))
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn names(&self) -> Vec<&str> {
self.records.iter().map(|r| r.name.as_str()).collect()
}
}
#[derive(Debug)]
pub struct ResultCache {
pub capacity: usize,
pub current_version: u64,
pub(super) entries: VecDeque<CacheEntry>,
}
impl ResultCache {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
current_version: 0,
entries: VecDeque::new(),
}
}
pub fn put(&mut self, entry: CacheEntry) {
self.entries.retain(|e| e.key != entry.key);
self.entries.push_front(entry);
while self.entries.len() > self.capacity {
self.entries.pop_back();
}
}
pub fn get(&mut self, key: &str) -> Option<&CacheEntry> {
let pos = self.entries.iter().position(|e| e.key == key)?;
let entry = self.entries.remove(pos)?;
if entry.version < self.current_version {
return None;
}
self.entries.push_front(entry);
self.entries.front()
}
pub fn invalidate_all(&mut self) {
self.current_version += 1;
}
pub fn remove(&mut self, key: &str) {
self.entries.retain(|e| e.key != key);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn evict_stale(&mut self) {
let ver = self.current_version;
self.entries.retain(|e| e.version >= ver);
}
}
#[derive(Debug, Default)]
pub struct SimulationRecordDatabase {
pub(super) records: HashMap<String, SimulationRecord>,
}
impl SimulationRecordDatabase {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, rec: SimulationRecord) {
self.records.insert(rec.name.clone(), rec);
}
pub fn get(&self, name: &str) -> Option<&SimulationRecord> {
self.records.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut SimulationRecord> {
self.records.get_mut(name)
}
pub fn delete(&mut self, name: &str) -> bool {
self.records.remove(name).is_some()
}
pub fn count(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn query(&self, q: &DatabaseQuery) -> Vec<&SimulationRecord> {
self.records.values().filter(|r| q.matches(r)).collect()
}
pub fn names(&self) -> Vec<&str> {
self.records.keys().map(|s| s.as_str()).collect()
}
pub fn clear(&mut self) {
self.records.clear();
}
}
#[derive(Debug, Clone)]
pub struct SnapshotEntry {
pub id: String,
pub sim_time: f64,
pub path: String,
pub file_size: usize,
pub loaded: bool,
pub tags: Vec<String>,
}
impl SnapshotEntry {
pub fn new(id: impl Into<String>, sim_time: f64, path: impl Into<String>) -> Self {
Self {
id: id.into(),
sim_time,
path: path.into(),
file_size: 0,
loaded: false,
tags: Vec::new(),
}
}
pub fn mark_loaded(&mut self) {
self.loaded = true;
}
pub fn add_tag(&mut self, tag: impl Into<String>) {
self.tags.push(tag.into());
}
}
#[derive(Debug, Default)]
pub struct SimulationDatabase {
pub rows: Vec<DbRow>,
pub name: String,
}
impl SimulationDatabase {
pub fn new(name: impl Into<String>) -> Self {
Self {
rows: Vec::new(),
name: name.into(),
}
}
pub fn insert(&mut self, row: DbRow) {
self.rows.push(row);
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn query_eq(&self, col: &str, val: &DbValue) -> Vec<&DbRow> {
self.rows
.iter()
.filter(|r| r.get(col).map(|v| v == val).unwrap_or(false))
.collect()
}
pub fn query_range(&self, col: &str, lo: f64, hi: f64) -> Vec<&DbRow> {
self.rows
.iter()
.filter(|r| {
r.get(col)
.and_then(|v| v.as_f64())
.map(|f| f >= lo && f <= hi)
.unwrap_or(false)
})
.collect()
}
pub fn project(&self, cols: &[&str]) -> Vec<HashMap<String, DbValue>> {
self.rows
.iter()
.map(|r| {
cols.iter()
.filter_map(|&c| r.get(c).map(|v| (c.to_string(), v.clone())))
.collect()
})
.collect()
}
pub fn delete_eq(&mut self, col: &str, val: &DbValue) -> usize {
let before = self.rows.len();
self.rows
.retain(|r| r.get(col).map(|v| v != val).unwrap_or(true));
before - self.rows.len()
}
pub fn clear(&mut self) {
self.rows.clear();
}
pub fn column_mean(&self, col: &str) -> Option<f64> {
let vals: Vec<f64> = self
.rows
.iter()
.filter_map(|r| r.get(col).and_then(|v| v.as_f64()))
.collect();
if vals.is_empty() {
None
} else {
Some(vals.iter().sum::<f64>() / vals.len() as f64)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DatabaseQuery {
pub time_start: Option<f64>,
pub time_end: Option<f64>,
pub name_prefix: Option<String>,
pub param_filter: Option<(String, String)>,
}
impl DatabaseQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_time_range(mut self, t_start: f64, t_end: f64) -> Self {
self.time_start = Some(t_start);
self.time_end = Some(t_end);
self
}
pub fn with_name_prefix(mut self, prefix: impl Into<String>) -> Self {
self.name_prefix = Some(prefix.into());
self
}
pub fn with_param(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
self.param_filter = Some((key.into(), val.into()));
self
}
pub fn matches(&self, rec: &SimulationRecord) -> bool {
if let Some(t0) = self.time_start
&& rec.timestamp < t0
{
return false;
}
if let Some(t1) = self.time_end
&& rec.timestamp > t1
{
return false;
}
if let Some(ref prefix) = self.name_prefix
&& !rec.name.starts_with(prefix.as_str())
{
return false;
}
if let Some((ref k, ref v)) = self.param_filter {
match rec.params.get(k.as_str()) {
Some(pv) if pv == v => {}
_ => return false,
}
}
true
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DbValue {
Int(i64),
Float(f64),
Text(String),
Bool(bool),
Null,
}
impl DbValue {
pub fn as_f64(&self) -> Option<f64> {
match self {
DbValue::Float(v) => Some(*v),
DbValue::Int(v) => Some(*v as f64),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
DbValue::Text(s) => Some(s.as_str()),
_ => None,
}
}
}