Code Monkey home page Code Monkey logo

py-ballisticcalc's Introduction

BallisticCalculator

LGPL library for small arms ballistic calculations based on point-mass (3 DoF) plus spin drift.

Table of contents

Installation

pip install py-ballisticcalc

# Using precompiled backend (improves performance)
pip install py-ballisticcalc[exts]

# Using matplotlib and pandas uses additional dependencies
pip install py-ballisticcalc[charts]

Usage

See Example.ipynb for detailed illustrations of all features and usage.

# Uncomment pyximport to compile instead of running pure python
#import pyximport; pyximport.install(language_level=3)

from py_ballisticcalc import DragModel, TableG7, TableG1
from py_ballisticcalc import Ammo, Atmo, Wind
from py_ballisticcalc import Weapon, Shot, Calculator
from py_ballisticcalc import Settings as Set
from py_ballisticcalc.unit import *

Simple Zero

# Establish 100-yard zero for a standard .308, G7 bc=0.22, muzzle velocity 2600fps
zero = Shot(weapon=Weapon(sight_height=2), ammo=Ammo(DragModel(0.22, TableG7), mv=Velocity.FPS(2600)))
calc = Calculator()
zero_distance = Distance.Yard(100)
zero_elevation = calc.set_weapon_zero(zero, zero_distance)
print(f'Barrel elevation for {zero_distance} zero: {zero_elevation << PreferredUnits.adjustment}')
Barrel elevation for 100.0yd zero: 1.33mil

Plot Trajectory with Danger Space

# Plot trajectory out to 500 yards
shot_result = calc.fire(zero, trajectory_range=500, extra_data=True)
ax = shot_result.plot()
# Find danger space for a half-meter tall target at 300 yards
danger_space = shot_result.danger_space(Distance.Yard(300), Distance.Meter(.5))
print(danger_space)
danger_space.overlay(ax)  # Highlight danger space on the plot
plt.show()
Danger space at 300.0yd for 19.7inch tall target ranges from 217.1yd to 355.7yd

plot

Print Range Card

# Range card for this zero with 5mph cross-wind from left to right
zero.winds = [Wind(Velocity.MPH(5), Angular.OClock(3))]
range_card = calc.fire(zero, trajectory_range=1000)
range_card.dataframe().to_clipboard()
range_card.dataframe(True)[['distance', 'velocity', 'mach', 'time', 'target_drop', 'drop_adj', 'windage', 'windage_adj']].set_index('distance')
distance velocity mach time target_drop drop_adj windage windage_adj
0.0 yd 2600.0 ft/s 2.33 mach 0.000 s -2.0 inch 0.00 mil -0.0 inch 0.00 mil
100.0 yd 2398.1 ft/s 2.15 mach 0.120 s -0.0 inch -0.00 mil 0.4 inch 0.12 mil
200.0 yd 2205.5 ft/s 1.98 mach 0.251 s -4.1 inch -0.57 mil 1.7 inch 0.25 mil
300.0 yd 2022.3 ft/s 1.81 mach 0.393 s -15.3 inch -1.44 mil 4.1 inch 0.39 mil
400.0 yd 1847.5 ft/s 1.65 mach 0.548 s -35.0 inch -2.48 mil 7.6 inch 0.54 mil
500.0 yd 1680.1 ft/s 1.50 mach 0.718 s -65.0 inch -3.68 mil 12.4 inch 0.70 mil
600.0 yd 1519.5 ft/s 1.36 mach 0.906 s -107.3 inch -5.06 mil 18.8 inch 0.89 mil
700.0 yd 1366.0 ft/s 1.22 mach 1.114 s -164.8 inch -6.66 mil 27.0 inch 1.09 mil
800.0 yd 1221.3 ft/s 1.09 mach 1.347 s -240.9 inch -8.52 mil 37.3 inch 1.32 mil
900.0 yd 1093.2 ft/s 0.98 mach 1.607 s -340.5 inch -10.71 mil 50.0 inch 1.57 mil
1000.0 yd 1029.8 ft/s 0.92 mach 1.891 s -469.0 inch -13.27 mil 64.8 inch 1.83 mil

Complex Example

Here we define a standard .50BMG, enable powder temperature sensitivity, and zero for a distance of 500 meters, in a 5°C atmosphere at altitude 1000ft ASL.

dm = DragModel(0.62, TableG1, 661, 0.51, 2.3)
ammo=Ammo(dm, Velocity.MPS(850), Temperature.Celsius(15))
ammo.calc_powder_sens(Velocity.MPS(820), Temperature.Celsius(0))
weapon = Weapon(sight_height=Distance.Centimeter(9), twist=15)
atmo = Atmo(altitude=Distance.Foot(1000), temperature=Unit.CELSIUS(5), humidity=.5)
zero = Shot(weapon=weapon, ammo=ammo, atmo=atmo)
zero_distance = Distance.Meter(500)
calc = Calculator()
zero_elevation = calc.set_weapon_zero(zero, zero_distance)
print(f'Barrel elevation for {zero_distance} zero: {zero_elevation << PreferredUnits.adjustment}')
print(f'Muzzle velocity at zero temperature {atmo.temperature} is {ammo.get_velocity_for_temp(atmo.temperature) << Velocity.MPS}')
Barrel elevation for 500.0m zero: 4.69mil
Muzzle velocity at zero temperature 5.0°C is 830.0m/s

