Skip to content

HitResult

HitResult provides the complete chronological output stream in records (what len(), iteration, indexing, dataframe(), and plot() all use), a stable scheduled-sample table in samples, and exact physical event roots in events. trajectory is a deprecated alias for records. See Trajectory Data for the output-view contract and examples.

HitResult dataclass

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

Computed trajectory data of the shot.

Attributes:

Name Type Description
shot

The parameters of the shot calculation.

records list[TrajectoryData]

Exact, chronological output records produced by the integrator. Default source for len(), iteration, indexing, dataframe(), and plot().

samples list[TrajectoryData]

Deterministic scheduled-sample table with nearby event flags annotated; its cardinality tracks the requested RANGE/TIME schedule regardless of the integrator's internal step choices, so it is the source to use when comparing results across engines or tolerances.

events list[TrajectoryData]

Exact physical event records (ZERO, MACH, APEX, and MRT).

trajectory list[TrajectoryData]

[DEPRECATED] Alias for records. Use records (exact stream) or samples (deterministic scheduled table) instead.

base_data list[BaseTrajData] | None

Flat sequence of accepted-step points (dense_output engines only), for local interpolation between adjacent pairs.

extra bool

[DEPRECATED] Whether extra_data was requested.

error RangeError | None

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.

Source code in py_ballisticcalc/trajectory_data.py
890
891
892
893
894
895
896
897
898
899
900
901
902
def __init__(
    self,
    props: ShotProps,
    records: list[TrajectoryData],
    base_data: list[BaseTrajData] | None = None,
    extra: bool = False,
    error: RangeError | None = None,
):
    object.__setattr__(self, "props", props)
    object.__setattr__(self, "records", records)
    object.__setattr__(self, "base_data", base_data)
    object.__setattr__(self, "extra", extra)
    object.__setattr__(self, "error", error)

events cached property

Return exact physical event records without output-table coalescing.

samples cached property

samples: list[TrajectoryData]

Return the deterministic scheduled-sample table.

Physical events remain exact in :attr:events; this presentation view annotates a scheduled sample with an event's flag only when the two are, to floating-point precision, the same instant (see :func:math.isclose's use below) — e.g. a RANGE sample requested at the same distance a zero was set for, which the integrator reaches by two different numerical paths that agree to solver residual. It never annotates merely the nearest sample when no sample is actually that close: with a coarse schedule (e.g. trajectory_step == trajectory_range, leaving only the launch and terminal samples) an APEX or ZERO in between is not close to either endpoint, so it would otherwise get glued onto whichever endpoint bisection happens to prefer — misrepresenting that endpoint's own state as the event's. Cardinality always equals the sampling schedule's, regardless of whether any annotation occurs, so it is stable across engines and solver tolerances even though which samples get annotated is not.

trajectory property

trajectory: list[TrajectoryData]

Deprecated alias for :attr:records.

flag

flag(flag: TrajFlag | int) -> TrajectoryData | None

Get first TrajectoryData row with the specified flag.

Parameters:

Name Type Description Default
flag TrajFlag | int

The flag to search for.

required

Returns:

Type Description
TrajectoryData | None

First TrajectoryData row with the specified flag.

Raises:

Type Description
AttributeError

If flag was not requested.

