Skip to content

Fix inconsistenies in the Nutils participants of the partitioned-heat-conduction tutorial #496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@
import precice


def main(side='Dirichlet', n=10, degree=1, timestep=.1, alpha=3., beta=1.2):

if side == 'Dirichlet':
x_grid = np.linspace(0, 1, n)
elif side == 'Neumann':
x_grid = np.linspace(1, 2, n)
else:
raise Exception('invalid side {!r}'.format(side))
def main(n=10, degree=1, timestep=.1, alpha=3., beta=1.2):

x_grid = np.linspace(0, 1, n)
y_grid = np.linspace(0, 1, n)

# define the Nutils mesh
domain, geom = mesh.rectilinear([x_grid, y_grid])
coupling_boundary = domain.boundary['right' if side ==
'Dirichlet' else 'left']
coupling_boundary = domain.boundary['right']
coupling_sample = coupling_boundary.sample('gauss', degree=degree * 2)

# Nutils namespace
Expand All @@ -44,56 +38,51 @@ def main(side='Dirichlet', n=10, degree=1, timestep=.1, alpha=3., beta=1.2):

# set boundary conditions at non-coupling boundaries
# top and bottom boundary are non-coupling for both sides
sqr = domain.boundary['top,bottom,left' if side == 'Dirichlet' else 'top,bottom,right'].integral(
'(u - uexact)^2 d:x' @ ns, degree=degree * 2)
sqr = domain.boundary['top,bottom,left'].integral('(u - uexact)^2 d:x' @ ns, degree=degree * 2)

if side == 'Dirichlet':
sqr += coupling_sample.integral('(u - readfunc)^2 d:x' @ ns)
else:
res += coupling_sample.integral('basis_n readfunc d:x' @ ns)
sqr += coupling_sample.integral('(u - readfunc)^2 d:x' @ ns)

# preCICE setup
participant = precice.Participant(side, "../precice-config.xml", 0, 1)
mesh_name = side + "-Mesh"
participant = precice.Participant('Dirichlet', "../precice-config.xml", 0, 1)
mesh_name = "Dirichlet-Mesh"
vertex_ids = participant.set_mesh_vertices(
mesh_name, coupling_sample.eval(ns.x))
precice_write = functools.partial(
participant.write_data,
mesh_name,
"Temperature" if side == "Neumann" else "Heat-Flux",
"Heat-Flux",
vertex_ids)
precice_read = functools.partial(
participant.read_data,
mesh_name,
"Heat-Flux" if side == "Neumann" else "Temperature",
"Temperature",
vertex_ids)

# helper functions to project heat flux to coupling boundary
if side == 'Dirichlet':
# To communicate the flux to the Neumann side we should not simply
# evaluate u_,i n_i as this is an unbounded term leading to suboptimal
# convergence. Instead we project ∀ v: ∫_Γ v flux = ∫_Γ v u_,i n_i and
# evaluate flux. While the right-hand-side contains the same unbounded
# term, we can use the strong identity du/dt - u_,ii = f to rewrite it
# to ∫_Ω [v (du/dt - f) + v_,i u_,i] - ∫_∂Ω\Γ v u_,k n_k, in which we
# recognize the residual and an integral over the exterior boundary.
# While the latter still contains the problematic unbounded term, we
# can use the fact that the flux is a known value at the top and bottom
# via the Dirichlet boundary condition, and impose it as constraints.
rightsqr = domain.boundary['right'].integral(
'flux^2 d:x' @ ns, degree=degree * 2)
rightcons = solver.optimize('fluxdofs', rightsqr, droptol=1e-10)
# rightcons is NaN in dofs that are NOT supported on the right boundary
fluxsqr = domain.boundary['right'].boundary['top,bottom'].integral(
'(flux - uexact_,0)^2 d:x' @ ns, degree=degree * 2)
fluxcons = solver.optimize('fluxdofs',
fluxsqr,
droptol=1e-10,
constrain=np.choose(np.isnan(rightcons),
[np.nan,
0.]))
# fluxcons is NaN in dofs that are supported on ONLY the right boundary
fluxres = coupling_sample.integral('basis_n flux d:x' @ ns) - res
# To communicate the flux to the Neumann side we should not simply
# evaluate u_,i n_i as this is an unbounded term leading to suboptimal
# convergence. Instead we project ∀ v: ∫_Γ v flux = ∫_Γ v u_,i n_i and
# evaluate flux. While the right-hand-side contains the same unbounded
# term, we can use the strong identity du/dt - u_,ii = f to rewrite it
# to ∫_Ω [v (du/dt - f) + v_,i u_,i] - ∫_∂Ω\Γ v u_,k n_k, in which we
# recognize the residual and an integral over the exterior boundary.
# While the latter still contains the problematic unbounded term, we
# can use the fact that the flux is a known value at the top and bottom
# via the Dirichlet boundary condition, and impose it as constraints.
rightsqr = domain.boundary['right'].integral(
'flux^2 d:x' @ ns, degree=degree * 2)
rightcons = solver.optimize('fluxdofs', rightsqr, droptol=1e-10)
# rightcons is NaN in dofs that are NOT supported on the right boundary
fluxsqr = domain.boundary['right'].boundary['top,bottom'].integral(
'(flux - uexact_,0)^2 d:x' @ ns, degree=degree * 2)
fluxcons = solver.optimize('fluxdofs',
fluxsqr,
droptol=1e-10,
constrain=np.choose(np.isnan(rightcons),
[np.nan,
0.]))
# fluxcons is NaN in dofs that are supported on ONLY the right boundary
fluxres = coupling_sample.integral('basis_n flux d:x' @ ns) - res

