34.4.4. 1-dimensional Euler equations for Perfect Ideal Gas on moving mesh (ALE)#

Integral equations for general domain \(v_t\). Starting from the balance equation for a control volume \(V\) at rest,

\[\frac{d}{dt}\int_{V} \mathbf{u} + \oint_{\partial V} \mathbf{F}(\mathbf{u}) \cdot \hat{\mathbf{n}} = \int_{V} \mathbf{s} \ ,\]

using Reynolds’ transport theorem, the balance equation for an arbitrary volume \(v_t\) follows

\[\frac{d}{dt}\int_{v_t} \mathbf{u} - \oint_{\partial v_t} \mathbf{u} \mathbf{v}_b \cdot \hat{\mathbf{n}} + \oint_{\partial v_t} \mathbf{F}(\mathbf{u}) \cdot \hat{\mathbf{n}} = \int_{v_t} \mathbf{s} \ ,\]

or collecting all the boundary terms

\[\frac{d}{dt}\int_{v_t} \mathbf{u} + \oint_{\partial v_t} \left[ \mathbf{F}(\mathbf{u}) - \mathbf{u} \mathbf{v}_b \right] \cdot \hat{\mathbf{n}} = \int_{v_t} \mathbf{s} \ .\]

Explicit Euler scheme

\[( \mathbf{u}_i^{n+1} V_i^{n+1} ) = ( \mathbf{u}_i^{n} V_i^{n} ) - \Delta t \sum_{j \in B_i} \mathbf{F}_j^n \ ,\]

Roe flux

Boundary conditions

34.4.4.1. Useful classes for numerical methods#

34.4.4.1.1. Basic libraries#

Hide code cell source
import numpy as np
import matplotlib.pyplot as plt

34.4.4.1.2. Parent class#

\(\texttt{HyperbolicSystem1d()}\) class with common functions (or just their templates) required to implement a finite volume method solver.

Hide code cell source
class HyperbolicSystem1d():
    """
    Analytical quantities of a Hyperbolic linear system in a 1-dimensional domain
    """
    def __init(self, **params):
        pass
    def flux(self,):
        """ Analytical flux, f """
        pass
    def A(self,):
        """ Advection matrix, A """
        pass
    def s(self,):
        """ Eigenvalues of matrix, s """
        pass
    def R(self,):
        """ Matrix of right eigenvectors, R """
        pass
    def L(self,):
        """ Matrix of left eigenvectors, L=inv(R) """
        pass
    def spectrum(self,):
        """ Spectrum of matrix A() """
        return self.s(), self.R(), self.L()

    def absA(self, u):
        """ A = R * S * L, |A| = R * |S| * L """
        return self.R(u) @ np.diag(np.abs(self.s(u))) @ self.L(u)

    def roe_intermediate_state(self,):
        """ Roe intermediate state for Roe linearization """
        pass

34.4.4.1.3. Euler equations for perfect ideal gas class#

