From f6f236996f3345b7bca7d2f7f6cce6be94fbf698 Mon Sep 17 00:00:00 2001 From: Murphy Date: Tue, 10 Oct 2023 19:38:41 +0300 Subject: [PATCH 01/15] * perform branch for a danger space fix --- tests/test_all.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/test_all.py b/tests/test_all.py index 5ac4a3a..22dc2ef 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -85,18 +85,23 @@ def print_output(data, at_elevation): if err == "Can't found zero crossing points": pass - @unittest.skip(reason="Fixme: danger_space") + # @unittest.skip(reason="Fixme: danger_space") def test_danger_space(self): - winds = [Wind()] - weapon = Weapon(Distance.Inch(0), Distance.Yard(400), 11.24) + zero_distance = Distance.Yard(100) + weapon = Weapon(Distance.Inch(4), zero_distance, 11.24) calc = Calculator(weapon, self.ammo, self.atmosphere) calc.update_elevation() - print('aim', calc.elevation << Angular.MOA) - zero_given = calc.zero_given_elevation(calc.elevation, winds) - print(zero_given.distance << Distance.Yard) - 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) + shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + zero_given_elevation = calc.zero_given_elevation(shot) + if len(zero_given_elevation) > 0: + zero = [p for p in zero_given_elevation if abs( + (p.distance >> Distance.Yard) - (zero_distance >> Distance.Yard) + ) <= 1e7][0] + else: + raise ArithmeticError + print(zero.distance << Distance.Yard, calc.danger_space(zero, Distance.Meter(1.7)) << Distance.Meter) + print(zero.distance << Distance.Yard, calc.danger_space(zero, Distance.Meter(1.5)) << Distance.Meter) + print(zero.distance << Distance.Yard, calc.danger_space(zero, Distance.Inch(10)) << Distance.Yard) class TestTrajectory(unittest.TestCase): From a66dd20b44b8f573574a1ef331c13faa64146184 Mon Sep 17 00:00:00 2001 From: Murphy Date: Tue, 10 Oct 2023 19:45:31 +0300 Subject: [PATCH 02/15] * perform branch for a danger space fix --- tests/{test_plot.py => test_plot.py.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_plot.py => test_plot.py.bak} (100%) diff --git a/tests/test_plot.py b/tests/test_plot.py.bak similarity index 100% rename from tests/test_plot.py rename to tests/test_plot.py.bak From 27137592dacf9fe160627051fc12ce7c5d6d7de3 Mon Sep 17 00:00:00 2001 From: Murphy Date: Tue, 10 Oct 2023 19:51:16 +0300 Subject: [PATCH 03/15] * perform branch for a danger space fix --- .github/workflows/python-package.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bd51cb3..9cc0dc5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,5 +34,9 @@ jobs: # python setup.py build_ext --inplace - name: Run unittest tests run: | - # python -m unittest discover -s . -p "tests*.py" pytest tests --no-header --no-summary -v + + continue-on-error: true + failure-commands: | + echo "Pytest failed, running no-captured" + pytest tests --no-header --no-summary -v -s \ No newline at end of file From f4e175183705954ad9cc9a253bdcedc047c79630 Mon Sep 17 00:00:00 2001 From: Murphy Date: Tue, 10 Oct 2023 19:52:49 +0300 Subject: [PATCH 04/15] * perform branch for a danger space fix --- .github/workflows/python-package.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9cc0dc5..3567f6e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,9 +34,9 @@ jobs: # python setup.py build_ext --inplace - name: Run unittest tests run: | - pytest tests --no-header --no-summary -v - - continue-on-error: true - failure-commands: | - echo "Pytest failed, running no-captured" - pytest tests --no-header --no-summary -v -s \ No newline at end of file + if pytest tests --no-header --no-summary -v; then + echo "Pytest succeeded." + else + echo "Pytest failed, running additional commands here." + # Add your additional commands here + fi From 479ba82106b0aa014c107ceba3f4447af4922c1f Mon Sep 17 00:00:00 2001 From: Dmytro Yaroshenko <73843436+o-murphy@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:02:17 +0300 Subject: [PATCH 05/15] Update python-package.yml --- .github/workflows/python-package.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3567f6e..9af6c01 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,6 +37,7 @@ jobs: if pytest tests --no-header --no-summary -v; then echo "Pytest succeeded." else - echo "Pytest failed, running additional commands here." + echo "Pytest failed, running without capture" # Add your additional commands here + pytest tests --no-header --no-summary -v -s fi From 29acee2d1e6e0c71217d69a8fcb9ce63af45a4e6 Mon Sep 17 00:00:00 2001 From: Murphy Date: Wed, 11 Oct 2023 18:55:51 +0300 Subject: [PATCH 06/15] * added mbc test --- tests/test_all.py | 72 +++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/tests/test_all.py b/tests/test_all.py index 22dc2ef..4dd3cbb 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -30,6 +30,30 @@ def test_mbc(self): 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) + self.assertAlmostEqual(cds[idx], cd, 3) + class TestInterface(unittest.TestCase): @@ -60,30 +84,30 @@ def print_output(data, at_elevation): for sh in range(0, 5): for reference_distance in range(100, 600, 200): - 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.weapon.sight_height = Distance.Inch(sh) - - 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 + 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.weapon.sight_height = Distance.Inch(sh) + + 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): From 446f06c65a521b14a3997a890ab22976b5e78316 Mon Sep 17 00:00:00 2001 From: Murphy Date: Wed, 11 Oct 2023 18:57:45 +0300 Subject: [PATCH 07/15] * added mbc test --- tests/test_all.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_all.py b/tests/test_all.py index 4dd3cbb..b54a715 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -52,7 +52,8 @@ def test_mbc_valid(self): for mach, cd in reference: idx = machs.index(mach) - self.assertAlmostEqual(cds[idx], cd, 3) + with self.subTest(mach=mach): + self.assertAlmostEqual(cds[idx], cd, 3) class TestInterface(unittest.TestCase): From 6b63a1fbbab08279818c6bee14e80efa2fb7cef4 Mon Sep 17 00:00:00 2001 From: Dmytro Yaroshenko <73843436+o-murphy@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:42:51 +0300 Subject: [PATCH 08/15] Added issue templates (#16) --- .github/ISSUE_TEMPLATE/bug_report.md | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md 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. From 6a81f8b2430d80f8911c4f1e81b62299c1597fe5 Mon Sep 17 00:00:00 2001 From: dbookstaber <99043839+dbookstaber@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:49:57 -0700 Subject: [PATCH 09/15] Renamed sight_angle -> zero_angle, shot_angle -> relative_angle Added comments on Shot attributes Fixed unit tests --- py_ballisticcalc/conditions.py | 23 ++++++++++++++------- py_ballisticcalc/interface.py | 8 ++++---- py_ballisticcalc/trajectory_calc.pyx | 16 +++++++-------- tests/test_all.py | 30 ++++++++++++++-------------- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/py_ballisticcalc/conditions.py b/py_ballisticcalc/conditions.py index fadd4d6..4c1379a 100644 --- a/py_ballisticcalc/conditions.py +++ b/py_ballisticcalc/conditions.py @@ -142,17 +142,26 @@ def __post_init__(self): @dataclass class Shot(TypedUnits): - """Stores shot parameters for the trajectory calculation""" + """ + Stores shot parameters for the trajectory calculation + + :param max_range: Downrange distance to stop computing trajectory + :param step: Distance between each TrajectoryData row to record + :param zero_angle: The angle between the barrel and horizontal when zeroed + :param relative_angle: Elevation adjustment added to zero_angle for a particular shot + :param cant_angle: Rotation of gun around barrel axis, relative to position when zeroed. + (Only relevant when Weapon.sight_height != 0) + """ max_range: [float, Distance] = field(default_factory=lambda: Set.Units.distance) step: [float, Distance] = field(default_factory=lambda: Set.Units.distance) - shot_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + zero_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) - sight_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) def __post_init__(self): - if not self.shot_angle: - self.shot_angle = 0 + if not self.relative_angle: + self.relative_angle = 0 if not self.cant_angle: self.cant_angle = 0 - if not self.sight_angle: - self.sight_angle = 0 + if not self.zero_angle: + self.zero_angle = 0 diff --git a/py_ballisticcalc/interface.py b/py_ballisticcalc/interface.py index 8b7ced1..eb0f722 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -38,7 +38,7 @@ def cdm(self): def update_elevation(self): """Recalculates barrel elevation for weapon and zero atmo""" self._calc = TrajectoryCalc(self.ammo) - self._elevation = self._calc.sight_angle(self.weapon, self.zero_atmo) + self._elevation = self._calc.zero_angle(self.weapon, self.zero_atmo) def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: """Calculates trajectory with current conditions @@ -48,9 +48,9 @@ def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: :return: trajectory table """ self._calc = TrajectoryCalc(self.ammo) - if not self._elevation and not shot.sight_angle: + if not self._elevation and not shot.zero_angle: self.update_elevation() - shot.sight_angle = self._elevation + shot.zero_angle = self._elevation data = self._calc.trajectory(self.weapon, current_atmo, shot, winds) return data @@ -120,7 +120,7 @@ def show_plot(self, shot, winds): [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 + #sh = self.weapon.sight_height >> Set.Units.drop # # scope line x_values = [0, df.distance.max()] # Adjust these as needed diff --git a/py_ballisticcalc/trajectory_calc.pyx b/py_ballisticcalc/trajectory_calc.pyx index 4b5f2bf..c213235 100644 --- a/py_ballisticcalc/trajectory_calc.pyx +++ b/py_ballisticcalc/trajectory_calc.pyx @@ -119,15 +119,15 @@ cdef class TrajectoryCalc: return step - def sight_angle(self, weapon: Weapon, atmo: Atmo): - return self._sight_angle(self.ammo, weapon, atmo) + def zero_angle(self, weapon: Weapon, atmo: Atmo): + return self._zero_angle(self.ammo, weapon, atmo) def trajectory(self, weapon: Weapon, atmo: Atmo, shot_info: Shot, winds: list[Wind], 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 _zero_angle(TrajectoryCalc self, object ammo, object weapon, object atmo): cdef: double calc_step = self.get_calc_step(weapon.zero_distance.units(10) >> Distance.Foot) double zero_distance = weapon.zero_distance >> Distance.Foot @@ -194,7 +194,7 @@ cdef class TrajectoryCalc: double step = shot_info.step >> Distance.Foot double calc_step = self.get_calc_step(step) - double maximum_range = (shot_info.max_range >> Distance.Foot) + step + double maximum_range = (shot_info.max_range >> Distance.Foot) + 1 int ranges_length = int(maximum_range / step) + 1 int len_winds = len(winds) @@ -203,8 +203,8 @@ cdef class TrajectoryCalc: double stability_coefficient = 1.0 double next_wind_range = 1e7 - double barrel_elevation = (shot_info.sight_angle >> Angular.Radian) + ( - shot_info.shot_angle >> Angular.Radian) + double barrel_elevation = (shot_info.zero_angle >> Angular.Radian) + ( + shot_info.relative_angle >> Angular.Radian) double alt0 = atmo.altitude >> Distance.Foot double sight_height = weapon.sight_height >> Distance.Foot @@ -354,8 +354,8 @@ cdef double calculate_stability_coefficient(object ammo, object rifle, object at cdef Vector wind_to_vector(object shot, object wind): cdef: - double sight_cosine = cos(shot.sight_angle >> Angular.Radian) - double sight_sine = sin(shot.sight_angle >> Angular.Radian) + double sight_cosine = cos(shot.zero_angle >> Angular.Radian) + double sight_sine = sin(shot.zero_angle >> Angular.Radian) double cant_cosine = cos(shot.cant_angle >> Angular.Radian) double cant_sine = sin(shot.cant_angle >> Angular.Radian) double range_velocity = (wind.velocity >> Velocity.FPS) * cos(wind.direction_from >> Angular.Radian) diff --git a/tests/test_all.py b/tests/test_all.py index b54a715..29246f1 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -94,7 +94,7 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh): try: calc.update_elevation() - shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) zero_crossing_points = calc.zero_given_elevation(shot) print_output(zero_crossing_points, calc.elevation) except ArithmeticError as err: @@ -103,7 +103,7 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh, elev=0): try: - shot = Shot(1000, Distance.Foot(0.2), sight_angle=0) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=0) zero_crossing_points = calc.zero_given_elevation(shot) print_output(zero_crossing_points, 0) except ArithmeticError as err: @@ -116,7 +116,7 @@ def test_danger_space(self): weapon = Weapon(Distance.Inch(4), zero_distance, 11.24) calc = Calculator(weapon, self.ammo, self.atmosphere) calc.update_elevation() - shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) zero_given_elevation = calc.zero_given_elevation(shot) if len(zero_given_elevation) > 0: zero = [p for p in zero_given_elevation if abs( @@ -138,10 +138,10 @@ def test_zero1(self): atmosphere = Atmo.icao() calc = TrajectoryCalc(ammo) - sight_angle = calc.sight_angle(weapon, atmosphere) + zero_angle = calc.zero_angle(weapon, atmosphere) - self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001651, 6, - f'TestZero1 failed {sight_angle >> Angular.Radian:.10f}') + 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) @@ -150,10 +150,10 @@ def test_zero2(self): atmosphere = Atmo.icao() calc = TrajectoryCalc(ammo) - sight_angle = calc.sight_angle(weapon, atmosphere) + zero_angle = calc.zero_angle(weapon, atmosphere) - self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001228, 6, - f'TestZero2 failed {sight_angle >> Angular.Radian:.10f}') + 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): @@ -197,10 +197,10 @@ def test_path_g1(self): ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS)) weapon = Weapon(Distance(2, Distance.Inch), Distance(100, Distance.Yard)) atmosphere = Atmo.icao() - shot_info = Shot(1000, 100, sight_angle=Angular(0.001228, Angular.Radian)) + shot_info = Shot(1000, 100, zero_angle=Angular(0.001228, Angular.Radian)) wind = [Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))] calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind) + data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -222,12 +222,12 @@ def test_path_g7(self): atmosphere = Atmo.icao() shot_info = Shot(Distance.Yard(1000), Distance.Yard(100), - sight_angle=Angular.MOA(4.221) + zero_angle=Angular.MOA(4.221) ) wind = [Wind(Velocity(5, Velocity.MPH), -45)] calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind) + data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -251,7 +251,7 @@ def setUp(self) -> None: self.atmo = Atmo.icao() self.shot = Shot(Distance.Yard(1000), Distance.Yard(100), - sight_angle=Angular.MOA(4.221) + zero_angle=Angular.MOA(4.221) ) self.wind = [Wind(Velocity(5, Velocity.MPH), -45)] @@ -261,7 +261,7 @@ def test__init__(self): self.assertTrue(self.calc) def test_elevation_performance(self): - self.calc.sight_angle(self.weapon, self.atmo) + self.calc.zero_angle(self.weapon, self.atmo) def test_path_performance(self): d = self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind) From 50b1fb80e6624ba3c1f6a01782a669f841e596ae Mon Sep 17 00:00:00 2001 From: dbookstaber <99043839+dbookstaber@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:34:57 -0700 Subject: [PATCH 10/15] Added look_angle parameter for uphill/downhill shots. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstration code: look_angle = Angular.Degree(30) weapon = Weapon(Distance.Inch(2), 100, look_angle, 12) calc = Calculator(weapon, ammo, zero_atmo) calc.update_elevation() print(f'Zero distance: {weapon.zero_distance >> Distance.Yard:.0f} yards; look-angle to zero: {look_angle >> Angular.Degree:.0f}°' f'\n\t==> horizontal distance to zero = {math.cos(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Yard):.2f} yards; vertical distance to zero = {math.sin(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Centimeter):.0f}cm' f'\nZero elevation: {calc.elevation >> Angular.Degree:.2f}°') --- py_ballisticcalc/munition.py | 18 ++++++++++++++++-- py_ballisticcalc/trajectory_calc.pyx | 10 +++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index d9f4f25..622445c 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -6,21 +6,35 @@ # 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 +from .unit import TypedUnits, Velocity, Temperature, is_unit, Distance, Angular __all__ = ('Weapon', 'Ammo') @dataclass class Weapon(TypedUnits): - """Creates Weapon properties""" + """ + :param zero_distance: Sight-line 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. + :param zero_look_angle: If = 0 then the target zero is horizontal with the gun at zero_distance. + If zero_look_angle != 0 then the target zero is at a different height than the gun, and + the horizontal distance to target = cos(look_angle) * zero_distance, and + the vertical distance to target from horizontal = sin(look_angle) * zero_distance. + """ sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height) zero_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance) + zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist) def __post_init__(self): if not self.twist: self.twist = 0 + if not self.zero_look_angle: + self.zero_look_angle = 0 @dataclass diff --git a/py_ballisticcalc/trajectory_calc.pyx b/py_ballisticcalc/trajectory_calc.pyx index c213235..5c2c4fe 100644 --- a/py_ballisticcalc/trajectory_calc.pyx +++ b/py_ballisticcalc/trajectory_calc.pyx @@ -130,14 +130,15 @@ cdef class TrajectoryCalc: cdef _zero_angle(TrajectoryCalc self, object ammo, object weapon, object atmo): cdef: double calc_step = self.get_calc_step(weapon.zero_distance.units(10) >> Distance.Foot) - double zero_distance = weapon.zero_distance >> Distance.Foot + double zero_distance = cos(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot) + double height_at_zero = sin(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot) double maximum_range = zero_distance + calc_step double sight_height = weapon.sight_height >> Distance.Foot double mach = atmo.mach >> Velocity.FPS double density_factor = atmo.density_factor() double muzzle_velocity = ammo.mv >> Velocity.FPS double barrel_azimuth = 0.0 - double barrel_elevation = 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) @@ -171,8 +172,8 @@ cdef class TrajectoryCalc: time += delta_range_vector.magnitude() / velocity if fabs(range_vector.x - zero_distance) < 0.5 * calc_step: - zero_finding_error = fabs(range_vector.y) - barrel_elevation -= range_vector.y / range_vector.x + zero_finding_error = fabs(range_vector.y - height_at_zero) + barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x break iterations_count += 1 @@ -184,7 +185,6 @@ cdef class TrajectoryCalc: cdef: double density_factor, mach double time, velocity, windage, delta_time, drag - double windage_adjustment, drop_adjustment, trajectory_angle double twist = weapon.twist >> Distance.Inch double length = ammo.length >> Distance.Inch From a398afc90504e0d9df618e83df0e93526cc84a11 Mon Sep 17 00:00:00 2001 From: David Bookstaber <99043839+dbookstaber@users.noreply.github.com> Date: Thu, 12 Oct 2023 01:56:37 -0600 Subject: [PATCH 11/15] Refactored angle terms and added "look angle" calculation (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Renamed sight_angle -> zero_angle, shot_angle -> relative_angle Added comments on Shot attributes Fixed unit tests * Added look_angle parameter for uphill/downhill shots. Demonstration code: look_angle = Angular.Degree(30) weapon = Weapon(Distance.Inch(2), 100, look_angle, 12) calc = Calculator(weapon, ammo, zero_atmo) calc.update_elevation() print(f'Zero distance: {weapon.zero_distance >> Distance.Yard:.0f} yards; look-angle to zero: {look_angle >> Angular.Degree:.0f}°' f'\n\t==> horizontal distance to zero = {math.cos(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Yard):.2f} yards; vertical distance to zero = {math.sin(look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Centimeter):.0f}cm' f'\nZero elevation: {calc.elevation >> Angular.Degree:.2f}°') --- py_ballisticcalc/conditions.py | 23 ++++++++++++++------- py_ballisticcalc/interface.py | 8 ++++---- py_ballisticcalc/munition.py | 18 +++++++++++++++-- py_ballisticcalc/trajectory_calc.pyx | 26 ++++++++++++------------ tests/test_all.py | 30 ++++++++++++++-------------- 5 files changed, 64 insertions(+), 41 deletions(-) diff --git a/py_ballisticcalc/conditions.py b/py_ballisticcalc/conditions.py index fadd4d6..4c1379a 100644 --- a/py_ballisticcalc/conditions.py +++ b/py_ballisticcalc/conditions.py @@ -142,17 +142,26 @@ def __post_init__(self): @dataclass class Shot(TypedUnits): - """Stores shot parameters for the trajectory calculation""" + """ + Stores shot parameters for the trajectory calculation + + :param max_range: Downrange distance to stop computing trajectory + :param step: Distance between each TrajectoryData row to record + :param zero_angle: The angle between the barrel and horizontal when zeroed + :param relative_angle: Elevation adjustment added to zero_angle for a particular shot + :param cant_angle: Rotation of gun around barrel axis, relative to position when zeroed. + (Only relevant when Weapon.sight_height != 0) + """ max_range: [float, Distance] = field(default_factory=lambda: Set.Units.distance) step: [float, Distance] = field(default_factory=lambda: Set.Units.distance) - shot_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) + zero_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) - sight_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) def __post_init__(self): - if not self.shot_angle: - self.shot_angle = 0 + if not self.relative_angle: + self.relative_angle = 0 if not self.cant_angle: self.cant_angle = 0 - if not self.sight_angle: - self.sight_angle = 0 + if not self.zero_angle: + self.zero_angle = 0 diff --git a/py_ballisticcalc/interface.py b/py_ballisticcalc/interface.py index 8b7ced1..eb0f722 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -38,7 +38,7 @@ def cdm(self): def update_elevation(self): """Recalculates barrel elevation for weapon and zero atmo""" self._calc = TrajectoryCalc(self.ammo) - self._elevation = self._calc.sight_angle(self.weapon, self.zero_atmo) + self._elevation = self._calc.zero_angle(self.weapon, self.zero_atmo) def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: """Calculates trajectory with current conditions @@ -48,9 +48,9 @@ def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: :return: trajectory table """ self._calc = TrajectoryCalc(self.ammo) - if not self._elevation and not shot.sight_angle: + if not self._elevation and not shot.zero_angle: self.update_elevation() - shot.sight_angle = self._elevation + shot.zero_angle = self._elevation data = self._calc.trajectory(self.weapon, current_atmo, shot, winds) return data @@ -120,7 +120,7 @@ def show_plot(self, shot, winds): [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 + #sh = self.weapon.sight_height >> Set.Units.drop # # scope line x_values = [0, df.distance.max()] # Adjust these as needed diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index d9f4f25..622445c 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -6,21 +6,35 @@ # 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 +from .unit import TypedUnits, Velocity, Temperature, is_unit, Distance, Angular __all__ = ('Weapon', 'Ammo') @dataclass class Weapon(TypedUnits): - """Creates Weapon properties""" + """ + :param zero_distance: Sight-line 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. + :param zero_look_angle: If = 0 then the target zero is horizontal with the gun at zero_distance. + If zero_look_angle != 0 then the target zero is at a different height than the gun, and + the horizontal distance to target = cos(look_angle) * zero_distance, and + the vertical distance to target from horizontal = sin(look_angle) * zero_distance. + """ sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height) zero_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance) + zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist) def __post_init__(self): if not self.twist: self.twist = 0 + if not self.zero_look_angle: + self.zero_look_angle = 0 @dataclass diff --git a/py_ballisticcalc/trajectory_calc.pyx b/py_ballisticcalc/trajectory_calc.pyx index 4b5f2bf..5c2c4fe 100644 --- a/py_ballisticcalc/trajectory_calc.pyx +++ b/py_ballisticcalc/trajectory_calc.pyx @@ -119,25 +119,26 @@ cdef class TrajectoryCalc: return step - def sight_angle(self, weapon: Weapon, atmo: Atmo): - return self._sight_angle(self.ammo, weapon, atmo) + def zero_angle(self, weapon: Weapon, atmo: Atmo): + return self._zero_angle(self.ammo, weapon, atmo) def trajectory(self, weapon: Weapon, atmo: Atmo, shot_info: Shot, winds: list[Wind], 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 _zero_angle(TrajectoryCalc self, object ammo, object weapon, object atmo): cdef: double calc_step = self.get_calc_step(weapon.zero_distance.units(10) >> Distance.Foot) - double zero_distance = weapon.zero_distance >> Distance.Foot + double zero_distance = cos(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot) + double height_at_zero = sin(weapon.zero_look_angle >> Angular.Radian) * (weapon.zero_distance >> Distance.Foot) double maximum_range = zero_distance + calc_step double sight_height = weapon.sight_height >> Distance.Foot double mach = atmo.mach >> Velocity.FPS double density_factor = atmo.density_factor() double muzzle_velocity = ammo.mv >> Velocity.FPS double barrel_azimuth = 0.0 - double barrel_elevation = 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) @@ -171,8 +172,8 @@ cdef class TrajectoryCalc: time += delta_range_vector.magnitude() / velocity if fabs(range_vector.x - zero_distance) < 0.5 * calc_step: - zero_finding_error = fabs(range_vector.y) - barrel_elevation -= range_vector.y / range_vector.x + zero_finding_error = fabs(range_vector.y - height_at_zero) + barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x break iterations_count += 1 @@ -184,7 +185,6 @@ cdef class TrajectoryCalc: cdef: double density_factor, mach double time, velocity, windage, delta_time, drag - double windage_adjustment, drop_adjustment, trajectory_angle double twist = weapon.twist >> Distance.Inch double length = ammo.length >> Distance.Inch @@ -194,7 +194,7 @@ cdef class TrajectoryCalc: double step = shot_info.step >> Distance.Foot double calc_step = self.get_calc_step(step) - double maximum_range = (shot_info.max_range >> Distance.Foot) + step + double maximum_range = (shot_info.max_range >> Distance.Foot) + 1 int ranges_length = int(maximum_range / step) + 1 int len_winds = len(winds) @@ -203,8 +203,8 @@ cdef class TrajectoryCalc: double stability_coefficient = 1.0 double next_wind_range = 1e7 - double barrel_elevation = (shot_info.sight_angle >> Angular.Radian) + ( - shot_info.shot_angle >> Angular.Radian) + double barrel_elevation = (shot_info.zero_angle >> Angular.Radian) + ( + shot_info.relative_angle >> Angular.Radian) double alt0 = atmo.altitude >> Distance.Foot double sight_height = weapon.sight_height >> Distance.Foot @@ -354,8 +354,8 @@ cdef double calculate_stability_coefficient(object ammo, object rifle, object at cdef Vector wind_to_vector(object shot, object wind): cdef: - double sight_cosine = cos(shot.sight_angle >> Angular.Radian) - double sight_sine = sin(shot.sight_angle >> Angular.Radian) + double sight_cosine = cos(shot.zero_angle >> Angular.Radian) + double sight_sine = sin(shot.zero_angle >> Angular.Radian) double cant_cosine = cos(shot.cant_angle >> Angular.Radian) double cant_sine = sin(shot.cant_angle >> Angular.Radian) double range_velocity = (wind.velocity >> Velocity.FPS) * cos(wind.direction_from >> Angular.Radian) diff --git a/tests/test_all.py b/tests/test_all.py index b54a715..29246f1 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -94,7 +94,7 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh): try: calc.update_elevation() - shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) zero_crossing_points = calc.zero_given_elevation(shot) print_output(zero_crossing_points, calc.elevation) except ArithmeticError as err: @@ -103,7 +103,7 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh, elev=0): try: - shot = Shot(1000, Distance.Foot(0.2), sight_angle=0) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=0) zero_crossing_points = calc.zero_given_elevation(shot) print_output(zero_crossing_points, 0) except ArithmeticError as err: @@ -116,7 +116,7 @@ def test_danger_space(self): weapon = Weapon(Distance.Inch(4), zero_distance, 11.24) calc = Calculator(weapon, self.ammo, self.atmosphere) calc.update_elevation() - shot = Shot(1000, Distance.Foot(0.2), sight_angle=calc.elevation) + shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) zero_given_elevation = calc.zero_given_elevation(shot) if len(zero_given_elevation) > 0: zero = [p for p in zero_given_elevation if abs( @@ -138,10 +138,10 @@ def test_zero1(self): atmosphere = Atmo.icao() calc = TrajectoryCalc(ammo) - sight_angle = calc.sight_angle(weapon, atmosphere) + zero_angle = calc.zero_angle(weapon, atmosphere) - self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001651, 6, - f'TestZero1 failed {sight_angle >> Angular.Radian:.10f}') + 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) @@ -150,10 +150,10 @@ def test_zero2(self): atmosphere = Atmo.icao() calc = TrajectoryCalc(ammo) - sight_angle = calc.sight_angle(weapon, atmosphere) + zero_angle = calc.zero_angle(weapon, atmosphere) - self.assertAlmostEqual(sight_angle >> Angular.Radian, 0.001228, 6, - f'TestZero2 failed {sight_angle >> Angular.Radian:.10f}') + 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): @@ -197,10 +197,10 @@ def test_path_g1(self): ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS)) weapon = Weapon(Distance(2, Distance.Inch), Distance(100, Distance.Yard)) atmosphere = Atmo.icao() - shot_info = Shot(1000, 100, sight_angle=Angular(0.001228, Angular.Radian)) + shot_info = Shot(1000, 100, zero_angle=Angular(0.001228, Angular.Radian)) wind = [Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))] calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind) + data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -222,12 +222,12 @@ def test_path_g7(self): atmosphere = Atmo.icao() shot_info = Shot(Distance.Yard(1000), Distance.Yard(100), - sight_angle=Angular.MOA(4.221) + zero_angle=Angular.MOA(4.221) ) wind = [Wind(Velocity(5, Velocity.MPH), -45)] calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind) + data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -251,7 +251,7 @@ def setUp(self) -> None: self.atmo = Atmo.icao() self.shot = Shot(Distance.Yard(1000), Distance.Yard(100), - sight_angle=Angular.MOA(4.221) + zero_angle=Angular.MOA(4.221) ) self.wind = [Wind(Velocity(5, Velocity.MPH), -45)] @@ -261,7 +261,7 @@ def test__init__(self): self.assertTrue(self.calc) def test_elevation_performance(self): - self.calc.sight_angle(self.weapon, self.atmo) + self.calc.zero_angle(self.weapon, self.atmo) def test_path_performance(self): d = self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind) From 9c4372a16a516ed551eeffd3c7647901b2e8df47 Mon Sep 17 00:00:00 2001 From: Murphy Date: Thu, 12 Oct 2023 17:18:24 +0300 Subject: [PATCH 12/15] * fix: ordering weapon attributes --- py_ballisticcalc/__init__.py | 2 +- py_ballisticcalc/interface.py | 12 +++--------- py_ballisticcalc/munition.py | 2 +- py_ballisticcalc/trajectory_data.py | 3 +-- tests/{test_plot.py.bak => plot.py} | 7 ++++--- 5 files changed, 10 insertions(+), 16 deletions(-) rename tests/{test_plot.py.bak => plot.py} (63%) diff --git a/py_ballisticcalc/__init__.py b/py_ballisticcalc/__init__.py index 5f22da4..5ccd8f5 100644 --- a/py_ballisticcalc/__init__.py +++ b/py_ballisticcalc/__init__.py @@ -4,7 +4,7 @@ __copyright__ = ("",) __credits__ = ["o-murphy"] -__version__ = "1.1.0b7" +__version__ = "1.1.0b8" 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 eb0f722..e5b62b5 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -96,20 +96,17 @@ def to_dataframe(trajectory: list[TrajectoryData]): trajectory = [p.in_def_units() for p in trajectory] return pd.DataFrame(trajectory, columns=col_names) - def show_plot(self, shot, winds): + def show_plot(self, shot, current_atmo, 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, + data = self._calc.trajectory(self.weapon, current_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: @@ -120,11 +117,8 @@ def show_plot(self, shot, winds): [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") diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index 622445c..bba77ea 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -27,8 +27,8 @@ class Weapon(TypedUnits): """ sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height) zero_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance) - zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist) + zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) def __post_init__(self): if not self.twist: diff --git a/py_ballisticcalc/trajectory_data.py b/py_ballisticcalc/trajectory_data.py index 30bebf3..694840d 100644 --- a/py_ballisticcalc/trajectory_data.py +++ b/py_ballisticcalc/trajectory_data.py @@ -94,6 +94,5 @@ def in_def_units(self) -> tuple: self.angle >> Set.Units.angular, self.energy >> Set.Units.energy, self.ogw >> Set.Units.ogw, - - self.flag + TrajFlag(self.flag) ) diff --git a/tests/test_plot.py.bak b/tests/plot.py similarity index 63% rename from tests/test_plot.py.bak rename to tests/plot.py index 033354a..b986f34 100644 --- a/tests/test_plot.py.bak +++ b/tests/plot.py @@ -6,10 +6,11 @@ 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) +weapon = Weapon(4, 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)) +atmo = Atmo.icao() -calc.show_plot(shot, [Wind()]) +shot = Shot(1200, Distance.Foot(0.2), zero_angle=calc.elevation, relative_angle=Angular.MOA(0)) +calc.show_plot(shot, atmo, [Wind()]) From 62bc7dbc141b9365d8bf2fc4f976c2566d3fd434 Mon Sep 17 00:00:00 2001 From: Murphy Date: Thu, 12 Oct 2023 17:28:13 +0300 Subject: [PATCH 13/15] * fix: ordering weapon attributes --- py_ballisticcalc/munition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index fe501fe..bba77ea 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -27,7 +27,6 @@ class Weapon(TypedUnits): """ sight_height: [float, Distance] = field(default_factory=lambda: Set.Units.sight_height) zero_distance: [float, Distance] = field(default_factory=lambda: Set.Units.distance) - zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) twist: [float, Distance] = field(default_factory=lambda: Set.Units.twist) zero_look_angle: [float, Angular] = field(default_factory=lambda: Set.Units.angular) From 8052649c313206e0d3871718286f7860dd3128b1 Mon Sep 17 00:00:00 2001 From: Dmytro Yaroshenko <73843436+o-murphy@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:35:06 +0300 Subject: [PATCH 14/15] Interface refactoring * added ShotTrajectory class for shot results * conditions refactored to shot attributes * updated Unit's __call__ method * removed is_unit function * trajectory step removed from the shot attributes, now it required for the Calculator.fire() * updated README.md --- README.md | 86 +++-- py_ballisticcalc/conditions.py | 14 +- py_ballisticcalc/drag_model.pyx | 4 +- py_ballisticcalc/example.py | 13 +- py_ballisticcalc/interface.py | 144 +++----- py_ballisticcalc/multiple_bc.py | 6 +- py_ballisticcalc/munition.py | 11 +- py_ballisticcalc/settings.py | 9 +- py_ballisticcalc/trajectory_calc.py.bak | 435 ++++++++++++++++++++++++ py_ballisticcalc/trajectory_calc.pyx | 39 ++- py_ballisticcalc/trajectory_data.py | 144 +++++++- py_ballisticcalc/unit.py | 47 +-- tests/plot.py | 11 +- tests/test_all.py | 50 +-- 14 files changed, 781 insertions(+), 232 deletions(-) create mode 100644 py_ballisticcalc/trajectory_calc.py.bak diff --git a/README.md b/README.md index 09d1030..f5a8638 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,50 @@ # BallisticCalculator -#### LGPL library for small arms ballistic calculations (Python 3.9+) - -## Table of contents -* [Installation](#installation) -* [Usage](#usage) +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) -* [Contributors](#contributors) -* [About project](#about-project) - -## Installation -**Stable release from pypi, installing from binaries** +* **[Older versions]()** + * [v1.0.x](https://github.com/o-murphy/py_ballisticcalc/tree/v1.0.12) +* **[Contributors](#contributors)** +* **[About project](#about-project)** -(Contains c-extensions which offer higher performance) +### Installation +#### Latest stable release from pypi** ```shell pip install py-ballisticcalc ``` - -**Build wheel package for your interpreter version by pypi sdist** - -Download and install MSVC or GCC depending on target platform +#### 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 -pip install Cython>=3.0.0a10 +# no binary from PyPi pip install py-ballisticcalc --no-binary :all: -``` -**Also use `git clone` to build your own package** +# master brunch +pip install git+https://github.com/o-murphy/py_ballisticcalc -(Contains cython files to build your own c-extensions) +# 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 @@ -79,19 +91,21 @@ Distance.Meter(100) > 10 # >>> True, compare unit with float by raw value #### An example of calculations ```python -from py_ballisticcalc import * +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.Mach = Velocity.FPS +Set.Units.velocity = Velocity.FPS Set.Units.temperature = Temperature.Celsius -Set.Units.distance = Distance.Meter +# Set.Units.distance = Distance.Meter Set.Units.sight_height = Distance.Centimeter -# set maximum inner Calculator step size, larger is faster, but accuracy is going down -Set.set_max_calc_step_size(Distance.Foot(1)) # same as default -# enable muzzle velocity correction by powder temperature -Set.USE_POWDER_SENSITIVITY = True # default False +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 @@ -104,21 +118,19 @@ dm = DragModel(0.223, TableG7, weight, diameter) ammo = Ammo(dm, length, 2750, 15) ammo.calc_powder_sens(2723, 0) -zero_atmo = Atmo.icao() +zero_atmo = Atmo.icao(100) # defining calculator instance calc = Calculator(weapon, ammo, zero_atmo) -calc.update_elevation() - -shot = Shot(1500, 100) -current_atmo = Atmo(100, 1000, 15, 72) -winds = [Wind(2, Angular.OClock(3))] +current_atmo = Atmo(110, 1000, 15, 72) +current_winds = [Wind(2, 90)] +shot = Shot(1500, atmo=current_atmo, winds=current_winds) -data = calc.trajectory(shot, current_atmo, winds) +shot_result = calc.fire(shot, trajectory_step=Distance.Yard(100)) -for p in data: - print(p.formatted()) +for p in shot_result: + print(p.formatted()) ``` #### Example of the formatted output: ```shell diff --git a/py_ballisticcalc/conditions.py b/py_ballisticcalc/conditions.py index 4c1379a..280dcf6 100644 --- a/py_ballisticcalc/conditions.py +++ b/py_ballisticcalc/conditions.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from .settings import Settings as Set -from .unit import Distance, Velocity, Temperature, Pressure, is_unit, TypedUnits, Angular +from .unit import Distance, Velocity, Temperature, Pressure, TypedUnits, Angular __all__ = ('Atmo', 'Wind', 'Shot') @@ -61,7 +61,7 @@ def __post_init__(self): @staticmethod def icao(altitude: [float, Distance] = 0): """Creates Atmosphere with ICAO values""" - altitude = altitude if is_unit(altitude) else Distance(altitude, Set.Units.distance) + altitude = Set.Units.distance(altitude) temperature = Temperature.Fahrenheit( cIcaoStandardTemperatureR + (altitude >> Distance.Foot) * cTemperatureGradient - cIcaoFreezingPointTemperatureR @@ -153,15 +153,23 @@ class Shot(TypedUnits): (Only relevant when Weapon.sight_height != 0) """ max_range: [float, Distance] = field(default_factory=lambda: Set.Units.distance) - step: [float, Distance] = field(default_factory=lambda: Set.Units.distance) zero_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) + atmo: Atmo = field(default=None) + winds: list[Wind] = field(default=None) + def __post_init__(self): + if not self.relative_angle: self.relative_angle = 0 if not self.cant_angle: self.cant_angle = 0 if not self.zero_angle: self.zero_angle = 0 + + if not self.atmo: + self.atmo = Atmo.icao() + if not self.winds: + self.winds = [Wind()] diff --git a/py_ballisticcalc/drag_model.pyx b/py_ballisticcalc/drag_model.pyx index 87b728c..60a6e98 100644 --- a/py_ballisticcalc/drag_model.pyx +++ b/py_ballisticcalc/drag_model.pyx @@ -3,7 +3,7 @@ import typing from libc.math cimport pow from .settings import Settings as Set -from .unit import Weight, Distance, is_unit +from .unit import Weight, Distance from .drag_tables import DragTablesSet __all__ = ('DragModel', ) @@ -63,7 +63,7 @@ cdef class DragModel: else: raise ValueError('Wrong drag data') - self.weight = weight if is_unit(weight) else Set.Units.weight(weight) + self.weight = Set.Units.weight(weight) self.diameter = Set.Units.diameter(diameter) self.sectional_density = self._get_sectional_density() self.form_factor = self._get_form_factor(self.value) diff --git a/py_ballisticcalc/example.py b/py_ballisticcalc/example.py index cf9fd80..1e5696e 100644 --- a/py_ballisticcalc/example.py +++ b/py_ballisticcalc/example.py @@ -42,17 +42,12 @@ # defining calculator instance calc = Calculator(weapon, ammo, zero_atmo) -calc.update_elevation() - -shot = Shot(1500, 100) -print(shot.max_range) -print(zero_atmo.temperature) current_atmo = Atmo(110, 1000, 15, 72) -winds = [Wind(2, 90)] -print(weapon.sight_height) +current_winds = [Wind(2, 90)] +shot = Shot(1500, atmo=current_atmo, winds=current_winds) -data = calc.trajectory(shot, current_atmo, winds) +shot_result = calc.fire(shot, Distance.Yard(100)) -for p in data: +for p in shot_result: print(p.formatted()) diff --git a/py_ballisticcalc/interface.py b/py_ballisticcalc/interface.py index e5b62b5..28d79de 100644 --- a/py_ballisticcalc/interface.py +++ b/py_ballisticcalc/interface.py @@ -1,14 +1,13 @@ """Implements basic interface for the ballistics calculator""" -import math from dataclasses import dataclass, field -from .conditions import Atmo, Wind, Shot +from .conditions import Atmo, Shot from .munition import Weapon, Ammo -from .settings import Settings as Set # 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 +from .trajectory_data import HitResult +from .unit import Angular, Distance +from .settings import Settings __all__ = ('Calculator',) @@ -19,12 +18,14 @@ class Calculator: weapon: Weapon ammo: Ammo - zero_atmo: Atmo - + zero_atmo: Atmo = field(default_factory=Atmo.icao) _elevation: Angular = field(init=False, repr=True, compare=False, default_factory=lambda: Angular.Degree(0)) _calc: TrajectoryCalc = field(init=False, repr=True, compare=False, default=None) + def __post_init__(self): + self.calculate_elevation() + @property def elevation(self): """get current barrel elevation""" @@ -35,112 +36,43 @@ def cdm(self): """returns custom drag function based on input data""" return self._calc.cdm - def update_elevation(self): + def calculate_elevation(self): """Recalculates barrel elevation for weapon and zero atmo""" self._calc = TrajectoryCalc(self.ammo) self._elevation = self._calc.zero_angle(self.weapon, self.zero_atmo) - def trajectory(self, shot: Shot, current_atmo: Atmo, winds: list[Wind]) -> list: + def fire(self, shot: Shot, trajectory_step: [float, Distance], + extra_data: bool = False) -> HitResult: """Calculates trajectory with current conditions :param shot: shot parameters - :param current_atmo: current atmosphere conditions - :param winds: current winds list + :param trajectory_step: step between trajectory points + :param filter_flags: filter trajectory points :return: trajectory table """ + step = Settings.Units.distance(trajectory_step) self._calc = TrajectoryCalc(self.ammo) - if not self._elevation and not shot.zero_angle: - self.update_elevation() + if not shot.zero_angle: shot.zero_angle = self._elevation - data = self._calc.trajectory(self.weapon, current_atmo, shot, winds) - return data - - 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()] - - 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: - """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.) - - :param trajectory: single point from trajectory table - :param target_height: error range for the target - :return: danger space for target_height specified - """ - - target_height = (target_height if is_unit(target_height) - 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, current_atmo, winds): - import matplotlib - import matplotlib.pyplot as plt - - matplotlib.use('TkAgg') - self._calc = TrajectoryCalc(self.ammo) - data = self._calc.trajectory(self.weapon, current_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) - - 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") - - # # scope line - x_values = [0, df.distance.max()] # 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() + data = self._calc.trajectory(self.weapon, shot, step, extra_data) + return HitResult(data, extra_data) + + # @staticmethod + # def danger_space(trajectory: TrajectoryData, target_height: [float, Distance]) -> Distance: + # """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.) + # + # :param trajectory: single point from trajectory table + # :param target_height: error range for the target + # :return: danger space for target_height specified + # """ + # + # target_height = (target_height if is_unit(target_height) + # 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)) diff --git a/py_ballisticcalc/multiple_bc.py b/py_ballisticcalc/multiple_bc.py index 1ba2df3..be19ba6 100644 --- a/py_ballisticcalc/multiple_bc.py +++ b/py_ballisticcalc/multiple_bc.py @@ -7,7 +7,7 @@ # 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 +from .unit import Distance, Weight, Velocity __all__ = ('MultiBC', ) @@ -53,7 +53,9 @@ def __init__(self, drag_table: Iterable[dict], diameter: Distance, weight: Weigh def _parse_mbc(self, mbc_table): table = [] for p in mbc_table: - v = (p['V'] if is_unit(p['V']) else Set.Units.velocity(p['V'])) >> Velocity.MPS + 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) diff --git a/py_ballisticcalc/munition.py b/py_ballisticcalc/munition.py index bba77ea..1d179f4 100644 --- a/py_ballisticcalc/munition.py +++ b/py_ballisticcalc/munition.py @@ -6,7 +6,7 @@ # 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, Angular +from .unit import TypedUnits, Velocity, Temperature, Distance, Angular __all__ = ('Weapon', 'Ammo') @@ -58,10 +58,8 @@ def calc_powder_sens(self, other_velocity: [float, Velocity], # creates temperature modifier in percent at each 15C v0 = self.mv >> Velocity.MPS t0 = self.powder_temp >> Temperature.Celsius - v1 = (other_velocity if is_unit(other_velocity) - else Set.Units.velocity(other_velocity)) >> Velocity.MPS - t1 = (other_temperature if is_unit(other_temperature) - else Set.Units.temperature(other_temperature)) >> 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) @@ -85,8 +83,7 @@ def get_velocity_for_temp(self, current_temp: [float, Temperature]) -> Velocity: temp_modifier = self.temp_modifier v0 = self.mv >> Velocity.MPS t0 = self.powder_temp >> Temperature.Celsius - t1 = (current_temp if is_unit(current_temp) - else Set.Units.temperature(current_temp)) >> Temperature.Celsius + t1 = Set.Units.temperature(current_temp) >> Temperature.Celsius t_delta = t1 - t0 muzzle_velocity = temp_modifier / (15 / v0) * t_delta + v0 diff --git a/py_ballisticcalc/settings.py b/py_ballisticcalc/settings.py index ff9fdc3..f99ec99 100644 --- a/py_ballisticcalc/settings.py +++ b/py_ballisticcalc/settings.py @@ -1,6 +1,7 @@ """Global settings of the py_ballisticcalc library""" +import logging -from .unit import Unit, Distance, is_unit +from .unit import Unit, Distance __all__ = ('Settings',) @@ -35,7 +36,9 @@ 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 " + "to 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 = (value if is_unit(value) - else cls.Units.distance(value).raw_value) >> Distance.Foot + cls._MAX_CALC_STEP_SIZE = cls.Units.distance(value) >> Distance.Foot diff --git a/py_ballisticcalc/trajectory_calc.py.bak b/py_ballisticcalc/trajectory_calc.py.bak new file mode 100644 index 0000000..7b54eca --- /dev/null +++ b/py_ballisticcalc/trajectory_calc.py.bak @@ -0,0 +1,435 @@ +from dataclasses import dataclass +from math import sqrt, fabs, pow, sin, cos, log10, floor, atan +from typing import NamedTuple + +from .conditions import * +from .munition import * +from .settings import Settings +from .trajectory_data import TrajectoryData, TrajFlag +from .unit import * + +__all__ = ('TrajectoryCalc',) + +# cZeroFindingAccuracy = 0.000005 +cZeroFindingAccuracy = 0.00000005 +cMinimumVelocity = 50.0 +cMaximumDrop = -15000 +cMaxIterations = 10 +cGravityConstant = -32.17405 + + +class CurvePoint(NamedTuple): + a: float + b: float + c: float + + +@dataclass +class Vector: + x: float + y: float + z: float + + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def magnitude(self): + return sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + + def mul_by_const(self, a): + return Vector(self.x * a, self.y * a, self.z * a) + + def mul_by_vector(self, b): + return self.x * b.x + self.y * b.y + self.z * b.z + + def add(self, b): + return Vector(self.x + b.x, self.y + b.y, self.z + b.z) + + def subtract(self, b): + 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 fabs(m) < 1e-10: + return Vector(self.x, self.y, self.z) + return self.mul_by_const(1.0 / m) + + def __add__(self, other): + return self.add(other) + + def __radd__(self, other): + return self.add(other) + + def __iadd__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.subtract(other) + + def __rsub__(self, other): + return self.subtract(other) + + def __isub__(self, other): + return self.subtract(other) + + def __mul__(self, other): + if isinstance(other, (int, float)): + return self.mul_by_const(other) + elif isinstance(other, Vector): + return self.mul_by_vector(other) + raise TypeError(other) + + def __rmul__(self, other): + return self.__mul__(other) + + def __imul__(self, other): + return self.__mul__(other) + + def __neg__(self): + return self.negate() + + +class TrajectoryCalc: + + 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) + + @staticmethod + def get_calc_step(step): + 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 sight_angle(self, weapon: Weapon, atmo: Atmo): + return self._sight_angle(self.ammo, weapon, atmo) + + def trajectory(self, weapon: Weapon, atmo: Atmo, + shot_info: Shot, winds: list[Wind], + filter_flags: TrajFlag = TrajFlag.RANGE): + return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, filter_flags) + + def _sight_angle(self, ammo, weapon, atmo): + calc_step = self.get_calc_step(weapon.zero_distance.units(10) >> Distance.Foot) + zero_distance = weapon.zero_distance >> Distance.Foot + maximum_range = zero_distance + calc_step + sight_height = weapon.sight_height >> Distance.Foot + mach = atmo.mach >> Velocity.FPS + density_factor = atmo.density_factor() + muzzle_velocity = ammo.mv >> Velocity.FPS + barrel_azimuth = 0.0 + barrel_elevation = 0.0 + 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, -sight_height, .0) + 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_match(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) + barrel_elevation -= range_vector.y / range_vector.x + break + + iterations_count += 1 + + return Angular.Radian(barrel_elevation) + + def _trajectory(self, ammo, weapon, atmo, + shot_info, winds, filter_flags): + + twist = 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 + maximum_range = (shot_info.max_range >> Distance.Foot) + 1 + calc_step = self.get_calc_step(step) + + ranges_length = int(floor(maximum_range / step)) + 1 + len_winds = len(winds) + current_item = 0 + current_wind = 0 + twist_coefficient = 0 + + stability_coefficient = 1.0 + next_wind_range = 1e7 + + barrel_elevation = (shot_info.sight_angle >> Angular.Radian) + ( + shot_info.shot_angle >> Angular.Radian) + alt0 = atmo.altitude >> Distance.Foot + sight_height = weapon.sight_height >> Distance.Foot + + next_range_distance = .0 + barrel_azimuth = .0 + + gravity_vector = Vector(.0, cGravityConstant, .0) + range_vector = Vector(.0, -sight_height, .0) + + ranges = [] + + previous_y = 0 + previous_mach = 0 + # _flag + + time = 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(shot_info, 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(ammo, weapon, atmo) + twist_coefficient = -1 if twist > 0 else 1 + + 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(shot_info, 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 check + if range_vector.y < 0 < previous_y: + _flag |= TrajFlag.ZERO + + # 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) * 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_y = range_vector.y + 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_match(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_match(self, mach): + 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 * ff + cd = st_cd * bc + cdm.append({'CD': cd, 'Mach': st_mach}) + + return cdm + + +def calculate_stability_coefficient(ammo, rifle, atmo): + weight = ammo.dm.weight >> Weight.Grain + diameter = ammo.dm.diameter >> Distance.Inch + twist = fabs(rifle.twist >> 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 / (pow(twist, 2) * pow(diameter, 3) * length * (1 + pow(length, 2))) + fv = pow(mv / 2800, 1.0 / 3.0) + ftp = ((ft + 460) / (59 + 460)) * (29.92 / pt) + return sd * fv * ftp + + +def wind_to_vector(shot, wind): + sight_cosine = cos(shot.sight_angle >> Angular.Radian) + sight_sine = sin(shot.sight_angle >> Angular.Radian) + cant_cosine = cos(shot.cant_angle >> Angular.Radian) + cant_sine = sin(shot.cant_angle >> Angular.Radian) + range_velocity = (wind.velocity >> Velocity.FPS) * cos(wind.direction_from >> Angular.Radian) + cross_component = (wind.velocity >> Velocity.FPS) * sin(wind.direction_from >> Angular.Radian) + 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) + + +def create_trajectory_row(time, range_vector, velocity_vector, + velocity, mach, windage, weight, flag): + drop_adjustment = get_correction(range_vector.x, range_vector.y) + windage_adjustment = get_correction(range_vector.x, windage) + 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 + ) + + +def get_correction(distance, offset): + if distance != 0: + return atan(offset / distance) + return 0 # better None + + +def calculate_energy(bullet_weight, velocity): + return bullet_weight * pow(velocity, 2) / 450400 + + +def calculate_ogv(bullet_weight, velocity): + return pow(bullet_weight, 2) * pow(velocity, 3) * 1.5e-12 + + +def calculate_curve(data_points): + 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, curve, mach): + 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/trajectory_calc.pyx b/py_ballisticcalc/trajectory_calc.pyx index 5c2c4fe..8e0c88e 100644 --- a/py_ballisticcalc/trajectory_calc.pyx +++ b/py_ballisticcalc/trajectory_calc.pyx @@ -4,7 +4,7 @@ cimport cython from .conditions import * from .munition import * from .settings import Settings -from .trajectory_data import TrajectoryData +from .trajectory_data import TrajectoryData, TrajFlag from .unit import * __all__ = ('TrajectoryCalc',) @@ -24,6 +24,9 @@ cdef enum CTrajFlag: 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 @@ -94,10 +97,12 @@ cdef class Vector: return self.negate() cdef class TrajectoryCalc: - cdef object ammo - cdef list _curve - cdef list _table_data - cdef double _bc + cdef: + object ammo + double step + list _curve + list _table_data + double _bc def __init__(self, ammo: Ammo): self.ammo = ammo @@ -122,10 +127,19 @@ cdef class TrajectoryCalc: def zero_angle(self, weapon: Weapon, atmo: Atmo): return self._zero_angle(self.ammo, weapon, atmo) - def trajectory(self, weapon: Weapon, atmo: Atmo, - shot_info: Shot, winds: list[Wind], - filter_flags: int = CTrajFlag.RANGE): - return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, filter_flags) + def trajectory(self, weapon: Weapon, shot_info: Shot, step: [float, Distance], + extra_data: bool = False): + cdef: + object dist_step = Settings.Units.distance(step) + object atmo = shot_info.atmo + list winds = shot_info.winds + CTrajFlag filter_flags = CTrajFlag.RANGE + + if extra_data: + print('ext', extra_data) + dist_step = Distance.Foot(0.2) + filter_flags = CTrajFlag.ALL + return self._trajectory(self.ammo, weapon, atmo, shot_info, winds, dist_step, filter_flags) cdef _zero_angle(TrajectoryCalc self, object ammo, object weapon, object atmo): cdef: @@ -176,12 +190,12 @@ cdef class TrajectoryCalc: barrel_elevation -= (range_vector.y - height_at_zero) / range_vector.x break - iterations_count += 1 + iterations_count += 1 return Angular.Radian(barrel_elevation) cdef _trajectory(TrajectoryCalc self, object ammo, object weapon, object atmo, - object shot_info, list[object] winds, CTrajFlag filter_flags): + object shot_info, list[object] winds, object dist_step, CTrajFlag filter_flags): cdef: double density_factor, mach double time, velocity, windage, delta_time, drag @@ -191,7 +205,8 @@ cdef class TrajectoryCalc: double diameter = ammo.dm.diameter >> Distance.Inch double weight = ammo.dm.weight >> Weight.Grain - double step = shot_info.step >> Distance.Foot + # double step = shot_info.step >> Distance.Foot + double step = dist_step >> Distance.Foot double calc_step = self.get_calc_step(step) double maximum_range = (shot_info.max_range >> Distance.Foot) + 1 diff --git a/py_ballisticcalc/trajectory_data.py b/py_ballisticcalc/trajectory_data.py index 694840d..40a6fc5 100644 --- a/py_ballisticcalc/trajectory_data.py +++ b/py_ballisticcalc/trajectory_data.py @@ -1,11 +1,28 @@ """Implements a point of trajectory class in applicable data types""" +import logging +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 -__all__ = ('TrajectoryData', 'TrajFlag') +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 pyplot as plt +except ImportError as error: + logging.warning("Install matplotlib to get results as a plot") + matplotlib = None + +__all__ = ('TrajectoryData', 'HitResult', 'TrajFlag', + # 'trajectory_plot', 'trajectory_dataframe' + ) class TrajFlag(Flag): @@ -17,8 +34,9 @@ class TrajFlag(Flag): ZERO_DOWN = 2 MACH = 4 RANGE = 8 + DANGER = 16 ZERO = ZERO_UP | ZERO_DOWN - ALL = RANGE | ZERO_UP | ZERO_DOWN | MACH + ALL = RANGE | ZERO_UP | ZERO_DOWN | MACH | DANGER class TrajectoryData(NamedTuple): @@ -58,6 +76,7 @@ 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}" @@ -96,3 +115,124 @@ def in_def_units(self) -> tuple: self.ogw >> Set.Units.ogw, TrajFlag(self.flag) ) + + +@dataclass(frozen=True) +class HitResult: + """Results of the 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 found zero crossing points") + return data + + @property + def dataframe(self): + """:return: the trajectory table as a DataFrame""" + if pd is None: + raise ImportError("Install pandas to convert trajectory as dataframe") + self.__check_extra__() + col_names = list(TrajectoryData._fields) + trajectory = [p.in_def_units() for p in self] + return pd.DataFrame(trajectory, columns=col_names) + + @property + def plot(self): + """:return: the graph of the trajectory""" + + if matplotlib is None: + raise ImportError("Install matplotlib to get results as a plot") + if not self.extra: + logging.warning("HitResult.plot: To show extended data" + "Use Calculator.fire(..., extra_data=True)") + matplotlib.use('TkAgg') + + df = self.dataframe + ax = df.plot(x='distance', y=['drop'], ylabel=Set.Units.drop.symbol) + + for p in self: + + 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") + + # # scope line + x_values = [0, df.distance.max()] # 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") + + 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) + + return plt + + +# def trajectory_dataframe(shot_result: 'HitResult') -> 'DataFrame': +# """:return: the trajectory table as a DataFrame""" +# if pd is None: +# raise ImportError("Install pandas to convert trajectory as dataframe") +# +# col_names = TrajectoryData._fields +# trajectory = [p.in_def_units() for p in shot_result] +# return pd.DataFrame(trajectory, columns=col_names) + + +# def trajectory_plot(calc: 'Calculator', shot: 'Shot') -> 'plot': +# """:return: the graph of the trajectory""" +# +# if matplotlib is None: +# raise ImportError("Install matplotlib to get results as a plot") +# +# matplotlib.use('TkAgg') +# shot_result = calc.fire(shot, Distance.Foot(0.2), True) +# df = trajectory_dataframe(shot_result) +# ax = df.plot(x='distance', y=['drop'], ylabel=Set.Units.drop.symbol) +# +# for p in shot_result: +# +# 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") +# +# # # scope line +# x_values = [0, df.distance.max()] # 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") +# +# 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) +# +# return plt diff --git a/py_ballisticcalc/unit.py b/py_ballisticcalc/unit.py index 7222bd9..61e5e74 100644 --- a/py_ballisticcalc/unit.py +++ b/py_ballisticcalc/unit.py @@ -5,11 +5,14 @@ import typing from enum import IntEnum from math import pi, atan, tan -from typing import NamedTuple, Callable +from typing import NamedTuple +from dataclasses import dataclass __all__ = ('Unit', 'AbstractUnit', 'UnitPropsDict', 'Distance', 'Velocity', 'Angular', 'Temperature', 'Pressure', - 'Energy', 'Weight', 'is_unit', 'TypedUnits') + 'Energy', 'Weight', + # 'is_unit', + 'TypedUnits') class Unit(IntEnum): @@ -33,7 +36,7 @@ class Unit(IntEnum): NAUTICAL_MILE = 14 MILLIMETER = 15 CENTIMETER = 16 - METER: Callable = 17 + METER = 17 KILOMETER = 18 LINE = 19 @@ -88,12 +91,14 @@ def symbol(self): """ return UnitPropsDict[self].symbol - def __call__(self: 'Unit', value: [int, float]) -> 'AbstractUnit': + 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: @@ -129,6 +134,7 @@ class UnitProps(NamedTuple): Unit.THOUSAND: UnitProps('thousand', 2, 'ths'), Unit.INCHES_PER_100YD: UnitProps('inches/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"), @@ -588,6 +594,7 @@ def from_raw(self, value: float, units: Unit): Joule = Unit.JOULE +@dataclass class TypedUnits: # pylint: disable=too-few-public-methods """ Abstract class to apply auto-conversion values to @@ -599,10 +606,10 @@ def __setattr__(self, key, value): converts value to specified units by type-hints in inherited dataclass """ - fields = self.__getattribute__('__dataclass_fields__') - - if key in fields and not isinstance(value, AbstractUnit): - default_factory = fields[key].default_factory + _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 @@ -612,18 +619,18 @@ def __setattr__(self, key, 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__}") +# 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__}") # Default units diff --git a/tests/plot.py b/tests/plot.py index b986f34..fb83770 100644 --- a/tests/plot.py +++ b/tests/plot.py @@ -5,12 +5,11 @@ dm = DragModel(0.22, TableG7, 168, 0.308) ammo = Ammo(dm, 1.22, Velocity(2600, Velocity.FPS)) -atmosphere = Atmo.icao() weapon = Weapon(4, 100, 11.24) -calc = Calculator(weapon, ammo, atmosphere) -calc.update_elevation() -atmo = Atmo.icao() +calc = Calculator(weapon, ammo) +calc.calculate_elevation() + +shot = Shot(1200, zero_angle=calc.elevation, relative_angle=Angular.Mil(0)) +calc.fire(shot, 0, extra_data=True).plot.show() -shot = Shot(1200, Distance.Foot(0.2), zero_angle=calc.elevation, relative_angle=Angular.MOA(0)) -calc.show_plot(shot, atmo, [Wind()]) diff --git a/tests/test_all.py b/tests/test_all.py index 29246f1..f22fea5 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -63,6 +63,7 @@ def setUp(self) -> None: self.ammo = Ammo(dm, 1.22, Velocity(2600, Velocity.FPS)) self.atmosphere = Atmo.icao() + @unittest.skip(reason="Deprecated: zero_given_elevation") def test_zero_given(self): # pylint: disable=consider-using-f-string output_fmt = "elev: {}\tscope: {}\tzero: {} {}\ttarget: {}\tdistance: {}\tdrop: {}" @@ -93,9 +94,10 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh): try: - calc.update_elevation() - shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) - zero_crossing_points = calc.zero_given_elevation(shot) + calc.calculate_elevation() + shot = Shot(1000, zero_angle=calc.elevation) + shot_result = calc.fire(shot, Distance.Foot(0.2)) + zero_crossing_points = shot_result.zero_given_elevation() print_output(zero_crossing_points, calc.elevation) except ArithmeticError as err: if err == "Can't found zero crossing points": @@ -103,21 +105,24 @@ def print_output(data, at_elevation): with self.subTest(zero=reference_distance, sh=sh, elev=0): try: - shot = Shot(1000, Distance.Foot(0.2), zero_angle=0) - zero_crossing_points = calc.zero_given_elevation(shot) + calc.calculate_elevation() + shot = Shot(1000, zero_angle=0) + shot_result = calc.fire(shot, Distance.Foot(0.2)) + zero_crossing_points = shot_result.zero_given_elevation() 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") + @unittest.skip(reason="Not implemented: danger_space") def test_danger_space(self): zero_distance = Distance.Yard(100) weapon = Weapon(Distance.Inch(4), zero_distance, 11.24) calc = Calculator(weapon, self.ammo, self.atmosphere) - calc.update_elevation() + calc.calculate_elevation() shot = Shot(1000, Distance.Foot(0.2), zero_angle=calc.elevation) - zero_given_elevation = calc.zero_given_elevation(shot) + shot_result = calc.fire(shot) + zero_given_elevation = shot_result.zero_given_elevation() if len(zero_given_elevation) > 0: zero = [p for p in zero_given_elevation if abs( (p.distance >> Distance.Yard) - (zero_distance >> Distance.Yard) @@ -197,10 +202,12 @@ def test_path_g1(self): ammo = Ammo(dm, 1.282, Velocity(2750, Velocity.FPS)) weapon = Weapon(Distance(2, Distance.Inch), Distance(100, Distance.Yard)) atmosphere = Atmo.icao() - shot_info = Shot(1000, 100, zero_angle=Angular(0.001228, Angular.Radian)) - wind = [Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))] + shot_info = Shot(1000, + zero_angle=Angular(0.001228, Angular.Radian), + atmo=atmosphere, + winds=[Wind(Velocity(5, Velocity.MPH), Angular(10.5, Angular.OClock))]) calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) + data = calc.trajectory(weapon, shot_info, Distance.Yard(100)) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -219,15 +226,12 @@ 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, 100, 11.24) - atmosphere = Atmo.icao() shot_info = Shot(Distance.Yard(1000), - Distance.Yard(100), - zero_angle=Angular.MOA(4.221) - ) - wind = [Wind(Velocity(5, Velocity.MPH), -45)] + zero_angle=Angular.MOA(4.221), + winds=[Wind(Velocity(5, Velocity.MPH), -45)]) calc = TrajectoryCalc(ammo) - data = calc.trajectory(weapon, atmosphere, shot_info, wind, TrajFlag.RANGE.value) + data = calc.trajectory(weapon, shot_info, Distance.Yard(100)) self.custom_assert_equal(len(data), 11, 0.1, "Length") @@ -249,11 +253,11 @@ def setUp(self) -> None: self.ammo = Ammo(self.dm, 1.282, 2750) self.weapon = Weapon(2, 100, 11.24) self.atmo = Atmo.icao() - self.shot = Shot(Distance.Yard(1000), - Distance.Yard(100), - zero_angle=Angular.MOA(4.221) - ) - self.wind = [Wind(Velocity(5, Velocity.MPH), -45)] + self.shot = Shot( + Distance.Yard(1000), + zero_angle=Angular.MOA(4.221), + winds=[Wind(Velocity(5, Velocity.MPH), -45)] + ) self.calc = TrajectoryCalc(self.ammo) @@ -264,7 +268,7 @@ def test_elevation_performance(self): self.calc.zero_angle(self.weapon, self.atmo) def test_path_performance(self): - d = self.calc.trajectory(self.weapon, self.atmo, self.shot, self.wind) + d = self.calc.trajectory(self.weapon, self.shot, Distance.Foot(0.2)) # [print(p.formatted()) for p in d] From a4689698f27872419e515b6ef713d57c6717d91c Mon Sep 17 00:00:00 2001 From: Murphy Date: Fri, 13 Oct 2023 11:53:44 +0300 Subject: [PATCH 15/15] * updated deprecated github actions --- .github/workflows/python-publish-test.yml | 2 +- .github/workflows/python-publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish-test.yml b/.github/workflows/python-publish-test.yml index ab6ef9e..1590f8b 100644 --- a/.github/workflows/python-publish-test.yml +++ b/.github/workflows/python-publish-test.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ac1e59d..2f36a80 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies