sfm/compliance.py renders the velocity-vs-frequency blasting compliance chart Blastware draws on its Event Report: - limit_at()/limit_curve() — the RI8507 Fig B-1 / 30 CFR 816.67 curve as data (Drywall 0.75 + plaster 0.50 lines): 0.030in low-freq bound, plateau, 0.008in rising diagonal to a 2.0 in/s cap at ~40 Hz, drawn continuous. - channel_compliance_points() — the per-cycle (freq, peak-velocity) scatter by the zero-crossing method (matches Blastware; cloud ceiling = channel PPV). - draw_compliance_chart() — matplotlib rendering (both lines + scatter, BW tick scales + channel markers). Verified against 7 BE12844 Blastware reports. docs/ri8507_compliance_curve.md captures the curve construction, the SHM basis, and the scatter method. Not yet wired into report_pdf.py — that placeholder is the next step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
36 lines
1.2 KiB
Python
36 lines
1.2 KiB
Python
"""USBM/OSMRE compliance curve + scatter logic (sfm.compliance).
|
||
Rendering is verified visually against Blastware reports."""
|
||
import math
|
||
|
||
import numpy as np
|
||
|
||
from sfm.compliance import limit_at, channel_compliance_points
|
||
|
||
|
||
def test_osmre_velocity_segments():
|
||
assert abs(limit_at(6.0) - 0.75) < 1e-9 # 3.5–12 Hz flat
|
||
assert abs(limit_at(50.0) - 2.00) < 1e-9 # 30–100 Hz flat
|
||
|
||
|
||
def test_displacement_segments():
|
||
assert abs(limit_at(2.0) - 2 * math.pi * 2.0 * 0.030) < 1e-9 # low-freq 0.030 in
|
||
assert abs(limit_at(20.0) - 2 * math.pi * 20.0 * 0.008) < 1e-9 # rising diagonal 0.008 in
|
||
|
||
|
||
def test_limit_clamps_below_1hz():
|
||
assert limit_at(0.1) == limit_at(1.0)
|
||
|
||
|
||
def test_scatter_ceiling_is_ppv_at_dominant_freq():
|
||
# ~27 Hz blast-like trace whose energy peaks mid-record (inside full cycles,
|
||
# as a real event does): the scatter cloud's ceiling is the trace PPV and the
|
||
# top point sits near the dominant frequency.
|
||
sps, n = 1024.0, 3328
|
||
t = np.arange(n) / sps
|
||
env = np.exp(-((t - 1.5) ** 2) / (2 * 0.3 ** 2))
|
||
x = 0.9 * env * np.sin(2 * np.pi * 27.0 * t)
|
||
f, v = channel_compliance_points(x, sps)
|
||
assert len(f) > 20
|
||
assert v.max() >= 0.99 * np.abs(x).max()
|
||
assert 20.0 < f[int(np.argmax(v))] < 35.0
|