Skip to content

HitResult

HitResult dataclass

HitResult(
    props: ShotProps,
    trajectory: list[TrajectoryData],
    base_data: Optional[list[BaseTrajData]],
    extra: bool = False,
    error: Optional[RangeError] = None,
)

Computed trajectory data of the shot.

Attributes:

Name Type Description
shot

The parameters of the shot calculation.

trajectory list[TrajectoryData]

Computed TrajectoryData points.

base_data Optional[list[BaseTrajData]]

Base trajectory data points for interpolation.

extra bool

[DEPRECATED] Whether extra_data was requested.

error Optional[RangeError]

RangeError, if any.

Methods:

Name Description
flag

Get first TrajectoryData row with the specified flag.

get_at

Get TrajectoryData where key_attribute==value.

zeros

Get all zero crossing points.

index_at_distance

Deprecated. Use get_at() instead.

get_at_distance

Deprecated. Use get_at('distance', d) instead.

get_at_time

Deprecated. Use get_at('time', t) instead.

danger_space

Calculate the danger space for a target.

dataframe

Return the trajectory table as a DataFrame.

plot

Return a graph of the trajectory.

Functions

flag
flag(
    flag: Union[TrajFlag, int],
) -> Optional[TrajectoryData]

Get first TrajectoryData row with the specified flag.

Parameters:

Name Type Description Default
flag Union[TrajFlag, int]

The flag to search for.

required

Returns:

Type Description
Optional[TrajectoryData]

First TrajectoryData row with the specified flag.

Raises:

Type Description
AttributeError

If flag was not requested.

Source code in py_ballisticcalc/trajectory_data.py
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
def flag(self, flag: Union[TrajFlag, int]) -> Optional[TrajectoryData]:
    """Get first TrajectoryData row with the specified flag.

    Args:
        flag: The flag to search for.

    Returns:
        First TrajectoryData row with the specified flag.

    Raises:
        AttributeError: If flag was not requested.
    """
    self._check_flag(flag)
    for row in self.trajectory:
        if row.flag & flag:
            return row
    return None
get_at
get_at(
    key_attribute: TRAJECTORY_DATA_ATTRIBUTES,
    value: Union[float, GenericDimension],
    *,
    epsilon: float = 1e-09,
    start_from_time: float = 0.0,
) -> TrajectoryData

Get TrajectoryData where key_attribute==value.

Interpolates to create new object if necessary. Preserves the units of the original trajectory data.

Parameters:

Name Type Description Default
key_attribute TRAJECTORY_DATA_ATTRIBUTES

The name of the TrajectoryData attribute to key on (e.g., 'time', 'distance').

required
value Union[float, GenericDimension]

The value of the key attribute to find. If a float is provided for a dimensioned attribute, it's assumed to be a .raw_value.

required
epsilon float

Allowed key value difference to match existing TrajectoryData object without interpolating.

1e-09
start_from_time float

The time to center the search from (default is 0.0). If the target value is at a local extremum then the search will only go forward in time.

0.0

Returns:

Type Description
TrajectoryData

TrajectoryData where key_attribute==value.

Raises:

Type Description
AttributeError

If TrajectoryData doesn't have the specified attribute.

KeyError

If the key_attribute is 'flag'.

ValueError

If interpolation is required and len(self.trajectory) < 3.

ArithmeticError

If trajectory doesn't reach the requested value.

Notes
  • Not all attributes are monotonic: Height typically goes up and then down. Velocity typically goes down, but for lofted trajectories can begin to increase. Windage can wander back and forth in complex winds. We even have (see ExtremeExamples.ipynb) backward-bending scenarios in which distance reverses!
  • The only guarantee is that time is strictly increasing.
