From 5c2050461e4fc5a443cbe4bb2ed37aa9a8943290 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 21 Jan 2026 20:11:43 -0800 Subject: [PATCH 01/17] update readme, update packaging --- .gitchangelog.rc | 296 ----------------------------------------------- README.md | 7 -- pyproject.toml | 19 ++- 3 files changed, 18 insertions(+), 304 deletions(-) delete mode 100644 .gitchangelog.rc diff --git a/.gitchangelog.rc b/.gitchangelog.rc deleted file mode 100644 index 882a96d5..00000000 --- a/.gitchangelog.rc +++ /dev/null @@ -1,296 +0,0 @@ -# -*- coding: utf-8; mode: python -*- -## -## Format -## -## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] -## -## Description -## -## ACTION is one of 'chg', 'fix', 'new' -## -## Is WHAT the change is about. -## -## 'chg' is for refactor, small improvement, cosmetic changes... -## 'fix' is for bug fixes -## 'new' is for new features, big improvement -## -## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' -## -## Is WHO is concerned by the change. -## -## 'dev' is for developpers (API changes, refactors...) -## 'usr' is for final users (UI changes) -## 'pkg' is for packagers (packaging changes) -## 'test' is for testers (test only related changes) -## 'doc' is for doc guys (doc only changes) -## -## COMMIT_MSG is ... well ... the commit message itself. -## -## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' -## -## They are preceded with a '!' or a '@' (prefer the former, as the -## latter is wrongly interpreted in github.) Commonly used tags are: -## -## 'refactor' is obviously for refactoring code only -## 'minor' is for a very meaningless change (a typo, adding a comment) -## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) -## 'wip' is for partial functionality but complete subfunctionality. -## -## Example: -## -## new: usr: support of bazaar implemented -## chg: re-indentend some lines !cosmetic -## new: dev: updated code to be compatible with last version of killer lib. -## fix: pkg: updated year of licence coverage. -## new: test: added a bunch of test around user usability of feature X. -## fix: typo in spelling my name in comment. !minor -## -## Please note that multi-line commit message are supported, and only the -## first line will be considered as the "summary" of the commit message. So -## tags, and other rules only applies to the summary. The body of the commit -## message will be displayed in the changelog without reformatting. -def writefile(lines): - # Develop Full String of Log - log = '' - for line in lines: - log += line - import re - log = re.sub(r'\[\w{1,20}\n? {0,8}\w{1,20}\]','', log) - print(log) - with open('source/changelog.rst','w') as changelog: - changelog.write(log) - print("Update CHANGELOG.rst Complete.") - -## -## ``ignore_regexps`` is a line of regexps -## -## Any commit having its full commit message matching any regexp listed here -## will be ignored and won't be reported in the changelog. -## -ignore_regexps = [ - r'\(private\)', - r'@minor', r'!minor', - r'@cosmetic', r'!cosmetic', - r'@refactor', r'!refactor', - r'@wip', r'!wip', - r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', - r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', - r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', - r'^$', ## ignore commits with empty messages -] - - -## ``section_regexps`` is a list of 2-tuples associating a string label and a -## list of regexp -## -## Commit messages will be classified in sections thanks to this. Section -## titles are the label, and a commit is classified under this section if any -## of the regexps associated is matching. -## -## Please note that ``section_regexps`` will only classify commits and won't -## make any changes to the contents. So you'll probably want to go check -## ``subject_process`` (or ``body_process``) to do some changes to the subject, -## whenever you are tweaking this variable. -## -section_regexps = [ - ('New', [ - r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', - ]), - ('Changes', [ - r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', - ]), - ('Fix', [ - r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', - ]), - - ('Other', None ## Match all lines - ), - -] - - -## ``body_process`` is a callable -## -## This callable will be given the original body and result will -## be used in the changelog. -## -## Available constructs are: -## -## - any python callable that take one txt argument and return txt argument. -## -## - ReSub(pattern, replacement): will apply regexp substitution. -## -## - Indent(chars=" "): will indent the text with the prefix -## Please remember that template engines gets also to modify the text and -## will usually indent themselves the text if needed. -## -## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns -## -## - noop: do nothing -## -## - ucfirst: ensure the first letter is uppercase. -## (usually used in the ``subject_process`` pipeline) -## -## - final_dot: ensure text finishes with a dot -## (usually used in the ``subject_process`` pipeline) -## -## - strip: remove any spaces before or after the content of the string -## -## - SetIfEmpty(msg="No commit message."): will set the text to -## whatever given ``msg`` if the current text is empty. -## -## Additionally, you can `pipe` the provided filters, for instance: -#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") -#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') -#body_process = noop -body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip - - -## ``subject_process`` is a callable -## -## This callable will be given the original subject and result will -## be used in the changelog. -## -## Available constructs are those listed in ``body_process`` doc. -subject_process = (strip | - ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | - SetIfEmpty("No commit message.") | ucfirst | final_dot) - - -## ``tag_filter_regexp`` is a regexp -## -## Tags that will be used for the changelog must match this regexp. -## -tag_filter_regexp = r'^[0-9]+(\.[0-9]+)?$' - - -## ``unreleased_version_label`` is a string or a callable that outputs a string -## -## This label will be used as the changelog Title of the last set of changes -## between last valid tag and HEAD if any. -unreleased_version_label = "(unreleased)" - - -## ``output_engine`` is a callable -## -## This will change the output format of the generated changelog file -## -## Available choices are: -## -## - rest_py -## -## Legacy pure python engine, outputs ReSTructured text. -## This is the default. -## -## - mustache() -## -## Template name could be any of the available templates in -## ``templates/mustache/*.tpl``. -## Requires python package ``pystache``. -## Examples: -## - mustache("markdown") -## - mustache("restructuredtext") -## -## - makotemplate() -## -## Template name could be any of the available templates in -## ``templates/mako/*.tpl``. -## Requires python package ``mako``. -## Examples: -## - makotemplate("restructuredtext") -## -output_engine = rest_py -#output_engine = mustache("restructuredtext") -#output_engine = mustache("markdown") -#output_engine = makotemplate("restructuredtext") - - -## ``include_merge`` is a boolean -## -## This option tells git-log whether to include merge commits in the log. -## The default is to include them. -include_merge = True - - -## ``log_encoding`` is a string identifier -## -## This option tells gitchangelog what encoding is outputed by ``git log``. -## The default is to be clever about it: it checks ``git config`` for -## ``i18n.logOutputEncoding``, and if not found will default to git's own -## default: ``utf-8``. -#log_encoding = 'utf-8' - - -## ``publish`` is a callable -## -## Sets what ``gitchangelog`` should do with the output generated by -## the output engine. ``publish`` is a callable taking one argument -## that is an interator on lines from the output engine. -## -## Some helper callable are provided: -## -## Available choices are: -## -## - stdout -## -## Outputs directly to standard output -## (This is the default) -## -## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start()) -## -## Creates a callable that will parse given file for the given -## regex pattern and will insert the output in the file. -## ``idx`` is a callable that receive the matching object and -## must return a integer index point where to insert the -## the output in the file. Default is to return the position of -## the start of the matched string. -## -## - FileRegexSubst(file, pattern, replace, flags) -## -## Apply a replace inplace in the given file. Your regex pattern must -## take care of everything and might be more complex. Check the README -## for a complete copy-pastable example. -## -publish = writefile -#publish = stdout - - -## ``revs`` is a list of callable or a list of string -## -## callable will be called to resolve as strings and allow dynamical -## computation of these. The result will be used as revisions for -## gitchangelog (as if directly stated on the command line). This allows -## to filter exaclty which commits will be read by gitchangelog. -## -## To get a full documentation on the format of these strings, please -## refer to the ``git rev-list`` arguments. There are many examples. -## -## Using callables is especially useful, for instance, if you -## are using gitchangelog to generate incrementally your changelog. -## -## Some helpers are provided, you can use them:: -## -## - FileFirstRegexMatch(file, pattern): will return a callable that will -## return the first string match for the given pattern in the given file. -## If you use named sub-patterns in your regex pattern, it'll output only -## the string matching the regex pattern named "rev". -## -## - Caret(rev): will return the rev prefixed by a "^", which is a -## way to remove the given revision and all its ancestor. -## -## Please note that if you provide a rev-list on the command line, it'll -## replace this value (which will then be ignored). -## -## If empty, then ``gitchangelog`` will act as it had to generate a full -## changelog. -## -## The default is to use all commits to make the changelog. -#revs = ["^1.0.3", ] -#revs = [ -# Caret( -# FileFirstRegexMatch( -# "CHANGELOG.rst", -# r"(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")), -# "HEAD" -#] -revs = [] diff --git a/README.md b/README.md index 82adf6b8..112b3e04 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![sphinx](https://github.com/engineerjoe440/ElectricPy/actions/workflows/sphinx-build.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/sphinx-build.yml) [![Documentation Status](https://readthedocs.org/projects/electricpy/badge/?version=latest)](https://electricpy.readthedocs.io/en/latest/?badge=latest) -![Tox Import Test](https://github.com/engineerjoe440/ElectricPy/workflows/Tox%20Tests/badge.svg) [![pytest](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pytest.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pytest.yml) [![pydocstyle](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pydocstyle.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pydocstyle.yml) @@ -32,12 +31,6 @@ development, education, and exploration in the realm of electrical engineering. Check out our full documentation: https://electricpy.readthedocs.io/en/latest/ -Antu dialog-warning **Documentation has recently been updated to use [ReadTheDocs](https://readthedocs.org/)** - -GitHub Pages are still active, and will continue to be for the forseeable -future, but they're intended for developmental updates rather than primary -documentation. - ## Features * Extensive set of common functions and formulas for electrical engineering and diff --git a/pyproject.toml b/pyproject.toml index 2ca60057..d38f09a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ - "NumPy", + "numpy >= 2.0.0", "matplotlib", "SciPy", "SymPy", @@ -41,6 +41,23 @@ dependencies = [ numerical = ["numdifftools"] fault = ["arcflash"] full = ["numdifftools", "arcflash"] +test = [ + "pytest >=2.7.3", + "xdoctest >= 1.1.3", + "pytest-pydocstyle >= 2.3.2", + "pygments >= 2.18.0", +] + +[tool.pytest.ini_options] +testpaths = [ + "test", + "electricpy", +] +addopts = "--pydocstyle --xdoctest" + +[tool.pydocstyle] +convention = "numpy" +match-dir = "electricpy/" [project.urls] Home = "https://electricpy.readthedocs.io/en/latest/index.html" From 07fda47bf02bc4a2e21c4b3fddb33d0747470202 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 21 Jan 2026 20:26:43 -0800 Subject: [PATCH 02/17] removed requirements files --- .github/workflows/pydocstyle.yml | 28 ---------------------------- .github/workflows/pytest.yml | 7 +++---- .github/workflows/sphinx.yml | 4 +--- .vscode/settings.json | 15 +++++++++++++++ cspell.json | 23 ----------------------- docsource/requirements.txt | 13 ------------- pyproject.toml | 15 ++++++++++++++- requirements.txt | 5 ----- test/requirements.txt | 2 -- 9 files changed, 33 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/pydocstyle.yml delete mode 100644 cspell.json delete mode 100644 docsource/requirements.txt delete mode 100644 requirements.txt delete mode 100644 test/requirements.txt diff --git a/.github/workflows/pydocstyle.yml b/.github/workflows/pydocstyle.yml deleted file mode 100644 index b1c1143a..00000000 --- a/.github/workflows/pydocstyle.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: pydocstyle - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pydocstyle - pip install . - python3 -c "import electricpy; print('electricpy.__file__')" - - name: Test NumpyDoc Style - run: | - cd electricpy - pydocstyle --convention=numpy \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 7e62afed..3401776e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 @@ -25,9 +25,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f test/requirements.txt ]; then pip install -r test/requirements.txt; fi - pip install . + pip install .[dev] python3 -c "import electricpy; print('electricpy.__file__')" - name: Test with pytest run: | - pytest --xdoctest + pytest --pydoctest --xdoctest diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 27656c69..0130ba78 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -23,17 +23,15 @@ jobs: run: | python -m pip install --upgrade pip cp logo/ElectricpyLogo.svg docsource/static/ElectricpyLogo.svg - pip install -r docsource/requirements.txt - name: Build Sphinx docs if: success() run: | - pip install . + pip install .[dev] python3 -c "import electricpy; print('electricpy.__file__')" sphinx-build -M html docsource docs - name: Generate Coverage Badge if: success() run: | - pip install -r test/requirements.txt coverage run --source=./electricpy -m pytest coverage-badge -o docs/html/coverage.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index 0bad4d33..078ed0cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,21 @@ "editor.rulers": [ 80, 120 ], + "cSpell.words": [ + "electricpy", + "epmath", + "fsolve", + "matplotlib", + "parallelz", + "phasor", + "phasorlist", + "phasorplot", + "phasors", + "powerset", + "pyplot", + "scipy", + "visu" + ], "python.testing.pytestArgs": [ "-m", "test" diff --git a/cspell.json b/cspell.json deleted file mode 100644 index 2e7da734..00000000 --- a/cspell.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "0.2", - "ignorePaths": [], - "dictionaryDefinitions": [], - "dictionaries": [], - "words": [ - "electricpy", - "epmath", - "fsolve", - "matplotlib", - "parallelz", - "phasor", - "phasorlist", - "phasorplot", - "phasors", - "powerset", - "pyplot", - "scipy", - "visu" - ], - "ignoreWords": [], - "import": [] -} diff --git a/docsource/requirements.txt b/docsource/requirements.txt deleted file mode 100644 index bf7380ac..00000000 --- a/docsource/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -wheel -sphinx -numpydoc -myst-parser[linkify] -sphinx-sitemap -sphinx-git -sphinx-immaterial -coverage-badge -numpy -matplotlib -scipy -sympy -numdifftools \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d38f09a0..c96a59bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,11 +41,24 @@ dependencies = [ numerical = ["numdifftools"] fault = ["arcflash"] full = ["numdifftools", "arcflash"] -test = [ +dev = [ "pytest >=2.7.3", "xdoctest >= 1.1.3", "pytest-pydocstyle >= 2.3.2", "pygments >= 2.18.0", + "wheel", + "sphinx", + "numpydoc", + "myst-parser[linkify]", + "sphinx-sitemap", + "sphinx-git", + "sphinx-immaterial", + "coverage-badge", + "numpy", + "matplotlib", + "scipy", + "sympy", + "numdifftools", ] [tool.pytest.ini_options] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ace01b04..00000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -NumPy -matplotlib -SciPy -SymPy -numdifftools diff --git a/test/requirements.txt b/test/requirements.txt deleted file mode 100644 index 5225ed33..00000000 --- a/test/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -xdoctest \ No newline at end of file From c217738454b9cce71d20d5d161564251be98acd2 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 21 Jan 2026 20:28:35 -0800 Subject: [PATCH 03/17] remove 2.0.0 requirement --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c96a59bf..da7e7c49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,10 @@ classifiers = [ dynamic = ["version"] dependencies = [ - "numpy >= 2.0.0", + "numpy", "matplotlib", - "SciPy", - "SymPy", + "scipy", + "sympy", "numdifftools", ] From 330c5f39169a3d43903f451cad1a0185261c60f0 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 21 Jan 2026 20:31:26 -0800 Subject: [PATCH 04/17] fix docstyle --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3401776e..940f9158 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -29,4 +29,4 @@ jobs: python3 -c "import electricpy; print('electricpy.__file__')" - name: Test with pytest run: | - pytest --pydoctest --xdoctest + pytest --pydocstyle --xdoctest From 7c5bffce0cf55121f9091080c623bcbb1cf1c1e4 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 21 Jan 2026 21:05:10 -0800 Subject: [PATCH 05/17] started migrating to md documentation --- .github/workflows/sphinx.yml | 2 +- docsource/additionalresources.md | 24 ++++++++++ docsource/additionalresources.rst | 41 ------------------ docsource/changes.rst | 4 -- docsource/conf.py | 4 +- docsource/constants.md | 22 ++++++++++ docsource/constants.rst | 26 ----------- docsource/index.rst | 1 - .../static/InductionMotorCircleExample.png | Bin 67607 -> 67608 bytes docsource/static/PhasorPlot.png | Bin 57152 -> 57153 bytes docsource/static/PowerTriangle.png | Bin 24314 -> 24315 bytes .../static/ReceivingEndPowerCircleExample.png | Bin 40813 -> 40814 bytes .../static/ReceivingPowerCircleExample.png | Bin 40813 -> 40814 bytes docsource/static/convbar-example.png | Bin 20696 -> 20697 bytes docsource/static/series-rlc-r10-l0.5.png | Bin 0 -> 60417 bytes docsource/static/series-rlc-r5-l0.4.png | Bin 0 -> 32352 bytes pyproject.toml | 1 - 17 files changed, 48 insertions(+), 77 deletions(-) create mode 100644 docsource/additionalresources.md delete mode 100644 docsource/additionalresources.rst delete mode 100644 docsource/changes.rst create mode 100644 docsource/constants.md delete mode 100644 docsource/constants.rst create mode 100644 docsource/static/series-rlc-r10-l0.5.png create mode 100644 docsource/static/series-rlc-r5-l0.4.png diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 0130ba78..09eb27f4 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -17,7 +17,7 @@ jobs: - name: Install Python uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" # I don't know where the "run" thing is documented. - name: Install dependencies run: | diff --git a/docsource/additionalresources.md b/docsource/additionalresources.md new file mode 100644 index 00000000..4706e69f --- /dev/null +++ b/docsource/additionalresources.md @@ -0,0 +1,24 @@ +# Additional Resources + + +## Generic and Data Science + +* NumPy: https://numpy.org/ +* SciPy: https://scipy.org/ +* Matplotlib: https://matplotlib.org/ +* SymPy: https://www.sympy.org/en/index.html +* Pyomo: https://www.pyomo.org/ +* Pint: https://pint.readthedocs.io/en/stable/ +* numdifftools: https://numdifftools.readthedocs.io/en/latest/ + +## Electrical Engineering Focus + +* Python COMTRADE File Interpreter: https://github.com/dparrini/python-comtrade +* Python COMTRADE Writer: https://github.com/relihanl/comtradehandlers +* Arc Flash Calculator: https://github.com/LiaungYip/arcflash +* PandaPower: https://www.pandapower.org/start/ +* PyPSA: https://github.com/PyPSA/PyPSA +* PyPower (no longer supported): https://pypi.org/project/PYPOWER/ +* minpower: http://adamgreenhall.github.io/minpower/index.html +* oemof (Open Energy MOdeling Framework): https://oemof.org/ +* PowerGAMA: https://bitbucket.org/harald_g_svendsen/powergama/wiki/Home diff --git a/docsource/additionalresources.rst b/docsource/additionalresources.rst deleted file mode 100644 index d8999ca2..00000000 --- a/docsource/additionalresources.rst +++ /dev/null @@ -1,41 +0,0 @@ -Additional Resources -================================================================================ - - -Generic and Data Science ------------------------- - - * NumPy: https://numpy.org/ - - * SciPy: https://scipy.org/ - - * Matplotlib: https://matplotlib.org/ - - * SymPy: https://www.sympy.org/en/index.html - - * Pyomo: https://www.pyomo.org/ - - * Pint: https://pint.readthedocs.io/en/stable/ - - * numdifftools: https://numdifftools.readthedocs.io/en/latest/ - -Electrical Engineering Focus ----------------------------- - - * Python COMTRADE File Interpreter: https://github.com/dparrini/python-comtrade - - * Python COMTRADE Writer: https://github.com/relihanl/comtradehandlers - - * Arc Flash Calculator: https://github.com/LiaungYip/arcflash - - * PandaPower: https://www.pandapower.org/start/ - - * PyPSA: https://github.com/PyPSA/PyPSA - - * PyPower (no longer supported): https://pypi.org/project/PYPOWER/ - - * minpower: http://adamgreenhall.github.io/minpower/index.html - - * oemof (Open Energy MOdeling Framework): https://oemof.org/ - - * PowerGAMA: https://bitbucket.org/harald_g_svendsen/powergama/wiki/Home diff --git a/docsource/changes.rst b/docsource/changes.rst deleted file mode 100644 index 1dd04244..00000000 --- a/docsource/changes.rst +++ /dev/null @@ -1,4 +0,0 @@ -Recent Changes -================================================================================ - -.. git_changelog:: diff --git a/docsource/conf.py b/docsource/conf.py index 0be00c19..759e1093 100644 --- a/docsource/conf.py +++ b/docsource/conf.py @@ -38,7 +38,7 @@ # -- Project information ----------------------------------------------------- project = 'electricpy' -copyright = '2022, Joe Stanley' +copyright = '2026, Joe Stanley' author = 'Joe Stanley' # The full version, including alpha/beta/rc tags @@ -56,8 +56,6 @@ 'sphinx.ext.mathjax', 'sphinx.ext.autosummary', 'sphinx.ext.viewcode', - 'numpydoc', - 'sphinx_git', 'myst_parser', 'sphinx_immaterial', ] diff --git a/docsource/constants.md b/docsource/constants.md new file mode 100644 index 00000000..34d51bef --- /dev/null +++ b/docsource/constants.md @@ -0,0 +1,22 @@ +# Constants + +In addition to the variety of funcitons provided, several common, and useful, +constants are provided to simplify arithmetic. + + +| Name | Value | +|----------------|----------------------------------------------------------------| +| `pi` | π (derived from numpy.pi) | +| `a` | :math:`1\angle{120}` | +| `p` | 10^-12 (pico) | +| `n` | 10^-9 (nano) | +| `u` | 10^-6 (micro) | +| `m` | 10^-3 (mili) | +| `k` | 10^3 (kila) | +| `M` | 10^6 (mega) | +| `G` | 10^9 (giga) | +| `u0` | :math:`µ0` (mu-not) 4πE-7 | +| `e0` | :math:`ε0` (epsilon-not) 8.854E-12 | +| `carson_r` | 9.869e-7 (Carson's Ristance Constant) | +| `WATTS_PER_HP` | 745.699872 | +| `KWH_PER_BTU` | 3412.14 | diff --git a/docsource/constants.rst b/docsource/constants.rst deleted file mode 100644 index cc98f7f4..00000000 --- a/docsource/constants.rst +++ /dev/null @@ -1,26 +0,0 @@ -Constants -================================================================================ - -.. _constants.py: - -In addition to the variety of funcitons provided, several common, and useful, -constants are provided to simplify arithmetic. - -============== ================================================================= -Name Value -============== ================================================================= -`pi` π (derived from numpy.pi) -`a` :math:`1\angle{120}` -`p` 10^-12 (pico) -`n` 10^-9 (nano) -`u` 10^-6 (micro) -`m` 10^-3 (mili) -`k` 10^3 (kila) -`M` 10^6 (mega) -`G` 10^9 (giga) -`u0` :math:`µ0` (mu-not) 4πE-7 -`e0` :math:`ε0` (epsilon-not) 8.854E-12 -`carson_r` 9.869e-7 (Carson's Ristance Constant) -`WATTS_PER_HP` 745.699872 -`KWH_PER_BTU` 3412.14 -============== ================================================================= \ No newline at end of file diff --git a/docsource/index.rst b/docsource/index.rst index e2346bd2..0aeeae95 100644 --- a/docsource/index.rst +++ b/docsource/index.rst @@ -36,7 +36,6 @@ Contents: electricpyapi constants additionalresources - changes Github PyPI diff --git a/docsource/static/InductionMotorCircleExample.png b/docsource/static/InductionMotorCircleExample.png index 4c2e29a30257204ea0442d04446d50f07a72a181..a04618542526d18a5f1d0de6217eece121aed692 100644 GIT binary patch delta 54 zcmbO}fn~-7mI*41Rufef6%7saEOZnyN=gcft@QPC6H5wm@=J0ull1b7()ABUm!4}( K+nUC>ksScz>Jtn+b;e!50!{Aq|9T5iVB&>L}saEib9l7na6|-5kktCOd&-PnaPw8D#?(9 zk|`lmzjNL9v-k17$M4<$?LYT(JRSFai?6lT=NiuIJkM)|>7CHnLeD`@B9XQn(^NGe zk;wf?B(h;zYJ5eiYv3#XA>*cI>SlQPyxVmv7h95!m7BBUWj9BAYwl~dF0S^MFC7%y zFClhhFSnhWo3pEoxVY2*{tmIrE*HdW&s^=mr_ecTo^mCTwptN?$Z{02?Mb8`PRCT0 zjNI=}J-BYla*{Q~uVT}tDdSRwv zP^#&}&c&Plyw{Xiio0biys8wOHEw>+`VhY=|MsZFQcC$!d(ySV0YPqF@}OEv3AWAD znFHKwJ@mxC5nqrOYbUe$_dkR$NYF(8=X>tgaw`7w_iPa^)UG7)n3A0A%asNmNu)1(zDYtiJ zf|7>jSBkxplhcP!pGL=1{nWLzZr!SU^-50W>UX2Or@ARe!wSz<)OB}nvo2=)_U+s3 z;$joNF!<|~w`a!#*^uF3%hE&VBnGJqbPGs9={Dbca-E%>hqHR~jDIlO*IQQZT$&kr zT_3Ww*kw#>Gl%4aVLkinVG-3ewe>>6WP3Zb97Qt*tyqHRwH%6WuCu9 zJMK&WG8xlJIm&LBeIhtEmigvpc5+!+*-Fpf^s7rV8ZRz=esX10eR*Z2Dz+nJE0=Hf z=uq&~l%t*ft3Rr#vey|5A89L9`>yMyD+lX7VjcYvfXDLo=Hky!nZD~QS$E#Fv;>&$ zy{Xq6_+dut{CUBJ-@g?~IV^U)O;w=rfBWTJL1hzf3@vZWMC`=u`qEIQb>l6PKaa}I zw39{VA6{PFBX;)X4eBi%ak-Co$zHR>T`zB}&J^3f6STW{(bWSt(wMFi5}lg*w?ihp z@yj+_`JM{RlP8y6-FA#n{q>uyXNk`GZI(6s2b%N(Rq4$Y92pMK{Bg! zXZqP+8Wa=5w6L%+JUJQm?84i;IM;9O?B2iL9PaGx4bIHuyDM(dA*Cx479Jj?$gOB+ zm(x1;edQ}ZKfh&-KiQKfPyC~z7##W@he=)R%jxy|^xTP-jxHcFa*Ny5t6h1t>~dvx;Y^gfK`PI&`zDO5W`3*<#CDa#Bry=|vt!T1F4!lI+2)4J}q z{)!9>3n(fQkBE%yDzP^d^Q*tP#rgX65LMoQurNB$-`|5ZZttFJE%RCudwKN-_3`7! zC%%8LYigo?<@sCL!9nz`34WlyDRSq?moMaJ&z>!-s2J_ZH58RmQ&mm#SzV0Q%94J?k7Y4UG7 za^whc+eg`hl>`3#8qYlc<{t5wf`h3LH-TTj+C)Cz(>^!)y6f?&gA+e~WGV;K&(6)& zB7w@+5D&HQ-Z7IpUwK`pr3|!Qy)!mGey%l6#M#9qu&_`}TSteuS59e{8$#EA^zHNh zyS^l`e}4e}>ihem+poNq@=A?OP1$ZT?4oydb^Y++!|eR6MY5yL!=&r-H*emY_)yGu zX`rk&)?;NsWMye)>&~4!CnhFpT3hMm<>fDbPJMFf*?AH@Zhkh( z#_+}?zH3MNUtHdL{rYunq?~d(C=w$=g-qJFx=D@ ztpC~)Yh`6M*S8|?wYWKwL+Z=@T?Y>yHdWeH@P;x^Qv`cZ zPmj?W%F)ET?5$$vmCY@9nv@S@t_H@(Z�CBSWAJRC+M4F8-_!W#S_;j!!~@ap&Pn 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 W;zdEhdv7rKbx!SqYLT*K$o~P12+olJ literal 0 HcmV?d00001 diff --git a/docsource/static/series-rlc-r5-l0.4.png b/docsource/static/series-rlc-r5-l0.4.png new file mode 100644 index 0000000000000000000000000000000000000000..5995f32735df70046bd1d74c06877adb50eab52d GIT binary patch literal 32352 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 H^3J~jREekA literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index da7e7c49..93b36e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ dev = [ "pygments >= 2.18.0", "wheel", "sphinx", - "numpydoc", "myst-parser[linkify]", "sphinx-sitemap", "sphinx-git", From d8a634c8f848cf68f9311f16bb54c37e468d54f3 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sun, 1 Feb 2026 11:17:36 -0800 Subject: [PATCH 06/17] cleaned up pytest errors --- conftest.py | 9 +++++++++ electricpy/geometry/__init__.py | 25 +++++++++++++++++-------- electricpy/geometry/circle.py | 23 ++++++++++++++--------- electricpy/geometry/triangle.py | 33 +++++++++++++++------------------ electricpy/passive.py | 8 ++++---- test/__init__.py | 1 - 6 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..e36004ec --- /dev/null +++ b/conftest.py @@ -0,0 +1,9 @@ +# Pytest configuration + +import pytest +import numpy as np + +@pytest.fixture(scope="session", autouse=True) +def setup_environment(): + """Use Legacy Numpy Printing Options for consistent test outputs.""" + np.set_printoptions(legacy='1.25') diff --git a/electricpy/geometry/__init__.py b/electricpy/geometry/__init__.py index 9b226519..3a797296 100644 --- a/electricpy/geometry/__init__.py +++ b/electricpy/geometry/__init__.py @@ -16,7 +16,7 @@ import math from dataclasses import dataclass -from typing import Iterable, Iterator, Optional, Tuple, Union +from typing import Iterator, Tuple, Union Number = Union[int, float] @@ -24,6 +24,8 @@ def _as_float(x) -> float: """ + Cast as a float. + Coerce numeric-like values (including complex with ~0 imag) to float. This defends against accidental cmath usage elsewhere in the library. """ @@ -40,7 +42,8 @@ def _is_close(a: float, b: float, *, rel_tol: float = 1e-9, abs_tol: float = 1e- @dataclass(frozen=False) class Point: - """A point in 2D space. + """ + A point in 2D space. Parameters ---------- @@ -49,6 +52,7 @@ class Point: y : float The y coordinate of the point """ + x: float y: float @@ -78,18 +82,18 @@ def is_close(self, other: "Point", *, tol: float = 1e-9) -> bool: return _is_close(self.x, other.x, rel_tol=tol, abs_tol=tol) and _is_close(self.y, other.y, rel_tol=tol, abs_tol=tol) def __repr__(self) -> str: + """Developer representation of the Point.""" return f"Point({self.x}, {self.y})" def __str__(self) -> str: + """Representation of the Point.""" return f"({self.x}, {self.y})" @dataclass(frozen=False) class Line: - """A line in 2D space in the form: + """A line in 2D space in the form (`ax + by + c = 0`).""" - ax + by + c = 0 - """ a: float b: float c: float @@ -149,6 +153,8 @@ def intersection(self, other: object) -> Point: def normalized(self) -> Tuple[float, float, float]: """ + Normalize the geometry coefficients. + Return a normalized (a,b,c) such that sqrt(a^2+b^2)=1 and sign is stable. This helps with comparisons and distances. """ @@ -178,6 +184,8 @@ def is_close(self, other: "Line", *, tol: float = 1e-9) -> bool: def __eq__(self, other: object) -> bool: """ + Evaluate the equality of geometric object. + Exact-ish equality (proportional coefficients), but using normalization for better behavior than raw ratio checks. This is safer than the original. """ @@ -186,10 +194,11 @@ def __eq__(self, other: object) -> bool: return self.is_close(other, tol=1e-9) def __repr__(self) -> str: + """Developer representation of the Line.""" return f"Line({self.a}, {self.b}, {self.c})" def __str__(self) -> str: - # Keep a readable form; avoid division by zero when possible. + """Keep a readable form; avoid division by zero when possible.""" if _is_close(self.a, 0.0): # by + c = 0 => y = -c/b return f"y = {-self.c / self.b}" @@ -220,8 +229,8 @@ def angle_btw_lines(l1: Line, l2: Line) -> float: raise ValueError("Cannot compute angle for degenerate line.") # Clamp to [-1,1] to protect against tiny numeric drift - cosang = max(-1.0, min(1.0, dot / (n1 * n2))) - ang = math.acos(cosang) + cosine_angle = max(-1.0, min(1.0, dot / (n1 * n2))) + ang = math.acos(cosine_angle) # Return acute angle if ang > math.pi / 2: diff --git a/electricpy/geometry/circle.py b/electricpy/geometry/circle.py index ed8e9d90..980bebb6 100644 --- a/electricpy/geometry/circle.py +++ b/electricpy/geometry/circle.py @@ -1,7 +1,6 @@ ################################################################################ """ -electricpy.geometry.circle - Collection of methods which operate on Cartesian -circles. +electricpy.geometry.circle - Collection of methods for Cartesian circles. >>> import electricpy.geometry.circle as circle @@ -13,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Generator, Iterable, Optional, Tuple, Union, overload +from typing import Generator, Tuple, Union import math from electricpy import geometry @@ -52,6 +51,7 @@ class Circle: radius : float The radius of the circle (must be >= 0) """ + center: Point radius: float @@ -139,9 +139,9 @@ def is_tangent(self, l: Line, *, tol: float = 1e-9) -> bool: def is_normal(self, l: Line, *, tol: float = 1e-9) -> bool: """ - Return True if the line passes through the circle's center (within tolerance). + Line passes through the circle's center (within tolerance). - IMPORTANT + Important --------- A line being a "normal to the circle" is only well-defined at a specific point of contact. This method keeps backward compatibility with the @@ -175,11 +175,11 @@ def equation(self) -> str: B = -2.0 * k C = const - def _term(coeff: float, var: str) -> str: - if _is_close(coeff, 0.0): + def _term(coef: float, var: str) -> str: + if _is_close(coef, 0.0): return "" - sign = " + " if coeff > 0 else " - " - mag = abs(coeff) + sign = " + " if coef > 0 else " - " + mag = abs(coef) # Prefer integer-like display when possible if _is_close(mag, round(mag)): mag_str = str(int(round(mag))) @@ -343,20 +343,25 @@ def intersetion(self, other) -> Union[None, Point, Tuple[Point, Point], str]: # Dunder methods # ------------------------------------------------------------------------- def __repr__(self) -> str: + """Representation of the Circle.""" return f"Circle(center={self.center}, radius={self.radius})" def __str__(self) -> str: + """Form of the Circle.""" return f"Circle(center={self.center}, radius={self.radius})" def __eq__(self, other: object) -> bool: + """Equality comparison for Circle.""" if isinstance(other, Circle): return self.center == other.center and self.radius == other.radius return False def __ne__(self, other: object) -> bool: + """Inequality comparison for Circle.""" return not self == other def __hash__(self) -> int: + """Hash for Circle.""" return hash((self.center, self.radius)) diff --git a/electricpy/geometry/triangle.py b/electricpy/geometry/triangle.py index 39e3accd..1ee192a4 100644 --- a/electricpy/geometry/triangle.py +++ b/electricpy/geometry/triangle.py @@ -1,7 +1,6 @@ ################################################################################ """ -electricpy.geometry.triangle - Collection of methods which operate on Cartesian -triangles. +electricpy.geometry.triangle - Collection of methods for Cartesian triangles. >>> import electricpy.geometry.triangle as triangle @@ -12,7 +11,7 @@ from __future__ import annotations -from typing import Iterable, Optional, Sequence, Tuple, Union +from typing import Tuple, Union import math from electricpy.geometry import Point, Line @@ -42,9 +41,7 @@ def _is_close(a: float, b: float, *, rel_tol: float = 1e-9, abs_tol: float = 1e- def _triangle_twice_area(p0: Point, p1: Point, p2: Point) -> float: - """ - Return twice the signed area (cross product magnitude). - """ + """Return twice the signed area (cross product magnitude).""" return (p1.x - p0.x) * (p2.y - p0.y) - (p1.y - p0.y) * (p2.x - p0.x) @@ -115,9 +112,7 @@ def perimeter(self) -> float: return self.a + self.b + self.c def perimeters(self) -> float: - """ - Backward-compatible alias for perimeter(). - """ + """Backward-compatible alias for perimeter().""" return self.perimeter() def area(self) -> float: @@ -151,9 +146,9 @@ def centroid(self) -> Point: def in_center(self) -> Point: """ - Return the incenter of the triangle. + Return the in_center of the triangle. - The incenter is the weighted average of vertices by the lengths of + The in_center is the weighted average of vertices by the lengths of the opposite sides. With our naming: a = |p0-p1| opposite vertex p2 b = |p1-p2| opposite vertex p0 @@ -172,7 +167,7 @@ def in_center(self) -> Point: def in_radius(self) -> float: """ - Return the inradius of the triangle. + Return the in_radius of the triangle. r = A / s where s is semiperimeter. """ @@ -184,12 +179,12 @@ def in_radius(self) -> float: def ortho_center(self) -> Point: """ - Return the orthocenter of the triangle. + Return the ortho_center of the triangle. Construct two altitudes: - altitude from p0 to line through p1-p2 - altitude from p1 to line through p0-p2 - Their intersection is the orthocenter. + Their intersection is the ortho_center. Requires Line objects returned by geometry.line_equation to support: - foot_perpendicular(Point) -> Point @@ -207,7 +202,7 @@ def ortho_center(self) -> Point: def circum_center(self) -> Point: """ - Return the circumcenter of the triangle. + Return the circum_center of the triangle. Intersection of perpendicular bisectors of two sides. """ @@ -217,13 +212,13 @@ def circum_center(self) -> Point: def circum_radius(self) -> float: """ - Return the circumradius of the triangle. + Return the circum_radius of the triangle. R = abc / (4A) """ A = self.area() if _is_close(A, 0.0, abs_tol=self._tol): - raise ValueError("Degenerate triangle: area is zero, circumradius undefined") + raise ValueError("Degenerate triangle: area is zero, circum_radius undefined") return (self.a * self.b * self.c) / (4.0 * A) # ------------------------------------------------------------------------- @@ -231,7 +226,9 @@ def circum_radius(self) -> float: # ------------------------------------------------------------------------- def __is_valid(self) -> bool: """ - Validate triangle: + Validate triangle. + + Checks: - triangle inequality with tolerance - non-collinear (non-degenerate) using cross-product area test """ diff --git a/electricpy/passive.py b/electricpy/passive.py index 65024554..bd344801 100644 --- a/electricpy/passive.py +++ b/electricpy/passive.py @@ -126,7 +126,7 @@ def capbacktoback(C1, C2, Lm, VLN=None, VLL=None): Function to calculate the maximum current and the frequency of the inrush current of two capacitors connected in parallel when one (energized) capacitor - is switched into another (non-engergized) capacitor. + is switched into another (non-energized) capacitor. .. note:: This formula is only valid for three-phase systems. @@ -443,7 +443,7 @@ def air_core_inductance(d: float, coil_l: float, n: int): def air_core_required_length(d: float, L: float, n: int): r""" - Compute Required Length of Air Core Inductor + Compute Required Length of Air Core Inductor. .. math:: l = \frac{1000 d^2 n^2 - 457418 d L}{1016127 L} @@ -467,7 +467,7 @@ def air_core_required_length(d: float, L: float, n: int): def air_core_required_diameter(coil_l: float, L: float, n: int): r""" - Compute Diameter of Air Core Inductor + Compute Diameter of Air Core Inductor. .. math:: 1000 n^2 d^2 - 457418 L d - 1016127 L l = 0 @@ -496,7 +496,7 @@ def air_core_required_diameter(coil_l: float, L: float, n: int): def air_core_required_num_turns(d: float, coil_l: float, L: float): r""" - Compute Required Number of Turns of Air Core Inductor + Compute Required Number of Turns of Air Core Inductor. .. math:: n = \sqrt{\frac{L(1016127 l + 457418 d)}{1000 d^2}} diff --git a/test/__init__.py b/test/__init__.py index 7fbe89ec..ea0e4fa7 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,6 +1,5 @@ from electricpy.geometry import Point from electricpy.geometry import Line -import numpy as np from numpy.testing import assert_almost_equal def compare_points(p1: Point, p2: Point) -> bool: From fda6145669e4708b2c42e0f313bd7cd8f64b27f0 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sun, 1 Feb 2026 11:33:36 -0800 Subject: [PATCH 07/17] fixed a test --- electricpy/compute.py | 17 +++++++++++++++-- electricpy/math.py | 29 ++++++++++++++--------------- test/test_compute.py | 17 ++++++++++++++--- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/electricpy/compute.py b/electricpy/compute.py index b2549535..197a6bba 100644 --- a/electricpy/compute.py +++ b/electricpy/compute.py @@ -264,7 +264,20 @@ def string_to_bits(str): The binary representation of the input string. """ - data = (''.join(format(ord(x), 'b') for x in str)) - return data + data = ''.join(format(ord(x), 'b') for x in str) + + # If empty, return as-is + if len(data) == 0: + return data + + # If length is already a power of two, return unchanged + n = len(data) + if n & (n - 1) == 0: + return data + + # Compute next power of two and pad with leading zeros + next_pow = 1 << n.bit_length() + pad_len = next_pow - n + return '0' * pad_len + data # END diff --git a/electricpy/math.py b/electricpy/math.py index 9188d577..a825b873 100644 --- a/electricpy/math.py +++ b/electricpy/math.py @@ -49,7 +49,7 @@ def step(t): r""" Step Function [ u(t) ]. - Simple implimentation of numpy.heaviside function to provide standard + Simple implementation of numpy.heaviside function to provide standard step-function as specified to be zero at :math:`x < 0`, and one at :math:`x \geq 0`. @@ -75,7 +75,7 @@ def funcrms(func, T): Root-Mean-Square (RMS) Evaluator for Callable Functions. Integral-based RMS calculator, evaluates the RMS value - of a repetative signal (f) given the signal's specific + of a repetitive signal (f) given the signal's specific period (T) Parameters @@ -89,8 +89,7 @@ def funcrms(func, T): ------- RMS: The RMS value of the function (f) over the interval ( 0, T ) """ - fn = lambda x: func(x) ** 2 - integral, _ = integrate(fn, 0, T) + integral, _ = integrate(lambda x: func(x) ** 2, 0, T) return _np.sqrt(1 / T * integral) @@ -140,7 +139,7 @@ def gausdist(x, mu=0, sigma=1): Returns ------- F: numpy.ndarray - Computed distribution of the gausian function at the + Computed distribution of the gaussian function at the points specified by (array) x """ # Define Integrand @@ -149,7 +148,7 @@ def integrand(sq): try: lx = len(x) # Find length of Input - except: + except TypeError: lx = 1 # Length 1 x = [x] # Pack into list F = _np.zeros(lx, dtype=_np.float64) @@ -157,7 +156,7 @@ def integrand(sq): x_tmp = x[i] # Evaluate X (altered by mu and sigma) X = (x_tmp - mu) / sigma - integral = integrate(integrand, _np.NINF, X) # Integrate + integral = integrate(integrand, -_np.inf, X) # Integrate result = 1 / _np.sqrt(2 * _np.pi) * integral[0] # Evaluate Result F[i] = result # Return only the 0-th value if there's only 1 value available @@ -197,7 +196,7 @@ def probdensity(func, x, x0=0, scale=True): sumx = _np.array([]) try: lx = len(x) # Find length of Input - except: + except TypeError: lx = 1 # Length 1 x = [x] # Pack into list # Recursively Find Probability Density @@ -210,7 +209,7 @@ def probdensity(func, x, x0=0, scale=True): if scale: mx = sumx.max() sumx /= mx - elif scale != False: + elif not scale: sumx /= scale return sumx @@ -220,7 +219,7 @@ def rfft(arr, dt=0.01, absolute=True, resample=True): """ RFFT Function. - This function is designed to evaluat the real FFT + This function is designed to evaluate the real FFT of a input signal in the form of an array or list. Parameters @@ -252,13 +251,13 @@ def rfft(arr, dt=0.01, absolute=True, resample=True): # Evaluate the Downsampling Ratio dn = int(dt * len(arr)) # Downsample to remove unnecessary points - fixedfft = filter.dnsample(fourier, dn) - return (fixedfft) + fixed_fft = filter.dnsample(fourier, dn) + return fixed_fft elif not resample: - return (fourier) + return fourier else: # Condition Resample Value resample = int(resample) # Downsample to remove unnecessary points - fixedfft = filter.dnsample(fourier, resample) - return fixedfft + fixed_fft = filter.dnsample(fourier, resample) + return fixed_fft diff --git a/test/test_compute.py b/test/test_compute.py index 1364baed..4e28d589 100644 --- a/test/test_compute.py +++ b/test/test_compute.py @@ -27,6 +27,17 @@ def test_crc_sender_and_remainder(): assert set(remainder) == {"0"} -def test_string_to_bits(): - bits = compute.string_to_bits("A") - assert bits == "01000001" +@pytest.mark.parametrize("character_string, expected_bits", [ + ("A", "01000001"), + ("B", "01000010"), + ("C", "01000011"), + ("a", "01100001"), + ("b", "01100010"), + ("c", "01100011"), + ("0", "00110000"), + ("1", "00110001"), + ("2", "00110010"), +]) +def test_string_to_bits(character_string, expected_bits): + bits = compute.string_to_bits(character_string) + assert bits == expected_bits From 5d964a0d8cac4f40b3749e8a54ca0b7fd1920b86 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sun, 1 Feb 2026 11:40:01 -0800 Subject: [PATCH 08/17] improved documentation and test --- electricpy/compute.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/electricpy/compute.py b/electricpy/compute.py index 197a6bba..92e2fd24 100644 --- a/electricpy/compute.py +++ b/electricpy/compute.py @@ -253,6 +253,14 @@ def string_to_bits(str): Converts a Pythonic string to the string's binary representation. + Examples + -------- + >>> from electricpy import compute as cmp + >>> cmp.string_to_bits("A") + '01000001' + >>> cmp.string_to_bits("Hello") + '0000010010001100101110110011011001101111' + Parameters ---------- str: string @@ -266,18 +274,11 @@ def string_to_bits(str): """ data = ''.join(format(ord(x), 'b') for x in str) - # If empty, return as-is - if len(data) == 0: - return data - - # If length is already a power of two, return unchanged - n = len(data) - if n & (n - 1) == 0: - return data + # Pad to Nearest Byte + offset = len(data) % 8 + if offset != 0: + data = (8 - offset) * '0' + data - # Compute next power of two and pad with leading zeros - next_pow = 1 << n.bit_length() - pad_len = next_pow - n - return '0' * pad_len + data + return data # END From fc5d9265e5ff83f7b6a776f7f316f81e798e188e Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sun, 1 Feb 2026 11:46:53 -0800 Subject: [PATCH 09/17] cleanup test and fix dig-sim --- electricpy/sim.py | 12 +++++++----- test/test_sim.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/electricpy/sim.py b/electricpy/sim.py index f5b44618..2cfa1e4f 100644 --- a/electricpy/sim.py +++ b/electricpy/sim.py @@ -79,7 +79,8 @@ def digifiltersim(fin, filter, freqs, NN=1000, dt=0.01, title="", figsize: tuple, optional The figure dimensions for each subplot, default=None """ - if (figsize != None): _plt.figure(figsize=figsize) + if figsize is not None: + _plt.figure(figsize=figsize) flen = len(freqs) for i in range(flen): # Gather frequency @@ -94,7 +95,7 @@ def digifiltersim(fin, filter, freqs, NN=1000, dt=0.01, title="", x[k] = fin(k * dt, freq) # Identify how many rows were provided - sz = filter.size + sz = len(filter) if isinstance(filter, (tuple, list)) else filter.size if (sz < 5): raise ValueError("ERROR: Too few filter arguments provided. " + "Refer to documentation for proper format.") @@ -131,10 +132,11 @@ def digifiltersim(fin, filter, freqs, NN=1000, dt=0.01, title="", _plt.plot(ytime, 'k', label="Output") _plt.title(title) _plt.grid(which='both') - if legend: _plt.legend(title="Frequency = " + str(freq) + "Hz") - if xlim != False: + if legend: + _plt.legend(title="Frequency = " + str(freq) + "Hz") + if not xlim: _plt.xlim(xlim) - elif xmxscale != None: + elif xmxscale is not None: _plt.xlim((0, xmxscale / (freq * dt))) _plt.tight_layout() diff --git a/test/test_sim.py b/test/test_sim.py index 4ecfafe3..9cd1a8e5 100644 --- a/test/test_sim.py +++ b/test/test_sim.py @@ -7,10 +7,14 @@ def test_digifiltersim_basic(): - fin = lambda t, f: np.sin(2 * np.pi * f * t) - filter_row = [0.0, 0.0, 1.0, 0.0, 0.0] - - plot_module = sim.digifiltersim(fin, filter_row, freqs=[1], NN=10, dt=0.1, legend=False) + plot_module = sim.digifiltersim( + lambda t, f: np.sin(2 * np.pi * f * t), + [0.0, 0.0, 1.0, 0.0, 0.0], + freqs=[1], + NN=10, + dt=0.1, + legend=False + ) assert plot_module is not None From d1bbde21afc15a003d188eea40f074e945f4af6f Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Sun, 1 Feb 2026 11:52:03 -0800 Subject: [PATCH 10/17] clean up lint issues --- electricpy/sim.py | 68 +++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/electricpy/sim.py b/electricpy/sim.py index 2cfa1e4f..bb076101 100644 --- a/electricpy/sim.py +++ b/electricpy/sim.py @@ -212,7 +212,7 @@ def step_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplot(122) _plt.title(errtitle) @@ -220,10 +220,10 @@ def step_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplots_adjust(wspace=0.3) - if filename != None: + if filename is not None: _plt.savefig(filename) return _plt @@ -297,7 +297,7 @@ def ramp_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplot(122) _plt.title(errtitle) @@ -305,10 +305,10 @@ def ramp_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplots_adjust(wspace=0.3) - if filename != None: + if filename is not None: _plt.savefig(filename) return _plt @@ -381,7 +381,7 @@ def parabolic_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplot(122) _plt.title(errtitle) @@ -389,10 +389,10 @@ def parabolic_response(system, npts=1000, dt=0.01, combine=True, xlim=False, _plt.grid() _plt.legend() _plt.xlabel("Time (seconds)") - if xlim != False: + if not xlim: _plt.xlim(xlim) _plt.subplots_adjust(wspace=0.3) - if filename != None: + if filename is not None: _plt.savefig(filename) return _plt @@ -652,58 +652,58 @@ def func_c(self, x): # Concatenated Function # Plot Forcing Functions if (plotforcing): - fffig = _plt.figure("Forcing Functions") + _ = _plt.figure("Forcing Functions") if fnc > 1: for x in range(fnc): _plt.plot(TT, fn_arr[x], label="f" + str(x + 1)) else: _plt.plot(TT, fn_arr, label="f1") - if xlim != False: + if not xlim: _plt.xlim(xlim) - if ylim != False: + if not ylim: _plt.ylim(ylim) _plt.title("Forcing Functions " + title) _plt.xlabel("Time (seconds)") _plt.legend(title="Forcing Functions") _plt.grid() - if filename != None: + if filename is not None: _plt.savefig('Simulation Forcing Functions.png') if plotstate: _plt.show() # Plot each state-variable over time - stvfig = _plt.figure("State Variables") + _ = _plt.figure("State Variables") for x in range(xtim_len): _plt.plot(TT, xtim[x], label="x" + str(x + 1)) - if xlim != False: + if not xlim: _plt.xlim(xlim) - if ylim != False: + if not ylim: _plt.ylim(ylim) _plt.title("Simulated Output Terms " + soltype[solution] + title) _plt.xlabel("Time (seconds)") _plt.legend(title="State Variable") _plt.grid() - if filename != None: + if filename is not None: _plt.savefig('Simulation Terms.png') if plotstate: _plt.show() # Plot combined output if (plotresult and solution == 3): - cofig = _plt.figure("Combined Output") + _ = _plt.figure("Combined Output") C = _np.asarray(C) # convert back to array for operation for i in range(cC): yout = yout + xtim[i] * C[0][i] # Sum all st-space var mult. by their coeff yout = _np.asarray(yout) # convert output to array for plotting purposes _plt.plot(TT, yout[0]) - if xlim != False: + if not xlim: _plt.xlim(xlim) - if ylim != False: + if not ylim: _plt.ylim(ylim) _plt.title("Combined Output " + title) _plt.xlabel("Time (seconds)") _plt.grid() - if filename != None: + if filename is not None: _plt.savefig('Simulation Combined Output.png') if plotresult: _plt.show() @@ -958,7 +958,6 @@ def func_c(self, x): # Concatenated Function Q_funcs = [] P_strgs = [] Q_strgs = [] - Vi_list = [] lists = [P_strgs, Q_strgs] i = 0 # Index ii = 0 # String Index @@ -986,12 +985,12 @@ def func_c(self, x): # Concatenated Function Padd = False Qadd = False for _j in range(N): - if P_list[_k] == None: + if P_list[_k] is None: continue # Don't Generate Requirements for Slack Bus if (_k != _j) and not Padd: # Skip i,i Terms ang_len += 1 Padd = True - if (_k != _j) and (Q_list[_k] != None) and not Qadd: + if (_k != _j) and (Q_list[_k] is not None) and not Qadd: mag_len += 1 Qadd = True Vxdim = ang_len + mag_len @@ -1006,20 +1005,21 @@ def func_c(self, x): # Concatenated Function # Add New Entry To Lists for LST in lists: LST.append(None) - if P_list[_k] == None: + if P_list[_k] is None: continue # Don't Generate Requirements for Slack Bus # Collect Other Terms Yind = "[{}][{}]".format(_k, _j) - if verbose: print("K:", _k, "\tJ:", _j) + if verbose: + print("K:", _k, "\tJ:", _j) # Generate Voltage-Related Strings if _k != _j: # Skip i,i Terms # Generate K-Related Strings - if V_list[_k][0] == None: # The Vk magnitude is unknown + if V_list[_k][0] is None: # The Vk magnitude is unknown Vkm = "Vx[{}]".format(_k + ang_len - magoff * _k) # Use Variable Magnitude Vka = "Vx[{}]".format(_k - angoff) # Use Variable Angle else: # The Vk magnitude is known Vkm = "V_list[{}][0]".format(_k) # Load Magnitude - if V_list[_k][1] == None: # The Vj angle is unknown + if V_list[_k][1] is None: # The Vj angle is unknown Vka = "Vx[{}]".format(_k - angoff) # Use Variable Angle else: Vka = "V_list[{}][1]".format(_k) # Load Angle @@ -1035,7 +1035,8 @@ def func_c(self, x): # Concatenated Function Vja = "V_list[{}][1]".format(_j) # Load Angle # Generate String and Append to List of Functions P_strgs[i] = (Pstr.format(Vkm, Vka, Vjm, Vja, Yind)) - if verbose: print("New P-String:", P_strgs[i]) + if verbose: + print("New P-String:", P_strgs[i]) # Generate Q Requirement if Q_list[_k] is not None: # Generate String and Append to List of Functions @@ -1045,7 +1046,8 @@ def func_c(self, x): # Concatenated Function else: Qgen = "" Q_strgs[i] = (Qstr.format(Vkm, Vka, Vjm, Vja, Yind, Qgen)) - if verbose: print("New Q-String:", Q_strgs[i]) + if verbose: + print("New Q-String:", Q_strgs[i]) # Increment Index at Each Interior Level i += 1 tempPstr = "P_funcs.append(lambda Vx: -P_list[{0}]".format(_k) @@ -1060,10 +1062,12 @@ def func_c(self, x): # Concatenated Function tempPstr += ")" tempQstr += ")" if any(P_strgs[ii:i]): - if verbose: print("Full P-Func Str:", tempPstr) + if verbose: + print("Full P-Func Str:", tempPstr) exec(tempPstr) if any(Q_strgs[ii:i]): - if verbose: print("Full Q-Func Str:", tempQstr) + if verbose: + print("Full Q-Func Str:", tempQstr) exec(tempQstr) ii = i # Increase Lower Index retset = (P_funcs, Q_funcs) From db61c3126fa72d00cfc102a59c282052db5a4331 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 22 Jul 2026 20:08:03 -0700 Subject: [PATCH 11/17] resolved issues with tests, added new tests, resolved linting issues --- .github/copilot-instructions.md | 7 + .pre-commit-config.yaml | 31 ++ .pylintrc | 680 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 10 +- docsource/conf.py | 11 +- docsource/render_images.py | 3 +- electricpy/__init__.py | 293 +++++++------- electricpy/bode.py | 12 +- electricpy/compute.py | 7 +- electricpy/constants.py | 31 +- electricpy/conversions.py | 22 +- electricpy/fault.py | 59 +-- electricpy/geometry/__init__.py | 17 +- electricpy/geometry/circle.py | 8 +- electricpy/geometry/triangle.py | 2 +- electricpy/latex.py | 4 +- electricpy/machines.py | 39 +- electricpy/math.py | 17 +- electricpy/passive.py | 27 +- electricpy/phasors.py | 31 +- electricpy/sim.py | 111 +++--- electricpy/thermal.py | 26 +- electricpy/visu.py | 20 +- pyproject.toml | 4 +- release-version.py | 4 +- ruff.toml | 2 + test/__init__.py | 8 +- test/test_bode.py | 7 +- test/test_bugfixes.py | 35 ++ test/test_circle.py | 16 +- test/test_compute.py | 5 + test/test_constants.py | 1 + test/test_conversions.py | 24 ++ test/test_convertion.py | 6 +- test/test_electricpy.py | 31 +- test/test_electricpy_init.py | 6 + test/test_fault.py | 3 + test/test_geometry.py | 69 ++-- test/test_geometry_init.py | 9 + test/test_imports.py | 68 ++-- test/test_latex.py | 8 + test/test_machines.py | 55 +++ test/test_math.py | 5 + test/test_passive.py | 33 ++ test/test_phasor.py | 5 + test/test_sim.py | 10 + test/test_thermal.py | 34 ++ test/test_triangle.py | 14 + test/test_version.py | 1 + test/test_visu.py | 9 +- 50 files changed, 1490 insertions(+), 450 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 ruff.toml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..e8a1d26e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +# AI Instructions + +* pylint and ruff must be satisfied for all Python code. +* All new code must have type hints for all function parameters and return types. +* All new code must have docstrings for all public functions and classes. +* All new code must have unit tests for all public functions and classes. +* Changes should be applied as patches not as full file replacements. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..dbede396 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.7 + hooks: + # Run the linter. + - id: ruff +- repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + files: ^electricpy/.*\.py$ + types: [python] + require_serial: true + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + "--rcfile=.pylintrc", # Link to your config file + ] diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..c1e4a4fa --- /dev/null +++ b/.pylintrc @@ -0,0 +1,680 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +prefer-stubs=no + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.12 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Regular expression matching correct parameter specification variable names. +# If left empty, parameter specification variable names will be checked with +# the set naming style. +#paramspec-rgx= + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Regular expression matching correct type variable tuple names. If left empty, +# type variable tuple names will be checked with the set naming style. +#typevartuple-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of positional arguments for function / method. +max-positional-arguments=10 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. Pylint's default of 100 is +# based on PEP 8's guidance that teams may choose line lengths up to 99 +# characters. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + too-many-arguments, + too-many-positional-arguments, + too-many-locals, + too-many-branches, + too-many-instance-attributes, + too-many-boolean-expressions, + too-many-return-statements, + no-member, + line-too-long, + trailing-whitespace, + missing-final-newline, + superfluous-parens, + invalid-name, + redefined-builtin, + duplicate-code, + use-yield-from, + too-many-lines, + too-many-statements, + unused-variable, + unused-import, + exec-used, + global-variable-undefined, + global-variable-not-assigned + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# Whether or not to search for fixme's in docstrings. +check-fixme-in-docstring=no + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: 'text', 'parseable', +# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs +# (visual studio) and 'github' (GitHub actions). You can also give a reporter +# class, e.g. mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The maximum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a4122b9..0b99a9a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,18 @@ # Contribution Guidelines for ElectricPy -*ElectricPy - The Electrical Engineer's Python Toolkit* + +> *ElectricPy - The Electrical Engineer's Python Toolkit* We'd *gladly* accept contributions that add functions, enhance documentation, improve testing practices, or better round out this project in general; but to help move things along more quickly, here are a few things to keep in mind. -### Adding New Functions +## Adding New Functions When adding additional functions to the package, we'd love to maintain consistency wherever possible, a few of these things to keep in mind include: **Documentation:** *(a must!)* + * Format function docstrings according to [NumPyDoc](https://numpydoc.readthedocs.io/en/latest/format.html) standards * Use the very first line of docstrings to give a brief (one-line) description @@ -22,6 +24,7 @@ a simple addition/multiplication, it's best to show the formula. would be *greatly* appreciated **Code:** + * When possible, use other core functions already in ElectricPy to build upon * When multiple voltages, currents, or other similar quantities need to be used in the same function, their variables should be uniquely named so to help @@ -30,8 +33,7 @@ such as `Vgenerator` and `Vline`. * Whenever possible, comments should be added in the code to help clarify what operations are being performed - -### Adding new Tests/Test Routines +## Adding new Tests/Test Routines When adding additional test functions and routines, they should be added using the `pytest` framework. diff --git a/docsource/conf.py b/docsource/conf.py index 759e1093..2927924c 100644 --- a/docsource/conf.py +++ b/docsource/conf.py @@ -14,7 +14,7 @@ print(parent_dir) # Generate all Documentation Images -from render_images import main as render_images +from render_images import main as render_images # noqa: E402 render_images() # Gather Version Information from Python File @@ -26,15 +26,6 @@ # MAJOR CHANGE . MINOR CHANGE . MICRO CHANGE print("Sphinx HTML Build For:", name," Version:", ver) - -# Verify Import -try: - import electricpy -except: - print("Couldn't import `electricpy` module!") - sys.exit(9) - - # -- Project information ----------------------------------------------------- project = 'electricpy' diff --git a/docsource/render_images.py b/docsource/render_images.py index 319cef8b..0c17bf69 100644 --- a/docsource/render_images.py +++ b/docsource/render_images.py @@ -3,7 +3,8 @@ ################################################################################ import os -import math, cmath +import math +import cmath import numpy as np import electricpy as ep from electricpy import visu diff --git a/electricpy/__init__.py b/electricpy/__init__.py index 04b2022a..fbd101e9 100644 --- a/electricpy/__init__.py +++ b/electricpy/__init__.py @@ -23,7 +23,8 @@ from scipy.integrate import quad as integrate from .version import NAME, VERSION -from .constants import * +from .constants import * # noqa: F403, F401 +from .constants import RHO_VALUES, De0, VLLcVLN, ILcIP, carson_r, u0, pi, e0 from .phasors import phasor, parallelz __version__ = VERSION @@ -79,8 +80,7 @@ def tcycle(ncycles=1, freq=60): # Return if isinstance(time, _np.ndarray) and len(time) == 1: return time[0] - else: - return time + return time # Define Reactance Calculator @@ -300,8 +300,8 @@ def cprint(val, unit=None, label=None, title=None, else: raise ValueError("Invalid Unit") # Generate Default Arrays - printarr = _np.array([]) # Empty array - numarr = _np.array([]) # Empty array + print_array = _np.array([]) # Empty array + num_array = _np.array([]) # Empty array # Operate on List/Array for i in range(row): _val = val[i] @@ -311,40 +311,40 @@ def cprint(val, unit=None, label=None, title=None, ang = _np.degrees(ang_r) # Convert to degrees mag = _np.around(mag, decimals) # Round ang = _np.around(ang, decimals) # Round - strg = "" + string = "" if _label is not None: - strg += _label + " " - strg += str(mag) + " ∠ " + str(ang) + "°" + string += _label + " " + string += str(mag) + " ∠ " + str(ang) + "°" if _unit is not None: - strg += " " + _unit - printarr = _np.append(printarr, strg) - numarr = _np.append(numarr, [mag, ang]) + string += " " + _unit + print_array = _np.append(print_array, string) + num_array = _np.append(num_array, [mag, ang]) # Reshape Arrays - printarr = _np.reshape(printarr, (row, col)) - numarr = _np.reshape(numarr, (sz, 2)) + print_array = _np.reshape(print_array, (row, col)) + num_array = _np.reshape(num_array, (sz, 2)) # Print if printval and row == 1: if title is not None: print(title) - print(strg) + print(string) elif printval and pretty: - strg = '' + string = '' start = True - for i in printarr: + for i in print_array: if not start: - strg += '\n' - strg += str(i[0]) + string += '\n' + string += str(i[0]) start = False if title is not None: print(title) - print(strg) + print(string) elif printval: if title is not None: print(title) - print(printarr) + print(print_array) # Return if Necessary if ret: - return (numarr) + return (num_array) elif isinstance(val, (int, float, complex)): # Handle Invalid Unit/Label if unit is not None and not isinstance(unit, str): @@ -355,22 +355,23 @@ def cprint(val, unit=None, label=None, title=None, ang = _np.degrees(ang_r) # Convert to degrees mag = _np.around(mag, decimals) # Round ang = _np.around(ang, decimals) # Round - strg = "" + string = "" if label is not None: - strg += label + " " - strg += str(mag) + " ∠ " + str(ang) + "°" + string += label + " " + string += str(mag) + " ∠ " + str(ang) + "°" if unit is not None: - strg += " " + unit + string += " " + unit # Print values (by default) if printval: if title is not None: print(title) - print(strg) + print(string) # Return values when requested if ret: return ([mag, ang]) else: raise ValueError("Invalid Input Type") + return None # Define Phase/Line Converter @@ -425,7 +426,7 @@ def phaseline(VLL=None, VLN=None, Iline=None, Iphase=None, realonly=None, 7967.434 ∠ -30.0° """ # Monitor for deprecated input - if 'complex' in kwargs.keys(): + if 'complex' in kwargs: if realonly is None: realonly = not kwargs['complex'] caller = _getframeinfo(_stack()[1][0]) @@ -544,14 +545,13 @@ def powerset(P=None, Q=None, S=None, PF=None, find=''): find = find.upper() if find == 'P': return P - elif find == 'Q': + if find == 'Q': return Q - elif find == 'S': + if find == 'S': return S - elif find == 'PF': + if find == 'PF': return PF - else: - return P, Q, S, PF + return P, Q, S, PF def slew_rate(V=None, freq=None, SR=None, find=''): @@ -593,12 +593,11 @@ def slew_rate(V=None, freq=None, SR=None, find=''): " parameters given to calculate.") if find == 'V': return V - elif find == 'freq': + if find == 'freq': return freq - elif find == 'SR': + if find == 'SR': return SR - else: - return V, freq, SR + return V, freq, SR # Define Non-Linear Power Factor Calculator @@ -708,29 +707,27 @@ def short_circuit_current(V, Z, t=None, f=None, mxcurrent=True, alpha=None): Irms = K * IAC # Return Values return Irms, IAC, K - elif alpha is None or omega is None: + if alpha is None or omega is None: raise ValueError("ERROR: Inappropriate Arguments Provided.") # Calculate Instantaneous if all angular values provided - else: - # Convert Degrees to Radians - omega = _np.radians(omega) - alpha = _np.radians(alpha) - theta = _np.radians(theta) - # Calculate T - T = X / (2 * _np.pi * f * R) # seconds - # Calculate iAC and iDC - iAC = _np.sqrt(2) * V / Z * _np.sin(omega * t + alpha - theta) - iDC = -_np.sqrt(2) * V / Z * \ - _np.sin(alpha - theta) * _np.exp(-t / T) - i = iAC + iDC - # Return Values - return i, iAC, iDC, T - elif (t is not None and f is None) or (t is None and f is not None): + # Convert Degrees to Radians + omega = _np.radians(omega) + alpha = _np.radians(alpha) + theta = _np.radians(theta) + # Calculate T + T = X / (2 * _np.pi * f * R) # seconds + # Calculate iAC and iDC + iAC = _np.sqrt(2) * V / Z * _np.sin(omega * t + alpha - theta) + iDC = -_np.sqrt(2) * V / Z * \ + _np.sin(alpha - theta) * _np.exp(-t / T) + i = iAC + iDC + # Return Values + return i, iAC, iDC, T + if (t is not None and f is None) or (t is None and f is not None): raise ValueError("ERROR: Inappropriate Arguments Provided.\n" + "Must provide both t and f or neither.") - else: - Iac = abs(V / Z) - return Iac + Iac = abs(V / Z) + return Iac # Alias to original Name @@ -847,8 +844,7 @@ def curdiv(Ri, Rset, Vin=None, Iin=None, Vout=False, combine=True): if Vout: # Asked for voltage across resistor of interest Vi = Ii * Ri return Ii, Vi - else: - return Ii + return Ii # Induction Machine Slip @@ -988,6 +984,7 @@ def dynetz(delta=None, wye=None, round=None): if round is not None: Zset = _np.around(Zset, round) return Zset # Return Wye Impedances + if delta is None and wye is not None: Z1, Z2, Z3 = wye # Gather particular impedances Zmultsum = Z1 * Z2 + Z2 * Z3 + Z3 * Z1 @@ -998,6 +995,7 @@ def dynetz(delta=None, wye=None, round=None): if round is not None: Zset = _np.around(Zset, round) return Zset # Return Delta Impedances + raise ValueError("ERROR: Either delta or wye impedances must be specified.") # calculating impedance of bridge network @@ -1139,9 +1137,9 @@ def zsource(S, V, XoR, Sbase=None, Vbase=None, perunit=True): if isinstance(nu, (list, _np.ndarray)): Zsource_pu = [] for angle in nu: - Zsource_pu.append(phasors(Zsource_pu, angle)) + Zsource_pu.append(phasor(Zsource_pu, angle)) else: - Zsource_pu = phasors(Zsource_pu, nu) + Zsource_pu = phasor(Zsource_pu, nu) if not perunit: Zsource = Zsource_pu * Vbase ** 2 / Sbase return Zsource @@ -1433,11 +1431,11 @@ def wrms(func, dw=0.1, NN=100, quad=False, plot=True, Stot = Sw2 = 0 # Power Density Spectrum Sxx = _np.array([]) - for n in range(NN): + for _n in range(NN): # Calculate Power Density Spectrum - Sxx = _np.append(Sxx, func(omega[n])) - Stot = Stot + Sxx[n] - Sw2 = Sw2 + (omega[n] ** 2) * Sxx[n] + Sxx = _np.append(Sxx, func(omega[_n])) + Stot = Stot + Sxx[_n] + Sw2 = Sw2 + (omega[_n] ** 2) * Sxx[_n] if quad: def intf(w): return w ** 2 * func(w) @@ -1464,6 +1462,7 @@ def intf(w): # Define Hartley's Equation for Data Capacity +# pylint: disable-next=redefined-outer-name def hartleydata(BW, M): """ Hartley Data Function. @@ -1543,8 +1542,7 @@ def zpu(S, VLL=None, VLN=None): raise ValueError("ERROR: One voltage must be provided.") if VLL is not None: return VLL ** 2 / S - else: - return (_np.sqrt(3) * VLN) ** 2 / S + return (_np.sqrt(3) * VLN) ** 2 / S # Define Per-Unit Current Formula @@ -1579,10 +1577,9 @@ def ipu(S, VLL=None, VLN=None, V1phs=None): raise ValueError("ERROR: One voltage must be provided.") if VLL is not None: return S / (_np.sqrt(3) * VLL) - elif VLN is not None: + if VLN is not None: return S / (3 * VLN) - else: - return S / V1phs + return S / V1phs # Define Per-Unit Change of Base Function @@ -1683,9 +1680,8 @@ def rxrecompose(x_pu, XoR, S3phs=None, VLL=None, VLN=None): # Recompose if S3phs is None: return z_pu - else: - z = zrecompose(z_pu, S3phs, VLL, VLN) - return z + z = zrecompose(z_pu, S3phs, VLL, VLN) + return z # Define Generator Internal Voltage Calculator @@ -1725,12 +1721,12 @@ def geninternalv(I, Zs, Vt, Vgn=None, Zm=None, Zmp=None, Zmpp=None, Ip=None, Ipp The internal voltage of the generator. """ # All Parameters Provided - if Zmp == Zmpp == Ip == Ipp is not None: + if all(param is not None for param in (Zmp, Zmpp, Ip, Ipp)): if Vgn is None: Vgn = 0 Ea = Zs * I + Zmp * Ip + Zmpp * Ipp + Vt + Vgn # Select Parameters Provided - elif Vgn == Zm == Ip == Ipp is None: + elif all(param is None for param in (Vgn, Zm, Ip, Ipp)): Ea = Zs * I + Vt # Invalid Parameter Set else: @@ -1781,9 +1777,8 @@ def funcfft(func, minfreq=60, maxmult=15, complex=False): if complex: return y # Split out useful values - else: - y *= 2 - return y[0].real, y[1:-1].real, -y[1:-1].imag + y *= 2 + return y[0].real, y[1:-1].real, -y[1:-1].imag def sampfft(data, dt, minfreq=60.0, complex=False): @@ -1824,7 +1819,7 @@ def sampfft(data, dt, minfreq=60.0, complex=False): "Too few data samples to evaluate FFT at specified minimum " "frequency." ) - elif FR == minfreq: + if FR == minfreq: # Evaluate FFT y = _np.fft.rfft(data) / len(data) else: @@ -1836,9 +1831,8 @@ def sampfft(data, dt, minfreq=60.0, complex=False): if complex: return (y) # Split out useful values - else: - y *= 2 - return y[0].real, y[1:-1].real, -y[1:-1].imag + y *= 2 + return y[0].real, y[1:-1].real, -y[1:-1].imag # Define FFT Plotting Function @@ -1946,12 +1940,12 @@ def fftsumplot(dc, real, imag=None, freq=60, xrange=None, npts=1000, # Initialize output with DC term yout = _np.ones(len(x)) * dc # Plot each iteration of the Fourier Series - for k in range(1, N): + for _k in range(1, N): if plotall: _plt.plot(x, yout) - yout += real[k - 1] * _np.cos(k * 2 * _np.pi * x / T) + yout += real[_k - 1] * _np.cos(_k * 2 * _np.pi * x / T) if imag is not None: - yout += imag[k - 1] * _np.sin(k * 2 * _np.pi * x / T) + yout += imag[_k - 1] * _np.sin(_k * 2 * _np.pi * x / T) _plt.plot(x, yout) _plt.title(title) _plt.xlabel("Time (seconds)") @@ -2005,23 +1999,20 @@ def harmonics(real, imag=None, dc=0, freq=60, domain=None): def _harmonic_(t): out = dc - for k in range(len(real)): - # Evaluate Current Coefficient - A = real[k] + for index, A in enumerate(real): if imag is not None: - B = imag[k] + B = imag[index] else: B = 0 - m = k + 1 + multiplier = index + 1 # Calculate Output - out += A * _np.cos(m * w * t) + B * _np.sin(m * w * t) + out += A * _np.cos(multiplier * w * t) + B * _np.sin(multiplier * w * t) # Return Value return (out) if domain is None: return _harmonic_ # Return as callable for external use - else: - return _harmonic_(domain) + return _harmonic_(domain) # Define Single Phase Motor Startup Capacitor Formula @@ -2100,9 +2091,9 @@ def pfcorrection(S, PFold, PFnew, VLL=None, VLN=None, V=None, freq=60): Qcorrected = _np.sqrt(Scorrected ** 2 - Pold ** 2) Qc = Qold - Qcorrected # Evaluate Capacitance Based on Voltage Input - if VLL == VLN == V is None: + if all(param is None for param in (VLL, VLN, V)): raise ValueError("One voltage must be specified.") - elif VLN is not None: + if VLN is not None: C = Qc / (2 * _np.pi * freq * 3 * VLN ** 2) else: if VLL is not None: @@ -2170,7 +2161,7 @@ def acpiv(S=None, I=None, VLL=None, VLN=None, V=None, PF=None): (96.4174949546675, 55.66666666666667, 167.0) """ # Validate Inputs - if S == I is None: + if S is None and I is None: raise ValueError("To few arguments.") # Convert Apparent Power to Complex if PF is not None: @@ -2180,31 +2171,30 @@ def acpiv(S=None, I=None, VLL=None, VLN=None, V=None, PF=None): if S is None: # Solve for Apparent Power S = V * _np.conj(I) return S - else: # Solve for Current - I = _np.conj(S / V) - return I + # Solve for Current + I = _np.conj(S / V) + return I # Solve Line-to-Line - elif VLL is not None: + if VLL is not None: if S is None: # Solve for Apparent Power S = _np.sqrt(3) * VLL * _np.conj(I) return S - else: # Solve for Current - I = _np.conj(S / (_np.sqrt(3) * VLL)) - return I + # Solve for Current + I = _np.conj(S / (_np.sqrt(3) * VLL)) + return I # Solve Line-to-Neutral - elif VLN is not None: + if VLN is not None: if S is None: # Solve for Apparent Power S = 3 * VLN * _np.conj(I) return S - else: # Solve for Current - I = _np.conj(S / (3 * VLN)) - return I + # Solve for Current + I = _np.conj(S / (3 * VLN)) + return I # Solve for Voltages - else: - V = S / _np.conj(I) - VLL = S / (_np.sqrt(3) * _np.conj(I)) - VLN = S / (3 * _np.conj(I)) - return VLL, VLN, V + V = S / _np.conj(I) + VLL = S / (_np.sqrt(3) * _np.conj(I)) + VLN = S / (3 * _np.conj(I)) + return VLL, VLN, V # Define Primary Ratio Function @@ -2346,33 +2336,33 @@ def suspension_insulators(number_capacitors, capacitance_ratio, Voltage): Voltage across each capacitive disk starting from top to bottom """ - m = _np.zeros((number_capacitors, number_capacitors)) + _m = _np.zeros((number_capacitors, number_capacitors)) # Iterate over capacitors for i in range(number_capacitors - 1): # Iterate over capacitors for j in range(number_capacitors - 1): # If inner iteration is less than outer iteration if i >= j: - m[i, j] = 1 / capacitance_ratio + _m[i, j] = 1 / capacitance_ratio for i in range(number_capacitors - 1): - m[i, i] = (1 + 1 / capacitance_ratio) + _m[i, i] = (1 + 1 / capacitance_ratio) - m[i, i + 1] = -1 + _m[i, i + 1] = -1 - m[number_capacitors - 1, :] = 1 + _m[number_capacitors - 1, :] = 1 v = _np.zeros((number_capacitors, 1)) v[number_capacitors - 1, 0] = Voltage - capacitor_disk_voltages = _np.matmul(_np.linalg.inv(m), v) + capacitor_disk_voltages = _np.matmul(_np.linalg.inv(_m), v) string_efficiency = ( (Voltage * 100) / (number_capacitors * capacitor_disk_voltages[-1, 0]) ) - return capacitor_disk_voltages, string_efficiency + return string_efficiency, capacitor_disk_voltages # Define Natural Frequency/Resonant Frequency Calculator @@ -2451,12 +2441,11 @@ def unbalance(A, B, C, all=False): # Gather Maximum Variation mx = max(dA, dB, dC) # Calculate Maximum Variation - unbalance = mx / avg + result = mx / avg # Return Results if all: return dA / avg, dB / avg, dC / avg - else: - return unbalance + return result # Define Cosine Filter Function @@ -2490,16 +2479,16 @@ def cosfilt(arr, Srate, domain=False): ind = _np.arange(Srate - 1, len(arr) - 1) # Define Cosine Coefficient Function - def cos(k, Srate): - return _np.cos(2 * _np.pi * k / Srate) + def cos(_k, Srate): + return _np.cos(2 * _np.pi * _k / Srate) # Calculate Constant const = 2 / Srate # Iteratively Calculate cosf = 0 - for k in range(0, Srate - 1): - slc = (ind - (Srate - 1)) + k - cosf += cos(k, Srate) * arr[slc] + for _k in range(0, Srate - 1): + slc = (ind - (Srate - 1)) + _k + cosf += cos(_k, Srate) * arr[slc] # Scale cosf = const * cosf # Return Cosine-Filtered Array @@ -2540,20 +2529,20 @@ def sinfilt(arr, Srate, domain=False): # Evaluate index set ind = _np.arange(Srate - 1, len(arr) - 1) - # Define Cosine Coefficient Function - def sin(k, Srate): - return _np.sin(2 * _np.pi * k / Srate) + # Define Sine Coefficient Function + def sin(_k, Srate): + return _np.sin(2 * _np.pi * _k / Srate) # Calculate Constant const = 2 / Srate # Iteratively Calculate sinf = 0 - for k in range(0, Srate - 1): - slc = (ind - (Srate - 1)) + k - sinf += sin(k, Srate) * arr[slc] + for _k in range(0, Srate - 1): + slc = (ind - (Srate - 1)) + _k + sinf += sin(_k, Srate) * arr[slc] # Scale sinf = const * sinf - # Return Cosine-Filtered Array + # Return Sine-Filtered Array if domain: xarray = _np.linspace(Srate + Srate / 4 - 1, len(arr) - 1, len(sinf)) xarray = xarray / Srate @@ -2562,12 +2551,13 @@ def sin(k, Srate): # Define Characteristic Impedance Calculator +# pylint: disable-next=redefined-outer-name def characterz(R, G, L, C, freq=60): r""" Characteristic Impedance Calculator. Function to evaluate the characteristic - impedance of a system with specefied + impedance of a system with specified line parameters as defined. System uses the standard characteristic impedance equation :eq:`Zc`. @@ -2591,7 +2581,7 @@ def characterz(R, G, L, C, freq=60): Returns ------- Zc: complex - Charcteristic Impedance of specified line. + Characteristic Impedance of specified line. """ # Evaluate omega w = 2 * _np.pi * freq @@ -2613,9 +2603,9 @@ def propagation_constants(z, y, length): From the above equation, the following formulas are derived to evaluate the desired constants. - .. math:: \gamma = \sqrt( z * y ) + .. math:: \gamma = \sqrt{ z * y } - .. math:: Z_{\text{surge}} = \sqrt( z / y ) + .. math:: Z_{\text{surge}} = \sqrt{ z / y } .. math:: \alpha = \Re{ \gamma } @@ -2624,9 +2614,9 @@ def propagation_constants(z, y, length): Parameters ---------- z: complex - Impedence of the transmission line: R+j*2*pi*f*L + Impedance of the transmission line: R+j*2*pi*f*L y: complex - Admitance of the transmission line g+j*2*pi*f*C + Admittance of the transmission line g+j*2*pi*f*C Returns ------- @@ -2641,7 +2631,7 @@ def propagation_constants(z, y, length): # Validate the line length is substantial enough for calculation if not (length > 500): raise ValueError( - "Long transmission line length should be grater than 500km" + "Long transmission line length should be greater than 500km" ) gamma = _np.sqrt(z * y) alpha = gamma.real @@ -2681,9 +2671,12 @@ def de_calc(rho, freq=60): rho = rho.upper() try: rho = RHO_VALUES[rho] - except KeyError: - raise ValueError("Invalid Earth Resistivity string try to select \ - from set of (SEA, SWAMP, AVG, AVERAGE, DAMP, DRY, SAND, SANDSTONE") + except KeyError as exc: + raise ValueError( + "Invalid Earth Resistivity string try to select " + "from set of (SEA, SWAMP, AVG, AVERAGE, DAMP, DRY, SAND, " + "SANDSTONE)" + ) from exc # Calculate De De = De0 * _np.sqrt(rho / freq) return De @@ -3116,8 +3109,6 @@ def wireresistance(length=None, diameter=None, rho=16.8 * 10 ** -9, R=None): R: [float], optional Wire resistance, unitless. """ - if R == length == diameter is None: - raise ValueError("To few arguments.") # Given length and diameter if length is not None and diameter is not None: # calculating the area @@ -3132,6 +3123,7 @@ def wireresistance(length=None, diameter=None, rho=16.8 * 10 ** -9, R=None): if R is not None and length is not None: A = rho * length / R return _np.sqrt(4 * A / pi) + raise ValueError("Too few arguments.") def parallel_plate_capacitance(A=None, d=None, e=e0, C=None): @@ -3167,8 +3159,6 @@ def parallel_plate_capacitance(A=None, d=None, e=e0, C=None): C: float, optional Capacitance, unitless. """ - if C == A == d is None: - raise ValueError("To few arguments.") # Given area and distance if A is not None and d is not None: return e * A / d @@ -3178,8 +3168,10 @@ def parallel_plate_capacitance(A=None, d=None, e=e0, C=None): # Given capacitance and area if C is not None and A is not None: return e * A / C + raise ValueError("Too few arguments.") +# pylint: disable-next=redefined-outer-name def solenoid_inductance(A=None, l=None, N=None, u=u0, L=None): r""" Solenoid Inductance Calculator. @@ -3217,8 +3209,6 @@ def solenoid_inductance(A=None, l=None, N=None, u=u0, L=None): L: float, optional Inductance, unitless. """ - if L == A == l == N is None: - raise ValueError("To few arguments.") # Given area, length and number of turns if A is not None and l is not None and N is not None: return N ** 2 * u * A / l @@ -3231,6 +3221,7 @@ def solenoid_inductance(A=None, l=None, N=None, u=u0, L=None): # Given inductance, area and length if L is not None and A is not None and l is not None: return _np.sqrt(L * l / (u * A)) + raise ValueError("Too few arguments.") def ic_555_astable(R=None, C=None, freq=None, t_high=None, t_low=None): @@ -3305,7 +3296,8 @@ def ic_555_astable(R=None, C=None, freq=None, t_high=None, t_low=None): raise TypeError("Not enough parqmeters are passed") -def ic_555_monostable(R=None, C=None, freq=None, t_high=None, t_low=None): +# pylint: disable-next=inconsistent-return-statements +def ic_555_monostable(R=None, C=None, t_high=None, t_low=None): """ 555 Integrated Circuit Calculator. @@ -3321,8 +3313,7 @@ def ic_555_monostable(R=None, C=None, freq=None, t_high=None, t_low=None): List of 2 resistor which are need in configuring IC 555. C: float, optional Capacitance between Threshold Pin and ground - f: float, optional - Electrical system frequency in Hertz. + # f removed. t_high: float, optional ON time of IC 555 t_low: float, optional diff --git a/electricpy/bode.py b/electricpy/bode.py index 2dd2c24c..a1c375f3 100644 --- a/electricpy/bode.py +++ b/electricpy/bode.py @@ -34,13 +34,13 @@ def _sys_condition(system, feedback): if ld < ln: den = _np.append(_np.zeros(ln - ld), den) # Pad beginning with zeros den = den + num # Add numerator and denominator - for i in range(len(num)): - if num[i] != 0: - num = num[i:] # Slice zeros off the front of the numerator + for ind, val in enumerate(num): + if val != 0: + num = num[ind:] # Slice zeros off the front of the numerator break # Break out of for loop - for i in range(len(den)): - if den[i] != 0: - den = den[i:] # Slice zeros off the front of the denominator + for ind, val in enumerate(den): + if val != 0: + den = den[ind:] # Slice zeros off the front of the denominator break # Break out of for loop system = (num, den) # Repack system return system # Return the conditioned system diff --git a/electricpy/compute.py b/electricpy/compute.py index 92e2fd24..c15a5fa7 100644 --- a/electricpy/compute.py +++ b/electricpy/compute.py @@ -39,7 +39,7 @@ def largest_integer(numBits, signed=True): signed: bool, optional Control to specify whether the value should be evaluated for signed, or unsigned, integers. Defaults to True. - + Returns ------- int: The maximum value that can be stored in an integer of numBits. @@ -54,11 +54,12 @@ def largest_integer(numBits, signed=True): >>> cmp.largest_integer(32, signed=True) 2147483647 """ + if numBits <= 0: + raise ValueError("numBits must be greater than zero") # Use Signed or Unsigned Formula if signed: return int(2 ** (numBits - 1) - 1) - else: - return int(2 ** (numBits) - 1) + return int(2 ** numBits - 1) # Define CRC Generator (Sender Side) diff --git a/electricpy/constants.py b/electricpy/constants.py index cf8aa790..31b51a6a 100644 --- a/electricpy/constants.py +++ b/electricpy/constants.py @@ -7,8 +7,8 @@ """ ################################################################################ -import numpy as _np import cmath as _c +import numpy as _np # Define Electrical Engineering Constants pi = _np.pi #: PI Constant 3.14159... @@ -49,17 +49,24 @@ [-1 / _np.sqrt(6), -1 / _np.sqrt(2), 1 / _np.sqrt(3)] ]) # Define Park Components Matricies -_rad = lambda th: _np.radians(th) -_Pdq0_im = lambda th: _np.sqrt(2 / 3) * _np.array([ - [_np.cos(_rad(th)), _np.cos(_rad(th) - 2 * pi / 3), _np.cos(_rad(th) + 2 * pi / 3)], - [-_np.sin(_rad(th)), -_np.sin(_rad(th) - 2 * pi / 3), -_np.sin(_rad(th) + 2 * pi / 3)], - [_np.sqrt(2) / 2, _np.sqrt(2) / 2, _np.sqrt(2) / 2] -]) -_Pabc_im = lambda th: _np.sqrt(2 / 3) * _np.array([ - [_np.cos(_rad(th)), -_np.sin(_rad(th)), _np.sqrt(2) / 2], - [_np.cos(_rad(th) - 2 * pi / 3), -_np.sin(_rad(th) - 2 * pi / 3), _np.sqrt(2) / 2], - [_np.cos(_rad(th) + 2 * pi / 3), -_np.sin(_rad(th) + 2 * pi / 3), _np.sqrt(2) / 2] -]) +def _rad(th): + return _np.radians(th) + + +def _Pdq0_im(th): + return _np.sqrt(2 / 3) * _np.array([ + [_np.cos(_rad(th)), _np.cos(_rad(th) - 2 * pi / 3), _np.cos(_rad(th) + 2 * pi / 3)], + [-_np.sin(_rad(th)), -_np.sin(_rad(th) - 2 * pi / 3), -_np.sin(_rad(th) + 2 * pi / 3)], + [_np.sqrt(2) / 2, _np.sqrt(2) / 2, _np.sqrt(2) / 2] + ]) + + +def _Pabc_im(th): + return _np.sqrt(2 / 3) * _np.array([ + [_np.cos(_rad(th)), -_np.sin(_rad(th)), _np.sqrt(2) / 2], + [_np.cos(_rad(th) - 2 * pi / 3), -_np.sin(_rad(th) - 2 * pi / 3), _np.sqrt(2) / 2], + [_np.cos(_rad(th) + 2 * pi / 3), -_np.sin(_rad(th) + 2 * pi / 3), _np.sqrt(2) / 2] + ]) Pdq0 = 2 / 3 * _np.array([[0, -_np.sqrt(3 / 2), _np.sqrt(3 / 2)], [1, -1 / 2, -1 / 2], [1 / 2, 1 / 2, 1 / 2]]) diff --git a/electricpy/conversions.py b/electricpy/conversions.py index af24905a..d365082f 100644 --- a/electricpy/conversions.py +++ b/electricpy/conversions.py @@ -12,10 +12,8 @@ """ ################################################################################ -from electricpy.constants import WATTS_PER_HP, Aabc, A012, KWH_PER_BTU - -# Import Required Packages import numpy as _np +from electricpy.constants import A012, Aabc, KWH_PER_BTU, WATTS_PER_HP # Define HP to Watts Calculation @@ -148,7 +146,7 @@ def rad_to_hz(radians): ------- hertz: float The frequency (represented in Hertz) - + Examples -------- >>> from electricpy import pi @@ -183,7 +181,7 @@ def hz_to_rad(hz): ------- radians: float The frequency (represented in radians/sec) - + Examples -------- >>> from electricpy import conversions as conv @@ -299,19 +297,17 @@ def seq_to_abc(M012, reference='A'): >>> phs_quantities = conv.seq_to_abc(seq_quantities) >>> # Returned Phase Quantities will Approximately Equal the Original Values """ - # Compute Dot Product - M = A012.dot(M012) # Condition Reference: reference = reference.upper() if reference == 'A': - pass + M = Aabc elif reference == 'B': - M = _np.roll(M, 1, 0) + M = _np.roll(Aabc, 1, 0) elif reference == 'C': - M = _np.roll(M, 2, 0) + M = _np.roll(Aabc, 2, 0) else: raise ValueError("Invalid Phase Reference.") - return M + return _np.linalg.inv(M).dot(M012) # Define Second Name for seq_to_abc @@ -409,7 +405,7 @@ def rad_to_rpm(rad): ------- rpm: float The angular velocity in revolutions-per-minute (RPM) - + Examples -------- >>> from electricpy import pi @@ -438,7 +434,7 @@ def rpm_to_rad(rpm): ------- rad: float The angular velocity in radians-per-second - + Examples -------- >>> from electricpy import pi diff --git a/electricpy/fault.py b/electricpy/fault.py index db20b0f1..630a6ca5 100644 --- a/electricpy/fault.py +++ b/electricpy/fault.py @@ -10,11 +10,11 @@ import matplotlib.pyplot as _plt from scipy.optimize import fsolve as _fsolve -from electricpy.constants import * +from electricpy.constants import XFMD1, XFMD11, XFM12, XFMY0 from electricpy.conversions import seq_to_abc -def _phaseroll(M012, reference): +def _phase_roll(M012, reference): # Compute Dot Product return seq_to_abc(M012, reference) @@ -67,7 +67,7 @@ def single_phase_to_ground_fault(Vth, Zseq, Rf=0, sequence=True, reference='A'): Ifault = _np.array([Ifault, Ifault, Ifault]) # Prepare Value for return if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain # Return Value return Ifault @@ -125,7 +125,7 @@ def double_phase_to_ground_fault(Vth, Zseq, Rf=0, sequence=True, reference='A'): Ifault = _np.array([If0, If1, If2]) # Return Currents if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain return Ifault # Alias Original Name @@ -182,7 +182,7 @@ def phase_to_phase_fault(Vth, Zseq, Rf=0, sequence=True, reference='A'): Ifault = _np.array([If0, If1, If2]) # Return Currents if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain return Ifault # Alias Original Name @@ -233,7 +233,7 @@ def three_phase_fault(Vth, Zseq, Rf=0, sequence=True, reference='A'): Ifault = _np.array([0, Ifault, 0]) # Prepare to Return Value if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain return Ifault # Alias Original Name @@ -288,7 +288,7 @@ def poleopen1(Vth, Zseq, sequence=True, reference='A'): Ifault = _np.array([If0, If1, If2]) # Return Currents if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain return Ifault @@ -340,7 +340,7 @@ def poleopen2(Vth, Zseq, sequence=True, reference='A'): Ifault = _np.array([If0, If1, If2]) # Return Currents if not sequence: - Ifault = _phaseroll(Ifault, reference) # Convert to ABC-Domain + Ifault = _phase_roll(Ifault, reference) # Convert to ABC-Domain return Ifault @@ -405,6 +405,7 @@ def short_circuit_mva(Zth=None, Isc=None, Vth=1): # Define Explicitly 3-Phase MVAsc Calculator +# pylint: disable-next=unused-argument def phs3mvasc(Vth, Zseq, Rf=0, Sbase=1): r""" Three-Phase MVA Short-Circuit Calculator. @@ -541,7 +542,7 @@ def busvolt(k, n, Vpf, Z0, Z1, Z2, If, sequence=True, reference='A'): # Perform Calculation Vf = Vfmat - Zmat.dot(If) if not sequence: - Vf = _phaseroll(Vf, reference) # Convert to ABC-Domain + Vf = _phase_roll(Vf, reference) # Convert to ABC-Domain return Vf @@ -709,7 +710,7 @@ def ct_satratburden(Inom, VArat=None, ANSIv=None, ALF=20, ): # Validate Inputs if VArat is None and ANSIv is None: raise ValueError("VArat or ANSIv must be specified.") - elif VArat is None: + if VArat is None: # Calculate VArat from ANSIv VArat = Inom * ANSIv / (20) # Determine Vsaturation @@ -744,7 +745,7 @@ def ct_vpeak(Zb, Ip, CTR): # Define Saturation Time Calculator -def ct_timetosat(Vknee, XoR, Rb, CTR, Imax, ts=None, npts=100, freq=60, +def ct_timetosat(Vknee, XoR, Rb, Imax, ts=None, npts=100, freq=60, plot=False): r""" Electrical Current Transformer (CT) Time to Saturation Function. @@ -760,8 +761,7 @@ def ct_timetosat(Vknee, XoR, Rb, CTR, Imax, ts=None, npts=100, freq=60, The X-over-R ratio of the system. Rb: float The total burden resistance in ohms. - CTR: float - The CT Ratio (primary/secondary, N) to be used. + # CTR parameter removed Imax: float The (maximum) current magnitude to use for calculation, typically the fault current. @@ -922,7 +922,7 @@ def equations(data): # Define Time-Overcurrent Trip Time Function -def toctriptime(I, Ipickup, TD, curve="U1", CTR=1): +def toctriptime(I, Ipickup, TD, curve="U1", CTR=1): # noqa: E741 """ Time OverCurrent Trip Time Function. @@ -974,7 +974,7 @@ def toctriptime(I, Ipickup, TD, curve="U1", CTR=1): # Define Time Overcurrent Reset Time Function -def tocreset(I, Ipickup, TD, curve="U1", CTR=1): +def tocreset(I, Ipickup, TD, curve="U1", CTR=1): # noqa: E741 """ Time OverCurrent Reset Time Function. @@ -1047,7 +1047,7 @@ def pickup(Iloadmax, Ifaultmin, scale=0, printout=False, units="A"): """ IL2 = 2 * Iloadmax IF2 = Ifaultmin / 2 - exponent = len(str(IL2).split('.')[0]) + exponent = len(str(IL2).split('.', maxsplit=1)[0]) setpoint = _np.ceil(IL2 * 10 ** (-exponent + 1 + scale)) * 10 ** (exponent - 1 - scale) if printout: print("Range Min:", IL2, units, "\t\tRange Max:", IF2, units) @@ -1061,8 +1061,9 @@ def pickup(Iloadmax, Ifaultmin, scale=0, printout=False, units="A"): # Define Time-Dial Coordination Function -def tdradial(I, CTI, Ipu_up, Ipu_dn=0, TDdn=0, curve="U1", scale=2, freq=60, - CTR_up=1, CTR_dn=1, tfixed=None): +def tdradial( # noqa: E741 + I, CTI, Ipu_up, Ipu_dn=0, TDdn=0, curve="U1", scale=2, freq=60, # noqa: E741 + CTR_up=1, CTR_dn=1, tfixed=None): """ Radial Time Dial Coordination Function. @@ -1346,7 +1347,7 @@ def symrmsfaultcur(V, R, X, t=1 / 60, freq=60): # Define Relay M Formula -def faultratio(I, Ipickup, CTR=1): +def faultratio(I, Ipickup, CTR=1): # noqa: E741 """ Fault Multiple of Pickup (Ratio) Calculator. @@ -1461,7 +1462,7 @@ def distmeasz(VLNmeas, If, Ip, Ipp, CTR=1, VTR=1, k0=None, z1=None, z0=None, The "measured" impedance as calculated by the relay. """ # Validate Residual Compensation Inputs - if k0 == z1 == z0 is None: + if all(param is None for param in (k0, z1, z0)): raise ValueError("Residual compensation arguments must be set.") if k0 is None and (z1 is None or z0 is None): raise ValueError("Both *z1* and *z0* must be specified.") @@ -1471,9 +1472,9 @@ def distmeasz(VLNmeas, If, Ip, Ipp, CTR=1, VTR=1, k0=None, z1=None, z0=None, # Convert Primary Units to Secondary V = VLNmeas / VTR Ir = (If + Ip + Ipp) / CTR - I = If / CTR + fault_current = If / CTR # Calculate Measured Impedance - Zmeas = V / (I + k0 * Ir) + Zmeas = V / (fault_current + k0 * Ir) return Zmeas @@ -1509,8 +1510,9 @@ def transmismatch(I1, I2, tap1, tap2): # Define High-Impedance Bus Protection Pickup Function -def highzvpickup(I, RL, Rct, CTR=1, threephase=False, Ks=1.5, - Vstd=400, Kd=0.5): +def highzvpickup( # noqa: E741 + I, RL, Rct, CTR=1, threephase=False, Ks=1.5, # noqa: E741 + Vstd=400, Kd=0.5): """ High Impedance Pickup Setting Function. @@ -1550,7 +1552,8 @@ def highzvpickup(I, RL, Rct, CTR=1, threephase=False, Ks=1.5, """ # Condition Based on threephase Argument n = 2 - if threephase: n = 1 + if threephase: + n = 1 # Evaluate Secure Voltage Pickup Vsens = Ks * (n * RL + Rct) * I / CTR # Evaluate Dependible Voltage Pickup @@ -1591,7 +1594,7 @@ def highzmini(N, Ie, Irly=None, Vset=None, Rrly=2000, Imov=0, CTR=1): bus protection element pickup. """ # Validate Inputs - if Irly == Vset is None: + if Irly is None and Vset is None: raise ValueError("Relay Current Required.") # Condition Inputs Ie = abs(Ie) @@ -1763,8 +1766,8 @@ def synmach_Isym(t, Eq, Xd, Xdp, Xdpp, Tdp, Tdpp): """ # Calculate Time-Constant Term t_c = ( - 1 / Xd + - (1 / Xdp - 1 / Xd) * _np.exp(-t / Tdp) + + 1 / Xd + + (1 / Xdp - 1 / Xd) * _np.exp(-t / Tdp) + (1 / Xdpp - 1 / Xdp) * _np.exp(-t / Tdpp) ) # Calculate Fault Current diff --git a/electricpy/geometry/__init__.py b/electricpy/geometry/__init__.py index 3a797296..d96890d3 100644 --- a/electricpy/geometry/__init__.py +++ b/electricpy/geometry/__init__.py @@ -273,15 +273,14 @@ def section(p1: Point, p2: Point, ratio: Union[Tuple[Number, Number], float]) -> if isinstance(ratio, (int, float)): t = _as_float(ratio) return Point(p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)) - else: - m, n = ratio - m = _as_float(m) - n = _as_float(n) - if _is_close(m + n, 0.0): - raise ZeroDivisionError("Invalid ratio: m+n must be nonzero.") - # Preserve original convention: m corresponds to p2 weight, n to p1 weight via: - # p = (n*p1 + m*p2)/(m+n) - return Point((n * p1.x + m * p2.x) / (m + n), (n * p1.y + m * p2.y) / (m + n)) + m, n = ratio + m = _as_float(m) + n = _as_float(n) + if _is_close(m + n, 0.0): + raise ZeroDivisionError("Invalid ratio: m+n must be nonzero.") + # Preserve original convention: m corresponds to p2 weight, n to p1 weight via: + # p = (n*p1 + m*p2)/(m+n) + return Point((n * p1.x + m * p2.x) / (m + n), (n * p1.y + m * p2.y) / (m + n)) def midpoint(p1: Point, p2: Point) -> Point: diff --git a/electricpy/geometry/circle.py b/electricpy/geometry/circle.py index 980bebb6..1b8ec15d 100644 --- a/electricpy/geometry/circle.py +++ b/electricpy/geometry/circle.py @@ -132,12 +132,12 @@ def normal(self, p: Point) -> Line: """ return Line.construct(p, self.center) - def is_tangent(self, l: Line, *, tol: float = 1e-9) -> bool: + def is_tangent(self, l: Line, *, tol: float = 1e-9) -> bool: # noqa: E741 """Return True if the line is tangent to the circle (within tolerance).""" d = l.distance(self.center) return _is_close(d, self.radius, rel_tol=tol, abs_tol=tol) - def is_normal(self, l: Line, *, tol: float = 1e-9) -> bool: + def is_normal(self, l: Line, *, tol: float = 1e-9) -> bool: # noqa: E741 """ Line passes through the circle's center (within tolerance). @@ -311,7 +311,7 @@ def intersection( # h = half-chord length h2 = r1 * r1 - a * a - if h2 < 0 and h2 > -tol: + if -tol < h2 < 0: h2 = 0.0 # clamp tiny negatives due to numeric error if h2 < -tol: return None # numeric safety; should not happen if cases above handled @@ -362,7 +362,7 @@ def __ne__(self, other: object) -> bool: def __hash__(self) -> int: """Hash for Circle.""" - return hash((self.center, self.radius)) + return hash((self.center.x, self.center.y, self.radius)) def construct(p0: Point, p1: Point, p2: Point) -> Circle: diff --git a/electricpy/geometry/triangle.py b/electricpy/geometry/triangle.py index 1ee192a4..1cdc2381 100644 --- a/electricpy/geometry/triangle.py +++ b/electricpy/geometry/triangle.py @@ -126,7 +126,7 @@ def area(self) -> float: radicand = s * (s - self.a) * (s - self.b) * (s - self.c) # Clamp tiny negatives caused by floating-point error. - if radicand < 0 and radicand > -self._tol: + if -self._tol < radicand < 0: radicand = 0.0 if radicand < -self._tol: diff --git a/electricpy/latex.py b/electricpy/latex.py index f8e6ab9c..21c43ad9 100644 --- a/electricpy/latex.py +++ b/electricpy/latex.py @@ -69,9 +69,9 @@ def rectstring(val, round): real = _np.around(val.real, round) # Round imag = _np.around(val.imag, round) # Round if imag > 0: - latex = str(real) + "+j" + str(imag) + latex = str(real) + "+\\mathrm{j}" + str(imag) else: - latex = str(real) + "-j" + str(abs(imag)) + latex = str(real) + "-\\mathrm{j}" + str(abs(imag)) return latex # Interpret as numpy array if simple list diff --git a/electricpy/machines.py b/electricpy/machines.py index efadc749..b821e65e 100644 --- a/electricpy/machines.py +++ b/electricpy/machines.py @@ -54,6 +54,7 @@ def transformertest(Poc=False, Voc=False, Ioc=False, Psc=False, Vsc=False, """ SC = False OC = False + Req = Xeq = Rc = Xm = None # Given Open-Circuit Values if (Poc is not None) and (Voc is not None) and (Ioc is not None): PF = Poc / (Voc * Ioc) @@ -71,14 +72,13 @@ def transformertest(Poc=False, Voc=False, Ioc=False, Psc=False, Vsc=False, # Return All if Found if OC and SC: return (Req, Xeq, Rc, Xm) - elif OC: + if OC: return (Rc, Xm) - elif SC: + if SC: return (Req, Xeq) - else: - raise ValueError( - "Not enough arguments were provided for transformertest." - ) + raise ValueError( + "Not enough arguments were provided for transformertest." + ) # Define Simple Transformer Phase Shift Function @@ -126,7 +126,7 @@ def phase_shift_transformer(style="DY", shift=30): # Find Direction v = orientation[style.upper()] # Calculate Shift - phase = _np.exp(1j * _np.radians(v * abs(shift))) + phase = _np.exp(1j * _np.radians(v * shift)) # Return return (phase) @@ -553,7 +553,7 @@ def indmachpkslip(Rr, Zth=None, Rs=0, Lm=0, Lls=0, Llr=0, Ls=None, # Define Induction Machine Phase-A, Rotor Current Calculator -def indmachiar(poles=0, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, +def indmachiar(Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, Ls=None, Lr=None, freq=60, calcX=True): r""" Induction Machine Rotor Current Calculator. @@ -577,8 +577,7 @@ def indmachiar(poles=0, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, Parameters ---------- - poles: int, optional - Number of poles for the induction machine. + # Removed poles. Vth: complex, optional Thevenin-equivalent stator voltage of the induction machine, may be calculated internally @@ -630,8 +629,6 @@ def indmachiar(poles=0, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, Lls = Ls - Lm if Lr is not None: # Use Lr instead of Llr Llr = Lr - Lm - if poles != 0: # Calculate Sync. Speed from Num. Poles - wsyn = w / (poles / 2) if calcX: # Convert Inductances to Reactances Lm *= w Lls *= w @@ -653,7 +650,7 @@ def indmachiar(poles=0, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, # Define Induction Machine Peak Torque Calculator -def indmachpktorq(Rr, poles=0, s_pk=None, Iar=None, Vth=None, Zth=None, Vas=0, +def indmachpktorq(Rr, s_pk=None, Iar=None, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, Ls=None, Lr=None, freq=60, calcX=True): r""" @@ -684,8 +681,7 @@ def indmachpktorq(Rr, poles=0, s_pk=None, Iar=None, Vth=None, Zth=None, Vas=0, ---------- Rr: float Rotor resistance in Ohms - poles: int, optional - Number of poles for the induction machine. + # Removed poles. s_pk: float, optional Peak induction machine slip, may be calculated internally if remaining machine characteristics are @@ -746,8 +742,6 @@ def indmachpktorq(Rr, poles=0, s_pk=None, Iar=None, Vth=None, Zth=None, Vas=0, Lls = Ls - Lm if Lr is not None: # Use Lr instead of Llr Llr = Lr - Lm - if poles != 0: # Calculate Sync. Speed from Num. Poles - wsyn = w / (poles / 2) if calcX: # Convert Inductances to Reactances Lm *= w Lls *= w @@ -779,7 +773,7 @@ def indmachpktorq(Rr, poles=0, s_pk=None, Iar=None, Vth=None, Zth=None, Vas=0, # Define Induction Machine Starting Torque Calculator -def indmachstarttorq(Rr, poles=0, Iar=None, Vth=None, Zth=None, Vas=0, Rs=0, +def indmachstarttorq(Rr, Iar=None, Vth=None, Zth=None, Vas=0, Rs=0, Lm=0, Lls=0, Llr=0, Ls=None, Lr=None, freq=60, calcX=True): r""" Induction Machine Starting Torque Calculator. @@ -812,8 +806,7 @@ def indmachstarttorq(Rr, poles=0, Iar=None, Vth=None, Zth=None, Vas=0, Rs=0, ---------- Rr: float Rotor resistance in Ohms - poles: int, optional - Number of poles for the induction machine. + # Removed poles. Iar: complex, optional Phase-A, Rotor Current in Amps, may be calculated internally if remaining machine characteristics are @@ -870,8 +863,6 @@ def indmachstarttorq(Rr, poles=0, Iar=None, Vth=None, Zth=None, Vas=0, Rs=0, Lls = Ls - Lm if Lr is not None: # Use Lr instead of Llr Llr = Lr - Lm - if poles != 0: # Calculate Sync. Speed from Num. Poles - wsyn = w / (poles / 2) if calcX: # Convert Inductances to Reactances Lm *= w Lls *= w @@ -1034,8 +1025,8 @@ def equations(val): F = (Ls * Iqs + Lm * Iqr) - LAMqs G = (Lm * Ids + Lr * Idr) - LAMdr H = (Lm * Iqs + Lr * Iqr) - LAMqr - I = (Lm / Lr * (LAMdr * Iqs - LAMqr * Ids)) - Tem - return A, B, C, D, E, F, G, H, I + torque_balance = (Lm / Lr * (LAMdr * Iqs - LAMqr * Ids)) - Tem + return A, B, C, D, E, F, G, H, torque_balance # Define Initial Guesses Idr0 = -1 diff --git a/electricpy/math.py b/electricpy/math.py index a825b873..2b1b217c 100644 --- a/electricpy/math.py +++ b/electricpy/math.py @@ -89,6 +89,8 @@ def funcrms(func, T): ------- RMS: The RMS value of the function (f) over the interval ( 0, T ) """ + if T <= 0: + raise ValueError("T must be greater than zero") integral, _ = integrate(lambda x: func(x) ** 2, 0, T) return _np.sqrt(1 / T * integral) @@ -114,6 +116,8 @@ def gaussian(x, mu=0, sigma=1): ------- Computed gaussian (numpy.ndarray) of the input x """ + if sigma == 0: + raise ValueError("sigma must be non-zero") return (1 / (sigma * _np.sqrt(2 * _np.pi)) * _np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))) @@ -253,11 +257,10 @@ def rfft(arr, dt=0.01, absolute=True, resample=True): # Downsample to remove unnecessary points fixed_fft = filter.dnsample(fourier, dn) return fixed_fft - elif not resample: + if not resample: return fourier - else: - # Condition Resample Value - resample = int(resample) - # Downsample to remove unnecessary points - fixed_fft = filter.dnsample(fourier, resample) - return fixed_fft + # Condition Resample Value + resample = int(resample) + # Downsample to remove unnecessary points + fixed_fft = filter.dnsample(fourier, resample) + return fixed_fft diff --git a/electricpy/passive.py b/electricpy/passive.py index bd344801..93dfa95a 100644 --- a/electricpy/passive.py +++ b/electricpy/passive.py @@ -109,11 +109,10 @@ def captransfer(t, Vs, R, Cs, Cd): """ if t < 0: raise ValueError("Time must be greater than zero.") - try: - tau = (R * Cs * Cd) / (Cs + Cd) - rvolt = Vs * _np.exp(-t / tau) - except ZeroDivisionError: - raise ZeroDivisionError("Sum of Source and Destination Capacitance must be non-zero.") + if Cs + Cd == 0: + raise ValueError("Sum of Source and Destination Capacitance must be non-zero.") + tau = (R * Cs * Cd) / (Cs + Cd) + rvolt = Vs * _np.exp(-t / tau) vfinal = Vs * Cs / (Cs + Cd) return rvolt, vfinal @@ -149,6 +148,10 @@ def capbacktoback(C1, C2, Lm, VLN=None, VLL=None): ifreq: float Transient current frequency """ + if VLL is None and VLN is None: + raise ValueError("Must provide either VLN or VLL.") + if VLL is None: + VLL = _np.sqrt(3) * VLN # Evaluate Max Current imax = _np.sqrt(2 / 3) * VLL * _np.sqrt((C1 * C2) / ((C1 + C2) * Lm)) # Evaluate Inrush Current Frequency @@ -287,8 +290,7 @@ def timedischarge(Vinit, Vmin, C, P, dt=1e-3, RMS=True, Eremain=False): if Eremain: E = capenergy(C, vcp) # calc. energy return t - dt, E - else: - return t - dt + return t - dt # Define Rectifier Capacitor Calculator @@ -321,7 +323,7 @@ def rectifiercap(Iload, fswitch, dVout): # Define Inductor Energy Formula -def inductorenergy(L, I): +def inductorenergy(L, I): # noqa: E741 r""" Energy Stored in Inductor Formula. @@ -579,13 +581,12 @@ def inductive_voltdiv(Vin=None, Vout=None, L1=None, L2=None, find=''): if find == 'vin': return Vin - elif find == 'vout': + if find == 'vout': return Vout - elif find == 'l1': + if find == 'l1': return L1 - elif find == 'l2': + if find == 'l2': return L2 - else: - return Vin, Vout, L1, L2 + return Vin, Vout, L1, L2 # END diff --git a/electricpy/phasors.py b/electricpy/phasors.py index 1476fa7f..985ddd2d 100644 --- a/electricpy/phasors.py +++ b/electricpy/phasors.py @@ -13,9 +13,10 @@ """ ################################################################################ -import numpy as _np import cmath as _c +import numpy as _np + # Define Phase Angle Generator def phs(ang): @@ -135,13 +136,15 @@ def phasorz(C=None, L=None, freq=60, complex=True): if w == 0: raise ValueError("freq must be non-zero.") + Z = None + # C Given in ohms, return as Z if C is not None: if C == 0: raise ValueError("C must be non-zero.") Z = -1 / (w * C) # L Given in ohms, return as Z - if L is not None: + elif L is not None: Z = w * L # If asked for imaginary number if complex: @@ -339,25 +342,27 @@ def compose(*arr): try: row, col = arr.shape # Passed Test, Valid Shape - retarr = _np.array([]) # Empty Return Array + return_array = _np.array([]) # Empty Return Array # Now, Determine whether is type 2 or 3 if col == 2: # Type 3 for i in range(row): # Iterate over each row item = arr[i][0] + 1j * arr[i][1] - retarr = _np.append(retarr, item) + return_array = _np.append(return_array, item) elif row == 2: # Type 2 for i in range(col): # Iterate over each column item = arr[0][i] + 1j * arr[1][i] - retarr = _np.append(retarr, item) + return_array = _np.append(return_array, item) else: raise ValueError("Invalid Array Shape, must be 2xN or Nx2.") # Successfully Generated Array, Return - return (retarr) - except Exception: # 1-Dimension Array + return (return_array) + except IndexError as exc: # 1-Dimension Array length = arr.size # Test for invalid Array Size if length != 2: - raise ValueError("Invalid Array Size, Saw Length of " + str(length)) + raise ValueError( + "Invalid Array Size, Saw Length of " + str(length) + ) from exc # Valid Size, Calculate and Return return arr[0] + 1j * arr[1] @@ -396,7 +401,7 @@ def parallelz(*args): try: L = len(Z) - except Exception: + except (AttributeError, TypeError): return Z if L == 0: @@ -405,14 +410,14 @@ def parallelz(*args): return Z[0] # Inverse-sum method with explicit zero checks - invsum = 0 + inverted_sum = 0 for Zi in Z: if Zi == 0: raise ValueError("Impedance values must be non-zero for parallel combination.") - invsum += 1 / Zi - if invsum == 0: + inverted_sum += 1 / Zi + if inverted_sum == 0: raise ValueError("Invalid impedances: reciprocal sum evaluates to zero.") - Zp = 1 / invsum + Zp = 1 / inverted_sum return Zp # END diff --git a/electricpy/sim.py b/electricpy/sim.py index bb076101..0d201384 100644 --- a/electricpy/sim.py +++ b/electricpy/sim.py @@ -81,6 +81,7 @@ def digifiltersim(fin, filter, freqs, NN=1000, dt=0.01, title="", """ if figsize is not None: _plt.figure(figsize=figsize) + filter = _np.asarray(filter) flen = len(freqs) for i in range(flen): # Gather frequency @@ -95,26 +96,28 @@ def digifiltersim(fin, filter, freqs, NN=1000, dt=0.01, title="", x[k] = fin(k * dt, freq) # Identify how many rows were provided - sz = len(filter) if isinstance(filter, (tuple, list)) else filter.size - if (sz < 5): + sz = len(filter) if isinstance(filter, (tuple, list, _np.ndarray)) else filter.size + if sz < 5: raise ValueError("ERROR: Too few filter arguments provided. " + "Refer to documentation for proper format.") - elif (sz == 5): + if sz == 5: rows = 1 + filter_rows = filter.reshape(1, 5) else: - rows, cols = filter.shape + rows, _ = filter.shape + filter_rows = filter # Operate with each individual filter set x_tmp = _np.copy(x) - nsteps = NN - 4 + n_steps = NN - 4 for row_n in range(rows): - row = filter[row_n] # Capture individual row + row = filter_rows[row_n] # Capture individual row A1 = row[0] A2 = row[1] B0 = row[2] B1 = row[3] B2 = row[4] T = 3 - for _ in range(nsteps): + for _ in range(n_steps): T = T + 1 # Apply Filtering Specified by Individual Row y[T] = (A1 * y[T - 1] + A2 * y[T - 2] + @@ -196,8 +199,7 @@ def step_response(system, npts=1000, dt=0.01, combine=True, xlim=False, step[i] = 1.0 # Simulate Response for each input (step, ramp, parabola) - # All 'x' values are variables that are considered don't-care - x, y1, x = _sig.lsim((system), step, TT) + _, y1, _ = _sig.lsim((system), step, TT) # Calculate error over all points for k in range(npts): @@ -281,8 +283,7 @@ def ramp_response(system, npts=1000, dt=0.01, combine=True, xlim=False, ramp[i] = (dt * i) # Simulate Response for each input (step, ramp, parabola) - # All 'x' values are variables that are considered don't-care - x, y2, x = _sig.lsim((system), ramp, TT) + _, y2, _ = _sig.lsim((system), ramp, TT) # Calculate error over all points for k in range(npts): @@ -365,8 +366,7 @@ def parabolic_response(system, npts=1000, dt=0.01, combine=True, xlim=False, parabola[i] = (dt * i) ** (2) # Simulate Response for each input (step, ramp, parabola) - # All 'x' values are variables that are considered don't-care - x, y3, x = _sig.lsim((system), parabola, TT) + _, y3, _ = _sig.lsim((system), parabola, TT) # Calculate error over all points for k in range(npts): @@ -474,13 +474,17 @@ def nparr_to_matrix(x, yx): # Define Function Concatinator Class class c_func_concat: + """Concatenate multiple scalar forcing functions into one vector output.""" + def __init__(self, funcs): # Initialize class with tupple of functions + """Store the supplied callables in index order for later evaluation.""" self.nfuncs = len(funcs) # Determine how many functions are in tuple self.func_reg = {} # Create empty keyed list of function handles for key in range(self.nfuncs): # Iterate adding to key self.func_reg[key] = funcs[key] # Fill keyed list with functions def func_c(self, x): # Concatenated Function + """Evaluate each stored function at ``x`` and return a column matrix.""" rets = _np.array([]) # Create blank numpy array to store function outputs for i in range(self.nfuncs): y = self.func_reg[i](x) # Calculate each function at value x @@ -519,6 +523,7 @@ def func_c(self, x): # Concatenated Function D = _np.asmatrix(D) # Create values for input testing + c_funcs = None if callable(func): # if f is a function, test as one mF = func(1) # f should return: int, float, tuple, _np.arr, _np.matrix elif isinstance(func, (tuple, list)): # if f is tupple of arguments @@ -547,10 +552,14 @@ def func_c(self, x): # Concatenated Function rF, cF = 1, 1 # Defualt for a function returning one value if isinstance(mF, tuple): # If function returns tuple - fn = lambda x: tuple_to_matrix(x, func) # Use conversion function + def fn(x): + return tuple_to_matrix(x, func) + rF, cF = fn(1).shape # Prepare for further testing elif isinstance(mF, _np.ndarray): # If function returns numpy array - fn = lambda x: nparr_to_matrix(x, func) # Use conversion function + def fn(x): + return nparr_to_matrix(x, func) + rF, cF = fn(1).shape # Prepare for further testing elif isinstance(mF, (int, float, _np.float64)): # If function returns int or float or numpy float fn = func # Pass function handle @@ -560,16 +569,18 @@ def func_c(self, x): # Concatenated Function elif (mF == "MultiFunctions"): # There are multiple functions in one argument fn = c_funcs.func_c # Gather function handle from function concatenation class rF, cF = fn(1).shape # Prepare for further testing - elif (mF == "NA"): # Function doesn't meet requirements - raise ValueError("Forcing function does not meet requirements." + - "\nFunction doesn't return data type: int, float, numpy.ndarray" + - "\n or numpy.matrixlib.defmatrix.matrix. Nor does function " + - "\ncontain tuple of function handles. Please review function.") + elif mF == "NA": # Function doesn't meet requirements + raise ValueError( + "Forcing function does not meet requirements." + + "\nFunction doesn't return data type: int, float, numpy.ndarray" + + "\n or numpy.matrixlib.defmatrix.matrix. Nor does function " + + "\ncontain tuple of function handles. Please review function." + ) # Test for size correlation between matricies if (cA != rA): # A isn't nxn matrix raise ValueError("Matrix 'A' is not NxN matrix.") - elif (rA != rB): # A and B matricies don't have same number of rows + if (rA != rB): # A and B matricies don't have same number of rows if (B.size % rA) == 0: # Elements in B divisible by rows in A _warn("WARNING: Reshaping 'B' matrix to match 'A' matrix.") B = _np.matrix.reshape(B, (rA, int(B.size / rA))) # Reshape Matrix @@ -583,9 +594,9 @@ def func_c(self, x): # Concatenated Function raise ValueError("'A' matrix dimensions don't match 'B' matrix dimensions.") elif (cB != rF) or (cF != 1): # Forcing Function matrix doesn't match B matrix raise ValueError("'B' matrix dimensions don't match forcing function dimensions.") - elif (solution == 3) and (cC != cA) or (rC != 1): # Number of elements in C don't meet requirements + if (solution == 3) and (cC != cA) or (rC != 1): # Number of elements in C don't meet requirements raise ValueError("'C' matrix dimensions don't match state-space variable dimensions.") - elif (solution == 3) and ((cD != rF) or (rD != 1)): # Number of elements in D don't meet requirements + if (solution == 3) and ((cD != rF) or (rD != 1)): # Number of elements in D don't meet requirements if (cD == rD) and (cD == 1) and (D[0] == 0): # D matrix is set to [0] D = _np.asmatrix(_np.zeros(rF)) # Re-create D to meet requirements _warn("WARNING: Autogenerating 'D' matrix of zeros to match forcing functions.") @@ -654,8 +665,8 @@ def func_c(self, x): # Concatenated Function if (plotforcing): _ = _plt.figure("Forcing Functions") if fnc > 1: - for x in range(fnc): - _plt.plot(TT, fn_arr[x], label="f" + str(x + 1)) + for index in range(fnc): + _plt.plot(TT, fn_arr[index], label="f" + str(index + 1)) else: _plt.plot(TT, fn_arr, label="f1") if not xlim: @@ -673,8 +684,8 @@ def func_c(self, x): # Concatenated Function # Plot each state-variable over time _ = _plt.figure("State Variables") - for x in range(xtim_len): - _plt.plot(TT, xtim[x], label="x" + str(x + 1)) + for index in range(xtim_len): + _plt.plot(TT, xtim[index], label="x" + str(index + 1)) if not xlim: _plt.xlim(xlim) if not ylim: @@ -805,16 +816,15 @@ def inv(m): F_norm = _np.linalg.norm(F_value, ord=2) # L2 norm of vector iteration_counter = 0 teps = eps + user_has_been_warned = False while abs(F_norm) > teps and iteration_counter < mxiter: try: # Try Solve Operation delta = _np.linalg.solve(J(X0), -F_value) teps = eps # Change Test Epsilon if Needed except _np.linalg.LinAlgError: # Use Least Square if Error # Warn User, but only Once - try: - tst = userhasbeenwarned - except NameError: - userhasbeenwarned = True + if not user_has_been_warned: + user_has_been_warned = True _warn("WARNING: Singular matrix, attempting LSQ method.") # Calculate Delta Using Least-Squares Inverse delta = - inv(J(X0)).dot(F_value) @@ -937,13 +947,17 @@ def nr_pq(Ybus, V_set, P_set, Q_set, extend=True, argshape=False, verbose=False) # Define Function Concatinator Class class c_func_concat: + """Concatenate multiple scalar forcing functions into one vector output.""" + def __init__(self, funcs): # Initialize class with tupple of functions + """Store the supplied callables in index order for later evaluation.""" self.nfuncs = len(funcs) # Determine how many functions are in tuple self.func_reg = {} # Create empty keyed list of function handles for key in range(self.nfuncs): # Iterate adding to key self.func_reg[key] = funcs[key] # Fill keyed list with functions def func_c(self, x): # Concatenated Function + """Evaluate each stored function at ``x`` and return a flat array.""" rets = _np.array([]) # Create blank numpy array to store function outputs for i in range(self.nfuncs): y = self.func_reg[i](x) # Calculate each function at value x @@ -1008,31 +1022,31 @@ def func_c(self, x): # Concatenated Function if P_list[_k] is None: continue # Don't Generate Requirements for Slack Bus # Collect Other Terms - Yind = "[{}][{}]".format(_k, _j) + Yind = f"[{_k}][{_j}]" if verbose: print("K:", _k, "\tJ:", _j) # Generate Voltage-Related Strings if _k != _j: # Skip i,i Terms # Generate K-Related Strings if V_list[_k][0] is None: # The Vk magnitude is unknown - Vkm = "Vx[{}]".format(_k + ang_len - magoff * _k) # Use Variable Magnitude - Vka = "Vx[{}]".format(_k - angoff) # Use Variable Angle + Vkm = f"Vx[{_k + ang_len - magoff * _k}]" # Use Variable Magnitude + Vka = f"Vx[{_k - angoff}]" # Use Variable Angle else: # The Vk magnitude is known - Vkm = "V_list[{}][0]".format(_k) # Load Magnitude + Vkm = f"V_list[{_k}][0]" # Load Magnitude if V_list[_k][1] is None: # The Vj angle is unknown - Vka = "Vx[{}]".format(_k - angoff) # Use Variable Angle + Vka = f"Vx[{_k - angoff}]" # Use Variable Angle else: - Vka = "V_list[{}][1]".format(_k) # Load Angle + Vka = f"V_list[{_k}][1]" # Load Angle # Generate J-Related Strings if V_list[_j][0] is None: # The Vj magnitude is unknown - Vjm = "Vx[{}]".format(_j + ang_len - magoff * _j) # Use Variable Magnitude - Vja = "Vx[{}]".format(_j - angoff) # Use Variable Angle + Vjm = f"Vx[{_j + ang_len - magoff * _j}]" # Use Variable Magnitude + Vja = f"Vx[{_j - angoff}]" # Use Variable Angle else: # The Vj magnitude is known - Vjm = "V_list[{}][0]".format(_j) # Load Magnitude + Vjm = f"V_list[{_j}][0]" # Load Magnitude if V_list[_j][1] is None: # The Vj angle is unknown - Vja = "Vx[{}]".format(_j - angoff) # Use Variable Angle + Vja = f"Vx[{_j - angoff}]" # Use Variable Angle else: - Vja = "V_list[{}][1]".format(_j) # Load Angle + Vja = f"V_list[{_j}][1]" # Load Angle # Generate String and Append to List of Functions P_strgs[i] = (Pstr.format(Vkm, Vka, Vjm, Vja, Yind)) if verbose: @@ -1042,7 +1056,7 @@ def func_c(self, x): # Concatenated Function # Generate String and Append to List of Functions if newentry: newentry = False - Qgen = "-Vx[{0}]**2*YBUS[{0}][{0}].imag".format(_k) + Qgen = f"-Vx[{_k}]**2*YBUS[{_k}][{_k}].imag" else: Qgen = "" Q_strgs[i] = (Qstr.format(Vkm, Vka, Vjm, Vja, Yind, Qgen)) @@ -1050,8 +1064,8 @@ def func_c(self, x): # Concatenated Function print("New Q-String:", Q_strgs[i]) # Increment Index at Each Interior Level i += 1 - tempPstr = "P_funcs.append(lambda Vx: -P_list[{0}]".format(_k) - tempQstr = "Q_funcs.append(lambda Vx: -Q_list[{0}]".format(_k) + tempPstr = f"P_funcs.append(lambda Vx: -P_list[{_k}]" + tempQstr = f"Q_funcs.append(lambda Vx: -Q_list[{_k}]" for _i in range(ii, i): P = P_strgs[_i] Q = Q_strgs[_i] @@ -1203,10 +1217,13 @@ def mbuspowerflow(Ybus, Vknown, Pknown, Qknown, X0='flatstart', eps=1e-4, Pknown = _np.roll(Pknown, (len(Pknown) - slackbus), 0).tolist() Qknown = _np.roll(Qknown, (len(Qknown) - slackbus), 0).tolist() # Generate F Function Array - F, shp = nr_pq(Ybus, Vknown, Pknown, Qknown, True, True, False) + nr_out = nr_pq(Ybus, Vknown, Pknown, Qknown, True, True, False) + F = nr_out[0] + shp = nr_out[1] + ang_len = shp[0] + mag_len = shp[1] # Handle Flat-Start Condition if X0 == 'flatstart': - ang_len, mag_len = shp X0 = _np.append(_np.zeros(ang_len), _np.ones(mag_len)) # Evaluate Jacobian J = jacobian(F) diff --git a/electricpy/thermal.py b/electricpy/thermal.py index 272246da..9c8f8fab 100644 --- a/electricpy/thermal.py +++ b/electricpy/thermal.py @@ -9,7 +9,15 @@ import numpy as _np -from electricpy.constants import * +from electricpy.constants import ( + COLD_JUNCTION_DATA, + COLD_JUNCTION_KEYS, + RTD_TYPES, + THERMO_COUPLE_DATA, + THERMO_COUPLE_KEYS, + THERMO_COUPLE_VOLTAGES, + m, +) # Define Cold-Junction-Voltage Calculator def coldjunction(Tcj, coupletype="K", To=None, Vo=None, P1=None, P2=None, @@ -63,14 +71,14 @@ def coldjunction(Tcj, coupletype="K", To=None, Vo=None, P1=None, P2=None, raise ValueError("Temperature out of range.") # Define Constant Lookup System lookup = ["B", "E", "J", "K", "N", "R", "S", "T"] - if not (coupletype in lookup): + if coupletype not in lookup: raise ValueError("Invalid Thermocouple Type") index = lookup.index(coupletype) # Define Constant Dictionary # Load Data Into Terms parameters = {} - for var in COLD_JUNCTION_DATA.keys(): - parameters[var] = parameters.get(var, None) or COLD_JUNCTION_DATA[var][index] + for var, values in COLD_JUNCTION_DATA.items(): + parameters[var] = parameters.get(var, None) or values[index] To, Vo, P1, P2, P3, P4, Q1, Q2 = [parameters[key] for key in COLD_JUNCTION_KEYS] # Define Formula Terms tx = (Tcj - To) @@ -143,13 +151,13 @@ def thermocouple(V, coupletype="K", fahrenheit=False, cjt=None, To=None, V += Vcj / m # Define Constant Lookup System lookup = ["B", "E", "J", "K", "N", "R", "S", "T"] - if not (coupletype in lookup): + if coupletype not in lookup: raise ValueError("Invalid Thermocouple Type") # Determine Array Selection vset = THERMO_COUPLE_VOLTAGES[coupletype] if V < vset[0] * m: raise ValueError("Voltage Below Lower Bound") - elif vset[0] <= V < vset[1]: + if vset[0] <= V < vset[1]: select = 0 elif vset[1] <= V < vset[2]: select = 1 @@ -175,7 +183,8 @@ def thermocouple(V, coupletype="K", fahrenheit=False, cjt=None, To=None, # Return Temperature if fahrenheit: temp = (temp * 9 / 5) + 32 - temp = _np.around(temp, round) + if round is not None: + temp = _np.around(temp, round) return temp @@ -230,7 +239,8 @@ def rtdtemp(RT, rtdtype="PT100", fahrenheit=False, Rref=None, Tref=None, # Return Temperature if fahrenheit: temp = (temp * 9 / 5) + 32 - temp = _np.around(temp, round) + if round is not None: + temp = _np.around(temp, round) return temp # END diff --git a/electricpy/visu.py b/electricpy/visu.py index f914a017..162f2c2f 100644 --- a/electricpy/visu.py +++ b/electricpy/visu.py @@ -477,8 +477,7 @@ def __init__( self.data = self.compute_efficiency() def __call__(self): - # noqa: D102 - __doc__ = self.__doc__ + __doc__ = self.__doc__ # noqa: D102, F841 return self.data def plot(self): @@ -843,6 +842,8 @@ def _build_circle( elif circle_type == "sending_end": center = Point(k * _c.cos(alpha - beta), -k * _c.sin(alpha - beta)) + else: + raise ValueError("Invalid circle_type") if V_ref is not None and P is not None and Q is not None: radius = abs(V) * abs(V_ref) / (abs(a2)) @@ -893,7 +894,7 @@ def _cal_parameters(self, type1, type2): if self.parameters["Q" + type1] is None: self.parameters["Q" + type1] = self.operating_point.y - if self.parameters["S" + type1] == None: + if self.parameters["S" + type1] is None: self.parameters["S" + type1] = ( self.operating_point.x + 1j * self.operating_point.y ) @@ -948,6 +949,7 @@ def print_data(self): for key, value in self.parameters.items(): print(key, " => ", value) + return None def __call__(self) -> dict: r"""Return the data of the circle.""" @@ -1042,11 +1044,11 @@ def receiving_end_power_circle( """ try: assert Vr is not None and A is not None and B is not None - except AssertionError: + except AssertionError as exc: raise ValueError( "Not enough attributes to build Receiving end power circle at least" " provide `Vr`, `A`, `B`" - ) + ) from exc # NOTE: # Original validation required (Sr is not None and power_factor is not None), @@ -1164,12 +1166,12 @@ class SeriesRLC(): .. math:: \text{resonance_frequency} = \frac{1}{\sqrt{L * C} \cdot 2 \pi} - + **Bandwidth:** .. math:: \text{bandwidth} = \frac{R}{L \cdot 2 \pi} - + **Quality Factor:** .. math:: \text{quality_factor} = 2\pi \frac{\text{freq}}{R} @@ -1177,7 +1179,7 @@ class SeriesRLC(): Given the characteristics listed below, and the Python code described in the associated example, the following plot will be generated. - + * Resistance: 5 ohms * Inductance: 0.4 henreys * Capacitance: 25.3e-6 farads @@ -1193,7 +1195,7 @@ class SeriesRLC(): .. image:: /static/series-rlc-r10-l0.5.png - + Examples -------- >>> from electricpy.visu import SeriesRLC diff --git a/pyproject.toml b/pyproject.toml index 93b36e57..4b356e70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ numerical = ["numdifftools"] fault = ["arcflash"] full = ["numdifftools", "arcflash"] dev = [ - "pytest >=2.7.3", + "pytest >=2.7.3,<9", "xdoctest >= 1.1.3", "pytest-pydocstyle >= 2.3.2", "pygments >= 2.18.0", @@ -58,6 +58,8 @@ dev = [ "scipy", "sympy", "numdifftools", + "pylint", + "ruff", ] [tool.pytest.ini_options] diff --git a/release-version.py b/release-version.py index 316f1706..fe407cbc 100644 --- a/release-version.py +++ b/release-version.py @@ -10,11 +10,11 @@ try: import electricpy as ep except ImportError: - import os, sys + import os + import sys sys.path.insert(0, os.getcwd()) import electricpy as ep -import requests response = requests.get(f"https://api.github.com/repos/{USERNAME}/{REPO}/releases/latest") try: diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..7c71fe60 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,2 @@ +[lint] +ignore = ["E741"] diff --git a/test/__init__.py b/test/__init__.py index ea0e4fa7..b1086255 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,8 +1,11 @@ +"""Shared test helper utilities.""" + from electricpy.geometry import Point from electricpy.geometry import Line from numpy.testing import assert_almost_equal def compare_points(p1: Point, p2: Point) -> bool: + """Return True when two points are approximately equal.""" try: assert_almost_equal(p1.x, p2.x) assert_almost_equal(p1.y, p2.y) @@ -11,6 +14,7 @@ def compare_points(p1: Point, p2: Point) -> bool: return True def compare_lines(l1: Line, l2: Line) -> bool: + """Return True when two lines are approximately equivalent.""" try: assert_almost_equal(l1.a / l2.a , l1.b / l2.b) assert_almost_equal(l1.b / l2.b , l1.c / l2.c) @@ -39,6 +43,4 @@ def compare_lines(l1: Line, l2: Line) -> bool: return False except AssertionError: return False - finally: - return True - \ No newline at end of file + return True diff --git a/test/test_bode.py b/test/test_bode.py index ec00d566..ffd01cf9 100644 --- a/test/test_bode.py +++ b/test/test_bode.py @@ -8,6 +8,7 @@ def test_sys_condition_feedback_padding(): + """Validate sys condition feedback padding behavior.""" num = np.array([1.0, 0.0]) den = np.array([1.0, 1.0, 0.0]) conditioned_num, conditioned_den = ep_bode._sys_condition((num, den), True) @@ -18,12 +19,16 @@ def test_sys_condition_feedback_padding(): def test_bode_validates_frequency_range(): + """Validate bode validates frequency range behavior.""" system = (np.array([1.0]), np.array([1.0, 1.0])) with pytest.raises(ValueError): ep_bode.bode(system, mn=10, mx=1, magnitude=False, angle=False) def test_sbode_returns_plot_module(): - func = lambda s: 1 / (s + 1) + """Validate sbode returns plot module behavior.""" + def func(s): + return 1 / (s + 1) + plot_module = ep_bode.sbode(func, NN=10, mn=1, mx=10, magnitude=False, angle=False) assert plot_module is None diff --git a/test/test_bugfixes.py b/test/test_bugfixes.py index 95adc945..643f5f76 100644 --- a/test/test_bugfixes.py +++ b/test/test_bugfixes.py @@ -7,6 +7,7 @@ def test_thermocouple_cjt_units_match_equivalent_voltage(): + """Validate thermocouple cjt units match equivalent voltage behavior.""" cjt = 25.0 Vcj = thermal.coldjunction(cjt, coupletype="K") temp_from_v = thermal.thermocouple(Vcj, coupletype="K") @@ -15,6 +16,7 @@ def test_thermocouple_cjt_units_match_equivalent_voltage(): def test_ic_555_astable_roundtrip_from_timing(): + """Validate round-trip behavior for ic 555 astable roundtrip from timing.""" R1, R2, C = 1000.0, 2000.0, 1e-6 result = ep.ic_555_astable(R=(R1, R2), C=C) t_high = result["t_high"] @@ -27,6 +29,7 @@ def test_ic_555_astable_roundtrip_from_timing(): def test_ic_555_astable_freq_branch_returns_combined_resistance(): + """Validate ic 555 astable freq branch returns combined resistance behavior.""" C = 1e-6 freq = 10.0 expected = (1 / freq) / (np.log(2) * C) @@ -35,6 +38,7 @@ def test_ic_555_astable_freq_branch_returns_combined_resistance(): def test_ic_555_monostable_uses_single_pulse_width(): + """Validate ic 555 monostable uses single pulse width behavior.""" R = 1000.0 C = 1e-6 T = R * C * np.log(3) @@ -42,7 +46,37 @@ def test_ic_555_monostable_uses_single_pulse_width(): assert result == pytest.approx(T, rel=1e-9) +def test_ic_555_monostable_solves_for_resistance_and_capacitance(): + """Validate ic 555 monostable solves for resistance and capacitance behavior.""" + R = 1500.0 + C = 2e-6 + T = R * C * np.log(3) + calc_r = ep.ic_555_monostable(R=None, C=C, t_high=T) + calc_c = ep.ic_555_monostable(R=R, C=None, t_high=T) + assert calc_r == pytest.approx(R, rel=1e-9) + assert calc_c == pytest.approx(C, rel=1e-9) + + +def test_short_circuit_rms_branch_uses_provided_frequency(): + """Validate short circuit rms branch uses provided frequency behavior.""" + Z = 1 + 1j + V = 1 + t = 0.01 + f = 50 + Irms, IAC, K = ep.short_circuit_current(V, Z, t=t, f=f, mxcurrent=False) + + R = abs(Z.real) + X = abs(Z.imag) + tau = t * f + expected_k = np.sqrt(1 + 2 * np.exp(-4 * np.pi * tau / (X / R))) + + assert IAC == pytest.approx(abs(V / Z), rel=1e-12) + assert K == pytest.approx(expected_k, rel=1e-12) + assert Irms == pytest.approx(expected_k * abs(V / Z), rel=1e-12) + + def test_abc_seq_roundtrip_respects_reference(): + """Validate round-trip behavior for abc seq roundtrip respects reference.""" abc = np.array([ep.phasor(1, 0), ep.phasor(1, -120), ep.phasor(1, 120)]) for reference in ("A", "B", "C"): seq = conversions.abc_to_seq(abc, reference=reference) @@ -51,5 +85,6 @@ def test_abc_seq_roundtrip_respects_reference(): def test_phasordata_accepts_float_npts(): + """Validate phasordata accepts float npts behavior.""" data = ep.phasors.phasordata(0, 1, npts=5.7) assert len(data) == 5 diff --git a/test/test_circle.py b/test/test_circle.py index c9b23b42..1ec50223 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -9,6 +9,7 @@ class TestArea: def test_0(self): + """Validate area scenario 0.""" c = Circle((0, 0), 1) assert c.area() == cmath.pi @@ -17,6 +18,7 @@ def test_0(self): assert c.area() == cmath.pi * 4 def test_1(self): + """Validate area scenario 1.""" c = Circle((0, 0), 1.1) assert c.area() == cmath.pi * 1.1**2 @@ -27,6 +29,7 @@ def test_1(self): class TestCircumference: def test_0(self): + """Validate circumference scenario 0.""" c = Circle((0, 0), 1) assert c.circumference() == cmath.pi * 2 @@ -35,6 +38,7 @@ def test_0(self): assert c.circumference() == cmath.pi * 4 def test_1(self): + """Validate circumference scenario 1.""" c = Circle((0, 0), 1.1) assert c.circumference() == cmath.pi * 2.2 @@ -45,6 +49,7 @@ def test_1(self): class TestTangent: def test_0(self): + """Validate tangent scenario 0.""" c = Circle((0, 0), 1) assert c.tangent(Point(0, 1)) == Line(0, 1, -1) @@ -53,6 +58,7 @@ def test_0(self): assert c.tangent(Point(-1, 0)) == Line(-1, 0, -1) def test_1(self): + """Validate tangent scenario 1.""" from test import compare_lines @@ -67,6 +73,7 @@ def test_1(self): class TestNormal: def test_0(self): + """Validate normal scenario 0.""" c = Circle((0, 0), 1) assert c.normal(Point(0, 1)) == Line(1, 0, 0) @@ -75,6 +82,7 @@ def test_0(self): assert c.normal(Point(-1, 0)) == Line(0, 1, 0) def test_1(self): + """Validate normal scenario 1.""" from test import compare_lines @@ -87,6 +95,7 @@ def test_1(self): def test_contains_point_and_tangent_validation(): + """Validate error handling for contains point and tangent validation.""" c = Circle((0, 0), 2) assert c.contains_point(Point(2, 0)) with pytest.raises(ValueError): @@ -102,6 +111,7 @@ def test_contains_point_and_tangent_validation(): def test_equation_and_parametric_errors(): + """Validate error handling for equation and parametric errors.""" c = Circle((1, 2), 3) eq = c.equation() assert "x^2 + y^2" in eq @@ -109,7 +119,7 @@ def test_equation_and_parametric_errors(): assert " - 4*y" in eq assert " - 4" in eq assert c.radius == 3 - + with pytest.raises(ValueError): list(c.parametric_equation(theta_resolution=0)) with pytest.raises(ValueError): @@ -133,6 +143,7 @@ def test_equation_and_parametric_errors(): def test_sector_and_intersection_cases(): + """Validate sector and intersection cases behavior.""" c1 = Circle((0, 0), 1) c2 = Circle((2, 0), 1) assert c1.sector_length(cmath.pi) == cmath.pi @@ -162,6 +173,7 @@ def test_sector_and_intersection_cases(): def test_construct_circle(): + """Validate construct circle behavior.""" p0 = Point(1, 0) p1 = Point(0, 1) p2 = Point(-1, 0) @@ -173,6 +185,7 @@ def test_construct_circle(): def test_point_coercion_and_repr(): + """Validate point coercion and repr behavior.""" pt = circle_mod._as_point((1, 2)) assert isinstance(pt, Point) assert pt == Point(1, 2) @@ -199,6 +212,7 @@ def test_point_coercion_and_repr(): def test_circle_init_validation_and_normal_error(): + """Validate error handling for circle init validation and normal error.""" with pytest.raises(TypeError): Circle((0, 0), "bad") with pytest.raises(ValueError): diff --git a/test/test_compute.py b/test/test_compute.py index 4e28d589..d8c8a564 100644 --- a/test/test_compute.py +++ b/test/test_compute.py @@ -4,6 +4,7 @@ def test_largest_integer_validates_num_bits(): + """Validate largest integer validates num bits behavior.""" with pytest.raises(ValueError): compute.largest_integer(0) with pytest.raises(ValueError): @@ -11,11 +12,14 @@ def test_largest_integer_validates_num_bits(): def test_largest_integer_signed_unsigned(): + """Validate largest integer signed unsigned behavior.""" + assert compute.largest_integer(1, signed=True) == 0 assert compute.largest_integer(8, signed=True) == 127 assert compute.largest_integer(8, signed=False) == 255 def test_crc_sender_and_remainder(): + """Validate crc sender and remainder behavior.""" data = "1101011011" key = "10011" codeword = compute.crcsender(data, key) @@ -39,5 +43,6 @@ def test_crc_sender_and_remainder(): ("2", "00110010"), ]) def test_string_to_bits(character_string, expected_bits): + """Validate string to bits behavior.""" bits = compute.string_to_bits(character_string) assert bits == expected_bits diff --git a/test/test_constants.py b/test/test_constants.py index 68ddc9e3..7866863f 100644 --- a/test/test_constants.py +++ b/test/test_constants.py @@ -4,6 +4,7 @@ def test_constants_shapes_and_values(): + """Validate constants shapes and values behavior.""" assert constants.pi == np.pi assert np.isclose(abs(constants.VLLcVLN), np.sqrt(3)) assert constants.Aabc.shape == (3, 3) diff --git a/test/test_conversions.py b/test/test_conversions.py index 06ee9941..9f6da958 100644 --- a/test/test_conversions.py +++ b/test/test_conversions.py @@ -5,6 +5,7 @@ def test_sequencez_reference_invariant_for_symmetric_matrix(): + """Validate sequencez reference invariant for symmetric matrix behavior.""" Zabc = np.array([[1 + 0j, 0, 0], [0, 2 + 0j, 0], [0, 0, 3 + 0j]]) ref_a = conversions.sequencez(Zabc, reference="A") ref_b = conversions.sequencez(Zabc, reference="B") @@ -15,6 +16,7 @@ def test_sequencez_reference_invariant_for_symmetric_matrix(): def test_abc_seq_round_trip_with_reference(): + """Validate round-trip behavior for abc seq round trip with reference.""" data = np.array([1 + 0j, 2 + 0j, 3 + 0j]) seq = conversions.abc_to_seq(data, reference="B") abc = conversions.seq_to_abc(seq, reference="B") @@ -22,6 +24,28 @@ def test_abc_seq_round_trip_with_reference(): def test_abc_to_seq_invalid_reference(): + """Validate error handling for abc to seq invalid reference.""" data = np.array([1 + 0j, 2 + 0j, 3 + 0j]) with pytest.raises(ValueError): conversions.abc_to_seq(data, reference="D") + + +@pytest.mark.parametrize("hz_value", [0.1, 1.0, 60.0, 1234.5]) +def test_hz_rad_round_trip(hz_value: float) -> None: + """Frequency should round-trip between Hz and rad/s.""" + radians = conversions.hz_to_rad(hz_value) + assert conversions.rad_to_hz(radians) == pytest.approx(hz_value, rel=1e-12) + + +@pytest.mark.parametrize("rpm_value", [1.0, 900.0, 1800.0, 3600.0]) +def test_rpm_hz_round_trip(rpm_value: float) -> None: + """Frequency should round-trip between RPM and Hz.""" + hz = conversions.rpm_to_hz(rpm_value) + assert conversions.hz_to_rpm(hz) == pytest.approx(rpm_value, rel=1e-12) + + +@pytest.mark.parametrize("watts", [1e-6, 1.0, 10.0, 5e3]) +def test_dbw_watts_round_trip(watts: float) -> None: + """Positive power should round-trip through dBW conversion.""" + dbw = conversions.watts_to_dbw(watts) + assert conversions.dbw_to_watts(dbw) == pytest.approx(watts, rel=1e-12) diff --git a/test/test_convertion.py b/test/test_convertion.py index 6c15c142..a3af1ac9 100644 --- a/test/test_convertion.py +++ b/test/test_convertion.py @@ -2,11 +2,13 @@ import numpy as np def test_abc_to_seq(): + """Validate abc to seq behavior.""" from electricpy.conversions import abc_to_seq a = cmath.rect(1, np.radians(120)) def test_0(): + """Validate 0 behavior.""" np.testing.assert_array_almost_equal(abc_to_seq([1, 1, 1]), [1+0j, 0j, 0j]) np.testing.assert_array_almost_equal(abc_to_seq([1, 0, 0]), [1/3+0j, 1/3+0j, 1/3+0j]) np.testing.assert_array_almost_equal(abc_to_seq([0, 1, 0]), [1/3+0j, a/3+0j, a*a/3+0j]) @@ -15,13 +17,15 @@ def test_0(): test_0() def test_seq_to_abc(): + """Validate seq to abc behavior.""" from electricpy.conversions import seq_to_abc a = cmath.rect(1, np.radians(120)) def test_0(): + """Validate 0 behavior.""" np.testing.assert_array_almost_equal(seq_to_abc([1, 1, 1]), [3+0j, 0j, 0j]) np.testing.assert_array_almost_equal(seq_to_abc([1, 0, 0]), [1+0j, 1+0j, 1+0j]) np.testing.assert_array_almost_equal(seq_to_abc([0, 1, 0]), [1+0j, a*a+0j, a+0j]) np.testing.assert_array_almost_equal(seq_to_abc([0, 0, 1]), [1+0j, a, a*a]) - test_0() \ No newline at end of file + test_0() diff --git a/test/test_electricpy.py b/test/test_electricpy.py index ee936823..2d671df8 100644 --- a/test/test_electricpy.py +++ b/test/test_electricpy.py @@ -10,6 +10,7 @@ from electricpy.passive import air_core_inductance def test_bridge_impedance(): + """Validate bridge impedance behavior.""" # Perfectly Balanced Wheat Stone Bridge from electricpy import bridge_impedance @@ -52,6 +53,7 @@ def test_bridge_impedance(): assert_almost_equal(zeq, zactual) def test_dynetz(): + """Validate dynetz behavior.""" from electricpy import dynetz z1 = complex(3, 3) @@ -67,6 +69,7 @@ def test_dynetz(): assert (za, zb, zc) == (3 * z1, 3 * z2, 3 * z3) def test_powerset(): + """Validate powerset behavior.""" from electricpy import powerset @@ -88,8 +91,8 @@ def test_powerset(): assert_almost_equal(PF, 0.6) def test_voltdiv(): + """Validate voltdiv behavior.""" from electricpy import voltdiv - from electricpy import phasors # Test case 0 R1 == R2 == Rload Vin = 10 @@ -131,6 +134,7 @@ def test_suspension_insulators(): assert_almost_equal(string_efficiency, string_efficiency_actual, decimal=2) def test_propagation_constants(): + """Validate propagation constants behavior.""" from electricpy import propagation_constants z = complex(0.5, 0.9) @@ -144,14 +148,17 @@ def test_propagation_constants(): assert_almost_equal(params_dict['beta'], beta_cal, decimal = 4) def test_funcrms(): + """Validate funcrms behavior.""" from electricpy.math import funcrms - f = lambda x:np.sin(x) + def f(x): + return np.sin(x) assert_almost_equal(funcrms(f,np.pi), 1/np.sqrt(2)) def test_convolve(): + """Validate convolve behavior.""" from electricpy.math import convolve A = (1,1,1) @@ -160,6 +167,7 @@ def test_convolve(): assert ([1,2,3,2,1] == convolve((A,B))).all() def test_ic_555_astable(): + """Validate ic 555 astable behavior.""" from electricpy import ic_555_astable @@ -176,6 +184,7 @@ def test_ic_555_astable(): assert_almost_equal(result['t_high'], 1.386*10**-5, decimal = 3) def test_slew_rate(): + """Validate slew rate behavior.""" from electricpy import slew_rate @@ -188,6 +197,7 @@ def test_slew_rate(): assert_almost_equal(1/(np.pi*2), freq) def test_t_attenuator(): + """Validate t attenuator behavior.""" Adb = 1 Z0 = 1 @@ -199,6 +209,7 @@ def test_t_attenuator(): assert_almost_equal(R2, 8.6673, decimal = 3) def test_pi_attenuator(): + """Validate pi attenuator behavior.""" Adb = 1 Z0 = 1 @@ -209,6 +220,7 @@ def test_pi_attenuator(): assert_almost_equal(R2, 0.11538, decimal = 3) def test_inductor_voltdiv(): + """Validate inductor voltdiv behavior.""" from electricpy.passive import inductive_voltdiv @@ -247,6 +259,7 @@ def test_inductor_voltdiv(): assert(L1 == 1) def test_induction_machine_slip(): + """Validate induction machine slip behavior.""" from electricpy import induction_machine_slip Nr = 1200 @@ -267,6 +280,7 @@ def test_induction_machine_slip(): assert induction_machine_slip(0, freq=freq, poles=p) == 1 def test_parallel_plate_capacitance(): + """Validate parallel plate capacitance behavior.""" from electricpy import parallel_plate_capacitance # Test 1: In the free space (by default e=e0=8.8542E-12) @@ -297,6 +311,7 @@ def test_parallel_plate_capacitance(): assert_almost_equal(parallel_plate_capacitance(C=C2, A=A2, e=e2), d2) def test_solenoid_inductance(): + """Validate solenoid inductance behavior.""" from electricpy import solenoid_inductance # Test 1: In the free space (by default u=u0=4πE-7) @@ -333,6 +348,7 @@ def test_solenoid_inductance(): assert_almost_equal(solenoid_inductance(L=L2, A=A2, N=N2, u=u2), l2) def test_syncspeed(): + """Validate syncspeed behavior.""" from electricpy import syncspeed assert syncspeed(4, freq = 60, rpm = True) == 1800 assert syncspeed(4, freq = 60, Hz = True) == 30 @@ -341,7 +357,8 @@ def test_syncspeed(): can not be zero"): syncspeed(0) -def test_tcycle(): +def test_tcycle_repeated(): + """Validate tcycle repeated behavior.""" from electricpy import tcycle # Test 0 @@ -372,6 +389,7 @@ def test_tcycle(): tcycle(ncycles=[1, 2, 3, 4], freq = [-2, -3, 4, 5]) def test_nr_pqd(): + """Validate nr pqd behavior.""" from electricpy import sim from numpy.testing import assert_array_almost_equal ybustest = [[-10j,10j], @@ -388,6 +406,7 @@ def test_nr_pqd(): assert iter == 4 # Iteration Counter def test_tcycle(): + """Validate tcycle behavior.""" from electricpy import tcycle # Test 0 @@ -420,6 +439,7 @@ def test_tcycle(): class TestPowerflow(): def test_0(self): + """Validate powerflow scenario 0.""" Vsend = phasor(1.01, 30) Vrecv = phasor(1, 0) Xline = 0.2 @@ -428,6 +448,7 @@ def test_0(self): assert_almost_equal(ans, 2.525) def test_1(self): + """Validate powerflow scenario 1.""" Vsend = 1.01 Vrecv = 1 Xline = 0.2 @@ -442,6 +463,7 @@ def check_result(self, expected_result): assert_almost_equal(computed_result, expected_result, decimal = 3) def test_0(self): + """Validate air core inductor scenario 0.""" self.coil_diameter = 1e-3 self.coil_length = 1e-3 self.turn = 1000 @@ -450,6 +472,7 @@ def test_0(self): self.check_result(expected_result) def test_1(self): + """Validate air core inductor scenario 1.""" self.coil_diameter = 1e-2 self.coil_length = 1e-2 self.turn = 251 @@ -459,10 +482,12 @@ def test_1(self): def test_tcycle_validation(): + """Validate error handling for tcycle validation.""" with pytest.raises(ValueError): tcycle(1, freq=-60) def test_reactance_inductive(): + """Validate reactance inductive behavior.""" result = reactance(5, freq=60) assert result > 0 diff --git a/test/test_electricpy_init.py b/test/test_electricpy_init.py index b9f0678a..5018755f 100644 --- a/test/test_electricpy_init.py +++ b/test/test_electricpy_init.py @@ -6,6 +6,7 @@ def test_tcycle_and_reactance(): + """Validate tcycle and reactance behavior.""" assert ep.tcycle(1, freq=60) == pytest.approx(1 / 60) assert ep.tcycle([1, 2], freq=[50, 100]) == pytest.approx(np.array([0.02, 0.02])) @@ -25,6 +26,7 @@ def test_tcycle_and_reactance(): def test_cprint_and_phaseline(): + """Validate cprint and phaseline behavior.""" arr = np.array([ep.phasor(1, 0), ep.phasor(2, 90)]) out = ep.cprint(arr, label="V", unit="V", printval=False, ret=True) assert out.shape == (2, 2) @@ -65,6 +67,7 @@ def test_cprint_and_phaseline(): def test_power_and_slew_helpers(): + """Validate power and slew helpers behavior.""" assert ep.powerset(P=4, Q=3, find="S") == pytest.approx(5.0) assert ep.powerset(P=4, Q=-3, find="PF") == pytest.approx(-0.8) assert ep.powerset(S=5, PF=0.8, find="P") == pytest.approx(4.0) @@ -86,6 +89,7 @@ def test_power_and_slew_helpers(): def test_pf_and_short_circuit(): + """Validate pf and short circuit behavior.""" assert ep.non_linear_pf(PFtrue=None, PFdist=0.8, PFdisp=0.9) == pytest.approx(0.72) assert ep.non_linear_pf(PFtrue=0.72, PFdist=None, PFdisp=0.9) == pytest.approx(0.8) assert ep.non_linear_pf(PFtrue=0.72, PFdist=0.8, PFdisp=None) == pytest.approx(0.9) @@ -113,6 +117,7 @@ def test_pf_and_short_circuit(): def test_dividers_and_basic_helpers(): + """Validate dividers and basic helpers behavior.""" assert ep.voltdiv(12, 4, 8) == pytest.approx(8.0) assert ep.voltdiv(12, 6, 12, Rload=12) == pytest.approx(6.0) @@ -129,6 +134,7 @@ def test_dividers_and_basic_helpers(): def test_electricpy_init_line_coverage_smoke(): + """Validate electricpy init line coverage smoke behavior.""" path = ep.__file__ with open(path, "r", encoding="utf-8") as handle: total_lines = len(handle.read().splitlines()) diff --git a/test/test_fault.py b/test/test_fault.py index 012ca188..95dd5d97 100644 --- a/test/test_fault.py +++ b/test/test_fault.py @@ -5,6 +5,7 @@ def test_single_phase_to_ground_fault_sequence(): + """Validate single phase to ground fault sequence behavior.""" v_th = 1 z_seq = (1, 1, 1) result = fault.single_phase_to_ground_fault(v_th, z_seq) @@ -14,6 +15,7 @@ def test_single_phase_to_ground_fault_sequence(): def test_phase_to_phase_fault_sequence(): + """Validate phase to phase fault sequence behavior.""" v_th = 1 z_seq = (1, 1, 1) result = fault.phase_to_phase_fault(v_th, z_seq) @@ -21,6 +23,7 @@ def test_phase_to_phase_fault_sequence(): def test_three_phase_fault_sequence(): + """Validate three phase fault sequence behavior.""" v_th = 1 z_seq = (1, 1, 1) result = fault.three_phase_fault(v_th, z_seq) diff --git a/test/test_geometry.py b/test/test_geometry.py index d0a8a871..ae07013e 100644 --- a/test/test_geometry.py +++ b/test/test_geometry.py @@ -7,6 +7,7 @@ class TestDistance(): def test_0(self): + """Validate distance scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) assert Geometry.distance(p1, p2) == 2*(2**0.5) @@ -24,6 +25,7 @@ def test_0(self): assert_array_almost_equal(d_output, d_actual, decimal=6) def test_1(self): + """Validate distance scenario 1.""" p1 = Point(1, 2) p2 = Point(1, 3) assert Geometry.distance(p1, p2) == 1 @@ -35,6 +37,7 @@ def test_1(self): class Testslope(): def test_0(self): + """Validate slope scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) assert Geometry.slope(p1, p2) == 1 @@ -44,6 +47,7 @@ def test_0(self): assert Geometry.slope(p2, p1) == -1/6 def test_1(self): + """Validate slope scenario 1.""" p1 = Point(1, 2) p2 = Point(2, 2) @@ -60,6 +64,7 @@ def test_1(self): class Testsection(): def test_0(self): + """Validate section scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) @@ -67,6 +72,7 @@ def test_0(self): assert p == Point(2, 3) def test_1(self): + """Validate section scenario 1.""" p1 = Point(-1, 3) p2 = Point(1, -3) @@ -78,6 +84,7 @@ def test_1(self): class Testline_equaltion(): def test_0(self): + """Validate line equaltion scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) assert Geometry.line_equation(p1, p2) == Line(1, -1, 1) @@ -87,6 +94,7 @@ def test_0(self): assert Geometry.line_equation(p1, p2) == Line(1, 6, 32) def test_1(self): + """Validate line equaltion scenario 1.""" p1 = Point(1, 2) p2 = Point(1, 3) assert Geometry.line_equation(p1, p2) == Line(1, 0, -1) @@ -96,6 +104,7 @@ def test_1(self): assert Geometry.line_equation(p1, p2) == Line(0, 1, -2) def test_2(self): + """Validate line equaltion scenario 2.""" assert Line(1, 2, 3) == Line(2, 4, 6) assert Line(1, -1, 0) == Line(3, -3, 0) assert Line(1, 0, -1) == Line(3, 0, -3) @@ -103,41 +112,45 @@ def test_2(self): class Testline_distance(): def test_0(self): + """Validate line distance scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) - l = Line.construct(p1, p2) - assert Geometry.line_distance(p1, l) == 0 - assert Geometry.line_distance(p2, l) == 0 + line = Line.construct(p1, p2) + assert Geometry.line_distance(p1, line) == 0 + assert Geometry.line_distance(p2, line) == 0 def test_1(self): + """Validate line distance scenario 1.""" p1 = Point(2, 0) p2 = Point(2, 4) p = Point(0, 0) - l = Line.construct(p1, p2) - assert Geometry.line_distance(p, l) == 2 - assert l.distance(p) == 2 + line = Line.construct(p1, p2) + assert Geometry.line_distance(p, line) == 2 + assert line.distance(p) == 2 - l = Line(0, 1, -3) - assert l.distance(p) == 3 + line = Line(0, 1, -3) + assert line.distance(p) == 3 class Testfoot_perpendicular(): def test_0(self): + """Validate foot perpendicular scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) - l = Line.construct(p1, p2) + line = Line.construct(p1, p2) p = Point(2, 2) - assert Geometry.foot_perpendicular(p, l) == Point(1.5, 2.5) + assert Geometry.foot_perpendicular(p, line) == Point(1.5, 2.5) p = Point(2, 3) - assert Geometry.foot_perpendicular(p, l) == Point(2, 3) + assert Geometry.foot_perpendicular(p, line) == Point(2, 3) def test_1(self): + """Validate foot perpendicular scenario 1.""" p = Point(-1, 3) - l = Line(3, -4, -16) + line = Line(3, -4, -16) - p_actual = l.foot_perpendicular(p) - p_image = l.image(p) + p_actual = line.foot_perpendicular(p) + p_image = line.image(p) p_desired = Point(68/25, -49/25) @@ -147,44 +160,51 @@ def test_1(self): class TestPerpendicularBisector(): def test_0(self): + """Validate perpendicular bisector scenario 0.""" p1 = Point(3, 0) p2 = Point(0, 3) - l = Geometry.perpendicular_bisector(p1, p2) - assert l == Line(1, -1, 0) + line = Geometry.perpendicular_bisector(p1, p2) + assert line == Line(1, -1, 0) def test_1(self): + """Validate perpendicular bisector scenario 1.""" p1 = Point(-3, 0) p2 = Point(0, 3) - l = Geometry.perpendicular_bisector(p1, p2) - assert l == Line(1, 1, 0) + line = Geometry.perpendicular_bisector(p1, p2) + assert line == Line(1, 1, 0) def test_2(self): + """Validate perpendicular bisector scenario 2.""" p1 = Point(3, 0) p2 = Point(5, 0) - l = Geometry.perpendicular_bisector(p1, p2) - assert l == Line(1, 0, -4) + line = Geometry.perpendicular_bisector(p1, p2) + assert line == Line(1, 0, -4) def test_3(self): + """Validate perpendicular bisector scenario 3.""" p1 = Point(0, 3) p2 = Point(0, 5) - l = Geometry.perpendicular_bisector(p1, p2) - assert l == Line(0, 1, -4) + line = Geometry.perpendicular_bisector(p1, p2) + assert line == Line(0, 1, -4) class Testcolinear(): def test_0(self): + """Validate colinear scenario 0.""" p1 = Point(1, 2) p2 = Point(3, 4) p3 = Point(5, 6) assert Geometry.colinear(p1, p2, p3) def test_1(self): + """Validate colinear scenario 1.""" p1 = Point(1, 2) p2 = Point(3, 4) p3 = Point(5, 7) assert not Geometry.colinear(p1, p2, p3) def test_2(self): + """Validate colinear scenario 2.""" p1 = Point(1, 0) p2 = Point(2, 0) p3 = Point(3, 0) @@ -193,18 +213,21 @@ def test_2(self): class TestAngleBtwLines(): def test_0(self): + """Validate angle btw lines scenario 0.""" l1 = Line(3, 4, 7) l2 = Line(4, -3, 5) assert cmath.pi/2 == Geometry.angle_btw_lines(l1, l2) def test_1(self): + """Validate angle btw lines scenario 1.""" l1 = Line(3, 0, 0) l2 = Line(4, 0, 0) assert 0 == Geometry.angle_btw_lines(l1, l2) def test_3(self): + """Validate angle btw lines scenario 3.""" l1 = Line(0, 4, 0) l2 = Line(0, 3, 0) - assert 0 == Geometry.angle_btw_lines(l1, l2) \ No newline at end of file + assert 0 == Geometry.angle_btw_lines(l1, l2) diff --git a/test/test_geometry_init.py b/test/test_geometry_init.py index c58ba58e..93ec81e5 100644 --- a/test/test_geometry_init.py +++ b/test/test_geometry_init.py @@ -5,6 +5,7 @@ def test_as_float_and_is_close(): + """Validate as float and is close behavior.""" assert geometry._as_float(1) == 1.0 assert geometry._as_float(1.25) == 1.25 assert geometry._as_float(1 + 1e-13j) == 1.0 @@ -16,6 +17,7 @@ def test_as_float_and_is_close(): def test_point_protocols_and_comparison(): + """Validate point protocols and comparison behavior.""" p1 = geometry.Point(1, 2) p2 = geometry.Point(1, 2) p3 = geometry.Point(1.0 + 1e-10, 2.0) @@ -32,6 +34,7 @@ def test_point_protocols_and_comparison(): def test_line_basic_operations(): + """Validate line basic operations behavior.""" p1 = geometry.Point(0, 0) p2 = geometry.Point(1, 1) line = geometry.line_equation(p1, p2) @@ -60,6 +63,7 @@ def test_line_basic_operations(): def test_line_vertical_horizontal_and_intersection(): + """Validate line vertical horizontal and intersection behavior.""" vertical = geometry.Line(1, 0, -2) horizontal = geometry.Line(0, 1, -3) @@ -89,6 +93,7 @@ def test_line_vertical_horizontal_and_intersection(): def test_line_helpers_and_geometry_functions(): + """Validate line helpers and geometry functions behavior.""" p1 = geometry.Point(0, 0) p2 = geometry.Point(3, 4) @@ -111,6 +116,7 @@ def test_line_helpers_and_geometry_functions(): def test_line_distance_and_reflection(): + """Validate line distance and reflection behavior.""" line = geometry.Line(0, 1, 0) p = geometry.Point(0, 2) @@ -127,6 +133,7 @@ def test_line_distance_and_reflection(): def test_perpendicular_bisector_and_line_equation_errors(): + """Validate error handling for perpendicular bisector and line equation errors.""" p1 = geometry.Point(0, 0) p2 = geometry.Point(2, 0) bisector = geometry.perpendicular_bisector(p1, p2) @@ -140,6 +147,7 @@ def test_perpendicular_bisector_and_line_equation_errors(): def test_angle_between_lines(): + """Validate angle between lines behavior.""" horizontal = geometry.Line(0, 1, 0) vertical = geometry.Line(1, 0, 0) angle = geometry.angle_btw_lines(horizontal, vertical) @@ -154,6 +162,7 @@ def test_angle_between_lines(): def test_degenerate_line_errors(): + """Validate error handling for degenerate line errors.""" with pytest.raises(AssertionError): geometry.Line(0, 0, 1) diff --git a/test/test_imports.py b/test/test_imports.py index 72340500..37b3475b 100644 --- a/test/test_imports.py +++ b/test/test_imports.py @@ -1,76 +1,52 @@ # Import ElectricPy modules just to make sure they load correctly +import importlib + # Test importing the package itself def test_import_by_name(): - try: - import electricpy - assert True - except: - assert False + """Validate import behavior for import by name.""" + assert importlib.import_module("electricpy") is not None # Test importing the `bode` module def test_import_bode(): - try: - from electricpy import bode - assert True - except: - assert False + """Validate import behavior for import bode.""" + assert importlib.import_module("electricpy.bode") is not None # Test importing the `constants` module def test_import_constants(): - try: - from electricpy import constants - assert True - except: - assert False + """Validate import behavior for import constants.""" + assert importlib.import_module("electricpy.constants") is not None # Test importing the `fault` module def test_import_fault(): - try: - from electricpy import fault - assert True - except: - assert False + """Validate import behavior for import fault.""" + assert importlib.import_module("electricpy.fault") is not None # Test importing the `sim` module def test_import_sim(): - try: - from electricpy import sim - assert True - except: - assert False + """Validate import behavior for import sim.""" + assert importlib.import_module("electricpy.sim") is not None # Test importing the `visu` module def test_import_visu(): - try: - from electricpy import visu - assert True - except: - assert False + """Validate import behavior for import visu.""" + assert importlib.import_module("electricpy.visu") is not None # Testing Imports of geometry submodule # Testing geometry import def test_Geometry(): - try: - from electricpy import geometry - assert True - except ImportError: - assert False + """Validate geometry behavior.""" + assert importlib.import_module("electricpy.geometry") is not None # Testing circle import from electricpy.geometry def test_circle(): - try: - from electricpy.geometry.circle import Circle - assert True - except ImportError: - assert False + """Validate circle behavior.""" + module = importlib.import_module("electricpy.geometry.circle") + assert module.Circle is not None # Testing triangle import from electricpy.geometry def test_triangle(): - try: - from electricpy.geometry.triangle import Triangle - assert True - except ImportError: - assert False - + """Validate triangle behavior.""" + module = importlib.import_module("electricpy.geometry.triangle") + assert module.Triangle is not None diff --git a/test/test_latex.py b/test/test_latex.py index 58be5d67..001650c9 100644 --- a/test/test_latex.py +++ b/test/test_latex.py @@ -2,6 +2,7 @@ def test_clatex_rectangular_format(): + """Validate clatex rectangular format behavior.""" latex_str = latex.clatex(1 + 2j, polar=False) assert latex_str.startswith("$") @@ -9,6 +10,13 @@ def test_clatex_rectangular_format(): assert r"\mathrm{j}" in latex_str +def test_clatex_rectangular_negative_imaginary(): + """Validate clatex rectangular negative imaginary behavior.""" + latex_str = latex.clatex(1 - 2j, polar=False) + assert "-\\mathrm{j}2.0" in latex_str + + def test_tflatex_basic(): + """Validate tflatex basic behavior.""" latex_str = latex.tflatex(([1, 1], [1, 2, 1]), predollar=False, postdollar=False) assert r"\frac" in latex_str diff --git a/test/test_machines.py b/test/test_machines.py index 00cb07a4..d9d950fe 100644 --- a/test/test_machines.py +++ b/test/test_machines.py @@ -1,9 +1,64 @@ import numpy as np +import pytest from electricpy import machines def test_phase_shift_transformer_respects_signed_shift(): + """Verify phase-shift polarity mirrors with opposite shift sign.""" phase_pos = machines.phase_shift_transformer(style="DY", shift=30) phase_neg = machines.phase_shift_transformer(style="DY", shift=-30) assert np.isclose(phase_pos, np.conj(phase_neg)) + + +def test_phase_shift_transformer_invalid_style_raises() -> None: + """Unsupported orientation keys should raise a lookup error.""" + with pytest.raises(KeyError): + machines.phase_shift_transformer(style="ZZ", shift=30) + + +def test_transformertest_open_circuit_only_returns_two_values() -> None: + """Open-circuit triplet should return only Rc and Xm.""" + rc, xm = machines.transformertest( + Poc=100.0, + Voc=200.0, + Ioc=1.0, + Psc=None, + Vsc=None, + Isc=None, + ) + assert rc > 0 + assert xm > 0 + + +def test_transformertest_short_circuit_only_returns_two_values() -> None: + """Short-circuit triplet should return only Req and Xeq.""" + req, xeq = machines.transformertest( + Poc=None, + Voc=None, + Ioc=None, + Psc=100.0, + Vsc=20.0, + Isc=10.0, + ) + assert req == pytest.approx(1.0, rel=1e-12) + assert xeq == pytest.approx(np.sqrt(3), rel=1e-12) + + +def test_transformertest_combined_returns_all_values() -> None: + """Providing both triplets should return four transformer parameters.""" + values = machines.transformertest( + Poc=100.0, + Voc=200.0, + Ioc=1.0, + Psc=100.0, + Vsc=20.0, + Isc=10.0, + ) + assert len(values) == 4 + + +def test_transformertest_missing_arguments_raises() -> None: + """Incomplete input should fail with a clear ValueError.""" + with pytest.raises(ValueError): + machines.transformertest(Poc=100.0, Voc=200.0, Ioc=None, Psc=None, Vsc=None, Isc=None) diff --git a/test/test_math.py b/test/test_math.py index 5287e1fd..091e3662 100644 --- a/test/test_math.py +++ b/test/test_math.py @@ -6,24 +6,29 @@ def test_gaussian_at_zero(): + """Validate gaussian at zero behavior.""" expected = 1 / np.sqrt(2 * np.pi) assert_allclose(epmath.gaussian(0, mu=0, sigma=1), expected) def test_gausdist_midpoint(): + """Validate gausdist midpoint behavior.""" assert_allclose(epmath.gausdist(0, mu=0, sigma=1), 0.5, atol=1e-6) def test_step_function(): + """Validate step function behavior.""" data = np.array([-1.0, 0.0, 1.0]) assert_allclose(epmath.step(data), np.array([0.0, 1.0, 1.0])) def test_gaussian_sigma_zero(): + """Validate gaussian sigma zero behavior.""" with pytest.raises(ValueError): epmath.gaussian(0, sigma=0) def test_funcrms_validation(): + """Validate error handling for funcrms validation.""" with pytest.raises(ValueError): epmath.funcrms(lambda x: x, T=0) diff --git a/test/test_passive.py b/test/test_passive.py index 445b7021..29d9d01f 100644 --- a/test/test_passive.py +++ b/test/test_passive.py @@ -4,23 +4,39 @@ def test_vcap_charge_discharge_initial(): + """Validate vcap charge discharge initial behavior.""" vs = 5 assert passive.vcapcharge(0, vs, 1, 1) == 0 assert passive.vcapdischarge(0, vs, 1, 1) == vs def test_capbacktoback_vln(): + """Validate capbacktoback vln behavior.""" imax, ifreq = passive.capbacktoback(1e-6, 1e-6, 1e-3, VLN=120) assert imax > 0 assert ifreq > 0 def test_captransfer_validation(): + """Validate error handling for captransfer validation.""" with pytest.raises(ValueError): passive.captransfer(1, 1, 1, 0, 0) +def test_captransfer_negative_time_raises(): + """Validate error handling for captransfer negative time raises.""" + with pytest.raises(ValueError): + passive.captransfer(-1, 1, 1, 1, 1) + + +def test_capbacktoback_requires_voltage_input(): + """Validate error handling for capbacktoback requires voltage input.""" + with pytest.raises(ValueError): + passive.capbacktoback(1e-6, 1e-6, 1e-3) + + def test_farads_matches_formula(): + """Validate farads matches formula behavior.""" var = 1000 voltage = 120 freq = 60 @@ -29,5 +45,22 @@ def test_farads_matches_formula(): def test_vcapcharge_negative_time(): + """Validate vcapcharge negative time behavior.""" with pytest.raises(ValueError): passive.vcapcharge(-1, 1, 1, 1) + + +def test_capenergy_matches_formula() -> None: + """Capacitor energy should follow 1/2*C*V^2.""" + c_val = 2.0e-6 + voltage = 400.0 + expected = 0.5 * c_val * voltage * voltage + assert passive.capenergy(c_val, voltage) == pytest.approx(expected, rel=1e-12) + + +def test_inductorenergy_matches_formula() -> None: + """Inductor energy should follow 1/2*L*I^2.""" + inductance = 5.0e-3 + current = 12.0 + expected = 0.5 * inductance * current * current + assert passive.inductorenergy(inductance, current) == pytest.approx(expected, rel=1e-12) diff --git a/test/test_phasor.py b/test/test_phasor.py index 0ecfa230..a4637ce1 100644 --- a/test/test_phasor.py +++ b/test/test_phasor.py @@ -5,6 +5,7 @@ from numpy.testing import assert_almost_equal def test_phasor(): + """Validate phasor behavior.""" magnitude = 10 # basic angles test case 0 z1 = phasor(magnitude, 0) @@ -36,6 +37,7 @@ def test_phasor(): class TestPhs(): def test_0(self): + """Validate phs scenario 0.""" inputs = [0, 90, 180, 270, 360] outputs = [phs(x) for x in inputs] @@ -45,6 +47,7 @@ def test_0(self): assert_almost_equal(x, y) def test_1(self): + """Validate phs scenario 1.""" inputs = [30, 45, 60, 135] outputs = [phs(x) for x in inputs] @@ -56,6 +59,7 @@ def test_1(self): class TestVectarray(): def test_0(self): + """Validate vectarray scenario 0.""" A = [2+3j, 4+5j, 6+7j, 8+9j] B = vectarray(A) @@ -65,6 +69,7 @@ def test_0(self): np.testing.assert_array_almost_equal(B, B_test) def test_1(self): + """Validate vectarray scenario 1.""" A = np.random.random(size = 16) B = vectarray(A) diff --git a/test/test_sim.py b/test/test_sim.py index 9cd1a8e5..c350d285 100644 --- a/test/test_sim.py +++ b/test/test_sim.py @@ -1,5 +1,6 @@ import matplotlib import numpy as np +import pytest matplotlib.use("Agg") @@ -7,6 +8,7 @@ def test_digifiltersim_basic(): + """Validate digifiltersim basic behavior.""" plot_module = sim.digifiltersim( lambda t, f: np.sin(2 * np.pi * f * t), [0.0, 0.0, 1.0, 0.0, 0.0], @@ -20,6 +22,14 @@ def test_digifiltersim_basic(): def test_step_response_returns_plot(): + """Step response helper should return a plotting module.""" system = (np.array([1.0]), np.array([1.0, 1.0])) plot_module = sim.step_response(system, npts=5, dt=0.1, combine=False, xlim=(0, 1)) assert plot_module is not None + + +def test_nr_pq_rejects_non_square_ybus() -> None: + """Power-flow generator requires a square Y-bus matrix.""" + ybus = np.array([[1 + 0j, 2 + 0j, 3 + 0j], [4 + 0j, 5 + 0j, 6 + 0j]]) + with pytest.raises(ValueError, match="Invalid Y-Bus Shape"): + sim.nr_pq(ybus, V_set=[[1, 0], [None, None]], P_set=[None, -1.0], Q_set=[None, -0.5]) diff --git a/test/test_thermal.py b/test/test_thermal.py index 24c10913..1cbeeaca 100644 --- a/test/test_thermal.py +++ b/test/test_thermal.py @@ -1,6 +1,40 @@ import electricpy.thermal as thermal +import pytest def test_rtdtemp_allows_no_rounding(): + """RTD helper should support returning an unrounded float.""" temp = thermal.rtdtemp(100.0, rtdtype="PT100", round=None) assert isinstance(temp, float) + + +def test_coldjunction_invalid_coupletype_raises() -> None: + """Unknown thermocouple types should be rejected.""" + with pytest.raises(ValueError): + thermal.coldjunction(25.0, coupletype="X") + + +def test_coldjunction_type_b_has_stricter_temperature_range() -> None: + """Type B enforces 0C lower bound while type K allows subzero values.""" + with pytest.raises(ValueError): + thermal.coldjunction(-1.0, coupletype="B") + assert isinstance(thermal.coldjunction(-1.0, coupletype="K"), float) + + +def test_thermocouple_invalid_coupletype_raises() -> None: + """Unknown thermocouple types should be rejected in temperature solve.""" + with pytest.raises(ValueError): + thermal.thermocouple(0.0, coupletype="X") + + +def test_thermocouple_voltage_above_upper_bound_raises() -> None: + """Clearly out-of-range voltages should fail fast.""" + with pytest.raises(ValueError): + thermal.thermocouple(1.0, coupletype="K") + + +def test_thermocouple_fahrenheit_conversion_matches_celsius() -> None: + """Fahrenheit option should apply the standard linear conversion.""" + celsius = thermal.thermocouple(0.0, coupletype="K", round=None) + fahrenheit = thermal.thermocouple(0.0, coupletype="K", fahrenheit=True, round=None) + assert fahrenheit == pytest.approx((celsius * 9.0 / 5.0) + 32.0, rel=1e-12) diff --git a/test/test_triangle.py b/test/test_triangle.py index f543985e..05cb38a1 100644 --- a/test/test_triangle.py +++ b/test/test_triangle.py @@ -9,6 +9,7 @@ class TestCentroid(): def test_0(self): + """Validate centroid scenario 0.""" p1 = Point(0, 1) p2 = Point(1, 0) p3 = Point(0, 0) @@ -16,6 +17,7 @@ def test_0(self): assert t.centroid() == Point(1/3, 1/3) def test_1(self): + """Validate centroid scenario 1.""" p1 = Point(1.1, 2.2) p2 = Point(3.1, 4.2) p3 = Point(5.1, 6.7) @@ -26,6 +28,7 @@ def test_1(self): class TestInCenter(): def test_0(self): + """Validate in center scenario 0.""" p1 = Point(0, 1) p2 = Point(1, 0) p3 = Point(0, 0) @@ -33,6 +36,7 @@ def test_0(self): assert compare_points(t.in_center(), Point(1/(2 + cmath.sqrt(2)), 1/(2 + cmath.sqrt(2)))) def test_1(self): + """Validate in center scenario 1.""" p1 = Point(0, 0) p2 = Point(1, 0) p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3)) @@ -42,6 +46,7 @@ def test_1(self): class TestOrthoCenter(): def test_0(self): + """Validate ortho center scenario 0.""" p1 = Point(0, 1) p2 = Point(1, 0) p3 = Point(0, 0) @@ -49,6 +54,7 @@ def test_0(self): assert compare_points(t.ortho_center(), Point(0, 0)) def test_1(self): + """Validate ortho center scenario 1.""" p1 = Point(0, 0) p2 = Point(1, 0) p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3)) @@ -58,6 +64,7 @@ def test_1(self): class TestCircumCenter(): def test_0(self): + """Validate circum center scenario 0.""" p1 = Point(0, 1) p2 = Point(1, 0) p3 = Point(0, 0) @@ -65,6 +72,7 @@ def test_0(self): assert compare_points(t.circum_center(), Point(0.5, 0.5)) def test_1(self): + """Validate circum center scenario 1.""" p1 = Point(0, 0) p2 = Point(1, 0) p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3)) @@ -73,6 +81,7 @@ def test_1(self): def test_triangle_perimeter_and_area(): + """Validate triangle perimeter and area behavior.""" p1 = Point(0, 0) p2 = Point(3, 0) p3 = Point(0, 4) @@ -83,6 +92,7 @@ def test_triangle_perimeter_and_area(): def test_triangle_radii(): + """Validate triangle radii behavior.""" p1 = Point(0, 0) p2 = Point(3, 0) p3 = Point(0, 4) @@ -92,6 +102,7 @@ def test_triangle_radii(): def test_triangle_invalid_points(): + """Validate error handling for triangle invalid points.""" p1 = Point(0, 0) p2 = Point(1, 1) p3 = Point(2, 2) @@ -100,6 +111,7 @@ def test_triangle_invalid_points(): def test_triangle_init_validation_and_helpers(): + """Validate error handling for triangle init validation and helpers.""" with pytest.raises(ValueError): triangle.Triangle(Point(0, 0), Point(1, 0)) @@ -124,6 +136,7 @@ def test_triangle_init_validation_and_helpers(): def test_triangle_area_and_centers_errors(): + """Validate error handling for triangle area and centers errors.""" tri = object.__new__(triangle.Triangle) tri.points = (Point(0, 0), Point(1, 0), Point(0, 1)) tri._tol = 1e-12 @@ -167,6 +180,7 @@ def test_triangle_area_and_centers_errors(): def test_triangle_validation_helper(): + """Validate error handling for triangle validation helper.""" tri = object.__new__(triangle.Triangle) tri.points = (Point(0, 0), Point(1, 0), Point(2, 0)) tri._tol = 1e-12 diff --git a/test/test_version.py b/test/test_version.py index 3dc550e6..4e4ce266 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -2,6 +2,7 @@ def test_version_fields(): + """Validate version fields behavior.""" assert version.NAME assert version.VERSION assert "." in version.VERSION diff --git a/test/test_visu.py b/test/test_visu.py index aa7c37aa..f184e540 100644 --- a/test/test_visu.py +++ b/test/test_visu.py @@ -1,12 +1,11 @@ import math import cmath -from matplotlib.legend import Legend -import matplotlib.pyplot as plt from numpy.testing import assert_almost_equal class Test_visualization: def test_induction_motor_circle(self): + """Validate induction motor circle behavior.""" from electricpy.visu import InductionMotorCircle open_circuit_test_data = {'V0': 400, 'I0': 9, 'W0': 1310} @@ -28,6 +27,7 @@ def test_induction_motor_circle(self): ) def test_power_circle(self): + """Validate power circle behavior.""" from electricpy.visu import receiving_end_power_circle data = { "A" : cmath.rect(0.895, math.radians(1.4)), @@ -42,6 +42,7 @@ def test_power_circle(self): assert_almost_equal(abs(power_circle()['Vs']), 224.909, decimal = 3) def test_rlc_frequency_response(self): + """Validate rlc frequency response behavior.""" # import RLC from electricpy.visu from electricpy.visu import SeriesRLC @@ -49,12 +50,14 @@ def test_rlc_frequency_response(self): rlc_obj1 = SeriesRLC( resistance=5, inductance=0.4, capacitance=25.3e-6, frequency=50 ) + assert rlc_obj1 is not None # gh1 = rlc_obj1.graph(lower_frequency_cut=0.1, upper_frequency_cut=100, samples=1000) rlc_obj2 = SeriesRLC( resistance=10, inductance=0.5, capacitance=25.3e-6, frequency=50 ) + assert rlc_obj2 is not None # gh2 = rlc_obj2.graph(lower_frequency_cut=0.1, upper_frequency_cut=100, samples=1000) @@ -62,5 +65,3 @@ def test_rlc_frequency_response(self): # plt.gca().add_artist(gh2.legend(rlc_obj2.legend(), title=f"(R, L, C) => (10, 0.5 25.3e-6)", loc='upper left')) # plt.show() - - From de75cdfabee5127f78488e862064087759016a5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:37:51 +0000 Subject: [PATCH 12/17] Fix 8 failing tests: ic_555_astable formula/freq branch, ic_555_monostable t_low=None crash, short_circuit_current RMS branch, suspension_insulators return order, visu.py missing docstring --- electricpy/__init__.py | 60 ++++++++++++++++++------------------------ electricpy/visu.py | 2 +- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/electricpy/__init__.py b/electricpy/__init__.py index fbd101e9..e49884ef 100644 --- a/electricpy/__init__.py +++ b/electricpy/__init__.py @@ -698,17 +698,15 @@ def short_circuit_current(V, Z, t=None, f=None, mxcurrent=True, alpha=None): # Calculate Asymmetrical (total) Current if t is not None if t is not None and f is not None: - # Calculate RMS if none of the angular values are provided - if alpha is None and omega is None: - # Calculate tau - tau = t / (1 / 60) + # Calculate RMS if no alpha (angle) provided + if alpha is None: + # Calculate tau using the provided frequency + tau = t * f K = _np.sqrt(1 + 2 * _np.exp(-4 * _np.pi * tau / (X / R))) IAC = abs(V / Z) Irms = K * IAC # Return Values return Irms, IAC, K - if alpha is None or omega is None: - raise ValueError("ERROR: Inappropriate Arguments Provided.") # Calculate Instantaneous if all angular values provided # Convert Degrees to Radians omega = _np.radians(omega) @@ -2330,11 +2328,11 @@ def suspension_insulators(number_capacitors, capacitance_ratio, Voltage): Returns ------- - string_efficiency: float - String efficiency of capacitive disks - capacitor_disk_voltages: float + capacitor_disk_voltages: numpy.ndarray Voltage across each capacitive disk starting from top to bottom + string_efficiency: float + String efficiency of capacitive disks """ _m = _np.zeros((number_capacitors, number_capacitors)) # Iterate over capacitors @@ -2362,7 +2360,7 @@ def suspension_insulators(number_capacitors, capacitance_ratio, Voltage): (Voltage * 100) / (number_capacitors * capacitor_disk_voltages[-1, 0]) ) - return string_efficiency, capacitor_disk_voltages + return capacitor_disk_voltages, string_efficiency # Define Natural Frequency/Resonant Frequency Calculator @@ -3280,11 +3278,11 @@ def ic_555_astable(R=None, C=None, freq=None, t_high=None, t_low=None): if t_high is not None and t_low is not None and C is not None: - x2 = t_low / C * _np.log(2) - x1 = t_high / C * _np.log(2) + x2 = t_low / (_np.log(2) * C) + x1 = t_high / (_np.log(2) * C) T = t_high + t_low freq = 1 / (T) - duty_cycle = t_high / (T) + duty_cycle = t_high * 100 / T return { 'time_period': T, @@ -3293,10 +3291,15 @@ def ic_555_astable(R=None, C=None, freq=None, t_high=None, t_low=None): 'R1': x1 - x2, 'R2': x2 } - raise TypeError("Not enough parqmeters are passed") + + if freq is not None and C is not None: + T = 1 / freq + R1_plus_2R2 = T / (_np.log(2) * C) + return {'R1_plus_2R2': R1_plus_2R2} + + raise TypeError("Not enough parameters are passed") -# pylint: disable-next=inconsistent-return-statements def ic_555_monostable(R=None, C=None, t_high=None, t_low=None): """ 555 Integrated Circuit Calculator. @@ -3309,25 +3312,21 @@ def ic_555_monostable(R=None, C=None, t_high=None, t_low=None): Parameters ---------- - R: list[float, float] or tuple(float, float), optional - List of 2 resistor which are need in configuring IC 555. + R: float, optional + Resistance used in configuring IC 555. C: float, optional Capacitance between Threshold Pin and ground - # f removed. t_high: float, optional - ON time of IC 555 + Pulse width (ON time) of IC 555 t_low: float, optional - OFF time of IC 555 + OFF time (not used in monostable mode) Returns ------- - dict: "time_period": Time period of oscillating IC 555 - "frequency": frequency of oscilation of IC 555 - "duty_cycle": ration between ON time and total time - "t_low": ON time of IC 555 - "t_high": OFF time of IC 555 + float: The solved parameter (R, C, or T=R*C*log(3)) depending on which + argument is None. """ - T = t_high + t_low + T = t_high if R is None: if not (C is not None and T is not None): raise ValueError( @@ -3342,14 +3341,7 @@ def ic_555_monostable(R=None, C=None, t_high=None, t_low=None): "provided" ) return T / (_np.log(3) * R) - - if T is None: - if not (R is not None and T is not None): - raise ValueError( - "To find Time delay , Resistance and Capacitance should be " - "provided" - ) - return R * C * _np.log(3) + return R * C * _np.log(3) def t_attenuator(Adb, Z0): diff --git a/electricpy/visu.py b/electricpy/visu.py index 162f2c2f..5fb46167 100644 --- a/electricpy/visu.py +++ b/electricpy/visu.py @@ -477,7 +477,7 @@ def __init__( self.data = self.compute_efficiency() def __call__(self): - __doc__ = self.__doc__ # noqa: D102, F841 + """Return the computed efficiency data for the induction motor circle.""" return self.data def plot(self): From 6f90c440a4745e8a98f0646379753007c4036884 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:39:28 +0000 Subject: [PATCH 13/17] Rename tau to t_cycles for clarity in short_circuit_current --- electricpy/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electricpy/__init__.py b/electricpy/__init__.py index e49884ef..cc63ec05 100644 --- a/electricpy/__init__.py +++ b/electricpy/__init__.py @@ -700,9 +700,9 @@ def short_circuit_current(V, Z, t=None, f=None, mxcurrent=True, alpha=None): if t is not None and f is not None: # Calculate RMS if no alpha (angle) provided if alpha is None: - # Calculate tau using the provided frequency - tau = t * f - K = _np.sqrt(1 + 2 * _np.exp(-4 * _np.pi * tau / (X / R))) + # Number of cycles elapsed at the provided frequency + t_cycles = t * f + K = _np.sqrt(1 + 2 * _np.exp(-4 * _np.pi * t_cycles / (X / R))) IAC = abs(V / Z) Irms = K * IAC # Return Values From 25c9c4904cfb59a94ef5795fb4e7902165793743 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 22 Jul 2026 20:52:14 -0700 Subject: [PATCH 14/17] drop python 3.8/3.9 --- .github/workflows/pytest.yml | 2 +- pyproject.toml | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 940f9158..19dd109d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 4b356e70..017a7152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,14 @@ maintainers = [ license = {file = "LICENSE"} classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent" + "Operating System :: OS Independent", + "Intended Audience :: Science/Research" ] dynamic = ["version"] From a1acb460a7d5299c7e37fdf18ba2dca2e33899dd Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 22 Jul 2026 21:08:36 -0700 Subject: [PATCH 15/17] update for sphinx build, updated readme --- .github/workflows/sphinx.yml | 1 + .vscode/settings.json | 12 +++- README.md | 116 +++++++++++++++++++---------------- pyproject.toml | 1 + 4 files changed, 74 insertions(+), 56 deletions(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 09eb27f4..bb4dd5bb 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -22,6 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install setuptools cp logo/ElectricpyLogo.svg docsource/static/ElectricpyLogo.svg - name: Build Sphinx docs if: success() diff --git a/.vscode/settings.json b/.vscode/settings.json index 078ed0cb..9d845d51 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,13 @@ }, "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.testing.cwd": "${workspaceRoot}" -} \ No newline at end of file + "python.testing.cwd": "${workspaceRoot}", + "markdownlint.config": { + "MD033": { + "allowed_elements": [ + "a", + "img" + ] + } + } +} diff --git a/README.md b/README.md index 112b3e04..2a7ebd6d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,45 @@ +# ElectricPy + logo -# ElectricPy - -*Electrical-Engineering-for-Python* +> *Electrical-Engineering-for-Python* + [![sphinx](https://github.com/engineerjoe440/ElectricPy/actions/workflows/sphinx-build.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/sphinx-build.yml) [![Documentation Status](https://readthedocs.org/projects/electricpy/badge/?version=latest)](https://electricpy.readthedocs.io/en/latest/?badge=latest) + [![pytest](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pytest.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pytest.yml) [![pydocstyle](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pydocstyle.yml/badge.svg?branch=master)](https://github.com/engineerjoe440/ElectricPy/actions/workflows/pydocstyle.yml) ![Coverage](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/coverage.svg) -[![](https://img.shields.io/pypi/v/electricpy.svg?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/electricpy/) -[![](https://pepy.tech/badge/electricpy)](https://pepy.tech/project/electricpy) -[![](https://img.shields.io/github/stars/engineerjoe440/electricpy?logo=github)](https://github.com/engineerjoe440/electricpy/) -[![](https://img.shields.io/pypi/l/electricpy.svg?color=blue)](https://github.com/engineerjoe440/electricpy/blob/master/LICENSE.txt) + +[![PyPI Link](https://img.shields.io/pypi/v/electricpy.svg?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/electricpy/) +[![download count](https://pepy.tech/badge/electricpy)](https://pepy.tech/project/electricpy) +[![github stars count](https://img.shields.io/github/stars/engineerjoe440/electricpy?logo=github)](https://github.com/engineerjoe440/electricpy/) +[![license](https://img.shields.io/pypi/l/electricpy.svg?color=blue)](https://github.com/engineerjoe440/electricpy/blob/master/LICENSE.txt) + +[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) +[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/) +[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) +[![Python 3.13](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/release/python-3130/) +[![Python 3.14](https://img.shields.io/badge/python-3.14-blue.svg)](https://www.python.org/downloads/release/python-3140/) + + [![Matrix](https://img.shields.io/matrix/electricpy:stanleysolutionsn.com?label=%23electricpy:stanleysolutionsnw.com&logo=matrix&server_fqdn=matrix.stanleysolutionsnw.com&style=for-the-badge)](https://matrix.to/#/#electricpy:stanleysolutionsnw.com) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/engineerjoe440) - Python Libraries with functions and constants related to electrical engineering. The functions and constants that make up these modules represent a library of material compiled with the intent of being used primarily for research, development, education, and exploration in the realm of electrical engineering. -Check out our full documentation: https://electricpy.readthedocs.io/en/latest/ +Check out our full documentation: [https://electricpy.readthedocs.io/en/latest/](https://electricpy.readthedocs.io/en/latest/) ## Features @@ -42,40 +52,39 @@ notebooks!) ### Samples Generated with ElectricPy | Phasor Plot | Power Triangle | Induction Motor Circle | -|-------------|----------------|------------------------| -| ![](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PhasorPlot.png) | ![](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PowerTriangle.png) | ![](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/InductionMotorCircleExample.png) | - +| ----------- | -------------- | ---------------------- | +| ![phasor plot](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PhasorPlot.png) | ![power triangle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PowerTriangle.png) | ![induction motor circle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/InductionMotorCircleExample.png) | -| RLC Frequency Response | | Receiving Power Circle | -|------------------------|----------------|------------------------| -| ![](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/series-rlc-r5-l0.4.png) | | ![](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/ReceivingPowerCircleExample.png) | +| RLC Frequency Response | Placeholder | Receiving Power Circle | +| --- | --- | --- | +| ![series rlc response](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/series-rlc-r5-l0.4.png) | - | ![receiving power circle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/ReceivingPowerCircleExample.png) | ## Installing / Getting Started 1. ElectricPy has a few basic installation options for use with `pip`. For most -common users, use the following command to install ElectricPy with `pip` + common users, use the following command to install ElectricPy with `pip` + + ```bash + pip install electricpy[full] + ``` -``` -pip install electricpy[full] -``` - 2. Check installation success in Python environment: -```python -import electricpy -electricpy._version_ -``` + ```python + import electricpy + electricpy._version_ + ``` 3. Start using the electrical engineering formulas -```python ->>> import electricpy as ep ->>> voltage = ep.phasor(67, 120) # 67 volts at angle 120 degrees ->>> voltage -(-33.499999999999986+58.02370205355739j) ->>> ep.cprint(voltage) -67.0 ∠ 120.0° -``` + ```python + >>> import electricpy as ep + >>> voltage = ep.phasor(67, 120) # 67 volts at angle 120 degrees + >>> voltage + (-33.499999999999986+58.02370205355739j) + >>> ep.cprint(voltage) + 67.0 ∠ 120.0° + ``` ### Installing from Source @@ -83,7 +92,7 @@ If you're looking to get the "latest and greatest" from electricpy, you'll want to install directly from GitHub, you can do that one of two ways, the easiest of which is to simply issue the following command for `pip` -``` +```bash pip install git+https://github.com/engineerjoe440/ElectricPy.git ``` @@ -92,27 +101,26 @@ and installing locally. 1. Clone/Download Source Code from [GitHub Repository](https://github.com/engineerjoe440/ElectricPy) 2. Open Terminal and Navigate to Folder with `cd` Commands: - - `cd \electricpy` + * `cd \electricpy` 3. Use Python to Install Module from `setup.py`: - - `pip install .` + * `pip install .` ### Dependencies -- [NumPy](https://numpy.org/) -- [matplotlib](https://matplotlib.org/) -- [SciPy](https://scipy.org/) -- [SymPy](https://www.sympy.org/en/index.html) +* [NumPy](https://numpy.org/) +* [matplotlib](https://matplotlib.org/) +* [SciPy](https://scipy.org/) +* [SymPy](https://www.sympy.org/en/index.html) #### Optional Dependencies For numerical analysis (install with `pip install electricpy[numerical]`): -- [numdifftools](https://numdifftools.readthedocs.io/en/latest/) +* [numdifftools](https://numdifftools.readthedocs.io/en/latest/) For fault analysis (install with `pip install electricpy[fault]`) -- [arcflash](https://github.com/LiaungYip/arcflash) - +* [arcflash](https://github.com/LiaungYip/arcflash) ## Get Involved / Contribute @@ -138,19 +146,19 @@ opportunity to take advantage of this project. **Come [chat about ElectricPy](https://matrix.to/#/#electricpy:stanleysolutionsnw.com)** -### Special thanks to... +### Special Thanks -- Stephen Weeks | Student - U of Idaho -- Jeremy Perhac | Student - U of Idaho -- Daniel Allen | Student - Universtiy of Idaho -- Dr. Dennis Sullivan | Proffessor - U of Idaho -- Dr. Brian Johnson | Proffessor - U of Idaho -- Dr. Joe Law | Proffessor - U of Idaho -- StackOverflow user gg349 -- Shaurya Uppal | Online Code Contributor -- Paul Ortman | Power Quality Engineer - Idaho Power | Instructor - U of Idaho +* Stephen Weeks | Student - U of Idaho +* Jeremy Perhac | Student - U of Idaho +* Daniel Allen | Student - Universtiy of Idaho +* Dr. Dennis Sullivan | Proffessor - U of Idaho +* Dr. Brian Johnson | Proffessor - U of Idaho +* Dr. Joe Law | Proffessor - U of Idaho +* StackOverflow user gg349 +* Shaurya Uppal | Online Code Contributor +* Paul Ortman | Power Quality Engineer - Idaho Power | Instructor - U of Idaho -*and* +> *and* contributors @@ -160,7 +168,7 @@ opportunity to take advantage of this project. For more information regarding this resource, please contact Joe Stanley -- +* ## License and Usage diff --git a/pyproject.toml b/pyproject.toml index 017a7152..011beea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ dev = [ "sphinx-git", "sphinx-immaterial", "coverage-badge", + "setuptools", "numpy", "matplotlib", "scipy", From 07072e47a88725f1a39520d82f15d2d23d266ebc Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 22 Jul 2026 21:15:34 -0700 Subject: [PATCH 16/17] use new coverage system --- .github/workflows/sphinx.yml | 4 ++-- README.md | 2 +- pyproject.toml | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index bb4dd5bb..d0467a50 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -22,7 +22,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install setuptools cp logo/ElectricpyLogo.svg docsource/static/ElectricpyLogo.svg - name: Build Sphinx docs if: success() @@ -34,7 +33,8 @@ jobs: if: success() run: | coverage run --source=./electricpy -m pytest - coverage-badge -o docs/html/coverage.svg + coverage xml -o coverage.xml + genbadge coverage -i coverage.xml -o docs/html/coverage.svg # https://github.com/marketplace/actions/github-pages #- if: success() diff --git a/README.md b/README.md index 2a7ebd6d..b1fa83ce 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ notebooks!) | ----------- | -------------- | ---------------------- | | ![phasor plot](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PhasorPlot.png) | ![power triangle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/PowerTriangle.png) | ![induction motor circle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/InductionMotorCircleExample.png) | -| RLC Frequency Response | Placeholder | Receiving Power Circle | +| RLC Frequency Response | - | Receiving Power Circle | | --- | --- | --- | | ![series rlc response](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/series-rlc-r5-l0.4.png) | - | ![receiving power circle](https://raw.githubusercontent.com/engineerjoe440/ElectricPy/gh-pages/_images/ReceivingPowerCircleExample.png) | diff --git a/pyproject.toml b/pyproject.toml index 011beea7..f04ad80c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,7 @@ dev = [ "sphinx-sitemap", "sphinx-git", "sphinx-immaterial", - "coverage-badge", - "setuptools", + "genbadge[coverage]", "numpy", "matplotlib", "scipy", From e312d11600b6dd55f33d430597fb491548950b38 Mon Sep 17 00:00:00 2001 From: Joe Stanley Date: Wed, 22 Jul 2026 21:19:05 -0700 Subject: [PATCH 17/17] use new coverage system --- .github/workflows/sphinx.yml | 11 ++++++----- pyproject.toml | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index d0467a50..58e22e91 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -26,15 +26,16 @@ jobs: - name: Build Sphinx docs if: success() run: | - pip install .[dev] - python3 -c "import electricpy; print('electricpy.__file__')" + python -m pip install .[dev] + python -c "import electricpy; print(electricpy.__file__)" sphinx-build -M html docsource docs - name: Generate Coverage Badge if: success() run: | - coverage run --source=./electricpy -m pytest - coverage xml -o coverage.xml - genbadge coverage -i coverage.xml -o docs/html/coverage.svg + python -m pip install coverage genbadge pytest + python -m coverage run --source=./electricpy -m pytest + python -m coverage xml -o coverage.xml + python -m genbadge.main coverage -i coverage.xml -o docs/html/coverage.svg # https://github.com/marketplace/actions/github-pages #- if: success() diff --git a/pyproject.toml b/pyproject.toml index f04ad80c..147766d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ dev = [ "sphinx-git", "sphinx-immaterial", "genbadge[coverage]", + "coverage", "numpy", "matplotlib", "scipy",