midas-plotting 0.3.2__tar.gz → 0.4.0__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 (23) hide show
  1. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/PKG-INFO +2 -2
  2. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/README.md +1 -1
  3. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/__init__.py +1 -1
  4. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/ff.py +11 -3
  5. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/grains.py +40 -12
  6. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/PKG-INFO +2 -2
  7. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/pyproject.toml +1 -1
  8. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/tests/test_ff.py +119 -13
  9. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/cli.py +0 -0
  10. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/ipf.py +0 -0
  11. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/laue.py +0 -0
  12. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/maps.py +0 -0
  13. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/mic.py +0 -0
  14. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting/solutions.py +0 -0
  15. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/SOURCES.txt +0 -0
  16. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/dependency_links.txt +0 -0
  17. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/entry_points.txt +0 -0
  18. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/requires.txt +0 -0
  19. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/midas_plotting.egg-info/top_level.txt +0 -0
  20. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/setup.cfg +0 -0
  21. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/tests/test_ipf.py +0 -0
  22. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/tests/test_laue.py +0 -0
  23. {midas_plotting-0.3.2 → midas_plotting-0.4.0}/tests/test_mic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: midas-plotting
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels.
5
5
  Author-email: Hemant Sharma <hsharma@anl.gov>
6
6
  License: BSD-3-Clause
@@ -88,7 +88,7 @@ Things the module will not let you get wrong:
88
88
  * **Symmetry comes from the file.** `Grains.csv` states its space group in the
89
89
  preamble; the plots use it. Defaulting to cubic would colour a hexagonal
90
90
  sample with the wrong IPF triangle and produce a plausible, wrong figure.
91
- * **Columns are read by name.** `Grains.csv` has 47 columns and
91
+ * **Columns are read by name.** `Grains.csv` has 53 columns (47 before 2026-08-21) and
92
92
  `midas-fit-grain` 0.5.6 shipped a cyclic rotation of three of them; a
93
93
  positional reader inherits that silently.
94
94
  * **Euler angles are cross-checked against `O11..O33`.** They describe the same
@@ -67,7 +67,7 @@ Things the module will not let you get wrong:
67
67
  * **Symmetry comes from the file.** `Grains.csv` states its space group in the
68
68
  preamble; the plots use it. Defaulting to cubic would colour a hexagonal
69
69
  sample with the wrong IPF triangle and produce a plausible, wrong figure.
70
- * **Columns are read by name.** `Grains.csv` has 47 columns and
70
+ * **Columns are read by name.** `Grains.csv` has 53 columns (47 before 2026-08-21) and
71
71
  `midas-fit-grain` 0.5.6 shipped a cyclic rotation of three of them; a
72
72
  positional reader inherits that silently.
73
73
  * **Euler angles are cross-checked against `O11..O33`.** They describe the same
@@ -48,7 +48,7 @@ from .solutions import (
48
48
  )
49
49
  from .mic import MicMap, read_mic
50
50
 
