From e4e11c39eb95cd37d7200a87e69be7218d716649 Mon Sep 17 00:00:00 2001
From: Stefano Serafin <serafin@jet02.img.univie.ac.at>
Date: Thu, 18 Jul 2024 16:22:51 +0200
Subject: [PATCH] moved state perturbations to a separate function, to enable
 re-use in other parts of the code

---
 models.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 70 insertions(+), 12 deletions(-)

diff --git a/models.py b/models.py
index 0e6abe7..c9d3957 100644
--- a/models.py
+++ b/models.py
@@ -5,9 +5,6 @@ from scipy.interpolate import CubicSpline
 
 verbose = True
 
-# Initiate a random number generator with preset seed, to ensure reproducibility
-RNG = np.random.default_rng(seed=240718)
-
 class CBL:
     def __init__(self,settings):
         for k, v in settings.items():
@@ -92,7 +89,9 @@ class CBL:
 
         # If desired, add ensemble perturbations to model state
         if self.nens>1 and self.perturb_ensemble_state:
+            x0 = self.perturb_state(x0)
 
+            """
             # Random or uniform perturbations perturbations
             # differ only by magnitude of random sample
             if self.perturbations_type == "random":
@@ -101,10 +100,10 @@ class CBL:
                 randomsize = 1
             
             if self.perturbations_type == "random" or self.perturbations_type == "uniform":
-                ppt = RNG.standard_normal((randomsize,self.nens))
+                ppt = self.RNG.standard_normal((randomsize,self.nens))
                 if self.is_bwind:
-                    ppu = RNG.standard_normal((self.nens,randomsize))
-                    ppv = RNG.standard_normal((self.nens,randomsize))
+                    ppu = self.RNG.standard_normal((self.nens,randomsize))
+                    ppv = self.RNG.standard_normal((self.nens,randomsize))
 
             # Smooth perturbations are slightly more complicated
             if self.perturbations_type == "smooth":
@@ -117,15 +116,15 @@ class CBL:
                 # Draw random perturbations, then interpolate
                 randomsize = self.perturbations_smooth_number
                 ppt = np.zeros((self.nz,self.nens))+np.nan
-                pert_t = RNG.standard_normal((randomsize,self.nens))
+                pert_t = self.RNG.standard_normal((randomsize,self.nens))
                 for n in range(self.nens):
                     f = CubicSpline(ipert,pert_t[:,n])
                     ppt[:,n] = f(np.arange(self.nz))
                 if self.is_bwind:
                     ppu = np.zeros((self.nz,self.nens))+np.nan
                     ppv = np.zeros((self.nz,self.nens))+np.nan
-                    pert_u = RNG.standard_normal((randomsize,self.nens))
-                    pert_v = RNG.standard_normal((randomsize,self.nens))
+                    pert_u = self.RNG.standard_normal((randomsize,self.nens))
+                    pert_v = self.RNG.standard_normal((randomsize,self.nens))
                     for n in range(self.nens):
                         f = CubicSpline(ipert,pert_u[:,n])
                         ppu[:,n] = f(np.arange(self.nz))
@@ -148,6 +147,7 @@ class CBL:
                     if self.is_bwind:
                         x0[self.nz:self.nz*2,n] += self.perturbations_uv_amplitude*ppu[:,n]
                         x0[self.nz*2:self.nz*3,n] += self.perturbations_uv_amplitude*ppv[:,n]
+            """
 
         # If desired, perturb parameters too
         # (last parameter_number elements of state vector)
@@ -215,11 +215,69 @@ class CBL:
         pp = np.zeros((self.parameter_number,self.nens))
 
         for k in range(-self.parameter_number,0):
-            dum = RNG.uniform(self.parameter_ensemble_min[k], self.parameter_ensemble_max[k], size=self.nens)
+            dum = self.RNG.uniform(self.parameter_ensemble_min[k], self.parameter_ensemble_max[k], size=self.nens)
             pp[k,:] = self.parameter_transform[k](dum , kind='dir')
 
         return pp
 
