Diseño de un cohete de agua

Introducción


In [56]:
from IPython.display import Image, SVG, Math, Latex, clear_output
import sys
import numpy as np

P0 = 1E5  # Pa
G = -9.81  # m/s
RHO_W = 998.0  # kg / m3
RHO_A = 1.0  # kg / m3
DT = 0.00001  # s
DT_BIG = 0.1  # s

# Constest related data
__L_WING__ = 0.025  # m
__Z_FS__ = 0.15  # m
__P__ = 7E5  # Pa
__ANGLE__ = np.radians(80.0)
__NU_PROP__ = 0.9

Un cohete de agua no es más que una botella rellena parcialmente con agua, que se propulsa mediante aire comprimido. Se puede encontrar más información en la wikipedia:

https://en.wikipedia.org/wiki/Water_rocket

En este caso el cohete será lanzado desde una plataforma con un cierto ángulo, y se trata de conseguir llegar lo más lejos posible.

Durante el vuelo el cohete estará sometido a diferentes fuerzas:


In [29]:
SVG(filename='./forces.svg')


Out[29]:
image/svg+xml C B W Fprop g U α D L DW LW DW DW

La primera fuerza, aplicada sobre el centro de gravedad del cohete $C$ (que variará según se descargue el agua del propulsor), será la fuerza de la gravedad $m \cdot \mathbf{g}$, siempre en la dirección $z$ negativa. Vamos a hacer la aproximación de que la fuerza propulsora $\mathbf{F}_{prop}$ siempre tendrá la dirección de avance del cohete, así que podemos aplicarla en el mismo punto $C$ (ya que no generará momento). El hecho es que la dirección del cohete no tiene necesariamente que coincidir con la dirección de avance, y de hecho normalmente no lo hará, así que aparecerá un ángulo de ataque $\alpha$ que conllevará la aparición de una resistencia $\mathbf{D}$, siempre en la dirección de avance del cohete y sentido contrario, y un empuje $\mathbf{L}$ perpendicular a la misma. Ambas fuerzas se aplicarán en un centro $B$. El cohete también incorporará unas alas estabilizadoras que tratarán de alinearlo con la dirección de avance con dos objetivos:

  • Si el cohete se encuentra alineado con la dirección de avance $\mathbf{D}$ será mínimo.
  • Si el cohete es inestable (algó que ocurrirá muy probablemente al principio, pues la masa de agua hará que el centro de gravedad se encuentre muy bajo), sólo los estabilizadores podrán manterlo en rumbo. En las siguientes imágenes se pueden ver una configuración estable (izquierda) y una inestable (derecha).

In [30]:
SVG(filename='./stable_unstable.svg')


Out[30]:
image/svg+xml C B U α D C U α B D

Como se puede observar si el centro de gravedad es muy bajo, o el de empuje muy alto, el momento que se genera trata de apartar más todavía el cohete de su trayectoria.


In [31]:
Image(filename='unstable_rocket.jpeg')


Out[31]:

Por tanto debemos ir caracterizando cada una de las fuerzas, para finalmente analizar la masa de lastre óptima, y el tamaño idóneo de los estabilizadores.

No obstante la optimización del cohete se deberá hacer siguiendo las bases del concurso:

  • Se empleará como propulsor una botella de 0.5l.
  • A dicha botella se le colocará una válvula de seguridad a la salida (como las empleadas para mangueras).
  • Los estabilizadores no podrán tener una envergadura mayor de 2.5 centímetros ($__L_WING__$).
  • El ángulo de lanzamiento, la cantidad de agua, y la presión de aire serán establecidas por la organización.

Así pues los datos $__Z_FS__$, $__P__$ y $__ANGLE__$ deberán ser establecidos a mano una vez conocidos.

Propulsión del cohete

Podemos esquematizar el problema como sigue:


In [32]:
SVG(filename='./scheme.svg')


Out[32]:
image/svg+xml x z Va, p p0 Vw

De tal forma que podemos estudiar la propulsión del cohete tomando un sistema de referencia no inercial sujeto al cohete. La mecánica se puede resumir como sigue:

  • La persión que el aire ejerce sobre la botella no se encuentra compensada con la del agua, pues la botella se encuentra abierta por debajo, lo que provoca una aceleración vertical del cohete.
  • El aire a presión, la gravedad, y la aceleración del propio cohete obligan al agua a abandonar la botella.
  • Al mismo tiempo que la botella se vacía de agua, el aire se expande y la presión disminuye, reduciendo también la fuerza propulsiva.

Por tanto tenemos que resolver tres problemas acoplados. Para resolverlos vamos a hacer 3 hipótesis muy discutibles, pero que son necesarias para poder llegar a un modelo asequible:

  • La propulsión se realiza con el cohete en posición vertical, de tal manera que en el esquema la superficie libre es siempre horizontal.
  • La descarga de agua y la consecuente expansión del gas es un proceso lento.
  • La presión en el agua decrece linealmente con la altura.

Por esa razón disponemos de la variable $__NU_PROP__$, que hace referencia a la eficiencia del propulsor respecto de la teórica, un valor que desafortunadamente deberemos establecer un poco a ojo.

Debemos comenzar definiendo la geometría de la botella. Al ser un cuerpo radial nos es suficiente con definir la función $R \left( z \right)$


In [33]:
%pylab inline

# From http://www.mzbn.com/en/sales/information
R = 0.5 * 65E-3  # m
L = 223E-3  # m
# From http://www.embotelladoras.org/maquinas-llenadoras/llenadora-de-botella-de-agua-embotellada-1500-botellashora
r0_bottle = 0.5 * 25E-3  # m
r0_valve = r0_bottle / 3.0  # m

z = (0.0, 0.01, 0.02, 0.04, 0.06, 0.175, 0.195)
r = (r0_valve, r0_bottle, r0_bottle, 0.029, 0.036, 0.036, 0.01)
geom_z = np.asarray(z)
geom_r = np.asarray(r)

plot(geom_r, geom_z, 'k-')
plot(-geom_r, geom_z, 'k-')
plot((-geom_r[-1], geom_r[-1]), (geom_z[-1], geom_z[-1]), 'k-')
axes().set_aspect('equal', 'datalim')


Populating the interactive namespace from numpy and matplotlib

Podemos crear una clase para la geometría que nos permita calcular fácilmente radios, áreas y volumenes: $$A(z) = \int_0^{2 \pi} \int_0^z R(z) \, dz \, d\theta = 2 \pi \int_0^z R(z) \, dz$$ $$S(z) = \int_0^{2 \pi} \int_0^{R(z)} r \, dr \, d\theta = \pi R(z)^2$$ $$V(z) = \int_0^{2 \pi} \int_0^z \int_0^{R(z)} r \, dr \, dz \, d\theta = \pi \int_0^z R(z)^2 \, dz$$ $$C(z) = \frac{1}{V(z)}\int_0^{2 \pi} \int_0^z \int_0^{R(z)} z \, r \, dr \, dz \, d\theta = \pi \int_0^z z \, R(z)^2 \, dz$$ Donde $A(z)$ es el área lateral de la botella hasta la altura $z$, $S(z)$ es el área transversal en $z$ (útil para obtener el área de la tapa, de la salida y de la superficie libre), $V(z)$ el volúmen bajo $z$, y $C(z)$ su centro de gravedad (de $V(z)$).


