source-modelling 2026.7.2__tar.gz → 2026.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/PKG-INFO +1 -1
  2. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/fsp.py +1 -7
  3. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/parse_utils.py +0 -1
  4. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/rupture_propagation.py +2 -2
  5. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/sources.py +10 -11
  6. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/stoch.py +46 -30
  7. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/PKG-INFO +1 -1
  8. source_modelling-2026.7.3/source_modelling.egg-info/scm_version.json +8 -0
  9. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/srf_parser.rs +47 -27
  10. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/srf_writer.rs +15 -16
  11. source_modelling-2026.7.3/uv.lock +2414 -0
  12. source_modelling-2026.7.2/source_modelling.egg-info/scm_version.json +0 -8
  13. source_modelling-2026.7.2/uv.lock +0 -2539
  14. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/deptry.yml +0 -0
  15. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/git-extension.yml +0 -0
  16. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/numpydoc.yml +0 -0
  17. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/publish-PyPI.yml +0 -0
  18. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/pytest.yml +0 -0
  19. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/ruff.yml +0 -0
  20. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/rust.yml +0 -0
  21. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/types.yml +0 -0
  22. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.github/workflows/wiki.yml +0 -0
  23. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.gitignore +0 -0
  24. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.isort.cfg +0 -0
  25. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/.python-version +0 -0
  26. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/Cargo.toml +0 -0
  27. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/MANIFEST.in +0 -0
  28. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/pyproject.toml +0 -0
  29. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/setup.cfg +0 -0
  30. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/setup.py +0 -0
  31. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.CPG +0 -0
  32. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.dbf +0 -0
  33. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.prj +0 -0
  34. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.sbn +0 -0
  35. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.sbx +0 -0
  36. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.shp +0 -0
  37. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.shp.xml +0 -0
  38. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/NZ_CFM/NZ_CFM_v1_0.shx +0 -0
  39. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/__init__.py +0 -0
  40. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/ccldpy.py +0 -0
  41. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/community_fault_model.py +0 -0
  42. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/gc2_distances.py +0 -0
  43. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/gsf.py +0 -0
  44. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/magnitude_scaling.py +0 -0
  45. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/moment.py +0 -0
  46. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/slip.py +0 -0
  47. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/srf.py +0 -0
  48. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling/trim.py +0 -0
  49. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/SOURCES.txt +0 -0
  50. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/dependency_links.txt +0 -0
  51. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/not-zip-safe +0 -0
  52. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/requires.txt +0 -0
  53. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/scm_file_list.json +0 -0
  54. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/source_modelling.egg-info/top_level.txt +0 -0
  55. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/lib.rs +0 -0
  56. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/pytypes.rs +0 -0
  57. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/scanner.rs +0 -0
  58. {source_modelling-2026.7.2 → source_modelling-2026.7.3}/src/types.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: source-modelling
3
- Version: 2026.7.2
3
+ Version: 2026.7.3
4
4
  Summary: Source modelling library
5
5
  Author: ucgmsim
6
6
  Requires-Python: >=3.12
@@ -65,8 +65,6 @@ def _normalise_value(value: float) -> float | None:
65
65
  class FSPParseError(Exception):
66
66
  """Exception raised for errors in parsing FSP files."""
67
67
 
68
- pass
69
-
70
68
 
71
69
  @dataclasses.dataclass
72
70
  class Segment:
@@ -278,11 +276,7 @@ class FSPFile:
278
276
  for line in fsp_file_handle:
279
277
  if line.startswith("% Data"):
280
278
  break
281
- if (
282
- line.startswith("% -")
283
- or line.strip() == "%"
284
- or line.startswith("% Event :")
285
- ):
279
+ if line.startswith(("% -", "% Event :")) or line.strip() == "%":
286
280
  continue
287
281
  # Strip the leading "% ", and deduplicate the spaces in the line.
288
282
  # This is required to normalise the string so that the parse
@@ -6,7 +6,6 @@ from typing import TextIO
6
6
  class ParseError(Exception):
7
7
  """Error for parsing files in source modelling."""
