From 3e2a0416ccd605e0dd6682289fc07b694e12557a Mon Sep 17 00:00:00 2001 From: turtlebasket Date: Tue, 13 Jul 2021 23:25:37 -0700 Subject: [PATCH] Initial Commit --- .gitignore | 141 ++++++++++++ app.py | 14 ++ app.ui | 586 +++++++++++++++++++++++++++++++++++++++++++++++++ assets/eq1.png | Bin 0 -> 21817 bytes assets/eq2.png | Bin 0 -> 20157 bytes build.cmd | 13 ++ crds_calc.py | 112 ++++++++++ memdb.py | 10 + widgets.py | 243 ++++++++++++++++++++ 9 files changed, 1119 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 app.ui create mode 100644 assets/eq1.png create mode 100644 assets/eq2.png create mode 100644 build.cmd create mode 100644 crds_calc.py create mode 100644 memdb.py create mode 100644 widgets.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c53e277 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Local stuff to exclude +.vscode/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..f470645 --- /dev/null +++ b/app.py @@ -0,0 +1,14 @@ +import sys +from PyQt5 import QtGui, QtWidgets, uic +from memdb import mem + +class AppWindow(QtWidgets.QMainWindow): + def __init__(self): + super(AppWindow, self).__init__() + uic.loadUi('app.ui', self) + self.show() + +if __name__ == '__main__': + app = QtWidgets.QApplication(sys.argv) + window = AppWindow() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/app.ui b/app.ui new file mode 100644 index 0000000..3adf916 --- /dev/null +++ b/app.ui @@ -0,0 +1,586 @@ + + + MainWindow + + + + 0 + 0 + 1091 + 580 + + + + CRDS Time Analyzer + + + QGroupBox: {font-style: bold; font-size: 8px } + + + + + + 260 + 0 + 821 + 561 + + + + + 8 + + + + 0 + + + + Raw Data + + + + + -1 + -1 + 811 + 531 + + + + + + + Groups + + + + + 0 + 0 + 651 + 531 + + + + + + + Time Constant + + + + + 0 + 0 + 651 + 531 + + + + + + + + + 10 + 70 + 241 + 141 + + + + + 8 + 75 + true + + + + Grouping Config + + + + + 10 + 20 + 221 + 111 + + + + 0 + + + + + + 0 + 0 + 221 + 71 + + + + + + + Minimum voltage out + + + + + + + 6 + + + 0.001000000000000 + + + + + + + + + + + 0 + 0 + 221 + 101 + + + + + + + Estimated pass time + + + + + + + 6 + + + 0.000600000000000 + + + + + + + Min peak height + + + + + + + 6 + + + 0.000400000000000 + + + + + + + Min peak prominence + + + + + + + 6 + + + 0.001200000000000 + + + + + + + Moving average size + + + + + + + 20 + + + + + + + + + + + + 60 + 220 + 151 + 28 + + + + + -1 + + + + * {font-size: 13px} + + + Correlate + + + + + + 10 + 320 + 241 + 171 + + + + + 8 + 75 + true + + + + Initial Fit Config + + + + + 10 + 80 + 221 + 89 + + + + + + + τ value + + + + + + + 6 + + + + + + + y0 value + + + + + + + 6 + + + + + + + a value + + + + + + + 6 + + + + + + + + + 10 + 20 + 221 + 51 + + + + + + + + 60 + 500 + 151 + 28 + + + + + -1 + + + + * {font-size: 13px} + + + Fit + + + + + + 10 + 10 + 241 + 51 + + + + + 8 + 75 + true + + + + Grouping Algorithm + + + + + 10 + 20 + 221 + 21 + + + + + VThreshold (V column required) + + + + + SpacedGroups + + + + + + + + 10 + 260 + 241 + 51 + + + + + 8 + 75 + true + + + + Peak Isolation Config + + + false + + + + + 9 + 20 + 221 + 25 + + + + + + + Individual Peak Range + + + + + + + 6 + + + + + + + + + + + 0 + 0 + 1091 + 21 + + + + + File + + + + + Help + + + + + + + + + Open CSV File + + + Ctrl+O + + + + + Quit + + + + + Export + + + Ctrl+E + + + + + GitHub Repo + + + + + Open LabView File + + + + + + spin_group_len + QDoubleSpinBox +
widgets
+
+ + spin_min_peakheight + QDoubleSpinBox +
widgets
+
+ + spin_min_peakprominence + QDoubleSpinBox +
widgets
+
+ + spin_moving_average_denom + QSpinBox +
widgets
+
+ + correlate_button + QPushButton +
widgets
+
+ + equation_view + QGraphicsView +
widgets
+
+ + fit_button + QPushButton +
widgets
+
+ + rawdata_graph + QWidget +
widgets
+ 1 +
+ + peaks_graph + QWidget +
widgets
+ 1 +
+ + timeconstant_graph + QWidget +
widgets
+ 1 +
+ + combo_grouping_algo + QComboBox +
widgets
+
+ + file_menu + QMenu +
widgets
+
+ + help_menu + QMenu +
widgets
+
+ + graph_tab + QTabWidget +
widgets
+ 1 +
+ + config_area + QStackedWidget +
widgets
+ 1 +
+ + spin_min_voltage + QDoubleSpinBox +
widgets
+
+
+ + +
diff --git a/assets/eq1.png b/assets/eq1.png new file mode 100644 index 0000000000000000000000000000000000000000..634bd3365d61076676d57b77c86141d284e305df GIT binary patch literal 21817 zcmc$`bx<7L7X=sv5(0!IxCKZc5G2Up5FL++-FM}ja|xD_57|N6<3CupKS1m&JQc^2{H$y0U|B;Xxl zs;mm&*Hb$=5&kD-!-U(w!3$$PNxmmfDkD+<>b?YyUs?Z9wR`dez4P(sX|Gj*!ILKn z!=i$G3eH;l^T^H$iZ@6{7~>zKh~-4dzw;kb`;ax`R9@CPYC26hlgN=pDjMMZ(d$vMM zGT@a>VGr~A{P-96gD*KI@WSg@`XLCs0=}`m0}i7}-$Xnf8le3DewsZumQuP1gE68# zIw&Y84gHzEqGG&)^3)Ga&2-PWlyN?{6i7x+ z<2?=zjzgMfs5~5)g0&B|R*P9LdovAFFU&kl{O0B~M8%|{eN9b`3G)~AtUm$Y8W{L$ za_CiXOiYzFuWepqqgr7}$(mj=#eb6-Eg*nP^YyE|Jp84<|IHXv0~9L$v$fTJug?UA z%ftD2i_xOuV#ARIkC;sD(WTcr=tLYriHV74TbSf!Wo0+A;D4*Y&itac`8MWv{ck~D zTMB7u=~r)1-fC-WN3gMYkDvpq#?DOREGi|H@D80aUs#s!doiA z2s!IF=&xf63Vz&Sf&%`VP(o~B0&y|1&zzi_i2;ko0@h&>A>$;n`nZITbE=Dlg)?}- z?Y1sRUh3HBfEw2qbZ|ose)FGIu$RQFyqigl`vii+Z=#v5o!!J`WTMn?1tj(V&5EL& z8g(JE9PI(h`*-ii=AS+99vYJ5_lbLcB}o5Ji~BPdjl%!!WIQYooun2QpOle|->1=Gf<#vS-+)OC4ULPJrr$E( z_AJ2o+WE$F#c}SwfO0!U#my#wCkjH~bzw7~(9zOPM5@&4u2Ni|>{`LfjXN`7xE4*9 z8JvcFOMELk<444BA`Zv5+9V{$M@LJ;jQIaqPZh!XI#!iOFFHK6lFIGkAl9!-u>a4W z@yNde-MKk0Uc4A^*$j)?+WG>~b`W@rJA%g`kxfQUdyi-QZ+kiF-vxwMEmiwhxt`Y zp7!?-#qS{1;_uBhRQm<^0UpB1#sR_=Q=p`U{Gu=O!8!Bl`nvGr_TK-j<>O>reEgui zG%qVP-z&MsH_~k zJ1;&rhi};~kihWhf}%Uy=otE`ZgVRt`c^t{Nl2`=7t71B3Fptq4gUEo_AoFl8^!0( z;zmZP%LM&*4h{!Z zS!u_7z+ojM^!4@mL`^N{C1~{T)&gxII|Bn^?5@lL)YR0#YVcck6@jbJ|5^Quy%Qfz zLux67|2)u{#C+pRF#|!TJIv(n|9mL=Q&hc+y?IKwVyndsnkLjfy6xwamPe_MD@OG_(*P?VQiQBe`>;2_}I-6Q6#Qg9zs(*ECo9O;?9 zijR+@gK+UV?YDkieoMlnb)wv=$=S^=rLp1P$udI& z!r0YbrgcHXTws%1T9&Cy|}eZR7&1PN7BYWF9UVFI>PK97@j0ae}$^&dcJ2$ z&76&6U}~C-2+pgj8rReQVchj^9`;Ry1J*V+wj+nv_V;N$Zo5(unXbFF`zOo2nSIbh zkB>ru9!u$OG2#scli)zpv+`pV8*pz)7aE zn3Q7#2R828kDHNP%3O2vaQ%Nlh||At{dr*_m{eTgEj~0ba5oZPSwrI!r{iAKTHFUJ z+OM1tNnMqKFz6N;;L}DQKKyU2{NXv^zaH8AKuq!C^?HSdHbw$EIEuDUei(a3HMNU} z$+5b+`rCL!rS_N|?cbVxRv?~)!3O#p1I*qE@cVqC;8622n7MzBtYD!(aDOMKDW4O8 zM$9Hd5kZo#r0)dy93t|6;aA49rR5zV;cs5Bimq<7`CQybUbFVfk`f8ps^IW_(#;G` z{+qMlsfQ2n|HTl$F@Ev$V3Z&%EUcKQq%U8UOJjcjHUL!xS-k4i_@*ZOhNP#tl%Vb3 z0vGyTWTvJT+gDWRHC7dgNhzd_lH3)Q9VnPc%y@n0xjNEUt@F%z9fOimV4B9J+jJJZ zX`(Oi&s6Ce8XJE^CgE-)azg=Ag)72svrp>UD{D#=&cT0MDIX|iYY(G#cEC0cHnJ0l zNS-qsz$013?z^R7Wnw+LX7O|mhXlNdd@&Uj6<<`9ifKbT{%kc5wC?WiD)2eJ%duB^ z-|2&kENw`8A3481FD=c&L*SygyGB|{Szdqx+l)-H1MofBjzG|t5)&6+_bP9q5Kp9w z)$fhm&18QG?ddu{pxsP&i`W?qx(_QXL^jlxivh1bKR7sGG~arg46zx532d;8x6{C|AilhY7I z+w3a2$ImJ}MxQh(8`;XW^>uMm)nv~#Wy4&<7$8D{!QeCU|9t|;x&D7yng7RDK_H{7 zOi4-oHMgKZf8N$m6I&$eeK0lx{^KC;iS%h{~>!6qlgBqhTY6qQ4b zH;2cw1dtzN=P=^%?(Xgr2ep`9c23vqtU8CI!39MX>-M(DdU)MJbR>>@5y`dUrd0YT zIy$KZ_d7!%gCC82HzTa52>F1GCAM)a(LdZDoi?XIMMpPTCYoO+o>1S=C}u1zeSCf# zeK6k%*;V*`b#q;~DoEQ;HtT6}z-%`{p- z|2%Bz&or3TDVSJkRDT=C&7B{U#ATH#Ydm4x#>OppSs?{BsN&|=0m>(3@{(j#)tD4l znUET&3SqBku1F;At5@g=Wtz~C)YR0AJBQH+X|mqVr5_eAmoZom%xSg`5~d|KA(dz|c&$O4aN7Q>|LRiuU&x z1ynXRB36hrh|d$oQa>>vpII^Ah$Mjnpz9ef7Z95q_)O6AET zh#(cAR4x)$b8eUEj5^=FjxSEeAFTcj>*?lvXJBMsNPMxud_cJEN01^lwN$}JRYgVk zR!}xb#z@gCy`yA&j=kFO21fOcge3pr71^mq#wcKeCcR8d>(E1-!9Wt)sCl;wFRO9{v5lCE5L51qe4 z${+l&3ywEmzefFJ`0(`nd@AzSJ9aWyn8lbSnbZD44#_o^u>Ld1)WaXVi7p`j;1x4S zM<6&vxs#|LBhe*K68$2*8X1r%ne>hWnbG$Fh#fuBH*0QFv@188vR>w^%GF+YYv`$m z#-SE?;hwX(Px_rwiT#p! z2XXHfyFTt+uW}ty6a6t)But3<`ZfHQZz<*9kWleYUpdunZEaywICz*@;Ms=0*gw`= z%BaHdxA-`;TGWp*3eKdYjv{pq8*K>EB`yMrGFgtaY4s1%(*z#&*m^ zkc`BE%%%ztNzTKCTZsQA@6S3FLFIq|guW^L$4EC}vo|wg&_|j#k#*5^GjSKABLy_| zQX|lsyCbl?yu3*9?DABel||))KE+jhLc&1pKIdAx(_$jlS7mW6EzeZ#;=UF(bL*}n zuDpC4`fI)gPp1q*4*Thq)aR&bjV4y#VNAL;-^dvG_4M=Pa zPGf`o@{so-NOurhwZ@=u`)#O?g&Fo602Clk(`N8VO3b8x6&!rB!Ff3F9D|#B(fzVn zEzHCSAD^_c<&Hos(^~U28k+1>4l%=+?EyRaS~dgDz%}_uY>USVyhGTX=liYQ)?ZzN zb>=cjTo?D_TxEmZS_`hlSSoZV8wD$?a>8A`MuyumubiA5_&7Y26I27k<1`BUlgup- zVYGH~>VzuqfOl)&^JVph7Ekf&Ix4#p2IY>_WVH8ka}bcrFJG z<8@XfcPN16hLb~Hgv}92h{s0qTr2RXU-<*&wRkwk3cd~!(zmy7jU_Uvr!?*2fvFQ0 zj^xn3>FF)@M$^_Nfl%X5eJ+U-rscM@25R_XeV`7*)aKO=4hf3O5p^hU4~U zwKh&|*PaU*`RDmlAvyzXo%5OwE4>7;FLNG7#+wE3+4}Dg|FyWgnCR#%UdH@XN2k#D zuP`w|Z)k@{7xwNjc;wbaRZx8Oh4XW5JODqGu-qBYe`whvYuU2x+F)?d&1lo%3_=7c zj(myd|6yJWY$Sz$U^bluRWh6t;qdHlNINUtpkOu$s?((sn-&r@1|*>>nCr zQtS<~{j$du5XEXSV9@aQMBMPG&HEkUhu?Wcsn1EPT8`_0vdX~FP_(B~|Hhag;DSp< zll!E}PLlq$1^u^y(6<^<+P?-S%-pvtPc@D1bav-ue`-SL-rk z3U$5j36JMpa4JWbA%diNwZCL~-21Bq8+~|2T}$D9)s75*iuQtXOs^pCdI`80nX`a$r7V|wbB1Y#5$L5>rH-w{$FrU#{zqgSGR45kLFa3+M zI2L9to_@Cc;v`__K)-*v?|{>gu=K4RlNL!P?e7wKq^OaRlCJ|1?sY1gO%vBy+ucr} zd5wR&S$Qj!w#RAdMK)Ifi7CDo^K{3d_JgyUBa|-ULoW9XUIS~v z@7WOi=%MY=CEh3bb(_tUK#YzD;K~WsjU-26)(f(RosdBTAeSv-8QXH`!^eF&52Y7H zEBKZ9iNjpQXVeZJuiOVfV?{eu`%lZx2bD{r%3H6c9Ax=^mXw;NT2cVmolL0iF`-l@ zi%=<+wF0{QYS0Diq1RCskgEbXhz&AmqXTsnLZ%pMMaX_@sp zk*l*9vfZ2-j&IBqxmVksK3CXHn|u@gvz3>IS&%}KaB+>DXmPc0d-B3-p?i<4V@2M} zOL!skA^LSMO0>!wzwMpct2XQ&)fx6Gc2YhMqKH4FKIB^8)N9S-hk}FEWXzmFvr+B7 z3Wvp4_0D%eHU;mQp|fum+;RC?3D{CmOPwlSH~p>`yTf&5c<*3&hHF@)vbLMR{0H||lghmrsB}6rzB<(pL71F%i(0F$ zFNboeDpW~QaWx6AlH0YfrZCp9prz|hBZA4epqckjjEF0~eQ~J-5Muu0yN4Ni;ujxl zrjx&UroF*q{K3Wb2X49a@@FklE`rFPyP&KnChqPw_jirL<1?k|JGjUfAC-7-Vv<+P zI_<^97b_KWD8M3Gm=3vJ*5OlLufndJg-}Db_<|RUzZLq6ab*CojOtalGPs*diAhiQ z`Ase)@I4YHJMrFn4qaUZWoBwu z<1EyCc0lVNip?GLjFaZMlX&;8uApr?W9hm(`%_#8J0tK+&dYm~&t7N=qP6dUXr%aJ z<*VK3$)i4)wtwQJ6cHQzQ!Z5tnhTGQODHsT%i*S!-Yja~s(kNVSC{Dd`&w$=JHF5} zWxDu$k^Zmp!RMpnJ=aILIex6I&aa3)O@#{J6!_v3ZBT$8&KNd$QTeRl;w` z&!3IZhBh_%aBdlcf0RxZ#6FqOwA=oF)<;2cgS+<*AMs7lJcu_ zU~KSfN=aOLfUt>8+WK;5GRx*tB_&p4mp7x?g+PVH-8bpw{=Fj^5tG%`g`$SnJFfIB zT{-1ANpK9pd*85D{#8Rcev(2{=O-N{;W3hkFl5 z*jw}SY#H;X!9sPozxYMJo0VajVC=E5WiKoPkSEu5opB9kM>v}0vERl*-Ti@4GNWm> zt9O!j{!5UH*UhP~+7kqPDac4)W_J|oM1S4dOB@Y^?;R2Ksj_+KIxl@o3I2qhZUi2i z>{9sU3hGF(7ZX7O342Ug3(^@z=R#q4X_WAHAxXoLjLMxZ|L_}|W#O&Pkq_aDmB?lQ zSFxkFs4~Wor+3rUTkME)m8?JY(yeSeYZ9o{5`L+&+%S7ypR-b%B>UUp@UI#s4#yXl zH)+sHnGU9^q={gdZLXYCe{vGNa0KIiE)&$XiU%vZWr1R#62eM+X{=x*~34vLgEPp;rByG zCS zD=~WaG{P2yE zsVZc5W24N~LAP-ZdvqF${C84Sol!K(f_1Z-s@Hhi$O|WXZLiZ({O*}r59oa|({gyE z?DxO!GJ=1nM2KU&xbET*LQhfcDx8W##YN6HkMeqa@)&R;Ci&NhP7b}e$(2o=j+))h ztDHr`4!HsPBE#*zwQ4QUP>GjBa81KHqgTl%H1YnIQF4j*sgj0nPw{=G`Mz79(U6~<12&s6tR)%kpuab9*EPZe6$5^gqEMQMfeSkhSQMZJ45UJ zXZJicu-U}86LIS<2s(Mh{ryEVqKS6e(7MY+U&%3HPf|=yJ{gU#9+$yA>AsJPrbeaO zF73qr?>Y=8mv(8_VpvJMVC1_OgW2MD-1xh*Q+i;$yhVjz@gt+~-FbD?FpNRX zx8Zm631*r>_J zg{EnqLaR)k8%#Uv@itAwg2!!1%0+h+uh!DEf45TSBCe_cxq{ippUcC=A`)SWM)HRx%e;Ww zPq=O9KD%370riuHv~~Jc{>xe3Ij3?#5fRg@5shm+SFW5xt6c1iU9VcrM(3hdR08cN z_6s~h2CRw!3H4@78%l?xQPR6; zb`@OTs8H=8&*u72NvWCg6qg(i|J;BQa0nl&`;>-5E`G3(4CJC=du6*&iCQY1QM5F> zR-rSb!g=dRS5c!`4-VcpGi)ZuOn=3QAYVK7JrdJsm;EoL;7Mn@iiRt+BF=hO8zpRbjK zOcw|4{!oa+CM?CJv2k?7A;es>b_#+bi(6Tr$@7cjnPq*!AIdBBelPsPx+0DDgzLCs z%!K^eJMedWu&9CHPi%YHNd54$96YcXiBoMt-nk9G5DC-Xg1*6s0{OR-phAZ*EH`sTbO2Aj&j zn~V%ih1;}?w$b z%da%)HEQXs3ip?SRK*dT1;12)v=jOg9{z_6rfcw)@@Im3Y_|5~7usKu0Mi8*Yk$W@ z|Hb0RT$Q4Q><_^gxE6B_W{H3Qo{Bldn(lu93HJ`;@kk6gLg4(U?)xSl0f@G#1!sAO zx;BZp&nP+t1cJUNe7;CKBD2VUu=OSnCePkzigsON z9Keb&MH=6a`xcVcpLUM2DDA5~4{u>uG1;g4ggM^{56Y+AU-bHZPF#(h@fGHm0(oeA z3SWa3Zfq6Y@-Z+Skg4MD(cqxb1`uJn+%YFzO9 zUhyB+QJ!5F6;BL0^CVA>^dpN;((;hr9f73NxkQmL2)_U<+l_3mZ=)NtJuguiR=uN+ zeG+-Z#*C(Q7m;snH%`4>(AYB>yM!ryzBfB7G~q@pxedh)r8aEbnzS30NDJTj<{Jyc zuZB9#&MwlSDH`8@?|81PmViTHADXtud{Il?`33mTa$5b%F%8K z9Wv+NoaMbLxWHkM+FzUi_FH;qCU8aE;}D_7Di zEp;|DEKEP9>fz`8)(0yqdc~=cQ4=Dl8|34Mi!IC?AM`wb23Q0}O>8^C6V3pg7enSm zg3alOd1FLY=MA~t=oMBwJjJ6j^?8no>dXxA&JhUt#Z@Xo?{yYFty4uL>q1<@l7Zii zXvuW7V@f;9+_zuMzbZ4d*xFYj-0ynAW71->Nj{Sq`|HFB-GRTFRe z*?KyE8EUw2OY(O^>cGDKeXrfgsP;XL020P{WG#9V#e%C_hT8l*Zi#d6$X38^nIeeJ zo}%eMmI(@gS8yaadX6416^xD`}^|E->7**#Jdnx>nAjY_t7tJ6ZGb>i!}7H6a1TTp6V*@=<7 zS?`ZVkrn@E2Vz#L^}*Lvb*}qYRzazPvi{+acAZQW(#5H;M}(|%M#Wp>MV>+E1+3F^ zJ~}!YyAIi0lxTA7|5#WUg_&}Hi>}(~>|u-Yxy>-r zW@S7emvfA8*t~TGv?_|5(rHdoM7 zReiRDzC~_CM1NxQCzhXlzC=QP<_+&13ZJY^C5`$;?BDZXO1R`JX|xD>E-ne9p(Hwt zxd)NEpw-SV96Qs8^}7p3iyyP{D%EvZx96dW^-E3ViIc0Pc2#8uVF45Q0@mFkkP9n) zdraMmqDC=lX_MaKnpMPY`De>HYbI`h^8t#JfTHKUh$rfEeRVj}T<0|a+Qv5&lYUUE zZWZlz&@``L!Fv}1JpGFbK+6DVR~U%!pMqq8V6RwQW8T0~nwO{MF)#VDJ^)6q!RHa1 ztk%l4hp3)8DdW!X)TE`8pw+z84^FI0eDQ)=^C;}6pH{lc(nK19LPTCs^|Rr+A^5jn z));$SVmzywi#C*CvS0e_`3HCg~Mi(BW-ytz`n<#sIQzcUaZN#gGg zq90yVDG4O-k`T=az--1%J#|=~BQ8is9?LxLAt;mIA2_nXBOLz%cqXot2l*~3bfx*=4gvf(vJ{-)qP9iI%9TJ!9X z7Jb!ZRq8Xf>YwfKZbdzPw*o~+VF0@+f|~6KkHUTsEyR7P@7*}7eNCFq#iek>@NA!E zv^6}G*V7+rz2^#Rc5?!~>^<6=wicL@=R}?cGibYcV-{$-RorQu=KIV(TS!bA;-kc* zr>6%s;u2%i0ZC}toa^AwNk{9$=Sbd*Ahn~QwyiL0+QmkXKs7}VmNVK5hw;taTs9AD z!5jq`D=}LX7=HG1XBqUX72ly|-M_ZJUd2&=ZDe3+HBXZn2?O7x=;GU|NdWZ@%QLj; zF$0J%+TqS4W3UY-ulR+6%9n!A?!bau>AcpCtgy^hw_CO&ZCc-(Xi?w0=@k(t*O|6ckQf2HMomuX0TE=OG3HEIt}4}>A=i5Ks(7OEYI|>n;q18C;DB%+(d%eFX?wF&57ccZ_sApz z8-9WS&gz^K!F8x{JsTT2>+Xltd;x%3+C>qadj;ge&nuxroL0?rx>;EP(rl=$T}A7c z3MLk>^SA@i2&hNyPY+A0L>vSvmR^Q0YhN8coRNnMFdgA&J4=D6Hg`BSV=_DyKuGt_ z?fEy++71UW(Up~zwV^3(rHP+wY!x~yw?d)ohd^ZF)#LU9#ohd>MOYI#A@Bb!)}*v% z(p=Yc6Qo3-#VLx4jCzmLp+IF?q7v9A>& zxe5J~#3Ok=|7n-)oL0muMgeySm?Duq_i7J4Q0j&<}8CT@isXs!J%+~#jZ2utrD2XZt zxle&vCWk}SAAqRY_>*C}wYoA>4Uj-~Fh?(y{Suxss4yh3;q+p5EdzQGwYX@Pc++Od zY$C^P*TW3okfb$&@ly-vuhP(Zc8t97rI>GYkSA|2;Lb;nXkc#uC@9kF>uaD+1vEs! zXi5P#wXnJH6!Ko@Zuq&8PRMhJdHpHqbxw-Ee>XK3$LklpfSwYPj)TeG67rt%cTI^4 zYY56m+%NU#38`yV(p+?x563(Wx5Kk&9&--87AMeDtGGgWivE{5B88EsA{ms?n3!M% zOeT_ALO*Nf^XG(sZhW##)p)XEWiy~9K}FG`7~3XdpwM@9!9x`6I-xce9c@uX#=uFb zj!wdp&C7^kMGSLVIvD)&Lh+2-?h|flcR?u#lr3oBRZ(!XXEX|s$4Cx=Nn&{yFN+K9 zAers7(9y}_yfXs}1^2Zm zC@Zqw3S+ic8jld~b&U>xq{+UM8n_RDde+z17YSu)vaL*;OW_g9B6}+;zkE4aUbZ_+ zsYO-;#ZecCY;J0D2d(Rc|N3S54$Jg@x5sl~%X`D*E$-mpR~QO2COeD0Ok2_HhD%h-iW&fQyWIKQ(!#@1ZuO#OxqSuKOb3J~OjtVe?5)Gkq!lMYKq)Ne;)7}s zRA7@{vxH0d;S-U?G_6k<6?}L#0h>oRv?poH#NjS)!>&=;OUR`GyJZ}LquQVdE&Jt< z=E{2Ge7hxLj_gr59%)p7FJkrr*+E`iJ=wHV9^=6=9cv_!yA38B?(1nK2($fWi}mgu z4INQ~o4DI*mK7XGoLf=>WT{M;u<$yKj!kbW9_CxvYKjhygJL%#p>LJDA|c*k+vue7 zF_%Le)j(>{)z`1kCrOeYvm&?>kMX`{>BXF_A_xkKe!W!&bNC=Muj!$-| z+ezCI%a^y!+u)4}*Z36sM0 zDDIoo;wHFLRZzT5Ma_hhzQKsz`^Kx*5?OTpAbDm52B)NeSy5khZp{bnV_HCp{s4(p zsX_I*CNVg2l+o8IQP7^nuV>>-+a8eCK6r^_X!ixPYg(58SoQ>bQUbc-HUBcPADoT` zCZ9HYii=-O6LMF%Ii&_>0h?;3ML$qMS-p-nBNdylaw0tO{a&f-PSF8U=qreKG|<
nSSh#9iX1g*F)9SM+&DjhNyXEOLaN_i#FRv-ds+^uay?;lH zod?7Q$T6Tbc3jkbrtPJw+CZb$B)H&yt9iWvJyfj2BqPTqL<1L7$(*Hj1?k_6*%v!D z%1-P?M;DdH)3$o1>5MVYO;W1-g>JE$9#T4zfn5r`gr8fP;fyXf`Jg@D)3mXvG?$Wy zyWI5pRNi#a*u;70aSh1b0W};snCQu4pAVX?ogJ1F>#UWsU%F)ltAnSfx8m1!*@^cg zQ|fSbgdD&+efWT_b`D}i#>Rkjw~7nyYi|yx!)uFy&IdII@=~81$LCffm$_^r2LOw# z<#s3esOd`?LqOaj-J6!=AG1LPN$!zM7%q~C@7z2$Otg2h%C2g?!V$-W){2(xA~>?n z<*;9EzYNke*9hMo^7aa+3^IvBj_Y`s>CfmT^l8wVeZ7zc zD@&R*B7W!BxjNI((#9uhxk$2DNXdL%D8VAeE}B=ogpq(Ep{c1u1evq1Yej1E7J`HJ z-Y=X2RB+;G)LZ##1_;Cf3}c|8Qsrb#+!C3kG%vj!s$1-@9^o!8`y7t;-5C(N+Ku3m zUM_g#?0KSRZIKE{P*v@L>z5b zMV`M`??pN73&s{|0SC!wc=D-Xqv)%m%lqBsLMo}t*L-|uZBOq?)eOTphk$;;QkIn@ zpgUn6k78g7p}A72-&>lIjYMK)1)BdE%P;opt4+_J;d&B!q^y$A@Pg?Xmm(8}9G|1d8_hWaIP$Eo@!bj^3wlS)9 zOYYSs+pv8L7EKFTxQNraVD=F59auwkJ(P3fQdltI{BTMc@cq>D(7;`hlmPGNVJn|M zQ522mUVNmd*LK{S`~0X4Jsf$EY5BWD0J_pKjl<-_D<16CaQ4=4?eAu^?|Tagis?fE zmS$mLf$=-EQ}8Zxrf_Oyg>0tTEix`4u2w~td0`spvQsmeqsgTG%kQaiUwxmv5gZZ{ zvxH4!-Sa_MnZCEg0}xKjl%tZ_($yoqA>a_~(PS2$H%P^^x%E7LrwVm!R;U>(ATQ&? zJr*<(^y&c?O9)^PdBHeIJxIjg#zVNFB5z12BDqhZQp$`?J~5?LCAD|v#7f}u4%=5e zHt+1E5yCN0&jV?*vRC{ZpQ+JUH8V-GP=4AGNu_V}2ZYzgoE#=r^m887Wr2O{=Lc~4 z(u#wbi8)I~pxdDeAc0g{-2FYfYAYZ+Cs<$C%iIoQjqk5Z)Sl@7zUN_Nd$&r_-rF_-4}y5A?^-Y z;i;W?TkuB~7GZvV{xkBSKs8Zw_<9f!=3&q9$ZUCVP50iDP}VKj#^MJvsT~H{T><@@ z@cZE;GRysF5e=_zG4+PKEH3L1*0pF666IS-{l)FMrY4GVl6tyd^TV{P^B)ucr0Y94 zVUp#)i6{e@Z#+y=pw&oP4{~IJw4)vqBM*C)04S33q_LB<*zC6|QZ}47n=cKIV@N)! z-rf!j4xaOlaj~)MKy>QbzHg6r%kv`Ky%DeA67@x8MYY#P*Uso;dy`)z#7W=eEWl-V zS%V8q>lu2^mtsOU3vR4)8{UC{ZD3$QQ;RAL#i-mmNecmH=Egu{hdW; zMbwZ(k8j#7_Z#Y}5Tnw)`NZGDZsF<13?U)t1OU^hT-LP0_$WXX6cV;2eWNR z$j|2Y#m22jc1yut`vZ{tQtxM_(hJx~fg~C1wk8Ew{QT^UmGIo>mBXHu#7>cj0%#{3 zL2wT!!V?vXmBpj6?P;*SsjtH3?V7>}+@k#gYUhH;@na6f#aNo|NQG@HiwK8B;cu7O2D;N= z4UP}@&#NqE(LtZ@0Wts%v$q}abl|ZBtKmP;VYdM-L%GA9tQtoC0#SK8YVc6u!l)ICv#%Lga+q?9`Sirf9tYMm%yT;DBfe^s5oX_5F+P)YDVE6&0{gj_e z+i*zng_^DW)C*w2v5~zp8-?SP3(!6vdGEe({LMFH5aM``s4=akQL9qeC%m}E(e8gF zN1me#8(Y7{<*sSl*Vg1C*%R}&- z67}eEDw2i7QS`g!5MUa`B&HhQzyH+u<{HO!ztzbC?GzGN07M*p^3o3iyyNx5h!NAj z^_8}p`*mWOJjXY(wn#|~&a0(PYpX9acSJn>r-U>#(weSs2EpCj>^Na-x{fCOfthe}Ao?z|i^whB`LUdzp1NDd_GfrlHdMWW8vlXV(WSlos?Fi~^Y@Af z?JD4JE1O%PeV`F16(KZrdA>uHC$uCWX z#}vI~jP-6^j^6dBU7}B0$*f7!$e79ESRCKArIYzkE^MKs8A;<_J=pZR-yT{j$yd;- zUWIMGI%fvDgc$8{4pzoxEi^$cCxXW{V`VhSs>z7?dvrjHpG{(#hMx}f7Xh{>6?erhlzlbPbhAvjyYt||>3m25THRokIW$vuZIng;pX*KX%w@u8c4V$=ae zbXoHT^}ZP~y)QC`9AAw51Pp(=P1Kk5qtZv|OcurSx)d(c>@%RWvFJ6h_{p4oKa8!; z{jV)%g%RWcb_D+>R^smMK35{AD1G{rlbKn?3s#S4}NabH1dnkQ$D?7HKm zB<$lZm~^Z^Wiu@s;SrELs`25khLb@80T={D1J-J`EWI>;#B}osm=A<`SAYRdcI=1S zcAifS2;pe`Dtk_(e%vV3wd9;2yBQe=J#5s3H=plLDT97=6F@#oe?SSl;yiY`+?^FU zXud^v*`mK3+>*^vzyv96>8_2_Xp>msvFUsdjbxb=SS&wS?y24259(x=&@A0Y9P2b9 z-~9=*hI?jcEoi-aM@XiEDGdlhYAKplYt`4I1Bs(hg)`{^DRU7`-=88=v!%nHhSyW5 zu=3Ru?~K@K^_#5LzpD4Qf2$Ew=bYOBk^|p(H}lL_|GB$|yTRNv!^sNjL&C$~TUC%p zShkT_9u>Y1blOy&VsJ6jRZh!eTL@156yyl}H;BRu)X9!T29xg(N~jS@9g*STa1xLt z?b{-dcCmhGEamPSdM>$~>SnjlVuF0zpaRpR3!&obR-T)RKVFAKkFX(7=X)2Gs++a9 z;w^A%K;F2mlt&JTkTg-2xBSg6((jsVNDIlUT{K?+w!;6V^D`I)+KVb}21MV03^B(JH!eTQ!g7IK`TD`FC=vz&*WKl7>uiLeK!bO5zi1_g}R z$c!@N?Ceedz(7p)3Ynt3vX^ZEy?sw=W6Rys0~Bs`RuCA^UPvW)OxDVlx33ajF{AntTBXy zj6E96ph0M|lPJno_nGeN{sH%gd;fuF=KIVu&*%GD-tTBrkV%bUA;N-UP_?JlnWhrE z$);RM$GJ8u8m8_2GezTy_|Tb@GlcMU zw+amd^EF|>t{pGzhqZl$^LBGg&`V?9r3CB5&`~}Y*CS~NMF5g zhjQ~g(1#g))V^b^4TFyuQ!hiroG(QcnY+!c{qz*H)@Sfi=VZlNMfg0?Ed!%;GEN6`JoDE9QNgZl zVXFBnfK;73N>BXt0wz4lPh$DdS0^xoq6sF!EIeszWB|m? zQHl-06okqf5pkhX2ULRwO;J_d4p6Fj<<6k0l1Zh;U3PMMD(S1sF*Tmrp48K0M!NVA z5RY#?0{z%8$;L%hsildJf$mLLU3$8J#(M{@@BxTTq27)AW$*oXOb!VwUc7jbw<@fD zbhg7O@NMs?s3yUi~1eDYkBn1mY6tZ)!cneefQ5EYB={;c99$^20jy z%Dt4tU^X`CXqI8YyT;|Otq^&Y^^<5zl-Hy5q0lc~W?o>d%}!byNKJAXly-s69%Oaw zHV0}bHcS(+VLScZd88HUhkn#^3SHJSYi7kTIUi^SEMs^%?xR}NFcbjS`70t>lVbR! zp>j_)3cXd9lZ3`m?o`n)1;s;{%NOrE4WKx2it4+1>ysf*!mDu1l9!BXhew>(!JvH3 zVUzrNUx3^^K&#rTdXK}iM@)lCx!aEs$bP{w!DDl6ckD>#x&$Lr3H@XZ zSEQIB@Ndj(c?TK|%w$zed}SId6cD!s*2XB6YLk`=qOWSpFRF?SoDFTP}SBnWEN_~i}v6Xz?Bqp9^@ z-~}Or9J%LxJ@J-kfltTk{n7zLURGpnp1Lf7h<17Xb6dw=*2y8DaSrP^R*7~x*iT;| z_4Yu4^#dQ6GlMy!p4FQTDkRg<4Q7H30V4)pX~hp>5pe>o6%+OSefIyztzF9dnLBEG z2}xMq1bQB6dk2eIwi_#dGP+&m(gmhYx;`GJS4VvWMbYQ4yIjL)E(t9GX%1`|#%69}goKYi2^l#m#mNz?rl`Krd;^ma_#9#8lZ z-PkjN94L83riAUBMW9jC#HjzorwrDjKG5tOv2Ep0C1g9ddf?^cOBYR@`Ouph0h7?1 zJ3CK>JFld(Z#&`i{O?;jeNkt#ZHwCbmTo>TlN0aEbB!YmDfM+of!lka>BjP2y~g>H zup71pCD0v@+~pWwVwfPeXZ6BLyl~3Hd&jy+J0Zqv?A!Zm(^W+&0C(H}Rxvd>HA(%s ze5xIy8I}@??l`Fm3PgRsGo9x6naoE8P(a1hm*G3U)sjG3W-sf%dUevt3gtdMT#D!c zJB(+YJhp4YkZCgQ8Bgx&N%6Iv=(J+tz|MLUqdQ{MXH9(d4U73_Yk)9_JEEET{kAKr z+PZG21-TJo!n@qGb#m@&ynC6kaccZKaO?EF%^jJeKd*e$)~26NQk2=dJ(X20^d~xX zL-gCmM!E-mFM6;%7nDhim~1?}O?tbmOD6j&LMgjdZ{v|D+;6bqZeY-KOL+hKdZRgF zc$31l$YABJo2ecBhLTkZbaaU(f<`$g9JP-ekCY_1M}l#FZ;*CU9pbX2Rufp*`=KzZ z2bT!%+x*y8NfMzb`t_v~u)oiMwhH`K-0g)W!24xs)%p-m-r44_CPua2JdCFiuoV?V zfdcrX%M>+9&-B^7B++?(*|ZD2r(Z;Na?zBl%z4?^d?COM2YBg~BHd#+_Q>__H1X`h=J5XQLLGVK zb!t(C&ff}|3TNzzhxAm{)Z{(-9d^K^FHKz&*8mbaXo3lgk%i`mNX8aAJRhV<^g6aj zbmdpqHf}AI#~Y>Og%9cRL$rp?jJ$IVQ_d-YdhvVNd10ME>Lk}t=s0>$|rb{4e(%kBKYIoxh0H+J^lk!Tl=DnOjB}0 z_LvAhj)UHv=F?Z)kti?AK;mWVa`H?<);AbkOm<5SdOqa_8r&bS9_d&{gydUC+b5`& z5f?~d)A!FhbCo&)BiK3~md8;SdN->UaE@C!KHndKsA0l?|EXmmRC6k@#dS3@mK6$+ zupY1aw!}+dWPaNOzLp$xECKqIq1!gpv?va$?W&F z*USK%fQ~`keU-%#O7I|mjmqvP6WH;OZUpAkHJDOL_~oi$_Z!4V#<@3=j!cw z!6LMdg}q_b3QE{AY&G%oM}<*&4-u>*3(o&oKoFOFBg$G0UVi=lb>#56<>r3tPiemW zC9LY>7zDg*V)9C2Yxt_Ma9=C#rqc(^+SS~M4KYq`UK+w2JWfTs^wTo;$R(kzAr}s9 z<^&R)cF#c^c4(lo)x8d>=k|q&7V_R+l2G-oc2V`Ice>;EZlnsimiBakU~9B@th}j< zsv8!im6u*zf0WEMmJslV%EhG>23{1yC#Is$9ER~fQR8)#PP}qg0HFh78`_m8O&dDA zI9644{>w82K_U$^DTZ+xsuD={pi~9*XH_u0Eit1I^_|z4ZD=m2N&GHXkYYFzPr>vt zWQh^vomBf_IO^VTTmR4i%|8B`g?TrbOsG!sXAwC8w$3lP-6l=|re6Jec6Wt(RCfGG zlNNsp<)o6dv<-~@zA+@5NJ+BqgxKW6y_(RcN0>jEqEl|D56T>mU{cpYBi;`HkQzSICmQu$nrMMA~XRWkAe$t2?3*%z1vg3_Is} z{p#l{e}Prj?io_gRl3zW=eJDD>^}o|MzN+DhEk=O`ba4;&_Q2G1_qoS`xmd?d}IXFozjM5K@G zea{#y)X{WW8Lz=7|Jd0ri^6q-#@;P587VF*07*_xCOycJLHw+9`E9uQF_&G|nKDEH zLqv1nH|k`}bL(Se#_-ktPoUsT)Qi7{>)X}MV|={T?=BgIEGd!cG~A;z zbs2QUM9#GFlRzARMeNiaKXGE&G=^LDA+m9jWG+nNzq&|~R-FgEV{(#*WG3{MFB2i!;b_QY>_omqbAdC#}1Z9NfhR0$}6|* z=asv(I03MK>m4#dKo3yRq$Br$Y=Y}L4s6xSQ>8%v1KeFy(=e+vZ=VITn>QPYZAq8z zALRKAamX``AeCI<~RR>gNL_q4j!LL%IgN(EhOcBbB*_Xi%%7uR!Z_vuen}?A-T>Z z&34e<2wPl`RS$l|G$pR)cCeZ1BRV%XxwIbSonaptwmV>C!JO^;CF*FtoJvaKrRLd{ zOq`S12D`T=AX{8qTsCKYfc6EKg^cM8rF|o`Q(hiz-Uhc19+`Z(JCTPA7~Zq+BfYOUoy_*C|9w#ueJ5Qn1Mx zZn|DHiT63UKbfn9S_zA#60R?&vMS+lxN=LTfpJ*noW>v4oFFp(zQdEO{v!i|UVm!= z&b?9SzYTu>V;#_cEuCQ=p#P7l=>PfEju`!vG_ywI!lhG4KzI7b1!F6tN`ou0{{p%} BmAU`` literal 0 HcmV?d00001 diff --git a/assets/eq2.png b/assets/eq2.png new file mode 100644 index 0000000000000000000000000000000000000000..a6746de22b0c1462475815e43d8f3f737d9c5e8d GIT binary patch literal 20157 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 literal 0 HcmV?d00001 diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..24cd15f --- /dev/null +++ b/build.cmd @@ -0,0 +1,13 @@ +echo WARNING: THIS WILL OVERWRITE app.py WITH A CONVERTED VERSION OF THE .UI FILE. + +set /p INPUT=Continue? [y/n] + +If %INPUT%=="y" goto yes +If %INPUT%=="n" goto no + +:yes +pyuic5.exe -x .\app.ui -o app.py +exit + +:no +exit diff --git a/crds_calc.py b/crds_calc.py new file mode 100644 index 0000000..f2b66af --- /dev/null +++ b/crds_calc.py @@ -0,0 +1,112 @@ +import numpy as np +from scipy.signal import find_peaks, correlate + +def spaced_groups( + x_data: np.array, + y_data: np.array, + group_len: float, + peak_minheight: float, + peak_prominence: float, + sma_denom: int +): + """ + Use SpacedGroups algo to separate groups + + Returns 2D array of raw data; every other group + """ + + # Helpers + def t2i(t): # STATIC time to index + delta_t = abs(x_data[0] - t) + timestep = abs(x_data[1] - x_data[0]) + return int(delta_t / timestep) + + def t2i_range(t): # time to index RANGE (just get the delta) + timestep = abs(x_data[1] - x_data[0]) + return int(t / timestep) + + def moving_average(x, w): + return np.convolve(x, np.ones(w), 'valid') / w + + def isolate_group(i): + i_min = i - t2i_range(group_len) + i_max = i + t2i_range(group_len) + return y_data.tolist()[i_min:i_max] + + # Detect peaks w/ averaged data + x_data_av = np.delete(x_data, [range(int(sma_denom / 2))]) + x_data_av = np.delete(x_data_av, [range(len(x_data)-int((sma_denom / 2) - 1), len(x_data_av))]) + y_data_av = moving_average(y_data, sma_denom) + peak_indices = find_peaks(y_data_av, height=peak_minheight, prominence=peak_prominence) # Get indices of all peaks + + peaks = [] + for p in peak_indices[0]: # Get x-values of all peaks + peaks.append(x_data[p]) + + + # Group peaks together + peak_groups = [[]] + group_index = 0 + for i in range(len(peaks)): + item = peaks[i] + next_item = 0 + prev_item = item-10 + try: + next_item = peaks[i+1] + prev_item = peaks[i-1] + except: + pass + + peak_groups[group_index].append(item) + + # Removed overlapping peak checks; don't really need that anymore + if abs(next_item - item) >= group_len: # Check if far enough away for new group + peak_groups.append([]) + group_index += 1 + + for g in peak_groups: # empty group check + if len(g) == 0: + peak_groups.remove(g) + + peaks_init = [] # Get peaks[0] for every group + for g in peak_groups: + peaks_init.append(g[0]) + + # Isolate group data + + groups_raw = [] # NOTE: Only contains every other group + for p in peaks_init: + if peaks_init.index(p) % 2 == 0: + groups_raw.append(isolate_group(t2i(p))) + else: + pass + + return groups_raw + +def correlate_groups(groups_raw): + """ + Overlay groups using `scipy.correlate`. + + Returns 2D array of overlayed groups + """ + + # Compare groups (scipy correlate) + + group_base = np.array(groups_raw[0]) + groups_adjusted = [group_base] + for x in groups_raw[1:len(groups_raw)-1]: + # calculate how much to shift + corr = correlate(group_base, np.array(x)) + shift = corr.tolist().index(max(corr)) + + # adjust alignment to fit on top of base group + diff = shift - len(x) + if diff < 0: + for i in range(abs(diff)): + x.pop(0) + elif diff > 0: + x = [0 for i in range(abs(diff))] + x + + groups_adjusted.append(x) + + return groups_adjusted \ No newline at end of file diff --git a/memdb.py b/memdb.py new file mode 100644 index 0000000..108d253 --- /dev/null +++ b/memdb.py @@ -0,0 +1,10 @@ +from sqlitedict import SqliteDict + +class ModSqliteDict(SqliteDict): + def __init__(self): + + # Initialize in-memory db + self.filename = ':memory:' + super().__init__() + +mem = ModSqliteDict() \ No newline at end of file diff --git a/widgets.py b/widgets.py new file mode 100644 index 0000000..a24e57b --- /dev/null +++ b/widgets.py @@ -0,0 +1,243 @@ +import numpy as np +from pandas import read_csv +import matplotlib +matplotlib.use('Qt5Agg') +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar +from matplotlib.figure import Figure +import crds_calc +import PyQt5 +from PyQt5 import QtWidgets, QtCore +from memdb import mem +import pathlib + +# Helper functions + +def display_warning(message: str): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.warning) + msg.setText("Warning") + msg.setInformativeText(message) + msg.setWindowTitle("Warning") + msg.exec_() + +def display_error(message: str): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setText("Error") + msg.setInformativeText(message) + msg.setWindowTitle("Error") + msg.exec_() + +# Create global object to send signals + +class Global(QtWidgets.QWidget): + grouping_algo_changed = QtCore.pyqtSignal() + csv_selected = QtCore.pyqtSignal() + correlation_complete = QtCore.pyqtSignal() + fitting_complete = QtCore.pyqtSignal() +globj = Global() + +# Menu definitions + +class file_menu(QtWidgets.QMenu): + def __init__(self, x): + super().__init__(x) + open_csv = QtWidgets.QAction("Open CSV File", self) + open_csv.setShortcut("Ctrl+O") + open_csv.triggered.connect(self.select_csv) + self.addAction(open_csv) + + def select_csv(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName(self) + data = read_csv(filename, comment="%", delimiter=";").to_numpy() + mem['x_data'] = data.transpose()[0] + mem['y_data'] = data.transpose()[1] + try: + mem['v_data'] = data[2].transpose() + except IndexError: + display_error('No voltage column detected. Functionality will be limited.') + + globj.csv_selected.emit() + +class help_menu(QtWidgets.QMenu): + def __init__(self, x): + super().__init__(x) + visit_repo = QtWidgets.QAction("Go to GitHub Repo", self) + visit_repo.triggered.connect(self.go_to_repo) + self.addAction(visit_repo) + + def go_to_repo(self): + PyQt5.QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://github.com/turtlebasket/crds-time-analyzer')) + +# Inputs / Parameter boxes + +class combo_grouping_algo(QtWidgets.QComboBox): + def change(self): + mem['grouping_algo'] = self.currentIndex() + globj.grouping_algo_changed.emit() + def __init__(self, x): + super().__init__(x) + self.currentIndexChanged.connect(self.change) + +class config_area(QtWidgets.QStackedWidget): + def __init__(self, x): + super().__init__(x) + self.setCurrentIndex(0) + globj.grouping_algo_changed.connect(lambda: self.setCurrentIndex(mem['grouping_algo'])) + +class spin_min_voltage(QtWidgets.QDoubleSpinBox): + def changeVal(self, val): + mem['min_voltage'] = float(val) + def __init__(self, x): + super().__init__(x) + mem['min_voltage'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class spin_group_len(QtWidgets.QDoubleSpinBox): + def changeVal(self, val): + mem['group_len'] = float(val) + def __init__(self, x): + super().__init__(x) + mem['group_len'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class spin_peak_len(QtWidgets.QDoubleSpinBox): + def changeVal(self, val): + mem['peak_len'] = float(val) + def __init__(self, x): + super().__init__(x) + mem['peak_len'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class spin_min_peakheight(QtWidgets.QDoubleSpinBox): + def changeVal(self, val): + mem['peak_minheight'] = float(val) + def __init__(self, x): + super().__init__(x) + mem['peak_minheight'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class spin_min_peakprominence(QtWidgets.QDoubleSpinBox): + def changeVal(self, val): + mem['peak_prominence'] = float(val) + def __init__(self, x): + super().__init__(x) + mem['peak_prominence'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class spin_moving_average_denom(QtWidgets.QSpinBox): + def changeVal(self, val): + mem['moving_avg_denom'] = int(val) + print(isinstance(val, str)) + def __init__(self, x): + super().__init__(x) + mem['moving_avg_denom'] = float(self.value()) + self.textChanged.connect(self.changeVal) + +class equation_view(QtWidgets.QGraphicsView): + def __init__(self, x): + super().__init__(x) + pix = PyQt5.QtGui.QPixmap(f"{pathlib.Path(__file__).parent.resolve()}/assets/eq2.png") + item = QtWidgets.QGraphicsPixmapItem(pix) + item.setScale(0.15) + scene = QtWidgets.QGraphicsScene() + scene.addItem(item) + self.setScene(scene) + +class correlate_button(QtWidgets.QPushButton): + def calc(self): + groups_raw = None + try: + if (mem['grouping_algo'] == 0): + display_error('VThreshold not yet implemented.') + elif (mem['grouping_algo'] == 1): + groups_raw = crds_calc.spaced_groups( + mem['x_data'], + mem['y_data'], + mem['group_len'], + mem['peak_minheight'], + mem['peak_prominence'], + mem['moving_avg_denom'] + ) + + mem['groups_correlated'] = crds_calc.correlate_groups(groups_raw) + globj.correlation_complete.emit() + + except KeyError: + display_error('Failed to correlate. Did you import a data file & set parameters?') + + + def __init__(self, x): + super().__init__(x) + self.pressed.connect(self.calc) + +class fit_button(QtWidgets.QPushButton): + def fit(self): + print("hi") + def __init__(self, x): + super().__init__(x) + self.pressed.connect(self.fit) + + +# Graph stuff + +class graph_tab(QtWidgets.QTabWidget): + def __init__(self, x): + super().__init__(x) + globj.csv_selected.connect(lambda: self.setCurrentIndex(0)) + globj.correlation_complete.connect(lambda: self.setCurrentIndex(1)) + globj.fitting_complete.connect(lambda: self.setCurrentIndex(2)) + +class MplCanvas(FigureCanvasQTAgg): + def __init__(self, parent=None, width=5, height=4, dpi=100): + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + fig.tight_layout() + super(MplCanvas, self).__init__(fig) + +class base_graph(QtWidgets.QWidget): + """ + Widget with embedded matplotlib graph & navigation toolbar + + Reference: https://www.mfitzp.com/tutorials/plotting-matplotlib/ + """ + + canv = None + + def __init__(self, x): + super().__init__(x) + self.canv = MplCanvas(self) + # Example + # canv.axes.plot([0,1,2,3,4], [10,1,20,3,40]) + toolbar = NavigationToolbar(self.canv, self) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(toolbar) + layout.addWidget(self.canv) + self.setLayout(layout) + + def plot(self): + self.canv.axes.plot(mem['x_data'], mem['y_data']) + + def plot_full(self): + self.canv.axes.clear() + self.plot() + self.canv.draw() + print("attempted plot") + +class rawdata_graph(base_graph): + def __init__(self, x): + super().__init__(x) + globj.csv_selected.connect(self.plot_full) + +class peaks_graph(base_graph): + def __init__(self, x): + super().__init__(x) + globj.correlation_complete.connect(self.plot_full) + + def plot(self): + for i in mem['groups_correlated']: + self.canv.axes.plot(i) + + +class timeconstant_graph(base_graph): + pass \ No newline at end of file