Skip to content

Commit

Permalink
Fix inconsistenies in the Nutils participants of the partitioned-heat…
Browse files Browse the repository at this point in the history
…-conduction tutorial (#496)

* Move Nutils code into respective participant folders

* Add logging to the run scripts of dirichlet- and neumann- Nutils
  • Loading branch information
IshaanDesai authored Mar 22, 2024
1 parent 3b07b1d commit f5377d9
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 84 deletions.
File renamed without changes.
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.

0 comments on commit f5377d9

Please sign in to comment.