October 16, 2025

Nitche

import numpy as np
from skfem.helpers import dot, ddot, sym_grad, mul
from scipy.sparse.linalg import minres
import scipy
from scipy.sparse import bmat, csr_matrix, coo_matrix, lil_matrix

import skfem
from skfem.helpers import dot, ddot, sym_grad
from skfem import InteriorBasis, FacetBasis, ElementTetP1, ElementVectorH1

from skfem.supermeshing import intersect, elementwise_quadrature


import numpy as np
from skfem import *
from skfem.supermeshing import intersect, elementwise_quadrature
from skfem.models.elasticity import (linear_elasticity, lame_parameters,
                                     linear_stress)
from skfem.helpers import dot, sym_grad, jump, mul
from skfem.io.json import from_file
from pathlib import Path


mesh_top = skfem.MeshTet.init_tensor(
    np.linspace(0, 2, 20),
    np.linspace(0, 2, 20),
    np.linspace(0, 1, 10)
)
mesh_bot = skfem.MeshTet.init_tensor(
    np.linspace(0.5, 1.5, 20),
    np.linspace(0.5, 1.5, 20),
    np.linspace(-0.5, 0.0, 5)
)


def is_contact_surface(x):
    return np.isclose(x[2], 0.0)


def is_force_surface(x):
    return np.isclose(x[2], 1.0)


def is_dirichlet_surface(x):
    return np.isclose(x[2], -0.5)


mesh_top = mesh_top.with_boundaries({
    "contact": is_contact_surface,
    "force": is_force_surface
})
mesh_bot = mesh_bot.with_boundaries({
    "contact": is_contact_surface,
    "dirichlet": is_dirichlet_surface
})


elem = skfem.ElementTetP1()
u_elem = skfem.ElementVectorH1(elem)
basis_top = skfem.InteriorBasis(mesh_top, u_elem)
basis_bot = skfem.InteriorBasis(mesh_bot, u_elem)

E, nu = 210e9, 0.3
mu = E/(2*(1+nu))
lam = E*nu/((1+nu)*(1-2*nu))
C = linear_stress(*lame_parameters(E, nu))


# @skfem.BilinearForm
# def a_elastic(u, v, w):
#     eps_u = sym_grad(u)      # symmetric gradient of u
#     eps_v = sym_grad(v)
#     return lam * np.trace(eps_u) * np.trace(eps_v) + 2*mu * ddot(eps_u, eps_v)


@skfem.BilinearForm
def a_elastic(u, v, w):
    eps_u = sym_grad(u)
    eps_v = sym_grad(v)
    I = np.eye(3).reshape(3, 3, 1)
    return lam * ddot(eps_u, I) * ddot(eps_v, I) + 2 * mu * ddot(eps_u, eps_v)


K1 = a_elastic.assemble(basis_top)
K2 = a_elastic.assemble(basis_bot)


basis_top = skfem.Basis(mesh_top, ElementVectorH1(skfem.ElementTetP1()))
basis_bot = skfem.Basis(mesh_bot, ElementVectorH1(skfem.ElementTetP1()))

elem_s = ElementTetP1()
elem_v = ElementVectorH1(elem_s)
m1t, orig1 = mesh_top.trace('contact', mtype=skfem.MeshTri, project=lambda p: p[[0, 1]])
m2t, orig2 = mesh_bot.trace('contact', mtype=skfem.MeshTri, project=lambda p: p[[0, 1]])
m12, t1, t2 = intersect(m1t, m2t)
quad1 = elementwise_quadrature(m1t, m12, t1)
quad2 = elementwise_quadrature(m2t, m12, t2)


fb_u_top = FacetBasis(
    mesh_top, elem_v, facets=orig1[t1], quadrature=quad1
)
fb_u_bot = FacetBasis(
    mesh_bot, elem_v, facets=orig2[t2], quadrature=quad2
)
fbasis = fb_u_top * fb_u_bot


alpha = 1e+1