In [34]:
class areaElement(object):
    def __init__(self, z, r):
        """Initialize the rocket bottle geometry.
        
        Args:
            z: Points where the radius is provided
            r: Radius associated to each z points
        Raises:
            ValueError: if r and z have different length, or if the
            lengths are lower than 2.
        """
        if len(z) != len(r):
            self.__z = None
            self.__r = None
            raise ValueError('Unmatching dimensions for z and r')
        if len(z) < 2:
            self.__z = None
            self.__r = None
            raise ValueError('At least 2 points should be provided')            
        z = np.asarray(z)
        r = np.asarray(r)
        self.__z = z
        self.__r = r
        # Compute the volumes
        self.__V = np.pi * np.asarray(
            [np.trapz(r[0:i + 1]**2, x=z[0:i + 1]) for i in range(len(z))])
        # Compute the areas
        self.__A = np.pi * np.asarray(
            [np.trapz(r[0:i + 1], x=z[0:i + 1]) for i in range(len(z))])
        self.__S = np.pi * r**2
        # The centers of gravity
        self.__C = np.pi * np.asarray(
            [np.trapz(z[0:i + 1] * r[0:i + 1]**2, x=z[0:i + 1]) for i in range(len(z))])
        self.__C[1:] /= self.__V[1:]  # V[0] = 0.0

    @property
    def r(self):
        """List of radius R(z)"""
        return self.__r

    @r.setter
    def r(self, value):
        self.__r = value

    @property
    def z(self):
        """List of z values"""
        return self.__z

    @z.setter
    def z(self, value):
        self.__z = value

    def getR(self, z=None):
        """Get the radius of the bottle for a specific heigh.
        
        Args:
            z: Heigh where the radius should be known. If None, the upper bound
            of the bottle will be used.
        Return:
            The radius at z. z value will be clamped by the bottle bounds.
        """
        if z == None:
            return self.__r[-1]
        return np.interp(z, self.__z, self.__r)

    def getN(self, z=None):
        """Get the normal (lateral wall) of the bottle for a specific heigh.
        
        Args:
            z: Heigh where the normal should be known. If None, the upper bound
            of the bottle will be used.
        Return:
            The normal at z (nr, nz). z value will be clamped by the bottle bounds.
        """
        if z == None:
            return np.asarray((self.__nr[-1], self.__nz[-1]))
        nr = np.interp(z, self.__z, self.__nr)
        nz = np.interp(z, self.__z, self.__nz)
        return np.asarray((nr, nz))

    def getA(self, z=None):
        """Get the lateral area of the bottle below a especific z coordinate.
        
        Args:
            z: Heigh where the area should be known. If None, the upper bound
            of the bottle will be used.
        Return:
            The lateral area below z. z value will be clamped by the bottle bounds.
        """
        if z == None:
            return self.__A[-1]
        return np.interp(z, self.__z, self.__A)

    def getS(self, z=None):
        """Get the transversal area of the bottle for a specific heigh.
        
        Args:
            z: Heigh where the transversal area should be known. If None, the upper bound
            of the bottle will be used.
        Return:
            The lateral area below z. z value will be clamped by the bottle bounds.
        """
        if z == None:
            return self.__S[-1]
        return np.interp(z, self.__z, self.__S)

    def getV(self, z=None):
        """Get the Volume up to a specific heigh.
        
        Args:
            z: Upper bound of the volume. If None, the upper bound of the bottle
            will be used.
        Return:
            The volume under z. z value will be clamped to the bottle bounds.
        """
        if z == None:
            return self.__V[-1]
        return np.interp(z, self.__z, self.__V)

    def getC(self, z=None):
        """Get the Center of gravity of the volume getV().
        
        Args:
            z: Upper bound of the volume. If None, the upper bound of the bottle
            will be used.
        Return:
            The center of gravity of the volume under z.
        """
        if z == None:
            return self.__C[-1]
        return np.interp(z, self.__z, self.__C)

bottle_test = areaElement(geom_z, geom_r)
A = bottle_test.getA() + bottle_test.getS()
V = bottle_test.getV()
C = bottle_test.getC()
Math('A = {:.2g} \\, \\mathrm{{m^2}}; V = {:.2g} \\, \\mathrm{{m^3}}, C = {:.2g} \\, \\mathrm{{m}}'.format(
    A, V, C))


Out[34]:
$$A = 0.019 \, \mathrm{m^2}; V = 0.00062 \, \mathrm{m^3}, C = 0.11 \, \mathrm{m}$$

Movimiento de la superficie libre

Vamos a definir el siguiente volumen de control y sus constornos:


In [35]:
SVG(filename='./control_vol.svg')


Out[35]:
image/svg+xml x z Vf = Vc ΣFS ΣW Σ0

De tal forma que podemos aplicar las equaciones de continuidad y cantidad de movimiento al volumen de agua

Ec. de continuidad / conservación de la masa

La ecuación de continuidad se puede escribir como:

$$\frac{d}{dt} \int_{V_f(t)} \rho d\Omega = \frac{d}{dt} \int_{V_c(t)} \rho d\Omega + \int_{\Sigma_c(t)} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma$$

La primera integral, que se refiere a la masa en el volumen de fluido, es nula para fluidos incompresibles, ya que el volumen de fluido es el conjunto de partículas fluidas:

$$\frac{d}{dt} \int_{V_f(t)} \rho d\Omega = \rho \frac{d}{dt} \int_{V_f(t)} d\Omega = 0$$

La segunda integral se refiere a la variación de masa en el volumen de control:

$$\frac{d}{dt} \int_{V_c(t)} \rho d\Omega = \rho \frac{d}{dt} \int_{V_c(t)} d\Omega = \rho \frac{d V_c(t)}{dt}$$

Donde:

$$V_c(t) = V(z_{FS})$$

La derivada en el tiempo del volumen de control se puede aproximar como el área de la superficie libre multiplicada por su velocidad:

$$\frac{d V(z_{FS})}{dt} = S(z_{FS}) \, \mathbf{u}(z_{FS}) \cdot \mathbf{k}$$

De tal forma que:

$$\frac{d}{dt} \int_{V_c(t)} \rho d\Omega = \rho \, S(z_{FS}) \, \mathbf{u}(z_{FS}) \cdot \mathbf{k}$$

La última de las integrales se puede resolver descomponiéndolas en los 3 contornos especificados anteriormente:

$$\int_{\Sigma_c(t)} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma = \int_{\Sigma_{FS}} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma + \int_{\Sigma_W} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma + \int_{\Sigma_{0}} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma$$

De tal forma que las dos primeras integrales son nulas, ya que:

$$\left. \mathbf{u} \right\vert_{FS} = \left. \mathbf{u_c} \right\vert_{FS}$$$$\left. \mathbf{u} \right\vert_{W} \cdot \left. \mathbf{n} \right\vert_W = \left. \mathbf{u_c} \right\vert_{W} \cdot \left. \mathbf{n} \right\vert_W = 0$$

La integral restante es entonces fácil de calcular (se debe tener en cuenta que la normal apunta al exterior, es decir $-\mathbf{k}$):

$$\int_{\Sigma_{0}} \rho \, \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \, d\sigma = - \rho \, \mathbf{u}(z_{FS}) \cdot \mathbf{k} \int_{\Sigma_{0}} d\sigma = - \rho \, \mathbf{u}(0) \cdot \mathbf{k} \, S(0)$$

Por tanto la ecuación de continuidad nos permite relacionar la velocidad de la superficie libre (el ritmo de descarga de agua) con la velocidad del flujo a la salida de la botella. En realidad repitiéndo el procedimiento para cualquier otro volumen de control limitado por $z < z_{FS}$ se puede llegar a la siguiente relación más general:

Continuity eq.
$S(z) \, \mathbf{u}(z) \cdot \mathbf{k} = S(0) \, \mathbf{u}(0) \cdot \mathbf{k} \, \longrightarrow \, \mathbf{u}(z) \cdot \mathbf{k} = \frac{R(0)^2}{R(z)^2} \mathbf{u}(0) \cdot \mathbf{k} $

Ec. Conservación de cantidad de movimiento

Tomando el mismo volumen de control (que varía en el tiempo) podemos escribir la ecuación de conservación de cantidad de movimiento de la siguiente forma (restringiéndola a la dirección $z$):

