ras-commander 0.48.0__py3-none-any.whl → 0.49.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,22 @@
1
1
  """
2
- Class: HdfResultsPlan
2
+ HdfResultsPlan: A module for extracting and analyzing HEC-RAS plan HDF file results.
3
3
 
4
- Attribution: A substantial amount of code in this file is sourced or derived
5
- from the https://github.com/fema-ffrd/rashdf library,
6
- released under MIT license and Copyright (c) 2024 fema-ffrd
4
+ Attribution:
5
+ Substantial code sourced/derived from https://github.com/fema-ffrd/rashdf
6
+ Copyright (c) 2024 fema-ffrd, MIT license
7
7
 
8
- The file has been forked and modified for use in RAS Commander.
8
+ Description:
9
+ Provides static methods for extracting unsteady flow results, volume accounting,
10
+ and reference data from HEC-RAS plan HDF files.
11
+
12
+ Available Functions:
13
+ - get_unsteady_info: Extract unsteady attributes
14
+ - get_unsteady_summary: Extract unsteady summary data
15
+ - get_volume_accounting: Extract volume accounting data
16
+ - get_runtime_data: Extract runtime and compute time data
17
+
18
+ Note:
19
+ All methods are static and designed to be used without class instantiation.
9
20
  """
10
21
 
11
22
  from typing import Dict, List, Union, Optional
@@ -14,7 +25,7 @@ import h5py
14
25
  import pandas as pd
15
26
  import xarray as xr
16
27
  from .Decorators import standardize_input, log_call
17
- from .HdfBase import HdfBase
28
+ from .HdfUtils import HdfUtils
18
29
  from .HdfResultsXsec import HdfResultsXsec
19
30
  from .LoggingConfig import get_logger
20
31
  import numpy as np
@@ -25,26 +36,27 @@ logger = get_logger(__name__)
25
36
 
26
37
  class HdfResultsPlan:
27
38
  """
28
- A class for handling HEC-RAS plan HDF file results related to unsteady flow and reference line/point outputs.
29
-
30
- This class provides methods for extracting and analyzing data from HEC-RAS plan HDF files,
31
- focusing on unsteady flow results, volume accounting, and reference line/point time series outputs.
39
+ Handles extraction of results data from HEC-RAS plan HDF files.
32
40
 
33
- Methods in this class use the @standardize_input decorator to handle different input types
34
- (e.g., plan number, file path) and the @log_call decorator for logging method calls.
41
+ This class provides static methods for accessing and analyzing:
42
+ - Unsteady flow results
43
+ - Volume accounting data
44
+ - Runtime statistics
45
+ - Reference line/point time series outputs
35
46
 
36
- Attributes:
37
- None
47
+ All methods use:
48
+ - @standardize_input decorator for consistent file path handling
49
+ - @log_call decorator for operation logging
50
+ - HdfUtils class for common HDF operations
38
51
 
39
52
  Note:
40
- This class is designed to work with HEC-RAS plan HDF files and requires the HdfBase class
41
- for some of its operations.
53
+ No instantiation required - all methods are static.
42
54
  """
43
55
 
44
56
  @staticmethod
45
57
  @log_call
46
58
  @standardize_input(file_type='plan_hdf')
47
- def get_results_unsteady_attrs(hdf_path: Path) -> Dict:
59
+ def get_unsteady_info(hdf_path: Path) -> pd.DataFrame:
48
60
  """
49
61
  Get unsteady attributes from a HEC-RAS HDF plan file.
50
62
 
@@ -52,7 +64,7 @@ class HdfResultsPlan:
52
64
  hdf_path (Path): Path to the HEC-RAS plan HDF file.
53
65
 
54
66
  Returns:
55
- Dict: A dictionary containing the unsteady attributes.
67
+ pd.DataFrame: A DataFrame containing the unsteady attributes.
56
68
 
57
69
  Raises:
58
70
  FileNotFoundError: If the specified HDF file is not found.
@@ -62,16 +74,22 @@ class HdfResultsPlan:
62
74
  with h5py.File(hdf_path, 'r') as hdf_file:
63
75
  if "Results/Unsteady" not in hdf_file:
64
76
  raise KeyError("Results/Unsteady group not found in the HDF file.")
65
- return dict(hdf_file["Results/Unsteady"].attrs)
77
+
78
+ # Create dictionary from attributes
79
+ attrs_dict = dict(hdf_file["Results/Unsteady"].attrs)
80
+
81
+ # Create DataFrame with a single row index
82
+ return pd.DataFrame(attrs_dict, index=[0])
83
+
66
84
  except FileNotFoundError:
67
85
  raise FileNotFoundError(f"HDF file not found: {hdf_path}")
68
86
  except Exception as e:
69
87
  raise RuntimeError(f"Error reading unsteady attributes: {str(e)}")
70
-
88
+
71
89
  @staticmethod
72
90
  @log_call
73
91
  @standardize_input(file_type='plan_hdf')
74
- def get_results_unsteady_summary_attrs(hdf_path: Path) -> Dict:
92
+ def get_unsteady_summary(hdf_path: Path) -> pd.DataFrame:
75
93
  """
