@anatolyg's method is the correct first step. After that,
- apply
find_peaks
; - perform a first piecewise polynomial regression: first-degree in x and second-degree in y; then
- as a further step that I don't demonstrate, you need to enforce that the boundary positions match between each segment and then do an end-to-end fit where the physical parameters are shared across the whole dataset.
import matplotlib.pyplot as pltimport numpy as npimport scipy.signaldef fit( t: np.ndarray, x: np.ndarray, y: np.ndarray,) -> tuple[ list[np.polynomial.Polynomial], # x(t) list[np.polynomial.Polynomial], # y(t)]: # Second-order differential. This assumes a monotonic t. d2ydt2 = np.gradient(y, 2) # Boundary conditions for each bounce segment bounce_idx, props = scipy.signal.find_peaks(d2ydt2) left_indices = (0, *bounce_idx) right_indices = (*bounce_idx, len(t)) # Boolean arrays selecting each segment segment_predicates = [ (d2ydt2 < 0) # Must be falling& (t >= left) # Must be within second-order peaks& (t < right) for left, right in zip(left_indices, right_indices) ] x_polys = [ np.polynomial.polynomial.Polynomial.fit( x=t[predicate], y=x[predicate], symbol='t', deg=1, ) for predicate in segment_predicates ] y_polys = [ np.polynomial.polynomial.Polynomial.fit( x=t[predicate], y=y[predicate], symbol='t', deg=2, ) for predicate in segment_predicates ] return x_polys, y_polysdef dump( x_polys: list[np.polynomial.Polynomial], y_polys: list[np.polynomial.Polynomial],) -> None: for i, (xp, yp) in enumerate(zip(x_polys, y_polys)): print(f'Bounce {i}:') print(f' x={xp}') print(f' y={yp}')def plot( t: np.ndarray, x: np.ndarray, y: np.ndarray, x_polys: list[np.polynomial.Polynomial], y_polys: list[np.polynomial.Polynomial],) -> plt.Figure: fig, ax = plt.subplots() ax.scatter(x, y, marker='+', label='experiment') tfine = np.linspace(start=t[0], stop=t[-1], num=201) for xp, yp in zip(x_polys, y_polys): xt = xp(tfine) yt = yp(tfine) use = yt >= 0 ax.plot(xt[use], yt[use]) return figdef main() -> None: x = np.array(( 7.410000e-03, 9.591000e-02, 2.844100e-01, 5.729100e-01, 9.614100e-01, 1.449910e+00, 2.038410e+00, 2.726910e+00, 3.373700e+00, 4.040770e+00, 4.800040e+00, 5.577610e+00, 6.355180e+00, 7.132750e+00, 7.910320e+00, 8.687900e+00, 9.465470e+00, 1.020976e+01, 1.092333e+01, 1.163690e+01, 1.235047e+01, 1.306404e+01, 1.377762e+01, 1.449119e+01, )) y = np.array(( 2.991964, 2.903274, 2.716584, 2.431894, 2.049204, 1.568514, 0.989824, 0.313134, 0.311512, 0.646741, 0.88397 , 1.0232 , 1.064429, 1.007658, 0.852887, 0.600116, 0.249345, 0.232557, 0.516523, 0.702488, 0.790454, 0.78042 , 0.672385, 0.466351, )) t = np.arange(len(x), dtype=x.dtype) x_polys, y_polys = fit(t=t, x=x, y=y) dump(x_polys=x_polys, y_polys=y_polys) plot(t=t, x=x, y=y, x_polys=x_polys, y_polys=y_polys) plt.show()if __name__ == '__main__': main()
Bounce 0: x=1.01716 + 1.35975·(-1.0 + 0.28571429t) y=2.252799 - 1.339415·(-1.0 + 0.28571429t) - 0.60025·(-1.0 + 0.28571429t)²Bounce 1: x=7.910324 + 1.555146·(-7.0 + 0.5t) y=0.852887 - 0.407542·(-7.0 + 0.5t) - 0.196·(-7.0 + 0.5t)²Bounce 2: x=13.77761667 + 0.713575·(-22.0 + t) y=0.672385 - 0.1570345·(-22.0 + t) - 0.0489995·(-22.0 + t)²