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,44 @@
1
+ from .boundary_base import BoundaryCondition
2
+ from .dirichlet import DirichletBC
3
+ from .neumann import NeumannBC
4
+ from .robin import RobinBC
5
+
6
+
7
+ BOUNDARY_REGISTRY: dict[str, type[BoundaryCondition]] = {
8
+ "dirichlet": DirichletBC,
9
+ "neumann": NeumannBC,
10
+ "robin": RobinBC,
11
+ }
12
+
13
+
14
+ def get_boundary(
15
+ name: str,
16
+ bd_func: str,
17
+ alpha: str = "0",
18
+ beta: str = "1",
19
+ use_time_derivative: bool = True,
20
+ ) -> BoundaryCondition:
21
+
22
+ key = name.lower()
23
+ if key not in BOUNDARY_REGISTRY:
24
+ raise ValueError(
25
+ f"Tipo de contorno desconhecido: '{name}'. "
26
+ f"Disponíveis: {list(BOUNDARY_REGISTRY.keys())}"
27
+ )
28
+ cls = BOUNDARY_REGISTRY[key]
29
+
30
+ if key == "dirichlet":
31
+ return cls(bd_func, use_time_derivative=use_time_derivative)
32
+ if key == "robin":
33
+ return cls(bd_func, alpha=alpha, beta=beta)
34
+ return cls(bd_func)
35
+
36
+
37
+ __all__ = [
38
+ "BoundaryCondition",
39
+ "DirichletBC",
40
+ "NeumannBC",
41
+ "RobinBC",
42
+ "BOUNDARY_REGISTRY",
43
+ "get_boundary",
44
+ ]
@@ -0,0 +1,36 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+
5
+ class BoundaryCondition(ABC):
6
+
7
+ def __init__(self, bd_func: str):
8
+ self.bd_func = bd_func
9
+
10
+ @abstractmethod
11
+ def apply(
12
+ self,
13
+ bd: str,
14
+ list_eq: List[List[str]],
15
+ n_part: List[int],
16
+ xd_var: List[str],
17
+ str_sp_vars: str = "",
18
+ ) -> List[List[str]]:
19
+
20
+ ...
21
+
22
+ @staticmethod
23
+ def _valid_sides_2d() -> List[str]:
24
+ return ["north", "south", "east", "west"]
25
+
26
+ @staticmethod
27
+ def _valid_sides_1d() -> List[str]:
28
+ return ["east", "west"]
29
+
30
+ def _check_side(self, bd: str, is_2d: bool) -> None:
31
+ valid = self._valid_sides_2d() if is_2d else self._valid_sides_1d()
32
+ if bd.lower() not in valid:
33
+ raise ValueError(
34
+ f"Contorno inválido: '{bd}'. Válidos para "
35
+ f"{'2D' if is_2d else '1D'}: {valid}"
36
+ )
@@ -0,0 +1,83 @@
1
+
2
+ from typing import List
3
+ from ...Auxs.FuncAux import repl_symbol as _repl_symbol
4
+ from .boundary_base import BoundaryCondition
5
+
6
+
7
+ class DirichletBC(BoundaryCondition):
8
+
9
+ def __init__(self, bd_func: str, use_time_derivative: bool = True):
10
+ super().__init__(bd_func)
11
+ self.use_time_derivative = use_time_derivative
12
+
13
+ def _replace_xy(self, expr: str, X: str, Y: str, str_sp_vars: str) -> str:
14
+ out = _repl_symbol(expr, str_sp_vars[0], X)
15
+ if len(str_sp_vars) == 2:
16
+ out = _repl_symbol(out, str_sp_vars[1], Y)
17
+ return out
18
+
19
+ def apply(
20
+ self,
21
+ bd: str,
22
+ list_eq: List[List[str]],
23
+ n_part: List[int],
24
+ xd_var: List[str],
25
+ str_sp_vars: str = "",
26
+ ) -> List[List[str]]:
27
+
28
+ is_2d = len(str_sp_vars) == 2
29
+ self._check_side(bd, is_2d)
30
+ bd = bd.lower()
31
+
32
+ if is_2d:
33
+ return self._apply_2d(bd, list_eq, n_part, xd_var, str_sp_vars)
34
+ return self._apply_1d(bd, list_eq, n_part, xd_var, str_sp_vars)
35
+
36
+ def _apply_2d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
37
+ Nx, Ny = n_part[0], n_part[1]
38
+ hx = f"h{xd_var[0]}_"
39
+ hy = f"h{xd_var[0]}_"
40
+ result = [[] for _ in range(len(list_eq))]
41
+
42
+ if bd == "north":
43
+ for func in range(len(list_eq)):
44
+ for i in range(Nx):
45
+ expr = self._replace_xy(self.bd_func, f"{i} * {hx}", f"{Ny-1} * {hy}", str_sp_vars)
46
+ result[func].append(expr)
47
+
48
+ elif bd == "south":
49
+ for func in range(len(list_eq)):
50
+ for i in range(Nx):
51
+ expr = self._replace_xy(self.bd_func, f"{i} * {hx}", f"0 * {hy}", str_sp_vars)
52
+ result[func].append(expr)
53
+
54
+ elif bd == "east":
55
+ for func in range(len(list_eq)):
56
+ for j in range(Ny):
57
+ expr = self._replace_xy(self.bd_func, f"{Nx-1} * {hx}", f"{j} * {hy}", str_sp_vars)
58
+ result[func].append(expr)
59
+
60
+ elif bd == "west":
61
+ for func in range(len(list_eq)):
62
+ for j in range(Ny):
63
+ expr = self._replace_xy(self.bd_func, f"0 * {hx}", f"{j} * {hy}", str_sp_vars)
64
+ result[func].append(expr)
65
+
66
+ return result
67
+
68
+ def _apply_1d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
69
+ Nx = n_part[0]
70
+ hx = f"h{xd_var[0]}_"
71
+ result = [[] for _ in range(len(list_eq))]
72
+
73
+ if bd == "west":
74
+ for func in range(len(list_eq)):
75
+ expr = self._replace_xy(self.bd_func, f"0 * {hx}", "", str_sp_vars)
76
+ result[func].append(expr)
77
+
78
+ elif bd == "east":
79
+ for func in range(len(list_eq)):
80
+ expr = self._replace_xy(self.bd_func, f"{Nx-1} * {hx}", "", str_sp_vars)
81
+ result[func].append(expr)
82
+
83
+ return result
@@ -0,0 +1,48 @@
1
+ from typing import List
2
+ from .boundary_base import BoundaryCondition
3
+
4
+ class NeumannBC(BoundaryCondition):
5
+
6
+ def __init__(self, bd_func: str):
7
+ super().__init__(bd_func)
8
+
9
+ def apply(
10
+ self,
11
+ bd: str,
12
+ list_eq: List[List[str]],
13
+ n_part: List[int],
14
+ xd_var: List[str],
15
+ str_sp_vars: str = "",
16
+ ) -> List[List[str]]:
17
+
18
+ is_2d = len(str_sp_vars) == 2
19
+ self._check_side(bd, is_2d)
20
+ bd = bd.lower()
21
+
22
+ if is_2d:
23
+ return self._apply_2d(bd, list_eq, n_part, xd_var, str_sp_vars)
24
+ return self._apply_1d(bd, list_eq, n_part, xd_var, str_sp_vars)
25
+
26
+ def _apply_2d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
27
+ Nx, Ny = n_part[0], n_part[1]
28
+ n_funcs = len(list_eq)
29
+ result = [[] for _ in range(n_funcs)]
30
+
31
+ if bd in ("west", "east"):
32
+ length = Ny
33
+ elif bd in ("south", "north"):
34
+ length = Nx
35
+ else:
36
+ raise ValueError(f"Lado invalido: {bd}")
37
+
38
+ for func in range(n_funcs):
39
+ for _ in range(length):
40
+ result[func].append("0")
41
+ return result
42
+
43
+ def _apply_1d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
44
+ n_funcs = len(list_eq)
45
+ result = [[] for _ in range(n_funcs)]
46
+ for func in range(n_funcs):
47
+ result[func].append("0")
48
+ return result
@@ -0,0 +1,30 @@
1
+ from typing import List
2
+ from .boundary_base import BoundaryCondition
3
+
4
+
5
+ class PeriodicBC(BoundaryCondition):
6
+ def apply(
7
+ self,
8
+ bd: str,
9
+ list_eq: List[List[str]],
10
+ n_part: List[int],
11
+ xd_var: List[str],
12
+ str_sp_vars: str = "",
13
+ ) -> List[List[str]]:
14
+
15
+ is_2d = len(str_sp_vars) == 2
16
+ self._check_side(bd, is_2d)
17
+ bd = bd.lower()
18
+
19
+ result = [[] for _ in range(len(list_eq))]
20
+
21
+ if not is_2d:
22
+ for func in range(len(list_eq)):
23
+ if bd == "west":
24
+ result[func].append(list_eq[func][-1])
25
+ elif bd == "east":
26
+ result[func].append(list_eq[func][0])
27
+ else:
28
+ raise NotImplementedError("Periódico 2D ainda não implementado.")
29
+
30
+ return result
@@ -0,0 +1,51 @@
1
+ from typing import List
2
+ from .boundary_base import BoundaryCondition
3
+
4
+
5
+ class RobinBC(BoundaryCondition):
6
+
7
+ def __init__(self, bd_func: str, alpha: str = "0", beta: str = "1"):
8
+ super().__init__(bd_func)
9
+ self.alpha = alpha
10
+ self.beta = beta
11
+
12
+ def apply(
13
+ self,
14
+ bd: str,
15
+ list_eq: List[List[str]],
16
+ n_part: List[int],
17
+ xd_var: List[str],
18
+ str_sp_vars: str = "",
19
+ ) -> List[List[str]]:
20
+
21
+ is_2d = len(str_sp_vars) == 2
22
+ self._check_side(bd, is_2d)
23
+ bd = bd.lower()
24
+
25
+ if is_2d:
26
+ return self._apply_2d(bd, list_eq, n_part, xd_var, str_sp_vars)
27
+ return self._apply_1d(bd, list_eq, n_part, xd_var, str_sp_vars)
28
+
29
+ def _apply_2d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
30
+ Nx, Ny = n_part[0], n_part[1]
31
+ n_funcs = len(list_eq)
32
+ result = [[] for _ in range(n_funcs)]
33
+
34
+ if bd in ("west", "east"):
35
+ length = Ny
36
+ elif bd in ("south", "north"):
37
+ length = Nx
38
+ else:
39
+ raise ValueError(f"Lado invalido: {bd}")
40
+
41
+ for func in range(n_funcs):
42
+ for _ in range(length):
43
+ result[func].append("0")
44
+ return result
45
+
46
+ def _apply_1d(self, bd, list_eq, n_part, xd_var, str_sp_vars):
47
+ n_funcs = len(list_eq)
48
+ result = [[] for _ in range(n_funcs)]
49
+ for func in range(n_funcs):
50
+ result[func].append("0")
51
+ return result
pdesolver/PDE.py ADDED
@@ -0,0 +1,29 @@
1
+ class PDE:
2
+
3
+ eq = ''
4
+ func = ''
5
+ expr_ic = ''
6
+ sp_var = []
7
+ ivar = []
8
+ ivar_boundary = []
9
+ ic = []
10
+
11
+ def __init__(self, eq, func, sp_var, ivar, ivar_boundary, expr_ic,
12
+ west_bd="Dirichlet", west_func_bd="0",
13
+ east_bd="Dirichlet", east_func_bd="0",
14
+ north_bd="Dirichlet", north_func_bd="0",
15
+ south_bd="Dirichlet", south_func_bd="0"):
16
+ self.eq = eq
17
+ self.func = func
18
+ self.expr_ic = expr_ic
19
+ self.sp_var = sp_var
20
+ self.ivar = ivar
21
+ self.ivar_boundary = ivar_boundary
22
+ self.west_bd = west_bd
23
+ self.west_func_bd = west_func_bd
24
+ self.east_bd = east_bd
25
+ self.east_func_bd = east_func_bd
26
+ self.north_bd = north_bd
27
+ self.north_func_bd = north_func_bd
28
+ self.south_bd = south_bd
29
+ self.south_func_bd = south_func_bd
pdesolver/PDES.py ADDED
@@ -0,0 +1,175 @@
1
+ from .Disc.Disc import df
2
+ from .Solvers.CN import cn
3
+ from .Solvers import RKF as SERKF45
4
+ from .Solvers.bdf2 import bdf2
5
+ from .Auxs.Visualize import visualize as _visualize
6
+ import sympy as sp
7
+ import numpy as np
8
+ import matplotlib
9
+ from .PDE import PDE
10
+ import json
11
+ from .Auxs.PDESEncoder import PDESEncoder
12
+
13
+
14
+ class PDES:
15
+
16
+ @property
17
+ def disc_n(self):
18
+ return self._disc_n
19
+
20
+ @disc_n.setter
21
+ def disc_n(self, value):
22
+ self._disc_n = value
23
+ self.ic = self._ic_calc(self.pdes)
24
+ self.disc_results = None
25
+ self.dirichlet_constraints = {}
26
+ self.neumann_constraints = {}
27
+
28
+
29
+ def __init__(self, pdes, disc_n, n_sp=1, n_temp=1):
30
+ self.pdes = pdes
31
+ self.eqs = [pde.eq for pde in pdes]
32
+ self.ivars = pdes[0].ivar
33
+ self.disc_results = None
34
+ self.dirichlet_constraints = {}
35
+ self.neumann_constraints = {}
36
+
37
+ self.funcs = [pde.func for pde in pdes]
38
+ self.sp_vars = pdes[0].sp_var
39
+ self._disc_n = disc_n
40
+ self.ic = self._ic_calc(pdes)
41
+ self.results = None
42
+
43
+ def _ic_calc(self, pdes):
44
+ all_ics = []
45
+ for pde in pdes:
46
+ expr = sp.parse_expr(pde.expr_ic)
47
+ sp_symbols = [sp.Symbol(v) for v in pde.sp_var]
48
+ grids = []
49
+ for i in range(len(pde.sp_var)):
50
+ a, b = pde.ivar_boundary[i]
51
+ grids.append(np.linspace(a, b, self.disc_n[i]))
52
+ mesh = np.meshgrid(*grids, indexing='ij')
53
+ f_ic = sp.lambdify(sp_symbols, expr, modules='numpy')
54
+ ic_values = f_ic(*mesh)
55
+ if np.isscalar(ic_values):
56
+ ic_values = np.broadcast_to(ic_values, mesh[0].shape)
57
+ all_ics.extend(ic_values.flatten().tolist())
58
+ return all_ics
59
+
60
+ def xs(self, vars):
61
+ nvars = vars.copy()
62
+ for i in range(len(nvars)):
63
+ nvars[i] = f'XX{i}'
64
+ return nvars
65
+
66
+ def discretize(self, method='central'):
67
+ flat_list, d_vars, dirichlet_constraints, neumann_constraints = df(
68
+ self, self.disc_n,
69
+ method=method,
70
+ west_bd = [pde.west_bd for pde in self.pdes],
71
+ west_func_bd = [pde.west_func_bd for pde in self.pdes],
72
+ east_bd = [pde.east_bd for pde in self.pdes],
73
+ east_func_bd = [pde.east_func_bd for pde in self.pdes],
74
+ north_bd = [pde.north_bd for pde in self.pdes],
75
+ north_func_bd = [pde.north_func_bd for pde in self.pdes],
76
+ south_bd = [pde.south_bd for pde in self.pdes],
77
+ south_func_bd = [pde.south_func_bd for pde in self.pdes],
78
+ )
79
+ self.disc_results = (flat_list, d_vars)
80
+ self.dirichlet_constraints = dirichlet_constraints
81
+ self.neumann_constraints = neumann_constraints
82
+
83
+ def solve(self, method='bdf2', tf=1.0, nt=100, tol=1e-6, verbose=False, **kwargs):
84
+ dt = tf / nt
85
+ dc = self.dirichlet_constraints
86
+ nc = getattr(self, 'neumann_constraints', {})
87
+
88
+ if method == 'bdf2':
89
+ self.results = bdf2(
90
+ self.disc_results[0], self.disc_results[1],
91
+ tf=tf, nt=nt, ic=self.ic,
92
+ n_funcs=len(self.funcs),
93
+ dirichlet_constraints=dc,
94
+ neumann_constraints=nc,
95
+ verbose=verbose,
96
+ **kwargs
97
+ )
98
+ elif method == 'CN':
99
+ self.results = cn(
100
+ self.disc_results[0], self.disc_results[1],
101
+ tf=tf, nt=nt, ic=self.ic,
102
+ n_funcs=len(self.funcs),
103
+ dirichlet_constraints=dc,
104
+ neumann_constraints=nc,
105
+ verbose=verbose,
106
+ **kwargs
107
+ )
108
+ elif method == 'RKF':
109
+ self.results = SERKF45.SERKF45_cuda(
110
+ self.disc_results[0],
111
+ ivar=self.ivars,
112
+ funcs=self.disc_results[1],
113
+ yn=self.ic,
114
+ sp_vars=self.sp_vars,
115
+ n=100,
116
+ n_funcs=len(self.funcs),
117
+ dt_init=dt,
118
+ tol=tol,
119
+ x0=0,
120
+ xn=nt * dt,
121
+ dirichlet_constraints=dc,
122
+ neumann_constraints=nc,
123
+ verbose=verbose
124
+ )
125
+ else:
126
+ raise ValueError(
127
+ f"Metodo '{method}' desconhecido. Use: 'bdf2', 'CN' ou 'RKF'."
128
+ )
129
+ return self.results
130
+
131
+ def visualize(self, mode='heatmap', func_idx=0, time_step=-1, **kwargs):
132
+ _visualize(self, mode=mode, func_idx=func_idx,
133
+ time_step=time_step, **kwargs)
134
+
135
+ def __repr__(self):
136
+ status = 'resolvido' if self.results is not None else 'nao resolvido'
137
+ return (f"PDES(funcs={self.funcs}, disc_n={self.disc_n}, "
138
+ f"sp_vars={self.sp_vars}, status='{status}')")
139
+
140
+ def save_to_json(self, filepath="pdes1.json"):
141
+ data = {
142
+ 'disc_n': list(self.disc_n),
143
+ 'pdes': [pde.__dict__ for pde in self.pdes],
144
+ 'results': self.results,
145
+ }
146
+ with open(filepath, 'w', encoding='utf-8') as f:
147
+ json.dump(data, f, cls=PDESEncoder, indent=4, ensure_ascii=False)
148
+ print(f"Objeto salvo com sucesso em: {filepath}")
149
+
150
+ @classmethod
151
+ def load_from_json(cls, filepath, pde_class=PDE):
152
+ import inspect
153
+ with open(filepath, 'r', encoding='utf-8') as f:
154
+ data = json.load(f)
155
+
156
+ init_params = set(inspect.signature(pde_class.__init__).parameters) - {'self'}
157
+
158
+ reconstructed_pdes = []
159
+ for pde_dict in data['pdes']:
160
+ kwargs = {k: v for k, v in pde_dict.items() if k in init_params}
161
+ reconstructed_pdes.append(pde_class(**kwargs))
162
+
163
+ obj = cls(pdes=reconstructed_pdes, disc_n=data['disc_n'])
164
+ obj.disc_results = None
165
+ obj.dirichlet_constraints = {}
166
+ obj.neumann_constraints = {}
167
+
168
+ if data.get('results') is not None:
169
+ raw = data['results']
170
+ try:
171
+ obj.results = np.array(raw)
172
+ except ValueError:
173
+ obj.results = np.array([np.array(r) for r in raw], dtype=object)
174
+
175
+ return obj
@@ -0,0 +1,151 @@
1
+ from tabnanny import verbose
2
+ import time
3
+ import numpy as np
4
+ import sympy as sp
5
+ from sympy.parsing.sympy_parser import parse_expr
6
+ import scipy.sparse as sp_sparse
7
+ from scipy.sparse.linalg import spsolve
8
+
9
+ from .solver_base import (
10
+ compile_equations, extract_linear_structure, detect_linearity,
11
+ eval_F, picard_step, newton_step,
12
+ make_history, save_to_history,
13
+ )
14
+
15
+
16
+ def _make_bc_lambda(expr_str: str):
17
+ t_sym, x_sym, y_sym = sp.symbols('t x y')
18
+ expr = parse_expr(expr_str)
19
+ return sp.lambdify((t_sym, x_sym, y_sym), expr, modules='numpy')
20
+
21
+
22
+ def cn(flat_list, d_vars, tf, nt, ic, n_funcs=None,
23
+ nonlinear_method='newton', tol_nl=1e-8, max_iter_nl=20,
24
+ dirichlet_constraints=None,
25
+ neumann_constraints=None,
26
+ verbose=False):
27
+ dt = tf / nt
28
+ n = len(d_vars)
29
+ u = np.array(ic, dtype=np.float64).flatten()
30
+
31
+ dirichlet_constraints = dirichlet_constraints or {}
32
+ neumann_constraints = neumann_constraints or {}
33
+
34
+ dirichlet_lambdas = {
35
+ idx: _make_bc_lambda(info['expr'])
36
+ for idx, info in dirichlet_constraints.items()
37
+ }
38
+ neumann_lambdas = {
39
+ idx: _make_bc_lambda(info['expr'])
40
+ for idx, info in neumann_constraints.items()
41
+ }
42
+
43
+ h_neumann = None
44
+ if neumann_constraints:
45
+ all_x = set(); all_y = set()
46
+ for info in dirichlet_constraints.values():
47
+ all_x.add(round(info['x'], 12))
48
+ all_y.add(round(info['y'], 12))
49
+ for info in neumann_constraints.values():
50
+ all_x.add(round(info['x'], 12))
51
+ all_y.add(round(info['y'], 12))
52
+ Nx_est = len(all_x); Ny_est = len(all_y)
53
+ if Nx_est >= 2:
54
+ h_neumann = 1.0 / (Nx_est - 1)
55
+ elif Ny_est >= 2:
56
+ h_neumann = 1.0 / (Ny_est - 1)
57
+ else:
58
+ raise RuntimeError(
59
+ "Nao foi possivel inferir o passo h da malha para Neumann."
60
+ )
61
+
62
+ def _apply_dirichlet(u, t_val):
63
+ for idx, info in dirichlet_constraints.items():
64
+ f = dirichlet_lambdas[idx]
65
+ u[idx] = float(f(t_val, info['x'], info['y']))
66
+ return u
67
+
68
+ def _apply_neumann(u, t_val):
69
+ if h_neumann is None:
70
+ return u
71
+ two_h = 2.0 * h_neumann
72
+ for idx, info in neumann_constraints.items():
73
+ f = neumann_lambdas[idx]
74
+ f_val = float(f(t_val, info['x'], info['y']))
75
+ u[idx] = (4.0 * u[info['idx_n1']]
76
+ - u[info['idx_n2']]
77
+ + two_h * f_val) / 3.0
78
+ return u
79
+
80
+ def _apply_bcs(u, t_val):
81
+ u = _apply_dirichlet(u, t_val)
82
+ u = _apply_neumann(u, t_val)
83
+ return u
84
+
85
+ u = _apply_bcs(u, 0.0)
86
+
87
+ final_list, use_groups, n_elements = make_history(n_funcs, n)
88
+
89
+ funcs = compile_equations(flat_list, d_vars)
90
+
91
+ overwrite_indices = list(dirichlet_constraints.keys()) + list(neumann_constraints.keys())
92
+ is_linear, L = detect_linearity(funcs, n,
93
+ dirichlet_indices=overwrite_indices)
94
+
95
+ I = sp_sparse.eye(n, format='csr')
96
+
97
+ if is_linear:
98
+ _, fonte_func = extract_linear_structure(funcs, n, verbose=False)
99
+ A_impl = I - (dt / 2.0) * L
100
+ A_expl = I + (dt / 2.0) * L
101
+ else:
102
+ if verbose:
103
+ print(f" [CN] EDP nao-linear detectada - usando {nonlinear_method.upper()} "
104
+ f"(tol={tol_nl:.0e}, max_iter={max_iter_nl})")
105
+ _, fonte_func = extract_linear_structure(funcs, n, verbose=False)
106
+
107
+ save_to_history(u, final_list, use_groups, n_funcs, n_elements)
108
+
109
+ t0 = time.time()
110
+ total_iters = 0
111
+
112
+ for passo in range(nt):
113
+ tempo_atual = passo * dt
114
+ tempo_proximo = (passo + 1) * dt
115
+
116
+ if is_linear:
117
+ f_n = fonte_func(tempo_atual)
118
+ f_n1 = fonte_func(tempo_proximo)
119
+ rhs = A_expl.dot(u) + (dt / 2.0) * (f_n + f_n1)
120
+ u = spsolve(A_impl, rhs)
121
+ u = _apply_bcs(u, tempo_proximo)
122
+
123
+ else:
124
+ F_n = eval_F(funcs, tempo_atual, u)
125
+ rhs_hist = u + (dt / 2.0) * F_n
126
+
127
+ if nonlinear_method == 'newton':
128
+ u, n_iter = newton_step(
129
+ funcs, u, tempo_proximo, dt, n, rhs_hist,
130
+ alpha=0.5, max_iter=max_iter_nl,
131
+ tol_nl=tol_nl, verbose=verbose
132
+ )
133
+ else:
134
+ u, n_iter = picard_step(
135
+ funcs, u, tempo_proximo, dt, n, rhs_hist,
136
+ alpha=0.5, max_iter=max_iter_nl,
137
+ tol_nl=tol_nl, verbose=verbose
138
+ )
139
+ u = _apply_bcs(u, tempo_proximo)
140
+ total_iters += n_iter
141
+
142
+ save_to_history(u, final_list, use_groups, n_funcs, n_elements)
143
+
144
+ elapsed = time.time() - t0
145
+ if verbose:
146
+ print(f" [CN] Loop de tempo: {elapsed:.3f}s", end="")
147
+ if not is_linear:
148
+ print(f" | Media iteracoes/passo: {total_iters/nt:.1f}", end="")
149
+ print()
150
+
151
+ return u, final_list