Linear / Nonlinear discrepancy

Hi everyone,

I’ve been struggling with implementing nonlinear materials into maxwells equations using the mpc nonlinear problem. To simplify the problem I have removed periodicity, allowing me to use the doflinx.fem.petsc.NonlinearProblem.

As the results were still not as expected I removed the nonlinear material property and assumed it to be linear for now. I also removed any MMF source from the problem (so no coils or magnets), and instead set a dirichlet boundary condition to a 0.01 for the magnetic vector potential that I am solving for. Comparing the linear solver, and nonlinear solver the nonlinear solver arrives at a vastly different result however.

The linear solution is correct, given that the disc has a permeability of 3000 with slots of permeability=1, reducing the magnetic vector potential inside the material. The nonlinear solution is practically a homogeneous field, which is incorrect.

Below is a MWE

To create the mesh:

from dataclasses import dataclass

from math import pi, radians, cos, sin, tan, ceil

import gmsh
from sympy import symbols, Eq, solve

mm = 1e-3  # [m] millimeter to meter conversion factor

@dataclass
class MachineDims:
    dimension: int = 2                    # [int] Dimension of the geometry, 2D or 3D
    periodicity: int = 1
    geom_start_angle: float = 0.0         # [rad] Start angle of the geometry
    geom_end_angle: float = 1/periodicity * 2 * pi  # [rad] end angle of the geometry in radians

    l_ag: float = 1.5*mm # [m] Radial length of the air gap
    slots = 48  # number of slots

@dataclass
class WedgeDims:
    r_inner: float
    r_outer: float

@dataclass
class StatorDims(WedgeDims):
    r_inner: float = 161.90/2*mm
    r_outer: float = 269.24/2*mm

@dataclass
class RotorDims(WedgeDims):
    r_inner: float = 110.64/2*mm
    r_outer: float = 160.40/2*mm

@dataclass
class SlotDims:
    a_slot_gap = radians(1.366)  # [rad]
    h_tooth_tip = 1.03*mm  # [m] height of the tooth tip
    w_tooth_tip = 1.535*mm  # [m] width of the tooth tip
    h_slot = 29.50*mm  # [m] height of the slot
    a_slot_widening = radians(1.25)  # [rad] angle of the slot widening
    h_insulation = 3.50*mm  # [m] height of the insulation

@dataclass
class DuctDims:
    a_duct = 65.0 / 180 * pi  # [rad]
    r_duct_maximum = 78.72*mm # [m]
    r_duct_minimum = 62.6*mm # [m]
    h_duct = 4.7*mm  # [m]
    o_magnet = 2.25*mm  # [m] offset of the magnet from the mirror plane
    h_magnet = 6.48*mm  # [m] height of the magnet
    w_magnet = 16.0*mm  # [m] width of the magnet
    w_duct_peak = 3.0*mm  # [m] width of the duct peak

def mirror_entities(entities, angle, point=(0,0,0)):
    """
    Mirror a list of entities across a plane defined by a normal vector and a point.
    :param entities: List of entities to be mirrored.
    :param angle: Angle to the plane to the x-axis.
    :param point: A point on the plane across which to mirror.
    :return: List of mirrored entities.
    """
    a = sin(angle)
    b = -cos(angle)
    c = 0
    d = -(a * point[0] + b * point[1] + c * point[2])

    e = factory.copy(entities)
    factory.mirror(e, a, b, c, d)
    return e

def repeat_shape_on_angle(out: list[tuple], occurrence: int = None, periodicity: int = None, angle_offset: float = None,
                          angle: float = None) -> list:
    """
    Repeat a shape defined in out[0] around the z-axis at an angle defined by the number of slots and periodicity.
    :param out: List containing the shape to be repeated.
    :param occurrence: Total number of slots in the machine.
    :param periodicity: Number of slots in one period.
    :param angle_offset: Offset angle to apply to each repetition.
    :param angle: Specific angle to rotate the shape. If None, it will be calculated based on slots and periodicity.
    :return: List of entities after rotation.
    """
    output = []
    for k, (order, shape) in enumerate(out):
        if order != 2:
            raise ValueError(f"Expected a surface (2), but got {order} at index {k}.")
        if occurrence is not None and periodicity is not None:
            out_new = [shape]
            for i in range(1, ceil(occurrence / periodicity)):
                if angle_offset is None:
                    angle_offset = 0
                angle = i * (2 * pi / occurrence) + angle_offset
                rotated_surface = factory.copy([(order, shape)])
                factory.rotate(rotated_surface, 0, 0, 0, 0, 0, 1, angle)
                out_new.append(rotated_surface[0][1])
        elif angle is not None:
            rotated_surface = factory.copy([(order, shape)])
            factory.rotate(rotated_surface, 0, 0, 0, 0, 0, 1, angle)
            out_new = [rotated_surface[0][1]]
        else:
            raise ValueError("Either angle or both slots and periodicity must be provided.")
        output.extend(out_new)
    return output

