diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ec4e046 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment: ** + - OS: [e.g. iOS] + - Python3 version: [e.g. 3.9, 3.11] + - Release [e.g. 1.0.12] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..ad9c270 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,24 @@ +name: Pylint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + #pylint $(git ls-files '*.py') + pylint ./py_ballisticcalc diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..c71d191 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,56 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package tests + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install build-essential -y + + python -m pip install --upgrade pip + python -m pip install setuptools pytest cython + + - name: Run unittest tests in pure python mode + run: | + if pytest tests --no-header --no-summary -v; then + echo "Pytest succeeded." + else + echo "Pytest failed, running without capture" + # Add your additional commands here + pytest tests -v + fi + + - name: Build cython modules + run: | + cd py_ballisticcalc_exts + python setup.py build_ext --inplace + cd .. + + - name: Run unittest tests in binary mode + run: | + if pytest tests --no-header --no-summary -v; then + echo "Pytest succeeded." + else + echo "Pytest failed, running without capture" + # Add your additional commands here + pytest tests --no-header --no-summary -v -s + fi diff --git a/.github/workflows/python-publish-test.yml b/.github/workflows/python-publish-test.yml new file mode 100644 index 0000000..05b40a8 --- /dev/null +++ b/.github/workflows/python-publish-test.yml @@ -0,0 +1,48 @@ +name: Upload Python Package to Test PyPI + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build package + run: python -m build + + - name: Build extensions package + run: | + cd py_ballisticcalc_exts + python -m build --outdir ../dist + cd .. + + - name: Publish package to Test PyPI + run: | + python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* --skip-existing --verbose --non-interactive + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: python-package-artifacts + path: dist/ diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..7844115 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,59 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build package + run: python -m build + + - name: Build extensions package + run: | + cd py_ballisticcalc_exts + python -m build --outdir ../dist + cd .. + + - name: Publish package to PyPI + run: | + python -m twine upload dist/* --skip-existing --verbose --non-interactive + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: python-package-artifacts + path: dist/ diff --git a/.gitignore b/.gitignore index 6cfc085..1a9a43c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,8 @@ -UsageDemo - Copy.ipynb -.vs/ProjectSettings.json -.vs/slnx.sqlite -.vs/VSWorkspaceState.json -.vs/py_ballistics/v15/.suo -.vscode/settings.json -py_ballisticcalc/atmosphere.c -py_ballisticcalc/drag.c -py_ballisticcalc/ExampleDetailed.py -py_ballisticcalc/Examples.ipynb -py_ballisticcalc/ExampleSimple.py -py_ballisticcalc/multiple_bc.c -py_ballisticcalc/projectile.c -py_ballisticcalc/shot_parameters.c -py_ballisticcalc/trajectory_calculator.c -py_ballisticcalc/trajectory_data.c -py_ballisticcalc/wind.c -py_ballisticcalc/__pycache__/__init__.cpython-310.pyc -py_ballisticcalc/__pycache__/drag_tables.cpython-310.pyc -py_ballisticcalc/__pycache__/interface.cpython-310.pyc -py_ballisticcalc/bmath/__pycache__/__init__.cpython-310.pyc -py_ballisticcalc/bmath/unit/angular.c -py_ballisticcalc/bmath/unit/distance.c -py_ballisticcalc/bmath/unit/energy.c -py_ballisticcalc/bmath/unit/pressure.c -py_ballisticcalc/bmath/unit/temperature.c -py_ballisticcalc/bmath/unit/velocity.c -py_ballisticcalc/bmath/unit/velocityNew.py -py_ballisticcalc/bmath/unit/weight.c -py_ballisticcalc/bmath/unit/__pycache__/__init__.cpython-310.pyc -py_ballisticcalc/bmath/unit/__pycache__/velocity.cpython-310.pyc -py_ballisticcalc/bmath/vector/vector.c -py_ballisticcalc/bmath/vector/__pycache__/__init__.cpython-310.pyc +**/__pycache__ +**/.idea +**/*.c +**/*.pyd +**/build +**/dist +**/*.egg-info +**/.venv diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..fb2b72a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,634 @@ +[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=9.5 + +# 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, setup.py, tests.py + +# 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 +# (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 + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# 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= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# 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=^_ + +# 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= + +# 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=5 + +# 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 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=2 + + +[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. +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 + +# 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] + +# 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 + + +[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) and msvs (visual +# studio). 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 minimum 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/Example.ipynb b/Example.ipynb new file mode 100644 index 0000000..91d641c --- /dev/null +++ b/Example.ipynb @@ -0,0 +1,684 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:py_balcalc:Library running in pure python mode. For better performance install 'py_ballisticcalc.exts' package\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Default units:\n", + "angular = degree\n", + "distance = yard\n", + "velocity = fps\n", + "pressure = inhg\n", + "temperature = fahrenheit\n", + "diameter = inch\n", + "length = inch\n", + "weight = grain\n", + "adjustment = mil\n", + "drop = inch\n", + "energy = foot-pound\n", + "ogw = pound\n", + "sight_height = inch\n", + "target_height = inch\n", + "twist = inch\n" + ] + } + ], + "source": [ + "# Uncomment pyximport to compile instead of running pure python\n", + "#import pyximport; pyximport.install(language_level=3)\n", + "\n", + "import copy\n", + "from matplotlib import pyplot as plt\n", + "from py_ballisticcalc import DragModel, TableG7, TableG1\n", + "from py_ballisticcalc import Ammo, Atmo, Wind\n", + "from py_ballisticcalc import Weapon, Shot, Calculator\n", + "from py_ballisticcalc import Settings as Set\n", + "from py_ballisticcalc.unit import *\n", + "\n", + "print(\"Default units:\\n\"+str(Set.Units)) # Print default units" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simple Zero" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Barrel elevation for 100.0yd zero: 1.33mil\n" + ] + } + ], + "source": [ + "# Establish 100-yard zero for a standard .308, G7 BC=0.22, muzzle velocity 2600fps\n", + "zero = Shot(weapon=Weapon(sight_height=2), ammo=Ammo(DragModel(0.22, TableG7), mv=Velocity.FPS(2600)))\n", + "calc = Calculator()\n", + "zero_distance = Distance.Yard(100)\n", + "zero_elevation = calc.set_weapon_zero(zero, zero_distance)\n", + "print(f'Barrel elevation for {zero_distance} zero: {zero_elevation << Set.Units.adjustment}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plot Trajectory with Danger Space" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Danger space at 300.0yd for 19.7inch tall target ranges from 217.1yd to 355.7yd\n" + ] + }, + { + "data": { + "image/png": "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ORSUFVBZU4ndbm/obgshhDgLO3bs4LrrrgMOz8c7dCnTarVqizPeeustZs+eTefOnfn000+prq7m3XffZffu3cTFxRESEkKXLl0YPXo0EyZMoLi4mIyMDPR6PZ6enpSVlQFgNptJS0ujoKCgXl5f+/bt8ff317Znn332lM8pLS0FoEWLFi73z5s3j+DgYDp27MjMmTOpqqrSHlu1ahWdOnUiLCxMuy8lJYWysjK2bdumtRkyZIjLMVNSUli1atUZv76zJZd6BeD84zcbzZiNZvxUP6x2q/NysLUGm81GrbWWWmstpZRiNBjxMDkXhxj08iskhBCNld1u1+biHZKYmMh///tfwHnJ9ptvvqGoqIj58+ezdetWnn76aa6//nrGjh2Lt7c3L774Iu3atQOgR48erFmzhsTERLp3766loQoICCAiIoJ169aRkpLCc889x7JlyxgzZgxZWVkUFhZy4MAB2rVrh8Hg3s+N7du3ExUVpd02m80nbe9wOLjnnnvo27cvHTt21O6//vrradWqFZGRkWzevJkHHniA1NRUbRQ0JyfHJegDtNs5OTknbVNWVkZ1dbWW0aE+yYifOIaiKOAAPy8/Qv1DCQ0Ixc/LT8sFaLVZKa8qJ68kj7ySPMqqyqi11criECGEaCAOh0O7ZHskvV7vnMqjqtolT71ez5QpU/j111/R6XQ89thj/PDDD7zzzjvMmTOH+fPns2TJEsaPH09UVBQ7d+7UjtepUydWrlwJQHJysjZy5eXlRZs2bVi2bBk9e/bkmmuu4dlnn6V9+/Zs3ryZkJAQFi1axOuvv+72n4Wvry9+fn7adqrAb+rUqWzdupXPPvvM5f7JkyeTkpJCp06dGDduHB9++CHffvst6enp7uy+20ngdxZUVaWq1tYg2+kGWZWVlYwfPx4fHx8iIiL4z3/+4/J469ateeqppxg/fjx+fn5MnjwZgK+//pounbsQFBBEcpdk5r0/D39vf8xGMyjQo3MPnnrqKa6++mq8fbyJjIzktddfkyBQCCHq0aHVtUdnB/j888/p378/7dq149///jebN29GURSqqqq0AK537974+/sTHR1N9+7dadOmDatXr8bX1xdfX192796tHa9Hjx7aZcwuXbrw559/As5Rtfj4eGpra0lPTyc/P59Jkyaxfft2Xn75ZUpLS4mMjKSsrIyKiop6+qmc2rRp0/jxxx/57bffiI6OPmnb3r17A5CWlgZAeHj4MfMWD90ODw8/aRs/P78GGe0DudR7Vqqtdto/+kuDnHv7kyl4mer+3zdjxgx+//135s+fT2hoKA899BDr16+na9euWpuXXnqJRx99lMceewyAdevWcfXVV/P4449zzTXXsHLlSu644w5CQkKYMGGCc4WWouPtt97mznvuZPoD0/n9t9+Zft90wqPDSRmaoq0QllQlQghx+g5VvRg+fDhTpkxh9OjRx6yOzc/PZ9OmTXz88cfodDpuu+02evfuza5du/j+++/517/+xahRo3jwwQd58MEH+emnnxg4cCCLFi0CoG/fvsybNw9wJpNPSEjQRgNjYmLYuHEjeXl55OXl0alTJ20BRK9evejUqROzZs1i+PDhXHnllVx55ZVkZmaSl5fHokWLeOutt7BarUyfPp2RI0fi7e3tsmq2oaiqyp133sm3337LsmXLiI2NPeVzNm7cCByurtSnTx/+7//+T0u8D7B48WL8/Pxo37691uann35yOc7ixYvp06fPOXw1p6fJjPjVJedOTU0NU6dOJSgoCB8fH6644op6W0XUmFVUVPDee+/x0ksvMXjwYDp16sQHH3xwTK6hQYMGMX36dOLj44mPj+fll19m8ODBPPLII7Rp04YJEyYwbdo0XnzxRcD5DVNRFC7qexFPP/E0yd2SmTptKiMvG8m7s9+lylJFUXkROcU5skJYCCHq4NDCikMO7fv6+vLrr7+63AfOHHHXXXcds2bN4pJLLqFt27Y8+OCD7Nmzh9zcXNatW8eoUaMAZ2qS8vJyDh48SI8ePbRLlj179iQ7OxtwjtzFxcWxc+dOrFYr48aNIz09nWHDhvHGG2+wYcMG/vjjDwCioqK48cYbycvL4+DBg1r/Q0JCuO6663j99ddZtmwZW7du5eabbyY0NLRRBH3gvLz78ccf88knn+Dr60tOTg45OTlUV1cDkJ6ezlNPPcW6devIyMjg+++/Z/z48fTr14/OnTsDMHToUNq3b8+NN97Ipk2b+OWXX/j3v//N1KlTtcvLU6ZMYc+ePdx///3s3LmTWbNm8cUXX3Dvvfc22GtvMiN+dcm5c++997JgwQK+/PJL/P39mTZtGmPHjmXFihVu6ZOnUc/2JxsmD4+nse6VHdLT06mtrdWGqcG5cikpKcmlXc+ePV1u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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot trajectory out to 500 yards\n", + "shot_result = calc.fire(zero, trajectory_range=500, extra_data=True)\n", + "ax = shot_result.plot()\n", + "# Find danger space for a half-meter tall target at 300 yards\n", + "danger_space = shot_result.danger_space(Distance.Yard(300), Distance.Meter(.5))\n", + "print(danger_space)\n", + "danger_space.overlay(ax) # Highlight danger space on the plot\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Print Range Card" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timedistancevelocitymachdropdrop_adjwindagewindage_adjangleenergyogwflag
00.00 s0.0 yd2600.0 ft/s2.33 mach-2.0 inch0.00 mil-0.0 inch0.00 mil0.0750 °0 ft·lb0 lb8
10.12 s100.0 yd2398.1 ft/s2.15 mach-0.0 inch-0.00 mil0.4 inch0.12 mil-0.0137 °0 ft·lb0 lb10
20.25 s200.0 yd2205.4 ft/s1.98 mach-4.1 inch-0.57 mil1.7 inch0.25 mil-0.1184 °0 ft·lb0 lb8
30.39 s300.0 yd2022.2 ft/s1.81 mach-15.3 inch-1.44 mil4.1 inch0.39 mil-0.2425 °0 ft·lb0 lb8
40.55 s400.0 yd1847.5 ft/s1.65 mach-35.0 inch-2.48 mil7.6 inch0.54 mil-0.3906 °0 ft·lb0 lb8
50.72 s500.0 yd1680.1 ft/s1.50 mach-65.0 inch-3.68 mil12.4 inch0.70 mil-0.5688 °0 ft·lb0 lb8
60.91 s600.0 yd1519.5 ft/s1.36 mach-107.3 inch-5.06 mil18.8 inch0.89 mil-0.7856 °0 ft·lb0 lb8
71.11 s700.0 yd1366.1 ft/s1.22 mach-164.8 inch-6.66 mil27.0 inch1.09 mil-1.0523 °0 ft·lb0 lb8
81.35 s800.0 yd1221.3 ft/s1.09 mach-240.9 inch-8.52 mil37.3 inch1.32 mil-1.3842 °0 ft·lb0 lb8
91.61 s900.0 yd1093.3 ft/s0.98 mach-340.5 inch-10.70 mil50.0 inch1.57 mil-1.8005 °0 ft·lb0 lb8
101.89 s1000.0 yd1030.7 ft/s0.92 mach-468.9 inch-13.27 mil64.8 inch1.83 mil-2.2949 °0 ft·lb0 lb8
\n", + "
" + ], + "text/plain": [ + " time distance velocity mach drop drop_adj \\\n", + "0 0.00 s 0.0 yd 2600.0 ft/s 2.33 mach -2.0 inch 0.00 mil \n", + "1 0.12 s 100.0 yd 2398.1 ft/s 2.15 mach -0.0 inch -0.00 mil \n", + "2 0.25 s 200.0 yd 2205.4 ft/s 1.98 mach -4.1 inch -0.57 mil \n", + "3 0.39 s 300.0 yd 2022.2 ft/s 1.81 mach -15.3 inch -1.44 mil \n", + "4 0.55 s 400.0 yd 1847.5 ft/s 1.65 mach -35.0 inch -2.48 mil \n", + "5 0.72 s 500.0 yd 1680.1 ft/s 1.50 mach -65.0 inch -3.68 mil \n", + "6 0.91 s 600.0 yd 1519.5 ft/s 1.36 mach -107.3 inch -5.06 mil \n", + "7 1.11 s 700.0 yd 1366.1 ft/s 1.22 mach -164.8 inch -6.66 mil \n", + "8 1.35 s 800.0 yd 1221.3 ft/s 1.09 mach -240.9 inch -8.52 mil \n", + "9 1.61 s 900.0 yd 1093.3 ft/s 0.98 mach -340.5 inch -10.70 mil \n", + "10 1.89 s 1000.0 yd 1030.7 ft/s 0.92 mach -468.9 inch -13.27 mil \n", + "\n", + " windage windage_adj angle energy ogw flag \n", + "0 -0.0 inch 0.00 mil 0.0750 ° 0 ft·lb 0 lb 8 \n", + "1 0.4 inch 0.12 mil -0.0137 ° 0 ft·lb 0 lb 10 \n", + "2 1.7 inch 0.25 mil -0.1184 ° 0 ft·lb 0 lb 8 \n", + "3 4.1 inch 0.39 mil -0.2425 ° 0 ft·lb 0 lb 8 \n", + "4 7.6 inch 0.54 mil -0.3906 ° 0 ft·lb 0 lb 8 \n", + "5 12.4 inch 0.70 mil -0.5688 ° 0 ft·lb 0 lb 8 \n", + "6 18.8 inch 0.89 mil -0.7856 ° 0 ft·lb 0 lb 8 \n", + "7 27.0 inch 1.09 mil -1.0523 ° 0 ft·lb 0 lb 8 \n", + "8 37.3 inch 1.32 mil -1.3842 ° 0 ft·lb 0 lb 8 \n", + "9 50.0 inch 1.57 mil -1.8005 ° 0 ft·lb 0 lb 8 \n", + "10 64.8 inch 1.83 mil -2.2949 ° 0 ft·lb 0 lb 8 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Range card for this zero with 5mph cross-wind from left to right\n", + "zero.winds = [Wind(Velocity.MPH(5), Angular.OClock(3))]\n", + "range_card = calc.fire(zero, trajectory_range=1000)\n", + "# for p in range_card: print(p.formatted())\n", + "range_card.dataframe().to_clipboard()\n", + "range_card.dataframe(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Firing Solutions\n", + "\n", + "## Different Distance, from Range Card\n", + "\n", + "First approach here shows getting firing solution by looking up the adjustment in the Range Card:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Zero trajectory hits -65.0inch at 500.0yd\n", + "Sight adjustment to hit 500.0yd target is -3.68mil\n", + "After adjustment: drop at 500.0yd is -0.0inch\n" + ] + } + ], + "source": [ + "# Now shooter is sighting a target at look-distance 500 yard (zero.look-angle):\n", + "new_target_distance = Distance.Yard(500)\n", + "# Get row for this distance from the range card\n", + "new_target = range_card.get_at_distance(new_target_distance)\n", + "print(f'Zero trajectory hits {new_target.drop << Set.Units.drop}'\n", + " f' at {(new_target.distance << Set.Units.distance)}')\n", + "\n", + "# Shooter looks up adjustment to hit new target:\n", + "hold = new_target.drop_adj # << Firing solution\n", + "\n", + "print(f'Sight adjustment to hit {(new_target.distance << Set.Units.distance)} target'\n", + " f' is {(hold << Set.Units.adjustment)}')\n", + "# Shooter dials that hold value for a 500-yard shot. Verification:\n", + "range_card.shot.relative_angle = Angular(-hold.unit_value, hold.units)\n", + "adjusted_result = calc.fire(range_card.shot, trajectory_range=1000)\n", + "trajectory_adjusted = adjusted_result.get_at_distance(new_target_distance)\n", + "print(f'After adjustment: drop at {trajectory_adjusted.distance << Set.Units.distance}'\n", + " f' is {trajectory_adjusted.drop << Set.Units.drop}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Different Look Angle\n", + "\n", + "Second approach here shows solving for barrel elevation to hit new target, then adjusting by the difference between that and the zero barrel elevation:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "To hit target at look-distance of 500.0yd sighted at a 30.0° look-angle, barrel elevation=4.39mil\n", + "Current zero has barrel elevated 1.33mil so hold for new shot is 3.05mil\n" + ] + }, + { + "data": { + "image/png": "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k5ODPXv2IDg4GNu3b0dERASioqJw4sQJNG7cGKtXr5YwYiIi/ZIJIYTUQRARGYJ33nkHv/32G/bu3QsAWL58OWJjY3H58mX06NEDnp6eiI2N1dTv3r07SkpKkJqaKlHERET6w5FGIqI/TJ48Gfv378eNGzcAABs2bMDbb78NmUyGX3/9FT4+Plr1fX19pQiTiEgScqkDICIyFJ6ennB3d8emTZsQEBCAtLQ07NmzR+qwiIgMAkcaiYgqmTRpEjZs2ID169fD398fzZs3BwB07NgRR48e1ap75MgRKUIkIpIEk0Yiokreeust/P777/jiiy8QHBys2T9jxgysW7cO69evx8WLFxEeHo60tDQJIyUi0i9OhCEiesL48eOxZ88e3Lx5EwqFQrM/KioKn3zyCUpKSjBixAgolUr8+OOPnAhDRC8FJo1ERE/o168fOnXqhJUrV0odChGRwWDSSET0h/v37yMxMREjR47E+fPn0b59e6lDIiIyGJw9TUT0B09PT9y/fx///Oc/mTASET2BI41EREREVCPOniYiIiKiGjFpJCIiIqIaMWkkIiIiohoxaSQiIiKiGjFpJCIiIqIaMWkkIiIiohoxaSQiIiKiGjFpJCIiIqIa/T+YMH4eyfXNGAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(Horizontal component of distance to this target is 433.0yd)\n" + ] + } + ], + "source": [ + "# Calculate elevation for a new shot at a different look-angle\n", + "new_shot = copy.copy(zero) # Copy the zero properties; NB: Not a deepcopy!\n", + "new_shot.look_angle = Angular.Degree(30)\n", + "new_elevation = calc.barrel_elevation_for_target(shot=new_shot, target_distance=new_target_distance)\n", + "# Firing solution:\n", + "hold = Angular.Mil((new_elevation >> Angular.Mil) - (zero.weapon.zero_elevation >> Angular.Mil))\n", + "\n", + "print(f'To hit target at look-distance of {new_target_distance << Set.Units.distance}'\n", + " f' sighted at a {new_shot.look_angle << Set.Units.angular} look-angle,' \n", + " f' barrel elevation={new_elevation << Set.Units.adjustment}')\n", + "print(f'Current zero has barrel elevated {zero.weapon.zero_elevation << Set.Units.adjustment}'\n", + " f' so hold for new shot is {hold << Set.Units.adjustment}') \n", + "\n", + "# Plot this shot\n", + "new_shot.relative_angle = hold\n", + "adjusted_result = calc.fire(new_shot, trajectory_range=500, extra_data=True)\n", + "adjusted_result.plot()\n", + "plt.show()\n", + "\n", + "from math import cos\n", + "horizontal = Distance(cos(new_shot.look_angle >> Angular.Radian)\n", + " * new_target_distance.unit_value, new_target_distance.units)\n", + "print(f'(Horizontal component of distance to this target is {horizontal})')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Change Defaults; Setup New Gun\n", + "\n", + "Now we'll switch to metric units and setup a standard .50BMG, zeroed for a distance of 500 meters, in a 5°C atmosphere at altitude 1000ft ASL." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Barrel elevation for 500.0m zero: 4.45mil\n" + ] + } + ], + "source": [ + "Set.Units.distance = Unit.METER\n", + "Set.Units.velocity = Unit.MPS\n", + "Set.Units.drop = Unit.CENTIMETER\n", + "Set.Units.sight_height = Unit.CENTIMETER\n", + "\n", + "# Standard .50BMG\n", + "dm = DragModel(0.62, TableG1, 661, 0.51)\n", + "ammo=Ammo(dm, 2.3, 850)\n", + "weapon = Weapon(sight_height=9, twist=15)\n", + "# Cool and windy\n", + "atmo = Atmo(altitude=Distance.Foot(1000), temperature=Unit.CELSIUS(5), humidity=.5)\n", + "zero = Shot(weapon=weapon, ammo=ammo, atmo=atmo)\n", + "zero_distance = Distance.Meter(500)\n", + "calc = Calculator()\n", + "zero_elevation = calc.set_weapon_zero(zero, zero_distance)\n", + "print(f'Barrel elevation for {zero_distance} zero: {zero_elevation << Set.Units.adjustment}')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "To hit target at look-distance of 700.0m sighted at a 30.0° look-angle, barrel elevation=5.86mil\n", + "Current zero has barrel elevated 4.45mil so hold for new shot is 1.41mil\n", + "Danger space at 700.0m for 300.0cm tall target at 30.0° look-angle ranges from 600.5m to 769.9m\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Firing solution for 700m target at 30°\n", + "new_shot = copy.copy(zero) # Copy the zero properties; NB: Not a deepcopy!\n", + "new_shot.look_angle = Angular.Degree(30)\n", + "new_target_distance = Distance.Meter(700)\n", + "new_elevation = calc.barrel_elevation_for_target(shot=new_shot, target_distance=new_target_distance)\n", + "print(f'To hit target at look-distance of {new_target_distance << Set.Units.distance}'\n", + " f' sighted at a {new_shot.look_angle << Set.Units.angular} look-angle,' \n", + " f' barrel elevation={new_elevation << Set.Units.adjustment}')\n", + "\n", + "# Firing solution:\n", + "hold = Angular.Mil((new_elevation >> Angular.Mil) - (zero.weapon.zero_elevation >> Angular.Mil))\n", + "print(f'Current zero has barrel elevated {zero.weapon.zero_elevation << Set.Units.adjustment}'\n", + " f' so hold for new shot is {hold << Set.Units.adjustment}') \n", + "\n", + "# Plot this shot\n", + "new_shot.relative_angle = hold\n", + "adjusted_result = calc.fire(new_shot, trajectory_range=3000, extra_data=True)\n", + "ax = adjusted_result.plot()\n", + "# Find danger space for a 3-meter tall target\n", + "danger_space = adjusted_result.danger_space(at_range=new_target_distance,\n", + " target_height=Distance.Meter(3),\n", + " look_angle=new_shot.look_angle)\n", + "print(danger_space)\n", + "# Highlight danger space on the plot\n", + "danger_space.overlay(ax, 'Danger Range\\n(3m Target)')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Units" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Default distance unit: FOOT\n", + "\tInstantiated from float (5): 5.0ft\n", + "\tInstantiated from Distance.Line(200): 1.67ft\n", + "100 meters: 100.0m\n", + "100 meters in yard: 109.4yd\n", + "100 meters, value in km: 0.1 (value type is )\n", + "100 meters in raw value: 3937.0078740157483 (raw type is )\n", + "Comparison: 100.0m == 100.0cm: False\n", + "Comparison: 100.0m > .1*100.0m: True\n" + ] + } + ], + "source": [ + "Set.Units.distance = Unit.FOOT\n", + "print(f'Default distance unit: {Set.Units.distance.name}')\n", + "# Can create value in default unit with either float or another unit of same type\n", + "print(f'\\tInstantiated from float (5): {Set.Units.distance(5)}')\n", + "print(f'\\tInstantiated from Distance.Line(200): {Set.Units.distance(Distance.Line(200))}')\n", + "\n", + "# Ways to define value in units\n", + "# 1. old syntax\n", + "unit_in_meter = Distance(100, Distance.Meter)\n", + "# 2. short syntax by Unit type class\n", + "unit_in_meter = Distance.Meter(100)\n", + "# 3. by Unit enum class\n", + "unit_in_meter = Unit.METER(100)\n", + "print(f'100 meters: {unit_in_meter}')\n", + "# >>> 100 meters: 100.0m\n", + "\n", + "# Convert unit\n", + "# 1. by .convert()\n", + "unit_in_yards = unit_in_meter.convert(Distance.Yard)\n", + "# 2. using shift syntax\n", + "unit_in_yards = unit_in_meter << Distance.Yard # '<<=' operator also supports\n", + "print(f'100 meters in {unit_in_yards.units.key}: {unit_in_yards}')\n", + "# >>> 100 meters in yard: 109.4yd\n", + "\n", + "# Get value in specified units (as float)\n", + "# 1. by .get_in()\n", + "value_in_km = unit_in_yards.get_in(Distance.Kilometer)\n", + "# 2. by shift syntax\n", + "value_in_km = unit_in_yards >> Distance.Kilometer # '>>=' operator also supports\n", + "print(f'100 meters, value in km: {value_in_km} (value type is {type(value_in_km)})')\n", + "# >>> 100 meters, value in km: 0.1 (value type is )\n", + "\n", + "# Getting unit raw value (a float)\n", + "rvalue = Distance.Meter(100).raw_value\n", + "rvalue = float(Distance.Meter(100))\n", + "print(f'100 meters in raw value: {rvalue} (raw type is {type(rvalue)})')\n", + "# >>> 100 meters in raw value: 3937.0078740157483 (raw type is )\n", + "\n", + "# Comparison operators supported: < > <= >= == !=\n", + "print(f'Comparison: {unit_in_meter} == {Distance.Centimeter(100)}: {unit_in_meter == Distance.Centimeter(100)}') # >>> False, compare two units by raw value\n", + "print(f'Comparison: {unit_in_meter} > .1*{unit_in_meter}: {unit_in_meter > .1*unit_in_meter.raw_value}') # >>> True, compare unit with float by raw value" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Manifest.in b/Manifest.in index ffbe40b..a01f4e7 100644 --- a/Manifest.in +++ b/Manifest.in @@ -1,3 +1,3 @@ -recursive-include py_ballisticcalc *.pyx -include requirements*.txt -include LICENSE \ No newline at end of file +prune py_ballisticcalc_exts +prune tests +include py.typed diff --git a/README.md b/README.md index d4d92c4..678ea71 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,16 @@ Installation ------------ ```python setup.py build_ext --inplace``` +## Usage Usage ------------ -See ```UsageDemo.ipynb``` +See ```Example.ipynb``` Info ----- -3 Degree-of-Freedom (3DOF) ballistic calculator. This version refactored, corrected, and enhanced by David Bookstaber +3 Degree-of-Freedom (3DOF+) + spin-drift ballistic calculator. This version refactored, corrected, and enhanced by David Bookstaber from version ported to Cython by o-murphy, from versions by Nikolay Gekht: * The online version of Go documentation is located here: https://godoc.org/github.com/gehtsoft-usa/go_ballisticcalc diff --git a/example.py b/example.py new file mode 100644 index 0000000..31ca691 --- /dev/null +++ b/example.py @@ -0,0 +1,30 @@ +"""Example of library usage""" + +from py_ballisticcalc import * +from py_ballisticcalc import Settings as Set + +# Modify default units +Set.Units.velocity = Velocity.FPS +Set.Units.temperature = Temperature.Celsius +Set.Units.distance = Distance.Meter +Set.Units.sight_height = Distance.Centimeter + +Set.USE_POWDER_SENSITIVITY = True # enable muzzle velocity correction my powder temperature + +# Define ammunition parameters +weight, diameter = 168, 0.308 # Numbers will be assumed to use default Settings.Units +length = Distance.Inch(1.282) # Or declare units explicitly +dm = DragModel(0.223, TableG7, weight, diameter) +ammo = Ammo(dm, length, 2750, 15) +ammo.calc_powder_sens(2723, 0) +gun = Weapon(sight_height=9, twist=12) +current_atmo = Atmo(110, 1000, 15, 72) +current_winds = [Wind(2, 90)] +shot = Shot(weapon=gun, ammo=ammo, atmo=current_atmo, winds=current_winds) +calc = Calculator() +calc.set_weapon_zero(shot, Distance.Meter(100)) + +shot_result = calc.fire(shot, trajectory_range=1000, trajectory_step=100) + +for p in shot_result: + print(p.formatted()) diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/py_ballisticcalc/__init__.py b/py_ballisticcalc/__init__.py index 152e774..31c0a5e 100644 --- a/py_ballisticcalc/__init__.py +++ b/py_ballisticcalc/__init__.py @@ -1,3 +1,16 @@ -__all__ = ['interface', 'atmosphere', 'bmath', 'drag', 'drag_tables', 'projectile', 'shot_parameters', - 'trajectory_calculator', 'trajectory_data', 'wind', 'multiple_bc'] -__version__ = "1.1.2" +__author__ = "dbookstaber" +__copyright__ = ("",) + +__credits__ = ["o-murphy"] +__version__ = "1.2" + + +from .backend import * +from .drag_tables import * +from .settings import * +from .multiple_bc import * +from .interface import * +from .trajectory_data import * +from .conditions import * +from .munition import * +from .unit import * diff --git a/py_ballisticcalc/atmosphere.pyx b/py_ballisticcalc/atmosphere.pyx deleted file mode 100644 index c8edb65..0000000 --- a/py_ballisticcalc/atmosphere.pyx +++ /dev/null @@ -1,139 +0,0 @@ -from libc.math cimport pow, sqrt, fabs -from .bmath.unit import * - - -cIcaoStandardTemperatureR: double = 518.67 -cIcaoFreezingPointTemperatureR: double = 459.67 -cTemperatureGradient: double = -3.56616e-03 -cIcaoStandardHumidity: double = 0.0 -cPressureExponent: double = -5.255876 -cSpeedOfSound: double = 49.0223 -cA0: double = 1.24871 -cA1: double = 0.0988438 -cA2: double = 0.00152907 -cA3: double = -3.07031e-06 -cA4: double = 4.21329e-07 -cA5: double = 3.342e-04 -cStandardTemperature: double = 59.0 -cStandardPressure: double = 29.92 -cStandardDensity: double = 0.076474 - - -cdef class Atmosphere: - cdef double _density, _humidity, _mach1 - cdef _mach - cdef _altitude - cdef _pressure - cdef _temperature - - def __init__(self, altitude: Distance, pressure: Pressure, temperature: Temperature, humidity: double): - - if humidity > 1: - humidity = humidity / 100 - - if humidity < 0 or humidity > 100: - self.create_default() - elif not altitude or not pressure or not temperature: - self.create_default() - else: - self._altitude = altitude - self._pressure = pressure - self._temperature = temperature - self._humidity = humidity - - self.calculate() - - def __str__(self) -> str: - return f'Altitude: {self._altitude}, Pressure: {self._pressure}, ' \ - f'Temperature: {self._temperature}, Humidity: {self.humidity_in_percent:.2f}' - - cdef string(self): - return self.__str__() - - cpdef create_default(self): - self._altitude = Distance(0.0, DistanceFoot) - self._pressure = Pressure(cStandardPressure, PressureInHg) - self._temperature = Temperature(cStandardTemperature, TemperatureFahrenheit) - self._humidity = 0.78 - - cpdef altitude(self): - return self._altitude - - cpdef temperature(self): - return self._temperature - - cpdef pressure(self): - return self._pressure - - cpdef double humidity(self): - return self._humidity - - cpdef double humidity_in_percent(self): - return self._humidity * 100 - - cpdef double density(self): - return self._density - - cpdef double density_factor(self): - return self._density / cStandardDensity - - cpdef mach(self): - return self._mach - - cdef (double, double) calculate0(self, double t, double p): - cdef double et0, et, hc, density, mach - - if t > 0: - et0 = cA0 + t * (cA1 + t * (cA2 + t * (cA3 + t * cA4))) - et = cA5 * self._humidity * et0 - hc = (p - 0.3783 * et) / cStandardPressure - else: - hc = 1.0 - - density = cStandardDensity * (cIcaoStandardTemperatureR / (t + cIcaoFreezingPointTemperatureR)) * hc - mach = sqrt(t + cIcaoFreezingPointTemperatureR) * cSpeedOfSound - return density, mach - - cdef calculate(self): - cdef double density, mach, mach1, t, p - t = self._temperature.get_in(TemperatureFahrenheit) - p = self._pressure.get_in(PressureInHg) - density, mach = self.calculate0(t, p) - self._density = density - self._mach1 = mach - self._mach = Velocity(mach, VelocityFPS) - - cpdef (double, double) get_density_factor_and_mach_for_altitude(self, altitude: double): - cdef double density, mach, t0, p, ta, tb, t, org_altitude - org_altitude = self._altitude.get_in(DistanceFoot) - if fabs(org_altitude - altitude) < 30: - density = self._density / cStandardDensity - mach = self._mach1 - return density, mach - - t0 = self._temperature.get_in(TemperatureFahrenheit) - p = self._pressure.get_in(PressureInHg) - - ta = cIcaoStandardTemperatureR + org_altitude * cTemperatureGradient - cIcaoFreezingPointTemperatureR - tb = cIcaoStandardTemperatureR + altitude * cTemperatureGradient - cIcaoFreezingPointTemperatureR - t = t0 + ta - tb - p = p * pow(t0 / t, cPressureExponent) - - density, mach = self.calculate0(t, p) - return density / cStandardDensity, mach - - -cpdef IcaoAtmosphere(altitude: Distance): - cdef temperature, pressure - temperature = Temperature( - cIcaoStandardTemperatureR + altitude.get_in(DistanceFoot) - * cTemperatureGradient - cIcaoFreezingPointTemperatureR, TemperatureFahrenheit) - - pressure = Pressure( - cStandardPressure * - pow(cIcaoStandardTemperatureR / ( - temperature.get_in(TemperatureFahrenheit) + cIcaoFreezingPointTemperatureR), - cPressureExponent), - PressureInHg) - - return Atmosphere(altitude, pressure, temperature, cIcaoStandardHumidity) diff --git a/py_ballisticcalc/backend.py b/py_ballisticcalc/backend.py new file mode 100644 index 0000000..a94c74a --- /dev/null +++ b/py_ballisticcalc/backend.py @@ -0,0 +1,15 @@ +"""Searching for an available backends""" + +from .logger import logger + +# trying to use cython based backend +try: + from py_ballisticcalc_exts import * # pylint: disable=wildcard-import + + logger.info("Binary modules found, running in binary mode") +except ImportError as error: + from .drag_model import * + from .trajectory_calc import * + + logger.warning("Library running in pure python mode. " + "For better performance install 'py_ballisticcalc.exts' package") diff --git a/py_ballisticcalc/bin_test.py b/py_ballisticcalc/bin_test.py deleted file mode 100644 index c58eaba..0000000 --- a/py_ballisticcalc/bin_test.py +++ /dev/null @@ -1,352 +0,0 @@ -import timeit -from datetime import datetime -import unittest -import pyximport - -from math import fabs -pyximport.install() - -from py_ballisticcalc.profile import * -from py_ballisticcalc.bmath import unit -from py_ballisticcalc.atmosphere import IcaoAtmosphere -from py_ballisticcalc.drag import DragTableG1 -from py_ballisticcalc.projectile import Projectile -from py_ballisticcalc.shot_parameters import ShotParameters -from py_ballisticcalc.trajectory_data import TrajectoryData -from py_ballisticcalc.weapon import Weapon - - -class TestProfile(unittest.TestCase): - """ - 0.22300000488758087 - -9.000000953674316 0.0 - -0.00047645941958762705 100.0496826171875 - -188.0503692626953 500.03924560546875 - -1475.96826171875 1000.0016479492188 - 1.5700003132224083e-05 def init - 0.09897580003598705 def init + make - 0.2844648000318557 max=2500m, step=1m - 0.04717749997507781 max=2500m, step=1m, max_step=5ft - """ - - @unittest.skip - def test_profile_bc(self): - - p = Profile() - data = p.calculate_trajectory() - print(data[0].drop().get_in(DistanceCentimeter), data[0].travelled_distance().get_in(DistanceMeter)) - print(data[1].drop().get_in(DistanceCentimeter), data[1].travelled_distance().get_in(DistanceMeter)) - print(data[5].drop().get_in(DistanceCentimeter), data[5].travelled_distance().get_in(DistanceMeter)) - print(data[10].drop().get_in(DistanceCentimeter), data[10].travelled_distance().get_in(DistanceMeter)) - p.calculate_drag_table() - print(p.dict()) - - def test_custom_df(self): - custom_drag_func = [ - {'A': 0.0, 'B': 0.18}, {'A': 0.4, 'B': 0.178}, {'A': 0.5, 'B': 0.154}, - {'A': 0.6, 'B': 0.129}, {'A': 0.7, 'B': 0.131}, {'A': 0.8, 'B': 0.136}, - {'A': 0.825, 'B': 0.14}, {'A': 0.85, 'B': 0.144}, {'A': 0.875, 'B': 0.153}, - {'A': 0.9, 'B': 0.177}, {'A': 0.925, 'B': 0.226}, {'A': 0.95, 'B': 0.26}, - {'A': 0.975, 'B': 0.349}, {'A': 1.0, 'B': 0.427}, {'A': 1.025, 'B': 0.45}, - {'A': 1.05, 'B': 0.452}, {'A': 1.075, 'B': 0.45}, {'A': 1.1, 'B': 0.447}, - {'A': 1.15, 'B': 0.437}, {'A': 1.2, 'B': 0.429}, {'A': 1.3, 'B': 0.418}, - {'A': 1.4, 'B': 0.406}, {'A': 1.5, 'B': 0.394}, {'A': 1.6, 'B': 0.382}, - {'A': 1.8, 'B': 0.359}, {'A': 2.0, 'B': 0.339}, {'A': 2.2, 'B': 0.321}, - {'A': 2.4, 'B': 0.301}, {'A': 2.6, 'B': 0.28}, {'A': 3.0, 'B': 0.25}, - {'A': 4.0, 'B': 0.2}, {'A': 5.0, 'B': 0.18} - ] - - p = Profile(drag_table=0, custom_drag_function=custom_drag_func) - data = p.calculate_trajectory() - - def test_time(self): - with self.subTest('def init') as st: - print(timeit.timeit(lambda: Profile(), number=1), 'def init') - - with self.subTest('def init + make'): - p = Profile() - print(timeit.timeit(lambda: p.calculate_trajectory(), number=1), 'def init + make', ) - - with self.subTest('max=2500m, step=1m'): - p = Profile( - maximum_distance=(2500, unit.DistanceMeter), - distance_step=(1, unit.DistanceMeter), - ) - print(timeit.timeit(lambda: p.calculate_trajectory(), number=1), 'max=2500m, step=1m') - - with self.subTest('max=2500m, step=1m, max_step=5ft'): - p = Profile( - maximum_distance=(2500, unit.DistanceMeter), - distance_step=(1, unit.DistanceMeter), - maximum_step_size=(5, unit.DistanceFoot) - ) - print(timeit.timeit(lambda: p.calculate_trajectory(), number=1), 'max=2500m, step=1m, max_step=5ft') - - with self.subTest('custom_df'): - print(timeit.timeit(self.test_custom_df, number=1), 'max=2500m, step=1m, max_step=5ft, custom_df') - - -class TestAtmo(unittest.TestCase): - - def test_create(self): - v = Atmosphere( - altitude=Distance(0, DistanceMeter), - pressure=Pressure(760, PressureMmHg), - temperature=Temperature(15, TemperatureCelsius), - humidity=0.5 - ) - - icao = IcaoAtmosphere(Distance(0, DistanceMeter)) - - # @unittest.SkipTest - def test_time(self): - t = timeit.timeit(self.test_create, number=1) - print(t) - - -class TestShotParams(unittest.TestCase): - - def test_create(self): - v = ShotParameters( - Angular(0, AngularDegree), - Distance(1000, DistanceFoot), - Distance(100, DistanceFoot) - ) - - def test_unlevel(self): - v = ShotParametersUnlevel( - Angular(0, AngularDegree), - Distance(1000, DistanceFoot), - Distance(100, DistanceFoot), - Angular(0, AngularDegree), - Angular(0, AngularDegree) - ) - - # @unittest.SkipTest - def test_time(self): - t = timeit.timeit(self.test_create, number=1) - print(datetime.fromtimestamp(t).time().strftime('%S.%fs')) - t = timeit.timeit(self.test_unlevel, number=1) - print(datetime.fromtimestamp(t).time().strftime('%S.%fs')) - - -class TestDrag(unittest.TestCase): - - def setUp(self) -> None: - self.bc = self.test_create() - - def test_create(self): - bc = BallisticCoefficient( - value=0.275, - drag_table=DragTableG7, - weight=Weight(178, WeightGrain), - diameter=Distance(0.308, DistanceInch), - custom_drag_table=[] - ) - return bc - - def test_drag(self): - return self.bc.drag(3) - - def test_time(self): - t = timeit.timeit(self.test_create, number=1) - print(datetime.fromtimestamp(t).time().strftime('%S.%fs')) - t = timeit.timeit(self.test_drag, number=50000) - print(datetime.fromtimestamp(t).time().strftime('%S.%fs')) - - -class TestG7Profile(unittest.TestCase): - - def test_drag(self): - bc = BallisticCoefficient( - value=0.223, - drag_table=DragTableG7, - weight=Weight(167, WeightGrain), - diameter=Distance(0.308, DistanceInch), - custom_drag_table=[] - ) - - print(bc.form_factor()) - print(bc.drag(3)) - - ret = bc.calculated_drag_function() - # print(ret) - - def test_mbc(self): - bc = MultipleBallisticCoefficient( - drag_table=DragTableG7, - weight=Weight(178, WeightGrain), - diameter=Distance(0.308, DistanceInch), - multiple_bc_table=[[0.275, 800], [0.255, 500], [0.26, 700], ], - velocity_units_flag=VelocityMPS - ) - - ret = bc.custom_drag_func() - # print(ret) - - def test_create(self): - bc = BallisticCoefficient( - value=0.223, - drag_table=DragTableG7, - weight=Weight(167, WeightGrain), - diameter=Distance(0.308, DistanceInch), - custom_drag_table=[] - ) - - p1 = ProjectileWithDimensions( - bc, - Distance(0.308, DistanceInch), - Distance(1.2, DistanceInch), - Weight(167, WeightGrain), - ) - - ammo = Ammunition(p1, Velocity(800, VelocityMPS)) - atmo = Atmosphere(Distance(0, DistanceMeter), Pressure(760, PressureMmHg), - Temperature(15, TemperatureCelsius), 0.5) - - zero = ZeroInfo(Distance(100, DistanceMeter), True, True, ammo, atmo) - twist = TwistInfo(TwistRight, Distance(11, DistanceInch)) - weapon = WeaponWithTwist(Distance(90, DistanceMillimeter), zero, twist) - wind = create_only_wind_info(Velocity(0, VelocityMPS), Angular(0, AngularDegree)) - calc = TrajectoryCalculator() - calc.set_maximum_calculator_step_size(Distance(1, DistanceFoot)) - print(timeit.timeit(lambda: calc.sight_angle(ammo, weapon, atmo), number=1)) - sight_angle = calc.sight_angle(ammo, weapon, atmo) - shot_info = ShotParameters(sight_angle, Distance(2500, DistanceMeter), Distance(1, DistanceMeter)) - return calc.trajectory(ammo, weapon, atmo, shot_info, wind) - - def test_time(self): - t = timeit.timeit(self.test_create, number=1) - print(datetime.fromtimestamp(t).time().strftime('%S.%fs')) - - -class TestPyBallisticCalc(unittest.TestCase): - - @unittest.skip - def test_zero1(self): - bc = BallisticCoefficient(0.365, DragTableG1) - projectile = Projectile(bc, unit.Weight(69, unit.WeightGrain)) - ammo = Ammunition(projectile, unit.Velocity(2600, unit.VelocityFPS)) - zero = ZeroInfo(unit.Distance(100, unit.DistanceYard)) - weapon = Weapon(unit.Distance(3.2, unit.DistanceInch), zero) - atmosphere = IcaoAtmosphere(Distance(0, DistanceMeter)) - calc = TrajectoryCalculator() - - sight_angle = calc.sight_angle(ammo, weapon, atmosphere) - - self.assertLess(fabs(sight_angle.get_in(unit.AngularRadian) - 0.001651), 1e-6, - f'TestZero1 failed {sight_angle.get_in(unit.AngularRadian):.10f}') - - @unittest.skip - def test_zero2(self): - bc = BallisticCoefficient(0.223, DragTableG7) - projectile = Projectile(bc, unit.Weight(168, unit.WeightGrain)) - ammo = Ammunition(projectile, unit.Velocity(2750, unit.VelocityFPS)) - zero = ZeroInfo(unit.Distance(100, unit.DistanceYard)) - weapon = Weapon(unit.Distance(2, unit.DistanceInch), zero) - atmosphere = IcaoAtmosphere(Distance(0, DistanceMeter)) - calc = TrajectoryCalculator() - - sight_angle = calc.sight_angle(ammo, weapon, atmosphere) - - self.assertLess(fabs(sight_angle.get_in(unit.AngularRadian) - 0.001228), 1e-6, - f'TestZero2 failed {sight_angle.get_in(unit.AngularRadian):.10f}') - - def assertEqualCustom(self, a, b, accuracy, name): - with self.subTest(): - self.assertFalse(fabs(a - b) > accuracy, f'Assertion {name} failed ({a}/{b}, {accuracy})') - - def validate_one(self, data: TrajectoryData, distance: float, velocity: float, mach: float, energy: float, - path: float, hold: float, windage: float, wind_adjustment: float, time: float, ogv: float, - adjustment_unit: int): - - self.assertEqualCustom(distance, data.travelled_distance().get_in(unit.DistanceYard), 0.001, "Distance") - self.assertEqualCustom(velocity, data.velocity().get_in(unit.VelocityFPS), 5, "Velocity") - self.assertEqualCustom(mach, data.mach_velocity(), 0.005, "Mach") - self.assertEqualCustom(energy, data.energy().get_in(unit.EnergyFootPound), 5, "Energy") - self.assertEqualCustom(time, data.time().total_seconds(), 0.06, "Time") - self.assertEqualCustom(ogv, data.optimal_game_weight().get_in(unit.WeightPound), 1, "OGV") - - if distance >= 800: - self.assertEqualCustom(path, data.drop().get_in(unit.DistanceInch), 4, 'Drop') - elif distance >= 500: - self.assertEqualCustom(path, data.drop().get_in(unit.DistanceInch), 1, 'Drop') - else: - self.assertEqualCustom(path, data.drop().get_in(unit.DistanceInch), 0.5, 'Drop') - - if distance > 1: - self.assertEqualCustom(hold, data.drop_adjustment().get_in(adjustment_unit), 0.5, 'Hold') - - if distance >= 800: - self.assertEqualCustom(windage, data.windage().get_in(unit.DistanceInch), 1.5, "Windage") - elif distance >= 500: - self.assertEqualCustom(windage, data.windage().get_in(unit.DistanceInch), 1, "Windage") - else: - self.assertEqualCustom(windage, data.windage().get_in(unit.DistanceInch), 0.5, "Windage") - - if distance > 1: - self.assertEqualCustom(wind_adjustment, data.windage_adjustment().get_in(adjustment_unit), 0.5, "WAdj") - - @unittest.skip - def test_path_g1(self): - bc = BallisticCoefficient(0.223, DragTableG1) - projectile = Projectile(bc, unit.Weight(168, unit.WeightGrain)) - ammo = Ammunition(projectile, unit.Velocity(2750, unit.VelocityFPS)) - zero = ZeroInfo(unit.Distance(100, unit.DistanceYard)) - weapon = Weapon(unit.Distance(2, unit.DistanceInch), zero) - atmosphere = IcaoAtmosphere(Distance(0, DistanceMeter)) - shot_info = ShotParameters(unit.Angular(0.001228, unit.AngularRadian), - unit.Distance(1000, unit.DistanceYard), - unit.Distance(100, unit.DistanceYard)) - wind = create_only_wind_info(unit.Velocity(5, unit.VelocityMPH), - unit.Angular(-45, unit.AngularDegree)) - calc = TrajectoryCalculator() - data = calc.trajectory(ammo, weapon, atmosphere, shot_info, wind) - - self.assertEqualCustom(len(data), 11, 0.1, "Length") - - test_data = [ - [data[0], 0, 2750, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, unit.AngularMOA], - [data[1], 100, 2351.2, 2.106, 2061, 0, 0, -0.6, -0.6, 0.118, 550, unit.AngularMOA], - [data[5], 500, 1169.1, 1.047, 509.8, -87.9, -16.8, -19.5, -3.7, 0.857, 67, unit.AngularMOA], - [data[10], 1000, 776.4, 0.695, 224.9, -823.9, -78.7, -87.5, -8.4, 2.495, 20, unit.AngularMOA] - ] - - for d in test_data: - with self.subTest(): - self.validate_one(*d) - - def test_path_g7(self): - bc = BallisticCoefficient(0.223, DragTableG7, - weight=Weight(167, WeightGrain), - diameter=Distance(0.308, DistanceInch), - custom_drag_table=[]) - projectile = ProjectileWithDimensions(bc, unit.Distance(0.308, unit.DistanceInch), - unit.Distance(1.282, unit.DistanceInch), - unit.Weight(168, unit.WeightGrain)) - ammo = Ammunition(projectile, unit.Velocity(2750, unit.VelocityFPS)) - zero = ZeroInfo(unit.Distance(100, unit.DistanceYard)) - twist = TwistInfo(TwistRight, unit.Distance(11.24, unit.DistanceInch)) - weapon = WeaponWithTwist(unit.Distance(2, unit.DistanceInch), zero, twist) - atmosphere = IcaoAtmosphere(Distance(0, DistanceMeter)) - shot_info = ShotParameters(unit.Angular(4.221, unit.AngularMOA), - unit.Distance(1000, unit.DistanceYard), - unit.Distance(100, unit.DistanceYard)) - wind = create_only_wind_info(unit.Velocity(5, unit.VelocityMPH), - unit.Angular(-45, unit.AngularDegree)) - - calc = TrajectoryCalculator() - data = calc.trajectory(ammo, weapon, atmosphere, shot_info, wind) - - self.assertEqualCustom(len(data), 11, 0.1, "Length") - - test_data = [ - [data[0], 0, 2750, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, unit.AngularMil], - [data[1], 100, 2544.3, 2.279, 2416, 0, 0, -0.35, -0.09, 0.113, 698, unit.AngularMil], - [data[5], 500, 1810.7, 1.622, 1226, -56.3, -3.18, -9.96, -0.55, 0.673, 252, unit.AngularMil], - [data[10], 1000, 1081.3, 0.968, 442, -401.6, -11.32, -50.98, -1.44, 1.748, 55, unit.AngularMil] - ] - - for d in test_data: - with self.subTest(): - self.validate_one(*d) diff --git a/py_ballisticcalc/bmath/__init__.py b/py_ballisticcalc/bmath/__init__.py deleted file mode 100644 index abbcede..0000000 --- a/py_ballisticcalc/bmath/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .vector import * -from .unit import * diff --git a/py_ballisticcalc/bmath/unit/__init__.py b/py_ballisticcalc/bmath/unit/__init__.py deleted file mode 100644 index 75d19c7..0000000 --- a/py_ballisticcalc/bmath/unit/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .weight import * -from .velocity import * -from .temperature import * -from .pressure import * -from .energy import * -from .distance import * -from .angular import * diff --git a/py_ballisticcalc/bmath/unit/distance.pyx b/py_ballisticcalc/bmath/unit/distance.pyx deleted file mode 100644 index 5406830..0000000 --- a/py_ballisticcalc/bmath/unit/distance.pyx +++ /dev/null @@ -1,125 +0,0 @@ -DistanceInch = 10 -DistanceFoot = 11 -DistanceYard = 12 -DistanceMile = 13 -DistanceNauticalMile = 14 -DistanceMillimeter = 15 -DistanceCentimeter = 16 -DistanceMeter = 17 -DistanceKilometer = 18 -DistanceLine = 19 - -cdef class Distance: - cdef double _value - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Distance' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == DistanceInch: - return value - elif units == DistanceFoot: - return value * 12 - elif units == DistanceYard: - return value * 36 - elif units == DistanceMile: - return value * 63360 - elif units == DistanceNauticalMile: - return value * 72913.3858 - elif units == DistanceLine: - return value / 10 - elif units == DistanceMillimeter: - return value / 25.4 - elif units == DistanceCentimeter: - return value / 2.54 - elif units == DistanceMeter: - return value / 25.4 * 1000 - elif units == DistanceKilometer: - return value / 25.4 * 1000000 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == DistanceInch: - return value - elif units == DistanceFoot: - return value / 12 - elif units == DistanceYard: - return value / 36 - elif units == DistanceMile: - return value / 63360 - elif units == DistanceNauticalMile: - return value / 72913.3858 - elif units == DistanceLine: - return value * 10 - elif units == DistanceMillimeter: - return value * 25.4 - elif units == DistanceCentimeter: - return value * 2.54 - elif units == DistanceMeter: - return value * 25.4 / 1000 - elif units == DistanceKilometer: - return value * 25.4 / 1000000 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Distance convert(self, units: int): - cdef double value = self.get_in(units) - return Distance(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == DistanceInch: - name = 'in' - accuracy = 1 - elif default == DistanceFoot: - name = 'ft' - accuracy = 2 - elif default == DistanceYard: - name = 'yd' - accuracy = 3 - elif default == DistanceMile: - name = 'mi' - accuracy = 3 - elif default == DistanceNauticalMile: - name = 'nm' - accuracy = 3 - elif default == DistanceLine: - name = 'ln' - accuracy = 1 - elif default == DistanceMillimeter: - name = 'mm' - accuracy = 0 - elif default == DistanceCentimeter: - name = 'cm' - accuracy = 1 - elif default == DistanceMeter: - name = 'm' - accuracy = 2 - elif default == DistanceKilometer: - name = 'km' - accuracy = 3 - else: - name = '?' - accuracy = 6 - - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/unit/energy.pyx b/py_ballisticcalc/bmath/unit/energy.pyx deleted file mode 100644 index 2a48c2d..0000000 --- a/py_ballisticcalc/bmath/unit/energy.pyx +++ /dev/null @@ -1,61 +0,0 @@ -EnergyFootPound: int = 30 -EnergyJoule: int = 31 - - -cdef class Energy: - cdef double _value - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Energy' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == EnergyFootPound: - return value - elif units == EnergyJoule: - return value * 0.737562149277 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == EnergyFootPound: - return value - elif units == EnergyJoule: - return value / 0.737562149277 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Energy convert(self, units: int): - cdef double value = self.get_in(units) - return Energy(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == EnergyFootPound: - name = "ft·lb" - accuracy = 0 - elif default == EnergyJoule: - name = "J" - accuracy = 0 - else: - name = '?' - accuracy = 6 - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/unit/pressure.pyx b/py_ballisticcalc/bmath/unit/pressure.pyx deleted file mode 100644 index 6f151b8..0000000 --- a/py_ballisticcalc/bmath/unit/pressure.pyx +++ /dev/null @@ -1,86 +0,0 @@ -PressureMmHg = 40 -PressureInHg = 41 -PressureBar = 42 -PressureHP = 43 -PressurePSI = 44 - - -cdef class Pressure: - cdef double _value - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Pressure' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == PressureMmHg: - return value - elif units == PressureInHg: - return value * 25.4 - elif units == PressureBar: - return value * 750.061683 - elif units == PressureHP: - return value * 750.061683 / 1000 - elif units == PressurePSI: - return value * 51.714924102396 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == PressureMmHg: - return value - elif units == PressureInHg: - return value / 25.4 - elif units == PressureBar: - return value / 750.061683 - elif units == PressureHP: - return value / 750.061683 * 1000 - elif units == PressurePSI: - return value / 51.714924102396 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Pressure convert(self, units: int): - cdef double value = self.get_in(units) - return Pressure(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == PressureMmHg: - name = 'mmHg' - accuracy = 0 - elif default == PressureMmHg: - name = 'inHg' - accuracy = 2 - elif default == PressureBar: - name = 'bar' - accuracy = 2 - elif default == PressureHP: - name = 'hPa' - accuracy = 4 - elif default == PressurePSI: - name = 'psi' - accuracy = 4 - else: - name = '?' - accuracy = 6 - - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/unit/temperature.pyx b/py_ballisticcalc/bmath/unit/temperature.pyx deleted file mode 100644 index 6888a45..0000000 --- a/py_ballisticcalc/bmath/unit/temperature.pyx +++ /dev/null @@ -1,77 +0,0 @@ -TemperatureFahrenheit: int = 50 -TemperatureCelsius: int = 51 -TemperatureKelvin: int = 52 -TemperatureRankin: int = 53 - - -cdef class Temperature: - cdef double _value - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Temperature' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == TemperatureFahrenheit: - return value - elif units == TemperatureRankin: - return value - 459.67 - elif units == TemperatureCelsius: - return value * 9 / 5 + 32 - elif units == TemperatureKelvin: - return (value - 273.15) * 9 / 5 + 32 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == TemperatureFahrenheit: - return value - elif units == TemperatureRankin: - return value + 459.67 - elif units == TemperatureCelsius: - return (value - 32) * 5 / 9 - elif units == TemperatureKelvin: - return (value - 32) * 5 / 9 + 273.15 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Temperature convert(self, units: int): - cdef double value = self.get_in(units) - return Temperature(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == TemperatureFahrenheit: - name = '°F' - accuracy = 1 - elif default == TemperatureRankin: - name = '°R' - accuracy = 1 - elif default == TemperatureCelsius: - name = '°C' - accuracy = 1 - elif default == TemperatureKelvin: - name = '°K' - accuracy = 1 - else: - name = '?' - accuracy = 6 - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/unit/unit_test.py b/py_ballisticcalc/bmath/unit/unit_test.py deleted file mode 100644 index da80a68..0000000 --- a/py_ballisticcalc/bmath/unit/unit_test.py +++ /dev/null @@ -1,139 +0,0 @@ -import unittest -import pyximport -pyximport.install() -from py_ballisticcalc.bmath.unit import * - -import math - - -def test_back_n_forth(test, value, units): - u = test.unit_class(value, units) - v = u.get_in(units) - test.assertTrue( - math.fabs(v - value) < 1e-7 - and math.fabs(v - u.get_in(units) < 1e-7), f'Read back failed for {units}') - - -class TestAngular(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Angular - self.unit_list = [ - AngularDegree, - AngularMOA, - AngularMRad, - AngularMil, - AngularRadian, - AngularThousand - ] - - def test_angular(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestDistance(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Distance - self.unit_list = [ - DistanceCentimeter, - DistanceFoot, - DistanceInch, - DistanceKilometer, - DistanceLine, - DistanceMeter, - DistanceMillimeter, - DistanceMile, - DistanceNauticalMile, - DistanceYard - ] - - def test_distance(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestEnergy(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Energy - self.unit_list = [ - EnergyFootPound, - EnergyJoule - ] - - def test_energy(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestPressure(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Pressure - self.unit_list = [ - PressureBar, - PressureHP, - PressureMmHg, - PressureInHg - ] - - def test_pressure(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestTemperature(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Temperature - self.unit_list = [ - TemperatureFahrenheit, - TemperatureKelvin, - TemperatureCelsius, - TemperatureRankin - ] - - def test_temperature(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestVelocity(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Velocity - self.unit_list = [ - VelocityFPS, - VelocityKMH, - VelocityKT, - VelocityMPH, - VelocityMPS - ] - - def test_velocity(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -class TestWeight(unittest.TestCase): - def setUp(self) -> None: - self.unit_class = Weight - self.unit_list = [ - WeightGrain, - WeightGram, - WeightKilogram, - WeightNewton, - WeightOunce, - WeightPound - ] - - def test_weight(self): - for u in self.unit_list: - with self.subTest(unit=u): - test_back_n_forth(self, 3, u) - - -if __name__ == '__main__': - unittest.main() diff --git a/py_ballisticcalc/bmath/unit/velocity.pyx b/py_ballisticcalc/bmath/unit/velocity.pyx deleted file mode 100644 index bc1bcc7..0000000 --- a/py_ballisticcalc/bmath/unit/velocity.pyx +++ /dev/null @@ -1,85 +0,0 @@ -VelocityMPS = 60 -VelocityKMH = 61 -VelocityFPS = 62 -VelocityMPH = 63 -VelocityKT = 64 - - -cdef class Velocity: - cdef double _value # Stored in m/s - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Velocity' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == VelocityMPS: - return value - elif units == VelocityKMH: - return value / 3.6 - elif units == VelocityFPS: - return value / 3.2808399 - elif units == VelocityMPH: - return value / 2.23693629 - elif units == VelocityKT: - return value / 1.94384449 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == VelocityMPS: - return value - elif units == VelocityKMH: - return value * 3.6 - elif units == VelocityFPS: - return value * 3.2808399 - elif units == VelocityMPH: - return value * 2.23693629 - elif units == VelocityKT: - return value * 1.94384449 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Velocity convert(self, units: int): - cdef double value = self.get_in(units) - return Velocity(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == VelocityMPS: - name = "m/s" - accuracy = 0 - elif default == VelocityKMH: - name = "km/h" - accuracy = 1 - elif default == VelocityFPS: - name = "ft/s" - accuracy = 1 - elif default == VelocityMPH: - name = "mph" - accuracy = 1 - elif default == VelocityKT: - name = "kt" - accuracy = 1 - else: - name = '?' - accuracy = 6 - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/unit/weight.pyx b/py_ballisticcalc/bmath/unit/weight.pyx deleted file mode 100644 index fc4ea5a..0000000 --- a/py_ballisticcalc/bmath/unit/weight.pyx +++ /dev/null @@ -1,94 +0,0 @@ -WeightGrain = 70 -WeightOunce = 71 -WeightGram = 72 -WeightPound = 73 -WeightKilogram = 74 -WeightNewton = 75 - - -cdef class Weight: - cdef double _value - cdef int _default_units - cdef __name__ - - def __init__(self, value: double, units: int): - self.__name__ = 'Weight' - self._value = self.to_default(value, units) - self._default_units = units - - cdef double to_default(self, value: double, units: int): - if units == WeightGrain: - return value - elif units == WeightGram: - return value * 15.4323584 - elif units == WeightKilogram: - return value * 15432.3584 - elif units == WeightNewton: - return value * 151339.73750336 - elif units == WeightPound: - return value / 0.000142857143 - elif units == WeightOunce: - return value * 437.5 - else: - raise KeyError(f'{self.__name__}: unit {units} is not supported') - - cdef double from_default(self, value: double, units: int): - if units == WeightGrain: - return value - elif units == WeightGram: - return value / 15.4323584 - elif units == WeightKilogram: - return value / 15432.3584 - elif units == WeightNewton: - return value / 151339.73750336 - elif units == WeightPound: - return value * 0.000142857143 - elif units == WeightOunce: - return value / 437.5 - else: - raise KeyError(f'KeyError: {self.__name__}: unit {units} is not supported') - - cpdef double get_value(self): - return self.from_default(self._value, self._default_units) - - cpdef double get_in(self, units: int): - return self.from_default(self._value, units) - - cpdef Weight convert(self, units: int): - cdef double value = self.get_in(units) - return Weight(value, units) - - def __str__(self): - return self.string() - - cdef string(self): - cdef name - cdef int accuracy - cdef int default = self._default_units - cdef double v = self.from_default(self._value, default) - if default == WeightGrain: - name = 'gr' - accuracy = 0 - elif default == WeightGram: - name = 'g' - accuracy = 1 - elif default == WeightKilogram: - name = 'kg' - accuracy = 3 - elif default == WeightNewton: - name = 'N' - accuracy = 3 - elif default == WeightPound: - name = 'lb' - accuracy = 3 - elif default == WeightOunce: - name = 'oz' - accuracy = 1 - else: - name = '?' - accuracy = 6 - - return f'{round(v, accuracy)} {name}' - - cpdef int units(self): - return self._default_units diff --git a/py_ballisticcalc/bmath/vector/__init__.py b/py_ballisticcalc/bmath/vector/__init__.py deleted file mode 100644 index e58cbab..0000000 --- a/py_ballisticcalc/bmath/vector/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .vector import * diff --git a/py_ballisticcalc/bmath/vector/vector.pyx b/py_ballisticcalc/bmath/vector/vector.pyx deleted file mode 100644 index 215f8de..0000000 --- a/py_ballisticcalc/bmath/vector/vector.pyx +++ /dev/null @@ -1,107 +0,0 @@ -from libc.math cimport sqrt, fabs -from typing import Any - -# x - down-range distance towards target -# y - vertical drop -# z - windage - -cdef struct vector: - double x - double y - double z - - -cdef class Vector: - cdef double _x - cdef double _y - cdef double _z - - def __init__(self, x: double, y: double, z: double): - self._x = x - self._y = y - self._z = z - - def __str__(self): - return f'{vector(self._x, self._y, self._z)}' - - cpdef double x(self): - return self._x - - cpdef double y(self): - return self._y - - cpdef double z(self): - return self._z - - cdef string(self): - cdef v = vector(self._x, self._y, self._z) - return f'{v}' - - cpdef Vector copy(self): - return Vector(self._x, self._y, self._z) - - cpdef double magnitude(self): - cdef double m = sqrt(self._x * self._x + self._y * self._y + self._z * self._z) - return m - - cpdef Vector multiply_by_const(self, float a): - return Vector(self._x * a, self._y * a, self._z * a) - - cpdef double multiply_by_vector(self, b: Vector): - cdef double var = self._x * b._x + self._y * b._y + self._z * b._z - return var - - cpdef Vector add(self, b: Vector): - return Vector(self._x + b._x, self._y + b._y, self._z + b._z) - - cpdef Vector subtract(self, b: Vector): - return Vector(self._x - b._x, self._y - b._y, self._z - b._z) - - cpdef Vector negate(self): - return Vector(-self._x, -self._y, -self._z) - - cpdef Vector normalize(self): - cdef double m = self.magnitude() - if fabs(m) < 1e-10: - return Vector(self._x, self._y, self._z) - return self.multiply_by_const(1.0 / m) - - def __add__(self, other: Vector): - return self.add(other) - - def __radd__(self, other: Vector): - return self.__add__(other) - - def __iadd__(self, other: Vector): - return self.__add__(other) - - def __sub__(self, other: Vector): - return self.subtract(other) - - def __rsub__(self, other: Vector): - return other.subtract(self) - - def __isub__(self, other: Vector): - return self.subtract(other) - - def __mul__(self, other: [Vector, float, int]): - if isinstance(other, int) or isinstance(other, float): - return self.multiply_by_const(other) - elif isinstance(other, Vector): - return self.multiply_by_vector(other) - else: - raise TypeError(other) - - def __rmul__(self, other: [Vector, float, int]): - return self.__mul__(other) - - def __imul__(self, other: [Vector, float, int]): - return self.__mul__(other) - - def __neg__(self): - return self.negate() - - def __iter__(self): - yield self.x() - yield self.y() - yield self.z() diff --git a/py_ballisticcalc/bmath/vector/vector_test.py b/py_ballisticcalc/bmath/vector/vector_test.py deleted file mode 100644 index 1635621..0000000 --- a/py_ballisticcalc/bmath/vector/vector_test.py +++ /dev/null @@ -1,98 +0,0 @@ -import timeit -import unittest -import pyximport -import math -pyximport.install() -from py_ballisticcalc.bmath.vector import Vector - - -class TestVector(unittest.TestCase): - - def test_create(self): - v = Vector(1, 2, 3) - mag = v.magnitude() - mc = v.multiply_by_const(10) - - b = Vector(1, 2, 3) - mv = v.multiply_by_vector(b) - a = v.add(b) - s = v.subtract(b) - neg = v.negate() - norm = v.normalize() - # print('\n'.join([str(i) for i in [mag, mc, mv, a, s, neg, norm]])) - x = v.x - - # @unittest.SkipTest - def test_time(self): - t = timeit.timeit(self.test_create, number=50000) - print(t) - - -class TestVectorCreation(unittest.TestCase): - - def test_create(self): - v = Vector(1, 2, 3) - - self.assertTrue(v.x() == 1 and v.y() == 2 and v.z() == 3, "Create failed") - - c = v.copy() - self.assertTrue(c.x() == 1 and c.y() == 2 and c.z() == 3, "Copy failed") - - -class TestUnary(unittest.TestCase): - - def test_unary(self): - v1 = Vector(1, 2, 3) - - self.assertFalse(math.fabs(v1.magnitude() - 3.74165738677) > 1e-7, "Magnitude failed") - - v2 = v1.negate() - self.assertTrue(v2.x() == -1.0 and v2.y() == -2.0 and v2.z() == -3.0, "Negate failed") - - v2 = v1.normalize() - self.assertFalse(v2.x() > 1.0 or v2.y() > 1.0 or v2.z() > 1.0, "Normalize failed") - - v1 = Vector(0, 0, 0) - v2 = v1.normalize() - - self.assertFalse(v2.x() != 0.0 or v2.y() != 0.0 or v2.z() != 0.0, "Normalize failed") - - -class TestBinary(unittest.TestCase): - - def test_binary(self): - v1 = Vector(1, 2, 3) - - v2 = v1.add(v1.copy()) - self.assertFalse(v2.x() != 2.0 or v2.y() != 4.0 or v2.z() != 6.0, "Add failed") - - v2 = v1.subtract(v2) - self.assertFalse(v2.x() != -1.0 or v2.y() != -2.0 or v2.z() != -3.0, "Subtract failed") - - self.assertFalse(v1.multiply_by_vector(v1.copy()) != float(1 + 4 + 9), "MultiplyByVector failed") - - v2 = v1.multiply_by_const(3) - self.assertFalse(v2.x() != 3.0 or v2.y() != 6.0 or v2.z() != 9.0, "MultiplyByConst failed") - - -class TestOperators(unittest.TestCase): - - def test_operators(self): - - v1 = Vector(1, 2, 3) - v2 = -v1 - self.assertTrue(v2.x() == -1.0 and v2.y() == -2.0 and v2.z() == -3.0, "Vector.__neg__() failed") - - v2 = v1 + v1.copy() - self.assertFalse(v2.x() != 2.0 or v2.y() != 4.0 or v2.z() != 6.0, "Vector.__add__() failed") - - v2 = v1 - v2 - self.assertFalse(v2.x() != -1.0 or v2.y() != -2.0 or v2.z() != -3.0, "Vector.__sub__() failed") - - self.assertFalse(v1 * v1.copy() != float(1 + 4 + 9), "Vector.__mull__(other: Vector) failed") - - v2 = v1 * 3 - self.assertFalse(v2.x() != 3.0 or v2.y() != 6.0 or v2.z() != 9.0, "Vector.__mull__(other: [float, int]) failed") - - self.assertEqual(tuple(v1), (1.0, 2.0, 3.0), "Vector.__iter__() failed") - self.assertEqual(len(tuple(v1)), 3, "Vector.__iter__() failed") diff --git a/py_ballisticcalc/conditions.py b/py_ballisticcalc/conditions.py new file mode 100644 index 0000000..73970c9 --- /dev/null +++ b/py_ballisticcalc/conditions.py @@ -0,0 +1,207 @@ +"""Classes to define zeroing or current environment conditions""" + +import math +from dataclasses import dataclass, field + +from .settings import Settings as Set +from .unit import Distance, Velocity, Temperature, Pressure, TypedUnits, Angular +from .munition import Weapon, Ammo + +__all__ = ('Atmo', 'Wind', 'Shot') + +cIcaoStandardTemperatureR: float = 518.67 +cIcaoFreezingPointTemperatureR: float = 459.67 # Misnamed: This is actually conversion from F to R +cTemperatureGradient: float = -3.56616e-03 +cIcaoStandardHumidity: float = 0.0 +cPressureExponent: float = -5.255876 +cSpeedOfSound: float = 49.0223 +cA0: float = 1.24871 +cA1: float = 0.0988438 +cA2: float = 0.00152907 +cA3: float = -3.07031e-06 +cA4: float = 4.21329e-07 +cA5: float = 3.342e-04 +cStandardTemperature: float = 59.0 # degrees F +cStandardPressure: float = 29.92 # InHg +cStandardDensity: float = 0.076474 # lb/ft^3 + +cIcaoTemperatureDeltaR: float = cIcaoStandardTemperatureR - cIcaoFreezingPointTemperatureR + + +@dataclass +class Atmo(TypedUnits): # pylint: disable=too-many-instance-attributes + """Stores atmosphere data for the trajectory calculation""" + + altitude: [float, Distance] = field(default_factory=lambda: Set.Units.distance) + pressure: [float, Pressure] = field(default_factory=lambda: Set.Units.pressure) + temperature: [float, Temperature] = field(default_factory=lambda: Set.Units.temperature) + humidity: float = 0 + density: float = field(init=False) + mach: Velocity = field(init=False) + _mach1: Velocity = field(init=False) + _a0: float = field(init=False) + _t0: float = field(init=False) + _p0: float = field(init=False) + _ta: float = field(init=False) + + def __post_init__(self): + if self.humidity > 1: + self.humidity = self.humidity / 100.0 + if not 0 <= self.humidity <= 1: + self.humidity = 0.0 + if not self.altitude: + self.altitude = Distance.Foot(0) + if not self.temperature: + self.temperature = Atmo.standard_temperature(self.altitude) + if not self.pressure: + self.pressure = Atmo.standard_pressure(self.altitude, self.temperature) + + self.calculate() + + @staticmethod + def standard_temperature(altitude: Distance) -> Temperature: + return Temperature.Fahrenheit(cIcaoStandardTemperatureR + + (altitude >> Distance.Foot) * cTemperatureGradient + - cIcaoFreezingPointTemperatureR) + + @staticmethod + def standard_pressure(altitude: Distance, temperature: Temperature) -> Pressure: + # TODO: Find correct formula + return Pressure.InHg(cStandardPressure) + + @staticmethod + def icao(altitude: [float, Distance] = 0, temperature: Temperature=None): + """Creates standard ICAO atmosphere at given altitude. + If temperature not specified uses standard temperature. + """ + altitude = Set.Units.distance(altitude) + if temperature is None: + temperature = Atmo.standard_temperature(altitude) + + # TODO: Pretty sure this needs to be a function of altitude too? + pressure = Pressure.InHg( + cStandardPressure * math.pow(cIcaoStandardTemperatureR + / ((temperature >> Temperature.Fahrenheit) + cIcaoFreezingPointTemperatureR), + cPressureExponent + ) + ) + + return Atmo( + altitude >> Set.Units.distance, + pressure >> Set.Units.pressure, + temperature >> Set.Units.temperature, + cIcaoStandardHumidity + ) + + def density_factor(self): + """:return: projectile density_factor""" + return self.density / cStandardDensity + + def calculate0(self, t, p) -> (float, float): + """:return: density and mach with specified atmosphere""" + if t > 0: + et0 = cA0 + t * (cA1 + t * (cA2 + t * (cA3 + t * cA4))) + et = cA5 * self.humidity * et0 + hc = (p - 0.3783 * et) / cStandardPressure + else: + hc = 1.0 + + density = cStandardDensity * ( + cIcaoStandardTemperatureR / (t + cIcaoFreezingPointTemperatureR) + ) * hc + mach = math.sqrt(t + cIcaoFreezingPointTemperatureR) * cSpeedOfSound + return density, mach + + def calculate(self) -> None: + """prepare the data for the calculation""" + self._t0 = self.temperature >> Temperature.Fahrenheit + self._p0 = self.pressure >> Pressure.InHg + self._a0 = self.altitude >> Distance.Foot + self._ta = self._a0 * cTemperatureGradient + cIcaoTemperatureDeltaR + + self.density, self._mach1 = self.calculate0(self._t0, self._p0) + self.mach = Velocity(self._mach1, Velocity.FPS) + + def get_density_factor_and_mach_for_altitude(self, altitude: float): + """:return: density factor for the specified altitude""" + if math.fabs(self._a0 - altitude) < 30: + density = self.density / cStandardDensity + mach = self._mach1 + return density, mach + + tb = altitude * cTemperatureGradient + cIcaoTemperatureDeltaR + t = self._t0 + self._ta - tb + p = self._p0 * math.pow(self._t0 / t, cPressureExponent) + + density, mach = self.calculate0(t, p) + return density / cStandardDensity, mach + + +@dataclass +class Wind(TypedUnits): + """ + Wind direction and velocity by down-range distance. + direction_from = 0 is blowing from behind shooter. + direction_from = 90 degrees is blowing from shooter's left towards right. + """ + velocity: [float, Velocity] = field(default_factory=lambda: Set.Units.velocity) + direction_from: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + until_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance) + + def __post_init__(self): + if not self.until_distance: + self.until_distance = Distance.Meter(9999) # TODO: Set to a fundamental max value + if not self.direction_from or not self.velocity: + self.direction_from = 0 + self.velocity = 0 + + +@dataclass +class Shot(TypedUnits): + """ + Stores shot parameters for the trajectory calculation. + + :param look_angle: Angle of sight line relative to horizontal. + If look_angle != 0 then any target in sight crosshairs will be at a different altitude: + With target_distance = sight distance to a target (i.e., as through a rangefinder): + * Horizontal distance X to target = cos(look_angle) * target_distance + * Vertical distance Y to target = sin(look_angle) * target_distance + :param relative_angle: Elevation adjustment added to weapon.zero_elevation for a particular shot. + :param cant_angle: Tilt of gun from vertical, which shifts any barrel elevation + from the vertical plane into the horizontal plane by sine(cant_angle) + """ + look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + relative_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + cant_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + + weapon: Weapon = field(default=None) + ammo: Ammo = field(default=None) + atmo: Atmo = field(default=None) + winds: list[Wind] = field(default=None) + + @property + def barrel_elevation(self) -> Angular: + """Barrel elevation in vertical plane from horizontal""" + return Angular.Radian((self.look_angle >> Angular.Radian) + + math.cos(self.cant_angle >> Angular.Radian) + * ((self.weapon.zero_elevation >> Angular.Radian) + + (self.relative_angle >> Angular.Radian))) + + @property + def barrel_azimuth(self) -> Angular: + """Horizontal angle of barrel relative to sight line""" + return Angular.Radian(math.sin(self.cant_angle >> Angular.Radian) + * ((self.weapon.zero_elevation >> Angular.Radian) + + (self.relative_angle >> Angular.Radian))) + + def __post_init__(self): + if not self.look_angle: + self.look_angle = 0 + if not self.relative_angle: + self.relative_angle = 0 + if not self.cant_angle: + self.cant_angle = 0 + if not self.atmo: + self.atmo = Atmo.icao() + if not self.winds: + self.winds = [Wind()] diff --git a/py_ballisticcalc/drag.pyx b/py_ballisticcalc/drag.pyx deleted file mode 100644 index b13d13c..0000000 --- a/py_ballisticcalc/drag.pyx +++ /dev/null @@ -1,208 +0,0 @@ -from libc.math cimport floor, pow -from .bmath.unit import * -from .drag_tables import * - -DragTableG1: int = 1 -DragTableG2: int = 2 -DragTableG5: int = 3 -DragTableG6: int = 4 -DragTableG7: int = 5 -DragTableG8: int = 6 -DragTableGS: int = 7 -DragTableGI: int = 8 - -cdef class BallisticCoefficient: - cdef double _value - cdef int _table - cdef list _table_data - cdef list _curve_data - cdef _weight, _diameter - cdef double _sectional_density, _form_factor - cdef list _custom_drag_table - - def __init__(self, value: double, drag_table: int, weight: Weight, diameter: Distance, custom_drag_table: list): - - self._table = drag_table - - self._weight = weight - self._diameter = diameter - self._sectional_density = self._get_sectional_density() - self._custom_drag_table = custom_drag_table - - if self._table == 0 and len(self._custom_drag_table) > 0: - self._form_factor = 0.999 # defined as form factor in lapua-like custom CD data - self._value = self._get_custom_bc() - self._table_data = make_data_points(self._custom_drag_table) - self._curve_data = calculate_curve(self._table_data) - - elif drag_table < DragTableG1 or DragTableG1 > DragTableGI: - raise ValueError(f"BallisticCoefficient: Unknown drag table {drag_table}") - elif value <= 0: - raise ValueError('BallisticCoefficient: Drag coefficient must be greater than zero') - elif self._table == 0 and len(custom_drag_table) == 0: - raise ValueError('BallisticCoefficient: Custom drag table must be longer than 0') - else: - self._value = value - self._form_factor = self._get_form_factor() - self._table_data = load_drag_table(self._table) - self._curve_data = calculate_curve(self._table_data) - - cpdef double drag(self, double mach): - cdef double cd - cd = calculate_by_curve(self._table_data, self._curve_data, mach) - return cd * 2.08551e-04 / self._value - - cpdef double value(self): - return self._value - - cpdef int table(self): - return self._table - - cdef double _get_custom_bc(self): - return self._sectional_density / self._form_factor - - cdef double _get_form_factor(self): - return self._sectional_density / self._value - - cdef double _get_sectional_density(self): - cdef double w, d - w = self._weight.get_in(WeightGrain) - d = self._diameter.get_in(DistanceInch) - return w / pow(d, 2) / 7000 - - cpdef double standard_cd(self, double mach): - return calculate_by_curve(self._table_data, self._curve_data, mach) - - cpdef double calculated_cd(self, double mach): - return self.standard_cd(mach) * self._form_factor - - cpdef list calculated_drag_function(self): - cdef standard_cd_table - cdef list calculated_cd_table - cdef double st_mach, st_cd, cd - - calculated_cd_table = [] - - for point in self._table_data: - st_mach = point.a() - st_cd = point.b() - cd = self.calculated_cd(st_mach) - calculated_cd_table.append({'A': st_mach, 'B': cd}) - - return calculated_cd_table - - cpdef form_factor(self): - return self._form_factor - -cdef class DataPoint: - cdef double _a, _b - - def __init__(self, a: double, b: double): - self._a = a - self._b = b - - cpdef double a(self): - return self._a - - cpdef double b(self): - return self._b - -cdef class CurvePoint: - cdef double _a, _b, _c - - def __init__(self, a: double, b: double, c: double): - self._a = a - self._b = b - self._c = c - - cpdef double a(self): - return self._a - - cpdef double b(self): - return self._b - - cpdef double c(self): - return self._c - -cpdef list make_data_points(drag_table: list): - table: list = [] - cdef data_point - for point in drag_table: - data_point = DataPoint(point['A'], point['B']) - table.append(data_point) - return table - -cpdef list calculate_curve(list data_points): - cdef double rate, x1, x2, x3, y1, y2, y3, a, b, c - cdef curve = [] - cdef curve_point - cdef int num_points, len_data_points, len_data_range - - rate = (data_points[1].b() - data_points[0].b()) / (data_points[1].a() - data_points[0].a()) - curve = [CurvePoint(0, rate, data_points[0].b() - data_points[0].a() * rate)] - len_data_points = int(len(data_points)) - len_data_range = len_data_points - 1 - - for i in range(1, len_data_range): - x1 = data_points[i - 1].a() - x2 = data_points[i].a() - x3 = data_points[i + 1].a() - y1 = data_points[i - 1].b() - y2 = data_points[i].b() - y3 = data_points[i + 1].b() - a = ((y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)) / ( - (x3 * x3 - x1 * x1) * (x2 - x1) - (x2 * x2 - x1 * x1) * (x3 - x1)) - b = (y2 - y1 - a * (x2 * x2 - x1 * x1)) / (x2 - x1) - c = y1 - (a * x1 * x1 + b * x1) - curve_point = CurvePoint(a, b, c) - curve.append(curve_point) - - num_points = len_data_points - rate = (data_points[num_points - 1].b() - data_points[num_points - 2].b()) / \ - (data_points[num_points - 1].a() - data_points[num_points - 2].a()) - curve_point = CurvePoint(0, rate, data_points[num_points - 1].b() - data_points[num_points - 2].a() * rate) - curve.append(curve_point) - return curve - -cpdef list load_drag_table(drag_table: int): - cdef table - - if drag_table == DragTableG1: - table = make_data_points(TableG1) - elif drag_table == DragTableG2: - table = make_data_points(TableG2) - elif drag_table == DragTableG5: - table = make_data_points(TableG5) - elif drag_table == DragTableG6: - table = make_data_points(TableG6) - elif drag_table == DragTableG7: - table = make_data_points(TableG7) - elif drag_table == DragTableG8: - table = make_data_points(TableG8) - elif drag_table == DragTableGI: - table = make_data_points(TableGI) - elif drag_table == DragTableGS: - table = make_data_points(TableGS) - else: - raise ValueError("Unknown drag table type") - return table - -cpdef double calculate_by_curve(data: list, curve: list, mach: double): - cdef int num_points, mlo, mhi, mid - - num_points = int(len(curve)) - mlo = 0 - mhi = num_points - 2 - - while mhi - mlo > 1: - mid = int(floor(mhi + mlo) / 2.0) - if data[mid].a() < mach: - mlo = mid - else: - mhi = mid - - if data[mhi].a() - mach > mach - data[mlo].a(): - m = mlo - else: - m = mhi - return curve[m].c() + mach * (curve[m].b() + curve[m].a() * mach) diff --git a/py_ballisticcalc/drag_model.py b/py_ballisticcalc/drag_model.py new file mode 100644 index 0000000..38057c8 --- /dev/null +++ b/py_ballisticcalc/drag_model.py @@ -0,0 +1,81 @@ +"""definitions for a bullet's drag model calculation""" + +import typing +from dataclasses import dataclass + +import math + +from .settings import Settings as Set +from .unit import Weight, Distance +from .drag_tables import DragTablesSet + +__all__ = ('DragModel', 'make_data_points') + + +@dataclass +class DragDataPoint: + + CD: float + Mach: float + + def __iter__(self): + yield self.CD + yield self.Mach + + def __repr__(self): + return f"DragDataPoint(CD={self.CD}, Mach={self.Mach})" + + +class DragModel: + """.weight and .diameter are only relevant for computing spin drift""" + def __init__(self, value: float, + drag_table: typing.Iterable, + weight: [float, Weight]=0, + diameter: [float, Distance]=0): + self.__post__init__(value, drag_table, weight, diameter) + + def __post__init__(self, value: float, drag_table, weight, diameter): + table_len = len(drag_table) + error = '' + + if table_len <= 0: + error = 'Custom drag table must be longer than 0' + elif value <= 0: + error = 'Drag coefficient must be greater than zero' + + if error: + raise ValueError(error) + + if drag_table in DragTablesSet: + self.value = value + elif table_len > 0: + self.value = 1 # or 0.999 + else: + raise ValueError('Wrong drag data') + + self.weight = Set.Units.weight(weight) + self.diameter = Set.Units.diameter(diameter) + if weight != 0 and diameter != 0: + self.sectional_density = self._get_sectional_density() + self.form_factor = self._get_form_factor(self.value) + self.drag_table = drag_table + + def _get_form_factor(self, bc: float): + return self.sectional_density / bc + + def _get_sectional_density(self) -> float: + w = self.weight >> Weight.Grain + d = self.diameter >> Distance.Inch + return sectional_density(w, d) + + @staticmethod + def from_mbc(mbc: 'MultiBC'): + return DragModel(1, mbc.cdm, mbc.weight, mbc.diameter) + + +def make_data_points(drag_table: typing.Iterable) -> list: + return [DragDataPoint(point['CD'], point['Mach']) for point in drag_table] + + +def sectional_density(weight: float, diameter: float): + return weight / math.pow(diameter, 2) / 7000 diff --git a/py_ballisticcalc/drag_tables.py b/py_ballisticcalc/drag_tables.py index 9db17e9..da0f297 100644 --- a/py_ballisticcalc/drag_tables.py +++ b/py_ballisticcalc/drag_tables.py @@ -1,666 +1,670 @@ +"""Templates of the common used drag tables""" + TableG1 = [ - {'A': 0.00, 'B': 0.2629}, - {'A': 0.05, 'B': 0.2558}, - {'A': 0.10, 'B': 0.2487}, - {'A': 0.15, 'B': 0.2413}, - {'A': 0.20, 'B': 0.2344}, - {'A': 0.25, 'B': 0.2278}, - {'A': 0.30, 'B': 0.2214}, - {'A': 0.35, 'B': 0.2155}, - {'A': 0.40, 'B': 0.2104}, - {'A': 0.45, 'B': 0.2061}, - {'A': 0.50, 'B': 0.2032}, - {'A': 0.55, 'B': 0.2020}, - {'A': 0.60, 'B': 0.2034}, - {'A': 0.70, 'B': 0.2165}, - {'A': 0.725, 'B': 0.2230}, - {'A': 0.75, 'B': 0.2313}, - {'A': 0.775, 'B': 0.2417}, - {'A': 0.80, 'B': 0.2546}, - {'A': 0.825, 'B': 0.2706}, - {'A': 0.85, 'B': 0.2901}, - {'A': 0.875, 'B': 0.3136}, - {'A': 0.90, 'B': 0.3415}, - {'A': 0.925, 'B': 0.3734}, - {'A': 0.95, 'B': 0.4084}, - {'A': 0.975, 'B': 0.4448}, - {'A': 1.0, 'B': 0.4805}, - {'A': 1.025, 'B': 0.5136}, - {'A': 1.05, 'B': 0.5427}, - {'A': 1.075, 'B': 0.5677}, - {'A': 1.10, 'B': 0.5883}, - {'A': 1.125, 'B': 0.6053}, - {'A': 1.15, 'B': 0.6191}, - {'A': 1.20, 'B': 0.6393}, - {'A': 1.25, 'B': 0.6518}, - {'A': 1.30, 'B': 0.6589}, - {'A': 1.35, 'B': 0.6621}, - {'A': 1.40, 'B': 0.6625}, - {'A': 1.45, 'B': 0.6607}, - {'A': 1.50, 'B': 0.6573}, - {'A': 1.55, 'B': 0.6528}, - {'A': 1.60, 'B': 0.6474}, - {'A': 1.65, 'B': 0.6413}, - {'A': 1.70, 'B': 0.6347}, - {'A': 1.75, 'B': 0.6280}, - {'A': 1.80, 'B': 0.6210}, - {'A': 1.85, 'B': 0.6141}, - {'A': 1.90, 'B': 0.6072}, - {'A': 1.95, 'B': 0.6003}, - {'A': 2.00, 'B': 0.5934}, - {'A': 2.05, 'B': 0.5867}, - {'A': 2.10, 'B': 0.5804}, - {'A': 2.15, 'B': 0.5743}, - {'A': 2.20, 'B': 0.5685}, - {'A': 2.25, 'B': 0.5630}, - {'A': 2.30, 'B': 0.5577}, - {'A': 2.35, 'B': 0.5527}, - {'A': 2.40, 'B': 0.5481}, - {'A': 2.45, 'B': 0.5438}, - {'A': 2.50, 'B': 0.5397}, - {'A': 2.60, 'B': 0.5325}, - {'A': 2.70, 'B': 0.5264}, - {'A': 2.80, 'B': 0.5211}, - {'A': 2.90, 'B': 0.5168}, - {'A': 3.00, 'B': 0.5133}, - {'A': 3.10, 'B': 0.5105}, - {'A': 3.20, 'B': 0.5084}, - {'A': 3.30, 'B': 0.5067}, - {'A': 3.40, 'B': 0.5054}, - {'A': 3.50, 'B': 0.5040}, - {'A': 3.60, 'B': 0.5030}, - {'A': 3.70, 'B': 0.5022}, - {'A': 3.80, 'B': 0.5016}, - {'A': 3.90, 'B': 0.5010}, - {'A': 4.00, 'B': 0.5006}, - {'A': 4.20, 'B': 0.4998}, - {'A': 4.40, 'B': 0.4995}, - {'A': 4.60, 'B': 0.4992}, - {'A': 4.80, 'B': 0.4990}, - {'A': 5.00, 'B': 0.4988} + {'Mach': 0.00, 'CD': 0.2629}, + {'Mach': 0.05, 'CD': 0.2558}, + {'Mach': 0.10, 'CD': 0.2487}, + {'Mach': 0.15, 'CD': 0.2413}, + {'Mach': 0.20, 'CD': 0.2344}, + {'Mach': 0.25, 'CD': 0.2278}, + {'Mach': 0.30, 'CD': 0.2214}, + {'Mach': 0.35, 'CD': 0.2155}, + {'Mach': 0.40, 'CD': 0.2104}, + {'Mach': 0.45, 'CD': 0.2061}, + {'Mach': 0.50, 'CD': 0.2032}, + {'Mach': 0.55, 'CD': 0.2020}, + {'Mach': 0.60, 'CD': 0.2034}, + {'Mach': 0.70, 'CD': 0.2165}, + {'Mach': 0.725, 'CD': 0.2230}, + {'Mach': 0.75, 'CD': 0.2313}, + {'Mach': 0.775, 'CD': 0.2417}, + {'Mach': 0.80, 'CD': 0.2546}, + {'Mach': 0.825, 'CD': 0.2706}, + {'Mach': 0.85, 'CD': 0.2901}, + {'Mach': 0.875, 'CD': 0.3136}, + {'Mach': 0.90, 'CD': 0.3415}, + {'Mach': 0.925, 'CD': 0.3734}, + {'Mach': 0.95, 'CD': 0.4084}, + {'Mach': 0.975, 'CD': 0.4448}, + {'Mach': 1.0, 'CD': 0.4805}, + {'Mach': 1.025, 'CD': 0.5136}, + {'Mach': 1.05, 'CD': 0.5427}, + {'Mach': 1.075, 'CD': 0.5677}, + {'Mach': 1.10, 'CD': 0.5883}, + {'Mach': 1.125, 'CD': 0.6053}, + {'Mach': 1.15, 'CD': 0.6191}, + {'Mach': 1.20, 'CD': 0.6393}, + {'Mach': 1.25, 'CD': 0.6518}, + {'Mach': 1.30, 'CD': 0.6589}, + {'Mach': 1.35, 'CD': 0.6621}, + {'Mach': 1.40, 'CD': 0.6625}, + {'Mach': 1.45, 'CD': 0.6607}, + {'Mach': 1.50, 'CD': 0.6573}, + {'Mach': 1.55, 'CD': 0.6528}, + {'Mach': 1.60, 'CD': 0.6474}, + {'Mach': 1.65, 'CD': 0.6413}, + {'Mach': 1.70, 'CD': 0.6347}, + {'Mach': 1.75, 'CD': 0.6280}, + {'Mach': 1.80, 'CD': 0.6210}, + {'Mach': 1.85, 'CD': 0.6141}, + {'Mach': 1.90, 'CD': 0.6072}, + {'Mach': 1.95, 'CD': 0.6003}, + {'Mach': 2.00, 'CD': 0.5934}, + {'Mach': 2.05, 'CD': 0.5867}, + {'Mach': 2.10, 'CD': 0.5804}, + {'Mach': 2.15, 'CD': 0.5743}, + {'Mach': 2.20, 'CD': 0.5685}, + {'Mach': 2.25, 'CD': 0.5630}, + {'Mach': 2.30, 'CD': 0.5577}, + {'Mach': 2.35, 'CD': 0.5527}, + {'Mach': 2.40, 'CD': 0.5481}, + {'Mach': 2.45, 'CD': 0.5438}, + {'Mach': 2.50, 'CD': 0.5397}, + {'Mach': 2.60, 'CD': 0.5325}, + {'Mach': 2.70, 'CD': 0.5264}, + {'Mach': 2.80, 'CD': 0.5211}, + {'Mach': 2.90, 'CD': 0.5168}, + {'Mach': 3.00, 'CD': 0.5133}, + {'Mach': 3.10, 'CD': 0.5105}, + {'Mach': 3.20, 'CD': 0.5084}, + {'Mach': 3.30, 'CD': 0.5067}, + {'Mach': 3.40, 'CD': 0.5054}, + {'Mach': 3.50, 'CD': 0.5040}, + {'Mach': 3.60, 'CD': 0.5030}, + {'Mach': 3.70, 'CD': 0.5022}, + {'Mach': 3.80, 'CD': 0.5016}, + {'Mach': 3.90, 'CD': 0.5010}, + {'Mach': 4.00, 'CD': 0.5006}, + {'Mach': 4.20, 'CD': 0.4998}, + {'Mach': 4.40, 'CD': 0.4995}, + {'Mach': 4.60, 'CD': 0.4992}, + {'Mach': 4.80, 'CD': 0.4990}, + {'Mach': 5.00, 'CD': 0.4988} ] TableG7 = [ - {'A': 0.00, 'B': 0.1198}, - {'A': 0.05, 'B': 0.1197}, - {'A': 0.10, 'B': 0.1196}, - {'A': 0.15, 'B': 0.1194}, - {'A': 0.20, 'B': 0.1193}, - {'A': 0.25, 'B': 0.1194}, - {'A': 0.30, 'B': 0.1194}, - {'A': 0.35, 'B': 0.1194}, - {'A': 0.40, 'B': 0.1193}, - {'A': 0.45, 'B': 0.1193}, - {'A': 0.50, 'B': 0.1194}, - {'A': 0.55, 'B': 0.1193}, - {'A': 0.60, 'B': 0.1194}, - {'A': 0.65, 'B': 0.1197}, - {'A': 0.70, 'B': 0.1202}, - {'A': 0.725, 'B': 0.1207}, - {'A': 0.75, 'B': 0.1215}, - {'A': 0.775, 'B': 0.1226}, - {'A': 0.80, 'B': 0.1242}, - {'A': 0.825, 'B': 0.1266}, - {'A': 0.85, 'B': 0.1306}, - {'A': 0.875, 'B': 0.1368}, - {'A': 0.90, 'B': 0.1464}, - {'A': 0.925, 'B': 0.1660}, - {'A': 0.95, 'B': 0.2054}, - {'A': 0.975, 'B': 0.2993}, - {'A': 1.0, 'B': 0.3803}, - {'A': 1.025, 'B': 0.4015}, - {'A': 1.05, 'B': 0.4043}, - {'A': 1.075, 'B': 0.4034}, - {'A': 1.10, 'B': 0.4014}, - {'A': 1.125, 'B': 0.3987}, - {'A': 1.15, 'B': 0.3955}, - {'A': 1.20, 'B': 0.3884}, - {'A': 1.25, 'B': 0.3810}, - {'A': 1.30, 'B': 0.3732}, - {'A': 1.35, 'B': 0.3657}, - {'A': 1.40, 'B': 0.3580}, - {'A': 1.50, 'B': 0.3440}, - {'A': 1.55, 'B': 0.3376}, - {'A': 1.60, 'B': 0.3315}, - {'A': 1.65, 'B': 0.3260}, - {'A': 1.70, 'B': 0.3209}, - {'A': 1.75, 'B': 0.3160}, - {'A': 1.80, 'B': 0.3117}, - {'A': 1.85, 'B': 0.3078}, - {'A': 1.90, 'B': 0.3042}, - {'A': 1.95, 'B': 0.3010}, - {'A': 2.00, 'B': 0.2980}, - {'A': 2.05, 'B': 0.2951}, - {'A': 2.10, 'B': 0.2922}, - {'A': 2.15, 'B': 0.2892}, - {'A': 2.20, 'B': 0.2864}, - {'A': 2.25, 'B': 0.2835}, - {'A': 2.30, 'B': 0.2807}, - {'A': 2.35, 'B': 0.2779}, - {'A': 2.40, 'B': 0.2752}, - {'A': 2.45, 'B': 0.2725}, - {'A': 2.50, 'B': 0.2697}, - {'A': 2.55, 'B': 0.2670}, - {'A': 2.60, 'B': 0.2643}, - {'A': 2.65, 'B': 0.2615}, - {'A': 2.70, 'B': 0.2588}, - {'A': 2.75, 'B': 0.2561}, - {'A': 2.80, 'B': 0.2533}, - {'A': 2.85, 'B': 0.2506}, - {'A': 2.90, 'B': 0.2479}, - {'A': 2.95, 'B': 0.2451}, - {'A': 3.00, 'B': 0.2424}, - {'A': 3.10, 'B': 0.2368}, - {'A': 3.20, 'B': 0.2313}, - {'A': 3.30, 'B': 0.2258}, - {'A': 3.40, 'B': 0.2205}, - {'A': 3.50, 'B': 0.2154}, - {'A': 3.60, 'B': 0.2106}, - {'A': 3.70, 'B': 0.2060}, - {'A': 3.80, 'B': 0.2017}, - {'A': 3.90, 'B': 0.1975}, - {'A': 4.00, 'B': 0.1935}, - {'A': 4.20, 'B': 0.1861}, - {'A': 4.40, 'B': 0.1793}, - {'A': 4.60, 'B': 0.1730}, - {'A': 4.80, 'B': 0.1672}, - {'A': 5.00, 'B': 0.1618}, + {'Mach': 0.00, 'CD': 0.1198}, + {'Mach': 0.05, 'CD': 0.1197}, + {'Mach': 0.10, 'CD': 0.1196}, + {'Mach': 0.15, 'CD': 0.1194}, + {'Mach': 0.20, 'CD': 0.1193}, + {'Mach': 0.25, 'CD': 0.1194}, + {'Mach': 0.30, 'CD': 0.1194}, + {'Mach': 0.35, 'CD': 0.1194}, + {'Mach': 0.40, 'CD': 0.1193}, + {'Mach': 0.45, 'CD': 0.1193}, + {'Mach': 0.50, 'CD': 0.1194}, + {'Mach': 0.55, 'CD': 0.1193}, + {'Mach': 0.60, 'CD': 0.1194}, + {'Mach': 0.65, 'CD': 0.1197}, + {'Mach': 0.70, 'CD': 0.1202}, + {'Mach': 0.725, 'CD': 0.1207}, + {'Mach': 0.75, 'CD': 0.1215}, + {'Mach': 0.775, 'CD': 0.1226}, + {'Mach': 0.80, 'CD': 0.1242}, + {'Mach': 0.825, 'CD': 0.1266}, + {'Mach': 0.85, 'CD': 0.1306}, + {'Mach': 0.875, 'CD': 0.1368}, + {'Mach': 0.90, 'CD': 0.1464}, + {'Mach': 0.925, 'CD': 0.1660}, + {'Mach': 0.95, 'CD': 0.2054}, + {'Mach': 0.975, 'CD': 0.2993}, + {'Mach': 1.0, 'CD': 0.3803}, + {'Mach': 1.025, 'CD': 0.4015}, + {'Mach': 1.05, 'CD': 0.4043}, + {'Mach': 1.075, 'CD': 0.4034}, + {'Mach': 1.10, 'CD': 0.4014}, + {'Mach': 1.125, 'CD': 0.3987}, + {'Mach': 1.15, 'CD': 0.3955}, + {'Mach': 1.20, 'CD': 0.3884}, + {'Mach': 1.25, 'CD': 0.3810}, + {'Mach': 1.30, 'CD': 0.3732}, + {'Mach': 1.35, 'CD': 0.3657}, + {'Mach': 1.40, 'CD': 0.3580}, + {'Mach': 1.50, 'CD': 0.3440}, + {'Mach': 1.55, 'CD': 0.3376}, + {'Mach': 1.60, 'CD': 0.3315}, + {'Mach': 1.65, 'CD': 0.3260}, + {'Mach': 1.70, 'CD': 0.3209}, + {'Mach': 1.75, 'CD': 0.3160}, + {'Mach': 1.80, 'CD': 0.3117}, + {'Mach': 1.85, 'CD': 0.3078}, + {'Mach': 1.90, 'CD': 0.3042}, + {'Mach': 1.95, 'CD': 0.3010}, + {'Mach': 2.00, 'CD': 0.2980}, + {'Mach': 2.05, 'CD': 0.2951}, + {'Mach': 2.10, 'CD': 0.2922}, + {'Mach': 2.15, 'CD': 0.2892}, + {'Mach': 2.20, 'CD': 0.2864}, + {'Mach': 2.25, 'CD': 0.2835}, + {'Mach': 2.30, 'CD': 0.2807}, + {'Mach': 2.35, 'CD': 0.2779}, + {'Mach': 2.40, 'CD': 0.2752}, + {'Mach': 2.45, 'CD': 0.2725}, + {'Mach': 2.50, 'CD': 0.2697}, + {'Mach': 2.55, 'CD': 0.2670}, + {'Mach': 2.60, 'CD': 0.2643}, + {'Mach': 2.65, 'CD': 0.2615}, + {'Mach': 2.70, 'CD': 0.2588}, + {'Mach': 2.75, 'CD': 0.2561}, + {'Mach': 2.80, 'CD': 0.2533}, + {'Mach': 2.85, 'CD': 0.2506}, + {'Mach': 2.90, 'CD': 0.2479}, + {'Mach': 2.95, 'CD': 0.2451}, + {'Mach': 3.00, 'CD': 0.2424}, + {'Mach': 3.10, 'CD': 0.2368}, + {'Mach': 3.20, 'CD': 0.2313}, + {'Mach': 3.30, 'CD': 0.2258}, + {'Mach': 3.40, 'CD': 0.2205}, + {'Mach': 3.50, 'CD': 0.2154}, + {'Mach': 3.60, 'CD': 0.2106}, + {'Mach': 3.70, 'CD': 0.2060}, + {'Mach': 3.80, 'CD': 0.2017}, + {'Mach': 3.90, 'CD': 0.1975}, + {'Mach': 4.00, 'CD': 0.1935}, + {'Mach': 4.20, 'CD': 0.1861}, + {'Mach': 4.40, 'CD': 0.1793}, + {'Mach': 4.60, 'CD': 0.1730}, + {'Mach': 4.80, 'CD': 0.1672}, + {'Mach': 5.00, 'CD': 0.1618}, ] TableG2 = [ - {'A': 0.00, 'B': 0.2303}, - {'A': 0.05, 'B': 0.2298}, - {'A': 0.10, 'B': 0.2287}, - {'A': 0.15, 'B': 0.2271}, - {'A': 0.20, 'B': 0.2251}, - {'A': 0.25, 'B': 0.2227}, - {'A': 0.30, 'B': 0.2196}, - {'A': 0.35, 'B': 0.2156}, - {'A': 0.40, 'B': 0.2107}, - {'A': 0.45, 'B': 0.2048}, - {'A': 0.50, 'B': 0.1980}, - {'A': 0.55, 'B': 0.1905}, - {'A': 0.60, 'B': 0.1828}, - {'A': 0.65, 'B': 0.1758}, - {'A': 0.70, 'B': 0.1702}, - {'A': 0.75, 'B': 0.1669}, - {'A': 0.775, 'B': 0.1664}, - {'A': 0.80, 'B': 0.1667}, - {'A': 0.825, 'B': 0.1682}, - {'A': 0.85, 'B': 0.1711}, - {'A': 0.875, 'B': 0.1761}, - {'A': 0.90, 'B': 0.1831}, - {'A': 0.925, 'B': 0.2004}, - {'A': 0.95, 'B': 0.2589}, - {'A': 0.975, 'B': 0.3492}, - {'A': 1.0, 'B': 0.3983}, - {'A': 1.025, 'B': 0.4075}, - {'A': 1.05, 'B': 0.4103}, - {'A': 1.075, 'B': 0.4114}, - {'A': 1.10, 'B': 0.4106}, - {'A': 1.125, 'B': 0.4089}, - {'A': 1.15, 'B': 0.4068}, - {'A': 1.175, 'B': 0.4046}, - {'A': 1.20, 'B': 0.4021}, - {'A': 1.25, 'B': 0.3966}, - {'A': 1.30, 'B': 0.3904}, - {'A': 1.35, 'B': 0.3835}, - {'A': 1.40, 'B': 0.3759}, - {'A': 1.45, 'B': 0.3678}, - {'A': 1.50, 'B': 0.3594}, - {'A': 1.55, 'B': 0.3512}, - {'A': 1.60, 'B': 0.3432}, - {'A': 1.65, 'B': 0.3356}, - {'A': 1.70, 'B': 0.3282}, - {'A': 1.75, 'B': 0.3213}, - {'A': 1.80, 'B': 0.3149}, - {'A': 1.85, 'B': 0.3089}, - {'A': 1.90, 'B': 0.3033}, - {'A': 1.95, 'B': 0.2982}, - {'A': 2.00, 'B': 0.2933}, - {'A': 2.05, 'B': 0.2889}, - {'A': 2.10, 'B': 0.2846}, - {'A': 2.15, 'B': 0.2806}, - {'A': 2.20, 'B': 0.2768}, - {'A': 2.25, 'B': 0.2731}, - {'A': 2.30, 'B': 0.2696}, - {'A': 2.35, 'B': 0.2663}, - {'A': 2.40, 'B': 0.2632}, - {'A': 2.45, 'B': 0.2602}, - {'A': 2.50, 'B': 0.2572}, - {'A': 2.55, 'B': 0.2543}, - {'A': 2.60, 'B': 0.2515}, - {'A': 2.65, 'B': 0.2487}, - {'A': 2.70, 'B': 0.2460}, - {'A': 2.75, 'B': 0.2433}, - {'A': 2.80, 'B': 0.2408}, - {'A': 2.85, 'B': 0.2382}, - {'A': 2.90, 'B': 0.2357}, - {'A': 2.95, 'B': 0.2333}, - {'A': 3.00, 'B': 0.2309}, - {'A': 3.10, 'B': 0.2262}, - {'A': 3.20, 'B': 0.2217}, - {'A': 3.30, 'B': 0.2173}, - {'A': 3.40, 'B': 0.2132}, - {'A': 3.50, 'B': 0.2091}, - {'A': 3.60, 'B': 0.2052}, - {'A': 3.70, 'B': 0.2014}, - {'A': 3.80, 'B': 0.1978}, - {'A': 3.90, 'B': 0.1944}, - {'A': 4.00, 'B': 0.1912}, - {'A': 4.20, 'B': 0.1851}, - {'A': 4.40, 'B': 0.1794}, - {'A': 4.60, 'B': 0.1741}, - {'A': 4.80, 'B': 0.1693}, - {'A': 5.00, 'B': 0.1648}, + {'Mach': 0.00, 'CD': 0.2303}, + {'Mach': 0.05, 'CD': 0.2298}, + {'Mach': 0.10, 'CD': 0.2287}, + {'Mach': 0.15, 'CD': 0.2271}, + {'Mach': 0.20, 'CD': 0.2251}, + {'Mach': 0.25, 'CD': 0.2227}, + {'Mach': 0.30, 'CD': 0.2196}, + {'Mach': 0.35, 'CD': 0.2156}, + {'Mach': 0.40, 'CD': 0.2107}, + {'Mach': 0.45, 'CD': 0.2048}, + {'Mach': 0.50, 'CD': 0.1980}, + {'Mach': 0.55, 'CD': 0.1905}, + {'Mach': 0.60, 'CD': 0.1828}, + {'Mach': 0.65, 'CD': 0.1758}, + {'Mach': 0.70, 'CD': 0.1702}, + {'Mach': 0.75, 'CD': 0.1669}, + {'Mach': 0.775, 'CD': 0.1664}, + {'Mach': 0.80, 'CD': 0.1667}, + {'Mach': 0.825, 'CD': 0.1682}, + {'Mach': 0.85, 'CD': 0.1711}, + {'Mach': 0.875, 'CD': 0.1761}, + {'Mach': 0.90, 'CD': 0.1831}, + {'Mach': 0.925, 'CD': 0.2004}, + {'Mach': 0.95, 'CD': 0.2589}, + {'Mach': 0.975, 'CD': 0.3492}, + {'Mach': 1.0, 'CD': 0.3983}, + {'Mach': 1.025, 'CD': 0.4075}, + {'Mach': 1.05, 'CD': 0.4103}, + {'Mach': 1.075, 'CD': 0.4114}, + {'Mach': 1.10, 'CD': 0.4106}, + {'Mach': 1.125, 'CD': 0.4089}, + {'Mach': 1.15, 'CD': 0.4068}, + {'Mach': 1.175, 'CD': 0.4046}, + {'Mach': 1.20, 'CD': 0.4021}, + {'Mach': 1.25, 'CD': 0.3966}, + {'Mach': 1.30, 'CD': 0.3904}, + {'Mach': 1.35, 'CD': 0.3835}, + {'Mach': 1.40, 'CD': 0.3759}, + {'Mach': 1.45, 'CD': 0.3678}, + {'Mach': 1.50, 'CD': 0.3594}, + {'Mach': 1.55, 'CD': 0.3512}, + {'Mach': 1.60, 'CD': 0.3432}, + {'Mach': 1.65, 'CD': 0.3356}, + {'Mach': 1.70, 'CD': 0.3282}, + {'Mach': 1.75, 'CD': 0.3213}, + {'Mach': 1.80, 'CD': 0.3149}, + {'Mach': 1.85, 'CD': 0.3089}, + {'Mach': 1.90, 'CD': 0.3033}, + {'Mach': 1.95, 'CD': 0.2982}, + {'Mach': 2.00, 'CD': 0.2933}, + {'Mach': 2.05, 'CD': 0.2889}, + {'Mach': 2.10, 'CD': 0.2846}, + {'Mach': 2.15, 'CD': 0.2806}, + {'Mach': 2.20, 'CD': 0.2768}, + {'Mach': 2.25, 'CD': 0.2731}, + {'Mach': 2.30, 'CD': 0.2696}, + {'Mach': 2.35, 'CD': 0.2663}, + {'Mach': 2.40, 'CD': 0.2632}, + {'Mach': 2.45, 'CD': 0.2602}, + {'Mach': 2.50, 'CD': 0.2572}, + {'Mach': 2.55, 'CD': 0.2543}, + {'Mach': 2.60, 'CD': 0.2515}, + {'Mach': 2.65, 'CD': 0.2487}, + {'Mach': 2.70, 'CD': 0.2460}, + {'Mach': 2.75, 'CD': 0.2433}, + {'Mach': 2.80, 'CD': 0.2408}, + {'Mach': 2.85, 'CD': 0.2382}, + {'Mach': 2.90, 'CD': 0.2357}, + {'Mach': 2.95, 'CD': 0.2333}, + {'Mach': 3.00, 'CD': 0.2309}, + {'Mach': 3.10, 'CD': 0.2262}, + {'Mach': 3.20, 'CD': 0.2217}, + {'Mach': 3.30, 'CD': 0.2173}, + {'Mach': 3.40, 'CD': 0.2132}, + {'Mach': 3.50, 'CD': 0.2091}, + {'Mach': 3.60, 'CD': 0.2052}, + {'Mach': 3.70, 'CD': 0.2014}, + {'Mach': 3.80, 'CD': 0.1978}, + {'Mach': 3.90, 'CD': 0.1944}, + {'Mach': 4.00, 'CD': 0.1912}, + {'Mach': 4.20, 'CD': 0.1851}, + {'Mach': 4.40, 'CD': 0.1794}, + {'Mach': 4.60, 'CD': 0.1741}, + {'Mach': 4.80, 'CD': 0.1693}, + {'Mach': 5.00, 'CD': 0.1648}, ] TableG5 = [ - {'A': 0.00, 'B': 0.1710}, - {'A': 0.05, 'B': 0.1719}, - {'A': 0.10, 'B': 0.1727}, - {'A': 0.15, 'B': 0.1732}, - {'A': 0.20, 'B': 0.1734}, - {'A': 0.25, 'B': 0.1730}, - {'A': 0.30, 'B': 0.1718}, - {'A': 0.35, 'B': 0.1696}, - {'A': 0.40, 'B': 0.1668}, - {'A': 0.45, 'B': 0.1637}, - {'A': 0.50, 'B': 0.1603}, - {'A': 0.55, 'B': 0.1566}, - {'A': 0.60, 'B': 0.1529}, - {'A': 0.65, 'B': 0.1497}, - {'A': 0.70, 'B': 0.1473}, - {'A': 0.75, 'B': 0.1463}, - {'A': 0.80, 'B': 0.1489}, - {'A': 0.85, 'B': 0.1583}, - {'A': 0.875, 'B': 0.1672}, - {'A': 0.90, 'B': 0.1815}, - {'A': 0.925, 'B': 0.2051}, - {'A': 0.95, 'B': 0.2413}, - {'A': 0.975, 'B': 0.2884}, - {'A': 1.0, 'B': 0.3379}, - {'A': 1.025, 'B': 0.3785}, - {'A': 1.05, 'B': 0.4032}, - {'A': 1.075, 'B': 0.4147}, - {'A': 1.10, 'B': 0.4201}, - {'A': 1.15, 'B': 0.4278}, - {'A': 1.20, 'B': 0.4338}, - {'A': 1.25, 'B': 0.4373}, - {'A': 1.30, 'B': 0.4392}, - {'A': 1.35, 'B': 0.4403}, - {'A': 1.40, 'B': 0.4406}, - {'A': 1.45, 'B': 0.4401}, - {'A': 1.50, 'B': 0.4386}, - {'A': 1.55, 'B': 0.4362}, - {'A': 1.60, 'B': 0.4328}, - {'A': 1.65, 'B': 0.4286}, - {'A': 1.70, 'B': 0.4237}, - {'A': 1.75, 'B': 0.4182}, - {'A': 1.80, 'B': 0.4121}, - {'A': 1.85, 'B': 0.4057}, - {'A': 1.90, 'B': 0.3991}, - {'A': 1.95, 'B': 0.3926}, - {'A': 2.00, 'B': 0.3861}, - {'A': 2.05, 'B': 0.3800}, - {'A': 2.10, 'B': 0.3741}, - {'A': 2.15, 'B': 0.3684}, - {'A': 2.20, 'B': 0.3630}, - {'A': 2.25, 'B': 0.3578}, - {'A': 2.30, 'B': 0.3529}, - {'A': 2.35, 'B': 0.3481}, - {'A': 2.40, 'B': 0.3435}, - {'A': 2.45, 'B': 0.3391}, - {'A': 2.50, 'B': 0.3349}, - {'A': 2.60, 'B': 0.3269}, - {'A': 2.70, 'B': 0.3194}, - {'A': 2.80, 'B': 0.3125}, - {'A': 2.90, 'B': 0.3060}, - {'A': 3.00, 'B': 0.2999}, - {'A': 3.10, 'B': 0.2942}, - {'A': 3.20, 'B': 0.2889}, - {'A': 3.30, 'B': 0.2838}, - {'A': 3.40, 'B': 0.2790}, - {'A': 3.50, 'B': 0.2745}, - {'A': 3.60, 'B': 0.2703}, - {'A': 3.70, 'B': 0.2662}, - {'A': 3.80, 'B': 0.2624}, - {'A': 3.90, 'B': 0.2588}, - {'A': 4.00, 'B': 0.2553}, - {'A': 4.20, 'B': 0.2488}, - {'A': 4.40, 'B': 0.2429}, - {'A': 4.60, 'B': 0.2376}, - {'A': 4.80, 'B': 0.2326}, - {'A': 5.00, 'B': 0.2280}, + {'Mach': 0.00, 'CD': 0.1710}, + {'Mach': 0.05, 'CD': 0.1719}, + {'Mach': 0.10, 'CD': 0.1727}, + {'Mach': 0.15, 'CD': 0.1732}, + {'Mach': 0.20, 'CD': 0.1734}, + {'Mach': 0.25, 'CD': 0.1730}, + {'Mach': 0.30, 'CD': 0.1718}, + {'Mach': 0.35, 'CD': 0.1696}, + {'Mach': 0.40, 'CD': 0.1668}, + {'Mach': 0.45, 'CD': 0.1637}, + {'Mach': 0.50, 'CD': 0.1603}, + {'Mach': 0.55, 'CD': 0.1566}, + {'Mach': 0.60, 'CD': 0.1529}, + {'Mach': 0.65, 'CD': 0.1497}, + {'Mach': 0.70, 'CD': 0.1473}, + {'Mach': 0.75, 'CD': 0.1463}, + {'Mach': 0.80, 'CD': 0.1489}, + {'Mach': 0.85, 'CD': 0.1583}, + {'Mach': 0.875, 'CD': 0.1672}, + {'Mach': 0.90, 'CD': 0.1815}, + {'Mach': 0.925, 'CD': 0.2051}, + {'Mach': 0.95, 'CD': 0.2413}, + {'Mach': 0.975, 'CD': 0.2884}, + {'Mach': 1.0, 'CD': 0.3379}, + {'Mach': 1.025, 'CD': 0.3785}, + {'Mach': 1.05, 'CD': 0.4032}, + {'Mach': 1.075, 'CD': 0.4147}, + {'Mach': 1.10, 'CD': 0.4201}, + {'Mach': 1.15, 'CD': 0.4278}, + {'Mach': 1.20, 'CD': 0.4338}, + {'Mach': 1.25, 'CD': 0.4373}, + {'Mach': 1.30, 'CD': 0.4392}, + {'Mach': 1.35, 'CD': 0.4403}, + {'Mach': 1.40, 'CD': 0.4406}, + {'Mach': 1.45, 'CD': 0.4401}, + {'Mach': 1.50, 'CD': 0.4386}, + {'Mach': 1.55, 'CD': 0.4362}, + {'Mach': 1.60, 'CD': 0.4328}, + {'Mach': 1.65, 'CD': 0.4286}, + {'Mach': 1.70, 'CD': 0.4237}, + {'Mach': 1.75, 'CD': 0.4182}, + {'Mach': 1.80, 'CD': 0.4121}, + {'Mach': 1.85, 'CD': 0.4057}, + {'Mach': 1.90, 'CD': 0.3991}, + {'Mach': 1.95, 'CD': 0.3926}, + {'Mach': 2.00, 'CD': 0.3861}, + {'Mach': 2.05, 'CD': 0.3800}, + {'Mach': 2.10, 'CD': 0.3741}, + {'Mach': 2.15, 'CD': 0.3684}, + {'Mach': 2.20, 'CD': 0.3630}, + {'Mach': 2.25, 'CD': 0.3578}, + {'Mach': 2.30, 'CD': 0.3529}, + {'Mach': 2.35, 'CD': 0.3481}, + {'Mach': 2.40, 'CD': 0.3435}, + {'Mach': 2.45, 'CD': 0.3391}, + {'Mach': 2.50, 'CD': 0.3349}, + {'Mach': 2.60, 'CD': 0.3269}, + {'Mach': 2.70, 'CD': 0.3194}, + {'Mach': 2.80, 'CD': 0.3125}, + {'Mach': 2.90, 'CD': 0.3060}, + {'Mach': 3.00, 'CD': 0.2999}, + {'Mach': 3.10, 'CD': 0.2942}, + {'Mach': 3.20, 'CD': 0.2889}, + {'Mach': 3.30, 'CD': 0.2838}, + {'Mach': 3.40, 'CD': 0.2790}, + {'Mach': 3.50, 'CD': 0.2745}, + {'Mach': 3.60, 'CD': 0.2703}, + {'Mach': 3.70, 'CD': 0.2662}, + {'Mach': 3.80, 'CD': 0.2624}, + {'Mach': 3.90, 'CD': 0.2588}, + {'Mach': 4.00, 'CD': 0.2553}, + {'Mach': 4.20, 'CD': 0.2488}, + {'Mach': 4.40, 'CD': 0.2429}, + {'Mach': 4.60, 'CD': 0.2376}, + {'Mach': 4.80, 'CD': 0.2326}, + {'Mach': 5.00, 'CD': 0.2280}, ] TableG6 = [ - {'A': 0.00, 'B': 0.2617}, - {'A': 0.05, 'B': 0.2553}, - {'A': 0.10, 'B': 0.2491}, - {'A': 0.15, 'B': 0.2432}, - {'A': 0.20, 'B': 0.2376}, - {'A': 0.25, 'B': 0.2324}, - {'A': 0.30, 'B': 0.2278}, - {'A': 0.35, 'B': 0.2238}, - {'A': 0.40, 'B': 0.2205}, - {'A': 0.45, 'B': 0.2177}, - {'A': 0.50, 'B': 0.2155}, - {'A': 0.55, 'B': 0.2138}, - {'A': 0.60, 'B': 0.2126}, - {'A': 0.65, 'B': 0.2121}, - {'A': 0.70, 'B': 0.2122}, - {'A': 0.75, 'B': 0.2132}, - {'A': 0.80, 'B': 0.2154}, - {'A': 0.85, 'B': 0.2194}, - {'A': 0.875, 'B': 0.2229}, - {'A': 0.90, 'B': 0.2297}, - {'A': 0.925, 'B': 0.2449}, - {'A': 0.95, 'B': 0.2732}, - {'A': 0.975, 'B': 0.3141}, - {'A': 1.0, 'B': 0.3597}, - {'A': 1.025, 'B': 0.3994}, - {'A': 1.05, 'B': 0.4261}, - {'A': 1.075, 'B': 0.4402}, - {'A': 1.10, 'B': 0.4465}, - {'A': 1.125, 'B': 0.4490}, - {'A': 1.15, 'B': 0.4497}, - {'A': 1.175, 'B': 0.4494}, - {'A': 1.20, 'B': 0.4482}, - {'A': 1.225, 'B': 0.4464}, - {'A': 1.25, 'B': 0.4441}, - {'A': 1.30, 'B': 0.4390}, - {'A': 1.35, 'B': 0.4336}, - {'A': 1.40, 'B': 0.4279}, - {'A': 1.45, 'B': 0.4221}, - {'A': 1.50, 'B': 0.4162}, - {'A': 1.55, 'B': 0.4102}, - {'A': 1.60, 'B': 0.4042}, - {'A': 1.65, 'B': 0.3981}, - {'A': 1.70, 'B': 0.3919}, - {'A': 1.75, 'B': 0.3855}, - {'A': 1.80, 'B': 0.3788}, - {'A': 1.85, 'B': 0.3721}, - {'A': 1.90, 'B': 0.3652}, - {'A': 1.95, 'B': 0.3583}, - {'A': 2.00, 'B': 0.3515}, - {'A': 2.05, 'B': 0.3447}, - {'A': 2.10, 'B': 0.3381}, - {'A': 2.15, 'B': 0.3314}, - {'A': 2.20, 'B': 0.3249}, - {'A': 2.25, 'B': 0.3185}, - {'A': 2.30, 'B': 0.3122}, - {'A': 2.35, 'B': 0.3060}, - {'A': 2.40, 'B': 0.3000}, - {'A': 2.45, 'B': 0.2941}, - {'A': 2.50, 'B': 0.2883}, - {'A': 2.60, 'B': 0.2772}, - {'A': 2.70, 'B': 0.2668}, - {'A': 2.80, 'B': 0.2574}, - {'A': 2.90, 'B': 0.2487}, - {'A': 3.00, 'B': 0.2407}, - {'A': 3.10, 'B': 0.2333}, - {'A': 3.20, 'B': 0.2265}, - {'A': 3.30, 'B': 0.2202}, - {'A': 3.40, 'B': 0.2144}, - {'A': 3.50, 'B': 0.2089}, - {'A': 3.60, 'B': 0.2039}, - {'A': 3.70, 'B': 0.1991}, - {'A': 3.80, 'B': 0.1947}, - {'A': 3.90, 'B': 0.1905}, - {'A': 4.00, 'B': 0.1866}, - {'A': 4.20, 'B': 0.1794}, - {'A': 4.40, 'B': 0.1730}, - {'A': 4.60, 'B': 0.1673}, - {'A': 4.80, 'B': 0.1621}, - {'A': 5.00, 'B': 0.1574}, + {'Mach': 0.00, 'CD': 0.2617}, + {'Mach': 0.05, 'CD': 0.2553}, + {'Mach': 0.10, 'CD': 0.2491}, + {'Mach': 0.15, 'CD': 0.2432}, + {'Mach': 0.20, 'CD': 0.2376}, + {'Mach': 0.25, 'CD': 0.2324}, + {'Mach': 0.30, 'CD': 0.2278}, + {'Mach': 0.35, 'CD': 0.2238}, + {'Mach': 0.40, 'CD': 0.2205}, + {'Mach': 0.45, 'CD': 0.2177}, + {'Mach': 0.50, 'CD': 0.2155}, + {'Mach': 0.55, 'CD': 0.2138}, + {'Mach': 0.60, 'CD': 0.2126}, + {'Mach': 0.65, 'CD': 0.2121}, + {'Mach': 0.70, 'CD': 0.2122}, + {'Mach': 0.75, 'CD': 0.2132}, + {'Mach': 0.80, 'CD': 0.2154}, + {'Mach': 0.85, 'CD': 0.2194}, + {'Mach': 0.875, 'CD': 0.2229}, + {'Mach': 0.90, 'CD': 0.2297}, + {'Mach': 0.925, 'CD': 0.2449}, + {'Mach': 0.95, 'CD': 0.2732}, + {'Mach': 0.975, 'CD': 0.3141}, + {'Mach': 1.0, 'CD': 0.3597}, + {'Mach': 1.025, 'CD': 0.3994}, + {'Mach': 1.05, 'CD': 0.4261}, + {'Mach': 1.075, 'CD': 0.4402}, + {'Mach': 1.10, 'CD': 0.4465}, + {'Mach': 1.125, 'CD': 0.4490}, + {'Mach': 1.15, 'CD': 0.4497}, + {'Mach': 1.175, 'CD': 0.4494}, + {'Mach': 1.20, 'CD': 0.4482}, + {'Mach': 1.225, 'CD': 0.4464}, + {'Mach': 1.25, 'CD': 0.4441}, + {'Mach': 1.30, 'CD': 0.4390}, + {'Mach': 1.35, 'CD': 0.4336}, + {'Mach': 1.40, 'CD': 0.4279}, + {'Mach': 1.45, 'CD': 0.4221}, + {'Mach': 1.50, 'CD': 0.4162}, + {'Mach': 1.55, 'CD': 0.4102}, + {'Mach': 1.60, 'CD': 0.4042}, + {'Mach': 1.65, 'CD': 0.3981}, + {'Mach': 1.70, 'CD': 0.3919}, + {'Mach': 1.75, 'CD': 0.3855}, + {'Mach': 1.80, 'CD': 0.3788}, + {'Mach': 1.85, 'CD': 0.3721}, + {'Mach': 1.90, 'CD': 0.3652}, + {'Mach': 1.95, 'CD': 0.3583}, + {'Mach': 2.00, 'CD': 0.3515}, + {'Mach': 2.05, 'CD': 0.3447}, + {'Mach': 2.10, 'CD': 0.3381}, + {'Mach': 2.15, 'CD': 0.3314}, + {'Mach': 2.20, 'CD': 0.3249}, + {'Mach': 2.25, 'CD': 0.3185}, + {'Mach': 2.30, 'CD': 0.3122}, + {'Mach': 2.35, 'CD': 0.3060}, + {'Mach': 2.40, 'CD': 0.3000}, + {'Mach': 2.45, 'CD': 0.2941}, + {'Mach': 2.50, 'CD': 0.2883}, + {'Mach': 2.60, 'CD': 0.2772}, + {'Mach': 2.70, 'CD': 0.2668}, + {'Mach': 2.80, 'CD': 0.2574}, + {'Mach': 2.90, 'CD': 0.2487}, + {'Mach': 3.00, 'CD': 0.2407}, + {'Mach': 3.10, 'CD': 0.2333}, + {'Mach': 3.20, 'CD': 0.2265}, + {'Mach': 3.30, 'CD': 0.2202}, + {'Mach': 3.40, 'CD': 0.2144}, + {'Mach': 3.50, 'CD': 0.2089}, + {'Mach': 3.60, 'CD': 0.2039}, + {'Mach': 3.70, 'CD': 0.1991}, + {'Mach': 3.80, 'CD': 0.1947}, + {'Mach': 3.90, 'CD': 0.1905}, + {'Mach': 4.00, 'CD': 0.1866}, + {'Mach': 4.20, 'CD': 0.1794}, + {'Mach': 4.40, 'CD': 0.1730}, + {'Mach': 4.60, 'CD': 0.1673}, + {'Mach': 4.80, 'CD': 0.1621}, + {'Mach': 5.00, 'CD': 0.1574}, ] TableG8 = [ - {'A': 0.00, 'B': 0.2105}, - {'A': 0.05, 'B': 0.2105}, - {'A': 0.10, 'B': 0.2104}, - {'A': 0.15, 'B': 0.2104}, - {'A': 0.20, 'B': 0.2103}, - {'A': 0.25, 'B': 0.2103}, - {'A': 0.30, 'B': 0.2103}, - {'A': 0.35, 'B': 0.2103}, - {'A': 0.40, 'B': 0.2103}, - {'A': 0.45, 'B': 0.2102}, - {'A': 0.50, 'B': 0.2102}, - {'A': 0.55, 'B': 0.2102}, - {'A': 0.60, 'B': 0.2102}, - {'A': 0.65, 'B': 0.2102}, - {'A': 0.70, 'B': 0.2103}, - {'A': 0.75, 'B': 0.2103}, - {'A': 0.80, 'B': 0.2104}, - {'A': 0.825, 'B': 0.2104}, - {'A': 0.85, 'B': 0.2105}, - {'A': 0.875, 'B': 0.2106}, - {'A': 0.90, 'B': 0.2109}, - {'A': 0.925, 'B': 0.2183}, - {'A': 0.95, 'B': 0.2571}, - {'A': 0.975, 'B': 0.3358}, - {'A': 1.0, 'B': 0.4068}, - {'A': 1.025, 'B': 0.4378}, - {'A': 1.05, 'B': 0.4476}, - {'A': 1.075, 'B': 0.4493}, - {'A': 1.10, 'B': 0.4477}, - {'A': 1.125, 'B': 0.4450}, - {'A': 1.15, 'B': 0.4419}, - {'A': 1.20, 'B': 0.4353}, - {'A': 1.25, 'B': 0.4283}, - {'A': 1.30, 'B': 0.4208}, - {'A': 1.35, 'B': 0.4133}, - {'A': 1.40, 'B': 0.4059}, - {'A': 1.45, 'B': 0.3986}, - {'A': 1.50, 'B': 0.3915}, - {'A': 1.55, 'B': 0.3845}, - {'A': 1.60, 'B': 0.3777}, - {'A': 1.65, 'B': 0.3710}, - {'A': 1.70, 'B': 0.3645}, - {'A': 1.75, 'B': 0.3581}, - {'A': 1.80, 'B': 0.3519}, - {'A': 1.85, 'B': 0.3458}, - {'A': 1.90, 'B': 0.3400}, - {'A': 1.95, 'B': 0.3343}, - {'A': 2.00, 'B': 0.3288}, - {'A': 2.05, 'B': 0.3234}, - {'A': 2.10, 'B': 0.3182}, - {'A': 2.15, 'B': 0.3131}, - {'A': 2.20, 'B': 0.3081}, - {'A': 2.25, 'B': 0.3032}, - {'A': 2.30, 'B': 0.2983}, - {'A': 2.35, 'B': 0.2937}, - {'A': 2.40, 'B': 0.2891}, - {'A': 2.45, 'B': 0.2845}, - {'A': 2.50, 'B': 0.2802}, - {'A': 2.60, 'B': 0.2720}, - {'A': 2.70, 'B': 0.2642}, - {'A': 2.80, 'B': 0.2569}, - {'A': 2.90, 'B': 0.2499}, - {'A': 3.00, 'B': 0.2432}, - {'A': 3.10, 'B': 0.2368}, - {'A': 3.20, 'B': 0.2308}, - {'A': 3.30, 'B': 0.2251}, - {'A': 3.40, 'B': 0.2197}, - {'A': 3.50, 'B': 0.2147}, - {'A': 3.60, 'B': 0.2101}, - {'A': 3.70, 'B': 0.2058}, - {'A': 3.80, 'B': 0.2019}, - {'A': 3.90, 'B': 0.1983}, - {'A': 4.00, 'B': 0.1950}, - {'A': 4.20, 'B': 0.1890}, - {'A': 4.40, 'B': 0.1837}, - {'A': 4.60, 'B': 0.1791}, - {'A': 4.80, 'B': 0.1750}, - {'A': 5.00, 'B': 0.1713}, + {'Mach': 0.00, 'CD': 0.2105}, + {'Mach': 0.05, 'CD': 0.2105}, + {'Mach': 0.10, 'CD': 0.2104}, + {'Mach': 0.15, 'CD': 0.2104}, + {'Mach': 0.20, 'CD': 0.2103}, + {'Mach': 0.25, 'CD': 0.2103}, + {'Mach': 0.30, 'CD': 0.2103}, + {'Mach': 0.35, 'CD': 0.2103}, + {'Mach': 0.40, 'CD': 0.2103}, + {'Mach': 0.45, 'CD': 0.2102}, + {'Mach': 0.50, 'CD': 0.2102}, + {'Mach': 0.55, 'CD': 0.2102}, + {'Mach': 0.60, 'CD': 0.2102}, + {'Mach': 0.65, 'CD': 0.2102}, + {'Mach': 0.70, 'CD': 0.2103}, + {'Mach': 0.75, 'CD': 0.2103}, + {'Mach': 0.80, 'CD': 0.2104}, + {'Mach': 0.825, 'CD': 0.2104}, + {'Mach': 0.85, 'CD': 0.2105}, + {'Mach': 0.875, 'CD': 0.2106}, + {'Mach': 0.90, 'CD': 0.2109}, + {'Mach': 0.925, 'CD': 0.2183}, + {'Mach': 0.95, 'CD': 0.2571}, + {'Mach': 0.975, 'CD': 0.3358}, + {'Mach': 1.0, 'CD': 0.4068}, + {'Mach': 1.025, 'CD': 0.4378}, + {'Mach': 1.05, 'CD': 0.4476}, + {'Mach': 1.075, 'CD': 0.4493}, + {'Mach': 1.10, 'CD': 0.4477}, + {'Mach': 1.125, 'CD': 0.4450}, + {'Mach': 1.15, 'CD': 0.4419}, + {'Mach': 1.20, 'CD': 0.4353}, + {'Mach': 1.25, 'CD': 0.4283}, + {'Mach': 1.30, 'CD': 0.4208}, + {'Mach': 1.35, 'CD': 0.4133}, + {'Mach': 1.40, 'CD': 0.4059}, + {'Mach': 1.45, 'CD': 0.3986}, + {'Mach': 1.50, 'CD': 0.3915}, + {'Mach': 1.55, 'CD': 0.3845}, + {'Mach': 1.60, 'CD': 0.3777}, + {'Mach': 1.65, 'CD': 0.3710}, + {'Mach': 1.70, 'CD': 0.3645}, + {'Mach': 1.75, 'CD': 0.3581}, + {'Mach': 1.80, 'CD': 0.3519}, + {'Mach': 1.85, 'CD': 0.3458}, + {'Mach': 1.90, 'CD': 0.3400}, + {'Mach': 1.95, 'CD': 0.3343}, + {'Mach': 2.00, 'CD': 0.3288}, + {'Mach': 2.05, 'CD': 0.3234}, + {'Mach': 2.10, 'CD': 0.3182}, + {'Mach': 2.15, 'CD': 0.3131}, + {'Mach': 2.20, 'CD': 0.3081}, + {'Mach': 2.25, 'CD': 0.3032}, + {'Mach': 2.30, 'CD': 0.2983}, + {'Mach': 2.35, 'CD': 0.2937}, + {'Mach': 2.40, 'CD': 0.2891}, + {'Mach': 2.45, 'CD': 0.2845}, + {'Mach': 2.50, 'CD': 0.2802}, + {'Mach': 2.60, 'CD': 0.2720}, + {'Mach': 2.70, 'CD': 0.2642}, + {'Mach': 2.80, 'CD': 0.2569}, + {'Mach': 2.90, 'CD': 0.2499}, + {'Mach': 3.00, 'CD': 0.2432}, + {'Mach': 3.10, 'CD': 0.2368}, + {'Mach': 3.20, 'CD': 0.2308}, + {'Mach': 3.30, 'CD': 0.2251}, + {'Mach': 3.40, 'CD': 0.2197}, + {'Mach': 3.50, 'CD': 0.2147}, + {'Mach': 3.60, 'CD': 0.2101}, + {'Mach': 3.70, 'CD': 0.2058}, + {'Mach': 3.80, 'CD': 0.2019}, + {'Mach': 3.90, 'CD': 0.1983}, + {'Mach': 4.00, 'CD': 0.1950}, + {'Mach': 4.20, 'CD': 0.1890}, + {'Mach': 4.40, 'CD': 0.1837}, + {'Mach': 4.60, 'CD': 0.1791}, + {'Mach': 4.80, 'CD': 0.1750}, + {'Mach': 5.00, 'CD': 0.1713}, ] TableGI = [ - {'A': 0.00, 'B': 0.2282}, - {'A': 0.05, 'B': 0.2282}, - {'A': 0.10, 'B': 0.2282}, - {'A': 0.15, 'B': 0.2282}, - {'A': 0.20, 'B': 0.2282}, - {'A': 0.25, 'B': 0.2282}, - {'A': 0.30, 'B': 0.2282}, - {'A': 0.35, 'B': 0.2282}, - {'A': 0.40, 'B': 0.2282}, - {'A': 0.45, 'B': 0.2282}, - {'A': 0.50, 'B': 0.2282}, - {'A': 0.55, 'B': 0.2282}, - {'A': 0.60, 'B': 0.2282}, - {'A': 0.65, 'B': 0.2282}, - {'A': 0.70, 'B': 0.2282}, - {'A': 0.725, 'B': 0.2353}, - {'A': 0.75, 'B': 0.2434}, - {'A': 0.775, 'B': 0.2515}, - {'A': 0.80, 'B': 0.2596}, - {'A': 0.825, 'B': 0.2677}, - {'A': 0.85, 'B': 0.2759}, - {'A': 0.875, 'B': 0.2913}, - {'A': 0.90, 'B': 0.3170}, - {'A': 0.925, 'B': 0.3442}, - {'A': 0.95, 'B': 0.3728}, - {'A': 1.0, 'B': 0.4349}, - {'A': 1.05, 'B': 0.5034}, - {'A': 1.075, 'B': 0.5402}, - {'A': 1.10, 'B': 0.5756}, - {'A': 1.125, 'B': 0.5887}, - {'A': 1.15, 'B': 0.6018}, - {'A': 1.175, 'B': 0.6149}, - {'A': 1.20, 'B': 0.6279}, - {'A': 1.225, 'B': 0.6418}, - {'A': 1.25, 'B': 0.6423}, - {'A': 1.30, 'B': 0.6423}, - {'A': 1.35, 'B': 0.6423}, - {'A': 1.40, 'B': 0.6423}, - {'A': 1.45, 'B': 0.6423}, - {'A': 1.50, 'B': 0.6423}, - {'A': 1.55, 'B': 0.6423}, - {'A': 1.60, 'B': 0.6423}, - {'A': 1.625, 'B': 0.6407}, - {'A': 1.65, 'B': 0.6378}, - {'A': 1.70, 'B': 0.6321}, - {'A': 1.75, 'B': 0.6266}, - {'A': 1.80, 'B': 0.6213}, - {'A': 1.85, 'B': 0.6163}, - {'A': 1.90, 'B': 0.6113}, - {'A': 1.95, 'B': 0.6066}, - {'A': 2.00, 'B': 0.6020}, - {'A': 2.05, 'B': 0.5976}, - {'A': 2.10, 'B': 0.5933}, - {'A': 2.15, 'B': 0.5891}, - {'A': 2.20, 'B': 0.5850}, - {'A': 2.25, 'B': 0.5811}, - {'A': 2.30, 'B': 0.5773}, - {'A': 2.35, 'B': 0.5733}, - {'A': 2.40, 'B': 0.5679}, - {'A': 2.45, 'B': 0.5626}, - {'A': 2.50, 'B': 0.5576}, - {'A': 2.60, 'B': 0.5478}, - {'A': 2.70, 'B': 0.5386}, - {'A': 2.80, 'B': 0.5298}, - {'A': 2.90, 'B': 0.5215}, - {'A': 3.00, 'B': 0.5136}, - {'A': 3.10, 'B': 0.5061}, - {'A': 3.20, 'B': 0.4989}, - {'A': 3.30, 'B': 0.4921}, - {'A': 3.40, 'B': 0.4855}, - {'A': 3.50, 'B': 0.4792}, - {'A': 3.60, 'B': 0.4732}, - {'A': 3.70, 'B': 0.4674}, - {'A': 3.80, 'B': 0.4618}, - {'A': 3.90, 'B': 0.4564}, - {'A': 4.00, 'B': 0.4513}, - {'A': 4.20, 'B': 0.4415}, - {'A': 4.40, 'B': 0.4323}, - {'A': 4.60, 'B': 0.4238}, - {'A': 4.80, 'B': 0.4157}, - {'A': 5.00, 'B': 0.4082}, + {'Mach': 0.00, 'CD': 0.2282}, + {'Mach': 0.05, 'CD': 0.2282}, + {'Mach': 0.10, 'CD': 0.2282}, + {'Mach': 0.15, 'CD': 0.2282}, + {'Mach': 0.20, 'CD': 0.2282}, + {'Mach': 0.25, 'CD': 0.2282}, + {'Mach': 0.30, 'CD': 0.2282}, + {'Mach': 0.35, 'CD': 0.2282}, + {'Mach': 0.40, 'CD': 0.2282}, + {'Mach': 0.45, 'CD': 0.2282}, + {'Mach': 0.50, 'CD': 0.2282}, + {'Mach': 0.55, 'CD': 0.2282}, + {'Mach': 0.60, 'CD': 0.2282}, + {'Mach': 0.65, 'CD': 0.2282}, + {'Mach': 0.70, 'CD': 0.2282}, + {'Mach': 0.725, 'CD': 0.2353}, + {'Mach': 0.75, 'CD': 0.2434}, + {'Mach': 0.775, 'CD': 0.2515}, + {'Mach': 0.80, 'CD': 0.2596}, + {'Mach': 0.825, 'CD': 0.2677}, + {'Mach': 0.85, 'CD': 0.2759}, + {'Mach': 0.875, 'CD': 0.2913}, + {'Mach': 0.90, 'CD': 0.3170}, + {'Mach': 0.925, 'CD': 0.3442}, + {'Mach': 0.95, 'CD': 0.3728}, + {'Mach': 1.0, 'CD': 0.4349}, + {'Mach': 1.05, 'CD': 0.5034}, + {'Mach': 1.075, 'CD': 0.5402}, + {'Mach': 1.10, 'CD': 0.5756}, + {'Mach': 1.125, 'CD': 0.5887}, + {'Mach': 1.15, 'CD': 0.6018}, + {'Mach': 1.175, 'CD': 0.6149}, + {'Mach': 1.20, 'CD': 0.6279}, + {'Mach': 1.225, 'CD': 0.6418}, + {'Mach': 1.25, 'CD': 0.6423}, + {'Mach': 1.30, 'CD': 0.6423}, + {'Mach': 1.35, 'CD': 0.6423}, + {'Mach': 1.40, 'CD': 0.6423}, + {'Mach': 1.45, 'CD': 0.6423}, + {'Mach': 1.50, 'CD': 0.6423}, + {'Mach': 1.55, 'CD': 0.6423}, + {'Mach': 1.60, 'CD': 0.6423}, + {'Mach': 1.625, 'CD': 0.6407}, + {'Mach': 1.65, 'CD': 0.6378}, + {'Mach': 1.70, 'CD': 0.6321}, + {'Mach': 1.75, 'CD': 0.6266}, + {'Mach': 1.80, 'CD': 0.6213}, + {'Mach': 1.85, 'CD': 0.6163}, + {'Mach': 1.90, 'CD': 0.6113}, + {'Mach': 1.95, 'CD': 0.6066}, + {'Mach': 2.00, 'CD': 0.6020}, + {'Mach': 2.05, 'CD': 0.5976}, + {'Mach': 2.10, 'CD': 0.5933}, + {'Mach': 2.15, 'CD': 0.5891}, + {'Mach': 2.20, 'CD': 0.5850}, + {'Mach': 2.25, 'CD': 0.5811}, + {'Mach': 2.30, 'CD': 0.5773}, + {'Mach': 2.35, 'CD': 0.5733}, + {'Mach': 2.40, 'CD': 0.5679}, + {'Mach': 2.45, 'CD': 0.5626}, + {'Mach': 2.50, 'CD': 0.5576}, + {'Mach': 2.60, 'CD': 0.5478}, + {'Mach': 2.70, 'CD': 0.5386}, + {'Mach': 2.80, 'CD': 0.5298}, + {'Mach': 2.90, 'CD': 0.5215}, + {'Mach': 3.00, 'CD': 0.5136}, + {'Mach': 3.10, 'CD': 0.5061}, + {'Mach': 3.20, 'CD': 0.4989}, + {'Mach': 3.30, 'CD': 0.4921}, + {'Mach': 3.40, 'CD': 0.4855}, + {'Mach': 3.50, 'CD': 0.4792}, + {'Mach': 3.60, 'CD': 0.4732}, + {'Mach': 3.70, 'CD': 0.4674}, + {'Mach': 3.80, 'CD': 0.4618}, + {'Mach': 3.90, 'CD': 0.4564}, + {'Mach': 4.00, 'CD': 0.4513}, + {'Mach': 4.20, 'CD': 0.4415}, + {'Mach': 4.40, 'CD': 0.4323}, + {'Mach': 4.60, 'CD': 0.4238}, + {'Mach': 4.80, 'CD': 0.4157}, + {'Mach': 5.00, 'CD': 0.4082}, ] TableGS = [ - {'A': 0.00, 'B': 0.4662}, - {'A': 0.05, 'B': 0.4689}, - {'A': 0.10, 'B': 0.4717}, - {'A': 0.15, 'B': 0.4745}, - {'A': 0.20, 'B': 0.4772}, - {'A': 0.25, 'B': 0.4800}, - {'A': 0.30, 'B': 0.4827}, - {'A': 0.35, 'B': 0.4852}, - {'A': 0.40, 'B': 0.4882}, - {'A': 0.45, 'B': 0.4920}, - {'A': 0.50, 'B': 0.4970}, - {'A': 0.55, 'B': 0.5080}, - {'A': 0.60, 'B': 0.5260}, - {'A': 0.65, 'B': 0.5590}, - {'A': 0.70, 'B': 0.5920}, - {'A': 0.75, 'B': 0.6258}, - {'A': 0.80, 'B': 0.6610}, - {'A': 0.85, 'B': 0.6985}, - {'A': 0.90, 'B': 0.7370}, - {'A': 0.95, 'B': 0.7757}, - {'A': 1.0, 'B': 0.8140}, - {'A': 1.05, 'B': 0.8512}, - {'A': 1.10, 'B': 0.8870}, - {'A': 1.15, 'B': 0.9210}, - {'A': 1.20, 'B': 0.9510}, - {'A': 1.25, 'B': 0.9740}, - {'A': 1.30, 'B': 0.9910}, - {'A': 1.35, 'B': 0.9990}, - {'A': 1.40, 'B': 1.0030}, - {'A': 1.45, 'B': 1.0060}, - {'A': 1.50, 'B': 1.0080}, - {'A': 1.55, 'B': 1.0090}, - {'A': 1.60, 'B': 1.0090}, - {'A': 1.65, 'B': 1.0090}, - {'A': 1.70, 'B': 1.0090}, - {'A': 1.75, 'B': 1.0080}, - {'A': 1.80, 'B': 1.0070}, - {'A': 1.85, 'B': 1.0060}, - {'A': 1.90, 'B': 1.0040}, - {'A': 1.95, 'B': 1.0025}, - {'A': 2.00, 'B': 1.0010}, - {'A': 2.05, 'B': 0.9990}, - {'A': 2.10, 'B': 0.9970}, - {'A': 2.15, 'B': 0.9956}, - {'A': 2.20, 'B': 0.9940}, - {'A': 2.25, 'B': 0.9916}, - {'A': 2.30, 'B': 0.9890}, - {'A': 2.35, 'B': 0.9869}, - {'A': 2.40, 'B': 0.9850}, - {'A': 2.45, 'B': 0.9830}, - {'A': 2.50, 'B': 0.9810}, - {'A': 2.55, 'B': 0.9790}, - {'A': 2.60, 'B': 0.9770}, - {'A': 2.65, 'B': 0.9750}, - {'A': 2.70, 'B': 0.9730}, - {'A': 2.75, 'B': 0.9710}, - {'A': 2.80, 'B': 0.9690}, - {'A': 2.85, 'B': 0.9670}, - {'A': 2.90, 'B': 0.9650}, - {'A': 2.95, 'B': 0.9630}, - {'A': 3.00, 'B': 0.9610}, - {'A': 3.05, 'B': 0.9589}, - {'A': 3.10, 'B': 0.9570}, - {'A': 3.15, 'B': 0.9555}, - {'A': 3.20, 'B': 0.9540}, - {'A': 3.25, 'B': 0.9520}, - {'A': 3.30, 'B': 0.9500}, - {'A': 3.35, 'B': 0.9485}, - {'A': 3.40, 'B': 0.9470}, - {'A': 3.45, 'B': 0.9450}, - {'A': 3.50, 'B': 0.9430}, - {'A': 3.55, 'B': 0.9414}, - {'A': 3.60, 'B': 0.9400}, - {'A': 3.65, 'B': 0.9385}, - {'A': 3.70, 'B': 0.9370}, - {'A': 3.75, 'B': 0.9355}, - {'A': 3.80, 'B': 0.9340}, - {'A': 3.85, 'B': 0.9325}, - {'A': 3.90, 'B': 0.9310}, - {'A': 3.95, 'B': 0.9295}, - {'A': 4.00, 'B': 0.9280}, + {'Mach': 0.00, 'CD': 0.4662}, + {'Mach': 0.05, 'CD': 0.4689}, + {'Mach': 0.10, 'CD': 0.4717}, + {'Mach': 0.15, 'CD': 0.4745}, + {'Mach': 0.20, 'CD': 0.4772}, + {'Mach': 0.25, 'CD': 0.4800}, + {'Mach': 0.30, 'CD': 0.4827}, + {'Mach': 0.35, 'CD': 0.4852}, + {'Mach': 0.40, 'CD': 0.4882}, + {'Mach': 0.45, 'CD': 0.4920}, + {'Mach': 0.50, 'CD': 0.4970}, + {'Mach': 0.55, 'CD': 0.5080}, + {'Mach': 0.60, 'CD': 0.5260}, + {'Mach': 0.65, 'CD': 0.5590}, + {'Mach': 0.70, 'CD': 0.5920}, + {'Mach': 0.75, 'CD': 0.6258}, + {'Mach': 0.80, 'CD': 0.6610}, + {'Mach': 0.85, 'CD': 0.6985}, + {'Mach': 0.90, 'CD': 0.7370}, + {'Mach': 0.95, 'CD': 0.7757}, + {'Mach': 1.0, 'CD': 0.8140}, + {'Mach': 1.05, 'CD': 0.8512}, + {'Mach': 1.10, 'CD': 0.8870}, + {'Mach': 1.15, 'CD': 0.9210}, + {'Mach': 1.20, 'CD': 0.9510}, + {'Mach': 1.25, 'CD': 0.9740}, + {'Mach': 1.30, 'CD': 0.9910}, + {'Mach': 1.35, 'CD': 0.9990}, + {'Mach': 1.40, 'CD': 1.0030}, + {'Mach': 1.45, 'CD': 1.0060}, + {'Mach': 1.50, 'CD': 1.0080}, + {'Mach': 1.55, 'CD': 1.0090}, + {'Mach': 1.60, 'CD': 1.0090}, + {'Mach': 1.65, 'CD': 1.0090}, + {'Mach': 1.70, 'CD': 1.0090}, + {'Mach': 1.75, 'CD': 1.0080}, + {'Mach': 1.80, 'CD': 1.0070}, + {'Mach': 1.85, 'CD': 1.0060}, + {'Mach': 1.90, 'CD': 1.0040}, + {'Mach': 1.95, 'CD': 1.0025}, + {'Mach': 2.00, 'CD': 1.0010}, + {'Mach': 2.05, 'CD': 0.9990}, + {'Mach': 2.10, 'CD': 0.9970}, + {'Mach': 2.15, 'CD': 0.9956}, + {'Mach': 2.20, 'CD': 0.9940}, + {'Mach': 2.25, 'CD': 0.9916}, + {'Mach': 2.30, 'CD': 0.9890}, + {'Mach': 2.35, 'CD': 0.9869}, + {'Mach': 2.40, 'CD': 0.9850}, + {'Mach': 2.45, 'CD': 0.9830}, + {'Mach': 2.50, 'CD': 0.9810}, + {'Mach': 2.55, 'CD': 0.9790}, + {'Mach': 2.60, 'CD': 0.9770}, + {'Mach': 2.65, 'CD': 0.9750}, + {'Mach': 2.70, 'CD': 0.9730}, + {'Mach': 2.75, 'CD': 0.9710}, + {'Mach': 2.80, 'CD': 0.9690}, + {'Mach': 2.85, 'CD': 0.9670}, + {'Mach': 2.90, 'CD': 0.9650}, + {'Mach': 2.95, 'CD': 0.9630}, + {'Mach': 3.00, 'CD': 0.9610}, + {'Mach': 3.05, 'CD': 0.9589}, + {'Mach': 3.10, 'CD': 0.9570}, + {'Mach': 3.15, 'CD': 0.9555}, + {'Mach': 3.20, 'CD': 0.9540}, + {'Mach': 3.25, 'CD': 0.9520}, + {'Mach': 3.30, 'CD': 0.9500}, + {'Mach': 3.35, 'CD': 0.9485}, + {'Mach': 3.40, 'CD': 0.9470}, + {'Mach': 3.45, 'CD': 0.9450}, + {'Mach': 3.50, 'CD': 0.9430}, + {'Mach': 3.55, 'CD': 0.9414}, + {'Mach': 3.60, 'CD': 0.9400}, + {'Mach': 3.65, 'CD': 0.9385}, + {'Mach': 3.70, 'CD': 0.9370}, + {'Mach': 3.75, 'CD': 0.9355}, + {'Mach': 3.80, 'CD': 0.9340}, + {'Mach': 3.85, 'CD': 0.9325}, + {'Mach': 3.90, 'CD': 0.9310}, + {'Mach': 3.95, 'CD': 0.9295}, + {'Mach': 4.00, 'CD': 0.9280}, ] + +DragTablesSet = [value for key, value in globals().items() if key.startswith("Table")] diff --git a/py_ballisticcalc/interface.py b/py_ballisticcalc/interface.py index 2a35ee4..adfd7c7 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -1,233 +1,65 @@ +"""Implements basic interface for the ballistics calculator""" from dataclasses import dataclass, field -from collections import OrderedDict -from enum import Enum -from typing import List -import math -import pandas as pd -import pyximport -pyximport.install() # Will compile .pyx if necessary -from .trajectory_calculator import * -from .trajectory_data import * -from .shot_parameters import * -from .atmosphere import * -from .drag import * -from .projectile import * -from .wind import * -from .bmath import unit -@dataclass -class Display: - units: str = '' # String to display - digits: int = 0 # Decimal points to display - def asDictEntry(self, s: str='', addSpace: bool=True): - display = self.units - if s: - display = s + (' ' if (addSpace and self.units) else '') + display - return (s, [display, self.digits]) - -UNIT_DISPLAY = { - unit.AngularRadian : Display('rad', 6), - unit.AngularDegree : Display('°', 4), - unit.AngularMOA : Display('MOA', 2), - unit.AngularMil : Display('mil', 2), - unit.AngularMRad : Display('mrad', 2), - unit.AngularThousand : Display('ths', 2), - unit.AngularInchesPer100Yd : Display('iph', 2), - unit.AngularCmPer100M : Display('cm/100m', 2), - unit.VelocityFPS : Display('fps', 0), - unit.VelocityMPH : Display('mph', 0), - unit.VelocityMPS : Display('m/s', 1), - unit.VelocityKMH : Display('km/h', 0), - unit.VelocityKT : Display('kt', 0), - unit.DistanceInch : Display('in.', 1), - unit.DistanceFoot : Display('ft', 2), - unit.DistanceYard : Display('yd', 0), - unit.DistanceMile : Display('mi', 3), - unit.DistanceNauticalMile : Display('nm', 3), - unit.DistanceMillimeter : Display('mm', 0), - unit.DistanceCentimeter : Display('cm', 1), - unit.DistanceMeter : Display('m', 2), - unit.DistanceKilometer : Display('km', 3), - unit.DistanceLine : Display('ln', 1), # No idea what this is - unit.EnergyFootPound : Display('ft·lb', 0), - unit.EnergyJoule : Display('J', 0), - unit.PressureMmHg : Display('mmHg', 2), - unit.PressureInHg : Display('inHg', 2), - unit.PressureBar : Display('bar', 2), - unit.PressureHP : Display('hPa', 4), - unit.PressurePSI : Display('psi', 4), - unit.TemperatureFahrenheit : Display('°F', 0), - unit.TemperatureCelsius : Display('°C', 0), - unit.TemperatureKelvin : Display('°K', 0), - unit.TemperatureRankin : Display('°R', 0), - unit.WeightGrain : Display('gr', 1), - unit.WeightOunce : Display('oz', 2), - unit.WeightGram : Display('g', 2), - unit.WeightPound : Display('lb', 3), - unit.WeightKilogram : Display('kg', 4), - unit.WeightNewton : Display('N', 3) -} +from .conditions import Shot +# pylint: disable=import-error,no-name-in-module,wildcard-import,unused-wildcard-import +from .backend import * +from .trajectory_data import HitResult +from .unit import Angular, Distance +from .settings import Settings -class ROW_TYPE(Enum): - TRAJECTORY = 1 - ZERO = 2 - MACH1 = 3 - -@dataclass -class Bullet: - BC: float # G7 Ballistic Coefficient - caliber: float - grains: float # Weight - length: float - muzzleVelocity: float - velocityUnits: unit.Velocity = unit.VelocityFPS +__all__ = ('Calculator',) -@dataclass -class Gun: - sightHeight: float = 0 - heightUnits: int = unit.DistanceInch #unit.Distance = unit.DistanceInch - # "Twist" is #/twistUnits in barrel length for rifling to complete one full circle - barrelTwist: float = 0 # Positive is right-hand, negative is left-hand - twistUnits: int = unit.DistanceInch #unit.Distance = unit.DistanceInch - -@dataclass -class Air: - """Defaults to standard atmosphere at sea level""" - altitude: float = 0 # Elevation above sea level - altUnits: unit.Distance = unit.DistanceFoot - pressure: float = 29.92 - pressureUnits: int = PressureInHg - temperature: float = 59 - tempUnits: int = TemperatureFahrenheit - humidity: float = 0 # Relative Humidity - windSpeed: float = 0 - windUnits: unit.Velocity = unit.VelocityMPH - windDirection: float = 0 # Degrees; wind blowing from behind shooter = 0 degrees; blowing to shooter's right = 90 degrees @dataclass class Calculator: - bullet: Bullet - gun: Gun = Gun() - air: Air = Air() - distanceUnits: int = unit.DistanceYard #unit.Distance = unit.DistanceYard - heightUnits: int = unit.DistanceInch #unit.Distance = unit.DistanceInch - angleUnits: int = unit.AngularMOA #unit.Angular = unit.AngularMOA - energyUnits: int = unit.EnergyFootPound + """Basic interface for the ballistics calculator""" - _bc: BallisticCoefficient = field(init=False, repr=False, compare=False) - _ammo: Ammunition = field(init=False, repr=False, compare=False) - _atmosphere: Atmosphere = field(init=False, repr=False, compare=False) - _projectile: ProjectileWithDimensions = field(init=False, repr=False, compare=False) - _wind: list[WindInfo] = field(init=False, repr=False, compare=False) + _calc: TrajectoryCalc = field(init=False, repr=False, compare=False, default=None) - def __post_init__(self): - self._updateObjects() + @property + def cdm(self): + """returns custom drag function based on input data""" + return self._calc.cdm - def _updateObjects(self): - """Create objects used by calculator based on current class datafields""" - self._bc = BallisticCoefficient(self.bullet.BC, DragTableG7, - unit.Weight(self.bullet.grains, unit.WeightGrain), - unit.Distance(self.bullet.caliber, unit.DistanceInch), TableG7) - self._projectile = ProjectileWithDimensions(self._bc, - unit.Distance(self.bullet.caliber, unit.DistanceInch), - unit.Distance(self.bullet.length, unit.DistanceInch), - unit.Weight(self.bullet.grains, unit.WeightGrain)) - self._ammo = Ammunition(self._projectile, unit.Velocity(self.bullet.muzzleVelocity, self.bullet.velocityUnits)) - self._atmosphere = Atmosphere(unit.Distance(self.air.altitude, self.air.altUnits), - Pressure(self.air.pressure, self.air.pressureUnits), - Temperature(self.air.temperature, self.air.tempUnits), - self.air.humidity) - self._wind = create_only_wind_info(unit.Velocity(self.air.windSpeed, self.air.windUnits), - unit.Angular(self.air.windDirection, unit.AngularDegree)) + def barrel_elevation_for_target(self, shot: Shot, target_distance: [float, Distance]) -> Angular: + """Calculates barrel elevation to hit target at zero_distance. - def elevationForZeroDistance(self, distance: Distance) -> float: + :param target_distance: Look-distance to "zero," which is point we want to hit. + This is the distance that a rangefinder would return with no ballistic adjustment. + NB: Some rangefinders offer an adjusted distance based on inclinometer measurement. + However, without a complete ballistic model these can only approximate the effects + on ballistic trajectory of shooting uphill or downhill. Therefore: + For maximum accuracy, use the raw sight distance and look_angle as inputs here. """ - Calculates barrel elevation to hit zero at given distance. - This is not always the second zero; depending on parameters - it might return the first (climbing) zero of the trajectory. - """ - calc = TrajectoryCalculator() - return calc.sight_angle(distance, Distance(self.gun.sightHeight, self.gun.heightUnits), - self._ammo, self._atmosphere).get_in(self.angleUnits) - - def zeroGivenElevation(self, elevation: float = None) -> TrajectoryData: - """ - Find the zero distance for a given barrel elevation. - This will always return the second (descending) zero. - """ - calc = TrajectoryCalculator() - shot = ShotParameters(unit.Angular(elevation, self.angleUnits), - unit.Distance(1e5, DistanceMile), unit.Distance(1e5, DistanceMile)) - data = calc.trajectory(self._ammo, self._atmosphere, shot, self._wind, - Distance(self.gun.barrelTwist, self.gun.twistUnits), - Distance(self.gun.sightHeight, self.gun.heightUnits), - stopAtZero=True) - if len(data) > 1: - return data[1] - else: - return data[0] # No downrange zero found, so just return starting row - # NOTE: We could speed up detecting this by checking whether the first row with negative angle - # also has negative drop, because it's not going to go positive after that! - - def dangerSpace(self, trajectory: TrajectoryData, targetHeight: float) -> float: - """Given a TrajectoryData row, we have the angle of travel of bullet at that point in its trajectory, which is at distance *d*. - "Danger Space" is defined for *d* and for a target of height `targetHeight` as the error range for the target, meaning - if the trajectory hits the center of the target when the target is exactly at *d*, then "Danger Space" is the distance - before or after *d* across which the bullet would still hit somewhere on the target. (This ignores windage; vertical only.)""" - # NOTE: Presently this is a coarse estimate based on point that is only good for small danger spaces – i.e., longer distances - # where trajectory is steeper. To get exact danger space instead run detailed trajectory to see exact distances where - # trajectory height crosses 1/2 targetHeight before and after *d*. - return -unit.Distance(targetHeight / math.tan(trajectory.angle().get_in(AngularRadian)), self.heightUnits).get_in(self.distanceUnits) + self._calc = TrajectoryCalc(shot.ammo) + target_distance = Settings.Units.distance(target_distance) + total_elevation = self._calc.zero_angle(shot, target_distance) + return Angular.Radian((total_elevation >> Angular.Radian) + - (shot.look_angle >> Angular.Radian)) + + def set_weapon_zero(self, shot: Shot, zero_distance: [float, Distance]) -> Angular: + """Sets shot.weapon.zero_elevation so that it hits a target at zero_distance. - def trajectory(self, range: float, step: float, elevation: float = None, zeroDistance: float = None, - stopAtZero: bool = False, stopAtMach1: bool = False) -> pd.DataFrame: - """We use zeroDistance if given. Otherwise elevation if given. Otherwise elevation = 0.""" - if zeroDistance is None: - if elevation is None: - elevation = 0 # If we have - else: - elevation = self.elevationForZeroDistance(Distance(zeroDistance, self.distanceUnits)) - calc = TrajectoryCalculator() - shot = ShotParameters(unit.Angular(elevation, self.angleUnits), - unit.Distance(range, self.distanceUnits), - unit.Distance(step, self.distanceUnits)) - data = calc.trajectory(self._ammo, self._atmosphere, shot, self._wind, - Distance(self.gun.barrelTwist, self.gun.twistUnits), - Distance(self.gun.sightHeight, self.gun.heightUnits), - stopAtZero=stopAtZero, stopAtMach1=stopAtMach1) - return self.trajectoryRowsToDataFrame(data) - - def trajectoryRowsToDataFrame(self, rows: List[TrajectoryData]) -> pd.DataFrame: - # Dictionary of column header names and display precision - self.tableCols = OrderedDict([UNIT_DISPLAY[self.distanceUnits].asDictEntry('Distance'), - UNIT_DISPLAY[self.bullet.velocityUnits].asDictEntry('Velocity'), - UNIT_DISPLAY[self.angleUnits].asDictEntry('Angle'), - Display(digits=2).asDictEntry('Mach'), - Display(digits=3).asDictEntry('Time'), - UNIT_DISPLAY[self.heightUnits].asDictEntry('Drop'), - UNIT_DISPLAY[self.angleUnits].asDictEntry('DropAngle'), - UNIT_DISPLAY[self.heightUnits].asDictEntry('Windage'), - UNIT_DISPLAY[self.angleUnits].asDictEntry('WindageAngle'), - UNIT_DISPLAY[self.energyUnits].asDictEntry('Energy') - ]) + :param target_distance: Look-distance to "zero," which is point we want to hit. + """ + shot.weapon.zero_elevation = self.barrel_elevation_for_target(shot, zero_distance) + return shot.weapon.zero_elevation - r = [] # List of trajectory table rows - for d in rows: - distance = d.travelled_distance().get_in(self.distanceUnits) - velocity = d.velocity().get_in(self.bullet.velocityUnits) - angle = d.angle().get_in(self.angleUnits) - mach = d.mach_velocity() - time = d.time().total_seconds() - drop = d.drop().get_in(self.heightUnits) - dropMOA = d.drop_adjustment().get_in(self.angleUnits) - wind = d.windage().get_in(self.heightUnits) - windMOA = d.windage_adjustment().get_in(self.angleUnits) - energy = d.energy().get_in(self.energyUnits) - note = '' - if d.row_type() == ROW_TYPE.MACH1.value: note = 'Mach1' - elif d.row_type() == ROW_TYPE.ZERO.value: note = 'Zero' - r.append([distance, velocity, angle, mach, time, drop, dropMOA, wind, windMOA, energy, note]) - - colNames = list(zip(*self.tableCols.values()))[0] + ('Note',) - self.trajectoryTable = pd.DataFrame(r, columns=colNames) - return self.trajectoryTable.round(dict(self.tableCols.values())).set_index(colNames[0]) + def fire(self, shot: Shot, trajectory_range: [float, Distance], + trajectory_step: [float, Distance] = 0, + extra_data: bool = False) -> HitResult: + """Calculates trajectory + :param shot: shot parameters (initial position and barrel angle) + :param range: Downrange distance at which to stop computing trajectory + :param trajectory_step: step between trajectory points to record + :param extra_data: True => store TrajectoryData for every calculation step; + False => store TrajectoryData only for each trajectory_step + """ + if not trajectory_step: + trajectory_step = trajectory_range / 10.0 + trajectory_range = Settings.Units.distance(trajectory_range) + step = Settings.Units.distance(trajectory_step) + self._calc = TrajectoryCalc(shot.ammo) + data = self._calc.trajectory(shot, trajectory_range, step, extra_data) + return HitResult(shot, data, extra_data) diff --git a/py_ballisticcalc/logger.py b/py_ballisticcalc/logger.py new file mode 100644 index 0000000..c3c21fd --- /dev/null +++ b/py_ballisticcalc/logger.py @@ -0,0 +1,13 @@ +"""Default logger for py_ballisticcalc library""" + +import logging + +__all__ = ('logger',) + +formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s") +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) + +logger = logging.getLogger('py_balcalc') +logger.addHandler(console_handler) +logger.setLevel(logging.INFO) diff --git a/py_ballisticcalc/multiple_bc.py b/py_ballisticcalc/multiple_bc.py new file mode 100644 index 0000000..33185c9 --- /dev/null +++ b/py_ballisticcalc/multiple_bc.py @@ -0,0 +1,110 @@ +"""Module to create custom drag function based on Multiple ballistic coefficients""" +import typing +from math import pow as math_pow +from typing import NamedTuple, Iterable + +from .conditions import Atmo +# pylint: disable=import-error,no-name-in-module +from .backend import make_data_points +from .settings import Settings as Set +from .unit import Distance, Weight, Velocity + +__all__ = ('MultiBC', ) + + +class MultiBCRow(NamedTuple): + """Multi-BC point, for internal usage""" + BC: float + V: Set.Units.velocity + + +class DragTableRow(NamedTuple): + """CDM point, for internal usage""" + CD: float + Mach: float + + +class BCMachRow(NamedTuple): + """BC-Mach point, for internal usage""" + BC: float + Mach: float + + +class MultiBC: # pylint: disable=too-few-public-methods + """Creates instance to calculate custom drag tabel based on input multi-bc table""" + + def __init__(self, drag_table: Iterable[dict], diameter: Distance, weight: Weight, + mbc_table: Iterable[dict]): + + self.mbc_table = mbc_table + self.weight = weight + self.diameter = diameter + self.sectional_density = self._get_sectional_density() + + atmosphere = Atmo.icao() + + altitude = Distance.Meter(0) >> Distance.Foot + _, mach = atmosphere.get_density_factor_and_mach_for_altitude(altitude) + self.speed_of_sound = Velocity.FPS(mach) >> Velocity.MPS + + self._table_data = make_data_points(drag_table) + self._bc_table = self._parse_mbc(mbc_table) + + def _parse_mbc(self, mbc_table): + table = [] + for p in mbc_table: + # print(p['V'], Set.Units.velocity) + # print(Set.Units.velocity(p['V'])) + v = Set.Units.velocity(p['V']) >> Velocity.MPS + mbc = MultiBCRow(p['BC'], v) + table.append(mbc) + return sorted(table, reverse=True) + + def _get_sectional_density(self) -> float: + w = self.weight >> Weight.Grain + d = self.diameter >> Distance.Inch + return w / math_pow(d, 2) / 7000 + + def _get_form_factor(self, bc) -> float: + return self.sectional_density / bc + + @staticmethod + def _get_counted_cd(form_factor, standard_cd): + return standard_cd * form_factor + + def _interpolate_bc_table(self) -> typing.Generator: + """ + Extends input bc table by creating bc value for each point of standard Drag Model + """ + bc_table = tuple(self._bc_table) + bc_mah = [BCMachRow(bc_table[0].BC, self._table_data[-1].Mach)] + bc_mah.extend( + [BCMachRow(point.BC, point.V / self.speed_of_sound) for point in bc_table] + ) + bc_mah.append(BCMachRow(bc_mah[-1].BC, self._table_data[0].Mach)) + + yield bc_mah[0].BC + + for bc_max, bc_min in zip(bc_mah, bc_mah[1:]): + df_part = [ + point for point in self._table_data if bc_max.Mach > point.Mach >= bc_min.Mach + ] + ddf = len(df_part) + bc_delta = (bc_max.BC - bc_min.BC) / ddf + for j in range(ddf): + yield bc_max.BC - bc_delta * j + + def _cdm_generator(self) -> typing.Generator: + bc_extended = reversed(list(self._interpolate_bc_table())) + form_factors = [self._get_form_factor(bc) for bc in bc_extended] + + for i, point in enumerate(self._table_data): + cd = form_factors[i] * point.CD + yield {'CD': cd, 'Mach': point.Mach} + + @property + def cdm(self) -> list[dict]: + """ + :return: custom drag function based on input multiple ballistic coefficients + """ + return list(self._cdm_generator()) diff --git a/py_ballisticcalc/multiple_bc.pyx b/py_ballisticcalc/multiple_bc.pyx deleted file mode 100644 index a7b4463..0000000 --- a/py_ballisticcalc/multiple_bc.pyx +++ /dev/null @@ -1,116 +0,0 @@ -from libc.math cimport pow - -from .drag import load_drag_table -from .bmath.unit import * -from .atmosphere import IcaoAtmosphere - -cdef class BCDataPoint: - cdef double _bc - cdef double _v - - def __init__(self, bc: double, v: double): - self._bc = bc - self._v = v - - cpdef double bc(self): - return self._bc - - cpdef double v(self): - return self._v - - cpdef set_v(self, float value): - self._v = value - - -cdef class MultipleBallisticCoefficient: - cdef list _custom_drag_table, _table_data, _bc_table, _multiple_bc_table - cdef double _sectional_density, _speed_of_sound - cdef int _units, _table - cdef _weight, _diameter - - def __init__(self, drag_table: int, diameter: Distance, weight: Weight, - multiple_bc_table: list[(double, double)], velocity_units_flag: int): - - cdef double altitude, density, mach - cdef _atmosphere - - self._multiple_bc_table = multiple_bc_table - self._table = drag_table - self._weight = weight - self._diameter = diameter - self._units = velocity_units_flag - self._sectional_density = self._get_sectional_density() - - _atmosphere = IcaoAtmosphere(Distance(0, DistanceFoot)) - - altitude = Distance(0, DistanceMeter).get_in(DistanceFoot) - density, mach = _atmosphere.get_density_factor_and_mach_for_altitude(altitude) - self._speed_of_sound = Velocity(mach, VelocityFPS).get_in(VelocityMPS) - - self._table_data = load_drag_table(self._table) - self._bc_table = self._create_bc_table_data_points() - self._custom_drag_table = [] - - cdef double _get_sectional_density(self): - cdef double w, d - w = self._weight.get_in(WeightGrain) - d = self._diameter.get_in(DistanceInch) - return w / pow(d, 2) / 7000 - - cdef double _get_form_factor(self, double bc): - return self._sectional_density / bc - - cdef list _bc_extended(self): - cdef list bc_mah, bc_extended, df_part - cdef bc_max, bc_min - cdef int ddf - cdef double bc_delta - - bc_mah = [BCDataPoint(point.bc(), point.v() / self._speed_of_sound) for point in self._bc_table] - bc_mah.insert(len(bc_mah), BCDataPoint(bc_mah[-1].bc(), self._table_data[0].a())) - bc_mah.insert(0, BCDataPoint(bc_mah[0].bc(), self._table_data[-1].a())) - bc_extended = [bc_mah[0].bc(), ] - - for i in range(1, len(bc_mah)): - bc_max = bc_mah[i - 1] - bc_min = bc_mah[i] - df_part = list(filter(lambda point: bc_max.v() > point.a() >= bc_min.v(), self._table_data)) - ddf = len(df_part) - bc_delta = (bc_max.bc() - bc_min.bc()) / ddf - for j in range(ddf): - bc_extended.append(bc_max.bc() - bc_delta * j) - - return bc_extended - - cdef double _get_counted_cd(self, double form_factor, double cdst): - return cdst * form_factor - - cdef list _create_bc_table_data_points(self): - cdef list bc_table - cdef data_point - cdef double bc, v, - self._multiple_bc_table.sort(reverse=True, key=lambda x: x[1]) - bc_table = [] - for bc, v in self._multiple_bc_table: - data_point = BCDataPoint(bc, Velocity(v, self._units).get_in(VelocityMPS)) - bc_table.append(data_point) - return bc_table - - cdef list _calculate_custom_drag_func(self): - cdef list bc_extended, drag_function - cdef int i - cdef point - cdef double form_factor, cd, bc - - bc_extended = self._bc_extended() - drag_function = [] - for i, point in enumerate(self._table_data): - bc = bc_extended[len(bc_extended) - 1 - i] - form_factor = self._get_form_factor(bc) - cd = form_factor * point.b() - drag_function.append({'A': point.a(), 'B': cd}) - self._custom_drag_table = drag_function - - cpdef list custom_drag_func(self): - self._calculate_custom_drag_func() - return self._custom_drag_table diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py new file mode 100644 index 0000000..366989e --- /dev/null +++ b/py_ballisticcalc/munition.py @@ -0,0 +1,94 @@ +"""Module for Weapon and Ammo properties definitions""" +import math +from dataclasses import dataclass, field + +from .drag_model import DragModel +from .settings import Settings as Set +from .unit import TypedUnits, Velocity, Temperature, Distance, Angular + +__all__ = ('Weapon', 'Ammo') + + +@dataclass +class Weapon(TypedUnits): + """ + :param sight_height: Vertical distance from center of bore line to center of sight line. + :param twist: Distance for barrel rifling to complete one complete turn. + Positive value => right-hand twist, negative value => left-hand twist. + :param zero_elevation: Angle of barrel relative to sight line when sight is set to "zero." + (Typically computed by ballistic Calculator.) + """ + sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height) + twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist) + zero_elevation: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + + def __post_init__(self): + if not self.sight_height: + self.sight_height = 0 + if not self.twist: + self.twist = 0 + if not self.zero_elevation: + self.zero_elevation = 0 + + +@dataclass +class Ammo(TypedUnits): + """ + :param dm: DragModel for projectile + :param length: Length of projectile + :param mv: Muzzle Velocity + :param temp_modifier: Coefficient for effect of temperature on mv + :param powder_temp: Baseline temperature that produces the given mv + """ + dm: DragModel = field(default=None) + length: [float, Distance] = field(default_factory=lambda: Set.Units.length) + mv: [float, Velocity] = field(default_factory=lambda: Set.Units.velocity) + temp_modifier: float = field(default=0) + powder_temp: [float, Temperature] = field(default_factory=lambda: Temperature.Celsius(15)) + + def __post_init__(self): + if not self.length: + self.length = 0 + + def calc_powder_sens(self, other_velocity: [float, Velocity], + other_temperature: [float, Temperature]) -> float: + """Calculates velocity correction by temperature change + :param other_velocity: other velocity + :param other_temperature: other temperature + :return: temperature modifier + """ + # (800-792) / (15 - 0) * (15/792) * 100 = 1.01 + # creates temperature modifier in percent at each 15C + v0 = self.mv >> Velocity.MPS + t0 = self.powder_temp >> Temperature.Celsius + v1 = Set.Units.velocity(other_velocity) >> Velocity.MPS + t1 = Set.Units.temperature(other_temperature) >> Temperature.Celsius + + v_delta = math.fabs(v0 - v1) + t_delta = math.fabs(t0 - t1) + v_lower = v1 if v1 < v0 else v0 + + if v_delta == 0 or t_delta == 0: + raise ValueError( + "Temperature modifier error, other velocity " + "and temperature can't be same as default" + ) + + self.temp_modifier = v_delta / t_delta * (15 / v_lower) # * 100 + + return self.temp_modifier + + def get_velocity_for_temp(self, current_temp: [float, Temperature]) -> Velocity: + """Calculates current velocity by temperature correction + :param current_temp: temperature on current atmosphere + :return: velocity corrected for temperature specified + """ + temp_modifier = self.temp_modifier + v0 = self.mv >> Velocity.MPS + t0 = self.powder_temp >> Temperature.Celsius + t1 = Set.Units.temperature(current_temp) >> Temperature.Celsius + + t_delta = t1 - t0 + muzzle_velocity = temp_modifier / (15 / v0) * t_delta + v0 + + return Velocity.MPS(muzzle_velocity) diff --git a/py_ballisticcalc/projectile.pyx b/py_ballisticcalc/projectile.pyx deleted file mode 100644 index 505b675..0000000 --- a/py_ballisticcalc/projectile.pyx +++ /dev/null @@ -1,61 +0,0 @@ -from .bmath.unit import * -from .drag import * - - -cdef class Projectile: - cdef _bullet_diameter - cdef _bullet_length - cdef _ballistic_coefficient - cdef _has_dimensions - cdef _weight - - def __init__(self, ballistic_coefficient: BallisticCoefficient, weight: Weight): - """ - projectile description with dimensions - :param ballistic_coefficient: BallisticCoefficient instance - :param weight: unit.Weight instance - """ - self._ballistic_coefficient = ballistic_coefficient - self._has_dimensions = False - self._weight = weight - - cpdef ballistic_coefficient(self): - return self._ballistic_coefficient - - cpdef bullet_weight(self): - return self._weight - - cpdef bullet_diameter(self): - return self._bullet_diameter - - cpdef bullet_length(self): - return self._bullet_length - - cpdef has_dimensions(self): - return self._has_dimensions - - -cdef class ProjectileWithDimensions(Projectile): - - def __init__(self, ballistic_coefficient: BallisticCoefficient, - bullet_diameter: Distance, - bullet_length: Distance, - weight: Weight): - super(ProjectileWithDimensions, self).__init__(ballistic_coefficient, weight) - self._has_dimensions = True - self._bullet_diameter = bullet_diameter - self._bullet_length = bullet_length - -cdef class Ammunition: - cdef _projectile - cdef _muzzle_velocity - - def __init__(self, bullet: Projectile, muzzle_velocity: Velocity): - self._projectile = bullet - self._muzzle_velocity = muzzle_velocity - - cpdef bullet(self): - return self._projectile - - cpdef muzzle_velocity(self): - return self._muzzle_velocity diff --git a/py_ballisticcalc/settings.py b/py_ballisticcalc/settings.py new file mode 100644 index 0000000..4a9bd8d --- /dev/null +++ b/py_ballisticcalc/settings.py @@ -0,0 +1,49 @@ +"""Global settings of the py_ballisticcalc library""" +import logging +import dataclasses +from .unit import Unit, Distance + +__all__ = ('Settings',) + +class Metadataclass(type): + """Provide representation method for static dataclasses.""" + def __repr__(cls): + return '\n'.join(f'{field.name} = {getattr(cls, field.name)!r}' + for field in dataclasses.fields(cls)) + +class Settings: # pylint: disable=too-few-public-methods + """Global settings class of the py_ballisticcalc library""" + + @dataclasses.dataclass + class Units(metaclass=Metadataclass): # pylint: disable=too-many-instance-attributes + """Default units for specified measures""" + angular: Unit = Unit.DEGREE + distance: Unit = Unit.YARD + velocity: Unit = Unit.FPS + pressure: Unit = Unit.IN_HG + temperature: Unit = Unit.FAHRENHEIT + diameter: Unit = Unit.INCH + length: Unit = Unit.INCH + weight: Unit = Unit.GRAIN + adjustment: Unit = Unit.MIL + drop: Unit = Unit.INCH + energy: Unit = Unit.FOOT_POUND + ogw: Unit = Unit.POUND + sight_height: Unit = Unit.INCH + target_height: Unit = Unit.INCH + twist: Unit = Unit.INCH + + _MAX_CALC_STEP_SIZE: float = 1 + USE_POWDER_SENSITIVITY: bool = False + + @classmethod + def set_max_calc_step_size(cls, value: [float, Distance]): + """_MAX_CALC_STEP_SIZE setter + :param value: [float, Distance] maximum calculation step (used internally) + """ + logging.warning("Settings._MAX_CALC_STEP_SIZE: change this property " + "only if you know what you are doing; " + "too big step can corrupt calculation accuracy") + if not isinstance(value, (Distance, float, int)): + raise ValueError("MIN_CALC_STEP_SIZE have to be a type of 'Distance'") + cls._MAX_CALC_STEP_SIZE = cls.Units.distance(value) >> Distance.Foot diff --git a/py_ballisticcalc/shot_parameters.pyx b/py_ballisticcalc/shot_parameters.pyx deleted file mode 100644 index 465ff70..0000000 --- a/py_ballisticcalc/shot_parameters.pyx +++ /dev/null @@ -1,39 +0,0 @@ -from .bmath.unit import * - - -cdef class ShotParameters: - cdef _sight_angle, _shot_angle, _cant_angle, _maximum_distance, _step - - def __init__(self, sight_angle: Angular, maximum_distance: Distance, step: Distance): - self._sight_angle = sight_angle - self._shot_angle = Angular(0, AngularRadian) - self._cant_angle = Angular(0, AngularRadian) - self._maximum_distance: Distance = maximum_distance - self._step: Distance = step - - cpdef sight_angle(self): - return self._sight_angle - - cpdef shot_angle(self): - return self._shot_angle - - cpdef cant_angle(self): - return self._cant_angle - - cpdef maximum_distance(self): - return self._maximum_distance - - cpdef step(self): - return self._step - - -cdef class ShotParametersUnlevel(ShotParameters): - - def __init__(self, sight_angle: Angular, maximum_distance: Distance, - step: Distance, shot_angle: Angular, cant_angle: Angular): - super(ShotParametersUnlevel, self).__init__(sight_angle, maximum_distance, step) - self._sight_angle = sight_angle - self._shot_angle = shot_angle - self._cant_angle = cant_angle - self._maximum_distance: Distance = maximum_distance - self._step: Distance = step diff --git a/py_ballisticcalc/trajectory_calc.py b/py_ballisticcalc/trajectory_calc.py new file mode 100644 index 0000000..000f215 --- /dev/null +++ b/py_ballisticcalc/trajectory_calc.py @@ -0,0 +1,477 @@ +# pylint: disable=missing-class-docstring,missing-function-docstring +"""pure python trajectory calculation backend""" + +import math +from dataclasses import dataclass +from typing import NamedTuple + +from .conditions import Atmo, Shot, Wind +from .munition import Ammo, Weapon +from .settings import Settings +from .trajectory_data import TrajectoryData, TrajFlag +from .unit import Distance, Angular, Velocity, Weight, Energy, Pressure, Temperature + +__all__ = ('TrajectoryCalc', ) + +cZeroFindingAccuracy = 0.000005 +cMinimumVelocity = 50.0 +cMaximumDrop = -15000 +cMaxIterations = 20 +cGravityConstant = -32.17405 + + +class CurvePoint(NamedTuple): + a: float + b: float + c: float + + +@dataclass +class Vector: + x: float + y: float + z: float + + def magnitude(self): + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + + def mul_by_const(self, a: float): + return Vector(self.x * a, self.y * a, self.z * a) + + def mul_by_vector(self, b: 'Vector'): + return self.x * b.x + self.y * b.y + self.z * b.z + + def add(self, b: 'Vector'): + return Vector(self.x + b.x, self.y + b.y, self.z + b.z) + + def subtract(self, b: 'Vector'): + return Vector(self.x - b.x, self.y - b.y, self.z - b.z) + + def negate(self): + return Vector(-self.x, -self.y, -self.z) + + def normalize(self): + m = self.magnitude() + if math.fabs(m) < 1e-10: + return Vector(self.x, self.y, self.z) + return self.mul_by_const(1.0 / m) + + def __add__(self, other: 'Vector'): + return self.add(other) + + def __radd__(self, other: 'Vector'): + return self.add(other) + + def __iadd__(self, other: 'Vector'): + return self.add(other) + + def __sub__(self, other: 'Vector'): + return self.subtract(other) + + def __rsub__(self, other: 'Vector'): + return self.subtract(other) + + def __isub__(self, other: 'Vector'): + return self.subtract(other) + + def __mul__(self, other: [int, float, 'Vector']): + if isinstance(other, (int, float)): + return self.mul_by_const(other) + if isinstance(other, Vector): + return self.mul_by_vector(other) + raise TypeError(other) + + def __rmul__(self, other: [int, float, 'Vector']): + return self.__mul__(other) + + def __imul__(self, other): + return self.__mul__(other) + + def __neg__(self): + return self.negate() + + +class TrajectoryCalc: + """All calculations are done in units of Feet and fps""" + + def __init__(self, ammo: Ammo): + self.ammo = ammo + self._bc = self.ammo.dm.value + self._table_data = ammo.dm.drag_table + self._curve = calculate_curve(self._table_data) + + def get_calc_step(self, step: float): + maximum_step = Settings._MAX_CALC_STEP_SIZE + step /= 2 + if step > maximum_step: + step_order = int(math.floor(math.log10(step))) + maximum_order = int(math.floor(math.log10(maximum_step))) + step /= math.pow(10, step_order - maximum_order + 1) + return step + + def zero_angle(self, shot_info: Shot, distance: Distance): + return self._zero_angle(shot_info, distance) + + def trajectory(self, shot_info: Shot, max_range: Distance, dist_step: Distance, + extra_data: bool = False): + atmo = shot_info.atmo + winds = shot_info.winds + filter_flags = TrajFlag.RANGE + + if extra_data: + #print('ext', extra_data) + dist_step = Distance.Foot(0.2) + filter_flags = TrajFlag.ALL + return self._trajectory(self.ammo, atmo, shot_info, winds, max_range, dist_step, filter_flags) + + def _zero_angle(self, shot_info: Shot, distance: Distance): + calc_step = self.get_calc_step(distance.units(10) >> Distance.Foot) + zero_distance = math.cos(shot_info.look_angle >> Angular.Radian) * (distance >> Distance.Foot) + height_at_zero = math.sin(shot_info.look_angle >> Angular.Radian) * (distance >> Distance.Foot) + maximum_range = zero_distance + calc_step + sight_height = shot_info.weapon.sight_height >> Distance.Foot + mach = shot_info.atmo.mach >> Velocity.FPS + density_factor = shot_info.atmo.density_factor() + muzzle_velocity = shot_info.ammo.mv >> Velocity.FPS + cant_cosine = math.cos(shot_info.cant_angle >> Angular.Radian) + cant_sine = math.sin(shot_info.cant_angle >> Angular.Radian) + + barrel_azimuth = 0.0 + barrel_elevation = math.atan(height_at_zero / zero_distance) + iterations_count = 0 + zero_finding_error = cZeroFindingAccuracy * 2 + gravity_vector = Vector(.0, cGravityConstant, .0) + + # x - distance towards target, y - drop and z - windage + while zero_finding_error > cZeroFindingAccuracy and iterations_count < cMaxIterations: + velocity = muzzle_velocity + time = 0.0 + range_vector = Vector(.0, -cant_cosine*sight_height, -cant_sine*sight_height) + velocity_vector = Vector( + math.cos(barrel_elevation) * math.cos(barrel_azimuth), + math.sin(barrel_elevation), + math.cos(barrel_elevation) * math.sin(barrel_azimuth) + ) * velocity + + while range_vector.x <= maximum_range: + if velocity < cMinimumVelocity or range_vector.y < cMaximumDrop: + break + + delta_time = calc_step / velocity_vector.x + + drag = density_factor * velocity * self.drag_by_mach(velocity / mach) + + velocity_vector -= (velocity_vector * drag - gravity_vector) * delta_time + delta_range_vector = Vector(calc_step, velocity_vector.y * delta_time, + velocity_vector.z * delta_time) + range_vector += delta_range_vector + velocity = velocity_vector.magnitude() + time += delta_range_vector.magnitude() / velocity + + if math.fabs(range_vector.x - zero_distance) < 0.5 * calc_step: + zero_finding_error = math.fabs(range_vector.y - height_at_zero) + if zero_finding_error > cZeroFindingAccuracy: + barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x + break + + iterations_count += 1 + + return Angular.Radian(barrel_elevation) + + def _trajectory(self, ammo: Ammo, atmo: Atmo, + shot_info: Shot, winds: list[Wind], + max_range: Distance, dist_step: Distance, filter_flags: TrajFlag): + + time = 0 + look_angle = shot_info.look_angle >> Angular.Radian + twist = shot_info.weapon.twist >> Distance.Inch + length = ammo.length >> Distance.Inch + diameter = ammo.dm.diameter >> Distance.Inch + weight = ammo.dm.weight >> Weight.Grain + + # step = shot_info.step >> Distance.Foot + step = dist_step >> Distance.Foot + calc_step = self.get_calc_step(step) + + maximum_range = (max_range >> Distance.Foot) + 1 + + ranges_length = int(maximum_range / step) + 1 + len_winds = len(winds) + current_wind = 0 + current_item = 0 + next_range_distance = .0 + previous_mach = .0 + ranges = [] + + stability_coefficient = 1.0 + twist_coefficient = 0 + next_wind_range = 1e7 + alt0 = atmo.altitude >> Distance.Foot + + barrel_elevation = shot_info.barrel_elevation >> Angular.Radian + barrel_azimuth = shot_info.barrel_azimuth >> Angular.Radian + sight_height = shot_info.weapon.sight_height >> Distance.Foot + cant_cosine = math.cos(shot_info.cant_angle >> Angular.Radian) + cant_sine = math.sin(shot_info.cant_angle >> Angular.Radian) + range_vector = Vector(.0, -cant_cosine*sight_height, -cant_sine*sight_height) + + gravity_vector = Vector(.0, cGravityConstant, .0) + + if len_winds < 1: + wind_vector = Vector(.0, .0, .0) + else: + if len_winds > 1: + next_wind_range = winds[0].until_distance() >> Distance.Foot + wind_vector = wind_to_vector(winds[0]) + + if Settings.USE_POWDER_SENSITIVITY: + velocity = ammo.get_velocity_for_temp(atmo.temperature) >> Velocity.FPS + else: + velocity = ammo.mv >> Velocity.FPS + + # x: downrange distance (towards target), y: drop, z: windage + velocity_vector = Vector(math.cos(barrel_elevation) * math.cos(barrel_azimuth), + math.sin(barrel_elevation), + math.cos(barrel_elevation) * math.sin(barrel_azimuth)) * velocity + + if twist != 0 and length and diameter: + stability_coefficient = calculate_stability_coefficient(shot_info.weapon.twist, ammo, atmo) + twist_coefficient = 1 if twist > 0 else -1 + + # With non-zero look_angle, rounding can suggest multiple adjacent zero-crossings + seen_zero = TrajFlag.NONE # Record when we see each zero crossing so we only register one + if range_vector.y >= 0: + seen_zero |= TrajFlag.ZERO_UP # We're starting above zero; we can only go down + elif range_vector.y < 0 and barrel_elevation < look_angle: + seen_zero |= TrajFlag.ZERO_DOWN # We're below and pointing down from look angle; no zeroes! + + while range_vector.x <= maximum_range + calc_step: + _flag = TrajFlag.NONE + + if velocity < cMinimumVelocity or range_vector.y < cMaximumDrop: + break + + density_factor, mach = atmo.get_density_factor_and_mach_for_altitude( + alt0 + range_vector.y) + + if range_vector.x >= next_wind_range: + current_wind += 1 + wind_vector = wind_to_vector(winds[current_wind]) + + if current_wind == len_winds - 1: + next_wind_range = 1e7 + else: + next_wind_range = winds[current_wind].until_distance() >> Distance.Foot + + # Zero-crossing checks + if range_vector.x > 0: + # Zero reference line is the sight line defined by look_angle + reference_height = range_vector.x * math.tan(look_angle) + # If we haven't seen ZERO_UP, we look for that first + if not seen_zero & TrajFlag.ZERO_UP: + if range_vector.y >= reference_height: + _flag |= TrajFlag.ZERO_UP + seen_zero |= TrajFlag.ZERO_UP + # We've crossed above sight line; now look for crossing back through it + elif not seen_zero & TrajFlag.ZERO_DOWN: + if range_vector.y < reference_height: + _flag |= TrajFlag.ZERO_DOWN + seen_zero |= TrajFlag.ZERO_DOWN + + # Mach crossing check + if (velocity / mach <= 1) and (previous_mach > 1): + _flag |= TrajFlag.MACH + + # Next range check + if range_vector.x >= next_range_distance: + _flag |= TrajFlag.RANGE + next_range_distance += step + current_item += 1 + + if _flag & filter_flags: + + windage = range_vector.z + + if twist != 0: + windage += (1.25 * (stability_coefficient + 1.2) + * math.pow(time, 1.83) * twist_coefficient) / 12 + + ranges.append(create_trajectory_row( + time, range_vector, velocity_vector, + velocity, mach, windage, weight, _flag.value + )) + + if current_item == ranges_length: + break + + previous_mach = velocity / mach + + velocity_adjusted = velocity_vector - wind_vector + + delta_time = calc_step / velocity_vector.x + velocity = velocity_adjusted.magnitude() + + drag = density_factor * velocity * self.drag_by_mach(velocity / mach) + + velocity_vector -= (velocity_adjusted * drag - gravity_vector) * delta_time + delta_range_vector = Vector(calc_step, + velocity_vector.y * delta_time, + velocity_vector.z * delta_time) + range_vector += delta_range_vector + velocity = velocity_vector.magnitude() + time += delta_range_vector.magnitude() / velocity + + return ranges + + def drag_by_mach(self, mach: float): + cd = calculate_by_curve(self._table_data, self._curve, mach) + return cd * 2.08551e-04 / self._bc + + @property + def cdm(self): + return self._cdm() + + def _cdm(self): + """ + Returns custom drag function based on input data + """ + drag_table = self.ammo.dm.drag_table + cdm = [] + bc = self.ammo.dm.value + + for point in drag_table: + st_mach = point['Mach'] + st_cd = calculate_by_curve(drag_table, self._curve, st_mach) + cd = st_cd * bc + cdm.append({'CD': cd, 'Mach': st_mach}) + + return cdm + + +def calculate_stability_coefficient(twist_rate: Distance, ammo: Ammo, atmo: Atmo): + weight = ammo.dm.weight >> Weight.Grain + diameter = ammo.dm.diameter >> Distance.Inch + twist = math.fabs(twist_rate >> Distance.Inch) / diameter + length = (ammo.length >> Distance.Inch) / diameter + ft = atmo.temperature >> Temperature.Fahrenheit + mv = ammo.mv >> Velocity.FPS + pt = atmo.pressure >> Pressure.InHg + sd = 30 * weight / ( + math.pow(twist, 2) * math.pow(diameter, 3) * length * (1 + math.pow(length, 2)) + ) + fv = math.pow(mv / 2800, 1.0 / 3.0) + ftp = ((ft + 460) / (59 + 460)) * (29.92 / pt) + return sd * fv * ftp + + +def wind_to_vector(wind: Wind) -> Vector: + """Calculate wind vector to add to projectile velocity vector each iteration: + Aerodynamic drag is function of velocity relative to the air stream. + + Wind angle of zero is blowing from behind shooter + Wind angle of 90-degree is blowing towards shooter's right + + NB: Presently we can only define Wind in the x-z plane, not any vertical component. + """ + # Downrange (x-axis) wind velocity component: + range_component = (wind.velocity >> Velocity.FPS) * math.cos(wind.direction_from >> Angular.Radian) + # Cross (z-axis) wind velocity component: + cross_component = (wind.velocity >> Velocity.FPS) * math.sin(wind.direction_from >> Angular.Radian) + return Vector(range_component, 0, cross_component) + + +def create_trajectory_row(time: float, range_vector: Vector, velocity_vector: Vector, + velocity: float, mach: float, windage: float, weight: float, flag: int): + drop_adjustment = get_correction(range_vector.x, range_vector.y) + windage_adjustment = get_correction(range_vector.x, windage) + trajectory_angle = math.atan(velocity_vector.y / velocity_vector.x) + + return TrajectoryData( + time=time, + distance=Distance.Foot(range_vector.x), + drop=Distance.Foot(range_vector.y), + drop_adj=Angular.Radian(drop_adjustment), + windage=Distance.Foot(windage), + windage_adj=Angular.Radian(windage_adjustment), + velocity=Velocity.FPS(velocity), + mach=velocity / mach, + energy=Energy.FootPound(calculate_energy(weight, velocity)), + angle=Angular.Radian(trajectory_angle), + ogw=Weight.Pound(calculate_ogv(weight, velocity)), + flag=flag + ) + + +def get_correction(distance: float, offset: float): + if distance != 0: + return math.atan(offset / distance) + return 0 # None + + +def calculate_energy(bullet_weight: float, velocity: float): + return bullet_weight * math.pow(velocity, 2) / 450400 + + +def calculate_ogv(bullet_weight: float, velocity: float): + return math.pow(bullet_weight, 2) * math.pow(velocity, 3) * 1.5e-12 + + +def calculate_curve(data_points): + # rate, x1, x2, x3, y1, y2, y3, a, b, c + # curve = [] + # curve_point + # num_points, len_data_points, len_data_range + + rate = (data_points[1]['CD'] - data_points[0]['CD']) \ + / (data_points[1]['Mach'] - data_points[0]['Mach']) + curve = [CurvePoint(0, rate, data_points[0]['CD'] - data_points[0]['Mach'] * rate)] + len_data_points = int(len(data_points)) + len_data_range = len_data_points - 1 + + for i in range(1, len_data_range): + x1 = data_points[i - 1]['Mach'] + x2 = data_points[i]['Mach'] + x3 = data_points[i + 1]['Mach'] + y1 = data_points[i - 1]['CD'] + y2 = data_points[i]['CD'] + y3 = data_points[i + 1]['CD'] + a = ((y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)) / ( + (x3 * x3 - x1 * x1) * (x2 - x1) - (x2 * x2 - x1 * x1) * (x3 - x1)) + b = (y2 - y1 - a * (x2 * x2 - x1 * x1)) / (x2 - x1) + c = y1 - (a * x1 * x1 + b * x1) + curve_point = CurvePoint(a, b, c) + curve.append(curve_point) + + num_points = len_data_points + rate = (data_points[num_points - 1]['CD'] - data_points[num_points - 2]['CD']) / \ + (data_points[num_points - 1]['Mach'] - data_points[num_points - 2]['Mach']) + curve_point = CurvePoint( + 0, rate, data_points[num_points - 1]['CD'] - data_points[num_points - 2]['Mach'] * rate + ) + curve.append(curve_point) + return curve + + +def calculate_by_curve(data: list, curve: list, mach: float): + """returning the calculated drag for a + specified mach based on previously calculated data""" + # num_points, mlo, mhi, mid + # cdef CurvePoint curve_m + + num_points = int(len(curve)) + mlo = 0 + mhi = num_points - 2 + + while mhi - mlo > 1: + mid = int(math.floor(mhi + mlo) / 2.0) + if data[mid]['Mach'] < mach: + mlo = mid + else: + mhi = mid + + if data[mhi]['Mach'] - mach > mach - data[mlo]['Mach']: + m = mlo + else: + m = mhi + curve_m = curve[m] + return curve_m.c + mach * (curve_m.b + curve_m.a * mach) diff --git a/py_ballisticcalc/trajectory_calculator.pyx b/py_ballisticcalc/trajectory_calculator.pyx deleted file mode 100644 index 68bcc94..0000000 --- a/py_ballisticcalc/trajectory_calculator.pyx +++ /dev/null @@ -1,273 +0,0 @@ -from libc.math cimport fabs, pow, sin, cos, log10, floor, atan -from .bmath.unit import * -from .bmath.vector import Vector -from .projectile import Ammunition -from .atmosphere import Atmosphere -from .shot_parameters import ShotParameters -from .wind import WindInfo -from .trajectory_data import * - -cdef double cZeroFindingAccuracy = 0.000005 -cdef double cMinimumVelocity = 50.0 -cdef double cMaximumDrop = -15000 -cdef int cMaxIterations = 10 -cdef double cGravityConstant = -32.17405 - -cdef class TrajectoryCalculator: - cdef _maximum_calculator_step_size - - def __init__(self): - self._maximum_calculator_step_size = Distance(1, DistanceFoot) - - cpdef maximum_calculator_step_size(self): - return self._maximum_calculator_step_size - - cpdef set_maximum_calculator_step_size(self, value: Distance): - self._maximum_calculator_step_size = value - - cdef double get_calculation_step(self, double step): - cdef step_order, maximum_order - step = step / 2 - cdef double maximum_step = self._maximum_calculator_step_size.get_in(DistanceFoot) - if step > maximum_step: - step_order = int(floor(log10(step))) - maximum_order = int(floor(log10(maximum_step))) - step = step / pow(10, float(step_order - maximum_order + 1)) - return step - - cpdef sight_angle(self, zero: Distance, sight_height: Distance, - ammunition: Ammunition, atmosphere: Atmosphere): - cdef double calculation_step, mach, density_factor, muzzle_velocity - cdef double barrel_azimuth, barrel_elevation - cdef double velocity, time, zero_distance, maximum_range - cdef double delta_time, drag, zero_finding_error - cdef int iterations_count - cdef gravity_vector, range_vector, velocity_vector, delta_range_vector - - calculation_step = self.get_calculation_step(Distance(10, zero.units()).get_in(DistanceFoot)) - mach = atmosphere.mach().get_in(VelocityFPS) - density_factor = atmosphere.density_factor() - muzzle_velocity = ammunition.muzzle_velocity().get_in(VelocityFPS) - gravity_vector = Vector(0, cGravityConstant, 0) - barrel_azimuth = 0.0 - barrel_elevation = 0.0 - - zero_finding_error = cZeroFindingAccuracy * 2 - iterations_count = 0 - while zero_finding_error > cZeroFindingAccuracy and iterations_count < cMaxIterations: - velocity = muzzle_velocity - time = 0.0 - - range_vector = Vector(0.0, -sight_height.get_in(DistanceFoot), 0.0) - velocity_vector = Vector( - cos(barrel_elevation) * cos(barrel_azimuth), - sin(barrel_elevation), - cos(barrel_elevation) * sin(barrel_azimuth) - ) * velocity - - zero_distance = zero.get_in(DistanceFoot) - maximum_range = zero_distance + calculation_step - - while range_vector.x() <= maximum_range: - if velocity < cMinimumVelocity or range_vector.y() < cMaximumDrop: - break - - delta_time = calculation_step / velocity_vector.x() - velocity = velocity_vector.magnitude() - drag = density_factor * velocity * ammunition.bullet().ballistic_coefficient().drag(velocity / mach) - velocity_vector = velocity_vector - (velocity_vector * drag - gravity_vector) * delta_time - delta_range_vector = Vector(calculation_step, - velocity_vector.y() * delta_time, - velocity_vector.z() * delta_time) - range_vector = range_vector + delta_range_vector - velocity = velocity_vector.magnitude() - time = time + delta_range_vector.magnitude() / velocity - if fabs(range_vector.x() - zero_distance) < 0.5 * calculation_step: - zero_finding_error = fabs(range_vector.y()) - barrel_elevation = barrel_elevation - range_vector.y() / range_vector.x() - break - - iterations_count += 1 - - return Angular(barrel_elevation, AngularRadian) - - cpdef trajectory(self, ammunition: Ammunition, atmosphere: Atmosphere, - shot_info: ShotParameters, wind_info: list[WindInfo], - twist: Distance, sight_height: Distance, - stopAtZero: bool = False, stopAtMach1: bool = False): - cdef double range_to, step, calculation_step, bullet_weight, stability_coefficient - cdef double barrel_azimuth, barrel_elevation, alt0, density_factor, mach - cdef double next_wind_range, time, muzzle_velocity, velocity, delta_time, drag - cdef double maximum_range, next_range_distance, twist_coefficient - cdef int current_item, ranges_length, current_wind, len_wind_info - cdef ranges, wind_vector - cdef velocity_adjusted, delta_range_vector - cdef gravity_vector, range_vector, velocity_vector - - range_to = shot_info.maximum_distance().get_in(DistanceFoot) - step = shot_info.step().get_in(DistanceFoot) - ranges_length = int(floor(range_to / step)) + 1 # We might include up to two extra rows: Zero and Mach1 - ranges = [] - calculation_step = self.get_calculation_step(step) - - bullet_weight = ammunition.bullet().bullet_weight().get_in(WeightGrain) - - stability_coefficient = 1.0 - twist_coefficient = 0 - if twist.get_value() != 0 and ammunition.bullet().has_dimensions(): - stability_coefficient = calculate_stability_coefficient(ammunition, twist, atmosphere) - if twist.get_value() < 0: - twist_coefficient = -1 - else: - twist_coefficient = 1 - - barrel_azimuth = .0 - barrel_elevation = shot_info.sight_angle().get_in(AngularRadian) - barrel_elevation = barrel_elevation + shot_info.shot_angle().get_in(AngularRadian) - alt0 = atmosphere.altitude().get_in(DistanceFoot) - - # Never used in upstream, uncomment on need - # density_factor, mach = atmosphere.get_density_factor_and_mach_for_altitude(alt0) - - current_wind = 0 - next_wind_range = 1e7 - len_wind_info = int(len(wind_info)) - if len_wind_info < 1: - wind_vector = Vector(0, 0, 0) - else: - if len(wind_info) > 1: - next_wind_range = wind_info[0].until_distance().get_in(DistanceFoot) - wind_vector = wind_to_vector(shot_info, wind_info[0]) - - muzzle_velocity = ammunition.muzzle_velocity().get_in(VelocityFPS) - gravity_vector = Vector(0, cGravityConstant, 0) - velocity = muzzle_velocity - time = .0 - - # x - distance towards target - # y - drop - # z - windage - - range_vector = Vector(.0, -sight_height.get_in(DistanceFoot), 0) - velocity_vector = Vector(cos(barrel_elevation) * cos(barrel_azimuth), sin(barrel_elevation), - cos(barrel_elevation) * sin(barrel_azimuth)) * velocity - current_item = 0 - maximum_range = range_to - next_range_distance = 0 - previousY = 0 # Used to find zero-crossing - previousMach = 0 # Used to find sound-barrier crossing - - while range_vector.x() <= maximum_range + calculation_step: - - if velocity < cMinimumVelocity or range_vector.y() < cMaximumDrop: - break - - density_factor, mach = atmosphere.get_density_factor_and_mach_for_altitude(alt0 + range_vector.y()) - - if range_vector.x() >= next_wind_range: - current_wind += 1 - wind_vector = wind_to_vector(shot_info, wind_info[current_wind]) - if current_wind == len_wind_info - 1: - next_wind_range = 1e7 - else: - next_wind_range = wind_info[current_wind].until_distance().get_in(DistanceFoot) - - if (range_vector.y() < 0) and (previousY > 0): # Zero-crossing - ranges.append(calculate_trajectory_row(ZERO, - time, range_vector, velocity_vector, velocity, mach, - bullet_weight, stability_coefficient, twist_coefficient)) - if stopAtZero: - break - elif range_vector.x() + calculation_step >= next_range_distance: - next_range_distance += step - elif (velocity / mach < 1) and (previousMach > 1): # Sound-crossing - ranges.append(calculate_trajectory_row(MACH1, - time, range_vector, velocity_vector, velocity, mach, - bullet_weight, stability_coefficient, twist_coefficient)) - if stopAtMach1: - break - elif range_vector.x() + calculation_step >= next_range_distance: - next_range_distance += step - elif range_vector.x() >= next_range_distance: - ranges.append(calculate_trajectory_row(TRAJECTORY, - time, range_vector, velocity_vector, velocity, mach, - bullet_weight, stability_coefficient, twist_coefficient)) - next_range_distance += step - current_item += 1 - if current_item == ranges_length: - break - - previousY = range_vector.y() - previousMach = velocity / mach - - delta_time = calculation_step / velocity_vector.x() - velocity_adjusted = velocity_vector - wind_vector - velocity = velocity_adjusted.magnitude() - drag = density_factor * velocity * ammunition.bullet().ballistic_coefficient().drag(velocity / mach) - velocity_vector = velocity_vector - (velocity_adjusted * drag - gravity_vector) * delta_time - delta_range_vector = Vector(calculation_step, - velocity_vector.y() * delta_time, - velocity_vector.z() * delta_time) - range_vector = range_vector + delta_range_vector - velocity = velocity_vector.magnitude() - time = time + delta_range_vector.magnitude() / velocity - - return ranges - - -cpdef calculate_trajectory_row(row_type, time, range_vector, velocity_vector, velocity, mach, - bullet_weight, stability_coefficient, twist_coefficient): - windage = range_vector.z() - if twist_coefficient != 0: - windage += (1.25 * (stability_coefficient + 1.2) * pow(time, 1.83) * twist_coefficient) / 12 - windage_adjustment = get_correction(range_vector.x(), windage) - drop_adjustment = get_correction(range_vector.x(), range_vector.y()) - return TrajectoryData(time=Timespan(time), - travel_distance=Distance(range_vector.x(), DistanceFoot), - drop=Distance(range_vector.y(), DistanceFoot), - drop_adjustment=Angular(drop_adjustment, AngularRadian), - windage=Distance(windage, DistanceFoot), - windage_adjustment=Angular(windage_adjustment, AngularRadian), - velocity=Velocity(velocity, VelocityFPS), - angle=Angular(atan(velocity_vector.y()/velocity_vector.x()), AngularRadian), - mach=velocity / mach, - energy=Energy(calculate_energy(bullet_weight, velocity), EnergyFootPound), - optimal_game_weight=Weight(calculate_ogv(bullet_weight, velocity), WeightPound), - row_type=row_type) - - -cdef double calculate_stability_coefficient(ammunition_info: Ammunition, barrelTwist: Distance, atmosphere: Atmosphere): - cdef double weight = ammunition_info.bullet().bullet_weight().get_in(WeightGrain) - cdef double diameter = ammunition_info.bullet().bullet_diameter().get_in(DistanceInch) - cdef double twist = barrelTwist.get_in(DistanceInch) / diameter - cdef double length = ammunition_info.bullet().bullet_length().get_in(DistanceInch) / diameter - cdef double sd = 30 * weight / (pow(twist, 2) * pow(diameter, 3) * length * (1 + pow(length, 2))) - cdef double mv = ammunition_info.muzzle_velocity().get_in(VelocityFPS) - cdef double fv = pow(mv / 2800, 1.0 / 3.0) - cdef double ft = atmosphere.temperature().get_in(TemperatureFahrenheit) - cdef double pt = atmosphere.pressure().get_in(PressureInHg) - cdef double ftp = ((ft + 460) / (59 + 460)) * (29.92 / pt) - return sd * fv * ftp - -cdef wind_to_vector(shot, wind): - cdef double sight_cosine = cos(shot.sight_angle().get_in(AngularRadian)) - cdef double sight_sine = sin(shot.sight_angle().get_in(AngularRadian)) - cdef double cant_cosine = cos(shot.cant_angle().get_in(AngularRadian)) - cdef double cant_sine = sin(shot.cant_angle().get_in(AngularRadian)) - cdef double range_velocity = wind.velocity().get_in(VelocityFPS) * cos(wind.direction().get_in(AngularRadian)) - cdef double cross_component = wind.velocity().get_in(VelocityFPS) * sin(wind.direction().get_in(AngularRadian)) - cdef double range_factor = -range_velocity * sight_sine - return Vector(range_velocity * sight_cosine, - range_factor * cant_cosine + cross_component * cant_sine, - cross_component * cant_cosine - range_factor * cant_sine) - -cdef get_correction(double distance, double offset): - if distance != 0: - return atan(offset / distance) - return 0 - -cdef double calculate_energy(double bullet_weight, double velocity): - return bullet_weight * pow(velocity, 2) / 450400 - -cdef double calculate_ogv(double bullet_weight, double velocity): - return pow(bullet_weight, 2) * pow(velocity, 3) * 1.5e-12 diff --git a/py_ballisticcalc/trajectory_data.py b/py_ballisticcalc/trajectory_data.py new file mode 100644 index 0000000..c25c580 --- /dev/null +++ b/py_ballisticcalc/trajectory_data.py @@ -0,0 +1,370 @@ +"""Implements a point of trajectory class in applicable data types""" +import logging +import math +import typing +from dataclasses import dataclass, field +from enum import Flag +from typing import NamedTuple + +from .settings import Settings as Set +from .unit import Angular, Distance, Weight, Velocity, Energy, AbstractUnit, Unit +from .conditions import Shot + +try: + import pandas as pd +except ImportError as error: + logging.warning("Install pandas to convert trajectory to dataframe") + pd = None + +try: + import matplotlib + from matplotlib import patches +except ImportError as error: + logging.warning("Install matplotlib to get results as a plot") + matplotlib = None + +__all__ = ('TrajectoryData', 'HitResult', 'TrajFlag') + + +PLOT_FONT_HEIGHT = 72 +PLOT_FONT_SIZE = 552 / PLOT_FONT_HEIGHT + + +class TrajFlag(Flag): + """Flags for marking trajectory row if Zero or Mach crossing + Also uses to set a filters for a trajectory calculation loop + """ + NONE = 0 + ZERO_UP = 1 + ZERO_DOWN = 2 + MACH = 4 + RANGE = 8 + DANGER = 16 + ZERO = ZERO_UP | ZERO_DOWN + ALL = RANGE | ZERO_UP | ZERO_DOWN | MACH | DANGER + + +class TrajectoryData(NamedTuple): + """ + Represents point of trajectory in applicable data types + + Attributes: + time (float): bullet flight time + distance (Distance): traveled distance + velocity (Velocity): velocity in current trajectory point + mach (float): velocity in current trajectory point in "Mach" number + drop (Distance): + drop_adj (Angular): + windage (Distance): + windage_adj (Angular): + angle (Angular) + mach float + energy (Energy): + ogw (Weight): optimal game weight + rtype (int): row type + """ + + time: float + distance: Distance + velocity: Velocity + mach: float # velocity in Mach + drop: Distance + drop_adj: Angular # drop_adjustment + windage: Distance + windage_adj: Angular # windage_adjustment + angle: Angular # Trajectory angle + energy: Energy + ogw: Weight + flag: typing.Union[TrajFlag, int] + + def formatted(self) -> tuple: + """ + :return: matrix of formatted strings for each value of trajectory in default units + """ + + def _fmt(v: AbstractUnit, u: Unit): + """simple formatter""" + return f"{v >> u:.{u.accuracy}f} {u.symbol}" + + return ( + f'{self.time:.2f} s', + _fmt(self.distance, Set.Units.distance), + _fmt(self.velocity, Set.Units.velocity), + f'{self.mach:.2f} mach', + _fmt(self.drop, Set.Units.drop), + _fmt(self.drop_adj, Set.Units.adjustment), + _fmt(self.windage, Set.Units.drop), + _fmt(self.windage_adj, Set.Units.adjustment), + _fmt(self.angle, Set.Units.angular), + _fmt(self.energy, Set.Units.energy), + _fmt(self.ogw, Set.Units.ogw), + + self.flag + ) + + def in_def_units(self) -> tuple: + """ + :return: matrix of floats of the trajectory in default units + """ + return ( + self.time, + self.distance >> Set.Units.distance, + self.velocity >> Set.Units.velocity, + self.mach, + self.drop >> Set.Units.drop, + self.drop_adj >> Set.Units.adjustment, + self.windage >> Set.Units.drop, + self.windage_adj >> Set.Units.adjustment, + self.angle >> Set.Units.angular, + self.energy >> Set.Units.energy, + self.ogw >> Set.Units.ogw, + TrajFlag(self.flag) + ) + + +class DangerSpace(NamedTuple): + """Stores the danger space data for distance specified""" + at_range: TrajectoryData + target_height: Distance + begin: TrajectoryData + end: TrajectoryData + look_angle: Angular + + def __str__(self) -> str: + return f'Danger space at {self.at_range.distance << Set.Units.distance} '\ + + f'for {self.target_height << Set.Units.drop} tall target '\ + + (f'at {self.look_angle << Angular.Degree} look-angle ' if self.look_angle != 0 else '')\ + + f'ranges from {self.begin.distance << Set.Units.distance} '\ + + f'to {self.end.distance << Set.Units.distance}' + + def overlay(self, ax: 'Axes', label: str=None): + """Highlights danger-space region on plot""" + if matplotlib is None: + raise ImportError("Install matplotlib to get results as a plot") + + begin_dist = self.begin.distance >> Set.Units.distance + begin_drop = self.begin.drop >> Set.Units.drop + end_dist = self.end.distance >> Set.Units.distance + end_drop = self.end.drop >> Set.Units.drop + range_dist = self.at_range.distance >> Set.Units.distance + range_drop = self.at_range.drop >> Set.Units.drop + h = self.target_height >> Set.Units.drop + + # Target position and height: + ax.plot((range_dist, range_dist), (range_drop + h / 2, range_drop - h / 2), + color='r', linestyle=':') + # Shaded danger-space region: + vertices = ( + (begin_dist, begin_drop), (end_dist, begin_drop), + (end_dist, end_drop), (begin_dist, end_drop) + ) + polygon = patches.Polygon(vertices, closed=True, + edgecolor='none', facecolor='r', alpha=0.3) + ax.add_patch(polygon) + if label is None: # Add default label + label = f"Danger space\nat {self.at_range.distance << Set.Units.distance}" + if label != '': + ax.text(begin_dist + (end_dist-begin_dist)/2, end_drop, label, + linespacing=1.2, fontsize=PLOT_FONT_SIZE, ha='center', va='top') + + +@dataclass(frozen=True) +class HitResult: + """Results of the shot""" + shot: Shot + trajectory: list[TrajectoryData] = field(repr=False) + extra: bool = False + + def __iter__(self): + for row in self.trajectory: + yield row + + def __getitem__(self, item): + return self.trajectory[item] + + def __check_extra__(self): + if not self.extra: + raise AttributeError( + f"{object.__repr__(self)} has no extra data. " + f"Use Calculator.fire(..., extra_data=True)" + ) + + def zeros(self) -> list[TrajectoryData]: + """:return: zero crossing points""" + self.__check_extra__() + data = [row for row in self.trajectory if row.flag & TrajFlag.ZERO.value] + if len(data) < 1: + raise ArithmeticError("Can't find zero crossing points") + return data + + def index_at_distance(self, d: Distance) -> int: + """ + :param d: Distance for which we want Trajectory Data + :return: Index of first trajectory row with .distance >= d; otherwise -1 + """ + # Get index of first trajectory point with distance >= at_range + return next((i for i in range(len(self.trajectory)) + if self.trajectory[i].distance >= d), -1) + + def get_at_distance(self, d: Distance) -> TrajectoryData: + """ + :param d: Distance for which we want Trajectory Data + :return: First trajectory row with .distance >= d + """ + i = self.index_at_distance(d) + if i < 0: + raise ArithmeticError( + f"Calculated trajectory doesn't reach requested distance {d}" + ) + return self.trajectory[i] + + def danger_space(self, + at_range: [float, Distance], + target_height: [float, Distance], + look_angle: [float, Angular] = Angular(0, Angular.Degree) + ) -> DangerSpace: + """ + Assume that the trajectory hits the center of a target at any distance. + Now we want to know how much ranging error we can tolerate if the critical region + of the target has height *h*. I.e., we want to know how far forward and backward + along the line of sight we can move a target such that the trajectory is still + within *h*/2 of the original drop. + + :param at_range: Danger space is calculated for a target centered at this distance + :param target_height: Target height (*h*) determines danger space + :param look_angle: Ranging errors occur along the look angle to the target + """ + self.__check_extra__() + + at_range = Set.Units.distance(at_range) + target_height = Set.Units.distance(target_height) + look_angle = Set.Units.angular(look_angle) + + # Get index of first trajectory point with distance >= at_range + i = self.index_at_distance(at_range) + if i < 0: + raise ArithmeticError( + f"Calculated trajectory doesn't reach requested distance {at_range}" + ) + + target_height_half = target_height.raw_value / 2.0 + tan_look_angle = math.tan(look_angle >> Angular.Radian) + + # Target_center height shifts along look_angle as: + # target_center' = target_center + (.distance' - .distance) * tan(look_angle) + + def find_begin_danger(row_num: int) -> TrajectoryData: + """ + Beginning of danger space is last .distance' < .distance where + |target_center - .drop'| >= target_height/2 + :param row_num: Index of the trajectory point for which we are calculating danger space + :return: Distance marking beginning of danger space + """ + center_row = self.trajectory[row_num] + for i in range(row_num - 1, 0, -1): + prime_row = self.trajectory[i] + target_center = center_row.drop.raw_value + tan_look_angle * ( + prime_row.distance.raw_value - center_row.distance.raw_value + ) + if abs(target_center - prime_row.drop.raw_value) >= target_height_half: + return self.trajectory[i] + return self.trajectory[0] + + def find_end_danger(row_num: int) -> TrajectoryData: + """ + End of danger space is first .distance' > .distance where + |target_center - .drop'| >= target_height/2 + :param row_num: Index of the trajectory point for which we are calculating danger space + :return: Distance marking end of danger space + """ + center_row = self.trajectory[row_num] + for i in range(row_num + 1, len(self.trajectory)): + prime_row = self.trajectory[i] + target_center = center_row.drop.raw_value + tan_look_angle * ( + prime_row.distance.raw_value - center_row.distance.raw_value) + if abs(target_center - prime_row.drop.raw_value) >= target_height_half: + return prime_row + return self.trajectory[-1] + + return DangerSpace(self.trajectory[i], + target_height, + find_begin_danger(i), + find_end_danger(i), + look_angle) + + def dataframe(self, formatted: bool = False): + """ + :param formatted: False for values as floats; True for strings with units + :return: the trajectory table as a DataFrame + """ + if pd is None: + raise ImportError("Install pandas to get trajectory as dataframe") + col_names = list(TrajectoryData._fields) + if formatted: + trajectory = [p.formatted() for p in self] + else: + trajectory = [p.in_def_units() for p in self] + return pd.DataFrame(trajectory, columns=col_names) + + def plot(self, look_angle: Angular = None) -> 'Axes': + """:return: graph of the trajectory""" + if look_angle is None: + look_angle = self.shot.look_angle + + if matplotlib is None: + raise ImportError("Install matplotlib to plot results") + if not self.extra: + logging.warning("HitResult.plot: To show extended data" + "Use Calculator.fire(..., extra_data=True)") + font_size = PLOT_FONT_SIZE + df = self.dataframe() + ax = df.plot(x='distance', y=['drop'], ylabel=Set.Units.drop.symbol) + max_range = self.trajectory[-1].distance >> Set.Units.distance + + for p in self.trajectory: + if TrajFlag(p.flag) & TrajFlag.ZERO: + ax.plot([p.distance >> Set.Units.distance, p.distance >> Set.Units.distance], + [df['drop'].min(), p.drop >> Set.Units.drop], linestyle=':') + ax.text((p.distance >> Set.Units.distance) + max_range/100, df['drop'].min(), + f"{(TrajFlag(p.flag) & TrajFlag.ZERO).name}", + fontsize=font_size, rotation=90) + if TrajFlag(p.flag) & TrajFlag.MACH: + ax.plot([p.distance >> Set.Units.distance, p.distance >> Set.Units.distance], + [df['drop'].min(), p.drop >> Set.Units.drop], linestyle=':') + ax.text((p.distance >> Set.Units.distance) + max_range/100, df['drop'].min(), + "Mach 1", fontsize=font_size, rotation=90) + + max_range_in_drop_units = self.trajectory[-1].distance >> Set.Units.drop + # Sight line + x_sight = [0, df.distance.max()] + y_sight = [0, max_range_in_drop_units * math.tan(look_angle >> Angular.Radian)] + ax.plot(x_sight, y_sight, linestyle='--', color=[.3,0,.3,.5]) + # Barrel pointing line + x_bbl = [0, df.distance.max()] + y_bbl = [-(self.shot.weapon.sight_height >> Set.Units.drop), + max_range_in_drop_units * math.tan(self.trajectory[0].angle >> Angular.Radian) + -(self.shot.weapon.sight_height >> Set.Units.drop)] + ax.plot(x_bbl, y_bbl, linestyle=':', color=[0,0,0,.5]) + # Line labels + sight_above_bbl = True if y_sight[1] > y_bbl[1] else False + angle = math.degrees(math.atan((y_sight[1]-y_sight[0])/(x_sight[1]-x_sight[0]))) + ax.text(x_sight[1], y_sight[1], "Sight line", linespacing=1.2, + rotation=angle, rotation_mode='anchor', transform_rotates_text=True, + fontsize=font_size, color=[.3,0,.3,1], ha='right', + va='bottom' if sight_above_bbl else 'top') + angle = math.degrees(math.atan((y_bbl[1]-y_bbl[0])/(x_bbl[1]-x_bbl[0]))) + ax.text(x_bbl[1], y_bbl[1], "Barrel pointing", linespacing=1.2, + rotation=angle, rotation_mode='anchor', transform_rotates_text=True, + fontsize=font_size, color='k', ha='right', + va='top' if sight_above_bbl else 'bottom') + + df.plot(x='distance', xlabel=Set.Units.distance.symbol, + y=['velocity'], ylabel=Set.Units.velocity.symbol, + secondary_y=True, color=[0,.3,0,.5], + ylim=[0, df['velocity'].max()], ax=ax) + + # Let secondary shine through + ax.set_zorder(1) + ax.set_facecolor([0,0,0,0]) + + return ax diff --git a/py_ballisticcalc/trajectory_data.pyx b/py_ballisticcalc/trajectory_data.pyx deleted file mode 100644 index 620608e..0000000 --- a/py_ballisticcalc/trajectory_data.pyx +++ /dev/null @@ -1,98 +0,0 @@ -from libc.math cimport fmod, floor -from .bmath.unit import * - -cdef class Timespan: - cdef double _time - - def __init__(self, time: double): - self._time = time - - cpdef double total_seconds(self): - return self._time - - cpdef seconds(self): - return fmod(floor(self._time), 60) - - cpdef minutes(self): - return fmod(floor(self._time / 60), 60) - -TRAJECTORY = 1 -ZERO = 2 -MACH1 = 3 - -cdef class TrajectoryData: - cdef _time - cdef _travel_distance - cdef _velocity - cdef _angle # Trajectory angle - cdef double _mach - cdef _drop - cdef _drop_adjustment - cdef _windage - cdef _windage_adjustment - cdef _energy - cdef _optimal_game_weight - cdef _row_type - - def __init__(self, - time: Timespan, - travel_distance: Distance, - velocity: Velocity, - angle: [Angular, None], - mach: double, - drop: Distance, - drop_adjustment: [Angular, None], - windage: Distance, - windage_adjustment: [Angular, None], - energy: Energy, - optimal_game_weight: Weight, - row_type: int = TRAJECTORY #ROW_TYPE = ROW_TYPE.TRAJECTORY - ): - self._time = time - self._travel_distance = travel_distance - self._velocity = velocity - self._angle = angle - self._mach = mach - self._drop = drop - self._drop_adjustment = drop_adjustment - self._windage = windage - self._windage_adjustment = windage_adjustment - self._energy = energy - self._optimal_game_weight = optimal_game_weight - self._row_type = row_type - - cpdef row_type(self): - return self._row_type - - cpdef time(self): - return self._time - - cpdef travelled_distance(self): - return self._travel_distance - - cpdef velocity(self): - return self._velocity - - cpdef angle(self): - return self._angle - - cpdef double mach_velocity(self): - return self._mach - - cpdef drop(self): - return self._drop - - cpdef drop_adjustment(self): - return self._drop_adjustment - - cpdef windage(self): - return self._windage - - cpdef windage_adjustment(self): - return self._windage_adjustment - - cpdef energy(self): - return self._energy - - def optimal_game_weight(self): - return self._optimal_game_weight diff --git a/py_ballisticcalc/unit.py b/py_ballisticcalc/unit.py new file mode 100644 index 0000000..3cb7f95 --- /dev/null +++ b/py_ballisticcalc/unit.py @@ -0,0 +1,638 @@ +""" +Useful types for units of measurement conversion for ballistics calculations +""" + +import typing +from enum import IntEnum +from math import pi, atan, tan +from typing import NamedTuple +from dataclasses import dataclass + +__all__ = ('Unit', 'AbstractUnit', 'UnitProps', 'UnitPropsDict', 'Distance', + 'Velocity', 'Angular', 'Temperature', 'Pressure', + 'Energy', 'Weight', 'TypedUnits') + + +class Unit(IntEnum): + """ + Usage of IntEnum simplify data serializing for using it with databases etc. + """ + RAD = 0 + DEGREE = 1 + MOA = 2 + MIL = 3 + MRAD = 4 + THOUSANDTH = 5 + INCHES_PER_100YD = 6 + CM_PER_100M = 7 + O_CLOCK = 8 + + INCH = 10 + FOOT = 11 + YARD = 12 + MILE = 13 + NAUTICAL_MILE = 14 + MILLIMETER = 15 + CENTIMETER = 16 + METER = 17 + KILOMETER = 18 + LINE = 19 + + FOOT_POUND = 30 + JOULE = 31 + + MM_HG = 40 + IN_HG = 41 + BAR = 42 + HPA = 43 + PSI = 44 + + FAHRENHEIT = 50 + CELSIUS = 51 + KELVIN = 52 + RANKIN = 53 + + MPS = 60 + KMH = 61 + FPS = 62 + MPH = 63 + KT = 64 + + GRAIN = 70 + OUNCE = 71 + GRAM = 72 + POUND = 73 + KILOGRAM = 74 + NEWTON = 75 + + @property + def key(self) -> str: + """ + :return: readable name of the unit of measure + """ + return UnitPropsDict[self].name + + @property + def accuracy(self) -> int: + """ + :return: default accuracy of the unit of measure + """ + return UnitPropsDict[self].accuracy + + @property + def symbol(self) -> str: + """ + :return: short symbol of the unit of measure in CI + """ + return UnitPropsDict[self].symbol + + def __repr__(self) -> str: + return UnitPropsDict[self].name + + def __call__(self: 'Unit', value: [int, float, 'AbstractUnit']) -> 'AbstractUnit': + """Creates new unit instance by dot syntax + :param self: unit as Unit enum + :param value: numeric value of the unit + :return: AbstractUnit instance + """ + if isinstance(value, AbstractUnit): + return value << self + if 0 <= self < 10: + obj = Angular(value, self) + elif 10 <= self < 20: + obj = Distance(value, self) + elif 30 <= self < 40: + obj = Energy(value, self) + elif 40 <= self < 50: + obj = Pressure(value, self) + elif 50 <= self < 60: + obj = Temperature(value, self) + elif 60 <= self < 70: + obj = Velocity(value, self) + elif 70 <= self < 80: + obj = Weight(value, self) + else: + raise TypeError(f"{self} Unit is not supported") + return obj + + +class UnitProps(NamedTuple): + """Properties of unit measure""" + name: str + accuracy: int + symbol: str + + +UnitPropsDict = { + Unit.RAD: UnitProps('radian', 6, 'rad'), + Unit.DEGREE: UnitProps('degree', 4, '°'), + Unit.MOA: UnitProps('moa', 2, 'moa'), + Unit.MIL: UnitProps('mil', 2, 'mil'), + Unit.MRAD: UnitProps('mrad', 2, 'mrad'), + Unit.THOUSANDTH: UnitProps('thousandth', 2, 'ths'), + Unit.INCHES_PER_100YD: UnitProps('inch/100yd', 2, 'in/100yd'), + Unit.CM_PER_100M: UnitProps('cm/100m', 2, 'cm/100m'), + Unit.O_CLOCK: UnitProps('h', 2, 'h'), + + Unit.INCH: UnitProps("inch", 1, "inch"), + Unit.FOOT: UnitProps("foot", 2, "ft"), + Unit.YARD: UnitProps("yard", 1, "yd"), + Unit.MILE: UnitProps("mile", 3, "mi"), + Unit.NAUTICAL_MILE: UnitProps("nautical mile", 3, "nm"), + Unit.MILLIMETER: UnitProps("millimeter", 3, "mm"), + Unit.CENTIMETER: UnitProps("centimeter", 3, "cm"), + Unit.METER: UnitProps("meter", 1, "m"), + Unit.KILOMETER: UnitProps("kilometer", 3, "km"), + Unit.LINE: UnitProps("line", 3, "ln"), + + Unit.FOOT_POUND: UnitProps('foot-pound', 0, 'ft·lb'), + Unit.JOULE: UnitProps('joule', 0, 'J'), + + Unit.MM_HG: UnitProps('mmhg', 0, 'mmHg'), + Unit.IN_HG: UnitProps('inhg', 6, 'Hg'), + Unit.BAR: UnitProps('bar', 2, 'bar'), + Unit.HPA: UnitProps('hpa', 4, 'hPa'), + Unit.PSI: UnitProps('psi', 4, 'psi'), + + Unit.FAHRENHEIT: UnitProps('fahrenheit', 1, '°F'), + Unit.CELSIUS: UnitProps('celsius', 1, '°C'), + Unit.KELVIN: UnitProps('kelvin', 1, '°K'), + Unit.RANKIN: UnitProps('rankin', 1, '°R'), + + Unit.MPS: UnitProps('mps', 0, 'm/s'), + Unit.KMH: UnitProps('kmh', 1, 'km/h'), + Unit.FPS: UnitProps('fps', 1, 'ft/s'), + Unit.MPH: UnitProps('mph', 1, 'mph'), + Unit.KT: UnitProps('kt', 1, 'kt'), + + Unit.GRAIN: UnitProps('grain', 0, 'gr'), + Unit.OUNCE: UnitProps('ounce', 1, 'oz'), + Unit.GRAM: UnitProps('gram', 1, 'g'), + Unit.POUND: UnitProps('pound', 0, 'lb'), + Unit.KILOGRAM: UnitProps('kilogram', 3, 'kg'), + Unit.NEWTON: UnitProps('newton', 3, 'N'), +} + + +class AbstractUnit: + """Abstract class for unit of measure instance definition + Stores defined unit and value, applies conversions to other units + """ + __slots__ = ('_value', '_defined_units') + + def __init__(self, value: [float, int], units: Unit): + """ + :param units: unit as Unit enum + :param value: numeric value of the unit + """ + self._value: float = self.to_raw(value, units) + self._defined_units: Unit = units + + def __str__(self) -> str: + """ + :return: readable unit value + """ + units = self._defined_units + props = UnitPropsDict[units] + v = self.from_raw(self._value, units) + return f'{round(v, props.accuracy)}{props.symbol}' + + def __repr__(self) -> str: + """ + :return: instance as readable view + """ + return f'<{self.__class__.__name__}: {self << self.units} ({round(self._value, 4)})>' + + def __float__(self): + return float(self._value) + + def __eq__(self, other): + return float(self) == other + + def __lt__(self, other): + return float(self) < other + + def __gt__(self, other): + return float(self) > other + + def __le__(self, other): + return float(self) <= other + + def __ge__(self, other): + return float(self) >= other + + def __lshift__(self, other: Unit): + return self.convert(other) + + def __rshift__(self, other: Unit): + return self.get_in(other) + + def __rlshift__(self, other: Unit): + return self.convert(other) + + def _unit_support_error(self, value: float, units: Unit): + """Validates the units + :param value: value of the unit + :param units: Unit enum type + :return: value in specified units + """ + if not isinstance(units, Unit): + err_msg = f"Type expected: {Unit}, {type(Unit).__name__} " \ + f"found: {type(units).__name__} ({value})" + raise TypeError(err_msg) + if units not in self.__dict__.values(): + raise ValueError(f'{self.__class__.__name__}: unit {units} is not supported') + return 0 + + def to_raw(self, value: float, units: Unit) -> float: + """Converts value with specified units to raw value + :param value: value of the unit + :param units: Unit enum type + :return: value in specified units + """ + return self._unit_support_error(value, units) + + def from_raw(self, value: float, units: Unit) -> float: + """Converts raw value to specified units + :param value: raw value of the unit + :param units: Unit enum type + :return: value in specified units + """ + return self._unit_support_error(value, units) + + def convert(self, units: Unit) -> 'AbstractUnit': + """Returns new unit instance in specified units + :param units: Unit enum type + :return: new unit instance in specified units + """ + value = self.get_in(units) + return self.__class__(value, units) + + def get_in(self, units: Unit) -> float: + """ + :param units: Unit enum type + :return: value in specified units + """ + return self.from_raw(self._value, units) + + @property + def units(self) -> Unit: + """ + :return: defined units + """ + return self._defined_units + + @property + def unit_value(self) -> float: + """Returns float value in defined units""" + return self.get_in(self.units) + + @property + def raw_value(self) -> float: + """Raw unit value getter + :return: raw unit value + """ + return self._value + + +class Distance(AbstractUnit): + """Distance unit""" + + def to_raw(self, value: float, units: Unit): + if units == Distance.Inch: + return value + if units == Distance.Foot: + result = value * 12 + elif units == Distance.Yard: + result = value * 36 + elif units == Distance.Mile: + result = value * 63360 + elif units == Distance.NauticalMile: + result = value * 72913.3858 + elif units == Distance.Line: + result = value / 10 + elif units == Distance.Millimeter: + result = value / 25.4 + elif units == Distance.Centimeter: + result = value / 2.54 + elif units == Distance.Meter: + result = value / 25.4 * 1000 + elif units == Distance.Kilometer: + result = value / 25.4 * 1000000 + else: + return super().to_raw(value, units) + return result + + def from_raw(self, value: float, units: Unit): + if units == Distance.Inch: + return value + if units == Distance.Foot: + result = value / 12 + elif units == Distance.Yard: + result = value / 36 + elif units == Distance.Mile: + result = value / 63360 + elif units == Distance.NauticalMile: + result = value / 72913.3858 + elif units == Distance.Line: + result = value * 10 + elif units == Distance.Millimeter: + result = value * 25.4 + elif units == Distance.Centimeter: + result = value * 2.54 + elif units == Distance.Meter: + result = value * 25.4 / 1000 + elif units == Distance.Kilometer: + result = value * 25.4 / 1000000 + else: + return super().from_raw(value, units) + return result + + Inch = Unit.INCH + Foot = Unit.FOOT + Yard = Unit.YARD + Mile = Unit.MILE + NauticalMile = Unit.NAUTICAL_MILE + Millimeter = Unit.MILLIMETER + Centimeter = Unit.CENTIMETER + Meter = Unit.METER + Kilometer = Unit.KILOMETER + Line = Unit.LINE + + +class Pressure(AbstractUnit): + """Pressure unit""" + + def to_raw(self, value: float, units: Unit): + if units == Pressure.MmHg: + return value + if units == Pressure.InHg: + result = value * 25.4 + elif units == Pressure.Bar: + result = value * 750.061683 + elif units == Pressure.hPa: + result = value * 750.061683 / 1000 + elif units == Pressure.PSI: + result = value * 51.714924102396 + else: + return super().to_raw(value, units) + return result + + def from_raw(self, value: float, units: Unit): + if units == Pressure.MmHg: + return value + if units == Pressure.InHg: + result = value / 25.4 + elif units == Pressure.Bar: + result = value / 750.061683 + elif units == Pressure.hPa: + result = value / 750.061683 * 1000 + elif units == Pressure.PSI: + result = value / 51.714924102396 + else: + return super().from_raw(value, units) + return result + + MmHg = Unit.MM_HG + InHg = Unit.IN_HG + Bar = Unit.BAR + hPa = Unit.HPA + PSI = Unit.PSI + + +class Weight(AbstractUnit): + """Weight unit""" + + def to_raw(self, value: float, units: Unit): + if units == Weight.Grain: + return value + if units == Weight.Gram: + result = value * 15.4323584 + elif units == Weight.Kilogram: + result = value * 15432.3584 + elif units == Weight.Newton: + result = value * 151339.73750336 + elif units == Weight.Pound: + result = value / 0.000142857143 + elif units == Weight.Ounce: + result = value * 437.5 + else: + return super().to_raw(value, units) + return result + + def from_raw(self, value: float, units: Unit): + if units == Weight.Grain: + return value + if units == Weight.Gram: + result = value / 15.4323584 + elif units == Weight.Kilogram: + result = value / 15432.3584 + elif units == Weight.Newton: + result = value / 151339.73750336 + elif units == Weight.Pound: + result = value * 0.000142857143 + elif units == Weight.Ounce: + result = value / 437.5 + else: + return super().from_raw(value, units) + return result + + Grain = Unit.GRAIN + Ounce = Unit.OUNCE + Gram = Unit.GRAM + Pound = Unit.POUND + Kilogram = Unit.KILOGRAM + Newton = Unit.NEWTON + + +class Temperature(AbstractUnit): + """Temperature unit""" + + def to_raw(self, value: float, units: Unit): + if units == Temperature.Fahrenheit: + return value + if units == Temperature.Rankin: + result = value - 459.67 + elif units == Temperature.Celsius: + result = value * 9 / 5 + 32 + elif units == Temperature.Kelvin: + result = (value - 273.15) * 9 / 5 + 32 + else: + return super().to_raw(value, units) + return result + + def from_raw(self, value: float, units: Unit): + if units == Temperature.Fahrenheit: + return value + if units == Temperature.Rankin: + result = value + 459.67 + elif units == Temperature.Celsius: + result = (value - 32) * 5 / 9 + elif units == Temperature.Kelvin: + result = (value - 32) * 5 / 9 + 273.15 + else: + return super().from_raw(value, units) + return result + + Fahrenheit = Unit.FAHRENHEIT + Celsius = Unit.CELSIUS + Kelvin = Unit.KELVIN + Rankin = Unit.RANKIN + + +class Angular(AbstractUnit): + """Angular unit""" + + def to_raw(self, value: float, units: Unit): + if units == Angular.Radian: + return value + if units == Angular.Degree: + result = value / 180 * pi + elif units == Angular.MOA: + result = value / 180 * pi / 60 + elif units == Angular.Mil: + result = value / 3200 * pi + elif units == Angular.MRad: + result = value / 1000 + elif units == Angular.Thousandth: + result = value / 3000 * pi + elif units == Angular.InchesPer100Yd: + result = atan(value / 3600) + elif units == Angular.CmPer100M: + result = atan(value / 10000) + elif units == Angular.OClock: + result = value / 6 * pi + else: + return super().to_raw(value, units) + if result > 2*pi: + result = result % (2*pi) + return result + + def from_raw(self, value: float, units: Unit): + if units == Angular.Radian: + return value + if units == Angular.Degree: + result = value * 180 / pi + elif units == Angular.MOA: + result = value * 180 / pi * 60 + elif units == Angular.Mil: + result = value * 3200 / pi + elif units == Angular.MRad: + result = value * 1000 + elif units == Angular.Thousandth: + result = value * 3000 / pi + elif units == Angular.InchesPer100Yd: + result = tan(value) * 3600 + elif units == Angular.CmPer100M: + result = tan(value) * 10000 + elif units == Angular.OClock: + result = value * 6 / pi + else: + return super().from_raw(value, units) + return result + + Radian = Unit.RAD + Degree = Unit.DEGREE + MOA = Unit.MOA + Mil = Unit.MIL + MRad = Unit.MRAD + Thousandth = Unit.THOUSANDTH + InchesPer100Yd = Unit.INCHES_PER_100YD + CmPer100M = Unit.CM_PER_100M + OClock = Unit.O_CLOCK + + +class Velocity(AbstractUnit): + """Velocity unit""" + + def to_raw(self, value: float, units: Unit): + if units == Velocity.MPS: + return value + if units == Velocity.KMH: + return value / 3.6 + if units == Velocity.FPS: + return value / 3.2808399 + if units == Velocity.MPH: + return value / 2.23693629 + if units == Velocity.KT: + return value / 1.94384449 + return super().to_raw(value, units) + + def from_raw(self, value: float, units: Unit): + if units == Velocity.MPS: + return value + if units == Velocity.KMH: + return value * 3.6 + if units == Velocity.FPS: + return value * 3.2808399 + if units == Velocity.MPH: + return value * 2.23693629 + if units == Velocity.KT: + return value * 1.94384449 + return super().from_raw(value, units) + + MPS = Unit.MPS + KMH = Unit.KMH + FPS = Unit.FPS + MPH = Unit.MPH + KT = Unit.KT + + +class Energy(AbstractUnit): + """Energy unit""" + + def to_raw(self, value: float, units: Unit): + if units == Energy.FootPound: + return value + if units == Energy.Joule: + return value * 0.737562149277 + return super().to_raw(value, units) + + def from_raw(self, value: float, units: Unit): + if units == Energy.FootPound: + return value + if units == Energy.Joule: + return value / 0.737562149277 + return super().from_raw(value, units) + + FootPound = Unit.FOOT_POUND + Joule = Unit.JOULE + + +@dataclass +class TypedUnits: # pylint: disable=too-few-public-methods + """ + Abstract class to apply auto-conversion values to + specified units by type-hints in inherited dataclasses + """ + + def __setattr__(self, key, value): + """ + converts value to specified units by type-hints in inherited dataclass + """ + + _fields = self.__getattribute__('__dataclass_fields__') + # fields(self.__class__)[0].name + if key in _fields and not isinstance(value, AbstractUnit): + default_factory = _fields[key].default_factory + if isinstance(default_factory, typing.Callable): + if isinstance(value, Unit): + value = None + else: + value = default_factory()(value) + + super().__setattr__(key, value) + + +# def is_unit(obj: [AbstractUnit, float, int]): +# """ +# Check if obj is inherited by AbstractUnit +# :return: False - if float or int +# """ +# if isinstance(obj, AbstractUnit): +# return True +# if isinstance(obj, (float, int)): +# return False +# if obj is None: +# return None +# raise TypeError(f"Expected Unit, int, or float, found {obj.__class__.__name__}") diff --git a/py_ballisticcalc/wind.pyx b/py_ballisticcalc/wind.pyx deleted file mode 100644 index f8661c1..0000000 --- a/py_ballisticcalc/wind.pyx +++ /dev/null @@ -1,47 +0,0 @@ -from .bmath.unit import * - - -cdef class WindInfo: - cdef _until_distance - cdef _velocity - cdef _direction - - def __init__(self, until_distance: Distance = None, - velocity: Velocity = None, direction: Angular = None): - self._until_distance = until_distance - self._velocity = velocity - self._direction = direction - - def __str__(self): - return f'Until distance: {self._until_distance}, Velocity: {self._velocity}, Direction: {self._direction}' - - cdef string(self): - return f'Until distance: {self._until_distance}, Velocity: {self._velocity}, Direction: {self._direction}' - - cpdef until_distance(self): - return self._until_distance - - cpdef velocity(self): - return self._velocity - - cpdef direction(self): - return self._direction - -cpdef create_no_wind(): - w = WindInfo() - return [w] - -cpdef create_only_wind_info(wind_velocity: Velocity, direction: Angular): - cdef until_distance, w - until_distance = Distance(9999, DistanceKilometer) - w = [WindInfo(until_distance, wind_velocity, direction)] - return w - -# cpdef add_wind_info(until_distance: Distance, -# velocity: Velocity, direction: Angular): -# return WindInfo(until_distance, velocity, direction) -# -# cdef Wind - -def create_wind_info(*winds: 'WindInfo') -> list['WindInfo']: - return list(winds) diff --git a/py_ballisticcalc_exts/LICENSE b/py_ballisticcalc_exts/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/py_ballisticcalc_exts/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/py_ballisticcalc_exts/Manifest.in b/py_ballisticcalc_exts/Manifest.in new file mode 100644 index 0000000..925062c --- /dev/null +++ b/py_ballisticcalc_exts/Manifest.in @@ -0,0 +1,3 @@ +recursive-include py_ballisticcalc_exts *.pyx +recursive-include py_ballisticcalc_exts *.pyi +include py.typed diff --git a/py_ballisticcalc_exts/README.md b/py_ballisticcalc_exts/README.md new file mode 100644 index 0000000..a6290fd --- /dev/null +++ b/py_ballisticcalc_exts/README.md @@ -0,0 +1,185 @@ +# BallisticCalculator +LGPL library for small arms ballistic calculations (Python 3.9+) + +### Table of contents +* **[Installation](#installation)** + * [Latest stable](#latest-stable-release-from-pypi) + * [From sources](#installing-from-sources) + * [Clone and build](#clone-and-build) +* **[Usage](#usage)** + * [Units of measure](#unit-manipulation-syntax) + * [An example of calculations](#an-example-of-calculations) + * [Output example](#example-of-the-formatted-output) +* **[Older versions]()** + * [v1.0.x](https://github.com/o-murphy/py_ballisticcalc/tree/v1.0.12) +* **[Contributors](#contributors)** +* **[About project](#about-project)** + +### Installation +#### Latest stable release from pypi** +```shell +pip install py-ballisticcalc +``` +#### Installing from sources +**MSVC** or **GCC** required +* Download and install **MSVC** or **GCC** depending on target platform +* Use one of the references you need: +```shell +# no binary from PyPi +pip install py-ballisticcalc== --no-binary py-ballisticcalc + +# master brunch +pip install git+https://github.com/o-murphy/py_ballisticcalc + +# specific branch +pip install git+https://github.com/o-murphy/py_ballisticcalc.git@ +``` + +#### Clone and build +**MSVC** or **GCC** required +```shell +git clone https://github.com/o-murphy/py_ballisticcalc +cd py_ballisticcalc +python -m venv venv +. venv/bin/activate +pip install cython +python setup.py build_ext --inplace +``` + +## Usage + +The library supports all the popular units of measurement, and adds different built-in methods to define and manipulate it +#### Unit manipulation syntax: + +```python +from py_ballisticcalc.unit import * + +# ways to define value in units +# 1. old syntax +unit_in_meter = Distance(100, Distance.Meter) +# 2. short syntax by Unit type class +unit_in_meter = Distance.Meter(100) +# 3. by Unit enum class +unit_in_meter = Unit.METER(100) + +# >>> : 100.0 m (3937.0078740157483) + +# convert unit +# 1. by method +unit_in_yard = unit_in_meter.convert(Distance.Yard) +# 2. using shift syntax +unit_in_yards = unit_in_meter << Distance.Yard # '<<=' operator also supports +# >>> : 109.36132983377078 yd (3937.0078740157483) + +# get value in specified units +# 1. by method +value_in_km = unit_in_yards.get_in(Distance.Kilometer) +# 2. by shift syntax +value_in_km = unit_in_yards >> Distance.Kilometer # '>>=' operator also supports +# >>> 0.1 + +# getting unit raw value: +rvalue = Distance.Meter(10).raw_value +rvalue = float(Distance.Meter(10)) + +# units comparison: +# supports operators like < > <= >= == != +Distance.Meter(100) == Distance.Centimeter(100) # >>> False, compare two units by raw value +Distance.Meter(100) > 10 # >>> True, compare unit with float by raw value +``` + +#### An example of calculations + +```python +from py_ballisticcalc import Velocity, Temperature, Distance +from py_ballisticcalc import DragModel, TableG7 +from py_ballisticcalc import Ammo, Atmo, Wind +from py_ballisticcalc import Weapon, Shot, Calculator +from py_ballisticcalc import Settings as Set + + +# set global library settings +Set.Units.velocity = Velocity.FPS +Set.Units.temperature = Temperature.Celsius +# Set.Units.distance = Distance.Meter +Set.Units.sight_height = Distance.Centimeter + +Set.set_max_calc_step_size(Distance.Foot(1)) +Set.USE_POWDER_SENSITIVITY = True # enable muzzle velocity correction my powder temperature + +# define params with default units +weight, diameter = 168, 0.308 +# or define with specified units +length = Distance.Inch(1.282) # length = Distance(1.282, Distance.Inch) + +weapon = Weapon(9, 100, 2) +dm = DragModel(0.223, TableG7, weight, diameter) + +ammo = Ammo(dm, length, 2750, 15) +ammo.calc_powder_sens(2723, 0) + +zero_atmo = Atmo.icao(100) + +# defining calculator instance +calc = Calculator(weapon, ammo, zero_atmo) + +current_atmo = Atmo(110, 1000, 15, 72) +current_winds = [Wind(2, 90)] +shot = Shot(1500, atmo=current_atmo, winds=current_winds) + +shot_result = calc.fire(shot, trajectory_step=Distance.Yard(100)) + +for p in shot_result: + print(p.formatted()) +``` +#### Example of the formatted output: +```shell +python -m py_ballisticcalc.example +``` + +``` +['0.00 s', '0.000 m', '2750.0 ft/s', '2.46 mach', '-9.000 cm', '0.00 mil', '0.000 cm', '0.00 mil', '3825 J'] +['0.12 s', '100.000 m', '2528.6 ft/s', '2.26 mach', '0.005 cm', '0.00 mil', '-3.556 cm', '-0.36 mil', '3233 J'] +['0.26 s', '200.050 m', '2317.2 ft/s', '2.08 mach', '-7.558 cm', '-0.38 mil', '-13.602 cm', '-0.69 mil', '2715 J'] +['0.41 s', '300.050 m', '2116.6 ft/s', '1.90 mach', '-34.843 cm', '-1.18 mil', '-30.956 cm', '-1.05 mil', '2266 J'] +['0.57 s', '400.000 m', '1926.5 ft/s', '1.73 mach', '-85.739 cm', '-2.18 mil', '-57.098 cm', '-1.45 mil', '1877 J'] +['0.75 s', '500.000 m', '1745.0 ft/s', '1.56 mach', '-165.209 cm', '-3.37 mil', '-94.112 cm', '-1.92 mil', '1540 J'] +['0.95 s', '600.000 m', '1571.4 ft/s', '1.41 mach', '-279.503 cm', '-4.74 mil', '-144.759 cm', '-2.46 mil', '1249 J'] +``` + +## Contributors +### This project exists thanks to all the people who contribute. +#### Special thanks to: +- **[David Bookstaber](https://github.com/dbookstaber)** - Ballistics Expert, Financial Engineer \ +*For the help in understanding and improvement of some calculation methods* +- **[Nikolay Gekht](https://github.com/nikolaygekht)** \ +*For the sources code on C# and GO-lang from which this project firstly was forked from* + +## About project + +The library provides trajectory calculation for projectiles including for various +applications, including air rifles, bows, firearms, artillery and so on. + +3DF model that is used in this calculator is rooted in old C sources of version 2 of the public version of JBM +calculator, ported to C#, optimized, fixed and extended with elements described in +Litz's "Applied Ballistics" book and from the friendly project of Alexandre Trofimov +and then ported to Go. + +Now it's also ported to python3 and expanded to support calculation trajectory by +multiple ballistics coefficients and using custom drag data (such as Doppler radar data ©Lapua, etc.) + +The online version of Go documentation is located here: https://godoc.org/github.com/gehtsoft-usa/go_ballisticcalc + +C# version of the package is located here: https://github.com/gehtsoft-usa/BallisticCalculator1 + +The online version of C# API documentation is located here: https://gehtsoft-usa.github.io/BallisticCalculator/web-content.html + +Go documentation can be obtained using godoc tool. + +The current status of the project is ALPHA version. + +#### RISK NOTICE + +The library performs very limited simulation of a complex physical process and so it performs a lot of approximations. Therefore, the calculation results MUST NOT be considered as completely and reliably reflecting actual behavior or characteristics of projectiles. While these results may be used for educational purpose, they must NOT be considered as reliable for the areas where incorrect calculation may cause making a wrong decision, financial harm, or can put a human life at risk. + +THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. diff --git a/py_ballisticcalc_exts/__init__.py b/py_ballisticcalc_exts/__init__.py new file mode 100644 index 0000000..6b4cf8a --- /dev/null +++ b/py_ballisticcalc_exts/__init__.py @@ -0,0 +1 @@ +from .py_ballisticcalc_exts import * diff --git a/py_ballisticcalc_exts/py.typed b/py_ballisticcalc_exts/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/py_ballisticcalc_exts/py_ballisticcalc_exts/__init__.py b/py_ballisticcalc_exts/py_ballisticcalc_exts/__init__.py new file mode 100644 index 0000000..fc39078 --- /dev/null +++ b/py_ballisticcalc_exts/py_ballisticcalc_exts/__init__.py @@ -0,0 +1,8 @@ +__author__ = "o-murphy" +__copyright__ = ("",) + +__credits__ = ["o-murphy"] +__version__ = "1.1.0" + +from .drag_model import * +from .trajectory_calc import * diff --git a/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyi b/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyi new file mode 100644 index 0000000..b07762f --- /dev/null +++ b/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyi @@ -0,0 +1,44 @@ +from .settings import Settings as Set +from .unit import * + + +__all__ = ('DragDataPoint', 'DragModel', 'make_data_points') + + +class DragDataPoint: + + def __init__(self, coeff: float, velocity: float): + self.coeff = coeff + self.velocity = velocity + + def __iter__(self): + yield self.coeff + yield self.velocity + + +class DragModel: + table: list + weight: Weight + diameter: Distance + value: float + + def __init__(self, value: float, + drag_table: list, + weight: [float, Weight], + diameter: [float, Distance]): + pass + + def drag(self, mach: float) -> float: + pass + + def cdm(self) -> list[dict]: + pass + + @staticmethod + def from_mbc(mbc: 'MultiBC') -> DragModel: + pass + + + +def make_data_points(drag_table: list) -> list: + ... diff --git a/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyx b/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyx new file mode 100644 index 0000000..a6e46cb --- /dev/null +++ b/py_ballisticcalc_exts/py_ballisticcalc_exts/drag_model.pyx @@ -0,0 +1,92 @@ +import typing + +from libc.math cimport pow + +from py_ballisticcalc.settings import Settings as Set +from py_ballisticcalc.unit import Weight, Distance +from py_ballisticcalc.drag_tables import DragTablesSet + +__all__ = ('DragModel', 'make_data_points') + +cdef class DragDataPoint: + cdef readonly double CD # BC or CD + cdef readonly Mach # muzzle velocity or Mach + + def __cinit__(self, cd: float, mach: float): + self.CD = cd + self.Mach = mach + + def __iter__(self): + yield self.CD + yield self.Mach + + def __repr__(self): + return f"DragDataPoint(CD={self.CD}, Mach={self.Mach})" + +cdef struct CurvePoint: + double a, b, c + +cdef struct DragTableRow: + double CD + double Mach + +cdef class DragModel: + cdef: + readonly object weight, diameter + readonly list drag_table + readonly double value, form_factor + double sectional_density + + def __init__(self, double value, + drag_table: typing.Iterable, + weight: [float, Weight]=0, + diameter: [float, Distance]=0): + self.__post__init__(value, drag_table, weight, diameter) + + cdef __post__init__(DragModel self, double value, object drag_table, double weight, double diameter): + cdef: + double table_len = len(drag_table) + str error = '' + + if table_len <= 0: + error = 'Custom drag table must be longer than 0' + elif value <= 0: + error = 'Drag coefficient must be greater than zero' + + if error: + raise ValueError(error) + + if drag_table in DragTablesSet: + self.value = value + elif table_len > 0: + self.value = 1 # or 0.999 + else: + raise ValueError('Wrong drag data') + + self.weight = Set.Units.weight(weight) + self.diameter = Set.Units.diameter(diameter) + if weight != 0 and diameter != 0: + self.sectional_density = self._get_sectional_density() + self.form_factor = self._get_form_factor(self.value) + self.drag_table = drag_table + + cdef double _get_form_factor(self, double bc): + return self.sectional_density / bc + + cdef double _get_sectional_density(self): + cdef double w, d + w = self.weight >> Weight.Grain + d = self.diameter >> Distance.Inch + return sectional_density(w, d) + + @staticmethod + def from_mbc(mbc: 'MultiBC'): + return DragModel(1, mbc.cdm, mbc.weight, mbc.diameter) + + +cpdef list make_data_points(drag_table: typing.Iterable): + return [DragDataPoint(point['CD'], point['Mach']) for point in drag_table] + + +cdef double sectional_density(double weight, double diameter): + return weight / pow(diameter, 2) / 7000 diff --git a/py_ballisticcalc_exts/py_ballisticcalc_exts/trajectory_calc.pyx b/py_ballisticcalc_exts/py_ballisticcalc_exts/trajectory_calc.pyx new file mode 100644 index 0000000..5cda226 --- /dev/null +++ b/py_ballisticcalc_exts/py_ballisticcalc_exts/trajectory_calc.pyx @@ -0,0 +1,477 @@ +from libc.math cimport sqrt, fabs, pow, sin, cos, tan, atan, log10, floor +cimport cython + +from py_ballisticcalc.conditions import Atmo, Shot +from py_ballisticcalc.munition import Ammo, Weapon +from py_ballisticcalc.settings import Settings +from py_ballisticcalc.trajectory_data import TrajectoryData +from py_ballisticcalc.unit import * + +__all__ = ('TrajectoryCalc',) + +cdef double cZeroFindingAccuracy = 0.000005 +cdef double cMinimumVelocity = 50.0 +cdef double cMaximumDrop = -15000 +cdef int cMaxIterations = 20 +cdef double cGravityConstant = -32.17405 + +cdef struct CurvePoint: + double a, b, c + +cdef enum CTrajFlag: + NONE = 0 + ZERO_UP = 1 + ZERO_DOWN = 2 + MACH = 4 + RANGE = 8 + DANGER = 16 + ZERO = ZERO_UP | ZERO_DOWN + ALL = RANGE | ZERO_UP | ZERO_DOWN | MACH | DANGER + +cdef class Vector: + cdef double x + cdef double y + cdef double z + + def __cinit__(Vector self, double x, double y, double z): + self.x = x + self.y = y + self.z = z + + cdef double magnitude(Vector self): + return sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + + cdef Vector mul_by_const(Vector self, double a): + return Vector(self.x * a, self.y * a, self.z * a) + + cdef double mul_by_vector(Vector self, Vector b): + return self.x * b.x + self.y * b.y + self.z * b.z + + cdef Vector add(Vector self, Vector b): + return Vector(self.x + b.x, self.y + b.y, self.z + b.z) + + cdef Vector subtract(Vector self, Vector b): + return Vector(self.x - b.x, self.y - b.y, self.z - b.z) + + cdef Vector negate(Vector self): + return Vector(-self.x, -self.y, -self.z) + + cdef Vector normalize(Vector self): + cdef double m = self.magnitude() + if fabs(m) < 1e-10: + return Vector(self.x, self.y, self.z) + return self.mul_by_const(1.0 / m) + + def __add__(Vector self, Vector other): + return self.add(other) + + def __radd__(Vector self, Vector other): + return self.add(other) + + def __iadd__(Vector self, Vector other): + return self.add(other) + + def __sub__(Vector self, Vector other): + return self.subtract(other) + + def __rsub__(Vector self, Vector other): + return self.subtract(other) + + def __isub__(Vector self, Vector other): + return self.subtract(other) + + def __mul__(Vector self, object other): + if isinstance(other, (int, float)): + return self.mul_by_const(other) + if isinstance(other, Vector): + return self.mul_by_vector(other) + raise TypeError(other) + + def __rmul__(Vector self, object other): + return self.__mul__(other) + + def __imul__(Vector self, object other): + return self.__mul__(other) + + def __neg__(Vector self): + return self.negate() + +cdef class TrajectoryCalc: + cdef: + object ammo + double step + list _curve + list _table_data + double _bc + + def __init__(self, ammo: Ammo): + self.ammo = ammo + self._bc = self.ammo.dm.value + self._table_data = ammo.dm.drag_table + self._curve = calculate_curve(self._table_data) + + cdef double get_calc_step(self, double step): + cdef: + int step_order, maximum_order + double maximum_step = Settings._MAX_CALC_STEP_SIZE + step /= 2 + if step > maximum_step: + step_order = int(floor(log10(step))) + maximum_order = int(floor(log10(maximum_step))) + step /= pow(10, step_order - maximum_order + 1) + return step + + def zero_angle(self, shot_info: Shot, distance: Distance): + return self._zero_angle(shot_info, distance) + + def trajectory(self, shot_info: Shot, max_range: Distance, dist_step: Distance, + extra_data: bool = False): + cdef: + object step = Settings.Units.distance(dist_step) + object atmo = shot_info.atmo + list winds = shot_info.winds + CTrajFlag filter_flags = CTrajFlag.RANGE + + if extra_data: + #print('ext', extra_data) + step = Distance.Foot(0.2) + filter_flags = CTrajFlag.ALL + return self._trajectory(self.ammo, atmo, shot_info, winds, max_range, step, filter_flags) + + cdef _zero_angle(TrajectoryCalc self, object shot_info, object distance): + cdef: + double calc_step = self.get_calc_step(distance.units(10) >> Distance.Foot) + double zero_distance = cos(shot_info.look_angle >> Angular.Radian) * (distance >> Distance.Foot) + double height_at_zero = sin(shot_info.look_angle >> Angular.Radian) * (distance >> Distance.Foot) + double maximum_range = zero_distance + calc_step + double sight_height = shot_info.weapon.sight_height >> Distance.Foot + double mach = shot_info.atmo.mach >> Velocity.FPS + double density_factor = shot_info.atmo.density_factor() + double muzzle_velocity = shot_info.ammo.mv >> Velocity.FPS + double cant_cosine = cos(shot_info.cant_angle >> Angular.Radian) + double cant_sine = sin(shot_info.cant_angle >> Angular.Radian) + + double barrel_azimuth = 0.0 + double barrel_elevation = atan(height_at_zero / zero_distance) + int iterations_count = 0 + double zero_finding_error = cZeroFindingAccuracy * 2 + Vector gravity_vector = Vector(.0, cGravityConstant, .0) + + double velocity, time, delta_time, drag + Vector range_vector, velocity_vector, delta_range_vector + + # x - distance towards target, y - drop and z - windage + while zero_finding_error > cZeroFindingAccuracy and iterations_count < cMaxIterations: + velocity = muzzle_velocity + time = 0.0 + range_vector = Vector(.0, -cant_cosine*sight_height, -cant_sine*sight_height) + velocity_vector = Vector( + cos(barrel_elevation) * cos(barrel_azimuth), + sin(barrel_elevation), + cos(barrel_elevation) * sin(barrel_azimuth) + ) * velocity + + while range_vector.x <= maximum_range: + if velocity < cMinimumVelocity or range_vector.y < cMaximumDrop: + break + + delta_time = calc_step / velocity_vector.x + + drag = density_factor * velocity * self.drag_by_mach(velocity / mach) + + velocity_vector -= (velocity_vector * drag - gravity_vector) * delta_time + delta_range_vector = Vector(calc_step, velocity_vector.y * delta_time, + velocity_vector.z * delta_time) + range_vector += delta_range_vector + velocity = velocity_vector.magnitude() + time += delta_range_vector.magnitude() / velocity + + if fabs(range_vector.x - zero_distance) < 0.5 * calc_step: + zero_finding_error = fabs(range_vector.y - height_at_zero) + if zero_finding_error > cZeroFindingAccuracy: + barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x + break + + iterations_count += 1 + + return Angular.Radian(barrel_elevation) + + cdef _trajectory(TrajectoryCalc self, object ammo, object atmo, object shot_info, + list[object] winds, object max_range, object dist_step, CTrajFlag filter_flags): + cdef: + double density_factor, mach + double time, velocity, windage, delta_time, drag + + double look_angle = shot_info.look_angle >> Angular.Radian + double twist = shot_info.weapon.twist >> Distance.Inch + double length = ammo.length >> Distance.Inch + double diameter = ammo.dm.diameter >> Distance.Inch + double weight = ammo.dm.weight >> Weight.Grain + + # double step = shot_info.step >> Distance.Foot + double step = dist_step >> Distance.Foot + double calc_step = self.get_calc_step(step) + + double maximum_range = (max_range >> Distance.Foot) + 1 + + int ranges_length = int(maximum_range / step) + 1 + int len_winds = len(winds) + int current_item, current_wind, twist_coefficient + double next_range_distance = .0 + double previous_mach = .0 + list ranges = [] + + double stability_coefficient = 1.0 + double next_wind_range = 1e7 + double alt0 = atmo.altitude >> Distance.Foot + + double barrel_elevation = shot_info.barrel_elevation >> Angular.Radian + double barrel_azimuth = shot_info.barrel_azimuth >> Angular.Radian + double sight_height = shot_info.weapon.sight_height >> Distance.Foot + double cant_cosine = cos(shot_info.cant_angle >> Angular.Radian) + double cant_sine = sin(shot_info.cant_angle >> Angular.Radian) + Vector range_vector = Vector(.0, -cant_cosine*sight_height, -cant_sine*sight_height) + Vector gravity_vector = Vector(.0, cGravityConstant, .0) + + Vector velocity_vector, velocity_adjusted, delta_range_vector, wind_vector + + object _flag, seen_zero # CTrajFlag + + if len_winds < 1: + wind_vector = Vector(.0, .0, .0) + else: + if len_winds > 1: + next_wind_range = winds[0].until_distance() >> Distance.Foot + wind_vector = wind_to_vector(winds[0]) + + if Settings.USE_POWDER_SENSITIVITY: + velocity = ammo.get_velocity_for_temp(atmo.temperature) >> Velocity.FPS + else: + velocity = ammo.mv >> Velocity.FPS + + # x - distance towards target, y - drop and z - windage + velocity_vector = Vector(cos(barrel_elevation) * cos(barrel_azimuth), sin(barrel_elevation), + cos(barrel_elevation) * sin(barrel_azimuth)) * velocity + + if twist != 0 and length and diameter: + stability_coefficient = calculate_stability_coefficient(shot_info.weapon.twist, ammo, atmo) + twist_coefficient = 1 if twist > 0 else -1 + + # With non-zero look_angle, rounding can suggest multiple adjacent zero-crossings + seen_zero = CTrajFlag.NONE # Record when we see each zero crossing so we only register one + if range_vector.y >= 0: + seen_zero |= CTrajFlag.ZERO_UP # We're starting above zero; we can only go down + elif range_vector.y < 0 and barrel_elevation < look_angle: + seen_zero |= CTrajFlag.ZERO_DOWN # We're below and pointing down from look angle; no zeroes! + + while range_vector.x <= maximum_range + calc_step: + _flag = CTrajFlag.NONE + + if velocity < cMinimumVelocity or range_vector.y < cMaximumDrop: + break + + density_factor, mach = atmo.get_density_factor_and_mach_for_altitude(alt0 + range_vector.y) + + if range_vector.x >= next_wind_range: + current_wind += 1 + wind_vector = wind_to_vector(winds[current_wind]) + + if current_wind == len_winds - 1: + next_wind_range = 1e7 + else: + next_wind_range = winds[current_wind].until_distance() >> Distance.Foot + + # Zero-crossing checks + if range_vector.x > 0: + # Zero reference line is the sight line defined by look_angle + reference_height = range_vector.x * tan(look_angle) + # If we haven't seen ZERO_UP, we look for that first + if not seen_zero & CTrajFlag.ZERO_UP: + if range_vector.y >= reference_height: + _flag |= CTrajFlag.ZERO_UP + seen_zero |= CTrajFlag.ZERO_UP + # We've crossed above sight line; now look for crossing back through it + elif not seen_zero & CTrajFlag.ZERO_DOWN: + if range_vector.y < reference_height: + _flag |= CTrajFlag.ZERO_DOWN + seen_zero |= CTrajFlag.ZERO_DOWN + + # Mach crossing check + if (velocity / mach <= 1) and (previous_mach > 1): + _flag |= CTrajFlag.MACH + + # Next range check + if range_vector.x >= next_range_distance: + _flag |= CTrajFlag.RANGE + next_range_distance += step + current_item += 1 + + if _flag & filter_flags: + + windage = range_vector.z + + if twist != 0: + windage += (1.25 * (stability_coefficient + 1.2) + * pow(time, 1.83) * twist_coefficient) / 12 + + ranges.append(create_trajectory_row( + time, range_vector, velocity_vector, + velocity, mach, windage, weight, _flag + )) + + if current_item == ranges_length: + break + + previous_mach = velocity / mach + + velocity_adjusted = velocity_vector - wind_vector + + delta_time = calc_step / velocity_vector.x + velocity = velocity_adjusted.magnitude() + + drag = density_factor * velocity * self.drag_by_mach(velocity / mach) + + velocity_vector -= (velocity_adjusted * drag - gravity_vector) * delta_time + delta_range_vector = Vector(calc_step, + velocity_vector.y * delta_time, + velocity_vector.z * delta_time) + range_vector += delta_range_vector + velocity = velocity_vector.magnitude() + time += delta_range_vector.magnitude() / velocity + + return ranges + + cdef double drag_by_mach(self, double mach): + cdef double cd = calculate_by_curve(self._table_data, self._curve, mach) + return cd * 2.08551e-04 / self._bc + + @property + def cdm(self): + return self._cdm() + + cdef _cdm(self): + """ + Returns custom drag function based on input data + """ + cdef: + # double ff = self.ammo.dm.form_factor + list drag_table = self.ammo.dm.drag_table + list cdm = [] + double bc = self.ammo.dm.value + + for point in drag_table: + st_mach = point['Mach'] + st_cd = calculate_by_curve(drag_table, self._curve, st_mach) + # cd = st_cd * ff + cd = st_cd * bc + cdm.append({'CD': cd, 'Mach': st_mach}) + + return cdm + +cdef double calculate_stability_coefficient(object twist_rate, object ammo, object atmo): + cdef: + double weight = ammo.dm.weight >> Weight.Grain + double diameter = ammo.dm.diameter >> Distance.Inch + double twist = fabs(twist_rate >> Distance.Inch) / diameter + double length = (ammo.length >> Distance.Inch) / diameter + double ft = atmo.temperature >> Temperature.Fahrenheit + double mv = ammo.mv >> Velocity.FPS + double pt = atmo.pressure >> Pressure.InHg + double sd = 30 * weight / (pow(twist, 2) * pow(diameter, 3) * length * (1 + pow(length, 2))) + double fv = pow(mv / 2800, 1.0 / 3.0) + double ftp = ((ft + 460) / (59 + 460)) * (29.92 / pt) + return sd * fv * ftp + +cdef Vector wind_to_vector(object wind): + cdef: + double range_component = (wind.velocity >> Velocity.FPS) * cos(wind.direction_from >> Angular.Radian) + double cross_component = (wind.velocity >> Velocity.FPS) * sin(wind.direction_from >> Angular.Radian) + return Vector(range_component, 0., cross_component) + +cdef create_trajectory_row(double time, Vector range_vector, Vector velocity_vector, + double velocity, double mach, double windage, double weight, object flag): + cdef: + double drop_adjustment = get_correction(range_vector.x, range_vector.y) + double windage_adjustment = get_correction(range_vector.x, windage) + double trajectory_angle = atan(velocity_vector.y / velocity_vector.x) + + return TrajectoryData( + time=time, + distance=Distance.Foot(range_vector.x), + drop=Distance.Foot(range_vector.y), + drop_adj=Angular.Radian(drop_adjustment), + windage=Distance.Foot(windage), + windage_adj=Angular.Radian(windage_adjustment), + velocity=Velocity.FPS(velocity), + mach=velocity / mach, + energy=Energy.FootPound(calculate_energy(weight, velocity)), + angle=Angular.Radian(trajectory_angle), + ogw=Weight.Pound(calculate_ogv(weight, velocity)), + flag=flag + ) + +@cython.cdivision(True) +cdef double get_correction(double distance, double offset): + if distance != 0: + return atan(offset / distance) + return 0 # better None + +cdef double calculate_energy(double bullet_weight, double velocity): + return bullet_weight * pow(velocity, 2) / 450400 + +cdef double calculate_ogv(double bullet_weight, double velocity): + return pow(bullet_weight, 2) * pow(velocity, 3) * 1.5e-12 + +cdef list calculate_curve(list data_points): + cdef double rate, x1, x2, x3, y1, y2, y3, a, b, c + cdef list curve = [] + cdef CurvePoint curve_point + cdef int num_points, len_data_points, len_data_range + + rate = (data_points[1]['CD'] - data_points[0]['CD']) / (data_points[1]['Mach'] - data_points[0]['Mach']) + curve = [CurvePoint(0, rate, data_points[0]['CD'] - data_points[0]['Mach'] * rate)] + len_data_points = int(len(data_points)) + len_data_range = len_data_points - 1 + + for i in range(1, len_data_range): + x1 = data_points[i - 1]['Mach'] + x2 = data_points[i]['Mach'] + x3 = data_points[i + 1]['Mach'] + y1 = data_points[i - 1]['CD'] + y2 = data_points[i]['CD'] + y3 = data_points[i + 1]['CD'] + a = ((y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)) / ( + (x3 * x3 - x1 * x1) * (x2 - x1) - (x2 * x2 - x1 * x1) * (x3 - x1)) + b = (y2 - y1 - a * (x2 * x2 - x1 * x1)) / (x2 - x1) + c = y1 - (a * x1 * x1 + b * x1) + curve_point = CurvePoint(a, b, c) + curve.append(curve_point) + + num_points = len_data_points + rate = (data_points[num_points - 1]['CD'] - data_points[num_points - 2]['CD']) / \ + (data_points[num_points - 1]['Mach'] - data_points[num_points - 2]['Mach']) + curve_point = CurvePoint(0, rate, data_points[num_points - 1]['CD'] - data_points[num_points - 2]['Mach'] * rate) + curve.append(curve_point) + return curve + +cdef double calculate_by_curve(list data, list curve, double mach): + cdef int num_points, mlo, mhi, mid + cdef CurvePoint curve_m + + num_points = int(len(curve)) + mlo = 0 + mhi = num_points - 2 + + while mhi - mlo > 1: + mid = int(floor(mhi + mlo) / 2.0) + if data[mid]['Mach'] < mach: + mlo = mid + else: + mhi = mid + + if data[mhi]['Mach'] - mach > mach - data[mlo]['Mach']: + m = mlo + else: + m = mhi + curve_m = curve[m] + return curve_m.c + mach * (curve_m.b + curve_m.a * mach) diff --git a/py_ballisticcalc_exts/pyproject.toml b/py_ballisticcalc_exts/pyproject.toml new file mode 100644 index 0000000..1d60f6d --- /dev/null +++ b/py_ballisticcalc_exts/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["setuptools", "wheel", 'cython'] +build-backend = "setuptools.build_meta" + + +[project] +name = "py_ballisticcalc.exts" + +authors = [ + { name="o-murphy", email="thehelixpg@gmail.com" }, +] +description = "LGPL library for small arms ballistic calculations (Python 3)" +readme = "README.md" +requires-python = ">=3.9" +keywords = ["py_ballisticcalc", "ballistics", "Cython", "ballistic calculator", "python", "python3"] +license = {file = "LICENSE"} +classifiers = [ + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Programming Language :: Python :: Implementation :: CPython", +] +dependencies = [] +dynamic = ["version"] + +[project.urls] +"Homepage" = "https://github.com/o-murphy/py_ballisticcalc" +"Bug Reports" = "https://github.com/o-murphy/py_ballisticcalc/issues" +#"Funding" = "https://donate.pypi.org" +#"Say Thanks!" = "" +"Source" = "https://github.com/o-murphy/py_ballisticcalc" + +[tool.setuptools] +py-modules = ["py_ballisticcalc_exts"] + + +[tool.setuptools.packages.find] +where = ["."] +include = ["py_ballisticcalc_exts*"] # alternatively: `exclude = ["additional*"]` + + +[tool.setuptools.dynamic] +version = {attr = "py_ballisticcalc_exts.__version__"} diff --git a/py_ballisticcalc_exts/setup.py b/py_ballisticcalc_exts/setup.py new file mode 100644 index 0000000..4ccd92e --- /dev/null +++ b/py_ballisticcalc_exts/setup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +"""setup.py script for py_ballisticcalc library""" + +import os +from pathlib import Path + +from setuptools import setup, Extension + +try: + from Cython.Build import cythonize + + # USE_CYTHON = True +except ImportError: + # USE_CYTHON = False + cythonize = False + + +def iter_extensions(path) -> list: + """ + iterate extensions in project directory + :rtype: list + :return: list of extensions paths + """ + founded_extensions = [] + extensions_dir = Path(path).parent + for ext_path in Path.iterdir(extensions_dir): + if ext_path.suffix == '.pyx': + ext_name = f"{'.'.join(extensions_dir.parts)}.{ext_path.stem}" + ext = Extension(ext_name, [ext_path.as_posix()]) + founded_extensions.append(ext) + return founded_extensions + + +def no_cythonize(exts, **_ignore): + """grep extensions sources without cythonization""" + for extension in exts: + sources = [] + for src_file in extension.sources: + path, ext = os.path.splitext(src_file) + + if ext in (".pyx", ".py"): + if extension.language == "c++": + ext = ".cpp" + else: + ext = ".c" + src_file = path + ext + sources.append(src_file) + extension.sources[:] = sources + return exts + + +extensions_paths = [ + 'py_ballisticcalc_exts/*.pyx', +] + +extensions = [] +for path_ in extensions_paths: + extensions += iter_extensions(path_) + +if cythonize: + compiler_directives = {"language_level": 3, "embedsignature": True} + extensions = cythonize(extensions, compiler_directives=compiler_directives) +else: + extensions = no_cythonize(extensions) + +setup( + ext_modules=extensions, +) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..49e84b5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + + +[project] +name = "py_ballisticcalc" + +authors = [ + { name="o-murphy", email="thehelixpg@gmail.com" }, +] +description = "LGPL library for small arms ballistic calculations (Python 3)" +readme = "README.md" +requires-python = ">=3.9" +keywords = ["py_ballisticcalc", "ballistics", "Cython", "ballistic calculator", "python", "python3"] +license = {file = "LICENSE"} +classifiers = [ + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Programming Language :: Python :: Implementation :: CPython", +] +dependencies = [] +dynamic = ["version"] + +[project.urls] +"Homepage" = "https://github.com/o-murphy/py_ballisticcalc" +"Bug Reports" = "https://github.com/o-murphy/py_ballisticcalc/issues" +#"Funding" = "https://donate.pypi.org" +#"Say Thanks!" = "" +"Source" = "https://github.com/o-murphy/py_ballisticcalc" + + +[tool.setuptools] +py-modules = ["py_ballisticcalc"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["py_ballisticcalc*"] # alternatively: `exclude = ["additional*"]` +exclude = ["py_ballisticcalc_exts*"] + + +[tool.setuptools.dynamic] +version = {attr = "py_ballisticcalc.__version__"} + + +[project.optional-dependencies] +exts = ['py_ballisticcalc.exts'] +lint = ['pylint', 'flake8'] +graph = ['matplotlib', 'pandas'] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 12611db..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -twine>=4.0.1 -wheel>=0.37.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 83d124e..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -py-ballisticcalc-stubs>=1.0.10 diff --git a/setup.py b/setup.py index 2f79506..9b80486 100644 --- a/setup.py +++ b/setup.py @@ -1,134 +1,7 @@ #!/usr/bin/env python -import io -import os -import re -from pathlib import Path -from setuptools import setup, find_packages, Extension +"""setup.py script for py_ballisticcalc library""" -try: - from Cython.Build import cythonize +from setuptools import setup - USE_CYTHON = True -except ImportError: - USE_CYTHON = False - - -def read(*names, **kwargs): - try: - with io.open( - os.path.join(os.path.dirname(__file__), *names), - encoding=kwargs.get("encoding", "utf8") - ) as fp: - return fp.read() - except IOError: - return '' - - -def find_version(*file_paths): - version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -def iter_extensions(path): - founded_extensions = [] - extensions_dir = Path(path).parent - for ext_path in Path.iterdir(extensions_dir): - if ext_path.suffix == '.pyx': - ext_name = f"{'.'.join(extensions_dir.parts)}.{ext_path.stem}" - ext = Extension(ext_name, [ext_path.as_posix()]) - founded_extensions.append(ext) - return founded_extensions - - -def no_cythonize(extensions, **_ignore): - for extension in extensions: - sources = [] - for sfile in extension.sources: - path, ext = os.path.splitext(sfile) - - if ext in (".pyx", ".py"): - if extension.language == "c++": - ext = ".cpp" - else: - ext = ".c" - sfile = path + ext - sources.append(sfile) - extension.sources[:] = sources - return extensions - - -# extensions_paths = [ -# Extension('*', ['py_ballisticcalc/*.pyx']), -# Extension('*', ['py_ballisticcalc/bmath/unit/*.pyx']), -# Extension('*', ['py_ballisticcalc/bmath/vector/*.pyx']), -# ] - -extensions_paths = [ - 'py_ballisticcalc/*.pyx', - 'py_ballisticcalc/bmath/unit/*.pyx', - 'py_ballisticcalc/bmath/vector/*.pyx', -] - -extensions = [] -for path in extensions_paths: - extensions += iter_extensions(path) - -# CYTHONIZE = bool(int(os.getenv("CYTHONIZE", 0))) and use_cython is not None - -# if CYTHONIZE: -if USE_CYTHON: - compiler_directives = {"language_level": 3, "embedsignature": True} - extensions = cythonize(extensions, compiler_directives=compiler_directives) -else: - extensions = no_cythonize(extensions) - -with open("requirements.txt") as fp: - install_requires = fp.read().strip().split("\n") - print(install_requires) - -with open("requirements-dev.txt") as fp: - dev_requires = fp.read().strip().split("\n") - -setup( - - name='py_ballisticcalc', - ext_modules=extensions, - install_requires=install_requires, - setup_requires=[ - 'setuptools>=18.0', # automatically handles Cython extensions - 'cython>=3.0.0a10', - ], - - extras_require={ - "dev": dev_requires, - "docs": ["sphinx", "sphinx-rtd-theme"] - }, - - version=find_version('py_ballisticcalc', '__init__.py'), - url='https://github.com/o-murphy/py_ballisticcalc', - download_url='https://pypi.python.org/pypi/py_ballisticcalc/', - project_urls={ - "Homepage": 'https://github.com/o-murphy/py_ballisticcalc', - "Code": 'https://github.com/o-murphy/py_ballisticcalc', - "Documentation": 'https://github.com/o-murphy/py_ballisticcalc', - "Bug Tracker": 'https://github.com/o-murphy/py_ballisticcalc/issues' - }, - license='LGPL-3.0', - author="o-murphy", - author_email='thehelixpg@gmail.com', - description='LGPL library for small arms ballistic calculations (Python 3)', - long_description=open('README.md').read(), - long_description_content_type="text/markdown", - zip_safe=True, - py_modules=find_packages() + [ - 'py_ballisticcalc.drag_tables', - 'py_ballisticcalc.bin_test', - 'py_ballisticcalc.bmath.unit.unit_test', - 'py_ballisticcalc.bmath.vector.vector_test', - ], -) +setup() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/plot.py b/tests/plot.py new file mode 100644 index 0000000..fe64fd0 --- /dev/null +++ b/tests/plot.py @@ -0,0 +1,25 @@ +import pyximport; pyximport.install(language_level=3) +from py_ballisticcalc import * +import matplotlib +from matplotlib import pyplot as plt + +matplotlib.use('TkAgg') + +Settings.Units.velocity = Velocity.MPS + +dm = DragModel(0.22, TableG7, 168, 0.308) +ammo = Ammo(dm, 1.22, Velocity(2600, Velocity.FPS)) +weapon = Weapon(4, 100, 11.24, Angular.Mil(0)) + +calc = Calculator(weapon, ammo) +calc.calculate_elevation() + +shot = Shot(1200, zero_angle=calc.elevation, relative_angle=Angular.Mil(0)) +shot_result = calc.fire(shot, 0, extra_data=True) +danger_space = shot_result.danger_space( + Distance.Yard(1000), Distance.Meter(1.5), Angular.Mil(0) +) +ax = shot_result.plot() +danger_space.overlay(ax) +# ax.legend() +plt.show() diff --git a/tests/test_binary_import.py b/tests/test_binary_import.py new file mode 100644 index 0000000..d9a32e9 --- /dev/null +++ b/tests/test_binary_import.py @@ -0,0 +1,14 @@ +import unittest +#import pyximport; pyximport.install(language_level=3) + +class TestImports(unittest.TestCase): + + def test_bin_import(self): + try: + import py_ballisticcalc_exts + except ImportError as err: + print(err) + py_ballisticcalc_exts = None + if py_ballisticcalc_exts: + from py_ballisticcalc.backend import DragModel + print(DragModel) diff --git a/tests/test_computer.py b/tests/test_computer.py new file mode 100644 index 0000000..9371602 --- /dev/null +++ b/tests/test_computer.py @@ -0,0 +1,163 @@ +"""Unittests for the py_ballisticcalc library""" + +import unittest +import copy +from py_ballisticcalc import DragModel, Ammo, Weapon, Calculator, Shot, Wind, Atmo, TableG7 +from py_ballisticcalc.unit import * + +class TestComputer(unittest.TestCase): + """Basic verifications that wind, spin, and cant values produce effects of correct sign and magnitude""" + + def setUp(self): + """Baseline shot has barrel at zero elevation""" + self.range = 1000 + self.step = 100 + self.dm = DragModel(0.22, TableG7, 168, 0.308) + self.ammo = Ammo(self.dm, 1.22, Velocity(2600, Velocity.FPS)) + self.weapon = Weapon(4, 12) + self.atmosphere = Atmo.icao() # Standard sea-level atmosphere + self.calc = Calculator() + self.baseline_shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=self.atmosphere) + self.baseline_trajectory = self.calc.fire(shot=self.baseline_shot, trajectory_range=self.range, trajectory_step=self.step) + +#region Cant_angle + def test_cant_zero_elevation(self): + """Cant_angle = 90 degrees with zero barrel elevation should match baseline with: + drop+=sight_height, windage-=sight_height + """ + canted = copy.copy(self.baseline_shot) + canted.cant_angle = Angular.Degree(90) + t = self.calc.fire(canted, trajectory_range=self.range, trajectory_step=self.step) + self.assertAlmostEqual(t.trajectory[5].drop.raw_value-self.weapon.sight_height.raw_value, + self.baseline_trajectory[5].drop.raw_value) + self.assertAlmostEqual(t.trajectory[5].windage.raw_value+self.weapon.sight_height.raw_value, + self.baseline_trajectory[5].windage.raw_value) + + def test_cant_positive_elevation(self): + """Cant_angle = 90 degrees with positive barrel elevation and zero twist should match baseline with: + drop+=sight_height, windage-=sight_height at muzzle, increasingly positive down-range + """ + canted = Shot(weapon=Weapon(sight_height=self.weapon.sight_height, twist=0, zero_elevation=Angular.Mil(2)), + ammo=self.ammo, atmo=self.atmosphere, cant_angle=Angular.Degree(90)) + t = self.calc.fire(canted, trajectory_range=self.range, trajectory_step=self.step) + self.assertAlmostEqual(t.trajectory[5].drop.raw_value-self.weapon.sight_height.raw_value, + self.baseline_trajectory[5].drop.raw_value, 2) + self.assertAlmostEqual(t.trajectory[0].windage.raw_value, -self.weapon.sight_height.raw_value) + self.assertGreater(t.trajectory[5].windage.raw_value, t.trajectory[3].windage.raw_value) + + def test_cant_zero_sight_height(self): + """Cant_angle = 90 degrees with sight_height=0 and barrel_elevation=0 should match baseline with: + drop+=baseline.sight_height, windage no change + """ + canted = Shot(weapon=Weapon(sight_height=0, twist=self.weapon.twist), + ammo=self.ammo, atmo=self.atmosphere, cant_angle=Angular.Degree(90)) + t = self.calc.fire(canted, trajectory_range=self.range, trajectory_step=self.step) + self.assertAlmostEqual(t.trajectory[5].drop.raw_value-self.weapon.sight_height.raw_value, + self.baseline_trajectory[5].drop.raw_value) + self.assertAlmostEqual(t.trajectory[5].windage, self.baseline_trajectory[5].windage) +#endregion Cant_angle + +#region Wind + def test_wind_from_left(self): + """Wind from left should increase windage""" + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=self.atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(3, Angular.OClock))]) + t = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].windage, self.baseline_trajectory[5].windage) + + def test_wind_from_right(self): + """Wind from right should decrease windage""" + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=self.atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(9, Angular.OClock))]) + t = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertLess(t.trajectory[5].windage, self.baseline_trajectory[5].windage) + + def test_wind_from_back(self): + """Wind from behind should decrease drop""" + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=self.atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(0, Angular.OClock))]) + t = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].drop, self.baseline_trajectory[5].drop) + + def test_wind_from_front(self): + """Wind from in front should increase drop""" + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=self.atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(6, Angular.OClock))]) + t = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertLess(t.trajectory[5].drop, self.baseline_trajectory[5].drop) +#endregion Wind + +#region Twist + def test_no_twist(self): + """Barrel with no twist should have no spin drift""" + shot = Shot(weapon=Weapon(twist=0), ammo=self.ammo, atmo=self.atmosphere) + t = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertEqual(t.trajectory[5].windage.raw_value, 0) + + def test_twist(self): + """Barrel with right-hand twist should have positive spin drift. + Barrel with left-hand twist should have negative spin drift. + Faster twist rates should produce larger drift. + """ + shot = Shot(weapon=Weapon(twist=12), ammo=self.ammo, atmo=self.atmosphere) + twist_right = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(twist_right.trajectory[5].windage.raw_value, 0) + shot = Shot(weapon=Weapon(twist=-8), ammo=self.ammo, atmo=self.atmosphere) + twist_left = self.calc.fire(shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertLess(twist_left.trajectory[5].windage.raw_value, 0) + # Faster twist should produce larger drift: + self.assertGreater(-twist_left.trajectory[5].windage.raw_value, twist_right.trajectory[5].windage.raw_value) +#endregion Twist + +#region Atmo + def test_humidity(self): + """Increasing relative humidity should decrease drop (due to decreasing density)""" + humid = Atmo(humidity=.9) # 90% humidity + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=humid) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].drop, self.baseline_trajectory[5].drop) + + def test_temp_atmo(self): + """Dropping temperature should increase drop (due to increasing density)""" + cold = Atmo(temperature=Temperature.Celsius(0)) + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=cold) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertLess(t.trajectory[5].drop, self.baseline_trajectory[5].drop) + + def test_altitude(self): + """Increasing altitude should decrease drop (due to decreasing density)""" + high = Atmo.icao(Distance.Foot(5000)) + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=high) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].drop, self.baseline_trajectory[5].drop) + + def test_pressure(self): + """Decreasing pressure should decrease drop (due to decreasing density)""" + thin = Atmo(pressure=Pressure.InHg(20.0)) + shot = Shot(weapon=self.weapon, ammo=self.ammo, atmo=thin) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].drop, self.baseline_trajectory[5].drop) +#endregion Atmo + +#region Ammo + def test_ammo_drag(self): + """Increasing ballistic coefficient (BC) should decrease drop""" + tdm = DragModel(self.dm.value+0.5, self.dm.drag_table, self.dm.weight, self.dm.diameter) + slick = Ammo(tdm, self.ammo.length, self.ammo.mv) + shot = Shot(weapon=self.weapon, ammo=slick, atmo=self.atmosphere) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertGreater(t.trajectory[5].drop, self.baseline_trajectory[5].drop) + + def test_ammo_optional(self): + """DragModel.weight and .diameter, and Ammo.length, are only relevant when computing + spin-drift. Drop should match baseline with those parameters omitted. + """ + tdm = DragModel(self.dm.value, self.dm.drag_table) + tammo = Ammo(tdm, mv=self.ammo.mv) + shot = Shot(weapon=self.weapon, ammo=tammo, atmo=self.atmosphere) + t = self.calc.fire(shot=shot, trajectory_range=self.range, trajectory_step=self.step) + self.assertEqual(t.trajectory[5].drop, self.baseline_trajectory[5].drop) +#endregion Ammo + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_danger_space.py b/tests/test_danger_space.py new file mode 100644 index 0000000..e305b8f --- /dev/null +++ b/tests/test_danger_space.py @@ -0,0 +1,37 @@ +import unittest +from py_ballisticcalc import * + + +class TestDangerSpace(unittest.TestCase): + + def setUp(self) -> None: + self.look_angle = Angular.Degree(0) + weight, diameter = 168, 0.308 + length = Distance.Inch(1.282) + dm = DragModel(0.223, TableG7, weight, diameter) + ammo = Ammo(dm, length, Velocity.FPS(2750), Temperature.Celsius(15)) + ammo.calc_powder_sens(2723, 0) + current_winds = [Wind(2, 90)] + shot = Shot(weapon=Weapon(), ammo=ammo, winds=current_winds) + calc = Calculator() + calc.set_weapon_zero(shot, Distance.Foot(300)) + self.shot_result = calc.fire(shot, trajectory_range=Distance.Yard(1000), trajectory_step=Distance.Yard(100), extra_data=True) + + def test_danger_space(self): + danger_space = self.shot_result.danger_space( + Distance.Yard(500), Distance.Meter(1.5), self.look_angle + ) + + self.assertAlmostEqual( + round(danger_space.begin.distance >> Distance.Yard, Distance.Yard.accuracy), 393.6, 1) + self.assertAlmostEqual( + round(danger_space.end.distance >> Distance.Yard, Distance.Yard.accuracy), 579.0, 1) + + danger_space = self.shot_result.danger_space( + Distance.Yard(500), Distance.Inch(10), self.look_angle + ) + + self.assertAlmostEqual( + round(danger_space.begin.distance >> Distance.Yard, Distance.Yard.accuracy), 484.5, 1) + self.assertAlmostEqual( + round(danger_space.end.distance >> Distance.Yard, Distance.Yard.accuracy), 514.8, 1) diff --git a/tests/test_mbc.py b/tests/test_mbc.py new file mode 100644 index 0000000..d67b60b --- /dev/null +++ b/tests/test_mbc.py @@ -0,0 +1,45 @@ +import unittest +from py_ballisticcalc import * + + +class TestMBC(unittest.TestCase): + + def test_mbc(self): + mbc = MultiBC( + drag_table=TableG7, + weight=Weight(178, Weight.Grain), + diameter=Distance(0.308, Distance.Inch), + mbc_table=[{'BC': p[0], 'V': p[1]} for p in ((0.275, 800), (0.255, 500), (0.26, 700))], + ) + dm = DragModel.from_mbc(mbc) + ammo = Ammo(dm, 1, 800) + cdm = TrajectoryCalc(ammo=ammo).cdm + self.assertIsNot(cdm, None) + ret = list(cdm) + self.assertEqual(ret[0], {'Mach': 0.0, 'CD': 0.1259323091692403}) + self.assertEqual(ret[-1], {'Mach': 5.0, 'CD': 0.15771258594668947}) + + def test_mbc_valid(self): + # Litz's multi-bc table comversion to CDM, 338LM 285GR HORNADY ELD-M + mbc = MultiBC( + drag_table=TableG7, + weight=Weight.Grain(285), + diameter=Distance.Inch(0.338), + mbc_table=[{'BC': p[0], 'V': Velocity.MPS(p[1])} for p in ((0.417, 745), (0.409, 662), (0.4, 580))], + ) + cdm = mbc.cdm + cds = [p['CD'] for p in cdm] + machs = [p['Mach'] for p in cdm] + + reference = ( + (1, 0.3384895315), + (2, 0.2573873416), + (3, 0.2069547831), + (4, 0.1652052415), + (5, 0.1381406102), + ) + + for mach, cd in reference: + idx = machs.index(mach) + with self.subTest(mach=mach): + self.assertAlmostEqual(cds[idx], cd, 3) diff --git a/tests/test_trajectory.py b/tests/test_trajectory.py new file mode 100644 index 0000000..e3c0a99 --- /dev/null +++ b/tests/test_trajectory.py @@ -0,0 +1,119 @@ +"""Unittests for the py_ballisticcalc library""" + +import unittest +from math import fabs +from py_ballisticcalc import * + + +class TestTrajectory(unittest.TestCase): + + def test_zero1(self): + dm = DragModel(0.365, TableG1, 69, 0.223) + ammo = Ammo(dm, 0.9, 2600) + weapon = Weapon(Distance(3.2, Distance.Inch)) + atmosphere = Atmo.icao() + calc = Calculator() + zero_angle = calc.barrel_elevation_for_target(Shot(weapon=weapon, ammo=ammo, atmo=atmosphere), + Distance(100, Distance.Yard)) + + self.assertAlmostEqual(zero_angle >> Angular.Radian, 0.001651, 6, + f'TestZero1 failed {zero_angle >> Angular.Radian:.10f}') + + def test_zero2(self): + dm = DragModel(0.223, TableG7, 69, 0.223) + ammo = Ammo(dm, 0.9, 2750) + weapon = Weapon(Distance(2, Distance.Inch)) + atmosphere = Atmo.icao() + calc = Calculator() + zero_angle = calc.barrel_elevation_for_target(Shot(weapon=weapon, ammo=ammo, atmo=atmosphere), + Distance(100, Distance.Yard)) + + self.assertAlmostEqual(zero_angle >> Angular.Radian, 0.001228, 6, + f'TestZero2 failed {zero_angle >> Angular.Radian:.10f}') + + def custom_assert_equal(self, a, b, accuracy, name): + with self.subTest(name=name): + self.assertLess(fabs(a - b), accuracy, f'Assertion {name} failed ({a}/{b}, {accuracy})') + + def validate_one(self, data: TrajectoryData, distance: float, velocity: float, + mach: float, energy: float, path: float, hold: float, + windage: float, wind_adjustment: float, time: float, ogv: float, + adjustment_unit: Unit): + + self.custom_assert_equal(distance, data.distance >> Distance.Yard, 0.001, "Distance") + self.custom_assert_equal(velocity, data.velocity >> Velocity.FPS, 5, "Velocity") + self.custom_assert_equal(mach, data.mach, 0.005, "Mach") + self.custom_assert_equal(energy, data.energy >> Energy.FootPound, 5, "Energy") + self.custom_assert_equal(time, data.time, 0.06, "Time") + self.custom_assert_equal(ogv, data.ogw >> Weight.Pound, 1, "OGV") + + if distance >= 800: + self.custom_assert_equal(path, data.drop >> Distance.Inch, 4, 'Drop') + elif distance >= 500: + self.custom_assert_equal(path, data.drop >> Distance.Inch, 1, 'Drop') + else: + self.custom_assert_equal(path, data.drop >> Distance.Inch, 0.5, 'Drop') + + if distance > 1: + self.custom_assert_equal(hold, data.drop_adj >> adjustment_unit, 0.5, 'Hold') + + if distance >= 800: + self.custom_assert_equal(windage, data.windage >> Distance.Inch, 1.5, "Windage") + elif distance >= 500: + self.custom_assert_equal(windage, data.windage >> Distance.Inch, 1, "Windage") + else: + self.custom_assert_equal(windage, data.windage >> Distance.Inch, 0.5, "Windage") + + if distance > 1: + self.custom_assert_equal(wind_adjustment, + data.windage_adj >> adjustment_unit, 0.5, "WAdj") + + def test_path_g1(self): + dm = DragModel(0.223, TableG1, 168, 0.308) + ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS)) + weapon = Weapon(Distance(2, Distance.Inch), zero_elevation=Angular(0.001228, Angular.Radian)) + atmosphere = Atmo.icao() + calc = TrajectoryCalc(ammo) + shot_info = Shot(weapon=weapon, atmo=atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))]) + data = calc.trajectory(shot_info, Distance.Yard(1000), Distance.Yard(100)) + + self.custom_assert_equal(len(data), 11, 0.1, "Length") + + test_data = [ + [data[0], 0, 2750, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, Angular.MOA], + [data[1], 100, 2351.2, 2.106, 2061, 0, 0, -0.6, -0.6, 0.118, 550, Angular.MOA], + [data[5], 500, 1169.1, 1.047, 509.8, -87.9, -16.8, -19.5, -3.7, 0.857, 67, Angular.MOA], + [data[10], 1000, 776.4, 0.695, 224.9, -823.9, -78.7, -87.5, -8.4, 2.495, 20, Angular.MOA] + ] + + for i, d in enumerate(test_data): + with self.subTest(f"validate one {i}"): + self.validate_one(*d) + + def test_path_g7(self): + dm = DragModel(0.223, TableG7, 168, 0.308) + ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS)) + weapon = Weapon(2, 12, zero_elevation=Angular.MOA(4.221)) + shot_info = Shot(weapon=weapon, ammo=ammo, winds=[Wind(Velocity(5, Velocity.MPH), -45)]) + + calc = TrajectoryCalc(ammo) + data = calc.trajectory(shot_info, Distance.Yard(1000), Distance.Yard(100)) + + self.custom_assert_equal(len(data), 11, 0.1, "Length") + + # Dist(yd), vel(fps), Mach, energy(ft-lb), drop(in), drop(mil), wind(in), wind(mil), time, ogw + test_data = [ + [data[0], 0, 2750, 2.46, 2821, -2.0, 0.0, 0.0, 0.00, 0.000, 880, Angular.Mil], + [data[1], 100, 2545, 2.28, 2416, 0.0, 0.0, -0.2, -0.06, 0.113, 698, Angular.Mil], + [data[5], 500, 1814, 1.62, 1227, -56.2, -3.2, -6.3, -0.36, 0.672, 252, Angular.Mil], + [data[10], 1000, 1086, 0.97, 440, -399.9, -11.3, -31.6, -0.90, 1.748, 54, Angular.Mil] + ] + + for i, d in enumerate(test_data): + with self.subTest(f"validate one {i}"): + self.validate_one(*d) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..2dea994 --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,193 @@ +import unittest +from py_ballisticcalc.unit import * + + +def back_n_forth(test, value, units): + u = test.unit_class(value, units) + v = u >> units + test.assertAlmostEqual(v, value, 7, f'Read back failed for {units}') + + +class TestAngular(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Angular + self.unit_list = [ + Angular.Degree, + Angular.MOA, + Angular.MRad, + Angular.Mil, + Angular.Radian, + Angular.Thousandth + ] + + def test_angular(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + def test_angle_truncation(self): + self.assertAlmostEqual(Angular(720, Angular.Degree), Angular(0, Angular.Degree)) + + +class TestDistance(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Distance + self.unit_list = [ + Distance.Centimeter, + Distance.Foot, + Distance.Inch, + Distance.Kilometer, + Distance.Line, + Distance.Meter, + Distance.Millimeter, + Distance.Mile, + Distance.NauticalMile, + Distance.Yard + ] + + def test_distance(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestEnergy(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Energy + self.unit_list = [ + Energy.FootPound, + Energy.Joule + ] + + def test_energy(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestPressure(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Pressure + self.unit_list = [ + Pressure.Bar, + Pressure.hPa, + Pressure.MmHg, + Pressure.InHg + ] + + def test_pressure(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestTemperature(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Temperature + self.unit_list = [ + Temperature.Fahrenheit, + Temperature.Kelvin, + Temperature.Celsius, + Temperature.Rankin + ] + + def test_temperature(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestVelocity(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Velocity + self.unit_list = [ + Velocity.FPS, + Velocity.KMH, + Velocity.KT, + Velocity.MPH, + Velocity.MPS + ] + + def test_velocity(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestWeight(unittest.TestCase): + + def setUp(self) -> None: + self.unit_class = Weight + self.unit_list = [ + Weight.Grain, + Weight.Gram, + Weight.Kilogram, + Weight.Newton, + Weight.Ounce, + Weight.Pound + ] + + def test_weight(self): + for u in self.unit_list: + with self.subTest(unit=u): + back_n_forth(self, 3, u) + + +class TestUnitConversionSyntax(unittest.TestCase): + + def setUp(self) -> None: + self.low = Distance.Yard(10) + self.high = Distance.Yard(100) + + def test__eq__(self): + self.assertEqual(self.low, 360) + self.assertEqual(360, self.low) + self.assertEqual(self.low, self.low) + self.assertEqual(self.low, Distance.Foot(30)) + + def test__ne__(self): + self.assertNotEqual(Distance.Yard(100), Distance.Yard(90)) + + def test__lt__(self): + self.assertLess(self.low, self.high) + self.assertLess(10, self.high) + self.assertLess(self.low, 9999) + + def test__gt__(self): + self.assertGreater(self.high, self.low) + self.assertGreater(self.high, 10) + self.assertGreater(9000, self.low) + + def test__ge__(self): + self.assertGreaterEqual(self.high, self.low) + self.assertGreaterEqual(self.high, self.high) + + self.assertGreaterEqual(self.high, 90) + self.assertGreaterEqual(self.high, 0) + + def test__le__(self): + self.assertLessEqual(self.low, self.high) + self.assertLessEqual(self.high, self.high) + + self.assertLessEqual(self.low, 360) + self.assertLessEqual(self.low, 360) + + def test__rshift__(self): + self.assertIsInstance(self.low >> Distance.Meter, (int, float)) + self.low >>= Distance.Meter + self.assertIsInstance(self.low, (int, float)) + + def test__lshift__(self): + desired_unit_type = Distance + desired_units = Distance.Foot + converted = self.low << desired_units + self.assertIsInstance(converted, desired_unit_type) + self.assertEqual(converted.units, desired_units) + self.low <<= desired_units + self.assertEqual(self.low.units, desired_units)