# write initial data
if participant.requires_initial_data():
Expand All @@ -118,8 +107,7 @@ def main(side='Dirichlet', n=10, degree=1, timestep=.1, alpha=3., beta=1.2):
x, u, uexact = bezier.eval(['x_i', 'u', 'uexact'] @ ns, lhs=lhs, t=t)
with treelog.add(treelog.DataLog()):
export.vtk(
side +
"-" +
"Dirichlet-" +
str(istep),
bezier.tri,
x,
Expand Down Expand Up @@ -157,14 +145,12 @@ def main(side='Dirichlet', n=10, degree=1, timestep=.1, alpha=3., beta=1.2):
lhs0=lhs0, dt=dt, t=t, readdata=readdata))

# write data to participant
if side == 'Dirichlet':
fluxdofs = solver.solve_linear(
'fluxdofs', fluxres, arguments=dict(
lhs0=lhs0, lhs=lhs, dt=dt, t=t), constrain=fluxcons)
write_data = coupling_sample.eval(
'flux' @ ns, fluxdofs=fluxdofs)
else:
write_data = coupling_sample.eval('u' @ ns, lhs=lhs)
fluxdofs = solver.solve_linear(
'fluxdofs', fluxres, arguments=dict(
lhs0=lhs0, lhs=lhs, dt=dt, t=t), constrain=fluxcons)
write_data = coupling_sample.eval(
'flux' @ ns, fluxdofs=fluxdofs)

precice_write(write_data)

# do the coupling
Expand Down
14 changes: 14 additions & 0 deletions partitioned-heat-conduction/dirichlet-nutils/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
set -e -u

. ../../tools/log.sh
exec > >(tee --append "$LOGFILE") 2>&1

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

rm -rf Dirichlet-*.vtk
NUTILS_RICHOUTPUT=no python3 heat.py

close_log
6 changes: 6 additions & 0 deletions partitioned-heat-conduction/neumann-nutils/clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
set -e -u

. ../../tools/cleaning-tools.sh

clean_nutils .
136 changes: 136 additions & 0 deletions partitioned-heat-conduction/neumann-nutils/heat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#! /usr/bin/env python3

from nutils import cli, mesh, function, solver, export
import functools
import treelog
import numpy as np
import precice


def main(n=10, degree=1, timestep=.1, alpha=3., beta=1.2):

x_grid = np.linspace(1, 2, n)
y_grid = np.linspace(0, 1, n)

# define the Nutils mesh
domain, geom = mesh.rectilinear([x_grid, y_grid])
coupling_boundary = domain.boundary['left']
coupling_sample = coupling_boundary.sample('gauss', degree=degree * 2)

