├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── documentation.yml ├── .gitignore ├── .pylintrc ├── AUTHORS.rst ├── CITATION.cff ├── CONTRIBUTING.rst ├── GUI.png ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .nojekyll ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── source │ ├── generated_arrhenius │ ├── arrhenius.rst │ └── modules.rst │ ├── generated_impedance │ ├── impedance.rst │ └── modules.rst │ ├── generated_voltammetry │ ├── modules.rst │ └── voltammetry.rst │ └── usage │ ├── images │ ├── GUI_Arrhenius.png │ └── GUI_Impedance.png │ └── usage.rst ├── logo.ico ├── logo.png ├── madap ├── __init__.py ├── data_acquisition │ ├── __init__.py │ └── data_acquisition.py ├── echem │ ├── __init__.py │ ├── arrhenius │ │ ├── __init__.py │ │ ├── arrhenius.py │ │ └── arrhenius_plotting.py │ ├── e_impedance │ │ ├── __init__.py │ │ ├── e_impedance.py │ │ └── e_impedance_plotting.py │ ├── procedure.py │ └── voltammetry │ │ ├── __init__.py │ │ ├── voltammetry.py │ │ ├── voltammetry_CA.py │ │ ├── voltammetry_CP.py │ │ ├── voltammetry_CV.py │ │ └── voltammetry_plotting.py ├── logger │ ├── __init__.py │ └── logger.py ├── plotting │ ├── __init__.py │ ├── plotting.py │ └── styles │ │ ├── nature.mplstyle │ │ ├── no-latex.mplstyle │ │ └── science.mplstyle └── utils │ ├── __init__.py │ ├── gui_elements.py │ ├── suggested_circuits.py │ └── utils.py ├── madap_cli.py ├── madap_gui.py ├── pages-publish.sh ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * test version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out source code 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.9.12' 19 | 20 | - name: Install Dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install sphinx sphinx_rtd_theme numpy pandas matplotlib optuna scikit_learn seaborn tqdm ruptures impedance attrs==21.4.0 24 | 25 | - name: Build Documentation 26 | run: | 27 | cd docs 28 | make clean 29 | make html 30 | 31 | - name: Deploy to GitHub Pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./docs/build/html 36 | publish_branch: gh-pages 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # file history 93 | .history/ 94 | 95 | # Logs 96 | src/madap/logs/ 97 | 98 | # VSCopde 99 | .vscode/ 100 | 101 | # Generated results 102 | results/ 103 | electrolyte_figures/ 104 | 105 | #Datasets 106 | data/ 107 | data_information/ 108 | 109 | # cache 110 | figures_creation/ 111 | 112 | # Unsued testfiles 113 | test/gui_test/ 114 | test/testfile/procedure_test/ 115 | test/testfile/other test 116 | 117 | # Exclude pyinstaller files 118 | pyinstaller_file.bat 119 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=4 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=fixme, 64 | signature-differs, 65 | expression-not-assigned, 66 | eval-used, 67 | logging-fstring-interpolation, 68 | too-many-statements, 69 | broad-except, 70 | len-as-condition, 71 | function-redefined, 72 | bare-except, 73 | C0103, 74 | chained-comparison 75 | 76 | # Enable the message, report, category or checker with the given id(s). You can 77 | # either give multiple identifier separated by comma (,) or put this option 78 | # multiple time (only on the command line, not in the configuration file where 79 | # it should appear only once). See also the "--disable" option for examples. 80 | enable=c-extension-no-member 81 | 82 | 83 | [REPORTS] 84 | 85 | # Python expression which should return a score less than or equal to 10. You 86 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 87 | # which contain the number of messages in each category, as well as 'statement' 88 | # which is the total number of statements analyzed. This score is used by the 89 | # global evaluation report (RP0004). 90 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 91 | 92 | # Template used to display messages. This is a python new-style format string 93 | # used to format the message information. See doc for all details. 94 | #msg-template= 95 | 96 | # Set the output format. Available formats are text, parseable, colorized, json 97 | # and msvs (visual studio). You can also give a reporter class, e.g. 98 | # mypackage.mymodule.MyReporterClass. 99 | output-format=text 100 | 101 | # Tells whether to display a full report or only the messages. 102 | reports=no 103 | 104 | # Activate the evaluation score. 105 | score=yes 106 | 107 | 108 | [REFACTORING] 109 | 110 | # Maximum number of nested blocks for function / method body 111 | max-nested-blocks=5 112 | 113 | # Complete name of functions that never returns. When checking for 114 | # inconsistent-return-statements if a never returning function is called then 115 | # it will be considered as an explicit return statement and no message will be 116 | # printed. 117 | never-returning-functions=sys.exit 118 | 119 | 120 | [BASIC] 121 | 122 | # Naming style matching correct argument names. 123 | argument-naming-style=snake_case 124 | 125 | # Regular expression matching correct argument names. Overrides argument- 126 | # naming-style. 127 | #argument-rgx= 128 | 129 | # Naming style matching correct attribute names. 130 | attr-naming-style=snake_case 131 | 132 | # Regular expression matching correct attribute names. Overrides attr-naming- 133 | # style. 134 | #attr-rgx= 135 | 136 | # Bad variable names which should always be refused, separated by a comma. 137 | bad-names=foo, 138 | bar, 139 | baz, 140 | toto, 141 | tutu, 142 | tata 143 | 144 | # Bad variable names regexes, separated by a comma. If names match any regex, 145 | # they will always be refused 146 | bad-names-rgxs= 147 | 148 | # Naming style matching correct class attribute names. 149 | class-attribute-naming-style=any 150 | 151 | # Regular expression matching correct class attribute names. Overrides class- 152 | # attribute-naming-style. 153 | #class-attribute-rgx= 154 | 155 | # Naming style matching correct class names. 156 | class-naming-style=PascalCase 157 | 158 | # Regular expression matching correct class names. Overrides class-naming- 159 | # style. 160 | #class-rgx= 161 | 162 | # Naming style matching correct constant names. 163 | const-naming-style=UPPER_CASE 164 | 165 | # Regular expression matching correct constant names. Overrides const-naming- 166 | # style. 167 | #const-rgx= 168 | 169 | # Minimum line length for functions/classes that require docstrings, shorter 170 | # ones are exempt. 171 | docstring-min-length=10 172 | 173 | # Naming style matching correct function names. 174 | function-naming-style=snake_case 175 | 176 | # Regular expression matching correct function names. Overrides function- 177 | # naming-style. 178 | #function-rgx= 179 | 180 | # Good variable names which should always be accepted, separated by a comma. 181 | good-names=i, 182 | j, 183 | k, 184 | e, 185 | f, 186 | ex, 187 | Run, 188 | df, 189 | ax, 190 | linKK, 191 | _ 192 | 193 | # Good variable names regexes, separated by a comma. If names match any regex, 194 | # they will always be accepted 195 | good-names-rgxs= 196 | 197 | # Include a hint for the correct naming format with invalid-name. 198 | include-naming-hint=no 199 | 200 | # Naming style matching correct inline iteration names. 201 | inlinevar-naming-style=any 202 | 203 | # Regular expression matching correct inline iteration names. Overrides 204 | # inlinevar-naming-style. 205 | #inlinevar-rgx= 206 | 207 | # Naming style matching correct method names. 208 | method-naming-style=snake_case 209 | 210 | # Regular expression matching correct method names. Overrides method-naming- 211 | # style. 212 | #method-rgx= 213 | 214 | # Naming style matching correct module names. 215 | module-naming-style=snake_case 216 | 217 | # Regular expression matching correct module names. Overrides module-naming- 218 | # style. 219 | #module-rgx= 220 | 221 | # Colon-delimited sets of names that determine each other's naming style when 222 | # the name regexes allow several styles. 223 | name-group= 224 | 225 | # Regular expression which should only match function or class names that do 226 | # not require a docstring. 227 | no-docstring-rgx=^_ 228 | 229 | # List of decorators that produce properties, such as abc.abstractproperty. Add 230 | # to this list to register other decorators that produce valid properties. 231 | # These decorators are taken in consideration only for invalid-name. 232 | property-classes=abc.abstractproperty 233 | 234 | # Naming style matching correct variable names. 235 | variable-naming-style=snake_case 236 | 237 | # Regular expression matching correct variable names. Overrides variable- 238 | # naming-style. 239 | #variable-rgx= 240 | 241 | 242 | [FORMAT] 243 | 244 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 245 | expected-line-ending-format= 246 | 247 | # Regexp for a line that is allowed to be longer than the limit. 248 | ignore-long-lines=^\s*(# )??$ 249 | 250 | # Number of spaces of indent required inside a hanging or continued line. 251 | indent-after-paren=4 252 | 253 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 254 | # tab). 255 | indent-string=' ' 256 | 257 | # Maximum number of characters on a single line. 258 | max-line-length=150 259 | 260 | # Maximum number of lines in a module. 261 | max-module-lines=1000 262 | 263 | # Allow the body of a class to be on the same line as the declaration if body 264 | # contains single statement. 265 | single-line-class-stmt=no 266 | 267 | # Allow the body of an if to be on the same line as the test if there is no 268 | # else. 269 | single-line-if-stmt=no 270 | 271 | 272 | [LOGGING] 273 | 274 | # The type of string formatting that logging methods do. `old` means using % 275 | # formatting, `new` is for `{}` formatting. 276 | logging-format-style=new 277 | 278 | # Logging modules to check that the string format arguments are in logging 279 | # function parameter format. 280 | logging-modules=logging 281 | 282 | 283 | [MISCELLANEOUS] 284 | 285 | # List of note tags to take in consideration, separated by a comma. 286 | notes=FIXME, 287 | XXX, 288 | TODO 289 | 290 | # Regular expression of note tags to take in consideration. 291 | #notes-rgx= 292 | 293 | 294 | [SIMILARITIES] 295 | 296 | # Ignore comments when computing similarities. 297 | ignore-comments=yes 298 | 299 | # Ignore docstrings when computing similarities. 300 | ignore-docstrings=no 301 | 302 | # Ignore imports when computing similarities. 303 | ignore-imports=yes 304 | 305 | # Minimum lines number of a similarity. 306 | min-similarity-lines=10 307 | 308 | 309 | [SPELLING] 310 | 311 | # Limits count of emitted suggestions for spelling mistakes. 312 | max-spelling-suggestions=4 313 | 314 | # Spelling dictionary name. Available dictionaries: none. To make it work, 315 | # install the python-enchant package. 316 | spelling-dict= 317 | 318 | # List of comma separated words that should not be checked. 319 | spelling-ignore-words= 320 | 321 | # A path to a file that contains the private dictionary; one word per line. 322 | spelling-private-dict-file= 323 | 324 | # Tells whether to store unknown words to the private dictionary (see the 325 | # --spelling-private-dict-file option) instead of raising a message. 326 | spelling-store-unknown-words=no 327 | 328 | 329 | [STRING] 330 | 331 | # This flag controls whether inconsistent-quotes generates a warning when the 332 | # character used as a quote delimiter is used inconsistently within a module. 333 | check-quote-consistency=no 334 | 335 | # This flag controls whether the implicit-str-concat should generate a warning 336 | # on implicit string concatenation in sequences defined over several lines. 337 | check-str-concat-over-line-jumps=no 338 | 339 | 340 | [TYPECHECK] 341 | 342 | # List of decorators that produce context managers, such as 343 | # contextlib.contextmanager. Add to this list to register other decorators that 344 | # produce valid context managers. 345 | contextmanager-decorators=contextlib.contextmanager 346 | 347 | # List of members which are set dynamically and missed by pylint inference 348 | # system, and so shouldn't trigger E1101 when accessed. Python regular 349 | # expressions are accepted. 350 | generated-members= 351 | 352 | # Tells whether missing members accessed in mixin class should be ignored. A 353 | # mixin class is detected if its name ends with "mixin" (case insensitive). 354 | ignore-mixin-members=yes 355 | 356 | # Tells whether to warn about missing members when the owner of the attribute 357 | # is inferred to be None. 358 | ignore-none=yes 359 | 360 | # This flag controls whether pylint should warn about no-member and similar 361 | # checks whenever an opaque object is returned when inferring. The inference 362 | # can return multiple potential results while evaluating a Python object, but 363 | # some branches might not be evaluated, which results in partial inference. In 364 | # that case, it might be useful to still emit no-member and other checks for 365 | # the rest of the inferred objects. 366 | ignore-on-opaque-inference=yes 367 | 368 | # List of class names for which member attributes should not be checked (useful 369 | # for classes with dynamically set attributes). This supports the use of 370 | # qualified names. 371 | ignored-classes=optparse.Values,thread._local,_thread._local 372 | 373 | # List of module names for which member attributes should not be checked 374 | # (useful for modules/projects where namespaces are manipulated during runtime 375 | # and thus existing member attributes cannot be deduced by static analysis). It 376 | # supports qualified module names, as well as Unix pattern matching. 377 | ignored-modules= 378 | 379 | # Show a hint with possible names when a member name was not found. The aspect 380 | # of finding the hint is based on edit distance. 381 | missing-member-hint=yes 382 | 383 | # The minimum edit distance a name should have in order to be considered a 384 | # similar match for a missing member name. 385 | missing-member-hint-distance=1 386 | 387 | # The total number of similar names that should be taken in consideration when 388 | # showing a hint for a missing member. 389 | missing-member-max-choices=1 390 | 391 | # List of decorators that change the signature of a decorated function. 392 | signature-mutators= 393 | 394 | 395 | [VARIABLES] 396 | 397 | # List of additional names supposed to be defined in builtins. Remember that 398 | # you should avoid defining new builtins when possible. 399 | additional-builtins= 400 | 401 | # Tells whether unused global variables should be treated as a violation. 402 | allow-global-unused-variables=yes 403 | 404 | # List of strings which can identify a callback function by name. A callback 405 | # name must start or end with one of those strings. 406 | callbacks=cb_, 407 | _cb 408 | 409 | # A regular expression matching the name of dummy variables (i.e. expected to 410 | # not be used). 411 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 412 | 413 | # Argument names that match this expression will be ignored. Default to name 414 | # with leading underscore. 415 | ignored-argument-names=_.*|^ignored_|^unused_ 416 | 417 | # Tells whether we should check for unused import in __init__ files. 418 | init-import=no 419 | 420 | # List of qualified module names which can have objects that can redefine 421 | # builtins. 422 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 423 | 424 | 425 | [CLASSES] 426 | 427 | # List of method names used to declare (i.e. assign) instance attributes. 428 | defining-attr-methods=__init__, 429 | __new__, 430 | setUp, 431 | __post_init__ 432 | 433 | # List of member names, which should be excluded from the protected access 434 | # warning. 435 | exclude-protected=_asdict, 436 | _fields, 437 | _replace, 438 | _source, 439 | _make 440 | 441 | # List of valid names for the first argument in a class method. 442 | valid-classmethod-first-arg=cls 443 | 444 | # List of valid names for the first argument in a metaclass class method. 445 | valid-metaclass-classmethod-first-arg=cls 446 | 447 | 448 | [DESIGN] 449 | 450 | # Maximum number of arguments for function / method. 451 | max-args=25 452 | 453 | # Maximum number of attributes for a class (see R0902). 454 | max-attributes=25 455 | 456 | # Maximum number of boolean expressions in an if statement (see R0916). 457 | max-bool-expr=6 458 | 459 | # Maximum number of branch for function / method body. 460 | max-branches=15 461 | 462 | # Maximum number of locals for function / method body. 463 | max-locals=25 464 | 465 | # Maximum number of parents for a class (see R0901). 466 | max-parents=7 467 | 468 | # Maximum number of public methods for a class (see R0904). 469 | max-public-methods=20 470 | 471 | # Maximum number of return / yield for function / method body. 472 | max-returns=8 473 | 474 | # Maximum number of statements in function / method body. 475 | max-statements=50 476 | 477 | # Minimum number of public methods for a class (see R0903). 478 | min-public-methods=1 479 | 480 | 481 | [IMPORTS] 482 | 483 | # List of modules that can be imported at any level, not just the top level 484 | # one. 485 | allow-any-import-level= 486 | 487 | # Allow wildcard imports from modules that define __all__. 488 | allow-wildcard-with-all=no 489 | 490 | # Analyse import fallback blocks. This can be used to support both Python 2 and 491 | # 3 compatible code, which means that the block might have code that exists 492 | # only in one or another interpreter, leading to false positives when analysed. 493 | analyse-fallback-blocks=no 494 | 495 | # Deprecated modules which should not be used, separated by a comma. 496 | deprecated-modules=optparse,tkinter.tix 497 | 498 | # Create a graph of external dependencies in the given file (report RP0402 must 499 | # not be disabled). 500 | ext-import-graph= 501 | 502 | # Create a graph of every (i.e. internal and external) dependencies in the 503 | # given file (report RP0402 must not be disabled). 504 | import-graph= 505 | 506 | # Create a graph of internal dependencies in the given file (report RP0402 must 507 | # not be disabled). 508 | int-import-graph= 509 | 510 | # Force import order to recognize a module as part of the standard 511 | # compatibility libraries. 512 | known-standard-library= 513 | 514 | # Force import order to recognize a module as part of a third party library. 515 | known-third-party=enchant 516 | 517 | # Couples of modules and preferred modules, separated by a comma. 518 | preferred-modules= 519 | 520 | 521 | [EXCEPTIONS] 522 | 523 | # Exceptions that will emit a warning when being caught. Defaults to 524 | # "BaseException, Exception". 525 | overgeneral-exceptions=builtins.BaseException, 526 | builtins.Exception 527 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Fuzhan Rahmanian 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: Rahmanian 5 | given-names: Fuzahn 6 | orcid: https://orcid.org/0000-0003-3996-4213 7 | title: Modular and Autonomous Data Analysis Platform (MADAP)" 8 | version: 1.0.0 9 | doi: 10.5281/zenodo.7374383 10 | date-released: 2022-11-28 11 | url: "https://github.com/fuzhanrahmanian/MADAP" 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/fuzhanrahmanian/madap/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | MADAP could always use more documentation, whether as part of the 42 | official MADAP docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/fuzhanrahmanian/MADAP/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `MADAP` for local development. 61 | 62 | 1. Fork the `MADAP` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/MADAP.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv MADAP 70 | $ cd madap/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 madap tests 83 | $ python setup.py madap or pytest 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 106 | Releasing 107 | --------- 108 | 109 | To create a new release of Luna, update the verion number in setup.cfg and tag your release accordingly with git tag . Make sure to push your tag with: 110 | 111 | $ git push --tags 112 | 113 | Once your commit is merged, it will automatically be deployed. 114 | 115 | Docs 116 | ---- 117 | To also deploy the documentation if something has changed there, use the `update script `__ 118 | 119 | -------------------------------------------------------------------------------- /GUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/GUI.png -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 1.2.8 (2025-04-28) 6 | ------------------- 7 | * Fixed bug from merge conflict 8 | 9 | 1.2.7 (2025-04-24) 10 | ------------------- 11 | * Added support for no time data in CV 12 | * Fixed bug in CV for no time data and CLI 13 | 14 | 1.2.6 (2023-12-16) 15 | ------------------- 16 | * Fixed bug in CP for differential capacity plot 17 | * Handled None and Infinite values in CP plots 18 | * Fixed index in CV 19 | 20 | 21 | 1.2.5 (2023-12-15) 22 | ------------------- 23 | * Fixed bug in CA for zero reaction rate constant 24 | * Fixed bug in index handling for CA 25 | * Fixed bug in plot ticks 26 | 27 | 1.2.3 (2023-12-11) 28 | ------------------- 29 | * Include python 3.8 30 | 31 | 1.2.1 (2023-12-11) 32 | ------------------- 33 | * Added Ciclic Voltammetry to the functions 34 | * CA, CP and CV can be used with multiple plots to chose from 35 | * New and Imrpoved GUI 36 | * Fixed saving bug 37 | 38 | 1.1.0 (2023-08-07) 39 | ------------------- 40 | * Fixes issue with mismatch array length when positive imaginary data is given 41 | 42 | 0.11.0 (2022-10-16) 43 | ------------------- 44 | 45 | * Fixed bugs concering the package installation. 46 | * Improved the documentation. 47 | * Imrpoved the file structure. 48 | * madap_gui and madap_cli are now in the same package and can be used as standalone scripts/commands. 49 | 50 | 0.10.0 (2022-10-03) 51 | ------------------- 52 | 53 | * Updated support of the python versions 54 | 55 | 0.9.0 (2022-10-02) 56 | ------------------ 57 | 58 | * Update documentation 59 | 60 | 0.8.0 (2022-10-02) 61 | ------------------ 62 | 63 | * First release on PyPI. 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, Fuzhan Rahmanian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.sh 3 | include *.txt 4 | include LICENSE 5 | recursive-include docs *.bat 6 | recursive-include docs *.nojekyll 7 | recursive-include docs *.py 8 | recursive-include docs *.rst 9 | recursive-include docs Makefile 10 | recursive-include madap *.mplstyle 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | MADAP 4 | ~~~~~ 5 | 6 | .. image:: https://github.com/fuzhanrahmanian/MADAP/blob/master/logo.png?raw=true 7 | :align: center 8 | :width: 240px 9 | 10 | Modular and Autonomous Data Analysis Platform (MADAP) is a 11 | well-documented python package which can be used for electrochmeical 12 | data analysis. 13 | 14 | This package consists of 3 main classes for analysis: 15 | 16 | - Voltammetry 17 | - Impedance spectroscopy 18 | - Arrhenius 19 | 20 | This package allows user to upload any common file format of data and 21 | the select the data of choice. The user can use to scientifically plot 22 | and get correspondence analysis from each procedure (i.e. by calling 23 | “eis_analysis” , Nyquist, bode as well as the correspondence equivalent 24 | circuit and its parameters will be drawn). This package can be installed 25 | via pip/conda and can be utilized with a GUI, command line or just 26 | directly importing the module in a python script. 27 | 28 | Documentation 29 | ~~~~~~~~~~~~~ 30 | 31 | A documentation for the implementation and use of MADAP can be found 32 | `here `__ 33 | 34 | 35 | Installation 36 | ~~~~~~~~~~~~ 37 | 38 | MADAP can be installed via pip: 39 | 40 | .. code:: bash 41 | 42 | pip install MADAP 43 | 44 | 45 | Usage 46 | ~~~~~ 47 | 48 | A brief tutorial video of the basic of MADAP usage can found `here `_. 49 | 50 | MADAP can be used in a python script as follows: 51 | 52 | .. code:: python 53 | 54 | from madap.echem.arrhenius import arrhenius 55 | from madap.echem.e_impedance import e_impedance 56 | from madap.data_acquisition import data_acquisition as da 57 | 58 | 59 | # Load the data 60 | data = da.acquire_data('data.csv') 61 | # Define the desired plots for Arrhenius analysis 62 | plots_arr = ["arrhenius", "arrhenius_fit"] 63 | # Define the desired plots for impedance analysis 64 | plots_eis = ["nyquist", "nyquist_fit", "bode", "residual"] 65 | # Define a save location# 66 | save_dir = "/results" 67 | 68 | ### Arrhenius 69 | # Instantiate the Arrhenius class for analysis (column names do not have to match exactly, this is just an example) 70 | Arr = arrhenius.Arrhenius(da.format_data(data["temperature"], da.format_data(data["conductivity"]))) 71 | # Perform analysis and plotting 72 | Arr.perform_all_actions(save_dir, plots = plots_arr) 73 | 74 | ### Impedance 75 | # Initialize the Impedance class for analysis (column names do not have to match exactly, this is just an example) 76 | Im = e_impedance.EImpedance(da.format_data(data["freq"]), da.format_data(data["real"]), da.format_data(data["img"])) 77 | # Initialis the EIS procedure. The initial value is the initial guess for the equivalent circuit (can also be left empty) 78 | Eis = e_impedance.EIS(Im, suggested_circuit = "R0-p(R1,CPE1)",initial_value =[860, 3e+5, 1e-09, 0.90]) 79 | # Analyze the data 80 | Eis.perform_all_actions(save_dir, plots = plots_eis) 81 | 82 | # More usages and options can be found in the documentation. 83 | 84 | MADAP can also be used via command line: 85 | 86 | .. code:: bash 87 | 88 | python -m madap_cli --file --procedure --results --header_list --plot 89 | 90 | MADAP can also be used via a GUI: 91 | 92 | .. code:: bash 93 | 94 | python -m madap_gui 95 | 96 | .. image:: https://github.com/fuzhanrahmanian/MADAP/raw/master/GUI.png 97 | :align: center 98 | :width: 800px 99 | 100 | 101 | License 102 | ~~~~~~~ 103 | 104 | MADAP is licensed under the MIT license. See the LICENSE file for more 105 | details. 106 | 107 | 108 | Citation 109 | ~~~~~~~~ 110 | 111 | If you use MADAP in your research, please cite this GitHub repository https://github.com/fuzhanrahmanian/MADAP. 112 | 113 | .. image:: https://zenodo.org/badge/494354435.svg 114 | :target: https://zenodo.org/badge/latestdoi/494354435 115 | 116 | Please also cite the following work: 117 | `[Rahmanian2023] `_ Rahmanian, F., Vogler, M., Wölke, C. et al. "Conductivity experiments for electrolyte formulations and their automated analysis." Sci Data 10, 43 (2023). 118 | 119 | References 120 | ~~~~~~~~~~ 121 | 122 | This package is based relies on the following packages and papers: 123 | - Impedance GitHub repository by Matthew D. Murbach and Brian Gerwe and Neal Dawson-Elli and Lok-kun Tsui: `link `__ 124 | - A Method for Improving the Robustness of linear Kramers-Kronig Validity Tests DOI: https://doi.org/10.1016/j.electacta.2014.01.034 125 | 126 | Acknowledgement 127 | ~~~~~~~~~~~~~~~ 128 | 129 | This project has received funding from the European Union’s [Horizon 2020 research and innovation programme](https://ec.europa.eu/programmes/horizon2020/en) under grant agreement [No 957189](https://cordis.europa.eu/project/id/957189). The project is part of BATTERY 2030+, the large-scale European research initiative for inventing the sustainable batteries of the future. 130 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/docs/.nojekyll -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS = 7 | SPHINXBUILD = python -msphinx 8 | SPHINXPROJ = arcana 9 | SOURCEDIR = . 10 | BUILDDIR = build 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath("..")) 16 | sys.path.insert(0, os.path.abspath(os.path.join("..","madap", "logger"))) 17 | sys.path.insert(0, os.path.abspath(os.path.join("..","madap", "echem"))) 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'MADAP' 21 | copyright = '2022, Fuzhan Rahmanian' 22 | author = 'Fuzhan Rahmanian' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.napoleon", 35 | "sphinx.ext.autosummary", 36 | "sphinx.ext.autodoc", 37 | "sphinx_rtd_theme" 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = [] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = 'sphinx_rtd_theme' 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] 60 | 61 | # Napoleon settings 62 | napoleon_google_docstring = True 63 | 64 | # Automatically generate autosummary pages 65 | autosummary_generate = True -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. MADAP documentation master file, created by 2 | sphinx-quickstart on Mon Jul 25 21:56:34 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to MADAP's documentation! 7 | ================================= 8 | 9 | This documentation is generated from the MADAP source code. It is 10 | intended to be used by developers and users of MADAP. 11 | 12 | 13 | What is MADAP? 14 | -------------- 15 | 16 | This is MADAP, a software package for the analysis of electrochemical data. 17 | This package consists of 3 main classes for analysis: 18 | 19 | - Voltammetry 20 | - Impedance spectroscopy 21 | - Arrhenius 22 | 23 | This package allows user to upload any common file format of data and 24 | the select the data of choice. The user can use to scientifically plot 25 | and get correspondence analysis from each procedure (i.e. by calling 26 | “eis_analysis” , Nyquist, bode as well as the correspondence equivalent 27 | circuit and its parameters will be drawn). This package can be installed 28 | via pip/conda and can be utilized with a GUI, command line or just 29 | directly importing the module in a python script. For more information, 30 | checkout the `pypi `__ package page and 31 | the `github `__ page where you 32 | can find the source code and releases. 33 | 34 | 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | :caption: Usage: 39 | 40 | source/usage/usage 41 | 42 | .. toctree:: 43 | :maxdepth: 6 44 | :caption: Packages: 45 | 46 | source/generated_arrhenius/modules 47 | source/generated_impedance/modules 48 | source/generated_voltammetry/modules 49 | 50 | Indices and tables 51 | ================== 52 | 53 | * :ref:`genindex` 54 | * :ref:`modindex` 55 | * :ref:`search` 56 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | set SPHINXPROJ=madap 13 | 14 | %SPHINXBUILD% >NUL 2>NUL 15 | if errorlevel 9009 ( 16 | echo. 17 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 18 | echo.installed, then set the SPHINXBUILD environment variable to point 19 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 20 | echo.may add the Sphinx directory to PATH. 21 | echo. 22 | echo.If you don't have Sphinx installed, grab it from 23 | echo.https://www.sphinx-doc.org/ 24 | exit /b 1 25 | ) 26 | 27 | if "%1" == "" goto help 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/generated_arrhenius/arrhenius.rst: -------------------------------------------------------------------------------- 1 | Arrhenius package 2 | ================= 3 | 4 | arrhenius.arrhenius 5 | -------------------------- 6 | 7 | .. automodule:: arrhenius.arrhenius 8 | :members: 9 | :show-inheritance: 10 | 11 | arrhenius.arrhenius\_plotting 12 | ------------------------------------ 13 | 14 | .. automodule:: arrhenius.arrhenius_plotting 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/generated_arrhenius/modules.rst: -------------------------------------------------------------------------------- 1 | Arrhenius 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | arrhenius 8 | -------------------------------------------------------------------------------- /docs/source/generated_impedance/impedance.rst: -------------------------------------------------------------------------------- 1 | Impedance package 2 | ================= 3 | 4 | e\_impedance.e\_impedance 5 | -------------------------- 6 | 7 | .. automodule:: e_impedance.e_impedance 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | e\_impedance.e\_impedance\_plotting 13 | ------------------------------------ 14 | 15 | .. automodule:: e_impedance.e_impedance_plotting 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/generated_impedance/modules.rst: -------------------------------------------------------------------------------- 1 | Impedance 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | impedance 8 | -------------------------------------------------------------------------------- /docs/source/generated_voltammetry/modules.rst: -------------------------------------------------------------------------------- 1 | Voltammetry 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | voltammetry -------------------------------------------------------------------------------- /docs/source/generated_voltammetry/voltammetry.rst: -------------------------------------------------------------------------------- 1 | Voltammetry package 2 | =================== 3 | 4 | voltammetry.voltammetry 5 | -------------------------- 6 | 7 | .. automodule:: voltammetry.voltammetry 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | voltammetry.voltammetry\_CA 13 | ------------------------------- 14 | 15 | .. automodule:: voltammetry.voltammetry_CA 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | 21 | voltammetry.voltammetry\_CP 22 | ------------------------------- 23 | 24 | .. automodule:: voltammetry.voltammetry_CP 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | :noindex: 29 | 30 | 31 | voltammetry.voltammetry\_CV 32 | ------------------------------- 33 | 34 | .. automodule:: voltammetry.voltammetry_CV 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | voltammetry.voltammetry\_plotting 41 | ------------------------------------ 42 | 43 | .. automodule:: voltammetry.voltammetry_plotting 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/usage/images/GUI_Arrhenius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/docs/source/usage/images/GUI_Arrhenius.png -------------------------------------------------------------------------------- /docs/source/usage/images/GUI_Impedance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/docs/source/usage/images/GUI_Impedance.png -------------------------------------------------------------------------------- /docs/source/usage/usage.rst: -------------------------------------------------------------------------------- 1 | .. include:: ./images 2 | 3 | Usage 4 | ===== 5 | 6 | MADAP as a python package 7 | ------------------------- 8 | 9 | The most simple usage of MADAP for Arrhenius and Impedance analysis is as follows: 10 | 11 | .. code:: python 12 | 13 | from madap.echem.arrhenius import arrhenius 14 | from madap.echem.e_impedance import e_impedance 15 | from madap.data_acquisition import data_acquisition as da 16 | 17 | 18 | # Load the data 19 | data = da.acquire_data('data.csv') 20 | # Define the desired plots for Arrhenius analysis 21 | plots_arr = ["arrhenius", "arrhenius_fit"] 22 | # Define the desired plots for impedance analysis 23 | plots_eis = ["nyquist", "nyquist_fit", "bode", "residual"] 24 | # Define a save location# 25 | save_dir = "/results" 26 | 27 | ### Arrhenius 28 | # Instantiate the Arrhenius class for analysis (column names do not have to match exactly, this is just an example) 29 | Arr = arrhenius.Arrhenius(da.format_data(data["temperature"], da.format_data(data["conductivity"]))) 30 | # Perform analysis and plotting 31 | Arr.perform_all_actions(save_dir, plots = plots_arr) 32 | 33 | ### Impedance 34 | # Initialize the Impedance class for analysis (column names do not have to match exactly, this is just an example) 35 | Im = e_impedance.EImpedance(da.format_data(data["freq"]), da.format_data(data["real"]), da.format_data(data["img"])) 36 | # Initialis the EIS procedure. The initial value is the initial guess for the equivalent circuit (can also be left empty) 37 | Eis = e_impedance.EIS(Im, suggested_circuit = "R0-p(R1,CPE1)",initial_value =[860, 3e+5, 1e-09, 0.90]) 38 | # Analyze the data 39 | Eis.perform_all_actions(save_dir, plots = plots_eis) 40 | 41 | # More usages and options can be found in the documentation. 42 | 43 | 44 | There are more options i.e. for impedance class, outlier detection can be used with respective quantile parameters. 45 | Furthermore, different plot settings 46 | and circuit options can be used as well. For more information, please refer to the documentation. 47 | It is possible to expand MADAP with custom analysis procedures. The implementation steps are as follows: 48 | 49 | 1. Create a new class in the respective folder (echem) and inherit from the base class (procedure.py). 50 | 51 | .. code:: python 52 | 53 | from madap.procedure import EChemProcedure 54 | from madap.logger import Logger 55 | 56 | class MyProcedure(EChemProcedure): 57 | def __init__(self, data, **kwargs): 58 | super().__init__(data, **kwargs) 59 | 60 | 2. Implement the abstract methods (see documentation for more information). 61 | 62 | .. code:: python 63 | 64 | def analyze(self): 65 | # Perform analysis 66 | pass 67 | 68 | def plot(self, save_dir, **kwargs): 69 | # Plot results 70 | pass 71 | 72 | def save_data(self, save_dir): 73 | # Save results 74 | pass 75 | 76 | def perform_all_actions(self, save_dir, **kwargs): 77 | # Perform all actions 78 | pass 79 | 80 | 3. Create a new class in the respective folder (echem) for the plotting and inherit from the base class (plotting.py). 81 | 82 | .. code:: python 83 | 84 | from madap.plotting.plotting import Plots 85 | from madap.logger import Logger 86 | 87 | class MyPlotting(Plots): 88 | def __init__(self, data, **kwargs): 89 | super().__init__(data, **kwargs) 90 | 91 | MADAP as a command line tool 92 | ---------------------------- 93 | 94 | MADAP can also be used via command line: 95 | 96 | .. code:: bash 97 | 98 | madap_cli --file --procedure --results --header_list --plot 99 | 100 | Note that when using the argument "impedance" for the option --procedure, another option --impedance_procedure is necessary to be specified. 101 | At the moment, the only possible impedance procedure is "EIS". Mottschotcky and Lissajous are not yet implemented. 102 | 103 | The possible EIS arguments include: 104 | 105 | * :code:`--plots` (e.g: ["nyquist", "nyquist_fit", "bode", "residual"]) 106 | * :code:`--voltage` (e.g: 0.1) 107 | * :code:`--cell_constant` (e.g: 0.5) 108 | * :code:`--suggested_circuits` (e.g: "R0-p(R1,CPE1)"). This can be left empty. Madap will attempt to find the best circuit accordingly. 109 | * :code:`--initial_values` (e.g: [860, 3e+5, 1e-09, 0.90]). This can be left empty. Madap will attempt to find the best initial value accordingly. 110 | 111 | MADAP as a GUI 112 | -------------- 113 | 114 | MADAP can also be used via a GUI: 115 | 116 | .. code:: bash 117 | 118 | madap_gui 119 | 120 | An example of the GUI for impedance is shown below: 121 | 122 | .. image:: images/GUI_Impedance.png 123 | :width: 600 124 | :align: center 125 | 126 | An example of the GUI for Arrhenius is shown below: 127 | 128 | .. image:: images/GUI_Arrhenius.png 129 | :width: 600 130 | :align: center 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/logo.ico -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/logo.png -------------------------------------------------------------------------------- /madap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/__init__.py -------------------------------------------------------------------------------- /madap/data_acquisition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/data_acquisition/__init__.py -------------------------------------------------------------------------------- /madap/data_acquisition/data_acquisition.py: -------------------------------------------------------------------------------- 1 | """This module is responsible for handaling the data acquisition and data cleaning into MADAP""" 2 | import os 3 | import pandas as pd 4 | import numpy as np 5 | 6 | from madap.logger import logger 7 | 8 | 9 | log = logger.get_logger("data_acquisition") 10 | EXTENSIONS = [".txt", ".csv", ".json", ".xlsx", ".hdf5", ".h5", ".pkl"] 11 | 12 | def acquire_data(data_path): 13 | """Acquire data from a given file 14 | 15 | Args: 16 | data_path (str): The path to the data 17 | 18 | Returns: 19 | Pandas DataFrame: Dataframe with extracted data 20 | """ 21 | _ , extension = os.path.splitext(data_path) 22 | 23 | log.info(f"Importing {extension} file.") 24 | 25 | if extension not in EXTENSIONS: 26 | log.error(f"Datatype not supported. Supported datatypes are: {EXTENSIONS}") 27 | raise ValueError("Datatype not supported") 28 | 29 | if extension in (".csv", ".txt"): 30 | try: 31 | df = pd.read_csv(data_path, sep=None, engine="python") 32 | except: 33 | df = pd.read_csv(data_path, sep=";", engine="python") 34 | 35 | if extension == ".xlsx": 36 | df = pd.read_excel(data_path) 37 | if extension == ".json": 38 | df = pd.read_json(data_path) 39 | if extension in (".hdf5", ".h5"): 40 | df = pd.read_hdf(data_path) 41 | if extension == ".pkl": 42 | df = pd.read_pickle(data_path) 43 | 44 | return df 45 | 46 | 47 | def format_data(data): 48 | """Convert the given data to the array of float 49 | 50 | Args: 51 | data (_type_): Given data 52 | 53 | Returns: 54 | A readable format of data for analysis 55 | """ 56 | if not data is None: 57 | if isinstance(data, list): 58 | data = np.array(data, dtype=np.float64) 59 | 60 | if not np.array_equal(data, data.astype(float)): 61 | data = data.astype(np.float) 62 | 63 | return data 64 | 65 | 66 | def select_data(data, selected_data:str): 67 | """ Function to subselect the data from the dataframe 68 | 69 | Args: 70 | data (DataFrame): The dataframe from which the data is to be selected 71 | select_data (str): The string containing the start and end row and column 72 | 73 | Returns: 74 | DataFrame: The subselected dataframe 75 | """ 76 | numbers = list(map(int, selected_data.split(","))) 77 | data = data.iloc[:, numbers[2]: numbers[3]].values.reshape(-1) 78 | 79 | # check datatype 80 | if not isinstance(data, np.ndarray): 81 | data = np.array(data) 82 | if isinstance(data[0], str): 83 | if len(data) == 1: 84 | data = np.array(eval(data[0])) 85 | else: 86 | for i, _ in enumerate(data): 87 | data[i] = eval(data[i]) 88 | 89 | return data 90 | 91 | 92 | def format_list(list_data): 93 | """ Format the selection plots into a list 94 | 95 | Args: 96 | plots (obj): The plots selected by the user 97 | 98 | Returns: 99 | list: The list of plots selected by the user 100 | # """ 101 | if isinstance(list_data, (int, str)): 102 | list_data = [list_data] 103 | if isinstance(list_data, tuple): 104 | list_data = list(list_data) 105 | return list_data 106 | 107 | 108 | def remove_outlier_specifying_quantile(df, columns, low_quantile = 0.05, high_quantile = 0.95): 109 | """removing the outliers from the data by specifying the quantile 110 | 111 | Args: 112 | df (dataframe): original dataframe 113 | columns (list): colummns for which the outliers are to be removed 114 | low_quantile (float): lower quantile 115 | high_quantile (float): upper quantile 116 | 117 | Returns: 118 | data: the cleaned dataframe 119 | """ 120 | # select the columns that needs to be studied for outliers 121 | detect_search = df[columns] 122 | # Check if the low_quantile and high_quantile are floats, if not convert them to float 123 | if not isinstance(low_quantile, float): 124 | low_quantile = float(low_quantile) 125 | if not isinstance(high_quantile, float): 126 | high_quantile = float(high_quantile) 127 | # get the lower quantile of the corresponding columns 128 | q_low = detect_search.quantile(low_quantile) 129 | # get the upper quantile of the corresponding columns 130 | q_high = detect_search.quantile(high_quantile) 131 | # detect the outliiers in the data and replace those values with nan 132 | detect_search = detect_search[(detect_search < q_high) & (detect_search > q_low)] 133 | # find the index of the outliers 134 | nan_indices = [*set(np.where(np.asanyarray(np.isnan(detect_search)))[0].tolist())] 135 | log.info(f"The outliers are remove from dataset {detect_search} \ 136 | \n and the indeces are {nan_indices}.") 137 | 138 | return detect_search, nan_indices 139 | 140 | 141 | def remove_nan_rows(df, nan_indices): 142 | """ Remove the rows with nan values 143 | 144 | Args: 145 | df (DataFramo): The dataframe from which the rows are to be removed 146 | nan_indices (list): The list of indices of the rows to be removed 147 | 148 | Returns: 149 | DataFrame: The dataframe with the rows removed 150 | """ 151 | # check if the index of the outliers is present in the dataframe 152 | available_nan_indices = [i for i in nan_indices if i in df.index.values] 153 | # removing the rows with nan and reset their indeces 154 | df = df.drop(df.index[available_nan_indices]).reset_index() 155 | # delete the columns 156 | if "index" in df.columns: 157 | del df["index"] 158 | remove_unnamed_col(df) 159 | return df 160 | 161 | 162 | def remove_unnamed_col(df): 163 | """ Remove the unnamed columns from the dataframe 164 | 165 | Args: 166 | df (DataFrame): The dataframe from which unnamed cloums were removed 167 | """ 168 | if "Unnamed: 0" in df.columns: 169 | del df["Unnamed: 0"] 170 | -------------------------------------------------------------------------------- /madap/echem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/echem/__init__.py -------------------------------------------------------------------------------- /madap/echem/arrhenius/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/echem/arrhenius/__init__.py -------------------------------------------------------------------------------- /madap/echem/arrhenius/arrhenius.py: -------------------------------------------------------------------------------- 1 | """This module defines the Arrhenius procedure""" 2 | import os 3 | import math 4 | import numpy as np 5 | 6 | from attrs import define, field 7 | from attrs.setters import frozen 8 | from sklearn.linear_model import LinearRegression 9 | from sklearn.metrics import mean_squared_error 10 | 11 | from madap.logger import logger 12 | from madap.utils import utils 13 | from madap.echem.procedure import EChemProcedure 14 | from madap.echem.arrhenius.arrhenius_plotting import ArrheniusPlotting as aplt 15 | 16 | 17 | log = logger.get_logger("arrhenius") 18 | @define 19 | class Arrhenius(EChemProcedure): 20 | """ Class definition for visualization and analysis of Arrhenius equation. 21 | 22 | Attributes: 23 | temperatures (np.array): Array of temperatures in Celcius. 24 | conductivity (np.array): Array of conductivity in S/cm. 25 | gas_constant (float): Gas constant in [J/mol.K]. 26 | activation (float): Activation energy in [mJ/mol]. 27 | arrhenius_constant (float): Arrhenius constant in [S.cm⁻¹]. 28 | inverted_scale_temperatures (np.array): Array of temperatures in 1000/K. 29 | fit_score (float): R2 score of the fit. 30 | ln_conductivity_fit (np.array): Array of log conductivity fit. 31 | intercept (float): Intercept of the fit. 32 | coefficients (float): Slope of the fit. 33 | """ 34 | temperatures = field(on_setattr=frozen) 35 | conductivity = field(on_setattr=frozen) 36 | gas_constant = 8.314 # [J/mol.K] 37 | activation = None # [mJ/mol] 38 | arrhenius_constant = None # [S.cm⁻¹] 39 | inverted_scale_temperatures = None 40 | fit_score = None 41 | ln_conductivity_fit = None 42 | intercept = None 43 | coefficients = None 44 | mse_calc = None 45 | figure = None 46 | 47 | def analyze(self): 48 | """Analyze the data and fit the Arrhenius equation. 49 | """ 50 | # the linear fit formula: ln(sigma) = -E/RT + ln(A) 51 | self._cel_to_thousand_over_kelvin() 52 | 53 | reg = LinearRegression().fit(self.inverted_scale_temperatures.values.reshape(-1,1), self._log_conductivity()) 54 | self.fit_score = reg.score(self.inverted_scale_temperatures.values.reshape(-1,1), self._log_conductivity()) 55 | self.coefficients, self.intercept = reg.coef_[0], reg.intercept_ 56 | self.arrhenius_constant = math.exp(reg.intercept_) 57 | self.activation = reg.coef_[0]*(-self.gas_constant) 58 | self.ln_conductivity_fit = reg.predict(self.inverted_scale_temperatures.values.reshape(-1,1)) 59 | self.mse_calc = mean_squared_error(self._log_conductivity(), self.ln_conductivity_fit) 60 | 61 | log.info(f"Arrhenius constant is {round(self.arrhenius_constant,4)} [S.cm⁻¹] \ 62 | and activation is {round(self.activation,4)} [mJ/mol] \ 63 | with the score {self.fit_score}") 64 | 65 | 66 | def plot(self, save_dir:str, plots:list, optional_name:str = None): 67 | """Plot the raw data and/or the results of the Arrhenius analysis. 68 | 69 | Args: 70 | save_dir (str): Directory where the plots should be saved. 71 | plots (list): List of plots included in the analysis. 72 | optional_name (str): Optional name for the analysis. 73 | """ 74 | plot_dir = utils.create_dir(os.path.join(save_dir, "plots")) 75 | plot = aplt() 76 | 77 | fig, available_axes = plot.compose_arrhenius_subplot(plots=plots) 78 | for sub_ax, plot_name in zip(available_axes, plots): 79 | if plot_name == "arrhenius": 80 | plot.arrhenius(subplot_ax=sub_ax, temperatures= self.temperatures, 81 | log_conductivity= self._log_conductivity(), 82 | inverted_scale_temperatures = self.inverted_scale_temperatures) 83 | elif plot_name == "arrhenius_fit": 84 | plot.arrhenius_fit(subplot_ax = sub_ax, temperatures = self.temperatures, log_conductivity = self._log_conductivity(), 85 | inverted_scale_temperatures = self.inverted_scale_temperatures, 86 | #intercept = self.intercept, slope = self.coefficients, 87 | ln_conductivity_fit=self.ln_conductivity_fit, activation= self.activation, 88 | arrhenius_constant = self.arrhenius_constant, r2_score= self.fit_score) 89 | else: 90 | log.error("Arrhenius class does not have the selected plot.") 91 | fig.tight_layout() 92 | self.figure = fig 93 | name = utils.assemble_file_name(optional_name, self.__class__.__name__) if \ 94 | optional_name else utils.assemble_file_name(self.__class__.__name__) 95 | plot.save_plot(fig, plot_dir, name) 96 | 97 | def save_data(self, save_dir:str, optional_name:str = None): 98 | """Save the results of the analysis. 99 | 100 | Args: 101 | save_dir (str): Directory where the data should be saved. 102 | optional_name (str): Optional name for the analysis. 103 | """ 104 | save_dir = utils.create_dir(os.path.join(save_dir, "data")) 105 | # Save the fitted circuit 106 | 107 | name = utils.assemble_file_name(optional_name, self.__class__.__name__, "linear_fit.json") if \ 108 | optional_name else utils.assemble_file_name(self.__class__.__name__, "linear_fit.json") 109 | 110 | meta_data = {"R2_score": self.fit_score, "MSE": self.mse_calc, 'fit_slope': self.coefficients, "fit_intercept": self.intercept, 111 | "arr_constant [S.cm⁻¹]": self.arrhenius_constant, "activation [mJ/mol]": self.activation, 112 | "gas_constant [J/mol.K]": self.gas_constant} 113 | 114 | utils.save_data_as_json(directory=save_dir, name=name, data=meta_data) 115 | # Save the dataset 116 | data = utils.assemble_data_frame(**{"temperatures [\u00b0C]": self.temperatures, 117 | "conductivity [S/cm]": self.conductivity, 118 | "inverted_scale_temperatures [1000/K]": self.inverted_scale_temperatures, 119 | "log_conductivty [ln(S/cm)]": self._log_conductivity(), 120 | "log_conductivity_fit [ln(S/cm)]":self.ln_conductivity_fit}) 121 | 122 | data_name = utils.assemble_file_name(optional_name, self.__class__.__name__, "data.csv") if \ 123 | optional_name else utils.assemble_file_name(self.__class__.__name__, "data.csv") 124 | utils.save_data_as_csv(save_dir, data, data_name) 125 | 126 | 127 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str = None): 128 | """Wrapper function to perform all actions:\n 129 | - Analyze the data \n 130 | - Plot the data \n 131 | - Save the data 132 | 133 | Args: 134 | save_dir (str): Directory where the data should be saved. 135 | plots (list): plots to be included in the analysis. 136 | optional_name (str): Optional name for the analysis. 137 | """ 138 | self.analyze() 139 | self.plot(save_dir=save_dir, plots=plots, optional_name=optional_name) 140 | self.save_data(save_dir=save_dir, optional_name=optional_name) 141 | 142 | @property 143 | def figure(self): 144 | """Get the figure of the analysis. 145 | 146 | Returns: 147 | obj: matplotlib.figure.Figure 148 | """ 149 | return self._figure 150 | 151 | @figure.setter 152 | def figure(self, figure): 153 | """Setter for the figure attribute. 154 | 155 | Args: 156 | figure (obj): matplotlib.figure.Figure 157 | """ 158 | self._figure = figure 159 | 160 | def _log_conductivity(self): 161 | """Convert the conductivity to log scale. 162 | 163 | Returns: 164 | np.array: Log of the conductivity. 165 | """ 166 | return np.log(self.conductivity) 167 | 168 | def _cel_to_thousand_over_kelvin(self): 169 | """Convert the temperatures from Celcius to 1000/K. 170 | """ 171 | converted_temps = 1000/(self.temperatures + 273.15) 172 | self.inverted_scale_temperatures = converted_temps 173 | -------------------------------------------------------------------------------- /madap/echem/arrhenius/arrhenius_plotting.py: -------------------------------------------------------------------------------- 1 | """This module handles the plotting of the Arrhenius procedure""" 2 | from matplotlib import pyplot as plt 3 | 4 | from madap.logger import logger 5 | from madap.plotting.plotting import Plots 6 | 7 | 8 | log = logger.get_logger("arrhenius_plotting") 9 | 10 | class ArrheniusPlotting(Plots): 11 | """General Plotting class for Arrhenius method. 12 | 13 | Args: 14 | Plots (class): Parent class for plotting all methods. 15 | """ 16 | def __init__(self) -> None: 17 | super().__init__() 18 | self.plot_type = "Arrhenius" 19 | 20 | def arrhenius(self, subplot_ax, temperatures, log_conductivity, inverted_scale_temperatures, 21 | ax_sci_notation = None, scientific_limit: int = 3): 22 | """Defines the Arrhenius plot for raw data 23 | 24 | Args: 25 | subplot_ax (ax): Subplot axis 26 | temperatures (np.array): Array of temperatures 27 | log_conductivity (np.array): Array of log conductivity in [S/cm] 28 | inverted_scale_temperatures (np.array): Array of inverted scale temperatures in [1000/K] 29 | ax_sci_notation (bool, optional): If True, adds scientific notation to the axis. Defaults to None. 30 | scientific_limit (int, optional): If ax_sci_notation is True, defines the number of significant digits. Defaults to None. 31 | """ 32 | 33 | log.info("Creating Arrhenius plot") 34 | subplot_ax2=subplot_ax.twiny() 35 | self.plot_identity(subplot_ax, xlabel=r"$\frac{1000}{T}$ $[K^{-1}]$", 36 | ylabel=r"$\ln$($\sigma$) [$ln(S.cm^{-1})$]", 37 | ax_sci_notation=ax_sci_notation, 38 | scientific_limit=scientific_limit) 39 | 40 | subplot_ax2.scatter(temperatures, log_conductivity, s=10, c="#20a387ff", rasterized=True) 41 | self.plot_identity(subplot_ax2, xlabel="$T$ [\u00b0C]") 42 | self.set_xtick_for_two_axes(subplot_ax, subplot_ax2, [f"{ct:.1f}" for ct in inverted_scale_temperatures], temperatures, invert_axes=True) 43 | 44 | def arrhenius_fit(self, subplot_ax, temperatures, log_conductivity, inverted_scale_temperatures, 45 | ln_conductivity_fit, activation, arrhenius_constant, r2_score, 46 | ax_sci_notation = None, scientific_limit: int=3): 47 | """ 48 | Defines the Arrhenius plot for fitted data 49 | 50 | Args: 51 | subplot_ax (ax): Subplot axis 52 | temperatures (np.array): Array of temperatures 53 | log_conductivity (np.array): Array of log conductivity in [S/cm] 54 | inverted_scale_temperatures (np.array): Array of inverted scale temperatures in [1000/K] 55 | ln_conductivity_fit (np.array): Array of fitted log conductivity in [S/cm] 56 | activation (float): Activation energy in [mJ/mol] 57 | arrhenius_constant (float): Arrhenius constant in [mJ/mol] 58 | r2_score (float): R2 score of the fit 59 | ax_sci_notation (bool, optional): Weather or not scientific notation should be adopted for axis. Defaults to None. 60 | scientific_limit (int, optional): Number of significant digits. Defaults to 3. 61 | """ 62 | log.info("Creating a fitted Arrhenius plot") 63 | subplot_ax2=subplot_ax.twiny() 64 | subplot_ax.plot(inverted_scale_temperatures, ln_conductivity_fit, c="#453781ff", linewidth=1, linestyle="--", 65 | label=f" E = {activation:.2f} [mJ.mol⁻¹] \n A = {arrhenius_constant:.2f} [S.cm⁻¹] \n R² = {r2_score:.2f}") 66 | 67 | subplot_ax2.scatter(temperatures, log_conductivity, s=10, c="#20a387ff", rasterized=True) 68 | subplot_ax2.invert_xaxis() 69 | self.plot_identity(subplot_ax, xlabel=r"$\frac{1000}{T}$ $[K^{-1}]$", 70 | ylabel=r"$\ln$($\sigma$) [$ln(S.cm^{-1})$]", 71 | ax_sci_notation=ax_sci_notation, 72 | scientific_limit=scientific_limit) 73 | subplot_ax.legend(loc="upper right", fontsize=5.5) 74 | 75 | self.plot_identity(subplot_ax2, xlabel="$T$ [\u00b0C]") 76 | 77 | def compose_arrhenius_subplot(self, plots:list): 78 | """Creates subplots template for the Arrhenius plots. 79 | 80 | Args: 81 | plots (list): List of plots to be composed. 82 | 83 | Returns: 84 | fig, ax: Figure and axis of the subplot. 85 | """ 86 | plt.close('all') 87 | if len(plots)==1: 88 | fig = plt.figure(figsize=(3, 3)) 89 | spec = fig.add_gridspec(1, 1) 90 | ax = fig.add_subplot(spec[0,0]) 91 | return fig, [ax] 92 | 93 | if len(plots) == 2: 94 | fig = plt.figure(figsize=(6, 3)) 95 | spec = fig.add_gridspec(1, 2) 96 | ax1 = fig.add_subplot(spec[0, 0]) 97 | ax2 = fig.add_subplot(spec[0, 1]) 98 | return fig, [ax1, ax2] 99 | 100 | return Exception(f"Number of plots not supported for plot type {self.plot_type}") 101 | -------------------------------------------------------------------------------- /madap/echem/e_impedance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/echem/e_impedance/__init__.py -------------------------------------------------------------------------------- /madap/echem/e_impedance/e_impedance.py: -------------------------------------------------------------------------------- 1 | """Impedance Analysis module.""" 2 | # for fit the EIS experiments, impedance python package has been used. 3 | # RefMurbach, M., Gerwe, B., Dawson-Elli, N., & Tsui, L. (2020). impedance.py: 4 | # A Python package for electrochemical impedance analysis. 5 | # Journal of Open Source Software, 5(). https://doi.org/10.21105/joss.02349 6 | # cite for EIS fitting: https://github.com/ECSHackWeek/impedance.py 7 | import os 8 | import warnings 9 | import random 10 | 11 | import numpy as np 12 | from impedance import validation 13 | from impedance import preprocessing 14 | from impedance.models import circuits 15 | #import impedance.validation as validation 16 | #import impedance.preprocessing as preprocessing 17 | #import impedance.models.circuits as circuits 18 | 19 | from attrs import define, field 20 | from attrs.setters import frozen 21 | 22 | from madap.logger import logger 23 | from madap.utils import utils 24 | from madap.utils.suggested_circuits import suggested_circuits 25 | from madap.data_acquisition import data_acquisition as da 26 | from madap.echem.procedure import EChemProcedure 27 | from madap.echem.e_impedance.e_impedance_plotting import ImpedancePlotting as iplt 28 | 29 | warnings.warn("deprecated", DeprecationWarning) 30 | np.seterr(divide='ignore', invalid='ignore') 31 | # reference the impedance library 32 | log = logger.get_logger("impedance") 33 | 34 | 35 | @define 36 | class EImpedance: 37 | """Class for data definition that will be used during the Impedance analysis. 38 | The data includes the following: frequency, real impedance, imaginary impedance, 39 | and the phase shift. These attributes are all pandas.Series 40 | and will stay immutable except the phase shift. 41 | """ 42 | frequency = field(on_setattr=frozen) 43 | real_impedance = field(on_setattr=frozen) 44 | imaginary_impedance = field( on_setattr=frozen) 45 | phase_shift = field(default=None) 46 | 47 | def __repr__(self) -> str: 48 | """Returns a string representation of the object.""" 49 | return f"Impedance(frequency={self.frequency}, real_impedance={self.real_impedance}, \ 50 | imaginary_impedance={self.imaginary_impedance}, phase_shift={self.phase_shift})" 51 | 52 | class EIS(EChemProcedure): 53 | """General EIS class for the analysis of the EIS data. 54 | 55 | Args: 56 | EChemProcedure (cls): Parent abstract class 57 | """ 58 | def __init__(self, impedance, voltage: float = None, suggested_circuit: str = None, 59 | initial_value = None, max_rc_element: int = 50, 60 | cut_off: float = 0.85, fit_type: str = 'complex', 61 | val_low_freq: bool = True, cell_constant="n", max_iterations: int = 5, 62 | threshold_error:float = 0.009): 63 | """ Initialize the EIS class. 64 | 65 | Args: 66 | impedance (np.array): Impedance data containing real and imaginary part. 67 | voltage (float, optional): Voltage of the EIS measurement. Defaults to None. 68 | suggested_circuit (str, optional): String defining the suggested circuit. Defaults to None. 69 | initial_value (list, optional): Initial value of the circuit's element. Defaults to None. 70 | max_rc_element (int, optional): Maximum number of RC element to be used in the circuit. Defaults to 50. 71 | cut_off (float, optional): Cut off value of the fitted elements. Defaults to 0.85. 72 | fit_type (str, optional): Fit type. Defaults to 'complex'. 73 | val_low_freq (bool, optional): If True, the low frequency is used for the fit. Defaults to True. 74 | cell_constant (str, optional): Cell constant. Defaults to "n". 75 | max_iterations (int, optional): Maximum number of iterations for evaluating the accuarcy of fit. Defaults to 5. 76 | """ 77 | self.impedance = impedance 78 | self.voltage = voltage 79 | self.suggested_circuit = suggested_circuit 80 | self.initial_value = initial_value 81 | self.max_rc_element = max_rc_element 82 | self.cut_off = cut_off 83 | self.fit_type = fit_type 84 | self.val_low_freq = val_low_freq 85 | self.cell_constant = cell_constant 86 | self.max_iterations = max_iterations 87 | self.threshold_error = threshold_error 88 | self.conductivity = None 89 | self.rmse_calc = None 90 | self.num_rc_linkk = None 91 | self.eval_fit_linkk = None 92 | self.z_linkk = None 93 | self.res_real = None 94 | self.res_imag = None 95 | self.chi_val = None 96 | self.custom_circuit = None 97 | self.z_fit = None 98 | self.z_fit_clean = None 99 | self.impedance.phase_shift = self._calculate_phase_shift() if self.impedance.phase_shift is None else self.impedance.phase_shift 100 | self.figure = None 101 | self.pos_img_index = None 102 | 103 | 104 | # Schönleber, M. et al. A Method for Improving the Robustness of 105 | # linear Kramers-Kronig Validity Tests. 106 | # Electrochimica Acta 131, 20–27 (2014) doi: 10.1016/j.electacta.2014.01.034. 107 | def analyze(self): 108 | """General function for performing the impedance analysis. 109 | This will fit the circuit and calculate the conductivity if is applicable. 110 | """ 111 | f_circuit, z_circuit = np.array(self.impedance.frequency), \ 112 | np.array(self.impedance.real_impedance + 113 | 1j*self.impedance.imaginary_impedance) 114 | 115 | self.num_rc_linkk, self.eval_fit_linkk , self.z_linkk, \ 116 | self.res_real, self.res_imag = validation.linKK(f_circuit, z_circuit, 117 | c=self.cut_off, 118 | max_M=self.max_rc_element, 119 | fit_type=self.fit_type, 120 | add_cap=self.val_low_freq) 121 | self.chi_val = self._chi_calculation() 122 | log.info(f"Chi value from lin_KK method is {self.chi_val}") 123 | 124 | if any(x < 0 for x in self.impedance.imaginary_impedance): 125 | # Find the indexes of the positive values 126 | self.pos_img_index = np.where(self.impedance.imaginary_impedance > 0) 127 | f_circuit, z_circuit = preprocessing.ignoreBelowX(f_circuit, z_circuit) 128 | 129 | # if the user did not choose any circuit, some default suggestions will be applied. 130 | if (self.suggested_circuit and self.initial_value) is None: 131 | 132 | for guess_circuit, guess_value in suggested_circuits.items(): 133 | # apply some random guess 134 | guess_value = self._initialize_random_guess(guess_value, min(self.impedance.real_impedance)) 135 | # fit the data with random circuit and its randomly guessed elements 136 | custom_circuit_guess = circuits.CustomCircuit(initial_guess=guess_value, circuit=guess_circuit) 137 | 138 | try: 139 | custom_circuit_guess.fit(f_circuit, z_circuit) 140 | 141 | except RuntimeError as exp: 142 | log.error(exp) 143 | continue 144 | 145 | z_fit_guess = custom_circuit_guess.predict(f_circuit) 146 | rmse_guess = circuits.fitting.rmse(z_circuit, z_fit_guess) 147 | log.info(f"With the guessed circuit {guess_circuit} the RMSE error is {rmse_guess}") 148 | 149 | if self.rmse_calc is None: 150 | self.rmse_calc = rmse_guess 151 | 152 | if rmse_guess <= self.rmse_calc: 153 | self.rmse_calc = rmse_guess 154 | self.custom_circuit = custom_circuit_guess 155 | self.z_fit = z_fit_guess 156 | else: 157 | self.custom_circuit = circuits.CustomCircuit(initial_guess=self.initial_value, circuit=self.suggested_circuit) 158 | self.custom_circuit.fit(f_circuit, z_circuit) 159 | self.z_fit = self.custom_circuit.predict(f_circuit) 160 | self.rmse_calc = circuits.fitting.rmse(z_circuit, self.z_fit) 161 | log.info(f"With the guessed circuit {self.suggested_circuit} the RMSE error is {self.rmse_calc}") 162 | 163 | # re-evaluating the fit 164 | iteration = 0 165 | random_choice = ["add", "subtract"] 166 | # get the sum of root mean square of the truth values 167 | rms = np.linalg.norm(z_circuit) / np.sqrt(len(z_circuit)) 168 | while iteration < self.max_iterations: 169 | iteration += 1 170 | try: 171 | # fit again if the threshold error is not satisfied 172 | if self.rmse_calc > (self.threshold_error * rms): 173 | # get the initial values according to previous fit and its uncertainty 174 | random_selection = random.choice(random_choice) 175 | if random_selection == "add": 176 | initial_guess_trial = [i+j for i,j in zip(self.custom_circuit.parameters_.tolist(), self.custom_circuit.conf_.tolist())] 177 | else: 178 | initial_guess_trial = [i-j for i,j in zip(self.custom_circuit.parameters_.tolist(), self.custom_circuit.conf_.tolist())] 179 | # refit 180 | self.custom_circuit = circuits.CustomCircuit(initial_guess=initial_guess_trial,\ 181 | circuit=self.suggested_circuit).fit(f_circuit, z_circuit, method="trf") 182 | self.z_fit = self.custom_circuit.predict(f_circuit) 183 | self.rmse_calc = circuits.fitting.rmse(z_circuit, self.z_fit) 184 | log.info(f"With re-evaluating the circuit {self.suggested_circuit} the RMSE error is now {self.rmse_calc}") 185 | 186 | 187 | except Exception as e: 188 | log.error(e) 189 | continue 190 | 191 | if self.cell_constant: 192 | # calculate the ionic conductivity if cell constant is available 193 | self.conductivity = self._conductivity_calculation() 194 | 195 | 196 | def plot(self, save_dir, plots, optional_name: str = None): 197 | """Plot the results of the analysis. 198 | 199 | Args: 200 | save_dir (str): directory where to save the data 201 | plots (list): list of plot types to be plotted 202 | optional_name (str, optional): name of the file to be saved. Defaults to None. 203 | """ 204 | plot_dir = utils.create_dir(os.path.join(save_dir, "plots")) 205 | plot = iplt() 206 | fig, available_axes = plot.compose_eis_subplot(plots=plots) 207 | 208 | for sub_ax, plot_name in zip(available_axes, plots): 209 | if plot_name =="nyquist": 210 | sub_ax = available_axes[0] if ((len(plots)==4) or (len(plots)==3)) else sub_ax 211 | 212 | plot.nyquist(subplot_ax=sub_ax, frequency=self.impedance.frequency, real_impedance=self.impedance.real_impedance, 213 | imaginary_impedance=self.impedance.imaginary_impedance, 214 | ax_sci_notation='both', scientific_limit=3, scientific_label_colorbar=False, legend_label=True, 215 | voltage=self.voltage, norm_color=True) 216 | 217 | elif plot_name == "nyquist_fit": 218 | sub_ax = available_axes[2] if len(plots)==4 else \ 219 | (available_axes[0] if (len(plots)==3 and (("residual" in plots) and ("bode" in plots))) else \ 220 | (available_axes[1] if (len(plots)==3 and (("residual" in plots) or ("bode" in plots))) else sub_ax)) 221 | 222 | plot.nyquist_fit(subplot_ax=sub_ax, frequency=self.impedance.frequency, real_impedance=self.impedance.real_impedance, 223 | imaginary_impedance=self.impedance.imaginary_impedance, fitted_impedance=self.z_fit, chi=self.chi_val, 224 | suggested_circuit=self.custom_circuit.circuit, 225 | ax_sci_notation="both", scientific_limit=3, scientific_label_colorbar=False, legend_label=True, 226 | voltage=self.voltage, norm_color=True) 227 | 228 | elif plot_name == "bode": 229 | sub_ax = available_axes[1] if len(plots)==4 else (available_axes[1] if (len(plots)==3 and ("residual" in plots)) else 230 | (available_axes[2] if (len(plots)==3 and (not "residual" in plots)) else sub_ax)) 231 | plot.bode(subplot_ax=sub_ax, frequency=self.impedance.frequency, real_impedance=self.impedance.real_impedance, 232 | imaginary_impedance=self.impedance.imaginary_impedance, 233 | phase_shift=self.impedance.phase_shift, ax_sci_notation="y", scientific_limit=3, log_scale="x") 234 | 235 | elif plot_name == "residual": 236 | sub_ax = available_axes[3] if len(plots)==4 else (available_axes[2] if len(plots)==3 else sub_ax) 237 | plot.residual(subplot_ax=sub_ax, frequency=self.impedance.frequency, res_real=self.res_real, 238 | res_imag=self.res_imag, log_scale='x') 239 | 240 | else: 241 | log.error("EIS class does not have the selected plot.") 242 | continue 243 | 244 | fig.tight_layout() 245 | self.figure = fig 246 | name = utils.assemble_file_name(optional_name, self.__class__.__name__) if \ 247 | optional_name else utils.assemble_file_name(self.__class__.__name__) 248 | 249 | plot.save_plot(fig, plot_dir, name) 250 | 251 | def save_data(self, save_dir:str, optional_name:str = None): 252 | """Save the results of the analysis. 253 | 254 | Args: 255 | save_dir (str): Directory where the data should be saved. 256 | optional_name (None): Optional name for the data. 257 | """ 258 | save_dir = utils.create_dir(os.path.join(save_dir, "data")) 259 | # Save the fitted circuit 260 | name = utils.assemble_file_name(optional_name, self.__class__.__name__, "circuit.json") if \ 261 | optional_name else utils.assemble_file_name(self.__class__.__name__, "circuit.json") 262 | 263 | self.custom_circuit.save(os.path.join(save_dir, f"{name}")) 264 | added_data = {'rc_linKK': self.num_rc_linkk, "eval_fit_linKK": self.eval_fit_linkk, "RMSE_fit_error": self.rmse_calc, 265 | "conductivity [S/cm]": self.conductivity, "chi_square": self.chi_val} 266 | utils.append_to_save_data(directory=save_dir, added_data=added_data, name=name) 267 | # check if the positive index is available 268 | if self.pos_img_index is not None: 269 | try: 270 | self._insert_nan_values() 271 | except Exception as e: 272 | log.error(e) 273 | else: 274 | self.z_fit_clean = self.z_fit 275 | 276 | 277 | # Save the dataset 278 | data = utils.assemble_data_frame(**{"frequency [Hz]": self.impedance.frequency, 279 | "impedance [\u03a9]": self.impedance.real_impedance + 1j*self.impedance.imaginary_impedance, 280 | "fit_impedance [\u03a9]": self.z_fit_clean, "residual_real":self.res_real, "residual_imag":self.res_imag, 281 | "Z_linKK [\u03a9]": self.z_linkk}) 282 | data_name = utils.assemble_file_name(optional_name, self.__class__.__name__, "data.csv") if \ 283 | optional_name else utils.assemble_file_name(self.__class__.__name__, "data.csv") 284 | 285 | utils.save_data_as_csv(save_dir, data, data_name) 286 | 287 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str = None): 288 | """ Wrapper function for executing all action 289 | 290 | Args: 291 | save_dir (str): Directory where the data should be saved. 292 | plots (list): List of plot types to be plotted. 293 | """ 294 | self.analyze() 295 | self.plot(save_dir, plots, optional_name=optional_name) 296 | self.save_data(save_dir=save_dir, optional_name=optional_name) 297 | 298 | @property 299 | def figure(self): 300 | """Get the figure of the plot. 301 | 302 | Returns: 303 | obj: Figure object for e_impendance plot. 304 | """ 305 | return self._figure 306 | 307 | @figure.setter 308 | def figure(self, figure): 309 | """Set the figure of the plot. 310 | 311 | Args: 312 | figure (obj): Figure object for e_impendance plot. 313 | """ 314 | self._figure = figure 315 | 316 | def _chi_calculation(self): 317 | """ Calculate the chi value of the fit. 318 | 319 | Returns: 320 | float: chi square value of the fit 321 | """ 322 | return np.sum(np.square(self.res_imag) + np.square(self.res_real)) 323 | 324 | def _conductivity_calculation(self): 325 | """ Calculate the conductivity of the circuit. 326 | 327 | Returns: 328 | float: conductivity of the circuit 329 | """ 330 | # Check if cell_constant is a float, if not make it a float 331 | if isinstance(self.cell_constant, str): 332 | self.cell_constant = float(self.cell_constant) 333 | conductivity = self.cell_constant * (1/self.custom_circuit.parameters_[0]) 334 | log.info(f"The calculated conductivity is {conductivity} [S.cm⁻¹]") 335 | return conductivity 336 | 337 | def _calculate_phase_shift(self): 338 | """calculate phase shift 339 | 340 | Args: 341 | imaginary_impedance (class): imaginary impedance data 342 | real_impedance (class): real impedance data 343 | 344 | Returns: 345 | phase shift: calculated phase shift based on real and imaginary data 346 | """ 347 | 348 | phase_shift_in_rad = np.arctan(da.format_data( 349 | abs(-self.impedance.imaginary_impedance)/da.format_data(abs(self.impedance.real_impedance)))) 350 | return np.rad2deg(phase_shift_in_rad) 351 | 352 | def _initialize_random_guess(self, guess_value, guess_initial_resistance): 353 | """Initialize the random guess for the circuit's resistace. 354 | 355 | Args: 356 | guess_value (list): list of initial guessed value without any guess for resistance. 357 | guess_initial_resistance (float): Value for the initial resistance guess. 358 | """ 359 | guess_value = [guess_initial_resistance if element == 'x' else element for element in guess_value] 360 | guess_value = [guess_initial_resistance if element == 'y' else element for element in guess_value] 361 | guess_value = [guess_initial_resistance if element == 'z' else element for element in guess_value] 362 | guess_value = [guess_initial_resistance if element == 't' else element for element in guess_value] 363 | return guess_value 364 | 365 | def _insert_nan_values(self): 366 | """Insert nan values in the fit for the positive imaginary impedance values. 367 | """ 368 | self.z_fit_clean = np.empty(len(self.z_fit) + len(self.pos_img_index[0]), dtype=np.complex128) 369 | self.z_fit_clean[:] = np.nan 370 | j = 0 371 | for i in enumerate(self.z_fit_clean): 372 | if i in self.pos_img_index[0].tolist(): 373 | continue 374 | self.z_fit_clean[i] = self.z_fit[j] 375 | j += 1 376 | 377 | class Mottschotcky(EIS, EChemProcedure): 378 | """ Class for performing the Mottschotcky procedure. 379 | 380 | Args: 381 | EIS (class): General EIS class 382 | EChemProcedure (class): General abstract EChem Procedure class 383 | """ 384 | def __init__(self, impedance, suggested_circuit: str = None, initial_value=None, 385 | max_rc_element: int = 20, cut_off: float = 0.85, fit_type: str = 'complex', val_low_freq=True): 386 | EIS.__init__(impedance, suggested_circuit, initial_value, max_rc_element, cut_off, fit_type, val_low_freq) 387 | 388 | def analyze(self): 389 | pass 390 | 391 | def plot(self, save_dir:str, plots:list, optional_name:str): 392 | pass 393 | # TODO 394 | # nyquist 395 | # fit nyquist 396 | # bode 397 | # mott v vs 1/cp_2 398 | def save_data(self, save_dir:str, optional_name:str): 399 | pass 400 | 401 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str): 402 | pass 403 | 404 | class Lissajous(EChemProcedure): 405 | """ Class for performing the Lissajous procedure.""" 406 | def __init__(self) -> None: 407 | return None 408 | 409 | def analyze(self): 410 | pass 411 | 412 | def plot(self, save_dir:str, plots:list, optional_name:str): 413 | return None 414 | # TODO 415 | # legend is the frequency 416 | # i vs t 417 | # v vs t 418 | # i vs E 419 | def save_data(self, save_dir:str, optional_name:str): 420 | pass 421 | 422 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str): 423 | pass 424 | -------------------------------------------------------------------------------- /madap/echem/e_impedance/e_impedance_plotting.py: -------------------------------------------------------------------------------- 1 | """Impedance Plotting module.""" 2 | import numpy as np 3 | import matplotlib.colors as mcl 4 | from matplotlib import pyplot as plt 5 | from matplotlib.lines import Line2D 6 | 7 | from madap.logger import logger 8 | from madap.plotting.plotting import Plots 9 | 10 | 11 | log = logger.get_logger("impedance_plotting") 12 | 13 | class ImpedancePlotting(Plots): 14 | """General Plotting class for Impedance method. 15 | 16 | Args: 17 | Plots (class): Parent class for plotting all methods. 18 | """ 19 | def __init__(self) -> None: 20 | super().__init__() 21 | self.plot_type = "impedance" 22 | 23 | def nyquist(self, subplot_ax, frequency, real_impedance, imaginary_impedance, 24 | colorbar:bool=True, ax_sci_notation = None, scientific_limit=None, 25 | scientific_label_colorbar=False, legend_label=False,voltage:float = None, 26 | color_map:str="viridis", norm_color=None): 27 | """Defines the nyquist plot for raw data 28 | 29 | Args: 30 | subplot_ax (ax): Subplot axis 31 | frequency (np.array): Frequency array. 32 | real_impedance (np.array): Real impedance array. 33 | imaginary_impedance (np.array): Imaginary impedance array. 34 | colorbar (bool, optional): If True, adds a colorbar. Defaults to True. 35 | ax_sci_notation (bool, optional): If True, adds scientific notation to the axis. Defaults to None. 36 | scientific_limit (int, optional): If ax_sci_notation is True, defines the number of significant digits. Defaults to None. 37 | scientific_label_colorbar (bool, optional): If True, adds scientific notation to the colorbar. Defaults to False. 38 | legend_label (bool, optional): If True, adds a legend. Defaults to False. 39 | voltage (float, optional): Voltage of the circuit. Defaults to None. 40 | color_map (str, optional): Color map. Defaults to "viridis". 41 | norm_color (bool, optional): If True, normalizes the colorbar. Defaults to None. 42 | """ 43 | 44 | log.info("Creating Nyquist plot") 45 | norm = mcl.LogNorm(vmin=min(frequency), vmax=max(frequency)) if norm_color else None 46 | 47 | label = f"v = {voltage} [V]" if (voltage and voltage != "") else None 48 | nyquist_plot = subplot_ax.scatter(real_impedance, -imaginary_impedance, 49 | c=frequency, norm=norm, s=10, 50 | cmap=color_map, rasterized=True, 51 | label=label) 52 | 53 | nyquist_plot.set_clim(min(frequency), max(frequency)) 54 | 55 | self.plot_identity(subplot_ax, xlabel=r"Z' $[\Omega]$", ylabel=r"-Z'' $[\Omega]$", 56 | #x_lim=[0, max(real_impedance)+200], 57 | #y_lim=[0, max(-imaginary_impedance)+200], 58 | x_lim=[0, max(np.max(real_impedance), np.max(-imaginary_impedance)) + 200], 59 | y_lim=[0, max(np.max(real_impedance), np.max(-imaginary_impedance)) + 200], 60 | ax_sci_notation=ax_sci_notation, 61 | scientific_limit=scientific_limit) 62 | 63 | if (legend_label and voltage is not None): 64 | _, labels = subplot_ax.get_legend_handles_labels() 65 | new_handles= [Line2D([0], [0], marker='o', 66 | markerfacecolor="black", 67 | markeredgecolor="black", markersize=3, ls='')] 68 | subplot_ax.legend(new_handles, labels, loc="upper left",fontsize=5.5) 69 | 70 | if colorbar: 71 | self.add_colorbar(nyquist_plot, subplot_ax, scientific_label_colorbar, 72 | scientific_limit=scientific_limit, 73 | colorbar_label=r"f $[Hz]$") 74 | 75 | def bode(self, subplot_ax, frequency, real_impedance, imaginary_impedance, phase_shift, 76 | ax_sci_notation=None, scientific_limit=None, log_scale='x'): 77 | """Defines the bode plot for raw data 78 | 79 | Args: 80 | subplot_ax (ax): Subplot axis 81 | frequency (np.array): Frequency array. 82 | real_impedance (np.array): Real impedance array. 83 | imaginary_impedance (np.array): Imaginary impedance array. 84 | phase_shift (np.array): Phase shift array. 85 | ax_sci_notation (bool, optional): If True, adds scientific notation to the axis. Defaults to None. 86 | scientific_limit (int, optional): If ax_sci_notation is True, defines the number of significant digits. Defaults to None. 87 | log_scale (str, optional): If 'x', plots the x axis in log scale. Defaults to 'x'. 88 | """ 89 | log.info("Creating Bode plot") 90 | impedance_magnitude = np.sqrt(real_impedance**2 + imaginary_impedance**2) 91 | subplot_ax.scatter(frequency, impedance_magnitude, rasterized=True, s=10,c="#453781ff") 92 | subplot_ax.tick_params(axis="y", colors="#453781ff") 93 | ax2 = subplot_ax.twinx() 94 | ax2.scatter(frequency, -phase_shift, rasterized=True, s=10, c="#20a387ff") 95 | ax2.tick_params(axis="y", colors="#20a387ff") 96 | self.plot_identity(subplot_ax, xlabel=r"f $[Hz]$", ylabel=r"|Z| $[\Omega]$", 97 | ax_sci_notation=ax_sci_notation, 98 | scientific_limit=scientific_limit, 99 | log_scale=log_scale) 100 | self.plot_identity(ax2, ylabel="\u03c6 [\u00b0]", 101 | y_lim=[self.round_tenth(-phase_shift)[0], 102 | self.round_tenth(-phase_shift)[1]], 103 | log_scale=log_scale, step_size_y=10) 104 | 105 | def nyquist_fit(self, subplot_ax, frequency, real_impedance, imaginary_impedance, 106 | fitted_impedance, chi, suggested_circuit, colorbar:bool=True, 107 | ax_sci_notation = None, scientific_limit:int=3, 108 | scientific_label_colorbar=False, legend_label=False, 109 | voltage:float = None, color_map:str="viridis", 110 | norm_color=None): 111 | """Defines the nyquist plot for fitted data 112 | 113 | Args: 114 | subplot_ax (ax): Subplot axis 115 | frequency (np.array): Frequency array. 116 | real_impedance (np.array): Real impedance array. 117 | imaginary_impedance (np.array): Imaginary impedance array. 118 | fitted_impedance (np.array): Fitted impedance array. 119 | chi (float): Chi value of the fit. 120 | suggested_circuit (str): The string definition of the suggested circuit. 121 | colorbar (bool, optional): If True, adds a colorbar. Defaults to True. 122 | ax_sci_notation (bool, optional): If True, adds scientific notation to the axis. Defaults to None. 123 | scientific_limit (int, optional): If ax_sci_notation is True, defines the number of significant digits. Defaults to None. 124 | scientific_label_colorbar (bool, optional): If True, adds scientific notation to the colorbar. Defaults to False. 125 | legend_label (bool, optional): If True, adds a legend. Defaults to False. 126 | voltage (float, optional): Voltage of the circuit. Defaults to None. 127 | color_map (str, optional): Color map. Defaults to "viridis". 128 | norm_color (bool, optional): If True, normalizes the colorbar. Defaults to None. 129 | """ 130 | log.info("Creating a fitted Nyquist plot") 131 | nyquist_label = fr" v = {voltage} [V], \ 132 | $\chi_{{linKK}}^{2}$ = {np.format_float_scientific(chi, 3)}" \ 133 | if voltage else fr"$\chi^{2}$ = {np.format_float_scientific(chi, 3)}" 134 | 135 | norm = mcl.LogNorm(vmin=min(frequency), vmax=max(frequency)) if norm_color else None 136 | nyquist_plot = subplot_ax.scatter(real_impedance, -imaginary_impedance, 137 | c=frequency, norm=norm, s=10, 138 | cmap=color_map,rasterized=True, 139 | label=nyquist_label) 140 | 141 | subplot_ax.plot(np.real(fitted_impedance), -np.imag(fitted_impedance), 142 | label=f"Fitted with {suggested_circuit}", color="k") 143 | 144 | self.plot_identity(subplot_ax, xlabel=r"Z' $[\Omega]$", ylabel=r"-Z'' $[\Omega]$", 145 | # x_lim=[0, max(real_impedance) +200], 146 | # y_lim=[0, max(-imaginary_impedance) +200], 147 | x_lim=[0, max(np.max(real_impedance), np.max(-imaginary_impedance)) + 200], 148 | y_lim=[0, max(np.max(real_impedance), np.max(-imaginary_impedance)) + 200], 149 | ax_sci_notation=ax_sci_notation, 150 | scientific_limit=scientific_limit) 151 | 152 | if legend_label: 153 | _, labels = subplot_ax.get_legend_handles_labels() 154 | new_handles= [Line2D([0], [0], marker='o', 155 | markerfacecolor="black", 156 | markeredgecolor="black", markersize=3, ls=''), 157 | Line2D([0], [0], 158 | color="black", markersize=3, lw=1)] 159 | subplot_ax.legend(new_handles, labels, loc="upper left", fontsize=5.5) 160 | 161 | if colorbar: 162 | self.add_colorbar(nyquist_plot, subplot_ax, scientific_label_colorbar, 163 | scientific_limit=scientific_limit, 164 | colorbar_label=r"f $[Hz]$") 165 | self.ax = subplot_ax 166 | 167 | def residual(self, subplot_ax, frequency, res_real, res_imag, 168 | log_scale='x', ax_sci_notation = None, 169 | scientific_limit:int=3): 170 | """Defines the residual plot for raw data 171 | 172 | Args: 173 | subplot_ax (ax): Subplot axis. 174 | frequency (np.array): Frequency array. 175 | res_real (np.array): Real residual array. 176 | res_imag (np.array): Imaginary residual array. 177 | log_scale (str, optional): If 'x', plots the x axis in log scale. Defaults to 'x'. 178 | ax_sci_notation (bool, optional): If True, adds scientific notation to the axis. Defaults to None. 179 | scientific_limit (int, optional): If ax_sci_notation is True, defines the number of significant digits. Defaults to None. 180 | """ 181 | 182 | log.info("Creating a residual plot") 183 | subplot_ax.plot(frequency, res_real, label=r"$\Delta_{real}$", color="#453781ff", 184 | linestyle="--", marker='o') 185 | subplot_ax.plot(frequency, res_imag, label=r"$\Delta_{imaginary}$", color="#20a387ff", 186 | linestyle="--", marker='o') 187 | 188 | self.plot_identity(subplot_ax, xlabel=r"f $[Hz]$", ylabel=r"$\Delta$(residuals)", 189 | ax_sci_notation=ax_sci_notation, 190 | scientific_limit=scientific_limit, log_scale=log_scale) 191 | subplot_ax.legend(loc="lower right", fontsize=5.5) 192 | 193 | def compose_eis_subplot(self, plots:list): 194 | """Compose the EIS subplot 195 | 196 | Args: 197 | plots (list): List of plots to be composed. 198 | 199 | Returns: 200 | fig, ax: Figure and axis of the subplot. 201 | """ 202 | 203 | plt.close('all') 204 | if len(plots)==1: 205 | fig = plt.figure(figsize=(3.5,3)) 206 | spec = fig.add_gridspec(1, 1) 207 | ax = fig.add_subplot(spec[0,0]) 208 | return fig, [ax] 209 | 210 | if len(plots) == 2: 211 | fig_size = 9 if ("nyquist" and "nyquist_fit") in plots else 8.5 212 | fig = plt.figure(figsize=(fig_size, 4)) 213 | spec = fig.add_gridspec(1, 2) 214 | ax1 = fig.add_subplot(spec[0, 0]) 215 | ax2= fig.add_subplot(spec[0, 1]) 216 | return fig, [ax1, ax2] 217 | 218 | if len(plots) == 3: 219 | fig_size= 7 if ("nyquist" and "nyquist_fit" and "bode") in plots else 6.5 220 | fig = plt.figure(figsize=(fig_size, 5)) 221 | spec = fig.add_gridspec(2, 2) 222 | if "residual" in plots: 223 | ax1 = fig.add_subplot(spec[0, 0]) 224 | ax2 = fig.add_subplot(spec[0, 1]) 225 | ax3 = fig.add_subplot(spec[1, :]) 226 | else: 227 | ax1 = fig.add_subplot(spec[0, 0]) 228 | ax2 = fig.add_subplot(spec[1, 0]) 229 | ax3 = fig.add_subplot(spec[:, 1]) 230 | return fig, [ax1, ax2, ax3] 231 | 232 | if len(plots) == 4: 233 | fig = plt.figure(figsize=(7.5, 6)) 234 | spec = fig.add_gridspec(2, 2) 235 | ax1 = fig.add_subplot(spec[0, 0]) 236 | ax2= fig.add_subplot(spec[0, 1]) 237 | ax3 = fig.add_subplot(spec[1, 0]) 238 | ax4 = fig.add_subplot(spec[1, 1]) 239 | return fig, [ax1, ax2, ax3, ax4] 240 | 241 | if len(plots) == 0: 242 | log.error("No plots for EIS were selected.") 243 | return Exception(f"No plots for EIS were selected for plot {self.plot_type}.") 244 | 245 | log.error("Maximum plots for EIS is exceeded.") 246 | return Exception(f"Maximum plots for EIS is exceeded for plot {self.plot_type}.") 247 | -------------------------------------------------------------------------------- /madap/echem/procedure.py: -------------------------------------------------------------------------------- 1 | """This module defines the abstract class for the blueprint from all method's behaviours.""" 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class EChemProcedure(ABC): 6 | """ Abstract class for the blueprint from all method's behaviours. 7 | 8 | Args: 9 | ABC (class): Abstract Base Class 10 | """ 11 | @abstractmethod 12 | def analyze(self): 13 | """Abstract method for the analysis of the data. 14 | """ 15 | 16 | @abstractmethod 17 | def plot(self, save_dir:str, plots:list, optional_name:str): 18 | """Abstract method for the plotting of the data. 19 | 20 | Args: 21 | save_dir (str): The directory where the plots are saved. 22 | plots (list): The plots that are saved. 23 | optional_name (str): The optional name of the plot. 24 | """ 25 | 26 | @abstractmethod 27 | def save_data(self, save_dir:str, optional_name:str): 28 | """Abstract method for the saving of the data. 29 | 30 | Args: 31 | save_dir (str): The directory where the data is saved. 32 | optional_name (str): The optional name of the data. 33 | """ 34 | 35 | @abstractmethod 36 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str): 37 | """Abstract method for the performing of all actions. 38 | 39 | Args: 40 | save_dir (str): The directory where the data is saved. 41 | plots (list): The plots that are saved. 42 | optional_name (str): The optional name of the data. 43 | """ 44 | -------------------------------------------------------------------------------- /madap/echem/voltammetry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/echem/voltammetry/__init__.py -------------------------------------------------------------------------------- /madap/echem/voltammetry/voltammetry.py: -------------------------------------------------------------------------------- 1 | """This module contains the Voltammetry class, which is used to analyze voltammetry data.""" 2 | 3 | import os 4 | 5 | import numpy as np 6 | import scipy.constants as const 7 | from scipy.stats import linregress 8 | 9 | from madap.echem.procedure import EChemProcedure 10 | from madap.logger import logger 11 | from madap.utils import utils 12 | 13 | log = logger.get_logger("voltammetry") 14 | 15 | 16 | class Voltammetry(EChemProcedure): 17 | """This class defines the voltammetry method.""" 18 | 19 | def __init__(self, voltage, current, time, args, charge=None) -> None: 20 | """Initialize the voltammetry method. 21 | Args: 22 | voltage (list): list of voltages 23 | current (list): list of currents 24 | time (list): list of times 25 | charge (list): list of charges 26 | args (argparse.Namespace): arguments 27 | """ 28 | self.figure = None 29 | self.faraday_constant = const.physical_constants["Faraday constant"][ 30 | 0 31 | ] # Unit: C/mol 32 | # gas constant 33 | self.gas_constant = const.physical_constants["molar gas constant"][ 34 | 0 35 | ] # Unit: J/mol/K 36 | self.voltage = voltage 37 | self.np_voltage = np.array(voltage) # Unit: V 38 | 39 | self.current = current 40 | self.np_current = np.array(self.current) # Unit: A 41 | 42 | self.time = time 43 | self.np_time = np.array(self.time) if self.time else None # Unit: s 44 | 45 | if self.np_time is not None: 46 | self.cumulative_charge = ( 47 | self._calculate_charge() if charge is None else charge 48 | ) 49 | self.np_cumulative_charge = np.array(self.cumulative_charge) # Unit: C 50 | self.convert_time() 51 | else: 52 | self.cumulative_charge = None 53 | self.np_cumulative_charge = None 54 | 55 | 56 | self.mass_of_active_material = ( 57 | float(args.mass_of_active_material) 58 | if args.mass_of_active_material is not None 59 | else None 60 | ) # Unit: g 61 | self.electrode_area = ( 62 | float(args.electrode_area) if args.electrode_area is not None else 1 63 | ) # Unit: cm^2 64 | self.concentration_of_active_material = ( 65 | float(args.concentration_of_active_material) 66 | if args.concentration_of_active_material is not None 67 | else 1 68 | ) # Unit: mol/cm^3 69 | 70 | self.window_size = ( 71 | int(args.window_size) 72 | if args.window_size is not None 73 | else len(self.np_time) - 1 74 | ) 75 | 76 | self.measured_current_unit = args.measured_current_units 77 | self.measured_time_unitis = args.measured_time_units 78 | self.number_of_electrons = int(args.number_of_electrons) 79 | 80 | self.convert_current() 81 | 82 | 83 | def save_figure(self, fig, plot, optional_name=None, plot_dir=None): 84 | """Save the figure in the plot directory. 85 | 86 | Args: 87 | fig (matplotlib.figure.Figure): figure to save 88 | plot (madap.plot.plot): plot object 89 | optional_name (str): optional name for the figure 90 | plot_dir (str): plot directory 91 | """ 92 | fig.tight_layout() 93 | self.figure = fig 94 | name = ( 95 | utils.assemble_file_name(optional_name, self.__class__.__name__) 96 | if optional_name 97 | else utils.assemble_file_name(self.__class__.__name__) 98 | ) 99 | plot.save_plot(fig, plot_dir, name) 100 | 101 | def _save_data_with_name(self, optional_name, class_name, save_dir, data): 102 | """Save the data in the save directory. 103 | 104 | Args: 105 | optional_name (str): optional name for the data 106 | class_name (str): class name 107 | save_dir (str): save directory 108 | data (pd.DataFrame): data to save 109 | """ 110 | data_name = ( 111 | utils.assemble_file_name(optional_name, class_name, "data.csv") 112 | if optional_name 113 | else utils.assemble_file_name(class_name, "data.csv") 114 | ) 115 | utils.save_data_as_csv(save_dir, data, data_name) 116 | 117 | def _assemble_name(self, save_dir, optional_name, class_name): 118 | """Assemble the name of the file. 119 | 120 | Args: 121 | save_dir (str): save directory 122 | optional_name (str): optional name for the file 123 | class_name (str): class name 124 | """ 125 | save_dir = utils.create_dir(os.path.join(save_dir, "data")) 126 | 127 | name = ( 128 | utils.assemble_file_name(optional_name, class_name, "params.json") 129 | if optional_name 130 | else utils.assemble_file_name(class_name, "params.json") 131 | ) 132 | return save_dir, name 133 | 134 | def convert_current(self): 135 | """Convert the current to A indipendently from the unit of measure""" 136 | if self.measured_current_unit == "uA": 137 | self.current = self.current * 1e-6 138 | elif self.measured_current_unit == "mA": 139 | self.current = self.current * 1e-3 140 | elif self.measured_current_unit == "A": 141 | self.current = self.current * 1e0 142 | else: 143 | log.error("Current unit not supported. Supported units are: uA, mA, A") 144 | raise ValueError("Current unit not supported") 145 | 146 | def convert_time(self): 147 | """Convert the time to s indipendently from the unit of measure""" 148 | if self.measured_time_unitis == "s": 149 | self.time = self.time * 1e0 150 | elif self.measured_time_unitis == "min": 151 | self.time = self.time * 60 152 | elif self.measured_time_unitis == "h": 153 | self.time = self.time * 3600 154 | elif self.measured_time_unitis == "ms": 155 | self.time = self.time * 1e-3 156 | else: 157 | log.error("Time unit not supported. Supported units are: s, min, h") 158 | raise ValueError("Time unit not supported") 159 | 160 | def analyze_best_linear_fit(self, x_data, y_data): 161 | """ 162 | Find the best linear region for the provided data. 163 | 164 | Args: 165 | x_data (np.array): Transformed time array (e.g., t^(-1/2) for diffusion or time for kinetics). 166 | y_data (np.array): Current array or transformed current array (e.g., log(current)). 167 | 168 | Returns: 169 | best fit (dict): Dictionary containing the best linear fit parameters: 170 | start_index (int): Start index of the best linear region. 171 | end_index (int): End index of the best linear region. 172 | slope (float): Slope of the best linear fit. 173 | intercept (float): Intercept of the best linear fit. 174 | r_squared (float): R-squared value of the best linear fit. 175 | """ 176 | 177 | best_fit = { 178 | "start": 0, 179 | "end": self.window_size, 180 | "r_squared": 0, 181 | "slope": 0, 182 | "intercept": 0, 183 | } 184 | for start in range(len(x_data) - self.window_size + 1): 185 | end = start + self.window_size 186 | slope, intercept, r_value, _, _ = linregress( 187 | x_data[start:end], y_data[start:end] 188 | ) 189 | r_squared = r_value**2 190 | if r_squared > best_fit["r_squared"]: 191 | best_fit.update( 192 | { 193 | "start": start, 194 | "end": end, 195 | "r_squared": r_squared, 196 | "slope": slope, 197 | "intercept": intercept, 198 | } 199 | ) 200 | log.info( 201 | f"Best linear fit found from {best_fit['start']} to {best_fit['end']} with R^2 = {best_fit['r_squared']}" 202 | ) 203 | return best_fit 204 | 205 | def _calculate_charge(self): 206 | """Calculate the cumulative charge passed in a voltammetry experiment.""" 207 | # Calculate the time intervals (delta t) 208 | delta_t = np.diff(self.np_time) 209 | 210 | # Calculate the charge for each interval as the product of the interval duration and the current at the end of the interval 211 | interval_charges = delta_t * self.np_current[1:] 212 | 213 | # Compute the cumulative charge 214 | return np.cumsum(np.insert(interval_charges, 0, 0)).tolist() 215 | -------------------------------------------------------------------------------- /madap/echem/voltammetry/voltammetry_CA.py: -------------------------------------------------------------------------------- 1 | """ This module defines the cyclic amperometry methods. It is a subclass of the Voltammetry class and the EChemProcedure class. 2 | It contains the cyclic amperometry methods for analyzing the data and plotting the results.""" 3 | import os 4 | 5 | import numpy as np 6 | 7 | from madap.utils import utils 8 | from madap.echem.voltammetry.voltammetry import Voltammetry 9 | from madap.echem.procedure import EChemProcedure 10 | from madap.logger import logger 11 | 12 | from madap.echem.voltammetry.voltammetry_plotting import VoltammetryPlotting as voltPlot 13 | 14 | log = logger.get_logger("cyclic_amperometry") 15 | 16 | 17 | class Voltammetry_CA(Voltammetry, EChemProcedure): 18 | """ This class defines the chrono amperometry method.""" 19 | def __init__(self, current, voltage, time, args, charge=None) -> None: 20 | super().__init__(voltage, current, time, args, charge=charge) 21 | self.applied_voltage = float(args.applied_voltage) if args.applied_voltage is not None else None # Unit: V 22 | self.diffusion_coefficient = None # Unit: cm^2/s 23 | self.reaction_order = None # 1 or 2 24 | self.reaction_rate_constant = None # Unit: 1/s or cm^3/mol/s 25 | self.best_fit_reaction_rate = None 26 | self.best_fit_diffusion = None 27 | 28 | def analyze(self): 29 | """ Analyze the data to calculate the diffusion coefficient and reaction rate constant: 30 | 1. Calculate the diffusion coefficient using Cottrell analysis. 31 | 2. Analyze the reaction kinetics to determine if the reaction is first or second order. 32 | """ 33 | # Calculate diffusion coefficient 34 | self._calculate_diffusion_coefficient() 35 | 36 | # Reaction kinetics analysis 37 | self._analyze_reaction_kinetics() 38 | 39 | 40 | def _calculate_diffusion_coefficient(self): 41 | """ Calculate the diffusion coefficient using Cottrell analysis.""" 42 | log.info("Calculating diffusion coefficient using Cottrell analysis...") 43 | # Find the best linear region for Cottrell analysis 44 | t_inv_sqrt = np.sqrt(1 / self.np_time[1:]) # Avoid division by zero 45 | best_fit = self.analyze_best_linear_fit(t_inv_sqrt, self.np_current[1:]) 46 | slope = best_fit['slope'] 47 | # Calculate D using the slope 48 | # Unit of D: cm^2/s 49 | # Cortrell equation: I = (nFAD^1/2 * C)/ (pi^1/2 * t^1/2) 50 | self.diffusion_coefficient = (slope ** 2 * np.pi) / (self.number_of_electrons ** 2 * \ 51 | self.faraday_constant ** 2 * \ 52 | self.electrode_area ** 2 * \ 53 | self.concentration_of_active_material ** 2) 54 | log.info(f"Diffusion coefficient: {self.diffusion_coefficient} cm^2/s") 55 | self.best_fit_diffusion = best_fit 56 | 57 | 58 | def _analyze_reaction_kinetics(self): 59 | """ 60 | Analyze the reaction kinetics to determine if the reaction is zero, first or second order. Higher order reactions are not considered here. 61 | for the zero order, the rate low: I = I0 - kt 62 | for the first order, the rate low: ln(I) = ln(I0) - kt 63 | for the second order, the rate low: 1/I = 1/I0 + kt 64 | and calculate the rate constant accordingly. 65 | """ 66 | # Analyze for zero-order kinetics 67 | log.info("Analyzing reaction kinetics for zero kinetic order...") 68 | zero_order_fit = self.analyze_best_linear_fit(x_data=self.np_time[1:], y_data=self.np_current[1:]) 69 | # Analyze for first-order kinetics 70 | log.info("Analyzing reaction kinetics for first kinetic order...") 71 | first_order_fit = self.analyze_best_linear_fit(x_data=self.np_time[1:], y_data=np.log(self.np_current[1:])) 72 | # Analyze for second-order kinetics 73 | log.info("Analyzing reaction kinetics for second kinetic order...") 74 | second_order_fit = self.analyze_best_linear_fit( x_data=self.np_time[1:], y_data=1/self.np_current[1:]) 75 | 76 | 77 | # Determine which order fits best 78 | if (zero_order_fit['r_squared'] > first_order_fit['r_squared']) and (zero_order_fit['r_squared'] > second_order_fit['r_squared']): 79 | log.info("The reaction is zero-order.") 80 | self.reaction_order = 0 81 | self.reaction_rate_constant = -zero_order_fit['slope'] 82 | self.best_fit_reaction_rate = zero_order_fit 83 | log.info(f"Reaction rate constant for zero order: {self.reaction_rate_constant} A/s") 84 | log.info("A positive rate constant indicates a decay process, while a negative one indicates an increasing process or growth.") 85 | 86 | if first_order_fit['r_squared'] > second_order_fit['r_squared']: 87 | log.info("The reaction is first-order.") 88 | self.reaction_order = 1 89 | # Assigning the negative of the slope for first-order kinetics 90 | self.reaction_rate_constant = -first_order_fit['slope'] 91 | self.best_fit_reaction_rate = first_order_fit 92 | log.info(f"Reaction rate constant for first order: {self.reaction_rate_constant} 1/s") 93 | log.info("A positive rate constant indicates a decay process, while a negative one indicates an increasing process or growth.") 94 | 95 | else: 96 | log.info("The reaction is second-order.") 97 | self.reaction_order = 2 98 | self.reaction_rate_constant = second_order_fit['slope'] 99 | self.best_fit_reaction_rate = second_order_fit 100 | log.info(f"Reaction rate constant for second order: {self.reaction_rate_constant} cm^3/mol/s") 101 | log.info("A positive rate constant indicates a typical second-order increasing concentration process.") 102 | 103 | 104 | def plot(self, save_dir, plots, optional_name: str = None): 105 | """Plot the data. 106 | 107 | Args: 108 | save_dir (str): The directory where the plot should be saved. 109 | plots (list): A list of plots to be plotted. 110 | optional_name (str): The optional name of the plot. 111 | """ 112 | plot_dir = utils.create_dir(os.path.join(save_dir, "plots")) 113 | plot = voltPlot(current=self.np_current, time=self.np_time, 114 | voltage=self.voltage, 115 | electrode_area=self.electrode_area, 116 | mass_of_active_material=self.mass_of_active_material, 117 | cumulative_charge=self.cumulative_charge, 118 | procedure_type=self.__class__.__name__, 119 | applied_voltage=self.applied_voltage) 120 | if self.voltage is None and "Voltage" in plots: 121 | log.warning("Measured voltage is not provided. Voltage plot is not available.") 122 | # Drop the voltage plot from the plots list 123 | plots = [plot for plot in plots if plot != "Voltage"] 124 | 125 | fig, available_axes = plot.compose_volt_subplot(plots=plots) 126 | for sub_ax, plot_name in zip(available_axes, plots): 127 | if plot_name == "CA": 128 | plot.CA(subplot_ax=sub_ax) 129 | elif plot_name == "Log_CA": 130 | if self.reaction_order == 0: 131 | y_data = self.np_current[1:] 132 | elif self.reaction_order == 1: 133 | y_data = np.log(self.np_current[1:]) 134 | elif self.reaction_order == 2: 135 | y_data = 1/self.np_current[1:] 136 | plot.log_CA(subplot_ax=sub_ax, y_data = y_data, 137 | reaction_rate=self.reaction_rate_constant, 138 | reaction_order=self.reaction_order, 139 | best_fit_reaction_rate=self.best_fit_reaction_rate) 140 | elif plot_name == "CC": 141 | plot.CC(subplot_ax=sub_ax) 142 | elif plot_name == "Cottrell": 143 | plot.cottrell(subplot_ax=sub_ax, diffusion_coefficient=self.diffusion_coefficient, best_fit_diffusion=self.best_fit_diffusion) 144 | elif plot_name == "Anson": 145 | plot.anson(subplot_ax=sub_ax, diffusion_coefficient=self.diffusion_coefficient) 146 | elif plot_name == "Voltage": 147 | plot.CP(subplot_ax=sub_ax) 148 | else: 149 | log.error("Voltammetry CA class does not have the selected plot.") 150 | continue 151 | 152 | self.save_figure(fig, plot, optional_name=optional_name, plot_dir=plot_dir) 153 | 154 | def save_data(self, save_dir:str, optional_name:str = None): 155 | """Save the data 156 | 157 | Args: 158 | save_dir (str): The directory where the data should be saved 159 | optional_name (str): The optional name of the data. 160 | """ 161 | log.info("Saving data...") 162 | # Create a directory for the data 163 | save_dir, name = self._assemble_name(save_dir, optional_name, self.__class__.__name__) 164 | if self.reaction_order == 0: 165 | reaction_rate_constant_unit = "A/s" 166 | elif self.reaction_order == 1: 167 | reaction_rate_constant_unit = "1/s" 168 | elif self.reaction_order == 2: 169 | reaction_rate_constant_unit = "cm^3/mol/s" 170 | # Add the settings and processed data to the dictionary 171 | added_data = { 172 | "Applied voltage [V]": self.applied_voltage, 173 | "Diffusion coefficient [cm^2/s]": self.diffusion_coefficient, 174 | "Reaction order": self.reaction_order, 175 | f"Reaction rate constant ({reaction_rate_constant_unit})": self.reaction_rate_constant, 176 | "Best fit reaction rate": self.best_fit_reaction_rate, 177 | "Best fit diffusion": self.best_fit_diffusion, 178 | "Electrode area [cm^2]": self.electrode_area, 179 | "Mass of active material [g]": self.mass_of_active_material, 180 | "Concentration of active material [mol/cm^3]": self.concentration_of_active_material, 181 | "Window size of fit": self.window_size 182 | } 183 | utils.save_data_as_json(save_dir, added_data, name) 184 | 185 | # Save the raw data 186 | data = utils.assemble_data_frame(**{ 187 | "voltage [V]": self.voltage, 188 | "current [A]": self.current, 189 | "time [s]": self.time, 190 | "cumulative_charge [C]": self.cumulative_charge 191 | }) 192 | 193 | self._save_data_with_name(optional_name, self.__class__.__name__, save_dir, data) 194 | 195 | 196 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str = None): 197 | """Perform all the actions for the cyclic amperometry method: analyze, plot, and save data.""" 198 | 199 | self.analyze() 200 | self.plot(save_dir, plots, optional_name=optional_name) 201 | self.save_data(save_dir=save_dir, optional_name=optional_name) 202 | 203 | @property 204 | def figure(self): 205 | """Get the figure of the plot. 206 | 207 | Returns: 208 | obj: Figure object for ca plot. 209 | """ 210 | return self._figure 211 | 212 | 213 | @figure.setter 214 | def figure(self, figure): 215 | """Set the figure of the plot. 216 | 217 | Args: 218 | figure (obj): Figure object for ca plot. 219 | """ 220 | self._figure = figure 221 | -------------------------------------------------------------------------------- /madap/echem/voltammetry/voltammetry_CP.py: -------------------------------------------------------------------------------- 1 | """This module contains the cyclic potentiometry class.""" 2 | import os 3 | import numpy as np 4 | 5 | 6 | from scipy.signal import savgol_filter, find_peaks 7 | from sklearn.cluster import KMeans 8 | from sklearn.metrics import silhouette_score 9 | 10 | import ruptures as rpt 11 | 12 | from madap.utils import utils 13 | from madap.echem.voltammetry.voltammetry import Voltammetry 14 | from madap.echem.procedure import EChemProcedure 15 | from madap.logger import logger 16 | 17 | from madap.echem.voltammetry.voltammetry_plotting import VoltammetryPlotting as voltPlot 18 | 19 | 20 | log = logger.get_logger("cyclic_potentiometry") 21 | 22 | 23 | class Voltammetry_CP(Voltammetry, EChemProcedure): 24 | """This class defines the cyclic potentiometry method.""" 25 | def __init__(self, voltage, current, time, args, charge=None) -> None: 26 | super().__init__(voltage, current, time, args, charge=charge) 27 | self.applied_current = float(args.applied_current) if args.applied_current is not None else None # Unit: A 28 | self.dQdV = None # Unit: C/V 29 | self.dQdV_unit = None 30 | self.dVdt = None # Unit: V/h 31 | self.dVdt_smoothed = None # Unit: V/h 32 | self.tao_initial = None # Unit: s 33 | self.stabilization_values = {} # transition time (s): transition voltage (V) 34 | self.transition_values = {} 35 | self.d_coefficient = None # Unit: cm^2/s 36 | self.penalty_value = float(args.penalty_value) if args.penalty_value is not None else 0.25 37 | 38 | self.positive_peaks = {} 39 | self.negative_peaks = {} 40 | 41 | def analyze(self): 42 | """Analyze the cyclic potentiometry data. These analysis include: 43 | 1. Calculate dQ/dV 44 | 2. Calculate dV/dt 45 | 3. Calculate initial stabilization time 46 | 4. Find potential transition times 47 | 5. Calculate diffusion coefficient 48 | """ 49 | # Calculate: dQ/dV, dV/dt, initial stabilization time, potential transition time(s), diffusion coefficient, 50 | self._calculate_dQdV() 51 | self._calculate_dVdt() 52 | self._calculate_initial_stabilization_time() 53 | self._find_potential_transition_times() 54 | self._calculate_diffusion_coefficient() 55 | 56 | 57 | def plot(self, save_dir, plots, optional_name: str = None): 58 | """Plot the cyclic potentiometry data. 59 | 60 | Args: 61 | save_dir (str): The directory where the plot should be saved 62 | plots (list): The list of plots to be plotted 63 | optional_name (str): The optional name of the plot. 64 | """ 65 | plot_dir = utils.create_dir(os.path.join(save_dir, "plots")) 66 | plot = voltPlot(current=self.np_current, time=self.np_time, 67 | voltage=self.np_voltage, 68 | electrode_area=self.electrode_area, 69 | mass_of_active_material=self.mass_of_active_material, 70 | cumulative_charge=self.cumulative_charge, 71 | procedure_type=self.__class__.__name__, 72 | applied_current=self.applied_current) 73 | fig, available_axes = plot.compose_volt_subplot(plots=plots) 74 | for sub_ax, plot_name in zip(available_axes, plots): 75 | if plot_name == "CP": 76 | plot.CP(subplot_ax=sub_ax) 77 | elif plot_name == "CC": 78 | plot.CC(subplot_ax=sub_ax) 79 | elif plot_name == "Cottrell": 80 | plot.cottrell(subplot_ax=sub_ax, diffusion_coefficient=self.d_coefficient) 81 | elif plot_name == "Voltage_Profile": 82 | plot.voltage_profile(subplot_ax=sub_ax) 83 | elif plot_name == "Potential_Rate": 84 | plot.potential_rate(subplot_ax=sub_ax, dVdt=self.dVdt, transition_values=self.transition_values, 85 | tao_initial=self.tao_initial) 86 | elif plot_name == "Differential_Capacity": 87 | plot.differential_capacity(subplot_ax=sub_ax, dQdV_no_nan=self.dQdV,positive_peaks=self.positive_peaks, 88 | negative_peaks=self.negative_peaks) 89 | else: 90 | log.error("Voltammetry CP class does not have the selected plot.") 91 | continue 92 | self.save_figure(fig, plot, optional_name=optional_name, plot_dir=plot_dir) 93 | 94 | 95 | def save_data(self, save_dir:str, optional_name:str = None): 96 | """Save the data 97 | 98 | Args: 99 | save_dir (str): The directory where the data should be saved 100 | optional_name (str): The optional name of the data. 101 | """ 102 | log.info("Saving data...") 103 | # Create a directory for the data 104 | save_dir, name = self._assemble_name(save_dir, optional_name, self.__class__.__name__) 105 | # add the settings and processed data to the dictionary 106 | added_data = { 107 | "Applied Current [A]": self.applied_current, 108 | "Penalty Value": self.penalty_value, 109 | "Initial Stabilization Time [s]": self.tao_initial, 110 | "Stabilization Values {time [s]: potential [V]}": self.stabilization_values, 111 | "Transition Values {time [s]: potential [V]}": self.transition_values, 112 | "Diffusion Coefficient [cm^2/s]": f"{self.d_coefficient} * Tau", 113 | "Positive_peaks {potential [V]: dQdV" + f"{self.dQdV_unit}" + "}": self.positive_peaks, 114 | "Negative_peaks {potential [V]: dQdV" + f"{self.dQdV_unit}" + "}": self.negative_peaks, 115 | } 116 | 117 | utils.save_data_as_json(save_dir, added_data, name) 118 | 119 | # save the raw data 120 | data = utils.assemble_data_frame(**{ 121 | "Time [s]": self.np_time, 122 | "Voltage [V]": self.np_voltage, 123 | "Current [A]": self.np_current, 124 | "Cumulative Charge [C]": self.np_cumulative_charge, 125 | "dQdV" + f"{self.dQdV_unit}": self.dQdV, 126 | "dVdt [V/h]": self.dVdt 127 | }) 128 | 129 | self._save_data_with_name(optional_name, self.__class__.__name__, save_dir, data) 130 | 131 | 132 | def perform_all_actions(self, save_dir:str, plots:list, optional_name:str = None): 133 | """ Perform all the actions for the cyclic potentiometry method: analyze, plot, and save data. 134 | 135 | Args: 136 | save_dir (str): The directory where the data should be saved 137 | plots (list): The list of plots to be plotted 138 | optional_name (str): The optional name of the data. 139 | """ 140 | self.analyze() 141 | self.plot(save_dir, plots, optional_name=optional_name) 142 | self.save_data(save_dir=save_dir, optional_name=optional_name) 143 | 144 | 145 | def _impute_mean_nearest_neighbors(self, data): 146 | """Impute NaN values using the mean of nearest neighbors. 147 | 148 | Args: 149 | data (np.array): data where the NaN values should be imputed 150 | """ 151 | n = len(data) 152 | for i in range(n): 153 | if np.isnan(data[i]): 154 | left = right = i 155 | # Move left index to the nearest non-NaN value 156 | while left >= 0 and np.isnan(data[left]): 157 | left -= 1 158 | # Move right index to the nearest non-NaN value 159 | while right < n and np.isnan(data[right]): 160 | right += 1 161 | 162 | # Compute mean of nearest non-NaN neighbors 163 | neighbors = [] 164 | if left >= 0: 165 | neighbors.append(data[left]) 166 | if right < n: 167 | neighbors.append(data[right]) 168 | 169 | data[i] = np.mean(neighbors) if neighbors else 0 170 | return data 171 | 172 | 173 | def _calculate_dQdV(self): 174 | """Calculate the differential of charge with respect to voltage. 175 | """ 176 | # Convert the cumulative charge from As to mAh 177 | cumulative_charge_mAh = self.np_cumulative_charge * (1000/3600) 178 | 179 | # Calculate the differential of charge with respect to voltage 180 | dQdV = np.gradient(cumulative_charge_mAh, self.np_voltage) # mAh/V 181 | # Impute NaN values in dQdV using the mean of nearest neighbors 182 | dQdV_no_nan = self._impute_mean_nearest_neighbors(dQdV) 183 | # If mass is available, convert it to mAh/gV 184 | if self.mass_of_active_material is not None: 185 | dQdV_no_nan /= self.mass_of_active_material # mAh/gV 186 | self.dQdV_unit = "mAh/gV" 187 | elif self.mass_of_active_material is None and self.electrode_area is not None: 188 | dQdV_no_nan /= self.electrode_area 189 | self.dQdV_unit = "mAh/cm^2V" 190 | else: 191 | self.dQdV_unit = "mAh/V" 192 | 193 | all_peaks, _ = find_peaks(dQdV_no_nan) 194 | dQdV_no_nan = np.array(dQdV_no_nan) 195 | 196 | # Find all negative peaks if dQdV has negative values 197 | if np.any(dQdV_no_nan < 0): 198 | negative_peaks, _ = find_peaks(-dQdV_no_nan) 199 | 200 | # Apply k-means clustering to categorize into two clusters 201 | n_possible_reactions = self._determine_cluster_number(all_peaks) 202 | # Just check for the number of k-means clusters if there are peaks 203 | if len(all_peaks) != 0: 204 | kmeans = KMeans(n_clusters=n_possible_reactions, random_state=42).fit(self.np_voltage[all_peaks].reshape(-1, 1)) 205 | labels = kmeans.labels_ 206 | if np.any(dQdV_no_nan < 0) and len(negative_peaks) != 0: 207 | negative_labels = kmeans.predict(self.np_voltage[negative_peaks].reshape(-1, 1)) 208 | # Find the most significant peak in each cluster 209 | for i in range(n_possible_reactions): 210 | # Positive Peaks 211 | cluster_peaks_of_dqdv = dQdV_no_nan[all_peaks][labels == i] 212 | index_of_max_peak = np.argmax(cluster_peaks_of_dqdv) 213 | self.positive_peaks[self.np_voltage[all_peaks][labels == i][index_of_max_peak]] =\ 214 | dQdV_no_nan[all_peaks][labels == i][index_of_max_peak] 215 | 216 | if np.any(dQdV_no_nan < 0) and len(negative_peaks) != 0: 217 | # Negative Peaks 218 | cluster_neg_peaks = dQdV_no_nan[negative_peaks][negative_labels == i] 219 | min_peak = np.argmin(cluster_neg_peaks) 220 | self.negative_peaks[self.np_voltage[negative_peaks][negative_labels == i][min_peak]] =\ 221 | dQdV_no_nan[negative_peaks][negative_labels == i][min_peak] 222 | 223 | self.dQdV = dQdV_no_nan 224 | 225 | 226 | def _calculate_dVdt(self): 227 | """Calculate the differential of voltage with respect to time. 228 | """ 229 | self.dVdt = np.gradient(self.np_voltage, self.np_time) * 3600 # V/h 230 | 231 | 232 | def _determine_cluster_number(self, data): 233 | """Determine the optimal number of clusters using the silhouette score. 234 | 235 | Args: 236 | data (np.array): data where the cluster number should be determined 237 | 238 | Returns: 239 | int: optimal number of clusters 240 | """ 241 | max_silhouette_score = -1 242 | optimal_n_clusters = 1 243 | # check if data is 1D 244 | if len(data.shape) == 1: 245 | data = data.reshape(-1, 1) 246 | for n_clusters in range(2, min(len(data), 10)): 247 | kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(data) 248 | silhouette_avg = silhouette_score(data, kmeans.labels_) 249 | if silhouette_avg > max_silhouette_score: 250 | max_silhouette_score = silhouette_avg 251 | optimal_n_clusters = n_clusters 252 | 253 | return optimal_n_clusters 254 | 255 | 256 | def _calculate_initial_stabilization_time(self): 257 | """Calculate the initial stabilization time using the Pelt algorithm. 258 | """ 259 | model = "l1" # L1 norm minimization 260 | algo = rpt.Pelt(model=model).fit(self.np_voltage) 261 | result = algo.predict(pen=self.penalty_value) 262 | if result: 263 | if result[0] == len(self.np_voltage): 264 | self.tao_initial = self.np_time[-1] 265 | self.stabilization_values[self.tao_initial] = self.np_voltage[-1] 266 | else: 267 | # The first change point is the end of the initial stabilization phase 268 | self.tao_initial = self.np_time[result[0]] 269 | self.stabilization_values[self.tao_initial] = self.np_voltage[result[0]] 270 | 271 | 272 | def _find_potential_transition_times(self, window_length=73, polyorder=3): 273 | """Find potential transition times and their corresponding transition voltages. 274 | This functions excludes the initial stabilization time. 275 | 276 | Args: 277 | window_length (int): length of the filter window 278 | polyorder (int): order of the polynomial to fit 279 | """ 280 | # Check if window_length is less than the length of the data 281 | if window_length > len(self.dVdt): 282 | window_length = polyorder + 2 283 | # check that windowlength is odd, and if not, make it odd 284 | if window_length % 2 == 0: 285 | window_length += 1 286 | # Apply Savitzky-Golay filter to smooth dV/dt data 287 | self.dVdt_smoothed = savgol_filter(self.dVdt, window_length, polyorder) 288 | 289 | # Calculate second derivative of the smoothed dV/dt data 290 | d2Vdt2 = np.gradient(self.dVdt_smoothed, self.np_time) 291 | 292 | # Threshold for identifying significant changes 293 | threshold = np.std(d2Vdt2) * 3 294 | 295 | # Find indices where the second derivative exceeds the threshold 296 | transition_indices = np.where(np.abs(d2Vdt2) > threshold)[0] 297 | 298 | # Filter out times within the initial stabilization phase 299 | #transition_indices = transition_indices[self.np_time[transition_indices] > self.tao_initial] 300 | if transition_indices.size > 0: 301 | if len(self.np_time[transition_indices].shape) == 1: 302 | data = self.np_time[transition_indices].reshape(-1, 1) 303 | else: 304 | data = self.np_time[transition_indices] 305 | n_possible_reactions = self._determine_cluster_number(data) 306 | if n_possible_reactions != 1: 307 | kmeans = KMeans(n_clusters=n_possible_reactions, random_state=0).fit(data) 308 | labels = kmeans.labels_ 309 | 310 | for i in range(n_possible_reactions): 311 | # check if the self.tao_initial is in the transition_indices then remove the cluster that contains the self.tao_initial 312 | if self.tao_initial is not None and self.tao_initial in self.np_time[transition_indices][labels == i]: 313 | continue 314 | # cluster the peaks 315 | cluster_peaks = self.dVdt_smoothed[transition_indices][labels == i] 316 | # find the max peak in the cluster 317 | max_peak = cluster_peaks[np.argmax(cluster_peaks)] 318 | # get the index of the max peak 319 | max_peak_index = np.where(self.dVdt_smoothed == max_peak)[0][0] 320 | self.transition_values = {self.np_time[max_peak_index]: self.np_voltage[max_peak_index]} 321 | else: 322 | # if all the times at the transition_indices are smaller than the self.tao_initial then we do not have a transition 323 | if np.all(self.np_time[transition_indices] < self.tao_initial): 324 | self.transition_values = {} 325 | else: 326 | if self.tao_initial is not None: 327 | transition_indices = transition_indices[self.np_time[transition_indices] > self.tao_initial] 328 | # max_peak_index = np.argmax(self.dVdt_smoothed[transition_indices]) 329 | if transition_indices.size > 0: 330 | self.transition_values = {self.np_time[i]: self.np_voltage[i] for i in transition_indices} 331 | #self.transition_value = {self.np_time[i]: self.np_voltage[i] for i in transition_indices} 332 | 333 | 334 | def _calculate_diffusion_coefficient(self): 335 | """Calculate the diffusion coefficient value using Sand's formula without tau. 336 | """ 337 | # Calculate the coefficient part of the diffusion coefficient using Sand's formula without tau 338 | # Coefficient = (4 * I^2) / ((n * F * A * c)^2 * pi) 339 | if self.applied_current is not None: 340 | current = np.abs(self.applied_current) 341 | else: 342 | current = np.abs(np.mean(self.np_current)) 343 | self.d_coefficient = (4 * current**2) / ((self.number_of_electrons * \ 344 | self.faraday_constant * \ 345 | self.electrode_area * \ 346 | self.concentration_of_active_material)**2 * np.pi) 347 | 348 | 349 | @property 350 | def figure(self): 351 | """Get the figure of the plot. 352 | 353 | Returns: 354 | obj: Figure object for ca plot. 355 | """ 356 | return self._figure 357 | 358 | 359 | @figure.setter 360 | def figure(self, figure): 361 | """Set the figure of the plot. 362 | 363 | Args: 364 | figure (obj): Figure object for ca plot. 365 | """ 366 | self._figure = figure 367 | -------------------------------------------------------------------------------- /madap/logger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/logger/__init__.py -------------------------------------------------------------------------------- /madap/logger/logger.py: -------------------------------------------------------------------------------- 1 | """This module defines the logger for the MADAP application.""" 2 | import sys 3 | import queue 4 | 5 | import logging 6 | from logging.handlers import QueueHandler 7 | 8 | APP_LOGGER_NAME = 'MADAP' 9 | log_queue = queue.Queue() # Queue for log messages 10 | 11 | class QueueLoggerHandler(QueueHandler): 12 | """Custom handler for logging messages to queue""" 13 | 14 | def setup_applevel_logger(logger_name=APP_LOGGER_NAME, file_name=None): 15 | """Sets up the app level logger 16 | 17 | Args: 18 | logger_name (str, optional): Logger name. Defaults to APP_LOGGER_NAME. 19 | file_name (str, optional): file name. Defaults to None. 20 | 21 | Returns: 22 | logger.Logger: app level logger 23 | """ 24 | 25 | logger = logging.getLogger(logger_name) 26 | logger.setLevel(logging.DEBUG) # Set the logging level 27 | 28 | # Create handlers 29 | stream_handler = logging.StreamHandler(sys.stdout) 30 | queue_handler = QueueLoggerHandler(log_queue) # Custom handler for queue 31 | 32 | # Create formatters and add it to handlers 33 | stream_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s') 34 | stream_handler.setFormatter(stream_format) 35 | 36 | # Add handlers to the logger 37 | logger.addHandler(stream_handler) 38 | logger.addHandler(queue_handler) 39 | 40 | if file_name: 41 | file_handler = logging.FileHandler(file_name) 42 | file_handler.setFormatter(stream_format) 43 | logger.addHandler(file_handler) 44 | 45 | return logger 46 | 47 | def get_logger(module_name): 48 | """Returns a logger with the name of the module calling this function 49 | 50 | Args: 51 | module_name (str): name of the module calling this function 52 | 53 | Returns: 54 | logging.Logger: logger with the name of the module calling this function 55 | """ 56 | return logging.getLogger(APP_LOGGER_NAME).getChild(module_name) 57 | -------------------------------------------------------------------------------- /madap/plotting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/plotting/__init__.py -------------------------------------------------------------------------------- /madap/plotting/plotting.py: -------------------------------------------------------------------------------- 1 | """ This module handels the general plotting functions for the MADAP application. """ 2 | import os 3 | import math 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | import matplotlib as mpl 8 | 9 | from madap.logger import logger 10 | 11 | 12 | mpl.use('svg') 13 | log = logger.get_logger("plotting") 14 | class Plots(): 15 | """_General class for multipurpose plotting 16 | """ 17 | def __init__(self) -> None: 18 | 19 | mpl.rcParams.update(mpl.rcParamsDefault) 20 | mpl.rc('font', size=20) 21 | mpl.rc('axes', titlesize=20) 22 | style_path, _ =os.path.split(__file__) 23 | plt.style.use([os.path.join(style_path, 'styles', 'nature.mplstyle'), 24 | os.path.join(style_path, 'styles', 'science.mplstyle'), 25 | os.path.join(style_path, 'styles', 'no-latex.mplstyle')]) 26 | plt.rcParams['text.usetex'] = False 27 | plt.rcParams['xtick.direction'] = 'in' 28 | plt.rcParams['ytick.direction'] = 'in' 29 | self.plot_type = "" 30 | self.ax = None 31 | 32 | 33 | def plot_identity(self, ax, xlabel:str=None, ylabel:str=None, x_lim:list=None, y_lim:list=None, 34 | rotation:float=0, ax_sci_notation:bool=False, scientific_limit=0, 35 | log_scale:str=None, step_size_x="auto", step_size_y="auto", x_label_fontsize=9, y_label_fontsize=9): 36 | """Defines the "identity" of the plot. 37 | This includes the x and y labels, the x and y limits, the rotation of the x labels, 38 | wether or not scientific notation should be used, 39 | the log scale and the step size of the x and y axis. 40 | 41 | Args: 42 | ax (matplotlib.axes): axis to which the identity line should be added 43 | xlabel (str, optional): label of the x-axis. Defaults to None. 44 | ylabel (str, optional): label of the y-axis. Defaults to None. 45 | x_lim (list, optional): limits of the x-axis. Defaults to None. 46 | y_lim (list, optional): limits of the y-axis. Defaults to None. 47 | rotation (float, optional): rotation of the x and ylabels. Defaults to 0. 48 | ax_sci_notation (bool, optional): whether or not scientific notation should be used. 49 | scientific_limit (int, optional): scientific notation limit. Defaults to 0. 50 | log_scale (str, optional): log scale of the x and y axis. Defaults to None. 51 | Can be "x", "y" 52 | base10 (bool, optional): base 10 log scale. Defaults to False. 53 | step_size_x (str, optional): step size of the x axis. Defaults to "auto". 54 | step_size_y (str, optional): step size of the y axis. Defaults to "auto". 55 | """ 56 | def calculate_ticks(lim, step_size, num_steps=5): 57 | if max(lim) == min(lim): 58 | # find index of max value 59 | max_index = np.where(lim == max(lim))[0][0] 60 | # Replace max value with max value * 1.5 61 | lim[max_index] = lim[max_index] * 1.5 62 | if step_size == "auto": 63 | raw_step = (max(lim) - min(lim)) / num_steps 64 | step = round(raw_step, -int(np.floor(np.log10(raw_step)))) # Adjust rounding precision 65 | else: 66 | step = step_size 67 | 68 | start = step * np.floor(min(lim) / step) 69 | end = step * np.ceil(max(lim) / step) 70 | return np.arange(start, end + step, step), [start, end] 71 | 72 | 73 | if xlabel: 74 | ax.set_xlabel(xlabel, fontsize=x_label_fontsize) 75 | if ylabel: 76 | ax.set_ylabel(ylabel, fontsize=y_label_fontsize) 77 | if x_lim: 78 | 79 | if not np.isnan(x_lim).any() and not np.isinf(x_lim).any(): 80 | x_ticks, x_lim_adj = calculate_ticks(x_lim, step_size_x) 81 | ax.set_xlim(x_lim_adj) 82 | ax.set_xticks(x_ticks) 83 | 84 | if y_lim: 85 | 86 | if not np.isnan(y_lim).any() and not np.isinf(y_lim).any(): 87 | y_ticks, y_lim_adj = calculate_ticks(y_lim, step_size_y) 88 | ax.set_ylim(y_lim_adj) 89 | ax.set_yticks(y_ticks) 90 | 91 | if rotation: 92 | ax.xaxis.set_tick_params(rotation=rotation) 93 | if ax_sci_notation: 94 | ax.ticklabel_format(style='sci', axis=ax_sci_notation, \ 95 | scilimits=(scientific_limit, scientific_limit)) 96 | if log_scale: 97 | ax.set_xscale('log') if log_scale=='x' else (ax.set_yscale('log') if log_scale=='y' else (ax.set_xscale('log'), ax.set_yscale('log'))) 98 | 99 | 100 | def add_colorbar(self, plot, ax, scientific_label_colorbar=None, scientific_limit=3, colorbar_label=None): 101 | """Adds a colorbar to a plot 102 | 103 | Args: 104 | plot (matplotlib.pyplot): plot to which the colorbar should be added 105 | ax (matplotlib.axes): axis to which the colorbar should be added 106 | scientific_label_colorbar (str, optional): whether or not the colorbar should be written 107 | scientific_limit (int, optional): scientific notation limit. Defaults to 3. 108 | colorbar_label (str, optional): label of the colorbar. Defaults to None. 109 | """ 110 | 111 | color_bar = plt.colorbar(plot, ax=ax) 112 | 113 | if colorbar_label: 114 | color_bar.set_label(colorbar_label, fontsize=7) 115 | color_bar.ax.yaxis.set_label_position('right') 116 | 117 | 118 | if scientific_label_colorbar: 119 | color_bar.formatter.set_powerlimits((scientific_limit, scientific_limit)) 120 | color_bar.update_ticks() 121 | 122 | 123 | def round_hundredth(self, nums): 124 | """Rounds a number to the nearest hundredth 125 | 126 | Args: 127 | nums (float): number to be rounded 128 | 129 | Returns: 130 | float: rounded number 131 | """ 132 | if not nums.all() < 0: 133 | min_num, max_num = int(math.ceil(min(nums) / 100.0)) * 100 - 100, \ 134 | int(math.ceil(max(nums) / 100.0)) * 100 135 | else: 136 | min_num, max_num = int(math.ceil(min(nums) / 100.0)) * 100 , \ 137 | int(math.ceil(max(nums) / 100.0)) * 100+ 100 138 | return min_num, max_num 139 | 140 | 141 | def round_tenth(self, nums): 142 | """Rounds a number to the nearest tenth 143 | 144 | Args: 145 | nums (float): number to be rounded 146 | 147 | Returns: 148 | float: rounded number 149 | """ 150 | if not nums.all() < 0: 151 | min_num, max_num = round(min(nums), -1) - 10, round(max(nums), -1) 152 | else: 153 | min_num, max_num = round(min(nums), -1) , round(max(nums), -1) + 10 154 | return min_num, max_num 155 | 156 | 157 | def set_xtick_for_two_axes(self, ax1, ax2, ax1_ticks, ax2_ticks, invert_axes=False): 158 | """Sets the xticks for two axes 159 | 160 | Args: 161 | ax1 (matplotlib.axes): first axis 162 | ax2 (matplotlib.axes): second axis 163 | ax1_ticks (list): ticks for the first axis 164 | ax2_ticks (list): ticks for the second axis 165 | invert_axes (bool, optional): whether or not the axes should be inverted. Default False. 166 | """ 167 | ax1.set_xlim(ax2.get_xlim()) 168 | ax1.set_xticks(ax2_ticks) 169 | ax1.set_xticklabels(ax1_ticks) 170 | if invert_axes: 171 | ax1.invert_xaxis() 172 | ax2.invert_xaxis() 173 | 174 | 175 | def save_plot(self, fig, directory, name): 176 | """Saves a plot 177 | 178 | Args: 179 | fig (matplotlib.pyplot): figure to be saved 180 | directory (str): directory in which the plot should be saved 181 | name (str): name of the plot 182 | """ 183 | log.info(f"Saving .png and .svg in {directory}") 184 | fig.savefig(os.path.join(directory, f"{name}.svg"), dpi=900) 185 | fig.savefig(os.path.join(directory, f"{name}.png"), dpi=900) 186 | 187 | def _cv_legend(self, subplot_ax): 188 | """Create the legend for the CV plot. The legend is created by combining the legend entries 189 | for each cycle into two groups: 'Cycle' and 'Other'. The 'Cycle' entries are sorted 190 | alphabetically by label. The 'Other' entries are sorted by the order in which they appear 191 | in the legend. Also, the legend is placed in the upper left corner of the plot and the entries 192 | that come more than twice with the same color are removed. 193 | 194 | Args: 195 | subplot_ax (matplotlib.axes): axis to which the plot should be added 196 | """ 197 | handles, labels = subplot_ax.get_legend_handles_labels() 198 | color_count = {} 199 | legend_entries = [] 200 | 201 | for handle, label in zip(handles, labels): 202 | color = handle.get_color() # Adjust this if using different types of plots 203 | # check if color is list then convert it to tuple 204 | if isinstance(color, list): 205 | color = tuple(color) 206 | # Count the occurrences of each color 207 | if color in color_count: 208 | color_count[color] += 1 209 | else: 210 | color_count[color] = 1 211 | 212 | # Add the handle/label to the list if the color count is less than or equal to 2 213 | if color_count[color] <= 2: 214 | legend_entries.append((handle, label)) 215 | 216 | # Split the legend entries into two groups 217 | cycle_entries = [] 218 | other_entries = [] 219 | for handle, label in legend_entries: 220 | if label.startswith("Cyc."): 221 | cycle_entries.append((handle, label)) 222 | else: 223 | other_entries.append((handle, label)) 224 | 225 | # Sort the 'Cycle' entries alphabetically by label 226 | cycle_entries.sort(key=lambda x: x[1]) 227 | 228 | # Combine the sorted 'Cycle' entries with the other entries 229 | sorted_entries = cycle_entries + other_entries 230 | 231 | # Create the legend with the sorted entries 232 | subplot_ax.legend(*zip(*sorted_entries), loc="upper left", fontsize=7, bbox_to_anchor=(1.05, 1.0)) 233 | -------------------------------------------------------------------------------- /madap/plotting/styles/nature.mplstyle: -------------------------------------------------------------------------------- 1 | # Matplotlib style for Nature journal figures. 2 | # In general, they advocate for all fonts to be panel labels to be sans serif 3 | # and all font sizes in a figure to be 7 pt and panel labels to be 8 pt bold. 4 | 5 | # Figure size 6 | figure.figsize : 3.3, 2.5 # max width is 3.5 for single column 7 | 8 | # Font sizes 9 | axes.labelsize: 7 10 | xtick.labelsize: 7 11 | ytick.labelsize: 7 12 | legend.fontsize: 7 13 | font.size: 7 14 | 15 | # Font Family 16 | font.family: sans-serif 17 | font.sans-serif: DejaVu Sans, Arial, Helvetica, Lucida Grande, Verdana, Geneva, Lucid, Avant Garde, sans-serif 18 | mathtext.fontset : dejavusans 19 | 20 | # Set line widths 21 | axes.linewidth : 0.5 22 | grid.linewidth : 0.5 23 | lines.linewidth : 1. 24 | lines.markersize: 3 25 | 26 | # Always save as 'tight' 27 | # savefig.bbox : tight 28 | # savefig.pad_inches : 0.01 # Use virtually all space when we specify figure dimensions 29 | 30 | # LaTeX packages 31 | text.latex.preamble : \usepackage{amsmath} \usepackage{amssymb} \usepackage{sfmath} 32 | -------------------------------------------------------------------------------- /madap/plotting/styles/no-latex.mplstyle: -------------------------------------------------------------------------------- 1 | # Deactivate LaTeX 2 | 3 | text.usetex : False 4 | -------------------------------------------------------------------------------- /madap/plotting/styles/science.mplstyle: -------------------------------------------------------------------------------- 1 | # Matplotlib style for scientific plotting 2 | # This is the base style for "SciencePlots" 3 | # see: https://github.com/garrettj403/SciencePlots 4 | 5 | # Set color cycle: blue, green, yellow, red, violet, gray 6 | axes.prop_cycle : cycler('color', ['0C5DA5', '00B945', 'FF9500', 'FF2C00', '845B97', '474747', '9e9e9e']) 7 | 8 | # Set default figure size 9 | figure.figsize : 3.5, 2.625 10 | 11 | # Set x axis 12 | xtick.direction : in 13 | xtick.major.size : 3 14 | xtick.major.width : 0.5 15 | xtick.minor.size : 1.5 16 | xtick.minor.width : 0.5 17 | xtick.minor.visible : True 18 | xtick.top : True 19 | 20 | # Set y axis 21 | ytick.direction : in 22 | ytick.major.size : 3 23 | ytick.major.width : 0.5 24 | ytick.minor.size : 1.5 25 | ytick.minor.width : 0.5 26 | ytick.minor.visible : True 27 | ytick.right : True 28 | 29 | # Set line widths 30 | axes.linewidth : 0.5 31 | grid.linewidth : 0.5 32 | lines.linewidth : 1. 33 | 34 | # Remove legend frame 35 | legend.frameon : False 36 | 37 | # Always save as 'tight' 38 | savefig.bbox : tight 39 | savefig.pad_inches : 0.05 40 | 41 | # Use serif fonts 42 | # font.serif : Times 43 | font.family : serif 44 | mathtext.fontset : dejavuserif 45 | 46 | # Use LaTeX for math formatting 47 | text.usetex : True 48 | text.latex.preamble : \usepackage{amsmath} \usepackage{amssymb} 49 | -------------------------------------------------------------------------------- /madap/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzhanrahmanian/MADAP/43716b856d4e5c53ccbb0765499fe6d89fca8751/madap/utils/__init__.py -------------------------------------------------------------------------------- /madap/utils/gui_elements.py: -------------------------------------------------------------------------------- 1 | """ Just a file containing some lengthy text used for the GUI helpers. """ 2 | 3 | HEADER_OR_SPECIFIC_HELP="If the data selection is 'Headers', insert the headers for: " \ 4 | "\nfrequency [Hz], real impedance [\u2126], imaginary impedance, phase shift \u03c6[\u00b0] (optional).Order is relevant.\n"\ 5 | "Example: 'freq, real, imag'" \ 6 | "\n \n"\ 7 | "If the data selection is 'Specific' insert the row and column numbers. Order is relevant." \ 8 | "\nFormat: 'start_row,end_row,start_column,end_column': 1,10,1,2 translates to rows 1 to 10 and columns 1 to 2."\ 9 | "\nExample: '0,40,0,1; 0,40,1,2; 0,40,2,3'" 10 | 11 | SUGGESTED_CIRCUIT_HELP="Suggested circuit (optional).\n" \ 12 | "Available circuit elements are: 's', 'C', 'Ws', 'K', 'W', 'Wo', 'R'," \ 13 | "'p', 'L', 'TLMQ', 'CPE', 'G', 'La', 'T', 'Gs'." \ 14 | "\nParallel circuit can be defined as p(element1, element2) " \ 15 | "and the series circuit like element1_element2."\ 16 | "\nFormat: R0-p(R1,CPE1)" 17 | 18 | INITIAL_VALUES_HELP="Initial values for the suggested circuit." \ 19 | "\nIt will be used just if the suggested circuit was defined. " \ 20 | "\nFormat: [element1, element2, ...], i.e.: [800,1e+14,1e-9,0.8] " 21 | 22 | 23 | VOLTAGE_HELP="Applied voltage (optional)" 24 | 25 | CELL_CONSTANT_HELP="Cell constant (optional)" 26 | 27 | UPPER_LIMIT_QUANTILE_HELP="Upper limit quantile (optional)." 28 | 29 | LOWER_LIMIT_QUANTILE_HELP="Lower limit quantile (optional)." 30 | 31 | WINDOW_SIZE_HELP="Window size for the best linear fit for getting diffusion coefficient and reaction rate constant (optional)." 32 | 33 | APPLIED_CURRENT_HELP = "Applied current (optional)." 34 | 35 | PENALTY_VALUE_HELP = "Penalty value for finding the inflection point or peak points (optional)." 36 | 37 | MEASURED_CURRENT_UNITS_HELP = "The unit in which the current is measured." 38 | 39 | MEASURED_TIME_UNITS_HELP = "The unit in which the time is measured." 40 | -------------------------------------------------------------------------------- /madap/utils/suggested_circuits.py: -------------------------------------------------------------------------------- 1 | """This is a helper file that contains a dictionary with all the suggested circuits 2 | for the different types of devices. The dictionary is called suggested_circuits 3 | """ 4 | 5 | suggested_circuits = {"R1-CPE1": ["x", 1, 1], 6 | "R1-C1": ["x", 1e-06], 7 | "R1-W1": ["x", 100], 8 | "R1-L1": ["x", 0.0001], 9 | "p(R1,C1)": ["x", 1e-06], 10 | "p(R1,L1)": ["x", 0.0001], 11 | "p(R1,CPE1)": ["x", 1, 1], 12 | "p(C1,R1-CPE1)": [0.0001, "x", 1, 1], 13 | "p(C1,R1-W1)": [0.0001, "x", 5], 14 | "R0-p(R1,C1)": ["x", "y", 0.001], 15 | "R0-p(CPE1,C1)": ["x", 0.001, 1, 0.01], 16 | "R0-p(R1,CPE1)": ["x", "y", 1e-09, 0.8], 17 | "R0-p(R1-Wo1,CPE1)": ["x", "y", 1, 0.1, 1, 1], 18 | "R0-p(R1-Wo1,C1)": ["x", "y", 1, 1, 1e-06], 19 | "L1-R1-p(CPE1,R2)": [0.0002, "x", 2, 0.8, "y"], 20 | "R0-p(R1-C1,C2)": ["x", "y", 0.001, 1], 21 | "R0-p(R1-W1,C1)": ["x", "y", 5, 1e-05], 22 | "R0-p(R1-W1,CPE1)": ["x", "y", 5, 1e-05, 0.8], 23 | "R0-p(R1-CPE1,C1)": ["x", "y", 2, 0.8, 0.01], 24 | "R1-p(CPE1,R2-CPE2)": ["x", 2, 0.8, "y", 0.01, 0.8], 25 | "R1-p(CPE1,R2-p(CPE2,R3))": ["x", 0.05, 0.8, "y", 0.001, 0.8, "z"], 26 | "R1-p(CPE1,R2-p(CPE2,p(R3,L1-R4)))": ["x", 0.05, 0.8, "y", 0.001, 0.8, "z", 1e-05, "t"], 27 | "R1-p(CPE1,R2)-p(CPE2,R3-CPE3)": ["x", 0.05, 0.8, "y", 0.001, 0.8, "z", 0.001, 0.8], 28 | "R1-p(CPE1,R2)-p(CPE2,R3)": ["x", 0.0001, 0.8, "y", 0.1, 0.8, "z"], 29 | "R1-p(CPE1,R2)-p(CPE2,p(R3,L1-R4)": ["x", 0.05, 0.8, "y", 0.001, 0.8, "z", 1e-05, "t"], 30 | "R1-p(CPE1,p(R2,L1-R3)": ["x", 0.05, 0.8, "y", 0.001, "z"], 31 | "R1-p(R2,CPE1-p(CPE2,R3))": ["x", "y", 0.05, 0.8, 0.001, 0.8, "z"], 32 | "R0-p(R1,CPE1)-p(R2,CPE2)-W1": ["x", "y", 0.08, 1, "z", 0.002, 0.62, 0.14], 33 | "R0-p(R1,C1)-p(R2-Wo1,C2)": ["x", "y", 100, "z", 0.05, 100, 1], 34 | "R0-p(R1,C1)-p(R2,C2)-Wo1": ["x", "y", 0.1, "z", 0.1, 0.001, 1], 35 | "p(C1,R1,R2-C2,R3-C3)": [1e-06, "x", "y", 0.0001, "z", 0.01], 36 | "p(C1,R1)-p(R2,C2)": [1e-05, "x", "y", 0.01], 37 | "p(C1,R1)-p(R2,C2)-p(R3,C3)": [1e-05, "x", "y", 0.001, "z", 0.1], 38 | "R1-p(R2,CPE1)-p(R3,CPE2)": ["x", "y", 0.05, 0.8, "z", 0.001, 0.8], 39 | "p(R1,C1)-p(R2,C2)-C3": ["x", 0.0001, "y", 0.01, 1], 40 | "p(CPE1,R1-p(R2,CPE2))": [2, 0.8, "x", "y", 1.5, 0.9]} 41 | -------------------------------------------------------------------------------- /madap/utils/utils.py: -------------------------------------------------------------------------------- 1 | """ This module defines some utility functions for the MADAP project. """ 2 | import time 3 | import json 4 | import os 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | from madap.logger import logger 10 | 11 | 12 | 13 | log = logger.get_logger("utils") 14 | 15 | 16 | def create_dir(directory): 17 | """Checks if a directory is present, if not creates one at the given location 18 | 19 | Args: 20 | directory (str): Location where the directory should be created 21 | """ 22 | 23 | if not os.path.exists(directory): 24 | os.makedirs(directory) 25 | log.info(f"Created directory {directory}.") 26 | else: 27 | log.info(f"Directory {directory} already exists.") 28 | return directory 29 | 30 | 31 | def assemble_file_name(*args): 32 | """Assemble a file name from the given arguments 33 | 34 | Returns: 35 | str: The assembled file name 36 | """ 37 | timestamp = time.strftime("%Y%m%d_%H%M%S_") 38 | return timestamp+"_".join(list(args)) 39 | 40 | 41 | def assemble_data_frame(**kwargs): 42 | """Assemble a data frame from the given arguments 43 | 44 | Returns: 45 | Pandas DataFrame: The assembled data frame 46 | """ 47 | try: 48 | df = pd.DataFrame.from_dict(kwargs, orient = "index") 49 | df = df.transpose() 50 | except AttributeError: 51 | df = pd.DataFrame(data=kwargs) 52 | return df 53 | 54 | 55 | def save_data_as_csv(directory, data, name): 56 | """Save the given data as csv 57 | 58 | Args: 59 | directory (str): The directory where the data should be saved 60 | data (Pandas DataFrame): The data that should be saved 61 | name (str): The name of the file 62 | """ 63 | log.info(f"Saving data in {directory}.csv") 64 | data.to_csv(os.path.join(directory, name)) 65 | 66 | 67 | def save_data_as_json(directory, data, name): 68 | """Save the given data as json 69 | 70 | Args: 71 | directory (str): The directory where the data should be saved 72 | data (dict): The data that should be saved 73 | name (str): The name of the file 74 | """ 75 | log.info(f"Saving data in {directory}.json") 76 | with open(os.path.join(directory, name), 'w', encoding="utf-8") as file: 77 | json.dump(data, file) 78 | 79 | 80 | def load_data_as_json(directory, name): 81 | """ Load the given data as json 82 | 83 | Args: 84 | directory (str): The directory where the data should be saved 85 | name (str): The name of the file 86 | """ 87 | log.info(f"Loading data from {directory} as json") 88 | with open(os.path.join(directory, name), 'r', encoding="utf-8") as file: 89 | data = json.load(file) 90 | return data 91 | 92 | 93 | def append_to_save_data(directory, added_data, name): 94 | """Append the given data to the existing data 95 | 96 | Args: 97 | directory (str): The directory where the data should be saved 98 | added_data (Pandas DataFrame): The data that should be appended 99 | name (str): The name of the file 100 | """ 101 | data = load_data_as_json(directory, name) 102 | data.update(added_data) 103 | save_data_as_json(directory, data, name) 104 | 105 | 106 | def convert_from_pd(obj): 107 | """Helper to convert pandas data to python data 108 | 109 | Args: 110 | obj (pandas.DataFrame): The data that should be converted 111 | Returns: 112 | dict: The converted data 113 | """ 114 | if isinstance(obj, pd.Series): 115 | return obj.to_dict() 116 | if isinstance(obj, dict): 117 | return {k: convert_from_pd(v) for k, v in obj.items()} 118 | if isinstance(obj, list): 119 | return [convert_from_pd(x) for x in obj] 120 | else: 121 | return obj 122 | 123 | 124 | def convert_numpy_to_python(data): 125 | """Convert numpy data to python data 126 | 127 | Args: 128 | data (pandas.DataFrame): The data that should be converted 129 | 130 | Returns: 131 | pd.DataFrame: The converted data 132 | """ 133 | # serializing numpy data to python data 134 | if isinstance(data, dict): 135 | return {k: convert_numpy_to_python(v) for k, v in data.items()} 136 | # serializing numpy int to python int 137 | if isinstance(data, (np.int64, np.int32, np.int16, np.int8)): 138 | return int(data) 139 | # serializing numpy float to python float 140 | if isinstance(data, (np.float64, np.float32, np.float16)): 141 | return float(data) 142 | # serializing numpy array to python list 143 | if isinstance(data, (np.ndarray,)): 144 | return data.tolist() 145 | 146 | return data 147 | 148 | 149 | def get_complementary_color(rgb): 150 | """Returns the complementary color of the given rgb color 151 | 152 | Args: 153 | rgb (list): rgb color 154 | Returns: 155 | list: complementary color 156 | """ 157 | # Convert to 0-255 scale 158 | rgb_255 = [int(x*255) for x in rgb[:3]] 159 | # Calculate complementary color 160 | comp_rgb_255 = [255 - x for x in rgb_255] 161 | # Convert back to 0-1 scale 162 | return [x / 255.0 for x in comp_rgb_255] #+ [1] # Add alpha value of 1 163 | -------------------------------------------------------------------------------- /madap_cli.py: -------------------------------------------------------------------------------- 1 | """ This module is the main entry point for MADAP. It defines the CLI to be used by the user.""" 2 | import argparse 3 | import os 4 | import re 5 | from pathlib import Path 6 | 7 | import pandas as pd 8 | 9 | from madap.data_acquisition import data_acquisition as da 10 | from madap.echem.arrhenius import arrhenius 11 | from madap.echem.e_impedance import e_impedance 12 | from madap.echem.voltammetry import (voltammetry_CA, voltammetry_CP, 13 | voltammetry_CV) 14 | from madap.logger import logger 15 | from madap.utils import utils 16 | 17 | log = logger.setup_applevel_logger(file_name = 'madap_debug.log') 18 | 19 | def _analyze_parser_args(): 20 | """Private function to analyze the parser arguments 21 | 22 | Returns: 23 | argparse: The parser with correct arguments 24 | """ 25 | first_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) 26 | # Options for procedure 27 | procedure = first_parser.add_argument_group("Options for the procedure") 28 | procedure.add_argument("-p", "--procedure", type=str, choices=['arrhenius', 'impedance', 'voltammetry'], 29 | help="Procedure of the analysis") 30 | proc = first_parser.parse_known_args()[0] 31 | if proc.procedure == "impedance": 32 | procedure.add_argument("-ip", "--impedance_procedure", type=str, required=True, choices=['EIS', 'Mottschotcky', 'Lissajous'], 33 | help="Which of the impedance procedures you want to use?") 34 | proc = first_parser.parse_known_args()[0] 35 | if proc.impedance_procedure == "EIS": 36 | eis = first_parser.add_argument_group("Options for the EIS procedure") 37 | # Add the arguments for the EIS procedure 38 | eis.add_argument("-pl", "--plots", required=True, choices=["nyquist" ,"nyquist_fit", "residual", "bode"], 39 | nargs="+", help="plots to be generated") 40 | eis.add_argument("-v", "--voltage", type=float, required=False, default=None, 41 | help="applied voltage [V] if applicable") 42 | eis.add_argument("-cc", "--cell_constant", type=float, required=False, default=None, 43 | help="cell constant if applicable") 44 | eis.add_argument("-sc", "--suggested_circuit", type=str, required=False, default=None, 45 | help="suggested circuit if applicable. \n Available elements are 's', 'C', 'Ws', 'K', 'W', 'Wo', 'R', \ 46 | 'p', 'L', 'TLMQ', 'CPE', 'G', 'La', 'T', 'Gs' \n Parallel circuit can be defined like p(element1, element2)\ 47 | and the series circuit like element1_element2") 48 | eis.add_argument("-iv", "--initial_values", required=False, default=None, 49 | help="initial values for the suggested circuit. \ 50 | \n format: element1, element2, ... \ 51 | \n it will be used just if the suggested_circuit is available.") 52 | 53 | elif proc.impedance_procedure == "Mottschotcky": 54 | # TODO 55 | pass 56 | elif proc.impedance_procedure == "Lissajous": 57 | # TODO 58 | pass 59 | 60 | elif proc.procedure == "arrhenius": 61 | arrhenius_pars = first_parser.add_argument_group("Options for the Arrhenius procedure") 62 | arrhenius_pars.add_argument("-pl", "--plots", choices=["arrhenius" ,"arrhenius_fit"], 63 | nargs="+", required=True, help="Plots to be generated") 64 | 65 | elif proc.procedure == "voltammetry": 66 | voltammetry_pars = first_parser.add_argument_group("Options for the Arrhenius procedure") 67 | voltammetry_pars.add_argument("-vp", "--voltammetry_procedure", type=str, required=True, 68 | choices=['CV', 'CA', "CP"],) 69 | voltammetry_pars.add_argument("-mc", "--measured_current_units", type=str, required=True, 70 | default="uA", choices=["uA", "mA", "A"], help="Measured current units") 71 | voltammetry_pars.add_argument("-mt", "--measured_time_units", type=str, required=False, 72 | default="s", choices=["ms", "s", "min", "h"], help="Measured time units") 73 | voltammetry_pars.add_argument("-ne", "--number_of_electrons", type=int, required=False, 74 | default=1, help="Number of electrons involved in the reaction") 75 | voltammetry_pars.add_argument("-cam", "--concentration_of_active_material", type=float, required=False, 76 | default=None, help="Concentration of the active material [mol/cm^3]") 77 | voltammetry_pars.add_argument("-mam", "--mass_of_active_material", type=float, required=False, 78 | default=None, help="Mass of the active material [g]") 79 | voltammetry_pars.add_argument("-ea", "--electrode_area", type=float, required=False, 80 | default=None, help="Electrode area [cm^2]") 81 | proc = first_parser.parse_known_args()[0] 82 | if proc.voltammetry_procedure == "CV": 83 | cv = first_parser.add_argument_group("Options for the CV procedure") 84 | cv.add_argument("-plcy", "--cycle_list", required=False, default=None, 85 | help="list of cycles to be plotted. \n format: [1, 2, 3] \n if it is not specified, all cycles will be plotted") 86 | cv.add_argument("-pl", "--plots", required=True, choices=["E-t", "I-t", "Peak Scan", "CV", "Tafel"], 87 | nargs="+", help="plots to be generated") 88 | cv.add_argument("-temp", "--temperature", type=float, required=False, default=None, 89 | help="temperature [K] if applicable") 90 | cv.add_argument("-w", "--window_size", type=int, required=True, default=1, help="window size for the moving average") 91 | cv.add_argument("-sc", "--applied_scan_rate", type=float, required=False, default=0.1, 92 | help="Applied scan rate [V/s] if applicable. Default is 0.1 V/s") 93 | elif proc.voltammetry_procedure == "CA": 94 | ca = first_parser.add_argument_group("Options for the CA procedure") 95 | ca.add_argument("-pl", "--plots", required=True, choices=["CA", "Log_CA", "CC", "Cottrell", "Anson", "Voltage"], 96 | nargs="+", help="plots to be generated") 97 | ca.add_argument("-a", "--applied_voltage", type=float, required=False, default=None, 98 | help="applied voltage [V] if applicable") 99 | ca.add_argument("-w", "--window_size", type=int, required=False, default=None, help="window size for the moving average") 100 | elif proc.voltammetry_procedure == "CP": 101 | cp = first_parser.add_argument_group("Options for the CP procedure") 102 | cp.add_argument("-pl", "--plots", required=True, \ 103 | choices=["CP", "CC", "Cottrell", "Voltage_Profile", "Potential_Rate", "Differential_Capacity"], 104 | nargs="+", help="plots to be generated") 105 | cp.add_argument("-ap", "--applied_potential", type=float, required=False, default=None, 106 | help="applied potential [V] if applicable") 107 | cp.add_argument("-pv", "--penalty_value", type=float, required=False, default=None, 108 | help="penalty value for the regularization") 109 | 110 | # Options for data import 111 | data = first_parser.add_argument_group("Options for data import") 112 | data.add_argument("-f", "--file", type=Path, required=True, metavar="FILE", help="Path to the data file") 113 | data.add_argument("-u", "--upper_limit_quantile", type=float, required=False, 114 | default=0.99, help="Upper quantile for detecting the outliers in data") 115 | data.add_argument("-l", "--lower_limit_quantile", type=float, required=False, 116 | default=0.01, help="Lower quantile for detecting the outliers in data") 117 | data_selection = data.add_mutually_exclusive_group() 118 | data_selection.add_argument("-sp", "--specific", type=str, nargs="+", 119 | help="row and column number of the frequency, real impedance, \ 120 | imaginary_impedance and phase shift \ 121 | \n write n if it is not applicable \n order is important.\ 122 | \n format: start_row,end_row,start_column,end_column \ 123 | \n 1,10,1,2 means rows 1 to 10 and columns 1 to 2") 124 | data_selection.add_argument("-hl", "--header_list", type=str, nargs="+", 125 | help="Definitions of the headers for frequency [Hz], \ 126 | real impedance [\u2126],\ 127 | imaginary_impedance [\u2126] and phase shift \u03c6 [\u00b0] \ 128 | \n write n if it is not applicable \n order is important.") 129 | 130 | parser = argparse.ArgumentParser(description='Use MADAP for electrochemical analysis', 131 | parents=[first_parser], 132 | formatter_class=argparse.RawDescriptionHelpFormatter) 133 | # Options for results 134 | parser.add_argument("-r", "--results", type=Path, required=True, 135 | help="Directory for saving results") 136 | 137 | return parser 138 | 139 | 140 | def call_impedance(data, result_dir, args): 141 | """calling the impedance procedure and parse the corresponding arguments 142 | 143 | Args: 144 | data (class): the given data frame for analysiswrite 145 | result_dir (str): the directory for saving results 146 | args (parser.args): Parsed arguments 147 | """ 148 | 149 | if args.header_list: 150 | # Check if args header is a list 151 | if isinstance(args.header_list, list): 152 | header_names = args.header_list[0].split(", ") if len(args.header_list) == 1 else \ 153 | args.header_list 154 | else: 155 | header_names = args.header_list 156 | 157 | phase_shift_data = None if len(header_names) == 3 else data[header_names[3]] 158 | 159 | _, nan_indices = da.remove_outlier_specifying_quantile(df = data, 160 | columns = [header_names[1], 161 | header_names[2]], 162 | low_quantile = args.lower_limit_quantile, 163 | high_quantile = args.upper_limit_quantile) 164 | # remove nan rows 165 | data = da.remove_nan_rows(data, nan_indices) 166 | # extracting the data 167 | freq_data, real_data, imag_data = data[header_names[0]],\ 168 | data[header_names[1]],\ 169 | data[header_names[2]] 170 | 171 | if args.specific: 172 | 173 | try: 174 | if len(args.specific) >= 3: 175 | row_col = args.specific 176 | else: 177 | row_col = re.split('; |;', args.specific[0]) 178 | 179 | except ValueError as e: 180 | log.error("The format of the specific data is not correct. Please check the help.") 181 | raise e 182 | 183 | selected_data = data.iloc[int(row_col[0].split(',')[0]): int(row_col[0].split(',')[1]), :] 184 | 185 | 186 | phase_shift_data = None if len(row_col) == 3 else da.select_data(data, row_col[3]) 187 | 188 | 189 | freq_data, real_data, imag_data = da.select_data(selected_data, row_col[0]), \ 190 | da.select_data(selected_data, row_col[1]), \ 191 | da.select_data(selected_data, row_col[2]) 192 | 193 | unprocessed_data = pd.DataFrame({"freq": freq_data, "real": real_data, "imag": imag_data}) 194 | 195 | _, nan_indices = da.remove_outlier_specifying_quantile(df = unprocessed_data, 196 | columns = ["real", "imag"], 197 | low_quantile = args.lower_limit_quantile, 198 | high_quantile = args.upper_limit_quantile) 199 | 200 | data = da.remove_nan_rows(unprocessed_data, nan_indices) 201 | freq_data, real_data, imag_data = data["freq"], data["real"], data["imag"] 202 | 203 | impedance = e_impedance.EImpedance(da.format_data(freq_data), da.format_data(real_data), 204 | da.format_data(imag_data), da.format_data(phase_shift_data)) 205 | 206 | if args.impedance_procedure == "EIS": 207 | log.info(f"The given voltage is {args.voltage} [V], cell constant is {args.cell_constant},\ 208 | suggested circuit is {args.suggested_circuit} \ 209 | and initial values are {args.initial_values}.") 210 | 211 | # Instantiate the procedure 212 | procedure = e_impedance.EIS(impedance, voltage=args.voltage, 213 | suggested_circuit=args.suggested_circuit, 214 | initial_value=eval(args.initial_values) 215 | if args.initial_values else None, 216 | cell_constant=args.cell_constant) 217 | 218 | elif args.impedance_procedure == "Mottschotcky": 219 | #TODO 220 | # # Instantiate the procedure 221 | pass 222 | elif args.impedance_procedure == "Lissajous": 223 | #TODO 224 | # # Instantiate the procedure 225 | pass 226 | 227 | # Format plots arguments 228 | plots = da.format_list(args.plots) 229 | 230 | # Perform all actions 231 | procedure.perform_all_actions(result_dir, plots=plots) 232 | 233 | return procedure 234 | 235 | def call_arrhenius(data, result_dir, args): 236 | """Calling the arrhenius procedure and parse the corresponding arguments 237 | 238 | Args: 239 | data (class): the given data frame for analysis 240 | result_dir (str): the directory for saving results 241 | args (parser.args): Parsed arguments 242 | """ 243 | 244 | 245 | if args.header_list: 246 | if isinstance(args.header_list, list): 247 | header_names = args.header_list[0].split(", ") if len(args.header_list) == 1 else \ 248 | args.header_list 249 | else: 250 | header_names = args.header_list 251 | 252 | temp_data, cond_data = data[header_names[0]], data[header_names[1]] 253 | if args.specific: 254 | 255 | try: 256 | if len(args.specific) == 2: 257 | row_col = args.specific 258 | else: 259 | row_col = re.split('; |;', args.specific[0]) 260 | 261 | except ValueError as e: 262 | log.error("The format of the specific data is not correct. Please check the help.") 263 | raise e 264 | 265 | selected_data = data.iloc[int(row_col[0].split(',')[0]): int(row_col[0].split(',')[1]), :] 266 | 267 | #row_col = args.specific[0].split(", ") 268 | 269 | temp_data, cond_data = da.select_data(selected_data, row_col[0]), \ 270 | da.select_data(selected_data, row_col[1]) 271 | if (not isinstance(temp_data, pd.Series)) and (not isinstance(cond_data, pd.Series)): 272 | temp_data, cond_data = pd.Series(temp_data).astype(float), pd.Series(cond_data).astype(float) 273 | # Instantiate the procedure 274 | arrhenius_cls = arrhenius.Arrhenius(da.format_data(temp_data), da.format_data(cond_data)) 275 | 276 | # Format the plots arguments 277 | plots = da.format_list(args.plots) 278 | 279 | # Perform all actions 280 | arrhenius_cls.perform_all_actions(result_dir, plots = plots) 281 | 282 | return arrhenius_cls 283 | 284 | def call_voltammetry(data, result_dir, args): 285 | """ Calling the voltammetry procedure and parse the corresponding arguments 286 | 287 | Args: 288 | data (class): the given data frame for analysis 289 | result_dir (str): the directory for saving results 290 | plots (list): list of plots to be generated 291 | """ 292 | if args.header_list: 293 | # Check if args header is a list 294 | if isinstance(args.header_list, list): 295 | header_names = args.header_list[0].split(", ") if len(args.header_list) == 1 else \ 296 | args.header_list 297 | else: 298 | header_names = args.header_list 299 | 300 | 301 | if len(header_names) == 2: 302 | current_data, voltage_data = data[header_names[0]], data[header_names[1]] 303 | time_data = None 304 | unavailable_plots = [] 305 | # Define unavailable plots based on voltammetry procedure 306 | if args.voltammetry_procedure == "CV": 307 | unavailable_plots = ["E-t", "I-t", "Peak Scan"] 308 | # Tafel analysis might be limited without time data 309 | if "Tafel" in args.plots: 310 | log.warning("Tafel analysis may be limited without time data.") 311 | elif args.voltammetry_procedure == "CA": 312 | unavailable_plots = ["CA", "Log_CA", "CC", "Cottrell", "Anson"] 313 | elif args.voltammetry_procedure == "CP": 314 | unavailable_plots = ["CP", "CC", "Cottrell", "Potential_Rate"] 315 | 316 | # Check if any requested plots are unavailable 317 | unavailable_requested = [plot for plot in args.plots if plot in unavailable_plots] 318 | if unavailable_requested: 319 | log.warning(f"The following plots are not available without time data: {unavailable_requested}") 320 | log.info("Will proceed with generating available plots only.") 321 | 322 | elif len(header_names) == 3: 323 | # extracting the data 324 | current_data, voltage_data, time_data = data[header_names[0]],\ 325 | data[header_names[1]],\ 326 | data[header_names[2]] 327 | 328 | if len(header_names) == 4: 329 | charge_data = da.format_data(data[header_names[3]]) 330 | else: 331 | charge_data = None 332 | cycle_list = da.format_list(eval(args.cycle_list)) if args.cycle_list else None 333 | 334 | if args.voltammetry_procedure == "CA": 335 | voltammetry_cls = voltammetry_CA.Voltammetry_CA(current=da.format_data(current_data), 336 | voltage=da.format_data(voltage_data), 337 | time =da.format_data(time_data), 338 | charge=charge_data, 339 | args=args) 340 | if args.voltammetry_procedure == "CV": 341 | voltammetry_cls = voltammetry_CV.Voltammetry_CV(current=da.format_data(current_data), 342 | voltage=da.format_data(voltage_data), 343 | time_params =da.format_data(time_data), 344 | scan_rate=charge_data, 345 | cycle_list=cycle_list, 346 | args=args) 347 | if args.voltammetry_procedure == "CP": 348 | voltammetry_cls = voltammetry_CP.Voltammetry_CP(current=da.format_data(current_data), 349 | voltage=da.format_data(voltage_data), 350 | time =da.format_data(time_data), 351 | charge=charge_data, 352 | args=args) 353 | 354 | # Format plots arguments and filter out unavailable plots if time_data is None 355 | plots = da.format_list(args.plots) 356 | if time_data is None: 357 | if args.voltammetry_procedure == "cv": 358 | unavailable_plots = ["E-t", "I-t", "Peak Scan"] 359 | elif args.voltammetry_procedure == "ca": 360 | unavailable_plots = ["CA", "Log_CA", "CC", "Cottrell", "Anson"] 361 | elif args.voltammetry_procedure == "cp": 362 | unavailable_plots = ["CP", "CC", "Cottrell", "Potential_Rate"] 363 | 364 | # Filter out unavailable plots 365 | plots = [plot for plot in plots if plot not in unavailable_plots] 366 | 367 | if not plots: 368 | log.error("All requested plots require time data which is not available.") 369 | raise ValueError("No available plots to generate. Please provide time data or select plots that don't require it.") 370 | 371 | try: 372 | voltammetry_cls.perform_all_actions(result_dir, plots=plots) 373 | except ValueError as e: 374 | log.error("The plot you selected is not available. Please check the help.") 375 | raise e 376 | return voltammetry_cls 377 | 378 | 379 | def start_procedure(args): 380 | """Function to prepare the data for analysis. 381 | It also prepares folder for results and plots. 382 | 383 | Args: 384 | args (object): Object containing arguments from parser or gui. 385 | """ 386 | 387 | data = da.acquire_data(args.file) 388 | da.remove_unnamed_col(data) 389 | log.info(f"the header of your data is: \n {data.head()}") 390 | result_dir = utils.create_dir(os.path.join(args.results, args.procedure)) 391 | 392 | if args.procedure in ["impedance", "Impedance"]: 393 | procedure = call_impedance(data, result_dir, args) 394 | 395 | elif args.procedure in ["arrhenius", "Arrhenius"]: 396 | procedure = call_arrhenius(data, result_dir, args) 397 | 398 | elif args.procedure in ["voltammetry", "Voltammetry"]: 399 | procedure = call_voltammetry(data, result_dir, args) 400 | log.info("==================================DONE==================================") 401 | return procedure 402 | 403 | def main(): 404 | """Main function to start the program. 405 | """ 406 | log.info("==================================WELCOME TO MADAP==================================") 407 | # Create the parser 408 | parser = _analyze_parser_args() 409 | # Parse the argument 410 | args = parser.parse_args() 411 | 412 | # Acquire data 413 | start_procedure(args) 414 | 415 | 416 | if __name__ == "__main__": 417 | main() 418 | -------------------------------------------------------------------------------- /madap_gui.py: -------------------------------------------------------------------------------- 1 | """This module implements the GUI application for MADAP """ 2 | import io 3 | import queue 4 | import threading 5 | 6 | import PySimpleGUI as sg 7 | 8 | import matplotlib.pyplot as plt 9 | from matplotlib.backends.backend_tkagg import FigureCanvasAgg 10 | 11 | from madap.logger.logger import log_queue 12 | from madap.utils import gui_elements 13 | from madap_cli import start_procedure 14 | class MadapGui: 15 | # pylint: disable=too-many-instance-attributes 16 | """This class implements the GUI application for MADAP 17 | """ 18 | eis_plots = ["nyquist" ,"nyquist_fit", "residual", "bode"] 19 | arrhenius_plots = ["arrhenius", "arrhenius_fit"] 20 | ca_plots = ["CA", "Log_CA", "CC", "Cottrell", "Anson", "Voltage"] 21 | cp_plots = ["CP", "CC", "Cottrell", "Voltage_Profile", "Potential_Rate", "Differential_Capacity"] 22 | cv_plots = ["E-t", "I-t", "Peak-Scan", "CV", "Tafel"] 23 | 24 | def __init__(self): 25 | self.procedure = "Impedance" 26 | self.impedance_procedure= "EIS" 27 | self.file = None 28 | self.results = None 29 | self.header_list = None 30 | self.specific= None 31 | self.plots = None 32 | self.voltage = None 33 | self.cell_constant = None 34 | self.suggested_circuit = None 35 | self.initial_values = None 36 | self.upper_limit_quantile = None 37 | self.lower_limit_quantile = None 38 | self.voltammetry_procedure = None 39 | self.applied_current = None 40 | self.measured_current_units = None 41 | self.measured_time_units = None 42 | self.applied_voltage = None 43 | self.mass_of_active_material = None 44 | self.electrode_area = None 45 | self.concentration_of_active_material = None 46 | self.number_of_electrons = None 47 | self.window_size = None 48 | self.cycle_list = None 49 | self.penalty_value = None 50 | self.temperature = None 51 | self.applied_scan_rate = None 52 | 53 | # pylint: disable=inconsistent-return-statements 54 | # pylint: disable=too-many-return-statements 55 | def validate_fields(self): 56 | """ This function validates the fields in the GUI 57 | 58 | Returns: 59 | bool: Returns false if any of the fields are invalid. 60 | """ 61 | if self.file == '': 62 | sg.popup_error('The data path is empty. Select a supported dataset file.', title='Input Error') 63 | return False 64 | if self.results == '': 65 | sg.popup_error('The result path is empty. Select a location for the results.', title='Input Error') 66 | return False 67 | if self.plots == []: 68 | sg.popup_error('Select the desired plot(s).', title='Input Error') 69 | return False 70 | if self.procedure == 'Impedance': 71 | if self.header_list and (len(self.header_list) not in [3,4]): 72 | sg.popup_error('Wrong number of header inputs.', title='Input Error') 73 | return False 74 | if self.specific and (len(self.specific) not in [3,4]): 75 | sg.popup_error('Wrong number of specific inputs.', title='Input Error') 76 | return False 77 | if self.procedure == 'Arrhenius': 78 | if self.header_list and (len(self.header_list) != 2): 79 | sg.popup_error('Wrong number of header inputs.', title='Input Error') 80 | return False 81 | if self.specific and (len(self.specific) != 2): 82 | sg.popup_error('Wrong number of specific inputs.', title='Input Error') 83 | return False 84 | # Check if the str of numbers of electrons is a number whole number 85 | if self.number_of_electrons and not self.number_of_electrons.isdigit(): 86 | sg.popup_error('Number of electrons must be a number.', title='Input Error') 87 | return False 88 | return True 89 | 90 | 91 | def draw_figure(element, figure): 92 | """ 93 | Draws the previously created "figure" in the supplied Image Element 94 | 95 | Args: 96 | element (PySimpleGUI.Image): The image element to draw the figure on 97 | figure (matplotlib.figure.Figure): The figure to draw 98 | """ 99 | 100 | plt.close('all') # erases previously drawn plots 101 | figure.set_dpi(120) 102 | canv = FigureCanvasAgg(figure) 103 | buf = io.BytesIO() 104 | canv.print_figure(buf, format='png') 105 | if buf is None: 106 | return None 107 | buf.seek(0) 108 | element.update(data=buf.read()) 109 | return canv 110 | 111 | 112 | def gui_layout(madap, colors): 113 | """ This function creates the layout of the GUI 114 | 115 | Args: 116 | madap (MadapGui): The MadapGui object 117 | colors (dict): The colors of the GUI 118 | 119 | Returns: 120 | list: The layout of the layout of the GUI 121 | """ 122 | 123 | # ----------- Create a layout with 3 buttons for the different procedures ----------- # 124 | layout_buttons = [[ sg.Button("Impedance", key="-BUT_Impedance-", button_color=('#F23D91', 'black'), font=("Arial", 14, "bold")), 125 | sg.Button("Arrhenius", key="-BUT_Arrhenius-", button_color=colors, font=("Arial", 14, "bold")), 126 | sg.Button("Voltammetry", key="-BUT_Voltammetry-", button_color=colors, font=("Arial", 14, "bold"))]] 127 | 128 | # ----------- Create a layout with a field for a data path and a result path ----------- # 129 | layout_data = [[sg.Text('Data Path', size=(10, 1)), sg.InputText(key='-DATA_PATH-', 130 | size=(55,1), 131 | default_text="path/to/data"), 132 | sg.FileBrowse(key='-BROWSE_DATA_PATH-')], 133 | [sg.Text('Result Path', size=(10, 1)), sg.InputText(key='-RESULT_PATH-', 134 | size=(55,1), 135 | default_text="path/to/results"), 136 | sg.FolderBrowse(key='-BROWSE_RESULT_PATH-')], 137 | ] 138 | 139 | # ----------- Create a layout with a field for a data selection options ----------- # 140 | layout_data_selection = [[sg.Text('Headers or specific',justification='left', font=("Arial", 13))], 141 | [sg.Combo(['Headers', 'Specific Region'], key='-HEADER_OR_SPECIFIC-', 142 | default_value='Headers')], 143 | [sg.InputText(key='-HEADER_OR_SPECIFIC_VALUE-', 144 | tooltip=gui_elements.HEADER_OR_SPECIFIC_HELP, 145 | default_text="freq, real, imag")]] 146 | 147 | 148 | # ----------- Create tabs for Impedance procedure ----------- # 149 | # pylint: disable=unnecessary-comprehension 150 | tab_layout_eis = [[sg.Text('This are the parameters for the EIS procedure', font=("Arial", 11))], 151 | [sg.Text('Voltage (optional)',justification='left', font=("Arial", 11),pad=(1,(10,0))), 152 | sg.InputText(key="-voltage-", tooltip=gui_elements.VOLTAGE_HELP, 153 | enable_events=True, size=(10,1), pad=((97,0), (10,0))), sg.Text('[V]', pad=((7,0),(10,0)))], 154 | 155 | [sg.Text('Cell constant (optional)',justification='left', font=("Arial", 11),pad=(1,(10,0))), 156 | sg.InputText(key="-cell_constant-", tooltip=gui_elements.CELL_CONSTANT_HELP, 157 | enable_events=True, size=(10,1), pad=((60,0), (10,0))), sg.Text('[1/cm]', pad=((7,0),(10,0)))], 158 | [sg.Text("Upper limit of quantile (optional)",justification='left', 159 | font=("Arial", 11), pad=(1,(10,0))), 160 | sg.InputText(key="-upper_limit_quantile-", tooltip=gui_elements.UPPER_LIMIT_QUANTILE_HELP, 161 | enable_events=True, default_text="0.99", size=(10,1), pad=((5,0), (10,0)))], 162 | [sg.Text("Lower limit of quantile (optional)",justification='left', 163 | font=("Arial", 11), pad=(1,(10,0))), 164 | sg.InputText(key="-lower_limit_quantile-", tooltip=gui_elements.LOWER_LIMIT_QUANTILE_HELP, 165 | enable_events=True, default_text="0.01", size=(10,1), pad=((5,0), (10,0)))], 166 | [sg.Text('Suggested Circuit',justification='left', font=("Arial", 11), 167 | pad=(1,(10,0))), 168 | sg.InputText(key="-suggested_circuit-", tooltip=gui_elements.SUGGESTED_CIRCUIT_HELP, 169 | default_text="R0-p(R1,CPE1)", size=(25,1), pad=((93,0), (10,0)))], 170 | [sg.Text('Initial Value', justification='left', font=("Arial", 11), pad=(1,(10,0))), 171 | sg.InputText(key="-initial_value-", enable_events=True, tooltip=gui_elements.INITIAL_VALUES_HELP, 172 | default_text="[860, 3e+5, 1e-09, 0.90]", size=(25,1), pad=((137,0), (10,0)))], 173 | [sg.Text('Plots',justification='left', font=("Arial", 11), pad=(1,(10,0)))], 174 | [sg.Listbox([x for x in madap.eis_plots], key='-PLOTS_Impedance-', 175 | size=(33,len(madap.eis_plots)), select_mode=sg.SELECT_MODE_MULTIPLE, 176 | expand_x=True, expand_y=True)]] 177 | 178 | tab_layout_liss = [[sg.Text('WORK IN PROGRESS: Lissajous')], 179 | [sg.Input(key='-inLiss-')]] 180 | 181 | tab_layout_mott = [[sg.Text('WORK IN PROGRESS: Mottschosky')], 182 | [sg.Input(key='-inMott-')]] 183 | 184 | # ----------- Create tabs for Voltammetry procedure ----------- # 185 | tab_layout_ca = [ 186 | [sg.Text('Applied Voltage (optional)',justification='left', font=("Arial", 11), pad=(1,(15,0))), 187 | sg.InputText(key='-inCAVoltage-', default_text="0.43", tooltip=gui_elements.VOLTAGE_HELP, size=(10, 1), 188 | pad=((5,0),(10,0))), sg.Text('[V]', pad=((7,0),(10,0)))], 189 | [sg.Text('Window size (optional)',justification='left', font=("Arial", 11), pad=(1,(10,0))), 190 | sg.InputText(key='-inVoltWindowSize-', default_text="20000", tooltip=gui_elements.WINDOW_SIZE_HELP, size=(10, 1), 191 | pad=((20,0),(10,0)))], 192 | [sg.Text('Plots',justification='left', font=("Arial", 11), pad=(1,(10,0)))], 193 | [sg.Listbox([x for x in madap.ca_plots], key='-PLOTS_CA-', 194 | size=(33,len(madap.ca_plots)), select_mode=sg.SELECT_MODE_MULTIPLE, 195 | expand_x=False, expand_y=False)]] 196 | tab_layout_cp = [[sg.Text('Applied Current (optional)',justification='left', font=("Arial", 11), pad=(1,(15,0))), 197 | sg.InputText(key='-inCPCurrent-', default_text="0.000005", tooltip=gui_elements.APPLIED_CURRENT_HELP, size=(10, 1), 198 | pad=((5,0),(10,0))), sg.Text('[A]', pad=((7,0),(10,0)))], 199 | [sg.Text('Penalty value (optional)',justification='left', font=("Arial", 11), pad=(1,(10,0))), 200 | sg.InputText(key='-inPenaltyValue-', default_text="0.25", tooltip=gui_elements.PENALTY_VALUE_HELP, size=(10, 1), 201 | pad=((20,1), (10,0)))], 202 | [sg.Text('Plots',justification='left', font=("Arial", 11), pad=(1,(10,0)))], 203 | [sg.Listbox([x for x in madap.cp_plots], key='-PLOTS_CP-', 204 | size=(33,len(madap.cp_plots)), select_mode=sg.SELECT_MODE_MULTIPLE, 205 | expand_x=False, expand_y=False)]] 206 | tab_layout_cv = [[sg.Text('Plotted Cycle(s) (optional)',justification='left', font=("Arial", 11), pad=(1,(15,0))), 207 | sg.InputText(key='-inPlotCycleList-', default_text="1", tooltip=gui_elements.WINDOW_SIZE_HELP, size=(10, 1), 208 | pad=(1,(10,0)))], 209 | [sg.Text('Temperature (optional)',justification='left', font=("Arial", 11), pad=(1,(10,0))), 210 | sg.Input(key='-inCVTemperature-', default_text="298.15", size=(10, 1), pad=((20,0),(10,0))), sg.Text('[K]', 211 | pad=((7,0),(10,0)))], 212 | [sg.Text('Applied Scan Rate (optional)',justification='left', font=("Arial", 11), pad=(1,(10,0))), 213 | sg.Input(key='-inCVScanRate-', default_text="0.1", size=(10, 1), pad=((20,0),(10,0))), sg.Text('[V/s]', 214 | pad=((7,0),(10,0)))], 215 | [sg.Text('Plots',justification='left', font=("Arial", 11), pad=(1,(10,0)))], 216 | [sg.Listbox([x for x in madap.cv_plots], key='-PLOTS_CV-', 217 | size=(33,len(madap.cv_plots)), select_mode=sg.SELECT_MODE_MULTIPLE, 218 | expand_x=False, expand_y=False)]] 219 | 220 | # ----------- Layout the Impedance Options (Three TABS) ----------- # 221 | layout_impedance = [[sg.TabGroup( 222 | [[sg.Tab('EIS', tab_layout_eis, key='-TAB_EIS-', expand_y=True), 223 | sg.Tab('Lissajous', tab_layout_liss, background_color='darkred', 224 | key='-TAB_Lissajous-', expand_y=True), 225 | sg.Tab('Mottschosky', tab_layout_mott, background_color='darkgreen', 226 | key='-TAB_Mottschosky-', expand_y=True)]], 227 | tab_location='topleft', selected_title_color='#F23D91', tab_background_color = "#FF7F69", 228 | enable_events=True, expand_y=True, font=("Arial", 12, "bold"), pad=(1,(5,0)))]] 229 | 230 | # ----------- Layout the Arrhenius Options ----------- # 231 | layout_arrhenius = [[sg.Text('Plots',justification='left', font=("Arial", 11), pad=(1,(20,0)))], 232 | [sg.Listbox([x for x in madap.arrhenius_plots], key='-PLOTS_Arrhenius-', 233 | size=(33,len(madap.arrhenius_plots)+1), 234 | select_mode=sg.SELECT_MODE_MULTIPLE, expand_x=True, 235 | expand_y=True)]] 236 | 237 | # ----------- TODO Layout the Voltammetry Options ----------- # 238 | layout_voltammetry = [ 239 | [sg.Text('Measured Current Units', justification='left', font=("Arial", 11), pad=(1,(15,0))), 240 | sg.Combo(['A', 'mA', 'uA'], key='-inVoltUnits-', default_value='A', enable_events=True, pad=(10,(15,0)), 241 | tooltip=gui_elements.MEASURED_CURRENT_UNITS_HELP, size=(5, 1))], 242 | 243 | [sg.Text('Measured Time Units', justification='left', font=("Arial", 11), pad=(1,(15,0))), 244 | sg.Combo(['h', 'min', 's', 'ms'], key='-inVoltTimeUnits-', default_value='s', enable_events=True, pad=(25,(15,0)), 245 | tooltip=gui_elements.MEASURED_TIME_UNITS_HELP, size=(5, 1))], 246 | 247 | [sg.Text('Number of electrons n', justification='left', font=("Arial", 11), pad=(1,(15,0))), 248 | sg.InputText(key='-inVoltNumberElectrons-', default_text="1", size=(5, 1), pad=(20,(15,0)))], 249 | 250 | [sg.Text('Concentration of active material (optional)', justification='left', font=("Arial", 11), pad=(1,(15,0))), 251 | sg.InputText(key='-inVoltConcentration-', default_text="1", size=(5, 1), pad=(10,(15,0))), 252 | sg.Text('[mol/cm3]', justification='left', font=("Arial", 11), pad=(1,(15,0)))], 253 | 254 | [sg.Text('Mass of active material (optional)',justification='left', font=("Arial", 11), pad=(1,(15,0))), 255 | sg.InputText(key='-inCAMass-', default_text="0.0001", size=(10, 1), pad=((65,5),(15,0))), 256 | sg.Text('[g]', justification='left', font=("Arial", 11), pad=(1,(15,0)))], 257 | 258 | [sg.Text('Electrode area (optional)',justification='left', font=("Arial", 11), pad=(1,(15,0))), 259 | sg.InputText(key='-inCAArea-', default_text="0.196", size=(10, 1), pad=((120,5),(15,0))), 260 | sg.Text('[cm2]', justification='left', font=("Arial", 11), pad=(1,(15,0)))], 261 | 262 | [sg.TabGroup([[sg.Tab('Chrono-Potentiometry', tab_layout_cp, key='-TAB_CP-', expand_y=True), 263 | sg.Tab('Chrono-Amperomtery', tab_layout_ca, key='-TAB_CA-', expand_y=True), 264 | sg.Tab('Cyclic Voltammetry', tab_layout_cv, key='-TAB_CV-', expand_y=True)]], 265 | tab_location='topleft', selected_title_color='#F23D91', background_color='#282312', 266 | tab_background_color = "#FF7F69", 267 | enable_events=True, expand_y=True, font=("Arial", 12, "bold"), pad=(1,(40,0)))] 268 | ] 269 | 270 | 271 | 272 | # ----------- Assemble the Procedure Column Element with the three layouts ----------- # 273 | procedure_column = [[sg.Column(layout_impedance, key='-COL_Impedance-', scrollable=True, 274 | vertical_scroll_only=True, expand_x=True, expand_y=True), 275 | sg.Column(layout_arrhenius, visible=False, key='-COL_Arrhenius-', 276 | scrollable=True, vertical_scroll_only=True, expand_x=True, 277 | expand_y=True), 278 | sg.Column(layout_voltammetry, visible=False, key='-COL_Voltammetry-', 279 | scrollable=True, vertical_scroll_only=True, expand_x=True, 280 | expand_y=True)]] 281 | 282 | # ----------- Assemble the left Column Element ----------- # 283 | col1 = sg.Column([[sg.Frame('Data Selection:', layout_data_selection, font=("Arial", 15, "bold"), 284 | size=(550, 120), expand_y=True)], 285 | [sg.Frame('Methods:', procedure_column, font=("Arial", 15, "bold"), size=(550, 550), 286 | expand_y=True)]], 287 | expand_x=True, expand_y=True) 288 | 289 | # ----------- Layout the right Column Element ----------- # 290 | col2 = sg.Column([[sg.Frame('Plots:', [[sg.Image(key='-IMAGE-')]], visible=False, font=("Arial", 15, "bold"), 291 | key='-COL_PLOTS-')]]) 292 | 293 | # ----------- Assemble the main layout ----------- # 294 | layout = [ 295 | [layout_buttons], 296 | [layout_data], 297 | [col1, col2], 298 | [sg.Text('',justification='left', font=("Arial", 13), pad=(1,(20,0)), key='-LOG-', 299 | enable_events=True)], 300 | [sg.Button('RUN'), sg.Button('EXIT')], 301 | [sg.Multiline(size=(100, 5), key='LogOutput', autoscroll=True, background_color='black', text_color='#0CF2F2')],] 302 | 303 | return layout 304 | 305 | 306 | def main(): 307 | # pylint: disable=too-many-branches 308 | """Main function of the GUI 309 | """ 310 | 311 | # Defining the custom theme 'MADAP' using the provided hex values 312 | sg.LOOK_AND_FEEL_TABLE['MADAP'] = { 313 | 'BACKGROUND': '#0D0D0D', 314 | 'TEXT': '#0CF2F2', 315 | 'INPUT': '#E5B9AB', 316 | 'TEXT_INPUT': '#2B2840', 317 | 'SCROLL': '#2B2840', 318 | 'BUTTON': ('white', '#2B2840'), 319 | 'PROGRESS': ('#F27166', '#0D0D0D'), 320 | 'BORDER': 1, 'SLIDER_DEPTH': 0, 'PROGRESS_DEPTH': 0, 321 | } 322 | 323 | # Select a theme 324 | sg.theme("MADAP") 325 | 326 | # Create class with initial values 327 | madap_gui = MadapGui() 328 | 329 | # Get primary colors and assemble window 330 | colors = (sg.theme_text_color(), sg.theme_background_color()) 331 | layout = gui_layout(madap_gui, colors) 332 | title = 'MADAP: Modular Automatic Data Analysis Platform' 333 | window = sg.Window(title, layout, icon="logo.ico", resizable=True) 334 | # Shared variable for the return value and a flag to indicate completion 335 | procedure_result = [None] # List to hold the return value 336 | procedure_complete = [False] # Flag to indicate completion 337 | procedure_error = [None] 338 | # Initialization for log update 339 | log_update_counter = 0 340 | log_update_max = 30 341 | is_procedure_running = False 342 | 343 | def procedure_wrapper(): 344 | try: 345 | procedure_result[0] = start_procedure(madap_gui) 346 | except ValueError as e: 347 | procedure_error[0] = e 348 | finally: 349 | procedure_complete[0] = True 350 | # Event loop 351 | while True: 352 | event, values = window.read(timeout=100) 353 | if event in (sg.WIN_CLOSED, 'EXIT'): 354 | break 355 | if event in ['-BUT_Impedance-', '-BUT_Arrhenius-', '-BUT_Voltammetry-']: 356 | if event == '-BUT_Voltammetry-': 357 | # empty HEADER_OR_SPECIFIC_VALUE 358 | window['-HEADER_OR_SPECIFIC_VALUE-']('current, voltage, time') 359 | event = event.strip('-BUT_') 360 | window[f'-COL_{madap_gui.procedure}-'].update(visible=False) 361 | window[f'-BUT_{madap_gui.procedure}-'].update(button_color=colors) 362 | window[f'-COL_{event}-'].update(visible=True) 363 | window[f'-BUT_{event}-'].update(button_color=('#F23D91', 'black')) 364 | madap_gui.procedure = event 365 | if values[0] in ['-TAB_EIS-', '-TAB_Lissajous-', '-TAB_Mottschotcky-']: 366 | madap_gui.impedance_procedure = values[0].strip('-TAB_') 367 | if values[1] in ['-TAB_CA-', '-TAB_CP-', '-TAB_CV-']: 368 | madap_gui.voltammetry_procedure = values[1].split('_')[1].strip("-") 369 | 370 | # Prevent the user from inoutting a value that is not a number in the voltage, cell constant and initial_value input field 371 | if event == '-voltage-' and len(values['-voltage-']) \ 372 | and values['-voltage-'][-1] not in '012345678890,.': 373 | window['-voltage-'].update(values['-voltage-'][:-1]) 374 | if event == '-cell_constant-' and len(values['-cell_constant-']) \ 375 | and values['-cell_constant-'][-1] not in '012345678890,.': 376 | window['-cell_constant-'].update(values['-cell_constant-'][:-1]) 377 | if event == '-upper_limit_quantile-' and len(values['-upper_limit_quantile-']) \ 378 | and values['-upper_limit_quantile-'][-1] not in '012345678890,.': 379 | window['-upper_limit_quantile-'].update(values['-upper_limit_quantile-'][:-1]) 380 | if event == '-lower_limit_quantile-' and len(values['-lower_limit_quantile-']) \ 381 | and values['-lower_limit_quantile-'][-1] not in '012345678890,.': 382 | window['-lower_limit_quantile-'].update(values['-lower_limit_quantile-'][:-1]) 383 | 384 | if event == '-initial_value-' and len(values['-initial_value-']) \ 385 | and values['-initial_value-'][-1] not in '012345678890,.e-+[]': 386 | window['-initial_value-'].update(values['-initial_value-'][:-1]) 387 | if event == 'RUN': 388 | window['-LOG-'].update('Starting procedure...') 389 | madap_gui.file = values['-DATA_PATH-'] 390 | madap_gui.results = values['-RESULT_PATH-'] 391 | # TODO: this needs to be expanded for Voltammetry 392 | if madap_gui.procedure in ('Impedance', 'Arrhenius'): 393 | madap_gui.plots = values[f'-PLOTS_{madap_gui.procedure}-'] 394 | if madap_gui.procedure == 'Voltammetry': 395 | madap_gui.plots = values[f'-PLOTS_{madap_gui.voltammetry_procedure}-'] 396 | madap_gui.voltage = values['-voltage-'] 397 | madap_gui.cell_constant = values['-cell_constant-'] 398 | madap_gui.suggested_circuit = values['-suggested_circuit-'] \ 399 | if not values['-suggested_circuit-'] == '' else None 400 | madap_gui.initial_values = values['-initial_value-'] \ 401 | if not values['-initial_value-'] == '' else None 402 | madap_gui.upper_limit_quantile = values['-upper_limit_quantile-'] \ 403 | if not values['-upper_limit_quantile-'] == '' else None 404 | madap_gui.lower_limit_quantile = values['-lower_limit_quantile-'] \ 405 | if not values['-lower_limit_quantile-'] == '' else None 406 | madap_gui.applied_current = values['-inCPCurrent-'] \ 407 | if not values['-inCPCurrent-'] == '' else None 408 | madap_gui.measured_current_units = values['-inVoltUnits-'] 409 | madap_gui.measured_time_units = values['-inVoltTimeUnits-'] 410 | madap_gui.applied_voltage = values['-inCAVoltage-'] \ 411 | if not values['-inCAVoltage-'] == '' else None 412 | madap_gui.mass_of_active_material = values['-inCAMass-'] \ 413 | if not values['-inCAMass-'] == '' else None 414 | madap_gui.electrode_area = values['-inCAArea-'] \ 415 | if not values['-inCAArea-'] == '' else None 416 | madap_gui.concentration_of_active_material = values['-inVoltConcentration-'] \ 417 | if not values['-inVoltConcentration-'] == '' else None 418 | madap_gui.number_of_electrons = values['-inVoltNumberElectrons-'] \ 419 | if not values['-inVoltNumberElectrons-'] == '' else None 420 | madap_gui.window_size = values['-inVoltWindowSize-'] \ 421 | if not values['-inVoltWindowSize-'] == '' else None 422 | madap_gui.cycle_list = values['-inPlotCycleList-'] \ 423 | if not values['-inPlotCycleList-'] == '' else None 424 | madap_gui.penalty_value = values['-inPenaltyValue-'] \ 425 | if not values['-inPenaltyValue-'] == '' else None 426 | madap_gui.temperature = values['-inCVTemperature-'] \ 427 | if not values['-inCVTemperature-'] == '' else None 428 | madap_gui.applied_scan_rate = values['-inCVScanRate-'] \ 429 | if not values['-inCVScanRate-'] == '' else None 430 | if values['-HEADER_OR_SPECIFIC-'] == 'Headers': 431 | madap_gui.specific = None 432 | madap_gui.header_list = values['-HEADER_OR_SPECIFIC_VALUE-'].replace(" ","") 433 | madap_gui.header_list = list(madap_gui.header_list.split(',')) 434 | else: 435 | madap_gui.header_list = None 436 | madap_gui.specific = values['-HEADER_OR_SPECIFIC_VALUE-'].replace(" ","") 437 | madap_gui.specific = list(madap_gui.specific.split(';')) 438 | 439 | # Validate the fields 440 | validation = madap_gui.validate_fields() 441 | if not validation: 442 | window['-LOG-'].update('Inputs were not valid! Try again.') 443 | continue 444 | window['LogOutput'].update('') 445 | window['RUN'].update(disabled=True) 446 | procedure_thread = threading.Thread(target=procedure_wrapper) 447 | procedure_thread.start() 448 | is_procedure_running = True 449 | log_update_counter = 0 # Reset the counter 450 | # Update log output while the procedure is running or GUI is active 451 | update_log_output(window) 452 | 453 | # Update log message periodically 454 | if is_procedure_running and not procedure_complete[0]: 455 | log_update_counter = (log_update_counter + 1) % log_update_max 456 | num_dots = (log_update_counter // 10) % 3 + 1 # Cycle the dots 457 | log_message = "Analysing" + "." * num_dots 458 | window['-LOG-'].update(log_message) 459 | 460 | # Check for completion or errors from the background process 461 | if procedure_complete[0]: 462 | # Reset the completion flag 463 | is_procedure_running = False 464 | procedure_complete[0] = False 465 | 466 | if procedure_error[0]: 467 | sg.popup(f'Error: Something went wrong. {procedure_error[0]}') 468 | procedure_error[0] = None 469 | else: 470 | procedure_return_value = procedure_result[0] 471 | window['-LOG-'].update('Generating plot...') 472 | window['-COL_PLOTS-'].update(visible=True) 473 | window['-IMAGE-']('') 474 | draw_figure(window['-IMAGE-'], procedure_return_value.figure) 475 | window['-LOG-'].update('DONE! Results and plots were saved in the given path') 476 | # Reset the completion flag and re-enable the RUN button 477 | #procedure_complete[0] = False 478 | window['RUN'].update(disabled=False) 479 | 480 | def update_log_output(window): 481 | """Reads log messages from the queue and updates the GUI.""" 482 | while not log_queue.empty(): 483 | try: 484 | record = log_queue.get_nowait() 485 | if record: 486 | window['LogOutput'].update(record.msg + '\n', append=True) 487 | except queue.Empty: 488 | break 489 | 490 | if __name__ == '__main__': 491 | main() 492 | -------------------------------------------------------------------------------- /pages-publish.sh: -------------------------------------------------------------------------------- 1 | STATUS="$(git status)" 2 | 3 | if [[ $STATUS == *"nothing to commit, working tree clean"* ]] 4 | then 5 | make -C ./docs html 6 | git push origin --delete gh-pages 7 | grep -v "build" ./.gitignore > tmpfile && mv tmpfile ./.gitignore 8 | cp docs/.nojekyll docs/build/html 9 | git add . 10 | git commit -m "autodeploy docs" 11 | git subtree push --prefix docs/build/html origin gh-pages 12 | git reset HEAD~ 13 | git checkout .gitignore 14 | else 15 | echo "Need clean working directory to publish" 16 | fi -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==21.4.0 2 | matplotlib 3 | numpy 4 | pandas 5 | PySimpleGUI 6 | pytest 7 | scikit_learn 8 | ruptures 9 | impedance==1.4.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import find_packages, setup 6 | 7 | with open('README.rst', encoding="utf-8") as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('HISTORY.rst', encoding="utf-8") as history_file: 11 | history = history_file.read() 12 | 13 | requirements = ["attrs==21.4.0", 14 | "matplotlib", 15 | "numpy", 16 | "pandas", 17 | "PySimpleGUI", 18 | "pytest", 19 | "scikit_learn", 20 | "ruptures", 21 | "impedance==1.4.1"] 22 | 23 | test_requirements = ['pytest>=3', ] 24 | 25 | setup( 26 | author="Fuzhan Rahmanian", 27 | author_email='fuzhanrahmanian@gmail.com', 28 | python_requires='>=3.8', 29 | classifiers=[ 30 | 'Development Status :: 3 - Alpha', 31 | 'Intended Audience :: Developers', 32 | 'Intended Audience :: Science/Research', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Natural Language :: English', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.8', 38 | 'Programming Language :: Python :: 3.9', 39 | 'Programming Language :: Python :: 3.10', 40 | 'Topic :: Scientific/Engineering :: Chemistry', 41 | 'Topic :: Scientific/Engineering :: Physics', 42 | 'Topic :: Scientific/Engineering :: Visualization', 43 | ], 44 | description="This is MADAP, a software package for the analysis of electrochemical data.", 45 | entry_points={ 46 | 'console_scripts': [ 47 | 'madap_cli=madap_cli:main', 48 | 'madap_gui=madap_gui:main', 49 | ], 50 | }, 51 | extras_require={ 52 | "dev": ["pytest>=3", ], 53 | }, 54 | install_requires=requirements, 55 | license="MIT license", 56 | long_description=readme + '\n\n' + history, 57 | include_package_data=True, 58 | keywords='madap', 59 | name='MADAP', 60 | packages=find_packages(), 61 | py_modules=['madap_cli', 'madap_gui'], 62 | package_data={'madap/plotting/styles': ['*.mplstyle']}, 63 | test_suite='tests', 64 | tests_require=test_requirements, 65 | url='https://github.com/fuzhanrahmanian/MADAP', 66 | version='1.2.8', 67 | zip_safe=False, 68 | ) 69 | --------------------------------------------------------------------------------