@skfem.BilinearForm
def bilin_mortar(u1, u2, v1, v2, w):
    n = w.n / np.linalg.norm(w.n, axis=0, keepdims=True)

    ju = u1 - u2
    jv = v1 - v2

    t1 = mul(C(sym_grad(u1)), n)
    t2 = mul(C(sym_grad(u2)), n)
    s1 = mul(C(sym_grad(v1)), n)
    s2 = mul(C(sym_grad(v2)), n)

    penalty = alpha / (w.h)

    return (
        penalty * dot(ju, jv)
        - 0.5 * (dot(t1 + t2, jv))
        - 0.5 * (dot(s1 + s2, ju))
    )

params = {
    'h': fb_u_top.mesh_parameters(),
    # 'n': fb_u_top.normals,
}

B = skfem.asm(bilin_mortar, fbasis, **params)

K = bmat([[K1, None],
          [None, K2]], 'csr') + B

# f_mortar = skfem.asm(lin_mortar, fbasis, **params)

print("K1:", K1.shape)
print("K2:", K2.shape)
print("B1:", B.shape)
F_facet_ids = mesh_top.facets_satisfying(is_force_surface)
# F_facet_ids = mesh.facets_satisfying(lambda x: np.isclose(x[0], 1.0))

fbasis = skfem.FacetBasis(mesh_top, elem_v, facets=F_facet_ids)
total_force = 100.0  # [N]


@skfem.Functional
def surface_measure(w):
    return 1.0


A_proj_z = surface_measure.assemble(fbasis)
pressure = total_force / A_proj_z


@skfem.LinearForm
def l_comp(v, w):
    return pressure * v[2]


top_F = l_comp.assemble(fbasis)
bot_F = np.zeros(mesh_bot.p.shape[1] * mesh_bot.dim())
F = np.hstack([top_F, bot_F])

D_facets = mesh_bot.facets_satisfying(is_dirichlet_surface)
D_dofs = basis_bot.get_dofs(facets=D_facets).all() + K1.shape[0]

Kc, Fc, uc, I = skfem.condense(K, F, D=D_dofs)
# u = skfem.solve(Kc, Fc, uc, I)
uc, info = minres(Kc, Fc, rtol=1e-10, maxiter=20000)

u = np.zeros(K.shape[0])
u[I] = uc

F_from_U = Kc.dot(u[I])

print("len(t1):", len(t1), "len(t2):", len(t2))
print(u)
print(np.sum(np.abs(F_from_U - Fc)))

F_pred_full = K @ u   # full system prediction
print("K.shape, u.shape, F_pred_full.shape", K.shape, u.shape, F_pred_full.shape)
print("F_pred_full.shape, F.shape", F_pred_full.shape, F.shape)
residual_full = F_pred_full - F

print("len(F_pred_full) =", len(F_pred_full))
print("len(F) =", len(F))
print("‖residual‖₂ =", np.linalg.norm(residual_full))

res_norm = np.linalg.norm(Fc - Kc @ uc)
rhs_norm = np.linalg.norm(Fc)
print("relative residual =", res_norm / rhs_norm)


# D_local = basis_bot.get_dofs(facets=D_facets).all()
# D_global = D_local + basis_top.N

# all_dofs = np.arange(K.shape[0])
# I = np.setdiff1d(all_dofs, D_global)

# Kc = K[I][:, I]
# Fc = F[I]

# x_reduced = skfem.solve(Kc, Fc)
# x = np.zeros_like(F)
# x[I] = x_reduced


