pycoustic 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
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.
- pycoustic/log.py +5 -0
- pycoustic/pycoustic_gui_app.py +277 -0
- pycoustic/streamlit-ai.py +522 -0
- pycoustic/survey.py +90 -1
- pycoustic/weather.py +3 -2
- {pycoustic-0.1.6.dist-info → pycoustic-0.1.8.dist-info}/METADATA +14 -6
- pycoustic-0.1.8.dist-info/RECORD +10 -0
- pycoustic-0.1.6.dist-info/RECORD +0 -8
- {pycoustic-0.1.6.dist-info → pycoustic-0.1.8.dist-info}/WHEEL +0 -0
pycoustic/log.py
CHANGED
@@ -6,6 +6,8 @@ import datetime as dt
|
|
6
6
|
|
7
7
|
class Log:
|
8
8
|
def __init__(self, path=""):
|
9
|
+
#TODO C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pycoustic\log.py:15: UserWarning:
|
10
|
+
#Parsing dates in %Y/%m/%d %H:%M format when dayfirst=True was specified. Pass `dayfirst=False` or specify a format to silence this warning.
|
9
11
|
"""
|
10
12
|
The Log class is used to store the measured noise data from one data logger.
|
11
13
|
The data must be entered in a .csv file with headings in the specific format "Leq A", "L90 125" etc.
|
@@ -341,6 +343,9 @@ class Log:
|
|
341
343
|
self._master = self._append_night_idx(data=self._master)
|
342
344
|
self._antilogs = self._append_night_idx(data=self._antilogs)
|
343
345
|
|
346
|
+
#C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pycoustic\log.py:339: PerformanceWarning:
|
347
|
+
#dropping on a non-lexsorted multi-index without a level parameter may impact performance.
|
348
|
+
|
344
349
|
def get_period_times(self):
|
345
350
|
"""
|
346
351
|
:return: the tuples of period start times.
|
@@ -0,0 +1,277 @@
|
|
1
|
+
import os
|
2
|
+
import tempfile
|
3
|
+
from typing import List, Dict
|
4
|
+
|
5
|
+
import pandas as pd
|
6
|
+
import plotly.graph_objects as go
|
7
|
+
import streamlit as st
|
8
|
+
|
9
|
+
from log import *
|
10
|
+
from survey import *
|
11
|
+
|
12
|
+
# Streamlit app config
|
13
|
+
st.set_page_config(page_title="Pycoustic Acoustic Survey Explorer", layout="wide")
|
14
|
+
|
15
|
+
# Graph colour palette config
|
16
|
+
COLOURS = {
|
17
|
+
"Leq A": "#9e9e9e", # light grey
|
18
|
+
"L90 A": "#4d4d4d", # dark grey
|
19
|
+
"Lmax A": "#fc2c2c", # red
|
20
|
+
}
|
21
|
+
# Graph template config
|
22
|
+
TEMPLATE = "plotly"
|
23
|
+
|
24
|
+
if "apply_agg" not in st.session_state:
|
25
|
+
st.session_state["apply_agg"] = False
|
26
|
+
if "period_last" not in st.session_state:
|
27
|
+
st.session_state["period_last"] = ""
|
28
|
+
|
29
|
+
with st.sidebar:
|
30
|
+
# File Upload in expander container
|
31
|
+
with st.expander("File Upload", expanded=True):
|
32
|
+
files = st.file_uploader(
|
33
|
+
"Select one or more CSV files",
|
34
|
+
type="csv",
|
35
|
+
accept_multiple_files=True,
|
36
|
+
)
|
37
|
+
if not files:
|
38
|
+
st.stop()
|
39
|
+
# Integration period entry in expander container
|
40
|
+
with st.expander("Integration Period", expanded=True):
|
41
|
+
int_period = st.number_input(
|
42
|
+
"Insert new integration period (must be larger than data)",
|
43
|
+
step=1,
|
44
|
+
value=15,
|
45
|
+
)
|
46
|
+
period_select = st.selectbox(
|
47
|
+
"Please select time period",
|
48
|
+
("second(s)", "minute(s)", "hour(s)"),
|
49
|
+
index=1,
|
50
|
+
)
|
51
|
+
|
52
|
+
# Build the period string
|
53
|
+
suffix_map = {"second(s)": "s", "minute(s)": "min", "hour(s)": "h"}
|
54
|
+
period = f"{int_period}{suffix_map.get(period_select, '')}"
|
55
|
+
|
56
|
+
# If the period changed since last time, reset the "apply_agg" flag
|
57
|
+
if st.session_state["period_last"] != period:
|
58
|
+
st.session_state["apply_agg"] = False
|
59
|
+
st.session_state["period_last"] = period
|
60
|
+
|
61
|
+
# Button to trigger aggregation for ALL positions
|
62
|
+
apply_agg_btn = st.button("Apply Integration Period")
|
63
|
+
if apply_agg_btn:
|
64
|
+
st.session_state["apply_agg"] = True
|
65
|
+
|
66
|
+
# Main Window / Data Load
|
67
|
+
with st.spinner("Processing Data...", show_time=True):
|
68
|
+
# Load each uploaded CSV into a pycoustic Log
|
69
|
+
logs: Dict[str, Log] = {}
|
70
|
+
for upload_file in files:
|
71
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv") as tmp:
|
72
|
+
tmp.write(upload_file.getbuffer())
|
73
|
+
path = tmp.name
|
74
|
+
try:
|
75
|
+
logs[upload_file.name] = Log(path)
|
76
|
+
except Exception as err:
|
77
|
+
st.error(f"Failed to load `{upload_file.name}` into Pycoustic: {err}")
|
78
|
+
finally:
|
79
|
+
os.unlink(path)
|
80
|
+
|
81
|
+
# Build Survey and pull summary + spectra
|
82
|
+
summary_df = leq_spec_df = lmax_spec_df = None
|
83
|
+
summary_error = ""
|
84
|
+
if logs:
|
85
|
+
try:
|
86
|
+
survey = Survey()
|
87
|
+
if callable(getattr(survey, "add_log", None)):
|
88
|
+
for name, lg in logs.items():
|
89
|
+
survey.add_log(lg, name=name)
|
90
|
+
elif hasattr(survey, "_logs"):
|
91
|
+
survey._logs = logs
|
92
|
+
|
93
|
+
summary_df = survey.resi_summary()
|
94
|
+
leq_spec_df = getattr(survey, "typical_leq_spectra", lambda: None)()
|
95
|
+
lmax_spec_df = getattr(survey, "lmax_spectra", lambda: None)()
|
96
|
+
except Exception as err:
|
97
|
+
summary_error = str(err)
|
98
|
+
else:
|
99
|
+
summary_error = "No valid logs loaded."
|
100
|
+
|
101
|
+
# Helper list of “position” names (i.e. filenames)
|
102
|
+
pos_list = list(logs.keys())
|
103
|
+
|
104
|
+
# Helper: turn a “spectra” DataFrame into a long‐format table for plotting
|
105
|
+
def spectra_to_rows(df: pd.DataFrame, pos_names: List[str]) -> pd.DataFrame | None:
|
106
|
+
if df is None:
|
107
|
+
return None
|
108
|
+
if not isinstance(df.columns, pd.MultiIndex):
|
109
|
+
tidy = df.reset_index().rename(columns={df.index.name or "index": "Period"})
|
110
|
+
if "Position" not in tidy.columns:
|
111
|
+
tidy.insert(0, "Position", pos_names[0] if pos_names else "Pos1")
|
112
|
+
return tidy
|
113
|
+
|
114
|
+
# If there is a MultiIndex
|
115
|
+
bands = [band for _, band in df.columns][: len({band for _, band in df.columns})]
|
116
|
+
set_len = len(bands)
|
117
|
+
blocks = []
|
118
|
+
for i, pos in enumerate(pos_names):
|
119
|
+
start, end = i * set_len, (i + 1) * set_len
|
120
|
+
if end > df.shape[1]:
|
121
|
+
break
|
122
|
+
sub = df.iloc[:, start:end].copy()
|
123
|
+
sub.columns = [str(b) for b in bands]
|
124
|
+
sub = sub.reset_index().rename(columns={df.index.names[-1] or "index": "Period"})
|
125
|
+
if "Position" not in sub.columns:
|
126
|
+
sub.insert(0, "Position", pos)
|
127
|
+
blocks.append(sub)
|
128
|
+
return pd.concat(blocks, ignore_index=True)
|
129
|
+
|
130
|
+
#Create tabs
|
131
|
+
ui_tabs = st.tabs(["Summary"] + pos_list)
|
132
|
+
|
133
|
+
#Summary tab
|
134
|
+
with ui_tabs[0]:
|
135
|
+
st.subheader("Broadband Summary")
|
136
|
+
if summary_df is not None:
|
137
|
+
st.dataframe(summary_df)
|
138
|
+
else:
|
139
|
+
st.warning(f"Summary unavailable: {summary_error}")
|
140
|
+
|
141
|
+
# Plot “Typical Leq Spectra” and “Lmax Spectra”, if available
|
142
|
+
for title, df_data in (
|
143
|
+
("Typical Leq Spectra", leq_spec_df),
|
144
|
+
("Lmax Spectra", lmax_spec_df),
|
145
|
+
):
|
146
|
+
tidy = spectra_to_rows(df_data, pos_list)
|
147
|
+
if tidy is None:
|
148
|
+
continue
|
149
|
+
|
150
|
+
freq_cols = [c for c in tidy.columns if c not in ("Position", "Period", "A")]
|
151
|
+
if freq_cols:
|
152
|
+
fig = go.Figure()
|
153
|
+
for pos in pos_list:
|
154
|
+
subset = tidy[tidy["Position"] == pos]
|
155
|
+
for _, row in subset.iterrows():
|
156
|
+
period_label = row["Period"]
|
157
|
+
# Cast to string so .lower() is safe
|
158
|
+
period_label_str = str(period_label)
|
159
|
+
mode = (
|
160
|
+
"lines+markers"
|
161
|
+
if period_label_str.lower().startswith("day")
|
162
|
+
else "lines"
|
163
|
+
)
|
164
|
+
label = (
|
165
|
+
f"{pos} {period_label_str}"
|
166
|
+
if len(pos_list) > 1
|
167
|
+
else period_label_str
|
168
|
+
)
|
169
|
+
fig.add_trace(
|
170
|
+
go.Scatter(
|
171
|
+
x=freq_cols,
|
172
|
+
y=row[freq_cols],
|
173
|
+
mode=mode,
|
174
|
+
name=label,
|
175
|
+
)
|
176
|
+
)
|
177
|
+
fig.update_layout(
|
178
|
+
template=TEMPLATE,
|
179
|
+
title=f"{title} - Day & Night",
|
180
|
+
xaxis_title="Octave band (Hz)",
|
181
|
+
yaxis_title="dB",
|
182
|
+
)
|
183
|
+
st.plotly_chart(fig, use_container_width=True)
|
184
|
+
else:
|
185
|
+
st.warning(f"No frequency columns found for `{title}`.")
|
186
|
+
|
187
|
+
# Position‐Specific Tabs
|
188
|
+
for tab, uf in zip(ui_tabs[1:], files):
|
189
|
+
with tab:
|
190
|
+
log = logs.get(uf.name)
|
191
|
+
if log is None:
|
192
|
+
st.error(f"Log for `{uf.name}` not found.")
|
193
|
+
continue
|
194
|
+
|
195
|
+
# Decide whether to show raw or aggregated data
|
196
|
+
if st.session_state["apply_agg"]:
|
197
|
+
# 1) Re-aggregate / resample using the chosen period
|
198
|
+
try:
|
199
|
+
df_used = log.as_interval(t=period)
|
200
|
+
df_used = df_used.reset_index().rename(
|
201
|
+
columns={df_used.index.name or "index": "Timestamp"}
|
202
|
+
)
|
203
|
+
subheader = "Integrated Survey Data"
|
204
|
+
except Exception as e:
|
205
|
+
st.error(f"Failed to apply integration period for `{uf.name}`: {e}")
|
206
|
+
continue
|
207
|
+
else:
|
208
|
+
# 2) Show the raw data (from log._master) if available
|
209
|
+
try:
|
210
|
+
raw_master = log._master # original DataFrame, indexed by Timestamp
|
211
|
+
df_used = raw_master.reset_index().rename(columns={"Time": "Timestamp"})
|
212
|
+
subheader = "Raw Survey Data"
|
213
|
+
except Exception as e:
|
214
|
+
st.error(f"Failed to load raw data for `{uf.name}`: {e}")
|
215
|
+
continue
|
216
|
+
|
217
|
+
# Prepare a flattened‐column header copy JUST FOR PLOTTING
|
218
|
+
df_plot = df_used.copy()
|
219
|
+
if isinstance(df_plot.columns, pd.MultiIndex):
|
220
|
+
flattened_cols = []
|
221
|
+
for lvl0, lvl1 in df_plot.columns:
|
222
|
+
lvl0_str = str(lvl0)
|
223
|
+
lvl1_str = str(lvl1) if lvl1 is not None else ""
|
224
|
+
flattened_cols.append(f"{lvl0_str} {lvl1_str}".strip())
|
225
|
+
df_plot.columns = flattened_cols
|
226
|
+
|
227
|
+
# Time‐history Graph (Leq A, L90 A, Lmax A) using df_plot
|
228
|
+
required_cols = {"Leq A", "L90 A", "Lmax A"}
|
229
|
+
if required_cols.issubset(set(df_plot.columns)):
|
230
|
+
fig = go.Figure()
|
231
|
+
fig.add_trace(
|
232
|
+
go.Scatter(
|
233
|
+
x=df_plot["Timestamp"],
|
234
|
+
y=df_plot["Leq A"],
|
235
|
+
name="Leq A",
|
236
|
+
mode="lines",
|
237
|
+
line=dict(color=COLOURS["Leq A"], width=1),
|
238
|
+
)
|
239
|
+
)
|
240
|
+
fig.add_trace(
|
241
|
+
go.Scatter(
|
242
|
+
x=df_plot["Timestamp"],
|
243
|
+
y=df_plot["L90 A"],
|
244
|
+
name="L90 A",
|
245
|
+
mode="lines",
|
246
|
+
line=dict(color=COLOURS["L90 A"], width=1),
|
247
|
+
)
|
248
|
+
)
|
249
|
+
fig.add_trace(
|
250
|
+
go.Scatter(
|
251
|
+
x=df_plot["Timestamp"],
|
252
|
+
y=df_plot["Lmax A"],
|
253
|
+
name="Lmax A",
|
254
|
+
mode="markers",
|
255
|
+
marker=dict(color=COLOURS["Lmax A"], size=3),
|
256
|
+
)
|
257
|
+
)
|
258
|
+
fig.update_layout(
|
259
|
+
template=TEMPLATE,
|
260
|
+
margin=dict(l=0, r=0, t=0, b=0),
|
261
|
+
xaxis=dict(
|
262
|
+
title="Time & Date (hh:mm & dd/mm/yyyy)",
|
263
|
+
type="date",
|
264
|
+
tickformat="%H:%M<br>%d/%m/%Y",
|
265
|
+
tickangle=0,
|
266
|
+
),
|
267
|
+
yaxis_title="Measured Sound Pressure Level dB(A)",
|
268
|
+
legend=dict(orientation="h", yanchor="top", y=-0.25, xanchor="left", x=0),
|
269
|
+
height=600,
|
270
|
+
)
|
271
|
+
st.plotly_chart(fig, use_container_width=True)
|
272
|
+
else:
|
273
|
+
st.warning(f"Required columns {required_cols} missing in {subheader}.")
|
274
|
+
|
275
|
+
# --- Finally, display the TABLE with MultiIndex intact ---
|
276
|
+
st.subheader(subheader)
|
277
|
+
st.dataframe(df_used, hide_index=True)
|
@@ -0,0 +1,522 @@
|
|
1
|
+
# Python 3.12
|
2
|
+
# Streamlit app entrypoint for PyCoustic-like workflow
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
import io
|
7
|
+
import re
|
8
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
import pandas as pd
|
12
|
+
import plotly.graph_objects as go
|
13
|
+
import streamlit as st
|
14
|
+
|
15
|
+
|
16
|
+
# -----------------------------
|
17
|
+
# Plotting configuration
|
18
|
+
# -----------------------------
|
19
|
+
COLOURS: List[str] = [
|
20
|
+
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
21
|
+
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
|
22
|
+
]
|
23
|
+
TEMPLATE: str = "plotly_white"
|
24
|
+
|
25
|
+
|
26
|
+
# -----------------------------
|
27
|
+
# Helpers
|
28
|
+
# -----------------------------
|
29
|
+
def _try_read_table(upload) -> pd.DataFrame:
|
30
|
+
# Try CSV first, then Excel
|
31
|
+
name = getattr(upload, "name", "uploaded")
|
32
|
+
data = upload.read() if hasattr(upload, "read") else upload.getvalue()
|
33
|
+
# ensure the buffer can be reused for Excel attempt
|
34
|
+
buf = io.BytesIO(data)
|
35
|
+
|
36
|
+
# CSV attempt
|
37
|
+
try:
|
38
|
+
df_csv = pd.read_csv(io.BytesIO(data))
|
39
|
+
if not df_csv.empty:
|
40
|
+
df_csv.attrs["__source_name__"] = name
|
41
|
+
return df_csv
|
42
|
+
except Exception:
|
43
|
+
pass
|
44
|
+
|
45
|
+
# Excel attempt
|
46
|
+
buf.seek(0)
|
47
|
+
try:
|
48
|
+
df_xls = pd.read_excel(buf)
|
49
|
+
if not df_xls.empty:
|
50
|
+
df_xls.attrs["__source_name__"] = name
|
51
|
+
return df_xls
|
52
|
+
except Exception:
|
53
|
+
pass
|
54
|
+
|
55
|
+
raise ValueError(f"Could not parse file: {name}")
|
56
|
+
|
57
|
+
|
58
|
+
def _flatten_columns(df: pd.DataFrame) -> pd.DataFrame:
|
59
|
+
# Flatten MultiIndex columns if any
|
60
|
+
if isinstance(df.columns, pd.MultiIndex):
|
61
|
+
flat = [" | ".join([str(x) for x in tup if x is not None]) for tup in df.columns]
|
62
|
+
df = df.copy()
|
63
|
+
df.columns = flat
|
64
|
+
return df
|
65
|
+
|
66
|
+
|
67
|
+
def _maybe_parse_datetime(df: pd.DataFrame) -> pd.DataFrame:
|
68
|
+
# Heuristic: try common datetime column names and parse them
|
69
|
+
dt_candidates = ["Datetime", "DateTime", "Timestamp", "Time", "Date", "Date_Time", "datetime", "time", "timestamp"]
|
70
|
+
for col in df.columns:
|
71
|
+
if col in dt_candidates or re.search(r"time|date|stamp", str(col), re.IGNORECASE):
|
72
|
+
try:
|
73
|
+
parsed = pd.to_datetime(df[col], errors="raise", utc=False, infer_datetime_format=True)
|
74
|
+
out = df.copy()
|
75
|
+
out[col] = parsed
|
76
|
+
return out
|
77
|
+
except Exception:
|
78
|
+
continue
|
79
|
+
return df
|
80
|
+
|
81
|
+
|
82
|
+
def _detect_position_col(df: pd.DataFrame) -> Optional[str]:
|
83
|
+
candidates = ["Position", "Pos", "Mic", "Channel", "Location", "Site"]
|
84
|
+
for c in candidates:
|
85
|
+
if c in df.columns:
|
86
|
+
return c
|
87
|
+
# also try case-insensitive exact matches
|
88
|
+
lower_map = {str(c).lower(): c for c in df.columns}
|
89
|
+
for c in candidates:
|
90
|
+
if c.lower() in lower_map:
|
91
|
+
return lower_map[c.lower()]
|
92
|
+
return None
|
93
|
+
|
94
|
+
|
95
|
+
def _metric_patterns() -> Dict[str, re.Pattern]:
|
96
|
+
# Detect wide spectral columns, e.g., "Leq_31.5", "Lmax_1000", "Leq 4k", "Lmax 1 kHz", "63 Hz"
|
97
|
+
# Strategy:
|
98
|
+
# - Either prefixed by a metric (Leq/Lmax) and then frequency
|
99
|
+
# - Or pure frequency with "Hz" and a separate metric column naming is handled by selection
|
100
|
+
def freq_part():
|
101
|
+
# numbers like 31.5, 1000, 1k, 2 kHz, etc.
|
102
|
+
return r"(?P<freq>(\d+(\.\d+)?)(\s*k(hz)?)?)"
|
103
|
+
|
104
|
+
# metric-first naming: "Leq_31.5", "Lmax 1000", "Leq-1k", "Leq 1 kHz"
|
105
|
+
metric_first = rf"^(?P<metric>Leq|Lmax)[\s_\-]*{freq_part()}(hz)?$"
|
106
|
+
# freq-first naming: "31.5", "63 Hz", "1k", with an optional suffix metric after a sep: "63Hz_Leq"
|
107
|
+
freq_first = rf"^{freq_part()}(hz)?[\s_\-]*(?P<metric>Leq|Lmax)?$"
|
108
|
+
return {
|
109
|
+
"metric_first": re.compile(metric_first, re.IGNORECASE),
|
110
|
+
"freq_first": re.compile(freq_first, re.IGNORECASE),
|
111
|
+
}
|
112
|
+
|
113
|
+
|
114
|
+
def _parse_freq_to_hz(freq_str: str) -> Optional[float]:
|
115
|
+
if freq_str is None:
|
116
|
+
return None
|
117
|
+
s = str(freq_str).strip().lower().replace(" ", "")
|
118
|
+
s = s.replace("khz", "k").replace("hz", "")
|
119
|
+
# handle "1k" or "1.0k"
|
120
|
+
m = re.match(r"^(\d+(\.\d+)?)k$", s)
|
121
|
+
if m:
|
122
|
+
return float(m.group(1)) * 1000.0
|
123
|
+
try:
|
124
|
+
return float(s)
|
125
|
+
except Exception:
|
126
|
+
return None
|
127
|
+
|
128
|
+
|
129
|
+
def spectra_to_rows(df: pd.DataFrame) -> pd.DataFrame:
|
130
|
+
"""
|
131
|
+
Convert wide spectral columns into a tidy long form:
|
132
|
+
Columns like "Leq_31.5", "Lmax 63", "63 Hz Leq" -> rows with Frequency (Hz), Metric, Value.
|
133
|
+
Non-spectral columns are carried along.
|
134
|
+
"""
|
135
|
+
df = _flatten_columns(df)
|
136
|
+
patterns = _metric_patterns()
|
137
|
+
|
138
|
+
spectral_cols: List[Tuple[str, str, float]] = [] # (original_col, metric, freq_hz)
|
139
|
+
for col in df.columns:
|
140
|
+
col_str = str(col)
|
141
|
+
matched = False
|
142
|
+
|
143
|
+
# metric first
|
144
|
+
m1 = patterns["metric_first"].match(col_str)
|
145
|
+
if m1:
|
146
|
+
metric = m1.group("metric").upper()
|
147
|
+
freq_hz = _parse_freq_to_hz(m1.group("freq"))
|
148
|
+
if metric in ("LEQ", "LMAX") and freq_hz is not None:
|
149
|
+
spectral_cols.append((col, metric, freq_hz))
|
150
|
+
matched = True
|
151
|
+
|
152
|
+
if matched:
|
153
|
+
continue
|
154
|
+
|
155
|
+
# frequency first
|
156
|
+
m2 = patterns["freq_first"].match(col_str)
|
157
|
+
if m2:
|
158
|
+
metric = m2.group("metric")
|
159
|
+
metric = metric.upper() if metric else None
|
160
|
+
freq_hz = _parse_freq_to_hz(m2.group("freq"))
|
161
|
+
if freq_hz is not None:
|
162
|
+
# If metric is not embedded, we will treat it as "LEQ" by default for plotting,
|
163
|
+
# but also keep the column name when we pivot.
|
164
|
+
spectral_cols.append((col, metric or "LEQ", freq_hz))
|
165
|
+
|
166
|
+
if not spectral_cols:
|
167
|
+
return pd.DataFrame(columns=["Frequency_Hz", "Metric", "Value"])
|
168
|
+
|
169
|
+
# Build tidy rows
|
170
|
+
id_cols = [c for c in df.columns if c not in [c0 for (c0, _, _) in spectral_cols]]
|
171
|
+
tidies: List[pd.DataFrame] = []
|
172
|
+
for (col, metric, f_hz) in spectral_cols:
|
173
|
+
block = pd.DataFrame(
|
174
|
+
{
|
175
|
+
"Frequency_Hz": f_hz,
|
176
|
+
"Metric": metric,
|
177
|
+
"Value": df[col].astype("float64").values,
|
178
|
+
}
|
179
|
+
)
|
180
|
+
# Attach IDs if present
|
181
|
+
if id_cols:
|
182
|
+
block = pd.concat([df[id_cols].reset_index(drop=True), block], axis=1)
|
183
|
+
tidies.append(block)
|
184
|
+
|
185
|
+
tidy = pd.concat(tidies, axis=0, ignore_index=True)
|
186
|
+
# Sort by frequency numeric
|
187
|
+
tidy = tidy.sort_values(["Metric", "Frequency_Hz"]).reset_index(drop=True)
|
188
|
+
return tidy
|
189
|
+
|
190
|
+
|
191
|
+
def _resample_if_possible(df: pd.DataFrame, how: str) -> pd.DataFrame:
|
192
|
+
"""
|
193
|
+
how: '', '1min', '5min', '1H', '1D'
|
194
|
+
"""
|
195
|
+
if not how:
|
196
|
+
return df
|
197
|
+
|
198
|
+
# find a datetime column
|
199
|
+
dt_col = None
|
200
|
+
for c in df.columns:
|
201
|
+
if pd.api.types.is_datetime64_any_dtype(df[c]):
|
202
|
+
dt_col = c
|
203
|
+
break
|
204
|
+
|
205
|
+
if dt_col is None:
|
206
|
+
return df # nothing to resample on
|
207
|
+
|
208
|
+
df_sorted = df.sort_values(dt_col)
|
209
|
+
df_sorted = df_sorted.set_index(dt_col)
|
210
|
+
|
211
|
+
# numeric only for resample
|
212
|
+
numeric_cols = df_sorted.select_dtypes(include=["number"]).columns
|
213
|
+
if len(numeric_cols) == 0:
|
214
|
+
return df
|
215
|
+
|
216
|
+
grouped = df_sorted[numeric_cols].resample(how).mean().reset_index()
|
217
|
+
# put back other columns in a sensible way (drop or take first)
|
218
|
+
return grouped
|
219
|
+
|
220
|
+
|
221
|
+
def _build_summary(df: pd.DataFrame, group_cols: List[str]) -> pd.DataFrame:
|
222
|
+
if not group_cols:
|
223
|
+
# simple numeric summary
|
224
|
+
numeric_cols = df.select_dtypes(include=["number"]).columns
|
225
|
+
if len(numeric_cols) == 0:
|
226
|
+
return pd.DataFrame()
|
227
|
+
s = df[numeric_cols].agg(["count", "mean", "std", "min", "max"]).T.reset_index()
|
228
|
+
s.columns = ["Metric"] + list(s.columns[1:])
|
229
|
+
return s
|
230
|
+
|
231
|
+
# groupby summary on numeric columns
|
232
|
+
numeric_cols = df.select_dtypes(include=["number"]).columns
|
233
|
+
if len(numeric_cols) == 0:
|
234
|
+
return pd.DataFrame()
|
235
|
+
|
236
|
+
g = df.groupby(group_cols, dropna=False)[numeric_cols].agg(["count", "mean", "std", "min", "max"])
|
237
|
+
g = g.reset_index()
|
238
|
+
|
239
|
+
# flatten resulting MultiIndex columns
|
240
|
+
out_cols = []
|
241
|
+
for tup in g.columns:
|
242
|
+
if isinstance(tup, tuple):
|
243
|
+
lvl0, lvl1 = tup
|
244
|
+
if lvl1 == "":
|
245
|
+
out_cols.append(str(lvl0))
|
246
|
+
elif lvl0 in group_cols:
|
247
|
+
out_cols.append(str(lvl0))
|
248
|
+
else:
|
249
|
+
out_cols.append(f"{lvl0}__{lvl1}")
|
250
|
+
else:
|
251
|
+
out_cols.append(str(tup))
|
252
|
+
g.columns = out_cols
|
253
|
+
return g
|
254
|
+
|
255
|
+
|
256
|
+
def _guess_resample_options(df: pd.DataFrame) -> List[Tuple[str, str]]:
|
257
|
+
# Label, pandas rule
|
258
|
+
has_dt = any(pd.api.types.is_datetime64_any_dtype(df[c]) for c in df.columns)
|
259
|
+
if not has_dt:
|
260
|
+
return [("None", "")]
|
261
|
+
return [
|
262
|
+
("None", ""),
|
263
|
+
("1 minute", "1min"),
|
264
|
+
("5 minutes", "5min"),
|
265
|
+
("15 minutes", "15min"),
|
266
|
+
("Hourly", "1H"),
|
267
|
+
("Daily", "1D"),
|
268
|
+
]
|
269
|
+
|
270
|
+
|
271
|
+
def _plot_spectra(tidy_spec: pd.DataFrame, color_by: Optional[str]) -> go.Figure:
|
272
|
+
fig = go.Figure()
|
273
|
+
if tidy_spec.empty:
|
274
|
+
fig.update_layout(template=TEMPLATE)
|
275
|
+
return fig
|
276
|
+
|
277
|
+
# X is frequency Hz (log10)
|
278
|
+
x = tidy_spec["Frequency_Hz"].to_numpy(dtype=float)
|
279
|
+
|
280
|
+
# determine trace grouping
|
281
|
+
if color_by and color_by in tidy_spec.columns:
|
282
|
+
groups = list(tidy_spec[color_by].astype(str).unique())
|
283
|
+
for i, key in enumerate(groups):
|
284
|
+
sub = tidy_spec[tidy_spec[color_by].astype(str) == str(key)]
|
285
|
+
# Keep metric separated as line style if available
|
286
|
+
if "Metric" in sub.columns and sub["Metric"].nunique() > 1:
|
287
|
+
for metric in sorted(sub["Metric"].unique()):
|
288
|
+
subm = sub[sub["Metric"] == metric]
|
289
|
+
fig.add_trace(
|
290
|
+
go.Scatter(
|
291
|
+
x=subm["Frequency_Hz"],
|
292
|
+
y=subm["Value"],
|
293
|
+
mode="lines+markers",
|
294
|
+
name=f"{key} – {metric}",
|
295
|
+
line=dict(color=COLOURS[i % len(COLOURS)], dash="solid" if metric == "LEQ" else "dash"),
|
296
|
+
marker=dict(size=6),
|
297
|
+
)
|
298
|
+
)
|
299
|
+
else:
|
300
|
+
fig.add_trace(
|
301
|
+
go.Scatter(
|
302
|
+
x=sub["Frequency_Hz"],
|
303
|
+
y=sub["Value"],
|
304
|
+
mode="lines+markers",
|
305
|
+
name=str(key),
|
306
|
+
line=dict(color=COLOURS[i % len(COLOURS)]),
|
307
|
+
marker=dict(size=6),
|
308
|
+
)
|
309
|
+
)
|
310
|
+
else:
|
311
|
+
# single trace per metric
|
312
|
+
if "Metric" in tidy_spec.columns:
|
313
|
+
for i, metric in enumerate(sorted(tidy_spec["Metric"].unique())):
|
314
|
+
sub = tidy_spec[tidy_spec["Metric"] == metric]
|
315
|
+
fig.add_trace(
|
316
|
+
go.Scatter(
|
317
|
+
x=sub["Frequency_Hz"],
|
318
|
+
y=sub["Value"],
|
319
|
+
mode="lines+markers",
|
320
|
+
name=str(metric),
|
321
|
+
line=dict(color=COLOURS[i % len(COLOURS)]),
|
322
|
+
marker=dict(size=6),
|
323
|
+
)
|
324
|
+
)
|
325
|
+
else:
|
326
|
+
fig.add_trace(
|
327
|
+
go.Scatter(
|
328
|
+
x=x,
|
329
|
+
y=tidy_spec["Value"],
|
330
|
+
mode="lines+markers",
|
331
|
+
name="Spectrum",
|
332
|
+
line=dict(color=COLOURS[0]),
|
333
|
+
marker=dict(size=6),
|
334
|
+
)
|
335
|
+
)
|
336
|
+
|
337
|
+
fig.update_layout(
|
338
|
+
template=TEMPLATE,
|
339
|
+
xaxis=dict(
|
340
|
+
type="log",
|
341
|
+
title="Frequency (Hz)",
|
342
|
+
tickvals=[31.5, 63, 125, 250, 500, 1000, 2000, 4000, 8000],
|
343
|
+
ticktext=["31.5", "63", "125", "250", "500", "1k", "2k", "4k", "8k"],
|
344
|
+
gridcolor="rgba(0,0,0,0.1)",
|
345
|
+
),
|
346
|
+
yaxis=dict(title="Level (dB)", gridcolor="rgba(0,0,0,0.1)"),
|
347
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
348
|
+
margin=dict(l=50, r=30, t=40, b=50),
|
349
|
+
)
|
350
|
+
return fig
|
351
|
+
|
352
|
+
|
353
|
+
def _download_csv_button(label: str, df: pd.DataFrame, key: str):
|
354
|
+
csv = df.to_csv(index=False).encode("utf-8")
|
355
|
+
st.download_button(
|
356
|
+
label=label,
|
357
|
+
data=csv,
|
358
|
+
file_name=f"{key}.csv",
|
359
|
+
mime="text/csv",
|
360
|
+
use_container_width=True,
|
361
|
+
)
|
362
|
+
|
363
|
+
|
364
|
+
# -----------------------------
|
365
|
+
# UI
|
366
|
+
# -----------------------------
|
367
|
+
def main():
|
368
|
+
st.set_page_config(page_title="PyCoustic – Streamlit", layout="wide")
|
369
|
+
|
370
|
+
st.title("PyCoustic – Streamlit App")
|
371
|
+
st.caption("Upload measurement logs, explore summaries and spectra, and export results.")
|
372
|
+
|
373
|
+
with st.sidebar:
|
374
|
+
st.header("Inputs")
|
375
|
+
uploads = st.file_uploader(
|
376
|
+
"Upload one or more files (CSV or Excel)",
|
377
|
+
type=["csv", "txt", "xlsx", "xls"],
|
378
|
+
accept_multiple_files=True,
|
379
|
+
)
|
380
|
+
|
381
|
+
st.markdown("---")
|
382
|
+
st.subheader("Options")
|
383
|
+
|
384
|
+
# These options approximate a typical workflow
|
385
|
+
resample_label = "Resample period"
|
386
|
+
resample_options: List[Tuple[str, str]] = [("None", "")]
|
387
|
+
# temporarily show None; we'll refine after reading a file
|
388
|
+
|
389
|
+
# placeholder UI to avoid reflow
|
390
|
+
resample_choice_label = st.selectbox(resample_label, [x[0] for x in resample_options], index=0, key="resample_placeholder")
|
391
|
+
|
392
|
+
group_hint = st.text_input("Optional Group Column (e.g., Position/Location)", value="")
|
393
|
+
|
394
|
+
st.markdown("---")
|
395
|
+
st.subheader("Spectra")
|
396
|
+
colour_by = st.text_input("Colour lines by column (e.g., Position or source)", value="source")
|
397
|
+
show_markers = st.checkbox("Show markers", value=True)
|
398
|
+
|
399
|
+
st.markdown("---")
|
400
|
+
st.subheader("Display")
|
401
|
+
show_raw_preview = st.checkbox("Show raw preview table", value=False)
|
402
|
+
|
403
|
+
if not uploads:
|
404
|
+
st.info("Upload files to begin.")
|
405
|
+
return
|
406
|
+
|
407
|
+
# Read and combine
|
408
|
+
logs: List[str] = []
|
409
|
+
frames: List[pd.DataFrame] = []
|
410
|
+
for uf in uploads:
|
411
|
+
try:
|
412
|
+
df = _try_read_table(uf)
|
413
|
+
df = _flatten_columns(df)
|
414
|
+
df = _maybe_parse_datetime(df)
|
415
|
+
df["source"] = getattr(uf, "name", df.attrs.get("__source_name__", "uploaded"))
|
416
|
+
frames.append(df)
|
417
|
+
logs.append(f"Loaded: {df['source'].iloc[0]} (rows={len(df)}, cols={len(df.columns)})")
|
418
|
+
except Exception as e:
|
419
|
+
logs.append(f"Error reading {getattr(uf, 'name', '?')}: {e}")
|
420
|
+
|
421
|
+
if not frames:
|
422
|
+
st.error("No readable files.")
|
423
|
+
st.text_area("Logs", value="\n".join(logs), height=160)
|
424
|
+
return
|
425
|
+
|
426
|
+
raw_master = pd.concat(frames, axis=0, ignore_index=True)
|
427
|
+
# Now that we have data, rebuild resample options
|
428
|
+
resample_options = _guess_resample_options(raw_master)
|
429
|
+
resample_choice_label = st.sidebar.selectbox(
|
430
|
+
"Resample period",
|
431
|
+
[x[0] for x in resample_options],
|
432
|
+
index=0,
|
433
|
+
key="resample_choice",
|
434
|
+
)
|
435
|
+
resample_rule = dict(resample_options)[resample_choice_label]
|
436
|
+
|
437
|
+
# Optional resampling
|
438
|
+
df_used = _resample_if_possible(raw_master, resample_rule) if resample_rule else raw_master
|
439
|
+
|
440
|
+
# Try to determine a reasonable group column
|
441
|
+
detected_group_col = _detect_position_col(df_used)
|
442
|
+
group_col = (st.sidebar.text_input("Detected Group Column", value=detected_group_col or "") or "").strip()
|
443
|
+
if not group_col and group_hint.strip():
|
444
|
+
group_col = group_hint.strip()
|
445
|
+
if group_col and group_col not in df_used.columns:
|
446
|
+
st.warning(f"Group column '{group_col}' not found. It will be ignored.")
|
447
|
+
group_col = ""
|
448
|
+
|
449
|
+
# Build spectra in tidy form
|
450
|
+
tidy_spec = spectra_to_rows(df_used)
|
451
|
+
|
452
|
+
tabs = st.tabs(["Summary", "Spectra", "Raw Data", "Logs"])
|
453
|
+
|
454
|
+
# Summary tab
|
455
|
+
with tabs[0]:
|
456
|
+
st.subheader("Summary")
|
457
|
+
group_cols: List[str] = []
|
458
|
+
if group_col:
|
459
|
+
group_cols.append(group_col)
|
460
|
+
if "source" in df_used.columns:
|
461
|
+
group_cols.append("source")
|
462
|
+
|
463
|
+
summary_df = _build_summary(df_used, group_cols)
|
464
|
+
if summary_df.empty:
|
465
|
+
st.info("No numeric data to summarize.")
|
466
|
+
else:
|
467
|
+
st.dataframe(summary_df, use_container_width=True, hide_index=True)
|
468
|
+
_download_csv_button("Download summary CSV", summary_df, "summary")
|
469
|
+
|
470
|
+
# Spectra tab
|
471
|
+
with tabs[1]:
|
472
|
+
st.subheader("Spectra")
|
473
|
+
|
474
|
+
# Allow user to filter a group (optional)
|
475
|
+
filters_cols = []
|
476
|
+
if group_col:
|
477
|
+
filters_cols.append(group_col)
|
478
|
+
if "source" in tidy_spec.columns:
|
479
|
+
filters_cols.append("source")
|
480
|
+
|
481
|
+
sub = tidy_spec.copy()
|
482
|
+
# Dynamic filters
|
483
|
+
if not sub.empty and filters_cols:
|
484
|
+
cols = st.columns(len(filters_cols))
|
485
|
+
for i, colname in enumerate(filters_cols):
|
486
|
+
with cols[i]:
|
487
|
+
uniq = sorted([str(x) for x in sub[colname].dropna().unique()])
|
488
|
+
if len(uniq) <= 1:
|
489
|
+
continue
|
490
|
+
selected = st.multiselect(f"Filter {colname}", options=uniq, default=uniq)
|
491
|
+
sub = sub[sub[colname].astype(str).isin(selected)]
|
492
|
+
|
493
|
+
# Plot
|
494
|
+
if sub.empty:
|
495
|
+
st.info("No spectral data detected in the uploaded files.")
|
496
|
+
else:
|
497
|
+
fig = _plot_spectra(sub, color_by=(colour_by if colour_by in sub.columns else None))
|
498
|
+
if not show_markers:
|
499
|
+
for tr in fig.data:
|
500
|
+
tr.mode = "lines"
|
501
|
+
st.plotly_chart(fig, use_container_width=True)
|
502
|
+
|
503
|
+
# Download tidy spectra
|
504
|
+
_download_csv_button("Download tidy spectra CSV", sub, "spectra_tidy")
|
505
|
+
|
506
|
+
# Raw Data tab
|
507
|
+
with tabs[2]:
|
508
|
+
st.subheader("Raw Data")
|
509
|
+
if show_raw_preview:
|
510
|
+
st.dataframe(raw_master, use_container_width=True, hide_index=True)
|
511
|
+
else:
|
512
|
+
st.caption("Enable 'Show raw preview table' in the sidebar to render the full table.")
|
513
|
+
_download_csv_button("Download combined raw CSV", raw_master, "raw_combined")
|
514
|
+
|
515
|
+
# Logs tab
|
516
|
+
with tabs[3]:
|
517
|
+
st.subheader("Logs")
|
518
|
+
st.text_area("Ingestion log", value="\n".join(logs), height=240, label_visibility="collapsed")
|
519
|
+
|
520
|
+
|
521
|
+
if __name__ == "__main__":
|
522
|
+
main()
|
pycoustic/survey.py
CHANGED
@@ -10,7 +10,7 @@ pd.set_option('display.max_rows', None)
|
|
10
10
|
|
11
11
|
#survey.leq_spectra() bug
|
12
12
|
#TODO: C:\Users\tonyr\PycharmProjects\pycoustic\.venv1\Lib\site-packages\pycoustic\survey.py:287: FutureWarning: The behavior of pd.concat with len(keys) != len(objs) is deprecated. In a future version this will raise instead of truncating to the smaller of the two sequences combi = pd.concat(all_pos, axis=1, keys=["UA1", "UA2"])
|
13
|
-
|
13
|
+
#TODO: Survey should make a deep copy of Log objects. Otherwise setting time periods messes it up for other instances.
|
14
14
|
|
15
15
|
class Survey:
|
16
16
|
"""
|
@@ -200,6 +200,7 @@ class Survey:
|
|
200
200
|
return combi
|
201
201
|
|
202
202
|
def counts(self, cols=None, day_t="60min", evening_t="60min", night_t="15min"):
|
203
|
+
#TODO Need to order rows and rename from 'date'
|
203
204
|
"""
|
204
205
|
Returns counts for each time period. For example, this can return the number of L90 occurrences at each decibel
|
205
206
|
level for daytime and night-time periods.
|
@@ -327,3 +328,91 @@ class Survey:
|
|
327
328
|
raise ValueError("No weather history available. Use Survey.weather() first.")
|
328
329
|
return pd.DataFrame([self._weatherhist.min(), self._weatherhist.max(), self._weatherhist.mean()],
|
329
330
|
index=["Min", "Max", "Mean"]).drop(columns=["dt"]).round(decimals=1)
|
331
|
+
|
332
|
+
|
333
|
+
# TODO: Fix this bug in weatherhist
|
334
|
+
# survey.weather(api_key=r"eef3f749e018627b70c2ead1475a1a32", postcode="HA8")
|
335
|
+
# dt temp pressure humidity clouds wind_speed wind_deg \
|
336
|
+
# 0 2025-09-03 08:59:00 17.52 998 97 75 6.69 210
|
337
|
+
# 1 2025-09-03 14:59:00 19.85 997 84 40 9.26 220
|
338
|
+
# 2 2025-09-03 20:59:00 16.27 1003.0 90.0 20.0 4.63 240.0
|
339
|
+
# 3 2025-09-04 02:59:00 14.59 1005.0 91.0 99.0 3.09 230.0
|
340
|
+
# 4 2025-09-04 08:59:00 15.08 1004 93 40 4.12 200
|
341
|
+
# 5 2025-09-04 14:59:00 18.73 1007 63 40 8.75 260
|
342
|
+
# 6 2025-09-04 20:59:00 15.64 1013.0 76.0 0.0 3.6 270.0
|
343
|
+
# 7 2025-09-05 02:59:00 11.42 1016.0 94.0 0.0 3.09 260.0
|
344
|
+
# 8 2025-09-05 08:59:00 14.12 1020.0 89.0 20.0 3.09 270.0
|
345
|
+
# 9 2025-09-05 14:59:00 22.16 1021.0 50.0 0.0 4.12 280.0
|
346
|
+
# 10 2025-09-05 20:59:00 17.38 1023.0 75.0 75.0 3.09 220.0
|
347
|
+
# 11 2025-09-06 02:59:00 14.37 1022.0 83.0 99.0 1.78 187.0
|
348
|
+
# 12 2025-09-06 08:59:00 16.44 1020.0 73.0 100.0 3.48 138.0
|
349
|
+
# 13 2025-09-06 14:59:00 23.21 1037.0 50.0 0.0 7.72 160.0
|
350
|
+
# 14 2025-09-06 20:59:00 18.5 1035.0 75.0 93.0 3.6 120.0
|
351
|
+
# 15 2025-09-07 02:59:00 16.06 1031.0 77.0 84.0 3.09 120.0
|
352
|
+
# 16 2025-09-07 08:59:00 18.78 1029.0 77.0 0.0 4.63 110.0
|
353
|
+
# 17 2025-09-07 14:59:00 23.82 1027.0 67.0 75.0 8.75 200.0
|
354
|
+
# 18 2025-09-07 20:59:00 19.38 1031.0 76.0 72.0 4.63 200.0
|
355
|
+
# 19 2025-09-08 02:59:00 14.49 1034.0 91.0 4.0 1.54 190.0
|
356
|
+
# 20 2025-09-08 08:59:00 14.84 1037.0 85.0 20.0 4.12 240.0
|
357
|
+
# rain wind_gust uvi
|
358
|
+
# 0 {'1h': 0.25} NaN NaN
|
359
|
+
# 1 {'1h': 1.27} 14.92 NaN
|
360
|
+
# 2 NaN NaN NaN
|
361
|
+
# 3 NaN NaN NaN
|
362
|
+
# 4 {'1h': 1.27} NaN NaN
|
363
|
+
# 5 {'3h': 0.13} NaN NaN
|
364
|
+
# 6 NaN NaN NaN
|
365
|
+
# 7 NaN NaN NaN
|
366
|
+
# 8 NaN NaN NaN
|
367
|
+
# 9 NaN NaN NaN
|
368
|
+
# 10 NaN NaN NaN
|
369
|
+
# 11 NaN 3.31 0.0
|
370
|
+
# 12 NaN 7.4 0.86
|
371
|
+
# 13 NaN NaN 2.96
|
372
|
+
# 14 NaN NaN 0.0
|
373
|
+
# 15 NaN NaN 0.0
|
374
|
+
# 16 NaN NaN 1.1
|
375
|
+
# 17 NaN NaN 2.24
|
376
|
+
# 18 NaN NaN 0.0
|
377
|
+
# 19 NaN NaN 0.0
|
378
|
+
# 20 NaN NaN 1.12
|
379
|
+
# survey.weather_summary()
|
380
|
+
# Traceback (most recent call last):
|
381
|
+
# File "<input>", line 1, in <module>
|
382
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pycoustic\survey.py", line 328, in weather_summary
|
383
|
+
# return pd.DataFrame([self._weatherhist.min(), self._weatherhist.max(), self._weatherhist.mean()],
|
384
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^
|
385
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\frame.py", line 11643, in min
|
386
|
+
# result = super().min(axis, skipna, numeric_only, **kwargs)
|
387
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
388
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\generic.py", line 12388, in min
|
389
|
+
# return self._stat_function(
|
390
|
+
# ^^^^^^^^^^^^^^^^^^^^
|
391
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\generic.py", line 12377, in _stat_function
|
392
|
+
# return self._reduce(
|
393
|
+
# ^^^^^^^^^^^^^
|
394
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\frame.py", line 11562, in _reduce
|
395
|
+
# res = df._mgr.reduce(blk_func)
|
396
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^
|
397
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\internals\managers.py", line 1500, in reduce
|
398
|
+
# nbs = blk.reduce(func)
|
399
|
+
# ^^^^^^^^^^^^^^^^
|
400
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\internals\blocks.py", line 404, in reduce
|
401
|
+
# result = func(self.values)
|
402
|
+
# ^^^^^^^^^^^^^^^^^
|
403
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\frame.py", line 11481, in blk_func
|
404
|
+
# return op(values, axis=axis, skipna=skipna, **kwds)
|
405
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
406
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\nanops.py", line 147, in f
|
407
|
+
# result = alt(values, axis=axis, skipna=skipna, **kwds)
|
408
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
409
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\nanops.py", line 404, in new_func
|
410
|
+
# result = func(values, axis=axis, skipna=skipna, mask=mask, **kwargs)
|
411
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
412
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\pandas\core\nanops.py", line 1098, in reduction
|
413
|
+
# result = getattr(values, meth)(axis)
|
414
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
415
|
+
# File "C:\Users\tonyr\PycharmProjects\pycoustic\.venv2\Lib\site-packages\numpy\_core\_methods.py", line 48, in _amin
|
416
|
+
# return umr_minimum(a, axis, None, out, keepdims, initial, where)
|
417
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
418
|
+
# TypeError: '<=' not supported between instances of 'dict' and 'dict'
|
pycoustic/weather.py
CHANGED
@@ -4,8 +4,8 @@ import datetime as dt
|
|
4
4
|
|
5
5
|
|
6
6
|
appid = ""
|
7
|
-
with open("tests/openweather_app_id.txt") as f:
|
8
|
-
|
7
|
+
# with open("tests/openweather_app_id.txt") as f:
|
8
|
+
# appid = f.readlines()[0]
|
9
9
|
|
10
10
|
w_dict = {
|
11
11
|
"start": "2022-09-16 12:00:00",
|
@@ -93,3 +93,4 @@ class WeatherHistory:
|
|
93
93
|
|
94
94
|
def get_weather_history(self):
|
95
95
|
return self._hist
|
96
|
+
|
@@ -1,17 +1,19 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: pycoustic
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.8
|
4
4
|
Summary:
|
5
5
|
Author: thumpercastle
|
6
6
|
Author-email: tony.ryb@gmail.com
|
7
|
-
Requires-Python: >=3.
|
7
|
+
Requires-Python: >=3.11.9,<4.0
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
11
|
Requires-Dist: numpy (==2.2.6)
|
12
12
|
Requires-Dist: openpyxl (==3.1.5)
|
13
13
|
Requires-Dist: pandas (==2.2.3)
|
14
|
+
Requires-Dist: plotly (==6.1.2)
|
14
15
|
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
16
|
+
Requires-Dist: streamlit (>=1.46.1,<2.0.0)
|
15
17
|
Description-Content-Type: text/markdown
|
16
18
|
|
17
19
|
# pycoustic - Toolkit for Analysing Noise Survey Data
|
@@ -70,6 +72,7 @@ pip install pycoustic
|
|
70
72
|
```
|
71
73
|
4. **Analyse the Survey Data**
|
72
74
|
The following are methods of the Survey() object representing the typical use cases for acoustic consultants in the UK.
|
75
|
+
|
73
76
|
### Survey.resi_summary()
|
74
77
|
This method provides a summary of the measurement data for residential projects, with a focus on typical assessment procedures in the UK.
|
75
78
|
It presents A-weighted Leqs for each day and night period (and evenings, if enabled), as well as the nth-highest LAmax during each night-time period.
|
@@ -79,21 +82,26 @@ pip install pycoustic
|
|
79
82
|
**lmax_n** *Int* *(default 10)* The nth-highest value to present.\
|
80
83
|
**lmax_t** *Str* *(default "2min")* The time period T over which Lmaxes are presented. This must be equal to or longer than the period of the raw data.
|
81
84
|
|
82
|
-
## Survey.
|
85
|
+
## Survey.modal()
|
83
86
|
Compute the modal L90 for daytime, evening (if enabled) and night-time periods. By default, this is set to T=60min for (23:00 to 07:00) periods, and T=15min for night-time (23:00 to 07:00) periods, as per BS 4142:2014.
|
84
87
|
|
88
|
+
## Survey.counts()
|
89
|
+
Returns the L90 counts for daytime, evening and night-time periods. You can also choose to include other columns.
|
90
|
+
|
85
91
|
## Survey.lmax_spectra()
|
86
92
|
Compute the Lmax Event spectra for the nth-highest Lmax during each night-time period.\
|
87
93
|
**Note** the date presented alongside the Lmax event is actually the starting date of the night-time period. i.e. an Lmax event with a stamp of 20/12/2024 at 01:22 would actually have occurred on 21/12/2024 at 01:22. These stamps can also sometimes be out by a minute (known bug).
|
88
94
|
|
89
|
-
## Survey.
|
95
|
+
## Survey.leq_spectra()
|
90
96
|
Compute the Leq spectra for daytime, evening (if enabled) and night-time periods. This will present the overall Leqs across the survey, not the Leq for each day.
|
91
|
-
|
97
|
+
|
92
98
|
## Survey.weather()
|
93
99
|
Returns a pandas dataframe of the weather history over the course of your survey.
|
94
100
|
Requires an **api_key** argument. This method makes a call to the OpenWeatherMap OneCall API (see https://openweathermap.org/api). You need to sign up and pass your API key as a string to the weather() method.
|
95
101
|
|
96
|
-
|
102
|
+
## Survey.weather_summary()
|
103
|
+
Returns a pandas dataframe summary of the weather history, comprising maximum, minimum and mean values.
|
104
|
+
You must have called Survey.weather() at least once before to get the summary.
|
97
105
|
|
98
106
|
### Other methods
|
99
107
|
### Known issues
|
@@ -0,0 +1,10 @@
|
|
1
|
+
pycoustic/__init__.py,sha256=jq9Tzc5nEgXh8eNf0AkAypmw3Dda9A-iSy-tyFaTksA,89
|
2
|
+
pycoustic/log.py,sha256=S9Vc2hgHShd2YnDAdBs6by_8hE6l1HIoPgZ0po-KT5I,17614
|
3
|
+
pycoustic/pycoustic_gui_app.py,sha256=Hs61Y8fAp7uoRONa4RLSVl0UvGXZZ96n5eJGilErlAU,11143
|
4
|
+
pycoustic/streamlit-ai.py,sha256=bGYPvrHmQEFua8IrJhBNAdXDzUnQvcjINaRx2-F-5Ms,18637
|
5
|
+
pycoustic/survey.py,sha256=pvdiR4wn1Zicr7rL1fZ5IgUnls5Ikg2j1RFw36WgcB0,23471
|
6
|
+
pycoustic/tkgui.py,sha256=YAy5f_qkXZ3yU8BvB-nIVQX1fYwPs_IkwmDEXHPMAa4,13997
|
7
|
+
pycoustic/weather.py,sha256=q9FbDKjY0WaNvaYMHeDk7Bhbq0_Q7ehsTM_vUaCjeAk,3753
|
8
|
+
pycoustic-0.1.8.dist-info/METADATA,sha256=AqC1SlTm94A-qFF0Sm13c2JZW4pVBdFfsYWWHBymN2o,8413
|
9
|
+
pycoustic-0.1.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
+
pycoustic-0.1.8.dist-info/RECORD,,
|
pycoustic-0.1.6.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
pycoustic/__init__.py,sha256=jq9Tzc5nEgXh8eNf0AkAypmw3Dda9A-iSy-tyFaTksA,89
|
2
|
-
pycoustic/log.py,sha256=HNdS2hKKbUdqY7iAMj9QJqoI9r4ZtJ7GCXnIx8XpTH4,17145
|
3
|
-
pycoustic/survey.py,sha256=KTNCt4kV63Dq06RCbh9G9Nl8Frk8NsTt6AhxiMIEixg,17746
|
4
|
-
pycoustic/tkgui.py,sha256=YAy5f_qkXZ3yU8BvB-nIVQX1fYwPs_IkwmDEXHPMAa4,13997
|
5
|
-
pycoustic/weather.py,sha256=_fA62mZxBCoQcIe_gziQ2WgDaWqr4cIW_6YqTXmz_is,3747
|
6
|
-
pycoustic-0.1.6.dist-info/METADATA,sha256=4K-HhUHP4yxvKveG9TzZRxIxDPmU13s6_9Cae8dmH8E,7987
|
7
|
-
pycoustic-0.1.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
8
|
-
pycoustic-0.1.6.dist-info/RECORD,,
|
File without changes
|