Hide code cell source
class Euler1dPIG(HyperbolicSystem1d):
    """
    Analytical quantities of a Hyperbolic linear system in a 1-dimensional domain
    Physical quantities:
    - conservative: (rho, m, Et)
    - physical/convective (rho, u, e) or (rho, u, s)
    Parameters:
    - gamma: cp/cv ratio

    PIG (Perfect Ideal Gas)
    gamma = cp / cv  --> cv =     1 / ( gamma - 1 ) * R
    cp - cv = R      --> cp = gamma / ( gamma - 1 ) * R
    p = rho * R * T = rho * R/cv e = (gamma-1) * rho * e = (gamma-1) * E
    e = cv * T
    """
    def __init__(self, gamma=5./3., name=""):
        """  """
        # Speed of sound, a, and pressure, p,  are functions of the TD state
        # ...add functions fun_a(...), fun_p(...)
        # - which physical variables?
        # - equations of state for different fluids (PIG, VdW,...)
        self.gamma = gamma
        self.gamma_1 = self.gamma - 1
        self.name = name

    def fun_p(self, u):
        """
        Pressure as a function of conservative variables
        p = rho R T = rho R/cv e = (gamma-1) rho e = (gamma-1) ( Et - .5* m^2/rho)
        """
        return self.gamma_1 * ( u[2] - .5 * u[1]**2 / u[0] )

    def phys_from_cons(self, u):
        """
        rho = rho
        u   = m / rho
        p   = (gamma-1) * E
            = (gamma-1) * ( Et - .5 * rho * u^2 )
            = (gamma-1) * ( Et - .5 * m^2 / rho )
        """
        return np.array([ u[0], u[1]/u[0], self.fun_p(u) ])

    def cons_from_phys(self, p):
        """
        rho = rho
        m   = rho * u
        Et  = rho * ( e + .5 * u^2 )
            = E + .5 * rho * u^2
            = p / ( gamma - 1 ) + .5 * rho * u^2
        """
        Et = p[2]/self.gamma_1 + .5 * p[0] * p[1]**2
        return np.array([ p[0], p[0]*p[1], Et])

    def flux(self, u):
        """ Numerical flux
        u: conservative variables (rho, m, Et)
        F = ( m, rho*u^2+p(...), u*(Et+p(...)) )
        """
        vel = u[1]/u[0]
        p = self.fun_p(u)
        return np.array([ u[1], u[0]*vel**2+p, vel*(u[2]+p) ])

    def A(self, u):
        """
        Convection matrix, A; input: conservative variables
        A = [[            0,        1,         0 ],
             [    u^2+p_rho,   2u+p_m,      p_Et ],
             [-u*ht+u*p_rho, ht+u*p_m, u*(1+p_Et)]]
        """
        vel = u[1]/u[0]
        p = self.fun_p(u)
        ht = ( u[2] + p ) / u[0]
        p_rho = .5 * self.gamma_1 * vel**2
        p_m   = - self.gamma_1 * vel
        p_Et  =   self.gamma_1
        return np.array(
            [[ .0, 1.,.0 ],
             [ -vel**2+p_rho, 2*vel+p_m, p_Et ],
             [  vel*(-ht+p_rho), ht+vel*p_m, vel*(1+p_Et) ]])

    def s(self, u):
        """
        Eigenvalues of A; input: conservative variables
        s = [u-a, u, u+a]
        """
        vel = u[1]/u[0]
        p = self.fun_p(u)
        # Speed of sound, a^2 = gamma R T = gamma p / rho
        a = np.sqrt(self.gamma * self.fun_p(u)/u[0])
        return np.array([vel-a, vel, vel+a])

    def R(self, u):
        """
        Right eigenvectors of A; input: conservative variables
        R = ...
        """
        vel = u[1]/u[0]
        p = self.fun_p(u)
        # Speed of sound, a^2 = gamma R T = gamma p / rho
        a = np.sqrt(self.gamma * self.fun_p(u)/u[0])
        ht = ( u[2] + p ) / u[0]
        p_rho = .5 * self.gamma_1 * vel**2
        p_m   = - self.gamma_1 * vel
        p_Et  =   self.gamma_1

        return np.array([
            [1, vel-a, ht-vel*a],
            [1, vel  , vel**2-p_rho/p_Et],
            [1, vel+a, ht+vel*a]]).T

    def L(self, u):
        """
        Left eigenvectors of A; input: conservative variables
        L = ...
        """
        vel = u[1]/u[0]
        p = self.fun_p(u)
        # Speed of sound, a^2 = gamma R T = gamma p / rho
        a = np.sqrt(self.gamma * self.fun_p(u)/u[0])
        ht = ( u[2] + p ) / u[0]
        p_rho = .5 * self.gamma_1 * vel**2
        p_m   = - self.gamma_1 * vel
        p_Et  =   self.gamma_1

        return np.array([
            [ p_rho + vel*a ,   p_m - a  ,   p_Et ],
            [-2*(p_rho-a**2),-2*p_m      ,-2*p_Et ],
            [ p_rho - vel*a ,   p_m + a  ,   p_Et ]
            ]) / ( 2. * a**2 )

    def spectrum(self, u):
        """ Spectrum of matrix A; input: conservative variables """
        return self.s(u), self.R(u), self.L(u)

    def roe_intermediate_state(self, u_0, u_1):
        """
        For p-sys:
          rho_roe = ANY! = ... CHOOSE ONE: average = 0.5*(rho_0+rho_1)
          vel_roe = (sqrt(rho_0)*vel_0+sqrt(rho_1)*vel_1)/(sqrt(rho_0)+sqrt(rho_1))
          - convective/physical: (rho, vel) = ( rho_roe, vel_roe )
          - conservatvie       : (rho, mom) = ( rho_roe, rho_roe*vel_roe)
          F_roe(u) ~ dFdu * du ~ A(u_roe(u0,u1)) * (u_1 - u_0)
        """
        #> Density (for a PIG, it's arbitrary once consistency is provided)
        rho_roe = np.sqrt(u_0[0]*u_1[0])  #.5*(u_0[0]+u_1[0])

        vel_0, vel_1 = u_0[1]/u_0[0], u_1[1]/u_1[0]
        p_0, p_1 = self.fun_p(u_0), self.fun_p(u_1)
        ht_0, ht_1 = ( u_0[2] + p_0 ) / u_0[0],  ( u_1[2] + p_1 ) / u_1[0]

        #> Velocity and total enthalpy (Roe linearization)
        vel_roe = (vel_0*np.sqrt(u_0[0])+vel_1*np.sqrt(u_1[0])) / \
                  (np.sqrt(u_0[0])+np.sqrt(u_1[0]))
        ht_roe  = ( ht_0*np.sqrt(u_0[0])+ ht_1*np.sqrt(u_1[0])) / \
                  (np.sqrt(u_0[0])+np.sqrt(u_1[0]))

        #> Enthalpy and pressure (PIG)
        h_roe = ht_roe - .5 * vel_roe**2
        # p = rho R T = rho R/cP h = (gamma-1)/gamma * rho * h
        p_roe = self.gamma_1/self.gamma * rho_roe * h_roe

        #> Momentum and total energy
        m_roe = rho_roe * vel_roe
        Et_roe = rho_roe * ht_roe - p_roe

        return np.array([rho_roe, m_roe, Et_roe])

    def absA_entropy_fix(self, u, delta=.1, vel=.0):
        """  """
        #> Eigenvalues s1 = u-a, s2 = u, s3 = u+a
        eigvals = self.s(u) - vel
        #> Speed of sound a = .5 * ( s3 - s1 )
        a = .5 * ( np.max(eigvals) - np.min(eigvals) )
        #> Entropy fix
        entropy_evals = np.abs(eigvals)
        entropy_evals[entropy_evals < delta*a] = \
            .5 * entropy_evals[entropy_evals < delta*a]**2 / ( delta * a ) + .5 * ( delta * a )

        return  self.R(u) @ np.diag(entropy_evals) @ self.L(u)

    def roe_flux(self, u_0, u_1, delta=.1, vel=.0, debug=False):
        """  """

        u_roe = self.roe_intermediate_state(u_0, u_1)

        if ( debug == True ):
            print("u_roe: ", u_roe)
            print("vel: ", vel)
            print("f0: ", self.flux(u_0))
            print("vel*u0 :", vel * u_0)
            print("f1: ", self.flux(u_1))
            print("vel*u1 :", vel * u_1)

        return .5 * (self.flux(u_1) - vel*u_1 + self.flux(u_0) - vel*u_0 - \
                     self.absA_entropy_fix(u_roe, delta=delta, vel=vel) @ ( u_1 - u_0 ) )

    def flux_at_boundary(self, boundary, u, vel=.0, debug=False):
        """
        boundary: boundary condition dict
        u: conservative variables (rho, m, Et) at boundary cell
        """

        if ( boundary["type"] == "wall" ):
            #> State in the ghost cell
            u_boundary = u.copy();
            u_boundary[1] =-u[1] + 2. * u[0] * vel                     # momentum
            u_boundary[2] = u[2] + 2. * u[0] * vel**2 - 2 * u[1] * vel # total energy

            if ( boundary["nor"] < 0 ):
                flux = self.roe_flux(u_boundary, u, vel=vel, debug=debug)
            else:
                flux = self.roe_flux(u, u_boundary, vel=vel, debug=debug)

            if ( debug == True ):
                print("> Boundary, nor:", boundary["nor"])
                print("u: ", u)
                print("u_boundary: ", u_boundary)
                print("flux: ", flux)
                print()

            return flux

        elif ( boundary["type"] == "inflow" or boundary["type"] == "outflow" ):

            delta_v = self.L(u) @ ( self.cons_from_phys(boundary["phys_ext"]) - u )
            eigval = self.s(u) - vel
            delta_v[ eigval*boundary["nor"] > 0 ] = .0
            u_flux = u + self.R(u) @ delta_v

            return self.flux(u_flux) - vel * u_flux

