Skip to content

Commit

Permalink
Interface refactoring
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
o-murphy authored Oct 13, 2023
1 parent 62bc7db commit 8052649
Show file tree
Hide file tree
Showing 14 changed files with 781 additions and 232 deletions.
86 changes: 49 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -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@<target_branch_name>
```

#### 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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions py_ballisticcalc/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()]
4 changes: 2 additions & 2 deletions py_ballisticcalc/drag_model.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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', )
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 4 additions & 9 deletions py_ballisticcalc/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
144 changes: 38 additions & 106 deletions py_ballisticcalc/interface.py
Original file line number Diff line number Diff line change
@@ -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',)

Expand All @@ -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"""
Expand All @@ -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))
Loading

0 comments on commit 8052649

Please sign in to comment.