8
8
 
9
- pass
10
9
 
11
10
 
12
11
  def _is_seperator(char: str) -> bool:
@@ -59,7 +59,7 @@ def spanning_tree_with_probabilities(
59
59
  trees = []
60
60
  probabilities = []
61
61
 
62
- for tree in mst.SpanningTreeIterator(graph): # type: ignore
62
+ for tree in mst.SpanningTreeIterator(graph): # ty: ignore[invalid-argument-type]
63
63
  p_tree = 1.0
64
64
  for u, v in graph.edges:
65
65
  if tree.has_edge(u, v):
@@ -223,7 +223,7 @@ def select_top_spanning_trees(
223
223
  cumulative_tree_weight = 0.0
224
224
  spanning_trees = []
225
225
 
226
- for spanning_tree in mst.SpanningTreeIterator(weighted_graph, minimum=False): # type: ignore
226
+ for spanning_tree in mst.SpanningTreeIterator(weighted_graph, minimum=False): # ty: ignore[invalid-argument-type]
227
227
  spanning_trees.append(spanning_tree)
228
228
  tree_log_probability = sum(
229
229
  spanning_tree[node_u][node_v]["weight"]
@@ -1042,7 +1042,7 @@ class Fault:
1042
1042
 
1043
1043
  # This relation can now be used to identify if the list of planes given is a line.
1044
1044
  points_into_graph: nx.DiGraph = nx.from_dict_of_lists(
1045
- points_into_relation, # type: ignore
1045
+ points_into_relation, # ty: ignore[invalid-argument-type]
1046
1046
  create_using=nx.DiGraph,
1047
1047
  )
1048
1048
  try:
@@ -1221,12 +1221,12 @@ class Fault:
1221
1221
  return self.fault_coordinates_to_wgs_depth_coordinates(np.array([1 / 2, 1 / 2]))
1222
1222
 
1223
1223
  @property
1224
- def geometry(self) -> shapely.Polygon | shapely.LineString: # numpydoc ignore=RT01
1224
+ def geometry(self) -> shapely.Geometry: # numpydoc ignore=RT01
1225
1225
  """shapely.Polygon or LineString: A shapely geometry for the fault (projected onto the surface).
1226
1226
 
1227
1227
  Geometry will be LineString if `dip = 90`.
1228
1228
  """
1229
- return shapely.normalize( # type: ignore
1229
+ return shapely.normalize(
1230
1230
  shapely.union_all([plane.geometry for plane in self.planes])
1231
1231
  )
1232
1232
 
@@ -1376,8 +1376,8 @@ class Fault:
1376
1376
  float
1377
1377
  The Rjb distance (in metres) to the point.
1378
1378
  """
1379
- return self.geometry.distance(
1380
- shapely.Point(coordinates.wgs_depth_to_nztm(point))
1379
+ return shapely.distance(
1380
+ self.geometry, shapely.Point(coordinates.wgs_depth_to_nztm(point))
1381
1381
  )
1382
1382
 
1383
1383
  def rx_ry_distance(self, point: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
@@ -1512,15 +1512,14 @@ class CoordinateBounds(NamedTuple):
1512
1512
  """float: Maximum normalised dip coordinate, in the range of [0, 1]."""
1513
1513
 
1514
1514
 
1515
+ DEFAULT_BOUNDS = CoordinateBounds(min_strike=0, min_dip=0, max_strike=1, max_dip=1)
1516
+
1517
+
1515
1518
  def closest_point_between_sources(
1516
1519
  source_a: IsSource,
1517
1520
  source_b: IsSource,
1518
- source_a_coordinate_bounds: CoordinateBounds = CoordinateBounds(
1519
- min_strike=0, min_dip=0, max_strike=1, max_dip=1
1520
- ),
1521
- source_b_coordinate_bounds: CoordinateBounds = CoordinateBounds(
1522
- min_strike=0, min_dip=0, max_strike=1, max_dip=1
1523
- ),
1521
+ source_a_coordinate_bounds: CoordinateBounds = DEFAULT_BOUNDS,
1522
+ source_b_coordinate_bounds: CoordinateBounds = DEFAULT_BOUNDS,
1524
1523
  ) -> tuple[np.ndarray, np.ndarray]:
1525
1524
  """Find the closest point between two sources that have local coordinates.
1526
1525
 
@@ -12,8 +12,9 @@ References
12
12
  .. [0] https://wiki.canterbury.ac.nz/display/QuakeCore/File+Formats+Used+In+Ground+Motion+Simulation#FileFormatsUsedInGroundMotionSimulation-Stochformat
13
13
  """
14
14
 
15
+ import dataclasses
15
16
  from pathlib import Path
16
- from typing import NamedTuple, TextIO, TypeAlias, cast
17
+ from typing import IO, NamedTuple, Self, TextIO, cast
17
18
 
18
19
  import numpy as np
19
20
  from numpy.typing import NDArray
@@ -23,9 +24,9 @@ from source_modelling import parse_utils
23
24
  from source_modelling.sources import Plane
24
25
 
25
26
  # Type aliases for NumPy arrays with specific shapes and dtypes
26
- FloatArray2D: TypeAlias = NDArray[np.float32] # 2D array of float32
27
- LatLonArray: TypeAlias = NDArray[np.float64] # Array for latitude/longitude
28
- CoordinateArray: TypeAlias = NDArray[np.float64] # Array for coordinates
27
+ type FloatArray2D = NDArray[np.float32] # 2D array of float32
28
+ type LatLonArray = NDArray[np.float64] # Array for latitude/longitude
29
+ type CoordinateArray = NDArray[np.float64] # Array for coordinates
29
30
 
30
31
 
31
32
  class StochHeader(NamedTuple):
@@ -149,6 +150,7 @@ def _read_stoch_plane(handle: TextIO) -> StochPlane:
149
150
  return StochPlane(header, slip, rise, trup)
150
151
 
151
152
 
153
+ @dataclasses.dataclass
152
154
  class StochFile:
153
155
  """
154
156
  Class for reading and accessing stochastic slip model files.
@@ -156,30 +158,10 @@ class StochFile:
156
158
  This class handles the parsing of stochastic slip model files and provides
157
159
  access to the contained planes and their properties.
158
160
 
159
- Parameters
160
- ----------
161
- filename : Path
162
- Path to the stochastic slip model file.
163
-
164
- Attributes
165
- ----------
166
- data : list[StochPlane]
167
- Structured raw data read from the stoch file.
168
-
169
- Raises
170
- ------
171
- ValueError
172
- If the number of planes specified in the file is not a positive integer.
173
-
174
- Notes
175
- -----
176
- Stochastic slip model files contain information about fault planes and their
177
- properties such as slip, rise time, and rupture time.
178
-
179
161
  Examples
180
162
  --------
181
163
  >>> # Assuming 'stoch_model.stoch' exists with valid stochastic slip model data
182
- >>> stoch_file = StochFile('stoch_model.stoch')
164
+ >>> stoch_file = StochFile.from_file('stoch_model.stoch')
183
165
  >>> planes = stoch_file.planes
184
166
  >>> slips = stoch_file.slip
185
167
  >>> rise_times = stoch_file.rise
@@ -190,15 +172,23 @@ class StochFile:
190
172
  ... print(f"Slip array shape for the first plane: {slips[0].shape}")
191
173
  """
192
174
 
193
- def __init__(self, filename: Path):
175
+ data: list[StochPlane]
176
+
177
+ @classmethod
178
+ def from_file(cls, filename: Path) -> Self:
194
179
  """
195
- Initialize a StochFile instance by reading data from the specified file.
180
+ Initialise a StochFile instance by reading data from the specified file.
196
181
 
197
182
  Parameters
198
183
  ----------
199
184
  filename : Path
200
185
  Path to the stochastic slip model file.
201
186
 
187
+ Returns
188
+ -------
189
+ StochFile
190
+ The stoch file parsed from the filename.
191
+
202
192
  Raises
203
193
  ------
204
194
  ValueError
@@ -209,10 +199,36 @@ class StochFile:
209
199
  n_planes = parse_utils.read_int(handle, "n_planes")
210
200
  if n_planes <= 0:
211
201
  raise parse_utils.ParseError(
212
- f"Expected non-negative integer number of planes, received: {n_planes}."
202
+ f"Expected positive integer number of planes, received: {n_planes}."
213
203
  )
214
204
  planes = [_read_stoch_plane(handle) for _ in range(n_planes)]
215
- self.data: list[StochPlane] = planes
205
+ return cls(planes)
206
+
207
+ def dump(self, handle: IO[str]) -> None:
208
+ """Write a stoch file to a file-like object.
209
+
210
+ Parameters
211
+ ----------
212
+ handle : File-like object
213
+ The object to write the stoch file output to.
214
+ """
215
+ if not self.data:
216
+ raise ValueError("Cannot dump empty stoch file")
217
+ handle.write(f"{len(self.data)}\n")
218
+ for plane in self.data:
219
+ header = plane.header
220
+ handle.write(
221
+ f"{header.longitude:10.4f} {header.latitude:10.4f} {header.nx:5d} {header.ny:5d} {header.dx:8.2f} {header.dy:8.2f}\n"
222
+ )
223
+ handle.write(
224
+ f"{header.strike:4.0f} {header.dip:4.0f} {header.average_rake:4.0f} {header.dtop:8.2f} {header.shypo:8.2f} {header.dhypo:8.2f}\n"
225
+ )
226
+ np.savetxt(
227
+ handle,
228
+ np.vstack((plane.slip, plane.rise, plane.trup)),
229
+ fmt="%13.5e",
230
+ delimiter="",
231
+ )
216
232
 
217
233
  @property
218
234
  def planes(self) -> list[Plane]:
@@ -226,7 +242,7 @@ class StochFile:
226
242
 
227
243
  Examples
228
244
  --------
229
- >>> stoch_file = StochFile('stoch_model.stoch')
245
+ >>> stoch_file = StochFile.from_file('stoch_model.stoch')
230
246
  >>> planes = stoch_file.planes
231
247
  >>> print(f"Number of planes: {len(planes)}")
232
248
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: source-modelling
3
- Version: 2026.7.2
3
+ Version: 2026.7.3
4
4
  Summary: Source modelling library
5
5
  Author: ucgmsim
6
6
  Requires-Python: >=3.12
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2026.7.3",
3
+ "distance": 0,
4
+ "node": "gaa2e1ec59c0742728fc6a281bb004bc7a572be30",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-24"
8
+ }
@@ -53,6 +53,7 @@ fn read_srf_header(
53
53
  let ndip = scanner.next()?;
54
54
  let len = scanner.next()?;
55
55
  let wid = scanner.next()?;
56
+ scanner.expect_end_of_line()?;
56
57
  let stk = scanner.next()?;
57
58
  let dip = scanner.next()?;
58
59
  let dtop = scanner.next()?;
@@ -81,6 +82,7 @@ fn read_srf_header(
81
82
  const APPROX_BYTES_PER_SLIP_VALUE: usize = 12;
82
83
 
83
84
  // Tiny PointHeader used to construct actual points later.
85
+ #[derive(Debug)]
84
86
  struct PointHeader {
85
87
  lon: f32,
86
88
  lat: f32,
@@ -206,11 +208,6 @@ fn read_srf_points_v2(
206
208
  let mut slipt1 = CsrMatrix::new(point_count, slipt1_capacity);
207
209
 
208
210
  for (i, plane) in planes.iter().enumerate() {
209
- // In version 2.0 (and version 2.0 only), it is possible to construct SRFs
210
- // with multiple POINT instantiations. Technically V1.0 SRFs could be
211
- // constructed with multiple POINTS instantiations but we are deliberately
212
- // parsing a stricter subset of the format.
213
-
214
211
  scanner.skip_token(b"POINTS")?;
215
212
  let plane_point_count = scanner.next()?;
216
213
  if plane.points() != plane_point_count {
@@ -293,7 +290,8 @@ mod tests {
293
290
 
294
291
  const SRF_V1: &[u8] = b"1.0\n\
295
292
  PLANE 1\n\
296
- 0.0 0.0 2 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
293
+ 0.0 0.0 2 1 4.0 2.0\n\
294
+ 90.0 45.0 0.0 0.0 1.0\n\
297
295
  POINTS 2\n\
298
296
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1\n\
299
297
  30.0 1.5 3 0.0 0 0.0 0\n\
@@ -304,7 +302,8 @@ POINTS 2\n\
304
302
 
305
303
  const SRF_V2: &[u8] = b"2.0\n\
306
304
  PLANE 1\n\
307
- 0.0 0.0 2 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
305
+ 0.0 0.0 2 1 4.0 2.0\n\
306
+ 90.0 45.0 0.0 0.0 1.0\n\
308
307
  POINTS 2\n\
309
308
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1 3.5 2.7\n\
310
309
  30.0 1.5 3 0.0 0 0.0 0\n\
@@ -347,8 +346,10 @@ POINTS 2\n\
347
346
  // Two 1x1 planes, each with its own POINTS block.
348
347
  const SRF_V2_TWO_PLANES: &[u8] = b"2.0\n\
349
348
  PLANE 2\n\
350
- 0.0 0.0 1 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
351
- 0.5 0.5 1 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
349
+ 0.0 0.0 1 1 4.0 2.0\n\
350
+ 90.0 45.0 0.0 0.0 1.0\n\
351
+ 0.5 0.5 1 1 4.0 2.0\n\
352
+ 90.0 45.0 0.0 0.0 1.0\n\
352
353
  POINTS 1\n\
353
354
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1 3.5 2.7\n\
354
355
  30.0 1.5 2 0.0 0 0.0 0\n\
@@ -381,6 +382,34 @@ POINTS 1\n\
381
382
  assert_eq!(srf.slipt1.data, vec![0.1, 0.2, 0.4]);
382
383
  }
383
384
 
385
+ const SRF_V1_TWO_PLANES: &[u8] = b"1.0\n\
386
+ PLANE 2\n\
387
+ 0.0 0.0 1 1 4.0 2.0\n\
388
+ 90.0 45.0 0.0 0.0 1.0\n\
389
+ 0.5 0.5 1 1 4.0 2.0\n\
390
+ 90.0 45.0 0.0 0.0 1.0\n\
391
+ POINTS 2\n\
392
+ 0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1\n\
393
+ 30.0 1.5 2 0.0 0 0.0 0\n\
394
+ 0.1 0.2\n\
395
+ 0.2 -43.1 5.5 90.0 45.0 1.0e10 0.6 0.1\n\
396
+ 45.0 2.0 1 0.0 0 0.0 0\n\
397
+ 0.4\n";
398
+
399
+ #[test]
400
+ fn parses_v1_with_multiple_planes() {
401
+ let mut scanner = scanner::Scanner::new(SRF_V1_TWO_PLANES);
402
+ let srf = read_srf_struct(&mut scanner).unwrap();
403
+ assert_eq!(srf.planes.len(), 2);
404
+ let metadata = match &srf.metadata {
405
+ SrfMetadataVersioned::V1(metadata) => metadata,
406
+ SrfMetadataVersioned::V2(_) => panic!("expected V1 metadata"),
407
+ };
408
+ assert_eq!(metadata.lon, vec![0.1, 0.2]);
409
+ assert_eq!(srf.slipt1.row_ptr, vec![0, 2, 3]);
410
+ assert_eq!(srf.slipt1.data, vec![0.1, 0.2, 0.4]);
411
+ }
412
+
384
413
  #[test]
385
414
  fn rejects_v1_point_count_mismatch() {
386
415
  // Plane declares 2x1 points but POINTS declares 3.
@@ -460,25 +489,16 @@ POINTS 1\n\
460
489
  assert!(read_srf_struct(&mut scanner).is_ok());
461
490
  }
462
491
 
492
+ const SRF_BAD_PLANE_HEADER: &[u8] = b"0.0 0.0 2 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0";
493
+
463
494
  #[test]
464
- fn mismatch_messages_read_correctly() {
465
- let err = SrfParseError::PointCountMismatch {
466
- declared: 3,
467
- expected: 2,
468
- };
469
- assert_eq!(
470
- err.to_string(),
471
- "PLANE headers expect 2 total points but POINTS declares 3"
472
- );
473
- let err = SrfParseError::PlanePointCountMismatch {
474
- plane: 1,
475
- declared: 2,
476
- expected: 1,
477
- };
478
- assert_eq!(
479
- err.to_string(),
480
- "plane 1 expects 1 points but its POINTS block declares 2"
481
- );
495
+ fn plane_header_rejects_no_newline() {
496
+ let mut scanner = scanner::Scanner::new(SRF_BAD_PLANE_HEADER);
497
+ let err = read_srf_header(&mut scanner, 1).unwrap_err();
498
+ assert!(matches!(
499
+ err,
500
+ SrfParseError::Scanner(scanner::ScannerError::NoNewlineFound { .. })
501
+ ));
482
502
  }
483
503
 
484
504
  fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
@@ -113,18 +113,13 @@ fn write_plane_header<W: Write>(writer: &mut W, planes: &[SrfPlane]) -> Result<(
113
113
  for plane in planes {
114
114
  writeln!(
115
115
  writer,
116
- "{} {} {} {} {} {} {} {} {} {} {}",
117
- plane.elon,
118
- plane.elat,
119
- plane.nstk,
120
- plane.ndip,
121
- plane.len,
122
- plane.wid,
123
- plane.stk,
124
- plane.dip,
125
- plane.dtop,
126
- plane.shyp,
127
- plane.dhyp
116
+ "{} {} {} {} {} {}",
117
+ plane.elon, plane.elat, plane.nstk, plane.ndip, plane.len, plane.wid,
118
+ )?;
119
+ writeln!(
120
+ writer,
121
+ "{} {} {} {} {}",
122
+ plane.stk, plane.dip, plane.dtop, plane.shyp, plane.dhyp
128
123
  )?;
129
124
  }
130
125
  Ok(())
@@ -165,7 +160,8 @@ mod tests {
165
160
 
166
161
  const SRF_V1: &[u8] = b"1.0\n\
167
162
  PLANE 1\n\
168
- 0.0 0.0 2 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
163
+ 0.0 0.0 2 1 4.0 2.0\n\
164
+ 90.0 45.0 0.0 0.0 1.0\n\
169
165
  POINTS 2\n\
170
166
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1\n\
171
167
  30.0 1.5 3 0.0 0 0.0 0\n\
@@ -176,8 +172,10 @@ POINTS 2\n\
176
172
 
177
173
  const SRF_V2_TWO_PLANES: &[u8] = b"2.0\n\
178
174
  PLANE 2\n\
179
- 0.0 0.0 1 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
180
- 0.5 0.5 1 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
175
+ 0.0 0.0 1 1 4.0 2.0\n\
176
+ 90.0 45.0 0.0 0.0 1.0\n\
177
+ 0.5 0.5 1 1 4.0 2.0\n\
178
+ 90.0 45.0 0.0 0.0 1.0\n\
181
179
  POINTS 1\n\
182
180
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1 3.5 2.7\n\
183
181
  30.0 1.5 2 0.0 0 0.0 0\n\
@@ -275,7 +273,8 @@ POINTS 1\n\
275
273
  fn empty_slip_row_roundtrips() {
276
274
  let data = b"1.0\n\
277
275
  PLANE 1\n\
278
- 0.0 0.0 1 1 4.0 2.0 90.0 45.0 0.0 0.0 1.0\n\
276
+ 0.0 0.0 1 1 4.0 2.0\n\
277
+ 90.0 45.0 0.0 0.0 1.0\n\
279
278
  POINTS 1\n\
280
279
  0.1 -43.0 5.0 90.0 45.0 1.0e10 0.5 0.1\n\
281
280
  30.0 1.5 0 0.0 0 0.0 0\n";