$$\frac{d}{dt} \int_{V_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, d\Omega + \int_{\Sigma_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, \left( \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \right) \, d\sigma = - \int_{\Sigma_c(t)} p \, \mathbf{n} \cdot \mathbf{k} \, d\sigma + \int_{V_c(t)} \rho \, \mathbf{a}_v \cdot \mathbf{k} \, d\Omega$$

donde $a_v$ son las fuerzas volumétricas (la aceleración de la gravedad y los términos no inerciales). En la anterior ecuación se han despreciado los efectos viscosos.

La primera integral no es nula, pero usando la ecuación de continuidad se puede llegar a una solución:

$$ \frac{d}{dt} \int_{V_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, d\Omega = \rho \, \frac{d}{dt} \int_{V_c(t)} \mathbf{u}(z) \cdot \mathbf{k} \, d\Omega = \rho \, \frac{d}{dt} \int_{0}^{2 \pi} \int_{0}^{z_{FS}} \int_{0}^{R(z)} r \, \mathbf{u}(z) \cdot \mathbf{k} \, dr \, dz\, d\theta = \rho \, 2 \pi \frac{d}{dt} \int_{0}^{z_{FS}} \int_{0}^{R(z)} r \, \frac{R(0)^2}{R(z)^2} \mathbf{u}(0) \cdot \mathbf{k} \, dr \, dz $$

De tal forma que la equación de continuidad nos ha permitido sustituir una velocidad dependiente de la coordenada $z$ (en un principio desconocida) por la geometría de la botella $R(z)$. Pero la verdadera ventaja es que el Jacobiano va a eliminar dicha función permitiendo integrar muy fácilmente:

$$ \rho \, 2 \pi \frac{d}{dt} \int_{0}^{z_{FS}} \frac{R(0)^2}{R(z)^2} \mathbf{u}(0) \cdot \mathbf{k} \, \int_{0}^{R(z)} r \, dr \, dz = \rho \, \pi \frac{d}{dt} \int_{0}^{z_{FS}} R(0)^2 \, \mathbf{u}(0) \cdot \mathbf{k} \, dz = \rho \, \pi \, R(0)^2 \, \frac{d}{dt} \left( z_{FS} \, \mathbf{u}(0) \cdot \mathbf{k} \right)$$

Y derivando:

$$ \frac{d}{dt} \int_{V_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, d\Omega = \rho \, \pi \, R(0)^2 \left( \frac{dz_{FS}}{dt} \, \mathbf{u}(0) \cdot \mathbf{k} + z_{FS} \frac{d\mathbf{u}(0)}{dt} \cdot \mathbf{k} \right) $$

La primera derivada tiene que ver con la velocidad de la superficie libre:

$$ \frac{dz_{FS}}{dt} = \mathbf{u}(z_{FS}) \cdot \mathbf{k} \, \longrightarrow \, \frac{dz_{FS}}{dt} \, \mathbf{u}(0) \cdot \mathbf{k} = \frac{R(z_{FS})^2}{R(0)^2} \left( \mathbf{u}(z_{FS}) \cdot \mathbf{k} \right)^2$$

La segunda derivada es más complicada, pero se simplifica notablemente si hacemos la hipótesis de que $\frac{dR(z_{FS})}{dt} \sim 0$:

$$z_{FS} \frac{d\mathbf{u}(0)}{dt} \cdot \mathbf{k} = z_{FS} \frac{R(z_{FS})^2}{R(0)^2} \frac{d\mathbf{u}(z_{FS})}{dt} \cdot \mathbf{k}$$

Y reuniendo toda la información podemos por fin dar el resultado de la primera integral, referente al cambio de cantidad de movimiento del fluido en el volumen de control:

$$ \frac{d}{dt} \int_{V_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, d\Omega = \rho \, \pi \, R(z_{FS})^2 \left( \left( \mathbf{u}(z_{FS}) \cdot \mathbf{k} \right)^2 + z_{FS} \frac{d\mathbf{u}(z_{FS})}{dt} \cdot \mathbf{k} \right) $$

La segunda integral hay que dividirla de nuevo en los tres contornos (Superficie libre, paredes y salida), pero al igual que pasaba con la ecuación de continuidad, el único término que permanece es del contorno de salida:

$$\int_{\Sigma_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, \left( \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \right) \, d\sigma = - \int_{\Sigma_0} \rho \, \left( \mathbf{u}(0) \cdot \mathbf{k} \right) \, \left( \mathbf{u}(0) \cdot \mathbf{k} \right) d\sigma = - \rho \, \left( \mathbf{u}(0) \cdot \mathbf{k} \right)^2 \int_{\Sigma_0} d\sigma = - \rho \, \left( \frac{R(z_{FS})^2}{R(0)^2} \mathbf{u}(z_{FS}) \cdot \mathbf{k} \right)^2 \pi R(0)^2 $$

Y por tanto:

$$\int_{\Sigma_c(t)} \rho \, \mathbf{u} \cdot \mathbf{k} \, \left( \left(\mathbf{u} - \mathbf{u_c}\right) \cdot \mathbf{n} \right) \, d\sigma = - \rho \, \pi \, R(z_{FS})^2 \left( \mathbf{u}(z_{FS}) \cdot \mathbf{k} \right)^2 \frac{R(z_{FS})^2}{R(0)^2}$$

La tercera integral también hay que dividirla en los tres contornos:

$$- \int_{\Sigma_c(t)} p \, \mathbf{n} \cdot \mathbf{k} \, d\sigma = -\int_{\Sigma_{FS}} p \, \mathbf{n} \cdot \mathbf{k} \, d\sigma - \int_{\Sigma_0} p_0 \, \mathbf{n} \cdot \mathbf{k} \, d\sigma - \int_{\Sigma_W} p(z) \, \mathbf{n}(z) \cdot \mathbf{k} \, d\sigma = - p \, \pi \, R(z_{FS})^2 + p_0 \, \pi \, R(0)^2 - 2 \, \pi \int_{0}^{z_{FS}} p(z) \, \mathbf{n}(z) \cdot \mathbf{k} \, dz$$

Donde la última integral hay que resolverla numéricamente. Nosotros supondremos que la presión decae linealmente.

Para la última integral tan sólo hay que sustituir el término de fuerzas volumétricas por la gravedad y los términos no inerciales:

$$\int_{V_c(t)} \rho \, \mathbf{a}_v \cdot \mathbf{k} \, d\Omega = \rho \left( \mathbf{g} - \frac{d\mathbf{U}}{dt} \right) \cdot \mathbf{k} \, V(z_{FS}) $$

donde $\frac{d\mathbf{U}}{dt}$ es la aceleración del cohete completo.

Recogiendo toda la información:

Continuity eq.
$$ \rho \, \pi \, R(z_{FS})^2 \, z_{FS} \frac{d \mathbf{u}(z_{FS})}{dt} \cdot \mathbf{k} = - \rho \, \pi \, \frac{R(z_{FS})^2}{R(0)^2} \left( \mathbf{u}(z_{FS}) \cdot \mathbf{k} \right)^2 \left( R(0)^2 - R(z_{FS})^2 \right) $$ $$ - \pi \left( p R(z_{FS})^2 - p_0 R(0)^2 + 2 \int_0^{z_{FS}} p(z) \, \mathbf{n}(z) \cdot \mathbf{k} dz \right) + \rho \left( \mathbf{g} - \frac{d \mathbf{U}}{dt} \right) \cdot \mathbf{k} \, V(z_{FS}) $$

Integral de presión

La integral de presión requiere de una clase algo más compleja:


In [36]:
class structure(areaElement):
    def __init__(self, z, r):
        """Initialize the rocket bottle geometry.
        
        Args:
            z: Points where the radius is provided
            r: Radius associated to each z points
        Raises:
            ValueError: if r and z have different length, or if the
            lengths are lower than 2.
        """
        areaElement.__init__(self, z, r)

    def getF(self, p, z_FS, z=None):
        """Get the pressure integral up to a specific heigh.
        
        Args:
            p: Air pressure
            z_FS: z coordinate of the Free surface
            z: Heigh where the radius should be known. If None, the upper bound
            of the bottle will be used.
        Return:
            The vertical component of the pressure integral.
        """
        if z == None:
            z = self.z[-1]
        if z_FS <= 0.0 or z <= 0.0 or p == P0:
            return 0.0
        # Create the list of walls to integrate
        i = 0
        zz = [self.z[i]]
        rr = [self.r[i]]
        while self.z[i + 1] < z:
            i += 1
            zz.append(self.z[i])
            rr.append(self.r[i])
        zz.append(z)
        rr.append(self.getR(z))
        # Solve the taps integrals
        result = p * self.getS(z) - P0 * self.getS(0.0)
        # Integrate the lateral walls
        for i in range(1, len(zz)):
            z0 = zz[i - 1]
            z1 = zz[i]
            r0 = rr[i - 1]
            r1 = rr[i]
            zi = 0.5 * (z0 + z1)
            ri = 0.5 * (r0 + r1)
            dr = r1 - r0
            if zi > z_FS:
                press = p
            else:
                press = P0 + (p - P0) / z_FS * zi
            result -= 2.0 * np.pi * press * ri * dr
        return result

bottle_test = structure(geom_z, geom_r)
F = bottle_test.getF(p=7E5, z_FS=0.15)
Math(r'F = {:.2g} N'.format(F))


Out[36]:
$$F = 1.9e+03 N$$

Objeto agua

Ahora ya podemos generar una clase "agua" que reciba una botella (clase structure), la altura inicial de la superficie libre, y permita integrar el movimiento de la superficie libre.


In [37]:
class water(object):
    """Water volume inside the bottle.
    
    The water volume is deflated while the rocket is propelled, pushed by the
    air.
    """
    def __init__(self, bottle, z, rho=RHO_W):
        """Init the water volume.

        Args:
            bottle: Bottle where the water should be contained.
            z: Initial water level.
            rho: Water density
            
        """
        self.__bottle = bottle
        self.z = z
        self.rho = rho
        self.u = 0.0
        self.dudt = 0.0

    def update(self, p, dUdt, g=G, dt=DT):
        """Update the water free surface position.

        Args:
            p: Air pressure.
            dUdt: Rocket acceleration
            dt: Time step
        
        """
        z = self.z
        if z <= 0.0:
            return
        # Free surface acceleration
        rho = self.rho
        rz = self.__bottle.getR(z)
        r0 = self.__bottle.getR(0)
        V = self.getV()
        dudt = -rho * np.pi * (rz / r0)**2 * self.u**2 * (r0**2 - rz**2)
        dudt -= self.__bottle.getF(p=p, z_FS=z, z=z)
        dudt += rho * (g - dUdt) * V
        dudt /= rho * np.pi * rz**2 * z
        # Time integration
        self.u += dt * dudt
        self.z += dt * self.u
        self.dudt = dudt
        if self.z < 0.0:
            self.z = 0.0

    def getZ(self):
        """Compute the water level.

        Return:
            Level of water
        
        """
        return self.z

    def getV(self):
        """Compute the volume of water.

        Return:
            Volume of water
        
        """
        return self.__bottle.getV(self.z)

    def getM(self):
        """Compute the water mass.

        Return:
            Mass of water
        
        """
        return self.getV() * self.rho

    def getC(self):
        """Get the Center of gravity of the water mass.
        
        Return:
            The center of gravity of the water mass.
        """
        return self.__bottle.getC(self.z)

Presión del aire

Bueno, ya hemos pasado lo peor... Para la presión del aire podemos usar el modelo de gas ideal (que para algo hemos puesto el agua en la botella). De la misma forma consideraremos que cuando el agua se haya agotado, el aire escapará de la botella inmediatamente, perdiendo toda su presión.

Por tanto se podrá calcular la presión como:

$$ p(t) = \frac{V_a(t=0)}{V_a(t)} p(t=0) $$

In [38]:
class air(object):
    """Air volume inside the bottle.
    
    The water volume is inflated while the rocket is propelled, pushing the
    water.
    """
    def __init__(self, bottle, z, p):
        """Init the air volume.

        Args:
            bottle: Bottle where the water should be contained.
            z: Initial water level.
            p: Initial air pressure
            
        """
        self.__bottle = bottle
        self.pV = p * (bottle.getV() - bottle.getV(z))
        self.p = p

    def update(self, z):
        """Update the air volume and pressure.

        Args:
            z: New free surface position.
        
        """
        if z <= 0.0:
            self.p = P0
            return
        V = self.__bottle.getV() - self.__bottle.getV(z)
        self.p = self.pV / V
        if self.p < P0:
            self.p = P0

Propulsor

Con la estructura, el aire y el agua ya podemos configurar el propulsor.


In [39]:
class propeller(structure):
    def __init__(self, z, r, z_FS, p):
        """Initialize the rocket bottle geometry.
        
        Args:
            z: Points where the radius is provided
            r: Radius associated to each z points
            z_FS: Initial water level
            p: Initial pressure
        Raises:
            ValueError: if r and z have different length, or if the
            lengths are lower than 2.
        """
        structure.__init__(self, z, r)
        self.water = water(self, z_FS)
        self.air = air(self, z_FS, p)

    def update(self, dUdt, g=G, dt=DT):
        """Update the physical state.

        Args:
            dUdt: Acceleration of the rocket (forward pointing)
            dt: Time step.
        
        """
        # Update the water and air
        self.water.update(self.air.p, dUdt, g=g, dt=DT)
        self.air.update(self.water.z)

    def getFprop(self):
        """Get the propeller force.
        
        Return:
            The propeller force.
        """
        p = self.air.p
        z_FS = self.water.z
        return self.getF(p, z_FS)

z_FS = __Z_FS__
p = __P__
bottle_test = propeller(geom_z, geom_r, z_FS, p)
t = [0.0]
p = [p]
while bottle_test.water.z > 0.0:
    bottle_test.update(0.0, dt=DT)
    t.append(t[-1] + DT)
    p.append(bottle_test.air.p)
plot(t, p, 'k-')
grid()
xlabel(r'$t$ [s]')
ylabel(r'$p$ [Pa]')


Out[39]:
<matplotlib.text.Text at 0x7f1f07c2c128>

Como se puede observar, aún sin aceleración del cohete, el proceso de descarga es muy rápido, como máximo del orden de las décimas de segundo para botellas de 0.5 litros.

Resistencia y empuje del cohete

Para analizar la aerodinámica del cohete en busca de la posición del centro de empuje, y de la resistencia y empuje generados para cada ángulo necesitamos usar un CFD, y más concretamente OpenFOAM (que es libre).

Definición de la geometría del casco (estructura exterior)

La estructura exterior del cohete esta dividida en tres partes (excluyendo las alas estabilizadoras):

  • El cuerpo cilíndrico central. Esta parte del casco en realidad comparte estructura con la botella propulsora, a la que se le añaden las otras dos partes siguientes.
  • Faldón del cohete. El faldón es una estructura cilíndrica dispuesta en la boca de la botella (la salida del propulsor).
  • Cabeza del cohete. Con la misión de hacer al cohete más aerodinámico (reduciendo su resistencia) se añade una cabeza en la parte superior del cohete (el fondo de la botella propulsora), cuya forma se debe determinar.

En realidad, de cara a definir la forma final del casco del cohete, las dos primeras partes terminan formando un único cilindro, de tal manera que la parte cuya forma se debe definir realmente es la cabeza del cohete.

Para simplificar definiremos la cabeza como una sección de circunferencia con su centro a la altura del final del cuerpo cilíndrico, y un radio creciente que dará cabezas cada vez más largas y puntiagudas:


In [40]:
SVG(filename='./hulls.svg')


Out[40]:
image/svg+xml

Concretamente consideraremos 3 geometrías, la primera (derecha), parecida a un torpedo, hecha con la cabeza de radio $R_{head} = R$, y otras dos más afiladas con $R_{head} = 1.5 R$ (centro) y $R_{head} = 2 R$ (izquierda).

Resultados

Todas las geometrías se prueban para velocidades de $10 \, \mathrm{m / s}$, lo que significa números de Reynolds $Re \sim 10^5$, y por tanto suficientemente altos.

Además cada geometría se prueba para ángulos de ataque de $\alpha = 0, 5, 10, 15 \, \mathrm{degs}$.

$R_{head} = R$

Con ésta configuración el casco del torpedo tiene un área de $A = 0.052 \, \mathrm{m^2}$


In [41]:
Image(filename='torpedoe.jpeg')


Out[41]:
$\alpha$ $L$ $D$ $z_{cor}$ $C_L$ $C_D$ $z_{cor} / z_{cyl}$
$0$ deg $0.0$ N $0.065$ N $0.120$ m $0.0$ $0.025$ $0.68$
$5$ deg $0.044$ N $0.079$ N $0.146$ m $0.017$ $0.031$ $0.83$
$10$ deg $0.070$ N $0.095$ N $0.168$ m $0.027$ $0.037$ $0.96$
$15$ deg $0.109$ N $0.110$ N $0.157$ m $0.042$ $0.042$ $0.90$

$R_{head} = 1.5 R$

Con ésta configuración el casco del torpedo tiene un área de $A = 0.053 \, \mathrm{m^2}$


In [42]:
Image(filename='bullet.jpeg')


Out[42]:
$\alpha$ $L$ $D$ $z_{cor}$ $C_L$ $C_D$ $z_{cor} / z_{cyl}$
$0$ deg $0.0$ N $0.063$ N $0.120$ m $0.0$ $0.024$ $0.68$
$5$ deg $0.044$ N $0.078$ N $0.154$ m $0.017$ $0.029$ $0.88$
$10$ deg $0.070$ N $0.094$ N $0.178$ m $0.027$ $0.035$ $1.02$
$15$ deg $0.106$ N $0.117$ N $0.171$ m $0.040$ $0.044$ $0.98$

$R_{head} = 2 R$

Con ésta configuración el casco del torpedo tiene un área de $A = 0.055 \, \mathrm{m^2}$


In [43]:
Image(filename='missil.jpeg')


Out[43]:
$\alpha$ $L$ $D$ $z_{cor}$ $C_L$ $C_D$ $z_{cor} / z_{cyl}$
$0$ deg $0.0$ N $0.062$ N $0.120$ m $0.0$ $0.023$ $0.68$
$5$ deg $0.045$ N $0.078$ N $0.160$ m $0.016$ $0.028$ $0.91$
$10$ deg $0.079$ N $0.091$ N $0.172$ m $0.029$ $0.033$ $0.98$
$15$ deg $0.095$ N $0.112$ N $0.193$ m $0.035$ $0.041$ $1.10$

Conclusiones sobre la geometría del casco

Del análisis mediante CFD se deduce que todas las geometrías se comportan de forma muy similar, lo que es una ventaja ya que la construcción del casco podrá ser más relajada. De ahora en adelante supondremos la última geometría ($R_{head} = 2 \, R$)

Otro resultado interesante que devuelve el CFD es que para grandes ángulos (10 y 15 grados) todas las geometrías empiezan a tener problemas de sembrado de vórtices, con los peligros que ello conlleva. Por ejemplo, la última geometría resultaba en las siguientes curvas de resistencia y empuje para 15 grados de ángulo de ataque, donde se observa claramente el carácter oscilante de las señales:


In [44]:
Image(filename='missil_15_plot.png')


Out[44]:

Ese comportamiento no nos conviene en absoluto, así que deberemos evitar alcanzar tales ángulos de ataque durante el vuelo.

Objeto casco

Sea como sea, ya podemos generar una clase casco para después poder utilizarla:


In [45]:
class hull(structure):
    def __init__(self, z, r, alpha, CL, CD, Cz, rho=RHO_A):
        """Initialize the rocket bottle geometry.
        
        Args:
            z: Points where the radius is provided
            r: Radius associated to each z points
            alpha: List of angles from which the aerodynamic coefficients are provided (radians).
            CL: Lift coefficient for each angle.
            CD: Drag coefficient for each angle.
            Cz: aerodynamic center heigh coeffcient (addimensionalized with the cylinder heigh)
            rho: Fluid where the hull is moving
        Raises:
            ValueError: if r and z have different length, or if the
            lengths are lower than 2, or if alpha, CL, CD, Cz have different lengths,
            or if alpha, CL, CD, Cz are lower than 2.
        """
        structure.__init__(self, z, r)
        if len(alpha) != len(CL) != len(CD) != len(Cz):
            raise ValueError('Unmatching dimensions for alpha, CL, CD, Cz')
        if len(alpha) < 2:
            raise ValueError('At least two angles should be provided')

        self.__alpha = []
        self.__CL = []
        self.__CD = []
        self.__Cz = []
        for i in range(0, len(alpha)):
            self.__alpha.append(-alpha[-(i + 1)])
            self.__CL.append(-CL[-(i + 1)])
            self.__CD.append(CD[-(i + 1)])
            self.__Cz.append(Cz[-(i + 1)])
        for i in range(0, len(alpha)):
            self.__alpha.append(alpha[i])
            self.__CL.append(CL[i])
            self.__CD.append(CD[i])
            self.__Cz.append(Cz[i])
        self.__alpha = np.asarray(self.__alpha)
        self.__CL = np.asarray(self.__CL)
        self.__CD = np.asarray(self.__CD)
        self.__Cz = np.asarray(self.__Cz)
        self.__rho = rho

        # Compute the area
        self.__Area = self.getA() + self.getS() + self.getS(z[0])
        
        # Compute the cylinder heigh
        r_max = max(r)
        z_id = numpy.where(r == r_max)[0][-1]
        self.__z_cyl = z[z_id]
            
            
    def getL(self, alpha, U):
        """Compute the lift force.

        Args:
            alpha: Angle with the input flow (radians)
            U: Input flow velocity (equal to the rocket speed).
        Return:
            The lift force.
        """
        CL = np.interp(alpha, self.__alpha, self.__CL)
        return 0.5 * self.__rho * self.__Area * U**2 * CL

    def getD(self, alpha, U):
        """Compute the drag force.

        Args:
            alpha: Angle with the input flow (radians)
            U: Input flow velocity (equal to the rocket speed).
        Return:
            The drag force.
        """
        CD = np.interp(alpha, self.__alpha, self.__CD)
        return 0.5 * self.__rho * self.__Area * U**2 * CD

    def getZ(self, alpha, U):
        """Compute the lift and drag forces application point heigh.

        Args:
            alpha: Angle with the input flow (radians)
        Return:
            The application point heigh.
        """
        Cz = np.interp(alpha, self.__alpha, self.__Cz)
        return self.__z_cyl * Cz

z = (0.0, 0.175, 0.18444, 0.19444, 0.20421, 0.21255, 0.22042, 0.22723, 0.23306, 0.23724)
r = (0.036, 0.036, 0.03531, 0.03326, 0.02993, 0.02546, 0.02, 0.01378, 0.00702, 0.0)
hull_z = np.asarray(z)
hull_r = np.asarray(r)

plot(r, z, 'k-')
axes().set_aspect('equal', 'datalim')



In [46]:
alpha = np.asarray((np.radians(0.0), np.radians(5.0), np.radians(10.0), np.radians(15.0)))
CL = np.asarray((0.0, 0.016, 0.029, 0.035))
CD = np.asarray((0.023, 0.028, 0.033, 0.041))
Cz = np.asarray((0.68, 0.91, 0.98, 1.1))

hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)
a = np.linspace(-15.0, 15.0, num=30)
l = []
d = []
z = []
for aa in a:
    l.append(hull_test.getL(np.radians(aa), 10.0))
    d.append(hull_test.getD(np.radians(aa), 10.0))
    z.append(hull_test.getZ(np.radians(aa), 10.0))
plot(a, l, 'r-', label='Lift')
plot(a, d, 'b-', label='Drag')
plot(a, z, 'k-', label='z')
legend(loc='best')
grid()
xlabel(r'$\alpha$ [deg]')
ylabel(r'$L, D, z$')


Out[46]:
<matplotlib.text.Text at 0x7f1f07c12438>

Estabilizadores

Los estabilizadores tan sólo son placas planas, para las que existen algunas fórmulas aproximadas, no obstante es mejor utilizar datos de perfiles que sean similares como pueda ser el NACA 0015 (simétrico):

$\alpha$ $C_{Lo}$ $C_{Do}$
$0$ deg $0.0$ $0.0101$
$5$ deg $0.5438$ $0.0141$
$10$ deg $0.9067$ $0.0270$
$15$ deg $0.4365$ $0.1754$

Como se puede observar, a partir de unos 10 grados de ángulo de ataque las cosas se ponen feas, ya que el flujo se desprende, de tal forma que el coeficiente de empuje se "atasca" mientras que el de resistencia se dispara.

Por tanto se debe evitar a toda costa llegar a esos ángulos.

Otra cosa que se tiene que tener en cuenta es el efecto de punta de pala, donde las presiones de ambas caras del perfil se igualan, generando un flujo ascendente y sus respectivos vórtices de punta de pala, de tal forma que se deben aplicar las siguientes correciones (se pueden encontrar por ejemplo en http://www.grc.nasa.gov/WWW/k-12/airplane/kitedown.html):

$$C_L = \frac{C_{Lo}}{1 + \frac{C_{Lo}}{\pi A_R}}$$$$C_D = C_{Do} + \frac{C_L^2}{0.7 \pi A_R}$$

Donde $A_R$ es la relación de aspecto:

$$A_R = \frac{l^2}{S}$$

In [47]:
SVG(filename='./wing_scheme.svg')


Out[47]:
image/svg+xml l

Respecto del centro de aplicación de la fuerza podemos asumir que sea el centro de gravedad del area del ala.

Así pues no es difícil crear una clase para el ala.


In [48]:
class wing():
    def __init__(self, z, r, alpha, CLo, CDo, rho=RHO_A):
        """Initialize the rocket bottle geometry.
        
        Args:
            z: Points where the radius is provided
            r: Radius associated to each z points
            alpha: List of angles from which the aerodynamic coefficients are provided (radians).
            CLo: Lift coefficient for each angle (without downwash effect).
            CDo: Drag coefficient for each angle (without downwash effect).
            rho: Fluid where the hull is moving
        Raises:
            ValueError: if r and z have different length, or if the
            lengths are lower than 2, or if alpha, CL, CD, Cz have different lengths,
            or if alpha, CL, CD, Cz are lower than 2.
        """
        if len(z) != len(r):
            self.__z = None
            self.__r = None
            raise ValueError('Unmatching dimensions for z and r')
        if len(z) < 2:
            self.__z = None
            self.__r = None
            raise ValueError('At least 2 points should be provided')
        z = np.asarray(z)
        r = np.asarray(r)
        self.__z = z
        self.__r = r

        if len(alpha) != len(CLo) != len(CDo):
            raise ValueError('Unmatching dimensions for alpha, CL, CD')
        if len(alpha) < 2:
            raise ValueError('At least two angles should be provided')
    
        self.__alpha = []
        self.__CLo = []
        self.__CDo = []
        for i in range(0, len(alpha)):
            self.__alpha.append(-alpha[-(i + 1)])
            self.__CLo.append(-CLo[-(i + 1)])
            self.__CDo.append(CDo[-(i + 1)])
        for i in range(0, len(alpha)):
            self.__alpha.append(alpha[i])
            self.__CLo.append(CLo[i])
            self.__CDo.append(CDo[i])
        self.__alpha = np.asarray(self.__alpha)
        self.__CLo = np.asarray(self.__CLo)
        self.__CDo = np.asarray(self.__CDo)
        self.__rho = rho

        # Compute the area
        self.__Area = np.trapz(r, x=z)

        # Compute the center of gravity
        self.__cor = np.trapz(r * z, x=z) / self.__Area

        # Compute the aspect ratio
        r_max = max(r)
        self.__Ar = r_max**2 / self.__Area

    @property
    def r(self):
        """List of radius R(z)"""
        return self.__r

    @r.setter
    def r(self, value):
        self.__r = value

    @property
    def z(self):
        """List of z values"""
        return self.__z

    @z.setter
    def z(self, value):
        self.__z = value
        
    def getL(self, alpha, U):
        """Compute the lift force.

        Args:
            alpha: Angle with the input flow (radians)
            U: Input flow velocity (equal to the rocket speed).
        Return:
            The lift force.
        """
        CLo = np.interp(alpha, self.__alpha, self.__CLo)
        CL = CLo / (1 + CLo / (np.pi * self.__Ar))
        return 0.5 * self.__rho * self.__Area * U**2 * CL

    def getD(self, alpha, U):
        """Compute the drag force.

        Args:
            alpha: Angle with the input flow (radians)
            U: Input flow velocity (equal to the rocket speed).
        Return:
            The drag force.
        """
        CDo = np.interp(alpha, self.__alpha, self.__CDo)
        CD = CDo + self.getL(alpha, U)**2 / (0.7 * np.pi * self.__Ar)
        return 0.5 * self.__rho * self.__Area * U**2 * CD

    def getZ(self, alpha, U):
        """Compute the lift and drag forces application point heigh.

        Args:
            alpha: Angle with the input flow (radians)
        Return:
            The application point heigh.
        """
        return self.__cor


z = (0.0, 0.006)
r = (__L_WING__, __L_WING__)
wing_z = np.asarray(z)
wing_r = np.asarray(r)

alpha = np.asarray((np.radians(0.0), np.radians(5.0), np.radians(10.0), np.radians(15.0)))
CLo = np.asarray((0.0, 0.5438, 0.9067, 0.4365))
CDo = np.asarray((0.0101, 0.0141, 0.0270, 0.1754))

wing_test = wing(wing_z, wing_r, alpha, CLo, CDo)
a = np.linspace(-15.0, 15.0, num=30)
l = []
d = []
z = []
for aa in a:
    l.append(wing_test.getL(np.radians(aa), 10.0))
    d.append(wing_test.getD(np.radians(aa), 10.0))
    z.append(wing_test.getZ(np.radians(aa), 10.0))
plot(a, l, 'r-', label='Lift')
plot(a, d, 'b-', label='Drag')
plot(a, z, 'k-', label='z')
legend(loc='best')
grid()
xlabel(r'$\alpha$ [deg]')
ylabel(r'$L, D, z$')


Out[48]:
<matplotlib.text.Text at 0x7f1f07a389b0>

El cohete como sólido rígido

Ahora que por fin conocemos las fuerzas que actuarán sobre el cohete a cada instante podemos por fin generar el sólido rígido que surcará los cielos.

Eso tampoco es tarea fácil, ya que se requiren de datos como la masa, el centro de gravedad y la inercia del cohete (sin agua en su interior), lo que puede ser difícil de conocer.

Sea como sea, mientras que para determinar las fuerzas habíamos usar básicamente una única dimension ($z$), ahora ya consideramos un cuerpo que se mueve en el plano 2D, teniendo una orientación $\mathbf{N}$ que por lo general no coincidirá con la velocidad $\mathbf{U}$, sobre el que actúan un conjunto de fuerzas que modifican tanto su posición como su orientación.

Todo ello se puede implementar en la siguiente clase:


In [49]:
class solid(object):
    def __init__(self, P, N, U=np.asarray((0.0, 0.0)), W=0.0):
        """Initialize the solid.
        
        Args:
            P: Initial Point
            N: Initial Direction
            U: Initial Velocity
            W: Initial Angular Velocity
        Raises:
            ValueError: if P, N, or U are not 2D entities.
        """
        if len(P) != len(N) != len(U) != 2:
            raise ValueError('P, N, and U must be 2D entities')
        self.__P = np.asarray(P)
        self.__N = np.asarray(N)
        self.__U = np.asarray(U)
        self.__W = W

    @property
    def P(self):
        """Position"""
        return self.__P

    @P.setter
    def P(self, value):
        self.__P = value

    @property
    def N(self):
        """Direction N"""
        return self.__N

    @N.setter
    def N(self, value):
        self.__N = value

    @property
    def U(self):
        """Velocity"""
        return self.__U

    @U.setter
    def U(self, value):
        self.__U = value

    @property
    def W(self):
        """Angular Velocity"""
        return self.__W

    @W.setter
    def W(self, value):
        self.__W = value

    def update(self, m, I, z_cog, F, z_F, dt=DT):
        """Time integration.
        
        Args:
            m: Rocket mass.
            I: Rocket inertia.
            z_cog: heigh of the center of gravity in local coordinates.
            F: List of forces applied to the rocket (in local coordinates).
            z_F: heigh of the centers of application of the forces in local coordinates.
        """
        # Compute the global force and moment
        Fl = np.asarray((0.0, 0.0))
        Ml = 0.0
        for i, f in enumerate(F):
            if len(f) != 2:
                raise ValueError('The forces should be 2D entities')
            f = np.asarray(f)
            Fl += f
            z_f = z_F[i] - z_cog
            Ml -= f[0] * z_f

        # Change the force to the global coordinates
        T = np.asarray((self.N[1], -self.N[0]))
        Fg = Fl[1] * self.N + Fl[0] * T
        Mg = Ml

        # Compute the acelerations
        dUdt = Fg / m
        dWdt = Mg / I

        # Integrate in time
        self.U += dt * dUdt
        self.P += dt * self.U
        self.W += dt * dWdt
        dTheta = dt * self.W
        self.N = np.matrix([[np.cos(dTheta), -np.sin(dTheta)],
                            [np.sin(dTheta), np.cos(dTheta)]]).dot(self.N)
        self.N = np.asarray(self.N)[0]
        return dUdt

De tal forma que nuestro cohete será finalmente un sólido rígido que cuenta con un propulsor, un casco, y un conjunto de alas:


In [54]:
class rocket(solid):
    def __init__(self, angle, m, I, z_cog, p, h, w):
        """Initialize the rocket.
        
        Args:
            angle: Launching angle (radians). 0 for the x axis.
            m: Rocket mass (without water)
            I: Rocket inertia (without water)
            z_cog: Rocket center of gravity heigh (without water)
            p: Propeller
            h: hull
            w: List of wings
        """
        # Solid initialization
        P = np.matrix([[np.cos(angle), -np.sin(angle)],
                       [np.sin(angle), np.cos(angle)]]).dot(np.asarray((z_cog, 0.0)))
        P = np.asarray(P)[0]
        N = np.matrix([[np.cos(angle), -np.sin(angle)],
                       [np.sin(angle), np.cos(angle)]]).dot(np.asarray((1.0, 0.0)))
        N = np.asarray(N)[0]
        solid.__init__(self, P, N, np.asarray((0.0, 0.0)), 0.0)
        
        self.__m = m
        self.__I = I
        self.__z = z_cog
        self.__p = p
        self.__h = h
        self.__w = w
        self.alpha = 0.0

    @property
    def p(self):
        """Propeller"""
        return self.__p

    @property
    def h(self):
        """Hull"""
        return self.__h

    @property
    def w(self):
        """Wings"""
        return self.__w

    def update(self, dt=DT):
        """Time integration.
        """
        # Get the current mass and center of gravity (they could be modified by the water)
        water = self.__p.water
        m_w = water.getM()
        z_w = water.getC()
        m = self.__m + m_w
        z = (self.__z * self.__m + z_w * m_w) / m
        
        # Modify the inertia due to the water mass and cog change
        z_0 = np.sqrt(self.__I / self.__m)
        dz = self.__z - z
        I = self.__m * (z_0 + dz)**2 + m_w * (z_w - z)**2

        # Get the attack angle
        U = np.linalg.norm(self.U)
        if U == 0.0:
            alpha = 0.0
        else:
            n = self.U.dot(self.N)
            t = self.U.dot(np.asarray((-self.N[1], self.N[0])))
            alpha = np.arctan2(t, n)
        self.alpha = alpha
        
        # Compute the corrected gravity
        gn = G * self.N[1]
        gt = -G * self.N[0]  # T = (N[1], -N[0])
        
        # Get the forces and its application points
        F = []
        z_F = []
        F.append(np.asarray((gt, gn)))
        z_F.append(z)
        F.append(np.asarray((0.0, __NU_PROP__ * self.__p.getFprop())))
        z_F.append(z)
        F.append(np.asarray((self.__h.getL(alpha, U),
                             self.__h.getD(alpha, U))))
        z_F.append(self.__h.getZ(alpha, U))
        for w in self.__w:
            F.append(np.asarray((w.getL(alpha, U),
                                 w.getD(alpha, U))))
            z_F.append(w.getZ(alpha, U))
            
        # Integrate
        dUdt = solid.update(self, m, I, z, F, z_F, dt)
        self.__p.update(dUdt.dot(self.N), gn, dt)

Podemos hacer un disparo de prueba (aquí hay que armarse de paciencia que toma su tiempo):


In [70]:
# Setup the propeller
z_FS = __Z_FS__
p = __P__
bottle_test = propeller(geom_z, geom_r, z_FS, p)

# Setup the hull
alpha = np.asarray((np.radians(0.0), np.radians(5.0), np.radians(10.0), np.radians(15.0)))
CL = np.asarray((0.0, 0.016, 0.029, 0.035))
CD = np.asarray((0.023, 0.028, 0.033, 0.041))
Cz = np.asarray((0.68, 0.91, 0.98, 1.1))
hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)

z = (0.0, 0.01)
r = (__L_WING__, __L_WING__)
wing_z = np.asarray(z)
wing_r = np.asarray(r)
wings_test = []

# Setup the horizontal wings
alphao = np.asarray((np.radians(0.0), np.radians(5.0), np.radians(10.0), np.radians(15.0)))
CLo = np.asarray((0.0, 0.5438, 0.9067, 0.4365))
CDo = np.asarray((0.0101, 0.0141, 0.0270, 0.1754))

wings_test.append(wing(wing_z, wing_r, alphao, CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alphao, CLo, CDo))

# And the vertical ones
wings_test.append(wing(wing_z, wing_r, alphao, 0.0 * CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alphao, 0.0 * CLo, CDo))

# Generate the rocket
mass = 5E-2
L_cyl = 0.175
cog = 0.8 * L_cyl
I = mass * (L_cyl - cog)**2
rocket_test = rocket(__ANGLE__, mass, I, cog, bottle_test, hull_test, wings_test)

t = [0.0]
x = [rocket_test.P[0]]
z = [rocket_test.P[1]]
a = [0.0]
while rocket_test.P[1] > 0.0:
    if rocket_test.p.water.z > 0.0:
        dt = DT
    else:
        dt = DT_BIG
    rocket_test.update(DT)
    t.append(t[-1] + DT)
    x.append(rocket_test.P[0])
    z.append(rocket_test.P[1])
    a.append(rocket_test.alpha)
    if rocket_test.alpha > np.radians(10.0) or rocket_test.alpha < -np.radians(10.0):
        break
plot(x, z, 'k-')
plot(x, np.degrees(a), 'r-')
grid()


Proceso de optimización

En la dinámica del cohete intervienen 3 tipos de fuerzas:

  • La fuerza gravitatoria, que siendo una fuerza de volumen siempre impone una aceleración $g$ sin importar la masa
  • La fuerza propulsiva, cuya aceleración es inversamente proporcional a la masa. La cantidad de fuerza es principalmente dependiente de la cantidad de agua y la presión del aire,.
  • Las fuerzas aerodinámicas, cuya aceleración es también inversamente proporcional a la masa. En éste caso la canmtidad de fuerza depende principalmente de la velocidad y el área.

Eso queire decir que, aunque la fuerza propulsora pida una masa muy pequeña, lo cierto es que cohetes muy livianos tendrán resistencias aerodinámicas muy importantes, y por tanto se verán muy penalizados.

Así pues, está claro que necesitamos calcular la masa óptima del cohete. Sin embargo existe la dificultad añadida de que los estabilizadores y la masa del cohete están estrechamente interrelacionados.

Lo que vamos a hacer es optimizar la masa para un tiro vertical donde los estabilizadores no juegan un papel relevante, eso nos dará una primera estimación de la masa óptima (Esto puede llevar un rato considerablemente largo):


In [64]:
# We must perform a initial shot to can start (with a minimum mass)
mass = 1E-2
L_cyl = 0.175
cog = 0.8 * L_cyl
I = mass * (L_cyl - cog)**2

# Setup the propeller
z_FS = __Z_FS__
p = __P__
bottle_test = propeller(geom_z, geom_r, z_FS, p)

# Setup the hull
hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)

# Setup the horizontal wings
wings_test = []
wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))

# And the vertical ones
wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))

