Skip to content

Shot

Shot dataclass

Shot(
    *,
    ammo: Ammo,
    atmo: Optional[Atmo] = None,
    weapon: Optional[Weapon] = None,
    winds: Optional[Sequence[Wind]] = None,
    look_angle: Optional[Union[float, Angular]] = None,
    relative_angle: Optional[Union[float, Angular]] = None,
    cant_angle: Optional[Union[float, Angular]] = None,
    azimuth: Optional[float] = None,
    latitude: Optional[float] = None,
)

All information needed to compute a ballistic trajectory.

Attributes:

Name Type Description
ammo Ammo

Ammo used for shot.

atmo Atmo

Atmosphere in effect during shot.

weapon Weapon

Weapon used for shot.

winds Sequence[Wind]

List of Wind in effect during shot, sorted by .until_distance.

look_angle slant_angle

Angle of sight line relative to horizontal. If look_angle != 0 then any target in sight crosshairs will be at a different altitude: With target_distance = sight distance to a target (i.e., as through a rangefinder): * Horizontal distance X to target = cos(look_angle) * target_distance * Vertical distance Y to target = sin(look_angle) * target_distance

cant_angle Angular

Tilt of gun from vertical. If weapon.sight_height != 0 then this shifts any barrel elevation from the vertical plane into the horizontal plane (as barrel_azimuth) by sine(cant_angle).

relative_angle Angular

Elevation adjustment (a.k.a. "hold") added to weapon.zero_elevation.

azimuth Optional[float]

Azimuth of the shooting direction in degrees [0, 360). Optional, for Coriolis effects. Should be geographic bearing where 0 = North, 90 = East, 180 = South, 270 = West. Difference from magnetic bearing is usually negligible.

latitude Optional[float]

Latitude of the shooting location in degrees [-90, 90]. Optional, for Coriolis effects.

barrel_elevation Angular

Total barrel elevation (in vertical plane) from horizontal. = look_angle + cos(cant_angle) * zero_elevation + relative_angle

barrel_azimuth Angular

Horizontal angle of barrel relative to sight line.

Parameters:

Name Type Description Default
ammo Ammo

Ammo instance used for shot.

required
atmo Optional[Atmo]

Atmosphere in effect during shot.

None
weapon Optional[Weapon]

Weapon instance used for shot.

None
winds Optional[Sequence[Wind]]

List of Wind in effect during shot.

None
look_angle Optional[Union[float, Angular]]

Angle of sight line relative to horizontal. If look_angle != 0 then any target in sight crosshairs will be at a different altitude: With target_distance = sight distance to a target (i.e., as through a rangefinder): * Horizontal distance X to target = cos(look_angle) * target_distance * Vertical distance Y to target = sin(look_angle) * target_distance

None
cant_angle Optional[Union[float, Angular]]

Tilt of gun from vertical. If weapon.sight_height != 0 then this shifts any barrel elevation from the vertical plane into the horizontal plane (as barrel_azimuth) by sine(cant_angle).

None
relative_angle Optional[Union[float, Angular]]

Elevation adjustment (a.k.a. "hold") added to weapon.zero_elevation.

None
azimuth Optional[float]

Azimuth of the shooting direction in degrees [0, 360). Optional, for Coriolis effects. Should be geographic bearing where 0 = North, 90 = East, 180 = South, 270 = West. Difference from magnetic bearing is usually negligible.

None
latitude Optional[float]

Latitude of the shooting location in degrees [-90, 90]. Optional, for Coriolis effects.