Source code in py_ballisticcalc/trajectory_data.py
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
def flag(self, flag: TrajFlag | int) -> TrajectoryData | None:
    """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)
    event_flags = TrajFlag.ZERO | TrajFlag.MACH | TrajFlag.APEX | TrajFlag.MRT
    rows = self.events if flag & event_flags else self.records
    for row in rows:
        if row.flag & flag:
            return row
    return None

get_at

get_at(
    key_attribute: TrajectoryDataAttribute,
    value: 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 TrajectoryDataAttribute

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

required
value 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'.

ArithmeticError

If the trajectory doesn't reach the requested value, including when fewer than 2 of self.records bracket it (3 are needed for PCHIP; with exactly 2 bracketing points a linear fallback is used instead of raising).

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
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
def get_at(
    self,
    key_attribute: TrajectoryDataAttribute,
    value: 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'.
        ArithmeticError: If the trajectory doesn't reach the requested value, including when
            fewer than 2 of self.records bracket it (3 are needed for PCHIP; with exactly 2
            bracketing points a linear fallback is used instead of raising).

    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")

    # ``samples`` is a presentation table of scheduled samples.  It may
    # coalesce physical events onto nearby RANGE rows, so it must not be
    # the fallback source for a numerical query.  ``base_data`` above is
    # preferred because it supplies the integrator's local interpolant;
    # without it, retain the exact record stream instead.
    traj = self.records
    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

    # Some extension engines expose a different sequence type (e.g. a
    # Cython CythonizedBaseTrajSeq with its own get_at()) rather than a
    # flat list of BaseTrajData; only the latter supports the local
    # Hermite interpolation below, built on demand from each adjacent
    # pair via TrajectoryStep -- base_data itself stays a flat, honest
    # point sequence rather than pre-materialising overlapping pairs.
    if self.base_data and isinstance(self.base_data[0], BaseTrajData):

        def step_key(data: BaseTrajData) -> float:
            return get_key_val(TrajectoryData.from_base_data(self.props, data))

        for start, end in zip(self.base_data, self.base_data[1:]):
            if end.time < start_from_time:
                continue
            step = TrajectoryStep(start, end)
            start_value = step_key(start)
            end_value = step_key(end)
            if start.time >= start_from_time and abs(start_value - key_value) < epsilon:
                return TrajectoryData.from_base_data(self.props, start)
            if abs(end_value - key_value) < epsilon:
                return TrajectoryData.from_base_data(self.props, end)
            if (start_value < key_value < end_value) or (end_value < key_value < start_value):
                if key_attribute == "time":
                    data = step.at_time(key_value)
                else:
                    data = step.at_value(step_key, key_value)
                if data.time >= start_from_time:
                    return TrajectoryData.from_base_data(self.props, data)
        raise ArithmeticError(f"Trajectory does not reach {key_attribute} = {value}")

    if n < 3:
        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]
        if n == 2:
            first_value = get_key_val(traj[0])
            second_value = get_key_val(traj[1])
            if (first_value < key_value < second_value) or (second_value < key_value < first_value):
                # ``TrajectoryData.interpolate(..., method='linear')``
                # selects its first segment below the middle point; repeating
                # the second point therefore supplies a two-point fallback
                # without inventing a third sample for PCHIP.
                return TrajectoryData.interpolate(key_attribute, value, traj[0], traj[1], traj[1], method="linear")
        raise ArithmeticError(f"Trajectory does not reach {key_attribute} = {value}")

    # 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:
        # key_value may be within epsilon of a point (endpoint or local extremum, e.g. trajectory
        # apex) that fell just outside every bracket above; O(n) scan is fine on this error path
        closest_point = min(traj, key=lambda pt: abs(get_key_val(pt) - key_value))
        if abs(get_key_val(closest_point) - key_value) < epsilon:
            return closest_point
        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
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
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.events 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
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
@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.records)) if self.records[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
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
@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.records[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
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
@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.records)) if self.records[i].time >= t - epsilon), -1)
    if idx < 0:
        raise ArithmeticError(f"Calculated trajectory doesn't reach requested time {t}")
    return self.records[idx]

danger_space

danger_space(
    at_range: float | Distance,
    target_height: 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 float | Distance

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

required
target_height 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
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
def danger_space(
    self,
    at_range: float | Distance,
    target_height: 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 >> Angular.Radian) - self.props.look_angle_rad) > 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.records[0]
    try:
        end_row = self.get_at("slant_height", slant_height_end, start_from_time=target_row.time)
    except ArithmeticError:
        end_row = self.records[-1]

    return DangerSpace(target_row, target_height, begin_row, end_row, Angular.Radian(self.props.look_angle_rad))

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
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
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: Angular | None = None) -> Axes

Return a graph of the trajectory.

Parameters:

Name Type Description Default
look_angle Angular | None

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
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
def plot(self, look_angle: Angular | None = None) -> Axes:
    """Return a graph of the trajectory.

    Args:
        look_angle (Angular | None, 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.

overlay

overlay(ax: Axes, label: str | None = None) -> None

Highlights danger-space region on plot.

Parameters:

Name Type Description Default
ax Axes

The axes to overlay on.

required
label str | None

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
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
def overlay(self, ax: Axes, label: str | None = 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