Units

from py_ballisticcalc.unit import *

# Print default units
from py_ballisticcalc import Settings, PreferredUnits
print(str(Settings.Units))

# Change default
PreferredUnits.distance = Unit.FOOT
print(f'Default distance unit: {PreferredUnits.distance.name}')
# Can create value in default unit with either float or another unit of same type
print(f'\tInstantiated from float (5): {PreferredUnits.distance(5)}')
print(f'\tInstantiated from Distance.Line(200): {PreferredUnits.distance(Distance.Line(200))}')

# Ways to define value in units
# 1. old syntax
unit_in_meter = Distance(100, Distance.Meter)
# 2. short syntax by Unit type class
unit_in_meter = Distance.Meter(100)
# 3. by Unit enum class
unit_in_meter = Unit.Meter(100)
print(f'100 meters: {unit_in_meter}')
# >>> 100 meters: 100.0m

# Convert unit
# 1. by .convert()
unit_in_yards = unit_in_meter.convert(Distance.Yard)
# 2. using shift syntax
unit_in_yards = unit_in_meter << Distance.Yard  # '<<=' operator also supports
print(f'100 meters in {unit_in_yards.units.key}: {unit_in_yards}')
# >>> 100 meters in yard: 109.4yd

# Get value in specified units (as float)
# 1. by .get_in()
value_in_km = unit_in_yards.get_in(Distance.Kilometer)
# 2. by shift syntax
value_in_km = unit_in_yards >> Distance.Kilometer  # '>>=' operator also supports
print(f'100 meters, value in km: {value_in_km}  (value type is {type(value_in_km)})')
# >>> 100 meters, value in km: 0.1  (value type is <class 'float'>)

# Getting unit raw value (a float)
rvalue = Distance.Meter(100).raw_value
rvalue = float(Distance.Meter(100))
print(f'100 meters in raw value: {rvalue}  (raw type is {type(rvalue)})')
# >>> 100 meters in raw value: 3937.0078740157483  (raw type is <class 'float'>)

# Comparison operators supported: < > <= >= == !=
print(f'Comparison: {unit_in_meter} == {Distance.Centimeter(100)}: {unit_in_meter == Distance.Centimeter(100)}')
# >>> False, compare two units by raw value
print(f'Comparison: {unit_in_meter} > .1*{unit_in_meter}: {unit_in_meter > .1*unit_in_meter.raw_value}')
# >>> True, compare unit with float by raw value

Concepts

Look angle

Look angle is the elevation of the sight line (a.k.a., Line of Sight, or LoS) relative to the horizon. For flat fire at angles close to horizontal this does not make a significant difference. When the look angle is significantly above or below the horizon the trajectory will be different because:

  1. Gravity is not orthogonal to the velocity
  2. Air density changes with altitude, so the drag effects will vary across an arcing trajectory.

The shooter typically cares about the line of sight (LoS): Sight adjustments (drop in the following figure) are made relative to LoS, and ranging errors – and hence danger space – follow the line of sight, not the horizon.

The following diagram shows how look distance and drop relate by look angle to the underlying (distance x, height y) trajectory data. Look-angle trigonometry

Danger Space

Danger space is a practical measure of sensitivity to ranging error. It is defined for a target of height h and distance d, and it indicates how far forward and backward along the line of sight the target can move such that the trajectory will still hit somewhere (vertically) on the target.

Danger Space

About project

The library provides trajectory calculation for ballistic projectiles including air rifles, bows, firearms, artillery, and so on.

The 3DoF model that is used in this calculator is rooted in public C code of JBM's calculator, ported to C#, optimized, fixed and extended with elements described in Litz's Applied Ballistics book and from the friendly project of Alexandre Trofimov and then ported to Go.

This Python3 implementation has been expanded to support multiple ballistic coefficients and custom drag functions, such as those derived from Doppler radar data.

The online version of Go documentation is located here.

C# version of the package is located here, and the online version of C# API documentation is located here.

Contributors

This project exists thanks to all the people who contribute.

Special thanks to:

  • David Bookstaber - Ballistics Expert
    For help understanding and improving the functionality
  • Nikolay Gekht
    For the sources code on C# and GO-lang from which this project firstly was forked

RISK NOTICE

