Skip to content

DragModel

drag_model

Drag model implementations for ballistic projectiles.

This module provides classes and functions for modeling aerodynamic drag of projectiles, including single and multi-BC (ballistic coefficient) models. Supports standard drag tables and custom drag data points.

Key Components
  • DragDataPoint: Individual drag coefficient at specific Mach number
  • BCPoint: Ballistic coefficient point for multi-BC models
  • DragModel: Primary drag model with ballistic coefficient and drag table
  • DragModelMultiBC: Multi-BC drag model for varying ballistic coefficients

Functions:

Name Description
- make_data_points

Convert drag table data to DragDataPoint objects

- sectional_density

Calculate sectional density from weight and diameter

- linear_interpolation

Linear interpolation utility function

The drag models use standard ballistic reference tables (G1, G7, etc.) and allow for custom drag functions based on Mach number vs drag coefficient data.

Classes:

Name Description
DragDataPoint

Drag coefficient at a specific Mach number.

DragModel

Aerodynamic drag model for ballistic projectiles.

BCPoint

Ballistic coefficient point for multi-BC drag models.

Attributes

Classes

DragDataPoint dataclass
DragDataPoint(Mach: float, CD: float)

Drag coefficient at a specific Mach number.

Attributes:

Name Type Description
Mach float

Velocity in Mach units (dimensionless)

CD float

Drag coefficient (dimensionless)

DragModel
DragModel(
    bc: float,
    drag_table: DragTableDataType,
    weight: Union[float, Weight] = 0,
    diameter: Union[float, Distance] = 0,
    length: Union[float, Distance] = 0,
)

Aerodynamic drag model for ballistic projectiles.

Represents the drag characteristics of a projectile using a ballistic coefficient and drag table.

The ballistic coefficient (BC) is defined as: BC = weight / (diameter^2 * form_factor) where weight is in pounds, diameter is in inches, and form_factor is relative to the selected drag model.

Attributes:

Name Type Description
BC

Ballistic coefficient (scales drag model for a particular projectile)

drag_table

List of DragDataPoint objects defining Mach vs CD

weight

Projectile weight (only needed for spin drift calculations)

diameter

Projectile diameter (only needed for spin drift calculations)

length

Projectile length (only needed for spin drift calculations)

sectional_density

Calculated sectional density (lb/in²)

form_factor

Calculated form factor (dimensionless)

Note

The weight, diameter, and length parameters are only required when computing spin drift. For basic trajectory calculations, only BC and drag_table are needed.

Parameters:

Name Type Description Default
bc float

Ballistic coefficient

required
drag_table DragTableDataType

Either list of DragDataPoint objects or list of dictionaries with 'Mach' and 'CD' keys

required
weight Union[float, Weight]

Projectile weight in grains (default: 0)

0
diameter Union[float, Distance]

Projectile diameter in inches (default: 0)

0
length Union[float, Distance]

Projectile length in inches (default: 0)

0

Raises:

Type Description
ValueError

If BC is not positive or drag_table is empty

TypeError

If drag_table format is invalid

Source code in py_ballisticcalc/drag_model.py
 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
def __init__(self,
             bc: float,
             drag_table: DragTableDataType,
             weight: Union[float, Weight] = 0,
             diameter: Union[float, Distance] = 0,
             length: Union[float, Distance] = 0) -> None:
    """Initialize a drag model with ballistic coefficient and drag table.

    Args:
        bc: Ballistic coefficient
        drag_table: Either list of DragDataPoint objects or list of
                    dictionaries with 'Mach' and 'CD' keys
        weight: Projectile weight in grains (default: 0)
        diameter: Projectile diameter in inches (default: 0)
        length: Projectile length in inches (default: 0)

    Raises:
        ValueError: If BC is not positive or drag_table is empty
        TypeError: If drag_table format is invalid
    """
    if len(drag_table) <= 0:
        raise ValueError('Received empty drag table')
    if bc <= 0:
        raise ValueError('Ballistic coefficient must be positive')
    if len(drag_table) < 2:
        warnings.warn('Drag table needs at least 2 entries to enable interpolation', UserWarning)

    self.drag_table = make_data_points(drag_table)

    self.BC = bc
    self.length = PreferredUnits.length(length)
    self.weight = PreferredUnits.weight(weight)
    self.diameter = PreferredUnits.diameter(diameter)
    if weight > 0 and diameter > 0:
        self.sectional_density = self._get_sectional_density()
        self.form_factor = self._get_form_factor(self.BC)