# Generate the rocket
rocket_test = rocket(0.5 * np.pi, mass, I, cog, bottle_test, hull_test, wings_test)

t = [0.0]
z = [rocket_test.P[1]]
while rocket_test.P[1] > 0.0 and rocket_test.U[1] >= 0.0:
    if rocket_test.p.water.z > 0.0:
        dt = DT
    else:
        dt = DT_BIG
    rocket_test.update(DT)
    t.append(t[-1] + DT)
    z.append(rocket_test.P[1])
plot(t, z, 'k-')
grid()

# Store the results
mass_max = mass
z_max = z[-1]
mm = [mass_max]
zz = [z_max]



In [65]:
# Now we can start checking configurations with other masses
dmass = 1E-2

while True:
    clear_output()
    print('mass = {:.2g} kg  ->  Z = {:.3g} m'.format(mass_max, z_max))
    mass = mass_max + dmass
    L_cyl = 0.175
    cog = 0.8 * L_cyl
    I = mass * (L_cyl - cog)**2
    print('checking = {:.2g} kg...'.format(mass))
    sys.stdout.flush()

    # Setup the propeller
    bottle_test = propeller(geom_z, geom_r, z_FS, p)

    # Setup the hull
    hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)

    # Setup the horizontal wings
    wings_test = []
    wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))
    wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))

    # And the vertical ones
    wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))
    wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))

    # Generate the rocket
    rocket_test = rocket(0.5 * np.pi, mass, I, cog, bottle_test, hull_test, wings_test)

    t = [0.0]
    z = [rocket_test.P[1]]
    while rocket_test.P[1] > 0.0 and rocket_test.U[1] >= 0.0:
        if rocket_test.p.water.z > 0.0:
            dt = DT
        else:
            dt = DT_BIG
        rocket_test.update(DT)
        t.append(t[-1] + DT)
        z.append(rocket_test.P[1])

    mm.append(mass)
    zz.append(z[-1])
        
    if z[-1] < z_max:
        print('FAIL (Z = {:.2g} m)'.format(z_max))
        break
    # Store the results
    mass_max = mass
    z_max = z[-1]