# Nutils namespace
ns = function.Namespace()
ns.x = geom
ns.basis = domain.basis('std', degree=degree)
ns.alpha = alpha # parameter of problem
ns.beta = beta # parameter of problem
ns.u = 'basis_n ?lhs_n' # solution
ns.dudt = 'basis_n (?lhs_n - ?lhs0_n) / ?dt' # time derivative
ns.flux = 'basis_n ?fluxdofs_n' # heat flux
ns.f = 'beta - 2 - 2 alpha' # rhs
ns.uexact = '1 + x_0 x_0 + alpha x_1 x_1 + beta ?t' # analytical solution
ns.readbasis = coupling_sample.basis()
ns.readfunc = 'readbasis_n ?readdata_n'

# define the weak form
res = domain.integral(
'(basis_n dudt - basis_n f + basis_n,i u_,i) d:x' @ ns,
degree=degree * 2)

# set boundary conditions at non-coupling boundaries
# top and bottom boundary are non-coupling for both sides
sqr = domain.boundary['top,bottom,right'].integral('(u - uexact)^2 d:x' @ ns, degree=degree * 2)

res += coupling_sample.integral('basis_n readfunc d:x' @ ns)

# preCICE setup
participant = precice.Participant("Neumann", "../precice-config.xml", 0, 1)
mesh_name = "Neumann-Mesh"
vertex_ids = participant.set_mesh_vertices(
mesh_name, coupling_sample.eval(ns.x))
precice_write = functools.partial(
participant.write_data,
mesh_name,
"Temperature",
vertex_ids)
precice_read = functools.partial(
participant.read_data,
mesh_name,
"Heat-Flux",
vertex_ids)

# write initial data
if participant.requires_initial_data():
precice_write(coupling_sample.eval(0.))

participant.initialize()
precice_dt = participant.get_max_time_step_size()
solver_dt = timestep
dt = min(precice_dt, solver_dt)

t = 0.
istep = 0

# initial condition
sqr0 = domain.integral('(u - uexact)^2' @ ns, degree=degree * 2)
lhs = solver.optimize('lhs', sqr0, arguments=dict(t=t))
bezier = domain.sample('bezier', degree * 2)

while True:

# generate output
x, u, uexact = bezier.eval(['x_i', 'u', 'uexact'] @ ns, lhs=lhs, t=t)
with treelog.add(treelog.DataLog()):
export.vtk(
"Neumann-" +
str(istep),
bezier.tri,
x,
Temperature=u,
reference=uexact)

if not participant.is_coupling_ongoing():
break

# save checkpoint
if participant.requires_writing_checkpoint():
checkpoint = lhs, t, istep

# prepare next timestep
precice_dt = participant.get_max_time_step_size()
dt = min(solver_dt, precice_dt)
lhs0 = lhs
istep += 1
# read data from participant
readdata = precice_read(dt)
t += dt

# update (time-dependent) boundary condition
cons = solver.optimize(
'lhs',
sqr,
droptol=1e-15,
arguments=dict(
t=t,
readdata=readdata))

# solve nutils timestep
lhs = solver.solve_linear(
'lhs', res, constrain=cons, arguments=dict(
lhs0=lhs0, dt=dt, t=t, readdata=readdata))

# write data to participant
write_data = coupling_sample.eval('u' @ ns, lhs=lhs)
precice_write(write_data)

# do the coupling
participant.advance(dt)

# read checkpoint if required
if participant.requires_reading_checkpoint():
lhs, t, istep = checkpoint

participant.finalize()


if __name__ == '__main__':
cli.run(main)
2 changes: 2 additions & 0 deletions partitioned-heat-conduction/neumann-nutils/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nutils==7
pyprecice==3
14 changes: 14 additions & 0 deletions partitioned-heat-conduction/neumann-nutils/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
set -e -u

. ../../tools/log.sh
exec > >(tee --append "$LOGFILE") 2>&1

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

rm -rf Neumann-*.vtk
NUTILS_RICHOUTPUT=no python3 heat.py

close_log
29 changes: 0 additions & 29 deletions partitioned-heat-conduction/nutils/run.sh

This file was deleted.