def export_combined_vtu(
    mesh_top, mesh_bot,
    u, file_path="combined.vtu"
):
    import meshio

    # ---------------------------
    # 1. coords and cells
    # ---------------------------
    if isinstance(mesh_top, skfem.MeshTet):
        cell_type = "tetra"
    elif isinstance(mesh_top, skfem.MeshHex):
        cell_type = "hexahedron"
    else:
        raise ValueError("Unsupported mesh type")

    n_top_nodes = mesh_top.p.shape[1]
    n_bot_nodes = mesh_bot.p.shape[1]

    # points
    points = np.vstack([mesh_top.p.T, mesh_bot.p.T])

    # cells (offset bottom mesh node indices)
    cells = [(
        cell_type,
        np.vstack([
            mesh_top.t.T,
            mesh_bot.t.T + n_top_nodes
        ])
    )]

    # u_top_bot = u[:(n_top_nodes+n_bot_nodes)*3]
    n_nodes_total = n_top_nodes + n_bot_nodes
    u_top_bot = u[: n_nodes_total * 3].reshape(n_nodes_total, 3)

    # ---------------------------
    # 3. node_tag: Dirichlet / Force / Contact
    # ---------------------------
    node_tag = np.zeros(n_top_nodes + n_bot_nodes, dtype=int)
    top_nodes_contact = np.unique(mesh_top.facets[:, mesh_top.boundaries["contact"]])
    bot_nodes_contact = np.unique(mesh_bot.facets[:, mesh_bot.boundaries["contact"]]) + n_top_nodes
    nodes_contact = np.hstack([top_nodes_contact, bot_nodes_contact])
    # dofs_dirichlet = basis_top.get_dofs("dirichlet").all()
    bot_nodes_dirichlet = np.unique(
        mesh_bot.facets[:, mesh_bot.boundaries["dirichlet"]]
    ) + n_top_nodes
    nodes_dirichlet = bot_nodes_dirichlet
    top_nodes_force = np.unique(mesh_top.facets[:, mesh_top.boundaries["force"]])
    nodes_force = top_nodes_force
    node_tag[nodes_contact] = 1
    node_tag[nodes_dirichlet] = 2
    node_tag[nodes_force] = 3

    # ---------------------------
    # 4. cells_tag
    # ---------------------------
    # cells_tag = np.zeros(mesh_top.nelements + mesh_bot.nelements, dtype=int)

    meshio_mesh = meshio.Mesh(
        points=points,
        cells=cells,
        point_data={
            "u": u_top_bot,
            "node_tag": node_tag,
        },
        # cell_data={
        #     "cells_tag": [cells_tag]
        # }
    )

    meshio_mesh.write(file_path)
    print(f"✅ Exported combined VTU: {file_path}")


export_combined_vtu(
    mesh_top,
    mesh_bot,
    u
)

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 2)
ax1.plot(F_from_U)
ax2.plot(Fc)

fig.savefig("mortar.png")

Mortar

import numpy as np
import scipy
from scipy.sparse import bmat
from scipy.sparse.linalg import minres

import skfem
from skfem.helpers import dot, ddot, sym_grad
from skfem import InteriorBasis, FacetBasis, ElementTetP1, ElementVectorH1

from skfem.supermeshing import intersect, elementwise_quadrature


mesh_top = skfem.MeshTet.init_tensor(
    np.linspace(0, 2, 20),
    np.linspace(0, 2, 20),
    np.linspace(0, 1, 10)
)
mesh_bot = skfem.MeshTet.init_tensor(
    np.linspace(0.5, 1.5, 20),
    np.linspace(0.5, 1.5, 20),
    np.linspace(-0.5, 0.0, 5)
    # np.linspace(-0.55, -0.05, 5)
)


def is_contact_surface(x):
    return np.isclose(x[2], 0.0)


def is_force_surface(x):
    return np.isclose(x[2], 1.0)


def is_dirichlet_surface(x):
    return np.isclose(x[2], -0.5)


mesh_top = mesh_top.with_boundaries({
    "contact": is_contact_surface,
    "force": is_force_surface
})
mesh_bot = mesh_bot.with_boundaries({
    "contact": is_contact_surface,
    "dirichlet": is_dirichlet_surface
})


elem = skfem.ElementTetP1()
u_elem = skfem.ElementVectorH1(elem)
basis_top = skfem.Basis(mesh_top, u_elem)
basis_bot = skfem.Basis(mesh_bot, u_elem)