BCPoint dataclass
BCPoint(
    BC: float,
    Mach: Optional[float] = None,
    V: Optional[Union[float, Velocity]] = None,
)

Ballistic coefficient point for multi-BC drag models.

Represents a single ballistic coefficient at a specific velocity or Mach number. Sorts by Mach number for constructing drag models (see DragModelMultiBC).

Attributes:

Name Type Description
BC float

Ballistic coefficient

Mach float

Mach number corresponding to this BC measurement

V Optional[Velocity]

Velocity corresponding to this BC measurement (optional)

Examples:

# Create a BCPoint with BC=0.5 at Mach 2.0
point1 = BCPoint(BC=0.5, Mach=2.0)

# Create a BCPoint with BC=0.4 at 1500fps
point2 = BCPoint(BC=0.4, V=Velocity.FPS(1500))

# Sort points by Mach number
points = [point2, point1]
points.sort()  # point1 will come before point2 since Mach 2.0 < Mach at 1500fps
Note

Either Mach or V must be specified, but not both. If V is provided then Mach will be calculated automatically using standard atmospheric conditions.

Parameters:

Name Type Description Default
BC float

Ballistic coefficient (must be positive)

required
Mach Optional[float]

Mach number (optional, mutually exclusive with V)

None
V Optional[Union[float, Velocity]]

Velocity (optional, mutually exclusive with Mach)

None

Raises:

Type Description
ValueError

If BC is not positive, or if both or neither of Mach and V are specified.

Source code in py_ballisticcalc/drag_model.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def __init__(self,
             BC: float,
             Mach: Optional[float] = None,
             V: Optional[Union[float, Velocity]] = None) -> None:
    """Initialize a BCPoint.

    Args:
        BC: Ballistic coefficient (must be positive)
        Mach: Mach number (optional, mutually exclusive with `V`)
        V: Velocity (optional, mutually exclusive with `Mach`)

    Raises:
        ValueError: If `BC` is not positive, or if both or neither of `Mach` and `V` are specified.
    """
    if BC <= 0:
        raise ValueError('Ballistic coefficient must be positive')
    if Mach and V:
        raise ValueError("You cannot specify both 'Mach' and 'V' at the same time")
    if not Mach and not V:
        raise ValueError("One of 'Mach' and 'V' must be specified")

    self.BC = BC
    self.V = PreferredUnits.velocity(V or 0)
    if V:
        self.Mach = (self.V >> Velocity.MPS) / self._machC()
    elif Mach:
        self.Mach = Mach

Functions

make_data_points
make_data_points(
    drag_table: DragTableDataType,
) -> List[DragDataPoint]

Convert drag table from list of dictionaries to list of DragDataPoints.

Handles both DragDataPoint objects and dictionaries with 'Mach' and 'CD' keys. Validates input format and provides clear error messages for invalid data.

Parameters:

Name Type Description Default
drag_table DragTableDataType

Either list of DragDataPoint objects or list of dictionaries with 'Mach' and 'CD' keys

required

Returns:

Type Description
List[DragDataPoint]

List of DragDataPoint objects ready for use in ballistic calculations

Raises:

Type Description
TypeError

If drag_table items are not DragDataPoint objects or valid dictionaries with required keys

Source code in py_ballisticcalc/drag_model.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def make_data_points(drag_table: DragTableDataType) -> List[DragDataPoint]:
    """Convert drag table from list of dictionaries to list of DragDataPoints.

    Handles both DragDataPoint objects and dictionaries with 'Mach' and 'CD' keys.
    Validates input format and provides clear error messages for invalid data.

    Args:
        drag_table: Either list of DragDataPoint objects or list of dictionaries
                    with 'Mach' and 'CD' keys

    Returns:
        List of DragDataPoint objects ready for use in ballistic calculations

    Raises:
        TypeError: If drag_table items are not DragDataPoint objects or valid
                   dictionaries with required keys
    """
    try:
        return [
            point if isinstance(point, DragDataPoint) else DragDataPoint(point['Mach'], point['CD'])
            for point in drag_table
        ]
    except (KeyError, TypeError) as exc:
        raise TypeError(
            "All items in drag_table must be of type DragDataPoint or dict with 'Mach' and 'CD' keys"
        ) from exc
sectional_density
sectional_density(weight: float, diameter: float) -> float

Calculate sectional density of a projectile.

Parameters:

Name Type Description Default
weight float

Projectile weight in grains

required
diameter float

Projectile diameter in inches

required

Returns:

Type Description
float

Sectional density in lb/in² (pounds per square inch)

Note

Formula: SD = weight / (diameter² * 7000) where 7000 converts grains to pounds (7000 grains = 1 pound)

Source code in py_ballisticcalc/drag_model.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def sectional_density(weight: float, diameter: float) -> float:
    """Calculate sectional density of a projectile.

    Args:
        weight: Projectile weight in grains
        diameter: Projectile diameter in inches

    Returns:
        Sectional density in lb/in² (pounds per square inch)

    Note:
        Formula: SD = weight / (diameter² * 7000)
        where 7000 converts grains to pounds (7000 grains = 1 pound)
    """
    return weight / math.pow(diameter, 2) / 7000
DragModelMultiBC
DragModelMultiBC(
    bc_points: List[BCPoint],
    drag_table: DragTableDataType,
    weight: Union[float, Weight] = 0,
    diameter: Union[float, Distance] = 0,
    length: Union[float, Distance] = 0,
) -> DragModel

Create a drag model with multiple ballistic coefficients.

Constructs a DragModel using multiple BC measurements at different velocities, interpolating between them to create a more accurate drag function. This is useful for projectiles whose BC varies significantly with velocity.

Parameters:

Name Type Description Default
bc_points List[BCPoint]

List of BCPoint objects with BC measurements at specific velocities

required
drag_table DragTableDataType

Standard drag table (G1, G7, etc.) or custom drag data

required
weight Union[float, Weight]

Projectile weight in grains (default: 0)

0
diameter Union[float, Distance]

Projectile diameter in inches (default: 0)

0
length Union[float, Distance]

Projectile length in inches (default: 0)

0

Returns:

Type Description
DragModel

DragModel with interpolated drag coefficients based on multiple BCs

Example
from py_ballisticcalc.drag_tables import TableG7
DragModelMultiBC([BCPoint(.21, V=Velocity.FPS(1500)), BCPoint(.22, V=Velocity.FPS(2500))],
                 drag_table=TableG7)
Note

If weight and diameter are provided, BC is set to sectional density. Otherwise, BC=1 and the drag_table contains final drag terms. BC points are automatically sorted by Mach number for interpolation.

Source code in py_ballisticcalc/drag_model.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def DragModelMultiBC(bc_points: List[BCPoint],
                     drag_table: DragTableDataType,
                     weight: Union[float, Weight] = 0,
                     diameter: Union[float, Distance] = 0,
                     length: Union[float, Distance] = 0) -> DragModel:
    """Create a drag model with multiple ballistic coefficients.

    Constructs a DragModel using multiple BC measurements at different velocities,
    interpolating between them to create a more accurate drag function. This is
    useful for projectiles whose BC varies significantly with velocity.

    Args:
        bc_points: List of BCPoint objects with BC measurements at specific velocities
        drag_table: Standard drag table (G1, G7, etc.) or custom drag data
        weight: Projectile weight in grains (default: 0)
        diameter: Projectile diameter in inches (default: 0) 
        length: Projectile length in inches (default: 0)

    Returns:
        DragModel with interpolated drag coefficients based on multiple BCs

    Example:
        ```python
        from py_ballisticcalc.drag_tables import TableG7
        DragModelMultiBC([BCPoint(.21, V=Velocity.FPS(1500)), BCPoint(.22, V=Velocity.FPS(2500))],
                         drag_table=TableG7)
        ```

    Note:
        If weight and diameter are provided, BC is set to sectional density.
        Otherwise, BC=1 and the drag_table contains final drag terms.
        BC points are automatically sorted by Mach number for interpolation.
    """
    weight = PreferredUnits.weight(weight)
    diameter = PreferredUnits.diameter(diameter)
    if weight > 0 and diameter > 0:
        bc = sectional_density(weight >> Weight.Grain, diameter >> Distance.Inch)
    else:
        bc = 1.0

    drag_table = make_data_points(drag_table)  # Convert from list of dicts to list of DragDataPoints

    bc_points.sort(key=lambda p: p.Mach)  # Make sure bc_points are sorted for linear interpolation
    bc_interp = linear_interpolation([x.Mach for x in drag_table],
                                     [x.Mach for x in bc_points],
                                     [x.BC / bc for x in bc_points])

    for i, point in enumerate(drag_table):
        point.CD = point.CD / bc_interp[i]
    return DragModel(bc, drag_table, weight, diameter, length)