+    def perturb_state(self,x0):
+        # Random or uniform perturbations perturbations
+        # differ only by magnitude of random sample
+        if self.perturbations_type == "random":
+            randomsize = self.nz
+        elif self.perturbations_type == "uniform":
+            randomsize = 1
+        
+        if self.perturbations_type == "random" or self.perturbations_type == "uniform":
+            ppt = self.RNG.standard_normal((randomsize,self.nens))
+            if self.is_bwind:
+                ppu = self.RNG.standard_normal((self.nens,randomsize))
+                ppv = self.RNG.standard_normal((self.nens,randomsize))
+
+        # Smooth perturbations are slightly more complicated
+        if self.perturbations_type == "smooth":
+
+            # Define positions of perturbations
+            # enforce two at boundaries to avoid extrapolation
+            ipert = np.arange(0,self.perturbations_smooth_number-1)*(self.nz//(self.perturbations_smooth_number))
+            ipert = np.append(ipert,self.nz-1)
+
+            # Draw random perturbations, then interpolate
+            randomsize = self.perturbations_smooth_number
+            ppt = np.zeros((self.nz,self.nens))+np.nan
+            pert_t = self.RNG.standard_normal((randomsize,self.nens))
+            for n in range(self.nens):
+                f = CubicSpline(ipert,pert_t[:,n])
+                ppt[:,n] = f(np.arange(self.nz))
+            if self.is_bwind:
+                ppu = np.zeros((self.nz,self.nens))+np.nan
+                ppv = np.zeros((self.nz,self.nens))+np.nan
+                pert_u = self.RNG.standard_normal((randomsize,self.nens))
+                pert_v = self.RNG.standard_normal((randomsize,self.nens))
+                for n in range(self.nens):
+                    f = CubicSpline(ipert,pert_u[:,n])
+                    ppu[:,n] = f(np.arange(self.nz))
+                    f = CubicSpline(ipert,pert_v[:,n])
+                    ppv[:,n] = f(np.arange(self.nz)) 
+
+        if self.perturbations_symmetric:
+            # if n is odd, the last member remains unperturbed
+            for n in range(self.nens//2):
+                x0[:self.nz,n] += self.perturbations_theta_amplitude*ppt[:,n]
+                x0[:self.nz,self.nens//2+n] -= self.perturbations_theta_amplitude*ppt[:,n]
+                if self.is_bwind:
+                    x0[self.nz:self.nz*2,n] += self.perturbations_uv_amplitude*ppu[:,n]
+                    x0[self.nz:self.nz*2,self.nens//2+n] -= self.perturbations_uv_amplitude*ppu[:,n]
+                    x0[self.nz*2:self.nz*3,n] += self.perturbations_uv_amplitude*ppv[:,n]
+                    x0[self.nz*2:self.nz*3,self.nens//2+n] -= self.perturbations_uv_amplitude*ppv[:,n]
+        else:
+            for n in range(self.nens):
+                x0[:self.nz,n] += self.perturbations_theta_amplitude*ppt[:,n]
+                if self.is_bwind:
+                    x0[self.nz:self.nz*2,n] += self.perturbations_uv_amplitude*ppu[:,n]
+                    x0[self.nz*2:self.nz*3,n] += self.perturbations_uv_amplitude*ppv[:,n]
+        return x0
+
     def cbl_model(self,x0,output_full_history=False,perturbed_parameters=np.nan):
         # Read settings in
         nt = self.nt
@@ -310,7 +368,7 @@ class CBL:
                 H0 = Hmax
             
             # Add random perturbations to the initial value
-            H0 += RNG.normal(scale=H0_perturbation_ampl_init)
+            H0 += self.RNG.normal(scale=H0_perturbation_ampl_init)
 
             # Set the surface momentum flux (ustar)
             ustar = 0
@@ -360,7 +418,7 @@ class CBL:
 
             # Add time-dependent surface perturbations
             # Then compute sensible heat flux and integrate T equation
-            H[0,j]     = H0 + RNG.normal(scale=H0_perturbation_ampl_time)
+            H[0,j]     = H0 + self.RNG.normal(scale=H0_perturbation_ampl_time)
             H[1:-1,j]  = -K[1:-1,j]*( (thetap[1:]-thetap[:-1])*rdz - gammac)
             H[nz,j]    = 2*H[nz-1,j]-H[nz-2,j]
             theta[:,j] = thetap[:]-dt*rdz*(H[1:,j]-H[:-1,j])
-- 
GitLab