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/.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/.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..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"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 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 --pydocstyle --xdoctest diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 27656c69..58e22e91 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -17,25 +17,25 @@ 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: | 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 . - 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: | - pip install -r test/requirements.txt - coverage run --source=./electricpy -m pytest - coverage-badge -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/.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/.vscode/settings.json b/.vscode/settings.json index 0bad4d33..9d845d51 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" @@ -13,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/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/README.md b/README.md index 82adf6b8..b1fa83ce 100644 --- a/README.md +++ b/README.md @@ -1,42 +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) -![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) ![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/ - -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. +Check out our full documentation: [https://electricpy.readthedocs.io/en/latest/](https://electricpy.readthedocs.io/en/latest/) ## Features @@ -49,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 | - | 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 @@ -90,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 ``` @@ -99,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 @@ -145,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 @@ -167,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/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/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/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..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,19 +26,10 @@ # 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' -copyright = '2022, Joe Stanley' +copyright = '2026, Joe Stanley' author = 'Joe Stanley' # The full version, including alpha/beta/rc tags @@ -56,8 +47,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/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/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/docsource/static/InductionMotorCircleExample.png b/docsource/static/InductionMotorCircleExample.png index 4c2e29a3..a0461854 100644 Binary files a/docsource/static/InductionMotorCircleExample.png and b/docsource/static/InductionMotorCircleExample.png differ diff --git a/docsource/static/PhasorPlot.png b/docsource/static/PhasorPlot.png index a2e9bbbd..72637d18 100644 Binary files a/docsource/static/PhasorPlot.png and b/docsource/static/PhasorPlot.png differ diff --git a/docsource/static/PowerTriangle.png b/docsource/static/PowerTriangle.png index 25912020..d9049764 100644 Binary files a/docsource/static/PowerTriangle.png and b/docsource/static/PowerTriangle.png differ diff --git a/docsource/static/ReceivingEndPowerCircleExample.png b/docsource/static/ReceivingEndPowerCircleExample.png index e9e450f2..049025c9 100644 Binary files a/docsource/static/ReceivingEndPowerCircleExample.png and b/docsource/static/ReceivingEndPowerCircleExample.png differ diff --git a/docsource/static/ReceivingPowerCircleExample.png b/docsource/static/ReceivingPowerCircleExample.png index e9e450f2..049025c9 100644 Binary files a/docsource/static/ReceivingPowerCircleExample.png and b/docsource/static/ReceivingPowerCircleExample.png differ diff --git a/docsource/static/convbar-example.png b/docsource/static/convbar-example.png index 21e20e76..f838c3cc 100644 Binary files a/docsource/static/convbar-example.png and b/docsource/static/convbar-example.png differ diff --git a/docsource/static/series-rlc-r10-l0.5.png b/docsource/static/series-rlc-r10-l0.5.png new file mode 100644 index 00000000..4d0b8246 Binary files /dev/null and b/docsource/static/series-rlc-r10-l0.5.png differ 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 00000000..5995f327 Binary files /dev/null and b/docsource/static/series-rlc-r5-l0.4.png differ diff --git a/electricpy/__init__.py b/electricpy/__init__.py index 04b2022a..cc63ec05 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 @@ -699,38 +698,34 @@ 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) - K = _np.sqrt(1 + 2 * _np.exp(-4 * _np.pi * tau / (X / R))) + # Calculate RMS if no alpha (angle) provided + if alpha is None: + # 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 return Irms, IAC, K - elif 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 +842,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 +982,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 +993,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 +1135,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 +1429,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 +1460,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 +1540,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 +1575,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 +1678,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 +1719,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 +1775,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 +1817,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 +1829,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 +1938,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 +1997,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 +2089,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 +2159,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 +2169,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 @@ -2340,33 +2328,33 @@ 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)) + _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]) @@ -2451,12 +2439,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 +2477,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 +2527,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 +2549,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 +2579,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 +2601,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 +2612,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 +2629,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 +2669,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 +3107,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 +3121,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 +3157,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 +3166,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 +3207,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 +3219,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): @@ -3289,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, @@ -3302,10 +3291,16 @@ 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") -def ic_555_monostable(R=None, C=None, freq=None, t_high=None, t_low=None): +def ic_555_monostable(R=None, C=None, t_high=None, t_low=None): """ 555 Integrated Circuit Calculator. @@ -3317,26 +3312,21 @@ def ic_555_monostable(R=None, C=None, freq=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: float, optional - Electrical system frequency in Hertz. 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( @@ -3351,14 +3341,7 @@ def ic_555_monostable(R=None, C=None, freq=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/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 b2549535..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) @@ -253,6 +254,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 @@ -264,7 +273,13 @@ def string_to_bits(str): The binary representation of the input string. """ - data = (''.join(format(ord(x), 'b') for x in str)) + data = ''.join(format(ord(x), 'b') for x in str) + + # Pad to Nearest Byte + offset = len(data) % 8 + if offset != 0: + data = (8 - offset) * '0' + data + return data # END 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 9b226519..d96890d3 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: @@ -264,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 ed8e9d90..1b8ec15d 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 @@ -132,16 +132,16 @@ 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 """ - 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))) @@ -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 @@ -343,21 +343,26 @@ 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: - return hash((self.center, self.radius)) + """Hash for Circle.""" + 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 39e3accd..1cdc2381 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: @@ -131,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: @@ -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/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 9188d577..2b1b217c 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,9 @@ 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) + 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) @@ -115,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))) @@ -140,7 +143,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 +152,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 +160,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 +200,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 +213,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 +223,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 +255,12 @@ 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) - elif not resample: - 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, dn) + return fixed_fft + if not resample: + return fourier + # 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 65024554..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 @@ -126,7 +125,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. @@ -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. @@ -443,7 +445,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 +469,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 +498,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}} @@ -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 f5b44618..0d201384 100644 --- a/electricpy/sim.py +++ b/electricpy/sim.py @@ -79,7 +79,9 @@ 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) + filter = _np.asarray(filter) flen = len(freqs) for i in range(flen): # Gather frequency @@ -94,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 = 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] + @@ -131,10 +135,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() @@ -194,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): @@ -210,7 +214,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) @@ -218,10 +222,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 @@ -279,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): @@ -295,7 +298,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) @@ -303,10 +306,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 @@ -363,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): @@ -379,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) @@ -387,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 @@ -472,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 @@ -517,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 @@ -545,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 @@ -558,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 @@ -581,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.") @@ -650,58 +663,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)) + 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 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") - for x in range(xtim_len): - _plt.plot(TT, xtim[x], label="x" + str(x + 1)) - if xlim != False: + _ = _plt.figure("State Variables") + for index in range(xtim_len): + _plt.plot(TT, xtim[index], label="x" + str(index + 1)) + 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() @@ -803,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) @@ -935,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 @@ -956,7 +972,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 @@ -984,12 +999,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 @@ -1004,50 +1019,53 @@ 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) + 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] == 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 + if V_list[_k][0] is None: # The Vk magnitude is unknown + 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 - if V_list[_k][1] == None: # The Vj angle is unknown - Vka = "Vx[{}]".format(_k - angoff) # Use Variable Angle + Vkm = f"V_list[{_k}][0]" # Load Magnitude + if V_list[_k][1] is None: # The Vj angle is unknown + 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: 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 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)) - 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) - 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] @@ -1058,10 +1076,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) @@ -1197,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..5fb46167 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__ + """Return the computed efficiency data for the induction motor circle.""" 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 2ca60057..147766d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,16 +24,22 @@ 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"] dependencies = [ - "NumPy", + "numpy", "matplotlib", - "SciPy", - "SymPy", + "scipy", + "sympy", "numdifftools", ] @@ -41,6 +47,38 @@ dependencies = [ numerical = ["numdifftools"] fault = ["arcflash"] full = ["numdifftools", "arcflash"] +dev = [ + "pytest >=2.7.3,<9", + "xdoctest >= 1.1.3", + "pytest-pydocstyle >= 2.3.2", + "pygments >= 2.18.0", + "wheel", + "sphinx", + "myst-parser[linkify]", + "sphinx-sitemap", + "sphinx-git", + "sphinx-immaterial", + "genbadge[coverage]", + "coverage", + "numpy", + "matplotlib", + "scipy", + "sympy", + "numdifftools", + "pylint", + "ruff", +] + +[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" 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/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/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 7fbe89ec..b1086255 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,9 +1,11 @@ +"""Shared test helper utilities.""" + 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: + """Return True when two points are approximately equal.""" try: assert_almost_equal(p1.x, p2.x) assert_almost_equal(p1.y, p2.y) @@ -12,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) @@ -40,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/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 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 1364baed..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) @@ -27,6 +31,18 @@ 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): + """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 4ecfafe3..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,15 +8,28 @@ 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) + """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], + freqs=[1], + NN=10, + dt=0.1, + legend=False + ) assert plot_module is not None 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() - -