def cut_from_surface(out: list, tool_surfaces: list) -> list:
    """
    Cut the tool surfaces from the output surface.
    :param out: List containing the surface to be cut.
    :param tool_surfaces: List of surfaces to be used as tools for cutting.
    :return: List of entities after cutting.
    """
    surfaces_to_cut = [(2, surface) for surface in tool_surfaces]
    cut_result, _ = factory.cut([(2, out[0])], surfaces_to_cut, removeObject=False, removeTool=False)
    return [tag for dim, tag in cut_result if dim == 2]  # Keep only the surface tags

def square_in_polar_coordinates(r1, r2, a1, a2) -> tuple:
    """
    Create a square in polar coordinates defined by two radii and two angles.
    :param r1: Inner radius of the square.
    :param r2: Outer radius of the square.
    :param a1: Start angle of the square.
    :param a2: End angle of the square.
    :return: List of points defining the square in polar coordinates.
    """
    x = [r1 * cos(a1), r1 * cos(a2), r2 * cos(a2), r2 * cos(a1)]
    y = [r1 * sin(a1), r1 * sin(a2), r2 * sin(a2), r2 * sin(a1)]
    z = [0, 0, 0, 0]  # All points are in the same plane (z=0)
    return x, y, z

def draw_square_from_points(out: dict) -> dict:
    """
    Draw a square from the points defined in the out dictionary.
    :param out: dictionary containing the points of the square.
    :return: dictionary containing the points, curves, curve loop, and surface of the square.
    """
    out["curve"].extend([
        factory.addLine(out["point"][0], out["point"][1]),  # Bottom line
        factory.addLine(out["point"][1], out["point"][2]),  # Right line
        factory.addLine(out["point"][2], out["point"][3]),  # Top line
        factory.addLine(out["point"][3], out["point"][0])  # Left Line
    ])

    # Create the curve loop and surface for the magnet
    out["curve_loop"] = factory.addCurveLoop(out["curve"])
    out["surface"] = [factory.addPlaneSurface([out["curve_loop"]])]
    return out

def solve_algebraic_system(param, eq1, eq2) -> tuple[float, float]:
    """
    Solve a system of algebraic equations.
    :param param: The parameters to solve for, typically a tuple of symbols.
    :param eq1: First equation in the system.
    :param eq2: Second equation in the system.
    :return: A tuple containing the solutions for x and y.
    """
    solutions = solve((eq1, eq2), param)
    if solutions[0][0] > 0 and solutions[0][1] > 0:
        return solutions[0]
    else:
        return solutions[1]

gmsh.initialize()
gmsh.model.add("GeometryBuilder")
machine_dims = MachineDims()
stator_dims = StatorDims()
rotor_dims = RotorDims()
slot_dims = SlotDims()
duct_dims = DuctDims()

## Build the geometry
factory = gmsh.model.occ

out = {}
# Create the center point
center_tag = factory.addPoint(0, 0, 0, 1e-2)
out['center'] = center_tag

coil_out = {"point": [], "curve": [], "curve_loop": None, "surface": []}

slots = machine_dims.slots  # Number of slots in the machine
a_slot_gap = slot_dims.a_slot_gap  # Angle of the slot gap
h_tooth_tip = slot_dims.h_tooth_tip  # Height of the tooth tip
h_slot = slot_dims.h_slot  # Height of the slot
h_insulation = slot_dims.h_insulation  # Height of the insulation

r1 = stator_dims.r_inner + h_tooth_tip + h_insulation
r2 = stator_dims.r_inner + h_tooth_tip + h_slot