51
- __version__ = "0.3.2"
51
+ __version__ = "0.4.0"
52
52
  __all__ = [
53
53
  "MicMap", "read_mic", "GrainList", "read_grains", "ff", "laue",
54
54
  "LaueSolutions", "LaueSpots", "read_solutions", "read_spots",
@@ -318,9 +318,17 @@ def completeness_hist(grains, ax=None, *, bins: int = 30,
318
318
 
319
319
  # ─── strain ─────────────────────────────────────────────────────────────────
320
320
  def strain_scalar(grains, kind: str = "hydrostatic", *,
321
- convention: str = "fab") -> np.ndarray:
321
+ convention: str = "ken") -> np.ndarray:
322
322
  """Reduce the per-grain strain tensor to one number per grain.
323
323
 
324
+ ``convention`` defaults to **eKen**, matching
325
+ ``midas_stress.io.read_grains_csv``'s ``strain`` key and its documented
326
+ recommendation, so a strain map from this module and a stress computed by
327
+ ``midas_stress`` from the same Grains.csv are the same tensor. It used to
328
+ default to ``"fab"``; both sites labelled their choice, so nothing was
329
+ mis-read, but the two answers silently differed. Pass ``convention="fab"``
330
+ for the lattice-parameter form.
331
+
324
332
  ``kind``:
325
333
  ``hydrostatic`` trace/3
326
334
  ``vonmises`` von Mises equivalent of the deviatoric part
@@ -352,7 +360,7 @@ def strain_scalar(grains, kind: str = "hydrostatic", *,
352
360
 
353
361
 
354
362
  def strain_map(grains, ax=None, *, kind: str = "hydrostatic",
355
- convention: str = "fab", plane: str = "xy", cmin: float = 0.0,
363
+ convention: str = "ken", plane: str = "xy", cmin: float = 0.0,
356
364
  cmap: str = "coolwarm", vmin=None, vmax=None,
357
365
  symmetric: bool = True, title: Optional[str] = None):
358
366
  """Grain map coloured by a strain scalar (µε).
@@ -392,7 +400,7 @@ def strain_map(grains, ax=None, *, kind: str = "hydrostatic",
392
400
  return ax
393
401
 
394
402
 
395
- def strain_distribution(grains, ax=None, *, convention: str = "fab",
403
+ def strain_distribution(grains, ax=None, *, convention: str = "ken",
396
404
  bins: int = 30, title: Optional[str] = None):
397
405
  """Histograms of the three normal strain components (µε)."""
398
406
  import matplotlib.pyplot as plt
@@ -4,11 +4,16 @@ The FF analogue of :mod:`midas_plotting.mic`: one place that knows the column
4
4
  layout so analysis scripts stop re-deriving it.
5
5
 
6
6
  Columns are looked up **by name** from the ``%ID ...`` header line, never by
7
- position. That matters more here than it looks: ``Grains.csv`` has grown to 47
8
- columns, and `midas-fit-grain` 0.5.6 shipped a cyclic rotation of the
9
- ``DiffPos`` / ``DiffOme`` / ``DiffAngle`` columns (fixed in 0.5.7) that a
10
- positional reader would silently inherit -- one grain's ω residual read 223.87°
11
- where the true value was 0.054°.
7
+ position. That matters more here than it looks: ``Grains.csv`` has been widened
8
+ repeatedly (19 -> 21 -> 23 -> 47 -> 53 columns), and `midas-fit-grain` 0.5.6
9
+ shipped a cyclic rotation of the ``DiffPos`` / ``DiffOme`` / ``DiffAngle``
10
+ columns (fixed in 0.5.7) that a positional reader would silently inherit --
11
+ one grain's ω residual read 223.87° where the true value was 0.054°.
12
+
13
+ Rows are split on **whitespace**, not on tabs: the C ``ProcessGrains`` writer
14
+ ends every data row with a tab, so a bare ``split("\t")`` produces a trailing
15
+ empty field and ``float("")`` raises on files that are otherwise perfectly
16
+ valid.
12
17
 
13
18
  As a further guard, :func:`read_grains` recomputes the orientation matrix from
14
19
  the Euler angles and compares it against the ``O11..O33`` columns. Both describe
@@ -125,8 +130,20 @@ class GrainList:
125
130
  def n_grains(self) -> int:
126
131
  return len(self)
127
132
 
128
- def strain(self, convention: str = "fab") -> np.ndarray:
129
- """``(N, 3, 3)`` strain tensor in the requested convention."""
133
+ def strain(self, convention: str = "ken") -> np.ndarray:
134
+ """``(N, 3, 3)`` strain tensor in the requested convention.
135
+
136
+ Defaults to **eKen** to agree with ``midas_stress.io.read_grains_csv``,
137
+ whose plain ``strain`` key is eKen and which documents it as the
138
+ recommended form (it is fit directly from the per-reflection
139
+ ``eps_ij g_i g_j = (d_obs - d_0)/d_0`` gauge equation, so it is tied to
140
+ the raw observables and has better noise properties than the
141
+ lattice-parameter form). These two packages are routinely pointed at
142
+ the same Grains.csv -- a strain map here and a stress calculation
143
+ there -- and while both label their choice, disagreeing on the default
144
+ meant the map and the stress were not the same tensor. Pass
145
+ ``convention="fab"`` for the lattice-parameter (Fable-Beaudoin) form.
146
+ """
130
147
  c = convention.lower()
131
148
  if c in ("fab", "fable", "efab"):
132
149
  s = self.strain_fab
@@ -163,11 +180,22 @@ def _parse(path: Path):
163
180
  if not line.strip():
164
181
  continue
165
182
  if not line.startswith("%"):
166
- rows.append(line.split("\t"))
183
+ # Whitespace-split, NOT `split("\t")`. The C `ProcessGrains`
184
+ # terminates every data row with a tab, so a bare tab-split yields
185
+ # a spurious empty final field and `float("")` raises -- which is
186
+ # exactly what every file under midas_stress/dev/paper3/runs/ and
187
+ # the bundled GrainsSim.csv did. Splitting on whitespace also
188
+ # accepts the space-separated flavours (NF mic2grains seed files).
189
+ # No value in this format ever contains an internal space, and a
190
+ # collapsed empty field would be caught by the column-count check
191
+ # in read_grains() below.
192
+ rows.append(line.split())
167
193
  continue
168
194
 
169
195
  body = line[1:]
170
- fields = [f.strip() for f in body.split("\t")]
196
+ # Same rule for the header, so a space-separated file's column names
197
+ # are found too. Column names never contain spaces.
198
+ fields = body.split()
171
199
  # Column header: names O11 (and therefore the whole grain record).
172
200
  if "O11" in fields or (fields and fields[0] in ("ID", "GrainID")
173
201
  and len(fields) > 5):
@@ -204,7 +232,7 @@ def read_grains(path, *, check_orientation: bool = True) -> GrainList:
204
232
  if columns is None:
205
233
  raise ValueError(
206
234
  f"{path}: no column header found. Expected a line beginning "
207
- "'%ID' or '%GrainID' listing tab-separated column names.")
235
+ "'%ID' or '%GrainID' listing the column names.")
208
236
  if not rows:
209
237
  raise ValueError(f"{path}: header present but no grain rows.")
210
238
 
@@ -213,8 +241,8 @@ def read_grains(path, *, check_orientation: bool = True) -> GrainList:
213
241
  if arr.shape[1] != len(columns):
214
242
  raise ValueError(
215
243
  f"{path}: {arr.shape[1]} data columns but {len(columns)} header "
216
- "names -- the file is malformed or tab/space separated "
217
- "inconsistently.")
244
+ "names -- the file is malformed (an empty field mid-row, or a "
245
+ "header that does not match the rows).")
218
246
 
219
247
  def col(name, required=True):
220
248
  if name not in idx:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: midas-plotting
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels.
5
5
  Author-email: Hemant Sharma <hsharma@anl.gov>
6
6
  License: BSD-3-Clause
@@ -88,7 +88,7 @@ Things the module will not let you get wrong:
88
88
  * **Symmetry comes from the file.** `Grains.csv` states its space group in the
89
89
  preamble; the plots use it. Defaulting to cubic would colour a hexagonal
90
90
  sample with the wrong IPF triangle and produce a plausible, wrong figure.
91
- * **Columns are read by name.** `Grains.csv` has 47 columns and
91
+ * **Columns are read by name.** `Grains.csv` has 53 columns (47 before 2026-08-21) and
92
92
  `midas-fit-grain` 0.5.6 shipped a cyclic rotation of three of them; a
93
93
  positional reader inherits that silently.
94
94
  * **Euler angles are cross-checked against `O11..O33`.** They describe the same
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "midas-plotting"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels."
9
9
  readme = "README.md"
10
10
  license = { text = "BSD-3-Clause" }
@@ -13,16 +13,38 @@ from midas_plotting.ipf import ( # noqa: E402
13
13
  direction_rgb, ipf_rgb, ipf_rgb_from_matrix,
14
14
  )
15
15
 
16
- _COLS = (["ID"] + [f"O{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
17
- + ["X", "Y", "Z", "a", "b", "c", "alpha", "beta", "gamma",
18
- "DiffPos", "DiffOme", "DiffAngle", "GrainRadius", "Confidence"]
19
- + [f"eFab{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
20
- + [f"eKen{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
21
- + ["RMSErrorStrain", "PhaseNr", "Eul0", "Eul1", "Eul2"])
22
-
23
-
24
- def _write_grains(tmp_path, n=4, sg=225, break_orientation=False):
25
- """A Grains.csv with the real preamble shape, incl. tab-indented phase info."""
16
+ # The CURRENT width is 53: the six DiffPos/Ome/Angle Pre/Post diagnostics were
17
+ # appended on 2026-08-21. The fixture used to stop at 47, which is why nothing
18
+ # here noticed the widening.
19
+ _DATA_COLS = ([f"O{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
20
+ + ["X", "Y", "Z", "a", "b", "c", "alpha", "beta", "gamma",
21
+ "DiffPos", "DiffOme", "DiffAngle",
22
+ "GrainRadius", "Confidence"]
23
+ + [f"eFab{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
24
+ + [f"eKen{i}{j}" for i in (1, 2, 3) for j in (1, 2, 3)]
25
+ + ["RMSErrorStrain", "PhaseNr", "Eul0", "Eul1", "Eul2",
26
+ "DiffPosPre", "DiffOmePre", "DiffAnglePre",
27
+ "DiffPosPost", "DiffOmePost", "DiffAnglePost"])
28
+ _COLS = ["ID"] + _DATA_COLS
29
+
30
+
31
+ def _write_grains(tmp_path, n=4, sg=225, break_orientation=False,
32
+ *, trailing_tab=False, id_token="ID", sep="\t"):
33
+ """A Grains.csv with the real preamble shape, incl. tab-indented phase info.
34
+
35
+ Parameters
36
+ ----------
37
+ trailing_tab : bool
38
+ Terminate every data row with a tab, which is what the C
39
+ ``ProcessGrains`` actually writes. A reader that does ``split("\t")``
40
+ then sees a spurious empty final field and ``float("")`` raises. Every
41
+ real file under ``midas_stress/dev/paper3/runs/*/data/*MPa/`` and the
42
+ bundled ``GrainsSim.csv`` is in this form, and this fixture's omission
43
+ of it is why the crash shipped.
44
+ id_token : "ID" or "GrainID"
45
+ Both spellings are written by live MIDAS writers.
46
+ sep : column separator for the header and data rows.
47
+ """
26
48
  from midas_stress.orientation import euler_to_orient_mat_batch
27
49
 
28
50
  rng = np.random.default_rng(0)
@@ -38,7 +60,7 @@ def _write_grains(tmp_path, n=4, sg=225, break_orientation=False):
38
60
  f"%\tSpaceGroup:{sg}",
39
61
  "%\tLattice Parameter:4.078200\t4.078200\t4.078200\t"
40
62
  "90.000000\t90.000000\t90.000000",
41
- "%" + "\t".join(_COLS),
63
+ "%" + sep.join([id_token] + _DATA_COLS),
42
64
  ]
43
65
  for k in range(n):
44
66
  row = [str(k + 1)] + [f"{v:.6f}" for v in om[k].reshape(-1)]
@@ -50,7 +72,9 @@ def _write_grains(tmp_path, n=4, sg=225, break_orientation=False):
50
72
  row += [f"{v:.4f}" for v in rng.normal(200, 50, 9)] # eKen
51
73
  row += ["400.0", "1"]
52
74
  row += [f"{v:.9f}" for v in eul[k]]
53
- lines.append("\t".join(row))
75
+ row += ["151.0", "0.06", "0.09", "150.0", "0.05", "0.08"] # pre/post
76
+ assert len(row) == len(_COLS)
77
+ lines.append(sep.join(row) + ("\t" if trailing_tab else ""))
54
78
  p = tmp_path / "Grains.csv"
55
79
  p.write_text("\n".join(lines) + "\n")
56
80
  return p, eul, om
@@ -111,6 +135,51 @@ def test_orientation_check_can_be_disabled(tmp_path):
111
135
  read_grains(p, check_orientation=False)
112
136
 
113
137
 
138
+ @pytest.mark.parametrize("trailing_tab", [False, True])
139
+ @pytest.mark.parametrize("id_token", ["ID", "GrainID"])
140
+ def test_trailing_tab_and_both_id_spellings(tmp_path, trailing_tab, id_token):
141
+ """The shipped crash: `line.split("\t")` on a tab-terminated row.
142
+
143
+ The C ``ProcessGrains`` ends every data row with a tab, so a tab-split
144
+ yields one extra empty field and ``float("")`` raised
145
+ ``could not convert string to float: ''`` on every real file under
146
+ ``midas_stress/dev/paper3/runs/*/data/*MPa/Grains.csv``.
147
+ """
148
+ p, eul, om = _write_grains(tmp_path, n=4, trailing_tab=trailing_tab,
149
+ id_token=id_token)
150
+ g = read_grains(p)
151
+ assert len(g) == 4
152
+ assert len(g.columns) == 53
153
+ np.testing.assert_array_equal(g.ids, [1, 2, 3, 4])
154
+ assert np.allclose(g.euler, eul, atol=1e-6)
155
+ assert np.allclose(g.orient_mat, om, atol=1e-5)
156
+ assert np.allclose(g.radius, [40.0, 50.0, 60.0, 70.0])
157
+
158
+
159
+ def test_space_separated_file_is_readable(tmp_path):
160
+ """NF ``mic2grains`` seed files are space-separated, not tab-separated."""
161
+ p, eul, _ = _write_grains(tmp_path, n=3, sep=" ")
162
+ g = read_grains(p)
163
+ assert len(g) == 3 and len(g.columns) == 53
164
+ assert np.allclose(g.euler, eul, atol=1e-6)
165
+
166
+
167
+ def test_current_width_exposes_pre_post_residual_columns(tmp_path):
168
+ """The 2026-08-21 widening: 47 -> 53. The extra columns must be readable
169
+ by name, and must not displace anything that was already there."""
170
+ p, _, _ = _write_grains(tmp_path, n=2)
171
+ g = read_grains(p)
172
+ idx = {c: i for i, c in enumerate(g.columns)}
173
+ for c in ("DiffPosPre", "DiffOmePre", "DiffAnglePre",
174
+ "DiffPosPost", "DiffOmePost", "DiffAnglePost"):
175
+ assert c in idx
176
+ np.testing.assert_allclose(g.raw[:, idx["DiffPosPre"]], 151.0)
177
+ np.testing.assert_allclose(g.raw[:, idx["DiffPosPost"]], 150.0)
178
+ # ...and the pre-existing block is untouched.
179
+ np.testing.assert_allclose(g.diff_pos, 150.0)
180
+ np.testing.assert_allclose(g.diff_ome, 0.05)
181
+
182
+
114
183
  def test_missing_header_is_an_error(tmp_path):
115
184
  p = tmp_path / "bad.csv"
116
185
  p.write_text("1\t2\t3\n")
@@ -127,7 +196,7 @@ def test_strain_is_not_rescaled(tmp_path):
127
196
  p, _, _ = _write_grains(tmp_path, n=4)
128
197
  g = read_grains(p)
129
198
  raw11 = g.strain_fab[:, 0, 0]
130
- assert np.allclose(ff.strain_scalar(g, "11"), raw11)
199
+ assert np.allclose(ff.strain_scalar(g, "11", convention="fab"), raw11)
131
200
  assert np.abs(ff.strain_scalar(g, "hydrostatic")).max() < 1e4
132
201
 
133
202
 
@@ -135,6 +204,9 @@ def test_strain_scalars(tmp_path):
135
204
  p, _, _ = _write_grains(tmp_path, n=4)
136
205
  g = read_grains(p)
137
206
  e = g.strain("fab")
207
+ assert np.allclose(ff.strain_scalar(g, "hydrostatic", convention="fab"),
208
+ np.trace(e, axis1=1, axis2=2) / 3.0)
209
+ e = g.strain() # default convention, checked below
138
210
  assert np.allclose(ff.strain_scalar(g, "hydrostatic"),
139
211
  np.trace(e, axis1=1, axis2=2) / 3.0)
140
212
  assert (ff.strain_scalar(g, "vonmises") >= 0).all()
@@ -246,3 +318,37 @@ def test_ff_detected_by_content_not_filename(tmp_path):
246
318
  other.write_text("1 2 3 4 5\n")
247
319
  assert not _looks_like_ff(other)
248
320
  assert not _looks_like_ff(tmp_path / "missing.csv")
321
+
322
+
323
+ # ── strain convention default (regression) ──────────────────────────────────
324
+ def test_strain_default_convention_is_eken(tmp_path):
325
+ """The default must be eKen, matching ``midas_stress.io``'s ``strain`` key.
326
+
327
+ midas_plotting defaulted to eFab while midas_stress.io mapped its plain
328
+ ``strain`` key to eKen and documented eKen as the recommendation. Both
329
+ sites labelled their choice, so nothing raised and nothing was mislabelled
330
+ -- but a strain map from here and a stress from there, on the SAME
331
+ Grains.csv, were different tensors.
332
+ """
333
+ p, _, _ = _write_grains(tmp_path, n=6)
334
+ g = read_grains(p)
335
+ # The fixture draws eFab and eKen independently, so a wrong default shows.
336
+ assert not np.allclose(g.strain_fab, g.strain_ken)
337
+ np.testing.assert_allclose(g.strain(), g.strain_ken)
338
+ for kind in ("hydrostatic", "vonmises", "11", "23"):
339
+ np.testing.assert_allclose(ff.strain_scalar(g, kind),
340
+ ff.strain_scalar(g, kind, convention="ken"))
341
+
342
+
343
+ def test_strain_default_agrees_with_midas_stress(tmp_path):
344
+ """Same file, same tensor, across the two packages.
345
+
346
+ midas_stress returns dimensionless strain (it divides the stored
347
+ microstrain by 1e6); midas_plotting keeps microstrain. That factor is the
348
+ ONLY difference the two defaults may have.
349
+ """
350
+ ms_io = pytest.importorskip("midas_stress.io")
351
+ p, _, _ = _write_grains(tmp_path, n=6)
352
+ g = read_grains(p)
353
+ ms = ms_io.read_grains_csv(str(p))
354
+ np.testing.assert_allclose(g.strain(), ms["strain"] * 1e6, rtol=1e-9)
File without changes