plot(mm, zz, 'k-')
xlabel(r'$m$ [kg]')
ylabel(r'$z$ [m]')
grid()


mass = 0.25 kg  ->  Z = 56.1 m
checking = 0.26 kg...
FAIL (Z = 56 m)

Muy bien, el máximo de altura se alcanza con unos 250 gramos de masa. Ahora vamos a recuperar el lanzamiento con ángulo, y a estudiar el tamaño de estabilizadores ideal.

Aunque a los estabilizadores hay que imponerles una condición adicional, y es que no pueden ser más grandes que el faldón del cohete:


In [66]:
r_max = max(geom_r)
z_id = numpy.where(geom_r == r_max)[0][0]
max_w_wing = geom_z[z_id]

In [67]:
# Again we are performing an initial shot to can start (with a minimum wing)
w_wing = 0.005

# Setup the propeller
bottle_test = propeller(geom_z, geom_r, z_FS, p)

# Setup the hull
hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)

z = (0.0, w_wing)
r = (__L_WING__, __L_WING__)
wing_z = np.asarray(z)
wing_r = np.asarray(r)
wings_test = []

# Setup the horizontal wings
wings_test.append(wing(wing_z, wing_r, alphao, CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alphao, CLo, CDo))

# And the vertical ones
wings_test.append(wing(wing_z, wing_r, alphao, 0.0 * CLo, CDo))
wings_test.append(wing(wing_z, wing_r, alphao, 0.0 * CLo, CDo))

