1use std::fmt;
9use std::sync::Arc;
10
11use crate::error::{Result, nd_err};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct Dimension {
16 name: Arc<str>,
17 size: usize,
18}
19
20impl Dimension {
21 pub fn new(name: impl Into<Arc<str>>, size: usize) -> Self {
23 Self {
24 name: name.into(),
25 size,
26 }
27 }
28
29 pub fn name(&self) -> &str {
31 &self.name
32 }
33
34 pub fn size(&self) -> usize {
36 self.size
37 }
38}
39
40impl fmt::Display for Dimension {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}={}", self.name, self.size)
43 }
44}
45
46impl<S: Into<Arc<str>>> From<(S, usize)> for Dimension {
47 fn from((name, size): (S, usize)) -> Self {
48 Self::new(name, size)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
56pub struct Dimensions {
57 dims: Arc<[Dimension]>,
58}
59
60impl Dimensions {
61 pub fn try_new(dims: impl IntoIterator<Item = impl Into<Dimension>>) -> Result<Self> {
63 let dims: Vec<Dimension> = dims.into_iter().map(Into::into).collect();
64 for (i, dim) in dims.iter().enumerate() {
65 if dim.name().is_empty() {
66 return Err(nd_err!(
67 InvalidDimensions,
68 "dimension at axis {i} has an empty name"
69 ));
70 }
71 if dims[..i].iter().any(|d| d.name() == dim.name()) {
72 return Err(nd_err!(
73 InvalidDimensions,
74 "duplicate dimension name '{}'",
75 dim.name()
76 ));
77 }
78 }
79 Ok(Self { dims: dims.into() })
80 }
81
82 pub fn scalar() -> Self {
84 Self::default()
85 }
86
87 pub fn rank(&self) -> usize {
89 self.dims.len()
90 }
91
92 pub fn is_scalar(&self) -> bool {
94 self.dims.is_empty()
95 }
96
97 pub fn iter(&self) -> std::slice::Iter<'_, Dimension> {
99 self.dims.iter()
100 }
101
102 pub fn as_slice(&self) -> &[Dimension] {
104 &self.dims
105 }
106
107 pub fn get(&self, axis: usize) -> Option<&Dimension> {
109 self.dims.get(axis)
110 }
111
112 pub fn shape(&self) -> Vec<usize> {
114 self.dims.iter().map(Dimension::size).collect()
115 }
116
117 pub fn names(&self) -> Vec<&str> {
119 self.dims.iter().map(Dimension::name).collect()
120 }
121
122 pub fn num_elements(&self) -> usize {
124 self.dims.iter().map(Dimension::size).product()
125 }
126
127 pub fn checked_num_elements(&self) -> Option<usize> {
129 self.dims
130 .iter()
131 .try_fold(1usize, |acc, d| acc.checked_mul(d.size()))
132 }
133
134 pub fn position(&self, name: &str) -> Option<usize> {
136 self.dims.iter().position(|d| d.name() == name)
137 }
138
139 pub fn contains(&self, name: &str) -> bool {
141 self.position(name).is_some()
142 }
143
144 pub fn axis(&self, name: &str) -> Result<usize> {
146 self.position(name)
147 .ok_or_else(|| nd_err!(InvalidArgument, "dimension '{name}' is not in {self}"))
148 }
149
150 pub fn size_of(&self, name: &str) -> Option<usize> {
152 self.position(name).map(|axis| self.dims[axis].size())
153 }
154
155 pub fn broadcast(all: &[&Dimensions]) -> Result<Dimensions> {
160 let mut out: Vec<Dimension> = Vec::new();
161 for dims in all {
162 for dim in dims.iter() {
163 match out.iter_mut().find(|d| d.name() == dim.name()) {
164 None => out.push(dim.clone()),
165 Some(existing) if existing.size() == dim.size() || dim.size() == 1 => {}
166 Some(existing) if existing.size() == 1 => *existing = dim.clone(),
167 Some(existing) => {
168 return Err(nd_err!(
169 ShapeMismatch,
170 "dimension '{}' has incompatible sizes {} and {}",
171 dim.name(),
172 existing.size(),
173 dim.size()
174 ));
175 }
176 }
177 }
178 }
179 Dimensions::try_new(out)
180 }
181
182 pub fn check_broadcast_to(&self, target: &Dimensions) -> Result<()> {
185 for dim in self.iter() {
186 let Some(axis) = target.position(dim.name()) else {
187 return Err(nd_err!(
188 ShapeMismatch,
189 "cannot broadcast {self} to {target}: dimension '{}' is missing from the target",
190 dim.name()
191 ));
192 };
193 let target_size = target.dims[axis].size();
194 if dim.size() != target_size && dim.size() != 1 {
195 return Err(nd_err!(
196 ShapeMismatch,
197 "cannot broadcast {self} to {target}: dimension '{}' has size {} but the target expects {target_size}",
198 dim.name(),
199 dim.size(),
200 ));
201 }
202 }
203 Ok(())
204 }
205
206 pub fn c_strides(&self) -> Vec<usize> {
208 let mut strides = vec![0usize; self.rank()];
209 let mut acc = 1usize;
210 for axis in (0..self.rank()).rev() {
211 strides[axis] = acc;
212 acc = acc.saturating_mul(self.dims[axis].size());
213 }
214 strides
215 }
216}
217
218impl<'a> IntoIterator for &'a Dimensions {
219 type Item = &'a Dimension;
220 type IntoIter = std::slice::Iter<'a, Dimension>;
221
222 fn into_iter(self) -> Self::IntoIter {
223 self.iter()
224 }
225}
226
227impl fmt::Display for Dimensions {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 write!(f, "(")?;
230 for (i, dim) in self.dims.iter().enumerate() {
231 if i > 0 {
232 write!(f, ", ")?;
233 }
234 write!(f, "{dim}")?;
235 }
236 write!(f, ")")
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn strides_are_c_order() {
246 let d = Dimensions::try_new([("time", 2), ("lat", 3), ("lon", 4)]).unwrap();
247 assert_eq!(d.c_strides(), vec![12, 4, 1]);
248 assert_eq!(d.num_elements(), 24);
249 assert_eq!(d.names(), vec!["time", "lat", "lon"]);
250 }
251
252 #[test]
253 fn scalar_has_one_element() {
254 let d = Dimensions::scalar();
255 assert_eq!(d.rank(), 0);
256 assert_eq!(d.num_elements(), 1);
257 assert!(d.c_strides().is_empty());
258 }
259
260 #[test]
261 fn invalid_names_rejected() {
262 assert!(Dimensions::try_new([("x", 2), ("x", 3)]).is_err());
263 assert!(Dimensions::try_new([("", 2)]).is_err());
264 }
265
266 #[test]
267 fn checked_num_elements_detects_overflow() {
268 let d = Dimensions::try_new([("a", usize::MAX), ("b", 2)]).unwrap();
269 assert_eq!(d.checked_num_elements(), None);
270 }
271
272 #[test]
273 fn broadcast_check_follows_the_rule() {
274 let target = Dimensions::try_new([("time", 2), ("lat", 3)]).unwrap();
275 let ok = |spec: &[(&str, usize)]| {
276 Dimensions::try_new(spec.iter().copied())
277 .unwrap()
278 .check_broadcast_to(&target)
279 };
280 assert!(ok(&[("lat", 3)]).is_ok());
281 assert!(ok(&[("time", 1)]).is_ok());
282 assert!(Dimensions::scalar().check_broadcast_to(&target).is_ok());
283 let missing = ok(&[("depth", 2)]).unwrap_err();
284 assert!(
285 missing.to_string().contains("'depth' is missing"),
286 "{missing}"
287 );
288 let size = ok(&[("lat", 2)]).unwrap_err();
289 assert!(size.to_string().contains("has size 2"), "{size}");
290 }
291}