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.

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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.

Functions

__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
146
147
148
149
150
151
152
153
154
155
156
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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.
    """
    pass
__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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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
246
247
248
249
250
251
252
253
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
255
256
257
258
259
260
261
262
263
264
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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))
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
284
285
286
287
288
289
290
291
292
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
294
295
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
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
347
348
349
350
@staticmethod
def iter_engines() -> Generator[EntryPoint, None, None]:
    """Iterate all available engines in the entry points."""
    yield from _EngineLoader.iter_engines()

_EngineLoader dataclass

_EngineLoader()

Methods:

Name Description
iter_engines

Iterate over all available engines in the entry points.

iter_engines classmethod

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

Iterate over all available engines in the entry points.

Source code in py_ballisticcalc/interface.py
55
56
57
58
59
60
61
@classmethod
def iter_engines(cls) -> Generator[EntryPoint, None, None]:
    """Iterate over all available engines in the entry points."""
    ballistic_entry_points = cls._get_entries_by_group()
    for ep in ballistic_entry_points:
        if ep.name.endswith(cls._entry_point_suffix):
            yield ep