None
Example
from py_ballisticcalc import Weapon, Ammo, Atmo, Wind, Unit, Shot
shot = Shot(
    ammo=Ammo(...),
    atmo=Atmo(...),
    weapon=Weapon(...),
    winds=[Wind(...), ... ]
    look_angle=Unit.Degree(5),
    cant_angle=Unit.Degree(0),
    relative_angle=Unit.Degree(1),
    azimuth=90.0,  # East
    latitude=45.0  # 45° North
)
Source code in py_ballisticcalc/shot.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def __init__(
    self,
    *,
    ammo: Ammo,
    atmo: Optional[Atmo] = None,
    weapon: Optional[Weapon] = None,
    winds: Optional[Sequence[Wind]] = None,
    look_angle: Optional[Union[float, Angular]] = None,
    relative_angle: Optional[Union[float, Angular]] = None,
    cant_angle: Optional[Union[float, Angular]] = None,
    azimuth: Optional[float] = None,
    latitude: Optional[float] = None,
):
    """Initialize `Shot` for trajectory calculations.

    Args:
        ammo: Ammo instance used for shot.
        atmo: Atmosphere in effect during shot.
        weapon: Weapon instance used for shot.
        winds: List of Wind in effect during shot.
        look_angle: Angle of sight line relative to horizontal.
            If `look_angle != 0` then any target in sight crosshairs will be at a different altitude:
                With target_distance = sight distance to a target (i.e., as through a rangefinder):
                    * Horizontal distance X to target = cos(look_angle) * target_distance
                    * Vertical distance Y to target = sin(look_angle) * target_distance
        cant_angle: Tilt of gun from vertical. If `weapon.sight_height != 0` then this shifts any barrel elevation
            from the vertical plane into the horizontal plane (as `barrel_azimuth`) by `sine(cant_angle)`.
        relative_angle: Elevation adjustment (a.k.a. "hold") added to `weapon.zero_elevation`.
        azimuth: Azimuth of the shooting direction in degrees [0, 360). Optional, for Coriolis effects.
            Should be geographic bearing where 0 = North, 90 = East, 180 = South, 270 = West.
            Difference from magnetic bearing is usually negligible.
        latitude: Latitude of the shooting location in degrees [-90, 90]. Optional, for Coriolis effects.

    Example:
        ```python
        from py_ballisticcalc import Weapon, Ammo, Atmo, Wind, Unit, Shot
        shot = Shot(
            ammo=Ammo(...),
            atmo=Atmo(...),
            weapon=Weapon(...),
            winds=[Wind(...), ... ]
            look_angle=Unit.Degree(5),
            cant_angle=Unit.Degree(0),
            relative_angle=Unit.Degree(1),
            azimuth=90.0,  # East
            latitude=45.0  # 45° North
        )
        ```
    """
    self.ammo = ammo
    self.atmo = atmo or Atmo.icao()
    self.weapon = weapon or Weapon()
    self.winds = winds or [Wind()]
    self.look_angle = PreferredUnits.angular(look_angle or 0)
    self.cant_angle = PreferredUnits.angular(cant_angle or 0)
    self.relative_angle = PreferredUnits.angular(relative_angle or 0)
    self._azimuth = azimuth
    self._latitude = latitude

Attributes

azimuth property writable
azimuth: Optional[float]

Azimuth of the shooting direction in degrees [0, 360).

Should be geographic bearing where 0 = North, 90 = East, 180 = South, 270 = West. However, difference from magnetic bearing is usually negligible.

latitude property writable
latitude: Optional[float]

Latitude of the shooting location in degrees [-90, 90].

winds property writable
winds: Sequence[Wind]

Sequence[Wind] sorted by until_distance.

barrel_azimuth property
barrel_azimuth: Angular

Horizontal angle of barrel relative to sight line.

barrel_elevation property writable
barrel_elevation: Angular

Total barrel elevation (in vertical plane) from horizontal.

Returns:

Type Description
Angular

Angle of barrel elevation in vertical plane from horizontal = look_angle + cos(cant_angle) * zero_elevation + relative_angle

slant_angle property writable
slant_angle: Angular

Synonym for look_angle.

Coriolis dataclass

Coriolis(
    sin_lat: float,
    cos_lat: float,
    sin_az: Optional[float],
    cos_az: Optional[float],
    range_east: Optional[float],
    range_north: Optional[float],
    cross_east: Optional[float],
    cross_north: Optional[float],
    flat_fire_only: bool,
    muzzle_velocity_fps: float,
)

Precomputed Coriolis helpers for applying Earth's rotation.

The calculator keeps ballistic state in a local range/up/cross (x, y, z) frame where the x axis points down-range, y points up, and z points to the shooter's right. Coriolis forces originate in the Earth-fixed East-North-Up (ENU) frame. This class precumputes the scalars to transform between the two frames.

If we are given latitude but not azimuth of the shot, this class falls back on a flat-fire approximation of Coriolis effects: north of the equator the deflection is to the right; south of the equator it is to the left. Given both azimuth \(A\) and latitude \(L\) we compute the full 3D Coriolis acceleration as:

\[ 2 \Omega \begin{bmatrix} -V_y \cos(L) \sin(A) - V_z \sin(L) \\ V_x \cos(L) \sin(A) + V_z \cos(L) \cos(A) \\ V_x \sin(L) - V_y \cos(L) \cos(A) \end{bmatrix} \]