# Generate the rocket
rocket_test = rocket(__ANGLE__, mass, I, cog, bottle_test, hull_test, wings_test)

t = [0.0]
x = [rocket_test.P[0]]
z = [rocket_test.P[1]]
a = [0.0]
while rocket_test.P[1] > 0.0:
    if rocket_test.p.water.z > 0.0:
        dt = DT
    else:
        dt = DT_BIG
    rocket_test.update(DT)
    t.append(t[-1] + DT)
    x.append(rocket_test.P[0])
    z.append(rocket_test.P[1])
    a.append(rocket_test.alpha)
    if rocket_test.alpha > np.radians(10.0) or rocket_test.alpha < -np.radians(10.0):
        break
plot(x, z, 'k-')
plot(x, np.degrees(a), 'r-')
grid()

# Store the result
wing_max = w_wing
x_max = x[-1]
ww = [wing_max]
xx = [x_max]



In [68]:
# Now we can start checking configurations with other masses
dwing = 5E-3

while True:
    clear_output()
    print('wing = {:.3g} m  ->  X = {:.3g} m'.format(wing_max, x_max))
    w_wing = wing_max + dwing
    print('checking = {:.3g} m...'.format(w_wing))
    sys.stdout.flush()

    if w_wing > max_w_wing:
        print('FAIL (The wing cannot be bigger than {} m)'.format(max_w_wing))
        break
    
    # Setup the propeller
    bottle_test = propeller(geom_z, geom_r, z_FS, p)

    # Setup the hull
    hull_test = hull(hull_z, hull_r, alpha, CL, CD, Cz)

    z = (0.0, w_wing)
    r = (__L_WING__, __L_WING__)
    wing_z = np.asarray(z)
    wing_r = np.asarray(r)
    wings_test = []

    # Setup the horizontal wings
    wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))
    wings_test.append(wing(wing_z, wing_r, alpha, CLo, CDo))

    # And the vertical ones
    wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))
    wings_test.append(wing(wing_z, wing_r, alpha, 0.0 * CLo, CDo))

    # Generate the rocket
    rocket_test = rocket(__ANGLE__, mass, I, cog, bottle_test, hull_test, wings_test)

    t = [0.0]
    x = [rocket_test.P[0]]
    z = [rocket_test.P[1]]
    a = [0.0]
    alpha_valid = True
    while rocket_test.P[1] > 0.0:
        if rocket_test.p.water.z > 0.0:
            dt = DT
        else:
            dt = DT_BIG
        rocket_test.update(DT)
        t.append(t[-1] + DT)
        x.append(rocket_test.P[0])
        z.append(rocket_test.P[1])
        a.append(rocket_test.alpha)
        if rocket_test.alpha > np.radians(10.0) or rocket_test.alpha < -np.radians(10.0):
            alpha_valid = False
            break

    ww.append(w_wing)
    xx.append(x[-1])

    if x[-1] < x_max and alpha_valid:
        print('FAIL (X = {:.3g} m)'.format(x[-1]))
        break
    # Store the results
    wing_max = w_wing
    x_max = x[-1]

plot(ww, xx, 'k-')
xlabel(r'$wing$ [m]')
ylabel(r'$x$ [m]')
grid()


wing = 0.06 m  ->  X = 40.2 m
checking = 0.065 m...
FAIL (The wing cannot be bigger than 0.06 m)

Podemos ver la trayectoria de éste último lanzamiento optimizado (incluyendo como en anteriores ocasiones el ángulo de ataque):


In [69]:
plot(x, z, 'k-')
plot(x, np.degrees(a), 'r-')
xlabel(r'$x$ [m]')
ylabel(r'$z$ [m]')
grid()



In [ ]: