1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! Asymmetric-unit sites: label, element, coordinates, occupancy and props.
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::Error;
use crate::elements::element_by_symbol;
/// One site of the asymmetric unit.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Site {
label: String,
element: String,
frac: [f64; 3],
occupancy: f64,
u_iso: Option<f64>,
u_aniso: Option<[f64; 6]>,
charge: Option<f64>,
disorder_group: Option<u32>,
props: BTreeMap<String, Value>,
}
impl Site {
/// Creates a fully occupied site with no displacement parameters.
///
/// The element is normalized through [`crate::elements::element_by_symbol`]
/// (`Fe2+` becomes `Fe`, `D` stays deuterium).
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] for an empty label, an unknown element,
/// non-finite coordinates or an occupancy outside `[0, 1]`.
pub fn new(label: impl Into<String>, element: &str, frac: [f64; 3]) -> Result<Self, Error> {
let label = label.into();
if label.trim().is_empty() {
return Err(Error::InvalidInput(
"site label must not be empty".to_owned(),
));
}
if !frac.iter().all(|value| value.is_finite()) {
return Err(Error::InvalidInput(
"site coordinates must be finite".to_owned(),
));
}
let element = element_by_symbol(element)?.symbol.clone();
Ok(Self {
label,
element,
frac,
occupancy: 1.0,
u_iso: None,
u_aniso: None,
charge: None,
disorder_group: None,
props: BTreeMap::new(),
})
}
/// Site label.
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
/// Canonical element symbol.
#[must_use]
pub fn element(&self) -> &str {
&self.element
}
/// Fractional coordinates.
#[must_use]
pub fn frac(&self) -> [f64; 3] {
self.frac
}
/// Occupancy in `[0, 1]`.
#[must_use]
pub fn occupancy(&self) -> f64 {
self.occupancy
}
/// Isotropic displacement parameter, when present.
#[must_use]
pub fn u_iso(&self) -> Option<f64> {
self.u_iso
}
/// Anisotropic displacement parameters `U11 U22 U33 U12 U13 U23`.
#[must_use]
pub fn u_aniso(&self) -> Option<[f64; 6]> {
self.u_aniso
}
/// Formal charge, when present.
#[must_use]
pub fn charge(&self) -> Option<f64> {
self.charge
}
/// Disorder assembly or group number.
#[must_use]
pub fn disorder_group(&self) -> Option<u32> {
self.disorder_group
}
/// Open extension map.
#[must_use]
pub fn props(&self) -> &BTreeMap<String, Value> {
&self.props
}
/// Whether the site is partially occupied.
#[must_use]
pub fn is_partial(&self) -> bool {
self.occupancy < 1.0
}
/// Copy with a different occupancy.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] when the occupancy is not finite or
/// outside `[0, 1]`.
pub fn with_occupancy(mut self, occupancy: f64) -> Result<Self, Error> {
if !occupancy.is_finite() || !(0.0..=1.0).contains(&occupancy) {
return Err(Error::InvalidInput(format!(
"occupancy must be in [0, 1], got {occupancy}"
)));
}
self.occupancy = occupancy;
Ok(self)
}
/// Copy with different fractional coordinates.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] for non-finite coordinates.
pub fn with_frac(mut self, frac: [f64; 3]) -> Result<Self, Error> {
if !frac.iter().all(|value| value.is_finite()) {
return Err(Error::InvalidInput(
"site coordinates must be finite".to_owned(),
));
}
self.frac = frac;
Ok(self)
}
/// Copy with an isotropic displacement parameter.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] when the value is not finite or is
/// negative.
pub fn with_u_iso(mut self, u_iso: f64) -> Result<Self, Error> {
if !u_iso.is_finite() || u_iso < 0.0 {
return Err(Error::InvalidInput(format!(
"Uiso must be non-negative and finite, got {u_iso}"
)));
}
self.u_iso = Some(u_iso);
Ok(self)
}
/// Copy with anisotropic displacement parameters.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] for non-finite values.
pub fn with_u_aniso(mut self, u_aniso: [f64; 6]) -> Result<Self, Error> {
if !u_aniso.iter().all(|value| value.is_finite()) {
return Err(Error::InvalidInput(
"anisotropic displacement parameters must be finite".to_owned(),
));
}
self.u_aniso = Some(u_aniso);
Ok(self)
}
/// Copy with a formal charge.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] for a non-finite charge.
pub fn with_charge(mut self, charge: f64) -> Result<Self, Error> {
if !charge.is_finite() {
return Err(Error::InvalidInput("site charge must be finite".to_owned()));
}
self.charge = Some(charge);
Ok(self)
}
/// Copy with a disorder group.
#[must_use]
pub fn with_disorder_group(mut self, group: u32) -> Self {
self.disorder_group = Some(group);
self
}
/// Copy without a disorder group.
#[must_use]
pub fn without_disorder_group(mut self) -> Self {
self.disorder_group = None;
self
}
/// Copy with one extension property.
#[must_use]
pub fn with_prop(mut self, key: impl Into<String>, value: Value) -> Self {
self.props.insert(key.into(), value);
self
}
}