def test_ic(): """ Test the initial conditions function for the single bubble model Test that the initial conditions returned by `sbm_ic` are correct based on the input and expected output """ # Set up the inputs profile = get_profile() T0 = 273.15 + 15. z0 = 1500. P = profile.get_values(z0, ['pressure']) composition = ['methane', 'ethane', 'propane', 'oxygen'] bub = dbm.FluidParticle(composition) yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 K = 1. K_T = 1. fdis = 1.e-4 t_hyd = 0. lag_time = True # Get the initial conditions (bub_obj, y0) = single_bubble_model.sbm_ic(profile, bub, np.array([0., 0., z0]), de, yk, T0, K, K_T, fdis, t_hyd, lag_time) # Check the initial condition values assert y0[0] == 0. assert y0[1] == 0. assert y0[2] == z0 assert y0[-1] == T0 * np.sum(y0[3:-1]) * seawater.cp() * 0.5 assert_approx_equal(bub.diameter(y0[3:-1], T0, P), de, significant=6) # Check the bub_obj parameters for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert bub_obj.T0 == T0 assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == K_T assert bub_obj.fdis == fdis assert bub_obj.t_hyd == t_hyd for i in range(len(composition) - 1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False
def test_ic(): """ Test the initial conditions function for the single bubble model Test that the initial conditions returned by `sbm_ic` are correct based on the input and expected output """ # Set up the inputs profile = get_profile() T0 = 273.15 + 15. z0 = 1500. P = profile.get_values(z0, ['pressure']) composition = ['methane', 'ethane', 'propane', 'oxygen'] bub = dbm.FluidParticle(composition) yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 K = 1. K_T = 1. fdis = 1.e-4 t_hyd = 0. lag_time = True # Get the initial conditions (bub_obj, y0) = single_bubble_model.sbm_ic(profile, bub, np.array([0., 0., z0]), de, yk, T0, K, K_T, fdis, t_hyd, lag_time) # Check the initial condition values assert y0[0] == 0. assert y0[1] == 0. assert y0[2] == z0 assert y0[-1] == T0 * np.sum(y0[3:-1]) * seawater.cp() * 0.5 assert_approx_equal(bub.diameter(y0[3:-1], T0, P), de, significant=6) # Check the bub_obj parameters for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert bub_obj.T0 == T0 assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == K_T assert bub_obj.fdis == fdis assert bub_obj.t_hyd == t_hyd for i in range(len(composition)-1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False
def outer_surf(yi, p): """ Compute the initial condition for the outer plume at the sea surface Computes the initial conditions for the first outer plume segment after the inner plume impinges on the free surface of the water body. It is assumed that the inner plume had significant volume flux and that this first outer plume segment will be viable. Parameters ---------- yi : `stratified_plume_model.InnerPlume` object Object for manipulating the inner plume state space p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. Returns ------- z0 : float Initial depth of the outer plume segment (m). y0 : ndarray Initial dependent variables state space for the outer plume segment. """ # The outer plume is a mixture of inner plume fluid and ambient fluid # entrained from the water surface Q = (1. + p.fe) * yi.Q T = (yi.T + yi.Ta * p.fe) * yi.Q / Q s = (yi.s + yi.Sa * p.fe) * yi.Q / Q c = (yi.c + yi.ca * p.fe) * yi.Q / Q rho = seawater.density(T, s, yi.P) # Use a Froude number approach to set the initial width and velocity u = outer_fr(yi.u, Q, yi.b, yi.rho_a, rho, p.g, p.Fro_0) # Calculate the outer plume state space variables y0 = [] Q = -Q y0.append(Q) y0.append(Q * (-u)) y0.append(s * Q) y0.append(p.rho_r * seawater.cp() * T * Q) y0.extend(c * Q) # Return the outer plume initial condition return (yi.z, np.array(y0))
def bent_plume_ic(profile, particles, Qj, A, D, X, phi_0, theta_0, Tj, Sj, Pj, rho_j, cj, chem_names, tracers, p): """ Build the Lagragian plume state space given the initial conditions Constructs the initial state space for a Lagrangian plume element from the initial values for the base plume variables (e.g., Q, J, u, S, T, etc.). Parameters ---------- profile : `ambient.Profile` object The ambient CTD object used by the single bubble model simulation. particles : list of `Particle` objects List of `bent_plume_model.Particle` objects containing the dispersed phase local conditions and behavior. Qj : Volume flux of continuous phase fluid at the discharge (m^3/s) A : Cross-sectional area of the discharge (M^2) D : float Diameter for the equivalent circular cross-section of the release (m) X : ndarray Release location (x, y, z) in (m) phi_0 : float Vertical angle from the horizontal for the discharge orientation (rad in range +/- pi/2) theta_0 : float Horizontal angle from the x-axis for the discharge orientation. The x-axis is taken in the direction of the ambient current. (rad in range 0 to 2 pi) Tj : float Temperature of the continuous phase fluid in the discharge (T) Sj : float Salinity of the continuous phase fluid in the discharge (psu) Pj : float Pressure at the discharge (Pa) rho_j : float Density of the continous phase fluid in the discharge (kg/m^3) cj : ndarray Concentration of passive tracers in the discharge (user-defined) chem_names : string list List of chemical parameters to track for the dissolution. Only the parameters in this list will be used to set background concentration for the dissolution, and the concentrations of these parameters are computed separately from those listed in `tracers` or inputed from the discharge through `cj`. tracers : string list List of passive tracers in the discharge. These can be chemicals present in the ambient `profile` data, and if so, entrainment of these chemicals will change the concentrations computed for these tracers. However, none of these concentrations are used in the dissolution of the dispersed phase. Hence, `tracers` should not contain any chemicals present in the dispersed phase particles. p : `stratified_plume_model.ModelParams` object Object containing the fixed model parameters for the stratified plume model. Returns ------- t : float Initial time for the simulation (s) q : ndarray Initial value of the plume state space """ # Set the dimensions of the initial Lagrangian plume element. b = D / 2. h = D / 5. # Measure the arc length along the plume s0 = 0. # The total discharge volume flux is the jet discharge since we assume # the void fraction of gas is negligible Q = Qj # Determine the time to fill the initial Lagrangian element dt = np.pi * b**2 * h / Q # Compute the mass of jet discharge in the initial Lagrangian element Mj = Qj * dt * rho_j # Evaluate the mass of particles in the intial Lagrangian element. Since # particles are tracked by number and mass per particle, we need to know # how many particles enter the Lagrangian element. This should be the # number flux in #/s time the fill time for the Lagrangian element, dt. # Store this value in the `Particle` objects for use throughout the model. nbe = np.zeros(len(particles)) for i in range(len(particles)): nbe[i] = particles[i].nb0 * dt particles[i].nbe = nbe[i] # Get the velocity in the component directions Uj = flux_to_velocity(Qj, A, phi_0, theta_0) # Compute the magnitude of the exit velocity V = np.sqrt(Uj[0]**2 + Uj[1]**2 + Uj[2]**2) # Build the continuous-phase portion of the model state space vector t = 0. q = [ Mj, Mj * Sj, Mj * seawater.cp() * Tj, Mj * Uj[0], Mj * Uj[1], Mj * Uj[2], h / V, X[0], X[1], X[2], s0 ] # Add in the state space for the dispersed phase particles q.extend(dispersed_phases.particles_state_space(particles, nbe)) # Add the ambient concentrations of the dispersed-phase chemicals ca = profile.get_values(X[2], chem_names) q.extend(Mj / rho_j * ca) # Add in the tracers discharged with the jet q.extend(Mj * cj) # Return the complete initial conditions return (t, np.array(q))
def inner_plume_ic(profile, particles, p, z, Q, A, S, T, chem_names): """ Build the inner plume state space given the initial conditions Constructs the state space for the inner plume from the initial values for Q, J, concentrations, and particle properties. The state space vector is organized as follows: y[0] = Q : Flow rate of entrained fluid y[1] = J : Momentum flux of entrained fluid y[2] = S : Salinity flux of entrained fluid y[3] = H : Heat flux of entrained fluid y[4:4 + np * (nchems + 1)] : Dispersed phase mass and heat fluxes y[5 + np * (nchems + 1):] : Mass fluxes of the dissolved components Parameters ---------- profile : `ambient.Profile` object The ambient CTD object used by the single bubble model simulation. particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase initial conditions p : `stratified_plume_model.ModelParams` object Object containing the fixed model parameters for the stratified plume model. z : float Depth of the release point (m) Q : float Initial volume flux of entrained seawater (m^3/s) A : float Cross-sectional area of the discharge (m^2) S : float Salinity of the entrained seawater (psu) T : float Temperature of the entrained seawater (K) chem_names : string list List of the names of the chemicals that will be tracked in the dissolved phase Returns ------- z : float Depth at the initial point of the plume (m) y : ndarray Initial inner plume state space (see description above) """ # Sequentially build the inner plume state space y = [Q, Q**2 / A, S * Q, p.rho_r * seawater.cp() * T * Q] # Add in the state space of the multiphase components nb0 = np.zeros(len(particles)) for i in range(len(particles)): nb0[i] = particles[i].nb0 y.extend(dispersed_phases.particles_state_space(particles, nb0)) # And the mass fluxes of dissolved components ca = profile.get_values(z, chem_names) y.extend(ca * Q) # Return the initial state space return (z, np.array(y))
def derivs_inner(z, y, yi, yo, particles, profile, p, neighbor): """ Calculate the derivatives for the system of ODEs for the inner plume Calculates the right-hand-side of the system of ODEs for the inner plume state space. These equations follow Socolofsky et al. (2008) very closely, with the exception that multiple dispersed phase particles are allowed within the inner plume. Heat transfer between the dispersed and continuous phase is also added. Parameters ---------- z : float Current value for the independent variable (depth in m). y : ndarray Current value for the inner plume state space vector. yi : `InnerPlume` Object for manipulating the inner plume state space yo : `OuterPlume` Object for manipulating the outer plume state space particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the outer plume state space Returns ------- yp : ndarray A vector of the derivatives of the inner plume state space. See Also -------- stratified_plume_model.InnerPlume, stratified_plume_model.OuterPlume, stratified_plume_model.inner_main, calculate Notes ----- It is important that the inner plume entrains fluid from either the ambient water (whenever the outer plume is not present) or the outer plume (whenever it is shrouding the inner plume). This is accomplished in `stratified_plume_model.OuterPlume`: if there is no outer plume segment, then the ambient conditions are stored in the outer plume variables. Thus, `yo.c[i]` is equivalent to `ca[i]` when there is no outer plume. This behavior is true for temperature, salinity, density and concentration. """ # Set up the output from the fuction to have the right size and type yp = np.zeros((yi.len, 1)) # Update the inner plume object with the corrent solution and compute # the inner plume shear entrainment coefficient yi.update(z, y, particles, profile, p) # Update the outer plume object at the current depth if z < np.min(neighbor.x): # This plume is above any existing outer plumes yo.update(z, np.zeros(yo.len), profile, p, yi.b) else: # Interpolate the outer plume solution to the current depth yo.update(z, neighbor(z), profile, p, yi.b) # Conservation of mass yp[0] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) + \ p.alpha_2 * yo.u) + yi.Ep # Conservation of momentum yp[1] = 1. / p.gamma_i * (np.pi * p.g * yi.b**2 / p.rho_r * \ (yi.Fb + p.lambda_2**2 * (1. - yi.Xi) * (yi.rho_a - \ yi.rho)) + 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * \ yo.u) * yo.u + p.alpha_2 * yo.u * yi.u) + yi.Ep * yi.u) # Conservation of salinity yp[2] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.s + p.alpha_2 * yo.u * yi.s) + yi.Ep * yi.s # Conservation of continuous phase fluid heat yp[3] = p.rho_r * seawater.cp() * (2. * np.pi * yi.b * (yi.alpha_s * \ (yi.u + p.c1 * yo.u) * yo.T + p.alpha_2 * yo.u * yi.T) + \ yi.Ep * yi.T) # Conservation equations for each dispersed phase idx = 4 # Track the mass dissolving into the continuous phase delDiss = np.zeros(yi.nchems) for i in range(yi.np): delDiss_p = np.zeros(yi.nchems) if particles[i].particle.issoluble: for j in range(yi.nchems): # Conservation of particle mass for soluble particles yp[idx] = -(particles[i].A * particles[i].nb0 / (yi.u + particles[i].us) * particles[i].beta[j] * (particles[i].Cs[j] - yi.c[j])) delDiss[j] += yp[idx] delDiss_p[j] += yp[idx] # Update continuous phase temperature with heat of solution yp[3] += yp[idx] * particles[i].particle.neg_dH_solR[j] * p.Ru / \ particles[i].particle.M[j] idx += 1 else: # Conservation of particle mass for insoluble particles yp[idx] = 0. idx += 1 # Conservation of particle heat including dissolution mass transfer yp[idx] = -particles[i].A * particles[i].nb0 / (yi.u + particles[i].us) * particles[i].rho_p * particles[i].cp * \ particles[i].beta_T * (particles[i].T - yi.T) + \ np.sum(delDiss_p) * particles[i].cp * particles[i].T # Take the heat leaving the particle and put it in the continuous # phase fluid yp[3] -= yp[idx] idx += 1 # Track the age of each particle by following its advection if yi.u + particles[i].us == 0.: yp[idx] = 0. else: yp[idx] = 1. / (yi.u + particles[i].us) idx += 1 # Track the location of each particle relative to the plume centerline yp[idx:idx + 3] = 0. idx += 3 # Conservation equations for the dissolved constituents. for i in range(yi.nchems): yp[idx] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.c[i] + p.alpha_2 * yo.u * yi.c[i]) + yi.Ep * \ yi.c[i] - delDiss[i] idx += 1 # z is positive downward (depth) return -yp
def derivs_outer(z, y, yi, yo, particles, profile, p, neighbor): """ Calculate the derivatives for the system of ODEs for the outer plume Calculates the right-hand-side of the system of ODEs for the outer plume state space. These equations follow those in Socolofsky et al. (2008). Parameters ---------- z : float Current value for the independent variable (depth in m). y : ndarray Current value for the outer plume state space vector. yi : `InnerPlume` Object for manipulating the inner plume state space yo : `OuterPlume` Object for manipulating the outer plume state space particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the outer plume state space Returns ------- yp : ndarray A vector of the derivatives of the outer plume state space. See Also -------- stratified_plume_model.InnerPlume, stratified_plume_model.OuterPlume, stratified_plume_model.outer_main, calculate """ # Set up the output from the function to have the correct size and type yp = np.zeros((yo.len, 1)) # Update the inner plume object at the current depth and the inner plume # shear entrainment coefficient if z > np.max(neighbor.x): yi.update(z, np.zeros(yi.len), particles, profile, p) else: yi.update(z, neighbor(z), particles, profile, p) # Update the outer plume object with the current solution yo.update(z, y, profile, p, yi.b) # Conservation of Mass: yp[0] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) + \ p.alpha_2 * yo.u) + 2. * np.pi * yo.b * p.alpha_3 * yo.u + yi.Ep # Conservation of Momentum: yp[1] = 1. / p.gamma_o * (-np.pi * p.g * (yo.b**2 - yi.b**2) / p.rho_r * \ (yo.rho_a - yo.rho) + 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + \ p.c1 * yo.u) * yo.u + p.alpha_2 * yo.u * yi.u) + yi.Ep * yi.u) # Conservation of Salinity: yp[2] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * yo.s + \ p.alpha_2 * yo.u * yi.s) + 2. * np.pi * yo.b * p.alpha_3 * \ yo.u * yo.Sa + yi.Ep * yi.s # Conservation of Heat: yp[3] = p.rho_r * seawater.cp() * (2. * np.pi * yi.b * (yi.alpha_s * \ (yi.u + p.c1 * yo.u) * yo.T + p.alpha_2 * yo.u * yi.T) + \ 2. * np.pi * yo.b * p.alpha_3 * yo.u * yo.Ta + yi.Ep * yi.T) # Conservation of tracked chemical constituents: idx = 4 for i in range(yo.nchems): yp[idx] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.c[i] + p.alpha_2 * yo.u * yi.c[i]) + 2. * np.pi * yo.b * \ p.alpha_3 * yo.u * yo.ca[i] + yi.Ep * yi.c[i] idx += 1 # z is positive downward (depth) return yp
def test_particle_obj(): """ Test the object behavior for the `PlumeParticle` object Test the instantiation and attribute data for the `PlumeParticle` object of the `stratified_plume_model` module. """ # Set up the base parameters describing a particle object T = 273.15 + 15. P = 150e5 Sa = 35. Ta = 273.15 + 4. composition = ['methane', 'ethane', 'propane', 'oxygen'] yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 lambda_1 = 0.85 K = 1. Kt = 1. fdis = 1e-6 # Compute a few derived quantities bub = dbm.FluidParticle(composition) nb0 = 1.e5 m0 = bub.masses_by_diameter(de, T, P, yk) # Create a `PlumeParticle` object bub_obj = dispersed_phases.PlumeParticle(bub, m0, T, nb0, lambda_1, P, Sa, Ta, K, Kt, fdis) # Check if the initialized attributes are correct for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert_array_almost_equal(bub_obj.m0, m0, decimal=6) assert bub_obj.T0 == T assert_array_almost_equal(bub_obj.m, m0, decimal=6) assert bub_obj.T == T assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == Kt assert bub_obj.fdis == fdis for i in range(len(composition) - 1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False assert bub_obj.nb0 == nb0 assert bub_obj.lambda_1 == lambda_1 # Including the values after the first call to the update method us_ans = bub.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = bub.density(m0, T, P) A_ans = bub.surface_area(m0, T, P, Sa, Ta) Cs_ans = bub.solubility(m0, T, P, Sa) beta_ans = bub.mass_transfer(m0, T, P, Sa, Ta) beta_T_ans = bub.heat_transfer(m0, T, P, Sa, Ta) assert bub_obj.us == us_ans assert bub_obj.rho_p == rho_p_ans assert bub_obj.A == A_ans assert_array_almost_equal(bub_obj.Cs, Cs_ans, decimal=6) assert_array_almost_equal(bub_obj.beta, beta_ans, decimal=6) assert bub_obj.beta_T == beta_T_ans # No need to test the properties or diameter objects since they are # inherited from the `single_bubble_model` and tested in `test_sbm`. # Check functionality of insoluble particle drop = dbm.InsolubleParticle(isfluid=True, iscompressible=True) m0 = drop.mass_by_diameter(de, T, P, Sa, Ta) drop_obj = dispersed_phases.PlumeParticle(drop, m0, T, nb0, lambda_1, P, Sa, Ta, K, fdis=fdis, K_T=Kt) assert len(drop_obj.composition) == 1 assert drop_obj.composition[0] == 'inert' assert_array_almost_equal(drop_obj.m0, m0, decimal=6) assert drop_obj.T0 == T assert_array_almost_equal(drop_obj.m, m0, decimal=6) assert drop_obj.T == T assert drop_obj.cp == seawater.cp() * 0.5 assert drop_obj.K == K assert drop_obj.K_T == Kt assert drop_obj.fdis == fdis assert drop_obj.diss_indices[0] == True assert drop_obj.nb0 == nb0 assert drop_obj.lambda_1 == lambda_1 # Including the values after the first call to the update method us_ans = drop.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = drop.density(T, P, Sa, Ta) A_ans = drop.surface_area(m0, T, P, Sa, Ta) beta_T_ans = drop.heat_transfer(m0, T, P, Sa, Ta) assert drop_obj.us == us_ans assert drop_obj.rho_p == rho_p_ans assert drop_obj.A == A_ans assert drop_obj.beta_T == beta_T_ans
def derivs_outer(z, y, yi, yo, particles, profile, p, neighbor): """ Calculate the derivatives for the system of ODEs for the outer plume Calculates the right-hand-side of the system of ODEs for the outer plume state space. These equations follow those in Socolofsky et al. (2008). Parameters ---------- z : float Current value for the independent variable (depth in m). y : ndarray Current value for the outer plume state space vector. yi : `InnerPlume` Object for manipulating the inner plume state space yo : `OuterPlume` Object for manipulating the outer plume state space particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the outer plume state space Returns ------- yp : ndarray A vector of the derivatives of the outer plume state space. See Also -------- stratified_plume_model.InnerPlume, stratified_plume_model.OuterPlume, stratified_plume_model.outer_main, calculate """ # Set up the output from the function to have the correct size and type yp = np.zeros((yo.len,1)) # Update the inner plume object at the current depth and the inner plume # shear entrainment coefficient if z > np.max(neighbor.x): yi.update(z, np.zeros(yi.len), particles, profile, p) else: yi.update(z, neighbor(z), particles, profile, p) # Update the outer plume object with the current solution yo.update(z, y, profile, p, yi.b) # Conservation of Mass: yp[0] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) + \ p.alpha_2 * yo.u) + 2. * np.pi * yo.b * p.alpha_3 * yo.u + yi.Ep # Conservation of Momentum: yp[1] = 1. / p.gamma_o * (-np.pi * p.g * (yo.b**2 - yi.b**2) / p.rho_r * \ (yo.rho_a - yo.rho) + 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + \ p.c1 * yo.u) * yo.u + p.alpha_2 * yo.u * yi.u) + yi.Ep * yi.u) # Conservation of Salinity: yp[2] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * yo.s + \ p.alpha_2 * yo.u * yi.s) + 2. * np.pi * yo.b * p.alpha_3 * \ yo.u * yo.Sa + yi.Ep * yi.s # Conservation of Heat: yp[3] = p.rho_r * seawater.cp() * (2. * np.pi * yi.b * (yi.alpha_s * \ (yi.u + p.c1 * yo.u) * yo.T + p.alpha_2 * yo.u * yi.T) + \ 2. * np.pi * yo.b * p.alpha_3 * yo.u * yo.Ta + yi.Ep * yi.T) # Conservation of tracked chemical constituents: idx = 4 for i in range(yo.nchems): yp[idx] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.c[i] + p.alpha_2 * yo.u * yi.c[i]) + 2. * np.pi * yo.b * \ p.alpha_3 * yo.u * yo.ca[i] + yi.Ep * yi.c[i] idx += 1 # z is positive downward (depth) return yp
def bent_plume_ic(profile, particles, Qj, A, D, X, phi_0, theta_0, Tj, Sj, Pj, rho_j, cj, chem_names, tracers, p): """ Build the Lagragian plume state space given the initial conditions Constructs the initial state space for a Lagrangian plume element from the initial values for the base plume variables (e.g., Q, J, u, S, T, etc.). Parameters ---------- profile : `ambient.Profile` object The ambient CTD object used by the single bubble model simulation. particles : list of `Particle` objects List of `bent_plume_model.Particle` objects containing the dispersed phase local conditions and behavior. Qj : Volume flux of continuous phase fluid at the discharge (m^3/s) A : Cross-sectional area of the discharge (M^2) D : float Diameter for the equivalent circular cross-section of the release (m) X : ndarray Release location (x, y, z) in (m) phi_0 : float Vertical angle from the horizontal for the discharge orientation (rad in range +/- pi/2) theta_0 : float Horizontal angle from the x-axis for the discharge orientation. The x-axis is taken in the direction of the ambient current. (rad in range 0 to 2 pi) Tj : float Temperature of the continuous phase fluid in the discharge (T) Sj : float Salinity of the continuous phase fluid in the discharge (psu) Pj : float Pressure at the discharge (Pa) rho_j : float Density of the continous phase fluid in the discharge (kg/m^3) cj : ndarray Concentration of passive tracers in the discharge (user-defined) chem_names : string list List of chemical parameters to track for the dissolution. Only the parameters in this list will be used to set background concentration for the dissolution, and the concentrations of these parameters are computed separately from those listed in `tracers` or inputed from the discharge through `cj`. tracers : string list List of passive tracers in the discharge. These can be chemicals present in the ambient `profile` data, and if so, entrainment of these chemicals will change the concentrations computed for these tracers. However, none of these concentrations are used in the dissolution of the dispersed phase. Hence, `tracers` should not contain any chemicals present in the dispersed phase particles. p : `stratified_plume_model.ModelParams` object Object containing the fixed model parameters for the stratified plume model. Returns ------- t : float Initial time for the simulation (s) q : ndarray Initial value of the plume state space """ # Set the dimensions of the initial Lagrangian plume element. b = D / 2. h = D / 5. # Measure the arc length along the plume s0 = 0. # The total discharge volume flux is the jet discharge since we assume # the void fraction of gas is negligible Q = Qj # Determine the time to fill the initial Lagrangian element dt = np.pi * b**2 * h / Q # Compute the mass of jet discharge in the initial Lagrangian element Mj = Qj * dt * rho_j # Evaluate the mass of particles in the intial Lagrangian element. Since # particles are tracked by number and mass per particle, we need to know # how many particles enter the Lagrangian element. This should be the # number flux in #/s time the fill time for the Lagrangian element, dt. # Store this value in the `Particle` objects for use throughout the model. nbe = np.zeros(len(particles)) for i in range(len(particles)): nbe[i] = particles[i].nb0 * dt particles[i].nbe = nbe[i] # Get the velocity in the component directions Uj = flux_to_velocity(Qj, A, phi_0, theta_0) # Compute the magnitude of the exit velocity V = np.sqrt(Uj[0]**2 + Uj[1]**2 + Uj[2]**2) # Build the continuous-phase portion of the model state space vector t = 0. q = [Mj, Mj * Sj, Mj * seawater.cp() * Tj, Mj * Uj[0], Mj * Uj[1], Mj * Uj[2], h / V, X[0], X[1], X[2], s0] # Add in the state space for the dispersed phase particles q.extend(dispersed_phases.particles_state_space(particles, nbe)) # Add the ambient concentrations of the dispersed-phase chemicals ca = profile.get_values(X[2], chem_names) q.extend(Mj / rho_j * ca) # Add in the tracers discharged with the jet q.extend(Mj*cj) # Return the complete initial conditions return (t, np.array(q))
def test_particle_obj(): """ Test the object behavior for the `Particle` object Test the instantiation and attribute data for the `Particle` object of the `single_bubble_model` module. """ # Set up the base parameters describing a particle object T = 273.15 + 15. P = 150e5 Sa = 35. Ta = 273.15 + 4. composition = ['methane', 'ethane', 'propane', 'oxygen'] yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 K = 1. Kt = 1. fdis = 1e-6 # Compute a few derived quantities bub = dbm.FluidParticle(composition) m0 = bub.masses_by_diameter(de, T, P, yk) # Create a `SingleParticle` object bub_obj = dispersed_phases.SingleParticle(bub, m0, T, K, fdis=fdis, K_T=Kt) # Check if the initial attributes are correct for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert_array_almost_equal(bub_obj.m0, m0, decimal=6) assert bub_obj.T0 == T assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == Kt assert bub_obj.fdis == fdis for i in range(len(composition)-1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False # Check if the values returned by the `properties` method match the input (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m0, T, P, Sa, Ta, 0.) us_ans = bub.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = bub.density(m0, T, P) A_ans = bub.surface_area(m0, T, P, Sa, Ta) Cs_ans = bub.solubility(m0, T, P, Sa) beta_ans = bub.mass_transfer(m0, T, P, Sa, Ta) beta_T_ans = bub.heat_transfer(m0, T, P, Sa, Ta) assert us == us_ans assert rho_p == rho_p_ans assert A == A_ans assert_array_almost_equal(Cs, Cs_ans, decimal=6) assert_array_almost_equal(beta, beta_ans, decimal=6) assert beta_T == beta_T_ans assert T == T_ans # Check that dissolution shuts down correctly m_dis = np.array([m0[0]*1e-10, m0[1]*1e-8, m0[2]*1e-3, 1.5e-5]) (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta[0] == 0. assert beta[1] == 0. assert beta[2] > 0. assert beta[3] > 0. m_dis = np.array([m0[0]*1e-10, m0[1]*1e-8, m0[2]*1e-7, 1.5e-16]) (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0.) assert np.sum(beta[0:-1]) == 0. assert us == 0. assert rho_p == seawater.density(Ta, Sa, P) # Check that heat transfer shuts down correctly (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, Ta, P, Sa, Ta, 0) assert beta_T == 0. (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta_T == 0. # Check the value returned by the `diameter` method de_p = bub_obj.diameter(m0, T, P, Sa, Ta) assert_approx_equal(de_p, de, significant=6) # Check functionality of insoluble particle drop = dbm.InsolubleParticle(isfluid=True, iscompressible=True) m0 = drop.mass_by_diameter(de, T, P, Sa, Ta) # Create a `Particle` object drop_obj = dispersed_phases.SingleParticle(drop, m0, T, K, fdis=fdis, K_T=Kt) # Check if the values returned by the `properties` method match the input (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties( np.array([m0]), T, P, Sa, Ta, 0) us_ans = drop.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = drop.density(T, P, Sa, Ta) A_ans = drop.surface_area(m0, T, P, Sa, Ta) beta_T_ans = drop.heat_transfer(m0, T, P, Sa, Ta) assert us == us_ans assert rho_p == rho_p_ans assert A == A_ans assert beta_T == beta_T_ans # Check that heat transfer shuts down correctly (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties(m_dis, Ta, P, Sa, Ta, 0) assert beta_T == 0. (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta_T == 0. # Check the value returned by the `diameter` method de_p = drop_obj.diameter(m0, T, P, Sa, Ta) assert_approx_equal(de_p, de, significant=6)
def outer_dis(yi, particles, profile, p, neighbor, z_0): """ Compute the initial condition for the outer plume at the DMPR Computes the initial conditions for the an outer plume segment at the depth of maximum plume rise (DMPR) following full dissolution of the dispersed phases. Parameters ---------- yi : `stratified_plume_model.InnerPlume` object Object for manipulating the inner plume state space. particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the inner plume state space. z_0 : float Top of the inner plume calculation (m). Returns ------- z0 : float Initial depth of the outer plume segment (m). y0 : ndarray Initial dependent variables state space for the outer plume segment. """ # Search for the maximum flux near the top of the plume Qmax = neighbor.y[0, 0] imax = 1 while Qmax < neighbor.y[imax, 0]: Qmax = neighbor.y[imax, 0] imax += 1 # Since most of this fluid will be regained as the outer plume descends # through Ep, take the initial volume flux as a small fraction (given # by model parameter qdis_ic) Q = p.qdis_ic * Qmax # Get the local plume properties at the top of the plume yi.update(z_0, neighbor(z_0), particles, profile, p) rho = yi.rho # Use a Froude number approach to set the initial width and velocity u = outer_fr(0.05, Q, yi.b, yi.rho_a, rho, p.g, p.Fro_0) # Calculate the outer plume state space variables y0 = [] Q = -Q y0.append(Q) y0.append(Q * (-u)) y0.append(yi.s * Q) y0.append(p.rho_r * seawater.cp() * yi.T * Q) y0.extend(yi.c * Q) # Return the outer plume initial condition return (yi.z, np.array(y0))
def derivs(t, q, q0_local, q1_local, profile, p, particles): """ Calculate the derivatives for the system of ODEs for a Lagrangian plume Calculates the right-hand-side of the system of ODEs for a Lagrangian plume integral model. The continuous phase model matches very closely the model of Lee and Cheung (1990), with adaptations for the shear entrainment following Jirka (2004). Multiphase extensions following the strategy in Socolofsky et al. (2008) with adaptation to Lagrangian plume models by Johansen (2000, 2003) and Yapa and Zheng (1997). Parameters ---------- t : float Current value for the independent variable (time in s). q : ndarray Current value for the plume state space vector. q0_local : `bent_plume_model.LagElement` Object containing the numerical solution at the previous time step q1_local : `bent_plume_model.LagElement` Object containing the numerical solution at the current time step profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the bent plume model. particles : list of `Particle` objects List of `bent_plume_model.Particle` objects containing the dispersed phase local conditions and behavior. Returns ------- yp : ndarray A vector of the derivatives of the plume state space. See Also -------- calculate """ # Set up the output from the function to have the right size and shape qp = np.zeros(q.shape) # Update the local Lagrangian element properties q1_local.update(t, q, profile, p, particles) # Get the entrainment flux md = entrainment(q0_local, q1_local, p) # Get the dispersed phase tracking variables (fe, up, dtp_dt) = track_particles(q0_local, q1_local, md, particles) # Conservation of Mass qp[0] = md # Conservation of salt and heat qp[1] = md * q1_local.Sa qp[2] = md * seawater.cp() * q1_local.Ta # Conservation of continuous phase momentum. Note that z is positive # down (depth). qp[3] = md * q1_local.ua qp[4] = md * q1_local.va qp[5] = -p.g / (p.gamma * p.rho_r) * ( q1_local.Fb + q1_local.M * (q1_local.rho_a - q1_local.rho)) + md * q1_local.wa # Constant h/V thickeness to velocity ratio qp[6] = 0. # Lagrangian plume element advection (x, y, z) and s along the centerline # trajectory qp[7] = q1_local.u qp[8] = q1_local.v qp[9] = q1_local.w qp[10] = q1_local.V # Conservation equations for each dispersed phase idx = 11 # Track the mass dissolving into the continuous phase per unit time dm = np.zeros(q1_local.nchems) # Compute mass and heat transfer for each particle for i in range(len(particles)): # Only simulate particles inside the plume if particles[i].integrate: # Dissolution and Biodegradation if particles[i].particle.issoluble: # Dissolution mass transfer for each particle component dm_pc = - particles[i].A * particles[i].nbe * \ particles[i].beta * (particles[i].Cs - q1_local.c_chems) * dtp_dt[i] # Update continuous phase temperature with heat of # solution qp[2] += np.sum(dm_pc * \ particles[i].particle.neg_dH_solR \ * p.Ru / particles[i].particle.M) # Biodegradation for for each particle component dm_pb = -particles[i].k_bio * particles[i].m * \ particles[i].nbe * dtp_dt[i] # Conservation of mass for dissolution and biodegradation qp[idx:idx + q1_local.nchems] = dm_pc + dm_pb # Update position in state space idx += q1_local.nchems else: # No dissolution dm_pc = np.zeros(q1_local.nchems) # Biodegradation for insoluble particles dm_pb = -particles[i].k_bio * particles[i].m * \ particles[i].nbe * dtp_dt[i] qp[idx] = dm_pb idx += 1 # Update the total mass dissolved dm += dm_pc # Heat transfer between the particle and the ambient qp[idx] = - particles[i].A * particles[i].nbe * \ particles[i].rho_p * particles[i].cp * \ particles[i].beta_T * (particles[i].T - \ q1_local.T) * dtp_dt[i] # Heat loss due to mass loss qp[idx] += np.sum(dm_pc + dm_pb) * particles[i].cp * \ particles[i].T # Take the heat leaving the particle and put it in the continuous # phase fluid qp[2] -= qp[idx] idx += 1 # Particle age qp[idx] = dtp_dt[i] idx += 1 # Follow the particles in the local coordinate system (l,n,m) # relative to the plume centerline qp[idx] = 0. idx += 1 qp[idx] = (up[i, 1] - fe * q[idx]) * dtp_dt[i] idx += 1 qp[idx] = (up[i, 2] - fe * q[idx]) * dtp_dt[i] idx += 1 else: idx += particles[i].particle.nc + 5 # Conservation equations for the dissolved constituents in the plume qp[idx:idx+q1_local.nchems] = md / q1_local.rho_a * q1_local.ca_chems \ - dm - q1_local.k_bio * q1_local.cpe idx += q1_local.nchems # Conservation equation for the passive tracers in the plume qp[idx:] = md / q1_local.rho_a * q1_local.ca_tracers # Return the slopes return qp
def outer_cpic(yi, yo, particles, profile, p, neighbor, z_0): """ Compute the initial condition for the outer plume at depth Computes the initial conditions for the an outer plume segment within the reservoir body. Part of the calculation determines whether or not the computed initial condition has enough downward momentum to be viable as an initial condition (e.g., whether or not it will be overwhelmed by the upward drag of the inner plume). Parameters ---------- yi : `stratified_plume_model.InnerPlume` object Object for manipulating the inner plume state space. yo : `stratified_plume_model.OuterPlume` object Object for manipulating the outer plume state space. particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the inner plume state space. z_0 : float Top of the inner plume calculation (m). Returns ------- z0 : float Initial depth of the outer plume segment (m). y0 : ndarray Initial dependent variables state space for the outer plume segment. flag : bool Outer plume viability flag: `True` means the outer plume segment is viable and should be integrated; `False` means the outer plume segment is too weak and should be discarded, moving down the inner plume to calculate the next outer plume initial condition. Notes ----- The iteration required to find a viable outer plume segment is conducted by the `stratified_plume_model.outer_main` function. This function computes the initial conditions for one attempt to find an outer plume segment and reports back (through `flag`) on the success. There is one caveat to the above statement. The model parameter `p.nwidths` determines the vertical scale over which this function may integrate to find the start to an outer plume, given as a integer number of times of the inner plume half-width. This function starts by searching one half-width. If `p.nwidths` is greater than one, it will continue to expand the search region. The physical interpretation of `p.nwidths` is to set a reasonable upper bound on the diameter of eddies shed from the inner plume in the peeling region into the outer plume. While the integral model does not have "eddies" per se, the search window size should still be representative of this type of length scale. """ # Start the iteration counters iter = 0 done = False # Compute the outer plume initial conditions until the outer plume is # viable or until the maximum number of widths is integrated while not done and iter < p.nwidths: # Update iteration counter iter += 1 # Get the inner plume properties at the top of this peeling region yi.update(z_0, neighbor(z_0), particles, profile, p) # Set the range to integrate to get the current peeling flux z_upper = z_0 z_lower = z_0 + iter * yi.b # Check if the bottom of the reservoir is encountered. if z_lower > profile.z_max: z_lower = profile.z_max # Find the indices in the raw data for the inner plume solution close # to where z_upper and z_lower occur i_upper = np.min(np.where(neighbor.x >= z_upper)[0]) i_lower = np.max(np.where(neighbor.x <= z_lower)[0]) # Get the grid of elevations where we will integrate the solution to # obtain the initial flux for the outer plume. This is needed # because the solution is so stiff: if we integrated over a fixed # step size, we could easily miss dramatic changes in the solution. # Hence, we integrate over the steps in the numerical solution # itself. n_grid = i_lower - i_upper + 3 zi = np.zeros(n_grid) zi[0] = z_upper zi[-1] = z_lower zi[1:-1] = neighbor.x[i_upper:i_lower+1] # Integrate the peeling fluid over this grid to get the total # contributions going into the outer plume Q = 0. tracer_vars = np.zeros(2 + yi.nchems) for i in range(len(zi)-1): yi.update(zi[i], neighbor(zi[i]), particles, profile, p) dz = zi[i+1] - zi[i] Q = Q + yi.Ep * dz tracer_vars = tracer_vars + np.hstack((yi.s, yi.T * p.rho_r * \ seawater.cp(), yi.c)) * yi.Ep * dz # Get the initial velocity of the peeling fluid using the modified # outer plume Froude number condition T = tracer_vars[1] / (Q * p.rho_r * seawater.cp()) s = tracer_vars[0] / Q c = tracer_vars[2:] / Q rho = seawater.density(T, s, yi.P) u = outer_fr(0.05, -Q, yi.b, yi.rho_a, rho, p.g, p.Fro_0) b = np.sqrt(Q**2 / (np.pi * (-Q) * u) + yi.b**2) dQdz = 2. * np.pi * yi.b * (p.alpha_1 * (yi.u + p.c1 * (-u)) + \ p.alpha_2 * (-u)) + 2. * np.pi * b * p.alpha_3 * (-u) + yi.Ep # Check whether this outer plume segment will be viable if dQdz > 0 or Q > 0 or np.isnan(Q): # This outer plume segment is not viable flag = False z0 = np.array([z_0, z_lower]) y0 = np.array([np.zeros(yo.len), np.zeros(yo.len)]) else: # This outer plume segmet is viable...stop integrating widths done = True # Check where the diffuser is if z_lower >= yi.z0: # This outer plume segment should not exist flag = False z0 = np.array([z_0, z_lower]) y0 = np.array([np.zeros(yo.len), np.zeros(yo.len)]) else: # This is the next outer plume segment to integrate flag = True z0 = z_lower y0 = [] y0.append(Q) y0.append(Q * (-u)) y0.append(s * Q) y0.append(p.rho_r * seawater.cp() * T * Q) y0.extend(c * Q) # Return the results of the initial conditions search return (z0, np.array(y0), flag)
def outer_dis(yi, particles, profile, p, neighbor, z_0): """ Compute the initial condition for the outer plume at the DMPR Computes the initial conditions for the an outer plume segment at the depth of maximum plume rise (DMPR) following full dissolution of the dispersed phases. Parameters ---------- yi : `stratified_plume_model.InnerPlume` object Object for manipulating the inner plume state space. particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the inner plume state space. z_0 : float Top of the inner plume calculation (m). Returns ------- z0 : float Initial depth of the outer plume segment (m). y0 : ndarray Initial dependent variables state space for the outer plume segment. """ # Search for the maximum flux near the top of the plume Qmax = neighbor.y[0,0] imax = 1 while Qmax < neighbor.y[imax,0]: Qmax = neighbor.y[imax,0] imax += 1 # Since most of this fluid will be regained as the outer plume descends # through Ep, take the initial volume flux as a small fraction (given # by model parameter qdis_ic) Q = p.qdis_ic * Qmax # Get the local plume properties at the top of the plume yi.update(z_0, neighbor(z_0), particles, profile, p) rho = yi.rho # Use a Froude number approach to set the initial width and velocity u = outer_fr(0.05, Q, yi.b, yi.rho_a, rho, p.g, p.Fro_0) # Calculate the outer plume state space variables y0 = [] Q = -Q y0.append(Q) y0.append(Q * (-u)) y0.append(yi.s * Q) y0.append(p.rho_r * seawater.cp() * yi.T * Q) y0.extend(yi.c * Q) # Return the outer plume initial condition return (yi.z, np.array(y0))
def derivs_inner(z, y, yi, yo, particles, profile, p, neighbor): """ Calculate the derivatives for the system of ODEs for the inner plume Calculates the right-hand-side of the system of ODEs for the inner plume state space. These equations follow Socolofsky et al. (2008) very closely, with the exception that multiple dispersed phase particles are allowed within the inner plume. Heat transfer between the dispersed and continuous phase is also added. Parameters ---------- z : float Current value for the independent variable (depth in m). y : ndarray Current value for the inner plume state space vector. yi : `InnerPlume` Object for manipulating the inner plume state space yo : `OuterPlume` Object for manipulating the outer plume state space particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the outer plume state space Returns ------- yp : ndarray A vector of the derivatives of the inner plume state space. See Also -------- stratified_plume_model.InnerPlume, stratified_plume_model.OuterPlume, stratified_plume_model.inner_main, calculate Notes ----- It is important that the inner plume entrains fluid from either the ambient water (whenever the outer plume is not present) or the outer plume (whenever it is shrouding the inner plume). This is accomplished in `stratified_plume_model.OuterPlume`: if there is no outer plume segment, then the ambient conditions are stored in the outer plume variables. Thus, `yo.c[i]` is equivalent to `ca[i]` when there is no outer plume. This behavior is true for temperature, salinity, density and concentration. """ # Set up the output from the fuction to have the right size and type yp = np.zeros((yi.len, 1)) # Update the inner plume object with the corrent solution and compute # the inner plume shear entrainment coefficient yi.update(z, y, particles, profile, p) # Update the outer plume object at the current depth if z < np.min(neighbor.x): # This plume is above any existing outer plumes yo.update(z, np.zeros(yo.len), profile, p, yi.b) else: # Interpolate the outer plume solution to the current depth yo.update(z, neighbor(z), profile, p, yi.b) # Conservation of mass yp[0] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) + \ p.alpha_2 * yo.u) + yi.Ep # Conservation of momentum yp[1] = 1. / p.gamma_i * (np.pi * p.g * yi.b**2 / p.rho_r * \ (yi.Fb + p.lambda_2**2 * (1. - yi.Xi) * (yi.rho_a - \ yi.rho)) + 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * \ yo.u) * yo.u + p.alpha_2 * yo.u * yi.u) + yi.Ep * yi.u) # Conservation of salinity yp[2] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.s + p.alpha_2 * yo.u * yi.s) + yi.Ep * yi.s # Conservation of continuous phase fluid heat yp[3] = p.rho_r * seawater.cp() * (2. * np.pi * yi.b * (yi.alpha_s * \ (yi.u + p.c1 * yo.u) * yo.T + p.alpha_2 * yo.u * yi.T) + \ yi.Ep * yi.T) # Conservation equations for each dispersed phase idx = 4 # Track the mass dissolving into the continuous phase delDiss = np.zeros(yi.nchems) for i in range(yi.np): delDiss_p = np.zeros(yi.nchems) if particles[i].particle.issoluble: for j in range(yi.nchems): # Conservation of particle mass for soluble particles yp[idx] = -(particles[i].A * particles[i].nb0 / (yi.u + particles[i].us) * particles[i].beta[j] * (particles[i].Cs[j] - yi.c[j])) delDiss[j] += yp[idx] delDiss_p[j] += yp[idx] # Update continuous phase temperature with heat of solution yp[3] += yp[idx] * particles[i].particle.neg_dH_solR[j] * p.Ru / \ particles[i].particle.M[j] idx += 1 else: # Conservation of particle mass for insoluble particles yp[idx] = 0. idx += 1 # Conservation of particle heat including dissolution mass transfer yp[idx] = -particles[i].A * particles[i].nb0 / (yi.u + particles[i].us) * particles[i].rho_p * particles[i].cp * \ particles[i].beta_T * (particles[i].T - yi.T) + \ np.sum(delDiss_p) * particles[i].cp * particles[i].T # Take the heat leaving the particle and put it in the continuous # phase fluid yp[3] -= yp[idx] idx += 1 # Track the age of each particle by following its advection if yi.u + particles[i].us == 0.: yp[idx] = 0. else: yp[idx] = 1. / (yi.u + particles[i].us) idx += 1 # Track the location of each particle relative to the plume centerline yp[idx:idx+3] = 0. idx += 3 # Conservation equations for the dissolved constituents. for i in range(yi.nchems): yp[idx] = 2. * np.pi * yi.b * (yi.alpha_s * (yi.u + p.c1 * yo.u) * \ yo.c[i] + p.alpha_2 * yo.u * yi.c[i]) + yi.Ep * \ yi.c[i] - delDiss[i] idx += 1 # z is positive downward (depth) return -yp
def outer_cpic(yi, yo, particles, profile, p, neighbor, z_0): """ Compute the initial condition for the outer plume at depth Computes the initial conditions for the an outer plume segment within the reservoir body. Part of the calculation determines whether or not the computed initial condition has enough downward momentum to be viable as an initial condition (e.g., whether or not it will be overwhelmed by the upward drag of the inner plume). Parameters ---------- yi : `stratified_plume_model.InnerPlume` object Object for manipulating the inner plume state space. yo : `stratified_plume_model.OuterPlume` object Object for manipulating the outer plume state space. particles : list of `Particle` objects List of `Particle` objects containing the dispersed phase local conditions and behavior. profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the stratified plume model. neighbor : `scipy.interpolate.interp1d` object Container holding the latest solution for the inner plume state space. z_0 : float Top of the inner plume calculation (m). Returns ------- z0 : float Initial depth of the outer plume segment (m). y0 : ndarray Initial dependent variables state space for the outer plume segment. flag : bool Outer plume viability flag: `True` means the outer plume segment is viable and should be integrated; `False` means the outer plume segment is too weak and should be discarded, moving down the inner plume to calculate the next outer plume initial condition. Notes ----- The iteration required to find a viable outer plume segment is conducted by the `stratified_plume_model.outer_main` function. This function computes the initial conditions for one attempt to find an outer plume segment and reports back (through `flag`) on the success. There is one caveat to the above statement. The model parameter `p.nwidths` determines the vertical scale over which this function may integrate to find the start to an outer plume, given as a integer number of times of the inner plume half-width. This function starts by searching one half-width. If `p.nwidths` is greater than one, it will continue to expand the search region. The physical interpretation of `p.nwidths` is to set a reasonable upper bound on the diameter of eddies shed from the inner plume in the peeling region into the outer plume. While the integral model does not have "eddies" per se, the search window size should still be representative of this type of length scale. """ # Start the iteration counters iter = 0 done = False # Compute the outer plume initial conditions until the outer plume is # viable or until the maximum number of widths is integrated while not done and iter < p.nwidths: # Update iteration counter iter += 1 # Get the inner plume properties at the top of this peeling region yi.update(z_0, neighbor(z_0), particles, profile, p) # Set the range to integrate to get the current peeling flux z_upper = z_0 z_lower = z_0 + iter * yi.b # Check if the bottom of the reservoir is encountered. if z_lower > profile.z_max: z_lower = profile.z_max # Find the indices in the raw data for the inner plume solution close # to where z_upper and z_lower occur i_upper = np.min(np.where(neighbor.x >= z_upper)[0]) i_lower = np.max(np.where(neighbor.x <= z_lower)[0]) # Get the grid of elevations where we will integrate the solution to # obtain the initial flux for the outer plume. This is needed # because the solution is so stiff: if we integrated over a fixed # step size, we could easily miss dramatic changes in the solution. # Hence, we integrate over the steps in the numerical solution # itself. n_grid = i_lower - i_upper + 3 zi = np.zeros(n_grid) zi[0] = z_upper zi[-1] = z_lower zi[1:-1] = neighbor.x[i_upper:i_lower + 1] # Integrate the peeling fluid over this grid to get the total # contributions going into the outer plume Q = 0. tracer_vars = np.zeros(2 + yi.nchems) for i in range(len(zi) - 1): yi.update(zi[i], neighbor(zi[i]), particles, profile, p) dz = zi[i + 1] - zi[i] Q = Q + yi.Ep * dz tracer_vars = tracer_vars + np.hstack((yi.s, yi.T * p.rho_r * \ seawater.cp(), yi.c)) * yi.Ep * dz # Get the initial velocity of the peeling fluid using the modified # outer plume Froude number condition T = tracer_vars[1] / (Q * p.rho_r * seawater.cp()) s = tracer_vars[0] / Q c = tracer_vars[2:] / Q rho = seawater.density(T, s, yi.P) u = outer_fr(0.05, -Q, yi.b, yi.rho_a, rho, p.g, p.Fro_0) b = np.sqrt(Q**2 / (np.pi * (-Q) * u) + yi.b**2) dQdz = 2. * np.pi * yi.b * (p.alpha_1 * (yi.u + p.c1 * (-u)) + \ p.alpha_2 * (-u)) + 2. * np.pi * b * p.alpha_3 * (-u) + yi.Ep # Check whether this outer plume segment will be viable if dQdz > 0 or Q > 0 or np.isnan(Q): # This outer plume segment is not viable flag = False z0 = np.array([z_0, z_lower]) y0 = np.array([np.zeros(yo.len), np.zeros(yo.len)]) else: # This outer plume segmet is viable...stop integrating widths done = True # Check where the diffuser is if z_lower >= yi.z0: # This outer plume segment should not exist flag = False z0 = np.array([z_0, z_lower]) y0 = np.array([np.zeros(yo.len), np.zeros(yo.len)]) else: # This is the next outer plume segment to integrate flag = True z0 = z_lower y0 = [] y0.append(Q) y0.append(Q * (-u)) y0.append(s * Q) y0.append(p.rho_r * seawater.cp() * T * Q) y0.extend(c * Q) # Return the results of the initial conditions search return (z0, np.array(y0), flag)
def test_particle_obj(): """ Test the object behavior for the `Particle` object Test the instantiation and attribute data for the `Particle` object of the `bent_plume_model` module. """ # Set up the base parameters describing a particle object T = 273.15 + 15. P = 150e5 Sa = 35. Ta = 273.15 + 4. composition = ['methane', 'ethane', 'propane', 'oxygen'] yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 lambda_1 = 0.85 K = 1. Kt = 1. fdis = 1e-6 x = 0. y = 0. z = 0. # Compute a few derived quantities bub = dbm.FluidParticle(composition) nb0 = 1.e5 m0 = bub.masses_by_diameter(de, T, P, yk) # Create a `PlumeParticle` object bub_obj = bent_plume_model.Particle(x, y, z, bub, m0, T, nb0, lambda_1, P, Sa, Ta, K, Kt, fdis) # Check if the initialized attributes are correct assert bub_obj.integrate == True assert bub_obj.sim_stored == False assert bub_obj.farfield == False assert bub_obj.t == 0. assert bub_obj.x == x assert bub_obj.y == y assert bub_obj.z == z for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert_array_almost_equal(bub_obj.m0, m0, decimal=6) assert bub_obj.T0 == T assert_array_almost_equal(bub_obj.m, m0, decimal=6) assert bub_obj.T == T assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == Kt assert bub_obj.fdis == fdis for i in range(len(composition)-1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False assert bub_obj.nb0 == nb0 assert bub_obj.lambda_1 == lambda_1 # Including the values after the first call to the update method us_ans = bub.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = bub.density(m0, T, P) A_ans = bub.surface_area(m0, T, P, Sa, Ta) Cs_ans = bub.solubility(m0, T, P, Sa) beta_ans = bub.mass_transfer(m0, T, P, Sa, Ta) beta_T_ans = bub.heat_transfer(m0, T, P, Sa, Ta) assert bub_obj.us == us_ans assert bub_obj.rho_p == rho_p_ans assert bub_obj.A == A_ans assert_array_almost_equal(bub_obj.Cs, Cs_ans, decimal=6) assert_array_almost_equal(bub_obj.beta, beta_ans, decimal=6) assert bub_obj.beta_T == beta_T_ans # Test the bub_obj.outside() method bub_obj.outside(Ta, Sa, P) assert bub_obj.us == 0. assert bub_obj.rho_p == seawater.density(Ta, Sa, P) assert bub_obj.A == 0. assert_array_almost_equal(bub_obj.Cs, np.zeros(len(composition))) assert_array_almost_equal(bub_obj.beta, np.zeros(len(composition))) assert bub_obj.beta_T == 0. assert bub_obj.T == Ta # No need to test the properties or diameter objects since they are # inherited from the `single_bubble_model` and tested in `test_sbm`. # No need to test the bub_obj.track(), bub_obj.run_sbm() since they will # be tested below for the simulation cases. # Check functionality of insoluble particle drop = dbm.InsolubleParticle(isfluid=True, iscompressible=True) m0 = drop.mass_by_diameter(de, T, P, Sa, Ta) drop_obj = bent_plume_model.Particle(x, y, z, drop, m0, T, nb0, lambda_1, P, Sa, Ta, K, fdis=fdis, K_T=Kt) assert len(drop_obj.composition) == 1 assert drop_obj.composition[0] == 'inert' assert_array_almost_equal(drop_obj.m0, m0, decimal=6) assert drop_obj.T0 == T assert_array_almost_equal(drop_obj.m, m0, decimal=6) assert drop_obj.T == T assert drop_obj.cp == seawater.cp() * 0.5 assert drop_obj.K == K assert drop_obj.K_T == Kt assert drop_obj.fdis == fdis assert drop_obj.diss_indices[0] == True assert drop_obj.nb0 == nb0 assert drop_obj.lambda_1 == lambda_1 # Including the values after the first call to the update method us_ans = drop.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = drop.density(T, P, Sa, Ta) A_ans = drop.surface_area(m0, T, P, Sa, Ta) beta_T_ans = drop.heat_transfer(m0, T, P, Sa, Ta) assert drop_obj.us == us_ans assert drop_obj.rho_p == rho_p_ans assert drop_obj.A == A_ans assert drop_obj.beta_T == beta_T_ans
def test_particle_obj(): """ Test the object behavior for the `Particle` object Test the instantiation and attribute data for the `Particle` object of the `single_bubble_model` module. """ # Set up the base parameters describing a particle object T = 273.15 + 15. P = 150e5 Sa = 35. Ta = 273.15 + 4. composition = ['methane', 'ethane', 'propane', 'oxygen'] yk = np.array([0.85, 0.07, 0.08, 0.0]) de = 0.005 K = 1. Kt = 1. fdis = 1e-6 # Compute a few derived quantities bub = dbm.FluidParticle(composition) m0 = bub.masses_by_diameter(de, T, P, yk) # Create a `SingleParticle` object bub_obj = dispersed_phases.SingleParticle(bub, m0, T, K, fdis=fdis, K_T=Kt) # Check if the initial attributes are correct for i in range(len(composition)): assert bub_obj.composition[i] == composition[i] assert_array_almost_equal(bub_obj.m0, m0, decimal=6) assert bub_obj.T0 == T assert bub_obj.cp == seawater.cp() * 0.5 assert bub_obj.K == K assert bub_obj.K_T == Kt assert bub_obj.fdis == fdis for i in range(len(composition) - 1): assert bub_obj.diss_indices[i] == True assert bub_obj.diss_indices[-1] == False # Check if the values returned by the `properties` method match the input (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m0, T, P, Sa, Ta, 0.) us_ans = bub.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = bub.density(m0, T, P) A_ans = bub.surface_area(m0, T, P, Sa, Ta) Cs_ans = bub.solubility(m0, T, P, Sa) beta_ans = bub.mass_transfer(m0, T, P, Sa, Ta) beta_T_ans = bub.heat_transfer(m0, T, P, Sa, Ta) assert us == us_ans assert rho_p == rho_p_ans assert A == A_ans assert_array_almost_equal(Cs, Cs_ans, decimal=6) assert_array_almost_equal(beta, beta_ans, decimal=6) assert beta_T == beta_T_ans assert T == T_ans # Check that dissolution shuts down correctly m_dis = np.array([m0[0] * 1e-10, m0[1] * 1e-8, m0[2] * 1e-3, 1.5e-5]) (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta[0] == 0. assert beta[1] == 0. assert beta[2] > 0. assert beta[3] > 0. m_dis = np.array([m0[0] * 1e-10, m0[1] * 1e-8, m0[2] * 1e-7, 1.5e-16]) (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0.) assert np.sum(beta[0:-1]) == 0. assert us == 0. assert rho_p == seawater.density(Ta, Sa, P) # Check that heat transfer shuts down correctly (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, Ta, P, Sa, Ta, 0) assert beta_T == 0. (us, rho_p, A, Cs, beta, beta_T, T_ans) = bub_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta_T == 0. # Check the value returned by the `diameter` method de_p = bub_obj.diameter(m0, T, P, Sa, Ta) assert_approx_equal(de_p, de, significant=6) # Check functionality of insoluble particle drop = dbm.InsolubleParticle(isfluid=True, iscompressible=True) m0 = drop.mass_by_diameter(de, T, P, Sa, Ta) # Create a `Particle` object drop_obj = dispersed_phases.SingleParticle(drop, m0, T, K, fdis=fdis, K_T=Kt) # Check if the values returned by the `properties` method match the input (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties(np.array([m0]), T, P, Sa, Ta, 0) us_ans = drop.slip_velocity(m0, T, P, Sa, Ta) rho_p_ans = drop.density(T, P, Sa, Ta) A_ans = drop.surface_area(m0, T, P, Sa, Ta) beta_T_ans = drop.heat_transfer(m0, T, P, Sa, Ta) assert us == us_ans assert rho_p == rho_p_ans assert A == A_ans assert beta_T == beta_T_ans # Check that heat transfer shuts down correctly (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties(m_dis, Ta, P, Sa, Ta, 0) assert beta_T == 0. (us, rho_p, A, Cs, beta, beta_T, T_ans) = drop_obj.properties(m_dis, T, P, Sa, Ta, 0) assert beta_T == 0. # Check the value returned by the `diameter` method de_p = drop_obj.diameter(m0, T, P, Sa, Ta) assert_approx_equal(de_p, de, significant=6)
def derivs(t, q, q0_local, q1_local, profile, p, particles): """ Calculate the derivatives for the system of ODEs for a Lagrangian plume Calculates the right-hand-side of the system of ODEs for a Lagrangian plume integral model. The continuous phase model matches very closely the model of Lee and Cheung (1990), with adaptations for the shear entrainment following Jirka (2004). Multiphase extensions following the strategy in Socolofsky et al. (2008) with adaptation to Lagrangian plume models by Johansen (2000, 2003) and Yapa and Zheng (1997). Parameters ---------- t : float Current value for the independent variable (time in s). q : ndarray Current value for the plume state space vector. q0_local : `bent_plume_model.LagElement` Object containing the numerical solution at the previous time step q1_local : `bent_plume_model.LagElement` Object containing the numerical solution at the current time step profile : `ambient.Profile` object The ambient CTD object used by the simulation. p : `ModelParams` object Object containing the fixed model parameters for the bent plume model. particles : list of `Particle` objects List of `bent_plume_model.Particle` objects containing the dispersed phase local conditions and behavior. Returns ------- yp : ndarray A vector of the derivatives of the plume state space. See Also -------- calculate """ # Set up the output from the function to have the right size and shape qp = np.zeros(q.shape) # Update the local Lagrangian element properties q1_local.update(t, q, profile, p, particles) # Get the entrainment flux md = entrainment(q0_local, q1_local, p) # Get the dispersed phase tracking variables (fe, up, dtp_dt) = track_particles(q0_local, q1_local, md, particles) # Conservation of Mass qp[0] = md # Conservation of salt and heat qp[1] = md * q1_local.Sa qp[2] = md * seawater.cp() * q1_local.Ta # Conservation of continuous phase momentum. Note that z is positive # down (depth). qp[3] = md * q1_local.ua qp[4] = md * q1_local.va qp[5] = - p.g / (p.gamma * p.rho_r) * (q1_local.Fb + q1_local.M * (q1_local.rho_a - q1_local.rho)) + md * q1_local.wa # Constant h/V thickeness to velocity ratio qp[6] = 0. # Lagrangian plume element advection (x, y, z) and s along the centerline # trajectory qp[7] = q1_local.u qp[8] = q1_local.v qp[9] = q1_local.w qp[10] = q1_local.V # Conservation equations for each dispersed phase idx = 11 # Track the mass dissolving into the continuous phase per unit time dm = np.zeros(q1_local.nchems) # Compute mass and heat transfer for each particle for i in range(len(particles)): # Only simulate particles inside the plume if particles[i].integrate: # Dissolution and Biodegradation if particles[i].particle.issoluble: # Dissolution mass transfer for each particle component dm_pc = - particles[i].A * particles[i].nbe * \ particles[i].beta * (particles[i].Cs - q1_local.c_chems) * dtp_dt[i] # Update continuous phase temperature with heat of # solution qp[2] += np.sum(dm_pc * \ particles[i].particle.neg_dH_solR \ * p.Ru / particles[i].particle.M) # Biodegradation for for each particle component dm_pb = -particles[i].k_bio * particles[i].m * \ particles[i].nbe * dtp_dt[i] # Conservation of mass for dissolution and biodegradation qp[idx:idx+q1_local.nchems] = dm_pc + dm_pb # Update position in state space idx += q1_local.nchems else: # No dissolution dm_pc = np.zeros(q1_local.nchems) # Biodegradation for insoluble particles dm_pb = -particles[i].k_bio * particles[i].m * \ particles[i].nbe * dtp_dt[i] qp[idx] = dm_pb idx += 1 # Update the total mass dissolved dm += dm_pc # Heat transfer between the particle and the ambient qp[idx] = - particles[i].A * particles[i].nbe * \ particles[i].rho_p * particles[i].cp * \ particles[i].beta_T * (particles[i].T - \ q1_local.T) * dtp_dt[i] # Heat loss due to mass loss qp[idx] += np.sum(dm_pc + dm_pb) * particles[i].cp * \ particles[i].T # Take the heat leaving the particle and put it in the continuous # phase fluid qp[2] -= qp[idx] idx += 1 # Particle age qp[idx] = dtp_dt[i] idx += 1 # Follow the particles in the local coordinate system (l,n,m) # relative to the plume centerline qp[idx] = 0. idx += 1 qp[idx] = (up[i,1] - fe * q[idx]) * dtp_dt[i] idx += 1 qp[idx] = (up[i,2] - fe * q[idx]) * dtp_dt[i] idx += 1 else: idx += particles[i].particle.nc + 5 # Conservation equations for the dissolved constituents in the plume qp[idx:idx+q1_local.nchems] = md / q1_local.rho_a * q1_local.ca_chems \ - dm - q1_local.k_bio * q1_local.cpe idx += q1_local.nchems # Conservation equation for the passive tracers in the plume qp[idx:] = md / q1_local.rho_a * q1_local.ca_tracers # Return the slopes return qp