sadickam commited on
Commit
604c2a8
Β·
verified Β·
1 Parent(s): cdfca12

Update helpers.py

Browse files
Files changed (1) hide show
  1. helpers.py +176 -72
helpers.py CHANGED
@@ -1,72 +1,176 @@
1
- """Utility functions shared by all modules."""
2
- import io, re, numpy as np, pandas as pd
3
- import plotly.graph_objects as go
4
- import streamlit as st
5
- from config import FREQUENCIES, TOTAL_DOTS, AI_BANDS
6
-
7
- # ── misc helpers ───────────────────────────────────────────────────────────
8
- def slugify(txt: str) -> str:
9
- return re.sub(r"[^0-9a-zA-Z_]+", "_", txt)
10
-
11
- def standardise_freq_cols(df: pd.DataFrame) -> pd.DataFrame:
12
- m = {"125Hz": "125", "250Hz": "250", "500Hz": "500", "1000Hz": "1000",
13
- "1KHz": "1000", "2KHz": "2000", "4KHz": "4000"}
14
- df.columns = [m.get(str(c).replace(" Hz", "").replace("KHz", "000").strip(),
15
- str(c).strip()) for c in df.columns]
16
- df.columns = pd.to_numeric(df.columns, errors="ignore")
17
- return df
18
-
19
- def validate_numeric(df): # True if all numeric
20
- return not df.empty and df.applymap(np.isreal).all().all()
21
-
22
- @st.cache_data(show_spinner=False)
23
- def read_upload(upload, *, header=0, index_col=None):
24
- raw = upload.getvalue()
25
- if upload.name.endswith(".csv"):
26
- return pd.read_csv(io.BytesIO(raw), header=header, index_col=index_col)
27
- return pd.read_excel(io.BytesIO(raw), header=header, index_col=index_col)
28
-
29
- def calc_abs_area(volume_m3, rt_s):
30
- return float("inf") if rt_s == 0 else 0.16 * volume_m3 / rt_s
31
-
32
- # ── plotting helpers ───────────────────────────────────────────────────────
33
- def plot_rt_band(y_cur, y_min, y_max, title):
34
- f = go.Figure()
35
- f.add_trace(go.Scatter(x=FREQUENCIES, y=y_cur, mode="lines+markers",
36
- name="Current", marker_color="#1f77b4"))
37
- f.add_trace(go.Scatter(x=FREQUENCIES, y=y_max, mode="lines",
38
- name="Max Std", line=dict(dash="dash", color="#ff7f0e")))
39
- f.add_trace(go.Scatter(x=FREQUENCIES, y=y_min, mode="lines",
40
- name="Min Std", line=dict(dash="dash", color="#2ca02c"),
41
- fill="tonexty", fillcolor="rgba(44,160,44,0.15)"))
42
- f.update_layout(template="plotly_white", title=title,
43
- xaxis_title="Frequency (Hz)",
44
- yaxis_title="Reverberation Time (s)",
45
- legend=dict(orientation="h", y=-0.2))
46
- return f
47
-
48
- def plot_bn_band(x, y_meas, y_min, y_max, title):
49
- f = go.Figure()
50
- f.add_trace(go.Bar(x=x, y=y_meas, name="Measured",
51
- marker_color="#1f77b4", opacity=0.6))
52
- f.add_shape(type="rect", x0=-.5, x1=len(x)-.5, y0=y_min, y1=y_max,
53
- fillcolor="rgba(255,0,0,0.15)", line=dict(width=0), layer="below")
54
- for y, c, lbl in [(y_max, "#ff0000", "Max Std"),
55
- (y_min, "#ff0000", "Min Std")]:
56
- f.add_shape(type="line", x0=-.5, x1=len(x)-.5, y0=y, y1=y,
57
- line=dict(color=c, dash="dash"))
58
- f.add_trace(go.Scatter(x=[None], y=[None], mode="lines",
59
- line=dict(color=c, dash="dash"),
60
- showlegend=True, name=lbl))
61
- f.update_layout(template="plotly_white", title=title,
62
- xaxis_title="Location", yaxis_title="Sound Level (dBA)",
63
- legend=dict(orientation="h", y=-0.2))
64
- return f
65
-
66
- # ── speech-intelligibility ─────────────────────────────────────────────────
67
- def articulation_index(dots: int):
68
- ai = dots / TOTAL_DOTS
69
- for (lo, hi), lbl in AI_BANDS.items():
70
- if lo <= ai <= hi:
71
- return ai, lbl
72
- return ai, "Out of range"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions shared by all modules."""
2
+
3
+ import io
4
+ import re
5
+ from typing import Tuple
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import plotly.graph_objects as go
10
+ import streamlit as st
11
+
12
+ from config import FREQUENCIES, TOTAL_DOTS, AI_BANDS
13
+
14
+
15
+ # ── misc helpers ──────────────────────────────────────────────────────────
16
+ def slugify(txt: str) -> str:
17
+ """Return a filesystem‑ / url‑safe identifier."""
18
+ return re.sub(r"[^0-9a-zA-Z_]+", "_", txt)
19
+
20
+
21
+ def standardise_freq_cols(df: pd.DataFrame) -> pd.DataFrame:
22
+ """
23
+ Renames common textual frequency headings to plain numbers and returns a
24
+ **new** DataFrame so the caller’s original is left untouched.
25
+ """
26
+ df = df.copy() # defensive copy
27
+ mapping = {
28
+ "125Hz": "125",
29
+ "250Hz": "250",
30
+ "500Hz": "500",
31
+ "1000Hz": "1000",
32
+ "1KHz": "1000",
33
+ "2KHz": "2000",
34
+ "4KHz": "4000",
35
+ }
36
+ df.columns = [
37
+ mapping.get(
38
+ str(c).replace(" Hz", "").replace("KHz", "000").strip(), str(c).strip()
39
+ )
40
+ for c in df.columns
41
+ ]
42
+ df.columns = pd.to_numeric(df.columns, errors="ignore")
43
+ return df
44
+
45
+
46
+ def validate_numeric(df: pd.DataFrame) -> bool:
47
+ """True iff every element of *df* is numeric."""
48
+ return not df.empty and df.applymap(np.isreal).all().all()
49
+
50
+
51
+ def read_upload(
52
+ upload, *, header: int | None = 0, index_col: int | None = None
53
+ ) -> pd.DataFrame:
54
+ """
55
+ Read an uploaded CSV or Excel file into a fresh DataFrame.
56
+
57
+ No caching is used so that every Streamlit session receives its own
58
+ independent object which can be mutated freely without leaking state.
59
+ """
60
+ raw: bytes = upload.getvalue()
61
+ if upload.name.lower().endswith(".csv"):
62
+ return pd.read_csv(io.BytesIO(raw), header=header, index_col=index_col)
63
+ return pd.read_excel(io.BytesIO(raw), header=header, index_col=index_col)
64
+
65
+
66
+ def calc_abs_area(volume_m3: float, rt_s: float) -> float:
67
+ """Sabine: absorption area required to achieve *rt_s* in a room of *volume_m3*."""
68
+ return float("inf") if rt_s == 0 else 0.16 * volume_m3 / rt_s
69
+
70
+
71
+ # ── plotting helpers ──────────────────────────────────────────────────────
72
+ def _base_layout(title: str, x_title: str, y_title: str) -> dict:
73
+ """Common Plotly layout options."""
74
+ return dict(
75
+ template="plotly_white",
76
+ title=title,
77
+ xaxis_title=x_title,
78
+ yaxis_title=y_title,
79
+ legend=dict(orientation="h", y=-0.2),
80
+ )
81
+
82
+
83
+ def plot_rt_band(
84
+ y_cur: list[float], y_min: list[float], y_max: list[float], title: str
85
+ ) -> go.Figure:
86
+ """RT60 band plot."""
87
+ fig = go.Figure()
88
+ fig.add_trace(
89
+ go.Scatter(
90
+ x=FREQUENCIES,
91
+ y=y_cur,
92
+ mode="lines+markers",
93
+ name="Current",
94
+ marker_color="#1f77b4",
95
+ )
96
+ )
97
+ fig.add_trace(
98
+ go.Scatter(
99
+ x=FREQUENCIES,
100
+ y=y_max,
101
+ mode="lines",
102
+ name="Max Std",
103
+ line=dict(dash="dash", color="#ff7f0e"),
104
+ )
105
+ )
106
+ fig.add_trace(
107
+ go.Scatter(
108
+ x=FREQUENCIES,
109
+ y=y_min,
110
+ mode="lines",
111
+ name="Min Std",
112
+ line=dict(dash="dash", color="#2ca02c"),
113
+ fill="tonexty",
114
+ fillcolor="rgba(44,160,44,0.15)",
115
+ )
116
+ )
117
+ fig.update_layout(**_base_layout(title, "Frequencyβ€―(Hz)", "Reverberation Timeβ€―(s)"))
118
+ return fig
119
+
120
+
121
+ def plot_bn_band(
122
+ x: pd.Series,
123
+ y_meas: pd.Series,
124
+ y_min: float,
125
+ y_max: float,
126
+ title: str,
127
+ ) -> go.Figure:
128
+ """Background‑noise bar plot with standard band overlay."""
129
+ fig = go.Figure()
130
+ fig.add_trace(
131
+ go.Bar(x=x, y=y_meas, name="Measured", marker_color="#1f77b4", opacity=0.6)
132
+ )
133
+
134
+ # standard band
135
+ fig.add_shape(
136
+ type="rect",
137
+ x0=-0.5,
138
+ x1=len(x) - 0.5,
139
+ y0=y_min,
140
+ y1=y_max,
141
+ fillcolor="rgba(255,0,0,0.15)",
142
+ line=dict(width=0),
143
+ layer="below",
144
+ )
145
+ for y, label in [(y_max, "Max Std"), (y_min, "Min Std")]:
146
+ fig.add_shape(
147
+ type="line",
148
+ x0=-0.5,
149
+ x1=len(x) - 0.5,
150
+ y0=y,
151
+ y1=y,
152
+ line=dict(color="#ff0000", dash="dash"),
153
+ )
154
+ fig.add_trace(
155
+ go.Scatter(
156
+ x=[None],
157
+ y=[None],
158
+ mode="lines",
159
+ line=dict(color="#ff0000", dash="dash"),
160
+ showlegend=True,
161
+ name=label,
162
+ )
163
+ )
164
+
165
+ fig.update_layout(**_base_layout(title, "Location", "Sound Levelβ€―(dBA)"))
166
+ return fig
167
+
168
+
169
+ # ── speech‑intelligibility helpers ────────────────────────────────────────
170
+ def articulation_index(dots: int) -> Tuple[float, str]:
171
+ """Return (AI value, interpretation label) given dots‑above‑curve count."""
172
+ ai = dots / TOTAL_DOTS
173
+ for (lo, hi), lbl in AI_BANDS.items():
174
+ if lo <= ai <= hi:
175
+ return ai, lbl
176
+ return ai, "Out of range"