The library performs very limited simulation of a complex physical process and so it performs a lot of approximations. Therefore, the calculation results MUST NOT be considered as completely and reliably reflecting actual behavior or characteristics of projectiles. While these results may be used for educational purpose, they must NOT be considered as reliable for the areas where incorrect calculation may cause making a wrong decision, financial harm, or can put a human life at risk.

THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.

py-ballisticcalc's People

Contributors

dbookstaber avatar nikolaygekht avatar o-murphy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

py-ballisticcalc's Issues

Commandline and TOML interfaces

Files

  • DragTable: list of {Mach: Cd} pairs sorted by Mach ascending
  • Weapon: [sight_height], [twist], [zero_elevation]
  • Ammo: <DragTable>, <BC>, [powder_temp, temp_modifier]

Problems:

  1. API expects Ammo to indicate muzzle velocity, but muzzle velocity depends on the Weapon, and also on Atmo.Temperature if temp_modifier is indicated.
  2. Specification of how weapon is zeroed. Shooters don't know their zero elevation (i.e., the angle between the barrel and sight line when scope is set to zero); what they know is zero distance for particular Ammo and Atmo.

Commandline Args

  • Angles (assume zero if not supplied)
    • Look-angle
    • Cant-angle
    • Holds
      • Vertical
      • Horizontal
  • Atmospherics (assume standard if not supplied)
    • Temperature
    • Pressure
    • Altitude
    • Humidity
    • Wind {<direction> <speed>} ...
  • Target:
    • <distance> (if supplied then we will solve for hold angle to hit)
    • <height> (if supplied then we will calculate danger space)
  • Range (trajectory calculated to distance)
  • Step (for tabular output)

MultiBC not working with TrajectoryCalc

multiple_bc.py does not produce a drag curve that reflects the BC used to generate it.

After reviewing, we concluded that module is doing a lot of unnecessary work. Deprecate it and instead we should interpolate multiple BCs directly in DragModel.

The following code can be used to test. Results should be the same whether we use the Baseline or Alternate definition.

# Baseline:
zero = Shot(weapon=Weapon(sight_height=2), ammo=Ammo(DragModel(0.305, TableG7), mv=Velocity.FPS(2800)))
# Alternate:
zero = Shot(weapon=Weapon(sight_height=2), ammo=Ammo(DragModel([(1, 0.305)], TableG7), mv=Velocity.FPS(2800)))
calc = Calculator()
zero_distance = Distance.Yard(1000)
zero_elevation = calc.set_weapon_zero(zero, zero_distance)
range_card = calc.fire(zero, trajectory_range=1500, trajectory_step=500)
range_card.dataframe(True)[['distance', 'time', 'velocity', 'drag']].set_index('distance')

Refactor the unit.py module to make it simplier and flexible

  • make the opperations with raw values available
  • add an arithmetic support between units of the same type and between different unit types|
  • make the arithmetic operations to return stacked type of units if the operation returns unavailable unit in base

Test cases for wind and cant

It appears that Shot.cant_angle hasn't been incorporated at all into the trajectory, except indirectly through its use in finding the relative wind vector.

Let's add some extreme sanity-checking test cases for wind and cant. For example, wind from 90 degrees should increase windage. From 270 degrees should reduce windage. From 0 should reduce drop at any point relative to zero wind. From 180 should increase drop.

Cant of 90 degrees with positive barrel elevation (and zero twist, so no spin drift) should cause the drop to match barrel-elevation=0, and should produce large positive windage.

Can't instantiate Weapon with itself

Trying to pass a Weapon instance to a new Shot:

zero = Shot(weapon=Weapon(sight_height=0), ammo=Ammo(DragModel(0.32, TableG7), mv=Velocity.FPS(1100)))
calc = Calculator()
zero_distance = Distance.Yard(100)
zero_elevation = calc.set_weapon_zero(zero, zero_distance)
s = Shot(zero.weapon, ammo=Ammo(DragModel(0.32, TableG7), mv=Velocity.FPS(1050)))

... produces a TypeError.

Zero given elevation unittests fails

  • test_zero_given not passing, skipping, zero_given_elevation not working properly if sight_height > 0
  • test_danger_space not passing, skipping, danger_space returns wrong values

Enforce sort order for winds

In the case of multiple winds: TrajectoryCalc assumes that Shot.winds[] is sorted by Wind.until_distance (ascending). However right now we do not enforce that or even check whether that assumption is valid.

What is the best way to handle this?

Units features

We have to be able to do arithmetic on Units. For example, suppose we have a some_distance instance of Distance, and we want to double it.

Here is one kludge:
Distance(2*some_distance.raw_value, Distance.Inch) << some_distance.unit

Problem: We have to read through the Distance class to see that its raw_value is in units of Distance.Inch.

Solution: Give all AbstractUnits child classes a raw_units property (can be static) that returns the raw units for the subclass. For example, Distance.raw_units would return Distance.Inch. Then we could instead do:
Distance(2*some_distance.raw_value, some_distance.raw_units) << some_distance.unit

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.