76
94
  Get results unsteady summary attributes from a HEC-RAS HDF plan file.
77
95
 
@@ -79,7 +97,7 @@ class HdfResultsPlan:
79
97
  hdf_path (Path): Path to the HEC-RAS plan HDF file.
80
98
 
81
99
  Returns:
82
- Dict: A dictionary containing the results unsteady summary attributes.
100
+ pd.DataFrame: A DataFrame containing the results unsteady summary attributes.
83
101
 
84
102
  Raises:
85
103
  FileNotFoundError: If the specified HDF file is not found.
@@ -89,16 +107,22 @@ class HdfResultsPlan:
89
107
  with h5py.File(hdf_path, 'r') as hdf_file:
90
108
  if "Results/Unsteady/Summary" not in hdf_file:
91
109
  raise KeyError("Results/Unsteady/Summary group not found in the HDF file.")
92
- return dict(hdf_file["Results/Unsteady/Summary"].attrs)
110
+
111
+ # Create dictionary from attributes
112
+ attrs_dict = dict(hdf_file["Results/Unsteady/Summary"].attrs)
113
+
114
+ # Create DataFrame with a single row index
115
+ return pd.DataFrame(attrs_dict, index=[0])
116
+
93
117
  except FileNotFoundError:
94
118
  raise FileNotFoundError(f"HDF file not found: {hdf_path}")
95
119
  except Exception as e:
96
120
  raise RuntimeError(f"Error reading unsteady summary attributes: {str(e)}")
97
-
121
+
98
122
  @staticmethod
99
123
  @log_call
100
124
  @standardize_input(file_type='plan_hdf')
101
- def get_results_volume_accounting_attrs(hdf_path: Path) -> Dict:
125
+ def get_volume_accounting(hdf_path: Path) -> pd.DataFrame:
102
126
  """
103
127
  Get volume accounting attributes from a HEC-RAS HDF plan file.
104
128
 
@@ -106,7 +130,7 @@ class HdfResultsPlan:
106
130
  hdf_path (Path): Path to the HEC-RAS plan HDF file.
107
131
 
108
132
  Returns:
109
- Dict: A dictionary containing the volume accounting attributes.
133
+ pd.DataFrame: A DataFrame containing the volume accounting attributes.
110
134
 
111
135
  Raises:
112
136
  FileNotFoundError: If the specified HDF file is not found.
@@ -116,7 +140,13 @@ class HdfResultsPlan:
116
140
  with h5py.File(hdf_path, 'r') as hdf_file:
117
141
  if "Results/Unsteady/Summary/Volume Accounting" not in hdf_file:
118
142
  raise KeyError("Results/Unsteady/Summary/Volume Accounting group not found in the HDF file.")
119
- return dict(hdf_file["Results/Unsteady/Summary/Volume Accounting"].attrs)
143
+
144
+ # Get attributes and create dictionary
145
+ attrs_dict = dict(hdf_file["Results/Unsteady/Summary/Volume Accounting"].attrs)
146
+
147
+ # Create DataFrame with a single row index
148
+ return pd.DataFrame(attrs_dict, index=[0])
149
+
120
150
  except FileNotFoundError:
121
151
  raise FileNotFoundError(f"HDF file not found: {hdf_path}")
122
152
  except Exception as e:
@@ -126,13 +156,29 @@ class HdfResultsPlan:
126
156
  @standardize_input(file_type='plan_hdf')
127
157
  def get_runtime_data(hdf_path: Path) -> Optional[pd.DataFrame]:
128
158
  """