linear_interpolation
linear_interpolation(
    x: Union[List[float], Tuple[float]],
    xp: Union[List[float], Tuple[float]],
    yp: Union[List[float], Tuple[float]],
) -> Union[List[float], Tuple[float]]

Perform piecewise linear interpolation.

Interpolates y-values for given x-values using linear interpolation between known data points. Handles extrapolation by returning boundary values for x-values outside the range of xp.

Parameters:

Name Type Description Default
x Union[List[float], Tuple[float]]

List of points for which we want interpolated values

required
xp Union[List[float], Tuple[float]]

List of existing x-coordinates (must be sorted in ascending order)

required
yp Union[List[float], Tuple[float]]

List of corresponding y-values for existing points

required

Returns:

Type Description
Union[List[float], Tuple[float]]

List of interpolated y-values corresponding to input x-values

Raises:

Type Description
AssertionError

If xp and yp lists have different lengths

Note
  • For x-values below min(xp), returns yp[0]
  • For x-values above max(xp), returns yp[-1]
  • Uses binary search for efficient interval location
Source code in py_ballisticcalc/drag_model.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def linear_interpolation(x: Union[List[float], Tuple[float]],
                         xp: Union[List[float], Tuple[float]],
                         yp: Union[List[float], Tuple[float]]) -> Union[List[float], Tuple[float]]:
    """Perform piecewise linear interpolation.

    Interpolates y-values for given x-values using linear interpolation between known data points.
    Handles extrapolation by returning boundary values for x-values outside the range of xp.

    Args:
        x: List of points for which we want interpolated values
        xp: List of existing x-coordinates (must be sorted in ascending order)
        yp: List of corresponding y-values for existing points

    Returns:
        List of interpolated y-values corresponding to input x-values

    Raises:
        AssertionError: If `xp` and `yp` lists have different lengths

    Note:
        - For x-values below `min(xp)`, returns `yp[0]`
        - For x-values above `max(xp)`, returns `yp[-1]`
        - Uses binary search for efficient interval location
    """
    assert len(xp) == len(yp), "xp and yp lists must have same length"
    # Validate xp strictly increasing to prevent zero-division and undefined intervals
    for i in range(1, len(xp)):
        if xp[i] <= xp[i - 1]:
            raise ValueError("xp must be strictly increasing with no duplicates")

    y = []
    for xi in x:
        if xi <= xp[0]:
            y.append(yp[0])
        elif xi >= xp[-1]:
            y.append(yp[-1])
        else:
            # Binary search to find interval containing xi
            left, right = 0, len(xp) - 1
            while left < right:
                mid = (left + right) // 2
                if xp[mid] <= xi < xp[mid + 1]:
                    slope = (yp[mid + 1] - yp[mid]) / (xp[mid + 1] - xp[mid])
                    y.append(yp[mid] + slope * (xi - xp[mid]))  # Interpolated value for xi
                    break
                if xi < xp[mid]:
                    right = mid
                else:
                    left = mid + 1
            if left == right:
                y.append(yp[left])
    return y