E, nu = 210e9, 0.3
mu = E/(2*(1+nu))
lam = E*nu/((1+nu)*(1-2*nu))


@skfem.BilinearForm
def a_elastic(u, v, w):
    eps_u = sym_grad(u)      # symmetric gradient of u
    eps_v = sym_grad(v)
    return lam * np.trace(eps_u) * np.trace(eps_v) + 2*mu * ddot(eps_u, eps_v)


K1 = a_elastic.assemble(basis_top)
K2 = a_elastic.assemble(basis_bot)


elem_s = ElementTetP1()
elem_v = ElementVectorH1(elem_s)
# elem_lam = skfem.ElementVector(skfem.ElementTriP0())
m1t, orig1 = mesh_top.trace(
    'contact', mtype=skfem.MeshTri,
    project=lambda p: p[[0, 1]]
)
m2t, orig2 = mesh_bot.trace(
    'contact', mtype=skfem.MeshTri,
    project=lambda p: p[[0, 1]]
)
m12, t1, t2 = intersect(m1t, m2t)
quad1 = elementwise_quadrature(m1t, m12, t1)
quad2 = elementwise_quadrature(m2t, m12, t2)

fb_u_top = FacetBasis(
    mesh_top, elem_v, facets=orig1[t1], quadrature=quad1
)
fb_u_bot = FacetBasis(
    mesh_bot, elem_v, facets=orig2[t2], quadrature=quad2
)
# fb_lam_v = FacetBasis(mesh_bot, elem_v, facets=orig2[t2], quadrature=quad2)
fb_lam_v = FacetBasis(mesh_top, elem_v, facets=orig1[t1], quadrature=quad1)

# contact_top = mesh_top.facets_satisfying(lambda x: np.isclose(x[2], 0.0, atol=1e-6))
# contact_bot = mesh_bot.facets_satisfying(lambda x: np.isclose(x[2], 0.0, atol=1e-6))
# fb_u_top = FacetBasis(mesh_top, elem_v, facets=contact_top)
# fb_u_bot = FacetBasis(mesh_bot, elem_v, facets=contact_bot)
# fb_lam_v = fb_u_top




@skfem.BilinearForm
def b_top_dot(u, v, w):
    return dot(v, u)   # λ·v

@skfem.BilinearForm
def b_bot_dot(u, v, w):
    return - dot(v, u)


B1 = skfem.asm(b_top_dot, fb_u_top, fb_lam_v)
B2 = skfem.asm(b_bot_dot, fb_u_bot, fb_lam_v)


print("B1 nnz =", B1.nnz, "sum =", B1.sum())
print("B2 nnz =", B2.nnz, "sum =", B2.sum())
print("B1 mean abs =", np.abs(B1.data).mean() if B1.nnz > 0 else 0)
print("B2 mean abs =", np.abs(B2.data).mean() if B2.nnz > 0 else 0)


# @skfem.BilinearForm
# def b_top(u, v, w):
#     return np.einsum('i...,i...->...', v, u)

# @skfem.BilinearForm
# def b_bot(u, v, w):
#     return -np.einsum('i...,i...->...', v, u)


K = bmat([
    [K1,        None,     B1.T],
    [None,      K2,      -B2.T],
    [B1,       -B2,      None]
], format='csr')

# K = bmat([
#     [K1,        None,     B1.T],
#     [None,      K2,      -B2.T],
#     [B1,       -B2,      Mlam]
# ], format='csr')

print("K1:", K1.shape)
print("K2:", K2.shape)
print("B1:", B1.shape)
print("B2:", B2.shape)


# K = bmat([
#     [K1,  None,   B1.T],
#     [None, K2,    None],
#     [B1,   None,  Mlam]
# ], format='csr')
# rankK = np.linalg.matrix_rank(K.toarray())
# print(f"rank(K) = {rankK} / {K.shape[0]}")


