Este proyecto tiene como propósito documentar y organizar un conjunto de códigos diseñados para la generación de números pseudoaleatorios, así como sus aplicaciones dentro del contexto de la simulación estocástica. A través de distintas técnicas y algoritmos, se busca explorar el comportamiento de estos generadores, su estructura matemática y su utilidad práctica en la simulación de fenómenos aleatorios. Esta documentación está orientada tanto a registrar el desarrollo técnico del proyecto como a facilitar su comprensión, reproducción y análisis.
El Generador Congruencial Multiplicativo (GCM) es un tipo de
generador de números pseudoaleatorios definido por la recurrencia:
\[x_{n+1} = (b * x_n ) \: mod \: m
\]
donde:
Cada número pseudoaleatorio \(u_n\) se obtiene normalizando el valor generado:
\[ u_n = \frac{x_n}{m} \]
Se han implementado varios generadores con diferentes parámetros clásicos:
RANDU: \[ m = 2^{31}, \quad b = 65539, \quad x_0 = 1 \]
MINSTD (Minimal Standard Generator): \[ m = 2^{31}, \quad b = 16807, \quad x_0 = 1 \]
RANF: \[ m = 2^{48}, \quad b = 44485709377909, \quad x_0 = 1 \]
ZX81 (basado en el generador de la computadora ZX81): \[ m = 2^{16} + 1, \quad b = 75, \quad x_0 = 1 \]
class GCM:
def __init__(self, modulo, multiplicador, semilla):
self.m = modulo
self.b = multiplicador
self.x0 = semilla
def muestra(self, n):
L = []
x = self.x0
for i in range(n):
x = (self.b * x) % self.m
L.append(x / self.m)
self.x0 = x
return L
RANDU = GCM(2**31, 65539, 1)
MINSTD = GCM(2**31, 16807, 1)
RANF = GCM(2**48, 44485709377909, 1)
ZX81 = GCM((2**16)+1, 75, 1)
## RANDU (10 muestras):
## [3.051897510886192e-05, 0.00018310965970158577, 0.0008239871822297573, 0.003295936156064272, 0.012359732296317816, 0.04449496837332845, 0.15573221957311034, 0.533938602078706, 0.8020416363142431, 0.006802399177104235]
## MINSTD (10 muestras):
## [7.826369255781174e-06, 0.1315377880819142, 0.7556042927317321, 0.44134794222190976, 0.7348649236373603, 0.8747715731151402, 0.2858293461613357, 0.9338209335692227, 0.7284304979257286, 0.7313786377198994]
## RANF (10 muestras):
## [0.15804498821804103, 0.8251314258663776, 0.3368007872298229, 0.8651650404273745, 0.07383628059541891, 0.5474493739795072, 0.5753776055978399, 0.6525021829214701, 0.7365476028938538, 0.5057520856606494]
## ZX81 (10 muestras):
## [0.0011443917176556754, 0.08582937882417566, 0.43720341181317424, 0.7902558859880678, 0.2691914491050857, 0.18935868288142577, 0.20190121610693196, 0.14259120801989716, 0.6943406014922868, 0.07554511192150999]
El Generador Congruencial Lineal (GCL) es una extensión del generador multiplicativo, e incluye una constante aditiva. Se define por la siguiente recurrencia:
\[ x_{n+1} = (b \cdot x_n + a) \: \bmod \: m \]
donde:
Cada número pseudoaleatorio se normaliza así:
\[ u_n = \frac{x_n}{m} \]
\[ m = 13, \quad b = 17, \quad a = 33, \quad x_0 = 77 \]
class GCL:
def __init__(self, modulo, multiplicador, constante, semilla):
self.m = modulo
self.b = multiplicador
self.a = constante
self.x0 = semilla
def muestra(self, n):
L = []
x = self.x0
for i in range(n):
x = (self.b * x + self.a) % self.m
L.append(x / self.m)
self.x0 = x
return L
gen1 = GCL(13, 17, 33, 77)
## GCL (muestra):
## [0.23076923076923078, 0.46153846153846156, 0.38461538461538464, 0.07692307692307693, 0.8461538461538461]
El Generador Congruencial Polinomial (GCP) es una generalización de los generadores congruenciales, en el que se utiliza un polinomio evaluado en la semilla para generar nuevos valores. Su fórmula se define como:
\[ x_{n+1} = \left( \sum_{i=0}^{k} b_i \cdot x_n^i \right) \bmod m \]
donde:
Y el valor pseudoaleatorio normalizado se calcula como:
\[ u_n = \frac{x_n}{m} \]
class GCP:
def __init__(self, modulo, coeficientes, semilla):
self.m = modulo
self.b = coeficientes
self.x0 = semilla
def muestra(self, n):
L = []
x = self.x0
for _ in range(n):
x = (sum(self.b[i] * (x ** i) for i in range(len(self.b)))) % self.m
L.append(x / self.m)
self.x0 = x
return L
gen1 = GCP(17, [5, 0, -1, 1], 2)
## GCP (1 muestra):
## [0.5294117647058824]
El Generador Congruencial Multiplicativo Combinado (GCMC) utiliza dos generadores congruenciales multiplicativos independientes y combina sus salidas para mejorar la calidad estadística del generador. La fórmula combinada se define como:
\[ \begin{aligned} x_{n+1} &= (b_1 \cdot x_n) \bmod m_1 \\ y_{n+1} &= (b_2 \cdot y_n) \bmod m_2 \\ z_{n+1} &= (x_{n+1} - y_{n+1}) \bmod m_1 \end{aligned} \]
donde:
El valor pseudoaleatorio combinado se normaliza como:
\[ u_n = \frac{z_n}{m_1} \]
\[ m_1 = 123, \quad m_2 = 131, \quad b_1 = 45, \quad b_2 = 28, \quad x_0 = 21, \quad y_0 = 11 \]
class GCMC:
def __init__(self, modulo1, modulo2, multiplicador1, multiplicador2, semilla1, semilla2):
self.m1 = modulo1
self.m2 = modulo2
self.b1 = multiplicador1
self.b2 = multiplicador2
self.x0 = semilla1
self.y0 = semilla2
def muestra(self, n):
L1 = []
L2 = []
L = []
x = self.x0
y = self.y0
for i in range(n):
x = (self.b1 * x) % self.m1
y = (self.b2 * y) % self.m2
z = (x - y) % self.m1
L1.append(x / self.m1)
L2.append(y / self.m2)
L.append(z / self.m1)
self.x0 = x
self.y0 = y
return L
gencomb1 = GCMC(123, 131, 45, 28, 21, 11)
## GCMC (5 muestras):
## [0.3089430894308943, 0.8455284552845529, 0.6097560975609756, 0.34959349593495936, 0.3983739837398374]
El Generador Congruencial Combinado Multivariado (GCCM) es una extensión combinada de generadores congruenciales multiplicativos que considera múltiples valores pasados (tipo ventana de memoria) para mejorar la calidad del número generado. Está definido por:
\[ \begin{aligned} x_{n+1} &= \left( \sum_{i=0}^{k-1} b_{1,i} \cdot x_{n-i} \right) \bmod m_1 \\ y_{n+1} &= \left( \sum_{i=0}^{k-1} b_{2,i} \cdot y_{n-i} \right) \bmod m_2 \\ z_{n+1} &= (x_{n+1} - y_{n+1}) \bmod m_1 \end{aligned} \]
donde:
Y la salida pseudoaleatoria es:
\[ u_n = \frac{z_n}{m_1} \]
class GCCM:
def __init__(self, modulo1, modulo2, multiplicador1, multiplicador2, semillasx, semillasy):
self.m1 = modulo1
self.m2 = modulo2
self.b1 = multiplicador1
self.b2 = multiplicador2
self.x0 = semillasx
self.y0 = semillasy
def muestra(self, n):
listaz = []
k = len(self.b1)
x = self.x0
y = self.y0
for i in range(n):
nuevo_x = sum(self.b1[i] * x[i] for i in range(k)) % self.m1
nuevo_y = sum(self.b2[i] * y[i] for i in range(k)) % self.m2
z = (nuevo_x - nuevo_y) % self.m1
x.insert(0, nuevo_x)
self.y0.insert(0, nuevo_y)
x.pop()
y.pop()
listaz.append(z / self.m1)
return listaz
gencombm1 = GCCM(13, 94, [56, 21], [23, 43], [4, 45], [65, 90])
## GCCM (5 muestras):
## [0.38461538461538464, 0.9230769230769231, 0.5384615384615384, 0.6153846153846154, 0.6923076923076923]
El Generador Congruencial Lineal Multivariado (GCLM) utiliza una combinación lineal de varios estados anteriores para generar el siguiente valor de la secuencia. Su fórmula general es:
\[ x_{n+1} = \left( \sum_{i=0}^{k-1} b_i \cdot x_{n-i} \right) \bmod m \]
donde:
El valor generado se normaliza en el intervalo \((0, 1)\) como:
\[ u_n = \frac{x_n}{m} \]
class GCLM:
def __init__(self, semilla, multiplicadores, modulo):
self.x0 = semilla
self.b = multiplicadores
self.m = modulo
def muestra(self, n):
resultados = []
estado = self.x0
k = len(estado)
for _ in range(n):
x = sum(self.b[i] * self.x0[i] for i in range(k)) % self.m
estado.insert(0, x)
estado.pop()
resultados.append(x / self.m)
return resultados
genlinm1 = GCLM([7, 11, 26], [3, 5, 2], 69)
## GCLM (5 muestras):
## [0.855072463768116, 0.391304347826087, 0.6521739130434783, 0.6231884057971014, 0.9130434782608695]
Estas funciones permiten representar números enteros como listas de
bits (0 y 1), manipular matrices binarias y
convertir resultados de vuelta a enteros.
# Convierte un número entero a su representación binaria como lista de bits (LSB primero)
def repbin(n):
L = []
while n > 0:
r = n % 2
n = n // 2
L.append(r)
return L
# Convierte un número entero a binario con longitud fija, rellenando con ceros a la izquierda si es necesario
def repbinlen(n, length):
L = []
while n > 0:
r = n % 2
n = n // 2
L.append(r)
while len(L) < length:
L.append(0)
return L
# Convierte una lista de bits a su valor entero correspondiente
def get_num(list_bin):
n = 0
for i in range(len(list_bin)):
if list_bin[i] == 1:
n += 2 ** i
return n
# Multiplica una matriz binaria por un vector binario (operaciones XOR y AND bit a bit)
def multiplicar(matriz, vector):
n = len(matriz)
resultado = [0 for _ in range(n)]
for i in range(n): # Iterar sobre filas
for j in range(n): # Iterar sobre columnas
resultado[i] ^= matriz[i][j] & vector[j]
return resultado
El Generador de Retroalimentación Lineal (LFSR) es un generador pseudoaleatorio basado en operaciones matriciales binarias, especialmente útil en criptografía y simulaciones donde se requiere eficiencia. Su estructura se basa en la siguiente relación matricial:
\[ x_{n+1} = A \cdot x_n \]
donde:
\[ A = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \end{bmatrix} \]
class linear_feedback_generator_size_4:
def __init__(self, semilla):
self.matriz = [[False, True, False, False],
[False, False, True, False],
[False, False, False, True],
[True, False, False, True]]
self.x0 = semilla
def muestra(self, n):
lista = []
semilla = self.x0
for i in range(n):
nueva_semilla = get_num(multiplicar(self.matriz, repbinlen(semilla, 4)))
lista.append(nueva_semilla / 16)
semilla = nueva_semilla
self.x0 = nueva_semilla
return lista
genlf4 = linear_feedback_generator_size_4(78)
## LFSR (muestras):
## [0.9375, 0.4375, 0.6875, 0.3125, 0.625]
El Generador de Retroalimentación Lineal (LFSR) de tamaño 8 utiliza una matriz binaria de transición para desplazar los bits de estado y aplicar una combinación lineal (XOR) que determina el nuevo valor. Este tipo de generador es eficiente y produce secuencias pseudoaleatorias con propiedades deseables cuando se diseñan correctamente.
El generador se basa en la siguiente operación matricial:
\[ x_{n+1} = A \cdot x_n \]
donde:
\[ u_n = \frac{x_n}{256} \]
\[ A = \begin{bmatrix} 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 1 & 1 & 0 & 0 & 1 & 1 & 1 & 1 \end{bmatrix} \]
class LinearFeedbackGeneratorSize8:
def __init__(self, semilla):
self.matriz = [
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 1, 1, 1, 1]
]
self.x0 = semilla
def muestra(self, n):
lista = []
semilla = self.x0
for _ in range(n):
nueva_semilla = get_num(multiplicar(self.matriz, repbinlen(semilla, 8)))
lista.append(nueva_semilla / 256)
semilla = nueva_semilla
self.x0 = semilla
return lista
genlf8 = LinearFeedbackGeneratorSize8(365)
## LFSR-8 (10 muestras):
## [0.7109375, 0.35546875, 0.17578125, 0.0859375, 0.04296875, 0.01953125, 0.5078125, 0.25390625, 0.125, 0.5625]
El Generador de Retroalimentación Lineal (LFSR) de tamaño 12 utiliza una matriz binaria de transición que realiza desplazamientos de bits en un vector de estado y aplica retroalimentación mediante una combinación lineal (XOR) en la última fila.
La evolución del sistema se describe mediante la operación:
\[ x_{n+1} = A \cdot x_n \]
donde:
\[ u_n = \frac{x_n}{4096} \]
\[ A = \begin{bmatrix} 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 1 & 1 & 0 & 0 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \end{bmatrix} \]
class LinearFeedbackGeneratorSize12:
def __init__(self, semilla):
self.matriz = [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]
]
self.x0 = semilla
def muestra(self, n):
lista = []
semilla = self.x0
for _ in range(n):
nueva_semilla = get_num(multiplicar(self.matriz, repbinlen(semilla, 12)))
lista.append(nueva_semilla / 4096)
semilla = nueva_semilla
self.x0 = semilla
return lista
genlf12 = LinearFeedbackGeneratorSize12(365)
## LFSR-12 (10 muestras):
## [0.04443359375, 0.022216796875, 0.010986328125, 0.50537109375, 0.252685546875, 0.126220703125, 0.56298828125, 0.781494140625, 0.890625, 0.9453125]
El Generador de Retroalimentación Lineal (LFSR) de tamaño 16 opera con una matriz de transición binaria que aplica desplazamientos de bits y una retroalimentación lineal basada en XOR. Este tipo de generador es eficiente para producir secuencias pseudoaleatorias con buena dispersión cuando se elige adecuadamente la matriz.
El proceso de generación sigue la relación:
\[ x_{n+1} = A \cdot x_n \]
donde:
\[ u_n = \frac{x_n}{65536} \]
\[ A = \begin{bmatrix} 0 & 1 & 0 & \cdots & 0 \\ 0 & 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & 0 & 0 & \cdots & 1 \end{bmatrix} \] Debido a su tamaño, la matriz se describe parcialmente. Es una matriz de desplazamiento (superdiagonal) con la siguiente fila de retroalimentación:
Última fila: \([1,\ 0,\ 0,\ \dots,\ 1,\ 0,\ 1,\ 1,\ 0]\)
class LinearFeedbackGeneratorSize16:
def __init__(self, semilla):
self.matriz = [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0]
]
self.x0 = semilla
def muestra(self, n):
lista = []
semilla = self.x0
for _ in range(n):
nueva_semilla = get_num(multiplicar(self.matriz, repbinlen(semilla, 16)))
lista.append(nueva_semilla / 65536)
semilla = nueva_semilla
self.x0 = semilla
return lista
genlf16 = LinearFeedbackGeneratorSize16(365)
## LFSR-16 (10 muestras):
## [0.502777099609375, 0.2513885498046875, 0.1256866455078125, 0.062835693359375, 0.0314178466796875, 0.0157012939453125, 0.507843017578125, 0.2539215087890625, 0.126953125, 0.5634765625]
El Generador de Retroalimentación Lineal (LFSR) de tamaño 20 aplica una operación de desplazamiento y retroalimentación XOR definida por una matriz binaria. Su estructura permite generar secuencias pseudoaleatorias con una longitud máxima cercana a \(2^{20} - 1\), dependiendo de la matriz usada.
Este generador se basa en la operación:
\[ x_{n+1} = A \cdot x_n \]
donde:
\[ u_n = \frac{x_n}{1048576} \]
Debido a su tamaño, la matriz se describe parcialmente. Es una matriz de desplazamiento (superdiagonal) con la siguiente fila de retroalimentación:
Última fila:
\([1,\ 0,\ 0,\ 1,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0,\ 0]\)
class LinearFeedbackGeneratorSize20:
def __init__(self, semilla):
self.matriz = [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
self.x0 = semilla
def muestra(self, n):
lista = []
semilla = self.x0
for _ in range(n):
nueva_semilla = get_num(multiplicar(self.matriz, repbinlen(semilla, 20)))
lista.append(nueva_semilla / 1048576)
semilla = nueva_semilla
self.x0 = semilla
return lista
genlf20 = LinearFeedbackGeneratorSize20(365)
## LFSR-20 (10 muestras):
## [0.0001735687255859375, 8.678436279296875e-05, 4.291534423828125e-05, 2.09808349609375e-05, 1.049041748046875e-05, 4.76837158203125e-06, 0.5000019073486328, 0.2500009536743164, 0.625, 0.3125]
El generador MRG32k3a es un generador múltiple recursivo combinado (Combined Multiple Recursive Generator) propuesto por Pierre L’Ecuyer. Es ampliamente utilizado por su largo período y buenas propiedades estadísticas.
El MRG32k3a combina dos generadores recursivos de orden 3 mediante la siguiente fórmula:
Sea:
La recursión para cada secuencia es:
\[ s_{n}^{(1)} = (a_{12} s_{n-2}^{(1)} - a_{13} s_{n-3}^{(1)}) \mod m_1 \]
\[ s_{n}^{(2)} = (a_{21} s_{n-1}^{(2)} - a_{23} s_{n-3}^{(2)}) \mod m_2 \]
Y la combinación final es:
\[ z_n = (s_{n}^{(1)} - s_{n}^{(2)}) \mod m_1 \]
El valor pseudoaleatorio generado se normaliza como:
\[ u_n = \begin{cases} \frac{z_n}{m_1} & \text{si } z_n > 0 \\ \frac{m_1 - 1}{m_1} & \text{si } z_n = 0 \end{cases} \]
class MRG32k3a:
def __init__(self, x0=1, x1=1, x2=20250409, y0=1, y1=1, y2=1):
self.m1 = 4294967087
self.m2 = 4294944443
self.a12 = 1403580
self.a13n = 810728
self.a21 = 527612
self.a23n = 1370589
self.s1 = [x2 % self.m1, x1 % self.m1, x0 % self.m1]
self.s2 = [y2 % self.m2, y1 % self.m2, y0 % self.m2]
def muestra(self, n=1):
resultados = []
for _ in range(n):
p1 = (self.a12 * self.s1[1] - self.a13n * self.s1[2]) % self.m1
self.s1 = [p1] + self.s1[:2]
p2 = (self.a21 * self.s2[0] - self.a23n * self.s2[2]) % self.m2
self.s2 = [p2] + self.s2[:2]
z = (self.s1[0] - self.s2[0]) % self.m1
resultados.append(z / self.m1 if z > 0 else (self.m1 - 1) / self.m1)
return resultados
## [0.0003395772238661628, 0.31734079130092296, 0.49998174223494346, 0.22045602301957756, 0.4334982178642246]
El Generador de Bernoulli produce variables aleatorias que toman el valor \(1\) con probabilidad \(p\) (éxito), y el valor \(0\) con probabilidad \(1 - p\) (fracaso). Es una de las distribuciones discretas más simples y se basa en la comparación de una variable aleatoria uniforme con el parámetro \(p\):
\[ X \sim \text{Bernoulli}(p) \]
\[ P(X = 1) = p, \quad P(X = 0) = 1 - p \]
La implementación se realiza generando una secuencia de valores uniformemente distribuidos \(U \sim \mathcal{U}(0,1)\) y aplicando:
\[ X = \begin{cases} 1 & \text{si } U < p \\ 0 & \text{si } U \geq p \end{cases} \]
class GeneradorBernoulli:
def __init__(self, probabilidad):
self.p = probabilidad
def muestra(self, n):
uniformes = generador.muestra(n)
return [1 if u < self.p else 0 for u in uniformes]
muestras_bernoulli = GeneradorBernoulli(0.80).muestra(5)
## Bernoulli (5 muestras, p = 0.25):
## [1, 1, 0, 1, 1]
La distribución binomial representa el número de éxitos en \(n\) ensayos independientes, cada uno con probabilidad de éxito \(p\). Es una generalización de la distribución Bernoulli:
\[ X \sim \text{Binomial}(n, p) \]
\[ P(X = k) = \binom{n}{k} p^k (1 - p)^{n - k}, \quad \text{para } k = 0, 1, \dots, n \]
Para simular esta distribución, se generan \(n\) variables independientes \(U_i \sim \mathcal{U}(0,1)\) y se cuentan cuántas cumplen \(U_i < p\):
\[ X = \sum_{i=1}^{n} \mathbb{I}_{\{U_i < p\}} \]
class GeneradorBinomial:
def __init__(self, p, n):
self.p = p
self.n = n
def muestra(self, m):
resultados = []
for _ in range(m):
uniformes = generador.muestra(self.n)
ensayos = [1 if u < self.p else 0 for u in uniformes]
resultados.append(sum(ensayos))
return resultados
muestras_binomial = GeneradorBinomial(p=0.25, n=10).muestra(5)
## Binomial (5 muestras, n=10, p=0.25):
## [1, 2, 2, 1, 1]
La distribución geométrica modela la cantidad de ensayos necesarios hasta obtener el primer éxito, en una secuencia de ensayos de Bernoulli independientes con probabilidad de éxito \(p\). Su función de probabilidad es:
\[ X \sim \text{Geom}(p) \]
\[ P(X = k) = (1 - p)^{k - 1} \cdot p, \quad \text{para } k = 1, 2, 3, \dots \]
Para simular esta distribución, se generan variables uniformes hasta obtener el primer valor menor a \(p\), y se cuenta cuántos intentos fueron necesarios:
class GeneradorGeometrico:
def __init__(self, p):
self.p = p
def muestra(self, n):
resultados = []
for _ in range(n):
conteo = 0
while True:
conteo += 1
u = generador.muestra(1)[0]
if u < self.p:
resultados.append(conteo)
break
return resultados
muestras_geometricas = GeneradorGeometrico(p=0.1).muestra(5)
## Geométrica (5 muestras, p=0.1):
## [32, 5, 6, 6, 9]
La distribución binomial negativa modela el número de ensayos necesarios hasta obtener \(r\) éxitos, con una probabilidad de éxito \(p\) en cada ensayo independiente. Su función de probabilidad es:
\[ X \sim \text{NegBin}(r, p) \]
\[ P(X = k) = \binom{k-1}{r-1} p^r (1 - p)^{k - r}, \quad \text{para } k = r, r+1, \dots \]
Para simularla, se generan ensayos hasta acumular \(r\) éxitos, y se registra el número total de intentos realizados.
class GeneradorBinomialNegativo:
def __init__(self, p, r):
self.p = p
self.r = r
def muestra(self, n):
resultados = []
for _ in range(n):
ensayos = 0
exitos = 0
while exitos < self.r:
ensayos += 1
u = generador.muestra(1)[0]
if u < self.p:
exitos += 1
resultados.append(ensayos)
return resultados
muestras_neg_bin = GeneradorBinomialNegativo(p=0.1, r=3).muestra(5)
## Binomial Negativa (5 muestras, p=0.1, r=3):
## [42, 52, 72, 20, 38]
La distribución uniforme discreta genera valores enteros dentro de un intervalo \([a, b]\), todos con igual probabilidad. Su función de probabilidad es:
\[ X \sim \text{Unif}(a, b) \]
\[ P(X = x) = \frac{1}{b - a + 1}, \quad \text{para } x = a, a+1, \dots, b \]
Se simula generando un número uniforme \(u \in (0, 1)\) y aplicando la transformación:
\[ X = \lfloor u \cdot (b - a + 1) \rfloor + a \]
class GeneradorUniformeDiscreto:
def __init__(self, a, b):
self.a = a
self.b = b
def muestra(self, n):
resultados = []
rango = self.b - self.a + 1
for _ in range(n):
u = generador.muestra(1)[0]
valor = int(u * rango) + self.a
resultados.append(valor)
return resultados
muestras_uniforme_discreto = GeneradorUniformeDiscreto(a=0, b=100).muestra(5)
## Uniforme Discreta (5 muestras, a=0, b=100):
## [84, 98, 28, 69, 66]
La distribución exponencial modela el tiempo entre eventos en un proceso de Poisson. Su función de densidad es:
\[ X \sim \text{Exp}(\lambda) \]
\[ f(x) = \lambda e^{-\lambda x}, \quad x \geq 0 \]
Para simularla, se usa el método de la inversa:
\[ X = -\frac{1}{\lambda} \ln(1 - U) \]
donde \(U\) es una variable aleatoria uniforme en \((0,1)\).
import math
class GeneradorExponencial:
def __init__(self, lamb):
self.lamb = lamb
def simulacion(self):
u = generador.muestra(1)[0]
return -math.log(1 - u) / self.lamb
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_exponenciales = GeneradorExponencial(lamb=0.5).muestra(5)
## Exponencial (5 muestras, lambda = 0.5):
## [1.3039628125141254, 1.0660299057264315, 5.799319886774467, 0.9132174873842833, 1.6542525069840919]
La distribución uniforme continua genera valores dentro del intervalo \([a, b]\), con densidad constante. Su función de densidad es:
\[ X \sim \text{Unif}(a, b) \]
\[ f(x) = \frac{1}{b - a}, \quad \text{para } a \leq x \leq b \]
Para simularla, se utiliza la transformación directa de una variable uniforme \(U \in (0,1)\):
\[ X = a + (b - a) \cdot U \]
class GeneradorUniformeContinua:
def __init__(self, a, b):
self.min = min(a, b)
self.max = max(a, b)
def simulacion(self):
u = generador.muestra(1)[0]
return self.min + (self.max - self.min) * u
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_uniforme_continua = GeneradorUniformeContinua(a=0, b=1).muestra(5)
## Uniforme Continua (5 muestras, a = 0, b = 1):
## [0.16519537114674052, 0.576132003313766, 0.49449745480668916, 0.6066155216616215, 0.894411314495831]
La distribución de Poisson modela el número de eventos en un intervalo fijo de tiempo o espacio, cuando los eventos ocurren con una tasa constante \(\lambda\) y de forma independiente entre sí. Se denota:
\[ X \sim \text{Poisson}(\lambda) \]
Su función de probabilidad es:
\[ P(X = k) = \frac{\lambda^k e^{-\lambda}}{k!}, \quad k = 0, 1, 2, \dots \]
Para simular esta distribución, se utiliza un método basado en tiempos entre llegadas (suma de variables exponenciales):
\[ \text{Generar } t_i \sim \text{Exp}(\lambda), \quad \text{contar cuántos } t_i \text{ caben en } [0,1] \]
import math
class GeneradorPoisson:
def __init__(self, lamb):
self.lamb = lamb
def simulacion(self):
n = 0
t = 0.0
while True:
u = generador.muestra(1)[0]
delta = -math.log(1 - u) / self.lamb
if t + delta > 1.0:
break
t += delta
n += 1
return n
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_poisson = GeneradorPoisson(lamb=15).muestra(5)
## Poisson (5 muestras, lambda = 15):
## [22, 15, 8, 15, 8]
Esta implementación alternativa para simular variables aleatorias con distribución Poisson se basa en la suma de tiempos entre llegadas generados por una distribución exponencial:
\[ t_i \sim \text{Exp}(\lambda) \]
El número de eventos \(N\) que ocurren en el intervalo \([0, 1]\) se calcula sumando iterativamente los \(t_i\) hasta que la suma exceda 1:
\[ N = \max\left\{n \in \mathbb{N} \mid \sum_{i=1}^{n} t_i \leq 1 \right\} \]
class GeneradorPoissonAlternativo:
def __init__(self, parametro_lamb):
self.parametro_lamb = parametro_lamb
self.exponencial = GeneradorExponencial(parametro_lamb)
def simulacion(self):
n = 0
t = self.exponencial.simulacion()
while t <= 1.0:
n += 1
t += self.exponencial.simulacion()
return n
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_poisson = GeneradorPoissonAlternativo(parametro_lamb=15).muestra(5)
## Poisson Alternativo (5 muestras, lambda = 15):
## [17, 11, 19, 14, 13]
Este generador produce variables aleatorias con distribución normal \(\mathcal{N}(\mu, \sigma^2)\) usando el método polar de Box-Muller, el cual transforma dos variables uniformes independientes \(U_1, U_2 \sim \text{Unif}(0,1)\) en una variable normal estándar:
Se generan dos valores: \[ V_1 = 2U_1 - 1, \quad V_2 = 2U_2 - 1 \]
Se calcula: \[ R = V_1^2 + V_2^2 \]
Si \(0 < R \leq 1\), se acepta y se obtiene: \[ Z = V_1 \cdot \sqrt{\frac{-2 \ln R}{R}} \]
Finalmente, la variable normal con media y varianza específicas es: \[ X = \mu + \sigma Z \]
class GeneradorNormal:
def __init__(self, media, varianza):
self.media = media
self.varianza = varianza
def simulacion(self):
while True:
u1 = generador.muestra(1)[0]
u2 = generador.muestra(1)[0]
v1 = 2 * u1 - 1
v2 = 2 * u2 - 1
r = v1 * v1 + v2 * v2
if 0 < r <= 1:
break
factor = math.sqrt(-2 * math.log(r) / r)
z1 = v1 * factor
return self.media + math.sqrt(self.varianza) * z1
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_normales = GeneradorNormal(media=0, varianza=1).muestra(10)
## Normal (10 muestras, media = 0, varianza = 1):
## [0.20466767400891167, -0.5555880474927649, 0.42941046677492706, -1.0927184609735743, -0.2131141365416757, -0.4833419316066803, 0.1665598132548493, -1.105867018213305, 1.6056721581789444, -0.9083520442161196]
Este generador produce variables aleatorias con distribución normal rectificada \(\mathcal{N}^{+}(\mu, \sigma^2)\), es decir, una distribución normal donde todos los valores negativos son truncados a cero. Se basa en el método anterior para simular una variable \(X \sim \mathcal{N}(\mu, \sigma^2)\), pero se impone la condición:
\[ X = \begin{cases} \mu + \sigma Z & \text{si } \mu + \sigma Z > 0 \\ 0 & \text{en otro caso} \end{cases} \]
class GeneradorNormalRectificado:
def __init__(self, media, varianza):
self.media = media
self.varianza = varianza
def simulacion(self):
while True:
u1 = generador.muestra(1)[0]
u2 = generador.muestra(1)[0]
v1 = 2 * u1 - 1
v2 = 2 * u2 - 1
r = v1*v1 + v2*v2
if 0 < r <= 1:
break
factor = math.sqrt(-2 * math.log(r) / r)
z = v1 * factor
x = self.media + math.sqrt(self.varianza) * z
return x if x > 0 else 0.0
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_nrect = GeneradorNormalRectificado(media=0, varianza=1).muestra(10)
## Muestras Normal Rectificada N(0,1):
## [1.0880483983117055, 0.5355938290489028, 0.0, 0.0, 1.0333716691083992, 0.0, 0.0, 0.0, 1.4488558084152579, 1.9141408081884506]
Este generador simula variables aleatorias con distribución normal truncada, es decir, variables \(X \sim \mathcal{N}(\mu, \sigma^2)\) que se restringen al intervalo \([a, b]\). La generación se realiza con el método anterior y se aplica rechazo si el valor cae fuera de los límites.
\[ X = \mu + \sigma Z, \quad \text{con } Z \sim \mathcal{N}(0, 1) \] Se acepta \(X\) solo si: \[ a \leq X \leq b \]
class GeneradorNormalAcotada:
def __init__(self, media, varianza, a, b):
self.media = media
self.varianza = varianza
self.a = a
self.b = b
def simulacion(self):
while True:
u1 = generador.muestra(1)[0]
u2 = generador.muestra(1)[0]
v1 = 2 * u1 - 1
v2 = 2 * u2 - 1
r = v1*v1 + v2*v2
if 0 < r <= 1:
break
factor = math.sqrt(-2 * math.log(r) / r)
z = v1 * factor
x = self.media + math.sqrt(self.varianza) * z
if self.a <= x <= self.b:
return x
return self.simulacion()
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
a = -2
b = 2
muestras_norm_acot = GeneradorNormalAcotada(media=0, varianza=1, a=a, b=b).muestra(10)
## Muestras Normal Acotada en [-2,2]:
## [-0.12381913653855846, 1.6104847902959503, -1.4359182477062387, 1.4956869166023063, 0.6696148747804004, 0.3400698127312014, 0.11977166074151453, 0.9345804605176253, 0.3217251398915182, -0.4933379139896068]
Este generador simula variables aleatorias con distribución \(\chi^2_k\), que representa la suma de los cuadrados de \(k\) variables normales estándar independientes: \[ X = \sum_{i=1}^k Z_i^2, \quad Z_i \sim \mathcal{N}(0,1) \]
class GeneradorChiCuadrada:
def __init__(self, grados_libertad):
self.n = grados_libertad
self.normal = GeneradorNormal(0,1)
def simulacion(self):
suma = 0.0
for _ in range(self.n):
z = self.normal.simulacion()
suma += z*z
return suma
def muestra(self, m):
return [self.simulacion() for _ in range(m)]
resultados_chi2 = GeneradorChiCuadrada(grados_libertad=5).muestra(10)
## Muestras Chi-cuadrada df=5:
## [3.48645258705134, 2.979149258311065, 4.474218806855241, 1.7893772241680626, 12.966314136000925, 2.5688324423980404, 11.9944310132163, 4.477538796423481, 3.483840630072731, 1.2831985286592444]
Este generador simula variables aleatorias con distribución Gamma con
forma entera positiva \(k\) y escala
\(\theta\).
Se basa en la propiedad de que una variable Gamma con \(k \in \mathbb{Z}^+\) puede representarse
como la suma de \(k\) variables
exponenciales independientes: \[
X = \sum_{i=1}^{k} E_i, \quad E_i \sim \text{Exponencial}(\theta)
\]
class GeneradorGamma:
"""
Simula muestras de la distribución Gamma(k, theta) con k entero positivo
usando la suma de k exponentiales (Erlang) y el generador uniforme global.
"""
def __init__(self, k, theta):
self.k = int(k)
self.theta = theta
if self.k < 1:
raise ValueError("El parámetro k debe ser entero positivo.")
def simulacion(self):
"""Una muestra individual de Gamma(k, theta)."""
suma = 0.0
for _ in range(self.k):
u = generador.muestra(1)[0]
suma += -math.log(1 - u) * self.theta
return suma
def muestra(self, n):
"""Genera n muestras de la distribución Gamma(k, theta)."""
return [self.simulacion() for _ in range(n)]
k = 2
theta = 5
muestras_gamma = GeneradorGamma(k, theta).muestra(10)
## Muestras Gamma(k=2, θ=5):
## [21.465229154128988, 18.75899859818425, 2.679820105737583, 4.6475763747124414, 14.329735434503124, 0.9143796875589865, 8.637781818685564, 2.357575822086353, 4.26166488168549, 6.1985534244500515]
La distribución de Cauchy es una distribución continua con colas
pesadas, definida por su mediana \(x_0\) y parámetro de
escala \(\gamma\).
No tiene media ni varianza definidas.
Su función de densidad es:
\[ f(x) = \frac{1}{\pi \gamma \left[1 + \left(\frac{x - x_0}{\gamma}\right)^2\right]}, \quad x \in \mathbb{R} \]
Para simular esta distribución se usa la transformación de una variable uniforme \(U \sim \mathcal{U}(0,1)\):
\[ X = x_0 + \gamma \cdot \tan\left[ \pi (U - 0.5) \right] \]
class GeneradorCauchy:
def __init__(self, x0, gamma):
self.x0 = x0
self.gamma = gamma
def simulacion(self):
u = generador.muestra(1)[0]
return self.x0 + self.gamma * math.tan(math.pi * (u - 0.5))
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
muestras_cauchy = GeneradorCauchy(x0=0, gamma=9).muestra(10)
## Muestras Cauchy:
## [21.457306344150044, 139.22382924419946, 6.609392917407011, -11.251811589457848, 10.741688560177748, 5.063569251370891, -29.904770196536756, 1.5980605084223518, -4.077301885170503, 12.100971098062239]
Aunque muchas distribuciones tienen una media bien
definida a la cual los promedios de muestras convergen por la Ley de los
Grandes Números, esto no ocurre con la distribución
Cauchy.
Esto se debe a que la distribución Cauchy no tiene esperanza
matemática finita.
En este experimento se observa cómo los promedios acumulados de \(n\) simulaciones de Cauchy fluctúan intensamente y no se estabilizan con el crecimiento de \(n\):
import matplotlib.pyplot as plt
x0, gamma, n_max = 0, 1, 10000
cauchy_gen = GeneradorCauchy(x0, gamma)
promedios_cauchy = []
suma = 0.0
for i in range(1, n_max + 1):
x = cauchy_gen.simulacion()
suma += x
promedios_cauchy.append(suma / i)
plt.plot(range(1, n_max + 1), promedios_cauchy)
plt.title("Promedios Acumulados de la Distribución Cauchy")
plt.xlabel("n")
plt.ylabel("Promedio acumulado")
plt.grid(True)
plt.show()
El método de aceptación-rechazo es una técnica para generar muestras de una distribución cuya función de densidad \(f_X(x)\) es complicada de muestrear directamente. Se basa en usar una densidad auxiliar \(f_Y(x)\), de la que sí es sencillo extraer muestras, y una constante \(c\) tal que
\[ f_X(x) \;\le\; c\,f_Y(x), \quad \forall\,x\in\text{soporte}(f_X). \]
El procedimiento general es:
Elegir una función auxiliar \(f_Y(x)\) que cumpla:
Calcular la constante \[ c \;=\; \sup_{x} \left\{ \frac{f_X(x)}{f_Y(x)} \right\}. \] En la práctica, a menudo se evalúa \(f_X(x)/f_Y(x)\) en puntos estratégicos o se aproxima por búsqueda en una rejilla.
Repetir hasta obtener \(n\) muestras:
Cada muestra aceptada \(y\) tiene probabilidad de ser tomada proporcional a \(f_X(y)\), de modo que la colección final de valores sigue la distribución objetivo \(f_X(x)\).
Queremos simular la siguiente función de densidad definida en el intervalo \([-1, 1]\):
\[ f_X(x) = \frac{\sqrt{3}}{\pi\,\bigl(x^2 + x + 1\bigr)}, \quad x \in [-1, 1]. \]
Utilizamos como densidad auxiliar
\[ f_Y(x) = \begin{cases} 0.5, & -1 \le x \le 1,\\ 0, & \text{en otro caso}, \end{cases} \]
es decir, \(f_Y(x)\sim\mathcal{U}(-1,1)\).
Utilizamos una densidad auxiliar uniforme \(f_Y(x) \sim \mathcal{U}(-1,1)\) y aplicamos el método de aceptación-rechazo usando el generador MRG32k3a.
import math
def f_x(x):
return (math.sqrt(3)) / (math.pi * (x**2 + x + 1))
def f_y(x):
return 0.5 if -1 <= x <= 1 else 0.0
def integral_fx(a, b, pasos=10000):
h = (b - a) / pasos
suma = 0.0
for i in range(pasos):
xi = a + (i + 0.5) * h
suma += f_x(xi)
return h * suma
integral = integral_fx(-1, 1)
print(f"Integral aproximada de f_X(x) en [-1, 1] = {integral:.6f}")
## Integral aproximada de f_X(x) en [-1, 1] = 1.000000
x_max = -0.5
fx_max = f_x(x_max)
fy_val = f_y(x_max)
c = fx_max / fy_val
print(f"f_X({x_max}) = {fx_max:.6f}, f_Y({x_max}) = {fy_val}")
## f_X(-0.5) = 0.735105, f_Y(-0.5) = 0.5
print(f"Constante c = f_X/f_Y = {c:.6f}")
## Constante c = f_X/f_Y = 1.470210
class MRG32k3a:
def __init__(self, x0=1, x1=1, x2=20250409, y0=1, y1=1, y2=1):
self.m1 = 4294967087
self.m2 = 4294944443
self.a12 = 1403580
self.a13n = 810728
self.a21 = 527612
self.a23n = 1370589
self.s1 = [x2 % self.m1, x1 % self.m1, x0 % self.m1]
self.s2 = [y2 % self.m2, y1 % self.m2, y0 % self.m2]
def muestra(self, n=1):
resultados = []
for _ in range(n):
p1 = (self.a12 * self.s1[1] - self.a13n * self.s1[2]) % self.m1
self.s1 = [p1] + self.s1[:2]
p2 = (self.a21 * self.s2[0] - self.a23n * self.s2[2]) % self.m2
self.s2 = [p2] + self.s2[:2]
z = (self.s1[0] - self.s2[0]) % self.m1
resultados.append(z / self.m1 if z > 0 else (self.m1 - 1) / self.m1)
return resultados
class GeneradorUniformeContinua:
def __init__(self, a, b):
self.min = min(a, b)
self.max = max(a, b)
def simulacion(self):
u = generador_principal.muestra(1)[0]
return self.min + (self.max - self.min) * u
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
generador_principal = MRG32k3a()
uniforme = GeneradorUniformeContinua(-1, 1)
def aceptar_rechazar(n, c):
muestras = []
while len(muestras) < n:
y = uniforme.muestra(1)[0]
u = generador_principal.muestra(1)[0]
if u <= f_x(y) / (c * f_y(y)):
muestras.append(y)
return muestras
muestras = aceptar_rechazar(10000, c)
import numpy as np
import matplotlib.pyplot as plt
plt.hist(muestras, bins=20, range=(-1,1), density=True, alpha=0.6, color='k', edgecolor='w')
x_vals = np.linspace(-1, 1, 1000)
fx_vals = [f_x(x) for x in x_vals]
plt.plot(x_vals, fx_vals, color='lime', lw=2)
plt.title('Simulación por Aceptación-Rechazo de $f_X(x)$ con MRG32k3a')
plt.xlabel('x')
plt.ylabel('Densidad')
plt.grid(True)
plt.show()
La distribución Gamma con parámetro de forma \(\alpha>0\) y escala \(\theta=1\) tiene función de densidad:
\[ f_{\Gamma}(x;\,\alpha) \;=\; \begin{cases} \dfrac{x^{\alpha - 1}\,e^{-x}}{\Gamma(\alpha)}, & x>0,\\ 0, & \text{en otro caso}. \end{cases} \]
Para simular \(X \sim \Gamma(\alpha,1)\) utilizamos un generador auxiliar \(Y\) definido por una mezcla: - Con probabilidad \(p = \dfrac{e\,\alpha}{e + \alpha}\), \(Y \sim \mathrm{Beta}(\alpha,1)\) en \((0,1)\). - Con probabilidad \(1 - p\), \(Y \sim \mathrm{Exponencial}(1)\) en \((1,\infty)\).
La densidad auxiliar correspondiente es:
\[ f_{Y}(y; \alpha) \;=\; \begin{cases} p \,\dfrac{y^{\alpha - 1}}{B(\alpha,1)} = p\,y^{\alpha - 1}, & 0<y\le 1,\\ (1-p)\,e^{-y}, & y > 1,\\ 0, & \text{en otro caso}, \end{cases} \] donde \(B(\alpha,1)=1\) y \[ p = \frac{e\,\alpha}{e + \alpha}. \]
El método de aceptación–rechazo acepta \(Y\) como muestra de \(\Gamma(\alpha,1)\) si:
\[ U \;\le\; \frac{f_{\Gamma}(Y;\,\alpha)}{f_{Y}(Y;\,\alpha)}, \quad U \sim \mathcal{U}(0,1). \]
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import gamma as gamma_func
from scipy.stats import beta, expon
import math
def f_gamma(x, alpha):
return (x**(alpha - 1) * math.exp(-x)) / gamma_func(alpha) if x > 0 else 0.0
def f_y_gamma(y, alpha):
p = (math.e * alpha) / (math.e + alpha)
if 0 < y <= 1:
return p * (y**(alpha - 1))
elif y > 1:
return (1 - p) * math.exp(-y)
else:
return 0.0
def simular_Y(alpha, generador_uniforme):
p = (math.e * alpha) / (math.e + alpha)
u = generador_uniforme.muestra(1)[0]
if u <= p:
return beta.rvs(alpha, 1)
else:
return expon.rvs()
def aceptar_rechazar_gamma(n, alpha, generador_uniforme):
muestras = []
while len(muestras) < n:
y = simular_Y(alpha, generador_uniforme)
u = generador_uniforme.muestra(1)[0]
if u <= f_gamma(y, alpha) / f_y_gamma(y, alpha):
muestras.append(y)
return np.array(muestras)
def graficar_gamma(muestras, alpha, bins=100):
x = np.linspace(0, np.max(muestras), 1000)
fx = [f_gamma(val, alpha) for val in x]
plt.figure(figsize=(10, 5))
plt.hist(muestras, bins=bins, density=True, alpha=0.6, color='k', label='Simulado')
plt.plot(x, fx, 'lime', label=f'Teórica Gamma($\\alpha$={alpha},1)', lw=2)
plt.title(f"Simulación Gamma($\\alpha$={alpha},\\theta=1) por Aceptación–Rechazo")
plt.xlabel("x")
plt.ylabel("Densidad")
plt.grid(True)
plt.legend()
plt.xlim(0, np.percentile(muestras, 99))
plt.show()
alpha = 0.6
n = 10000
muestras_gamma = aceptar_rechazar_gamma(n, alpha, uniforme)
graficar_gamma(muestras_gamma, alpha, bins=100)
print(f"Muestras Gamma(α={alpha},1): media aproximada = {np.mean(muestras_gamma):.4f}, varianza aproximada = {np.var(muestras_gamma):.4f}")
## Muestras Gamma(α=0.6,1): media aproximada = 0.4965, varianza aproximada = 0.3695
La distribución Beta con parámetros de forma enteros \(\alpha>0\) y \(\beta>0\) tiene densidad en \((0,1)\):
\[ f_{\,\mathrm{Beta}}(x;\,\alpha,\beta) \;=\; \frac{x^{\alpha - 1}\,(1 - x)^{\beta - 1}}{B(\alpha,\beta)}, \quad B(\alpha,\beta) = \frac{\Gamma(\alpha)\,\Gamma(\beta)}{\Gamma(\alpha + \beta)}. \]
Una manera de simular \(X \sim \mathrm{Beta}(\alpha,\beta)\) es usar la relación con la distribución Gamma: - Si \(U \sim \Gamma(\alpha,1)\) y \(V \sim \Gamma(\beta,1)\) son independientes, entonces \[ X = \frac{U}{\,U + V\,} \;\sim\; \mathrm{Beta}(\alpha,\beta). \] - Para \(\alpha,\beta\in\mathbb{Z}^+\), cada \(\Gamma(k,1)\) puede obtenerse como suma de \(k\) variables exponenciales iid de parámetro 1: \[ U = \sum_{i=1}^{\alpha} E_i, \quad V = \sum_{j=1}^{\beta} F_j, \quad E_i, F_j \sim \mathrm{Exp}(1). \]
import math
import numpy as np
class GeneradorBeta:
def __init__(self, alpha, beta):
if not (isinstance(alpha, int) and alpha > 0 and isinstance(beta, int) and beta > 0):
raise ValueError("alpha y beta deben ser enteros positivos")
self.alpha = alpha
self.beta = beta
self.expo = GeneradorExponencial(1) # Exponencial con parámetro λ = 1
def simulacion(self):
# Generar U = suma de alpha variables Exp(1)
sum_x = 0.0
for _ in range(self.alpha):
sum_x += self.expo.simulacion()
# Generar V = suma de beta variables Exp(1)
sum_y = 0.0
for _ in range(self.beta):
sum_y += self.expo.simulacion()
# X = U / (U + V) ~ Beta(alpha, beta)
return sum_x / (sum_x + sum_y)
def muestra(self, n):
return [self.simulacion() for _ in range(n)]
gen_beta = GeneradorBeta(alpha=2, beta=5)
muestras_beta = gen_beta.muestra(10)
print("Muestras Beta(2,5):", muestras_beta)
## Muestras Beta(2,5): [0.14305765581915905, 0.45397318654392466, 0.14382747984538258, 0.14135660285305562, 0.4964655165493996, 0.23837913465831212, 0.3088257338841022, 0.13944713869064626, 0.5291457224114471, 0.4755434895169242]
Un Proceso de Poisson Homogéneo con tasa \(\lambda > 0\) es un proceso estocástico \(\{N(t)\}_{t \ge 0}\) tal que:
Para simular eventos en \([0, T]\), se genera una sucesión de intervalos exponenciales \[ \Delta_i = -\frac{1}{\lambda} \ln\bigl(1 - U_i\bigr), \quad U_i \sim \mathcal{U}(0,1), \] y se acumulan hasta superar \(T\).
import math
class ProcesoPoissonHomogeneo:
def __init__(self, lam):
self.lam = lam
def simulacion(self, T):
"""
Simula los tiempos de llegada de eventos en [0, T]
en un Proceso de Poisson Homogéneo con tasa lambda.
"""
tiempos = []
t = 0.0
while True:
u = generador.muestra(1)[0]
delta = -math.log(1 - u) / self.lam
t += delta
if t > T:
break
tiempos.append(t)
return tiempos
def muestra(self, m, T):
"""
Genera m trayectorias completas en [0, T],
devolviendo una lista de listas de tiempos de llegada.
"""
return [self.simulacion(T) for _ in range(m)]
import matplotlib.pyplot as plt
proceso = ProcesoPoissonHomogeneo(lam=3)
T = 15
tiempos_llegada = proceso.simulacion(T)
tiempos = [0.0] + tiempos_llegada + [T]
cuentas = list(range(0, len(tiempos_llegada) + 1)) + [len(tiempos_llegada)]
plt.figure(figsize=(8, 4))
plt.step(tiempos, cuentas, where='post')
plt.xlabel('Tiempo')
plt.ylabel('Número de eventos $N(t)$')
plt.title(f'Proceso de Poisson Homogéneo (λ={proceso.lam}, T={T})')
plt.ylim(0, max(cuentas) + 1)
## (0.0, 48.0)
plt.grid(True)
plt.show()
El Proceso de Wiener (o Movimiento Browniano) \(\{W(t)\}_{t \ge 0}\) es un proceso estocástico continuo que satisface:
Para simularlo en un intervalo \([0, T]\) con \(n_{\text{pasos}}\) pasos, dividimos el tiempo en subintervalos de tamaño \[ \Delta t \;=\; \frac{T}{n_{\text{pasos}}}. \] Entonces, definimos \[ W(t_{j}) \;=\; W(t_{j-1}) \;+\; \sqrt{\Delta t}\,Z_{j}, \quad Z_{j} \sim \mathcal{N}(0,1), \] donde \(t_{j} = j\,\Delta t\).
En este ejemplo utilizamos dos generadores pseudoaleatorios en
paralelo para aplicar el método polar de Box–Muller y obtener las
variables \(Z_{j}\):
- MRG32k3a como generador primario,
- GeneradorUniformeContinua (o un generador uniforme
global) como generador secundario.
import numpy as np
import math
class ProcesoWiener:
def __init__(self, T, n_pasos, n_trayectorias=1):
self.T = T
self.n_pasos = n_pasos
self.dt = T / n_pasos
self.n_trayectorias = n_trayectorias
def simulacion(self):
tiempos = np.linspace(0, self.T, self.n_pasos + 1)
trayectorias = np.zeros((self.n_trayectorias, self.n_pasos + 1))
generador_principal = MRG32k3a()
generador_secundario = GeneradorUniformeContinua(0, 1)
for i in range(self.n_trayectorias):
j = 1
while j <= self.n_pasos:
u1 = generador_principal.muestra(1)[0]
u2 = generador_secundario.muestra(1)[0]
v1 = 2 * u1 - 1
v2 = 2 * u2 - 1
r2 = v1 * v1 + v2 * v2
if r2 == 0 or r2 > 1:
continue
factor = math.sqrt(-2 * math.log(r2) / r2)
z1 = v1 * factor
z2 = v2 * factor
trayectorias[i, j] = trayectorias[i, j-1] + math.sqrt(self.dt) * z1
j += 1
if j <= self.n_pasos:
trayectorias[i, j] = trayectorias[i, j-1] + math.sqrt(self.dt) * z2
j += 1
return tiempos, trayectorias
import matplotlib.pyplot as plt
T = 1.0
n_pasos = 50
n_trayectorias = 20
pw = ProcesoWiener(T=T, n_pasos=n_pasos, n_trayectorias=n_trayectorias)
t, W = pw.simulacion()
plt.figure(figsize=(8, 4))
for traj in W:
plt.plot(t, traj)
plt.title('Trayectorias de Proceso de Wiener')
plt.xlabel('t')
plt.ylabel('W(t)')
plt.grid(True)
plt.show()
Para estimar \(\pi\), usamos el método de Monte Carlo generando puntos uniformes en el cuadrado \([-1,1]^2\). Si \((X,Y)\) es uniforme en \([-1,1]^2\), entonces
\[ P\bigl(X^2 + Y^2 \le 1\bigr) \;=\; \frac{\text{área del círculo de radio 1}}{\text{área del cuadrado}} \;=\; \frac{\pi}{4}. \]
Por lo tanto, si \(N\) puntos caen dentro del círculo, una estimación de \(\pi\) es
\[ \hat{\pi} \;=\; 4 \,\frac{\#\{(X_i,Y_i) : X_i^2 + Y_i^2 \le 1\}}{N}. \]
Para obtener un intervalo de confianza al 95% sobre la media de \(\hat{\pi}\), repetimos el experimento \(n\) veces obteniendo estimaciones \(\hat{\pi}_1, \dots, \hat{\pi}_n\). Sea
\[ Y_n \;=\; \frac{1}{n} \sum_{i=1}^n \hat{\pi}_i, \qquad S_n^2 \;=\; \frac{1}{n-1} \sum_{i=1}^n (\hat{\pi}_i - Y_n)^2. \]
Con \(t_{\alpha/2,\,n-1}\) el cuantil de la distribución t de Student con \(n-1\) grados de libertad, el margen de error para un nivel de confianza del \(100(1-\alpha)\%\) es
\[ \text{margen} \;=\; t_{\,0.975,\,n-1} \,\frac{S_n}{\sqrt{n}}. \]
El intervalo de confianza al 95% para \(\pi\) es entonces
\[ \bigl(Y_n - \text{margen},\, Y_n + \text{margen}\bigr). \]
import math
import numpy as np
from scipy.stats import t
class EstimadorPi:
"""
Estima π usando Monte Carlo en [-1,1]^2,
calculando un intervalo de confianza al 95% vía t-Student.
"""
def __init__(self, generador):
self.generador = generador
def estimar_pi(self, N):
dentro = 0
for _ in range(N):
x = 2 * self.generador.muestra(1)[0] - 1
y = 2 * self.generador.muestra(1)[0] - 1
if x**2 + y**2 <= 1:
dentro += 1
return 4 * dentro / N
def experimento(self, N_puntos, n_repeticiones):
estimaciones = [self.estimar_pi(N_puntos) for _ in range(n_repeticiones)]
Yn = np.mean(estimaciones)
Sn2 = np.var(estimaciones, ddof=1)
Sn = math.sqrt(Sn2)
t_critico = t.ppf(0.975, df=n_repeticiones - 1)
margen = t_critico * Sn / math.sqrt(n_repeticiones)
IC = (Yn - margen, Yn + margen)
return {
"media": Yn,
"varianza_muestral": Sn2,
"intervalo_confianza": IC,
"estimaciones": estimaciones
}
def analizar_varianza_conforme_n(self, N_puntos, max_n):
varianzas = []
ns = range(2, max_n + 1)
for n in ns:
resultados = self.experimento(N_puntos, n)
varianzas.append(resultados["varianza_muestral"])
return ns, varianzas
generador_pi = MRG32k3a()
estimador = EstimadorPi(generador_pi)
resultado = estimador.experimento(N_puntos=1000, n_repeticiones=50)
print(f"Estimación media de π: {resultado['media']:.6f}")
## Estimación media de π: 3.145120
print(f"Varianza muestral: {resultado['varianza_muestral']:.6f}")
## Varianza muestral: 0.002043
print(f"Intervalo de confianza 95%: {resultado['intervalo_confianza']}")
## Intervalo de confianza 95%: (np.float64(3.132272998495353), np.float64(3.157967001504647))
Un sistema M/M/1 representa un proceso de colas con:
Los tiempos entre llegadas \(\{\tau_i\}\) son iid con distribución \(\mathrm{Exp}(\lambda)\):
\[ \tau_i = -\frac{1}{\lambda} \ln(1 - U_i), \quad U_i \sim \mathcal{U}(0,1). \]
Los tiempos de servicio \(\{s_i\}\) son iid con distribución \(\mathrm{Exp}(\mu)\):
\[ s_i = -\frac{1}{\mu} \ln(1 - V_i), \quad V_i \sim \mathcal{U}(0,1). \]
Para simular hasta un tiempo máximo \(T\), se alternan eventos de llegada y salida:
Se registran los tiempos de espera en la cola para cada cliente \(i\),
donde el tiempo de espera \(W_i =
t_{\text{inicio servicio},i} - t_{\text{llegada},i}\).
import math
import numpy as np
generador_principal = MRG32k3a()
class Exponencial:
"""
Genera un tiempo exponencial con parámetro lambda
usando el generador uniforme MRG32k3a.
"""
def __init__(self, parametro_lamb):
self.parametro_lamb = parametro_lamb
def simulacion(self):
u = generador_principal.muestra(1)[0]
return -math.log(1 - u) / self.parametro_lamb
class SistemaColasMM1:
"""
Simula un sistema de colas M/M/1 hasta tiempo_max:
- tasa_llegada = λ (Procesos de Poisson)
- tasa_servicio = μ (Exp(μ) para servicio)
"""
def __init__(self, tasa_llegada, tasa_servicio, generador):
self.lambda_ = tasa_llegada
self.mu = tasa_servicio
self.generador = generador
self.llegadas = Exponencial(self.lambda_)
self.servicios = Exponencial(self.mu)
def simular(self, tiempo_max):
tiempo = 0.0
cliente = 0
servidor_ocupado = False
fila = []
tiempos_espera = []
siguiente_llegada = self.llegadas.simulacion()
siguiente_salida = float('inf')
while tiempo < tiempo_max:
if siguiente_llegada < siguiente_salida:
tiempo = siguiente_llegada
cliente += 1
if not servidor_ocupado:
servidor_ocupado = True
siguiente_salida = tiempo + self.servicios.simulacion()
tiempos_espera.append(0.0)
else:
fila.append(tiempo)
siguiente_llegada = tiempo + self.llegadas.simulacion()
else:
tiempo = siguiente_salida
if fila:
inicio = fila.pop(0)
espera = tiempo - inicio
tiempos_espera.append(espera)
siguiente_salida = tiempo + self.servicios.simulacion()
else:
servidor_ocupado = False
siguiente_salida = float('inf')
return {
"clientes_atendidos": cliente,
"tiempos_espera": tiempos_espera,
"prom_espera": np.mean(tiempos_espera) if tiempos_espera else 0.0,
"max_espera": max(tiempos_espera) if tiempos_espera else 0.0
}
generador = generador_principal
sistema = SistemaColasMM1(tasa_llegada=0.5, tasa_servicio=0.8, generador=generador)
resultado = sistema.simular(tiempo_max=120)
print("Clientes atendidos: ", resultado["clientes_atendidos"])
## Clientes atendidos: 48
print("Promedio de espera: ", resultado["prom_espera"])
## Promedio de espera: 1.8298604435737016
print("Máximo tiempo de espera:", resultado["max_espera"])
## Máximo tiempo de espera: 9.326055689964058
Dado un parámetro de probabilidad \(p \in (0,1)\) para una distribución de Bernoulli, queremos comparar dos estimadores:
Estimador frecuentista: \[ \hat{p}_1 = \frac{k}{n}, \quad k = \sum_{i=1}^n X_i,\quad X_i \sim \text{Bernoulli}(p). \]
Estimador bayesiano con prior \(\mathrm{Beta}(1,1)\) (uniforme): \[ \hat{p}_2 = \frac{k + 1}{\,n + 2\,}. \]
En cada repetición, generamos \(n\)
muestras de una Bernoulli con parámetro \(p\), calculamos \(k\), y obtenemos \(\hat{p}_1\) y \(\hat{p}_2\).
Luego repetimos el experimento \(R\)
veces para observar la variabilidad de ambos estimadores.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
def comparar_estimadores_con_zoom(p, n, repeticiones):
p1_list = []
p2_list = []
for _ in range(repeticiones):
gen_bernoulli = GeneradorBernoulli(p)
muestras = gen_bernoulli.muestra(n)
k = sum(muestras)
# Estimador frecuentista
p1 = k / n
# Estimador bayesiano con prior Beta(1,1)
p2 = (k + 1) / (n + 2)
p1_list.append(p1)
p2_list.append(p2)
indices = np.arange(1, repeticiones + 1)
y_min = min(min(p1_list), min(p2_list))
y_max = max(max(p1_list), max(p2_list))
margen = 0.02
y_min_zoom = max(0, y_min - margen)
y_max_zoom = min(1, y_max + margen)
plt.figure(figsize=(10, 5))
plt.scatter(indices, p1_list, color='blue', s=10, alpha=0.6, label='p1 (frecuentista)')
plt.scatter(indices, p2_list, color='red', s=10, alpha=0.6, label='p2 (bayesiano)')
plt.axhline(y=p, color='green', linestyle='--', linewidth=2, label=f'p real = {p}')
plt.ylim(y_min_zoom, y_max_zoom)
plt.title(f"Estimaciones de $\\hat{{p}}_1$ y $\\hat{{p}}_2$ (p={p}, n={n}, repeticiones={repeticiones})")
plt.xlabel("Repetición")
plt.ylabel("Estimador de $p$")
plt.legend()
plt.grid(True)
plt.show()
return p1_list, p2_list
p_true = 0.3
n_muestras = 50
n_reps = 200
p1_list, p2_list = comparar_estimadores_con_zoom(p_true, n_muestras, n_reps)
indices = np.arange(1, n_reps + 1)
tabla_estimaciones = pd.DataFrame({
"Iteración": indices,
"p1 (frecuentista)": p1_list,
"p2 (bayesiano)" : p2_list
})
print(tabla_estimaciones)
## Iteración p1 (frecuentista) p2 (bayesiano)
## 0 1 0.32 0.326923
## 1 2 0.32 0.326923
## 2 3 0.28 0.288462
## 3 4 0.30 0.307692
## 4 5 0.36 0.365385
## .. ... ... ...
## 195 196 0.32 0.326923
## 196 197 0.22 0.230769
## 197 198 0.36 0.365385
## 198 199 0.36 0.365385
## 199 200 0.30 0.307692
##
## [200 rows x 3 columns]
En la gráfica obtenida para \(p=0.3\), \(n=50\) y \(R=200\) repeticiones, observamos las estimaciones:
A partir de la dispersión de puntos, podemos extraer las siguientes conclusiones:
Queremos simular la SDE de la forma
\[ dX_t \;=\; f(t,\,X_t)\,dt \;+\; g(t,\,X_t)\,dW_t, \]
donde \(W_t\) es un proceso de Wiener estándar. Usaremos el esquema de Euler–Maruyama y el método de Box–Muller para generar los incrementos \(dW\).
Para obtener \(Z_{i}\), usamos dos
generadores globales: - MRG32k3a como generador
principal (uniformes \(U_1\)).
- LFSR20 como generador secundario (uniformes \(U_2\)).
Ambos producen números en \((0,1)\). Aplicamos el método polar de Box–Muller:
import numpy as np
import math
import matplotlib.pyplot as plt
generador_principal = MRG32k3a()
generador_secundario = LinearFeedbackGeneratorSize20(87685)
class SimuladorSDE:
"""
Simula la SDE dX = f(t, X)·dt + g(t, X)·dW usando
Euler–Maruyama y Box–Muller para generar los incrementos dW.
Parámetros:
- f: función determinista f(t, x)
- g: función de difusión g(t, x)
- x0: valor inicial X(0)
- T: horizonte de tiempo
- N: número de pasos discretos
- semilla: (opcional) semilla entera para reproducibilidad
"""
def __init__(self, f, g, x0, T, N, semilla=None):
self.f = f
self.g = g
self.x0 = x0
self.T = T
self.N = N
self.dt = T / N
self.tiempo = np.linspace(0, T, N + 1)
self.semilla = semilla
def simular(self):
if self.semilla is not None:
generador_principal.__init__(
x0=self.semilla,
x1=self.semilla + 1,
x2=self.semilla + 2,
y0=self.semilla + 3,
y1=self.semilla + 4,
y2=self.semilla + 5
)
generador_secundario.__init__(semilla=self.semilla + 12345)
X = np.zeros(self.N + 1)
X[0] = self.x0
for i in range(1, self.N + 1):
t = self.tiempo[i - 1]
while True:
u1 = generador_principal.muestra(1)[0]
u2 = generador_secundario.muestra(1)[0]
v1 = 2 * u1 - 1
v2 = 2 * u2 - 1
r2 = v1 * v1 + v2 * v2
if 0 < r2 <= 1:
break
z = v1 * math.sqrt(-2 * math.log(r2) / r2)
dW = z * math.sqrt(self.dt)
X[i] = (
X[i - 1]
+ self.f(t, X[i - 1]) * self.dt
+ self.g(t, X[i - 1]) * dW
)
return self.tiempo, X
def graficar(self, tiempo, X, titulo="Simulación de SDE"):
plt.figure(figsize=(10, 5))
plt.plot(tiempo, X, label="Trayectoria simulada")
plt.xlabel("Tiempo")
plt.ylabel("X(t)")
plt.title(titulo)
plt.grid(True)
plt.legend()
plt.show()
def f_gbm(t, x):
mu = 0.1
return mu * x
def g_gbm(t, x):
sigma = 0.3
return sigma * x
sim_gbm = SimuladorSDE(
f=f_gbm,
g=g_gbm,
x0=1.0,
T=1.0,
N=500,
semilla=2025
)
tiempo_gbm, X_gbm = sim_gbm.simular()
sim_gbm.graficar(tiempo_gbm, X_gbm, titulo="Geometric Brownian Motion (Euler–Maruyama)")