Domain: Physics — Physics (general)

Problem:
In atmospheric-entry research and in the qualification of high-performance launch systems, vertically fired experimental projectiles are routinely used to validate aerothermodynamic models under realistic atmospheric conditions. Even before reaching high altitudes, such projectiles traverse layers in which gravity, density, temperature, viscosity, sound speed and wind intensity vary significantly with height, strongly influencing the predicted peak altitude and the instant at which upward motion ceases.

Consider one such experimental launch, performed at sea level, involving a projectile of mass  

\(m = 10.0\,\mathrm{kg}\), diameter \(D = 0.20\,\mathrm{m}\), and frontal area \(A = 0.10\,\mathrm{m^{2}}\).  
The projectile is released vertically with initial velocity  

\(v_{0} = 1500.0\,\mathrm{m\,s^{-1}}\) at initial altitude  
\(x_{0} = 0.0\,\mathrm{m}\).  

The objective is to determine, with high temporal resolution, the instant at which the projectile reaches the top of its trajectory.

The local gravitational field decreases with altitude \(x\) according to  

\[g(x) = g_{0}\!\left(\frac{R}{R + x}\right)^{2},\]

where  

\(g_{0} = 9.81\,\mathrm{m\,s^{-2}}\)  

and  

\(R = 6.371\times10^{6}\,\mathrm{m}\).

Ambient density is described by the standard exponential law  

\[\rho(x) = \rho_{0}\,e^{-x/h},\]

with  

\(\rho_{0} = 1.225\,\mathrm{kg\,m^{-3}}\)  


and  

\(h = 8500.0\,\mathrm{m}\).

Temperature decreases linearly with height up to the tropopause:

\[T(x) = \max\{\,1,\; T_{0} - Lx\,\},\]

where  

\(T_{0} = 288.0\,\mathrm{K}\)  

and  

\(L = 0.0065\,\mathrm{K\,m^{-1}}\).

The speed of sound is
\[a(x) = \sqrt {\gamma\,R_{\mathrm{air}}\,T(x)},\]

with  

\(\gamma = 1.4\)  

and  

\(R_{\mathrm{air}} = 287.0\,\mathrm{J\,kg^{-1}\,K^{-1}}\).

Dynamic viscosity follows Sutherland’s law,

\[\mu(T) = \mu_{0}\left(\frac{T}{T_{0}}\right)^{3/2}\!\frac{T_{0}+S}{T+S},\]

with  

\(\mu_{0}=1.8\times10^{-5}\,\mathrm{Pa\,s}\)  

and  
\(S = 110.4\,\mathrm{K}\).

The vertical wind field varies with altitude according to  

\[w(x) = w_{0} + \mathrm{shear}\; x\,e^{-x/H_{w}},\]

where  

\(w_{0}=2.5\,\mathrm{m\,s^{-1}}\),  
\(\mathrm{shear}=1.0\),  

and  

\(H_{w}=10\,000.0\,\mathrm{m}\).

The relative velocity between projectile and air is  
\(v_{\mathrm{rel}} = v - w(x)\).  
The associated Mach number is  
\(M = |v_{\mathrm{rel}}|/a(x)\).  

The Reynolds number is  

\[\mathrm{Re} = \frac{\rho(x)\,|v_{\mathrm{rel}}|\,D}{\mu(T)}.\]

The aerodynamic drag coefficient depends simultaneously on Mach and Reynolds numbers:

\[C_{D}(M,\mathrm{Re})
= \max\!\Bigl(
C_{D,\min},\;
C_{D0}\bigl(1+\alpha M^{2}+\tfrac{\beta}{2}[1+\tanh(\tfrac{M-1}{\delta})]\bigr)
\bigl[1+\tfrac{k_{\mathrm{Re}}}{\sqrt{\mathrm{Re}+\varepsilon}}\bigr]^{-1}
\Bigr),\]

with parameters  

\[C_{D0}=0.47,\qquad
\alpha=0.20,\qquad
\beta=0.40,\qquad
\delta=0.08,\qquad
k_{\mathrm{Re}}=5.0,\qquad
C_{D,\min}=0.05,\]

and numerical safeguard  
\(\varepsilon = 10^{-12}\)  
applied only inside the Reynolds-correction term.

A numerical simulation must be performed using a classical fourth-order Runge–Kutta method with constant time step  

\[dt = 0.05\,\mathrm{s}.\]

The integration begins at  

\(t=0\),  
\(x(0)=x_{0}\),  
\(v(0)=v_{0}\)  

