Skip to content

Interface

Calculator dataclass

Calculator(
    config: Optional[ConfigT] = None,
    engine: EngineFactoryProtocolEntry = DEFAULT_ENTRY,
)

Bases: Generic[ConfigT]

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.

Methods:

Name Description
__post_init__

Loads the engine class.

__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.

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

__post_init__
__post_init__() -> 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.

Source code in py_ballisticcalc/interface.py
111
112
113
114
115
116
117
118
119
def __post_init__(self) -> 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._engine_class = _EngineLoader.load(self.engine)
__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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
191
192
193
194
195
196
197
198
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
200
201
202
203
204
205
206
207
208
209
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.__post_init__()
barrel_elevation_for_target
barrel_elevation_for_target(
    shot: Shot, target_distance: Union[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 Union[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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def barrel_elevation_for_target(self, shot: Shot, target_distance: Union[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: Union[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 Union[float, Distance]

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

required
Source code in py_ballisticcalc/interface.py
229
230
231
232
233
234
235
236
237
def set_weapon_zero(self, shot: Shot, zero_distance: Union[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: Union[float, Distance],
    trajectory_step: Optional[
        Union[float, Distance]
    ] = None,
    *,
    extra_data: bool = False,
    dense_output: bool = False,
    time_step: float = 0.0,
    flags: Union[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 Union[float, Distance]

Distance at which to stop computing the trajectory.

required
trajectory_step Optional[Union[float, Distance]]

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 Union[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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def fire(
    self,
    shot: Shot,
    trajectory_range: Union[float, Distance],
    trajectory_step: Optional[Union[float, Distance]] = None,
    *,
    extra_data: bool = False,
    dense_output: bool = False,
    time_step: float = 0.0,
    flags: Union[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
292
293
294
295
@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
53
54
55
56
57
58
59
@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