a1 = machine_dims.geom_start_angle + (2*pi / slots) / 2 - a_slot_gap
a2 = machine_dims.geom_start_angle + (2*pi / slots) / 2 + a_slot_gap

# Calculate the coordinates of the coil points
x, y, z = square_in_polar_coordinates(r1, r2, a1, a2)
for i in range(len(x)):
    point_tag = factory.addPoint(x[i], y[i], z[i], 1e-3)
    coil_out["point"].append(point_tag)
coil_out = draw_square_from_points(coil_out)

coils = {"PhaseA1": coil_out,
         "PhaseA2": {"surface": []},
         "PhaseC1": {"surface": []},
         "PhaseC2": {"surface": []},
         "PhaseB1": {"surface": []},
         "PhaseB2": {"surface": []}}

# Repeat the coil surface for all slots
for i, coil in enumerate(coils.keys()):
    if coil == "PhaseA1":
        coils[coil]["name"] = coil  # type: ignore
        coils[coil]["surface"] = repeat_shape_on_angle([(2, coil_out["surface"][0])],
                                                       occurrence=round(slots / (len(coils.keys()))),
                                                       periodicity=1,
                                                       angle_offset=0)
        continue
    coils[coil]["surface"] = repeat_shape_on_angle([(2, coil_out["surface"][0])], angle=i * (2 * pi / slots))
    coils[coil]["surface"] = repeat_shape_on_angle([(2, coils[coil]["surface"][0])],
                                                   occurrence=round(slots / (len(coils.keys()))),
                                                   periodicity=1)
    coils[coil]["name"] = coil  # type: ignore
coil_out = coils
slot_out = {"point": [], "curve": [], "curve_loop": None, "surface": []}

slots = machine_dims.slots  # Number of slots in the machine
periodicity = machine_dims.periodicity # Periodicity of the machine geometry
a_slot_gap = slot_dims.a_slot_gap  # Angle of the slot gap
h_tooth_tip = slot_dims.h_tooth_tip  # Height of the tooth tip
w_tooth_tip = slot_dims.w_tooth_tip  # Width of the tooth tip
h_slot = slot_dims.h_slot  # Height of the slot
a_slot_widening = slot_dims.a_slot_widening  # Angle of the slot widening

r1 = stator_dims.r_inner  # Inner radius of the stator
r2 = stator_dims.r_inner + h_tooth_tip
r4 = stator_dims.r_inner + h_tooth_tip + h_slot - 1e-4

a1 = machine_dims.geom_start_angle + (2*pi / slots) / 2 - a_slot_gap # Start angle of the slot
a2 = a1 - 1/2*pi
a3 = a2 - a_slot_widening
a4 = machine_dims.geom_start_angle + (2*pi / slots) / 2

# Calculate the coordinates of the slot points
x = [r1*cos(a1), r2*cos(a1), r2*cos(a1) + w_tooth_tip*cos(a2), r2*cos(a1) + w_tooth_tip*cos(a2) - h_slot*sin(a3), r4*cos(a4),]
y = [r1*sin(a1), r2*sin(a1), r2*sin(a1) + w_tooth_tip*sin(a2), r2*sin(a1) + w_tooth_tip*sin(a2) + h_slot*cos(a3), r4*sin(a4),]
z = [0] * len(x)
for i in range(len(x)):
    point_tag = factory.addPoint(x[i], y[i], z[i], 1e-3)
    slot_out["point"].append(point_tag)

# Repeat the points on the other half of the slot
points = [(0, x) for x in slot_out["point"][0:-1][::-1]]
mirrored_points = mirror_entities(points, angle=a4, point=(0, 0, 0))
slot_out["point"].extend([x[1] for x in mirrored_points])

# Create the curves for the slot
slot_out["curve"].extend([
    factory.addLine(slot_out["point"][0], slot_out["point"][1]),  # Inner line
    factory.addLine(slot_out["point"][1], slot_out["point"][2]),  # Tooth tip line
    factory.addLine(slot_out["point"][2], slot_out["point"][3]),  # Slot wall
    factory.addCircleArc(slot_out["point"][3], slot_out["point"][4], slot_out["point"][5]),  # Outer arc
    factory.addLine(slot_out["point"][5], slot_out["point"][6]),  # Slot wall
    factory.addLine(slot_out["point"][6], slot_out["point"][7]),  # Tooth tip line
    factory.addLine(slot_out["point"][7], slot_out["point"][8]),  # Inner line
    factory.addLine(slot_out["point"][8], slot_out["point"][0])])   # Closing line

