├── .gitattributes ├── .idea ├── .gitignore ├── betsi-gui.iml ├── betsi.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── betsi ├── .idea │ ├── .gitignore │ ├── betsiv8.iml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── markdown-navigator-enh.xml │ ├── markdown-navigator.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── __init__.py ├── __main__.py ├── __pycache__ │ ├── lib.cpython-37.pyc │ ├── plotting.cpython-37.pyc │ └── utils.cpython-37.pyc ├── data │ ├── Al fumarate.csv │ ├── DMOF-1.csv │ ├── EXP_ZIF-8powder.csv │ ├── HKUST-1.csv │ ├── MCM-41.csv │ ├── MIL-100.csv │ ├── MIL-101.csv │ ├── MOF-5.csv │ ├── Mg-MOF-74.csv │ ├── NU-1000.csv │ ├── NU-1102.csv │ ├── NU-1104.csv │ ├── NU-1105.csv │ ├── PCN-777.csv │ ├── TPB-DMTP-COF.csv │ ├── UiO-66-NH2.csv │ ├── UiO-66.csv │ ├── ZIF-8.csv │ └── Zeolite-13X.csv ├── gui.py ├── lib.py ├── plotting.py └── utils.py ├── cli.py ├── cli.spec ├── docs └── images │ ├── BETSI-logo.jpeg │ ├── a2ml_logo.png │ ├── betsi_logo.PNG │ ├── executables-banner.PNG │ ├── step-1.png │ ├── step-10.png │ ├── step-11.png │ ├── step-12.png │ ├── step-13.png │ ├── step-14.png │ ├── step-2.png │ ├── step-3.png │ ├── step-4.png │ ├── step-5.png │ ├── step-6.png │ ├── step-7.png │ ├── step-8.png │ └── step-9.png ├── executables ├── BETSI_v1.1.0_windows.exe └── mac_executables.zip ├── requirements.txt └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | executables/betsi-linux filter=lfs diff=lfs merge=lfs -text 2 | executables/betsi-windows.exe filter=lfs diff=lfs merge=lfs -text 3 | executables/**/* filter=lfs diff=lfs merge=lfs -text 4 | executables/* filter=lfs diff=lfs merge=lfs -text 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Default ignored files 3 | /workspace.xml -------------------------------------------------------------------------------- /.idea/betsi-gui.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/betsi.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - easy_install distribute 6 | - pip install -r requirements.txt 7 | script: cd betsi; python3 gui.py 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Adsorption and Advanced Materials Lab (AAML), Department of 4 | Chemical Engineering & Biotechnology, University of Cambridge. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft betsi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 |

3 | A2ML 4 |

5 | 6 |

7 | 8 | [![Build Status](https://travis-ci.com/nakulrampal/betsi-gui.svg?token=Z8uG4PAMYmS7Xn1zqF5i&branch=master)](https://travis-ci.com/nakulrampal/betsi-gui) 9 | [![Documentation Status](https://readthedocs.org/projects/aamplify/badge/?version=latest)](https://aamplify.readthedocs.io/en/latest/?badge=latest) 10 | [![Gitter](https://badges.gitter.im/betsi-gui/community.svg)](https://gitter.im/betsi-gui/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Commit Activity](https://img.shields.io/github/commit-activity/m/nakulrampal/betsi-gui)](https://github.com/nakulrampal/betsi-gui/pulse) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/nakulrampal/github-gui/LICENSE.txt) 11 | 12 |

13 | 14 |

15 | banner 16 |

17 | 18 | ## Software Requirements: 19 | 20 | ### OS Requirements 21 | This package is supported for *windows*, *macOS* and *Linux*. The package has been tested on the following systems: 22 | + Windows: 10 (10.0.19041) 23 | + macOS: Mojave (10.14.1) 24 | + Linux: Ubuntu 16.04 25 | 26 | ### Python Dependencies 27 | 28 | `betsi-gui` mainly depends on the Python scientific stack. 29 | 30 | ``` 31 | numpy==1.19.3 32 | scipy==1.5.4 33 | matplotlib==3.2.2 34 | PyQt5==5.9.2 35 | pandas==1.1.5 36 | seaborn==0.11.0 37 | statsmodels==0.12.1 38 | ``` 39 | ## Running BETSI from an executable 40 | 41 | Download the executables for *Windows* or *Linux* found in the repositories run them on your machine. This will automatically run the code for you and take you immediately to the [Instructions of use](#instructions-of-use) found below. If instead you wish to download the source code and install BETSI on your machine, please follow the steps below. 42 | 43 | ## Steps to install BETSI 44 | 45 | *Estimated installation time*: ***10 minutes*** 46 | 47 | Download Anaconda from https://anaconda.org for your operating system. Once you have done so, open the Anaconda Navigator program. 48 | 49 | # step-1 50 | 51 | Next, create a new environment by clicking ```Create``` on the bottom left corner. You can give your environment and arbitrary name (we have called ours ```betsi```) and select as a package ```Python 3.7```. 52 | 53 | # step-2 54 | 55 | If you have successfully created a new environment, it should appear under the base environment. Next, click the :arrow_forward: button in the newly created environment and select ```Open Terminal``` 56 | 57 | # step-4 58 | 59 | This will prompt a command terminal in the new environment. 60 | 61 | # step-5 62 | 63 | ### :star: Option 1: via ```pypi``` 64 | 65 | Next, type in the command: ```pip install -i https://test.pypi.org/simple/ betsi-gui``` 66 | 67 | step-6 68 | 69 | ### :point_right: Option 2: using the source code 70 | 71 | Navigate to the location where you have stored the source code and enter the directory ```betsi-gui```. Once in the directory, run the command ```python setup.py install```, followed by the command ```pip install .``` 72 | 73 | To read more about using the command line please visit: [windows](https://www.computerhope.com/issues/chusedos.htm), [linux](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview), [macOS](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line). 74 | 75 | # 76 | :heavy_check_mark: This will install BETSI in the newly created environment and download all the relevant python packages from our test server. 77 | 78 | ## Instructions of use 79 | 80 | *Estimated run time*: ***5 minutes*** 81 | 82 | Next, to run BETSI, type in the command: ```python -m betsi``` 83 | 84 | # step-7 85 | 86 | Run the command, which will prompt the BETSI GUI. This step may take some time. The BETSI GUI will appear with its default settings as laid out in the Rouquerol criteria. Run an isotherm in the GUI by dragging a correct ```.csv``` file into the empty space on the right. Test isotherms can be found in the repository. Note that isotherms will only run successfully in BETSI if they are in the same format as the exemplary isotherms, further information can be found in section [Test Dataset](#test-dataset) below. 87 | 88 | # step-9 89 | 90 | The code will run automatically and two windows appear. For a full explanation of all figures, please refer to the Supplementary Information of the manuscript, Section S5. 91 | 92 | # step-10 93 | 94 | Further, you can interact with the GUI by manually selecting other Rouquerol-permitted BET areas. In the ```Filtered BET areas``` plot, click on one of the other points. All plots will automatically update to the new selected linear region/BET area. The ```active``` plot is always shown in yellow. 95 | 96 | # step-11 97 | 98 | To output BETSI data, select an output directory and click ```Export Results``` in the GUI. 99 | 100 | # step-12 101 | 102 | The specified directory will contain ```.pdf``` prints of the two active plots (BETSI analysis and regression diagnostics), a ```.json``` file specifying the filter criteria, a ```.txt``` file featuring a small summary, and a folder containing all matrices that the program uses. 103 | 104 | # step-14 105 | 106 | Analyse a new isotherm in BETSI by clearing the current plot either via ```Tools-> Clear```, or by pressing the hotkey combination ```CMD/CTRL+C```. 107 | 108 | # step-13 109 | 110 | 111 | ## Test Dataset 112 | 113 | A test dataset of isotherms is supplied on this repository. To run the isotherms in BETSI, download the dataset and drag isotherms into the BETSI GUI as described above. If you would like to try BETSI with your own dataset, you will need to convert it first into the same format as the test isotherms: It must be a 2-column ```.csv``` file with the relative pressure in the first column and the adsorbed quantity in the second. The first row will not be read as this usually contains the header. You must use an adsorption isotherm only, a desorption swing, or discontinuity in the adsorption from pressure equilibration issues will result in an error, with the PChip interpolation method. 114 | 115 | ## License 116 | 117 | BETSI is distributed under the MIT open source license (see [`LICENSE.txt`](LICENSE.txt)). 118 | 119 | ## Acknowledgements 120 | 121 | Main Developers: James Rampersad, Johannes W. M. Osterrieth, and Nakul Rampal 122 | 123 | This work is supported by: 124 | * [Cambridge International Scholarship](https://www.cambridgetrust.org/) funded by the Cambridge Commonwealth, European & International Trust; 125 | * [Trinity-Henry Barlow Scholarship](https://www.trin.cam.ac.uk/) (Honorary) funded by Trinity College, Cambridge. 126 | 127 | A2ML -------------------------------------------------------------------------------- /betsi/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /betsi/.idea/betsiv8.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /betsi/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /betsi/.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /betsi/.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /betsi/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /betsi/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /betsi/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /betsi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/betsi/__init__.py -------------------------------------------------------------------------------- /betsi/__main__.py: -------------------------------------------------------------------------------- 1 | from betsi.gui import runbetsi 2 | 3 | def main(): 4 | runbetsi() # pylint: disable=no-value-for-parameter 5 | 6 | if __name__ == "__main__": 7 | main() -------------------------------------------------------------------------------- /betsi/__pycache__/lib.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/betsi/__pycache__/lib.cpython-37.pyc -------------------------------------------------------------------------------- /betsi/__pycache__/plotting.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/betsi/__pycache__/plotting.cpython-37.pyc -------------------------------------------------------------------------------- /betsi/__pycache__/utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/betsi/__pycache__/utils.cpython-37.pyc -------------------------------------------------------------------------------- /betsi/data/Al fumarate.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 6.51E-05,0.17817321 3 | 9.45E-05,0.349631375 4 | 0.000140029,0.59994436 5 | 0.000168502,1.155145865 6 | 0.000212523,3.039106608 7 | 0.000256869,22.95957802 8 | 0.000285953,42.88725781 9 | 0.000318927,62.80599158 10 | 0.00032856,63.58629756 11 | 0.000397185,67.50137289 12 | 0.000427058,87.43908993 13 | 0.000458442,107.3250352 14 | 0.000511064,127.1861418 15 | 0.000580619,145.2756391 16 | 0.000637337,156.142065 17 | 0.000890495,175.8379637 18 | 0.001019138,182.5277364 19 | 0.001355172,190.5388013 20 | 0.001632038,195.2730373 21 | 0.002111946,200.3510141 22 | 0.002599008,203.990202 23 | 0.003298337,208.0650155 24 | 0.004368838,212.1661725 25 | 0.005230467,214.6878989 26 | 0.006777789,218.0580083 27 | 0.008474152,220.9052842 28 | 0.010761704,223.9901988 29 | 0.014077117,227.1368389 30 | 0.017025411,229.3487793 31 | 0.022138234,232.3009849 32 | 0.027920961,234.8889913 33 | 0.035532871,237.4528331 34 | 0.044729196,239.8483793 35 | 0.056862545,242.3131476 36 | 0.074640451,244.9876403 37 | 0.094446941,247.2833845 38 | 0.119931747,249.5926078 39 | 0.149860444,251.759821 40 | 0.188606945,254.05 41 | 0.237609088,256.4403221 42 | 0.30241524,259.2404553 43 | 0.375744873,262.2958431 44 | 0.474393139,266.7574807 45 | 0.538919084,270.3468677 46 | 0.602620402,274.6711857 47 | 0.64403838,278.1431281 48 | 0.684098233,282.0798043 49 | 0.723912436,286.6403969 50 | 0.763564176,292.0588002 51 | 0.790607423,296.3022283 52 | 0.815628686,300.7223269 53 | 0.840576799,305.7748296 54 | 0.865722821,311.7378262 55 | 0.91707011,330.6836475 56 | 0.940410038,346.4244636 57 | 0.961980007,368.4154227 58 | 0.97070987,381.5347285 -------------------------------------------------------------------------------- /betsi/data/DMOF-1.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.0000475,0.291525027 3 | 0.0000969,0.57683055 4 | 0.000123181,0.620048828 5 | 0.000128577,0.658135595 6 | 0.000164533,0.95282625 7 | 0.000217301,5.996396582 8 | 0.000257214,15.96630557 9 | 0.000279122,25.93740552 10 | 0.000300744,35.90617865 11 | 0.000322968,45.87471218 12 | 0.000351409,56.18553815 13 | 0.000368692,66.15244743 14 | 0.000383691,76.11937752 15 | 0.000393716,86.08662015 16 | 0.000410668,96.05266401 17 | 0.000431396,106.0217067 18 | 0.000433515,115.9905568 19 | 0.000434096,125.9564861 20 | 0.00044681,135.9195637 21 | 0.000448218,155.8530741 22 | 0.000460131,212.5 23 | 0.000469896,242.8 24 | 0.000479868,263.6 25 | 0.000495921,293.5893529 26 | 0.000505841,312.6029111 27 | 0.000522755,322.5586345 28 | 0.000561279,332.5075569 29 | 0.000612371,351.4762077 30 | 0.000762905,361.3968901 31 | 0.001046492,371.2898879 32 | 0.001338775,376.7 33 | 0.001690835,381.1722028 34 | 0.002200656,385.8 35 | 0.002830961,390.9188471 36 | 0.003348623,396.2006375 37 | 0.003953084,406.0520849 38 | 0.004340588,415.523028 39 | 0.005364889,425.1448932 40 | 0.006599917,430.6897216 41 | 0.008409006,435.1444608 42 | 0.010647117,438.35 43 | 0.01347842,440.7835726 44 | 0.017400821,442.8405229 45 | 0.022037446,444.346239 46 | 0.028872495,445.7078586 47 | 0.037948501,446.8042356 48 | 0.043221172,447.2464063 49 | 0.056330468,448.001404 50 | 0.075704826,448.6718223 51 | 0.096747454,449.1243456 52 | 0.12106687,449.47 53 | 0.148103775,449.7321326 54 | 0.182104311,449.9648932 55 | 0.230197892,450.2006076 56 | 0.294028489,450.4356866 57 | 0.376152789,450.6699096 58 | 0.475149693,450.9570748 59 | 0.539634859,451.200884 60 | 0.602891293,451.544312 61 | 0.644112905,451.8738368 62 | 0.683964225,452.333743 63 | 0.723676637,453.0147327 64 | 0.762441498,454.1258688 65 | 0.789400207,455.3847044 66 | 0.815016979,457.0811051 67 | 0.840145607,459.2774529 68 | 0.86521186,462.0303932 69 | 0.93003418,467.3296084 70 | 0.958849806,467.7860947 71 | 0.98337829,468.3480288 -------------------------------------------------------------------------------- /betsi/data/EXP_ZIF-8powder.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po),Quantity Adsorbed (cm³/g STP) 2 | 4.19974E-05,20.28274449 3 | 9.96909E-05,40.67296372 4 | 0.000148121,60.87679503 5 | 0.000188506,81.25108951 6 | 0.000224278,101.619451 7 | 0.000259844,122.0302949 8 | 0.000299953,142.3989108 9 | 0.000349057,162.7862552 10 | 0.000417927,183.0768101 11 | 0.000521085,203.3467253 12 | 0.000684773,223.5674918 13 | 0.000949862,243.5863528 14 | 0.001422718,263.1878155 15 | 0.00193056,275.0843134 16 | 0.002936687,288.4217818 17 | 0.003871099,295.8545087 18 | 0.004945074,301.7994394 19 | 0.005730774,305.3298612 20 | 0.006698907,312.6281648 21 | 0.008054492,323.3365919 22 | 0.008892109,328.0720786 23 | 0.009781263,332.3208478 24 | 0.02074716,356.4611424 25 | 0.029110932,364.3844726 26 | 0.038163686,400.8022049 27 | 0.04836001,414.1358694 28 | 0.063774299,420.0631319 29 | 0.069776865,421.7029946 30 | 0.08004303,424.0256576 31 | 0.090083618,425.9749438 32 | 0.10043044,427.7245243 33 | 0.145027143,433.4843278 34 | 0.202710154,438.9966576 35 | 0.254860289,443.1539479 36 | 0.305344376,446.72724 37 | 0.354387044,449.9327603 38 | 0.399269895,452.6201371 39 | 0.449413315,455.3270013 40 | 0.499421734,457.8319896 41 | 0.549130569,460.2240887 42 | 0.599553456,462.617951 43 | 0.648965909,465.067526 44 | 0.699055532,467.8544535 45 | 0.748964062,471.0471959 46 | 0.7992439,474.8147805 47 | 0.809235821,475.7689968 48 | 0.819317209,476.7695132 49 | 0.829266798,477.7855803 50 | 0.839403863,478.8588776 51 | 0.850174553,480.0949458 52 | 0.859575733,481.2178317 53 | 0.869079932,482.5717543 54 | 0.879065428,484.1244541 55 | 0.889536882,485.8798938 56 | 0.899627608,487.7359597 57 | 0.90986306,489.8854979 58 | 0.919854649,492.4174808 59 | 0.929147394,495.2036614 60 | 0.939055955,498.7431648 61 | 0.948884511,503.2296235 62 | 0.959088154,509.3877026 63 | 0.969051619,517.7201809 64 | 0.978076108,529.0286753 65 | 0.986984423,548.9853612 66 | 0.994455819,585.4406004 -------------------------------------------------------------------------------- /betsi/data/HKUST-1.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 4.60E-07,29.24223573 3 | 8.08E-07,58.6913572 4 | 1.24E-06,88.1822873 5 | 1.95E-06,117.7708096 6 | 3.47E-06,147.0208837 7 | 1.98E-05,176.6272698 8 | 3.01E-05,188.1 9 | 3.99E-05,197.3 10 | 4.84E-05,205.7335818 11 | 6.00E-05,219.4 12 | 7.19E-05,234.899105 13 | 8.00E-05,247.5 14 | 9.10E-05,263.9058595 15 | 0.000122165,292.8992342 16 | 0.000169951,309.4 17 | 0.000240034,320.8391657 18 | 0.000469896,333.9 19 | 0.001029937,346.7523766 20 | 0.002010342,351.561349 21 | 0.003045762,354.0055815 22 | 0.003948507,355.4263289 23 | 0.004830186,356.484218 24 | 0.005912671,357.5251441 25 | 0.006904737,358.3168714 26 | 0.007945685,359.0361979 27 | 0.00893684,359.633011 28 | 0.009950202,360.2001188 29 | 0.019566394,363.8561184 30 | 0.02937333,366.7213338 31 | 0.039573751,369.6 32 | 0.053048755,373.5872441 33 | 0.059782481,375.5479842 34 | 0.070285857,377.6609013 35 | 0.080382115,379.2731153 36 | 0.090693352,380.7148902 37 | 0.100863524,381.9894138 38 | 0.145578459,386.4698639 39 | 0.201715088,390.404903 40 | 0.255897983,393.2731922 41 | 0.306354706,395.4325597 42 | 0.355727318,396.994765 43 | 0.400710563,398.472277 44 | 0.450996492,399.9312104 45 | 0.500912037,401.2288073 46 | 0.550729386,402.3886749 47 | 0.600539736,403.4473961 48 | 0.650423732,404.4381916 49 | 0.699409212,405.3195484 50 | 0.749430462,406.161677 51 | 0.799589806,406.9765303 52 | 0.810603847,407.2760134 53 | 0.820750466,407.4391274 54 | 0.830251162,407.5884184 55 | 0.840610582,407.7410674 56 | 0.850090332,407.8706136 57 | 0.860139894,408.0103314 58 | 0.870536548,408.1640723 59 | 0.880439116,408.3117647 60 | 0.890351942,408.4410411 61 | 0.900085449,408.5758706 62 | 0.909885718,408.7267665 63 | 0.920408124,408.8781608 64 | 0.930173819,409.027047 65 | 0.940068359,409.1678296 66 | 0.949686839,409.3365257 67 | 0.959833706,409.4983328 68 | 0.969298233,409.6727584 69 | 0.979271939,409.8817061 70 | 0.989260685,410.1012315 71 | 0.998416184,410.5466624 -------------------------------------------------------------------------------- /betsi/data/MCM-41.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 9.79E-03,141.8502983 3 | 1.30E-02,147.3 4 | 0.016992215,153.9 5 | 0.021993683,161.7 6 | 0.02998674,172.9 7 | 0.039658038,183.7882938 8 | 0.056022606,197.1 9 | 0.071295439,206.8902405 10 | 0.104927504,225.1116935 11 | 0.129699104,236.6158331 12 | 0.159960572,249.4753063 13 | 0.191745691,262.3267321 14 | 0.222604245,274.802244 15 | 0.252936316,287.7227679 16 | 0.284027412,303.5133822 17 | 0.305135298,320.3 18 | 0.316012885,333.9432213 19 | 0.333871196,392.7 20 | 0.343960535,420.7730051 21 | 0.372991369,458.9 22 | 0.42755174,493.6786212 23 | 0.447576408,495.090764 24 | 0.476895088,496.9751534 25 | 0.499735922,498.29126 26 | 0.529857594,499.8883749 27 | 0.560582034,501.3842062 28 | 0.591212993,502.7760393 29 | 0.621878896,504.132203 30 | 0.652784998,505.4119977 31 | 0.683268469,506.6427636 32 | 0.713947513,507.8582389 33 | 0.744563968,509.0861455 34 | 0.775127231,510.350724 35 | 0.806787691,511.6682351 36 | 0.836549791,512.9731992 37 | 0.867018855,514.4407391 38 | 0.897225551,516.3071265 39 | 0.937868963,520.8782567 -------------------------------------------------------------------------------- /betsi/data/MIL-100.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.001034,197.1289999 3 | 0.001108417,199.5241455 4 | 0.00118819,202.0821304 5 | 0.001273703,204.8119929 6 | 0.001365372,207.7227303 7 | 0.001463638,210.8231249 8 | 0.001568975,214.1215167 9 | 0.001681894,217.6255089 10 | 0.00180294,221.3415887 11 | 0.001932698,225.2746433 12 | 0.002071794,229.4273454 13 | 0.002220901,233.7993736 14 | 0.002380739,238.3864309 15 | 0.00255208,243.1790121 16 | 0.002735753,248.1608543 17 | 0.002932645,253.3070025 18 | 0.003143708,258.5813915 19 | 0.00336996,263.9338311 20 | 0.003612496,269.2962544 21 | 0.003872487,274.5780503 22 | 0.004151189,279.6602664 23 | 0.00444995,284.3884148 24 | 0.004770213,288.5635505 25 | 0.005113525,292.0917185 26 | 0.005481545,295.4880853 27 | 0.005876051,298.8025947 28 | 0.00629895,302.0144892 29 | 0.006752285,305.1071346 30 | 0.007238246,308.0709686 31 | 0.007759182,310.9074818 32 | 0.00831761,313.6345179 33 | 0.008916228,316.2932685 34 | 0.009557928,318.952217 35 | 0.010245811,321.6227374 36 | 0.010983202,324.287628 37 | 0.011773662,326.9384946 38 | 0.012621011,329.5701329 39 | 0.013529344,332.1824813 40 | 0.01450305,334.783238 41 | 0.015546834,337.3913298 42 | 0.016665738,340.0414743 43 | 0.01786517,342.7901286 44 | 0.019150925,345.7007472 45 | 0.020529216,348.6742774 46 | 0.022006702,351.6818036 47 | 0.023590522,354.7289731 48 | 0.02528833,357.8292806 49 | 0.02710833,361.0073584 50 | 0.029059314,364.303283 51 | 0.031150711,367.7567135 52 | 0.033392625,371.1060915 53 | 0.03579589,374.45872 54 | 0.038372117,378.2027532 55 | 0.041133755,382.8800127 56 | 0.044094149,388.543566 57 | 0.047267601,395.5080957 58 | 0.050669447,404.618763 59 | 0.054316123,416.0045976 60 | 0.05822525,424.1716129 61 | 0.062415717,430.9169639 62 | 0.066907771,436.1151516 63 | 0.071723119,440.0867187 64 | 0.076885026,445.2976561 65 | 0.082418435,451.4351027 66 | 0.088350084,458.5978572 67 | 0.094708633,466.4460405 68 | 0.101524806,472.6192715 69 | 0.108831538,479.6663773 70 | 0.116664136,491.1593287 71 | 0.125060445,506.1904901 72 | 0.134061035,523.4437608 73 | 0.143709398,530.6339937 74 | 0.154052152,533.1608508 75 | 0.165139274,536.3349618 76 | 0.177024334,540.5861805 77 | 0.189764762,541.0808972 78 | 0.203422118,543.6179283 79 | 0.218062392,543.7850362 80 | 0.233756326,545.8906345 81 | 0.25057975,548.8538363 82 | 0.268613956,549.48699 83 | 0.287946082,551.5766383 84 | 0.308669539,552.5692394 85 | 0.330884462,553.5069729 86 | 0.354698192,555.5232089 87 | 0.380225793,556.6596188 88 | 0.407590615,558.3361394 89 | 0.43692488,559.6240671 90 | 0.46837033,560.1315564 91 | 0.502078907,562.1355582 92 | 0.538213488,563.9579248 93 | 0.576948671,564.5252933 94 | 0.618471623,565.8052343 95 | 0.662982978,566.6433628 96 | 0.710697812,568.4923111 97 | 0.76184668,570.9781344 98 | 0.816676727,571.4921361 99 | 0.875452888,572.482785 100 | 0.938459166,574.9414501 -------------------------------------------------------------------------------- /betsi/data/MIL-101.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 7.24E-05,0.08753826 3 | 0.000143586,4.448491732 4 | 0.000194756,24.30986923 5 | 0.00022592,43.86848354 6 | 0.000262361,63.7515091 7 | 0.000320934,83.63128716 8 | 0.000342678,84.53192965 9 | 0.000410737,98.22305237 10 | 0.000490593,118.09 11 | 0.000529528,128.9174716 12 | 0.000645145,148.7499184 13 | 0.000669906,149.0715688 14 | 0.000824694,168.9379598 15 | 0.001013574,188.7064537 16 | 0.00101671,188.8450428 17 | 0.00130365,207.7157656 18 | 0.00169959,227.3744034 19 | 0.00211443,241.6754578 20 | 0.00259059,254.8634509 21 | 0.003406121,271.8776872 22 | 0.004145448,284.5848683 23 | 0.005452415,302.1592916 24 | 0.006729352,316.2096342 25 | 0.008632363,333.190744 26 | 0.010891724,349.6066038 27 | 0.013800564,366.7296068 28 | 0.017400576,384.1509461 29 | 0.02206847,402.7675775 30 | 0.027823594,422.1405734 31 | 0.034458049,441.6472115 32 | 0.04178732,461.039695 33 | 0.044868541,468.661881 34 | 0.053004415,487.8835366 35 | 0.054801326,491.9567727 36 | 0.065061032,514.1039896 37 | 0.071749888,528.2637419 38 | 0.081785963,551.2667698 39 | 0.088453998,568.1409809 40 | 0.099542319,591.8859035 41 | 0.114468276,610.1018687 42 | 0.139194114,629.6962073 43 | 0.155310622,640.70694 44 | 0.181102132,660.5313581 45 | 0.192015037,686.0379762 46 | 0.206189304,712.0552605 47 | 0.246715808,720.2126766 48 | 0.302737488,727.348092 49 | 0.372465882,733.9154027 50 | 0.474121004,740.9614942 51 | 0.537798314,744.4696542 52 | 0.6024543,747.5405809 53 | 0.643579302,749.3566697 54 | 0.684178419,751.0499752 55 | 0.724252702,752.7013834 56 | 0.76441783,754.3729568 57 | 0.790465116,755.4935687 58 | 0.815945002,756.6426051 59 | 0.841290606,757.8866495 60 | 0.866639427,759.2780631 61 | 0.913918632,762.7669094 62 | 0.941891352,766.2296465 63 | 0.965929407,772.2981292 -------------------------------------------------------------------------------- /betsi/data/MOF-5.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 4.88E-05,1.035999212 3 | 0.000103039,1.908035393 4 | 0.000152256,2.155714654 5 | 0.000192992,2.701018481 6 | 0.000227689,3.151022853 7 | 0.000262075,3.805326161 8 | 0.000339317,17.8541585 9 | 0.000410328,37.67918299 10 | 0.000436056,44.47981173 11 | 0.000530897,64.29315122 12 | 0.00068157,83.51374464 13 | 0.000825555,91.83641416 14 | 0.001018034,98.67791888 15 | 0.001387476,112.6054566 16 | 0.001754256,124.3881237 17 | 0.002082039,134.3365982 18 | 0.002634584,154.1134249 19 | 0.003096,173.9258918 20 | 0.003421564,190.6924562 21 | 0.00373714,210.4792227 22 | 0.003990249,230.2631753 23 | 0.004203087,250.0246113 24 | 0.004384442,269.7867715 25 | 0.004550507,289.5232301 26 | 0.004701429,309.269059 27 | 0.004831217,329.0177787 28 | 0.004959268,348.7242874 29 | 0.005083147,368.4494647 30 | 0.005198735,388.1651011 31 | 0.005324749,407.8353851 32 | 0.005452929,427.5402802 33 | 0.005573198,447.2241306 34 | 0.005699939,466.9447989 35 | 0.005873153,486.6044365 36 | 0.006055369,506.4382458 37 | 0.006277627,526.0839057 38 | 0.006538265,545.7713291 39 | 0.006870036,565.4039292 40 | 0.007278209,585.0635997 41 | 0.007840088,604.6021745 42 | 0.008534543,623.8435096 43 | 0.009537856,643.1983601 44 | 0.01071683,659.8969769 45 | 0.012716909,679.3168505 46 | 0.013356843,683.8118561 47 | 0.016783566,703.1560794 48 | 0.017190992,704.6365863 49 | 0.021664124,719.7607782 50 | 0.029557787,736.5798583 51 | 0.037496831,747.2871359 52 | 0.043815956,753.5464095 53 | 0.056354812,763.0255306 54 | 0.075000773,772.764789 55 | 0.094622889,780.0294297 56 | 0.12134011,787.3459355 57 | 0.148705066,793.2239549 58 | 0.186663973,799.9997939 59 | 0.233461347,807.52346 60 | 0.292643557,816.2340696 61 | 0.376872515,825.7953808 62 | 0.477271717,831.2825856 63 | 0.557467879,832.9012232 64 | 0.622324826,833.3172592 65 | 0.663688643,833.2265224 66 | 0.703952672,832.993448 67 | 0.744084723,832.6912456 68 | 0.784234934,832.31166 69 | 0.810369834,831.9487744 70 | 0.835704039,831.6707888 71 | 0.86122081,831.3101984 72 | 0.886535362,830.9686392 73 | 0.936028809,830.3224928 74 | 0.962543788,830.0199416 75 | 0.987971674,829.8951608 -------------------------------------------------------------------------------- /betsi/data/Mg-MOF-74.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.0000341,2.679731275 3 | 0.0000585,3.337321679 4 | 0.0000829,9.384074149 5 | 0.000106975,29.27503048 6 | 0.000125602,49.16683188 7 | 0.000144069,69.05532465 8 | 0.000163778,88.93400688 9 | 0.000187348,108.8053837 10 | 0.000208741,117.6434662 11 | 0.00026291,123.3817269 12 | 0.00032538,143.2548701 13 | 0.000393299,163.0549543 14 | 0.00040154,166.1491326 15 | 0.000460131,177.7 16 | 0.000541423,185.8657739 17 | 0.000650309,188.4257665 18 | 0.000858217,194.4493198 19 | 0.001130007,201.0504363 20 | 0.001352074,204.5451253 21 | 0.001629688,207.8585873 22 | 0.002048444,211.8505979 23 | 0.002672633,216.3557661 24 | 0.00340465,219.8561064 25 | 0.004236093,222.3693104 26 | 0.005229068,224.4735318 27 | 0.006804549,227.0193331 28 | 0.008670638,229.0923041 29 | 0.010602531,230.66 30 | 0.013560594,232.4924596 31 | 0.017513597,234.2534634 32 | 0.02198175,235.6939878 33 | 0.027797594,237.1443946 34 | 0.035558333,238.6413654 35 | 0.045087695,240.0641898 36 | 0.057084319,241.4817518 37 | 0.073852847,242.98 38 | 0.093504522,244.4183565 39 | 0.117917986,245.8493671 40 | 0.148441409,247.3471616 41 | 0.187453493,248.9415548 42 | 0.236737861,250.6734233 43 | 0.30102165,252.5762015 44 | 0.376151248,254.5711403 45 | 0.475697694,256.9706346 46 | 0.539978839,258.3099839 47 | 0.603025345,259.5759029 48 | 0.644358128,260.2908649 49 | 0.684218752,260.9270644 50 | 0.72439712,261.5376325 51 | 0.764352288,262.2155243 52 | 0.790607354,262.6794448 53 | 0.81598638,263.1544686 54 | 0.841261992,263.7248281 55 | 0.866822335,264.3598643 56 | 0.914684081,266 57 | 0.942892019,267.3403463 58 | 0.967733315,269.1591428 -------------------------------------------------------------------------------- /betsi/data/NU-1000.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.003831,245.411 3 | 0.004887863,260.7 4 | 0.005746,280.193 5 | 0.006480225,310.4 6 | 0.006637632,318.6 7 | 0.007204856,344.6 8 | 0.007662,355.556 9 | 0.009205078,364.1 10 | 0.011492,371.014 11 | 0.013393309,377.9 12 | 0.018536751,394.8 13 | 0.02107,401.932 14 | 0.028731,419.324 15 | 0.049801,448.309 16 | 0.066803487,463 17 | 0.080447,473.43 18 | 0.099601,488.889 19 | 0.109178,494.686 20 | 0.120671,504.348 21 | 0.133720965,515.5 22 | 0.147487,527.536 23 | 0.160894,539.13 24 | 0.168556,546.86 25 | 0.178132,556.522 26 | 0.18771,566.184 27 | 0.197287,583.575 28 | 0.199488238,600.2 29 | 0.201118,614.493 30 | 0.204948,643.478 31 | 0.206864,660.87 32 | 0.208461689,678.2 33 | 0.210695,693.72 34 | 0.224103,699.517 35 | 0.268157,707.246 36 | 0.298803,709.179 37 | 0.33328,713.043 38 | 0.358181,716.908 39 | 0.402235,720.773 40 | 0.45012,724.638 41 | 0.503751,726.57 42 | 0.530567,728.502 43 | 0.555468,730.435 44 | 0.603353,730.435 45 | 0.655069,734.3 46 | 0.704869,736.232 47 | 0.735516,738.164 48 | 0.760417,738.164 49 | 0.806386,742.029 50 | 0.856187,742.029 51 | 0.904072,745.894 52 | 0.940464,747.826 53 | 0.999842,757.488 -------------------------------------------------------------------------------- /betsi/data/NU-1102.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.009596,19.41 3 | 0.009612,38.89 4 | 0.009916,88.27 5 | 0.010389,141.52 6 | 0.011031505,234 7 | 0.011550807,318.5 8 | 0.012094554,390.4 9 | 0.012523,419.91 10 | 0.014087,450.63 11 | 0.015418,477.63 12 | 0.017685,512.55 13 | 0.018549,556.89 14 | 0.019567,592.81 15 | 0.020351,627.73 16 | 0.020587,649.53 17 | 0.022229,673.56 18 | 0.024027,691.89 19 | 0.025434,706.26 20 | 0.027544,734.5 21 | 0.029967,756.8 22 | 0.032625,778.85 23 | 0.035154,802.93 24 | 0.037262,822.2 25 | 0.043028,842.26 26 | 0.048236,867.44 27 | 0.054746,888.88 28 | 0.059644,908.95 29 | 0.062374,936.48 30 | 0.067892,962.64 31 | 0.074278,993.91 32 | 0.080293,1024.59 33 | 0.084014,1047.01 34 | 0.090585,1063.54 35 | 0.09759,1088.92 36 | 0.105525,1106.24 37 | 0.115814,1131.24 38 | 0.134969,1165.69 39 | 0.15282,1192.87 40 | 0.161188,1200.36 41 | 0.207176,1222.5 42 | 0.226389,1227.66 43 | 0.247586,1231.85 44 | 0.256944,1234.82 45 | 0.269588,1237.61 46 | 0.291032,1243.17 47 | 0.30975,1244.6 48 | 0.33423,1248.6 49 | 0.359331,1252.79 50 | 0.383316,1255.42 51 | 0.40854,1258.83 52 | 0.434137,1261.25 53 | 0.460167,1263.68 54 | 0.484834,1264.14 55 | 0.510306,1267.36 56 | 0.535346,1271.55 57 | 0.58598,1274.44 58 | 0.611212,1278.79 59 | 0.661582,1281.73 60 | 0.686571,1282.43 61 | 0.711659,1284.99 62 | 0.762915,1285.44 63 | 0.788102,1287.38 64 | 0.813091,1288.08 65 | 0.838867,1289.71 66 | 0.889237,1290.47 67 | 0.894846,1291.73 68 | 0.911964,1293.02 69 | 0.944823,1295.3 -------------------------------------------------------------------------------- /betsi/data/NU-1104.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.002546,85.6082 3 | 0.005092,238.784 4 | 0.006288706,261.9 5 | 0.007638,279.347 6 | 0.009022805,295.4 7 | 0.010183,306.397 8 | 0.012729,324.438 9 | 0.015275,342.478 10 | 0.017821,347.005 11 | 0.020367,378.559 12 | 0.022913,396.6 13 | 0.025459,410.136 14 | 0.028004,441.69 15 | 0.03055,459.731 16 | 0.033096,468.763 17 | 0.035642,477.794 18 | 0.038188,491.331 19 | 0.040734,504.867 20 | 0.045825,522.93 21 | 0.050917,559.011 22 | 0.053463,572.547 23 | 0.056009,581.579 24 | 0.058555,599.62 25 | 0.0611,640.183 26 | 0.063646,667.233 27 | 0.066192,676.264 28 | 0.068738,707.818 29 | 0.071284,730.364 30 | 0.07383,752.909 31 | 0.076376,811.49 32 | 0.078921,829.531 33 | 0.081467,852.076 34 | 0.084013,892.639 35 | 0.086559,906.175 36 | 0.091651,942.256 37 | 0.094197,960.297 38 | 0.096742,978.338 39 | 0.099288,991.874 40 | 0.101835,1000.91 41 | 0.10438,1014.44 42 | 0.106926,1027.98 43 | 0.112018,1041.54 44 | 0.114563,1059.58 45 | 0.119655,1086.65 46 | 0.122201,1091.18 47 | 0.124747,1104.71 48 | 0.127293,1118.25 49 | 0.129839,1127.28 50 | 0.132384,1136.31 51 | 0.13493,1154.35 52 | 0.137476,1167.89 53 | 0.142568,1181.45 54 | 0.145114,1190.48 55 | 0.150205,1204.04 56 | 0.152751,1208.57 57 | 0.155297,1222.1 58 | 0.160388,1231.16 59 | 0.162935,1244.69 60 | 0.165481,1253.72 61 | 0.168026,1262.76 62 | 0.173118,1285.32 63 | 0.175664,1294.35 64 | 0.178209,1307.89 65 | 0.180756,1321.43 66 | 0.183302,1334.96 67 | 0.185847,1348.5 68 | 0.188394,1362.06 69 | 0.190939,1371.04 70 | 0.193485,1411.63 71 | 0.19603,1452.19 72 | 0.198577,1524.29 73 | 0.201122,1542.33 74 | 0.203668,1564.87 75 | 0.206215,1578.41 76 | 0.20876,1591.95 77 | 0.211306,1609.99 78 | 0.213851,1628.03 79 | 0.216398,1641.56 80 | 0.218943,1650.6 81 | 0.224036,1664.15 82 | 0.226581,1673.19 83 | 0.231673,1700.26 84 | 0.234219,1713.79 85 | 0.23931,1722.85 86 | 0.241856,1727.38 87 | 0.25204,1736.48 88 | 0.259677,1741.05 89 | 0.264769,1750.1 90 | 0.269861,1754.65 91 | 0.272406,1754.67 92 | 0.277498,1759.22 93 | 0.282589,1763.77 94 | 0.285136,1763.8 95 | 0.290227,1763.84 96 | 0.297865,1768.41 97 | 0.30041,1772.94 98 | 0.308048,1773.01 99 | 0.310595,1777.54 100 | 0.315686,1777.58 101 | 0.320778,1782.13 102 | 0.325869,1782.18 103 | 0.330961,1782.22 104 | 0.34369,1786.84 105 | 0.351328,1791.41 106 | 0.35642,1791.46 107 | 0.364057,1796.03 108 | 0.369149,1796.07 109 | 0.376786,1796.14 110 | 0.381878,1800.69 111 | 0.38697,1800.74 112 | 0.402245,1800.87 113 | 0.407337,1800.92 114 | 0.412428,1800.96 115 | 0.425158,1801.08 116 | 0.432796,1805.65 117 | 0.440432,1805.72 118 | 0.455708,1810.36 119 | 0.465891,1810.45 120 | 0.488804,1815.16 121 | 0.501533,1819.77 122 | 0.511717,1819.87 123 | 0.516808,1819.91 124 | 0.524446,1824.48 125 | 0.529538,1824.53 126 | 0.537176,1824.6 127 | 0.542267,1824.64 128 | 0.549905,1824.71 129 | 0.560088,1824.8 130 | 0.570271,1824.89 131 | 0.580454,1829.49 132 | 0.593184,1834.1 133 | 0.600822,1834.17 134 | 0.611005,1834.26 135 | 0.623734,1834.37 136 | 0.631372,1838.95 137 | 0.636464,1838.99 138 | 0.654285,1843.66 139 | 0.661922,1843.72 140 | 0.672106,1843.81 141 | 0.687381,1843.95 142 | 0.689927,1843.97 143 | 0.702655,1848.59 144 | 0.710293,1848.66 145 | 0.715385,1848.7 146 | 0.733206,1848.86 147 | 0.735752,1848.88 148 | 0.745935,1848.97 149 | 0.753573,1849.04 150 | 0.763756,1849.13 151 | 0.768848,1849.18 152 | 0.77394,1853.73 153 | 0.779031,1853.77 154 | 0.784123,1853.82 155 | 0.796852,1853.93 156 | 0.81722,1854.11 157 | 0.832494,1858.75 158 | 0.840132,1858.82 159 | 0.847769,1858.89 160 | 0.863045,1863.53 161 | 0.868136,1863.57 162 | 0.87832,1863.67 163 | 0.885957,1863.73 164 | 0.903778,1863.89 165 | 0.957242,1868.87 166 | 0.964878,1868.94 167 | 0.987791,1873.65 168 | 0.992883,1878.2 -------------------------------------------------------------------------------- /betsi/data/NU-1105.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.000145,42.426 3 | 0.000228,66.6694 4 | 0.000249,72.7303 5 | 0.00029,84.852 6 | 0.000311,90.9128 7 | 0.000353,103.035 8 | 0.000436,127.278 9 | 0.003921,145.501 10 | 0.004004,169.7449999 11 | 0.004149,212.171 12 | 0.004232,236.414 13 | 0.004315,260.6579999 14 | 0.00778,272.8199999 15 | 0.007842,291.003 16 | 0.011307,303.166 17 | 0.014793,321.3890001 18 | 0.01832,351.734 19 | 0.021785,363.897 20 | 0.02527,382.12 21 | 0.028735,394.2830001 22 | 0.03222,412.507 23 | 0.035706,430.7300001 24 | 0.039171,442.8929999 25 | 0.042615,448.995 26 | 0.0461,467.218 27 | 0.049565,479.381 28 | 0.05303,491.543 29 | 0.053071,503.665 30 | 0.056536,515.828 31 | 0.05998,521.93 32 | 0.066868,534.133 33 | 0.070333,546.296 34 | 0.077242,564.56 35 | 0.080686,570.662 36 | 0.084171,588.886 37 | 0.087615,594.988 38 | 0.091101,613.2110001 39 | 0.09803,637.5359999 40 | 0.104939,655.8009999 41 | 0.115333,692.289 42 | 0.129089,710.635 43 | 0.132595,734.92 44 | 0.139525,759.2450001 45 | 0.149878,783.611 46 | 0.16023,807.9770001 47 | 0.17743,832.426 48 | 0.191247,868.9550001 49 | 0.205003,887.301 50 | 0.215335,905.606 51 | 0.222223,917.81 52 | 0.225709,936.033 53 | 0.232617,954.298 54 | 0.236144,984.643 55 | 0.239651,1008.93 56 | 0.239734,1033.17 57 | 0.239816,1057.41 58 | 0.243302,1075.64 59 | 0.243406,1105.94 60 | 0.246892,1124.17 61 | 0.257285,1160.65 62 | 0.264153,1166.8 63 | 0.274485,1185.1 64 | 0.281332,1185.18 65 | 0.284796,1197.35 66 | 0.295108,1209.59 67 | 0.305419,1221.84 68 | 0.319153,1234.12 69 | 0.329486,1252.43 70 | 0.346644,1264.75 71 | 0.356935,1270.94 72 | 0.367225,1277.12 73 | 0.387806,1289.49 74 | 0.40152,1295.71 75 | 0.415234,1301.94 76 | 0.425503,1302.06 77 | 0.44264,1308.33 78 | 0.463201,1314.63 79 | 0.483762,1320.94 80 | 0.497475,1327.16 81 | 0.518015,1327.41 82 | 0.538596,1339.779999 83 | 0.548866,1339.9 84 | 0.559136,1340.020001 85 | 0.565982,1340.1 86 | 0.583119,1346.37 87 | 0.596813,1346.529999 88 | 0.631066,1353 89 | 0.65505,1359.35 90 | 0.692727,1365.86 91 | 0.713267,1366.11 92 | 0.73723,1366.4 93 | 0.764616,1366.720001 94 | 0.764637,1372.780001 95 | 0.792043,1379.17 96 | 0.81943,1379.5 97 | 0.822875,1385.6 98 | 0.857107,1386.01 99 | 0.891381,1398.540001 100 | 0.915344,1398.83 101 | 0.918788,1404.93 102 | 0.956444,1405.38 103 | 0.966735,1411.56 104 | 0.966756,1417.63 105 | 0.980469,1423.85 106 | 0.990781,1436.09 107 | 0.994225,1442.2 -------------------------------------------------------------------------------- /betsi/data/PCN-777.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.002879,120.229 3 | 0.005757,173.643 4 | 0.010075,221.693 5 | 0.018711,261.669 6 | 0.023028,280.32 7 | 0.035982,306.875 8 | 0.047496,322.759 9 | 0.066207,346.566 10 | 0.087796,370.334 11 | 0.094992,380.93 12 | 0.109385,394.103 13 | 0.126656,415.256 14 | 0.149684,436.333 15 | 0.168395,454.795 16 | 0.188546,473.237 17 | 0.208695,494.352 18 | 0.228845,515.467 19 | 0.246116,531.275 20 | 0.269145,555.025 21 | 0.289294,578.813 22 | 0.303687,610.694 23 | 0.310883,618.617 24 | 0.318079,629.213 25 | 0.322398,637.174 26 | 0.329594,645.096 27 | 0.331034,650.423 28 | 0.33679,661.037 29 | 0.339669,666.344 30 | 0.345426,679.632 31 | 0.348305,692.957 32 | 0.355501,826.492 33 | 0.356941,834.491 34 | 0.358379,928.013 35 | 0.388605,1341.87 36 | 0.394362,1352.48 37 | 0.401558,1363.08 38 | 0.402997,1395.13 39 | 0.418829,1400.27 40 | 0.451933,1405.18 41 | 0.490793,1410.01 42 | 0.51814,1414.99 43 | 0.567074,1419.69 44 | 0.611692,1421.78 45 | 0.633281,1426.84 46 | 0.726835,1428.28 47 | 0.752741,1433.28 48 | 0.77577,1438.32 49 | 0.823266,1440.37 50 | 0.860687,1442.55 51 | 0.899547,1447.38 -------------------------------------------------------------------------------- /betsi/data/TPB-DMTP-COF.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.001665872,196.9815483 3 | 0.00194442,204.5961096 4 | 0.002868962,225.7845273 5 | 0.003965587,245.5310835 6 | 0.004867865,259.2492878 7 | 0.005726495,270.593821 8 | 0.006790925,282.9579083 9 | 0.007894831,294.2735885 10 | 0.008900371,303.4024294 11 | 0.009887064,311.5334684 12 | 0.011998184,325.7 13 | 0.013995772,337.7 14 | 0.019197382,364.2804376 15 | 0.023006005,380.5 16 | 0.028849446,402.0046323 17 | 0.038241534,432.0550174 18 | 0.0500177,465.0455606 19 | 0.057059164,483.0427724 20 | 0.069001998,511.3181513 21 | 0.07905353,533.2231994 22 | 0.088927524,553.5114555 23 | 0.098902977,572.6886873 24 | 0.117842895,606.2501179 25 | 0.137931437,638.7028528 26 | 0.158643117,670.0773885 27 | 0.178416291,700.0028433 28 | 0.197220756,730.8059603 29 | 0.214357902,773.9836293 30 | 0.224922361,892.8 31 | 0.235340888,999.3264478 32 | 0.268274737,1042.279024 33 | 0.295846618,1060.152312 34 | 0.326118904,1078.688371 35 | 0.342338021,1087.506136 36 | 0.360029297,1096.14 37 | 0.380357356,1105.531046 38 | 0.399925353,1114.12165 39 | 0.448298456,1134.842174 40 | 0.500040283,1156.094989 41 | 0.549395048,1176.262333 42 | 0.599734426,1197.327239 43 | 0.64932966,1219.068473 44 | 0.699352169,1242.598927 45 | 0.74962873,1269.762338 46 | 0.798779288,1301.439784 47 | 0.847691866,1344.863378 48 | 0.896305484,1413.54769 49 | 0.946154066,1564.014228 50 | 0.998120009,2313.935013 -------------------------------------------------------------------------------- /betsi/data/UiO-66-NH2.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.000000848,19.5368906 3 | 0.00000229,39.26330117 4 | 0.00000569,58.89848944 5 | 0.0000104,78.53675968 6 | 0.0000212,98.16156468 7 | 0.0000417,117.7725542 8 | 0.0000881,137.364894 9 | 0.00019284,156.8928206 10 | 0.000441225,176.3458212 11 | 0.000989339,194.9833077 12 | 0.001921761,210.941578 13 | 0.00291461,221.4565494 14 | 0.003944906,229.2971183 15 | 0.004813169,234.4486662 16 | 0.00594435,239.9628281 17 | 0.006835363,243.6246739 18 | 0.007914356,247.5072786 19 | 0.008883336,250.6453829 20 | 0.009932936,253.7236773 21 | 0.01909818,275.1052661 22 | 0.029132958,293.9211205 23 | 0.038429457,310.3121552 24 | 0.050828881,325.7113692 25 | 0.057356098,331.6109696 26 | 0.069821708,340.1961137 27 | 0.0802729,345.4545957 28 | 0.090591613,349.2613875 29 | 0.100871653,352.2163788 30 | 0.145607594,360.6474742 31 | 0.204338998,367.2167836 32 | 0.256823283,371.5722959 33 | 0.307448459,375.18 34 | 0.355311849,378.2737613 35 | 0.400438035,380.9933042 36 | 0.450466992,383.8371455 37 | 0.500562855,386.59486 38 | 0.550628559,389.3255242 39 | 0.600467466,392.1355654 40 | 0.650693889,395.113767 41 | 0.70078011,398.3998811 42 | 0.749541023,402.0117645 43 | 0.799036389,406.4862224 44 | 0.809765349,407.7992544 45 | 0.820897926,409.0453038 46 | 0.830182771,410.1992839 47 | 0.840517528,411.5328422 48 | 0.850135475,412.8666983 49 | 0.860012207,414.3654304 50 | 0.870324132,416.0635373 51 | 0.880063477,417.8667216 52 | 0.890036621,419.9242135 53 | 0.899697316,422.2159538 54 | 0.910224633,425.0406414 55 | 0.920571549,428.2501146 56 | 0.929731796,431.8828211 57 | 0.939225811,436.3450083 58 | 0.948782819,442.3544504 59 | 0.95900507,450.9633684 60 | 0.968527785,463.1038984 61 | 0.97741276,482.2617709 62 | 0.986178212,513.237696 63 | 0.992911451,555.9569992 -------------------------------------------------------------------------------- /betsi/data/UiO-66.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 1.23E-07,2.514208097 3 | 2.48E-07,5.037251954 4 | 3.50E-07,7.558244677 5 | 5.48E-07,10.08585684 6 | 7.55E-07,12.59727996 7 | 9.99E-07,15.10458709 8 | 1.22E-06,17.63878786 9 | 1.47E-06,20.16339997 10 | 1.97E-06,22.69132411 11 | 2.84E-06,25.2214746 12 | 3.85E-06,27.73191927 13 | 5.17E-06,30.25412583 14 | 6.88E-06,32.77569526 15 | 8.83E-06,35.29920653 16 | 1.14E-05,37.83511452 17 | 1.44E-05,40.36452153 18 | 1.78E-05,42.88055967 19 | 2.15E-05,45.41154666 20 | 2.54E-05,47.9453635 21 | 2.91E-05,50.45887736 22 | 3.30E-05,52.99521562 23 | 3.67E-05,55.50982846 24 | 4.04E-05,58.04535971 25 | 4.40E-05,60.58747066 26 | 4.76E-05,63.12764682 27 | 5.12E-05,65.66821784 28 | 5.48E-05,68.20299503 29 | 5.86E-05,70.74937929 30 | 6.25E-05,73.27186156 31 | 6.65E-05,75.79976796 32 | 7.07E-05,78.3308866 33 | 7.53E-05,80.86536352 34 | 7.99E-05,83.38577928 35 | 8.49E-05,85.92587128 36 | 9.02E-05,88.45030224 37 | 9.59E-05,90.98016072 38 | 0.000101866,93.50766048 39 | 0.00010843,96.02943816 40 | 0.000115669,98.56759392 41 | 0.000123923,101.0975406 42 | 0.000132532,103.6438156 43 | 0.000142085,106.1716869 44 | 0.000152225,108.6991722 45 | 0.000163528,111.2194978 46 | 0.000175053,113.7443723 47 | 0.000188404,116.2740431 48 | 0.000203102,118.7988271 49 | 0.000219533,121.3245868 50 | 0.000237273,123.8480286 51 | 0.000256856,126.3777168 52 | 0.000278382,128.9050681 53 | 0.000302122,131.4281643 54 | 0.000328336,133.9662684 55 | 0.000357464,136.4882939 56 | 0.000389421,139.0076204 57 | 0.000425039,141.5317433 58 | 0.00046396,144.0385752 59 | 0.000507494,146.5478842 60 | 0.000555773,149.0602994 61 | 0.000609421,151.5697608 62 | 0.000668535,154.060653 63 | 0.000733715,156.5337288 64 | 0.000805316,158.9949881 65 | 0.0008836,161.4439358 66 | 0.000969555,163.8718429 67 | 0.001180284,168.6 68 | 0.001369924,172.5 69 | 0.001659897,177.7 70 | 0.001983868,182.6426257 71 | 0.002481232,188.8 72 | 0.0029778,193.9555603 73 | 0.003900276,202.0860302 74 | 0.004901601,209.4802401 75 | 0.005748579,214.9546489 76 | 0.006815202,220.9544905 77 | 0.007932263,226.4712673 78 | 0.008908562,230.7932965 79 | 0.009924807,234.9431672 80 | 0.012216106,242 81 | 0.015344472,250.3 82 | 0.020657011,260.2120996 83 | 0.029153998,267.6667152 84 | 0.043038331,274.4918795 85 | 0.05607993,278.4784614 86 | 0.060133027,279.4787193 87 | 0.070310191,281.615879 88 | 0.080444318,283.3976849 89 | 0.090170995,284.8714471 90 | 0.100546237,286.2448034 91 | 0.110563966,287.4462928 92 | 0.120602203,288.5181411 93 | 0.130585935,289.5010852 94 | 0.140095215,290.39 95 | 0.150374866,291.2636477 96 | 0.160507882,292.0511271 97 | 0.170667982,292.8048628 98 | 0.179965883,293.4867474 99 | 0.190645793,294.2087023 100 | 0.200301599,294.8255949 101 | 0.210335014,295.4385895 102 | 0.220487623,296.0307331 103 | 0.230573416,296.5861908 104 | 0.240846958,297.1541411 105 | 0.25056775,297.6650242 106 | 0.260794834,298.1822797 107 | 0.269580715,298.6360556 108 | 0.280301794,299.1344551 109 | 0.290696258,299.6123679 110 | 0.300168734,299.9998531 111 | 0.319236927,300.8437555 112 | 0.340946451,301.7427779 113 | 0.360937434,302.53696 114 | 0.38070078,303.2960293 115 | 0.401083839,304.0663297 116 | 0.421098485,304.7946982 117 | 0.440714586,305.5083048 118 | 0.460879323,306.2269201 119 | 0.479423448,306.9263143 120 | 0.500683264,307.6755514 121 | 0.519294854,308.373187 122 | 0.540988798,309.1130853 123 | 0.559146623,309.8063301 124 | 0.580857521,310.5774736 125 | 0.600769168,311.3005092 126 | 0.620648506,312.0202711 127 | 0.640945441,312.7833075 128 | 0.660586966,313.5369189 129 | 0.680476479,314.3573156 130 | 0.69949284,315.2304864 131 | 0.719410474,316.1909329 132 | 0.739506409,317.1909509 133 | 0.759642624,318.3008609 134 | 0.779642839,319.4802959 135 | 0.799644973,320.7616671 136 | 0.809048329,321.4671465 137 | 0.820769307,322.3134339 138 | 0.829732712,323.0572258 139 | 0.83925335,323.9152679 140 | 0.850505865,324.9141619 141 | 0.860630845,325.9756673 142 | 0.869346002,326.9457206 143 | 0.880763469,328.290526 144 | 0.889852706,329.500242 145 | 0.90087879,331.1254877 146 | 0.909388657,332.5817953 147 | 0.919315551,334.4979075 148 | 0.929206017,336.7771364 149 | 0.93917914,339.6217573 150 | 0.949129528,343.3265922 151 | 0.986264135,404.2262582 152 | 0.989723137,439.3506036 153 | 0.992201704,501.9726082 -------------------------------------------------------------------------------- /betsi/data/ZIF-8.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 4.20E-05,20.349 3 | 9.97E-05,40.698 4 | 0.000148121,60.914 5 | 0.000188506,81.263 6 | 0.000224278,101.612 7 | 0.000259844,122.094 8 | 0.000299953,142.31 9 | 0.000349057,162.26 10 | 0.000417927,183.54 11 | 0.000521085,203.49 12 | 0.000684773,223.44 13 | 0.000949862,243.39 14 | 0.001422718,263.34 15 | 0.00193056,275.31 16 | 0.002936687,288.61 17 | 0.003871099,295.26 18 | 0.004945074,301.91 19 | 0.005730774,305.9 20 | 0.006698907,312.55 21 | 0.008054492,323.19 22 | 0.008892109,328.51 23 | 0.009781263,332.5 24 | 0.02074716,356.44 25 | 0.029110932,364.42 26 | 0.038163686,400.33 27 | 0.04836001,413.63 28 | 0.063774299,420.28 29 | 0.069776865,421.61 30 | 0.08004303,424.27 31 | 0.090083618,425.6 32 | 0.10043044,428.26 33 | 0.145027143,433.58 34 | 0.202710154,438.9 35 | 0.254860289,442.89 36 | 0.305344376,446.88 37 | 0.354387044,449.54 38 | 0.399269895,452.2 39 | 0.449413315,454.86 40 | 0.499421734,457.52 41 | 0.549130569,460.18 42 | 0.599553456,462.84 43 | 0.648965909,465.5 44 | 0.699055532,468.16 45 | 0.748964062,470.82 46 | 0.7992439,474.81 47 | 0.809235821,476.14 48 | 0.819317209,476.14 49 | 0.829266798,477.47 50 | 0.839403863,478.8 51 | 0.850174553,480.13 52 | 0.859575733,481.46 53 | 0.869079932,482.79 54 | 0.879065428,484.12 55 | 0.889536882,485.45 56 | 0.899627608,488.11 57 | 0.90986306,489.44 58 | 0.919854649,492.1 59 | 0.929147394,494.76 60 | 0.939055955,498.75 61 | 0.948884511,502.74 62 | 0.959088154,509.39 63 | 0.969051619,517.37 64 | 0.978076108,529.34 65 | 0.986984423,549.29 66 | 0.994455819,585.2 -------------------------------------------------------------------------------- /betsi/data/Zeolite-13X.csv: -------------------------------------------------------------------------------- 1 | Relative Pressure (P/Po), Quantity Adsorbed (cm3/g STP) 2 | 0.00002,2.026477494 3 | 0.0000579,2.541243526 4 | 0.0000907,3.547198566 5 | 0.0000997,5.866784332 6 | 0.000120002,15.07999999 7 | 0.000135878,25.76287669 8 | 0.000149981,40.33 9 | 0.000153726,45.67806745 10 | 0.000162203,64.12239133 11 | 0.000169951,75.52 12 | 0.00017989,84.02434859 13 | 0.000206222,88.10310565 14 | 0.000247599,90.41453575 15 | 0.000279922,98.40000001 16 | 0.00030673,110.3127874 17 | 0.000320061,118.2 18 | 0.000331941,130.1702139 19 | 0.000340176,149.9101801 20 | 0.000410145,162.7 21 | 0.000537827,169.2831879 22 | 0.000900104,175.9 23 | 0.001686888,181.7420809 24 | 0.00209589,183.1710764 25 | 0.002623178,184.5665064 26 | 0.003299735,185.9154892 27 | 0.004185916,187.207991 28 | 0.005255232,188.3560885 29 | 0.006657108,189.4913756 30 | 0.008491008,190.6152549 31 | 0.01080895,191.6753808 32 | 0.013672316,192.6618808 33 | 0.017379148,193.6303044 34 | 0.022120016,194.5457703 35 | 0.027972214,195.4006837 36 | 0.035502441,196.2615519 37 | 0.04518963,197.0870784 38 | 0.057104756,197.8552004 39 | 0.073503814,198.6615603 40 | 0.092897268,199.3819823 41 | 0.117365134,200.0680882 42 | 0.148033492,200.7177207 43 | 0.186991608,201.3422758 44 | 0.236269718,201.9196185 45 | 0.298618506,202.4531402 46 | 0.376375557,202.9157132 47 | 0.475676689,203.3086953 48 | 0.5396658,203.5210373 49 | 0.603274987,203.6931052 50 | 0.644041058,203.8083676 51 | 0.684269297,203.9188626 52 | 0.724374314,204.0281004 53 | 0.764422491,204.1528227 54 | 0.790647003,204.2464609 55 | 0.815998166,204.363534 56 | 0.841457924,204.4757084 57 | 0.866631605,204.6170522 58 | 0.915618717,205.0325277 59 | 0.942539338,205.5096857 60 | 0.967868769,206.3637334 -------------------------------------------------------------------------------- /betsi/gui.py: -------------------------------------------------------------------------------- 1 | # basics 2 | import os 3 | 4 | from PyQt5 import QtCore 5 | from PyQt5 import QtGui 6 | # qt gui framework 7 | from PyQt5.QtWidgets import * 8 | # matplotlib for plotting 9 | import matplotlib.pyplot as plt 10 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 11 | from matplotlib.figure import Figure 12 | import logging 13 | import sys 14 | 15 | import traceback 16 | 17 | # core lib 18 | plt.ion() 19 | from betsi.lib import * 20 | from betsi.plotting import * 21 | 22 | 23 | class BETSI_gui(QMainWindow): 24 | """Defines the main window of the BETSI gui and adds all the toolbars. 25 | The main widget is the BETSI_widget""" 26 | 27 | def __init__(self): 28 | super().__init__() 29 | 30 | self.title = 'BETSI' 31 | self.setWindowTitle(self.title) 32 | 33 | # define the menu bar 34 | menubar = self.menuBar() 35 | fileMenu = menubar.addMenu('&File') 36 | toolsMenu = menubar.addMenu('&Tools') 37 | 38 | # option for importing a single file. 39 | importfile_menu = fileMenu.addAction('&Import File') 40 | importfile_menu.setShortcut("Ctrl+I") 41 | importfile_menu.triggered.connect(self.import_file) 42 | 43 | # option for changing the root output directory. 44 | outputdir_menu = fileMenu.addAction('&Set Output Directory') 45 | outputdir_menu.setShortcut("Ctrl+O") 46 | outputdir_menu.triggered.connect(self.set_output_dir) 47 | 48 | # option for running on a chosen directory. 49 | directoryrun_menu = fileMenu.addAction('&Analyse Directory') 50 | directoryrun_menu.setShortcut("Ctrl+D") 51 | directoryrun_menu.triggered.connect(self.analyse_dir) 52 | 53 | # option for clearing the memory and resetting to defaults 54 | clear_menu = toolsMenu.addAction('&Clear') 55 | clear_menu.setShortcut("Ctrl+C") 56 | clear_menu.triggered.connect(self.clear) 57 | 58 | # option for replotting 59 | replot_menu = toolsMenu.addAction('&Replot') 60 | replot_menu.triggered.connect(self.replot_betsi) 61 | 62 | self.betsimagic_menu_action = QAction( 63 | 'BETSI Magic', self, checkable=True) 64 | self.betsimagic_menu_action.setStatusTip( 65 | "Resets the settings to default values and performs BETSI calculation") 66 | self.betsimagic_menu_action.setChecked(False) 67 | self.betsimagic_menu_action.triggered.connect(self.trigger_betsimagic) 68 | 69 | betsimagic_menu = toolsMenu.addAction(self.betsimagic_menu_action) 70 | 71 | # define the widget 72 | self.betsi_widget = BETSI_widget(self) 73 | self.setCentralWidget(self.betsi_widget) 74 | 75 | # enable drag and drop 76 | self.setAcceptDrops(True) 77 | 78 | def set_output_dir(self): 79 | self.betsi_widget.set_output_dir() 80 | 81 | def analyse_dir(self): 82 | dir_path = QFileDialog.getExistingDirectory( 83 | self, 'Select Directory', os.getcwd()) 84 | print(f'Imported directory: {Path(dir_path).name}') 85 | self.betsi_widget.analyse_directory(dir_path) 86 | 87 | ## New lines added 88 | def import_file(self): 89 | ## file_path = QFileDialog.getOpenFileName( 90 | ## self, 'Select File', os.getcwd(), '*.csv')[0] 91 | file_path = QFileDialog.getOpenFileName( 92 | self, 'Select File', os.getcwd(), "*.csv *.aif *.txt")[0] 93 | self.betsi_widget.target_filepath = file_path 94 | self.betsi_widget.populate_table(csv_path=file_path) 95 | print(f'Imported file: {Path(file_path).name}') 96 | 97 | def dragEnterEvent(self, e): 98 | data = e.mimeData() 99 | urls = data.urls() 100 | drag_type = [u.scheme() for u in urls] 101 | paths = [u.toLocalFile() for u in urls] 102 | extensions = [os.path.splitext(p)[-1] for p in paths] 103 | # accept files only for now. 104 | ## New lines added 105 | ##if all(dt == 'file' for dt in drag_type) and all(ext == '.csv' for ext in extensions): 106 | if all(dt == 'file' for dt in drag_type) and all(ext in ['.csv', '.aif', '.txt'] for ext in extensions): 107 | e.accept() 108 | elif len(drag_type) == 1 and os.path.isdir(paths[0]): 109 | e.ignore() 110 | else: 111 | e.ignore() 112 | 113 | def dropEvent(self, e): 114 | data = e.mimeData() 115 | urls = data.urls() 116 | paths = [u.toLocalFile() for u in urls] 117 | 118 | # Single path to csv file 119 | ## new lines added 120 | ##if len(paths) == 1 and Path(paths[0]).suffix == '.csv': 121 | if len(paths) == 1 and Path(paths[0]).suffix in ['.csv', '.aif', '.txt']: 122 | self.betsi_widget.target_filepath = paths[0] 123 | self.betsi_widget.populate_table(csv_path=paths[0]) 124 | print(f'Imported file: {Path(paths[0]).name}') 125 | ## New lines added 126 | self.betsi_widget.bet_object = None 127 | self.betsi_widget.bet_filter_result = None 128 | 129 | self.betsi_widget.run_calculation() 130 | 131 | def clear(self): 132 | self.betsi_widget.clear() 133 | 134 | def replot_betsi(self): 135 | self.betsi_widget.run_calculation() 136 | 137 | def trigger_betsimagic(self, state): 138 | if state: 139 | self.betsi_widget.set_defaults() 140 | self.betsi_widget.set_editable(not state) 141 | 142 | def closeEvent(self, evt): 143 | print('Closing') 144 | try: 145 | plt.close(self.bet_widget.current_fig) 146 | plt.close(self.bet_widget.current_fig_2) 147 | except: 148 | pass 149 | 150 | 151 | class BETSI_widget(QWidget): 152 | """Widget containing all the options for running the BETSI analysis and display of the data. 153 | 154 | Args: 155 | parent: QMainWindow the widget belongs to 156 | 157 | """ 158 | 159 | def __init__(self, parent=None): 160 | super().__init__(parent) 161 | 162 | # basic properties 163 | self.output_dir = os.getcwd() 164 | self.current_fig = None 165 | self.current_fig_2 = None 166 | 167 | self.target_filepath = None 168 | self.bet_object = None 169 | self.bet_filter_result = None 170 | 171 | # Create label boxes to display info 172 | self.current_output_label = QLabel() 173 | self.current_targetpath_label = QLabel() 174 | 175 | self.current_output_label.setText( 176 | f"Output Directory: {self.output_dir}") 177 | self.current_targetpath_label.setText( 178 | f"Loaded File: {self.target_filepath}") 179 | self.current_output_label.setFont(QtGui.QFont("Times", 10)) 180 | self.current_targetpath_label.setFont(QtGui.QFont("Times", 10)) 181 | 182 | self.current_output_label.setWordWrap(True) 183 | self.current_output_label.setAlignment(QtCore.Qt.AlignLeft) 184 | #self.current_output_label.setStyleSheet("background-color: lightgreen") 185 | self.current_targetpath_label.setWordWrap(True) 186 | self.current_targetpath_label.setAlignment(QtCore.Qt.AlignLeft) 187 | #self.current_targetpath_label.setStyleSheet("background-color: lightblue") 188 | 189 | # add a group box containing controls 190 | self.criteria_box = QGroupBox("BET area selection criteria") 191 | self.criteria_box.setMaximumWidth(500) 192 | self.criteria_box.setMaximumHeight(800) 193 | self.criteria_box.setMinimumWidth(400) 194 | #self.criteria_box.setMaximumHeight(650) 195 | self.min_points_label = QLabel(self.criteria_box) 196 | self.min_points_label.setText('Minimum number of points in the linear region: [3,10]') 197 | self.min_points_edit = QLineEdit() 198 | self.min_points_edit.setMaximumWidth(75) 199 | self.min_points_slider = QSlider(QtCore.Qt.Horizontal) 200 | self.minr2_label = QLabel('Minimum R2: [0.8,0.999]') 201 | self.minr2_edit = QLineEdit() 202 | self.minr2_edit.setMaximumWidth(75) 203 | self.minr2_slider = QSlider(QtCore.Qt.Horizontal) 204 | self.rouq1_tick = QCheckBox("Rouquerol criterion 1: Monotonic") 205 | self.rouq2_tick = QCheckBox("Rouquerol criterion 2: Positive C") 206 | self.rouq3_tick = QCheckBox("Rouquerol criterion 3: Pressure in linear range") 207 | self.rouq4_tick = QCheckBox("Rouquerol criterion 4: Error in %, [5,75]") 208 | self.rouq5_tick = QCheckBox("Rouquerol criterion 5: End at the knee") 209 | self.rouq4_edit = QLineEdit() 210 | self.rouq4_edit.setMaximumWidth(75) 211 | self.rouq4_slider = QSlider(QtCore.Qt.Horizontal) 212 | ## New lines added 213 | self.adsorbate_label = QLabel('Adsorbate:') 214 | self.adsorbate_combo_box = QComboBox() 215 | self.adsorbate_combo_box.addItems(["N2", "Ar", "Kr", "Xe", "CO2", "Custom"]) 216 | self.adsorbate_cross_section_label = QLabel('Cross sectional area (nm2):') 217 | self.adsorbate_cross_section_edit = QLineEdit() 218 | self.adsorbate_cross_section_edit.setMaximumWidth(75) 219 | self.adsorbate_molar_volume_label = QLabel('Molar volume (mol/m3):') 220 | self.adsorbate_molar_volume_edit = QLineEdit() 221 | self.adsorbate_molar_volume_edit.setMaximumWidth(75) 222 | self.note_label = QLabel( 223 | """Hints: 224 | - For convenience, it is best to first set your desired criteria before importing the input file. 225 | - Drag and drop the input file into the BETSI window. 226 | - Valid input file formats: *.csv, *.txt, *.aif 227 | - Make sure to read the warnings that may pop up after BET calculation. 228 | - After the first run, by modifiying any of the parameters above, the calculations will rerun automatically. 229 | """) 230 | self.note_label.setStyleSheet("background-color: lightblue") 231 | self.note_label.setMargin(10) 232 | #self.note_label.setIndent(10) 233 | self.note_label.setWordWrap(True) 234 | self.note_label.setAlignment(QtCore.Qt.AlignLeft) 235 | 236 | 237 | self.export_button = QPushButton('Export Results') 238 | self.export_button.pressed.connect(self.export) 239 | 240 | # any change in states updates the display. 241 | self.rouq1_tick.stateChanged.connect(self.maybe_run_calculation) 242 | self.rouq2_tick.stateChanged.connect(self.maybe_run_calculation) 243 | self.rouq3_tick.stateChanged.connect(self.maybe_run_calculation) 244 | self.rouq4_tick.stateChanged.connect(self.maybe_run_calculation) 245 | self.rouq5_tick.stateChanged.connect(self.maybe_run_calculation) 246 | 247 | # define widget parameters 248 | self.rouq4_slider.setRange(5, 75) 249 | self.minr2_slider.setRange(800, 999) 250 | self.min_points_slider.setRange(3, 10) 251 | 252 | # set the defaults 253 | self.set_defaults() 254 | 255 | # connect the actions 256 | self.minr2_edit.editingFinished.connect(self.minr2_edit_changed) 257 | ##self.minr2_edit.returnPressed.connect(self.minr2_edit_changed) 258 | self.rouq4_edit.editingFinished.connect(self.rouq4_edit_changed) 259 | ##self.rouq4_edit.returnPressed.connect(self.rouq4_edit_changed) 260 | self.min_points_edit.editingFinished.connect( 261 | self.min_points_edit_changed) 262 | ##self.min_points_edit.returnPressed.connect( 263 | ## self.min_points_edit_changed) 264 | ##self.rouq4_slider.valueChanged.connect(self.rouq4_slider_changed) 265 | ##self.minr2_slider.valueChanged.connect(self.minr2_slider_changed) 266 | ##self.min_points_slider.valueChanged.connect( 267 | ## self.min_points_slider_changed) 268 | 269 | ## New lines added 270 | self.adsorbate_combo_box.activated.connect(self.adsorbate_combo_box_changed) 271 | ##self.adsorbate_cross_section_edit.returnPressed.connect(self.adsorbate_related_edit_changed) 272 | self.adsorbate_cross_section_edit.editingFinished.connect(self.adsorbate_related_edit_changed) 273 | 274 | self.adsorbate_molar_volume_edit.editingFinished.connect(self.adsorbate_related_edit_changed) 275 | ##self.adsorbate_molar_volume_edit.returnPressed.connect(self.adsorbate_related_edit_changed) 276 | 277 | 278 | ##self.minr2_edit.editingFinished.connect(self.line_edit_changed) 279 | ##self.rouq4_edit.editingFinished.connect(self.line_edit_changed) 280 | ##self.min_points_edit.editingFinished.connect( 281 | ## self.line_edit_changed) 282 | ##self.adsorbate_cross_section_edit.editingFinished.connect(self.line_edit_changed) 283 | ##self.adsorbate_molar_volume_edit.editingFinished.connect(self.line_edit_changed) 284 | 285 | 286 | # add the table for results 287 | self.results_table = QTableWidget(self) 288 | self.results_table.setFixedWidth(520) 289 | self.clean_table() 290 | 291 | # create layout 292 | criteria_layout = QGridLayout() 293 | criteria_layout.addWidget( 294 | self.min_points_label, criteria_layout.rowCount(), 1, 1, 1) 295 | criteria_layout.addWidget( 296 | self.min_points_edit, criteria_layout.rowCount() - 1, 2) 297 | ##criteria_layout.addWidget( 298 | ## self.min_points_slider, criteria_layout.rowCount(), 1, 1, 2) 299 | criteria_layout.addWidget( 300 | self.minr2_label, criteria_layout.rowCount(), 1) 301 | criteria_layout.addWidget( 302 | self.minr2_edit, criteria_layout.rowCount() - 1, 2) 303 | ##criteria_layout.addWidget( 304 | ## self.minr2_slider, criteria_layout.rowCount(), 1, 1, 2) 305 | criteria_layout.addWidget( 306 | self.rouq1_tick, criteria_layout.rowCount(), 1) 307 | criteria_layout.addWidget( 308 | self.rouq2_tick, criteria_layout.rowCount(), 1) 309 | criteria_layout.addWidget( 310 | self.rouq3_tick, criteria_layout.rowCount(), 1) 311 | criteria_layout.addWidget( 312 | self.rouq4_tick, criteria_layout.rowCount(), 1) 313 | criteria_layout.addWidget( 314 | self.rouq4_edit, criteria_layout.rowCount() - 1, 2) 315 | ##criteria_layout.addWidget( 316 | ## self.rouq4_slider, criteria_layout.rowCount(), 1, 1, 2) 317 | criteria_layout.addWidget( 318 | self.rouq5_tick, criteria_layout.rowCount(), 1) 319 | ## New lines added 320 | criteria_layout.addWidget( 321 | self.adsorbate_label, criteria_layout.rowCount(), 1) 322 | criteria_layout.addWidget( 323 | self.adsorbate_combo_box, criteria_layout.rowCount() - 1, 2) 324 | criteria_layout.addWidget( 325 | self.adsorbate_cross_section_label, criteria_layout.rowCount(), 1) 326 | criteria_layout.addWidget( 327 | self.adsorbate_cross_section_edit, criteria_layout.rowCount() - 1, 2) 328 | criteria_layout.addWidget( 329 | self.adsorbate_molar_volume_label, criteria_layout.rowCount(), 1) 330 | criteria_layout.addWidget( 331 | self.adsorbate_molar_volume_edit, criteria_layout.rowCount() - 1, 2) 332 | 333 | criteria_layout.addWidget( 334 | self.note_label, criteria_layout.rowCount(), 1, 1, 2) 335 | 336 | criteria_layout.addWidget( 337 | self.export_button, criteria_layout.rowCount(), 1, 1, 2) 338 | 339 | # criteria_layout.addWidget( 340 | # self.current_output_label, criteria_layout.rowCount(), 1, 1, 1) 341 | # criteria_layout.addWidget( 342 | # self.current_targetpath_label, criteria_layout.rowCount(), 1, 1, 1) 343 | criteria_layout.addWidget( 344 | self.current_output_label, criteria_layout.rowCount(), 1, 1, 2) 345 | criteria_layout.addWidget( 346 | self.current_targetpath_label, criteria_layout.rowCount(), 1, 1, 2) 347 | 348 | 349 | 350 | criteria_layout.addItem(QSpacerItem( 351 | 20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding), criteria_layout.rowCount() + 1, 1) 352 | self.criteria_box.setLayout(criteria_layout) 353 | 354 | main_layout = QGridLayout() 355 | main_layout.addWidget(self.criteria_box, 1, 1) 356 | main_layout.addWidget(self.results_table, 1, 2) 357 | 358 | self.setLayout(main_layout) 359 | 360 | def set_output_dir(self): 361 | """Defines the output directory of the BETSI analysis""" 362 | dir_path = QFileDialog.getExistingDirectory( 363 | self, 'Select Output Directory', os.getcwd()) 364 | print(f"Output directory set to {dir_path}") 365 | self.output_dir = dir_path 366 | self.current_output_label.setText( 367 | f"Output Directory: {self.output_dir}") 368 | 369 | def maybe_run_calculation(self): 370 | self.check_rouq_compatibility() 371 | ## New lines added 372 | self.check_adsorbate_compatibility() 373 | ##if self.target_filepath is not None: 374 | if self.target_filepath is not None: 375 | if self.adsorbate_combo_box.currentText() != "Custom": 376 | self.run_calculation() 377 | elif float(self.adsorbate_cross_section_edit.text()) > 0 and float(self.adsorbate_molar_volume_edit.text()) > 0: 378 | self.run_calculation() 379 | 380 | def plot_bet(self): 381 | # if plt.get_fignums() != [1, 2]: 382 | # plt.close('all') 383 | plt.close('all') 384 | if self.current_fig is not None: 385 | self.current_fig.clear() 386 | self.current_fig_2.clear() 387 | try: 388 | # check if the figure has been closed, if it doesn't reset it to none and replot 389 | if self.current_fig is not None and not plt.fignum_exists(self.current_fig.number): 390 | self.current_fig = None 391 | self.current_fig_2 = None 392 | fig = create_matrix_plot(self.bet_filter_result, self.rouq3_tick.isChecked(), self.rouq4_tick.isChecked(), name=Path( 393 | self.target_filepath).stem, fig=self.current_fig) 394 | fig_2 = regression_diagnostics_plots(self.bet_filter_result, name=Path( 395 | self.target_filepath).stem, fig_2=self.current_fig_2) 396 | # connect the picker event to the figure 397 | if self.current_fig is None: 398 | fig.canvas.mpl_connect('pick_event', self.point_picker) 399 | self.current_fig = fig 400 | self.current_fig.tight_layout(pad=0.3, rect=[0, 0, 1, 0.95]) 401 | self.current_fig_2 = fig_2 402 | plt.figure(num=1) 403 | plt.draw() 404 | plt.figure(num=2).canvas.manager.window.move(500,0) 405 | plt.draw() 406 | except TypeError: 407 | pass 408 | 409 | def run_calculation(self): 410 | """ Applies the currently specified filters to the currently specified target csv file. """ 411 | 412 | ## assert self.target_filepath, "You must provide a csv file before calling run." 413 | if not self.target_filepath: 414 | warnings = "You must provide an input file (e.g. *.csv, *.txt, *.aif) before calling run. Press \"Clear\" and try again. Please refer to the \"Hints\" box for a quick guide." 415 | information = "" 416 | self.show_dialog(warnings, information) 417 | return 418 | 419 | use_rouq1 = self.rouq1_tick.isChecked() 420 | use_rouq2 = self.rouq2_tick.isChecked() 421 | use_rouq3 = self.rouq3_tick.isChecked() 422 | use_rouq4 = self.rouq4_tick.isChecked() 423 | use_rouq5 = self.rouq5_tick.isChecked() 424 | min_num_pts = int(self.min_points_edit.text()) 425 | min_r2 = float(self.minr2_edit.text()) 426 | max_perc_error = float(self.rouq4_edit.text()) 427 | adsorbate = self.adsorbate_combo_box.currentText() 428 | cross_sectional_area = float(self.adsorbate_cross_section_edit.text()) 429 | molar_volume = float(self.adsorbate_molar_volume_edit.text()) 430 | 431 | ## New lines or modifications made 432 | # Retrieve the BETSI results object if non-existent 433 | if self.bet_object is None: 434 | pressure, q_adsorbed, comments_to_data = get_data(input_file=self.target_filepath) 435 | if len(pressure) == 0 or len(q_adsorbed) == 0: 436 | warnings = "You must provide a valid input file. BETSI cannot read it. Press \"Clear\" and try again with a valid one." 437 | information = "" 438 | self.show_dialog(warnings, information) 439 | return 440 | self.bet_object = BETResult(pressure, q_adsorbed) 441 | self.bet_object.comments_to_data = comments_to_data 442 | self.bet_object.original_pressure_data = pressure 443 | self.bet_object.original_q_adsorbed_data = q_adsorbed 444 | 445 | 446 | # Apply the currently selected filters. 447 | self.bet_filter_result = BETFilterAppliedResults(self.bet_object, 448 | min_num_pts=min_num_pts, 449 | min_r2=min_r2, 450 | use_rouq1=use_rouq1, 451 | use_rouq2=use_rouq2, 452 | use_rouq3=use_rouq3, 453 | use_rouq4=use_rouq4, 454 | use_rouq5=use_rouq5, 455 | max_perc_error=max_perc_error, 456 | adsorbate=adsorbate, 457 | cross_sectional_area=cross_sectional_area, 458 | molar_volume=molar_volume) 459 | 460 | ## Adds interpolated points to adsorption data if no valid area was found by the original data 461 | if not self.bet_filter_result.has_valid_areas: 462 | iter_num = 0 463 | self.bet_object.comments_to_data['interpolated_points_added'] = True 464 | while (not self.bet_filter_result.has_valid_areas) and (iter_num < 3): 465 | print('Adding extra interpolated points to the data') 466 | 467 | pressure = self.bet_object.pressure 468 | q_adsorbed = self.bet_object.q_adsorbed 469 | comments_to_data = self.bet_object.comments_to_data 470 | interpolated_points_added = self.bet_object.comments_to_data['interpolated_points_added'] 471 | original_pressure_data = self.bet_object.original_pressure_data 472 | original_q_adsorbed_data = self.bet_object.original_q_adsorbed_data 473 | 474 | self.bet_object = None 475 | self.bet_filter_result = None 476 | pressure_added_points, q_adsorbed_added_points = isotherm_pchip_reconstruction(pressure, q_adsorbed) 477 | self.bet_object = BETResult(pressure_added_points, q_adsorbed_added_points) 478 | self.bet_object.comments_to_data = comments_to_data 479 | self.bet_object.comments_to_data['interpolated_points_added'] = interpolated_points_added 480 | self.bet_object.original_pressure_data = original_pressure_data 481 | self.bet_object.original_q_adsorbed_data = original_q_adsorbed_data 482 | 483 | # Apply the currently selected filters. 484 | self.bet_filter_result = BETFilterAppliedResults(self.bet_object, 485 | min_num_pts=min_num_pts, 486 | min_r2=min_r2, 487 | use_rouq1=use_rouq1, 488 | use_rouq2=use_rouq2, 489 | use_rouq3=use_rouq3, 490 | use_rouq4=use_rouq4, 491 | use_rouq5=use_rouq5, 492 | max_perc_error=max_perc_error, 493 | adsorbate=adsorbate, 494 | cross_sectional_area=cross_sectional_area, 495 | molar_volume=molar_volume) 496 | iter_num += 1 497 | 498 | ## Warnings for pop-up message box 499 | warnings = "" 500 | information = "" 501 | if self.bet_object.comments_to_data['has_negative_pressure_points']: 502 | warnings += "- Imported adsorption data has negative pressure point(s)!\n" 503 | information += "- Negative pressure point(s) have been removed from the data.\n" 504 | if not self.bet_object.comments_to_data['monotonically_increasing_pressure']: 505 | warnings += "- The pressure points are not monotonically increasing!\n" 506 | information += "- Non-monotonically increasing pressure point(s) have been removed from the data.\n" 507 | if not self.bet_object.comments_to_data['rel_pressure_between_0_and_1']: 508 | warnings += "- The relative pressure values must lie between 0 and 1!\n" 509 | information += "- Relative pressure point(s) above 1.0 have been removed from the data.\n" 510 | if self.bet_object.comments_to_data['interpolated_points_added']: 511 | warnings += "- No valid areas found with the chosen minimum number of points! So, interpolated points are added to the data!\n" 512 | 513 | if self.bet_filter_result.has_valid_areas: 514 | self.plot_bet() 515 | if warnings != "": 516 | warnings = "Consider the following warning(s):\n" + warnings 517 | if information != "": 518 | information = "Note(s):\n" + information 519 | self.show_dialog(warnings, information) 520 | else: 521 | if warnings == "": 522 | warnings = "No valid areas found! Try again with a new set of data and/or change your criteria" 523 | self.show_dialog(warnings, information) 524 | else: 525 | warnings_1 = "No valid areas found! Try again with a new set of data and/or change your criteria.\n" 526 | warnings_2 = "Consider the following warning(s):\n" 527 | warnings = warnings_1 + warnings_2 + warnings 528 | information = "Note(s):\n" + information 529 | self.show_dialog(warnings, information) 530 | 531 | ## New lines addedd 532 | def show_dialog(self, warnings, information): 533 | dialog = QMessageBox() 534 | dialog.setText(warnings) 535 | dialog.setWindowTitle('Warnings') 536 | if warnings.find("No valid areas found!") != -1: 537 | dialog.setIcon(QMessageBox().Critical) 538 | dialog.setStandardButtons(QMessageBox.Ok) 539 | dialog.addButton("Clear", QMessageBox.AcceptRole) 540 | if information != "": 541 | information += "\n\nPress \"Clear\" if you want to clear input data, reset to default values and start over with a new set of data.\n" 542 | else: 543 | information = "Press \"Clear\" if you want to clear input data, reset to default values and start over with a new set of data.\n" 544 | elif warnings.find("You must provide a") != -1: 545 | dialog.setIcon(QMessageBox().Critical) 546 | dialog.addButton("Clear", QMessageBox.AcceptRole) 547 | else: 548 | dialog.setIcon(QMessageBox().Warning) 549 | dialog.setInformativeText(information) 550 | 551 | dialog.buttonClicked.connect(self.dialog_clicked) 552 | dialog.exec_() 553 | 554 | def dialog_clicked(self, dialog_button): 555 | if dialog_button.text() == "Clear": 556 | self.clear() 557 | 558 | def point_picker(self, event): 559 | line = event.artist 560 | picked_coords = line.get_offsets()[event.ind][0] 561 | # redefine min_i and min_j 562 | self.bet_filter_result.find_nearest_idx(picked_coords) 563 | # replot based on the new min_i and min_j 564 | self.plot_bet() 565 | 566 | def export(self): 567 | """ Print out the plot, filter config and results to the output directory. """ 568 | if self.bet_filter_result is not None: 569 | 570 | # Create a local sub-directory for export. 571 | target_path = Path(self.target_filepath) 572 | output_subdir = Path(self.output_dir) / target_path.name 573 | output_subdir.mkdir(exist_ok=True) 574 | 575 | self.bet_filter_result.export(output_subdir) 576 | 577 | self.current_fig.savefig(str(output_subdir / f'{target_path.stem}_plot.pdf'), bbox_inches='tight') 578 | plt.show() 579 | self.current_fig_2.savefig(str(output_subdir / f'{target_path.stem}_RD_plots.pdf')) 580 | plt.show() 581 | 582 | def analyse_directory(self, dir_path): 583 | """ Run BETSI on all csv files within dir_path. Use current filter config.""" 584 | use_rouq1 = self.rouq1_tick.isChecked() 585 | use_rouq2 = self.rouq2_tick.isChecked() 586 | use_rouq3 = self.rouq3_tick.isChecked() 587 | use_rouq4 = self.rouq4_tick.isChecked() 588 | use_rouq5 = self.rouq5_tick.isChecked() 589 | min_num_points = int(self.min_points_edit.text()) 590 | min_r2 = float(self.minr2_edit.text()) 591 | max_perc_error = float(self.rouq4_edit.text()) 592 | 593 | adsorbate = self.adsorbate_combo_box.currentText() 594 | cross_sectional_area = float(self.adsorbate_cross_section_edit.text()) 595 | molar_volume = float(self.adsorbate_molar_volume_edit.text()) 596 | 597 | ## New lines added 598 | ##csv_paths = Path(dir_path).glob('*.csv') 599 | csv_paths = Path(dir_path).glob('*.csv') 600 | aif_paths = Path(dir_path).glob('*.aif') 601 | txt_paths = Path(dir_path).glob('*.txt') 602 | input_file_paths = (*csv_paths, *aif_paths, txt_paths) 603 | 604 | ##for file_path in csv_paths: 605 | for file_path in input_file_paths: 606 | # Update the table with current file 607 | self.populate_table(csv_path=str(file_path)) 608 | 609 | # Run the analysis 610 | analyse_file(input_file=str(file_path), 611 | output_dir=self.output_dir, 612 | min_num_pts=min_num_points, 613 | min_r2=min_r2, 614 | use_rouq1=use_rouq1, 615 | use_rouq2=use_rouq2, 616 | use_rouq3=use_rouq3, 617 | use_rouq4=use_rouq4, 618 | use_rouq5=use_rouq5, 619 | max_perc_error=max_perc_error, 620 | adsorbate=adsorbate, 621 | cross_sectional_area=cross_sectional_area, 622 | molar_volume=molar_volume) 623 | 624 | def set_defaults(self): 625 | """Sets the widget to default state 626 | """ 627 | # set default values for the tick marks 628 | self.rouq1_tick.setCheckState(True) 629 | self.rouq2_tick.setCheckState(True) 630 | self.rouq3_tick.setCheckState(True) 631 | self.rouq4_tick.setCheckState(True) 632 | self.rouq5_tick.setCheckState(False) 633 | 634 | # the ticks can only be on or off - Not sure why I need to do this every time, but doesn't matter 635 | self.rouq1_tick.setTristate(False) 636 | self.rouq2_tick.setTristate(False) 637 | self.rouq3_tick.setTristate(False) 638 | self.rouq4_tick.setTristate(False) 639 | self.rouq5_tick.setTristate(False) 640 | # set defaults for text fields 641 | self.minr2_edit.setText('0.995') 642 | self.rouq4_edit.setText('20') 643 | self.min_points_edit.setText('10') 644 | 645 | ## New lines added 646 | ## set defaults for adsorbate related parameters 647 | self.adsorbate_combo_box.setCurrentIndex(0) 648 | #self.adsorbate_cross_section_edit.setText('0.0') 649 | #self.adsorbate_molar_volume_edit.setText('0.0') 650 | self.adsorbate_cross_section_edit.setText(str(cross_sectional_area[self.adsorbate_combo_box.currentText()] * 1.0E18)) 651 | self.adsorbate_molar_volume_edit.setText(str(mol_vol[self.adsorbate_combo_box.currentText()])) 652 | 653 | 654 | self.line_edit_values_before = {"minr2_edit": self.minr2_edit.text(), \ 655 | "rouq4_edit": self.rouq4_edit.text(), \ 656 | "min_points_edit": self.min_points_edit.text(), \ 657 | "adsorbate_cross_section_edit": self.adsorbate_cross_section_edit.text(), \ 658 | "adsorbate_molar_volume_edit": self.adsorbate_molar_volume_edit.text()} 659 | 660 | 661 | # trigger the corresponding sliders 662 | self.rouq4_edit_changed() 663 | self.minr2_edit_changed() 664 | self.min_points_edit_changed() 665 | # check the compatibility 666 | self.check_rouq_compatibility() 667 | 668 | ## check the compatibility for adsorbate 669 | self.check_adsorbate_compatibility() 670 | 671 | # if there is a figure, replot 672 | self.plot_bet() 673 | 674 | def clear(self): 675 | """Closes all plots and removes all data from memory""" 676 | # remove the bet filter result from memory 677 | self.bet_filter_result = None 678 | self.bet_object = None 679 | # clear the table 680 | self.clean_table() 681 | # close the plot 682 | if self.current_fig is not None: 683 | plt.close(fig=self.current_fig) 684 | plt.close(fig=self.current_fig_2) 685 | 686 | ## New lines added here 687 | ##reset the parameters to defaults 688 | self.target_filepath = None 689 | self.current_targetpath_label.setText( 690 | f"Loaded File: {self.target_filepath}") 691 | self.set_defaults() 692 | 693 | def set_editable(self, state): 694 | if state: 695 | self.rouq1_tick.setEnabled(True) 696 | self.rouq2_tick.setEnabled(True) 697 | self.rouq3_tick.setEnabled(True) 698 | self.rouq4_tick.setEnabled(True) 699 | self.rouq5_tick.setEnabled(True) 700 | self.minr2_edit.setEnabled(True) 701 | self.rouq4_edit.setEnabled(True) 702 | self.min_points_edit.setEnabled(True) 703 | self.rouq4_slider.setEnabled(True) 704 | self.minr2_slider.setEnabled(True) 705 | self.min_points_slider.setEnabled(True) 706 | self.adsorbate_combo_box.setEnabled(True) 707 | self.adsorbate_cross_section_edit.setEnabled(True) 708 | self.adsorbate_molar_volume_edit.setEnabled(True) 709 | else: 710 | self.rouq1_tick.setEnabled(False) 711 | self.rouq2_tick.setEnabled(False) 712 | self.rouq3_tick.setEnabled(False) 713 | self.rouq4_tick.setEnabled(False) 714 | self.rouq5_tick.setEnabled(False) 715 | self.minr2_edit.setEnabled(False) 716 | self.rouq4_edit.setEnabled(False) 717 | self.min_points_edit.setEnabled(False) 718 | self.rouq4_slider.setEnabled(False) 719 | self.minr2_slider.setEnabled(False) 720 | self.min_points_slider.setEnabled(False) 721 | self.adsorbate_combo_box.setEnabled(False) 722 | self.adsorbate_cross_section_edit.setEnabled(False) 723 | self.adsorbate_molar_volume_edit.setEnabled(False) 724 | 725 | def check_rouq_compatibility(self): 726 | use_rouq1 = self.rouq1_tick.isChecked() 727 | use_rouq2 = self.rouq2_tick.isChecked() 728 | use_rouq3 = self.rouq3_tick.isChecked() 729 | use_rouq4 = self.rouq4_tick.isChecked() 730 | use_rouq5 = self.rouq5_tick.isChecked() 731 | if not (use_rouq3 or use_rouq4): 732 | self.rouq2_tick.setEnabled(True) 733 | else: 734 | self.rouq2_tick.setChecked(True) 735 | self.rouq2_tick.setEnabled(False) 736 | 737 | def check_adsorbate_compatibility(self): 738 | if self.adsorbate_combo_box.currentText() != "Custom": 739 | self.adsorbate_cross_section_edit.setEnabled(False) 740 | self.adsorbate_molar_volume_edit.setEnabled(False) 741 | self.adsorbate_cross_section_edit.setText("{0:0.3f}".format(cross_sectional_area[self.adsorbate_combo_box.currentText()] * 1.0E18)) 742 | self.adsorbate_molar_volume_edit.setText("{0:0.3f}".format(mol_vol[self.adsorbate_combo_box.currentText()])) 743 | #self.adsorbate_cross_section_edit.setText("0.0") 744 | #self.adsorbate_molar_volume_edit.setText("0.0") 745 | else: 746 | self.adsorbate_cross_section_edit.setEnabled(True) 747 | self.adsorbate_molar_volume_edit.setEnabled(True) 748 | 749 | def populate_table(self, csv_path): 750 | self.results_table.setColumnCount(2) 751 | self.results_table.setRowCount(1) 752 | self.results_table.setHorizontalHeaderLabels( 753 | ['Relative pressure (p/p\u2080)', 'Quantity adsorbed (cm\u00B3/g)']) 754 | self.results_table.setColumnWidth(0, 250) 755 | self.results_table.setColumnWidth(1, 250) 756 | 757 | # Change the box title 758 | self.current_targetpath_label.setText( 759 | f"Loaded File: {self.target_filepath}") 760 | 761 | if csv_path is not None and Path(csv_path).exists(): 762 | pressure, q_adsorbed, comments_to_data = get_data(input_file=csv_path) 763 | self.results_table.setRowCount(len(pressure)) 764 | for i in range(len(pressure)): 765 | self.results_table.setItem( 766 | i, 0, QTableWidgetItem(str(pressure[i]))) 767 | self.results_table.setItem( 768 | i, 1, QTableWidgetItem(str(q_adsorbed[i]))) 769 | 770 | def clean_table(self): 771 | """Cleans the table""" 772 | self.results_table.setColumnCount(2) 773 | self.results_table.setRowCount(0) 774 | self.results_table.setHorizontalHeaderLabels( 775 | ['Relative pressure (p/p\u2080)', 'Quantity adsorbed (cm\u00B3/g)']) 776 | self.results_table.setColumnWidth(0, 250) 777 | self.results_table.setColumnWidth(1, 250) 778 | 779 | 780 | def minr2_edit_changed(self): 781 | value = self.minr2_edit.text() 782 | mn = self.minr2_slider.minimum() 783 | mx = self.minr2_slider.maximum() 784 | try: 785 | value = int(round(float(value) * 1000)) 786 | if value < mn: 787 | value = mn 788 | self.minr2_edit.setText(str(value / 1000)) 789 | elif value > mx: 790 | value = mx 791 | self.minr2_edit.setText(str(value / 1000)) 792 | self.minr2_slider.setValue(value) 793 | except (ValueError, TypeError): 794 | self.minr2_edit.setText('0.995') 795 | if self.line_edit_values_before["minr2_edit"] != self.minr2_edit.text(): 796 | self.line_edit_values_before["minr2_edit"] = self.minr2_edit.text() 797 | self.maybe_run_calculation() 798 | 799 | def rouq4_edit_changed(self): 800 | value = self.rouq4_edit.text() 801 | mn = self.rouq4_slider.minimum() 802 | mx = self.rouq4_slider.maximum() 803 | try: 804 | value = int(float(value)) 805 | if value < mn: 806 | value = mn 807 | self.rouq4_edit.setText(str(value)) 808 | if value > mx: 809 | value = mx 810 | self.rouq4_edit.setText(str(value)) 811 | self.rouq4_slider.setValue(value) 812 | except (ValueError, TypeError): 813 | self.rouq4_edit.setText('20') 814 | if self.line_edit_values_before["rouq4_edit"] != self.rouq4_edit.text(): 815 | self.line_edit_values_before["rouq4_edit"] = self.rouq4_edit.text() 816 | self.maybe_run_calculation() 817 | 818 | def min_points_edit_changed(self): 819 | value = self.min_points_edit.text() 820 | mn = self.min_points_slider.minimum() 821 | mx = self.min_points_slider.maximum() 822 | try: 823 | value = int(round(float(value))) 824 | if value < mn: 825 | value = mn 826 | self.min_points_edit.setText(str(value)) 827 | if value > mx: 828 | value = mx 829 | self.min_points_edit.setText(str(value)) 830 | self.min_points_slider.setValue(value) 831 | except (ValueError, TypeError): 832 | self.min_points_edit.setText('10') 833 | if self.line_edit_values_before["min_points_edit"] != self.min_points_edit.text(): 834 | self.line_edit_values_before["min_points_edit"] = self.min_points_edit.text() 835 | self.maybe_run_calculation() 836 | 837 | def rouq4_slider_changed(self): 838 | value = self.rouq4_slider.value() 839 | self.rouq4_edit.setText(str(value)) 840 | self.maybe_run_calculation() 841 | 842 | def minr2_slider_changed(self): 843 | value = self.minr2_slider.value() 844 | self.minr2_edit.setText(str(value / 1000)) 845 | self.maybe_run_calculation() 846 | 847 | def min_points_slider_changed(self): 848 | value = self.min_points_slider.value() 849 | self.min_points_edit.setText(str(value)) 850 | self.maybe_run_calculation() 851 | 852 | def adsorbate_related_edit_changed(self): 853 | if not self.is_float(self.adsorbate_cross_section_edit.text()): 854 | self.adsorbate_cross_section_edit.setText("0.0") 855 | if not self.is_float(self.adsorbate_molar_volume_edit.text()): 856 | self.adsorbate_molar_volume_edit.setText("0.0") 857 | if self.line_edit_values_before["adsorbate_cross_section_edit"] != self.adsorbate_cross_section_edit.text() or \ 858 | self.line_edit_values_before["adsorbate_molar_volume_edit"] != self.adsorbate_molar_volume_edit.text(): 859 | self.line_edit_values_before["adsorbate_cross_section_edit"] = self.adsorbate_cross_section_edit.text() 860 | self.line_edit_values_before["adsorbate_molar_volume_edit"] = self.adsorbate_molar_volume_edit.text() 861 | self.maybe_run_calculation() 862 | 863 | def adsorbate_combo_box_changed(self): 864 | if self.adsorbate_combo_box.currentText() == "Custom": 865 | self.adsorbate_cross_section_edit.setText("0.0") 866 | self.adsorbate_molar_volume_edit.setText("0.0") 867 | self.maybe_run_calculation() 868 | 869 | def line_edit_changed(self): 870 | # modify_check = [] 871 | # modify_check.append(self.minr2_edit.isModified()) 872 | # modify_check.append(self.rouq4_edit.isModified()) 873 | # modify_check.append(self.min_points_edit.isModified()) 874 | # modify_check.append(self.adsorbate_cross_section_edit.isModified()) 875 | # modify_check.append(self.adsorbate_molar_volume_edit.isModified()) 876 | current_line_edit_values = self.get_line_edit_current_values() 877 | if self.line_edit_values_before != current_line_edit_values: 878 | self.maybe_run_calculation() 879 | self.line_edit_values_before = current_line_edit_values 880 | 881 | def get_line_edit_current_values(self): 882 | current_line_edit_values = [] 883 | current_line_edit_values.append(self.minr2_edit.text()) 884 | current_line_edit_values.append(self.rouq4_edit.text()) 885 | current_line_edit_values.append(self.min_points_edit.text()) 886 | current_line_edit_values.append(self.adsorbate_cross_section_edit.text()) 887 | current_line_edit_values.append(self.adsorbate_molar_volume_edit.text()) 888 | return current_line_edit_values 889 | 890 | def is_float(self, string): 891 | try: 892 | float(string) 893 | return True 894 | except ValueError: 895 | return False 896 | 897 | def __del__(self): 898 | try: 899 | plt.close(self.current_fig_2) 900 | plt.close(self.current_fig) 901 | except: 902 | pass 903 | 904 | class OutputCanvas(FigureCanvas): 905 | def __init__(self, parent, dpi=100): 906 | self.fig = Figure(dpi=dpi) 907 | self.fig.subplots_adjust(left=0.05, right=0.95) 908 | FigureCanvas.__init__(self, self.fig) 909 | self.setParent(parent) 910 | FigureCanvas.setSizePolicy(self, 911 | QSizePolicy.Expanding, 912 | QSizePolicy.Expanding) 913 | FigureCanvas.updateGeometry(self) 914 | 915 | class OutputCanvas_2(FigureCanvas): 916 | def __init__(self,parent,dpi=100): 917 | self.fig_2 = Figure(dpi=dpi) 918 | self.fig_2.subplots_adjust(left=.05, right=.95) 919 | FigureCanvas.__init__(self,self.fig_2) 920 | self.setParent(parent) 921 | FigureCanvas.setSizePolicy(self, 922 | QSizePolicy.Expanding, 923 | QSizePolicy.Expanding) 924 | FigureCanvas.updateGeometry(self) 925 | 926 | class OutLog(logging.Handler): 927 | def __init__(self, out_widget): 928 | """(edit, out=None, color=None) -> can write stdout, stderr to a 929 | QTextEdit. 930 | """ 931 | logging.Handler.__init__(self) 932 | self.out_widget = out_widget 933 | 934 | def emit(self, message): 935 | self.write(message.getMessage() + '\n') 936 | 937 | def write(self, m): 938 | self.out_widget.moveCursor(QtGui.QTextCursor.End) 939 | self.out_widget.insertPlainText(m) 940 | 941 | 942 | def runbetsi(): 943 | QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) 944 | app = QApplication(sys.argv) 945 | ex = BETSI_gui() 946 | ex.show() 947 | sys.exit(app.exec_()) 948 | 949 | runbetsi() -------------------------------------------------------------------------------- /betsi/lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions for computing the BET areas. 4 | Created on Thu Mar 21 16:24:04 2019 5 | 6 | @author: jwmo2, ls604, jrampersad 7 | """ 8 | import os 9 | from pathlib import Path 10 | 11 | import matplotlib 12 | import numpy as np 13 | from matplotlib import pyplot as plt 14 | from scipy.interpolate import splev 15 | from scipy.optimize import minimize 16 | 17 | from betsi.plotting import create_matrix_plot,regression_diagnostics_plots 18 | from betsi.utils import * 19 | from pprint import pprint 20 | from scipy.interpolate import splrep, pchip_interpolate 21 | matplotlib.use('Qt5Agg') 22 | 23 | ##NITROGEN_RADIUS = 1.62E-19 24 | ##NITROGEN_MOL_VOL = 44.64117195 25 | AVOGADRO_N = 6.02E+23 26 | ## in units of m2 27 | cross_sectional_area = {"N2": 1.62E-19, "Ar": 1.66E-19, "Kr": 2.02E-19, "Xe": 2.32E-19, "CO2": 1.95E-19, "Custom": None} 28 | ## in units of mmol/litre or mol/m3 29 | mol_vol = {"N2": 44.64117195, "Ar": 44.642857, "Kr": 44.642857, "Xe": 44.642857, "CO2": 44.642857, "Custom": None} 30 | 31 | class BETResult: 32 | """ 33 | Structure to hold the unfiltered results obtained from running initial BET analysis on 34 | an isotherm. 35 | """ 36 | 37 | def __init__(self, pressure, q_adsorbed): 38 | 39 | def zero_matrix(dim): 40 | # Helper function to create a square matrix of dimension dim. 41 | return np.zeros([dim, dim]) 42 | 43 | # Pressure & Q_Adsorbed 44 | self.pressure = pressure 45 | self.q_adsorbed = q_adsorbed 46 | 47 | # Apply linearised BET equation 48 | self.linear_y = (pressure / (q_adsorbed * (1. - pressure))) 49 | self.rouq_y = q_adsorbed * (1. - pressure) 50 | 51 | # Fit a line to isotherm. 52 | self.x_range = np.linspace(pressure[0], pressure[len(pressure) -1 ], 10000) 53 | self.fitted_spline = get_fitted_spline(pressure, q_adsorbed) 54 | 55 | # Number of points in each segment: 56 | num_points = len(pressure) 57 | self.point_count = zero_matrix(num_points) 58 | 59 | # Gradients, Intercept and Residual of Linearised BET: 60 | self.fit_grad = zero_matrix(num_points) 61 | self.fit_intercept = zero_matrix(num_points) 62 | self.fit_rsquared = zero_matrix(num_points) 63 | 64 | # 'C' constants and Monolayer loadings: 65 | self.c = zero_matrix(num_points) 66 | self.nm = zero_matrix(num_points) 67 | 68 | # Pressure at the Monolayer loading, error and percentage error: 69 | self.calc_pressure = zero_matrix(num_points) 70 | self.error = zero_matrix(num_points) + 300 71 | self.pc_error = zero_matrix(num_points) + 300 72 | 73 | # Identify the Isotherm Knee 74 | self.knee_index = np.argmax(np.diff(self.rouq_y) < 0) 75 | 76 | # Binary rouquerol `pass` matrices: 77 | self.rouq1 = zero_matrix(num_points) 78 | self.rouq2 = zero_matrix(num_points) 79 | self.rouq3 = zero_matrix(num_points) 80 | self.rouq4 = zero_matrix(num_points) 81 | self.rouq5 = zero_matrix(num_points) 82 | 83 | #Corresponding pressure matrix 84 | self.corresponding_pressure = zero_matrix(num_points) 85 | self.corresponding_pressure_pchip = zero_matrix(num_points) 86 | 87 | # Fill all the matrices 88 | self._compute_betsi_data(pressure, q_adsorbed) 89 | 90 | def _compute_betsi_data(self, pressure, q_adsorbed): 91 | """ Computes Monolayer loadings and applies the rouquerel criteria individually to produce a 92 | dictionary of matrices that can be used in downstream analysis and plotting. 93 | 94 | Args: 95 | pressure: Array of relative pressure values. 96 | q_adsorbed: Array of Nitrogen uptake values. 97 | 98 | Returns: 99 | A Betsi Result Object 100 | """ 101 | 102 | # Fit a line to isotherm. 103 | fitted_spline = get_fitted_spline(pressure, q_adsorbed) 104 | fitted_pchip = get_pchip_interpolation(pressure,q_adsorbed) 105 | 106 | def distance_to_interpolation(_s, _mono): 107 | # Calculate distance of monolayer loading to fitted spline. 108 | return (splev(_s, fitted_spline, der=0, ext=0) - _mono) ** 2 109 | 110 | def distance_to_pchip(_s,_mono): 111 | # Create a BET results object: 112 | 113 | return (fitted_pchip.__call__(_s,nu=0,extrapolate=None) - _mono) ** 2 114 | 115 | 116 | num_points = len(pressure) 117 | 118 | for i in range(num_points): 119 | for j in range(i + 1, num_points + 1): 120 | 121 | # Set the number of points 122 | self.point_count[i, j - 1] = j - i 123 | 124 | # Fit a straight line to points i:j of the linearised equation. Compute C and Nm. 125 | x = np.concatenate( 126 | [np.ones([num_points, 1]), pressure[:, None]], axis=1) 127 | params, residuals, _, _ = np.linalg.lstsq( 128 | x[i:j, :], self.linear_y[i:j], rcond=None) 129 | if residuals: 130 | r2 = 1. - residuals / \ 131 | (self.linear_y[i:j].size * self.linear_y[i:j].var()) 132 | self.fit_rsquared[i, j - 1] = r2 133 | 134 | # Set the linearised BET parameters from the fit. 135 | self.fit_intercept[i, j - 1] = params[0] 136 | self.fit_grad[i, j - 1] = params[1] 137 | self.c[i, j - 1] = self.fit_grad[i, j - 1] / \ 138 | self.fit_intercept[i, j - 1] + 1. 139 | self.nm[i, j - 1] = 1. / \ 140 | (self.fit_grad[i, j - 1] + self.fit_intercept[i, j - 1]) 141 | 142 | for i in range(num_points): 143 | for j in range(i + 1, num_points): 144 | 145 | # ROUQUEROL CRITERIA 1. vol_adsorbed * (1. - pressure) increases monotonically. 146 | deltas = np.diff(self.rouq_y[i:(j + 1)]) 147 | if not (deltas < 0).any(): 148 | self.rouq1[i, j] = 1 149 | deltas_2 = np.diff(self.linear_y[i:(j+1)]) 150 | if (deltas_2 < 0).any(): 151 | self.rouq1[i,j]=0 152 | 153 | # ROUQUEROL CRITERIA 2. Resulting C value must be positive. 154 | if self.c[i, j] <= 0: 155 | continue 156 | self.rouq2[i, j] = 1 157 | 158 | # ROUQUEROL CRITERIA 3. Pressure corresponding to Nm should lie in linear range 159 | self.calc_pressure[i, j] = 1. / (np.sqrt(self.c[i, j]) + 1) 160 | opt_res = minimize(fun=distance_to_interpolation, 161 | x0=self.calc_pressure[i, j], 162 | args=(self.nm[i, j])) 163 | self.corresponding_pressure[i,j] = opt_res.x 164 | opt_res_pchip = minimize(fun=distance_to_pchip,x0=self.calc_pressure[i, j], args=(self.nm[i, j])) 165 | self.corresponding_pressure_pchip[i,j] = opt_res_pchip.x 166 | 167 | 168 | if not pressure[i] < self.corresponding_pressure_pchip[i,j] < pressure[j]: 169 | continue 170 | self.rouq3[i, j] = 1 171 | 172 | # ROUQUEROL CRITERIA 4. Relative Pressure should be *close* to P from BET Theory. 173 | self.error[i, j] = abs( 174 | self.corresponding_pressure_pchip[i,j] - self.calc_pressure[i, j]) 175 | self.pc_error[i, j] = ( 176 | self.error[i, j] / self.corresponding_pressure_pchip[i,j]) * 100. 177 | 178 | # ROUQUEROL CRITERIA 5. Linear region must end at the knee 179 | if j == self.knee_index: 180 | self.rouq5[i, j] = 1 181 | 182 | class BETFilterAppliedResults: 183 | """ 184 | Structure obtained from applying a set of custom filters to a BETResult. 185 | 186 | After initialisation with an initialised BETResult object and set of desired features, the 187 | BETFilterAppliedResults object contains all data required to produce any plot. 188 | """ 189 | 190 | def __init__(self, bet_result, **kwargs): 191 | 192 | # Transfer all the properties from the original BET calculation 193 | self.__dict__.update(bet_result.__dict__) 194 | self.filter_params = kwargs 195 | 196 | # Apply the selected filters in turn 197 | filter_mask = np.ones_like(bet_result.c) 198 | 199 | if kwargs.get('use_rouq1', True): 200 | filter_mask = filter_mask * bet_result.rouq1 201 | 202 | if kwargs.get('use_rouq2', True): 203 | filter_mask = filter_mask * bet_result.rouq2 204 | 205 | if kwargs.get('use_rouq3', True): 206 | filter_mask = filter_mask * bet_result.rouq3 207 | 208 | if kwargs.get('use_rouq4', True): 209 | max_perc_error = kwargs.get('max_perc_error', 20) 210 | filter_mask = filter_mask * (bet_result.pc_error < max_perc_error) 211 | 212 | if kwargs.get('use_rouq5', False): 213 | filter_mask = filter_mask * bet_result.rouq5 214 | 215 | adsorbate = kwargs.get('adsorbate', "N2") 216 | 217 | if adsorbate == "Custom": 218 | cross_sectional_area[adsorbate] = 1.0E-18 * kwargs.get('cross_sectional_area') 219 | mol_vol[adsorbate] = kwargs.get('molar_volume') 220 | 221 | # Filter results that have less than the minimum points 222 | min_points = kwargs.get('min_num_pts', 10) 223 | filter_mask = filter_mask * (bet_result.point_count > min_points) 224 | 225 | # Block out results that have less than the minimum R2 226 | min_r2 = kwargs.get('min_r2', 0.9) 227 | filter_mask = filter_mask * (bet_result.fit_rsquared > min_r2) 228 | 229 | ## assert np.sum(filter_mask) != 0, "NO valid areas found" 230 | self.has_valid_areas = False 231 | self.original_pressure_data = bet_result.original_pressure_data 232 | self.original_q_adsorbed_data = bet_result.original_q_adsorbed_data 233 | self.comments_to_data = bet_result.comments_to_data 234 | self.adsorbate = adsorbate 235 | 236 | if np.sum(filter_mask) != 0: 237 | self.has_valid_areas = True 238 | 239 | # Compute valid BET areas 240 | ##self.bet_areas = NITROGEN_RADIUS * AVOGADRO_N * \ 241 | ## NITROGEN_MOL_VOL * bet_result.nm * 0.000001 242 | self.bet_areas = cross_sectional_area[adsorbate] * AVOGADRO_N * \ 243 | mol_vol[adsorbate] * bet_result.nm * 0.000001 244 | self.bet_areas_filtered = self.bet_areas * filter_mask 245 | self.valid_indices = np.where(self.bet_areas_filtered > 0) 246 | 247 | # Define isotherm knee as ending on highest P 248 | self.list = np.where(self.valid_indices[1] == np.amax(self.valid_indices[1])) 249 | self.lower = (self.valid_indices[0])[self.list] 250 | self.upper = (self.valid_indices[1])[self.list] 251 | self.valid_knee_indices = (self.lower, self.upper) 252 | self.knee_only_bet_areas_filtered = self.bet_areas * bet_result.rouq5 253 | 254 | 255 | # Define the valid cases 256 | self.num_valid = len(self.valid_indices[0]) 257 | self.valid_bet_areas = self.bet_areas[self.valid_indices] 258 | self.valid_pc_errors = bet_result.pc_error[self.valid_indices] 259 | self.valid_knee_bet_areas = self.bet_areas[self.valid_knee_indices] 260 | self.valid_knee_pc_errors = bet_result.pc_error[self.valid_knee_indices] 261 | self.valid_calc_pressures = bet_result.calc_pressure[self.valid_indices] 262 | self.valid_nm = bet_result.nm[self.valid_indices] 263 | 264 | ## New lines added 265 | ## was needed in the plotting.py file 266 | self.pc_errors = bet_result.pc_error 267 | 268 | # Find min error and corresponding indices 269 | knee_only_filter = np.zeros([len(bet_result.pressure),len(bet_result.pressure)]) 270 | knee_only_filter[self.valid_knee_indices] = 1 271 | knee_filter = filter_mask * knee_only_filter 272 | filtered_pcerrors = bet_result.pc_error + 1000.0 * (1 - filter_mask) 273 | knee_filtered_pcerrors = bet_result.pc_error + \ 274 | 1000.0 * (1 - knee_filter) 275 | min_i, min_j = np.unravel_index( 276 | np.argmin(knee_filtered_pcerrors), filtered_pcerrors.shape) 277 | self.min_i = min_i 278 | self.min_j = min_j 279 | 280 | self.compute_BET_curve() 281 | 282 | self.std_area = np.std(self.bet_areas[self.valid_indices]) 283 | 284 | def compute_BET_curve(self): 285 | """Function for computing BET curve. This is separated to a different function to allow custom min_i min_j""" 286 | 287 | # Compute BET curve at min point 288 | numerator = (self.nm[self.min_i, self.min_j] * 289 | self.c[self.min_i, self.min_j] * self.x_range) 290 | denominator = (1. - self.x_range) + (1. - self.x_range)\ 291 | * (self.c[self.min_i, self.min_j] - 1.) * self.x_range 292 | 293 | with np.errstate(divide='ignore'): 294 | self.bet_curve = numerator / denominator 295 | 296 | self.min_area = self.bet_areas[self.min_i, self.min_j] 297 | 298 | def find_nearest_idx(self, coords): 299 | """Finds min_i and min_j for the given monlayer error coordinates 300 | """ 301 | # find the index bet areas 302 | betarea_idx = np.abs(self.valid_bet_areas - coords[0]).argmin() 303 | pcerror_idx = np.abs(self.valid_pc_errors - coords[1]).argmin() 304 | 305 | # get the indices 306 | min_i = self.valid_indices[0][betarea_idx] #+ 1 307 | min_j = self.valid_indices[1][pcerror_idx] #+ 1 308 | 309 | self.min_i = min_i 310 | self.min_j = min_j 311 | 312 | self.compute_BET_curve() 313 | 314 | def export(self, filepath): 315 | """ Write all relevant information to the directory at filepath. 316 | 317 | """ 318 | filepath = Path(filepath) 319 | 320 | # Write out the filter settings used to get these results. 321 | with (filepath / 'filter_summary.json').open('w') as fp: 322 | pprint(self.filter_params, fp) 323 | 324 | # Write out the key results. 325 | with (filepath / 'results.txt').open('w') as fp: 326 | print(f"Best area has: ", file=fp) 327 | print(f"Area: {self.min_area} ", file=fp) 328 | print( 329 | f"Total points: {self.point_count[self.min_i, self.min_j]} ", file=fp) 330 | print( 331 | f"R-Squared: {self.fit_rsquared[self.min_i, self.min_j]} ", file=fp) 332 | print( 333 | f"Linear Gradient: {self.fit_grad[self.min_i, self.min_j]} ", file=fp) 334 | print( 335 | f"Intercept: {self.fit_intercept[self.min_i, self.min_j]} ", file=fp) 336 | print( 337 | f"C: {self.c[self.min_i, self.min_j]} ", file=fp) 338 | print( 339 | f"Monolayer Loading: {self.nm[self.min_i, self.min_j]} ", file=fp) 340 | print( 341 | f"Calculated Pressure: {self.calc_pressure[self.min_i, self.min_j]} ", file=fp) 342 | print( 343 | f"Read pressure: {self.corresponding_pressure_pchip[self.min_i,self.min_j]} ", file =fp) 344 | print( 345 | f"Error: {self.error[self.min_i, self.min_j]} ", file=fp) 346 | 347 | # Write out a set of csv files 348 | matrices_f = filepath / 'matrices' 349 | matrices_f.mkdir(exist_ok=True) 350 | np.savetxt(str(matrices_f / 'point_counts.csv'), 351 | self.point_count, delimiter=',', fmt='%i') 352 | np.savetxt(str(matrices_f / 'rouq1.csv'), 353 | self.rouq1, delimiter=',', fmt='%i') 354 | np.savetxt(str(matrices_f / 'rouq2.csv'), 355 | self.rouq2, delimiter=',', fmt='%i') 356 | np.savetxt(str(matrices_f / 'rouq3.csv'), 357 | self.rouq3, delimiter=',', fmt='%i') 358 | np.savetxt(str(matrices_f / 'rouq4.csv'), 359 | self.rouq4, delimiter=',', fmt='%i') 360 | np.savetxt(str(matrices_f / 'rouq5.csv'), 361 | self.rouq5, delimiter=',', fmt='%i') 362 | np.savetxt(str(matrices_f / 'bet_areas_filtered.csv'), 363 | self.bet_areas_filtered, delimiter=',', fmt='%1.3f') 364 | np.savetxt(str(matrices_f / 'bet_areas.csv'), 365 | self.bet_areas, delimiter=',', fmt='%1.3f') 366 | np.savetxt(str(matrices_f / 'fit_rsquared.csv'), 367 | self.fit_rsquared, delimiter=',', fmt='%1.3f') 368 | np.savetxt(str(matrices_f / 'fit_grad.csv'), 369 | self.fit_grad, delimiter=',', fmt='%1.3f') 370 | np.savetxt(str(matrices_f / 'fit_intercept.csv'), 371 | self.fit_intercept, delimiter=',', fmt='%1.3f') 372 | np.savetxt(str(matrices_f / 'c_value.csv'), 373 | self.c, delimiter=',', fmt='%1.3f') 374 | np.savetxt(str(matrices_f / 'nm.csv'), 375 | self.nm, delimiter=',', fmt='%1.3f') 376 | np.savetxt(str(matrices_f / 'calc_pressure.csv'), 377 | self.calc_pressure, delimiter=',', fmt='%1.3f') 378 | np.savetxt(str(matrices_f / 'error.csv'), 379 | self.error, delimiter=',', fmt='%1.3f') 380 | np.savetxt(str(matrices_f / 'pc_error.csv'), 381 | self.pc_error, delimiter=',', fmt='%1.3f') 382 | 383 | 384 | def analyse_file(input_file, output_dir=None, **kwargs): 385 | """ Entry point for performing BET analysis on a single named csv file. 386 | If the output directory does not exist, one is created automatically.""" 387 | 388 | if output_dir is None: 389 | output_dir = Path(os.getcwd() + '/bet_output') 390 | 391 | if isinstance(input_file, str): 392 | input_file = Path(input_file) 393 | 394 | if isinstance(output_dir, str): 395 | output_dir = Path(output_dir) 396 | 397 | output_subdir = output_dir / input_file.name 398 | output_subdir.mkdir(exist_ok=True, parents=True) 399 | 400 | # Compute unfiltered results 401 | pressure, q_adsorbed, comments_to_data = get_data(input_file=input_file) 402 | betsi_unfiltered = BETResult(pressure, q_adsorbed) 403 | 404 | # Apply custom filters: 405 | betsi_filtered = BETFilterAppliedResults(betsi_unfiltered, **kwargs) 406 | 407 | # Export the results 408 | betsi_filtered.export(output_subdir) 409 | 410 | # Create and save a PDF plot 411 | fig = create_matrix_plot(betsi_filtered, name=input_file.stem) 412 | 413 | #fig.tight_layout(pad=0.3, rect=[0, 0, 1, 0.95]) 414 | fig.savefig( 415 | str(output_subdir / f'{input_file.stem}_combined_plot.pdf'), bbox_inches='tight') 416 | #plt.tight_layout() 417 | plt.show() 418 | 419 | # Create and show Diagnostics plot 420 | fig_2 = regression_diagnostics_plots(betsi_filtered,name=input_file.stem) 421 | fig_2.tight_layout(pad=.3, rect=[0,0,1,.95]) 422 | plt.show() 423 | 424 | 425 | if __name__ == "__main__": 426 | analyse_file( 427 | Path(r"/Users/johannesosterrieth/Desktop/q_nu1105.csv")) 428 | -------------------------------------------------------------------------------- /betsi/plotting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines a series of plotting functions that can be produced directly from a filtered BET results 3 | object. 4 | 5 | The plots can all be made individually or as part of the larger 2x3 plot matrix. 6 | """ 7 | from matplotlib import pyplot as plt 8 | from matplotlib.ticker import FormatStrFormatter 9 | from scipy.interpolate import splev, pchip_interpolate 10 | import numpy as np 11 | import pandas as pd 12 | import seaborn as sns 13 | import statsmodels.api as sm 14 | from statsmodels.graphics.gofplots import ProbPlot 15 | import matplotlib.gridspec as gridspec 16 | import matplotlib.font_manager 17 | import matplotlib as mpl 18 | 19 | 20 | mpl.rc('font', family='Arial',size=9) 21 | 22 | def regression_diagnostics_plots(bet_filtered, name, fig_2=None): 23 | """ Creates 4 regression diagnostics plots in 2 x 2 matrix 24 | Args: 25 | fit: Matplotlib Figure 26 | bet_filtered : A BETFilterAppliedResults object 27 | name: A string, name to give as a title. 28 | 29 | Returns: 30 | Fig, the updated matplotlib figure 31 | 32 | """ 33 | # Obtaining Regression Diagnostics 34 | 35 | # Gather data and put in DF 36 | min_i = bet_filtered.min_i 37 | min_j = bet_filtered.min_j + 1 38 | p = bet_filtered.pressure 39 | lin_q = bet_filtered.linear_y 40 | P = pd.DataFrame(p) 41 | LIN_Q = pd.DataFrame(lin_q) 42 | dataframe = pd.concat([P, LIN_Q], axis=1) 43 | 44 | # Helper functions 45 | 46 | num_points = len(p) 47 | 48 | def graph(formula, x_range, label=None, ax=None): 49 | """Helper function for plotting cook Distance lines 50 | """ 51 | x = x_range 52 | y = formula(x) 53 | if ax is None: 54 | plt.plot(x, y, label=label, lw=1, ls='--', color='black', alpha = 0.75) 55 | else: 56 | ax.plot(x, y, label=label, lw=1, ls='--', color='black', alpha = 0.75) 57 | 58 | # OLS regression 59 | x = sm.add_constant(p) 60 | model = sm.OLS(lin_q[min_i:min_j], x[min_i:min_j]) 61 | fit = model.fit() 62 | fit_values = fit.fittedvalues 63 | fit_resid = fit.resid 64 | fit_stud_resid = fit.get_influence().resid_studentized_internal 65 | fit_stud_resid_abs_sqrt = np.sqrt(np.abs(fit_stud_resid)) 66 | fit_abs_resid = np.abs(fit_resid) 67 | fit_leverage = fit.get_influence().hat_matrix_diag 68 | fit_CD = fit.get_influence().cooks_distance[0] 69 | 70 | # Make new figure 71 | if fig_2 is None: 72 | fig_2 = plt.figure(constrained_layout=False, figsize=(6.29921, 9.52756)) 73 | mpl.rc('font', family='Arial',size=9) 74 | fig_2.suptitle(f"BETSI Regression Diagnostics for {name}, (Adsorbate: {bet_filtered.adsorbate})\n") 75 | 76 | # "Residual vs fitted" plot 77 | resid_vs_fit = fig_2.add_subplot(2, 2, 1) 78 | sns.residplot(fit_values, fit_resid, data=dataframe, 79 | lowess=True, 80 | scatter_kws={'alpha': .5, 'color': 'red'}, 81 | line_kws={'color': 'black', 'lw': 1, 'alpha': 0.75}, 82 | ax=resid_vs_fit) 83 | resid_vs_fit.axes.set 84 | resid_vs_fit.axes.set_title('Residuals vs Fitted',fontsize=11) 85 | resid_vs_fit.axes.set_xlabel('Fitted Values') 86 | resid_vs_fit.locator_params(axis='x', nbins=4) 87 | resid_vs_fit.axes.set_ylabel('Residuals') 88 | resid_vs_fit.tick_params(axis='both', which='major', labelsize=9) 89 | resid_vs_fit.tick_params(axis='both', which='minor', labelsize=9) 90 | 91 | dfit_values = (max(fit_values) - min(fit_values)) * 1 92 | resid_vs_fit.axes.set_xlim(min(fit_values) - dfit_values, max(fit_values) + dfit_values) 93 | dfit_resid = (max(fit_resid) - min(fit_resid)) * 1 94 | resid_vs_fit.axes.set_ylim(min(fit_resid) - dfit_resid, max(fit_resid) + dfit_resid) 95 | 96 | # "Normal Q-Q" plot 97 | QQ = ProbPlot(fit_stud_resid) 98 | 99 | qq_plot = QQ.qqplot(line='45',markerfacecolor='red',markeredgecolor='red',color='black', alpha=.3, lw=.5, ax=fig_2.add_subplot(2, 2, 2)) 100 | qq_plot.axes[1].set_title('Normal Q-Q') 101 | qq_plot.axes[1].set_xlabel('Theoretical Quantiles') 102 | qq_plot.axes[1].set_ylabel('Studentized Residuals') 103 | qq_plot.axes[1].tick_params(axis='both', which='major') 104 | qq_plot.axes[1].tick_params(axis='both', which='minor') 105 | 106 | abs_norm_resid = np.flip(np.argsort(np.abs(fit_stud_resid)), 0) 107 | abs_norm_resid_top_3 = abs_norm_resid[:3] 108 | for r, i in enumerate(abs_norm_resid_top_3): 109 | # Add annotations 110 | qq_plot.axes[0].annotate(i, xy=(np.flip(QQ.theoretical_quantiles, 0)[r], fit_stud_resid[i]), size = 9) 111 | 112 | # "Scale-location" plot 113 | scale_loc = fig_2.add_subplot(2, 2, 3) 114 | scale_loc.scatter(fit_values, fit_stud_resid_abs_sqrt, alpha=.5, c = 'red') 115 | sns.regplot(fit_values, fit_stud_resid_abs_sqrt, scatter=False, ci=False, lowess=True, line_kws={'color': 'black', 'lw': 1, 'alpha': .75}, ax=scale_loc) 116 | scale_loc.set_title('Scale-Location') 117 | scale_loc.set_xlabel('Fitted Values') 118 | scale_loc.set_ylabel('$\mathregular{\sqrt{|Studentized\ Residuals|}}$') 119 | scale_loc.tick_params(axis='both', which='major', labelsize=9) 120 | scale_loc.tick_params(axis='both', which='minor', labelsize=9) 121 | 122 | abs_sq_norm_resid = np.flip(np.argsort(fit_stud_resid_abs_sqrt), 0) 123 | abs_sq_norm_resid_top_3 = abs_sq_norm_resid[:3] 124 | for i in abs_norm_resid_top_3: 125 | # Add annotations 126 | scale_loc.axes.annotate(i, xy=(fit_values[i], fit_stud_resid_abs_sqrt[i]), size=11) 127 | 128 | scale_loc.axes.set_xlim(min(fit_values) - .2 * max(fit_values), max(fit_values) + .2 * max(fit_values)) 129 | scale_loc.locator_params(axis='x', nbins=4) 130 | 131 | # "Residuals vs leverage" plot 132 | res_vs_lev = fig_2.add_subplot(2, 2, 4) 133 | res_vs_lev.scatter(fit_leverage, fit_stud_resid, alpha=.5, color = 'red') 134 | sns.regplot(fit_leverage, fit_stud_resid, scatter=False, ci=False, lowess=True, line_kws={'color': 'black', 'lw': 1, 'alpha': .75}, ax=res_vs_lev) 135 | res_vs_lev.axes.set_title('Residuals vs Leverage') 136 | res_vs_lev.axes.set_xlabel('Leverage') 137 | res_vs_lev.axes.set_ylabel('Studentized Residuals') 138 | res_vs_lev.tick_params(axis='both', which='major') 139 | res_vs_lev.tick_params(axis='both', which='minor') 140 | 141 | leverage_top_3 = np.flip(np.argsort(fit_CD), 0)[:3] 142 | for i in leverage_top_3: 143 | # Add annotations 144 | res_vs_lev.axes.annotate(i, xy=(fit_leverage[i], fit_stud_resid[i]), size=9) 145 | 146 | p_3 = p[min_i:min_j] 147 | p_2 = len(fit.params) # number of model parameters 148 | graph(lambda p_3: np.sqrt((.5 * p_2 * (1 - p_3)) / p_3), np.linspace(.001, max(fit_leverage), 50), 'Cook\'s Distance', ax=res_vs_lev) # .5 line 149 | graph(lambda p_3: -1 * np.sqrt((.5 * p_2 * (1 - p_3)) / p_3), np.linspace(.001, max(fit_leverage), 50), ax=res_vs_lev) 150 | graph(lambda p_3: np.sqrt((1 * p_2 * (1 - p_3)) / p_3), np.linspace(.001, max(fit_leverage), 50), ax=res_vs_lev) # 1 line 151 | graph(lambda p_3: -1 * np.sqrt((1 * p_2 * (1 - p_3)) / p_3), np.linspace(.001, max(fit_leverage), 50), ax=res_vs_lev) # 1 line 152 | 153 | res_vs_lev.legend(prop={'size': 9}) 154 | 155 | plt.subplots_adjust(bottom=0.07, top=0.91, hspace=.255, wspace=0.315, left=0.12, right=0.92) 156 | 157 | return fig_2 158 | 159 | 160 | def create_matrix_plot(bet_filtered, rouq3, rouq4, name, fig=None): 161 | """ Creates all 6 of the key plots in a 2x3 matrix 162 | 163 | Args: 164 | fig: Matplotlib Figure 165 | bet_filtered: A BETFilterAppliedResults object. 166 | name: A string, name to give as a title. 167 | 168 | Returns: 169 | Fig, the updated matplotlib figure 170 | 171 | """ 172 | # Make Isotherm Plot 173 | if fig is None: 174 | fig = plt.figure(figsize=(6.29921, 9.52756)) 175 | 176 | fig.set_size_inches(6.29921, 9.52756) 177 | fig.suptitle(f"BETSI Analysis for {name}, (Adsorbate: {bet_filtered.adsorbate})\n", fontname="Arial", fontsize = '11') 178 | fig.subplots_adjust(hspace=1.0, top=0.91, bottom=0.07, left=0.052, right=0.865, wspace=0.315) 179 | 180 | gs = gridspec.GridSpec(9, 2, figure=fig) 181 | 182 | # Plot "Adsorption isotherm" 183 | ax = fig.add_subplot(gs[:3,0]) 184 | plot_isotherm(bet_filtered, ax) 185 | 186 | # Plot "Roquerol representation" 187 | ax = fig.add_subplot(gs[:3,1]) 188 | plot_roquerol_representation(bet_filtered, ax) 189 | 190 | # Plot "Linear range" 191 | ax = fig.add_subplot(gs[3:6,0]) 192 | plot_linear_y(bet_filtered, ax) 193 | 194 | # Plot "Filtered BET areas" 195 | if not rouq3 and not rouq4: 196 | ## New lines added 197 | x_coords = bet_filtered.valid_bet_areas 198 | y_coords = bet_filtered.valid_pc_errors 199 | pc_error_higher_than_290_indexes = np.where(y_coords > 290)[0] 200 | y_coords_lower_than_290 = np.delete(y_coords, pc_error_higher_than_290_indexes) 201 | if not (len(y_coords_lower_than_290) == 0 or len(pc_error_higher_than_290_indexes) == 0): 202 | ax = fig.add_subplot(gs[3:4, 1:]) 203 | ax2 = fig.add_subplot(gs[4:6, 1:]) 204 | # Plots a figure with a break in the y-axis 205 | plot_area_error_1(bet_filtered, ax, ax2) 206 | else: 207 | ax = fig.add_subplot(gs[3:6,1]) 208 | # Plots a figure with NO break in the y-axis 209 | plot_area_error_2(bet_filtered, ax) 210 | else: 211 | ax = fig.add_subplot(gs[3:6,1]) 212 | # Plots a figure with NO break in the y-axis 213 | plot_area_error_2(bet_filtered, ax) 214 | 215 | # Plot "Filtered monolayer-loadings" 216 | ax = fig.add_subplot(gs[6:9,0]) 217 | plot_monolayer_loadings(bet_filtered, ax) 218 | 219 | # Plot "Distribution of filtered BET areas" 220 | ax = fig.add_subplot(gs[6:9,1]) 221 | plot_box_and_whisker(bet_filtered, ax) 222 | 223 | plt.tight_layout() 224 | return fig 225 | 226 | 227 | def plot_isotherm(bet_filtered, ax=None): 228 | """ Plot the Isotherm alongside the selected linear region, spline interpolation, point corresponding to lowest error and fit from BET theory. 229 | 230 | Args: 231 | bet_filtered: A BETFilterAppliedResults object. 232 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 233 | 234 | """ 235 | if ax is None: 236 | # When this is not part of a larger plot 237 | fig = plt.figure() 238 | ax = fig.add_subplot(1, 1, 1) 239 | 240 | # Set axis details 241 | mpl.rc('font', family='Arial',size=9) 242 | ax.set_title(f"Adsorption Isotherm", fontname="Arial", fontsize = '11') 243 | ax.set_xlabel(r'$\mathregular{P/P_0}$') 244 | ax.set_ylabel(r'$\mathregular{N_2 uptake}$ (STP) $\mathregular{cm^3 g^{-1}}$') 245 | ax.set_xlim([-0.1, 1.1]) 246 | ax.set_ylim([0.0, max(bet_filtered.q_adsorbed)]) 247 | ax.tick_params(axis='both', which='major', labelsize=9) 248 | ax.tick_params(axis='both', which='minor', labelsize=9) 249 | 250 | # Get min_i, min_j from the filtered result 251 | min_i = bet_filtered.min_i 252 | min_j = bet_filtered.min_j 253 | 254 | # Plot the isotherm itself 255 | ax.scatter(bet_filtered.pressure[:min_i], bet_filtered.q_adsorbed[:min_i], color='black', edgecolors='black', label='Adsorption Isotherm', alpha=0.50) 256 | ax.scatter(bet_filtered.pressure[min_j+1:], bet_filtered.q_adsorbed[min_j+1:], color='black', edgecolors='black',alpha = 0.50) 257 | 258 | # Plot the part corresponding to the selected linear region 259 | ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.q_adsorbed[min_i:min_j + 1], marker='s',color='red', edgecolors='red', label='Linear Range', alpha=0.5) 260 | 261 | # plot pchip interpolation 262 | ax.plot(bet_filtered.x_range, pchip_interpolate(bet_filtered.pressure, bet_filtered.q_adsorbed, bet_filtered.x_range), color='black', alpha=.75, label='Pchip Interpolation') 263 | # plot corresponding pressure 264 | ax.scatter(bet_filtered.corresponding_pressure_pchip[min_i, min_j], bet_filtered.nm[min_i, min_j], marker='^', color='blue', edgecolor='blue', label='$\mathregular{N_m}$ Read', alpha=0.50) 265 | 266 | # Plot selected Monolayer loading (single point) 267 | ax.scatter(bet_filtered.calc_pressure[min_i, min_j], bet_filtered.nm[min_i, min_j], marker='v', color='green', edgecolors='green', edgecolor='green', label='$\mathregular{N_m}$ BET') 268 | 269 | # Plot the BET curve derived from BET theory 270 | ax.plot(bet_filtered.x_range, bet_filtered.bet_curve, c='g', label='BET Fit', alpha=.5) 271 | 272 | # Add a legend 273 | ax.autoscale(False) 274 | ax.legend(prop={'size': 9}) 275 | 276 | 277 | def plot_roquerol_representation(bet_filtered, ax=None): 278 | """ Plot the Roquerol representation with points corresponding to those in the selected linear region highlighted. 279 | 280 | Args: 281 | bet_filtered: A BETFilterAppliedResults object. 282 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 283 | 284 | """ 285 | 286 | if ax is None: 287 | # When this is not part of a larger plot 288 | fig = plt.figure() 289 | ax = fig.add_subplot(1, 1, 1) 290 | 291 | # Set axis details 292 | mpl.rc('font', family='Arial',size=9) 293 | ax.set_title(f"Rouquerol Representation", fontname="Arial", fontsize = '11') 294 | ax.set_xlabel(r'$\mathregular{P/P_0}$') 295 | ax.set_ylabel(r'$\mathregular{N(1-P/P_0)}$', fontname="Arial") 296 | ax.tick_params(axis='both', which='major', labelsize=9) 297 | ax.tick_params(axis='both', which='minor', labelsize=9) 298 | 299 | # Get min_i, min_j from the filtered result 300 | min_i = bet_filtered.min_i 301 | min_j = bet_filtered.min_j 302 | 303 | # Plot the main Roquerol representation scatter 304 | ax.scatter(bet_filtered.pressure[:min_i], bet_filtered.rouq_y[:min_i], edgecolors='black', color='black', label=r'$\mathregular{N(1-P/P_0)}$', alpha=0.5) 305 | ax.scatter(bet_filtered.pressure[min_j+1:], bet_filtered.rouq_y[min_j+1:], edgecolors='black', color='black', alpha=0.5) 306 | 307 | # Plot the part corresponding to the linear region 308 | ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.rouq_y[min_i:min_j + 1], marker='s', color='red', edgecolors='none', label='Linear Range', alpha=0.50) 309 | 310 | # Add a legend 311 | ax.legend(prop={'size': 9}) 312 | 313 | 314 | def plot_linear_y(bet_filtered, ax=None): 315 | """ Plot the selected linear region of the linearised BET equation and print the formula of the 316 | straight line alongside it. 317 | 318 | Args: 319 | bet_filtered: A BETFilterAppliedResults object. 320 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 321 | 322 | """ 323 | if ax is None: 324 | # When this is not part of a larger plot: 325 | fig = plt.figure() 326 | ax = fig.add_subplot(1, 1, 1) 327 | 328 | # Set axis details 329 | mpl.rc('font', family='Arial',size=9) 330 | ax.set_title(f"Linear Range", fontname="Arial",fontsize='11') 331 | ax.set_xlabel(r'$\mathregular{P/P_0}$') 332 | ax.set_ylabel(r'$\mathregular{P/N(P_0 - P)}$', fontname="Arial", fontsize = '9') 333 | ax.tick_params(axis='both', which='major', labelsize=9) 334 | ax.tick_params(axis='both', which='minor', labelsize=9) 335 | 336 | # Get min_i, min_j from the filtered result 337 | min_i = bet_filtered.min_i 338 | min_j = bet_filtered.min_j 339 | 340 | ### Plot the points in the selected linear region 341 | ##ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.linear_y[min_i:min_j + 1], marker='s', color='r', edgecolors='red', label='Interpolated Points', alpha = 0.50) 342 | 343 | ## Detect data points that were in the original adsorption data 344 | if bet_filtered.comments_to_data['interpolated_points_added']: 345 | # Plot the points in the selected linear region 346 | ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.linear_y[min_i:min_j + 1], marker='s', color='green', edgecolors='green', label='Interpolated Points', alpha = 0.50) 347 | 348 | ###original_pressure_points_used = [x for x in bet_filtered.pressure[min_i:min_j + 1] if x in bet_filtered.original_pressure_data] 349 | original_pressure_points_used = [x for x in bet_filtered.original_pressure_data if bet_filtered.pressure[min_i] <= x <= bet_filtered.pressure[min_j]] 350 | original_points_indexes = [] 351 | for x in original_pressure_points_used: 352 | ##original_points_indexes.append(np.where(bet_filtered.pressure[min_i:min_j + 1] == x)[0][0]) 353 | original_points_indexes.append(np.where(abs((bet_filtered.pressure[min_i:min_j + 1]-x)/x) * 100 < 0.01)[0][0]) 354 | original_linear_y_points_used = bet_filtered.linear_y[min_i:min_j + 1][original_points_indexes] 355 | if len(original_pressure_points_used) != 0: 356 | ax.scatter(original_pressure_points_used, original_linear_y_points_used, marker='s', color='red', edgecolors='red', label='Original Points', alpha = 0.50) 357 | # Add a legend 358 | ax.autoscale(False) 359 | ax.legend(loc=4, prop={'size': 9}) 360 | else: 361 | # Plot the points in the selected linear region 362 | ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.linear_y[min_i:min_j + 1], marker='s', color='r', edgecolors='red', label='Interpolated Points', alpha = 0.50) 363 | 364 | 365 | 366 | # Plot the straight line obtained from the linear regression 367 | largest_valid_x = max(bet_filtered.pressure[min_i:min_j + 1]) 368 | intercept_at_opt = bet_filtered.fit_intercept[min_i, min_j] 369 | grad_at_opt = bet_filtered.fit_grad[min_i, min_j] 370 | highest_valid_pressure = max(bet_filtered.pressure[min_i:min_j + 1]) 371 | end_y = grad_at_opt * highest_valid_pressure + intercept_at_opt 372 | 373 | ax.plot([0, largest_valid_x], [intercept_at_opt, end_y], color='black',alpha=.75) 374 | 375 | # Set the plot limits 376 | smallest_y = 0.1 * min(bet_filtered.linear_y[min_i:min_j + 1]) 377 | biggest_y = 1.1 * max(bet_filtered.linear_y[min_i:min_j + 1]) 378 | smallest_x = 0.1 * min(bet_filtered.pressure[min_i:min_j + 1]) 379 | biggest_x = 1.1 * max(bet_filtered.pressure[min_i:min_j + 1]) 380 | 381 | ax.set_ylim(smallest_y, biggest_y) 382 | ax.set_xlim(smallest_x, biggest_x) 383 | 384 | # Print the equation of the straight line. 385 | ax.yaxis.set_major_formatter(FormatStrFormatter('%.1E')) 386 | ##y_eqn = r"y = {0:.8f}$x$ + {1:.8f}".format(bet_filtered.fit_grad[min_i, min_j], bet_filtered.fit_intercept[min_i, min_j]) 387 | y_eqn = r"y = {0:0.4e}$x$ + {1:0.4e}".format(bet_filtered.fit_grad[min_i, min_j], bet_filtered.fit_intercept[min_i, min_j]) 388 | r_eqn = r"$R^2$ = {0:.8f}".format(bet_filtered.fit_rsquared[min_i, min_j]) 389 | ax.text(0.05, 0.9, y_eqn, {'color': 'black', 'fontsize': 9}, transform=ax.transAxes) 390 | ax.text(0.05, 0.825, r_eqn, {'color': 'black', 'fontsize': 9}, transform=ax.transAxes) 391 | 392 | 393 | def plot_area_error_1(bet_filtered, ax=None, ax2=None): 394 | """ Plot the distribution of valid BET areas, highlight those ending on the `knee` and print 395 | start and end indices. 396 | 397 | Args: 398 | bet_filtered: A BETFilterAppliedResults object. 399 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 400 | 401 | """ 402 | 403 | min_i = bet_filtered.min_i 404 | min_j = bet_filtered.min_j 405 | 406 | if ax is None: 407 | # When this is not part of a larger plot 408 | fig = plt.figure() 409 | ax = fig.add_subplot(1, 1, 1) 410 | 411 | # Set axis details 412 | mpl.rc('font', family='Arial',size=9) 413 | ax.set_title('Filtered BET areas ', fontname="Arial") 414 | ax2.set_xlabel(r'BET Area $\mathregular{m^2 g^{-1}}$', fontname="Arial", fontsize = '9') 415 | ax2.set_ylabel(r'Percentage Error %', fontname="Arial", fontsize = '9') 416 | ax2.yaxis.set_label_coords(-0.1, 0.78) 417 | ax.tick_params(axis='both', which='major', labelsize=9) 418 | ax.tick_params(axis='both', which='minor', labelsize=9) 419 | ax2.tick_params(axis='both', which='major', labelsize=9) 420 | ax2.tick_params(axis='both', which='minor', labelsize=9) 421 | 422 | x_coords = bet_filtered.valid_bet_areas 423 | y_coords = bet_filtered.valid_pc_errors 424 | 425 | pc_error_higher_than_290_indexes = np.where(y_coords > 290)[0] 426 | y_coords_lower_than_290 = np.delete(y_coords, pc_error_higher_than_290_indexes) 427 | 428 | ##x_coords_nonvalid = np.array([x for x in x_coords if x not in bet_filtered.valid_knee_bet_areas]) 429 | ##y_coords_nonvalid = np.array([y for y in y_coords if y not in bet_filtered.valid_knee_pc_errors]) 430 | ## New lines added 431 | ## Find indices that are in valid_indices but not in valid_knee_indices 432 | temp_x = [] 433 | temp_y = [] 434 | for i in range(len(bet_filtered.valid_indices[0])): 435 | a = [bet_filtered.valid_indices[0][i], bet_filtered.valid_indices[1][i]] 436 | temp_checker = 0 437 | for j in range(len(bet_filtered.valid_knee_indices[0])): 438 | b = [bet_filtered.valid_knee_indices[0][j], bet_filtered.valid_knee_indices[1][j]] 439 | if a==b: 440 | temp_checker = 1 441 | if temp_checker == 0: 442 | temp_x.append(a[0]) 443 | temp_y.append(a[1]) 444 | bet_filtered.valid_indeces_but_not_knee = np.array([temp_x,temp_y]) 445 | bet_filtered.valid_indeces_but_not_knee = tuple(bet_filtered.valid_indeces_but_not_knee) 446 | 447 | x_coords_nonvalid = bet_filtered.bet_areas[bet_filtered.valid_indeces_but_not_knee] 448 | y_coords_nonvalid = bet_filtered.pc_error[bet_filtered.valid_indeces_but_not_knee] 449 | 450 | 451 | # Scatter plot of the Error across valid areas 452 | ## New lines added 453 | ax.scatter(x_coords_nonvalid, y_coords_nonvalid, color='red', edgecolors='red', picker=5, alpha =0.5) 454 | ax.scatter(bet_filtered.valid_knee_bet_areas, bet_filtered.valid_knee_pc_errors, color='b', edgecolors='b', marker='s', picker=5, alpha=0.5) 455 | ax.scatter(bet_filtered.bet_areas[min_i, min_j], bet_filtered.pc_error[min_i, min_j], marker='s', color='yellow', edgecolors='yellow') 456 | 457 | ax2.scatter(x_coords_nonvalid, y_coords_nonvalid, color='r', edgecolors='r', picker=5, alpha = 0.5) 458 | ax2.scatter(bet_filtered.valid_knee_bet_areas, bet_filtered.valid_knee_pc_errors, color='b', edgecolors='b', marker='s', picker=5, alpha=.5) 459 | ax2.scatter(bet_filtered.bet_areas[min_i, min_j], bet_filtered.pc_error[min_i, min_j], marker='s', color='orange', edgecolors='orange') 460 | 461 | # Axis settings 462 | ax.set_ylim(290, 310) 463 | ## define the y-axis upper limit of the lower figure (ax2) 464 | ax2.set_ylim(top=max(y_coords_lower_than_290)*1.1) 465 | 466 | ax.spines['bottom'].set_visible(False) 467 | ax2.spines['top'].set_visible(False) 468 | ax.xaxis.set_visible(False) 469 | ax.tick_params(labeltop='off') 470 | ax2.tick_params(labeltop='off') 471 | ax2.xaxis.tick_bottom() 472 | 473 | d = .015 # how big to make the diagonal lines in axes coordinates 474 | # arguments to pass plot, just so we don't keep repeating them 475 | kwargs = dict(transform=ax.transAxes, color='k', clip_on=False) 476 | ax.plot((-d, +d), (-d, +d), **kwargs) # top-left diagonal 477 | ax.plot((1 - d, 1 + d), (-d, +d), **kwargs) # top-right diagonal 478 | 479 | kwargs.update(transform=ax2.transAxes) # switch to the bottom axes 480 | ax2.plot((-d, +d), (1 - d, 1 + d), **kwargs) # bottom-left diagonal 481 | ax2.plot((1 - d, 1 + d), (1 - d, 1 + d), **kwargs) # bottom-right diagonal 482 | 483 | # Add text annotation to each error point 484 | for i, type in enumerate(x_coords): 485 | index = f"({bet_filtered.valid_indices[0][i] + 1}," \ 486 | f" {bet_filtered.valid_indices[1][i] + 1})" 487 | plt.text(x_coords[i], y_coords[i], index, fontsize=7, clip_on=True) 488 | 489 | 490 | def plot_area_error_2(bet_filtered, ax=None): 491 | """ Plot the distribution of valid BET areas, highlight those ending on the `knee` and print start and end indices. 492 | 493 | Args: 494 | bet_filtered: A BETFilterAppliedResults object. 495 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 496 | 497 | """ 498 | min_i = bet_filtered.min_i 499 | min_j = bet_filtered.min_j 500 | 501 | if ax is None: 502 | # When this is not part of a larger plot 503 | fig = plt.figure() 504 | ax = fig.add_subplot(1, 1, 1) 505 | 506 | # Set axis details 507 | ax.set_title('Filtered BET areas ', fontname="Arial") 508 | ax.set_xlabel(r'BET Area $\mathregular{m^2 g^{-1}}$', fontname="Arial", fontsize = '9') 509 | ax.set_ylabel(r'Percentage Error %', fontname="Arial", fontsize = '9') 510 | ax.tick_params(axis='both', which='major', labelsize=9) 511 | ax.tick_params(axis='both', which='major', labelsize=9) 512 | 513 | x_coords = bet_filtered.valid_bet_areas 514 | y_coords = bet_filtered.valid_pc_errors 515 | ##x_coords_nonvalid = np.array([x for x in x_coords if x not in bet_filtered.valid_knee_bet_areas]) 516 | ##y_coords_nonvalid = np.array([y for y in y_coords if y not in bet_filtered.valid_knee_pc_errors]) 517 | ## New lines added 518 | ## Find indices that are in valid_indices but not in valid_knee_indices 519 | temp_x = [] 520 | temp_y = [] 521 | for i in range(len(bet_filtered.valid_indices[0])): 522 | a = [bet_filtered.valid_indices[0][i], bet_filtered.valid_indices[1][i]] 523 | temp_checker = 0 524 | for j in range(len(bet_filtered.valid_knee_indices[0])): 525 | b = [bet_filtered.valid_knee_indices[0][j], bet_filtered.valid_knee_indices[1][j]] 526 | if a==b: 527 | temp_checker = 1 528 | if temp_checker == 0: 529 | temp_x.append(a[0]) 530 | temp_y.append(a[1]) 531 | bet_filtered.valid_indeces_but_not_knee = np.array([temp_x,temp_y]) 532 | bet_filtered.valid_indeces_but_not_knee = tuple(bet_filtered.valid_indeces_but_not_knee) 533 | if len(bet_filtered.valid_indeces_but_not_knee[0]) > 0: 534 | x_coords_nonvalid = bet_filtered.bet_areas[bet_filtered.valid_indeces_but_not_knee] 535 | y_coords_nonvalid = bet_filtered.pc_error[bet_filtered.valid_indeces_but_not_knee] 536 | else: 537 | x_coords_nonvalid = np.empty(0) 538 | y_coords_nonvalid = np.empty(0) 539 | 540 | 541 | # Scatter plot of the Error across valid areas 542 | ax.scatter(x_coords_nonvalid, y_coords_nonvalid, color='red', edgecolors='red', picker=5, alpha=0.5) 543 | ax.scatter(bet_filtered.valid_knee_bet_areas, bet_filtered.valid_knee_pc_errors, color='b', edgecolors='b', marker='s', picker=5, alpha=0.50) 544 | ax.scatter(bet_filtered.bet_areas[min_i, min_j], bet_filtered.pc_error[min_i, min_j], marker='s', color='yellow', edgecolors='yellow') 545 | 546 | # Add text annotation to each error point. 547 | for i, type in enumerate(x_coords): 548 | index = f"({bet_filtered.valid_indices[0][i] + 1}," \ 549 | f" {bet_filtered.valid_indices[1][i] + 1})" 550 | plt.text(x_coords[i], y_coords[i], index, fontsize=7, clip_on=True) 551 | 552 | # Set the Y-limit on the errors 553 | ax.set_ylim(0, max(y_coords) * 1.1) 554 | 555 | def plot_monolayer_loadings(bet_filtered, ax=None): 556 | """ Plot the distribution of monolayer loadings alonside the Isotherm and fitted spline. 557 | 558 | Args: 559 | bet_filtered: A BETFilterAppliedResults object. 560 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 561 | 562 | """ 563 | 564 | if ax is None: 565 | # When this is not part of a larger plot 566 | fig = plt.figure() 567 | ax = fig.add_subplot(1, 1, 1) 568 | 569 | # Set axis details 570 | mpl.rc('font', family='Arial',size=9) 571 | ax.set_title("Filtered Monolayer-Loadings", fontname="Arial") 572 | ax.set_xlabel(r'$\mathregular{P/P_0}$', fontname="Arial", fontsize = '9') 573 | ax.set_ylabel(r'$\mathregular{N_2 uptake}$ (STP) $\mathregular{cm^3 g^{-1}}$') 574 | ax.tick_params(axis='both', which='major', labelsize=9) 575 | ax.tick_params(axis='both', which='minor', labelsize=9) 576 | 577 | # Get min_i, min_j from the filtered result 578 | min_i = bet_filtered.min_i 579 | min_j = bet_filtered.min_j 580 | 581 | # Plot the Isotherm itself 582 | ax.scatter(bet_filtered.pressure[:min_i], bet_filtered.q_adsorbed[:min_i], color='black', edgecolors='black', alpha=0.5) 583 | ax.scatter(bet_filtered.pressure[min_j+1:], bet_filtered.q_adsorbed[min_j+1:], color='black', edgecolors='black', label='Adsorption Isotherm', alpha=0.5) 584 | # Plot the fitted spline 585 | ax.plot(bet_filtered.x_range, pchip_interpolate(bet_filtered.pressure, bet_filtered.q_adsorbed, bet_filtered.x_range), color='black',alpha=.5, label='Pchip Interpolation') 586 | 587 | # Plot the valid monolayer loadings. 588 | ax.scatter(bet_filtered.valid_calc_pressures, bet_filtered.valid_nm, marker='^', color='blue', edgecolors='blue', label='$\mathregular{N_m}$ valid', alpha=0.5) 589 | 590 | # Plot the valid optimum linear range 591 | ax.scatter(bet_filtered.pressure[min_i:min_j + 1], bet_filtered.q_adsorbed[min_i:min_j + 1], marker='s', color='red', edgecolors='red', label='Linear Range', alpha=0.5) 592 | 593 | # Plot the single optimum Monolayer loading 594 | ax.scatter(bet_filtered.calc_pressure[min_i, min_j], bet_filtered.nm[min_i, min_j], marker='s', color='orange', edgecolors='orange', label='N$_m$ BET') 595 | 596 | # Plot the Fit obtained from the BET equation 597 | ax.plot(bet_filtered.x_range, bet_filtered.bet_curve, c='g',alpha=.5, label='BET Fit') 598 | 599 | # Set the Xlimits and add a legend 600 | ax.set_xlim([-0.001, max(bet_filtered.valid_calc_pressures)]) 601 | ax.set_ylim([0.0, max(bet_filtered.q_adsorbed)]) 602 | 603 | # Add a legend 604 | ax.autoscale(False) 605 | ax.legend(loc=4, prop={'size': 9}) 606 | 607 | 608 | def plot_box_and_whisker(bet_filtered, ax=None): 609 | """ Plot a box and whisker plot for the valid BET areas. 610 | 611 | Args: 612 | bet_filtered: A BETFilterAppliedResults object. 613 | ax: Optional matplotlib axis object. If none is provided, an axis for a single subplot is made. 614 | 615 | """ 616 | if ax is None: 617 | # When this is not part of a larger plot: 618 | fig = plt.figure() 619 | ax = fig.add_subplot(1, 1, 1) 620 | 621 | # Set axis details. 622 | mpl.rc('font', family='Arial',size=9) 623 | ax.set_title('Distribution of filtered BET Areas', fontname="Arial") 624 | ax.set_ylabel(r'BET Area $\mathregular{m^2 g^{-1}}$', fontname="Arial", fontsize = '9') 625 | ax.tick_params(axis='both', which='major', labelsize=9) 626 | ax.tick_params(axis='both', which='minor', labelsize=9) 627 | 628 | min_i = bet_filtered.min_i 629 | min_j = bet_filtered.min_j 630 | 631 | y_min = [bet_filtered.bet_areas[min_i, min_j]] 632 | x_min = np.random.normal(1, 0.04, 1) 633 | 634 | y = list(set(bet_filtered.valid_knee_bet_areas) - set(y_min)) 635 | x = np.random.normal(1, 0.04, size=len(y)) 636 | 637 | y_2 = list(set(bet_filtered.valid_bet_areas) - set(y) - set(y_min)) 638 | x_2 = np.random.normal(1, 0.04, size=len(y_2)) 639 | 640 | # Plot the filtered BET areas 641 | ax.scatter(x_2, y_2, alpha=0.5, color='red', edgecolor='red') 642 | ax.scatter(x, y, color='blue', edgecolor='blue', alpha=0.5) 643 | ax.scatter(x_min, y_min, marker='s', color='orange', edgecolor='orange') 644 | 645 | if len(x)==0: 646 | ax.set_xlim([.75,1.25]) 647 | dy = y_min[0]*0.25 648 | ax.set_ylim(y_min[0] - dy, y_min[0] + dy) 649 | else: 650 | ax.set_xlim([.75, 1.25]) 651 | dy = (max(bet_filtered.valid_bet_areas)-min(bet_filtered.valid_bet_areas)) * 1 652 | ax.set_ylim(min(bet_filtered.valid_bet_areas) - dy, max(bet_filtered.valid_bet_areas) + dy) 653 | # if len(y_2)==0: 654 | # dy = (max(y)-min(y)) * 1 655 | # ax.set_ylim(min(y) - dy, max(y) + dy) 656 | # else: 657 | # dy = (max(y_2) - min(y_2)) * 1 658 | # ax.set_ylim(min(y_2) - dy, max(y_2) + dy) 659 | 660 | # Make the boxplot of valid areas 661 | medianprops = dict(linestyle='--', linewidth=1, color='black', alpha=0.35) # median line properties 662 | ax.boxplot(bet_filtered.valid_bet_areas, showfliers=False, medianprops=medianprops) 663 | ax.set_xticks([]) 664 | 665 | # Write BET area 666 | called_BET_area = """BET Area = {0:0.0f} $m^2/g$""".format(np.around((bet_filtered.bet_areas[min_i, min_j])), decimals=0, out=None) 667 | ax.text(0.05, 0.90, called_BET_area, {'color': 'black', 'fontsize': 9}, transform=ax.transAxes) 668 | 669 | points_range = f"Selected Points Range: ({min_i+1},{min_j+1})" 670 | ax.text(0.05, 0.80, points_range, {'color': 'black', 'fontsize': 9}, transform=ax.transAxes) 671 | -------------------------------------------------------------------------------- /betsi/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | from pathlib import Path 5 | 6 | import numpy as np 7 | from scipy.interpolate import splrep, PchipInterpolator, pchip_interpolate 8 | 9 | 10 | def get_data(input_file): 11 | """Read pressure and Nitrogen uptake data from file. Asserts pressures in units of bar. 12 | 13 | Args: 14 | input_file: Path the path to the input file 15 | 16 | Returns: 17 | Pressure and Quantity adsorbed. 18 | """ 19 | input_file = Path(input_file) 20 | 21 | if str(input_file).find('.txt') != -1 or str(input_file).find('.aif') != -1: 22 | pressure, q_adsorbed = get_data_for_txt_aif(str(input_file)) 23 | else: 24 | try: 25 | data = np.loadtxt(str(input_file), skiprows=0, delimiter=',') 26 | pressure = data[:, 0] 27 | q_adsorbed = data[:, 1] 28 | except ValueError: 29 | data = np.loadtxt(str(input_file), skiprows=1, delimiter=',') 30 | pressure = data[:, 0] 31 | q_adsorbed = data[:, 1] 32 | 33 | ## New lines added here 34 | comments_to_data = {'has_negative_pressure_points': False,\ 35 | 'monotonically_increasing_pressure': True,\ 36 | 'rel_pressure_between_0_and_1': True} 37 | ## removes negetive relative pressure points if any 38 | negative_pressure_indexes = np.where(pressure < 0)[0] 39 | if len(negative_pressure_indexes) > 0: 40 | comments_to_data['has_negative_pressure_points'] = True 41 | pressure = np.delete(pressure, negative_pressure_indexes) 42 | q_adsorbed = np.delete(q_adsorbed, negative_pressure_indexes) 43 | 44 | ## checks if relative pressure points are monotonically increasing, if not, 45 | ## removes problematic points 46 | if not (pressure == np.sort(pressure)).all(): 47 | comments_to_data['monotonically_increasing_pressure'] = False 48 | temp_index = 0 49 | temp_pressure = pressure 50 | temp_q_adsorbed = q_adsorbed 51 | while temp_index < len(temp_pressure)-1: 52 | if temp_pressure[temp_index+1] <= temp_pressure[temp_index]: 53 | temp_pressure = np.delete(temp_pressure, [temp_index+1]) 54 | temp_q_adsorbed = np.delete(temp_q_adsorbed, [temp_index+1]) 55 | else: 56 | temp_index += 1 57 | pressure = temp_pressure 58 | q_adsorbed = temp_q_adsorbed 59 | 60 | ## checks if relative pressure points lie between 0 and 1 (bar) 61 | if not (pressure < 1.1).all(): 62 | comments_to_data['rel_pressure_between_0_and_1'] = False 63 | pressure_above_one_indexes = np.where(pressure > 1.1)[0] 64 | pressure = np.delete(pressure, pressure_above_one_indexes) 65 | q_adsorbed = np.delete(q_adsorbed, pressure_above_one_indexes) 66 | 67 | comments_to_data['interpolated_points_added'] = False 68 | 69 | ## assert (pressure < 1.1).all(), "Relative pressure must lie between 0 and 1 bar." 70 | return pressure, q_adsorbed, comments_to_data 71 | 72 | 73 | def get_fitted_spline(pressure, q_adsorbed): 74 | """ Fits a cubic spline to the isotherm. 75 | 76 | Args: 77 | pressure: Array of relative pressure values. 78 | q_adsorbed: Array of Nitrogen uptake values. 79 | 80 | Returns: 81 | tck tuple of spline parameters. 82 | """ 83 | return splrep(pressure, q_adsorbed, s=50, k=3, quiet=True) 84 | 85 | def get_pchip_interpolation(pressure,q_adsorbed): 86 | """ Fits isotherm with shape preserving pchip interpolation 87 | 88 | Args: 89 | pressure: Array of relative pressure values 90 | q_adsorbed: Array of Nitrogen uptake values 91 | 92 | Returns: 93 | Pchip parameters 94 | """ 95 | return PchipInterpolator(pressure,q_adsorbed, axis =0, extrapolate=None) 96 | 97 | def isotherm_pchip_reconstruction(pressure, q_adsorbed): 98 | """ Fits isotherm with a pchip interpolation. Can use this to 99 | calculate BET area for difficult isotherms 100 | 101 | Args: pressure: Array of relative pressure values 102 | q_adsorbed: Array of Nitrogen uptake values 103 | 104 | Returns: Array of interpolated nitrogen uptake values 105 | 106 | """ 107 | ## x_range = np.linspace(pressure[0], pressure[len(pressure) -1 ], 500) 108 | ## y_pchip = pchip_interpolate(pressure,q_adsorbed,x_range, der=0, axis=0) 109 | 110 | ## pressure = x_range 111 | ## q_adsorbed = y_pchip 112 | 113 | ## New lines added from here 114 | ## Add new interpolated points using pchip interpolation while having the original data points in the list as well 115 | x_range = np.linspace(np.log10(pressure[0]), np.log10(pressure[len(pressure) -1 ]), len(pressure)) 116 | delta_x = abs(x_range[1] - x_range[0])/2 117 | for p in np.log10(pressure[1:-1]): 118 | to_be_deleted_indexes = [] 119 | index = np.searchsorted(x_range,p) 120 | if 0 <= index < len(x_range) and abs(p - x_range[index]) < delta_x: 121 | to_be_deleted_indexes.append(index) 122 | if 0 <= (index+1) < len(x_range) and abs(p - x_range[index+1]) < delta_x: 123 | to_be_deleted_indexes.append(index+1) 124 | if 0 <= (index-1) < len(x_range) and abs(p - x_range[index-1]) < delta_x: 125 | to_be_deleted_indexes.append(index-1) 126 | x_range = np.delete(x_range, to_be_deleted_indexes) 127 | x_range = np.append(x_range[1:-1], np.log10(pressure)) 128 | x_range.sort() 129 | x_range = np.power(10, x_range) 130 | 131 | y_pchip = pchip_interpolate(pressure,q_adsorbed,x_range, der=0, axis=0) 132 | 133 | pressure_new = x_range 134 | q_adsorbed_new = y_pchip 135 | 136 | return pressure_new, q_adsorbed_new 137 | 138 | def get_data_for_txt_aif(input_file): 139 | """ Read pressure and Nitrogen uptake data from file if the file extension is *.txt or *.aif. 140 | this function will be called in the get_data() function, and should not be called alone anywhere in the code 141 | as it might cause some errors. 142 | 143 | Args: 144 | input_file: String of the path to the input file 145 | 146 | Returns: 147 | Pressure and Quantity adsorbed. 148 | """ 149 | 150 | pressure = [] 151 | q_adsorbed = [] 152 | 153 | with open(input_file, 'r') as f: 154 | input_file_lines = f.readlines() 155 | 156 | if len(input_file_lines) == 0: 157 | pressure = np.array(pressure) 158 | q_adsorbed = np.array(q_adsorbed) 159 | return pressure, q_adsorbed 160 | 161 | index_start = 0 162 | index_stop = len(input_file_lines) - 1 163 | index_iter = 0 164 | first_loop_index = 0 165 | 166 | if input_file.find(".aif") != -1: 167 | for line in input_file_lines: 168 | if line.find("loop_") != -1 and first_loop_index == 0: 169 | first_loop_index = index_iter; 170 | if line.find("_adsorp_pressure") != -1: 171 | _adsorp_pressure_index = index_iter; 172 | if line.find("_adsorp_p0") != -1: 173 | _adsorp_p0_index = index_iter; 174 | if line.find("_adsorp_amount") != -1: 175 | index_start = index_iter 176 | if index_start > 0 and (line.find("loop_") != -1 or index_iter + 1 == len(input_file_lines)): 177 | index_stop = index_iter 178 | break 179 | index_iter += 1 180 | for index in range(index_start, index_stop + 1): 181 | val = [] 182 | for t in input_file_lines[index].split(): 183 | try: 184 | val.append(float(t)) 185 | except ValueError: 186 | pass 187 | if len(val) >= 3: 188 | pressure.append(val[_adsorp_pressure_index - first_loop_index - 1]/val[_adsorp_p0_index - first_loop_index - 1]) 189 | q_adsorbed.append(val[index_start - first_loop_index - 1]) 190 | else: 191 | delimiter = " " 192 | if input_file_lines[round(len(input_file_lines)/2)].find(",") != -1: 193 | delimiter = "," 194 | elif input_file_lines[round(len(input_file_lines)/2)].find(" ") != -1: 195 | delimiter = " " 196 | elif input_file_lines[round(len(input_file_lines)/2)].find("\t") != -1: 197 | delimiter = "\t" 198 | for line in input_file_lines: 199 | val = [] 200 | for t in line.split(delimiter): 201 | try: 202 | val.append(float(t)) 203 | except ValueError: 204 | pass 205 | if len(val) >= 2: 206 | pressure.append(val[0]) 207 | q_adsorbed.append(val[1]) 208 | if len(pressure) == 0 or len(q_adsorbed) == 0: 209 | print("You must provide a valid input file!") 210 | 211 | pressure = np.array(pressure) 212 | q_adsorbed = np.array(q_adsorbed) 213 | 214 | return pressure, q_adsorbed -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from betsi.gui import runbetsi 2 | 3 | runbetsi() # pylint: disable=no-value-for-parameter -------------------------------------------------------------------------------- /cli.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['cli.py'], 7 | pathex=['D:\\betsi-windows'], 8 | binaries=[], 9 | datas=[], 10 | hiddenimports=[], 11 | hookspath=[], 12 | runtime_hooks=[], 13 | excludes=['statsmodels'], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher, 17 | noarchive=False) 18 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\statsmodels", prefix="statsmodels") 19 | a.datas += Tree("D:\\anaconda\Lib\\site-packages\\numpy", prefix="numpy") 20 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\pandas", prefix="pandas") 21 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\scipy", prefix="scipy") 22 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\seaborn", prefix="seaborn") 23 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\pathlib2", prefix="pathlib") 24 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\matplotlib", prefix="matplotlib") 25 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\PyQt5", prefix="pyqt") 26 | a.datas += Tree("D:\\anaconda\\Lib\\site-packages\\patsy", prefix="patsy") 27 | 28 | 29 | pyz = PYZ(a.pure, a.zipped_data, 30 | cipher=block_cipher) 31 | exe = EXE(pyz, 32 | a.scripts, 33 | a.binaries, 34 | a.zipfiles, 35 | a.datas, 36 | [], 37 | name='cli', 38 | debug=False, 39 | bootloader_ignore_signals=False, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | runtime_tmpdir=None, 44 | console=True ) 45 | -------------------------------------------------------------------------------- /docs/images/BETSI-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/BETSI-logo.jpeg -------------------------------------------------------------------------------- /docs/images/a2ml_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/a2ml_logo.png -------------------------------------------------------------------------------- /docs/images/betsi_logo.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/betsi_logo.PNG -------------------------------------------------------------------------------- /docs/images/executables-banner.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/executables-banner.PNG -------------------------------------------------------------------------------- /docs/images/step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-1.png -------------------------------------------------------------------------------- /docs/images/step-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-10.png -------------------------------------------------------------------------------- /docs/images/step-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-11.png -------------------------------------------------------------------------------- /docs/images/step-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-12.png -------------------------------------------------------------------------------- /docs/images/step-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-13.png -------------------------------------------------------------------------------- /docs/images/step-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-14.png -------------------------------------------------------------------------------- /docs/images/step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-2.png -------------------------------------------------------------------------------- /docs/images/step-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-3.png -------------------------------------------------------------------------------- /docs/images/step-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-4.png -------------------------------------------------------------------------------- /docs/images/step-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-5.png -------------------------------------------------------------------------------- /docs/images/step-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-6.png -------------------------------------------------------------------------------- /docs/images/step-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-7.png -------------------------------------------------------------------------------- /docs/images/step-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-8.png -------------------------------------------------------------------------------- /docs/images/step-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakulrampal/betsi-gui/e68e3def588ef46116ce32677cdf51401f261701/docs/images/step-9.png -------------------------------------------------------------------------------- /executables/BETSI_v1.1.0_windows.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3258183805b8a5c3e82b984e353d6614e8fb48c1ffef32f825193577e5812be9 3 | size 183088922 4 | -------------------------------------------------------------------------------- /executables/mac_executables.zip: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:36e250cf9f445bdf2c3e7f76e0785f3e10fd16d76c90d78475b07844c2e6fae1 3 | size 144889483 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.19.3 2 | scipy==1.5.4 3 | matplotlib==3.2.2 4 | PyQt5==5.9.2 5 | pandas==1.1.5 6 | seaborn==0.11.0 7 | statsmodels==0.12.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | # The text of the README file 8 | README = (HERE / "README.md").read_text() 9 | 10 | with open("requirements.txt", "r") as fh: 11 | REQUIREMENTS = [line.strip().split(";")[0] for line in fh] 12 | 13 | # This call to setup() does all the work 14 | setup( 15 | name="betsi-gui", 16 | version="1.0.20", 17 | description="BET Surface Identification - a program that fully implements the rouquerol criteria", 18 | long_description=README, 19 | long_description_content_type="text/markdown", 20 | url="https://github.com/fairen-group/betsi-gui", 21 | author="James Rampersad, Johannes W.M. Osterrieth & Nakul Rampal", 22 | author_email="nr472@cam.ac.uk", 23 | license="MIT", 24 | classifiers=[ 25 | "License :: OSI Approved :: MIT License", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | ], 29 | python_requires=">=3.7.3", 30 | packages=find_packages(), 31 | include_package_data=True, 32 | install_requires= REQUIREMENTS, 33 | entry_points={ 34 | "console_scripts": [ 35 | "betsi=betsi.__main__:main", 36 | ] 37 | }, 38 | ) --------------------------------------------------------------------------------