Attributes:

Name Type Description
sin_lat float

Sine of the firing latitude, used to project the Earth's rotation vector.

cos_lat float

Cosine of the firing latitude.

sin_az Optional[float]

Sine of the firing azimuth, or None when azimuth is unknown (flat-fire fallback).

cos_az Optional[float]

Cosine of the firing azimuth, or None when azimuth is unknown.

range_east Optional[float]

Projection of the local range axis onto geographic east (None in flat-fire mode).

range_north Optional[float]

Projection of the local range axis onto geographic north (None in flat-fire mode).

cross_east Optional[float]

Projection of the local cross axis onto geographic east (None in flat-fire mode).

cross_north Optional[float]

Projection of the local cross axis onto geographic north (None in flat-fire mode).

flat_fire_only bool

True when no azimuth is provided and only the 2D flat-fire approximation should run.

muzzle_velocity_fps float

Muzzle velocity in feet per second (only needed by the flat-fire approximation).

Methods:

Name Description
create

Build a Coriolis helper for a shot when latitude is available.

coriolis_acceleration_local

Compute the Coriolis acceleration for a velocity expressed in the local frame.

flat_fire_offsets

Estimate flat-fire vertical and horizontal corrections.

adjust_range

Apply the flat-fire offsets to a range vector when necessary.

Attributes

full_3d property
full_3d: bool

Whether full 3D Coriolis model is available (i.e., both azimuth and latitude).

Functions

create classmethod
create(
    latitude: Optional[float],
    azimuth: Optional[float],
    muzzle_velocity_fps: float,
) -> Optional[Coriolis]

Build a Coriolis helper for a shot when latitude is available.

Parameters:

Name Type Description Default
latitude Optional[float]

Latitude of the shooting location in degrees [-90, 90].

required
azimuth Optional[float]

Azimuth of the shooting direction in degrees [0, 360).

required
muzzle_velocity_fps float

Muzzle velocity in feet per second for the projectile.

required

Returns:

Type Description
Optional[Coriolis]

A populated Coriolis instance when the shot specifies a latitude, otherwise None.

Notes

When azimuth is omitted we fall back to the flat fire approximation, which only corrects for the horizontal drift term that dominates short-range, low-arc engagements.

Source code in py_ballisticcalc/conditions.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
@classmethod
def create(
    cls, latitude: Optional[float], azimuth: Optional[float], muzzle_velocity_fps: float
) -> Optional[Coriolis]:
    """Build a `Coriolis` helper for a shot when latitude is available.

    Args:
        latitude: Latitude of the shooting location in degrees [-90, 90].
        azimuth: Azimuth of the shooting direction in degrees [0, 360).
        muzzle_velocity_fps: Muzzle velocity in feet per second for the projectile.

    Returns:
        A populated `Coriolis` instance when the shot specifies a latitude, otherwise `None`.

    Notes:
        When azimuth is omitted we fall back to the *flat fire* approximation, which only corrects
        for the horizontal drift term that dominates short-range, low-arc engagements.
    """
    if latitude is None:
        return None

    lat_rad = math.radians(latitude)
    sin_lat = math.sin(lat_rad)
    cos_lat = math.cos(lat_rad)

    if azimuth is None:
        return cls(
            sin_lat=sin_lat,
            cos_lat=cos_lat,
            muzzle_velocity_fps=muzzle_velocity_fps,
            sin_az=None,
            cos_az=None,
            range_east=None,
            range_north=None,
            cross_east=None,
            cross_north=None,
            flat_fire_only=True,
        )

    azimuth_rad = math.radians(azimuth)

    return cls(
        sin_lat=sin_lat,
        cos_lat=cos_lat,
        muzzle_velocity_fps=muzzle_velocity_fps,
        sin_az=math.sin(azimuth_rad),
        cos_az=math.cos(azimuth_rad),
        range_east=math.sin(azimuth_rad),
        range_north=math.cos(azimuth_rad),
        cross_east=math.cos(azimuth_rad),
        cross_north=-math.sin(azimuth_rad),
        flat_fire_only=False,
    )
coriolis_acceleration_local
coriolis_acceleration_local(velocity: Vector) -> Vector

Compute the Coriolis acceleration for a velocity expressed in the local frame.

Parameters:

Name Type Description Default
velocity Vector

Projectile velocity vector in the local range/up/cross basis (feet per second).

required

Returns:

Type Description
Vector

