From 9d5ba2d7c21d60d78a0d394642024477fd0db0e0 Mon Sep 17 00:00:00 2001 From: bryce Date: Sun, 17 Aug 2025 10:51:49 +1200 Subject: [PATCH] POC Working Only one seismograph at a time - all options working - Spectrograph mode to be in a later update - NEXT: "Area - based" config (allows yout to select areas) --- .gitignore | 4 + assets/styles/app_stlyes.qss | 0 config/app_styles.qss | 66 +++++++++ src/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 162 bytes src/__pycache__/main.cpython-313.pyc | Bin 0 -> 844 bytes src/data/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 167 bytes .../__pycache__/geonet_api.cpython-313.pyc | Bin 0 -> 2311 bytes .../miniseed_parser.cpython-313.pyc | Bin 0 -> 2454 bytes src/data/geonet_api.py | 55 ++++++++ src/data/miniseed_parser.py | 55 ++++++++ src/main.py | 7 +- src/ui/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 165 bytes .../__pycache__/main_window.cpython-313.pyc | Bin 0 -> 8483 bytes .../waveform_chart.cpython-313.pyc | Bin 0 -> 3342 bytes src/ui/main_window.py | 132 ++++++++++++++++-- src/ui/waveform_chart.py | 48 +++++++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 168 bytes .../__pycache__/mpl_styles.cpython-313.pyc | Bin 0 -> 1317 bytes src/utils/mpl_styles.py | 28 ++++ 19 files changed, 385 insertions(+), 10 deletions(-) delete mode 100644 assets/styles/app_stlyes.qss create mode 100644 config/app_styles.qss create mode 100644 src/__pycache__/__init__.cpython-313.pyc create mode 100644 src/__pycache__/main.cpython-313.pyc create mode 100644 src/data/__pycache__/__init__.cpython-313.pyc create mode 100644 src/data/__pycache__/geonet_api.cpython-313.pyc create mode 100644 src/data/__pycache__/miniseed_parser.cpython-313.pyc create mode 100644 src/data/geonet_api.py create mode 100644 src/data/miniseed_parser.py create mode 100644 src/ui/__pycache__/__init__.cpython-313.pyc create mode 100644 src/ui/__pycache__/main_window.cpython-313.pyc create mode 100644 src/ui/__pycache__/waveform_chart.cpython-313.pyc create mode 100644 src/ui/waveform_chart.py create mode 100644 src/utils/__pycache__/__init__.cpython-313.pyc create mode 100644 src/utils/__pycache__/mpl_styles.cpython-313.pyc create mode 100644 src/utils/mpl_styles.py diff --git a/.gitignore b/.gitignore index e69de29..71fb7e7 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +## git ignores these +# + + venv/ diff --git a/assets/styles/app_stlyes.qss b/assets/styles/app_stlyes.qss deleted file mode 100644 index e69de29..0000000 diff --git a/config/app_styles.qss b/config/app_styles.qss new file mode 100644 index 0000000..f13ac94 --- /dev/null +++ b/config/app_styles.qss @@ -0,0 +1,66 @@ +QMainWindow { + background-color: #A7FAFC; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Halvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +QLabel, QGroupBox, QWidget, QComboBox, QPushButton { + color: #1A202C; +} + +QLabel#headerLabel { + font-size: 32px; + font-weight: bold; + color: #1A202C; + padding-bottom: 10px; +} + +QLabel { + font-size: 16px; + color: #4A5568; +} + +QLabel[property="controlLabel"] { + font-weight: bold; + color: #333333; +} + +QComboBox { + padding: 5px; + border: 1px solid #CBD5E0; + border-radius: 4ps; + background-color: white; +} + +QComboBox::drop-down { + border-left: 1px solid #CBD5E0; + width: 20px; +} + +QComboBox::down-arrow { + +} + +QPushButton { + background-color: #3B82F6; + color: white; + border: none; + padding: 8px 16px; + border-radius: 6px; + font-weight: bold; +} + +QPushButton:hover { + background-color: #2563EB; +} + +QPushButton:disabled { + background-color: #9CA3AF; + color: #E5E7EB; +} + +QWidget#waveformChartContainer { + background-color: white; + border: 1px solid #E2E8F0; + border-radius: 8px; + padding: 16px; +} diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa6c62d74befbe3f3d99bc1fd479509db6d7cb3e GIT binary patch literal 162 zcmey&%ge<81ikO)Wq|0%AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklFPk+1S3G~wunE8)6(ma= zg5C~-b0|TH6oC&s;RIu?sIBN+ bVq$??&>SJ0=uVi=zacUO0Q5`F#pKb#<20+!A zgN0$^9MnmjSed^nUbBdt*m$wO-}GE3^1=XPbJg*JEib5rZH%TIEXKBo;wBRf#v*sv zr|(i|b^LnY(VRmC*$h@C;Q^a8$&LKNa9;qGOp+}l(~|Z?%Ryx<$t5!fq)ax)lkCVR zbv!fr-^8e%pgAbR=429$d~!aS2eko9Upvay9KL7oHd`#;@tdeZDrzvFSq4{Rr7mJ2 zsl)4sTnWGxLrFy=Y)i^IUZgB@Eluhuo=ms~`7@G0!2loN|+j^Wz8_eFf|eC}MD zx#O3&^zNt9#pDkCvS=^P3v)m%2L8*?znKZ#%c~ literal 0 HcmV?d00001 diff --git a/src/data/__pycache__/__init__.cpython-313.pyc b/src/data/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a9f6397e105e10a5e8f657075c7b1036674696a GIT binary patch literal 167 zcmey&%ge<81ikO)Wq|0%AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekklPO4oIE6`YwwZ$ODM`lJw#v*1Q3jjRMD^~yj literal 0 HcmV?d00001 diff --git a/src/data/__pycache__/geonet_api.cpython-313.pyc b/src/data/__pycache__/geonet_api.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..200d5f20e8f02fb16a97f3be0f9efbf50dbe3e99 GIT binary patch literal 2311 zcmaJ@O>7fK6rT0(dL91+#}Iy$3^ZxtHreGzK}`rhfslrPEUk#U`x@)9w0V#)?xlJ%OB&oRBq6t2$5GSN)sgtAg0bWQVg+~z|7 zes1{f4nZ&v&>~;6TgnN}S%C2vn&`}QPsX$8b+$6@v@j`*15mm*?aH8xD3#=z%>Rg)#v^$>n?-BZAUlJunl<3yX*T z$1d`AyF5DU9z=N|5mkQx@TECoY6Ze@Rb3@{gO)HS8qQOrgg>--h?A+Ccy2bG#2t>x zk~Bk?*p!5!F;g{*hK@&>NfT4Ar<#!?c$COXG9I~g`PxWg%+}Tls;-k|N86e1)xJ1? z+tFS&w10e@&x4;kH)|dKVK)4GLf3z`mhkm<2*R3K+sH>Pxm0 zFv=2C7k?)(%ay!M`Gtiwk(VD?)Bv2>zZ4Q-%B!E{tC*T@R4Ffw zR-niXPHEM&yk$Vlw0eNcS8@^T~z7hK4MKHD<9B2jywu4GDsO%yaoF;}E(doxG9$#vNFKmf3Eiv@hvtVC$ zPu^VF)EeQFTjD9cx8nt)KiT^L(6@(Y!j}Z}-Fy3?eZdY1FM9e9)!VWY@4b5kR6#9UitOE2X=*_HuETdEbOFwjdnpq^j+vI tak^yWD$C>?9fK)7fK6rQ!c_S(TAX@01JNr%uTF^TO`#GirERJhHLLLwWhr7l<-dmJyZciq`F zAw)=RPdP$WgnDW?Rh4=GwG#By6H+-5(+XPym5|y?dkV^-ReR~1wbu)+%_y1ocHWye z-}~mxjJN!LAA&X{-N?^N2>r@GtmbGaTf?ASLkW}+X3(@i17>qta{y7- zq9f?~olgo1%teWuJ(UsoXQOq1bi)7E9}xK(8bqn4HD#@Z;x5bGmP06s4m5ldLc(#x zp$ZYCI+8*UdRLeXBBVN#&bZs2VQF#5>Ax**X8io9>VkM;%5RNAv`$O6rG=pfP0k}! z2F#_!q$|~o7w@utEe)`@M)s2BVaudDBqEe>9uYz?s7gsO-e+4aEh(jTGQ(U=7fZp zbhTLpTd{4K6cg@nlSd2{H9Kyx_bS?rQbU%n2g$v#FW%H^)=0vW^md~C&oEC=LQ=gG z-kgv_$DOaBoDlTNpFpYj=4GlA9Tyc<(Fw`og%yL~tZc|wQ4I}i3wmh<>js>gB7B@D z8AA{IPE%Rca~dt`4F`kDnP;cTxS;3;)^a$jWDG@9Wvaj`dRZ})g#y7a7ncf}VJPZi z*yo#|i+a?Dq4AZ|9D|NWO%aUZ(|Llayli7M;Zc%N$OS;0qgoLg;4?8l1+n79C{wi& zj-!a;iI!-_@p7I}f{C2Tn=mmYB}#On8akG-nNl4T2gci~39+u%`;kmuR#j3MiE@s{ z|8Lg~nWx{@t`cKeqf2e=1ugU90I&cB8jA!%0562At#HW2Qh}r?cz|QBpvguHuaX9v zu7iWMd#fWCpN)%0R;<~VpUd^KFD^s`5Sv}}r!bK*mTylg$K@amZ$Oe0zRpx2X$0Um<=4}$f zI&?c}=C0>Lmh^^B zwXXi_{;U4=>F<(v_J8|9W$>+sT@z0|Xy@yjKIF&MZNrsq!?lA)Z!g_isvexG9GrT= z$K2`rtM^u`a~b%%S-!n`Yqff4=E0$v4W(E+9IHi7{4nI{2N_|?{wdBZgd{|Svp>m{QqoO7I#$%O`j?AjU^#Mn=XWW*`dy*={PC literal 0 HcmV?d00001 diff --git a/src/ui/__pycache__/main_window.cpython-313.pyc b/src/ui/__pycache__/main_window.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed2497e9b153c8c47374f16bd61b6e591d018c34 GIT binary patch literal 8483 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 literal 0 HcmV?d00001 diff --git a/src/ui/__pycache__/waveform_chart.cpython-313.pyc b/src/ui/__pycache__/waveform_chart.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..544994ac93beac119351d4240c150f516ee4c010 GIT binary patch literal 3342 zcmb7G-ESMm5#PJx$4VsiX%UiG3zQEU!dQYRgG z^xn~y{80FzD6GPOTcAeWr!qx>Do^ddkpIApSTJ2!sDTCn^j1VEk{~agz2ovILw@KI z+?}1BnVp@z`OO~1V!a63uYOeD_#ueUzuAO0n7i$06NYCEk&=uj zE(dZ!5_D;4Ig|?%?9zedo?I{Kb?M-8Bo`$ymkurW<@!l~5{;rMB!|b5j6V|QByTG+ zPzt7d4%s9v(eTO#MxmrTG_vykqP2Zf-LYy84XxZ%*L5>3P;te9e(;)6su6ujt=w1b zmAh9;C18d;=4~Jc)N0k-QOZ?Qv7H@Lw>57suWxURj^<(h3pnOShzK$wqAU;v__UbIMgX+tH>AqHET*tJavXtvW4 zf&M+=8CvTs-W6BT#}MjOVNURvNwmg8k5&=qt|F8B3fw%i_*%>NyvBg3EO@`UfVY-> zlWycBUA{MX6yxi2Z31}xoF~H>=Gx~4KiTo!27V53+8^`7KFz7MQ=H1jIpvS}QJ?0N z@7o)qk>Ha4c(OmfrM1&sR+i*|99)2NnVnOvP4ZQ+Gr?(p{8rCs}{Tm*td<5=zHz6ZUyN?BjWwbQ3i#9y0hl zWXW+@fG#c4aM38)#zUP-g{pCAF)ciEFRdqtKq8Js!Q53>lu_0S`maK}SR~-A6N=in>roF(EtEy&H zN`&PyVK6`=j^UWPVlvR9Q4qOe8l_4Z6bS5ow`uGu4A%hLRL3>RV9q0h+inns!6&7x zZdyd~*T8FSOR1_vE!#Be7^Mvdj=%sMfIVRAW|4+q72KpfilUjSZ7Yh+5H6W~M042< ztE^|&$&RLHuUcA-9oNp@A=aj@0a1CV1I}*TtEpT1eM5hsldMg&Y|Y3%XaljLF(l4Z zcgQ$9c${Cnd+!;3CoIbLyMVjjJwh*WY&YD%qtD|_oO~IdtWWR9&pZx1iF|>hPb0gL z7EU#As(x>8aPR!yN)u;0{D}shcz&;mC%*}yp0Oq#|1vT8V(P-*ukI%n9tWCu6eL@C z6l}NfOassSWxbV}Z=~j1sl`TWv5A*DmQFYD>F0&kM5Zy3fgQl!squPhPiqWc08jAC z_*DJkUoU-fX+M7Mm}j2epSJ$E)x>YV8jAnn?C;LDhEk28RNdYm`Z36c!Pa=PH8%VC z*zDfs{uns+q>t?z-VHyE?Z#R--N5O+>Az(@g`F4LD=nO8;6#0RkGx>ig;$_}cX11| z4eM{cz^6aQsUrv?(mKTnChWUq^P^uvXrHl?1C!TVzFVWcxGEk4hj!8G*35^yXCBRS zAHYH2D5rjmU^a~2muxYMY;hRf08Q&1SzylH5pTiOM^F+~0u|{*Iq@~QAjqNoNgkb0 zc0c?%FNZE*xo@oFe&;&{)#rDe1LgaCKBw}XvcfqC`8p*yWfJ}ssn|uOrQ3wnRx0eI zdkCu_Gz|HASKoHX88!=QCgc(gu)HU4F-}aaIaaakDBBDG$PD8J3PgR74v{RIgsPSS z=}3bvGEt#QL)!|-sx-9Yjy40Pq}#{^*Cey9@Z6+{2d_C)3v(SfJecdtLaxqA~*JbISR zZ|vS^MW-6ksb+MVecNuDvqb#|9{>L92#ROErz5j=6x{ht=v$l%d^XazcsT%pV6k&U zdq8Y z0d{kyv7T91wJp6;usNaJbLP}iiLhiP>_+KemoyY1c(8jFX`qYm)tjI@5SGaF6rg&h z<`|}(@!#)WZDdFM6nTemzcKu-7vJ{?LmL(hn>{Z6BME}=FLbGaE*+r60lIU5rVr5M U0XqLpASEPzd;Px%2G`F208I$7RR910 literal 0 HcmV?d00001 diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 79db347..99f39d6 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,24 +1,138 @@ - -from PyQt6.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget -from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QComboBox, QHBoxLayout +from PyQt6.QtCore import Qt, QTimer +from src.ui.waveform_chart import WaveformChart +from src.data.geonet_api import fetch_waveform_data +import sys class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("NZ EarthQuake Viewer (POC)") self.setGeometry(100, 100, 1200, 800) # x, y, width, height - + self.setMinimumSize(800, 600) + central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(15) - self.hello_label = QLabel("Hello from PyQt! This is Working.", self) + controls_layout = QHBoxLayout() - self. hello_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.app_title_label = QLabel("NZ Seismic Viewer", self) + self.app_title_label.setObjectName("headerLabel") + main_layout.addWidget(self.app_title_label) - self.hello_label.setStyleSheet("font-size: 24px; color: #333; padding: 20px;") + #Station & channel selection + station_label = QLabel("Station:", self) + station_label.setProperty("controlLabel", True) + self.station_selector = QComboBox(self) + self.station_selector.addItems(["WEL", "MAZ", "LTZ", "WRRZ"]) - main_layout.addWidget(self.hello_label) + channel_label = QLabel("Channel:", self) + channel_label.setProperty("controlLabel", True) + self.channel_selector = QComboBox(self) + self.channel_selector.addItems([ + "BHZ", "BHN", "BHE", # Common Broadband + "HHZ", "HHN", "HHE", # High-Gain Broadband + "LHZ", "LHN", "LHE", # Low-Gain Broadband, Long Period - + "EHZ", "EHN", "EHE", # Short-Period (Popular) + + "HNZ", "HNN", "HNE", # High-Gain, Strong Motion + + # OTHER COMMON Channels that could be encountered in the future + # ONLY UNCOMMENT THE LINE WHEN NEEDED! + # "BMZ", "BNN", "BNE", # Broadband Variants + # "ENZ", "ENN", "ENE", # Short-Period Variants + # "SHZ", "SHN", "SHE", # other Short-Period variants (Less Common in NZ) + ]) + self.channel_selector.setCurrentText("HHE") # Default + + #Duration Selector + duration_label = QLabel("Duration (min):", self) + duration_label.setProperty("controlLabel", True) + self.duration_selector = QComboBox(self) + self.duration_selector.addItems(["5", "15", "20", "30", "60", "90", "120"]) # 60 =1hr | 90 = 1.5hrs | 120 = 2hrs + self.duration_selector.setCurrentText("30") # Default to 30 mins + + #Location Code Selector (GeoNet requires this to not be '--') + location_label = QLabel("Location:", self) + location_label.setProperty("controlLabel", True) + self.location_selector = QComboBox(self) + self.location_selector.addItems(["--", "00", "01", "02", "03", "10", "20", "22", "30"]) # DOUBLE CHECK AS GET INTO THE MULTIPLE GRAPHS + self.location_selector.setCurrentText("10") # most responsive I've found on swarm - BC + + # Fetch Button + self.fetch_button = QPushButton("Fetch waveform", self) + self.fetch_button.clicked.connect(self.fetch_and_plot_waveform) + + # LAYOUT + controls_layout.addWidget(station_label) + controls_layout.addWidget(self.station_selector) + controls_layout.addSpacing(15) + controls_layout.addWidget(channel_label) + controls_layout.addWidget(self.channel_selector) + controls_layout.addSpacing(15) + controls_layout.addWidget(location_label) + controls_layout.addWidget(self.location_selector) + controls_layout.addSpacing(15) + controls_layout.addWidget(duration_label) + controls_layout.addWidget(self.duration_selector) + controls_layout.addStretch(1) + controls_layout.addWidget(self.fetch_button) + + main_layout.addLayout(controls_layout) + + self.status_label = QLabel("Ready.", self) + self.status_label.setObjectName("statusLabel") + + main_layout.addWidget(self.status_label) + + self.waveform_chart = WaveformChart(self) + self.waveform_chart.setObjectName("waveformChartContainer") + + main_layout.addWidget(self.waveform_chart) + + self.polling_timer = QTimer(self) + self.polling_timer.setInterval(15 * 1000) # 15 seconds + self.polling_timer.timeout.connect(self.fetch_and_plot_waveform) + self.polling_timer.start() + + self.fetch_and_plot_waveform() + + def set_status(self, message, is_error=False): + self.status_label.setText(message) + + if is_error: + self.status_label.setStyleSheet("color: #EF4444; font-weight: bold;") + else: + self.status_label.setStyleSheet("color: #6B7280; font-weight: normal;") + + def fetch_and_plot_waveform(self): + station = self.station_selector.currentText() + channel = self.channel_selector.currentText() + location_code = self.location_selector.currentText() + self.set_status(f"Fetching data for {station}-{channel}...", is_error=False) + self.fetch_button.setEnabled(False) + + try: + traces = fetch_waveform_data(station, channel, location=location_code, duration_seconds=1800) + + if traces: + selected_trace = next((t for t in traces if t['channel'] == channel), None) + if selected_trace and selected_trace['points']: + self.waveform_chart.plot_waveform(selected_trace['points'], f"Live Waveform: {station}-{channel}") + self.set_status(f"Data for {station}-{channel} updated. Last sample at {selected_trace['points'][-1]['x'].strftime('%H:%M:%S')}", is_error=False) + else: + self.set_status(f"No waveform points found for {station}-{channel} in this window", is_error=True) + self.waveform_chart.plot_waveform([], f"No data: {station}-{channel}") + else: + self.set_status(f"No Data returned from GeoNet for {station}-{channel}.", is_error=True) + self.waveform_chart.plot_waveform([], f"No data: {station}-{channel}") + except Exception as e: + self.set_status(f"Error fetching waveform: {e}", is_error=True) + print(f"Detailed error in main_window.py: {e}", file=sys.stderr) + finally: + self.fetch_button.setEnabled(True) diff --git a/src/ui/waveform_chart.py b/src/ui/waveform_chart.py new file mode 100644 index 0000000..48a2299 --- /dev/null +++ b/src/ui/waveform_chart.py @@ -0,0 +1,48 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel +from PyQt6.QtCore import Qt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import matplotlib.dates as mdates +from src.utils.mpl_styles import apply_mpl_styles + +class WaveformChart(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.layout = QVBoxLayout(self) + self.layout.setContentsMargins(0,0,0,0) + self.layout.setSpacing(0) + + # Stylesheet for matplot + apply_mpl_styles() + + self.title_label = QLabel("Waveform Chart", self) + self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.layout.addWidget(self.title_label) + + # Matplot + self.fig = Figure(figsize=(10,4), dpi=100) + self.canvas = FigureCanvas(self.fig) + self.layout.addWidget(self.canvas) + + self.ax = self.fig.add_subplot(111) + + # Axis - dateTime + self.formatter = mdates.DateFormatter('%H:%M:%S') + self.ax.xaxis.set_major_formatter(self.formatter) + self.ax.tick_params(axis='x', rotation=45) + + self.fig.tight_layout(pad=0.5) + + def plot_waveform(self, points, title=""): + self.title_label.setText(title) + self.ax.clear() + + if points: + x_data = [p['x'] for p in points] + y_data = [p['y'] for p in points] + self.ax.plot(x_data, y_data) + self.fig.autofmt_xdate() + + self.canvas.draw() + + diff --git a/src/utils/__pycache__/__init__.cpython-313.pyc b/src/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4244756d1f624760c88168ada86e482bd935d73 GIT binary patch literal 168 zcmey&%ge<81ikO)Wq|0%AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl=A{fzwFRQ;r) z%H&jim;B_?+|<01V*P-k{H)YuAS16THL<89qp&nFJGCq`wLG;*zqlw_zqBMXr&vEe qJ~J<~BtBlRpz;=nO>TZlX-=wL5i8JKkiEqq#z$sGM#ds$APWF`1}pCX literal 0 HcmV?d00001 diff --git a/src/utils/__pycache__/mpl_styles.cpython-313.pyc b/src/utils/__pycache__/mpl_styles.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d456efb079840a3d4cc55305ee3f6aa32b97de4 GIT binary patch literal 1317 zcmZ8gOK%%D5a#M-J))N_+wnv3CJxZ5g|&)fwJuPkP-F*vI6)Eyz4Tzv5+yNNX*XQ1 zZSmBe`U`UJQN88ZztLVAEKs08PQERw_R>>lRt^j#OD51tTLDv6|24teKV}r(N0x%h2iY@?8N0^~m;5m8?Fi#7BMOp$Z(?!4~S^-?9D}bx? zI^bG_>vY31z}mD3tv*3^gKh!FXn~sb*rCxhW+yR8|7Pu$KFzjV*B0C}nb~nhp70bO zDbwT1bb97Prd-=mwm31LwoR8wHsp#+&+IwUJY%OE=Anr$UOoFz3`NH?pD|&diBZ>V zQwkw%XYS6(x zb>+AZbU8{sSET3k%AKpg+2Z^>#>oy<8di^x)?3I6KcsI*^+|ULFP+lpK_=W z)42owoPFz?6!z)jkvQR!DdCJFL1NqL%q))3`~n|FEvB=IpR0M@%JUL1UpY1#JHC2U ziwAMnRzX^J9%B!9LCSZ#O!1$|6uT+-${ILBZk@<+hg%0u#~(rydDdg;Jm(!CBh8sq zgO@&g!A}K0h3rwG3dwf@jFqLAOk)%4|9D_{?3wQ`p2Um zj($(9{+d_~No8&Q&F0Qz9L9nDxmlZh7AAp(DN_9i_yY1YsqRcfm_eQ;)!L*M=8#`Q zy&L9{7f|00i^xl)`r)J!mXR-#s`WY(E+MbX;w&RyA=UciWw?s`I_BCB*O0HH?uQ%5 zH%YbedO55jzkxnSlY8M7S|)h?X8GYwG;U$K#c&(>ZSwKnn>)?FJ^