CPP-snippets 0.0.1
A silly C++ project to use for demonstrating code integration
Loading...
Searching...
No Matches
postprocessor.py
1import os
2import numpy as np
3import matplotlib.pyplot as plt
4from matplotlib.animation import FuncAnimation
5
7 """
8 Loads multiple simulation CSV outputs and provides visualization tools.
9
10 CSV dictionary example:
11 {"phi": "phi.csv", "efield": "efield.csv", "x": "x.csv", "time": "time.csv", "dx": "dx.csv"}
12
13 Node-based fields are plotted at nodes (x.csv).
14 Cell-based fields are plotted at cell centers (dx.csv used to compute positions).
15 """
16 def __init__(self, folder="./output", csv_dict=None, cell_fields=None):
17 """
18 Parameters
19 ----------
20 folder : str
21 Relative path to folder containing the CSV files.
22 csv_dict : dict
23 Dictionary mapping attribute names -> CSV filenames.
24 Special keys:
25 - "x": spatial node positions (single column)
26 - "time": time array (single column)
27 - "dx": cell sizes (single column)
28 cell_fields : list
29 List of names that are defined on cells (not nodes).
30 """
31 self.colormap = plt.cm.viridis
32 plt.rcParams.update({'font.size':15,'lines.linewidth':2.5,'figure.autolayout':True})
33
34 self.folder = folder
35 if csv_dict is None:
36 raise ValueError("You must provide a csv_dict mapping names to CSV files.")
37 if cell_fields is None:
38 cell_fields = []
39
40 self._names = list(csv_dict.keys())
41 self._cell_fields = cell_fields
42
43 # Initialize default x, time, dx arrays
44 self.x = None
45 self.time = None
46 self.dx = None
47 self.x_cell = None
48
49 # Load CSV files
50 for name, fname in csv_dict.items():
51 path = os.path.join(folder, fname)
52 if not os.path.exists(path):
53 raise FileNotFoundError(f"CSV file not found: {path}")
54 print(f"[PostProcessor] Loading {name} from {path}")
55 data = np.loadtxt(path, skiprows=1) # first row includes metadata comment
56
57 # Flatten for 1D arrays
58 if name in ("x", "time", "dx"):
59 data = data.flatten()
60 setattr(self, name, data)
61 else:
62 setattr(self, name, data)
63
64 # Set default x and time if not provided
65 first_field = next((n for n in self._names if n not in ("x", "time", "dx")), None)
66 if first_field is None:
67 raise ValueError("No field CSVs found (excluding 'x', 'time', 'dx').")
68
69 data_field = getattr(self, first_field)
70
71 if self.x is None:
72 self.x = np.arange(data_field.shape[1])
73 if self.time is None:
74 self.time = np.arange(data_field.shape[0])
75
76 # Compute cell-centered positions if dx is provided
77 if hasattr(self, "dx") and self.dx is not None:
78 if len(self.dx) != len(self.x)-1:
79 raise ValueError("dx length must be number of nodes - 1")
80 self.x_cell = self.x[:-1] + 0.5*self.dx
81
82 # ------------------------------------------------------------
83 # Determine plotting x-axis based on node vs cell field
84 # ------------------------------------------------------------
85 def _get_x_for_field(self, name):
86 if name in self._cell_fields:
87 if self.x_cell is None:
88 raise ValueError(f"Field '{name}' is cell-based but dx/x not defined")
89 return self.x_cell
90 else:
91 return self.x
92
93 # ------------------------------------------------------------
94 # Plot a single timestep (step for cell fields)
95 # ------------------------------------------------------------
96 def plot_timestep(self, name, t, ax=None):
97 if not hasattr(self, name):
98 raise AttributeError(f"No field named '{name}' loaded.")
99 data = getattr(self, name)
100
101 if t < 0 or t >= data.shape[0]:
102 raise IndexError(f"Timestep {t} is out of range")
103
104 x_axis = self._get_x_for_field(name)
105
106 if ax is None:
107 fig, ax = plt.subplots()
108
109 if name in self._cell_fields:
110 # Plot as constant per cell using step function
111 x_step = np.zeros(len(x_axis)+1)
112 x_step[:-1] = x_axis
113 # extend last cell to keep value constant
114 last_dx = self.dx[-1] if self.dx is not None else x_axis[-1]-x_axis[-2]
115 x_step[-1] = x_axis[-1] + last_dx
116
117 y_step = np.zeros(len(x_axis)+1)
118 y_step[:-1] = data[t, :]
119 y_step[-1] = data[t, -1]
120
121 ax.step(x_step, y_step, where='post', lw=2)
122 else:
123 ax.plot(x_axis, data[t, :], lw=2)
124
125 ax.set_title(f"{name} at t = {self.time[t]:.3e}")
126 ax.set_xlabel("x-position")
127 ax.set_ylabel(name)
128 ax.grid(True)
129
130 if ax is None:
131 plt.show()
132
133 # ------------------------------------------------------------
134 # Animate a field (step for cell fields)
135 # ------------------------------------------------------------
136 def animate(self, name, step=1, interval=80):
137 if not hasattr(self, name):
138 raise AttributeError(f"No field named '{name}' loaded.")
139 data = getattr(self, name)
140
141 x_axis = self._get_x_for_field(name)
142 num_nodes = len(x_axis)
143
144 fig, ax = plt.subplots()
145 line, = ax.plot([], [], lw=2)
146
147 # Extend last cell for cell fields
148 last_dx = self.dx[-1] if (name in self._cell_fields and self.dx is not None) else (x_axis[-1]-x_axis[-2])
149 ax.set_xlim(np.min(x_axis), np.max(x_axis) + last_dx)
150 ymin, ymax = np.min(data), np.max(data)
151 ax.set_ylim(ymin, ymax)
152 ax.set_xlabel("x-position")
153 ax.set_ylabel(name)
154 ax.grid(True)
155
156 frames = range(0, data.shape[0], step)
157
158 def init():
159 line.set_data([], [])
160 return line,
161
162 def update(frame):
163 if name in self._cell_fields:
164 x_step = np.zeros(len(x_axis)+1)
165 x_step[:-1] = x_axis
166 x_step[-1] = x_axis[-1] + last_dx
167 y_step = np.zeros(len(x_axis)+1)
168 y_step[:-1] = data[frame, :]
169 y_step[-1] = data[frame, -1]
170 line.set_data(x_step, y_step)
171 else:
172 line.set_data(x_axis, data[frame])
173 ax.set_title(f"{name} at t = {self.time[frame]:.3e}")
174 return line,
175
176 anim = FuncAnimation(
177 fig, update, frames=frames,
178 init_func=init, blit=True, interval=interval
179 )
180
181 plt.show()
182 return anim
183
184 def plot_columns_over_time(self, name: str, columns=None):
185 """
186 Plot selected columns of a variable over time.
187
188 Parameters
189 ----------
190 name : str
191 Name of the variable loaded from CSV.
192 columns : list or array-like, optional
193 Indices of columns to plot. If None, all columns are plotted.
194 """
195 if not hasattr(self, name):
196 raise AttributeError(f"No field named '{name}' loaded.")
197
198 data = getattr(self, name)
199
200 if self.time is None:
201 raise ValueError("Time vector not loaded. Provide a 'time' CSV in initialization.")
202
203 n_cols = data.shape[1]
204
205 # If no specific columns given → use all
206 if columns is None:
207 columns = list(range(n_cols))
208 else:
209 # Convert to list and validate
210 columns = list(columns)
211 for c in columns:
212 if c < 0 or c >= n_cols:
213 raise IndexError(f"Column index {c} out of range (0 to {n_cols-1}).")
214
215 # Colormap for selected columns only
216 colors = self.colormap(np.linspace(0, 1, len(columns)))
217
218 plt.figure(figsize=(8, 5))
219
220 for idx, col in enumerate(columns):
221 plt.plot(self.time, data[:, col], label=f"Col {col}", color=colors[idx])
222
223 plt.xlabel("Time")
224 plt.ylabel(name)
225 plt.title(f"{name}: selected columns vs. time")
226 plt.legend()
227 plt.grid(True)
228 plt.tight_layout()
229 plt.show()
230
231 def animate_histogram(self, name: str, step=1, interval=80):
232 """
233 Create an animated histogram where each row of `data` is one timestep.
234
235 Parameters
236 ----------
237 data : np.ndarray
238 2D array of shape (timesteps, bins).
239 bin_edges : np.ndarray, optional
240 1D array specifying the histogram bin edges. If None, bins = [0, 1, ..., N].
241 interval : int
242 Delay between frames in milliseconds.
243 repeat : bool
244 Whether the animation should loop.
245 """
246 if not hasattr(self, name):
247 raise AttributeError(f"No field named '{name}' loaded.")
248 data = getattr(self, name)
249 if self.time is None:
250 raise ValueError("Time vector not loaded. Provide a 'time' CSV in initialization.")
251
252 timesteps, nbins = data.shape
253 bin_edges = np.arange(nbins + 1)
254
255 fig, ax = plt.subplots()
256 bars = ax.bar(bin_edges[:-1], data[0], width=np.diff(bin_edges), align="edge")
257
258 ax.set_xlim(bin_edges[0], bin_edges[-1])
259 ax.set_ylim(0, 1.1 * data.max())
260 ax.set_xlabel("Bin")
261 ax.set_ylabel("Counts")
262 ax.set_title("Animated Histogram")
263
264 def update(frame):
265 for b, h in zip(bars, data[frame]):
266 b.set_height(h)
267 ax.set_title(f"Timestep {frame}")
268 return bars
269
270 ani = FuncAnimation(
271 fig, update, frames=timesteps, interval=interval, blit=False
272 )
273
274 plt.show()
275 return ani
_get_x_for_field(self, name)
animate_histogram(self, str name, step=1, interval=80)
__init__(self, folder="./output", csv_dict=None, cell_fields=None)
plot_columns_over_time(self, str name, columns=None)
time
_names
folder
x
dx
float x_cell
_cell_fields
colormap