1"""Grids, masks, designs and presets."""
3import numpy as np
4import pytest
6import cymsim
7from cymsim import Design, DesignError, Plate, make_grid, presets
8from cymsim.plate import remove_plane
10from conftest import STEEL
13def test_grid_spacing_and_weights():
14 g = make_grid(0.3, 0.2, 0.01)
15 assert (g.nx, g.ny) == (31, 21)
16 assert abs(g.hx - 0.01) < 1e-12
17 w = g.node_weight
18 assert w[5, 5] == 1 and w[0, 5] == 0.5 and w[0, 0] == 0.25
19 assert abs(g.area - 0.3 * 0.2) < 1e-12
20 assert abs(g.x[0] + 0.15) < 1e-12 and abs(g.y[-1] - 0.1) < 1e-12
23def test_disc_with_hole_mask_by_cell_centers():
24 R, r0 = 0.1, 0.02
25 g = make_grid(2 * R, 2 * R, 0.01, lambda x, y, r, t: (r <= R) & (r >= r0))
26 # The area of the staircase plate is unbiased to within a cell's worth.
27 assert abs(g.area / (np.pi * (R**2 - r0**2)) - 1) < 0.03
28 assert not g.mask[g.ny // 2, g.nx // 2], "the hole is empty"
29 assert not g.mask[0, 0], "the corner is outside the disc"
30 # Every node in the plate has a cell of its own.
31 assert (g.node_weight[g.mask] > 0).all()
32 assert (g.node_weight[~g.mask] == 0).all()
35def test_remove_plane_and_profile_stats():
36 g = make_grid(0.2, 0.1, 0.01)
37 f = 3.0 + 2.0 * g.X - 5.0 * g.Y + 1e-3 * g.X**2
38 r = remove_plane(f, g)
39 # The plane is gone: the weighted fit of the residual is zero to rounding.
40 w = g.node_weight
41 assert abs((w * r).sum()) < 1e-12 and abs((w * r * g.X).sum()) < 1e-12 and abs((w * r * g.Y).sum()) < 1e-12
42 pl = Plate.uniform(g, STEEL, 1e-3, profile=f)
43 p2p, slope = pl.profile_stats
44 assert p2p < 1e-3 * 0.2**2 and slope < 1e-3 * 0.2
47def test_design_evaluate_checks():
48 d = Design("bad", STEEL, 0.1, 0.1, lambda x, y, r, t: x) # negative on half the plate
49 with pytest.raises(DesignError, match="positive"):
50 d.evaluate(0.01)
51 d = Design("empty", STEEL, 0.1, 0.1, 1e-3, inside=lambda x, y, r, t: r < 0)
52 with pytest.raises(DesignError, match="no grid cells"):
53 d.evaluate(0.01)
54 d = Design("shape", STEEL, 0.1, 0.1, lambda x, y, r, t: np.ones(3))
55 with pytest.raises(DesignError, match="shape"):
56 d.evaluate(0.01)
57 with pytest.raises(DesignError):
58 Design("thin", STEEL, 0.1, 0.1, 0.0)
61def test_every_preset_evaluates():
62 for name, fn in presets.PRESETS.items():
63 pl = fn().evaluate(0.012)
64 assert pl.grid.count > 20, name
65 assert pl.mass > 0.05, name
66 assert np.isfinite(pl.w0).all() and (pl.w0[~pl.grid.mask] == 0).all()
67 assert isinstance(pl.summary(), str)
68 crash = presets.preset("crash-16").evaluate(0.008)
69 assert crash.is_flat and abs(crash.thickness_range[0] - 0.7e-3) < 1e-9 and abs(crash.thickness_range[1] - 1.2e-3) < 1e-9
70 bell = presets.preset("crash-bell-16").evaluate(0.008)
71 assert not bell.is_flat and bell.profile_stats[1] > 0.1
72 with pytest.raises(KeyError):
73 presets.preset("no-such-preset")
76def test_hammered_pattern_is_reproducible_and_asymmetric():
77 a = presets.crash_hammered_16(seed=1).evaluate(0.012)
78 b = presets.crash_hammered_16(seed=1).evaluate(0.012)
79 c = presets.crash_hammered_16(seed=2).evaluate(0.012)
80 assert np.array_equal(a.w0, b.w0)
81 assert not np.allclose(a.w0, c.w0)
82 assert not np.allclose(a.w0, a.w0[::-1, :]), "the dents break the symmetry"
85def test_strike_patch_and_listener_terms():
86 pl = presets.flat_disc().evaluate(0.01)
87 patch = pl.strike_patch(cymsim.Strike(x=0.9, y=0.5, radius=5e-3))
88 assert patch.mask.sum() >= 1 and patch.mask[patch.nearest]
89 assert abs(patch.cy) < 0.01
90 weight, delay = pl.listener_terms(cymsim.Listener(distance=1.0, angle=0))
91 sel = weight > 0
92 assert (delay[sel] >= 1.0 / 343).all()
93 # On axis the weights sum to about rho A / (2 pi r).
94 assert abs(weight.sum() / (1.2 * pl.area / (2 * np.pi)) - 1) < 0.02