34.4.4.1.4. Time dependent mesh#

Time dependent mesh class, producing a mesh with uniform spacing, given the law of motion of the extreme points of the domain.

Hide code cell source
class TimeDependentMesh():
    def __init__(self, ne, dt, fun_x0 = lambda t: .0, fun_x1 = lambda t: 1., t0 = .0):
        """  """
        #> Number of elements
        self.ne = ne
        self.fun_x0 = fun_x0
        self.fun_x1 = fun_x1
        self.dt = dt             # maybe it could be updated in variable-step algorithms

        #> Initialize domain
        self.update_domain(t0)

    def update_domain(self, t):
        """
        t: t old
        """

        #> Position of the interfaces (uniform mesh) at time t[n]
        self.xi = np.linspace(self.fun_x0(t), self.fun_x1(t), self.ne+1)

        #> Position of the interfaces (uniform mesh) at time t[n+1]
        self.xi_p1 = np.linspace(self.fun_x0(t+self.dt), self.fun_x1(t+self.dt), self.ne+1)

        #> Velocity of the interfaces
        self.vi = ( self.xi_p1 - self.xi ) / self.dt

        #> Cell volumes
        self.dx = self.xi[1:] - self.xi[:-1]
        self.dx_p1 = self.xi_p1[1:] - self.xi_p1[:-1]