Source code in py_ballisticcalc/trajectory_data.py
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
def get_at(self, key_attribute: TRAJECTORY_DATA_ATTRIBUTES,
                 value: Union[float, GenericDimension], *,
                 epsilon: float = 1e-9,
                 start_from_time: float=0.0) -> TrajectoryData:
    """Get TrajectoryData where key_attribute==value.

    Interpolates to create new object if necessary. Preserves the units of the original trajectory data.

    Args:
        key_attribute: The name of the TrajectoryData attribute to key on (e.g., 'time', 'distance').
        value: The value of the key attribute to find. If a float is provided
               for a dimensioned attribute, it's assumed to be a .raw_value.
        epsilon: Allowed key value difference to match existing TrajectoryData object without interpolating.
        start_from_time: The time to center the search from (default is 0.0).  If the target value is
                         at a local extremum then the search will only go forward in time.

    Returns:
        TrajectoryData where key_attribute==value.

    Raises:
        AttributeError: If TrajectoryData doesn't have the specified attribute.
        KeyError: If the key_attribute is 'flag'.
        ValueError: If interpolation is required and len(self.trajectory) < 3.
        ArithmeticError: If trajectory doesn't reach the requested value.

    Notes:
        * Not all attributes are monotonic: Height typically goes up and then down.
            Velocity typically goes down, but for lofted trajectories can begin to increase.
            Windage can wander back and forth in complex winds. We even have (see ExtremeExamples.ipynb)
            backward-bending scenarios in which distance reverses!
        * The only guarantee is that time is strictly increasing.
    """
    key_attribute = TRAJECTORY_DATA_SYNONYMS.get(key_attribute, key_attribute)  # Resolve synonyms
    if not hasattr(TrajectoryData, key_attribute):
        raise AttributeError(f"TrajectoryData has no attribute '{key_attribute}'")
    if key_attribute == 'flag':
        raise KeyError("Cannot interpolate based on 'flag' attribute")

    traj = self.trajectory
    n = len(traj)
    key_value = value.raw_value if isinstance(value, GenericDimension) else value

    def get_key_val(td):
        """Helper to get the raw value of the key attribute from a TrajectoryData point."""
        val = getattr(td, key_attribute)
        return val.raw_value if hasattr(val, 'raw_value') else val

    if n < 3:  # We won't interpolate on less than 3 points, but check for an exact match in the existing rows.
        if abs(get_key_val(traj[0]) - key_value) < epsilon:
            return traj[0]
        if n > 1 and abs(get_key_val(traj[1]) - key_value) < epsilon:
            return traj[1]
        raise ValueError("Interpolation requires at least 3 TrajectoryData points.")

    # Find the starting index based on start_from_time
    start_idx = 0
    if start_from_time > 0:
        start_idx = next((i for i, td in enumerate(traj) if td.time >= start_from_time), 0)
    curr_val = get_key_val(traj[start_idx])
    if abs(curr_val - key_value) < epsilon:  # Check for exact match
        return traj[start_idx]
    # Determine search direction from the starting point
    search_forward = True  # Default to forward search
    if start_idx == n - 1:  # We're at the last point, search backwards            
        search_forward = False
    if 0 < start_idx < n - 1:
        # We're in the middle of the trajectory, determine local direction towards key_value
        next_val = get_key_val(traj[start_idx + 1])
        if (next_val > curr_val and key_value > curr_val) or (next_val < curr_val and key_value < curr_val):
            search_forward = True
        else:
            search_forward = False

    # Search for the target value in the determined direction
    target_idx = -1
    if search_forward:  # Search forward from start_idx            
        for i in range(start_idx, n - 1):
            curr_val = get_key_val(traj[i])
            next_val = get_key_val(traj[i + 1])
            # Check if key_value is between curr_val and next_val
            if ((curr_val < key_value <= next_val) or (next_val <= key_value < curr_val)):
                target_idx = i + 1
                break
    if not search_forward or target_idx == -1:  # Search backward from start_idx
        for i in range(start_idx, 0, -1):
            curr_val = get_key_val(traj[i])
            prev_val = get_key_val(traj[i - 1])
            # Check if key_value is between prev_val and curr_val
            if ((prev_val <= key_value < curr_val) or (curr_val < key_value <= prev_val)):
                target_idx = i
                break

    # Check if we found a valid index
    if target_idx == -1:
        raise ArithmeticError(f"Trajectory does not reach {key_attribute} = {value}")
    # Check for exact match here
    if abs(get_key_val(traj[target_idx]) - key_value) < epsilon:
        return traj[target_idx]
    if target_idx == 0:  # Step forward from first point so we can interpolate
        target_idx = 1
    # Choose three bracketing points (p0, p1, p2)
    if target_idx >= n - 1:  # At or after the last point
        p0, p1, p2 = traj[n - 3], traj[n - 2], traj[n - 1]
    else:
        p0, p1, p2 = traj[target_idx - 1], traj[target_idx], traj[target_idx + 1]
    return TrajectoryData.interpolate(key_attribute, value, p0, p1, p2)
zeros
zeros() -> list[TrajectoryData]

Get all zero crossing points.

Returns:

Type Description
list[TrajectoryData]

Zero crossing points.

Raises:

Type Description
AttributeError

If extra_data was not requested.

ArithmeticError

If zero crossing points are not found.

Source code in py_ballisticcalc/trajectory_data.py
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
def zeros(self) -> list[TrajectoryData]:
    """Get all zero crossing points.

    Returns:
        Zero crossing points.

    Raises:
        AttributeError: If extra_data was not requested.
        ArithmeticError: If zero crossing points are not found.
    """
    self._check_flag(TrajFlag.ZERO)
    data = [row for row in self.trajectory if row.flag & TrajFlag.ZERO]
    if len(data) < 1:
        raise ArithmeticError("Can't find zero crossing points")
    return data
index_at_distance
index_at_distance(d: Distance) -> int

Deprecated. Use get_at() instead.

Parameters:

Name Type Description Default
d Distance

Distance for which we want Trajectory Data.

required

Returns:

Type Description
int

Index of first trajectory row with .distance >= d; otherwise -1.

Source code in py_ballisticcalc/trajectory_data.py
861
862
863
864
865
866
867
868
869
870
871
872
873
@deprecated(reason="Use get_at() instead for better flexibility.")
def index_at_distance(self, d: Distance) -> int:
    """Deprecated. Use get_at() instead.

    Args:
        d: Distance for which we want Trajectory Data.

    Returns:
        Index of first trajectory row with .distance >= d; otherwise -1.
    """
    epsilon = 1e-1  # small value to avoid floating point issues
    return next((i for i in range(len(self.trajectory))
                 if self.trajectory[i].distance.raw_value >= d.raw_value - epsilon), -1)
get_at_distance
get_at_distance(d: Distance) -> TrajectoryData

Deprecated. Use get_at('distance', d) instead.

Parameters:

Name Type Description Default
d Distance

Distance for which we want Trajectory Data.

required

Returns:

Type Description
TrajectoryData

First trajectory row with .distance >= d.

Raises:

Type Description
ArithmeticError

If trajectory doesn't reach requested distance.

Source code in py_ballisticcalc/trajectory_data.py
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
@deprecated(reason="Use get_at('distance', d)")
def get_at_distance(self, d: Distance) -> TrajectoryData:
    """Deprecated. Use get_at('distance', d) instead.

    Args:
        d: Distance for which we want Trajectory Data.

    Returns:
        First trajectory row with .distance >= d.

    Raises:
        ArithmeticError: If trajectory doesn't reach requested distance.
    """
    if (i := self.index_at_distance(d)) < 0:
        raise ArithmeticError(
            f"Calculated trajectory doesn't reach requested distance {d}"
        )
    return self.trajectory[i]
get_at_time
get_at_time(t: float) -> TrajectoryData

Deprecated. Use get_at('time', t) instead.

Parameters:

Name Type Description Default
t float

Time for which we want Trajectory Data.

required

Returns:

Type Description
TrajectoryData

First trajectory row with .time >= t.

Raises:

Type Description
ArithmeticError

If trajectory doesn't reach requested time.

Source code in py_ballisticcalc/trajectory_data.py
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
@deprecated(reason="Use get_at('time', t)")
def get_at_time(self, t: float) -> TrajectoryData:
    """Deprecated. Use get_at('time', t) instead.

    Args:
        t: Time for which we want Trajectory Data.

    Returns:
        First trajectory row with .time >= t.

    Raises:
        ArithmeticError: If trajectory doesn't reach requested time.
    """
    epsilon = 1e-6  # small value to avoid floating point issues
    idx = next((i for i in range(len(self.trajectory))
                 if self.trajectory[i].time >= t - epsilon), -1)
    if idx < 0:
        raise ArithmeticError(
            f"Calculated trajectory doesn't reach requested time {t}"
        )
    return self.trajectory[idx]
danger_space
danger_space(
    at_range: Union[float, Distance],
    target_height: Union[float, Distance],
) -> DangerSpace

Calculate the danger space for a target.

Assumes that the trajectory hits the center of a target at any distance.
Determines how much ranging error can be tolerated if the critical region
of the target has target_height *h*. Finds how far forward and backward along the
line of sight a target can move such that the trajectory is still within *h*/2
of the original drop at_range.

Parameters:

Name Type Description Default
at_range Union[float, Distance]

Danger space is calculated for a target centered at this sight distance.

required
target_height Union[float, Distance]

Target height (h) determines danger space.

required

Returns:

Name Type Description
DangerSpace DangerSpace

The calculated danger space.

Raises:

Type Description
ArithmeticError

If trajectory doesn't reach requested distance.

