pycoustic 0.1.1__py3-none-any.whl → 0.1.3__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/__init__.py +2 -1
- pycoustic/log.py +7 -1
- pycoustic/survey.py +19 -0
- pycoustic/tkgui.py +349 -0
- pycoustic/weather.py +99 -0
- pycoustic-0.1.3.dist-info/METADATA +103 -0
- pycoustic-0.1.3.dist-info/RECORD +8 -0
- {pycoustic-0.1.1.dist-info → pycoustic-0.1.3.dist-info}/WHEEL +1 -1
- pycoustic-0.1.1.dist-info/METADATA +0 -16
- pycoustic-0.1.1.dist-info/RECORD +0 -6
pycoustic/__init__.py
CHANGED
pycoustic/log.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import pandas as pd
|
2
2
|
import numpy as np
|
3
3
|
import datetime as dt
|
4
|
-
|
4
|
+
|
5
5
|
|
6
6
|
|
7
7
|
class Log:
|
@@ -356,3 +356,9 @@ class Log:
|
|
356
356
|
return False
|
357
357
|
else:
|
358
358
|
return True
|
359
|
+
|
360
|
+
def get_start(self):
|
361
|
+
return self._start
|
362
|
+
|
363
|
+
def get_end(self):
|
364
|
+
return self._end
|
pycoustic/survey.py
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
import pandas as pd
|
2
2
|
import numpy as np
|
3
|
+
from .weather import WeatherHistory
|
3
4
|
|
4
5
|
|
6
|
+
DECIMALS=0
|
7
|
+
|
8
|
+
# pd.set_option('display.max_columns', None)
|
9
|
+
# pd.set_option('display.max_rows', None)
|
10
|
+
|
5
11
|
class Survey:
|
6
12
|
"""
|
7
13
|
Survey Class is an overarching class which takes multiple Log objects and processes and summarises them together.
|
@@ -12,6 +18,7 @@ class Survey:
|
|
12
18
|
|
13
19
|
def __init__(self):
|
14
20
|
self._logs = {}
|
21
|
+
self._weather = WeatherHistory()
|
15
22
|
|
16
23
|
def _insert_multiindex(self, df=None, super=None, name1="Position", name2="Date"):
|
17
24
|
subs = df.index.to_list() # List of subheaders (dates)
|
@@ -281,6 +288,18 @@ class Survey:
|
|
281
288
|
combi = combi.transpose()
|
282
289
|
return combi
|
283
290
|
|
291
|
+
def get_start_end(self):
|
292
|
+
starts = [self._logs[key].get_start() for key in self._logs.keys()]
|
293
|
+
ends = [self._logs[key].get_end() for key in self._logs.keys()]
|
294
|
+
return min(starts), max(ends)
|
295
|
+
|
296
|
+
def weather(self, interval=6, api_key="", country="GB", postcode="WC1", tz="",):
|
297
|
+
start, end = self.get_start_end()
|
298
|
+
self._weather.reinit(start=start, end=end, interval=interval, api_key=api_key, country=country,
|
299
|
+
postcode=postcode, tz=tz, units="metric")
|
300
|
+
self._weather.compute_weather_history()
|
301
|
+
return self._weather.get_weather_history()
|
302
|
+
|
284
303
|
# def typical_leq_spectra(self, leq_cols=None):
|
285
304
|
# """
|
286
305
|
# DEPRECATED 2025/06/05. Replaced by .leq_spectra() **TT**
|
pycoustic/tkgui.py
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
import tkinter as tk
|
2
|
+
from tkinter import filedialog ,Toplevel, IntVar, Checkbutton, Button
|
3
|
+
from tkinter import ttk
|
4
|
+
from tkinter import messagebox
|
5
|
+
from log import Log
|
6
|
+
from survey import Survey
|
7
|
+
import os
|
8
|
+
import pandas as pd
|
9
|
+
from tkinter import StringVar
|
10
|
+
#import matplotlib
|
11
|
+
#from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
12
|
+
|
13
|
+
#from src import log # Assuming log is an instance of a class with the required methods
|
14
|
+
test = 0
|
15
|
+
|
16
|
+
class Application(tk.Tk):
|
17
|
+
|
18
|
+
def __init__(self):
|
19
|
+
super().__init__()
|
20
|
+
self.title("src Log Viewer")
|
21
|
+
self.geometry("1200x800")
|
22
|
+
import tkinter as tk
|
23
|
+
|
24
|
+
# Create an instance of Survey
|
25
|
+
self.survey = Survey()
|
26
|
+
|
27
|
+
self.create_widgets()
|
28
|
+
|
29
|
+
def create_widgets(self):
|
30
|
+
|
31
|
+
self.grid_columnconfigure((0,1,2), weight=1)
|
32
|
+
|
33
|
+
self.grid_columnconfigure((3,4,5), weight=2)
|
34
|
+
|
35
|
+
self.log_label = ttk.Label(self, text="csv file name:")
|
36
|
+
self.log_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
|
37
|
+
|
38
|
+
self.log_file = ttk.Label(self, text="Click Browse to select")
|
39
|
+
self.log_file.grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
40
|
+
|
41
|
+
self.browse_button1 = ttk.Button(self, text="Browse", command=self.browse_log)
|
42
|
+
self.browse_button1.grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
43
|
+
|
44
|
+
self.analysis_label = ttk.Label(self, text="Select Analysis Type:")
|
45
|
+
self.analysis_label.grid(row=3, column=0, padx=5, pady=10, sticky="e")
|
46
|
+
|
47
|
+
self.analysis_var = StringVar()
|
48
|
+
self.analysis_combobox = ttk.Combobox(self, textvariable=self.analysis_var, values=["resi_summary", "modal_l90", "lmax_spectra", "Typical_leq_spectra"])
|
49
|
+
self.analysis_combobox.set("resi_summary")
|
50
|
+
self.analysis_combobox.grid(row=3, column=1, padx=5, pady=10, sticky="w")
|
51
|
+
self.analysis_var.trace("w", self.on_analysischange)
|
52
|
+
|
53
|
+
self.parameters_label = ttk.Label(self, text="Parameters")
|
54
|
+
self.parameters_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
|
55
|
+
|
56
|
+
self.parameters_entry = ttk.Entry(self, width=20)
|
57
|
+
self.parameters_entry.insert(0, 'None,None,10,2min')
|
58
|
+
self.parameters_entry.grid(row=4, column=1, padx=5, pady=10, sticky="w")
|
59
|
+
|
60
|
+
self.parameters_label = ttk.Label(self, text="(leq_cols,max_cols,lmax_n,lmax_t )")
|
61
|
+
self.parameters_label.grid(row=4, column=2, padx=5, pady=5, sticky="w")
|
62
|
+
|
63
|
+
self.execute_button = ttk.Button(self, text="Select Columns", command=self.Column_Selection_Modal)
|
64
|
+
self.execute_button.grid(row=5, column=0, padx=5, pady=10, sticky="e")
|
65
|
+
|
66
|
+
self.execute_button = ttk.Button(self, text="Execute", command=self.execute_code)
|
67
|
+
self.execute_button.grid(row=5, column=2, padx=5, pady=10, sticky="w")
|
68
|
+
|
69
|
+
self.tree = ttk.Treeview(self, show="headings")
|
70
|
+
self.tree_scroll_y = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
|
71
|
+
self.tree_scroll_y.grid(row=6, column=6, sticky="ns")
|
72
|
+
|
73
|
+
self.tree_scroll_x = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
|
74
|
+
self.tree_scroll_x.grid(row=7, column=0, columnspan=3, sticky="ew")
|
75
|
+
|
76
|
+
self.tree.configure(yscrollcommand=self.tree_scroll_y.set, xscrollcommand=self.tree_scroll_x.set)
|
77
|
+
|
78
|
+
self.tree.grid(row=6, column=0, columnspan=4, padx=20, pady=20, sticky="nsew")
|
79
|
+
"""
|
80
|
+
# Button to open the modal dialog
|
81
|
+
open_dialog_button = ttk.Button(self, text="Show Time Series", command=self.open_modal_dialog)
|
82
|
+
open_dialog_button.grid(row=5, column=2, padx=5, pady=10, sticky="w")
|
83
|
+
|
84
|
+
|
85
|
+
# Commented out section that displays a graph
|
86
|
+
def open_modal_dialog(self):
|
87
|
+
dialog = Toplevel(self)
|
88
|
+
dialog.title("Modal Dialog")
|
89
|
+
|
90
|
+
# Make the dialog modal
|
91
|
+
dialog.transient(self)
|
92
|
+
dialog.grab_set()
|
93
|
+
|
94
|
+
# Center the dialog on the screen
|
95
|
+
window_width = self.winfo_width()
|
96
|
+
window_height = self.winfo_height()
|
97
|
+
window_x = self.winfo_x()
|
98
|
+
window_y = self.winfo_y()
|
99
|
+
|
100
|
+
dialog_width = 900
|
101
|
+
dialog_height = 600
|
102
|
+
|
103
|
+
position_right = int(window_x + (window_width / 2) - (dialog_width / 2))
|
104
|
+
position_down = int(window_y + (window_height / 2) - (dialog_height / 2))
|
105
|
+
|
106
|
+
dialog.geometry(f"{dialog_width}x{dialog_height}+{position_right}+{position_down}")
|
107
|
+
import matplotlib.pyplot as plt
|
108
|
+
|
109
|
+
# Create a figure and axis
|
110
|
+
fig, ax = plt.subplots()
|
111
|
+
|
112
|
+
# Plot the data
|
113
|
+
#print ("----",self.df.columns[1][0],self.df.columns[1][1] )
|
114
|
+
#print ("----",type(self.df.columns[1]))
|
115
|
+
|
116
|
+
leq_a_column = [col for col in self.df.columns if isinstance(col, tuple) and col[0] == "Leq" and col[1] == "A"]
|
117
|
+
if not leq_a_column:
|
118
|
+
messagebox.showerror("Error", "No columns found with ('Leq', 'A') in the header.")
|
119
|
+
return
|
120
|
+
|
121
|
+
l90_a_columns = [col for col in self.df.columns if isinstance(col, tuple) and col[0] == "L90" and col[1] == "A"]
|
122
|
+
if not l90_a_columns:
|
123
|
+
messagebox.showerror("Error", "No columns found with ('L90', 'A') in the header.")
|
124
|
+
return
|
125
|
+
|
126
|
+
|
127
|
+
self.df["Date"] = pd.to_datetime(self.df.index)
|
128
|
+
#print(self.df)
|
129
|
+
|
130
|
+
ax.plot(self.df["Date"], self.df[leq_a_column[0]], label='LAeq')
|
131
|
+
ax.plot(self.df["Date"], self.df[l90_a_column[0]], label='LA90')
|
132
|
+
#ax.plot(self.df['Index'], self.df['L90 A'], label='la90')
|
133
|
+
|
134
|
+
# Format the x-axis to show dates properly
|
135
|
+
fig.autofmt_xdate()
|
136
|
+
|
137
|
+
# Add labels and title
|
138
|
+
ax.set_xlabel('Date')
|
139
|
+
ax.set_ylabel('Values')
|
140
|
+
ax.set_title('Time Series Line Chart')
|
141
|
+
ax.legend()
|
142
|
+
|
143
|
+
# Create a canvas to display the plot in the Tkinter dialog
|
144
|
+
canvas = FigureCanvasTkAgg(fig, master=dialog)
|
145
|
+
canvas.draw()
|
146
|
+
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
147
|
+
#else:
|
148
|
+
# messagebox.showerror("Error", "DataFrame does not contain required columns: 'date', 'Laeq', 'la90'")
|
149
|
+
|
150
|
+
# OK button to close the dialog
|
151
|
+
ok_button = ttk.Button(dialog, text="OK", command=dialog.destroy)
|
152
|
+
ok_button.pack(pady=20)
|
153
|
+
|
154
|
+
"""
|
155
|
+
|
156
|
+
def on_analysischange(self, *args):
|
157
|
+
analysis_type = self.analysis_combobox.get()
|
158
|
+
if analysis_type == "resi_summary":
|
159
|
+
self.parameters_entry.config(state='normal')
|
160
|
+
else:
|
161
|
+
self.parameters_entry.config(state='disabled')
|
162
|
+
return
|
163
|
+
|
164
|
+
|
165
|
+
def browse_log(self):
|
166
|
+
self.logpath = os.getcwd ()
|
167
|
+
file_path = tk.filedialog.askopenfilename(initialdir=self.logpath, filetypes=[("CSV files", "*.csv")])
|
168
|
+
if file_path:
|
169
|
+
self.logname = os.path.basename(file_path)
|
170
|
+
self.logpath = os.path.dirname(file_path)
|
171
|
+
self.log_file.config(text=self.logname)
|
172
|
+
|
173
|
+
strPath = self.logpath + "\\" + self.logname
|
174
|
+
self.log = Log(path=strPath)
|
175
|
+
self.survey.add_log(data=self.log, name="Position 1")
|
176
|
+
|
177
|
+
self.df = self.log.get_data()
|
178
|
+
|
179
|
+
# Clear the treeview
|
180
|
+
for item in self.tree.get_children():
|
181
|
+
self.tree.delete(item)
|
182
|
+
|
183
|
+
# Round the dataframe values to 1 decimal place, except for the column headers
|
184
|
+
self.df = self.df.round(1)
|
185
|
+
|
186
|
+
# Insert new data into the treeview
|
187
|
+
self.tree["columns"] = ["Index"] + list(self.df.columns)
|
188
|
+
self.tree.heading("Index", text="Index")
|
189
|
+
self.tree.column("Index", width=150, stretch=True)
|
190
|
+
|
191
|
+
i = 0
|
192
|
+
for col in self.df.columns:
|
193
|
+
self.tree.heading(col, text=col)
|
194
|
+
i=i+1
|
195
|
+
if i < 12:
|
196
|
+
self.tree.column(col, width=75,stretch=True, anchor="center")
|
197
|
+
else:
|
198
|
+
self.tree.column(col, width=0,stretch=False, anchor="center")
|
199
|
+
|
200
|
+
for index, row in self.df.iterrows():
|
201
|
+
self.tree.insert("", "end", values=[index] + list(row))
|
202
|
+
return
|
203
|
+
|
204
|
+
def execute_code(self):
|
205
|
+
analysis_type = self.analysis_combobox.get()
|
206
|
+
parameters = self.parameters_entry.get()
|
207
|
+
|
208
|
+
try:
|
209
|
+
df = pd.DataFrame()
|
210
|
+
if analysis_type == "resi_summary":
|
211
|
+
params = parameters.split(",")
|
212
|
+
p = [None if x == "None" else x for x in params]
|
213
|
+
df =self.survey.resi_summary(leq_cols=p[0], max_cols=p[1], lmax_n=int(params[2]), lmax_t=params[3])
|
214
|
+
print ("df id a ",type(df))
|
215
|
+
elif analysis_type == "modal_l90":
|
216
|
+
df = self.survey.modal()
|
217
|
+
elif analysis_type == "lmax_spectra":
|
218
|
+
df = self.survey.lmax_spectra()
|
219
|
+
elif analysis_type == "Leq_spectra":
|
220
|
+
df = self.survey.leq_spectra()
|
221
|
+
else:
|
222
|
+
messagebox.showerror("Error", "Please select an analysis type.")
|
223
|
+
return
|
224
|
+
except Exception as e:
|
225
|
+
messagebox.showerror("Error", f"An error occurred: {e}")
|
226
|
+
return
|
227
|
+
|
228
|
+
# Clear the treeview
|
229
|
+
for item in self.tree.get_children():
|
230
|
+
self.tree.delete(item)
|
231
|
+
|
232
|
+
# Set new column headers based on the dataframe
|
233
|
+
#self.tree["columns"] = list(df.columns)
|
234
|
+
|
235
|
+
# Insert new data into the treeview
|
236
|
+
self.tree["columns"] = ["Index"] + list(df.columns)
|
237
|
+
self.tree.heading("Index", text="Index")
|
238
|
+
self.tree.column("Index", width=150, anchor="center", stretch=True)
|
239
|
+
|
240
|
+
|
241
|
+
for col in df.columns:
|
242
|
+
self.tree.heading(col, text=col)
|
243
|
+
self.tree.column(col, width=75, anchor="center", stretch=True)
|
244
|
+
|
245
|
+
# Insert new data into the treeview
|
246
|
+
for index, row in df.iterrows():
|
247
|
+
self.tree.insert("", "end", values=[index] + list(row))
|
248
|
+
|
249
|
+
# Copy the DataFrame to the clipboard
|
250
|
+
df.to_clipboard(index=True)
|
251
|
+
messagebox.showinfo("Success", "DataFrame copied to clipboard.")
|
252
|
+
|
253
|
+
|
254
|
+
def Column_Selection_Modal(self):
|
255
|
+
# Create a modal dialog
|
256
|
+
#print ("def Column_Selection_Modal(self):")
|
257
|
+
|
258
|
+
dialog = Toplevel(self)
|
259
|
+
dialog.title("Select Columns")
|
260
|
+
|
261
|
+
# Make the dialog modal
|
262
|
+
dialog.transient(self)
|
263
|
+
dialog.grab_set()
|
264
|
+
|
265
|
+
#self.window.update_idletasks()
|
266
|
+
window_width = self.winfo_width()
|
267
|
+
window_height = self.winfo_height()
|
268
|
+
window_x = self.winfo_x()
|
269
|
+
window_y = self.winfo_y()
|
270
|
+
|
271
|
+
dialog_width = 300
|
272
|
+
dialog_height = 600
|
273
|
+
|
274
|
+
position_right = int(window_x + (window_width / 2) - (dialog_width / 2))
|
275
|
+
position_down = int(window_y + (window_height / 2) - (dialog_height / 2))
|
276
|
+
|
277
|
+
dialog.geometry(f"{dialog_width}x{dialog_height}+{position_right}+{position_down}")
|
278
|
+
|
279
|
+
# Configure grid rows and columns
|
280
|
+
for i in range(12): # Adjust the range as needed
|
281
|
+
dialog.rowconfigure(i,weight =1,uniform = 'a')
|
282
|
+
|
283
|
+
for i in range(3): # Adjust the range as needed
|
284
|
+
dialog.grid_columnconfigure(i, weight =1,minsize = 20,uniform = 'a')
|
285
|
+
|
286
|
+
# Get the column identifiers
|
287
|
+
column_ids = self.tree["columns"]
|
288
|
+
#print ("col ids",column_ids)
|
289
|
+
|
290
|
+
# Get the column headers
|
291
|
+
column_headers = [self.tree.heading(col)["text"] for col in column_ids]
|
292
|
+
|
293
|
+
# Get the column widths
|
294
|
+
column_widths = [self.tree.column(col)["width"] for col in column_ids]
|
295
|
+
|
296
|
+
|
297
|
+
self.column_vars = {}
|
298
|
+
for i, column in enumerate(column_headers):
|
299
|
+
# Check if the column is visible in self.tree
|
300
|
+
self.column_vars[column] = IntVar(master=dialog, value=1 if column_widths[i] > 0 else 0)
|
301
|
+
self.cb = []
|
302
|
+
|
303
|
+
# Create checkboxes for each column
|
304
|
+
for i, column in enumerate(column_headers):
|
305
|
+
#print(f"Column: {column}, Value: {self.column_vars[column].get()}")
|
306
|
+
self.cb.append( Checkbutton(dialog, text=column, variable=self.column_vars[column]))
|
307
|
+
#self.cb[i].grid(row=i+1, column=(i-1)/10, sticky='w')
|
308
|
+
self.cb[i].grid(row=i%10, column=i//10, sticky='w')
|
309
|
+
|
310
|
+
for i, column in enumerate(column_headers):
|
311
|
+
self.column_vars[column].set(1 if column_widths[i] > 0 else 0)
|
312
|
+
|
313
|
+
# OK button to apply the selection
|
314
|
+
ttk.Button(dialog, text="OK", command=lambda: self.apply_column_selection(dialog)).grid(row=13, column=1, ipady=10)
|
315
|
+
# Cancel button
|
316
|
+
def on_cancel():
|
317
|
+
#print("Cancel clicked")
|
318
|
+
dialog.destroy()
|
319
|
+
|
320
|
+
cancel_button = ttk.Button(dialog, text="Cancel", command=on_cancel)
|
321
|
+
cancel_button.grid(row=13,column = 2, sticky = "w",ipady = 10)
|
322
|
+
|
323
|
+
return
|
324
|
+
|
325
|
+
def apply_column_selection(self, dialog):
|
326
|
+
|
327
|
+
#print("def apply_column_selection")
|
328
|
+
|
329
|
+
# Get the selected columns
|
330
|
+
selected_columns = [column for column, var in self.column_vars.items() if var.get() == 1]
|
331
|
+
|
332
|
+
# Set the column widths based on the selection
|
333
|
+
for column in self.tree["columns"]:
|
334
|
+
column_text = self.tree.heading(column)["text"]
|
335
|
+
if column_text in selected_columns:
|
336
|
+
self.tree.column(column, width=250,stretch=True, anchor="center") # Set to a default width
|
337
|
+
else:
|
338
|
+
self.tree.column(column, width=0,stretch=False, anchor="center") # Hide the column
|
339
|
+
|
340
|
+
# Display the selected columns in the listbox
|
341
|
+
#for item in self.tree:
|
342
|
+
# display_text = " | ".join(str(item[column]) for column in selected_columns)
|
343
|
+
# self.listbox.insert(END, display_text)
|
344
|
+
# Close the dialog
|
345
|
+
dialog.destroy()
|
346
|
+
|
347
|
+
if __name__ == "__main__":
|
348
|
+
app = Application()
|
349
|
+
app.mainloop()
|
pycoustic/weather.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
import requests
|
2
|
+
import pandas as pd
|
3
|
+
import datetime as dt
|
4
|
+
|
5
|
+
test=0
|
6
|
+
|
7
|
+
appid = ""
|
8
|
+
with open("tests/openweather_app_id.txt") as f:
|
9
|
+
appid = f.readlines()[0]
|
10
|
+
|
11
|
+
w_dict = {
|
12
|
+
"start": "2022-09-16 12:00:00",
|
13
|
+
"end": "2022-09-17 18:00:00",
|
14
|
+
"interval": 6,
|
15
|
+
"api_key": appid,
|
16
|
+
"country": "GB",
|
17
|
+
"postcode": "WC1",
|
18
|
+
"tz": "GB"
|
19
|
+
}
|
20
|
+
|
21
|
+
def test_weather_obj(weather_test_dict):
|
22
|
+
hist = WeatherHistory(start=w_dict["start"], end=w_dict["end"], interval=w_dict["interval"],
|
23
|
+
api_key=w_dict["api_key"], country=w_dict["country"], postcode=w_dict["postcode"],
|
24
|
+
tz=w_dict["tz"])
|
25
|
+
hist.compute_weather_history()
|
26
|
+
return hist
|
27
|
+
|
28
|
+
#TODO: Make this take the start and end times of a Survey object.
|
29
|
+
#TODO: Implement post codes instead of coordinates
|
30
|
+
class WeatherHistory:
|
31
|
+
def __init__(self):
|
32
|
+
return
|
33
|
+
|
34
|
+
def reinit(self, start=None, end=None, interval=6, api_key="", country="GB", postcode="WC1", tz="",
|
35
|
+
units="metric"):
|
36
|
+
if api_key==None:
|
37
|
+
raise ValueError("API key is missing")
|
38
|
+
if type(start) == str:
|
39
|
+
self._start = dt.datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
|
40
|
+
else:
|
41
|
+
self._start = start
|
42
|
+
if type(end) == str:
|
43
|
+
self._end = dt.datetime.strptime(end, "%Y-%m-%d %H:%M:%S")
|
44
|
+
else:
|
45
|
+
self._end = end
|
46
|
+
self._interval = interval
|
47
|
+
self._api_key = str(api_key)
|
48
|
+
self._lat, self._lon = self.get_latlon(api_key=api_key, country=country, postcode=postcode)
|
49
|
+
self._hist = None
|
50
|
+
self._units = units
|
51
|
+
|
52
|
+
def get_latlon(self, api_key="", country="GB", postcode=""):
|
53
|
+
query = str("http://api.openweathermap.org/geo/1.0/zip?zip=" + postcode + "," + country + "&appid=" + api_key)
|
54
|
+
resp = requests.get(query)
|
55
|
+
return resp.json()["lat"], resp.json()["lon"]
|
56
|
+
|
57
|
+
def _construct_api_call(self, timestamp):
|
58
|
+
base = "https://api.openweathermap.org/data/3.0/onecall/timemachine?"
|
59
|
+
query = str(base + "lat=" + str(self._lat) + "&" + "lon=" + str(self._lon) + "&" + "units=" + self._units + \
|
60
|
+
"&" + "dt=" + str(timestamp) + "&" + "appid=" + self._api_key)
|
61
|
+
print(query)
|
62
|
+
return query
|
63
|
+
|
64
|
+
def _construct_timestamps(self):
|
65
|
+
next_time = (self._start + dt.timedelta(hours=self._interval))
|
66
|
+
timestamps = [int(self._start.timestamp())]
|
67
|
+
while next_time < self._end:
|
68
|
+
timestamps.append(int(next_time.timestamp()))
|
69
|
+
next_time += dt.timedelta(hours=self._interval)
|
70
|
+
return timestamps
|
71
|
+
|
72
|
+
def _make_and_parse_api_call(self, query):
|
73
|
+
response = requests.get(query)
|
74
|
+
print(response.json())
|
75
|
+
# This drops some unwanted cols like lat, lon, timezone and tz offset.
|
76
|
+
resp_dict = response.json()["data"][0]
|
77
|
+
del resp_dict["weather"] # delete weather key as not useful.
|
78
|
+
# TODO: parse 'weather' nested dict.
|
79
|
+
return resp_dict
|
80
|
+
|
81
|
+
def compute_weather_history(self):
|
82
|
+
# construct timestamps
|
83
|
+
timestamps = self._construct_timestamps()
|
84
|
+
# make calls to API
|
85
|
+
responses = []
|
86
|
+
for ts in timestamps:
|
87
|
+
print(f"ts: {ts}")
|
88
|
+
query = self._construct_api_call(timestamp=ts)
|
89
|
+
response_dict = self._make_and_parse_api_call(query=query)
|
90
|
+
responses.append(pd.Series(response_dict))
|
91
|
+
df = pd.concat(responses, axis=1).transpose()
|
92
|
+
for col in ["dt", "sunrise", "sunset"]:
|
93
|
+
df[col] = df[col].apply(lambda x: dt.datetime.fromtimestamp(int(x))) # convert timestamp into datetime
|
94
|
+
print(df)
|
95
|
+
self._hist = df
|
96
|
+
return df
|
97
|
+
|
98
|
+
def get_weather_history(self):
|
99
|
+
return self._hist
|
@@ -0,0 +1,103 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: pycoustic
|
3
|
+
Version: 0.1.3
|
4
|
+
Summary:
|
5
|
+
Author: thumpercastle
|
6
|
+
Author-email: tony.ryb@gmail.com
|
7
|
+
Requires-Python: >=3.10,<4.0
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Requires-Dist: numpy (==2.2.6)
|
14
|
+
Requires-Dist: openpyxl (==3.1.5)
|
15
|
+
Requires-Dist: pandas (==2.2.3)
|
16
|
+
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
|
19
|
+
# pycoustic - Toolkit for Analysing Noise Survey Data
|
20
|
+
## Overview
|
21
|
+
pycoustic is a Python-based toolkit designed to assist acoustic consultants and engineers in analyzing noise survey data. The library provides various tools and utilities for processing, interpreting, and visualizing noise measurements.
|
22
|
+
|
23
|
+
## Requirements
|
24
|
+
This toolkit was written in Python 3.12. See the pyrpoject.toml for the latest dependencies.
|
25
|
+
|
26
|
+
## Features
|
27
|
+
- Import and process noise survey data from .csv files
|
28
|
+
- Analyse and recompute sound levels and statistical meausres (Leq, Lmax, L90 etc.)
|
29
|
+
- Handle large datasets from multiple measurement positions efficiently
|
30
|
+
- Visualise noise data through customisable plots (WIP)
|
31
|
+
- Generate reports with summary statistics (WIP)
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
The library is not currently available through pip or Anaconda (we're working on it). You can pull the code from git, or simply download the files on this GitHub page.
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
**Before you start**
|
38
|
+
Make sure your input data is in the correct format. See the file UA1_py.csv for reference. Namely:
|
39
|
+
- Your data must be in .csv format.
|
40
|
+
- Timestamps should ideally be in the format *dd-mm-yyyy hh:mm*, other formats may work, but their performance is not tried and tested.
|
41
|
+
- Column headers should be in the Tuple-like format as per the example csv files attached. The measurement index should be first, and the frequency band or weighting should be second. e.g. the LAeq column should have the header *"Leq A"*, the L90 column at 125 Hz should have the header *"L90 125"*, and so on.
|
42
|
+
- Make sure you past your data and columns into a fresh csv tab. If you end up having to chop the data and delete columns or rows, repaste it into a fresh tab when it is ready to be presented to the toolkit. Failure to do so can result in a ValueError. See **Troubleshooting** below.
|
43
|
+
- If you do use this toolkit, please attribute it appropriately, and carry out your own checks to ensure you are satisfied with the outputs. See **Terms of Use** below.
|
44
|
+
### Basic Workflow
|
45
|
+
1. **Import the library**\
|
46
|
+
Import the library into your script or active console.
|
47
|
+
```
|
48
|
+
from pycoustic import*
|
49
|
+
```
|
50
|
+
2. **Load Data**\
|
51
|
+
A single measurement position can be loaded into a Log object.
|
52
|
+
```
|
53
|
+
log1 = Log(path="path/to/data_for_pos1.csv")
|
54
|
+
log2 = Log(path="path/to/data_for_pos2.csv")
|
55
|
+
```
|
56
|
+
3. **Combine Data**\
|
57
|
+
The data from multiple measurement positions can be combined together within a Survey object.
|
58
|
+
First we need to create a Survey object, and then add each Log one at a time.
|
59
|
+
```
|
60
|
+
surv = Survey()
|
61
|
+
surv.add_log(data=log1, name="Position 1")
|
62
|
+
surv.add_log(data=log2, name="Position 2")
|
63
|
+
```
|
64
|
+
4. **Analyse the Survey Data**
|
65
|
+
The following are methods of the Survey() object representing the typical use cases for acoustic consultants in the UK.
|
66
|
+
### Survey.resi_summary()
|
67
|
+
This method provides a summary of the measurement data for residential projects, with a focus on typical assessment procedures in the UK.
|
68
|
+
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.
|
69
|
+
Optional arguments are:\
|
70
|
+
**leq_cols** *List of tuples* *(default [("Leq", "A")]* Which column(s) you want to present as Leqs - this can be any Leq or statistical column.\
|
71
|
+
**max_cols** *List of tuples* *(default [("Leq", "A")]* Which column(s) you want to present as an nth-highest value - this can be any column.\
|
72
|
+
**lmax_n** *Int* *(default 10)* The nth-highest value to present.\
|
73
|
+
**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.
|
74
|
+
|
75
|
+
### Survey.modal_l90()
|
76
|
+
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.
|
77
|
+
|
78
|
+
### Survey.lmax_spectra()
|
79
|
+
Compute the Lmax Event spectra for the nth-highest Lmax during each night-time period.\
|
80
|
+
**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).
|
81
|
+
|
82
|
+
### Survey.typical_leq_spectra()
|
83
|
+
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.
|
84
|
+
|
85
|
+
|
86
|
+
### Other methods
|
87
|
+
### Known issues
|
88
|
+
- Lmax night-time timestamps can sometimes by out by a minute.
|
89
|
+
- [RESOLVED 07/02/2025] modal_L90 keyword 'cols' does not work due to headers not being properly allocated.
|
90
|
+
## Troubleshooting
|
91
|
+
### ValueError: NaTType does not support time
|
92
|
+
This error occurs when trying to create a Log() object with a csv file. It occurs because the source csv file contains empty cells which were previously allocated (filled). It usually happens when you have entered data into some row(s) or column(s) and then deleted it, leaving previously-full cells which are now empty.\
|
93
|
+
**Solution:** Create a new tab in your source csv file, and paste in your headers and data as you wish it to be presented to the toolkit, avoiding having to delete any columns and rows. Delete the old tab. If you do have to delete any data in the new tab, you will need to repeat the process to ensure this error is not thrown up again.
|
94
|
+
|
95
|
+
## Terms of use
|
96
|
+
The pycoustic toolkit was built by Tony Trup of [Timbral](https://www.timbral.co.uk).
|
97
|
+
Neither I nor Timbral Ltd. accept any liability for the outputs of this toolkit. You use it at your own risk, and you should carry out your own checks and balances to ensure you are satistfied that the output is accurate.
|
98
|
+
This is an open source project, and I welcome suggestions for changes, improvements or new features. You can also write your own methods or functions and share them with me, either by getting in touch offline, or by creating a new branch from this Git repository.
|
99
|
+
You may use this toolkit subject to the licence conditions below. For clarity, you may use this toolkit or adaptations of it in your day-to-day engineering work, but incorporating it into a commercial software product or service is not permitted.
|
100
|
+
This project is being shared under a [Creative Commons CC BY-NC-SA 4.0 Licence](https://creativecommons.org/licenses/by-nc-sa/4.0/).
|
101
|
+
Attribution — You must give appropriate credit , provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
102
|
+
NonCommercial — You may not use the material for commercial purposes.
|
103
|
+
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
pycoustic/__init__.py,sha256=0UGHX-vdulM9x1j8g_DcR_BbtBk0gEFg9l4KjLl4Qrk,85
|
2
|
+
pycoustic/log.py,sha256=HNdS2hKKbUdqY7iAMj9QJqoI9r4ZtJ7GCXnIx8XpTH4,17145
|
3
|
+
pycoustic/survey.py,sha256=6bQW1UniUgUdj7jOpqTagdos6IRVPhURegmq9PgBYjQ,18453
|
4
|
+
pycoustic/tkgui.py,sha256=vvB0D29GER-i7lC_SUuzKrzbh1EFAAhpRaHi7kggtYk,13985
|
5
|
+
pycoustic/weather.py,sha256=xEp5-ZYlYJXB-G2QQuEETTfyMALAWJtSWtLn5XHDRFo,3822
|
6
|
+
pycoustic-0.1.3.dist-info/METADATA,sha256=nJy_wiOSY2K08rQKE2z420Ln0MtTqoXKQvRsFuCfzQo,7476
|
7
|
+
pycoustic-0.1.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
8
|
+
pycoustic-0.1.3.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: pycoustic
|
3
|
-
Version: 0.1.1
|
4
|
-
Summary:
|
5
|
-
Author: thumpercastle
|
6
|
-
Author-email: tony.ryb@gmail.com
|
7
|
-
Requires-Python: >=3.10,<4.0
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: Programming Language :: Python :: 3.10
|
10
|
-
Classifier: Programming Language :: Python :: 3.11
|
11
|
-
Requires-Dist: numpy (==2.2.6)
|
12
|
-
Requires-Dist: openpyxl (==3.1.5)
|
13
|
-
Requires-Dist: pandas (==2.2.3)
|
14
|
-
Description-Content-Type: text/markdown
|
15
|
-
|
16
|
-
|
pycoustic-0.1.1.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pycoustic/__init__.py,sha256=Midt6cul_fLEpsI002sx-Yi_-gZV2Hs36mxIlnDs82I,48
|
2
|
-
pycoustic/log.py,sha256=TagYIfubFl5BnHwSPunELYejbr13mJvLoENCkHkSaJ4,17054
|
3
|
-
pycoustic/survey.py,sha256=fv5K7s-fOYks8ei-NRa19b-zWlT_ZQuDJMjKgYesXXM,17639
|
4
|
-
pycoustic-0.1.1.dist-info/METADATA,sha256=WQMdpIRk-aLBHE0efTyRMv29mlvPLWPNys5U_-r8GFA,436
|
5
|
-
pycoustic-0.1.1.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
|
6
|
-
pycoustic-0.1.1.dist-info/RECORD,,
|