34.4.4.2. Numerical simulation#

34.4.4.2.1. System of equations#

Initialized the system of equations to be solved

Hide code cell source
# Number of unknown fields, nu
#> P-system, Shallow Water
#  - conservative       : u = (rho, mom)
#  - physical/convective: p = (rho, vel), with mom = rho*vel
#> Euler
#  - conservative       : u = (rho, mom, Et)
#  - physical/convective: p = (rho, vel, e) or = (rho, vel, s), with mom = rho*vel

gamma = 5. / 3.

#> Parameters and initialization of the system
# S = Psys1d(a=1);  nu = 2
# S = ShallowWater1d(g=1);  nu = 2
# S = Psys1dLinearized(a=1);  nu = 2
S = Euler1dPIG(gamma=gamma);  nu = 3

34.4.4.2.2. Domain#

Hide code cell source
#> Parameters
ne = 50
dt = .0025

fun_x0 = lambda t: -.3 * t + 0. # -(t/0.1)**2
fun_x1 = lambda t:  .0 * t + 1. # -(t/0.1)**2

mesh = TimeDependentMesh(ne=ne, dt=dt, fun_x0=fun_x0, fun_x1=fun_x1)
xc = .5 * ( mesh.xi[:-1] + mesh.xi[1:] )

34.4.4.2.2.1. VV of the moving mesh#

Hide code cell source
#> Quick check and validation of the moving mesh

it = 0

X, T = [], []

while it < 20:

    mesh.update_domain(it*mesh.dt)
    X.append(mesh.xi)
    T.append(it)

    it += 1

X = np.array(X)

fig, ax = plt.subplots(1,1, figsize=(5,5))
ax.plot(X, T, color="black")
ax.grid()
plt.show()
../../_images/865b78666e507aaeca45d389a0d4cb204ad8f7f61f768dac21ce02ed8fdcc914.png

34.4.4.2.3. Boundary conditions#

Hide code cell source
#> Boundary conditions

boundary_0 = {  # left boundary
    "type": "wall",
    "nor" : -1.
}
# boundary_1 = {  # right boundary
#     "type": "wall",
#     "nor" : 1.
# }
#> Inflow and outflow bcs are characteristic-based bcs, with the same implementation.
# They're essentially the same
# boundary_0 = {  # left boundary
#     "type": "inflow",
#     "phys_ext": np.array([2., .0, 2.]),   # rho_ext, u_ext, p_ext
#     "nor": -1.                             # nx
# }
boundary_1 = {  # right boundary
    "type": "outflow",
    "phys_ext": np.array([1., .0, 1.]),    # rho_ext, u_ext, p_ext
    "nor": 1.                              # nx
}

