pycoustic 0.1.0__py3-none-any.whl → 0.1.2__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 CHANGED
@@ -0,0 +1,2 @@
1
+ from .log import Log
2
+ from .survey import Survey
pycoustic/log.py CHANGED
@@ -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,5 +1,6 @@
1
1
  import pandas as pd
2
2
  import numpy as np
3
+ from .weather import WeatherHistory
3
4
 
4
5
 
5
6
  class Survey:
@@ -281,6 +282,11 @@ class Survey:
281
282
  combi = combi.transpose()
282
283
  return combi
283
284
 
285
+ def get_start_end(self):
286
+ starts = [self._logs[key].get_start() for key in self._logs.keys()]
287
+ ends = [self._logs[key].get_end() for key in self._logs.keys()]
288
+ return min(starts), max(ends)
289
+
284
290
  # def typical_leq_spectra(self, leq_cols=None):
285
291
  # """
286
292
  # DEPRECATED 2025/06/05. Replaced by .leq_spectra() **TT**
pycoustic/tkgui.py ADDED
@@ -0,0 +1,348 @@
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
+
15
+ class Application(tk.Tk):
16
+
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.title("src Log Viewer")
20
+ self.geometry("1200x800")
21
+ import tkinter as tk
22
+
23
+ # Create an instance of Survey
24
+ self.survey = Survey()
25
+
26
+ self.create_widgets()
27
+
28
+ def create_widgets(self):
29
+
30
+ self.grid_columnconfigure((0,1,2), weight=1)
31
+
32
+ self.grid_columnconfigure((3,4,5), weight=2)
33
+
34
+ self.log_label = ttk.Label(self, text="csv file name:")
35
+ self.log_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
36
+
37
+ self.log_file = ttk.Label(self, text="Click Browse to select")
38
+ self.log_file.grid(row=0, column=1, padx=5, pady=5, sticky="w")
39
+
40
+ self.browse_button1 = ttk.Button(self, text="Browse", command=self.browse_log)
41
+ self.browse_button1.grid(row=0, column=2, padx=5, pady=5, sticky="w")
42
+
43
+ self.analysis_label = ttk.Label(self, text="Select Analysis Type:")
44
+ self.analysis_label.grid(row=3, column=0, padx=5, pady=10, sticky="e")
45
+
46
+ self.analysis_var = StringVar()
47
+ self.analysis_combobox = ttk.Combobox(self, textvariable=self.analysis_var, values=["resi_summary", "modal_l90", "lmax_spectra", "Typical_leq_spectra"])
48
+ self.analysis_combobox.set("resi_summary")
49
+ self.analysis_combobox.grid(row=3, column=1, padx=5, pady=10, sticky="w")
50
+ self.analysis_var.trace("w", self.on_analysischange)
51
+
52
+ self.parameters_label = ttk.Label(self, text="Parameters")
53
+ self.parameters_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
54
+
55
+ self.parameters_entry = ttk.Entry(self, width=20)
56
+ self.parameters_entry.insert(0, 'None,None,10,2min')
57
+ self.parameters_entry.grid(row=4, column=1, padx=5, pady=10, sticky="w")
58
+
59
+ self.parameters_label = ttk.Label(self, text="(leq_cols,max_cols,lmax_n,lmax_t )")
60
+ self.parameters_label.grid(row=4, column=2, padx=5, pady=5, sticky="w")
61
+
62
+ self.execute_button = ttk.Button(self, text="Select Columns", command=self.Column_Selection_Modal)
63
+ self.execute_button.grid(row=5, column=0, padx=5, pady=10, sticky="e")
64
+
65
+ self.execute_button = ttk.Button(self, text="Execute", command=self.execute_code)
66
+ self.execute_button.grid(row=5, column=2, padx=5, pady=10, sticky="w")
67
+
68
+ self.tree = ttk.Treeview(self, show="headings")
69
+ self.tree_scroll_y = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
70
+ self.tree_scroll_y.grid(row=6, column=6, sticky="ns")
71
+
72
+ self.tree_scroll_x = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
73
+ self.tree_scroll_x.grid(row=7, column=0, columnspan=3, sticky="ew")
74
+
75
+ self.tree.configure(yscrollcommand=self.tree_scroll_y.set, xscrollcommand=self.tree_scroll_x.set)
76
+
77
+ self.tree.grid(row=6, column=0, columnspan=4, padx=20, pady=20, sticky="nsew")
78
+ """
79
+ # Button to open the modal dialog
80
+ open_dialog_button = ttk.Button(self, text="Show Time Series", command=self.open_modal_dialog)
81
+ open_dialog_button.grid(row=5, column=2, padx=5, pady=10, sticky="w")
82
+
83
+
84
+ # Commented out section that displays a graph
85
+ def open_modal_dialog(self):
86
+ dialog = Toplevel(self)
87
+ dialog.title("Modal Dialog")
88
+
89
+ # Make the dialog modal
90
+ dialog.transient(self)
91
+ dialog.grab_set()
92
+
93
+ # Center the dialog on the screen
94
+ window_width = self.winfo_width()
95
+ window_height = self.winfo_height()
96
+ window_x = self.winfo_x()
97
+ window_y = self.winfo_y()
98
+
99
+ dialog_width = 900
100
+ dialog_height = 600
101
+
102
+ position_right = int(window_x + (window_width / 2) - (dialog_width / 2))
103
+ position_down = int(window_y + (window_height / 2) - (dialog_height / 2))
104
+
105
+ dialog.geometry(f"{dialog_width}x{dialog_height}+{position_right}+{position_down}")
106
+ import matplotlib.pyplot as plt
107
+
108
+ # Create a figure and axis
109
+ fig, ax = plt.subplots()
110
+
111
+ # Plot the data
112
+ #print ("----",self.df.columns[1][0],self.df.columns[1][1] )
113
+ #print ("----",type(self.df.columns[1]))
114
+
115
+ leq_a_column = [col for col in self.df.columns if isinstance(col, tuple) and col[0] == "Leq" and col[1] == "A"]
116
+ if not leq_a_column:
117
+ messagebox.showerror("Error", "No columns found with ('Leq', 'A') in the header.")
118
+ return
119
+
120
+ l90_a_columns = [col for col in self.df.columns if isinstance(col, tuple) and col[0] == "L90" and col[1] == "A"]
121
+ if not l90_a_columns:
122
+ messagebox.showerror("Error", "No columns found with ('L90', 'A') in the header.")
123
+ return
124
+
125
+
126
+ self.df["Date"] = pd.to_datetime(self.df.index)
127
+ #print(self.df)
128
+
129
+ ax.plot(self.df["Date"], self.df[leq_a_column[0]], label='LAeq')
130
+ ax.plot(self.df["Date"], self.df[l90_a_column[0]], label='LA90')
131
+ #ax.plot(self.df['Index'], self.df['L90 A'], label='la90')
132
+
133
+ # Format the x-axis to show dates properly
134
+ fig.autofmt_xdate()
135
+
136
+ # Add labels and title
137
+ ax.set_xlabel('Date')
138
+ ax.set_ylabel('Values')
139
+ ax.set_title('Time Series Line Chart')
140
+ ax.legend()
141
+
142
+ # Create a canvas to display the plot in the Tkinter dialog
143
+ canvas = FigureCanvasTkAgg(fig, master=dialog)
144
+ canvas.draw()
145
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
146
+ #else:
147
+ # messagebox.showerror("Error", "DataFrame does not contain required columns: 'date', 'Laeq', 'la90'")
148
+
149
+ # OK button to close the dialog
150
+ ok_button = ttk.Button(dialog, text="OK", command=dialog.destroy)
151
+ ok_button.pack(pady=20)
152
+
153
+ """
154
+
155
+ def on_analysischange(self, *args):
156
+ analysis_type = self.analysis_combobox.get()
157
+ if analysis_type == "resi_summary":
158
+ self.parameters_entry.config(state='normal')
159
+ else:
160
+ self.parameters_entry.config(state='disabled')
161
+ return
162
+
163
+
164
+ def browse_log(self):
165
+ self.logpath = os.getcwd ()
166
+ file_path = tk.filedialog.askopenfilename(initialdir=self.logpath, filetypes=[("CSV files", "*.csv")])
167
+ if file_path:
168
+ self.logname = os.path.basename(file_path)
169
+ self.logpath = os.path.dirname(file_path)
170
+ self.log_file.config(text=self.logname)
171
+
172
+ strPath = self.logpath + "\\" + self.logname
173
+ self.log = Log(path=strPath)
174
+ self.survey.add_log(data=self.log, name="Position 1")
175
+
176
+ self.df = self.log.get_data()
177
+
178
+ # Clear the treeview
179
+ for item in self.tree.get_children():
180
+ self.tree.delete(item)
181
+
182
+ # Round the dataframe values to 1 decimal place, except for the column headers
183
+ self.df = self.df.round(1)
184
+
185
+ # Insert new data into the treeview
186
+ self.tree["columns"] = ["Index"] + list(self.df.columns)
187
+ self.tree.heading("Index", text="Index")
188
+ self.tree.column("Index", width=150, stretch=True)
189
+
190
+ i = 0
191
+ for col in self.df.columns:
192
+ self.tree.heading(col, text=col)
193
+ i=i+1
194
+ if i < 12:
195
+ self.tree.column(col, width=75,stretch=True, anchor="center")
196
+ else:
197
+ self.tree.column(col, width=0,stretch=False, anchor="center")
198
+
199
+ for index, row in self.df.iterrows():
200
+ self.tree.insert("", "end", values=[index] + list(row))
201
+ return
202
+
203
+ def execute_code(self):
204
+ analysis_type = self.analysis_combobox.get()
205
+ parameters = self.parameters_entry.get()
206
+
207
+ try:
208
+ df = pd.DataFrame()
209
+ if analysis_type == "resi_summary":
210
+ params = parameters.split(",")
211
+ p = [None if x == "None" else x for x in params]
212
+ df =self.survey.resi_summary(leq_cols=p[0], max_cols=p[1], lmax_n=int(params[2]), lmax_t=params[3])
213
+ print ("df id a ",type(df))
214
+ elif analysis_type == "modal_l90":
215
+ df = self.survey.modal()
216
+ elif analysis_type == "lmax_spectra":
217
+ df = self.survey.lmax_spectra()
218
+ elif analysis_type == "Leq_spectra":
219
+ df = self.survey.leq_spectra()
220
+ else:
221
+ messagebox.showerror("Error", "Please select an analysis type.")
222
+ return
223
+ except Exception as e:
224
+ messagebox.showerror("Error", f"An error occurred: {e}")
225
+ return
226
+
227
+ # Clear the treeview
228
+ for item in self.tree.get_children():
229
+ self.tree.delete(item)
230
+
231
+ # Set new column headers based on the dataframe
232
+ #self.tree["columns"] = list(df.columns)
233
+
234
+ # Insert new data into the treeview
235
+ self.tree["columns"] = ["Index"] + list(df.columns)
236
+ self.tree.heading("Index", text="Index")
237
+ self.tree.column("Index", width=150, anchor="center", stretch=True)
238
+
239
+
240
+ for col in df.columns:
241
+ self.tree.heading(col, text=col)
242
+ self.tree.column(col, width=75, anchor="center", stretch=True)
243
+
244
+ # Insert new data into the treeview
245
+ for index, row in df.iterrows():
246
+ self.tree.insert("", "end", values=[index] + list(row))
247
+
248
+ # Copy the DataFrame to the clipboard
249
+ df.to_clipboard(index=True)
250
+ messagebox.showinfo("Success", "DataFrame copied to clipboard.")
251
+
252
+
253
+ def Column_Selection_Modal(self):
254
+ # Create a modal dialog
255
+ #print ("def Column_Selection_Modal(self):")
256
+
257
+ dialog = Toplevel(self)
258
+ dialog.title("Select Columns")
259
+
260
+ # Make the dialog modal
261
+ dialog.transient(self)
262
+ dialog.grab_set()
263
+
264
+ #self.window.update_idletasks()
265
+ window_width = self.winfo_width()
266
+ window_height = self.winfo_height()
267
+ window_x = self.winfo_x()
268
+ window_y = self.winfo_y()
269
+
270
+ dialog_width = 300
271
+ dialog_height = 600
272
+
273
+ position_right = int(window_x + (window_width / 2) - (dialog_width / 2))
274
+ position_down = int(window_y + (window_height / 2) - (dialog_height / 2))
275
+
276
+ dialog.geometry(f"{dialog_width}x{dialog_height}+{position_right}+{position_down}")
277
+
278
+ # Configure grid rows and columns
279
+ for i in range(12): # Adjust the range as needed
280
+ dialog.rowconfigure(i,weight =1,uniform = 'a')
281
+
282
+ for i in range(3): # Adjust the range as needed
283
+ dialog.grid_columnconfigure(i, weight =1,minsize = 20,uniform = 'a')
284
+
285
+ # Get the column identifiers
286
+ column_ids = self.tree["columns"]
287
+ #print ("col ids",column_ids)
288
+
289
+ # Get the column headers
290
+ column_headers = [self.tree.heading(col)["text"] for col in column_ids]
291
+
292
+ # Get the column widths
293
+ column_widths = [self.tree.column(col)["width"] for col in column_ids]
294
+
295
+
296
+ self.column_vars = {}
297
+ for i, column in enumerate(column_headers):
298
+ # Check if the column is visible in self.tree
299
+ self.column_vars[column] = IntVar(master=dialog, value=1 if column_widths[i] > 0 else 0)
300
+ self.cb = []
301
+
302
+ # Create checkboxes for each column
303
+ for i, column in enumerate(column_headers):
304
+ #print(f"Column: {column}, Value: {self.column_vars[column].get()}")
305
+ self.cb.append( Checkbutton(dialog, text=column, variable=self.column_vars[column]))
306
+ #self.cb[i].grid(row=i+1, column=(i-1)/10, sticky='w')
307
+ self.cb[i].grid(row=i%10, column=i//10, sticky='w')
308
+
309
+ for i, column in enumerate(column_headers):
310
+ self.column_vars[column].set(1 if column_widths[i] > 0 else 0)
311
+
312
+ # OK button to apply the selection
313
+ ttk.Button(dialog, text="OK", command=lambda: self.apply_column_selection(dialog)).grid(row=13, column=1, ipady=10)
314
+ # Cancel button
315
+ def on_cancel():
316
+ #print("Cancel clicked")
317
+ dialog.destroy()
318
+
319
+ cancel_button = ttk.Button(dialog, text="Cancel", command=on_cancel)
320
+ cancel_button.grid(row=13,column = 2, sticky = "w",ipady = 10)
321
+
322
+ return
323
+
324
+ def apply_column_selection(self, dialog):
325
+
326
+ #print("def apply_column_selection")
327
+
328
+ # Get the selected columns
329
+ selected_columns = [column for column, var in self.column_vars.items() if var.get() == 1]
330
+
331
+ # Set the column widths based on the selection
332
+ for column in self.tree["columns"]:
333
+ column_text = self.tree.heading(column)["text"]
334
+ if column_text in selected_columns:
335
+ self.tree.column(column, width=250,stretch=True, anchor="center") # Set to a default width
336
+ else:
337
+ self.tree.column(column, width=0,stretch=False, anchor="center") # Hide the column
338
+
339
+ # Display the selected columns in the listbox
340
+ #for item in self.tree:
341
+ # display_text = " | ".join(str(item[column]) for column in selected_columns)
342
+ # self.listbox.insert(END, display_text)
343
+ # Close the dialog
344
+ dialog.destroy()
345
+
346
+ if __name__ == "__main__":
347
+ app = Application()
348
+ app.mainloop()
pycoustic/weather.py ADDED
@@ -0,0 +1,94 @@
1
+ import requests
2
+ import pandas as pd
3
+ import datetime as dt
4
+
5
+
6
+ appid = ""
7
+ with open("tests/openweather_app_id.txt") as f:
8
+ appid = f.readlines()[0]
9
+
10
+ w_dict = {
11
+ "start": "2022-09-16 12:00:00",
12
+ "end": "2022-09-17 18:00:00",
13
+ "interval": 6,
14
+ "api_key": appid,
15
+ "country": "GB",
16
+ "postcode": "WC1",
17
+ "tz": "GB"
18
+ }
19
+
20
+ def test_weather_obj(weather_test_dict):
21
+ hist = WeatherHistory(start=w_dict["start"], end=w_dict["end"], interval=w_dict["interval"],
22
+ api_key=w_dict["api_key"], country=w_dict["country"], postcode=w_dict["postcode"],
23
+ tz=w_dict["tz"])
24
+ hist.compute_weather_history()
25
+ return hist
26
+
27
+ #TODO: Make this take the start and end times of a Survey object.
28
+ #TODO: Implement post codes instead of coordinates
29
+ class WeatherHistory:
30
+ def __init__(self, start=None, end=None, interval=6, api_key="", country="GB", postcode="WC1", tz="",
31
+ units="metric"):
32
+ # self._history_df = pd.DataFrame()
33
+ if type(start) == str:
34
+ self._start = dt.datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
35
+ else:
36
+ self._start = start
37
+ if type(end) == str:
38
+ self._end = dt.datetime.strptime(end, "%Y-%m-%d %H:%M:%S")
39
+ else:
40
+ self._end = end
41
+ self._interval = interval
42
+ self._api_key = str(api_key)
43
+ self._lat, self._lon = self.get_latlon(api_key=api_key, country=country, postcode=postcode)
44
+ self._hist = None
45
+ self._units = units
46
+
47
+ def get_latlon(self, api_key="", country="GB", postcode=""):
48
+ query = str("http://api.openweathermap.org/geo/1.0/zip?zip=" + postcode + "," + country + "&appid=" + api_key)
49
+ resp = requests.get(query)
50
+ return resp.json()["lat"], resp.json()["lon"]
51
+
52
+ def _construct_api_call(self, timestamp):
53
+ base = "https://api.openweathermap.org/data/3.0/onecall/timemachine?"
54
+ query = str(base + "lat=" + str(self._lat) + "&" + "lon=" + str(self._lon) + "&" + "units=" + self._units + \
55
+ "&" + "dt=" + str(timestamp) + "&" + "appid=" + self._api_key)
56
+ print(query)
57
+ return query
58
+
59
+ def _construct_timestamps(self):
60
+ next_time = (self._start + dt.timedelta(hours=self._interval))
61
+ timestamps = [int(self._start.timestamp())]
62
+ while next_time < self._end:
63
+ timestamps.append(int(next_time.timestamp()))
64
+ next_time += dt.timedelta(hours=self._interval)
65
+ return timestamps
66
+
67
+ def _make_and_parse_api_call(self, query):
68
+ response = requests.get(query)
69
+ print(response.json())
70
+ # This drops some unwanted cols like lat, lon, timezone and tz offset.
71
+ resp_dict = response.json()["data"][0]
72
+ del resp_dict["weather"] # delete weather key as not useful.
73
+ # TODO: parse 'weather' nested dict.
74
+ return resp_dict
75
+
76
+ def compute_weather_history(self):
77
+ # construct timestamps
78
+ timestamps = self._construct_timestamps()
79
+ # make calls to API
80
+ responses = []
81
+ for ts in timestamps:
82
+ print(f"ts: {ts}")
83
+ query = self._construct_api_call(timestamp=ts)
84
+ response_dict = self._make_and_parse_api_call(query=query)
85
+ responses.append(pd.Series(response_dict))
86
+ df = pd.concat(responses, axis=1).transpose()
87
+ for col in ["dt", "sunrise", "sunset"]:
88
+ df[col] = df[col].apply(lambda x: dt.datetime.fromtimestamp(int(x))) # convert timestamp into datetime
89
+ print(df)
90
+ self._hist = df
91
+ return df
92
+
93
+ def get_weather_history(self):
94
+ return self._hist
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pycoustic
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary:
5
5
  Author: thumpercastle
6
6
  Author-email: tony.ryb@gmail.com
@@ -8,9 +8,12 @@ Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
11
13
  Requires-Dist: numpy (==2.2.6)
12
14
  Requires-Dist: openpyxl (==3.1.5)
13
15
  Requires-Dist: pandas (==2.2.3)
16
+ Requires-Dist: streamlit (==1.46.0)
14
17
  Description-Content-Type: text/markdown
15
18
 
16
19
 
@@ -0,0 +1,8 @@
1
+ pycoustic/__init__.py,sha256=Midt6cul_fLEpsI002sx-Yi_-gZV2Hs36mxIlnDs82I,48
2
+ pycoustic/log.py,sha256=pXSm01MHJ3gNGpJq14_lRV_NrF9S1ruHTK_77CDhsHs,17160
3
+ pycoustic/survey.py,sha256=SqttTArZuldrafOf7fMNSQLEuHo8oqM89KYxqTAtQ88,17897
4
+ pycoustic/tkgui.py,sha256=SGTAvJg1OzUcmfdi97TwThRvAPBJHPKKJi-jyrshors,13975
5
+ pycoustic/weather.py,sha256=bKdCQrn_Dg6WHIXXKUOvgfVn7n6r6AoVBBMcdlu7ViM,3739
6
+ pycoustic-0.1.2.dist-info/METADATA,sha256=t2f3wkScvHapwUHK5cElbmO4aR34oclGB2gEpNxvto8,574
7
+ pycoustic-0.1.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
8
+ pycoustic-0.1.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.5.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +0,0 @@
1
- pycoustic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pycoustic/log.py,sha256=TagYIfubFl5BnHwSPunELYejbr13mJvLoENCkHkSaJ4,17054
3
- pycoustic/survey.py,sha256=fv5K7s-fOYks8ei-NRa19b-zWlT_ZQuDJMjKgYesXXM,17639
4
- pycoustic-0.1.0.dist-info/METADATA,sha256=DpBXurGEo1WU6bXqW71oAKu2j9lE9swIMX7U97KvJfU,436
5
- pycoustic-0.1.0.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
6
- pycoustic-0.1.0.dist-info/RECORD,,