source: trunk/LMDZ.COMMON/libf/evolution/deftank/visu_layering.py @ 3786

Last change on this file since 3786 was 3777, checked in by jbclement, 4 weeks ago

PEM:
Big improvements of Python scripts to output the stratifications (user-friendly prompt, error checks, code modularity, nice visualization, etc).
JBC

  • Property svn:executable set to *
File size: 11.7 KB
Line 
1#!/usr/bin/env python3
2#######################################################################################
3### Python script to output the stratification data from the "restartpem#.nc" files ###
4#######################################################################################
5
6import os
7import sys
8import numpy as np
9import netCDF4 as nc
10import matplotlib.pyplot as plt
11
12
13def get_int_input(prompt: str, min_val: int, max_val: int) -> int:
14    """
15    Prompt the user for an integer between min_val and max_val (inclusive).
16    If min_val == max_val, return min_val without prompting.
17    """
18    if min_val == max_val:
19        print(f"{prompt} (only possible value: {min_val})")
20        return min_val
21
22    while True:
23        try:
24            value = int(input(f"{prompt} (integer between {min_val} and {max_val}): "))
25            if min_val <= value <= max_val:
26                return value
27            print(f"Invalid value! Please enter a number between {min_val} and {max_val}.")
28        except ValueError:
29            print("Invalid input. Please enter a valid integer.")
30
31
32def get_yes_no_input(prompt: str) -> bool:
33    """
34    Prompt the user with a yes/no question. Returns True for yes, False for no.
35    """
36    while True:
37        choice = input(f"{prompt} (y/n): ").strip().lower()
38        if choice in ['y', 'yes']:
39            return True
40        elif choice in ['n', 'no']:
41            return False
42        else:
43            print("Please respond with y or n.")
44
45
46def load_slope_variables(nc_dataset: nc.Dataset, slope_index: int) -> dict:
47    """
48    Load all relevant stratification variables for a given slope index (0-based).
49    Returns a dict of NumPy arrays.
50    """
51    idx_str = str(slope_index + 1).zfill(2)
52    vars_base = {
53        'top_elev': f"stratif_slope{idx_str}_top_elevation",
54        'h_co2': f"stratif_slope{idx_str}_h_co2ice",
55        'h_h2o': f"stratif_slope{idx_str}_h_h2oice",
56        'h_dust': f"stratif_slope{idx_str}_h_dust",
57        'h_air': f"stratif_slope{idx_str}_h_pore",
58        'volfrac': f"stratif_slope{idx_str}_icepore_volfrac",
59    }
60
61    data = {}
62    for key, var_name in vars_base.items():
63        if var_name not in nc_dataset.variables:
64            sys.exit(f"Error: Variable '{var_name}' not found in the NetCDF file.")
65        data[key] = nc_dataset.variables[var_name][:]
66    return data
67
68
69def calculate_contents(data: dict, grid_index: int, exclude_subsurface: bool) -> (np.ndarray, np.ndarray, np.ndarray):
70    """
71    For a given slope's stratification data and a specified grid index (0-based),
72    compute the height array, layer thicknesses, and layer-wise volume fractions.
73    If exclude_subsurface is True, omit layers whose top elevation <= 0.
74
75    Returns:
76      - height: 1D array of elevations (length = number_of_layers + 1)
77      - contents: 2D array of shape (5, number_of_layers + 1), each row for a component
78      - thicknesses: 1D array of layer thicknesses (length = number_of_layers)
79    """
80    top = data['top_elev']
81    h_co2 = data['h_co2']
82    h_h2o = data['h_h2o']
83    h_dust = data['h_dust']
84    h_air = data['h_air']
85    volfrac = data['volfrac']
86
87    layers = top.shape[1]
88    # Compute raw height and thickness arrays
89    raw_height = np.zeros(layers + 1)
90    raw_thickness = np.zeros(layers)
91
92    total_thickness0 = (
93        h_co2[0, 0, grid_index]
94        + h_h2o[0, 0, grid_index]
95        + h_dust[0, 0, grid_index]
96        + h_air[0, 0, grid_index]
97    )
98    raw_height[0] = top[0, 0, grid_index] - total_thickness0
99    raw_height[1:] = top[0, :, grid_index]
100
101    for i in range(layers):
102        raw_thickness[i] = (
103            h_co2[0, i, grid_index]
104            + h_h2o[0, i, grid_index]
105            + h_dust[0, i, grid_index]
106            + h_air[0, i, grid_index]
107        )
108
109    include_mask = np.ones(layers, dtype=bool)
110    if exclude_subsurface:
111        include_mask = raw_height[1:] > 0
112    if exclude_subsurface and not include_mask.any() and raw_height[0] <= 0:
113        sys.exit("Error: No layers remain above the surface (elevation > 0).")
114
115    filt_layers = np.where(include_mask)[0]
116    num_filt = len(filt_layers)
117    height = np.zeros(num_filt + 1)
118    thicknesses = np.zeros(num_filt)
119
120    if exclude_subsurface and raw_height[0] <= 0:
121        height[0] = raw_height[filt_layers[0] + 1] - raw_thickness[filt_layers[0]]
122    else:
123        height[0] = raw_height[0]
124
125    for idx, layer_idx in enumerate(filt_layers):
126        height[idx + 1] = raw_height[layer_idx + 1]
127        thicknesses[idx] = raw_thickness[layer_idx]
128
129    contents = np.zeros((5, num_filt + 1))
130    for idx, layer_idx in enumerate(filt_layers):
131        thickness = raw_thickness[layer_idx]
132        if thickness <= 0:
133            continue
134        co2 = h_co2[0, layer_idx, grid_index] / thickness
135        h2o = h_h2o[0, layer_idx, grid_index] / thickness
136        dust = h_dust[0, layer_idx, grid_index] / thickness
137        air = h_air[0, layer_idx, grid_index] * (1.0 - volfrac[0, layer_idx, grid_index]) / thickness
138        pore = h_air[0, layer_idx, grid_index] * volfrac[0, layer_idx, grid_index] / thickness
139        contents[:, idx + 1] = [co2, h2o, dust, air, pore]
140
141    if num_filt > 0:
142        contents[:, 0] = contents[:, 1]
143
144    return height, contents, thicknesses
145
146
147def plot_profiles(height: np.ndarray, contents: np.ndarray, thicknesses: np.ndarray, labels: tuple[str, ...]) -> None:
148    """
149    Create and save plots:
150      1. Simple step profiles (each component in its own subplot)
151      2. Overlaid step profiles (all components on one plot)
152      3. Stacked fill-between profile with relative fractions
153    """
154    n_components = contents.shape[0]
155    colors = ['r', 'b', 'y', 'violet', 'c']
156
157    # 1. Simple subplots
158    fig, axes = plt.subplots(1, n_components, sharey=True, constrained_layout=True)
159    fig.suptitle('Simple Content Profiles for Stratification')
160
161    for idx, ax in enumerate(axes):
162        ax.step(contents[idx, :], height, where='post', color=colors[idx])
163        ax.set_xlabel('Volume Fraction [m^3/m^3]')
164        ax.set_title(labels[idx])
165        if idx == 0:
166            ax.set_ylabel('Elevation [m]')
167        # Add major and minor grids on y-axis
168        ax.grid(which='major', axis='y', linestyle='--', linewidth=0.5, color='0.8')
169        ax.minorticks_on()
170        ax.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
171    plt.savefig('layering_simple_profiles.png')
172
173    # 2. Overlaid step profiles
174    plt.figure()
175    for idx, label in enumerate(labels):
176        plt.step(contents[idx, :], height, where='post', color=colors[idx], label=label)
177    plt.grid(axis='x', color='0.95')
178    plt.grid(axis='y', color='0.95')
179    plt.minorticks_on()
180    plt.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
181    plt.xlim(0, 1)
182    plt.xlabel('Volume Fraction [m^3/m^3]')
183    plt.ylabel('Elevation [m]')
184    plt.title('Content Profiles for Stratification')
185    plt.legend()
186    plt.savefig('layering_overlaid_profiles.png')
187
188    # 3. Stacked fill-between profile with relative fractions
189    plt.figure()
190    cumulative = np.zeros_like(contents[0, :])
191    for idx, label in enumerate(labels):
192        plt.fill_betweenx(
193            height,
194            cumulative,
195            cumulative + contents[idx, :],
196            step='pre',
197            label=label,
198            color=colors[idx]
199        )
200        cumulative += contents[idx, :]
201    plt.vlines(x=0., ymin=height[0], ymax=height[-1], color='k', linestyle='-')
202    plt.vlines(x=1., ymin=height[0], ymax=height[-1], color='k', linestyle='-')
203    for h in height:
204        plt.hlines(y=h, xmin=0.0, xmax=1.0, color='k', linestyle='--', linewidth=0.5)
205    plt.xlabel('Volume Fraction [m^3/m^3]')
206    plt.ylabel('Elevation [m]')
207    plt.title('Stacked Content Profiles for Stratification')
208    plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
209    plt.tight_layout()
210    plt.savefig('layering_stacked_profiles.png')
211
212
213def plot_stratum_layer(contents: np.ndarray, istr_index: int, labels: tuple[str, ...]) -> None:
214    """
215    Plot detailed composition of the specified stratum index (0-based) as a bar chart.
216    """
217    layer_fractions = contents[:, istr_index + 1]
218    plt.figure()
219    x_positions = np.arange(len(labels))
220    plt.bar(x_positions, layer_fractions, color=['r', 'b', 'y', 'violet', 'c'])
221    plt.xticks(x_positions, labels, rotation=45, ha='right')
222    plt.ylim(0, 1)
223    plt.ylabel('Volume Fraction [m^3/m^3]')
224    plt.title(f'Composition of Stratum {istr_index + 1}')
225    # Add major and minor grids on y-axis for the histogram
226    plt.grid(which='major', axis='y', linestyle='--', linewidth=0.5, color='0.8')
227    plt.minorticks_on()
228    plt.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
229    plt.tight_layout()
230    plt.savefig(f'stratum_{istr_index+1}_composition.png')
231
232
233def main():
234    if len(sys.argv) > 1:
235        filename = sys.argv[1]
236    else:
237        filename = input("Enter the NetCDF file name: ").strip()
238
239    if not os.path.isfile(filename):
240        sys.exit(f"Error: File '{filename}' does not exist.")
241
242    with nc.Dataset(filename, 'r') as dataset:
243        required_dims = ['Time', 'physical_points', 'nslope', 'nb_str_max']
244        for dim in required_dims:
245            if dim not in dataset.dimensions:
246                sys.exit(f"Error: Missing dimension '{dim}' in file.")
247
248        ngrid = len(dataset.dimensions['physical_points'])
249        nslope = len(dataset.dimensions['nslope'])
250        nb_str_max = len(dataset.dimensions['nb_str_max'])
251
252        print(f"File '{filename}' opened successfully.")
253        print(f"Number of grid points (physical_points): {ngrid}")
254        print(f"Number of slopes: {nslope}")
255        print(f"Maximum number of strata per slope: {nb_str_max}")
256
257        igrid_input = get_int_input(
258            "Enter grid point number", 1, ngrid
259        ) - 1
260        islope_input = get_int_input(
261            "Enter slope number", 1, nslope
262        ) - 1
263
264        show_subsurface = get_yes_no_input("Show subsurface layers?")
265        exclude_sub = not show_subsurface
266
267        # Load data for the chosen slope to determine number of surface strata
268        slope_data = load_slope_variables(dataset, islope_input)
269
270        # Compute raw heights to count strata above surface
271        top = slope_data['top_elev']
272        layers = top.shape[1]
273        raw_height = np.zeros(layers + 1)
274        # Compute initial subsurface bottom
275        h_co2 = slope_data['h_co2']
276        h_h2o = slope_data['h_h2o']
277        h_dust = slope_data['h_dust']
278        h_air = slope_data['h_air']
279        total_thickness0 = (
280            h_co2[0, 0, igrid_input]
281            + h_h2o[0, 0, igrid_input]
282            + h_dust[0, 0, igrid_input]
283            + h_air[0, 0, igrid_input]
284        )
285        raw_height[0] = top[0, 0, igrid_input] - total_thickness0
286        raw_height[1:] = top[0, :, igrid_input]
287
288        include_mask = np.ones(layers, dtype=bool)
289        if exclude_sub:
290            include_mask = raw_height[1:] > 0
291        # Count number of strata above surface
292        filt_layers = np.where(include_mask)[0]
293        nb_str_surf_max = len(filt_layers)
294        if not show_subsurface:
295            if nb_str_surf_max == 0:
296                print("No stratum layers remain after filtering. Cannot proceed.")
297                return
298
299        # Prompt for stratum index based on surface strata count
300        istr_input = get_int_input(
301            "Enter stratum index for detailed plot", 1, nb_str_surf_max if exclude_sub else nb_str_max
302        ) - 1
303
304        height_arr, contents_arr, thicknesses_arr = calculate_contents(
305            slope_data, igrid_input, exclude_sub
306        )
307
308        component_labels = ("CO2 Ice", "H2O Ice", "Dust", "Air", "Pore ice")
309
310        plot_profiles(height_arr, contents_arr, thicknesses_arr, component_labels)
311        plot_stratum_layer(contents_arr, istr_input, component_labels)
312
313        # Show all figures at once
314        plt.show()
315
316
317if __name__ == '__main__':
318    main()
319
Note: See TracBrowser for help on using the repository browser.