Skip to content

Interface

Calculator

Calculator(
    *,
    config: ConfigT,
    engine: EngineFactoryProtocol[ConfigT],
)
Calculator(
    *, config: Any = None, engine: str | None = None
)
Calculator(
    *,
    config: Any = None,
    engine: EngineFactoryProtocolEntry = None,
)

The main interface for the ballistics calculator.

This class provides thread-safe access to the underlying integration engines by creating a new, isolated engine instance for every method call.

Crucially: The engine instance is not created here. To ensure thread safety (especially in free-threaded Python), each method call must operate on a new, isolated engine instance.

Methods:

Name Description
__enter__

Enter the runtime context for this Calculator.

__exit__

Exit the runtime context.

__getattr__

Delegate attribute access to the underlying engine instance.

__getstate__

Called by pickle for serialization.

__setstate__

Called by pickle for deserialization.

barrel_elevation_for_target

Calculate barrel elevation to hit target at zero_distance.

aim

Calculate a target's zero solution and retain its trajectory point.

aiming_solution_for_target

Return :meth:aim's solution for callers using the descriptive API name.

set_weapon_zero

Set shot.weapon.zero_elevation so that it hits a target at zero_distance.

fire

Calculate the trajectory for the given shot parameters.

iter_engines

Iterate all available engines in the entry points.

Source code in py_ballisticcalc/interface.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def __init__(
    self,
    *,
    config: Any = None,
    engine: EngineFactoryProtocolEntry = None,
) -> None:
    """
    Loads the engine class.

    Crucially: The engine instance is not created here. To ensure
    thread safety (especially in free-threaded Python), each method call
    must operate on a new, isolated engine instance.
    """
    self.config = config
    self.engine = engine
    self._engine_factory = _EngineLoader.load(self.engine)

Attributes

_engine_instance property
_engine_instance: EngineProtocol

Creates and returns a fresh, isolated engine instance upon every access.

This implementation is the core mechanism for ensuring thread safety in the Calculator class, particularly essential in free-threaded Python (e.g., CPython with GIL disabled, Python 3.13+).

Thread Safety Rationale

Instead of using traditional synchronization primitives (like threading.Lock) to protect a single, shared engine instance, this method employs isolation. Since the underlying engine instances are not guaranteed to be thread-safe internally, generating a new instance for each operation ensures that no two concurrent threads will ever modify the same engine object. This approach eliminates race conditions without introducing the overhead or potential deadlocks associated with locking mechanisms.

Performance Consideration

Note: The overall performance of concurrent operations critically depends on the initialization cost of the underlying engine class (self._engine_class). If the engine's constructor performs extensive I/O, loads large data tables, or executes complex setup, repeated instantiation may introduce significant overhead. For optimal performance, the engine's initialization (__init__) should be designed to be as lightweight as possible.

Returns:

Type Description
EngineProtocol

EngineProtocol[Any]: A new, single-use engine instance configured with the Calculator's current settings.

Methods:

__enter__
__enter__() -> Self

Enter the runtime context for this Calculator.

Returns:

Name Type Description
Self Self

The Calculator instance.

Example

with Calculator(config, RK4IntegrationEngine) as calc: ... result = calc.fire(shot, Distance.Meter(1000))

Source code in py_ballisticcalc/interface.py
234
235
236
237
238
239
240
241
242
243
244
def __enter__(self) -> Self:
    """Enter the runtime context for this Calculator.

    Returns:
        Self: The Calculator instance.

    Example:
        >>> with Calculator(config, RK4IntegrationEngine) as calc:
        ...     result = calc.fire(shot, Distance.Meter(1000))
    """
    return self
__exit__
__exit__(
    exc_type: type[BaseException] | None,
    exc_val: BaseException | None,
    exc_tb: TracebackType | None,
) -> None

Exit the runtime context.

This is a no-op as Calculator is stateless and thread-safe by design — each method call creates an isolated engine instance.

Parameters:

Name Type Description Default
exc_type type[BaseException] | None

Exception type if an exception was raised, None otherwise.

required
exc_val BaseException | None

Exception instance if an exception was raised, None otherwise.

required
exc_tb TracebackType | None

Traceback if an exception was raised, None otherwise.

required
Source code in py_ballisticcalc/interface.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def __exit__(
    self,
    exc_type: type[BaseException] | None,
    exc_val: BaseException | None,
    exc_tb: TracebackType | None,
) -> None:
    """Exit the runtime context.

    This is a no-op as Calculator is stateless and thread-safe
    by design — each method call creates an isolated engine instance.

    Args:
        exc_type: Exception type if an exception was raised, None otherwise.
        exc_val: Exception instance if an exception was raised, None otherwise.
        exc_tb: Traceback if an exception was raised, None otherwise.
    """