A Vector containing the Coriolis acceleration components in the same local basis.

Vector

Returns the ZERO_VECTOR when only the flat-fire approximation is available.

Source code in py_ballisticcalc/conditions.py
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
def coriolis_acceleration_local(self, velocity: Vector) -> Vector:
    """Compute the Coriolis acceleration for a velocity expressed in the local frame.

    Args:
        velocity: Projectile velocity vector in the local range/up/cross basis (feet per second).

    Returns:
        A `Vector` containing the Coriolis acceleration components in the same local basis.
        Returns the `ZERO_VECTOR` when only the flat-fire approximation is available.
    """
    if not self.full_3d:
        return ZERO_VECTOR

    assert self.range_east is not None and self.range_north is not None
    assert self.cross_east is not None and self.cross_north is not None

    vel_east = velocity.x * self.range_east + velocity.z * self.cross_east
    vel_north = velocity.x * self.range_north + velocity.z * self.cross_north
    vel_up = velocity.y

    factor = -2.0 * cEarthAngularVelocityRadS
    accel_east = factor * (self.cos_lat * vel_up - self.sin_lat * vel_north)
    accel_north = factor * (self.sin_lat * vel_east)
    accel_up = factor * (-self.cos_lat * vel_east)

    accel_range = accel_east * self.range_east + accel_north * self.range_north
    accel_cross = accel_east * self.cross_east + accel_north * self.cross_north
    return Vector(accel_range, accel_up, accel_cross)
flat_fire_offsets
flat_fire_offsets(
    time: float, distance_ft: float, drop_ft: float
) -> Tuple[float, float]

Estimate flat-fire vertical and horizontal corrections.

Parameters:

Name Type Description Default
time float

Time of flight in seconds for the sample point.

required
distance_ft float

Down-range distance in feet at the sample point.

required
drop_ft float

Local vertical displacement in feet (positive is up).

required

Returns:

Type Description
float

A tuple (vertical_ft, horizontal_ft) of offsets that should be applied to the range/up/cross vector.

float

Both values are zero when Shot has both latitude and azimuth and so can compute a full 3D solution.

Source code in py_ballisticcalc/conditions.py
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
def flat_fire_offsets(self, time: float, distance_ft: float, drop_ft: float) -> Tuple[float, float]:
    """Estimate flat-fire vertical and horizontal corrections.

    Args:
        time: Time of flight in seconds for the sample point.
        distance_ft: Down-range distance in feet at the sample point.
        drop_ft: Local vertical displacement in feet (positive is up).

    Returns:
        A tuple `(vertical_ft, horizontal_ft)` of offsets that should be applied to the range/up/cross vector.
        Both values are zero when `Shot` has both latitude and azimuth and so can compute a full 3D solution.
    """
    if not self.flat_fire_only:
        return 0.0, 0.0

    horizontal = cEarthAngularVelocityRadS * distance_ft * self.sin_lat * time
    vertical = 0.0
    if self.sin_az is not None:  # This should not happen if not full_3d, but approximation provided for reference
        vertical_factor = -2.0 * cEarthAngularVelocityRadS * self.muzzle_velocity_fps * self.cos_lat * self.sin_az
        vertical = drop_ft * (vertical_factor / cGravityImperial)
    return vertical, horizontal
adjust_range
adjust_range(time: float, range_vector: Vector) -> Vector

Apply the flat-fire offsets to a range vector when necessary.

Parameters:

Name Type Description Default
time float

Time of flight in seconds for the sample point.

required
range_vector Vector

Original range/up/cross vector (feet) produced by the integrator.

required

Returns:

Type Description
Vector

Either the original vector (for full 3D solutions) or a new vector with the flat-fire offsets applied.

Source code in py_ballisticcalc/conditions.py
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
def adjust_range(self, time: float, range_vector: Vector) -> Vector:
    """Apply the flat-fire offsets to a range vector when necessary.

    Args:
        time: Time of flight in seconds for the sample point.
        range_vector: Original range/up/cross vector (feet) produced by the integrator.

    Returns:
        Either the original vector (for full 3D solutions) or a new vector with the flat-fire offsets applied.
    """
    if not self.flat_fire_only:
        return range_vector

    delta_y, delta_z = self.flat_fire_offsets(time, range_vector.x, range_vector.y)
    if delta_y == 0.0 and delta_z == 0.0:
        return range_vector
    return Vector(range_vector.x, range_vector.y + delta_y, range_vector.z + delta_z)