129
- Extract runtime and compute time data from a single HDF file.
159
+ Extract detailed runtime and computational performance metrics from HDF file.
130
160
 
131
161
  Args:
132
- hdf_path (Path): The full path to the HDF file.
162
+ hdf_path (Path): Path to HEC-RAS plan HDF file
133
163
 
134
164
  Returns:
135
- Optional[pd.DataFrame]: DataFrame containing runtime and compute time data, or None if data extraction fails.
165
+ Optional[pd.DataFrame]: DataFrame containing:
166
+ - Plan identification (name, file)
167
+ - Simulation timing (start, end, duration)
168
+ - Process-specific compute times
169
+ - Performance metrics (simulation speeds)
170
+ Returns None if required data cannot be extracted
171
+
172
+ Notes:
173
+ - Times are reported in multiple units (ms, s, hours)
174
+ - Compute speeds are calculated as simulation-time/compute-time ratios
175
+ - Process times include: geometry, preprocessing, event conditions,
176
+ and unsteady flow computations
177
+
178
+ Example:
179
+ >>> runtime_stats = HdfResultsPlan.get_runtime_data('path/to/plan.hdf')
180
+ >>> if runtime_stats is not None:
181
+ >>> print(f"Total compute time: {runtime_stats['Complete Process (hr)'][0]:.2f} hours")
136
182
  """
137
183
  if hdf_path is None:
138
184
  logger.error(f"Could not find HDF file for input")
@@ -206,196 +252,6 @@ class HdfResultsPlan:
206
252
 
207
253
  return compute_summary_df
208
254
 
209
-
210
-
211
- @staticmethod
212
- @log_call
213
- @standardize_input(file_type='plan_hdf')
214
- def reference_timeseries_output(hdf_path: Path, reftype: str = "lines") -> xr.Dataset:
215
- """
216
- Get timeseries output for reference lines or points.
217
-
218
- Args:
219
- hdf_path (Path): Path to the HDF file.
220
- reftype (str): Type of reference, either "lines" or "points" (default "lines").
221
-
222
- Returns:
223
- xr.Dataset: Dataset containing the timeseries output for reference lines or points.
224
-
225
- Raises:
226
- FileNotFoundError: If the specified HDF file is not found.
227
- ValueError: If an invalid reftype is provided.
228
- """
229
- try:
230
- with h5py.File(hdf_path, 'r') as hdf_file:
231
- return HdfResultsPlan._reference_timeseries_output(hdf_file, reftype)
232
- except FileNotFoundError:
233
- raise FileNotFoundError(f"HDF file not found: {hdf_path}")
234
- except ValueError as ve:
235
- raise ValueError(f"Invalid reftype: {str(ve)}")
236
- except Exception as e:
237
- raise RuntimeError(f"Error getting reference timeseries output: {str(e)}")
238
-
239
-
240
- @staticmethod
241
- def _reference_timeseries_output(hdf_file: h5py.File, reftype: str = "lines") -> xr.Dataset:
242
- """
243
- Private method to return timeseries output data for reference lines or points from a HEC-RAS HDF plan file.
244
-
245
- Parameters
246
- ----------
247
- hdf_file : h5py.File
248
- Open HDF file object.
249
- reftype : str, optional
250
- The type of reference data to retrieve. Must be either "lines" or "points".
251
- (default: "lines")
252
-
253
- Returns
254
- -------
255
- xr.Dataset
256
- An xarray Dataset with reference line or point timeseries data.
257
- Returns an empty Dataset if the reference output data is not found.
258
-
259
- Raises
260
- ------
261
- ValueError
262
- If reftype is not "lines" or "points".
263
- """
264
- if reftype == "lines":
265
- output_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Reference Lines"
266
- abbrev = "refln"
267
- elif reftype == "points":
268
- output_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Reference Points"
269
- abbrev = "refpt"
270
- else:
271
- raise ValueError('reftype must be either "lines" or "points".')
272
-
273
- try:
274
- reference_group = hdf_file[output_path]
275
- except KeyError:
276
- logger.error(f"Could not find HDF group at path '{output_path}'. "
277
- f"The Plan HDF file may not contain reference {reftype[:-1]} output data.")
278
- return xr.Dataset()
279
-
280
- reference_names = reference_group["Name"][:]
281
- names = []
282
- mesh_areas = []
283
- for s in reference_names:
284
- name, mesh_area = s.decode("utf-8").split("|")
285
- names.append(name)
286
- mesh_areas.append(mesh_area)
287
-
288
- times = HdfBase._get_unsteady_datetimes(hdf_file)
289
-
290
- das = {}
291
- for var in ["Flow", "Velocity", "Water Surface"]:
292
- group = reference_group.get(var)
293
- if group is None:
294
- continue
295
- values = group[:]
296
- units = group.attrs["Units"].decode("utf-8")
297
- da = xr.DataArray(
298
- values,
299
- name=var,
300
- dims=["time", f"{abbrev}_id"],
301
- coords={
302
- "time": times,
303
- f"{abbrev}_id": range(values.shape[1]),
304
- f"{abbrev}_name": (f"{abbrev}_id", names),
305
- "mesh_name": (f"{abbrev}_id", mesh_areas),
306
- },
307
- attrs={"units": units, "hdf_path": f"{output_path}/{var}"},
308
- )
309
- das[var] = da
310
- return xr.Dataset(das)
311
-
312
255
 
313
256
 
314
257
 
315
- @staticmethod
316
- @log_call
317
- @standardize_input(file_type='plan_hdf')
318
- def reference_lines_timeseries_output(hdf_path: Path) -> xr.Dataset:
319
- """
320
- Get timeseries output for reference lines.
321
-
322
- Args:
323
- hdf_path (Path): Path to the HDF file.
324
-
325
- Returns:
326
- xr.Dataset: Dataset containing the timeseries output for reference lines.
327
-
328
- Raises:
329
- FileNotFoundError: If the specified HDF file is not found.
330
- """
331
- return HdfResultsPlan.reference_timeseries_output(hdf_path, reftype="lines")
332
-
333
- @staticmethod
334
- @log_call
335
- @standardize_input(file_type='plan_hdf')
336
- def reference_points_timeseries_output(hdf_path: Path) -> xr.Dataset:
337
- """
338
- Get timeseries output for reference points.
339
-
340
- Args:
341
- hdf_path (Path): Path to the HDF file.
342
-
343
- Returns:
344
- xr.Dataset: Dataset containing the timeseries output for reference points.
345
-
346
- Raises:
347
- FileNotFoundError: If the specified HDF file is not found.
348
- """
349
- return HdfResultsPlan.reference_timeseries_output(hdf_path, reftype="points")
350
-
351
- @staticmethod
352
- @log_call
353
- @standardize_input(file_type='plan_hdf')
354
- def reference_summary_output(hdf_path: Path, reftype: str = "lines") -> pd.DataFrame:
355
- """
356
- Get summary output for reference lines or points.
357
-
358
- Args:
359
- hdf_path (Path): Path to the HDF file.
360
- reftype (str): Type of reference, either "lines" or "points" (default "lines").
361
-
362
- Returns:
363
- pd.DataFrame: DataFrame containing the summary output for reference lines or points.
364
-
365
- Raises:
366
- ValueError: If an invalid reftype is provided.
367
- """
368
- if not hdf_path.exists():
369
- logger.error(f"HDF file not found: {hdf_path}")
370
- return pd.DataFrame() # Return an empty DataFrame if the path doesn't exist
371
-
372
- try:
373
- # Get the timeseries output
374
- ds = HdfResultsPlan.reference_timeseries_output(hdf_path, reftype)
375
-
376
- if 'station' not in ds.dims:
377
- logger.error("No 'station' dimension found in the dataset.")
378
- return pd.DataFrame() # Return an empty DataFrame if 'station' dimension is missing
379
-
380
- # Calculate summary statistics
381
- summary = ds.groupby('station').agg({
382
- 'WSE': ['min', 'max', 'mean'],
383
- 'Q': ['min', 'max', 'mean']
384
- })
385
-
386
- # Flatten column names
387
- summary.columns = ['_'.join(col).strip() for col in summary.columns.values]
388
-
389
- # Reset index to make 'station' a column
390
- summary = summary.reset_index()
391
-
392
- return summary
393
- except ValueError as ve:
394
- logger.error(f"Invalid reftype: {str(ve)}")
395
- return pd.DataFrame() # Return an empty DataFrame on ValueError
396
- except Exception as e:
397
- logger.error(f"Error in reference_summary_output: {str(e)}")
398
- return pd.DataFrame() # Return an empty DataFrame on general error
399
-
400
-
401
-
@@ -0,0 +1,182 @@
1
+ """
2
+ Class: HdfResultsPlot
3
+
4
+ A collection of static methods for visualizing HEC-RAS results data from HDF files using matplotlib.
5
+
6
+ Public Functions:
7
+ plot_results_mesh_variable(variable_df, variable_name, colormap='viridis', point_size=10):
8
+ Generic plotting function for any mesh variable with customizable styling.
9
+
10
+ plot_results_max_wsel(max_ws_df):
11
+ Visualizes the maximum water surface elevation distribution across mesh cells.
12
+
13
+ plot_results_max_wsel_time(max_ws_df):
14
+ Displays the timing of maximum water surface elevation for each cell,
15
+ including statistics about the temporal distribution.
16
+
17
+ Requirements:
18
+ - matplotlib
19
+ - pandas
20
+ - geopandas (for geometry handling)
21
+
22
+ Input DataFrames must contain:
23
+ - 'geometry' column with Point objects containing x,y coordinates
24
+ - Variable data columns as specified in individual function docstrings
25
+ """
26
+
27
+ import matplotlib.pyplot as plt
28
+ import pandas as pd
29
+ from typing import Dict
30
+ from .Decorators import log_call
31
+ from .HdfMesh import HdfMesh
32
+
33
+ class HdfResultsPlot:
34
+ """
35
+ A class containing static methods for plotting HEC-RAS results data.
36
+
37
+ This class provides visualization methods for various types of HEC-RAS results,
38
+ including maximum water surface elevations and timing information.
39
+ """
40
+
41
+ @staticmethod
42
+ @log_call
43
+ def plot_results_max_wsel(max_ws_df: pd.DataFrame) -> None:
44
+ """
45
+ Plots the maximum water surface elevation per cell.
46
+
47
+ Args:
48
+ max_ws_df (pd.DataFrame): DataFrame containing merged data with coordinates
49
+ and max water surface elevations.
50
+ """
51
+ # Extract x and y coordinates from the geometry column
52
+ max_ws_df['x'] = max_ws_df['geometry'].apply(lambda geom: geom.x if geom is not None else None)
53
+ max_ws_df['y'] = max_ws_df['geometry'].apply(lambda geom: geom.y if geom is not None else None)
54
+
55
+ if 'x' not in max_ws_df.columns or 'y' not in max_ws_df.columns:
56
+ print("Error: 'x' or 'y' columns not found in the merged dataframe.")
57
+ print("Available columns:", max_ws_df.columns.tolist())
58
+ return
59
+
60
+ fig, ax = plt.subplots(figsize=(12, 8))
61
+ scatter = ax.scatter(max_ws_df['x'], max_ws_df['y'],
62
+ c=max_ws_df['maximum_water_surface'],
63
+ cmap='viridis', s=10)
64
+
65
+ ax.set_title('Max Water Surface per Cell')
66
+ ax.set_xlabel('X Coordinate')
67
+ ax.set_ylabel('Y Coordinate')
68
+ plt.colorbar(scatter, label='Max Water Surface (ft)')
69
+
70
+ ax.grid(True, linestyle='--', alpha=0.7)
71
+ plt.rcParams.update({'font.size': 12})
72
+ plt.tight_layout()
73
+ plt.show()
74
+
75
+ @staticmethod
76
+ @log_call
77
+ def plot_results_max_wsel_time(max_ws_df: pd.DataFrame) -> None:
78
+ """
79
+ Plots the time of the maximum water surface elevation (WSEL) per cell.
80
+
81
+ Args:
82
+ max_ws_df (pd.DataFrame): DataFrame containing merged data with coordinates
83
+ and max water surface timing information.
84
+ """
85
+ # Convert datetime strings using the renamed utility function
86
+ max_ws_df['max_wsel_time'] = pd.to_datetime(max_ws_df['maximum_water_surface_time'])
87
+
88
+ # Extract coordinates
89
+ max_ws_df['x'] = max_ws_df['geometry'].apply(lambda geom: geom.x if geom is not None else None)
90
+ max_ws_df['y'] = max_ws_df['geometry'].apply(lambda geom: geom.y if geom is not None else None)
91
+
92
+ if 'x' not in max_ws_df.columns or 'y' not in max_ws_df.columns:
93
+ raise ValueError("x and y coordinates are missing from the DataFrame. Make sure the 'geometry' column exists and contains valid coordinate data.")
94
+
95
+ fig, ax = plt.subplots(figsize=(12, 8))
96
+
97
+ min_time = max_ws_df['max_wsel_time'].min()
98
+ color_values = (max_ws_df['max_wsel_time'] - min_time).dt.total_seconds() / 3600
99
+
100
+ scatter = ax.scatter(max_ws_df['x'], max_ws_df['y'],
101
+ c=color_values, cmap='viridis', s=10)
102
+
103
+ ax.set_title('Time of Maximum Water Surface Elevation per Cell')
104
+ ax.set_xlabel('X Coordinate')
105
+ ax.set_ylabel('Y Coordinate')
106
+
107
+ cbar = plt.colorbar(scatter)
108
+ cbar.set_label('Hours since simulation start')
109
+ cbar.set_ticks(range(0, int(color_values.max()) + 1, 6))
110
+ cbar.set_ticklabels([f'{h}h' for h in range(0, int(color_values.max()) + 1, 6)])
111
+
112
+ ax.grid(True, linestyle='--', alpha=0.7)
113
+ plt.rcParams.update({'font.size': 12})
114
+ plt.tight_layout()
115
+ plt.show()
116
+
117
+ # Print timing information
118
+ print(f"\nSimulation Start Time: {min_time}")
119
+ print(f"Time Range: {color_values.max():.1f} hours")
120
+ print("\nTiming Statistics (hours since start):")
121
+ print(color_values.describe())
122
+
123
+ @staticmethod
124
+ @log_call
125
+ def plot_results_mesh_variable(variable_df: pd.DataFrame, variable_name: str, colormap: str = 'viridis', point_size: int = 10) -> None:
126
+ """
127
+ Plot any mesh variable with consistent styling.
128
+
129
+ Args:
130
+ variable_df (pd.DataFrame): DataFrame containing the variable data
131
+ variable_name (str): Name of the variable (for labels)
132
+ colormap (str): Matplotlib colormap to use. Default: 'viridis'
133
+ point_size (int): Size of the scatter points. Default: 10
134
+
135
+ Returns:
136
+ None
137
+
138
+ Raises:
139
+ ImportError: If matplotlib is not installed
140
+ ValueError: If required columns are missing from variable_df
141
+ """
142
+ try:
143
+ import matplotlib.pyplot as plt
144
+ except ImportError:
145
+ logger.error("matplotlib is required for plotting. Please install it with 'pip install matplotlib'")
146
+ raise ImportError("matplotlib is required for plotting")
147
+
148
+ # Get cell coordinates if not in variable_df
149
+ if 'geometry' not in variable_df.columns:
150
+ cell_coords = HdfMesh.mesh_cell_points(plan_hdf_path)
151
+ merged_df = pd.merge(variable_df, cell_coords, on=['mesh_name', 'cell_id'])
152
+ else:
153
+ merged_df = variable_df
154
+
155
+ # Extract coordinates, handling None values
156
+ merged_df = merged_df.dropna(subset=['geometry'])
157
+ merged_df['x'] = merged_df['geometry'].apply(lambda geom: geom.x if geom is not None else None)
158
+ merged_df['y'] = merged_df['geometry'].apply(lambda geom: geom.y if geom is not None else None)
159
+
160
+ # Drop any rows with None coordinates
161
+ merged_df = merged_df.dropna(subset=['x', 'y'])
162
+
163
+ if len(merged_df) == 0:
164
+ logger.error("No valid coordinates found for plotting")
165
+ raise ValueError("No valid coordinates found for plotting")
166
+
167
+ # Create plot
168
+ fig, ax = plt.subplots(figsize=(12, 8))
169
+ scatter = ax.scatter(merged_df['x'], merged_df['y'],
170
+ c=merged_df[variable_name],
171
+ cmap=colormap,
172
+ s=point_size)
173
+
174
+ # Customize plot
175
+ ax.set_title(f'{variable_name} per Cell')
176
+ ax.set_xlabel('X Coordinate')
177
+ ax.set_ylabel('Y Coordinate')
178
+ plt.colorbar(scatter, label=variable_name)
179
+ ax.grid(True, linestyle='--', alpha=0.7)
180
+ plt.rcParams.update({'font.size': 12})
181
+ plt.tight_layout()
182
+ plt.show()