34.4.4.2.4. Initial conditions#

Hide code cell source
#> Initial conditions, in physical variables

# #> Initial condition 0: uniform
# rhoL, rhoR = 1.0, 1.0
# velL, velR = 0.0, 0.0
# pL  , pR   = 1., 1.

#> Initial condition 1: u=0, T uniform, pL != pR
rhoL, rhoR = 1., 1.
velL, velR = 0., 0.
pL  , pR   = 1., 1.

rhoL, momL, EtL = S.cons_from_phys(np.array([rhoL, velL, pL]))
rhoR, momR, EtR = S.cons_from_phys(np.array([rhoR, velR, pR]))

#> Write initial conditions into uc0 array
uc0 = np.zeros((nu,ne))
uc0[0,:] = np.where( xc<.5, rhoL, rhoR )
uc0[1,:] = np.where( xc<.5, momL, momR )
uc0[2,:] = np.where( xc<.5, EtL , EtR  )

34.4.4.2.5. Simulation parameters#

Hide code cell source
#> Time
t0, t1, dt = 0., 2.5, dt # 25
nt = int((t1-t0)/dt)+1
tv = np.arange(nt)*dt

#> Numerical schemes
#> Time integration
time_integration = 'ee'       # ...
numerical_flux   = 'roe'      # ...

#> Time loop
uuc = np.zeros((nt, nu, ne))    # Array to store solution, for small dimensional pbs
uc = uc0.copy()
uVc = uc * mesh.dx[:,np.newaxis].T
uuc[0,:,:] = uc

34.4.4.2.6. Time loop#

Hide code cell source
#> Time loop
it = 0

X, T = [], []

while it < nt-1:

    t = tv[it]

    # print();  print(f"Time: {t}");  print(f"State:\n{uc}")
    mesh.update_domain(t)

    #> Evaluate fluxes at internal boundaries
    if numerical_flux == 'roe':
        flux = np.array([ S.roe_flux(uc[:,ia], uc[:,ia+1], vel=mesh.vi[ia+1]) for ia in range(ne-1) ]).T

    #> Flux at boundaries - Treat boundary conditions
    flux_0 = S.flux_at_boundary(boundary_0, uc[:, 0], vel=mesh.vi[ 0], debug=False)
    flux_1 = S.flux_at_boundary(boundary_1, uc[:,-1], vel=mesh.vi[-1], debug=False)

    # stop

    # flux = np.append(np.append(flux_0, flux), flux_1)
    flux = np.concatenate((flux_0[:,np.newaxis], flux, flux_1[:, np.newaxis]), axis=1)

    #> Time integration
    if time_integration == 'ee':
        dt = tv[it+1] - tv[it]
        # source = np.zeros((nu,ne))
        # uc += ( flux[:,:-1] - flux[:,1:] ) * dt / dx
        uVc += ( flux[:,:-1] - flux[:,1:] ) * dt
        uc = uVc / mesh.dx_p1[:,np.newaxis].T

    it += 1                   # Updating counter
    uuc[it,:,:] = uc.copy()   # Storing results
    X.append(mesh.xi)
    T.append(t*np.ones(ne))

X.append(mesh.xi_p1)
T.append((t+dt)*np.ones(ne))

X = np.array(X)
T = np.array(T)

Xc = .5 * ( X[:,:-1] + X[:,1:] )  # Cell centers

34.4.4.2.7. Post-processing#

Hide code cell source
fig, ax = plt.subplots(5,1, figsize=(5,20))

iax = 0
m0 = ax[iax].pcolormesh(Xc, T, uuc[:,0,:])
# ax[iax].imshow(uuc[:,0,:], aspect="auto")
# ax[0].set_colorbar()
ax[iax].set_xlabel('x')
ax[iax].set_ylabel('t')
ax[iax].set_title('Density')
fig.colorbar(m0, ax=ax[iax])