__getattr__
__getattr__(item: str) -> Any

Delegate attribute access to the underlying engine instance.

This method is called when an attribute is requested on the Calculator instance that is not found through normal attribute lookup (i.e., it's not a direct attribute of Calculator or its class). It then attempts to retrieve the attribute from the _engine_instance.

Parameters:

Name Type Description Default
item str

The name of the attribute to retrieve.

required

Returns:

Name Type Description
Any Any

The value of the attribute from _engine_instance.

Raises:

Type Description
AttributeError

If the attribute is not found on either the Calculator object or its _engine_instance.

Examples:

>>> calc = Calculator(engine=DEFAULT_ENTRY)
>>> calc_step = calc.get_calc_step()
>>> print(calc_step)
0.0025
>>> try:
...     calc.unknown_method()
... except AttributeError as e:
...     print(e)
'Calculator' object or its underlying engine 'RK4IntegrationEngine' has no attribute 'unknown_method'
Source code in py_ballisticcalc/interface.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def __getattr__(self, item: str) -> Any:
    """Delegate attribute access to the underlying engine instance.

    This method is called when an attribute is requested on the `Calculator`
    instance that is not found through normal attribute lookup (i.e., it's
    not a direct attribute of `Calculator` or its class). It then attempts
    to retrieve the attribute from the `_engine_instance`.

    Args:
        item: The name of the attribute to retrieve.

    Returns:
        Any: The value of the attribute from `_engine_instance`.

    Raises:
        AttributeError: If the attribute is not found on either the
            `Calculator` object or its `_engine_instance`.

    Examples:
        >>> calc = Calculator(engine=DEFAULT_ENTRY)
        >>> calc_step = calc.get_calc_step()
        >>> print(calc_step)
        0.0025
        >>> try:
        ...     calc.unknown_method()
        ... except AttributeError as e:
        ...     print(e)
        'Calculator' object or its underlying engine 'RK4IntegrationEngine' has no attribute 'unknown_method'
    """
    engine_instance = self._engine_instance
    if hasattr(engine_instance, item):
        return getattr(engine_instance, item)
    raise AttributeError(
        f"'{self.__class__.__name__}' object or its underlying engine "
        f"'{engine_instance.__class__.__name__}' has no attribute '{item}'"
    )
__getstate__
__getstate__()

Called by pickle for serialization. We only serialize the public fields required for reconstruction. We explicitly exclude the calculated fields like _engine_class to ensure proper re-initialization in the new process.

Source code in py_ballisticcalc/interface.py
333
334
335
336
337
338
339
340
def __getstate__(self):
    """
    Called by pickle for serialization.
    We only serialize the public fields required for reconstruction.
    We explicitly exclude the calculated fields like _engine_class
    to ensure proper re-initialization in the new process.
    """
    return {"config": self.config, "engine": self.engine}
__setstate__
__setstate__(state)

Called by pickle for deserialization. We manually set the fields and call post_init to reload the engine class.

Source code in py_ballisticcalc/interface.py
342
343
344
345
346
347
348
349
350
351
def __setstate__(self, state):
    """
    Called by pickle for deserialization.
    We manually set the fields and call __post_init__ to reload the engine class.
    """
    # Set the serialized fields
    self.config = state["config"]
    self.engine = state["engine"]
    # Manually run __post_init__ to load the _engine_class
    self._engine_factory = _EngineLoader.load(self.engine)
barrel_elevation_for_target
barrel_elevation_for_target(
    shot: Shot, target_distance: float | Distance
) -> Angular

Calculate barrel elevation to hit target at zero_distance.

Parameters:

Name Type Description Default
shot Shot

Shot instance we want to zero.

required
target_distance float | Distance

Look-distance to "zero," which is point we want to hit. This is the distance that a rangefinder would return with no ballistic adjustment.

required
Note

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.

Source code in py_ballisticcalc/interface.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def barrel_elevation_for_target(self, shot: Shot, target_distance: float | Distance) -> Angular:
    """Calculate barrel elevation to hit target at zero_distance.

    Args:
        shot: Shot instance we want to zero.
        target_distance: Look-distance to "zero," which is point we want to hit.
            This is the distance that a rangefinder would return with no ballistic adjustment.

    Note:
        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.
    """
    target_distance = PreferredUnits.distance(target_distance)
    total_elevation = self._engine_instance.zero_angle(shot, target_distance)
    return Angular.Radian((total_elevation >> Angular.Radian) - (shot.look_angle >> Angular.Radian))
aim
aim(
    shot: Shot, target_distance: float | Distance
) -> tuple[Angular, Angular, TrajectoryData]

Calculate a target's zero solution and retain its trajectory point.

Parameters:

Name Type Description Default
shot Shot

Shot instance to solve.

required
target_distance float | Distance

Look-distance to the target. This is the distance that a rangefinder would return with no ballistic adjustment.

required

Returns:

Type Description
Angular

The vertical hold relative to the weapon's zero, the windage

Angular

angle, and the trajectory point evaluated by the successful

TrajectoryData

zero-finding iteration. The windage angle uses the sign

tuple[Angular, Angular, TrajectoryData]

convention of :attr:TrajectoryData.windage_angle.

Raises:

Type Description
NotImplementedError

If the selected engine has no zero_point API.

Source code in py_ballisticcalc/interface.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def aim(self, shot: Shot, target_distance: float | Distance) -> tuple[Angular, Angular, TrajectoryData]:
    """Calculate a target's zero solution and retain its trajectory point.

    Args:
        shot: Shot instance to solve.
        target_distance: Look-distance to the target.
            This is the distance that a rangefinder would return with no ballistic adjustment.

    Returns:
        The vertical hold relative to the weapon's zero, the windage
        angle, and the trajectory point evaluated by the successful
        zero-finding iteration.  The windage angle uses the sign
        convention of :attr:`TrajectoryData.windage_angle`.

    Raises:
        NotImplementedError: If the selected engine has no ``zero_point`` API.
    """
    target_distance = PreferredUnits.distance(target_distance)
    total_elevation, point = self._engine_instance.zero_point(shot, target_distance)
    target_zero_elevation = Angular.Radian(
        (total_elevation >> Angular.Radian) - (shot.look_angle >> Angular.Radian)
    )
    vertical_hold = Angular.Radian(
        (target_zero_elevation >> Angular.Radian) - (shot.weapon.zero_elevation >> Angular.Radian)
    )
    return vertical_hold, point.windage_angle, point
aiming_solution_for_target
aiming_solution_for_target(
    shot: Shot, target_distance: float | Distance
) -> tuple[Angular, Angular, TrajectoryData]

Return :meth:aim's solution for callers using the descriptive API name.

Parameters:

Name Type Description Default
shot Shot

Shot instance to solve.

required
target_distance float | Distance

Look-distance to the target.

required

Returns:

Type Description
Angular

The vertical hold relative to the weapon's zero, windage angle,

Angular

and terminal trajectory point.

Source code in py_ballisticcalc/interface.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def aiming_solution_for_target(
    self, shot: Shot, target_distance: float | Distance
) -> tuple[Angular, Angular, TrajectoryData]:
    """Return :meth:`aim`'s solution for callers using the descriptive API name.

    Args:
        shot: Shot instance to solve.
        target_distance: Look-distance to the target.

    Returns:
        The vertical hold relative to the weapon's zero, windage angle,
        and terminal trajectory point.
    """
    return self.aim(shot, target_distance)
set_weapon_zero
set_weapon_zero(
    shot: Shot, zero_distance: float | Distance
) -> Angular

Set shot.weapon.zero_elevation so that it hits a target at zero_distance.

Parameters:

Name Type Description Default
shot Shot

Shot instance to zero.

required
zero_distance float | Distance

Look-distance to "zero," which is point we want to hit.

required
Source code in py_ballisticcalc/interface.py
413
414
415
416
417
418
419
420
421
def set_weapon_zero(self, shot: Shot, zero_distance: float | Distance) -> Angular:
    """Set shot.weapon.zero_elevation so that it hits a target at zero_distance.

    Args:
        shot: Shot instance to zero.
        zero_distance: Look-distance to "zero," which is point we want to hit.
    """
    shot.weapon.zero_elevation = self.barrel_elevation_for_target(shot, zero_distance)
    return shot.weapon.zero_elevation
fire
fire(
    shot: Shot,
    trajectory_range: float | Distance,
    trajectory_step: float | Distance | None = None,
    *,
    extra_data: bool = False,
    dense_output: bool = False,
    time_step: float = 0.0,
    flags: TrajFlag | int = NONE,
    raise_range_error: bool = True,
) -> HitResult

Calculate the trajectory for the given shot parameters.

Parameters:

Name Type Description Default
shot Shot

Shot parameters, including position and barrel angle.

required
trajectory_range float | Distance

Distance at which to stop computing the trajectory.

required
trajectory_step float | Distance | None

Distance between recorded trajectory points. Defaults to trajectory_range.

None
extra_data bool

[DEPRECATED] Requests flags=TrajFlags.ALL and trajectory_step=PreferredUnits.distance(1).

False
dense_output bool

HitResult stores all calculation steps so it can interpolate any point.

False
time_step float

Maximum time between recorded points. If > 0, points are recorded at least this frequently. Defaults to 0.0.

0.0
flags TrajFlag | int

Flags for specific points of interest. Defaults to TrajFlag.NONE.

NONE
raise_range_error bool

If True, raises RangeError if returned by integration.

True

Returns:

Name Type Description
HitResult HitResult

Object containing computed trajectory.

Source code in py_ballisticcalc/interface.py
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
def fire(
    self,
    shot: Shot,
    trajectory_range: float | Distance,
    trajectory_step: float | Distance | None = None,
    *,
    extra_data: bool = False,
    dense_output: bool = False,
    time_step: float = 0.0,
    flags: TrajFlag | int = TrajFlag.NONE,
    raise_range_error: bool = True,
) -> HitResult:
    """Calculate the trajectory for the given shot parameters.

    Args:
        shot: Shot parameters, including position and barrel angle.
        trajectory_range: Distance at which to stop computing the trajectory.
        trajectory_step: Distance between recorded trajectory points. Defaults to `trajectory_range`.
        extra_data: [DEPRECATED] Requests flags=TrajFlags.ALL and trajectory_step=PreferredUnits.distance(1).
        dense_output: HitResult stores all calculation steps so it can interpolate any point.
        time_step: Maximum time between recorded points. If > 0, points are recorded at least this frequently.
                   Defaults to 0.0.
        flags: Flags for specific points of interest. Defaults to TrajFlag.NONE.
        raise_range_error: If True, raises RangeError if returned by integration.

    Returns:
        HitResult: Object containing computed trajectory.
    """
    trajectory_range = PreferredUnits.distance(trajectory_range)
    dist_step = trajectory_range
    filter_flags = flags
    if trajectory_step:
        dist_step = PreferredUnits.distance(trajectory_step)
        filter_flags |= TrajFlag.RANGE
        if dist_step.raw_value > trajectory_range.raw_value:
            dist_step = trajectory_range

    if extra_data:
        warnings.warn(
            "extra_data is deprecated and will be removed in future versions. "
            "Explicitly specify desired TrajectoryData frequency and flags.",
            DeprecationWarning,
        )
        dist_step = PreferredUnits.distance(1.0)  # << For compatibility with v2.1
        filter_flags = TrajFlag.ALL

    result = self._engine_instance.integrate(
        shot, trajectory_range, dist_step, time_step, filter_flags, dense_output=dense_output
    )
    if result.error and raise_range_error:
        raise result.error
    return result
iter_engines staticmethod
iter_engines() -> Generator[EntryPoint, None, None]

Iterate all available engines in the entry points.

Source code in py_ballisticcalc/interface.py
476
477
478
479
@staticmethod
def iter_engines() -> Generator[EntryPoint, None, None]:
    """Iterate all available engines in the entry points."""
    yield from _EngineLoader.iter_engines()

_EngineLoader dataclass

_EngineLoader()

Discovers and loads engine factories.

Supported entry point layouts
  • New: group py_ballisticcalc.engines.<engine> with name <method>, addressed as "<engine>+<method>" or "<engine>.<method>" (e.g. "cython+rk4").
  • Legacy (temporary): group py_ballisticcalc with names like rk4_engine. Entries duplicating a new-style entry (same target) are skipped.
  • Direct: "<module>:<factory>" path.

Methods:

Name Description
engine_id

Public identifier of an entry point: <engine>+<method> for new-style, plain name for legacy.

iter_engines

Iterate over all available engines (new-style first, legacy deduplicated by target).

engine_id classmethod

engine_id(ep: EntryPoint) -> str

Public identifier of an entry point: <engine>+<method> for new-style, plain name for legacy.

Source code in py_ballisticcalc/interface.py
79
80
81
82
83
84
@classmethod
def engine_id(cls, ep: EntryPoint) -> str:
    """Public identifier of an entry point: ``<engine>+<method>`` for new-style, plain name for legacy."""
    if ep.group.startswith(cls._engines_group_prefix):
        return f"{ep.group.removeprefix(cls._engines_group_prefix)}+{ep.name}"
    return ep.name

iter_engines classmethod

iter_engines() -> Generator[EntryPoint, None, None]

Iterate over all available engines (new-style first, legacy deduplicated by target).

Source code in py_ballisticcalc/interface.py
86
87
88
89
90
91
92
93
94
95
96
@classmethod
def iter_engines(cls) -> Generator[EntryPoint, None, None]:
    """Iterate over all available engines (new-style first, legacy deduplicated by target)."""
    seen: set[str] = set()
    for ep in cls._get_engine_entries():
        seen.add(ep.value)
        yield ep
    for ep in sorted(cls._get_entries_by_group(), key=lambda e: e.name):
        if ep.name.endswith(cls._entry_point_suffix) and ep.value not in seen:
            seen.add(ep.value)
            yield ep