# Create the curve loop and surface for the slot
slot_out["curve_loop"] = factory.addCurveLoop(slot_out["curve"])
slot_out["surface"] = [factory.addPlaneSurface([slot_out["curve_loop"]])]

# Cut the coil out of the slot surface
if coil_out is not None:
    for key, coil_surface in coil_out.items():
        slot_out["surface"].append(cut_from_surface(slot_out["surface"], coil_surface["surface"])[0])
old_surface = slot_out["surface"][0]
factory.remove([(2, old_surface)], recursive=True)
slot_out["surface"] = slot_out["surface"][1:]

# Repeat the slot surface for all slots
slot_out["surface"] = repeat_shape_on_angle([(2, slot_out["surface"][0])], slots, periodicity)
slot_out["name"] = "Slot"

stator_out = {"point": [], "curve": [], "curve_loop": None, "surface": []}
r1 = stator_dims.r_inner  # Inner radius of the stator
r2 = stator_dims.r_outer  # Outer radius of the stator
a1 = machine_dims.geom_start_angle  # Start angle of the geometry
a2 = machine_dims.geom_end_angle    # End angle of the geometry

# Create the points for the stator
x, y, z = square_in_polar_coordinates(r1, r2, a1, a2)
for i in range(4):
    point_tag = factory.addPoint(x[i], y[i], z[i], 1e-3)
    stator_out["point"].append(point_tag)

# Create the curves for the wedge
stator_out["curve"].append(factory.addCircle(0, 0, 0, r1))  # Inner circle
stator_out["curve"].append(factory.addCircle(0, 0, 0, r2))  # Outer circle

curve_loop, surface = [], []
for i in range(2):
    curve_loop.append(factory.addCurveLoop([stator_out["curve"][i]]))
    surface.append(factory.addPlaneSurface([curve_loop[i]]))
stator_out["surface"] = cut_from_surface([surface[1]], [surface[0]])

surface_to_cut = [slot_out, coil_out]
for surface_tc in surface_to_cut:
    if "PhaseA1" in surface_tc.keys():
        for key, coil_surface in surface_tc.items():
            if "surface" in coil_surface:
                stator_out["surface"] = cut_from_surface(stator_out["surface"], coil_surface["surface"])
            else:
                raise ValueError(f"Coil {key} does not contain a surface to cut from the slot.")
    elif surface_tc["name"] in {"Slot"}:
        name = "Stator"
        stator_out["surface"] = cut_from_surface(stator_out["surface"], surface_tc["surface"])
stator_out["name"] = name

gmsh.model.occ.synchronize()
geometry = {"Center": out,
            "Stator": {"geom": stator_out},
            "Slot": {"geom": slot_out},
            "PhaseA1": {"geom": coils["PhaseA1"]},
            "PhaseA2": {"geom": coils["PhaseA2"]},
            "PhaseB1": {"geom": coils["PhaseB1"]},
            "PhaseB2": {"geom": coils["PhaseB2"]},
            "PhaseC1": {"geom": coils["PhaseC1"]},
            "PhaseC2": {"geom": coils["PhaseC2"]}}

for key in geometry.keys():
    if key in ["Center", "Bounds"]:
        continue
    out = geometry[key]["geom"]
    out["bbox"] = []
    for s in out["surface"]:
        out["bbox"].append(factory.getBoundingBox(2, s))

gmsh.model.occ.removeAllDuplicates()
gmsh.model.occ.synchronize()

def is_same_bbox(bbox1, bbox2, tol=1e-6):
    return all(abs(a - b) < tol for a, b in zip(bbox1, bbox2))

# Find the updated tags
for key in geometry.keys():
    if key in ["Center", "Bounds"]:
        continue
    out = geometry[key]["geom"]
    new_s = []
    for i, s in enumerate(out["surface"]):
        old_bbox = out["bbox"][i]  # Assuming all surfaces have the same bounding box
        new_tag = None
        for (dim, tag) in factory.getEntities(2):
            bbox = factory.getBoundingBox(dim, tag)
            if is_same_bbox(bbox, old_bbox):
                new_tag = tag
                break
        new_s.append(new_tag)
    geometry[key]["geom"]["surface"] = new_s