# Assemble RHS vector
F_facet_ids = mesh_top.facets_satisfying(is_force_surface)
# F_facet_ids = mesh.facets_satisfying(lambda x: np.isclose(x[0], 1.0))

fbasis = skfem.FacetBasis(mesh_top, elem_v, facets=F_facet_ids)
total_force = 100.0  # [N]


@skfem.Functional
def surface_measure(w):
    return 1.0


A_proj_z = surface_measure.assemble(fbasis)
pressure = total_force / A_proj_z


# ---- traction: t = p * n (l(v) = ∫_Γ (p n) · v ds)----
@skfem.LinearForm
def l_comp(v, w):
    return - pressure * v[2]


top_F = l_comp.assemble(fbasis)
bot_F = np.zeros(mesh_bot.p.shape[1] * mesh_bot.dim())
F = np.hstack([top_F, bot_F, np.zeros(B1.shape[0])])


n1, n2, nl = basis_top.N, basis_bot.N, fb_lam_v.N
dirichlet_facets = mesh_bot.boundaries['dirichlet']
dirichlet_nodes = np.unique(
    mesh_bot.facets[:, dirichlet_facets].ravel()
)
D_dofs = basis_bot.get_dofs(
    nodes=dirichlet_nodes
).nodal["u^3"] + K1.shape[0]
# D_dofs = basis_bot.get_dofs(facets=dirichlet_facets).all() + K1.shape[0]

print('D_dofs', D_dofs.shape)
print("B2 nnz:", B2.nnz)


# res = skfem.condense(K, F, D=D_dofs)
# print(type(res), len(res))
# u = skfem.solve(Kc, Fc, uc, I)
print("before condense K.shape =", K.shape)
Kc, Fc, uc, I = skfem.condense(K, F, D=D_dofs)
# uc, info = minres(Kc, Fc)
uc, info = minres(Kc, Fc, rtol=1e-10, maxiter=20000)

u = np.zeros(K.shape[0])
u[I] = uc

print("Full system:")
print("K.shape =", K.shape)
print("F.shape =", F.shape)
print("Reduced system:")
print("Kc.shape =", Kc.shape)
print("Fc.shape =", Fc.shape)
print("len(I) =", len(I))
print("len(D_dofs) =", len(D_dofs))


F_from_U = Kc.dot(u[I])

residual = Fc - Kc @ uc
print("sum(abs(res)) =", np.sum(np.abs(residual)))


F_pred_full = K @ u   # full system prediction
print("K.shape, u.shape, F_pred_full.shape", K.shape, u.shape, F_pred_full.shape)
print("F_pred_full.shape, F.shape", F_pred_full.shape, F.shape)
residual_full = F_pred_full - F

print("len(F_pred_full) =", len(F_pred_full))
print("len(F) =", len(F))
print("‖residual‖₂ =", np.linalg.norm(residual_full))

res_norm = np.linalg.norm(Fc - Kc @ uc)
rhs_norm = np.linalg.norm(Fc)
print("relative residual =", res_norm / rhs_norm)
print("MINRES info =", info)


print("sum of absolute residuals", np.sum(np.abs(F_from_U - Fc)))
print("len(t1):", len(t1), "len(t2):", len(t2))
print("u", u)


