diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 18e27ba..2033788 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -20,4 +20,5 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + #pylint $(git ls-files '*.py') + pylint ./py_ballisticcalc diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 65a4cbe..bd51cb3 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,6 +11,7 @@ jobs: strategy: fail-fast: false matrix: + os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11"] steps: @@ -21,14 +22,17 @@ jobs: 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 flake8 + # python -m pip install flake8 python -m pip install cython + python -m pip install pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - sudo apt-get update - sudo apt-get install build-essential -y - python setup.py build_ext --inplace + + # python setup.py build_ext --inplace - name: Run unittest tests run: | - # python -m unittest discover -s . -p "test_*.py" - python -m unittest discover -s . -p "tests*.py" + # python -m unittest discover -s . -p "tests*.py" + pytest tests --no-header --no-summary -v diff --git a/README.md b/README.md index 963b9c1..09d1030 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,40 @@ # BallisticCalculator -LGPL library for small arms ballistic calculations (Python 3.9+) +#### LGPL library for small arms ballistic calculations (Python 3.9+) -### Table of contents +## 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) +* [Contributors](#contributors) * [About project](#about-project) -### Installation -#### Latest stable release from pypi** +## Installation +**Stable release from pypi, installing from binaries** + +(Contains c-extensions which offer higher performance) ```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 :all: -# master brunch -pip install git+https://github.com/o-murphy/py_ballisticcalc +**Build wheel package for your interpreter version by pypi sdist** -# specific branch -pip install git+https://github.com/o-murphy/py_ballisticcalc.git@ +Download and install MSVC or GCC depending on target platform +```shell +pip install Cython>=3.0.0a10 +pip install py-ballisticcalc --no-binary :all: ``` -#### Clone and build -**MSVC** or **GCC** required +**Also use `git clone` to build your own package** + +(Contains cython files to build your own c-extensions) ```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 + +## Usage The library supports all the popular units of measurement, and adds different built-in methods to define and manipulate it #### Unit manipulation syntax: @@ -111,13 +102,13 @@ weapon = Weapon(9, 100, 2) dm = DragModel(0.223, TableG7, weight, diameter) ammo = Ammo(dm, length, 2750, 15) -ammo.calc_powder_sens(2723, 0) # optional, uses if USE_POWDER_SENSITIVITY flag enabled +ammo.calc_powder_sens(2723, 0) zero_atmo = Atmo.icao() # defining calculator instance calc = Calculator(weapon, ammo, zero_atmo) -calc.update_elevation() # calculates zero barrel elevation +calc.update_elevation() shot = Shot(1500, 100) @@ -144,7 +135,15 @@ python -m py_ballisticcalc.example ['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'] ``` -### About project +## 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. diff --git a/py_ballisticcalc/__init__.py b/py_ballisticcalc/__init__.py index 4c385c2..5f22da4 100644 --- a/py_ballisticcalc/__init__.py +++ b/py_ballisticcalc/__init__.py @@ -3,8 +3,8 @@ __author__ = "o-murphy" __copyright__ = ("",) -__credits__ = ["O-Murphy"] -__version__ = "1.1.0b5" +__credits__ = ["o-murphy"] +__version__ = "1.1.0b7" from .drag_model import * # pylint: disable=import-error from .drag_tables import * diff --git a/py_ballisticcalc/interface.py b/py_ballisticcalc/interface.py index ddaa8b1..8b7ced1 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -5,7 +5,8 @@ from .conditions import Atmo, Wind, Shot from .munition import Weapon, Ammo from .settings import Settings as Set -from .trajectory_calc import TrajectoryCalc # pylint: disable=import-error +# pylint: disable=import-error,no-name-in-module +from .trajectory_calc import TrajectoryCalc from .trajectory_data import TrajectoryData, TrajFlag from .unit import Angular, Distance, is_unit @@ -53,19 +54,19 @@ def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: data = self._calc.trajectory(self.weapon, current_atmo, shot, winds) return data - def zero_given_elevation(self, elevation: [float, Angular] = 0, + def zero_given_elevation(self, shot: Shot, winds: list[Wind] = None) -> TrajectoryData: """Find the zero distance for a given barrel elevation""" self._calc = TrajectoryCalc(self.ammo) if not winds: winds = [Wind()] - elevation = elevation if is_unit(elevation) else Set.Units.angular - shot = Shot(1000, Distance.Foot(100), sight_angle=elevation) - data = self._calc.trajectory(self.weapon, self.zero_atmo, shot, winds, - brake_flags=TrajFlag.ZERO) - # No downrange zero found, so just return starting row - return data[-1] + data = self._calc.trajectory( + self.weapon, self.zero_atmo, shot, winds, + filter_flags=(TrajFlag.ZERO_UP | TrajFlag.ZERO_DOWN).value) + if len(data) < 1: + raise ArithmeticError("Can't found zero crossing points") + return data @staticmethod def danger_space(trajectory: TrajectoryData, target_height: [float, Distance]) -> Distance: @@ -87,3 +88,65 @@ def danger_space(trajectory: TrajectoryData, target_height: [float, Distance]) - else Set.Units.target_height(target_height)) >> Distance.Yard traj_angle_tan = math.tan(trajectory.angle >> Angular.Radian) return Distance.Yard(-(target_height / traj_angle_tan)) + + @staticmethod + def to_dataframe(trajectory: list[TrajectoryData]): + import pandas as pd + col_names = TrajectoryData._fields + trajectory = [p.in_def_units() for p in trajectory] + return pd.DataFrame(trajectory, columns=col_names) + + def show_plot(self, shot, winds): + import matplotlib + import matplotlib.pyplot as plt + matplotlib.use('TkAgg') + self._calc = TrajectoryCalc(self.ammo) + # self.update_elevation() + data = self._calc.trajectory(self.weapon, self.zero_atmo, shot, winds, + TrajFlag.ALL.value) # Step in 10-yard increments to produce smoother curves + df = self.to_dataframe(data) + ax = df.plot(x='distance', y=['drop'], ylabel=Set.Units.drop.symbol) + + # zero_d = self.weapon.zero_distance >> Set.Units.distance + # zero = ax.plot([zero_d, zero_d], [df['drop'].min(), df['drop'].max()], linestyle='--') + + for p in data: + + 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=':') + 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='--', label='mach') + ax.text(p.distance >> Set.Units.distance, df['drop'].min(), " Mach") + + sh = self.weapon.sight_height >> Set.Units.drop + + # # scope line + x_values = [0, df.distance.max()] # Adjust these as needed + # y_values = [sh, sh - df.distance.max() * math.tan(self.elevation >> Angular.Degree)] # Adjust these as needed + y_values = [0, 0] # Adjust these as needed + ax.plot(x_values, y_values, linestyle='--', label='scope line') + ax.text(df.distance.max() - 100, -100, "Scope") + + # # # barrel line + # elevation = self.elevation >> Angular.Degree + # + # y = sh / math.cos(elevation) + # x0 = sh / math.sin(elevation) + # x1 = sh * math.sin(elevation) + # x_values = [0, x0] + # y_values = [-sh, 0] + # ax.plot(x_values, y_values, linestyle='-.', label='barrel line') + + df.plot(x='distance', xlabel=Set.Units.distance.symbol, + y=['velocity'], ylabel=Set.Units.velocity.symbol, + secondary_y=True, + ylim=[0, df['velocity'].max()], ax=ax) + plt.title = f"{self.weapon.sight_height} {self.weapon.zero_distance}" + + plt.show() + # ax = df.plot(y=[c.tableCols['Drop'][0]], ylabel=UNIT_DISPLAY[c.heightUnits].units) + # df.plot(y=[c.tableCols['Velocity'][0]], ylabel=UNIT_DISPLAY[c.bullet.velocityUnits].units, secondary_y=True, + # ylim=[0, df[c.tableCols['Velocity'][0]].max()], ax=ax) + # plt.show() diff --git a/py_ballisticcalc/multiple_bc.py b/py_ballisticcalc/multiple_bc.py index 0964002..1ba2df3 100644 --- a/py_ballisticcalc/multiple_bc.py +++ b/py_ballisticcalc/multiple_bc.py @@ -4,7 +4,8 @@ from typing import NamedTuple, Iterable from .conditions import Atmo -from .drag_model import make_data_points # pylint: disable=import-error +# pylint: disable=import-error,no-name-in-module +from .drag_model import make_data_points from .settings import Settings as Set from .unit import Distance, Weight, Velocity, is_unit diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index 5c3f1d8..d9f4f25 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -3,7 +3,8 @@ import math from dataclasses import dataclass, field -from .drag_model import DragModel # pylint: disable=import-error +# pylint: disable=import-error,no-name-in-module +from .drag_model import DragModel from .settings import Settings as Set from .unit import TypedUnits, Velocity, Temperature, is_unit, Distance diff --git a/py_ballisticcalc/trajectory_calc.pyx b/py_ballisticcalc/trajectory_calc.pyx index 8cf78ca..4b5f2bf 100644 --- a/py_ballisticcalc/trajectory_calc.pyx +++ b/py_ballisticcalc/trajectory_calc.pyx @@ -1,11 +1,10 @@ from libc.math cimport sqrt, fabs, pow, sin, cos, log10, floor, atan cimport cython - from .conditions import * from .munition import * from .settings import Settings -from .trajectory_data import TrajectoryData, TrajFlag +from .trajectory_data import TrajectoryData from .unit import * __all__ = ('TrajectoryCalc',) @@ -19,6 +18,13 @@ 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 + cdef class Vector: cdef double x cdef double y @@ -118,8 +124,8 @@ cdef class TrajectoryCalc: def trajectory(self, weapon: Weapon, atmo: Atmo, shot_info: Shot, winds: list[Wind], - brake_flags: TrajFlag = TrajFlag.NONE): - return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, brake_flags) + filter_flags: int = CTrajFlag.RANGE): + return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, filter_flags) cdef _sight_angle(TrajectoryCalc self, object ammo, object weapon, object atmo): cdef: @@ -174,7 +180,7 @@ cdef class TrajectoryCalc: return Angular.Radian(barrel_elevation) cdef _trajectory(TrajectoryCalc self, object ammo, object weapon, object atmo, - object shot_info, list[object] winds, object brake_flags): + object shot_info, list[object] winds, CTrajFlag filter_flags): cdef: double density_factor, mach double time, velocity, windage, delta_time, drag @@ -186,10 +192,11 @@ cdef class TrajectoryCalc: double weight = ammo.dm.weight >> Weight.Grain double step = shot_info.step >> Distance.Foot - double maximum_range = (shot_info.max_range >> Distance.Foot) + 1 double calc_step = self.get_calc_step(step) - int ranges_length = int(floor(maximum_range / step)) + 1 + double maximum_range = (shot_info.max_range >> Distance.Foot) + step + + int ranges_length = int(maximum_range / step) + 1 int len_winds = len(winds) int current_item, current_wind, twist_coefficient @@ -235,6 +242,8 @@ cdef class TrajectoryCalc: twist_coefficient = -1 if twist > 0 else 1 while range_vector.x <= maximum_range + calc_step: + _flag = CTrajFlag.NONE + if velocity < cMinimumVelocity or range_vector.y < cMaximumDrop: break @@ -249,38 +258,41 @@ cdef class TrajectoryCalc: else: next_wind_range = winds[current_wind].until_distance() >> Distance.Foot - if range_vector.x >= next_range_distance: - _flag = TrajFlag.NONE - - windage = range_vector.z + # Zero-crossing check + if range_vector.x > 0: # skipping zero point when "sight_height == 0" + if range_vector.y < 0 <= previous_y: + _flag |= CTrajFlag.ZERO_DOWN + elif range_vector.y >= 0 > previous_y: + _flag |= CTrajFlag.ZERO_UP - if twist != 0: - windage += (1.25 * (stability_coefficient + 1.2) * pow(time, 1.83) * twist_coefficient) / 12 + # Mach crossing check + if (velocity / mach <= 1) and (previous_mach > 1): + _flag |= CTrajFlag.MACH - # Zero-crossing check - if range_vector.y + sight_height < 0 < previous_y + sight_height: - _flag |= TrajFlag.ZERO + # Next range check + if range_vector.x >= next_range_distance: + _flag |= CTrajFlag.RANGE + next_range_distance += step + current_item += 1 - # Mach crossing check - if (velocity / mach < 1) and (previous_mach > 1): - _flag |= TrajFlag.MACH + if _flag & filter_flags: - ranges.append( - create_trajectory_row(time, range_vector, velocity_vector, - velocity, mach, windage, weight, _flag) - ) + windage = range_vector.z - if _flag & brake_flags & (TrajFlag.ZERO | TrajFlag.MACH): - break + if twist != 0: + windage += (1.25 * (stability_coefficient + 1.2) + * pow(time, 1.83) * twist_coefficient) / 12 - next_range_distance += step - current_item += 1 + ranges.append(create_trajectory_row( + time, range_vector, velocity_vector, + velocity, mach, windage, weight, _flag + )) if current_item == ranges_length: break - previous_y = range_vector.y - previous_mach = velocity / mach + previous_y = range_vector.y + previous_mach = velocity / mach velocity_adjusted = velocity_vector - wind_vector diff --git a/py_ballisticcalc/trajectory_data.py b/py_ballisticcalc/trajectory_data.py index 00da8dc..30bebf3 100644 --- a/py_ballisticcalc/trajectory_data.py +++ b/py_ballisticcalc/trajectory_data.py @@ -1,5 +1,5 @@ """Implements a point of trajectory class in applicable data types""" -from enum import Flag, auto +from enum import Flag from typing import NamedTuple from .settings import Settings as Set @@ -10,11 +10,15 @@ class TrajFlag(Flag): """Flags for marking trajectory row if Zero or Mach crossing - Also uses to set a brake points of trajectory calculation loop + Also uses to set a filters for a trajectory calculation loop """ - NONE = auto() - ZERO = auto() - MACH = auto() + NONE = 0 + ZERO_UP = 1 + ZERO_DOWN = 2 + MACH = 4 + RANGE = 8 + ZERO = ZERO_UP | ZERO_DOWN + ALL = RANGE | ZERO_UP | ZERO_DOWN | MACH class TrajectoryData(NamedTuple): @@ -48,7 +52,7 @@ class TrajectoryData(NamedTuple): angle: Angular # Trajectory angle energy: Energy ogw: Weight - flag: TrajFlag = TrajFlag.NONE + flag: TrajFlag def formatted(self) -> tuple: """ @@ -69,7 +73,9 @@ def _fmt(v: AbstractUnit, u: Unit): _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) + _fmt(self.ogw, Set.Units.ogw), + + self.flag ) def in_def_units(self) -> tuple: @@ -87,5 +93,7 @@ def in_def_units(self) -> tuple: self.windage_adj >> Set.Units.adjustment, self.angle >> Set.Units.angular, self.energy >> Set.Units.energy, - self.ogw >> Set.Units.ogw + self.ogw >> Set.Units.ogw, + + self.flag ) diff --git a/py_ballisticcalc/unit.py b/py_ballisticcalc/unit.py index 8e7c705..7222bd9 100644 --- a/py_ballisticcalc/unit.py +++ b/py_ballisticcalc/unit.py @@ -197,15 +197,7 @@ def __repr__(self): """Returns instance as readable view :return: instance as readable view """ - return f'<{self.__class__.__name__}: {self >> self.units} ' \ - f'{self.units.symbol} ({self._value})>' - - # def __format__(self, format_spec: str = "{v:.{a}f} {s}"): - # """ - # :param format_spec: (str) - # """ - # return format_spec.format(v=self._value, a=self._defined_units.key, \ - # s=self._defined_units.symbol) + return f'<{self.__class__.__name__}: {self << self.units} ({round(self._value, 4)})>' def __float__(self): return float(self._value) @@ -633,39 +625,6 @@ def is_unit(obj: [AbstractUnit, float, int]): return None raise TypeError(f"Expected Unit, int, or float, found {obj.__class__.__name__}") -# class Convertor: -# def __init__(self, measure=None, unit: int = 0, default_unit: int = 0): -# self.measure = measure -# self.unit = unit -# self.default_unit = default_unit -# -# def fromRaw(self, value): -# return self.measure(value, self.default_unit).get_in(self.unit) -# -# def toRaw(self, value): -# return self.measure(value, self.unit).get_in(self.default_unit) -# -# @property -# def accuracy(self): -# return self.measure.accuracy(self.unit) -# -# @property -# def unit_name(self): -# return self.measure.name(self.unit) - - -# u = Weight -# for k, v in u.__dict__.items(): -# if k in Unit.__members__: -# print(f"Unit.{k}: UnitProps('{k.lower()}', {u.accuracy(v)}, '{u.name(v)}'),") - -# Unit.Centimeter.meta = 1 -# -# m = MetaUnit(17, MeasureType.Distance) -# print(m, m.measure_type) -# -# print(Unit.Meter.measure_type) - # Default units # Angular.Radian diff --git a/pyproject.toml b/pyproject.toml index b63fb0a..191439d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "Cython"] +requires = ["setuptools", "wheel", "cython"] build-backend = "setuptools.build_meta" @@ -31,7 +31,6 @@ classifiers = [ dependencies = [] dynamic = ["version"] - [project.urls] "Homepage" = "https://github.com/o-murphy/py_ballisticcalc" "Bug Reports" = "https://github.com/o-murphy/py_ballisticcalc/issues" @@ -50,4 +49,6 @@ version = {attr = "py_ballisticcalc.__version__"} [project.optional-dependencies] -dev = ["cython>=3.0.0a10"] +dev = ['Cython', 'setuptools'] +lint = ['pylint', 'flake8'] +plot = ['matplotlib', 'pandas'] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py_ballisticcalc/tests.py b/tests/test_all.py similarity index 82% rename from py_ballisticcalc/tests.py rename to tests/test_all.py index c55da66..5ac4a3a 100644 --- a/py_ballisticcalc/tests.py +++ b/tests/test_all.py @@ -10,7 +10,7 @@ from py_ballisticcalc import Temperature, Pressure, Energy, Unit from py_ballisticcalc import DragModel, Ammo, Weapon, Wind, Shot, Atmo from py_ballisticcalc import TableG7, TableG1, MultiBC -from py_ballisticcalc import TrajectoryData, TrajectoryCalc +from py_ballisticcalc import TrajectoryData, TrajectoryCalc, TrajFlag class TestMBC(unittest.TestCase): @@ -38,21 +38,52 @@ def setUp(self) -> None: self.ammo = Ammo(dm, 1.22, Velocity(2600, Velocity.FPS)) self.atmosphere = Atmo.icao() - @unittest.skip(reason="Fixme: zero_given_elevation") def test_zero_given(self): - - for sh in [0, 2, 3]: + # pylint: disable=consider-using-f-string + output_fmt = "elev: {}\tscope: {}\tzero: {} {}\ttarget: {}\tdistance: {}\tdrop: {}" + + def print_output(data, at_elevation): + for point in data: + print( + output_fmt.format( + at_elevation, + sight_height, + point.distance << Distance.Yard, + TrajFlag(point.flag), + target_distance, + point.distance << Distance.Yard, + point.drop << Distance.Inch + ) + ) + print() + + for sh in range(0, 5): for reference_distance in range(100, 600, 200): - with self.subTest(zero_range=reference_distance, sight_height=sh): - weapon = Weapon(Distance.Inch(sh), Distance.Yard(reference_distance), 11.24) + target_distance = Distance.Yard(reference_distance) + sight_height = Distance.Inch(sh) + weapon = Weapon(sight_height, target_distance, 11.24) calc = Calculator(weapon, self.ammo, self.atmosphere) - calc.update_elevation() - # print('\nelevation', calc.elevation << Angular.MOA) calc.weapon.sight_height = Distance.Inch(sh) - zero_given = calc.zero_given_elevation(calc.elevation) - zero_range = zero_given.distance >> Distance.Yard - self.assertAlmostEqual(zero_range, reference_distance, 7) + + with self.subTest(zero=reference_distance, sh=sh): + try: + calc.update_elevation() + shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + zero_crossing_points = calc.zero_given_elevation(shot) + print_output(zero_crossing_points, calc.elevation) + except ArithmeticError as err: + if err == "Can't found zero crossing points": + pass + + with self.subTest(zero=reference_distance, sh=sh, elev=0): + try: + shot = Shot(1000, Distance.Foot(0.2), sight_angle=0) + zero_crossing_points = calc.zero_given_elevation(shot) + print_output(zero_crossing_points, 0) + except ArithmeticError as err: + if err == "Can't found zero crossing points": + pass @unittest.skip(reason="Fixme: danger_space") def test_danger_space(self): @@ -63,7 +94,7 @@ def test_danger_space(self): print('aim', calc.elevation << Angular.MOA) zero_given = calc.zero_given_elevation(calc.elevation, winds) print(zero_given.distance << Distance.Yard) - print(calc.test_danger_space(zero_given, Distance.Meter(1.7)) << Distance.Meter) + print(calc.danger_space(zero_given, Distance.Meter(1.7)) << Distance.Meter) print(calc.danger_space(zero_given, Distance.Meter(1.5)) << Distance.Meter) print(calc.danger_space(zero_given, Distance.Inch(10)) << Distance.Yard) @@ -95,7 +126,7 @@ def test_zero2(self): f'TestZero2 failed {sight_angle >> Angular.Radian:.10f}') def custom_assert_equal(self, a, b, accuracy, name): - with self.subTest(): + 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, @@ -203,10 +234,11 @@ def test_elevation_performance(self): self.calc.sight_angle(self.weapon, self.atmo) def test_path_performance(self): - self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind) + d = self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind) + # [print(p.formatted()) for p in d] -def test_back_n_forth(test, value, units): +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}') @@ -227,7 +259,7 @@ def setUp(self) -> None: def test_angular(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestDistance(unittest.TestCase): @@ -249,7 +281,7 @@ def setUp(self) -> None: def test_distance(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestEnergy(unittest.TestCase): @@ -263,7 +295,7 @@ def setUp(self) -> None: def test_energy(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestPressure(unittest.TestCase): @@ -279,7 +311,7 @@ def setUp(self) -> None: def test_pressure(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestTemperature(unittest.TestCase): @@ -295,7 +327,7 @@ def setUp(self) -> None: def test_temperature(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestVelocity(unittest.TestCase): @@ -312,7 +344,7 @@ def setUp(self) -> None: def test_velocity(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestWeight(unittest.TestCase): @@ -330,7 +362,7 @@ def setUp(self) -> None: def test_weight(self): for u in self.unit_list: with self.subTest(unit=u): - test_back_n_forth(self, 3, u) + back_n_forth(self, 3, u) class TestUnitConversionSyntax(unittest.TestCase): diff --git a/tests/test_plot.py b/tests/test_plot.py new file mode 100644 index 0000000..033354a --- /dev/null +++ b/tests/test_plot.py @@ -0,0 +1,15 @@ +import pyximport; pyximport.install(language_level=3) +from py_ballisticcalc import * + +Settings.Units.velocity = Velocity.MPS + +dm = DragModel(0.22, TableG7, 168, 0.308) +ammo = Ammo(dm, 1.22, Velocity(2600, Velocity.FPS)) +atmosphere = Atmo.icao() +weapon = Weapon(3, 100, 11.24) + +calc = Calculator(weapon, ammo, atmosphere) +calc.update_elevation() +shot = Shot(1200, Distance.Foot(0.2), sight_angle=calc.elevation, shot_angle=Angular.Mil(0)) + +calc.show_plot(shot, [Wind()])