{ "cells": [ { "cell_type": "markdown", "id": "a0530418", "metadata": {}, "source": [ "# Transient transport: UFL spatial forms and explicit backward Euler\n", "\n", "Write the mass, conservative transport, local interface and global balance\n", "before choosing assembly or solvers. Time stepping updates only source loads\n", "while the generic offline system owns unchanged operator factors.\n", "\n", "This notebook has no ready-made transport solver. The time loop follows the\n", "backward-Euler mathematical equations. Run with the locked `introduction`\n", "profile; the actual native UFL path always executes.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "companion-bootstrap", "metadata": { "tags": [ "pymhm-companion-bootstrap" ] }, "outputs": [], "source": [ "from pathlib import Path\n", "import os\n", "import sys\n", "from pymhm.io.workspace import workspace_from_archive\n", "\n", "# Download checksum-verified support files; extraction does not execute them.\n", "COMPANION_URL = \"https://ipes-lncc.github.io/pymhm/downloads/ead821d4151bfa5b93a11665e2c8e7464c609a8e26b74e1b6280ab684ad5aad2/transient_variational-companion.zip\"\n", "COMPANION_SHA256 = \"ead821d4151bfa5b93a11665e2c8e7464c609a8e26b74e1b6280ab684ad5aad2\"\n", "WORKSPACE = Path(\n", " os.environ.get(\"PYMHM_WORKSPACE\", Path.cwd() / \".pymhm-companions\" / COMPANION_SHA256)\n", ")\n", "ROOT = workspace_from_archive(COMPANION_URL, sha256=COMPANION_SHA256, directory=WORKSPACE)\n", "os.environ[\"PYMHM_WORKSPACE\"] = str(ROOT)\n", "if str(ROOT) not in sys.path:\n", " sys.path.insert(0, str(ROOT))\n", "\n", "# Prepare only this notebook's declared inputs, without executing its equations.\n", "from scripts.notebook_reproduction import notebook_workspace\n", "\n", "ROOT = notebook_workspace(\"transport/transient_variational.ipynb\", directory=ROOT)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "c7c51596", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "import os\n", "import sys\n", "ROOT = Path(os.environ.get(\"PYMHM_WORKSPACE\", Path.cwd())).resolve()\n", "if not (ROOT / \"examples\").is_dir():\n", " ROOT = next((p for p in ROOT.parents if (p / \"examples\").is_dir()), ROOT)\n", "if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT))\n", "OUTPUT = ROOT / \"build\" / \"native-teaching\"\n", "OUTPUT.mkdir(parents=True, exist_ok=True)\n", "print(\"Workspace:\", ROOT)\n" ] }, { "cell_type": "markdown", "id": "316b1bc3", "metadata": {}, "source": [ "## 1. Manufactured concentration and physical flux\n", "\n", "Use capacity $\\rho=1$, diffusion $K=1$, reaction $\\sigma=1/2$ and constant\n", "transport velocity $\\beta=(1/4,-1/2)$. On the unit square,\n", "\n", "$$\n", "\\begin{aligned}\n", "\\partial_t c+\\nabla\\cdot(-\\nabla c+\\beta c)+\\tfrac12 c&=f,\\\\\n", "c(x,y,t)&=e^{-t}\\sin(\\pi x)\\sin(\\pi y),\\\\\n", "f(x,y,t)&=e^{-t}[(2\\pi^2-\\tfrac12)\\phi+\\beta\\cdot\\nabla\\phi],\\\\\n", "\\phi(x,y)&=\\sin(\\pi x)\\sin(\\pi y).\n", "\\end{aligned}\n", "$$\n", "\n", "The exterior concentration is zero. The **physical transport flux** is\n", "$j=-\\nabla c+\\beta c$; the local half-advection multiplier is\n", "$\\lambda_K=(-\\nabla c+\\beta c/2)\\cdot n_K$.\n", "Initial values are independently projected in the local capacity-weighted L2\n", "mass, rather than assigned by a discrete manufactured operator.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "9a0e8a1e", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import ufl\n", "from pymhm import (\n", " TriangleMesh,FaceSpace,SkeletonSpace,MeshHierarchy,LocalContext,\n", " Equation,columns,compile_form,bind_interface,bind_problem,assemble,\n", ")\n", "from pymhm.core.online import OfflineMultiscaleSystem\n", "from pymhm.linalg.linear import factorize,solve_linear\n", "from pymhm.fem.scalar.operators import triangle_quadrature\n", "\n", "def exact_concentration(points,time):\n", " \"\"\"Return independently manufactured physical concentration.\"\"\"\n", " return np.exp(-time)*np.sin(np.pi*points[:,0])*np.sin(np.pi*points[:,1])\n", "\n", "macro=TriangleMesh.unit_square(2)\n", "local_refinement=16\n", "skeleton=SkeletonSpace(macro,tuple(FaceSpace.uniform(1,local_refinement // 2) for _ in macro.faces))\n", "hierarchy=MeshHierarchy(macro,tuple(macro.submesh(i,local_refinement) for i in range(len(macro.cells))))\n", "interface=bind_interface(skeleton,convention=\"normal\")\n" ] }, { "cell_type": "markdown", "id": "479cb5bf", "metadata": {}, "source": [ "## 2. Declare the backward-Euler local equations\n", "\n", "For each time increment $\\Delta t$, write\n", "\n", "$$\n", "\\begin{aligned}\n", "\\tfrac1{\\Delta t}(c^{n+1},v)_K+a_K(c^{n+1},v)\n", " +\\langle\\lambda_K^{n+1},v\\rangle&=(f^{n+1},v)_K\n", " +\\tfrac1{\\Delta t}(c^n,v)_K,\\\\\n", "a_K(c,v)&=(\\nabla c,\\nabla v)_K+\\tfrac12(c,v)_K\\\\\n", "&\\quad+\\tfrac12[(\\beta\\cdot\\nabla c,v)_K-(c,\\beta\\cdot\\nabla v)_K].\n", "\\end{aligned}\n", "$$\n", "\n", "The source and previous-state mass use the same Galerkin test. A SUPG variant\n", "would require consistent Petrov terms in **both** source and old-state mass.\n", "The positive mass removes the constant nullspace, so the constant below is a\n", "retained coarse mode, not `kernel`. The piecewise P1 trace uses half as many face segments as the fine local P2 mesh.\n", "This enriched local trace lifting avoids redundant multipliers; temporal errors\n", "must still be measured above its fixed spatial approximation floor.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "acf2d28c", "metadata": {}, "outputs": [], "source": [ "def local_time_equations(local: LocalContext,dt: float):\n", " \"\"\"Write Q-independent triangular P2 mass, skew transport and interface forms.\"\"\"\n", " binding=local.native_space(degree=2)\n", " V=binding.space\n", " c,v=ufl.TrialFunction(V),ufl.TestFunction(V)\n", " x=ufl.SpatialCoordinate(binding.mesh)\n", " beta=ufl.as_vector((0.25,-0.5))\n", " phi=ufl.sin(np.pi*x[0])*ufl.sin(np.pi*x[1])\n", " grad_phi=np.pi*ufl.as_vector((ufl.cos(np.pi*x[0])*ufl.sin(np.pi*x[1]),\n", " ufl.sin(np.pi*x[0])*ufl.cos(np.pi*x[1])))\n", " force_profile=(2*np.pi**2-0.5)*phi+ufl.dot(beta,grad_phi)\n", " dx=ufl.Measure(\"dx\",domain=binding.mesh,metadata={\"quadrature_degree\":12})\n", " mass=compile_form(c*v*dx)\n", " initial_load=compile_form(phi*v*dx)\n", " initial=solve_linear(mass,initial_load)\n", " source_profile=compile_form(force_profile*v*dx)\n", " a=((1/dt+0.5)*c*v+ufl.inner(ufl.grad(c),ufl.grad(v))\n", " +0.5*(ufl.dot(beta,ufl.grad(c))*v-c*ufl.dot(beta,ufl.grad(v))))*dx\n", " L=np.exp(-dt)*source_profile+mass@initial/dt\n", " b=local.trace_pairings(lambda phi,ds:phi*v*ds)\n", " balance=local.trace_pairings(lambda phi,ds:-phi*c*ds,axis=\"rows\")\n", " local.field(\"concentration\",binding)\n", " return local.equations(\n", " a=a,L=L,b=b,c=balance,\n", " coarse_basis=np.ones((binding.size,1)),moments=columns(v*dx),\n", " metadata={\"mass\":mass,\"initial\":initial,\"source_profile\":source_profile},\n", " )\n" ] }, { "cell_type": "markdown", "id": "52ecf4a4", "metadata": {}, "source": [ "## 3. Global equation, assembly and unchanged operator reuse\n", "\n", "Zero exterior concentration means the additional global load is zero. Local\n", "`c` pairings impose continuity and those boundary moments. No pressure-style\n", "gauge is required for this scalar time-step operator.\n", "\n", "A fixed $\\Delta t$ permits one local/global factor preparation. Changing the\n", "step size changes the operator. The time loop writes the source update and\n", "calls the existing numerical owners; it implements no alternate elimination or\n", "factorization algorithm.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "dc8b9a6e", "metadata": {}, "outputs": [], "source": [ "from functools import partial\n", "\n", "def trajectory(dt,final_time=1.0):\n", " \"\"\"Advance the explicitly declared backward-Euler equations at a fixed step.\"\"\"\n", " steps=int(round(final_time/dt))\n", " if not np.isclose(steps*dt,final_time): raise ValueError(\"dt must divide final time\")\n", " problem=bind_problem(hierarchy,interface,partial(local_time_equations,dt=dt),\n", " global_equation=Equation(0,0),retained=1)\n", " system=assemble(problem)\n", " records=system.local_metadata\n", " previous=tuple(record[\"initial\"] for record in records)\n", " maximum_residual=0.0\n", " with OfflineMultiscaleSystem(system) as prepared, factorize(system.matrix) as global_factor:\n", " for step in range(1,steps+1):\n", " time=step*dt\n", " loads=tuple(np.exp(-time)*record[\"source_profile\"]+record[\"mass\"]@old/dt\n", " for record,old in zip(records,previous,strict=True))\n", " updated=prepared.with_loads(loads)\n", " solution=updated.solve(factorization=global_factor)\n", " maximum_residual=max(maximum_residual,float(solution.raw_residual))\n", " previous=solution.fields\n", " return solution,maximum_residual\n", "\n", "solution,maximum_residual=trajectory(0.05)\n", "concentration=solution.field(\"concentration\")\n", "print({\"maximum_original_equation_residual\":maximum_residual,\"steps\":20,\"operator_builds\":1})\n" ] }, { "cell_type": "markdown", "id": "68f62c9d", "metadata": {}, "source": [ "## 4. Integrate the physical concentration error\n", "\n", "Integrate independently on each fine triangle, with physical area weights and\n", "polynomial-exact quadrature sufficient for the represented field. Keep the\n", "same spatial spaces in the time-refinement experiment. The analytical solution\n", "is available, so physical field errors have no numerical-reference ambiguity.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3db98e33", "metadata": {}, "outputs": [], "source": [ "def concentration_error(solution,final_time=1.0,order=8):\n", " \"\"\"Integrate the concentration L2 error independently on every fine cell.\"\"\"\n", " bary,weights=triangle_quadrature(order)\n", " squared=0.0\n", " for field in solution.field(\"concentration\"):\n", " fine=field.mesh\n", " points=np.einsum(\"qi,tia->tqa\",bary,fine.points[fine.cells])\n", " error=field.evaluate(points.reshape(-1,2)).reshape(points.shape[:2])-exact_concentration(points.reshape(-1,2),final_time).reshape(points.shape[:2])\n", " squared+=float(np.sum(fine.areas[:,None]*weights*error**2))\n", " return float(np.sqrt(squared))\n", "\n", "print(\"Concentration L2 error:\",concentration_error(solution))\n" ] }, { "cell_type": "markdown", "id": "28397e53", "metadata": {}, "source": [ "## 5. Verify the independent conforming Galerkin baseline\n", "\n", "Use the same capacity, diffusion, conservative advection, reaction, source and\n", "boundary data on globally conforming P2 meshes. Each reference uses backward\n", "Euler with the smallest tutorial time step. Three independent mesh resolutions\n", "measure the reference's own spatial refinement difference; this remains a\n", "numerical baseline, while the analytical concentration supplies the exact norm.\n", "The fine reference is used for comparison and is not the MHM macro mesh.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a06a506d", "metadata": {}, "outputs": [], "source": [ "import basix\n", "import basix.ufl\n", "from pymhm import MultiscaleProblem,solve\n", "from pymhm.backends.spaces import bind_space\n", "from pymhm.core.multiscale import with_global_load\n", "from pymhm.postprocessing.fields import DiscreteField,FieldDefinition\n", "\n", "def classical_trajectory(n,dt=0.00625,final_time=1.0):\n", " \"\"\"Advance an independently assembled globally conforming P2 Galerkin equation.\"\"\"\n", " fine=TriangleMesh.unit_square(n)\n", " element=basix.ufl.element(\"Lagrange\",\"triangle\",2,lagrange_variant=basix.LagrangeVariant.equispaced)\n", " binding=bind_space(fine,element)\n", " try:\n", " V=binding.space\n", " c,v=ufl.TrialFunction(V),ufl.TestFunction(V)\n", " x=ufl.SpatialCoordinate(binding.mesh)\n", " beta=ufl.as_vector((0.25,-0.5))\n", " phi=ufl.sin(np.pi*x[0])*ufl.sin(np.pi*x[1])\n", " grad_phi=np.pi*ufl.as_vector((ufl.cos(np.pi*x[0])*ufl.sin(np.pi*x[1]),\n", " ufl.sin(np.pi*x[0])*ufl.cos(np.pi*x[1])))\n", " source=(2*np.pi**2-0.5)*phi+ufl.dot(beta,grad_phi)\n", " dx=ufl.Measure(\"dx\",domain=binding.mesh,metadata={\"quadrature_degree\":12})\n", " mass=compile_form(c*v*dx)\n", " initial_load=compile_form(phi*v*dx)\n", " source_load=compile_form(source*v*dx)\n", " a=((1/dt+0.5)*c*v+ufl.inner(ufl.grad(c),ufl.grad(v))\n", " +0.5*(ufl.dot(beta,ufl.grad(c))*v-c*ufl.dot(beta,ufl.grad(v))))*dx\n", " coordinates=V.tabulate_dof_coordinates()[:,:2]\n", " boundary=np.flatnonzero(np.any(np.isclose(coordinates,0)|np.isclose(coordinates,1),axis=1))\n", " fixed={int(i):0.0 for i in boundary}\n", " initial_problem=MultiscaleProblem.from_global(Equation(mass,initial_load),binding.size,fixed=fixed)\n", " previous=solve(assemble(initial_problem)).trace\n", " definition=MultiscaleProblem.from_global(Equation(a,source_load),binding.size,fixed=fixed)\n", " system=assemble(definition)\n", " free=np.setdiff1d(np.arange(binding.size),boundary)\n", " steps=int(round(final_time/dt))\n", " with factorize(system.matrix[free][:,free]) as factor:\n", " for step in range(1,steps+1):\n", " load=np.exp(-step*dt)*source_load+mass@previous/dt\n", " previous=solve(with_global_load(system,load),factorization=factor).trace\n", " return DiscreteField(FieldDefinition(\"concentration\",descriptor=binding.descriptor()),previous)\n", " finally:\n", " binding.close()\n", "\n", "def field_distance(first,second,order=8):\n", " \"\"\"Measure a physical concentration difference on the finer reference geometry.\"\"\"\n", " bary,weights=triangle_quadrature(order)\n", " fine=second.mesh\n", " points=np.einsum(\"qi,tia->tqa\",bary,fine.points[fine.cells])\n", " flattened=points.reshape(-1,2)\n", " difference=(first.evaluate(flattened)-second.evaluate(flattened)).reshape(points.shape[:2])\n", " return float(np.sqrt(np.sum(fine.areas[:,None]*weights*difference**2)))\n", "\n", "references={n:classical_trajectory(n) for n in (16,32,64)}\n", "reference=references[64]\n", "reference_refinement={\"16 to 32\":field_distance(references[16],references[32]),\n", " \"32 to 64\":field_distance(references[32],references[64])}\n", "print(\"Classical P2 spatial refinement differences at the same dt:\",reference_refinement)\n" ] }, { "cell_type": "markdown", "id": "b1222db9", "metadata": {}, "source": [ "## 5. Plot the final-time concentration\n", "\n", "Draw analytical, reconstructed and error fields in separate panels. The original\n", "macro mesh is visible in each panel and local incident values are preserved.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a497a619", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from matplotlib.collections import LineCollection\n", "\n", "def scalar_panel(ax, fields, exact, title, *, error=False):\n", " \"\"\"Plot each macrocell independently, with original macro topology visible.\"\"\"\n", " artists=[]\n", " for field in fields:\n", " fine=field.mesh\n", " values=field.evaluate(fine.points)\n", " if values.ndim==2: values=np.linalg.norm(values,axis=1)\n", " if exact is not None:\n", " truth=exact(fine.points)\n", " if truth.ndim==2: truth=np.linalg.norm(truth,axis=1)\n", " values=abs(values-truth) if error else truth\n", " artist=ax.tripcolor(fine.points[:,0],fine.points[:,1],fine.cells,values,shading=\"gouraud\")\n", " artists.append(artist)\n", " lower=min(a.get_clim()[0] for a in artists);upper=max(a.get_clim()[1] for a in artists)\n", " for a in artists:a.set_clim(lower,upper)\n", " ax.add_collection(LineCollection(macro.points[macro.faces],colors=\"black\",linewidths=0.65,alpha=0.55))\n", " ax.set(title=title,xlabel=\"x\",ylabel=\"y\",aspect=\"equal\")\n", " return artists[-1]\n", "figure,axes=plt.subplots(1,4,figsize=(15,4.2),layout=\"constrained\")\n", "for ax,title,truth,error in zip(axes,(\"Analytical\\nconcentration\",\"MHM\\nconcentration\",\"Classical P2\\nconcentration\",\"Absolute error\\nconcentration\"),\n", " (lambda x:exact_concentration(x,1.0),None,None,lambda x:exact_concentration(x,1.0)),\n", " (False,False,False,True),strict=True):\n", " chosen=(reference,) if title.startswith(\"Classical\") else concentration\n", " artist=scalar_panel(ax,chosen,truth,title,error=error)\n", " figure.colorbar(artist,ax=ax,pad=0.025,shrink=0.85)\n", "figure.savefig(OUTPUT/\"transient-concentration-fields.png\",dpi=180)\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "e322bc16", "metadata": {}, "source": [ "## 6. Temporal convergence with fixed spatial approximation\n", "\n", "Backward Euler has first-order temporal error for this smooth solution.\n", "Use several steps whose errors remain above the fixed spatial floor. Measure\n", "the final consecutive rates rather than fitting one slope through coarse\n", "meshes. Report original-equation checks at every time step.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a9ccb659", "metadata": {}, "outputs": [], "source": [ "from examples.tutorial_convergence import plot_method_series\n", "rows=[]\n", "for dt in (0.1,0.05,0.025,0.0125,0.00625):\n", " state,residual=trajectory(dt)\n", " low=concentration_error(state,order=6)\n", " high=concentration_error(state,order=8)\n", " np.testing.assert_allclose(low,high,rtol=1e-8,atol=1e-12)\n", " rows.append({\"dt\":dt,\"concentration_L2\":high,\"global_compatibility_relative\":residual})\n", "sizes=np.array([row[\"dt\"] for row in rows])\n", "errors=np.array([row[\"concentration_L2\"] for row in rows])\n", "rates=np.log(errors[:-1]/errors[1:])/np.log(sizes[:-1]/sizes[1:])\n", "print(rows)\n", "print(\"Observed backward-Euler orders:\",rates)\n", "figure=plot_method_series({\"method\":\"transient-transport-time\",\"sizes\":sizes.tolist(),\n", " \"errors\":{\"concentration L2\":errors.tolist()},\"rates\":{\"concentration L2\":rates.tolist()},\n", " \"expected\":{\"concentration L2\":1.0},\"refinement\":\"Time step Δt\"})\n", "for extension in (\"png\",\"svg\",\"pdf\"):\n", " figure.savefig(OUTPUT/f\"transient-native-convergence.{extension}\",dpi=180)\n", "plt.show()\n", "\n", "import json\n", "import hashlib\n", "\n", "# The public runner supplies the literal downloaded or checkout source path.\n", "# A manually renamed notebook needs PYMHM_NOTEBOOK_SOURCE set explicitly.\n", "source_file = Path(os.environ.get(\"PYMHM_NOTEBOOK_SOURCE\", \"transient_variational.ipynb\"))\n", "if not source_file.is_file() and \"PYMHM_NOTEBOOK_SOURCE\" not in os.environ:\n", " source_file = Path.cwd()/\"notebooks/transport/transient_variational.ipynb\"\n", "source_bytes = source_file.read_bytes()\n", "source_notebook = json.loads(source_bytes)\n", "scientific_cells = [\n", " {\"cell_type\": cell[\"cell_type\"],\n", " \"source\": \"\".join(cell[\"source\"]) if isinstance(cell[\"source\"], list) else cell[\"source\"]}\n", " for cell in source_notebook[\"cells\"]\n", " if \"pymhm-companion-bootstrap\" not in cell.get(\"metadata\", {}).get(\"tags\", [])\n", "]\n", "scientific_bytes = json.dumps(\n", " scientific_cells, ensure_ascii=False, sort_keys=True, separators=(\",\", \":\")\n", ").encode(\"utf-8\")\n", "study={\n", " \"case\":\"native UFL MHM conservative transient transport\",\n", " \"operator\":\"capacity=1, diffusion=1, beta=(0.25,-0.5), reaction=0.5\",\n", " \"exact\":\"exp(-t)*sin(pi*x)*sin(pi*y)\",\n", " \"final_time\":1.0,\"macro_divisions\":2,\"macro_cells\":len(macro.cells),\n", " \"local_degree\":2,\"local_edge_subdivisions\":local_refinement,\n", " \"trace_degree\":1,\"face_segments\":local_refinement//2,\n", " \"time_integrator\":\"backward Euler\",\"assembly_quadrature_degree\":12,\n", " \"error_quadrature_orders\":[6,8],\"rows\":rows,\"concentration_L2_rates\":rates.tolist(),\n", " \"classical_reference\":{\"family\":\"conforming P2 Galerkin\",\"resolutions\":[16,32,64],\n", " \"dt\":0.00625,\"spatial_L2_differences\":reference_refinement},\n", " \"source_notebook_sha256\": hashlib.sha256(source_bytes).hexdigest(),\n", " \"scientific_cells_sha256\": hashlib.sha256(scientific_bytes).hexdigest(),\n", " \"scientific_cells_hash_scope\": (\n", " \"UTF-8 JSON sequence of {cell_type, source}; sort_keys=True, \"\n", " \"ensure_ascii=False, separators=(comma,colon); excludes only cells \"\n", " \"tagged pymhm-companion-bootstrap, which install companion resources.\"\n", " ),\n", " \"figures\": {\n", " name: hashlib.sha256((OUTPUT/name).read_bytes()).hexdigest()\n", " for name in (\"transient-concentration-fields.png\", \"transient-native-convergence.png\",\n", " \"transient-native-convergence.svg\", \"transient-native-convergence.pdf\")\n", " },\n", "}\n", "(OUTPUT/\"transient-native-study.json\").write_text(json.dumps(study,indent=2)+\"\\n\")\n" ] }, { "cell_type": "markdown", "id": "1cbfbad7", "metadata": {}, "source": [ "## References and scope\n", "\n", "- Harder, Paredes and Valentin (2015), *On a Multiscale Hybrid-Mixed Method for\n", " Advective-Reactive Dominated Problems with Heterogeneous Coefficients*.\n", " https://doi.org/10.1137/130938499\n", "- Araya, Jaillet, Paredes and Valentin (2024), *Generalizing the multiscale\n", " hybrid-mixed method for reactive-advective-diffusive equations*.\n", " https://doi.org/10.1016/j.cma.2024.117089\n", "\n", "The temporal order belongs to backward Euler. This smooth analytical experiment\n", "is not a reproduction of a heterogeneous published transient geometry, a\n", "maximum-principle claim, or a spatial convergence theorem.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }