Ejemplo 5.12 de Ertekin, T., et. al. (2001) Basic Applied Reservoir Simulation.
Para la malla 1D con bloques centradas mostrada en la figura inferior, determinar la distribución de presión durante el primer año de producción. La presión inicial es de 6000 psi. Las propiedades de la roca y del fluido son Δx = 1000 ft, Δy = 1000 ft and Δz = 75 ft, Bo = 1 RB/STB, co = 3.5 x 10^-6 psi-1, kx = 15 md, ϕ = 0.18, μo = 10 cp. Emplea la formulación implícita y un paso de tiempo de Δt = 15 days.
En la siguiente sección definimos los datos de entrada. Importamos las librerias numpy y pandas para la creación de arreglos y dataframe. Para la visualización de los resultados importamos matplotlib.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
# Definición de parámetros iniciales
= 5 # numero de celdas
nx = 0.001127 # factor de conversión
beta = 5.614 # factor de conversión
alpha
# Dimensiones
= 1000 # ft, dimension in x direction
dx = 1000 # ft, dimension in y direction
dy = 75 # ft, dimension in z direction
dz
# Propiedades del yacimiento
= 0.18 # porosidad fracción
poro = 0 # compresibilidad de la roca psi^-1
Cr = 0.0000035 # compresibilidad del fluido psi^-1
Cf = 15 # permeabilidad, md
k = 1 # factor de volumen de formación del aceite, RB/STB
Bo = 10 # viscosidad, cp
vis
# Información del pozo
= 3.5 # in, radio de pozo
rw = 0 # skin
s = 4 # celda de pozo
cellP = -150 # gasto de producción del pozo, STB/D
qo
# Tiempo
= 15 # paso de tiempo, días
dt = 360 # tiempo total de simulación
TT = 6000 # psi, Icondición inicial Po
Definimos los términos de transmisibilidad en la dirección x
\[T_{x_{i\pm1/2}} = \beta_c\frac{k_x A_x}{\mu B \Delta x}\]
Definimos el termino de acumulación
\[\frac{V_b \phi (c_o + c_{\phi})}{\alpha B_o \Delta t}\]
# Cálculos adicionales
= dy * dz # Faces area
Ax = dx * dy * dz # Cells volume
vol = ((beta * Ax * k) / (vis * Bo * dx)) # East transmisibility
TE = ((beta * Ax * k) / (vis * Bo * dx)) # West transmisibility
TW
= ((vol * poro * (Cf + Cr)) / (alpha * Bo * dt))
Acum = dt
time = np.full(nx, Po) # Presión al tiempo n
Pt = np.full(nx, Po) # Presión al tiempo n + 1
Ptdt
# Results dataframes initialization
# For simplicity in Python, we'll use pandas DataFrame
= pd.DataFrame(data={'Time': [0], **{i+1: Pt[i] for i in range(nx)}})
results_cells = np.linspace(dx/2, (dx*nx)-(dx/2), num=nx)
cells_x = pd.DataFrame(columns=['Time', 'Pwf']) results_pwf
Thomas’ algorithm
Este algoritmo es aplicable a problemas de flujo en una dimensión
def thomas(a,b,c,d,n):
= [0] * n
cp = [0] * n
dp
0] = c[0]/b[0]
cp[0] = d[0]/b[0]
dp[
#Forward elimination
for j in range(1,n):
= b[j]-cp[j-1]*a[j]
m = c[j]/m
cp[j] = (d[j]-dp[j-1]*a[j])/m
dp[j]
= [0] * n
x -1] = dp[n-1]
x[n
for j in reversed(range(0,n-1)):
= dp[j]-cp[j]*x[j+1]
x[j]
return(x)
Definimos un ciclo de tiempo usando while(). Como se definió anteriormente, el vector Pt representan los valores de presión al tiempo n, valores conocidos, y el vector Ptdt, los valores de presión al tiempo n + 1, que son los valores incógnita del problema. En este caso la función thomas() nos estará regresando como resultados los valores del vector Ptdt. En cada iteración reasignamos los valores de ptdt a pt para calcular al siguiente paso de tiempo.
Para este ejemplo definimos los vectores a,b,c,d empleando de la ceda 2 a 5, ya que la presión de la celda 1 se mantiene constante por el tipo de frontera.
Debemos de modificar los valores del vector b de las celdas 2 y 5 por las condiciones de frontera. De manera similar modificamos el valor de la celda 2 del vector d, para el cual agregamos el valor conocido de la celda 1.
Con estas modificaciones corremos el ciclo while y obtenemos los resultados, observamos que los valores de la celda 1 se mantiene con un valor de 6000 para todos los pasos de tiempo.
# Variables para el control del ciclo
= 0
current_time = 0
steps
# Preparar vectores para el sistema tridiagonal
= np.zeros(nx-1)
a = np.zeros(nx-1)
b = np.zeros(nx-1)
c = np.zeros(nx-1)
d
while current_time < TT:
# Actualizar vectores para el sistema tridiagonal basado en condiciones de simulación
for i in range(nx-1):
= TW
a[i] = -(Acum + TE + TW)
b[i] = TE
c[i] = -Acum * Pt[i+1]
d[i]
# Condiciones de frontera
0] = -(Acum + TE + TW)
b[
-2] = -(Acum + TW)
b[nx
-2] = d[cellP-2] - qo
d[cellP
0] = -Acum * Pt[1] - Po * TW
d[
# Resolver el sistema para encontrar presiones en el siguiente paso de tiempo
= thomas(a, b, c, d,nx-1)
x
# Actualizar presiones y tiempo
#Pt = Ptdt
for j in range(1,nx):
= x[j-1]
Ptdt[j] = Ptdt[j]
Pt[j]
+= dt
current_time += 1
steps
# Almacenar resultados
= results_cells.append(pd.Series([current_time] + list(Ptdt), index=results_cells.columns), ignore_index=True)
results_cells
"Simulación completada con {} pasos.".format(steps)
'Simulación completada con 24 pasos.'
print(results_cells)
Time 1 2 3 4 5
0 0 6000 6000 6000 6000 6000
1 15 6000 5995 5968 5805 5964
2 30 6000 5984 5921 5654 5907
3 45 6000 5968 5867 5531 5837
4 60 6000 5948 5811 5426 5761
5 75 6000 5925 5754 5332 5682
6 90 6000 5901 5697 5246 5601
7 105 6000 5876 5642 5166 5520
8 120 6000 5850 5588 5090 5440
9 135 6000 5824 5536 5017 5362
10 150 6000 5798 5485 4947 5285
11 165 6000 5773 5435 4879 5210
12 180 6000 5748 5387 4814 5137
13 195 6000 5723 5340 4751 5065
14 210 6000 5699 5294 4689 4995
15 225 6000 5675 5250 4629 4927
16 240 6000 5652 5207 4570 4861
17 255 6000 5630 5165 4513 4797
18 270 6000 5608 5124 4458 4734
19 285 6000 5587 5084 4404 4673
20 300 6000 5566 5045 4352 4613
21 315 6000 5546 5007 4301 4555
22 330 6000 5527 4970 4251 4499
23 345 6000 5508 4934 4203 4444
24 360 6000 5489 4899 4156 4391
Por último, graficamos la presión de la celda 4, donde esta ubicado el pozo, contra tiempo, y generamos los perfiles de presión a lo largo del yacimiento para cada paso de tiempo.
#Suponiendo que 'results_cells' es tu DataFrame y 'cellP' es el número de celda del pozo
=(10, 6));
plt.figure(figsize'Time'], results_cells[cellP], label=f'Presión en Celda {cellP}');
plt.plot(results_cells['Evolución de la Presión en el Tiempo');
plt.title('Tiempo (días)');
plt.xlabel('Presión (psi)');
plt.ylabel(;
plt.legend()True);
plt.grid(;
plt.show()
#Grafio perfiles de presión
=(10, 6));
plt.figure(figsize
# Iterar sobre cada fila de la tabla para graficar
for index, row in results_cells.iterrows():
1:], label=f"Time: {row['Time']}");
plt.plot(row[
# Configuración del gráfico
[<matplotlib.lines.Line2D object at 0x00000000647B8128>]
[<matplotlib.lines.Line2D object at 0x00000000647B8400>]
[<matplotlib.lines.Line2D object at 0x00000000647B86A0>]
[<matplotlib.lines.Line2D object at 0x00000000647B8940>]
[<matplotlib.lines.Line2D object at 0x00000000647B8BE0>]
[<matplotlib.lines.Line2D object at 0x00000000647B8E80>]
[<matplotlib.lines.Line2D object at 0x0000000064C39160>]
[<matplotlib.lines.Line2D object at 0x0000000064C39400>]
[<matplotlib.lines.Line2D object at 0x0000000064C396A0>]
[<matplotlib.lines.Line2D object at 0x0000000064C39940>]
[<matplotlib.lines.Line2D object at 0x00000000647B8160>]
[<matplotlib.lines.Line2D object at 0x0000000064C39E48>]
[<matplotlib.lines.Line2D object at 0x0000000064C46128>]
[<matplotlib.lines.Line2D object at 0x0000000064C463C8>]
[<matplotlib.lines.Line2D object at 0x0000000064C46668>]
[<matplotlib.lines.Line2D object at 0x0000000064C46908>]
[<matplotlib.lines.Line2D object at 0x0000000064C46BA8>]
[<matplotlib.lines.Line2D object at 0x0000000064C46E48>]
[<matplotlib.lines.Line2D object at 0x0000000064C55128>]
[<matplotlib.lines.Line2D object at 0x0000000064C553C8>]
[<matplotlib.lines.Line2D object at 0x0000000064C55668>]
[<matplotlib.lines.Line2D object at 0x0000000064C55908>]
[<matplotlib.lines.Line2D object at 0x0000000064C55BA8>]
[<matplotlib.lines.Line2D object at 0x0000000064C55E48>]
[<matplotlib.lines.Line2D object at 0x0000000064C64128>]
'Celda');
plt.xlabel('Presión');
plt.ylabel('Perfil de presión');
plt.title(=(1.12, 1),loc='upper right',prop={'size': 5.8});
plt.legend(bbox_to_anchorTrue);
plt.grid(
# Mostrar el gráfico
; plt.show()
Usando la librería pyvista podemos generar una malla estructurada con las dimensiones de las celdas definidas, coloreamos las celdas de acuerdo con el valor de presión y visualizamos cada paso de tiempo añadiendo una slider widget. Por último, añadimos una línea para representar la ubicación del pozo en la celda 4.
import pyvista as pv
= 5, 1, 1
ni, nj, nk = 1000, 1000, 75
si, sj, sk
= np.arange(0, (ni + 1) * si, si)
xcorn = np.repeat(xcorn, 2)
xcorn = xcorn[1:-1]
xcorn = np.tile(xcorn, 4 * nj * nk)
xcorn
= np.arange(0, (nj + 1) * sj, sj)
ycorn = np.repeat(ycorn, 2)
ycorn = ycorn[1:-1]
ycorn = np.tile(ycorn, (2 * ni, 2 * nk))
ycorn = np.transpose(ycorn)
ycorn = ycorn.flatten()
ycorn
= np.arange(0, (nk + 1) * sk, sk)
zcorn = np.repeat(zcorn, 2)
zcorn = zcorn[1:-1]
zcorn = np.repeat(zcorn, (4 * ni * nj))
zcorn
= np.stack((xcorn, ycorn, zcorn))
corners = corners.transpose()
corners
= np.asarray((ni, nj, nk)) + 1
dims = pv.ExplicitStructuredGrid(dims, corners)
grid = grid.compute_connectivity()
grid
#grid.cell_data["colors"] = [5000,4000,3500,3500,2000]
#grid = grid.plot(show_edges=True)
= pv.Plotter()
pl #pl.add_mesh(grid, show_edges=True)
def create_mesh(value):
= int(value)
fila = list(results_cells.iloc[fila,1:])
presion "colors"] = presion
grid.cell_data[
3500, 500, 0], [3500, 500, 350]]), color='w', width=5, label=None, name=None)
pl.add_lines(np.array([[
=True)
pl.add_mesh(grid, show_edges#grid.cell_data["colors"] = presion
#pl.add_mesh(sphere, name="sphere", show_edges=True)
= pl.add_slider_widget(
slider
create_mesh,1, len(results_cells)],
[="%0.0f"
fmt
)
; pl.show()