iax = 1
m0 = ax[iax].pcolormesh(Xc, T, uuc[:,1,:])
# ax[0].set_colorbar()
ax[iax].set_xlabel('x')
ax[iax].set_ylabel('t')
ax[iax].set_title('Momentum')
fig.colorbar(m0, ax=ax[iax])

iax = 2
m0 = ax[iax].pcolormesh(Xc, T, uuc[:,2,:])
# ax[0].set_colorbar()
ax[2].set_xlabel('x')
ax[iax].set_ylabel('t')
ax[iax].set_title('Total Energy')
fig.colorbar(m0, ax=ax[iax])

iax = 3
m0 = ax[iax].pcolormesh(Xc, T, uuc[:,1,:]/uuc[:,0,:])
# ax[0].set_colorbar()
ax[iax].set_xlabel('x')
ax[iax].set_ylabel('t')
ax[iax].set_title('Velocity')
fig.colorbar(m0, ax=ax[iax])

# pressure
# p = (gamma-1) * ( Et - .5 * m^2 / rho )
iax = 4
m0 = ax[iax].pcolormesh(Xc, T, S.gamma_1 * (uuc[:,2,:]-.5*uuc[:,1,:]**2/uuc[:,0,:]))
# ax[0].set_colorbar()
ax[iax].set_xlabel('x')
ax[iax].set_ylabel('t')
ax[iax].set_title('Pressure')
fig.colorbar(m0, ax=ax[iax])

plt.tight_layout()
/tmp/ipykernel_3208/54342096.py:4: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.
  m0 = ax[iax].pcolormesh(Xc, T, uuc[:,0,:])
/tmp/ipykernel_3208/54342096.py:13: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.
  m0 = ax[iax].pcolormesh(Xc, T, uuc[:,1,:])
/tmp/ipykernel_3208/54342096.py:21: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.
  m0 = ax[iax].pcolormesh(Xc, T, uuc[:,2,:])
/tmp/ipykernel_3208/54342096.py:29: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.
  m0 = ax[iax].pcolormesh(Xc, T, uuc[:,1,:]/uuc[:,0,:])
/tmp/ipykernel_3208/54342096.py:39: UserWarning: The input coordinates to pcolormesh are interpreted as cell centers, but are not monotonically increasing or decreasing. This may lead to incorrectly calculated cell edges, in which case, please supply explicit cell edges to pcolormesh.
  m0 = ax[iax].pcolormesh(Xc, T, S.gamma_1 * (uuc[:,2,:]-.5*uuc[:,1,:]**2/uuc[:,0,:]))
../../_images/a7a208a90e73acbd6f02d61eefff4da990b3ad9da7775f7ff9aca6a691e9a8ce.png
Hide code cell source
n_plots = 10
i_dt = int(np.floor(nt/n_plots))

fig, ax = plt.subplots(n_plots, figsize=(5,20))
for i in range(n_plots):
    p = S.gamma_1 * (uuc[i_dt*i,2,:]-.5*uuc[i_dt*i,1,:]**2/uuc[i_dt*i,0,:])
    ax[i].plot(xc,uuc[i_dt*i,0,:], '-', label="rho")
    ax[i].plot(xc,uuc[i_dt*i,1,:], '--', label="m")
    # ax[i].plot(xc,uuc[i_dt*i,2,:], label='Et')
    ax[i].plot(xc,uuc[i_dt*i,1,:]/uuc[i_dt*i,0,:], ':', label="v")
    ax[i].plot(xc, p, '-x', label="p")
    ax[i].plot(xc, np.sqrt(S.gamma * p / uuc[i_dt*i,0,:]), label="a")

    ax[i].grid()
    ax[i].legend(bbox_to_anchor=(1.05, 1.05), loc='upper left')

# Pressure: p = (gamma-1) * ( Et - .5 * m^2 / rho )
# rho, m, Et = 1, 1, 2
# gamma = 5/3
# -> u = 1
# -> p = 2/3 * ( 2 - .5 * 1 ) = 2 / 3 * 1.5 = 1
../../_images/404772a68fa55b14f352f8c49c2b20be6b54580a4f75e5746b6eb6eea68756ad.png