for key in geometry.keys():
    if key in ["Center", "Bounds"]:
        continue
    out = geometry[key]["geom"]
    out["tag"] = gmsh.model.addPhysicalGroup(2, out["surface"])
    gmsh.model.setPhysicalName(2, out["tag"], out["name"])

gmsh.model.mesh.generate(2)
gmsh.write("geometry.msh")

gmsh.fltk.run()
gmsh.finalize()

To define and solve the problem:

from math import pi

import numpy as np
import pyvista
import ufl
from petsc4py import PETSc
from basix.ufl import element
from dolfinx import default_scalar_type
from dolfinx.fem import functionspace,  Function, locate_dofs_topological, dirichletbc, Expression
from dolfinx.io.gmshio import read_from_msh
from dolfinx.mesh import locate_entities_boundary
from dolfinx.plot import vtk_mesh
from mpi4py.MPI import COMM_WORLD
from ufl import Measure, TestFunction, inner, grad, derivative, TrialFunction

geometry = {'Center': {'center': 1},
            'PhaseA1': {'geom': {'curve': [1, 2, 3, 4], 'curve_loop': 1, 'name': 'PhaseA1', 'point': [2, 3, 4, 5], 'surface': [1, 2, 3, 4, 5, 6, 7, 8], 'tag': 3}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'PhaseA2': {'geom': {'name': 'PhaseA2', 'surface': [9, 10, 11, 12, 13, 14, 15, 16], 'tag': 4}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'PhaseB1': {'geom': {'name': 'PhaseB1', 'surface': [33, 34, 35, 36, 37, 38, 39, 40], 'tag': 5}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'PhaseB2': {'geom': {'name': 'PhaseB2', 'surface': [41, 42, 43, 44, 45, 46, 47, 48], 'tag': 6}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'PhaseC1': {'geom': {'name': 'PhaseC1', 'surface': [17, 18, 19, 20, 21, 22, 23, 24], 'tag': 7}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'PhaseC2': {'geom': {'name': 'PhaseC2', 'surface': [25, 26, 27, 28, 29, 30, 31, 32], 'tag': 8}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'Slot': {'geom': {'curve': [193, 194, 195, 196, 197, 198, 199, 200], 'curve_loop': 49, 'name': 'Slot', 'point': [194, 195, 196, 197, 198, 199, 200, 201, 202], 'surface': [102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196], 'tag': 2}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 1.0}},
            'Stator': {'geom': {'curve': [765, 766], 'curve_loop': None, 'name': 'Stator', 'point': [767, 768, 769, 770], 'surface': [101], 'tag': 1}, 'material': {'mu_0': 4*pi * 1e-7, 'mu_r': 3000}},
}
mesh, cell_tags, facet_tags = read_from_msh("geometry.msh", comm=COMM_WORLD, rank=0, gdim=2)
mesh.topology.create_connectivity(2, 1)
mesh.topology.create_connectivity(1, 2)
mesh.topology.create_connectivity(2, 0)
mesh.topology.create_connectivity(0, 2)

dx = Measure("dx", subdomain_data=cell_tags, domain=mesh, metadata={"quadrature_degree": 2})

cell = mesh.ufl_cell().cellname()

# Define function space
CG_V = element("Lagrange", cell, 2, shape=())  # CG: Continuous Galerkin vector potential (Sized for Az only)
DG_0 = element("DG", cell, 0, shape=())  # DG: Discontinuous Galerkin scalar space

V = functionspace(mesh, CG_V)
V_DG = functionspace(mesh, DG_0)

# Boundary conditions
def dirichlet_bounds(x):
    return np.isclose(x[0]**2+x[1]**2, (269.24/2/1000)**2)

tdim = mesh.topology.dim - 1
facets = locate_entities_boundary(mesh, dim=tdim, marker=dirichlet_bounds)
dofs = locate_dofs_topological(V, tdim, facets)
bc = dirichletbc(default_scalar_type(0.1), dofs, V)
bcs = [bc]

# Define variational problem
Az = Function(V)
Az.x.array[:] = 0.0
u = TrialFunction(V)
v = TestFunction(V)
nu = Function(V_DG)

dm = V_DG.dofmap
c2d = dm.cell_dofs
for key in geometry.keys() - {"Center"}:
    out = geometry[key]
    tag = out["geom"]["tag"]

    val = 1 / (out["material"]["mu_0"] * out["material"]["mu_r"])
    for c in cell_tags.find(tag):
        nu.x.array[c2d(c)] = val
nu.x.scatter_forward()

a = inner(nu * grad(u), grad(v)) * dx
Jz = Function(V)
Jz.x.array[:] = 0
L = Jz * v * dx
## Linear variational problem
from dolfinx.fem.petsc import LinearProblem
problem = LinearProblem(a,
                        L,
                        u=Az,
                        bcs=bcs,
                        petsc_options={"ksp_type": "gmres",
                                       "ksp_rtol": 1e-6,
                                       "ksp_atol": 1e-10,
                                       "ksp_max_it": 1000})
problem.solve()
## Nonlinear variational problem
# R = inner(nu * grad(Az), grad(v)) * dx
# F = R - L
#
# from dolfinx.fem.petsc import NonlinearProblem
# from dolfinx.nls.petsc import NewtonSolver
# problem = NonlinearProblem(F, u=Az, bcs=bcs)
# solver = NewtonSolver(mesh.comm, problem)
# solver.convergence_criterion = "residual"
#
# n, converged = solver.solve(Az)
# print(f"Number of iterations: {n:d}")

## ------ Plot results ------
# Plot vector potential
plotter = pyvista.Plotter()
Az_grid = pyvista.UnstructuredGrid(*vtk_mesh(V))
Az_grid.point_data["A_z"] = Az.x.array
Az_grid.set_active_scalars("A_z")
plotter.add_mesh(Az_grid, show_edges=False)
plotter.add_mesh(Az_grid.extract_all_edges(), color="grey", opacity=0.25)

plotter.view_xy()
plotter.show()

# Plot flux density
plotter = pyvista.Plotter()
grid = pyvista.UnstructuredGrid(*vtk_mesh(V))
B_func = Function(V)
B_expr = Expression(ufl.sqrt(Az.dx(1) ** 2 + (-Az.dx(0)) ** 2 + 1e-12), V.element.interpolation_points())
B_func.interpolate(B_expr)

num_dofs = V.dofmap.index_map.size_local
values = np.zeros((num_dofs, 3), dtype=np.float64)
values[:, :V.mesh.geometry.dim] = B_func.x.array.real.reshape(num_dofs, V.dofmap.index_map_bs)

grid = pyvista.UnstructuredGrid(*vtk_mesh(V))
grid.point_data["B"] = values
grid.set_active_vectors("B")
plotter.add_mesh(grid, show_edges=False)
plotter.add_mesh(Az_grid.extract_all_edges(), color="grey", opacity=0.25)
plotter.view_xy()
plotter.show()

# Plot permeability
num_cells = V.mesh.topology.index_map(V.mesh.topology.dim).size_local
cell_values = 1 / (nu.x.array.real.reshape(num_cells, V.dofmap.index_map_bs) * 4 * pi * 1e-7)

grid = pyvista.UnstructuredGrid(*vtk_mesh(V.mesh, V.mesh.topology.dim))
grid.cell_data["nu"] = cell_values.flatten()
grid.set_active_scalars("nu")

plotter = pyvista.Plotter()
plotter.add_mesh(grid, show_edges=False, cmap="coolwarm")
plotter.add_mesh(Az_grid.extract_all_edges(), color="grey", opacity=0.25)
plotter.view_xy()
plotter.show()

I’m completely lost as to why the nonlinear solver arrives at this (wrong) result. Does anyone have any ideas?

You are not setting any solver options for the KsP solver that is used for each sub-step of the Newton iteration.
See for instance

where it shows how to tune the solver.

I tested using a small variety of solver types and preconditioners, but so far without any luck, the results stay the same.

Given that the problem currently is in fact linear, wouldn’t you expect the nonlinear solver to solve the problem in a single iteration to the same solution if the same solver settings are applied? From my understanding the nonlinear solver iteratively solves a linear problem anyhow.

Did you try preonly with lu factorization?

If use the wrong solver to solve the linear subproblem, it might not converge, and PETSC doesn’t throw errors if ksp_error_if_not_converged is set to true

I just did, the solution gets a bit closer to what you would expect in terms of gradient. But the magnitude is still far off

1 Like