Source code in py_ballisticcalc/trajectory_data.py
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
def danger_space(self,
                 at_range: Union[float, Distance],
                 target_height: Union[float, Distance],
                 ) -> DangerSpace:
    """Calculate the danger space for a target.

        Assumes that the trajectory hits the center of a target at any distance.
        Determines how much ranging error can be tolerated if the critical region
        of the target has target_height *h*. Finds how far forward and backward along the
        line of sight a target can move such that the trajectory is still within *h*/2
        of the original drop at_range.

    Args:
        at_range: Danger space is calculated for a target centered at this sight distance.
        target_height: Target height (*h*) determines danger space.

    Returns:
        DangerSpace: The calculated danger space.

    Raises:
        ArithmeticError: If trajectory doesn't reach requested distance.
    """
    target_at_range = PreferredUnits.distance(at_range)
    target_height = PreferredUnits.target_height(target_height)
    target_height_half = target_height.raw_value / 2.0

    target_row = self.get_at('slant_distance', target_at_range)
    is_climbing = target_row.angle.raw_value - self.props.look_angle.raw_value > 0
    slant_height_begin = target_row.slant_height.raw_value + (-1 if is_climbing else 1) * target_height_half
    slant_height_end = target_row.slant_height.raw_value - (-1 if is_climbing else 1) * target_height_half
    try:
        begin_row = self.get_at('slant_height', slant_height_begin, start_from_time=target_row.time)
    except ArithmeticError:
        begin_row = self.trajectory[0]
    try:
        end_row = self.get_at('slant_height', slant_height_end, start_from_time=target_row.time)
    except ArithmeticError:
        end_row = self.trajectory[-1]

    return DangerSpace(target_row,
                       target_height,
                       begin_row,
                       end_row,
                       self.props.look_angle)
dataframe
dataframe(formatted: bool = False) -> DataFrame

Return the trajectory table as a DataFrame.

Parameters:

Name Type Description Default
formatted bool

False for values as floats; True for strings in PreferredUnits. Default is False.

False

Returns:

Type Description
DataFrame

The trajectory table as a DataFrame.

Raises:

Type Description
ImportError

If pandas or plotting dependencies are not installed.

Source code in py_ballisticcalc/trajectory_data.py
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
def dataframe(self, formatted: bool = False) -> DataFrame:
    """Return the trajectory table as a DataFrame.

    Args:
        formatted: False for values as floats; True for strings in PreferredUnits. Default is False.

    Returns:
        The trajectory table as a DataFrame.

    Raises:
        ImportError: If pandas or plotting dependencies are not installed.
    """
    try:
        from py_ballisticcalc.visualize.dataframe import hit_result_as_dataframe
        return hit_result_as_dataframe(self, formatted)
    except ImportError as err:
        raise ImportError(
            "Use `pip install py_ballisticcalc[charts]` to get trajectory as pandas.DataFrame"
        ) from err
plot
plot(look_angle: Optional[Angular] = None) -> Axes

Return a graph of the trajectory.

Parameters:

Name Type Description Default
look_angle Optional[Angular]

Look angle for the plot. Defaults to None.

None

Returns:

Type Description
Axes

The plot Axes object.

Raises:

Type Description
ImportError

If plotting dependencies are not installed.

Source code in py_ballisticcalc/trajectory_data.py
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
def plot(self, look_angle: Optional[Angular] = None) -> Axes:
    """Return a graph of the trajectory.

    Args:
        look_angle (Optional[Angular], optional): Look angle for the plot. Defaults to None.

    Returns:
        The plot Axes object.

    Raises:
        ImportError: If plotting dependencies are not installed.
    """
    try:
        from py_ballisticcalc.visualize.plot import hit_result_as_plot  # type: ignore[attr-defined]
        return hit_result_as_plot(self, look_angle)
    except ImportError as err:
        raise ImportError(
            "Use `pip install py_ballisticcalc[charts]` to get results as a plot"
        ) from err

DangerSpace

Bases: NamedTuple

Stores the danger space data for distance specified.

Methods:

Name Description
overlay

Highlights danger-space region on plot.

Functions

overlay
overlay(ax: Axes, label: Optional[str] = None) -> None

Highlights danger-space region on plot.

Parameters:

Name Type Description Default
ax Axes

The axes to overlay on.

required
label Optional[str]

Label for the overlay. Defaults to None.

None

Raises:

Type Description
ImportError

If plotting dependencies are not installed.

Source code in py_ballisticcalc/trajectory_data.py
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
def overlay(self, ax: Axes, label: Optional[str] = None) -> None:
    """Highlights danger-space region on plot.

    Args:
        ax: The axes to overlay on.
        label: Label for the overlay. Defaults to None.

    Raises:
        ImportError: If plotting dependencies are not installed.
    """
    try:
        from py_ballisticcalc.visualize.plot import add_danger_space_overlay  # type: ignore[attr-defined]
        add_danger_space_overlay(self, ax, label)
    except ImportError as err:
        raise ImportError(
            "Use `pip install py_ballisticcalc[charts]` to get results as a plot"
        ) from err