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

Last change on this file since 3792 was 3792, checked in by jbclement, 2 weeks ago

PEM:
Updates in the Python scripts to visualize the layering structure.
JBC

  • Property svn:executable set to *
File size: 11.8 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_pore': f"stratif_slope{idx_str}_h_pore",
58        'poreice_volfrac': f"stratif_slope{idx_str}_poreice_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_pore = data['h_pore']
85    poreice_volfrac = data['poreice_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_pore[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_pore[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_pore[0, layer_idx, grid_index] * (1.0 - poreice_volfrac[0, layer_idx, grid_index]) / thickness
138        poreice = h_pore[0, layer_idx, grid_index] * poreice_volfrac[0, layer_idx, grid_index] / thickness
139        contents[:, idx + 1] = [co2, h2o, dust, air, poreice]
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_xlim(0, 1)
164        ax.set_xlabel('Volume Fraction [m^3/m^3]')
165        ax.set_title(labels[idx])
166        if idx == 0:
167            ax.set_ylabel('Elevation [m]')
168        # Add major and minor grids on y-axis
169        ax.grid(which='major', axis='y', linestyle='--', linewidth=0.5, color='0.8')
170        ax.minorticks_on()
171        ax.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
172    plt.savefig('layering_simple_profiles.png')
173
174    # 2. Overlaid step profiles
175    plt.figure()
176    for idx, label in enumerate(labels):
177        plt.step(contents[idx, :], height, where='post', color=colors[idx], label=label)
178    plt.grid(axis='x', color='0.95')
179    plt.grid(axis='y', color='0.95')
180    plt.minorticks_on()
181    plt.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
182    plt.xlim(0, 1)
183    plt.xlabel('Volume Fraction [m^3/m^3]')
184    plt.ylabel('Elevation [m]')
185    plt.title('Content Profiles for Stratification')
186    plt.legend()
187    plt.savefig('layering_overlaid_profiles.png')
188
189    # 3. Stacked fill-between profile with relative fractions
190    plt.figure()
191    cumulative = np.zeros_like(contents[0, :])
192    for idx, label in enumerate(labels):
193        plt.fill_betweenx(
194            height,
195            cumulative,
196            cumulative + contents[idx, :],
197            step='pre',
198            label=label,
199            color=colors[idx]
200        )
201        cumulative += contents[idx, :]
202    plt.vlines(x=0., ymin=height[0], ymax=height[-1], color='k', linestyle='-')
203    plt.vlines(x=1., ymin=height[0], ymax=height[-1], color='k', linestyle='-')
204    for h in height:
205        plt.hlines(y=h, xmin=0.0, xmax=1.0, color='k', linestyle='--', linewidth=0.5)
206    plt.xlabel('Volume Fraction [m^3/m^3]')
207    plt.ylabel('Elevation [m]')
208    plt.title('Stacked Content Profiles for Stratification')
209    plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
210    plt.tight_layout()
211    plt.savefig('layering_stacked_profiles.png')
212
213
214def plot_stratum_layer(contents: np.ndarray, istr_index: int, labels: tuple[str, ...]) -> None:
215    """
216    Plot detailed composition of the specified stratum index (0-based) as a bar chart.
217    """
218    layer_fractions = contents[:, istr_index + 1]
219    plt.figure()
220    x_positions = np.arange(len(labels))
221    plt.bar(x_positions, layer_fractions, color=['r', 'b', 'y', 'violet', 'c'])
222    plt.xticks(x_positions, labels, rotation=45, ha='right')
223    plt.ylim(0, 1)
224    plt.ylabel('Volume Fraction [m^3/m^3]')
225    plt.title(f'Composition of Stratum {istr_index + 1}')
226    # Add major and minor grids on y-axis for the histogram
227    plt.grid(which='major', axis='y', linestyle='--', linewidth=0.5, color='0.8')
228    plt.minorticks_on()
229    plt.grid(which='minor', axis='y', linestyle=':', linewidth=0.3, color='0.9')
230    plt.tight_layout()
231    plt.savefig(f'stratum_{istr_index+1}_composition.png')
232
233
234def main():
235    if len(sys.argv) > 1:
236        filename = sys.argv[1]
237    else:
238        filename = input("Enter the NetCDF file name: ").strip()
239
240    if not os.path.isfile(filename):
241        sys.exit(f"Error: File '{filename}' does not exist.")
242
243    with nc.Dataset(filename, 'r') as dataset:
244        required_dims = ['Time', 'physical_points', 'nslope', 'nb_str_max']
245        for dim in required_dims:
246            if dim not in dataset.dimensions:
247                sys.exit(f"Error: Missing dimension '{dim}' in file.")
248
249        ngrid = len(dataset.dimensions['physical_points'])
250        nslope = len(dataset.dimensions['nslope'])
251        nb_str_max = len(dataset.dimensions['nb_str_max'])
252
253        print(f"File '{filename}' opened successfully.")
254        print(f"Number of grid points (physical_points): {ngrid}")
255        print(f"Number of slopes: {nslope}")
256        print(f"Maximum number of strata per slope: {nb_str_max}")
257
258        igrid_input = get_int_input(
259            "Enter grid point number", 1, ngrid
260        ) - 1
261        islope_input = get_int_input(
262            "Enter slope number", 1, nslope
263        ) - 1
264
265        show_subsurface = get_yes_no_input("Show subsurface layers?")
266        exclude_sub = not show_subsurface
267
268        # Load data for the chosen slope to determine number of surface strata
269        slope_data = load_slope_variables(dataset, islope_input)
270
271        # Compute raw heights to count strata above surface
272        top = slope_data['top_elev']
273        layers = top.shape[1]
274        raw_height = np.zeros(layers + 1)
275        # Compute initial subsurface bottom
276        h_co2 = slope_data['h_co2']
277        h_h2o = slope_data['h_h2o']
278        h_dust = slope_data['h_dust']
279        h_pore = slope_data['h_pore']
280        total_thickness0 = (
281            h_co2[0, 0, igrid_input]
282            + h_h2o[0, 0, igrid_input]
283            + h_dust[0, 0, igrid_input]
284            + h_pore[0, 0, igrid_input]
285        )
286        raw_height[0] = top[0, 0, igrid_input] - total_thickness0
287        raw_height[1:] = top[0, :, igrid_input]
288
289        include_mask = np.ones(layers, dtype=bool)
290        if exclude_sub:
291            include_mask = raw_height[1:] > 0
292        # Count number of strata above surface
293        filt_layers = np.where(include_mask)[0]
294        nb_str_surf_max = len(filt_layers)
295        if not show_subsurface:
296            if nb_str_surf_max == 0:
297                print("No stratum layers remain after filtering. Cannot proceed.")
298                return
299
300        # Prompt for stratum index based on surface strata count
301        istr_input = get_int_input(
302            "Enter stratum index for detailed plot", 1, nb_str_surf_max if exclude_sub else nb_str_max
303        ) - 1
304
305        height_arr, contents_arr, thicknesses_arr = calculate_contents(
306            slope_data, igrid_input, exclude_sub
307        )
308
309        component_labels = ("CO2 Ice", "H2O Ice", "Dust", "Air", "Pore ice")
310
311        plot_profiles(height_arr, contents_arr, thicknesses_arr, component_labels)
312        plot_stratum_layer(contents_arr, istr_input, component_labels)
313
314        # Show all figures at once
315        plt.show()
316
317
318if __name__ == '__main__':
319    main()
320
Note: See TracBrowser for help on using the repository browser.