and proceeds until the first sign change of the velocity from positive to non-positive, indicating the instant at which the projectile reaches its apex.

Immediately before the zero crossing, the last four pairs \((v_i,t_i)\) obtained during the integration shall be retained. A cubic Lagrange interpolant of \(t(v)\) is then constructed using these four points, and the value of this interpolant evaluated at \(v=0\) provides the refined estimate of the apex time.

Implement the described procedure in Python and print, as final output, only the apex time with three decimal places and no units.

Answer: \(81.902\)

Solution code:
import math

def projectile_ascent_rk4_drag_complex(
    dt, v0, x0,
    g0,
    Cd0, A, m, D,                # drag baseline & geometry
    rho0, h,                     # density model
    gamma, R_air, T0, L,         # sound speed / temperature model
    mu0, S,                      # Sutherland viscosity
    w0, shear, Hw                # wind profile
):
    """
    Vertical ascent of a projectile in a stratified atmosphere with variable gravity
    and advanced aerodynamic drag. The drag coefficient follows the exact Cd(M, Re)
    law specified in the problem statement, combining Mach-dependent behavior and
    a Reynolds-number correction. Relative velocity is defined with respect to a
    vertical wind profile. The system is integrated using classical 4th-order
    Runge–Kutta (RK4). The apex time is refined via cubic Lagrange interpolation
    of t(v) evaluated at v = 0 using the last four points with v > 0.

    Parameters
    ----------
    dt : float
        RK4 time step [s], dt > 0
    v0 : float
        Initial upward velocity [m/s]
    x0 : float
        Initial altitude [m]
    g0 : float
        Gravitational acceleration at sea level [m/s^2]

    Cd0 : float
        Baseline drag coefficient (dimensionless)
    A : float
        Cross-sectional area [m^2]
    m : float
        Mass of the projectile [kg]
    D : float
        Characteristic diameter for Reynolds number [m]

    rho0 : float
        Sea-level air density [kg/m^3]
    h : float
        Exponential density scale height [m]

    gamma : float
        Ratio of specific heats (≈1.4 for air)
    R_air : float
        Specific gas constant for air [J/(kg·K)] (≈287)
    T0 : float
        Sea-level temperature [K]
    L : float
        Temperature lapse rate [K/m] (≈0.0065 up to ~11 km)

    mu0 : float
        Dynamic viscosity at T0 [Pa·s]
    S : float
        Sutherland constant [K]

    w0 : float
        Base vertical wind [m/s]
    shear : float
        Wind shear coefficient [dimensionless]
    Hw : float
        Wind decay height scale [m]

    Returns
    -------
    t_hist : list of float
        Time history [s]
    x_hist : list of float
        Altitude history [m]
    v_hist : list of float
        Velocity history [m/s]
    t_apex : float
        Refined apex time (v = 0) [s]
    """

    # Earth radius for variable gravity
    R_earth = 6_371_000.0  # [m]

    # Drag tuners fixed to the prompt values (not user-overridable)
    alpha = 0.20
    beta = 0.40
    delta = 0.08
    k_Re = 5.0
    Cd_min = 0.05
    eps_Re = 1e-12

    # ---------- Atmosphere & wind models ----------

    def T(x):
        """Tropospheric temperature profile, clamped to T >= 1 K."""
        Tx = T0 - L * x
        return max(1.0, Tx)

    def a_sound(x):
        """Speed of sound a(x)."""
        return math.sqrt(gamma * R_air * T(x))

    def mu(x):
        """Dynamic viscosity via Sutherland’s law."""
        Tx = T(x)
        return mu0 * (Tx / T0)**1.5 * (T0 + S) / (Tx + S)

    def rho(x):
        """Exponential density profile."""
        return rho0 * math.exp(-x / h)

    def w(x):
        """Vertical wind profile with shear and exponential decay."""
        return w0 + shear * x * math.exp(-x / Hw)

    # ---------- Dimensionless groups ----------

    def reynolds(x, v_rel):
        mu_x = mu(x)
        # avoid division by zero
        Re = rho(x) * abs(v_rel) * D / max(mu_x, 1e-16)
        return Re

    def mach(x, v_rel):
        a = a_sound(x)
        return abs(v_rel) / max(a, 1e-16)

    # ---------- Cd(M, Re) model (exact prompt expression) ----------

    def Cd_eff(x, v):
        v_rel = v - w(x)
        M = mach(x, v_rel)
        Re = reynolds(x, v_rel)

        # Mach dependence (quadratic + transonic tanh bump)
        base_M = 1.0 + alpha * M * M + 0.5 * beta * (1.0 + math.tanh((M - 1.0) / delta))

        # Reynolds correction: [1 + k_Re / sqrt(Re + eps_Re)]^{-1}
        corr_Re = 1.0 / (1.0 + k_Re / math.sqrt(Re + eps_Re))

        Cd_raw = Cd0 * base_M * corr_Re
        Cd_clamped = max(Cd_min, Cd_raw)
        return Cd_clamped, v_rel

    # ---------- Acceleration with variable gravity and drag ----------

    def dvdt(x, v):
        Cd_val, v_rel = Cd_eff(x, v)
        g_x = g0 * (R_earth / (R_earth + x))**2
        drag_term = 0.5 * rho(x) * Cd_val * A * v_rel * abs(v_rel) / m
        return -g_x - drag_term

    # ---------- RK4 integrator for (x, v) ----------

    t_hist = [0.0]
    x_hist = [x0]
    v_hist = [v0]

    # buffer for last four (v,t) with v > 0
    vt_buffer = [(v0, 0.0)]

    max_steps = int(1e7)
    steps = 0

    t = 0.0
    x = x0
    v = v0

    while v > 0.0 and steps < max_steps:
        # k1
        k1x = v
        k1v = dvdt(x, v)

        # k2
        x_mid = x + 0.5 * dt * k1x
        v_mid = v + 0.5 * dt * k1v
        k2x = v_mid
        k2v = dvdt(x_mid, v_mid)

        # k3
        x_mid = x + 0.5 * dt * k2x
        v_mid = v + 0.5 * dt * k2v
        k3x = v_mid
        k3v = dvdt(x_mid, v_mid)

        # k4
        x_end = x + dt * k3x
        v_end = v + dt * k3v
        k4x = v_end
        k4v = dvdt(x_end, v_end)

        # RK4 update
        x_new = x + (dt / 6.0) * (k1x + 2.0 * k2x + 2.0 * k3x + k4x)
        v_new = v + (dt / 6.0) * (k1v + 2.0 * k2v + 2.0 * k3v + k4v)
        t_new = t + dt

        # append to full histories
        x_hist.append(x_new)
        v_hist.append(v_new)
        t_hist.append(t_new)

        # guard against runaway values
        if x_new < -1.0 or x_new > 2.0e7:
            break

        # update vt_buffer only with v > 0 (points before crossing)
        if v_new > 0.0:
            vt_buffer.append((v_new, t_new))
            if len(vt_buffer) > 4:
                vt_buffer.pop(0)
            # advance state
            x, v, t = x_new, v_new, t_new
            steps += 1
        else:
            # first non-positive velocity: crossing occurred between last v>0 and this v<=0
            t = t_new
            x = x_new
            v = v_new
            break

    # ---------- Cubic Lagrange interpolation of t(v) at v = 0 ----------

    if len(vt_buffer) >= 4:
        v4, t4 = zip(*vt_buffer)  # unpack to two tuples of length 4
        t_apex = 0.0
        for i in range(4):
            vi = v4[i]
            Li = 1.0
            for j in range(4):
                if j == i:
                    continue
                vj = v4[j]
                denom = vi - vj
                if denom == 0.0:
                    denom = 1e-16
                Li *= (0.0 - vj) / denom
            t_apex += t4[i] * Li
    else:
        # fallback: if for some reason fewer than 4 positive points exist,
        # use the last recorded time as a crude approximation
        t_apex = t_hist[-1]

    # Required output: print only t_apex with three decimals and no units
    print(f"{t_apex:.3f}")

    return t_hist, x_hist, v_hist, t_apex


# Example usage with the prompt parameters
if __name__ == "__main__":
    dt   = 0.05
    v0   = 1500.0
    x0   = 0.0
    g0   = 9.81
    A    = 0.10
    m    = 10.0
    D    = 0.20
    rho0 = 1.225
    h    = 8500.0
    gamma = 1.4
    R_air = 287.0
    T0   = 288.0
    L    = 0.0065
    mu0  = 1.8e-5
    S    = 110.4
    w0   = 2.5
    shear = 1.0
    Hw   = 10_000.0
    Cd0  = 0.47

    t_hist, x_hist, v_hist, t_apex = projectile_ascent_rk4_drag_complex(
        dt, v0, x0, g0,
        Cd0, A, m, D,
        rho0, h,
        gamma, R_air, T0, L,
        mu0, S,
        w0, shear, Hw
    )