pdesolver 0.1.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.
@@ -0,0 +1,24 @@
1
+ import re
2
+
3
+ import sympy as sp
4
+ from sympy.parsing.sympy_parser import parse_expr
5
+
6
+ def symbol_references(in_list):
7
+ slist = []
8
+
9
+ for e in in_list:
10
+ globals()[e] = sp.Symbol(e)
11
+ slist.append(e)
12
+ return slist
13
+
14
+ def d_dt(expr_str: str) -> str:
15
+ t = sp.Symbol('t')
16
+ try:
17
+ e = parse_expr(expr_str, evaluate=False)
18
+ return str(sp.diff(e, t))
19
+ except Exception:
20
+ return expr_str
21
+
22
+ def repl_symbol(expr: str, sym: str, repl: str) -> str:
23
+ pattern = rf'(?<![A-Za-z0-9_]){sym}(?![A-Za-z0-9_])'
24
+ return re.sub(pattern, repl, expr)
@@ -0,0 +1,13 @@
1
+ import json
2
+ import numpy as np
3
+ import sympy as sp
4
+
5
+ class PDESEncoder(json.JSONEncoder):
6
+ def default(self, obj):
7
+ if isinstance(obj, np.ndarray):
8
+ return obj.tolist()
9
+ if isinstance(obj, sp.Basic):
10
+ return str(obj)
11
+ if hasattr(obj, '__dict__'):
12
+ return obj.__dict__
13
+ return super().default(obj)
@@ -0,0 +1,266 @@
1
+
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.animation import FuncAnimation
5
+
6
+
7
+ def visualize(pdes_obj, mode='heatmap', func_idx=0, time_step=-1, **kwargs):
8
+
9
+ if pdes_obj.results is None:
10
+ print("Erro: rode .solve() antes de visualizar.")
11
+ return
12
+
13
+ _, historico = pdes_obj.results
14
+
15
+ if not historico or not historico[func_idx]:
16
+ print("Erro: histórico vazio.")
17
+ return
18
+
19
+ if _is_1d(pdes_obj):
20
+ x = _get_x1d(pdes_obj)
21
+ if mode in ('plot1d', 'plot'):
22
+ plot1d(historico, x, pdes_obj.funcs, func_idx, time_step, **kwargs)
23
+ elif mode == 'plot1d_all':
24
+ plot1d_all(historico, x, pdes_obj.funcs, func_idx, **kwargs)
25
+ elif mode == 'heatmap1d':
26
+ heatmap1d(historico, x, pdes_obj.funcs, pdes_obj.pdes, func_idx, **kwargs)
27
+ elif mode == 'animation1d':
28
+ animate1d(historico, x, pdes_obj.funcs, func_idx, **kwargs)
29
+ else:
30
+ print(f"Modo '{mode}' não reconhecido para 1D. "
31
+ f"Use: 'plot1d', 'plot1d_all', 'heatmap1d', 'animation1d'.")
32
+ return
33
+
34
+ nx, ny = pdes_obj.disc_n
35
+ x = np.linspace(0, 1, nx)
36
+ y = np.linspace(0, 1, ny)
37
+ X, Y = np.meshgrid(x, y, indexing='ij')
38
+
39
+ if mode == 'heatmap':
40
+ plot_heatmap(historico, X, Y, pdes_obj.funcs, pdes_obj.disc_n, func_idx, time_step)
41
+ elif mode == 'animation':
42
+ animate2d(historico, X, Y, pdes_obj.funcs, pdes_obj.disc_n, func_idx, **kwargs)
43
+ elif mode == 'plot3d':
44
+ plot3d(historico, X, Y, pdes_obj.funcs, pdes_obj.disc_n, func_idx, time_step, **kwargs)
45
+ elif mode == 'animation3d':
46
+ animate3d(historico, X, Y, pdes_obj.funcs, pdes_obj.disc_n, func_idx, **kwargs)
47
+ else:
48
+ print(f"Modo '{mode}' desconhecido para 2D. "
49
+ f"Use: 'heatmap', 'animation', 'plot3d', 'animation3d'.")
50
+
51
+
52
+ def _is_1d(pdes_obj):
53
+ return len(pdes_obj.disc_n) == 1
54
+
55
+
56
+ def _get_x1d(pdes_obj):
57
+ a, b = pdes_obj.pdes[0].ivar_boundary[0]
58
+ return np.linspace(a, b, pdes_obj.disc_n[0])
59
+
60
+
61
+ def plot1d(historico, x, funcs, func_idx, time_step, **kwargs):
62
+ color = kwargs.get('color', 'steelblue')
63
+ lw = kwargs.get('lw', 2)
64
+
65
+ data = np.array(historico[func_idx][time_step])
66
+ label = time_step if time_step >= 0 else len(historico[func_idx]) + time_step
67
+
68
+ fig, ax = plt.subplots(figsize=(7, 4))
69
+ ax.plot(x, data, color=color, linewidth=lw, label=f't = passo {label}')
70
+ ax.set_xlabel('x')
71
+ ax.set_ylabel(funcs[func_idx])
72
+ ax.set_title(f"Perfil de {funcs[func_idx]} — passo {label}")
73
+ ax.legend()
74
+ ax.grid(True, alpha=0.3)
75
+ plt.tight_layout()
76
+ plt.show()
77
+
78
+
79
+ def plot1d_all(historico, x, funcs, func_idx, **kwargs):
80
+ n_profiles = kwargs.get('n_profiles', 10)
81
+ cmap = kwargs.get('cmap', 'viridis')
82
+ lw = kwargs.get('lw', 1.5)
83
+ tf_real = kwargs.get('tf', None)
84
+
85
+ n_passos = len(historico[func_idx])
86
+ indices = np.linspace(0, n_passos - 1, n_profiles, dtype=int)
87
+ cm = plt.get_cmap(cmap)
88
+ colors = [cm(i / (n_profiles - 1)) for i in range(n_profiles)]
89
+
90
+ fig, ax = plt.subplots(figsize=(8, 5))
91
+
92
+ for idx_cor, idx_passo in enumerate(indices):
93
+ data = np.array(historico[func_idx][idx_passo])
94
+ if tf_real is not None:
95
+ t_val = tf_real * idx_passo / (n_passos - 1)
96
+ lbl = f't = {t_val:.2f}'
97
+ else:
98
+ lbl = f'passo {idx_passo}'
99
+ ax.plot(x, data, color=colors[idx_cor], linewidth=lw, label=lbl)
100
+
101
+ ax.set_xlabel('x')
102
+ ax.set_ylabel(funcs[func_idx])
103
+ ax.set_title(f"Evolução de {funcs[func_idx]} ao longo do tempo")
104
+ ax.legend(fontsize=8, loc='best')
105
+ ax.grid(True, alpha=0.3)
106
+
107
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=0, vmax=1))
108
+ sm.set_array([])
109
+ cbar = plt.colorbar(sm, ax=ax, pad=0.02)
110
+ cbar.set_label('t / tf')
111
+
112
+ plt.tight_layout()
113
+ plt.show()
114
+
115
+
116
+ def heatmap1d(historico, x, funcs, pdes, func_idx, **kwargs):
117
+ cmap = kwargs.get('cmap', 'viridis')
118
+ tf_real = kwargs.get('tf', None)
119
+
120
+ matriz = np.array(historico[func_idx])
121
+ n_passos = matriz.shape[0]
122
+ t_max = tf_real if tf_real is not None else n_passos - 1
123
+ a, b = pdes[0].ivar_boundary[0]
124
+
125
+ fig, ax = plt.subplots(figsize=(8, 5))
126
+ im = ax.imshow(
127
+ matriz.T,
128
+ aspect='auto',
129
+ origin='lower',
130
+ extent=[0, t_max, a, b],
131
+ cmap=cmap
132
+ )
133
+ ax.set_xlabel('t')
134
+ ax.set_ylabel('x')
135
+ ax.set_title(f"Evolução espaciotemporal de {funcs[func_idx]}")
136
+ plt.colorbar(im, ax=ax, label=funcs[func_idx])
137
+ plt.tight_layout()
138
+ plt.show()
139
+
140
+
141
+ def animate1d(historico, x, funcs, func_idx, **kwargs):
142
+ frames_step = kwargs.get('frames_step', 1)
143
+ interval = kwargs.get('interval', 50)
144
+ color = kwargs.get('color', 'steelblue')
145
+ lw = kwargs.get('lw', 2)
146
+ tf_real = kwargs.get('tf', None)
147
+
148
+ n_passos = len(historico[func_idx])
149
+ frames = list(range(0, n_passos, frames_step))
150
+
151
+ todos = np.concatenate([historico[func_idx][f] for f in frames])
152
+ y_min, y_max = todos.min(), todos.max()
153
+ margem = (y_max - y_min) * 0.05 or 0.1
154
+
155
+ fig, ax = plt.subplots(figsize=(8, 4))
156
+ linha, = ax.plot(x, historico[func_idx][0], color=color, linewidth=lw)
157
+ ax.set_xlim(x[0], x[-1])
158
+ ax.set_ylim(y_min - margem, y_max + margem)
159
+ ax.set_xlabel('x')
160
+ ax.set_ylabel(funcs[func_idx])
161
+ ax.grid(True, alpha=0.3)
162
+ titulo = ax.set_title('')
163
+
164
+ def update(frame_idx):
165
+ passo = frames[frame_idx]
166
+ linha.set_ydata(np.array(historico[func_idx][passo]))
167
+ if tf_real is not None:
168
+ t_val = tf_real * passo / (n_passos - 1)
169
+ titulo.set_text(f"{funcs[func_idx]} — t = {t_val:.3f}")
170
+ else:
171
+ titulo.set_text(f"{funcs[func_idx]} — passo {passo}")
172
+ return linha, titulo
173
+
174
+ anim = FuncAnimation(fig, update, frames=len(frames),
175
+ interval=interval, blit=True)
176
+ plt.tight_layout()
177
+ plt.show()
178
+
179
+ def plot_heatmap(historico, X, Y, funcs, disc_n, func_idx, time_step):
180
+ data = np.array(historico[func_idx][time_step]).reshape(disc_n)
181
+ plt.figure(figsize=(7, 6))
182
+ contorno = plt.contourf(X, Y, data, levels=30, cmap='hot')
183
+ plt.title(f"Distribuição de {funcs[func_idx]} no passo {time_step}")
184
+ plt.colorbar(contorno)
185
+ plt.show()
186
+
187
+
188
+ def animate2d(historico, X, Y, funcs, disc_n, func_idx, **kwargs):
189
+ frames_step = kwargs.get('frames_step', 1)
190
+ interval = kwargs.get('interval', 50)
191
+
192
+ fig, ax = plt.subplots(figsize=(7, 6))
193
+ Z_ini = np.array(historico[func_idx][0]).reshape(disc_n)
194
+ contorno = ax.contourf(X, Y, Z_ini, levels=30, cmap='hot')
195
+ fig.colorbar(contorno, ax=ax)
196
+
197
+ def update(frame):
198
+ ax.clear()
199
+ Z = np.array(historico[func_idx][frame]).reshape(disc_n)
200
+ cont = ax.contourf(X, Y, Z, levels=30, cmap='hot')
201
+ ax.set_title(f"Evolução de {funcs[func_idx]} - Passo {frame}")
202
+ ax.set_xlabel('x')
203
+ ax.set_ylabel('y')
204
+ return cont,
205
+
206
+ frames_list = range(0, len(historico[func_idx]), frames_step)
207
+ anim = FuncAnimation(fig, update, frames=frames_list,
208
+ interval=interval, blit=False)
209
+ plt.show()
210
+
211
+
212
+ def plot3d(historico, X, Y, funcs, disc_n, func_idx, time_step, **kwargs):
213
+ cmap = kwargs.get('cmap', 'hot')
214
+ alpha = kwargs.get('alpha', 1.0)
215
+ elev = kwargs.get('elev', 30)
216
+ azim = kwargs.get('azim', -60)
217
+
218
+ data = np.array(historico[func_idx][time_step]).reshape(disc_n)
219
+ label = time_step if time_step >= 0 else len(historico[func_idx]) + time_step
220
+
221
+ fig = plt.figure(figsize=(8, 6))
222
+ ax = fig.add_subplot(111, projection='3d')
223
+ ax.plot_surface(X, Y, data, cmap=cmap, alpha=alpha)
224
+ ax.set_title(f"Superfície 3D de {funcs[func_idx]} - Passo {label}")
225
+ ax.set_xlabel('x')
226
+ ax.set_ylabel('y')
227
+ ax.set_zlabel(str(funcs[func_idx]))
228
+ ax.view_init(elev=elev, azim=azim)
229
+ plt.tight_layout()
230
+ plt.show()
231
+
232
+
233
+ def animate3d(historico, X, Y, funcs, disc_n, func_idx, **kwargs):
234
+ frames_step = kwargs.get('frames_step', 1)
235
+ interval = kwargs.get('interval', 50)
236
+ cmap = kwargs.get('cmap', 'hot')
237
+ alpha = kwargs.get('alpha', 1.0)
238
+ elev = kwargs.get('elev', 30)
239
+ azim = kwargs.get('azim', -60)
240
+
241
+ todos_frames = [
242
+ np.array(historico[func_idx][f]).reshape(disc_n)
243
+ for f in range(0, len(historico[func_idx]), frames_step)
244
+ ]
245
+ z_min = min(Z.min() for Z in todos_frames)
246
+ z_max = max(Z.max() for Z in todos_frames)
247
+
248
+ fig = plt.figure(figsize=(8, 6))
249
+ ax = fig.add_subplot(111, projection='3d')
250
+
251
+ def update(frame):
252
+ ax.clear()
253
+ Z = todos_frames[frame]
254
+ ax.plot_surface(X, Y, Z, cmap=cmap, alpha=alpha)
255
+ ax.set_title(
256
+ f"Evolução 3D de {funcs[func_idx]} - Passo {frame * frames_step}"
257
+ )
258
+ ax.set_xlabel('x')
259
+ ax.set_ylabel('y')
260
+ ax.set_zlabel(str(funcs[func_idx]))
261
+ ax.set_zlim(z_min, z_max)
262
+ ax.view_init(elev=elev, azim=azim)
263
+
264
+ anim = FuncAnimation(fig, update, frames=len(todos_frames),
265
+ interval=interval, blit=False)
266
+ plt.show()
@@ -0,0 +1,2 @@
1
+ from .FuncAux import symbol_references, repl_symbol, d_dt
2
+ from .Visualize import visualize
pdesolver/Disc/Disc.py ADDED
@@ -0,0 +1,372 @@
1
+ from typing import List, Tuple
2
+ import re
3
+ from ..Auxs.FuncAux import repl_symbol as _repl_symbol
4
+ from .boundaries import get_boundary
5
+
6
+ def _build_discretized_eqs(
7
+ eqrs: List[str],
8
+ xd_var: List[str],
9
+ str_sp_vars: str,
10
+ method: str,
11
+ ) -> List[str]:
12
+
13
+ SCHEMES = {
14
+ "forward": {
15
+ "d2x": "({v}_i+1_j - 2*{v}_ii_j + {v}_i-1_j)/ h{v}_ ** 2",
16
+ "d1x": "({v}_i+1_j - {v}_ii_j)/ h{v}_",
17
+ "d2y": "({v}_ii_j+1 - 2*{v}_ii_j + {v}_ii_j-1)/ h{v}_ ** 2",
18
+ "d1y": "({v}_ii_j+1 - {v}_ii_j)/ h{v}_",
19
+ },
20
+ "central": {
21
+ "d2x": "({v}_i+1_j - 2*{v}_ii_j + {v}_i-1_j)/ h{v}_ ** 2",
22
+ "d1x": "({v}_i+1_j - {v}_i-1_j)/(2* h{v}_)",
23
+ "d2y": "({v}_ii_j+1 - 2*{v}_ii_j + {v}_ii_j-1)/ h{v}_ ** 2",
24
+ "d1y": "({v}_ii_j+1 - {v}_ii_j-1)/(2* h{v}_)",
25
+ },
26
+ "backward": {
27
+ "d2x": "({v}_i+1_j - 2*{v}_ii_j + {v}_i-1_j)/ h{v}_ ** 2",
28
+ "d1x": "({v}_ii_j - {v}_i-1_j)/ h{v}_",
29
+ "d2y": "({v}_ii_j+1 - 2*{v}_ii_j + {v}_ii_j-1)/ h{v}_ ** 2",
30
+ "d1y": "({v}_ii_j - {v}_ii_j-1)/ h{v}_",
31
+ },
32
+ }
33
+
34
+ if method not in SCHEMES:
35
+ raise ValueError(
36
+ f"Método inválido: '{method}'. Use 'forward', 'central' ou 'backward'."
37
+ )
38
+
39
+ s = SCHEMES[method]
40
+
41
+ for j in range(len(eqrs)):
42
+ for k, sp_var in enumerate(str_sp_vars):
43
+ for v in xd_var:
44
+ if k == 0:
45
+ eqrs[j] = eqrs[j].replace(f"d2{v}{str_sp_vars}/d{sp_var}2", s["d2x"].format(v=v))
46
+ eqrs[j] = eqrs[j].replace(f"d{v}{str_sp_vars}/d{sp_var}", s["d1x"].format(v=v))
47
+ elif k == 1:
48
+ eqrs[j] = eqrs[j].replace(f"d2{v}{str_sp_vars}/d{sp_var}2", s["d2y"].format(v=v))
49
+ eqrs[j] = eqrs[j].replace(f"d{v}{str_sp_vars}/d{sp_var}", s["d1y"].format(v=v))
50
+
51
+ for v in xd_var:
52
+ eqrs[j] = eqrs[j].replace(f"{v}{str_sp_vars}", f"{v}_ii_j")
53
+
54
+ for j in range(len(eqrs)):
55
+ eqrs[j] = _repl_symbol(eqrs[j], str_sp_vars[0], f"ii * h{xd_var[0]}_")
56
+ if len(str_sp_vars) == 2:
57
+ eqrs[j] = _repl_symbol(eqrs[j], str_sp_vars[1], f"j * h{xd_var[0]}_")
58
+
59
+ return eqrs
60
+
61
+
62
+ def _expand_indices(
63
+ eqrs: List[str],
64
+ n_part: List[int],
65
+ str_sp_vars: str,
66
+ ) -> List[List[str]]:
67
+
68
+ partial = []
69
+ for eq in eqrs:
70
+ row = [
71
+ eq
72
+ .replace("i+1", str(i + 1))
73
+ .replace("i-1", str(i - 1))
74
+ .replace("i-2", str(i - 2))
75
+ .replace("i+2", str(i + 2))
76
+ .replace("ii", str(i))
77
+ for i in range(1, n_part[0] - 1)
78
+ ]
79
+ partial.append(row)
80
+
81
+ list_eq: List[List[str]] = [[] for _ in partial]
82
+
83
+ if len(str_sp_vars) == 2:
84
+ for j_eq, row in enumerate(partial):
85
+ for eq_i in row:
86
+ for k in range(1, n_part[1] - 1):
87
+ list_eq[j_eq].append(
88
+ eq_i
89
+ .replace("j+1", str(k + 1))
90
+ .replace("j-1", str(k - 1))
91
+ .replace("j-2", str(k - 2))
92
+ .replace("j+2", str(k + 2))
93
+ .replace("j", str(k))
94
+ )
95
+ else:
96
+ for j_eq, row in enumerate(partial):
97
+ for eq_i in row:
98
+ list_eq[j_eq].append(eq_i.replace("j", "0"))
99
+
100
+ return list_eq
101
+
102
+
103
+ def _build_position_labels(n_part, str_sp_vars, n_funcs):
104
+ positions = []
105
+
106
+ if len(str_sp_vars) == 2:
107
+ for func in range(n_funcs):
108
+ aux = []
109
+ for i in range(n_part[0]):
110
+ for j in range(n_part[1]):
111
+ if i == 0: aux.append(f"W{func}_{i}_{j}")
112
+ elif i == n_part[0] - 1: aux.append(f"E{func}_{i}_{j}")
113
+ elif j == 0: aux.append(f"S{func}_{i}_{j}")
114
+ elif j == n_part[1] - 1: aux.append(f"N{func}_{i}_{j}")
115
+ else: aux.append(f"Ce{func}_{i}_{j}")
116
+ positions.append(aux)
117
+ else:
118
+ positions = [[""] * n_part[0] for _ in range(n_funcs)]
119
+
120
+ return positions
121
+
122
+ def df(
123
+ pdes,
124
+ n_part: List[int],
125
+ west_bd=None,
126
+ method: str = "forward",
127
+ north_bd=None,
128
+ south_bd=None,
129
+ east_bd=None,
130
+ north_func_bd=None,
131
+ south_func_bd=None,
132
+ west_func_bd=None,
133
+ east_func_bd=None,
134
+ north_alpha_bd: str = "0",
135
+ south_alpha_bd: str = "0",
136
+ east_alpha_bd: str = "0",
137
+ north_beta_bd: str = "1",
138
+ south_beta_bd: str = "1",
139
+ east_beta_bd: str = "1",
140
+ ) -> Tuple[List[str], List[str], dict, dict]:
141
+
142
+ n_funcs_total = len(pdes.funcs)
143
+
144
+ def _as_list(val, default="neumann"):
145
+ if val is None:
146
+ return [default] * n_funcs_total
147
+ if isinstance(val, list):
148
+ return val
149
+ return [val] * n_funcs_total
150
+
151
+ west_bd = _as_list(west_bd, "neumann")
152
+ east_bd = _as_list(east_bd, "neumann")
153
+ north_bd = _as_list(north_bd, "neumann")
154
+ south_bd = _as_list(south_bd, "neumann")
155
+ west_func_bd = _as_list(west_func_bd, "0")
156
+ east_func_bd = _as_list(east_func_bd, "0")
157
+ north_func_bd = _as_list(north_func_bd, "0")
158
+ south_func_bd = _as_list(south_func_bd, "0")
159
+
160
+ xd_var = pdes.xs(pdes.funcs)
161
+ eqrs = [eq.split("=")[1] for eq in pdes.eqs]
162
+ str_sp_vars = "".join(pdes.sp_vars)
163
+
164
+ for j in range(len(eqrs)):
165
+ for i, func in enumerate(pdes.funcs):
166
+ eqrs[j] = eqrs[j].replace(str(func), f"{xd_var[i]}{str_sp_vars}")
167
+
168
+
169
+ eqrs = _build_discretized_eqs(eqrs, xd_var, str_sp_vars, method)
170
+
171
+ list_eq = _expand_indices(eqrs, n_part, str_sp_vars)
172
+
173
+ n_funcs = len(pdes.funcs)
174
+
175
+ if len(str_sp_vars) == 2:
176
+ list_south = [[] for _ in range(n_funcs)]
177
+ list_north = [[] for _ in range(n_funcs)]
178
+ list_west = [[] for _ in range(n_funcs)]
179
+ list_east = [[] for _ in range(n_funcs)]
180
+
181
+ for func in range(n_funcs):
182
+ s_bc = get_boundary(south_bd[func], south_func_bd[func], south_alpha_bd, south_beta_bd)
183
+ list_south[func] = s_bc.apply("south", list_eq, n_part, xd_var, str_sp_vars)[func]
184
+
185
+ n_bc = get_boundary(north_bd[func], north_func_bd[func], north_alpha_bd, north_beta_bd)
186
+ list_north[func] = n_bc.apply("north", list_eq, n_part, xd_var, str_sp_vars)[func]
187
+
188
+ w_bc = get_boundary(west_bd[func], west_func_bd[func])
189
+ list_west[func] = w_bc.apply("west", list_eq, n_part, xd_var, str_sp_vars)[func] \
190
+ if west_bd[func].lower() in ("dirichlet", "neumann", "robin") else []
191
+
192
+ e_bc = get_boundary(east_bd[func], east_func_bd[func], east_alpha_bd, east_beta_bd)
193
+ east_col = e_bc.apply("east", list_eq, n_part, xd_var, str_sp_vars)[func]
194
+ list_east[func] = [list_south[func][-1]] + east_col + [list_north[func][-1]]
195
+
196
+ list_positions = _build_position_labels(n_part, str_sp_vars, n_funcs)
197
+
198
+ if len(str_sp_vars) == 1:
199
+ for func in range(n_funcs):
200
+ C = 0
201
+ for i in range(n_part[0]):
202
+ if i == 0:
203
+ bc = get_boundary(west_bd[func], west_func_bd[func])
204
+ list_positions[func][i] = bc.apply(
205
+ "west", list_eq, n_part, xd_var, str_sp_vars
206
+ )[func][0]
207
+ elif i == n_part[0] - 1:
208
+ bc = get_boundary(east_bd[func], east_func_bd[func], east_alpha_bd, east_beta_bd)
209
+ list_positions[func][i] = bc.apply(
210
+ "east", list_eq, n_part, xd_var, str_sp_vars
211
+ )[func][0]
212
+ else:
213
+ list_positions[func][i] = list_eq[func][C]
214
+ C += 1
215
+
216
+ elif len(str_sp_vars) == 2:
217
+ for func in range(n_funcs):
218
+ C = 0
219
+ for idx in range(len(list_positions[func])):
220
+ label = list_positions[func][idx]
221
+ if "S" in label: list_positions[func][idx] = list_south[func].pop(0) if list_south[func] else label
222
+ elif "N" in label: list_positions[func][idx] = list_north[func].pop(0) if list_north[func] else label
223
+ elif "E" in label: list_positions[func][idx] = list_east[func].pop(0) if list_east[func] else label
224
+ elif "W" in label: list_positions[func][idx] = list_west[func].pop(0) if list_west[func] else label
225
+ elif "C" in label:
226
+ list_positions[func][idx] = list_eq[func][C]
227
+ C += 1
228
+
229
+ d_vars: List[str] = []
230
+ if len(str_sp_vars) == 2:
231
+ for func in range(n_funcs):
232
+ for i in range(n_part[0]):
233
+ for j in range(n_part[1]):
234
+ name = f"XX{func}_{i}_{j}"
235
+ if name not in d_vars:
236
+ d_vars.append(name)
237
+ else:
238
+ for func in range(n_funcs):
239
+ for i in range(n_part[0]):
240
+ name = f"XX{func}_{i}_0"
241
+ if name not in d_vars:
242
+ d_vars.append(name)
243
+
244
+ flat_list_positions: List[str] = []
245
+ for L in list_positions:
246
+ flat_list_positions.extend(L)
247
+
248
+ hx_val = str(1.0 / (n_part[0] - 1))
249
+ for i in range(len(flat_list_positions)):
250
+ for func_name in xd_var:
251
+ flat_list_positions[i] = flat_list_positions[i].replace(
252
+ f"h{func_name}_", hx_val
253
+ )
254
+
255
+ _h_pattern = re.compile(r'\bh[A-Za-z0-9]+_\b')
256
+ for i in range(len(flat_list_positions)):
257
+ flat_list_positions[i] = _h_pattern.sub(hx_val, flat_list_positions[i])
258
+
259
+ dirichlet_constraints: dict = {}
260
+ neumann_constraints: dict = {}
261
+ hx = 1.0 / (n_part[0] - 1)
262
+ hy = 1.0 / (n_part[1] - 1) if len(str_sp_vars) == 2 else 1.0
263
+
264
+ if len(str_sp_vars) == 2:
265
+ sides = {
266
+ 'west': west_bd,
267
+ 'east': east_bd,
268
+ 'north': north_bd,
269
+ 'south': south_bd,
270
+ }
271
+ func_exprs = {
272
+ 'west': west_func_bd,
273
+ 'east': east_func_bd,
274
+ 'north': north_func_bd,
275
+ 'south': south_func_bd,
276
+ }
277
+ Nx, Ny = n_part[0], n_part[1]
278
+
279
+ any_neumann = any(
280
+ sides[s][f].lower() == 'neumann'
281
+ for s in sides for f in range(n_funcs)
282
+ )
283
+ if any_neumann and (Nx < 3 or Ny < 3):
284
+ raise ValueError(
285
+ f"Neumann com discretizacao one-sided de 2a ordem exige "
286
+ f"min(Nx, Ny) >= 3. Recebido: Nx={Nx}, Ny={Ny}."
287
+ )
288
+
289
+ def _idx(func, i, j):
290
+ return func * Nx * Ny + i * Ny + j
291
+
292
+ for func in range(n_funcs):
293
+ for i in range(Nx):
294
+ for j in range(Ny):
295
+ idx = _idx(func, i, j)
296
+ side = None
297
+ if i == 0: side = 'west'
298
+ elif i == Nx - 1: side = 'east'
299
+ elif j == 0: side = 'south'
300
+ elif j == Ny - 1: side = 'north'
301
+ if side is None:
302
+ continue
303
+ bc_kind = sides[side][func].lower()
304
+
305
+ if bc_kind == 'dirichlet':
306
+ dirichlet_constraints[idx] = {
307
+ 'expr': func_exprs[side][func],
308
+ 'x': i * hx,
309
+ 'y': j * hy,
310
+ }
311
+ elif bc_kind == 'neumann':
312
+ if side == 'south':
313
+ n1 = _idx(func, i, 1)
314
+ n2 = _idx(func, i, 2)
315
+ elif side == 'north':
316
+ n1 = _idx(func, i, Ny - 2)
317
+ n2 = _idx(func, i, Ny - 3)
318
+ elif side == 'west':
319
+ n1 = _idx(func, 1, j)
320
+ n2 = _idx(func, 2, j)
321
+ else:
322
+ n1 = _idx(func, Nx - 2, j)
323
+ n2 = _idx(func, Nx - 3, j)
324
+
325
+ neumann_constraints[idx] = {
326
+ 'expr': func_exprs[side][func],
327
+ 'x': i * hx,
328
+ 'y': j * hy,
329
+ 'side': side,
330
+ 'idx_n1': n1,
331
+ 'idx_n2': n2,
332
+ }
333
+ else:
334
+ Nx = n_part[0]
335
+ for func in range(n_funcs):
336
+ offset = func * Nx
337
+ if west_bd[func].lower() == 'dirichlet':
338
+ dirichlet_constraints[offset] = {
339
+ 'expr': west_func_bd[func],
340
+ 'x': 0.0,
341
+ 'y': 0.0,
342
+ }
343
+ elif west_bd[func].lower() == 'neumann':
344
+ if Nx < 3:
345
+ raise ValueError("Neumann 1D exige Nx >= 3.")
346
+ neumann_constraints[offset] = {
347
+ 'expr': west_func_bd[func],
348
+ 'x': 0.0,
349
+ 'y': 0.0,
350
+ 'side': 'west',
351
+ 'idx_n1': offset + 1,
352
+ 'idx_n2': offset + 2,
353
+ }
354
+ if east_bd[func].lower() == 'dirichlet':
355
+ dirichlet_constraints[offset + Nx - 1] = {
356
+ 'expr': east_func_bd[func],
357
+ 'x': (Nx - 1) * hx,
358
+ 'y': 0.0,
359
+ }
360
+ elif east_bd[func].lower() == 'neumann':
361
+ if Nx < 3:
362
+ raise ValueError("Neumann 1D exige Nx >= 3.")
363
+ neumann_constraints[offset + Nx - 1] = {
364
+ 'expr': east_func_bd[func],
365
+ 'x': (Nx - 1) * hx,
366
+ 'y': 0.0,
367
+ 'side': 'east',
368
+ 'idx_n1': offset + Nx - 2,
369
+ 'idx_n2': offset + Nx - 3,
370
+ }
371
+
372
+ return flat_list_positions, d_vars, dirichlet_constraints, neumann_constraints
@@ -0,0 +1 @@
1
+ from .Disc import df