def export_combined_vtu(
    mesh_top, mesh_bot,
    u, file_path="combined.vtu"
):
    import meshio

    # ---------------------------
    # 1. coords and cells
    # ---------------------------
    if isinstance(mesh_top, skfem.MeshTet):
        cell_type = "tetra"
    elif isinstance(mesh_top, skfem.MeshHex):
        cell_type = "hexahedron"
    else:
        raise ValueError("Unsupported mesh type")

    n_top_nodes = mesh_top.p.shape[1]
    n_bot_nodes = mesh_bot.p.shape[1]

    # points
    points = np.vstack([mesh_top.p.T, mesh_bot.p.T])

    # cells (offset bottom mesh node indices)
    cells = [(
        cell_type,
        np.vstack([
            mesh_top.t.T,
            mesh_bot.t.T + n_top_nodes
        ])
    )]

    # u_top_bot = u[:(n_top_nodes+n_bot_nodes)*3]
    n_nodes_total = n_top_nodes + n_bot_nodes
    u_top_bot = u[: n_nodes_total * 3].reshape(n_nodes_total, 3)

    # ---------------------------
    # 3. node_tag: Dirichlet / Force / Contact
    # ---------------------------
    node_tag = np.zeros(n_top_nodes + n_bot_nodes, dtype=int)
    top_nodes_contact = np.unique(mesh_top.facets[:, mesh_top.boundaries["contact"]])
    bot_nodes_contact = np.unique(mesh_bot.facets[:, mesh_bot.boundaries["contact"]]) + n_top_nodes
    nodes_contact = np.hstack([top_nodes_contact, bot_nodes_contact])
    # dofs_dirichlet = basis_top.get_dofs("dirichlet").all()
    bot_nodes_dirichlet = np.unique(
        mesh_bot.facets[:, mesh_bot.boundaries["dirichlet"]]
    ) + n_top_nodes
    nodes_dirichlet = bot_nodes_dirichlet
    top_nodes_force = np.unique(mesh_top.facets[:, mesh_top.boundaries["force"]])
    nodes_force = top_nodes_force
    node_tag[nodes_contact] = 1
    node_tag[nodes_dirichlet] = 2
    node_tag[nodes_force] = 3

    # ---------------------------
    # 4. cells_tag
    # ---------------------------
    # cells_tag = np.zeros(mesh_top.nelements + mesh_bot.nelements, dtype=int)

    meshio_mesh = meshio.Mesh(
        points=points,
        cells=cells,
        point_data={
            "u": u_top_bot,
            "node_tag": node_tag,
        },
        # cell_data={
        #     "cells_tag": [cells_tag]
        # }
    )

    meshio_mesh.write(file_path)
    print(f"✅ Exported combined VTU: {file_path}")


export_combined_vtu(
    mesh_top,
    mesh_bot,
    u
)

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 2)
ax1.plot(F_from_U)
# ax1.plot(Fc)
ax2.plot(Fc)

fig.savefig("mortar.png")


print(f"fb_u_top.N = {fb_u_top.N}")
print(f"fb_u_bot.N = {fb_u_bot.N}")
print(f"fb_lam_v.N = {fb_lam_v.N}")
print("B1 nnz:", B1.nnz, "B2 nnz:", B2.nnz)

print("K1:", K1.shape)
print("K2:", K2.shape)
print("B1:", B1.shape)
print("B2:", B2.shape)
print("basis_top.N:", basis_top.N)

print("min(u2[z]) =", np.min(u[K1.shape[0]:K1.shape[0]+K2.shape[0]:3]))
print("max(u2[z]) =", np.max(u[K1.shape[0]:K1.shape[0]+K2.shape[0]:3]))

overlap = np.intersect1d(dirichlet_nodes, np.unique(mesh_bot.facets[:, mesh_bot.boundaries["contact"]]))
print("overlap nodes:", overlap.size)

u1 = u[:B1.shape[0]]
u2 = u[B1.shape[0]:-B1.shape[0]]
λ = u[-B1.shape[0]:]
lambda_norm = np.linalg.norm(u[-B1.shape[0]:].reshape(-1,3), axis=1)
print("λ norm mean:", lambda_norm.mean(), "max:", lambda_norm.max())

print("‖K1 u1 + B1^T λ - f1‖ =", np.linalg.norm(K1 @ u1 + B1.T @ λ - top_F))
print("‖K2 u2 - B2^T λ - f2‖ =", np.linalg.norm(K2 @ u2 - B2.T @ λ - bot_F))
print("‖B1 u1 - B2 u2‖ =", np.linalg.norm(B1 @ u1 - B2 @ u2))