Пример #1
0
 def solve(self, rhs=None, *, lhs0=None, constrain=None, rconstrain=None, **solverargs):
   nrows, ncols = self.shape
   if lhs0 is None:
     x = numpy.zeros(ncols)
   else:
     x = numpy.array(lhs0, dtype=float)
     assert x.shape == (ncols,)
   if constrain is None:
     J = numpy.ones(ncols, dtype=bool)
   else:
     assert constrain.shape == (ncols,)
     if constrain.dtype == bool:
       J = ~constrain
     else:
       J = numpy.isnan(constrain)
       x[~J] = constrain[~J]
   if rconstrain is None:
     assert nrows == ncols
     I = J
   else:
     assert rconstrain.shape == (nrows,) and constrain.dtype == bool
     I = ~rconstrain
   assert I.sum() == J.sum(), 'constrained matrix is not square: {}x{}'.format(I.sum(), J.sum())
   if rhs is None:
     rhs = 0.
   b = (rhs - self.matvec(x))[J]
   if b.any():
     x[J] += wrapped(self if I.all() and J.all() else self.submatrix(I, J), b, **solverargs)
     if not numpy.isfinite(x).all():
       raise MatrixError('solver returned non-finite left hand side')
     log.info('solver returned with residual {:.0e}'.format(numpy.linalg.norm((rhs - self.matvec(x))[J])))
   else:
     log.info('skipping solver because initial vector is exact')
   return x
Пример #2
0
    def test_send(self):
        def titles():
            a = yield 'value'
            while True:
                a = yield 'value={!r}'.format(a)

        with treelog.iter.wrap(titles(), 'abc') as items:
            for i, item in enumerate(items):
                self.assertEqual(item, 'abc'[i])
            treelog.info('hi')
        self.assertMessages(
            ('pushcontext', 'value'), ('recontext', "value='a'"),
            ('recontext', "value='b'"), ('recontext', "value='c'"),
            ('write', 'hi', treelog.proto.Level.info), ('popcontext', ))
Пример #3
0
 def generate(self):
     treelog.user('my message')
     with treelog.infofile('test.dat', 'w') as f:
         f.write('test1')
     with treelog.context('my context'):
         with treelog.iter.plain('iter', 'abc') as items:
             for c in items:
                 treelog.info(c)
         with treelog.context('empty'):
             pass
         treelog.error('multiple..\n  ..lines')
         with treelog.userfile('test.dat', 'wb') as f:
             treelog.info('generating')
             f.write(b'test2')
     self.generate_test()
     with treelog.context('context step={}', 0) as format:
         treelog.info('foo')
         format(1)
         treelog.info('bar')
     with treelog.errorfile('same.dat', 'wb') as f:
         f.write(b'test3')
     with treelog.debugfile('dbg.dat', 'wb') as f:
         f.write(b'test4')
     treelog.debug('dbg')
     treelog.warning('warn')
Пример #4
0
 def test_badvalue(self):
     status, output = self._cli('--outrootdir=' + self.outrootdir,
                                '--nopdb', '--iarg=1', '--farg=x',
                                '--sarg=1')
     with self.subTest('outdir'):
         self.assertFalse(
             os.path.isdir(os.path.join(self.outrootdir, self.scriptname)),
             'outdir directory found')
     with self.subTest('argparse'):
         log.info(output)
         self.assertNotIn('all OK', output)
     with self.subTest('exitstatus'):
         self.assertIsNotNone(status)
         self.assertEqual(status.code, 2)
Пример #5
0
 def __call__(self, p0, q0, p1, q1, relax):
     if not numpy.isfinite(p1):
         log.info('residual norm {} / {}'.format(p1, round(relax, 5)))
         return relax * self.minscale, False
     # To determine optimal relaxation we minimize a polynomial estimation for the residual norm:
     # P(scale) = A + B scale + C scale^2 + D scale^3 ~= |res(lhs+scale*relax*dlhs)|^2
     scale = _minimize2(p0=p0, q0=relax * q0, p1=p1, q1=relax * q1)
     log.info(
         'residual norm {:+.2f}% / {} with estimated minimum at {:.0f}%'.
         format(100 * numpy.sqrt(p1 / p0) - 100, round(relax, 5),
                scale * 100))
     relax *= min(max(scale, self.minscale), self.rebound)
     if not numpy.isfinite(relax) or relax <= self.failrelax:
         raise SolverError('stuck in local minimum')
     return min(relax, 1), p1 < p0 and scale > self.maxscale
Пример #6
0
 def test_good(self):
     args = [
         '--outrootdir=' + self.outrootdir, '--nopdb', '--iarg=1',
         '--farg=1', '--sarg=1'
     ]
     status, output = self._cli(*args)
     with self.subTest('outdir'):
         self.assertTrue(
             os.path.isdir(os.path.join(self.outrootdir, self.scriptname)),
             'output directory not found')
     with self.subTest('argparse'):
         log.info(output)
         self.assertIn('all OK', output)
     with self.subTest('exitstatus'):
         self.assertIsNotNone(status)
         self.assertEqual(status.code, 0)
Пример #7
0
    def run(self, collector: 'ResultCollector', context: Dict, workpath: Path,
            logdir: Path) -> bool:
        kwargs = {
            'cwd': workpath,
            'capture_output': True,
            'shell': False,
        }

        if isinstance(self._command, str):
            kwargs['shell'] = True
            command = render(self._command, context, mode='shell')
        else:
            command = [render(arg, context) for arg in self._command]

        with log.context(self.name):
            log.debug(
                command if isinstance(command, str) else ' '.join(command))
            with time() as duration:
                result = subprocess.run(command, **kwargs)
            duration = duration()

            if logdir:
                stdout_path = logdir / f'{self.name}.stdout'
                with open(stdout_path, 'wb') as f:
                    f.write(result.stdout)
                stderr_path = logdir / f'{self.name}.stderr'
                with open(stderr_path, 'wb') as f:
                    f.write(result.stderr)

            stdout = result.stdout.decode()
            for capture in self._capture:
                capture.find_in(collector, stdout)
            if self._capture_walltime:
                collector.collect(f'walltime/{self.name}', duration)

            if result.returncode:
                log.error(f"Command returned exit status {result.returncode}")
                if logdir:
                    log.error(f"stdout stored in {stdout_path}")
                    log.error(f"stderr stored in {stderr_path}")
                return False
            else:
                log.info(f"Success ({duration:.3g}s)")

        return True
Пример #8
0
 def solve(self,
           rhs=None,
           *,
           lhs0=None,
           constrain=None,
           rconstrain=None,
           **solverargs):
     nrows, ncols = self.shape
     if lhs0 is None:
         x = numpy.zeros(ncols)
     else:
         x = numpy.array(lhs0, dtype=float)
         assert x.shape == (ncols, )
     if constrain is None:
         J = numpy.ones(ncols, dtype=bool)
     else:
         assert constrain.shape == (ncols, )
         if constrain.dtype == bool:
             J = ~constrain
         else:
             J = numpy.isnan(constrain)
             x[~J] = constrain[~J]
     if rconstrain is None:
         assert nrows == ncols
         I = J
     else:
         assert rconstrain.shape == (nrows, ) and constrain.dtype == bool
         I = ~rconstrain
     assert I.sum() == J.sum(
     ), 'constrained matrix is not square: {}x{}'.format(I.sum(), J.sum())
     if rhs is None:
         rhs = 0.
     b = (rhs - self.matvec(x))[J]
     if b.any():
         x[J] += wrapped(
             self if I.all() and J.all() else self.submatrix(I, J), b,
             **solverargs)
         if not numpy.isfinite(x).all():
             raise MatrixError('solver returned non-finite left hand side')
         log.info('solver returned with residual {:.0e}'.format(
             numpy.linalg.norm((rhs - self.matvec(x))[J])))
     else:
         log.info('skipping solver because initial vector is exact')
     return x
Пример #9
0
Файл: ifem.py Проект: TheBB/SISO
    def combine_fields(self):
        """Combine fields which have the same suffix to a unified vectorized field, e.g.
        u_x, u_y, u_z -> u.
        """
        candidates = dict()
        for fname in self._fields:
            if len(fname) > 2 and fname[-2] == '_' and fname[
                    -1] in 'xyz' and fname[:-2] not in self._fields:
                candidates.setdefault(fname[:-2], []).append(fname)

        for fname, sourcenames in candidates.items():
            if not (1 < len(sourcenames) < 4):
                continue

            sourcenames = sorted(sourcenames, key=lambda s: s[-1])
            sources = [self._fields[s] for s in sourcenames]
            sourcenames = ', '.join(sourcenames)
            self._fields[fname] = CombinedField(fname, sources)
            log.info(f"Creating combined field {sourcenames} -> {fname}")
Пример #10
0
    def solve_withinfo(self, tol, maxiter=float('inf')):
        '''execute nonlinear solver, return lhs and info

    Like :func:`solve`, but return a 2-tuple of the solution and the
    corresponding info object which holds information about the final residual
    norm and other generator-dependent information.
    '''

        with log.iter.wrap(_progress(self.__class__.__name__, tol),
                           self) as items:
            i = 0
            for lhs, info in items:
                if info.resnorm <= tol:
                    break
                if i > maxiter:
                    raise SolverError('failed to reach target tolerance')
                i += 1
            log.info('converged in {} steps to residual {:.1e}'.format(
                i, info.resnorm))
        return lhs, info
Пример #11
0
 def solve(self, rhs):
   log.info('solving {0}x{0} system using MKL Pardiso'.format(self.shape[0]))
   if self._factors:
     log.info('reusing existing factorization')
     pardiso, iparm, mtype = self._factors
     phase = 33 # solve, iterative refinement
   else:
     pardiso = Pardiso()
     iparm = numpy.zeros(64, dtype=numpy.int32) # https://software.intel.com/en-us/mkl-developer-reference-c-pardiso-iparm-parameter
     iparm[0] = 1 # supply all values in components iparm[1:64]
     iparm[1] = 2 # fill-in reducing ordering for the input matrix: nested dissection algorithm from the METIS package
     iparm[9] = 13 # pivoting perturbation threshold 1e-13 (default for nonsymmetric)
     iparm[10] = 1 # enable scaling vectors (default for nonsymmetric)
     iparm[12] = 1 # enable improved accuracy using (non-) symmetric weighted matching (default for nonsymmetric)
     iparm[34] = 1 # zero base indexing
     mtype = 11 # real and nonsymmetric
     phase = 13 # analysis, numerical factorization, solve, iterative refinement
     self._factors = pardiso, iparm, mtype
   lhs = numpy.empty(self.shape[1], dtype=numpy.float64)
   pardiso(phase=phase, mtype=mtype, iparm=iparm, n=self.shape[0], nrhs=1, b=rhs, x=lhs, a=self.data, ia=self.indptr, ja=self.index[1])
   return lhs
Пример #12
0
 def _solver(self, rhs, solver, *, atol, rtol, **solverargs):
     if self.shape[0] != self.shape[1]:
         raise MatrixError(
             'constrained matrix is not square: {}x{}'.format(*self.shape))
     if rhs.shape[0] != self.shape[0]:
         raise MatrixError(
             'right-hand size shape does not match matrix shape')
     rhsnorm = numpy.linalg.norm(rhs, axis=0).max()
     atol = max(atol, rtol * rhsnorm)
     if rhsnorm <= atol:
         treelog.info(
             'skipping solver because initial vector is within tolerance')
         return numpy.zeros_like(rhs)
     solver_method, solver_name = self._method('solver', solver)
     treelog.info('solving {} dof system to {} using {} solver'.format(
         self.shape[0],
         'tolerance {:.0e}'.format(atol) if atol else 'machine precision',
         solver_name))
     try:
         lhs = solver_method(rhs, atol=atol, **solverargs)
     except MatrixError:
         raise
     except Exception as e:
         raise MatrixError('solver failed with error: {}'.format(e)) from e
     if not numpy.isfinite(lhs).all():
         raise MatrixError('solver returned non-finite left hand side')
     resnorm = numpy.linalg.norm(rhs - self @ lhs, axis=0).max()
     treelog.info('solver returned with residual {:.0e}'.format(resnorm))
     if resnorm > atol > 0:
         raise ToleranceNotReached(lhs)
     return lhs
Пример #13
0
 def solve(self, rhs, atol=0, solver='spsolve', callback=None, precon=None, **solverargs):
   if solver == 'spsolve':
     log.info('solving system using sparse direct solver')
     return scipy.sparse.linalg.spsolve(self.core, rhs)
   assert atol, 'tolerance must be specified for iterative solver'
   rhsnorm = numpy.linalg.norm(rhs)
   if rhsnorm <= atol:
     return numpy.zeros(self.shape[1])
   log.info('solving system using {} iterative solver'.format(solver))
   solverfun = getattr(scipy.sparse.linalg, solver)
   myrhs = rhs / rhsnorm # normalize right hand side vector for best control over scipy's stopping criterion
   mytol = atol / rhsnorm
   niter = numpy.array(0)
   def mycallback(arg):
     niter[...] += 1
     # some solvers provide the residual, others the left hand side vector
     res = numpy.linalg.norm(myrhs - self.matvec(arg)) if numpy.ndim(arg) == 1 else float(arg)
     if callback:
       callback(res)
     with log.context('residual {:.2e} ({:.0f}%)'.format(res, 100. * numpy.log10(res) / numpy.log10(mytol) if res > 0 else 0)):
       pass
   M = self.getprecon(precon) if isinstance(precon, str) else precon(self.core) if callable(precon) else precon
   mylhs, status = solverfun(self.core, myrhs, M=M, tol=mytol, callback=mycallback, **solverargs)
   if status != 0:
     raise MatrixError('{} solver failed with status {}'.format(solver, status))
   log.info('solver converged in {} iterations'.format(niter))
   return mylhs * rhsnorm
Пример #14
0
 def __call__(self, res0, dres0, res1, dres1):
     if not numpy.isfinite(res1).all():
         log.info('non-finite residual')
         return self.minscale, False
     # To determine optimal relaxation we minimize a polynomial estimation for
     # the residual norm: P(x) = p0 + q0 x + c x^2 + d x^3
     p0 = res0 @ res0
     q0 = 2 * res0 @ dres0
     p1 = res1 @ res1
     q1 = 2 * res1 @ dres1
     if q0 >= 0:
         raise SolverError('search vector does not reduce residual')
     c = math.fsum([-3 * p0, 3 * p1, -2 * q0, -q1])
     d = math.fsum([2 * p0, -2 * p1, q0, q1])
     # To minimize P we need to determine the roots for P'(x) = q0 + 2 c x + 3 d x^2
     # For numerical stability we use Citardauq's formula: x = -q0 / (c +/- sqrt(D)),
     # with D the discriminant
     D = c**2 - 3 * q0 * d
     # If D <= 0 we have at most one duplicate root, which we ignore. For D > 0,
     # taking into account that q0 < 0, we distinguish three situations:
     # - d > 0 => sqrt(D) > abs(c): one negative, one positive root
     # - d = 0 => sqrt(D) = abs(c): one negative root
     # - d < 0 => sqrt(D) < abs(c): two roots of same sign as c
     scale = -q0 / (c + math.sqrt(D)) if D > 0 and (c > 0
                                                    or d > 0) else math.inf
     if scale >= 1 and p1 > p0:  # this should not happen, but just in case
         log.info('failed to estimate scale factor')
         return self.minscale, False
     log.info(
         'estimated residual minimum at {:.0f}% of update vector'.format(
             scale * 100))
     return min(max(scale, self.minscale),
                self.maxscale), scale >= self.acceptscale and p1 < p0
Пример #15
0
 def getprecon(self, name):
   name = name.lower()
   assert self.shape[0] == self.shape[1], 'constrained matrix must be square'
   log.info('building {} preconditioner'.format(name))
   if name == 'splu':
     try:
       precon = self.scipy.sparse.linalg.splu(self.core.tocsc()).solve
     except RuntimeError as e:
       raise MatrixError(e) from e
   elif name == 'spilu':
     try:
       precon = self.scipy.sparse.linalg.spilu(self.core.tocsc(), drop_tol=1e-5, fill_factor=None, drop_rule=None, permc_spec=None, diag_pivot_thresh=None, relax=None, panel_size=None, options=None).solve
     except RuntimeError as e:
       raise MatrixError(e) from e
   elif name == 'diag':
     diag = self.core.diagonal()
     if not diag.all():
       raise MatrixError("building 'diag' preconditioner: diagonal has zero entries")
     precon = numpy.reciprocal(diag).__mul__
   else:
     raise MatrixError('invalid preconditioner {!r}'.format(name))
   return self.scipy.sparse.linalg.LinearOperator(self.shape, precon, dtype=float)
Пример #16
0
    def solve_withinfo(self, tol=0., maxiter=float('inf')):
        '''execute nonlinear solver, return lhs and info

    Like :func:`solve`, but return a 2-tuple of the solution and the
    corresponding info object which holds information about the final residual
    norm and other generator-dependent information.
    '''

        if not tol:
            warnings.deprecation(
                'solve with zero tolerance is deprecated and will be removed; proceeding with tol=1e-12'
            )
            tol = 1e-12
        with log.iter.wrap(_progress(self.__class__.__name__, tol),
                           self) as items:
            for i, (lhs, info) in enumerate(items):
                if info.resnorm <= tol:
                    break
                if i > maxiter:
                    raise SolverError('failed to reach target tolerance')
            log.info('converged with residual {:.1e}'.format(info.resnorm))
        return lhs, info
Пример #17
0
def setup(scriptname: str,
          kwargs: typing.List[typing.Tuple[str, str, str]],
          outrootdir: str = '~/public_html',
          outdir: typing.Optional[str] = None,
          cachedir: str = 'cache',
          cache: bool = False,
          nprocs: int = 1,
          matrix: str = 'auto',
          richoutput: typing.Optional[bool] = None,
          outrooturi: typing.Optional[str] = None,
          outuri: typing.Optional[str] = None,
          verbose: typing.Optional[int] = 4,
          pdb: bool = False,
          gracefulexit: bool = True,
          **unused):
    '''Set up compute environment.'''

    from . import cache as _cache, parallel as _parallel, matrix as _matrix

    for name in unused:
        warnings.warn(
            'ignoring unused configuration variable {!r}'.format(name))

    if outdir is None:
        outdir = os.path.join(os.path.expanduser(outrootdir), scriptname)
        if outrooturi is None:
            outrooturi = pathlib.Path(
                outrootdir).expanduser().resolve().as_uri()
        outuri = outrooturi.rstrip('/') + '/' + scriptname
    elif outuri is None:
        outuri = pathlib.Path(outdir).resolve().as_uri()

    if richoutput is None:
        richoutput = sys.stdout.isatty()

    consolellog = treelog.RichOutputLog() if richoutput else treelog.StdoutLog(
    )
    if verbose is not None:
        consolellog = treelog.FilterLog(consolellog,
                                        minlevel=tuple(Level)[5 - verbose])
    htmllog = _htmllog(outdir, scriptname, kwargs)

    if nprocs == 1:
        os.environ['MKL_THREADING_LAYER'] = 'SEQUENTIAL'

    with htmllog, \
         _status(outuri+'/'+htmllog.filename, richoutput), \
         treelog.set(treelog.TeeLog(consolellog, htmllog)), \
         _traceback(richoutput=richoutput, postmortem=pdb, exit=gracefulexit), \
         warnings.via(treelog.warning), \
         _cache.enable(os.path.join(outdir, cachedir)) if cache else _cache.disable(), \
         _parallel.maxprocs(nprocs), \
         _matrix.backend(matrix), \
         _signal_handler(signal.SIGINT, functools.partial(_breakpoint, richoutput)):

        treelog.info('nutils v{}'.format(_version()))
        treelog.info('start', time.ctime())
        yield
        treelog.info('finish', time.ctime())
Пример #18
0
 def getprecon(self, name):
   name = name.lower()
   assert self.shape[0] == self.shape[1], 'constrained matrix must be square'
   log.info('building {} preconditioner'.format(name))
   if name == 'splu':
     try:
       precon = scipy.sparse.linalg.splu(self.core.tocsc()).solve
     except RuntimeError as e:
       raise MatrixError(e) from e
   elif name == 'spilu':
     try:
       precon = scipy.sparse.linalg.spilu(self.core.tocsc(), drop_tol=1e-5, fill_factor=None, drop_rule=None, permc_spec=None, diag_pivot_thresh=None, relax=None, panel_size=None, options=None).solve
     except RuntimeError as e:
       raise MatrixError(e) from e
   elif name == 'diag':
     diag = self.core.diagonal()
     if not diag.all():
       raise MatrixError("building 'diag' preconditioner: diagonal has zero entries")
     precon = numpy.reciprocal(diag).__mul__
   else:
     raise MatrixError('invalid preconditioner {!r}'.format(name))
   return scipy.sparse.linalg.LinearOperator(self.shape, precon, dtype=float)
Пример #19
0
 def resume(self, history):
     mask, vmask = _invert(self.constrain, self.target)
     if history:
         lhs, info = history[-1]
         lhs, vlhs = _redict(lhs, self.target)
         res, jac = self._eval(lhs, mask)
         assert numpy.linalg.norm(res) == info.resnorm
         relax = info.relax
     else:
         lhs, vlhs = _redict(self.lhs0, self.target)
         res, jac = self._eval(lhs, mask)
         relax = self.relax0
         yield lhs, types.attributes(resnorm=numpy.linalg.norm(res),
                                     relax=relax)
     while True:
         dlhs = -jac.solve_leniently(
             res, **self.solveargs)  # compute new search vector
         res0 = res
         dres = jac @ dlhs  # == -res if dlhs was solved to infinite precision
         vlhs[vmask] += relax * dlhs
         res, jac = self._eval(lhs, mask)
         scale, accept = self.linesearch(res0, relax * dres, res,
                                         relax * (jac @ dlhs))
         while not accept:  # line search
             assert scale < 1
             oldrelax = relax
             relax *= scale
             if relax <= self.failrelax:
                 raise SolverError('stuck in local minimum')
             vlhs[vmask] += (relax - oldrelax) * dlhs
             res, jac = self._eval(lhs, mask)
             scale, accept = self.linesearch(res0, relax * dres, res,
                                             relax * (jac @ dlhs))
         log.info('update accepted at relaxation', round(relax, 5))
         relax = min(relax * scale, 1)
         yield lhs, types.attributes(resnorm=numpy.linalg.norm(res),
                                     relax=relax)
Пример #20
0
 def solve(self, rhs):
     log.info('solving {0}x{0} system using MKL Pardiso'.format(
         self.shape[0]))
     if self._factors:
         log.info('reusing existing factorization')
         pardiso, iparm, mtype = self._factors
         phase = 33  # solve, iterative refinement
     else:
         pardiso = Pardiso()
         iparm = numpy.zeros(
             64, dtype=numpy.int32
         )  # https://software.intel.com/en-us/mkl-developer-reference-c-pardiso-iparm-parameter
         iparm[0] = 1  # supply all values in components iparm[1:64]
         iparm[
             1] = 2  # fill-in reducing ordering for the input matrix: nested dissection algorithm from the METIS package
         iparm[
             9] = 13  # pivoting perturbation threshold 1e-13 (default for nonsymmetric)
         iparm[
             10] = 1  # enable scaling vectors (default for nonsymmetric)
         iparm[
             12] = 1  # enable improved accuracy using (non-) symmetric weighted matching (default for nonsymmetric)
         iparm[34] = 1  # zero base indexing
         mtype = 11  # real and nonsymmetric
         phase = 13  # analysis, numerical factorization, solve, iterative refinement
         self._factors = pardiso, iparm, mtype
     lhs = numpy.empty(self.shape[1], dtype=numpy.float64)
     pardiso(phase=phase,
             mtype=mtype,
             iparm=iparm,
             n=self.shape[0],
             nrhs=1,
             b=rhs,
             x=lhs,
             a=self.data,
             ia=self.indptr,
             ja=self.index[1])
     return lhs
Пример #21
0
def call(func, kwargs, scriptname, funcname=None):
  '''set up compute environment and call function'''

  outdir = config.outdir or os.path.join(os.path.expanduser(config.outrootdir), scriptname)

  with contextlib.ExitStack() as stack:

    stack.enter_context(cache.enable(os.path.join(outdir, config.cachedir)) if config.cache else cache.disable())
    stack.enter_context(matrix.backend(config.matrix))
    stack.enter_context(log.set(log.FilterLog(log.RichOutputLog() if config.richoutput else log.StdoutLog(), minlevel=5-config.verbose)))
    if config.htmloutput:
      htmllog = stack.enter_context(log.HtmlLog(outdir, title=scriptname, htmltitle='<a href="http://www.nutils.org">{}</a> {}'.format(SVGLOGO, html.escape(scriptname)), favicon=FAVICON))
      uri = (config.outrooturi.rstrip('/') + '/' + scriptname if config.outrooturi else pathlib.Path(outdir).resolve().as_uri()) + '/' + htmllog.filename
      if config.richoutput:
        t0 = time.perf_counter()
        bar = lambda running: '{0} [{1}] {2[0]}:{2[1]:02d}:{2[2]:02d}'.format(uri, 'RUNNING' if running else 'STOPPED', _hms(time.perf_counter()-t0))
        stack.enter_context(stickybar.activate(bar, update=1))
      else:
        log.info('opened log at', uri)
      htmllog.write('<ul style="list-style-position: inside; padding-left: 0px; margin-top: 0px;">{}</ul>'.format(''.join(
        '<li>{}={} <span style="color: gray;">{}</span></li>'.format(param.name, kwargs.get(param.name, param.default), param.annotation)
          for param in inspect.signature(func).parameters.values())), level=1, escape=False)
      stack.enter_context(log.add(htmllog))
    stack.enter_context(warnings.via(lambda msg: log.warning(msg)))
    stack.callback(signal.signal, signal.SIGINT, signal.signal(signal.SIGINT, _sigint_handler))

    log.info('nutils v{}'.format(_version()))
    log.info('start', time.ctime())
    try:
      func(**kwargs)
    except (KeyboardInterrupt, SystemExit, pdb.bdb.BdbQuit):
      log.error('killed by user')
      return 1
    except:
      log.error(traceback.format_exc())
      if config.pdb:
        print(_mkbox(
          'YOUR PROGRAM HAS DIED. The Python debugger',
          'allows you to examine its post-mortem state',
          'to figure out why this happened. Type "h"',
          'for an overview of commands to get going.'))
        pdb.post_mortem()
      return 2
    else:
      log.info('finish', time.ctime())
      return 0
Пример #22
0
        def solve(self,
                  rhs,
                  atol=0,
                  solver='spsolve',
                  callback=None,
                  precon=None,
                  **solverargs):
            if solver == 'spsolve':
                log.info('solving system using sparse direct solver')
                return scipy.sparse.linalg.spsolve(self.core, rhs)
            assert atol, 'tolerance must be specified for iterative solver'
            rhsnorm = numpy.linalg.norm(rhs)
            if rhsnorm <= atol:
                return numpy.zeros(self.shape[1])
            log.info('solving system using {} iterative solver'.format(solver))
            solverfun = getattr(scipy.sparse.linalg, solver)
            myrhs = rhs / rhsnorm  # normalize right hand side vector for best control over scipy's stopping criterion
            mytol = atol / rhsnorm
            niter = numpy.array(0)

            def mycallback(arg):
                niter[...] += 1
                # some solvers provide the residual, others the left hand side vector
                res = numpy.linalg.norm(myrhs -
                                        self.matvec(arg)) if numpy.ndim(
                                            arg) == 1 else float(arg)
                if callback:
                    callback(res)
                with log.context('residual {:.2e} ({:.0f}%)'.format(
                        res, 100. * numpy.log10(res) /
                        numpy.log10(mytol) if res > 0 else 0)):
                    pass

            M = self.getprecon(precon) if isinstance(precon, str) else precon(
                self.core) if callable(precon) else precon
            mylhs, status = solverfun(self.core,
                                      myrhs,
                                      M=M,
                                      tol=mytol,
                                      callback=mycallback,
                                      **solverargs)
            if status != 0:
                raise MatrixError('{} solver failed with status {}'.format(
                    solver, status))
            log.info('solver converged in {} iterations'.format(niter))
            return mylhs * rhsnorm
Пример #23
0
def main(nelems: int, degree: int, reynolds: float, rotation: float,
         timestep: float, maxradius: float, seed: int, endtime: float):
    '''
  Flow around a cylinder.

  .. arguments::

     nelems [24]
       Element size expressed in number of elements along the cylinder wall.
       All elements have similar shape with approximately unit aspect ratio,
       with elements away from the cylinder wall growing exponentially.
     degree [3]
       Polynomial degree for velocity space; the pressure space is one degree
       less.
     reynolds [1000]
       Reynolds number, taking the cylinder radius as characteristic length.
     rotation [0]
       Cylinder rotation speed.
     timestep [.04]
       Time step
     maxradius [25]
       Target exterior radius; the actual domain size is subject to integer
       multiples of the configured element size.
     seed [0]
       Random seed for small velocity noise in the intial condition.
     endtime [inf]
       Stopping time.
  '''

    elemangle = 2 * numpy.pi / nelems
    melems = int(numpy.log(2 * maxradius) / elemangle + .5)
    treelog.info('creating {}x{} mesh, outer radius {:.2f}'.format(
        melems, nelems, .5 * numpy.exp(elemangle * melems)))
    domain, geom = mesh.rectilinear([melems, nelems], periodic=(1, ))
    domain = domain.withboundary(inner='left', outer='right')

    ns = function.Namespace()
    ns.uinf = 1, 0
    ns.r = .5 * function.exp(elemangle * geom[0])
    ns.Re = reynolds
    ns.phi = geom[1] * elemangle  # add small angle to break element symmetry
    ns.x_i = 'r <cos(phi), sin(phi)>_i'
    ns.J = ns.x.grad(geom)
    ns.unbasis, ns.utbasis, ns.pbasis = function.chain([  # compatible spaces
        domain.basis(
            'spline', degree=(degree, degree - 1), removedofs=((0, ), None)),
        domain.basis('spline', degree=(degree - 1, degree)),
        domain.basis('spline', degree=degree - 1),
    ]) / function.determinant(ns.J)
    ns.ubasis_ni = 'unbasis_n J_i0 + utbasis_n J_i1'  # piola transformation
    ns.u_i = 'ubasis_ni ?lhs_n'
    ns.p = 'pbasis_n ?lhs_n'
    ns.sigma_ij = '(u_i,j + u_j,i) / Re - p δ_ij'
    ns.h = .5 * elemangle
    ns.N = 5 * degree / ns.h
    ns.rotation = rotation
    ns.uwall_i = '0.5 rotation <-sin(phi), cos(phi)>_i'

    inflow = domain.boundary['outer'].select(
        -ns.uinf.dotnorm(ns.x),
        ischeme='gauss1')  # upstream half of the exterior boundary
    sqr = inflow.integral('(u_i - uinf_i) (u_i - uinf_i)' @ ns,
                          degree=degree * 2)
    cons = solver.optimize(
        'lhs', sqr, droptol=1e-15)  # constrain inflow semicircle to uinf

    sqr = domain.integral('(u_i - uinf_i) (u_i - uinf_i) + p^2' @ ns,
                          degree=degree * 2)
    lhs0 = solver.optimize('lhs', sqr)  # set initial condition to u=uinf, p=0

    numpy.random.seed(seed)
    lhs0 *= numpy.random.normal(1, .1, lhs0.shape)  # add small velocity noise

    res = domain.integral(
        '(ubasis_ni u_i,j u_j + ubasis_ni,j sigma_ij + pbasis_n u_k,k) d:x'
        @ ns,
        degree=9)
    res += domain.boundary['inner'].integral(
        '(N ubasis_ni - (ubasis_ni,j + ubasis_nj,i) n_j) (u_i - uwall_i) d:x / Re'
        @ ns,
        degree=9)
    inertia = domain.integral('ubasis_ni u_i d:x' @ ns, degree=9)

    bbox = numpy.array(
        [[-2, 46 / 9],
         [-2, 2]])  # bounding box for figure based on 16x9 aspect ratio
    bezier0 = domain.sample('bezier', 5)
    bezier = bezier0.subset((bezier0.eval(
        (ns.x - bbox[:, 0]) * (bbox[:, 1] - ns.x)) > 0).all(axis=1))
    interpolate = util.tri_interpolator(
        bezier.tri, bezier.eval(ns.x),
        mergetol=1e-5)  # interpolator for quivers
    spacing = .05  # initial quiver spacing
    xgrd = util.regularize(bbox, spacing)

    with treelog.iter.plain(
            'timestep',
            solver.impliciteuler('lhs',
                                 residual=res,
                                 inertia=inertia,
                                 lhs0=lhs0,
                                 timestep=timestep,
                                 constrain=cons,
                                 newtontol=1e-10)) as steps:
        for istep, lhs in enumerate(steps):

            t = istep * timestep
            x, u, normu, p = bezier.eval(
                ['x_i', 'u_i', 'sqrt(u_k u_k)', 'p'] @ ns, lhs=lhs)
            ugrd = interpolate[xgrd](u)

            with export.mplfigure('flow.png', figsize=(12.8, 7.2)) as fig:
                ax = fig.add_axes([0, 0, 1, 1],
                                  yticks=[],
                                  xticks=[],
                                  frame_on=False,
                                  xlim=bbox[0],
                                  ylim=bbox[1])
                im = ax.tripcolor(x[:, 0],
                                  x[:, 1],
                                  bezier.tri,
                                  p,
                                  shading='gouraud',
                                  cmap='jet')
                import matplotlib.collections
                ax.add_collection(
                    matplotlib.collections.LineCollection(x[bezier.hull],
                                                          colors='k',
                                                          linewidths=.1,
                                                          alpha=.5))
                ax.quiver(xgrd[:, 0],
                          xgrd[:, 1],
                          ugrd[:, 0],
                          ugrd[:, 1],
                          angles='xy',
                          width=1e-3,
                          headwidth=3e3,
                          headlength=5e3,
                          headaxislength=2e3,
                          zorder=9,
                          alpha=.5)
                ax.plot(0,
                        0,
                        'k',
                        marker=(3, 2, t * rotation * 180 / numpy.pi - 90),
                        markersize=20)
                cax = fig.add_axes([0.9, 0.1, 0.01, 0.8])
                cax.tick_params(labelsize='large')
                fig.colorbar(im, cax=cax)

            if t >= endtime:
                break

            xgrd = util.regularize(bbox, spacing, xgrd + ugrd * timestep)

    return lhs0, lhs
Пример #24
0
def parsegmsh(fname: util.readtext, name='gmsh'):
    """Gmsh parser

  Parser for Gmsh files in `.msh` format. Only files with physical groups are
  supported. See the `Gmsh manual
  <http://geuz.org/gmsh/doc/texinfo/gmsh.html>`_ for details.

  Parameters
  ----------
  fname : :class:`str`
      Path to mesh file.

  Returns
  -------
  Keyword arguments for :func:`simplex`
  """

    # split sections
    sections = dict(
        re.findall(r'^\$(\w+)\n(.*)\n\$End\1$', fname,
                   re.MULTILINE | re.DOTALL))
    missing = {'MeshFormat', 'Nodes', 'Elements'}.difference(sections)
    if missing:
        raise ValueError(
            'invalid or incomplete gmsh data: missing section {}'.format(
                ', '.join(missing)))

    # parse section MeshFormat
    version, filetype, datasize = sections.pop('MeshFormat').split()
    if not version.startswith('2.'):
        raise ValueError(
            'gmsh version {} is not supported; please use -format msh2'.format(
                version))
    if filetype != '0':
        raise ValueError('binary gmsh data is not supported')

    # parse section PhysicalNames
    if 'PhysicalNames' not in sections:
        tagmapbydim = None
    else:
        N, *PhysicalNames = sections.pop('PhysicalNames').splitlines()
        assert int(N) == len(PhysicalNames)
        tagmapbydim = {}, {}, {}, {}  # tagid->tagname dictionary
        for line in PhysicalNames:
            nd, tagid, tagname = line.split(' ', 2)
            nd = int(nd)
            tagmapbydim[nd][tagid] = tagname.strip('"')

    # parse section Nodes
    N, *Nodes = sections.pop('Nodes').splitlines()
    nnodes = len(Nodes)
    assert int(N) == nnodes
    nodes = numpy.empty((nnodes, 3))
    nodemap = {}
    for i, line in enumerate(Nodes):
        n, *c = line.split()
        nodemap[n] = i
        nodes[i] = c
    assert not numpy.isnan(nodes).any()

    # parse section Elements
    N, *Elements = sections.pop('Elements').splitlines()
    assert int(N) == len(Elements)
    inodesbydim = [], [], [], []  # nelems-list of 4-tuples of node numbers
    tagnamesbydim = {}, {}, {}, {}  # tag->ielems dictionary
    etype2nd = {
        '15': 0,
        '1': 1,
        '2': 2,
        '4': 3,
        '8': 1,
        '9': 2,
        '11': 3,
        '26': 1,
        '21': 2,
        '23': 2,
        '27': 1
    }
    for line in Elements:
        n, e, t, m, *w = line.split()
        nd = etype2nd[e]
        ntags = int(t) - 1
        assert ntags >= 0
        inodes = tuple(nodemap[nodeid] for nodeid in w[ntags:])
        if not inodesbydim[nd] or inodesbydim[nd][
                -1] != inodes:  # multiple tags are repeated in consecutive lines
            inodesbydim[nd].append(inodes)
        if tagmapbydim:
            tagname = tagmapbydim[nd][m]
            tagnamesbydim[nd].setdefault(tagname,
                                         []).append(len(inodesbydim[nd]) - 1)
    inodesbydim = [
        numpy.array(e) if e else numpy.empty((0, nd + 1), dtype=int)
        for nd, e in enumerate(inodesbydim)
    ]

    # determine the dimension of the mesh
    while not inodesbydim[-1].size:
        inodesbydim.pop()
    ndims = len(inodesbydim) - 1  # topological dimension
    while nodes.shape[1] > ndims and not nodes[:, -1].any():
        nodes = nodes[:, :-1]

    # parse section Periodic
    N, *Periodic = sections.pop('Periodic', '0').splitlines()
    nperiodic = int(N)
    vertex_identities = []  # slave, master
    n = 0
    for line in Periodic:
        words = line.split()
        if words[0] == 'Affine':
            pass
        elif len(words) == 1:
            n = int(words[0])  # initialize for counting backwards
        elif len(words) == 2:
            vertex_identities.append([nodemap[w] for w in words])
            n -= 1
        else:
            assert len(words) == 3  # discard content
            assert n == 0  # check if batch of slave/master nodes matches announcement
            nperiodic -= 1
    assert nperiodic == 0  # check if number of periodic blocks matches announcement
    assert n == 0  # check if last batch of slave/master nodes matches announcement

    # warn about unused sections
    for section in sections:
        warnings.warn('section {!r} defined but not used'.format(section))

    # separate geometric dofs and sort vertices
    geomdofs = inodesbydim[ndims]
    if geomdofs.shape[1] > ndims + 1:  # higher order geometry
        inodesbydim = [n[:, :i + 1] for i, n in enumerate(inodesbydim)
                       ]  # remove high order info

    if vertex_identities:
        slaves, masters = numpy.array(vertex_identities).T
        keep = numpy.ones(len(nodes), dtype=bool)
        keep[slaves] = False
        assert keep[masters].all()
        renumber = keep.cumsum() - 1
        renumber[slaves] = renumber[masters]
        inodesbydim = [renumber[n] for n in inodesbydim]

    if geomdofs is inodesbydim[
            ndims]:  # geometry is linear and non-periodic, dofs follow in-place sorting of inodesbydim
        degree = 1
    elif geomdofs.shape[
            1] == ndims + 1:  # linear elements: match sorting of inodesbydim
        degree = 1
        shuffle = inodesbydim[ndims].argsort(axis=1)
        geomdofs = geomdofs[
            numpy.arange(len(geomdofs))[:, _],
            shuffle]  # gmsh conveniently places the primary ndim+1 vertices first
    else:  # higher order elements: match sorting of inodesbydim and renumber higher order coefficients
        degree, nodeorder = {  # for gmsh node ordering conventions see http://gmsh.info/doc/texinfo/gmsh.html#Node-ordering
            (2, 6): (2, (0, 3, 1, 5, 4, 2)),
            (2, 10): (3, (0, 3, 4, 1, 8, 9, 5, 7, 6, 2)),
            (2, 15): (4, (0, 3, 4, 5, 1, 11, 12, 13, 6, 10, 14, 7, 9, 8, 2)),
            (3, 10): (2, (0, 4, 1, 6, 5, 2, 7, 9, 8, 3))
        }[ndims, geomdofs.shape[1]]
        enum = numpy.empty([degree + 1] * (ndims + 1), dtype=int)
        bari = tuple(
            numpy.array([
                index[::-1] for index in numpy.ndindex(*enum.shape)
                if sum(index) == degree
            ]).T)
        enum[bari] = numpy.arange(
            geomdofs.shape[1]
        )  # maps baricentric index to corresponding enumerated index
        shuffle = inodesbydim[ndims].argsort(axis=1)
        geomdofs = geomdofs[:, nodeorder]  # convert from gmsh to nutils order
        for i in range(
                ndims
        ):  # strategy: apply shuffle to geomdofs by sequentially swapping vertices...
            for j in range(i + 1,
                           ndims + 1):  # ...considering all j > i pairs...
                m = shuffle[:,
                            i] == j  # ...and swap vertices if vertex j is shuffled into i...
                r = enum.swapaxes(
                    i, j
                )[bari]  # ...using the enum table to generate the appropriate renumbering
                geomdofs[m, :] = geomdofs[numpy.ix_(m, r)]
                m = shuffle[:, j] == i
                shuffle[m, j] = shuffle[
                    m, i]  # update shuffle to track changed vertex positions

    inodesbydim[ndims].sort(axis=1)
    if tagnamesbydim[ndims - 1]:
        inodesbydim[ndims - 1].sort(axis=1)
        edges = {
            tuple(inodes[:iedge]) + tuple(inodes[iedge + 1:]): (ielem, iedge)
            for ielem, inodes in enumerate(inodesbydim[ndims])
            for iedge in range(ndims + 1)
        }

    vtags = {
        name: numpy.array(inodes)
        for name, inodes in tagnamesbydim[ndims].items()
    }
    btags = {
        name: numpy.array([
            edges[tuple(inodesbydim[ndims - 1][ibelem])] for ibelem in ibelems
        ])
        for name, ibelems in tagnamesbydim[ndims - 1].items()
    }
    ptags = {
        name: inodesbydim[0][ipelems][..., 0]
        for name, ipelems in tagnamesbydim[0].items()
    }

    log.info('\n- '.join([
        'loaded {}d gmsh topology consisting of #{} elements'.format(
            ndims, len(geomdofs))
    ] + [
        name + ' groups: ' + ', '.join('{} #{}'.format(n, len(e))
                                       for n, e in tags.items())
        for name, tags in (('volume', vtags), ('boundary', btags),
                           ('point', ptags)) if tags
    ]))

    return dict(nodes=inodesbydim[ndims],
                cnodes=geomdofs,
                coords=nodes,
                tags=vtags,
                btags=btags,
                ptags=ptags)
Пример #25
0
def optimize(target: types.strictstr,
             functional: sample.strictintegral,
             *,
             tol: types.strictfloat = 0.,
             arguments: argdict = {},
             droptol: float = None,
             constrain: types.frozenarray = None,
             lhs0: types.frozenarray[types.strictfloat] = None,
             relax0: float = 1.,
             linesearch=None,
             failrelax: types.strictfloat = 1e-6,
             **kwargs):
    '''find the minimizer of a given functional

  Parameters
  ----------
  target : :class:`str`
      Name of the target: a :class:`nutils.function.Argument` in ``residual``.
  functional : scalar :class:`nutils.sample.Integral`
      The functional the should be minimized by varying target
  tol : :class:`float`
      Target residual norm.
  arguments : :class:`collections.abc.Mapping`
      Defines the values for :class:`nutils.function.Argument` objects in
      `residual`.  The ``target`` should not be present in ``arguments``.
      Optional.
  droptol : :class:`float`
      Threshold for leaving entries in the return value at NaN if they do not
      contribute to the value of the functional.
  constrain : :class:`numpy.ndarray` with dtype :class:`float`
      Defines the fixed entries of the coefficient vector
  lhs0 : :class:`numpy.ndarray`
      Coefficient vector, starting point of the iterative procedure.
  relax0 : :class:`float`
      Initial relaxation value.
  linesearch : :class:`nutils.solver.LineSearch`
      Callable that defines relaxation logic.
  failrelax : :class:`float`
      Fail with exception if relaxation reaches this lower limit.

  Yields
  ------
  :class:`numpy.ndarray`
      Coefficient vector corresponding to the functional optimum
  '''

    if linesearch is None:
        linesearch = NormBased.legacy(kwargs)
    solveargs = _strip(kwargs, 'lin')
    if kwargs:
        raise TypeError('unexpected keyword arguments: {}'.format(
            ', '.join(kwargs)))
    residual = functional.derivative(target)
    jacobian = residual.derivative(target)
    lhs, cons = _parse_lhs_cons(lhs0, constrain, residual.shape)
    val, res, jac = sample.eval_integrals(functional, residual, jacobian,
                                          **{target: lhs}, **arguments)
    if droptol is not None:
        nan = ~(cons | jac.rowsupp(droptol))
        cons = cons | nan
    resnorm = numpy.linalg.norm(res[~cons])
    if jacobian.contains(target):
        if tol <= 0:
            raise ValueError(
                'nonlinear optimization problem requires a nonzero "tol" argument'
            )
        solveargs.setdefault('rtol', 1e-3)
        firstresnorm = resnorm
        relax = relax0
        accept = True
        with log.context('newton {:.0f}%', 0) as reformat:
            while not numpy.isfinite(resnorm) or resnorm > tol:
                if accept:
                    reformat(100 * numpy.log(firstresnorm / resnorm) /
                             numpy.log(firstresnorm / tol))
                    lhs0 = lhs
                    dlhs = -jac.solve_leniently(
                        res, constrain=cons, **solveargs)
                    res0 = res[~cons]
                    dres0 = (
                        jac @ dlhs
                    )[~cons]  # == -res0 if dlhs was solved to infinite precision
                    resnorm0 = resnorm
                lhs = lhs0 + relax * dlhs
                val, res, jac = sample.eval_integrals(functional, residual,
                                                      jacobian,
                                                      **{target:
                                                         lhs}, **arguments)
                resnorm = numpy.linalg.norm(res[~cons])
                scale, accept = linesearch(res0, relax * dres0, res[~cons],
                                           relax * (jac @ dlhs)[~cons])
                relax = min(relax * scale, 1)
                if relax <= failrelax:
                    raise SolverError('stuck in local minimum')
            log.info('converged with residual {:.1e}'.format(resnorm))
    elif resnorm > tol:
        solveargs.setdefault('atol', tol)
        dlhs = -jac.solve(res, constrain=cons, **solveargs)
        lhs = lhs + dlhs
        val += (res + jac @ dlhs / 2).dot(dlhs)
    if droptol is not None:
        lhs = numpy.choose(nan, [lhs, numpy.nan])
        log.info('constrained {}/{} dofs'.format(
            len(lhs) - nan.sum(), len(lhs)))
    log.info('optimum value {:.2e}'.format(val))
    return lhs
Пример #26
0
def call(func, kwargs, scriptname, funcname=None):
    '''set up compute environment and call function'''

    outdir = config.outdir or os.path.join(
        os.path.expanduser(config.outrootdir), scriptname)

    with contextlib.ExitStack() as stack:

        stack.enter_context(
            cache.enable(os.path.join(outdir, config.cachedir)) if config.
            cache else cache.disable())
        stack.enter_context(matrix.backend(config.matrix))
        stack.enter_context(
            log.set(
                log.FilterLog(log.RichOutputLog()
                              if config.richoutput else log.StdoutLog(),
                              minlevel=5 - config.verbose)))
        if config.htmloutput:
            html = stack.enter_context(
                log.HtmlLog(
                    outdir,
                    title=scriptname,
                    htmltitle='<a href="http://www.nutils.org">{}</a> {}'.
                    format(SVGLOGO, scriptname),
                    favicon=FAVICON))
            uri = (config.outrooturi.rstrip('/') + '/' +
                   scriptname if config.outrooturi else pathlib.Path(
                       outdir).resolve().as_uri()) + '/' + html.filename
            if config.richoutput:
                t0 = time.perf_counter()
                bar = lambda running: '{0} [{1}] {2[0]}:{2[1]:02d}:{2[2]:02d}'.format(
                    uri, 'RUNNING'
                    if running else 'STOPPED', _hms(time.perf_counter() - t0))
                stack.enter_context(stickybar.activate(bar, update=1))
            else:
                log.info('opened log at', uri)
            html.write(
                '<ul style="list-style-position: inside; padding-left: 0px; margin-top: 0px;">{}</ul>'
                .format(''.join(
                    '<li>{}={} <span style="color: gray;">{}</span></li>'.
                    format(param.name, kwargs.get(param.name, param.default),
                           param.annotation)
                    for param in inspect.signature(func).parameters.values())),
                level=1,
                escape=False)
            stack.enter_context(log.add(html))
        stack.enter_context(warnings.via(log.warning))
        stack.callback(signal.signal, signal.SIGINT,
                       signal.signal(signal.SIGINT, _sigint_handler))

        log.info('nutils v{}'.format(_version()))
        log.info('start', time.ctime())
        try:
            func(**kwargs)
        except (KeyboardInterrupt, SystemExit, pdb.bdb.BdbQuit):
            log.error('killed by user')
            return 1
        except:
            log.error(traceback.format_exc())
            if config.pdb:
                print(
                    _mkbox('YOUR PROGRAM HAS DIED. The Python debugger',
                           'allows you to examine its post-mortem state',
                           'to figure out why this happened. Type "h"',
                           'for an overview of commands to get going.'))
                pdb.post_mortem()
            return 2
        else:
            log.info('finish', time.ctime())
            return 0
Пример #27
0
def main(nelems: int, etype: str, btype: str, degree: int,
         epsilon: typing.Optional[float], contactangle: float, timestep: float,
         mtol: float, seed: int, circle: bool, stab: stab):
    '''
  Cahn-Hilliard equation on a unit square/circle.

  .. arguments::

     nelems [20]
       Number of elements along domain edge.
     etype [square]
       Type of elements (square/triangle/mixed).
     btype [std]
       Type of basis function (std/spline), with availability depending on the
       configured element type.
     degree [2]
       Polynomial degree.
     epsilon []
       Interface thickness; defaults to an automatic value based on the
       configured mesh density if left unspecified.
     contactangle [90]
       Wall contact angle in degrees.
     timestep [.01]
       Time step.
     mtol [.01]
       Threshold value for chemical potential peak to peak difference, used as
       a stop criterion.
     seed [0]
       Random seed for the initial condition.
     circle [no]
       Select circular domain as opposed to a unit square.
     stab [linear]
       Stabilization method (linear/optimal/none).
  '''

    mineps = 1. / nelems
    if epsilon is None:
        treelog.info('setting epsilon={}'.format(mineps))
        epsilon = mineps
    elif epsilon < mineps:
        treelog.warning('epsilon under crititical threshold: {} < {}'.format(
            epsilon, mineps))

    domain, geom = mesh.unitsquare(nelems, etype)
    bezier = domain.sample('bezier', 5)  # sample for plotting

    ns = function.Namespace()
    if not circle:
        ns.x = geom
    else:
        angle = (geom - .5) * (numpy.pi / 2)
        ns.x = function.sin(angle) * function.cos(angle)[[1, 0
                                                          ]] / numpy.sqrt(2)
    ns.epsilon = epsilon
    ns.ewall = .5 * numpy.cos(contactangle * numpy.pi / 180)
    ns.cbasis = ns.mbasis = domain.basis('std', degree=degree)
    ns.c = 'cbasis_n ?c_n'
    ns.dc = 'cbasis_n (?c_n - ?c0_n)'
    ns.m = 'mbasis_n ?m_n'
    ns.F = '.5 (c^2 - 1)^2 / epsilon^2'
    ns.dF = stab.value
    ns.dt = timestep

    nrg_mix = domain.integral('F J(x)' @ ns, degree=7)
    nrg_iface = domain.integral('.5 sum:k(d(c, x_k)^2) J(x)' @ ns, degree=7)
    nrg_wall = domain.boundary.integral('(abs(ewall) + c ewall) J(x)' @ ns,
                                        degree=7)
    nrg = nrg_mix + nrg_iface + nrg_wall + domain.integral(
        '(dF - m dc - .5 dt epsilon^2 sum:k(d(m, x_k)^2)) J(x)' @ ns, degree=7)

    numpy.random.seed(seed)
    state = dict(c=numpy.random.normal(0, .5, ns.cbasis.shape),
                 m=numpy.random.normal(0, .5,
                                       ns.mbasis.shape))  # initial condition

    with treelog.iter.plain('timestep', itertools.count()) as steps:
        for istep in steps:

            E = sample.eval_integrals(nrg_mix, nrg_iface, nrg_wall, **state)
            treelog.user(
                'energy: {0:.3f} ({1[0]:.0f}% mixture, {1[1]:.0f}% interface, {1[2]:.0f}% wall)'
                .format(sum(E), 100 * numpy.array(E) / sum(E)))

            x, c, m = bezier.eval(['x', 'c', 'm'] @ ns, **state)
            export.triplot('phase.png', x, c, tri=bezier.tri, clim=(-1, 1))
            export.triplot('chempot.png', x, m, tri=bezier.tri)

            if numpy.ptp(m) < mtol:
                break

            state['c0'] = state['c']
            state = solver.optimize(['c', 'm'],
                                    nrg,
                                    arguments=state,
                                    tol=1e-10)

    return state
Пример #28
0
    def resume(self, history):
        mask, vmask = _invert(self.constrain, self.target)
        if history:
            lhs, info = history[-1]
            lhs, vlhs = _redict(lhs, self.target)
            nrg, res, jac = self._eval(lhs, mask)
            assert nrg == info.energy
            assert numpy.linalg.norm(res) == info.resnorm
            relax = info.relax
        else:
            lhs, vlhs = _redict(self.lhs0, self.target)
            nrg, res, jac = self._eval(lhs, mask)
            relax = 0
            yield lhs, types.attributes(resnorm=numpy.linalg.norm(res),
                                        energy=nrg,
                                        relax=relax)

        while True:
            nrg0 = nrg
            dlhs = -jac.solve_leniently(res, **self.solveargs)
            vlhs[vmask] += dlhs  # baseline: vanilla Newton

            # compute first two ritz values to determine approximate path of steepest descent
            dlhsnorm = numpy.linalg.norm(dlhs)
            k0 = dlhs / dlhsnorm
            k1 = -res / dlhsnorm  # = jac @ k0
            a = k1 @ k0
            k1 -= k0 * a  # orthogonalize
            c = numpy.linalg.norm(k1)
            k1 /= c  # normalize
            b = k1 @ (jac @ k1)
            # at this point k0 and k1 are orthonormal, and [k0 k1]^T jac [k0 k1] = [a c; c b]
            D = numpy.hypot(b - a, 2 * c)
            L = numpy.array([
                a + b - D, a + b + D
            ]) / 2  # 2nd order ritz values: eigenvalues of [a c; c b]
            v0, v1 = res + dlhs * L[:, numpy.newaxis]
            V = numpy.array(
                [v1, -v0]
            ).T / D  # ritz vectors times dlhs -- note: V.dot(L) = -res, V.sum() = dlhs
            log.info('spectrum: {:.1e}..{:.1e} ({}definite)'.format(
                *L, 'positive '
                if L[0] > 0 else 'negative ' if L[-1] < 0 else 'in'))

            eL = 0
            for irelax in itertools.count(
            ):  # line search along steepest descent curve
                r = numpy.exp(relax - numpy.log(D))  # = exp(relax) / D
                eL0 = eL
                eL = numpy.exp(-r * L)
                vlhs[vmask] -= V.dot(eL - eL0)
                nrg, res, jac = self._eval(lhs, mask)
                slope = res.dot(V.dot(eL * L))
                log.info('energy {:+.2e} / e{:+.1f} and {}creasing'.format(
                    nrg - nrg0, relax, 'in' if slope > 0 else 'de'))
                if numpy.isfinite(nrg) and numpy.isfinite(
                        res).all() and nrg <= nrg0 and slope <= 0:
                    relax += self.rampup
                    break
                relax += self.rampdown
                if relax <= self.failrelax:
                    raise SolverError('stuck in local minimum')

            yield lhs, types.attributes(resnorm=numpy.linalg.norm(res),
                                        energy=nrg,
                                        relax=relax)
Пример #29
0
def optimize(target,
             functional: sample.strictintegral,
             *,
             tol: types.strictfloat = 0.,
             arguments: argdict = {},
             droptol: float = None,
             constrain: arrayordict = None,
             lhs0: types.frozenarray[types.strictfloat] = None,
             relax0: float = 1.,
             linesearch=None,
             failrelax: types.strictfloat = 1e-6,
             **kwargs):
    '''find the minimizer of a given functional

  Parameters
  ----------
  target : :class:`str`
      Name of the target: a :class:`nutils.function.Argument` in ``residual``.
  functional : scalar :class:`nutils.sample.Integral`
      The functional the should be minimized by varying target
  tol : :class:`float`
      Target residual norm.
  arguments : :class:`collections.abc.Mapping`
      Defines the values for :class:`nutils.function.Argument` objects in
      `residual`.  The ``target`` should not be present in ``arguments``.
      Optional.
  droptol : :class:`float`
      Threshold for leaving entries in the return value at NaN if they do not
      contribute to the value of the functional.
  constrain : :class:`numpy.ndarray` with dtype :class:`float`
      Defines the fixed entries of the coefficient vector
  lhs0 : :class:`numpy.ndarray`
      Coefficient vector, starting point of the iterative procedure.
  relax0 : :class:`float`
      Initial relaxation value.
  linesearch : :class:`nutils.solver.LineSearch`
      Callable that defines relaxation logic.
  failrelax : :class:`float`
      Fail with exception if relaxation reaches this lower limit.

  Yields
  ------
  :class:`numpy.ndarray`
      Coefficient vector corresponding to the functional optimum
  '''

    if linesearch is None:
        linesearch = NormBased.legacy(kwargs)
    solveargs = _strip(kwargs, 'lin')
    if kwargs:
        raise TypeError('unexpected keyword arguments: {}'.format(
            ', '.join(kwargs)))
    if any(t not in functional.argshapes for t in target):
        if not droptol:
            raise ValueError(
                'target {} does not occur in integrand; consider setting droptol>0'
                .format(', '.join(t for t in target
                                  if t not in functional.argshapes)))
        target = [t for t in target if t in functional.argshapes]
        if not target:
            return {}
    residual = [functional.derivative(t) for t in target]
    jacobian = _derivative(residual, target)
    lhs0, constrain = _parse_lhs_cons(lhs0, constrain, target,
                                      functional.argshapes, arguments)
    mask, vmask = _invert(constrain, target)
    lhs, vlhs = _redict(lhs0, target)
    val, res, jac = _integrate_blocks(functional,
                                      residual,
                                      jacobian,
                                      arguments=lhs,
                                      mask=mask)
    if droptol is not None:
        supp = jac.rowsupp(droptol)
        res = res[supp]
        jac = jac.submatrix(supp, supp)
        nan = numpy.zeros_like(vmask)
        nan[vmask] = ~supp  # return value is set to nan if dof is not supported and not constrained
        vmask[
            vmask] = supp  # dof is computed if it is supported and not constrained
        assert vmask.sum() == len(res)
    resnorm = numpy.linalg.norm(res)
    if any(jacobian.contains(t) for jacobian in jacobian for t in target):
        if tol <= 0:
            raise ValueError(
                'nonlinear optimization problem requires a nonzero "tol" argument'
            )
        solveargs.setdefault('rtol', 1e-3)
        firstresnorm = resnorm
        relax = relax0
        accept = True
        with log.context('newton {:.0f}%', 0) as reformat:
            while not numpy.isfinite(resnorm) or resnorm > tol:
                if accept:
                    reformat(100 * numpy.log(firstresnorm / resnorm) /
                             numpy.log(firstresnorm / tol))
                    dlhs = -jac.solve_leniently(res, **solveargs)
                    res0 = res
                    dres = jac @ dlhs  # == -res0 if dlhs was solved to infinite precision
                    relax0 = 0
                vlhs[vmask] += (relax - relax0) * dlhs
                relax0 = relax  # currently applied relaxation
                val, res, jac = _integrate_blocks(functional,
                                                  residual,
                                                  jacobian,
                                                  arguments=lhs,
                                                  mask=mask)
                resnorm = numpy.linalg.norm(res)
                scale, accept = linesearch(res0, relax * dres, res,
                                           relax * (jac @ dlhs))
                relax = min(relax * scale, 1)
                if relax <= failrelax:
                    raise SolverError('stuck in local minimum')
            log.info('converged with residual {:.1e}'.format(resnorm))
    elif resnorm > tol:
        solveargs.setdefault('atol', tol)
        dlhs = -jac.solve(res, **solveargs)
        vlhs[vmask] += dlhs
        val += (res + jac @ dlhs / 2).dot(dlhs)
    if droptol is not None:
        vlhs[nan] = numpy.nan
        log.info('constrained {}/{} dofs'.format(
            len(vlhs) - nan.sum(), len(vlhs)))
    log.info('optimum value {:.2e}'.format(val))
    return lhs
Пример #30
0
  return iter(title, builtins.range(*args))

def enumerate(title, iterable):
  warnings.deprecation('log.enumerate is deprecated; use log.iter.percentage instead')
  return iter(title, builtins.enumerate(iterable), length=_len(iterable))

def zip(title, *iterables):
  warnings.deprecation('log.zip is deprecated; use log.iter.percentage instead')
  return iter(title, builtins.zip(*iterables), length=min(map(_len, iterables)))

def count(title, start=0, step=1):
  warnings.deprecation('log.count is deprecated; use log.iter.percentage instead')
  return iter(title, itertools.count(start, step))

if distutils.version.StrictVersion(treelog.version) >= distutils.version.StrictVersion('1.0b5'):
  from treelog import debug, info, user, warning, error, debugfile, infofile, userfile, warningfile, errorfile, context
else:
  debug = lambda *args, **kwargs: treelog.debug(*args, **kwargs)
  info = lambda *args, **kwargs: treelog.info(*args, **kwargs)
  user = lambda *args, **kwargs: treelog.user(*args, **kwargs)
  warning = lambda *args, **kwargs: treelog.warning(*args, **kwargs)
  error = lambda *args, **kwargs: treelog.error(*args, **kwargs)
  debugfile = lambda *args, **kwargs: treelog.debugfile(*args, **kwargs)
  infofile = lambda *args, **kwargs: treelog.infofile(*args, **kwargs)
  userfile = lambda *args, **kwargs: treelog.userfile(*args, **kwargs)
  warningfile = lambda *args, **kwargs: treelog.warningfile(*args, **kwargs)
  errorfile = lambda *args, **kwargs: treelog.errorfile(*args, **kwargs)
  context = lambda *args, **kwargs: treelog.context(title, *initargs, **initkwargs)

# vim:sw=2:sts=2:et
Пример #31
0
def parsegmsh(mshdata):
    """Gmsh parser

  Parser for Gmsh data in ``msh2`` or ``msh4`` format. See the `Gmsh manual
  <http://geuz.org/gmsh/doc/texinfo/gmsh.html>`_ for details.

  Parameters
  ----------
  mshdata : :class:`io.BufferedIOBase`
      Msh file contents.

  Returns
  -------
  :class:`dict`:
      Keyword arguments for :func:`simplex`
  """

    try:
        from meshio import gmsh
    except ImportError as e:
        raise Exception(
            'parsegmsh requires the meshio module to be installed') from e

    msh = gmsh.main.read_buffer(mshdata)

    if not msh.cell_sets:
        # Old versions of the gmsh file format repeat elements that have multiple
        # tags. To support this we edit the meshio data to bring it in the same
        # form as the new files by deduplicating cells and creating cell_sets.
        renums = []
        for icell, cells in enumerate(msh.cells):
            keep = (cells.data[1:] != cells.data[:-1]).any(axis=1)
            if keep.all():
                renum = numpy.arange(len(cells.data))
            else:
                msh.cells[icell] = cells._replace(
                    data=cells.data[numpy.hstack([True, keep])])
                renum = numpy.hstack([0, keep.cumsum()])
            renums.append(renum)
        for name, (itag, nd) in msh.field_data.items():
            msh.cell_sets[name] = [
                renum[data == itag]
                for data, renum in zip(msh.cell_data['gmsh:physical'], renums)
            ]

    # Coords is a 2d float-array such that coords[inode,idim] == coordinate.
    coords = msh.points

    # Nodes is a dictionary that maps a topological dimension to a 2d int-array
    # dictionary such that nodes[nd][ielem,ilocal] == inode, where ilocal < nd+1
    # for linear geometries or larger for higher order geometries. Since meshio
    # stores nodes by simplex type and cell, simplex types are mapped to
    # dimensions and gathered, after which cells are concatenated under the
    # assumption that there is only one simplex type per dimension.
    nodes = {('ver', 'lin', 'tri', 'tet').index(typename[:3]):
             numpy.concatenate(datas, axis=0)
             for typename, datas in util.gather(
                 (cells.type, cells.data) for cells in msh.cells)}

    # Identities is a 2d [master, slave] int-aray that pairs matching nodes on
    # periodic walls. For the topological connectivity, all slaves in the nodes
    # arrays will be replaced by their master counterpart.
    identities = numpy.zeros((0, 2), dtype=int) if not msh.gmsh_periodic \
      else numpy.concatenate([d for a, b, c, d in msh.gmsh_periodic], axis=0)

    # It may happen that meshio provides periodicity relations for nodes that
    # have no associated coordinate, typically because they are not part of any
    # physical group. We need to filter these out to avoid errors further down.
    mask = identities < len(coords)
    keep = mask.any(axis=1)
    assert mask[keep].all()
    identities = identities[keep]

    # Tags is a list of (nd, name, ndelems) tuples that define topological groups
    # per dimension. Since meshio associates group names with cells, which are
    # concatenated in nodes, element ids are offset and concatenated to match.
    tags = [
        (
            nd,
            name,
            numpy.concatenate([
                selection + sum(
                    len(cells.data) for cells in msh.cells[:icell] if
                    cells.type == msh.cells[icell].type)  # offset into nodes
                for icell, selection in enumerate(msh.cell_sets[name])
            ])) for name, (itag, nd) in msh.field_data.items()
    ]

    # determine the dimension of the topology
    ndims = max(nodes)

    # determine the dimension of the geometry
    assert not numpy.isnan(coords).any()
    while coords.shape[1] > ndims and not coords[:, -1].any():
        coords = coords[:, :-1]

    # separate geometric, topological nodes
    cnodes = nodes[ndims]
    if cnodes.shape[1] > ndims + 1:  # higher order geometry
        nodes = {nd: n[:, :nd + 1]
                 for nd, n in nodes.items()}  # remove high order info

    if len(identities):
        slaves, masters = identities.T
        keep = numpy.ones(len(coords), dtype=bool)
        keep[slaves] = False
        assert keep[masters].all()
        renumber = keep.cumsum() - 1
        renumber[slaves] = renumber[masters]
        nodes = {nd: renumber[n] for nd, n in nodes.items()}

    vnodes = nodes[ndims]
    bnodes = nodes.get(ndims - 1)
    pnodes = nodes.get(0)

    if cnodes is vnodes:  # geometry is linear and non-periodic, dofs follow in-place sorting of nodes
        degree = 1
    elif cnodes.shape[
            1] == ndims + 1:  # linear elements: match sorting of nodes
        degree = 1
        shuffle = vnodes.argsort(axis=1)
        cnodes = cnodes[
            numpy.arange(len(cnodes))[:, _],
            shuffle]  # gmsh conveniently places the primary ndim+1 vertices first
    else:  # higher order elements: match sorting of nodes and renumber higher order coefficients
        degree, nodeorder = {  # for meshio's node ordering conventions see http://www.vtk.org/VTK/img/file-formats.pdf
            (2, 6): (2, (0, 3, 1, 5, 4, 2)),
            (2, 10): (3, (0, 3, 4, 1, 8, 9, 5, 7, 6, 2)),
            (2, 15): (4, (0, 3, 4, 5, 1, 11, 12, 13, 6, 10, 14, 7, 9, 8, 2)),
            (3, 10): (2, (0, 4, 1, 6, 5, 2, 7, 8, 9, 3))
        }[ndims, cnodes.shape[1]]
        enum = numpy.empty([degree + 1] * (ndims + 1), dtype=int)
        bari = tuple(
            numpy.array([
                index[::-1] for index in numpy.ndindex(*enum.shape)
                if sum(index) == degree
            ]).T)
        enum[bari] = numpy.arange(
            cnodes.shape[1]
        )  # maps baricentric index to corresponding enumerated index
        shuffle = vnodes.argsort(axis=1)
        cnodes = cnodes[:, nodeorder]  # convert from gmsh to nutils order
        for i in range(
                ndims
        ):  # strategy: apply shuffle to cnodes by sequentially swapping vertices...
            for j in range(i + 1,
                           ndims + 1):  # ...considering all j > i pairs...
                m = shuffle[:,
                            i] == j  # ...and swap vertices if vertex j is shuffled into i...
                r = enum.swapaxes(
                    i, j
                )[bari]  # ...using the enum table to generate the appropriate renumbering
                cnodes[m, :] = cnodes[numpy.ix_(m, r)]
                m = shuffle[:, j] == i
                shuffle[m, j] = shuffle[
                    m, i]  # update shuffle to track changed vertex positions

    vnodes.sort(axis=1)
    nnodes = vnodes[:, -1].max() + 1

    vtags, btags, ptags = {}, {}, {}
    edge_vertices = numpy.arange(ndims + 1).repeat(ndims).reshape(
        ndims, ndims + 1)[:, ::-1].T  # nedges x ndims
    for nd, name, ielems in tags:
        if nd == ndims:
            vtags[name] = numpy.array(ielems)
        elif nd == ndims - 1:
            edgenodes = bnodes[ielems]  # all edge elements in msh file
            nodemask = numeric.asboolean(
                edgenodes.ravel(), size=nnodes,
                ordered=False)  # all elements sharing at least 1 edge node
            ielems, = (nodemask[vnodes].sum(axis=1) >= ndims).nonzero(
            )  # all elements sharing at least ndims edge nodes
            edgemap = {
                tuple(b): (ielem, iedge)
                for ielem, a in zip(
                    ielems, vnodes[ielems[:, _, _], edge_vertices[_, :, :]])
                for iedge, b in enumerate(a)
            }
            belems = (
                edgemap.get(tuple(sorted(n))) for n in edgenodes
            )  # map every edge element to its corresponding (ielem, iedge) combination
            belems = filter(
                None, belems
            )  # remove spurious edge elements that have no adjacent volume element
            btags[name] = numpy.array(list(belems))
        elif nd == 0:
            ptags[name] = pnodes[ielems][..., 0]

    log.info('\n- '.join([
        'loaded {}d gmsh topology consisting of #{} elements'.format(
            ndims, len(cnodes))
    ] + [
        name + ' groups: ' + ', '.join('{} #{}'.format(n, len(e))
                                       for n, e in tags.items())
        for name, tags in (('volume', vtags), ('boundary', btags),
                           ('point', ptags)) if tags
    ]))

    return dict(nodes=vnodes,
                cnodes=cnodes,
                coords=coords,
                tags=vtags,
                btags=btags,
                ptags=ptags)
Пример #32
0
  def solve(self, rhs=None, *, lhs0=None, constrain=None, rconstrain=None, solver='direct', atol=0., rtol=0., **solverargs):
    '''Solve system given right hand side vector and/or constraints.

    Args
    ----
    rhs : :class:`float` vector or :any:`None`
        Right hand side vector. `None` implies all zeros.
    lhs0 : class:`float` vector or :any:`None`
        Initial values. `None` implies all zeros.
    constrain : :class:`float` or :class:`bool` array, or :any:`None`
        Column constraints. For float values, a number signifies a constraint,
        NaN signifies a free dof. For boolean, a True value signifies a
        constraint to the value in `lhs0`, a False value signifies a free dof.
        `None` implies no constraints.
    rconstrain : :class:`bool` array or :any:`None`
        Row constrains. A True value signifies a constrains, a False value a free
        dof. `None` implies that the constraints follow those defined in
        `constrain` (by implication the matrix must be square).
    solver : :class:`str`
        Name of the solver algorithm. The set of available solvers depends on
        the type of the matrix (i.e. the active backend), although all matrices
        should implement at least the 'direct' solver.
    **kwargs :
        All remaining arguments are passed on to the selected solver method.

    Returns
    -------
    :class:`numpy.ndarray`
        Left hand side vector.
    '''
    nrows, ncols = self.shape
    if rhs is None:
      rhs = numpy.zeros(nrows)
    if lhs0 is None:
      x = numpy.zeros((ncols,)+rhs.shape[1:])
    else:
      x = numpy.array(lhs0, dtype=float)
      while x.ndim < rhs.ndim:
        x = x[...,numpy.newaxis].repeat(rhs.shape[x.ndim], axis=x.ndim)
      assert x.shape == (ncols,)+rhs.shape[1:]
    if constrain is None:
      J = numpy.ones(ncols, dtype=bool)
    else:
      assert constrain.shape == (ncols,)
      if constrain.dtype == bool:
        J = ~constrain
      else:
        J = numpy.isnan(constrain)
        x[~J] = constrain[~J]
    if rconstrain is None:
      assert nrows == ncols
      I = J
    else:
      assert rconstrain.shape == (nrows,) and constrain.dtype == bool
      I = ~rconstrain
    n = I.sum()
    if J.sum() != n:
      raise MatrixError('constrained matrix is not square: {}x{}'.format(I.sum(), J.sum()))
    b = (rhs - self @ x)[J]
    bnorm = numpy.linalg.norm(b)
    atol = max(atol, rtol * bnorm)
    if bnorm > atol:
      log.info('solving {} dof system to {} using {} solver'.format(n, 'tolerance {:.0e}'.format(atol) if atol else 'machine precision', solver))
      try:
        x[J] += getattr(self.submatrix(I, J), 'solve_'+solver)(b, atol=atol, **solverargs)
      except Exception as e:
        raise MatrixError('solver failed with error: {}'.format(e)) from e
      if not numpy.isfinite(x).all():
        raise MatrixError('solver returned non-finite left hand side')
      resnorm = numpy.linalg.norm((rhs - self @ x)[J])
      log.info('solver returned with residual {:.0e}'.format(resnorm))
      if resnorm > atol > 0:
        raise ToleranceNotReached(x)
    else:
      log.info('skipping solver because initial vector is within tolerance')
    return x
Пример #33
0
def gmsh(fname, name='gmsh'):
  """Gmsh parser

  Parser for Gmsh files in `.msh` format. Only files with physical groups are
  supported. See the `Gmsh manual
  <http://geuz.org/gmsh/doc/texinfo/gmsh.html>`_ for details.

  Parameters
  ----------
  fname : :class:`str`
      Path to mesh file.
  name : :class:`str` or :any:`None`
      Name of parsed topology, defaults to 'gmsh'.

  Returns
  -------
  topo : :class:`nutils.topology.SimplexTopology`
      Topology of parsed Gmsh file.
  geom : :class:`nutils.function.Array`
      Isoparametric map.
  """

  # create lines iterable
  if isinstance(fname, pathlib.Path):
    lines = fname.open()
  elif isinstance(fname, str):
    if fname.startswith('$MeshFormat'):
      lines = iter(fname.splitlines())
    else:
      lines = open(fname)
  else:
    raise ValueError("expected the contents of a Gmsh MSH file (as 'str') or a filename (as 'str' or 'pathlib.Path') but got {!r}".format(fname))

  # split sections
  sections = {}
  for line in lines:
    line = line.strip()
    assert line[0]=='$'
    sname = line[1:]
    slines = []
    for sline in lines:
      sline = sline.strip()
      if sline == '$End'+sname:
        break
      slines.append(sline)
    sections[sname] = slines

  # discard section MeshFormat
  sections.pop('MeshFormat', None)

  # parse section PhysicalNames
  PhysicalNames = sections.pop('PhysicalNames', [0])
  assert int(PhysicalNames[0]) == len(PhysicalNames)-1
  tagmapbydim = {}, {}, {}, {} # tagid->tagname dictionary
  for line in PhysicalNames[1:]:
    nd, tagid, tagname = line.split(' ', 2)
    nd = int(nd)
    tagmapbydim[nd][tagid] = tagname.strip('"')

  # determine the dimension of the mesh
  ndims = 2 if not tagmapbydim[3] else 3
  if ndims == 3 and tagmapbydim[1]:
    raise NotImplementedError('Physical line groups are not supported in volumetric meshes')

  # parse section Nodes
  Nodes = sections.pop('Nodes')
  nnodes = len(Nodes)-1
  assert int(Nodes[0]) == nnodes
  nodes = numpy.empty((nnodes, 3))
  nodemap = {}
  for i, line in enumerate(Nodes[1:]):
    n, *c = line.split()
    nodemap[n] = i
    nodes[i] = c
  assert not numpy.isnan(nodes).any()
  if ndims == 2:
    assert numpy.all(nodes[:,2]) == 0, 'Non-zero z-coordinates found in 2D mesh.'
    nodes = nodes[:,:2]

  # parse section Elements
  Elements = sections.pop('Elements')
  assert int(Elements[0]) == len(Elements)-1
  inodesbydim = [], [], [], [] # nelems-list of 4-tuples of node numbers
  tagnamesbydim = {}, {}, {}, {} # tag->ielems dictionary
  etype2nd = {'15': 0, '1': 1, '2': 2, '4': 3, '8': 1, '9': 2}
  for line in Elements[1:]:
    n, e, t, m, *w = line.split()
    nd = etype2nd[e]
    ntags = int(t) - 1
    assert ntags >= 0
    tagname = tagmapbydim[nd][m]
    inodes = tuple(nodemap[nodeid] for nodeid in w[ntags:])
    if not inodesbydim[nd] or inodesbydim[nd][-1] != inodes: # multiple tags are repeated in consecutive lines
      inodesbydim[nd].append(inodes)
    tagnamesbydim[nd].setdefault(tagname, []).append(len(inodesbydim[nd])-1)
  inodesbydim = [numpy.array(e) if e else numpy.empty((0,nd+1), dtype=int) for nd, e in enumerate(inodesbydim)]

  # parse section Periodic
  Periodic = sections.pop('Periodic', [0])
  nperiodic = int(Periodic[0])
  vertex_identities = [] # slave, master
  n = 0
  for line in Periodic[1:]:
    words = line.split()
    if len(words) == 1:
      n = int(words[0]) # initialize for counting backwards
    elif len(words) == 2:
      vertex_identities.append([nodemap[w] for w in words])
      n -= 1
    else:
      assert len(words) == 3 # discard content
      assert n == 0 # check if batch of slave/master nodes matches announcement
      nperiodic -= 1
  assert nperiodic == 0 # check if number of periodic blocks matches announcement
  assert n == 0 # check if last batch of slave/master nodes matches announcement

  # warn about unused sections
  for section in sections:
    warnings.warn('section {!r} defined but not used'.format(section))

  # separate geometric dofs and sort vertices
  geomdofs = inodesbydim[ndims]
  if geomdofs.shape[1] > ndims+1: # higher order geometry
    inodesbydim = [n[:,:i+1] for i, n in enumerate(inodesbydim)] # remove high order info

  if vertex_identities:
    slaves, masters = numpy.array(vertex_identities).T
    keep = numpy.ones(len(nodes), dtype=bool)
    keep[slaves] = False
    assert keep[masters].all()
    renumber = keep.cumsum()-1
    renumber[slaves] = renumber[masters]
    inodesbydim = [renumber[n] for n in inodesbydim]

  if geomdofs is inodesbydim[ndims]: # geometry is linear and non-periodic, dofs follow in-place sorting of inodesbydim
    degree = 1
  else: # match sorting of inodesbydim and renumber higher order coeffcients
    shuffle = inodesbydim[ndims].argsort(axis=1)
    if geomdofs.shape[1] == ndims+1:
      degree = 1
    elif ndims == 2 and geomdofs.shape[1] == 6:
      degree = 2
      fullshuffle = numpy.concatenate([shuffle, numpy.take([4,5,3], shuffle)], axis=1).take([0,5,1,4,3,2], axis=1)
      fullgeomdofs = geomdofs[numpy.arange(len(geomdofs))[:,_], fullshuffle]
    else:
      raise NotImplementedError
    geomdofs = geomdofs[numpy.arange(len(geomdofs))[:,_], shuffle]
  for e in inodesbydim:
    e.sort(axis=1)

  # create simplex topology
  root = transform.Identifier(ndims, name)
  topo = topology.SimplexTopology(inodesbydim[ndims], [(root, transform.Simplex(c)) for c in nodes[geomdofs]])
  log.info('created topology consisting of {} elements'.format(len(topo)))

  if tagnamesbydim[ndims-1]: # separate boundary and interface elements by tag
    elemref = element.getsimplex(ndims)
    edgeref = element.getsimplex(ndims-1)
    edges = {}
    for elemtrans, vtx in zip(topo.transforms, inodesbydim[ndims].tolist()):
      for iedge, edgetrans in enumerate(elemref.edge_transforms):
        edges.setdefault(tuple(vtx[:iedge] + vtx[iedge+1:]), []).append(elemtrans+(edgetrans,))
    tagsbelems = {}
    tagsielems = {}
    for name, ibelems in tagnamesbydim[ndims-1].items():
      for ibelem in ibelems:
        edge, *oppedge = edges[tuple(inodesbydim[ndims-1][ibelem])]
        if oppedge:
          tagsielems.setdefault(name, []).append((edge, oppedge[0]))
        else:
          tagsbelems.setdefault(name, []).append(edge)
    if tagsbelems:
      topo = topo.withgroups(bgroups={tagname: topology.UnstructuredTopology((edgeref,)*len(tagbelems), tagbelems, tagbelems, ndims=ndims-1) for tagname, tagbelems in tagsbelems.items()})
      log.info('boundary groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagsbelems.items()))
    if tagsielems:
      topo = topo.withgroups(igroups={tagname: topology.UnstructuredTopology((edgeref,)*len(tagielems), (trans for trans, opp in tagielems), (opp for trans, opp in tagielems), ndims=ndims-1) for tagname, tagielems in tagsielems.items()})
      log.info('interface groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagsielems.items()))

  if tagnamesbydim[0]: # create points topology and separate by tag
    ptransforms = {inodes[0]: [] for inodes in inodesbydim[0]}
    pref = element.getsimplex(0)
    for ref, trans, inodes in zip(topo.references, topo.transforms, inodesbydim[ndims]):
      for ivertex, inode in enumerate(inodes):
        if inode in ptransforms:
          offset = ref.vertices[ivertex]
          ptransforms[inode].append(trans + (transform.Matrix(linear=numpy.zeros(shape=(ndims,0)), offset=offset),))
    pgroups = {}
    for name, ipelems in tagnamesbydim[0].items():
      tagptransforms = tuple(ptrans for ipelem in ipelems for inode in inodesbydim[0][ipelem] for ptrans in ptransforms[inode])
      pgroups[name] = topology.UnstructuredTopology((pref,)*len(tagptransforms), tagptransforms, tagptransforms, ndims=0)
    topo = topo.withgroups(pgroups=pgroups)
    log.info('point groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in pgroups.items()))

  if tagnamesbydim[ndims]: # create volume groups
    vgroups = {}
    simplex = element.getsimplex(ndims)
    for name, ielems in tagnamesbydim[ndims].items():
      if len(ielems) == len(topo):
        vgroups[name] = ...
      elif ielems:
        refs = numpy.array([simplex.empty] * len(topo), dtype=object)
        refs[ielems] = simplex
        vgroups[name] = topology.SubsetTopology(topo, refs)
    topo = topo.withgroups(vgroups=vgroups)
    log.info('volume groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagnamesbydim[ndims].items()))

  # create geometry
  if degree == 1:
    geom = function.rootcoords(ndims)
  else:
    coeffs = element.getsimplex(ndims).get_poly_coeffs('lagrange', degree=degree)
    basis = function.PlainBasis([coeffs] * len(fullgeomdofs), fullgeomdofs, len(nodes), topo.transforms)
    geom = (basis[:,_] * nodes).sum(0)

  return topo, geom
Пример #34
0
def optimize(target: types.strictstr,
             functional: sample.strictintegral,
             *,
             tol: types.strictfloat = 0.,
             arguments: argdict = {},
             droptol: float = None,
             constrain: types.frozenarray = None,
             lhs0: types.frozenarray[types.strictfloat] = None,
             solveargs: types.frozendict = {},
             searchrange: types.tuple[float] = (.01, 2 / 3),
             rebound: types.strictfloat = 2.,
             failrelax: types.strictfloat = 1e-6,
             **linargs):
    '''find the minimizer of a given functional

  Parameters
  ----------
  target : :class:`str`
      Name of the target: a :class:`nutils.function.Argument` in ``residual``.
  functional : scalar :class:`nutils.sample.Integral`
      The functional the should be minimized by varying target
  tol : :class:`float`
      Target residual norm.
  arguments : :class:`collections.abc.Mapping`
      Defines the values for :class:`nutils.function.Argument` objects in
      `residual`.  The ``target`` should not be present in ``arguments``.
      Optional.
  droptol : :class:`float`
      Threshold for leaving entries in the return value at NaN if they do not
      contribute to the value of the functional.
  constrain : :class:`numpy.ndarray` with dtype :class:`float`
      Defines the fixed entries of the coefficient vector
  lhs0 : :class:`numpy.ndarray`
      Coefficient vector, starting point of the iterative procedure.

  Yields
  ------
  :class:`numpy.ndarray`
      Coefficient vector corresponding to the functional optimum
  '''

    if 'newtontol' in linargs:
        warnings.deprecation(
            'argument "newtontol" is deprecated, use "tol" instead')
        tol = linargs.pop('newtontol')
    solveargs = _striplin(linargs, solveargs)
    residual = functional.derivative(target)
    jacobian = residual.derivative(target)
    lhs, cons = _parse_lhs_cons(lhs0, constrain, residual.shape)
    val, res, jac = sample.eval_integrals(functional, residual, jacobian,
                                          **{target: lhs}, **arguments)
    if droptol is not None:
        nan = ~(cons | jac.rowsupp(droptol))
        cons = cons | nan
    resnorm = numpy.linalg.norm(res[~cons])
    if jacobian.contains(target):
        if tol <= 0:
            raise ValueError(
                'nonlinear optimization problem requires a nonzero "tol" argument'
            )
        solveargs.setdefault('rtol', 1e-3)
        linesearch = LineSearch(searchrange, rebound, failrelax)
        firstresnorm = resnorm
        relax = 1
        accept = True
        with log.context('newton {:.0f}%', 0) as reformat:
            while resnorm > tol:
                if accept:
                    reformat(100 * numpy.log(firstresnorm / resnorm) /
                             numpy.log(firstresnorm / tol))
                    dlhs = -jac.solve_leniently(
                        res, constrain=cons, **solveargs)
                    lhs0 = lhs
                    resnorm0 = resnorm
                lhs = lhs0 + relax * dlhs
                val, res, jac = sample.eval_integrals(functional, residual,
                                                      jacobian,
                                                      **{target:
                                                         lhs}, **arguments)
                resnorm = numpy.linalg.norm(res[~cons])
                relax, accept = linesearch(
                    resnorm0**2, -2 * resnorm0**2, resnorm**2,
                    2 * (jac @ dlhs)[~cons].dot(res[~cons]), relax)
            log.info('converged with residual {:.1e}'.format(resnorm))
    elif resnorm > tol:
        solveargs.setdefault('atol', tol)
        dlhs = -jac.solve(res, constrain=cons, **solveargs)
        lhs = lhs + dlhs
        val += (res + jac @ dlhs / 2).dot(dlhs)
    if droptol is not None:
        lhs = numpy.choose(nan, [lhs, numpy.nan])
        log.info('constrained {}/{} dofs'.format(
            len(lhs) - nan.sum(), len(lhs)))
    log.info('optimum value {:.2e}'.format(val))
    return lhs
Пример #35
0
def main(nrefine: int, traction: float, radius: float, poisson: float):
    '''
  Horizontally loaded linear elastic plate with IGA hole.

  .. arguments::

     nrefine [2]
       Number of uniform refinements starting from 1x2 base mesh.
     traction [.1]
       Far field traction (relative to Young's modulus).
     radius [.5]
       Cut-out radius.
     poisson [.3]
       Poisson's ratio, nonnegative and strictly smaller than 1/2.
  '''

    # create the coarsest level parameter domain
    domain, geom0 = mesh.rectilinear([1, 2])
    bsplinebasis = domain.basis('spline', degree=2)
    controlweights = numpy.ones(12)
    controlweights[1:3] = .5 + .25 * numpy.sqrt(2)
    weightfunc = bsplinebasis.dot(controlweights)
    nurbsbasis = bsplinebasis * controlweights / weightfunc

    # create geometry function
    indices = [0, 2], [1, 2], [2, 1], [2, 0]
    controlpoints = numpy.concatenate([
        numpy.take([0, 2**.5 - 1, 1], indices) * radius,
        numpy.take([0, .3, 1], indices) * (radius + 1) / 2,
        numpy.take([0, 1, 1], indices)
    ])
    geom = (nurbsbasis[:, numpy.newaxis] * controlpoints).sum(0)

    radiuserr = domain.boundary['left'].integral(
        (function.norm2(geom) - radius)**2 * function.J(geom0),
        degree=9).eval()**.5
    treelog.info('hole radius exact up to L2 error {:.2e}'.format(radiuserr))

    # refine domain
    if nrefine:
        domain = domain.refine(nrefine)
        bsplinebasis = domain.basis('spline', degree=2)
        controlweights = domain.project(weightfunc,
                                        onto=bsplinebasis,
                                        geometry=geom0,
                                        ischeme='gauss9')
        nurbsbasis = bsplinebasis * controlweights / weightfunc

    ns = function.Namespace()
    ns.x = geom
    ns.lmbda = 2 * poisson
    ns.mu = 1 - poisson
    ns.ubasis = nurbsbasis.vector(2)
    ns.u_i = 'ubasis_ni ?lhs_n'
    ns.X_i = 'x_i + u_i'
    ns.strain_ij = '(d(u_i, x_j) + d(u_j, x_i)) / 2'
    ns.stress_ij = 'lmbda strain_kk δ_ij + 2 mu strain_ij'
    ns.r2 = 'x_k x_k'
    ns.R2 = radius**2 / ns.r2
    ns.k = (3 - poisson) / (1 + poisson)  # plane stress parameter
    ns.scale = traction * (1 + poisson) / 2
    ns.uexact_i = 'scale (x_i ((k + 1) (0.5 + R2) + (1 - R2) R2 (x_0^2 - 3 x_1^2) / r2) - 2 δ_i1 x_1 (1 + (k - 1 + R2) R2))'
    ns.du_i = 'u_i - uexact_i'

    sqr = domain.boundary['top,bottom'].integral('(u_i n_i)^2 J(x)' @ ns,
                                                 degree=9)
    cons = solver.optimize('lhs', sqr, droptol=1e-15)
    sqr = domain.boundary['right'].integral('du_k du_k J(x)' @ ns, degree=20)
    cons = solver.optimize('lhs', sqr, droptol=1e-15, constrain=cons)

    # construct residual
    res = domain.integral('d(ubasis_ni, x_j) stress_ij J(x)' @ ns, degree=9)

    # solve system
    lhs = solver.solve_linear('lhs', res, constrain=cons)

    # vizualize result
    bezier = domain.sample('bezier', 9)
    X, stressxx = bezier.eval(['X', 'stress_00'] @ ns, lhs=lhs)
    export.triplot('stressxx.png',
                   X,
                   stressxx,
                   tri=bezier.tri,
                   hull=bezier.hull,
                   clim=(numpy.nanmin(stressxx), numpy.nanmax(stressxx)))

    # evaluate error
    err = domain.integral('<du_k du_k, sum:ij(d(du_i, x_j)^2)>_n J(x)' @ ns,
                          degree=9).eval(lhs=lhs)**.5
    treelog.user('errors: L2={:.2e}, H1={:.2e}'.format(*err))

    return err, cons, lhs
Пример #36
0
def gmsh(fname, name='gmsh'):
  """Gmsh parser

  Parser for Gmsh files in `.msh` format. Only files with physical groups are
  supported. See the `Gmsh manual
  <http://geuz.org/gmsh/doc/texinfo/gmsh.html>`_ for details.

  Parameters
  ----------
  fname : :class:`str`
      Path to mesh file.
  name : :class:`str` or :any:`None`
      Name of parsed topology, defaults to 'gmsh'.

  Returns
  -------
  topo : :class:`nutils.topology.SimplexTopology`
      Topology of parsed Gmsh file.
  geom : :class:`nutils.function.Array`
      Isoparametric map.
  """

  # create lines iterable
  if isinstance(fname, pathlib.Path):
    lines = fname.open()
  elif isinstance(fname, str):
    if fname.startswith('$MeshFormat'):
      lines = iter(fname.splitlines())
    else:
      lines = open(fname)
  else:
    raise ValueError("expected the contents of a Gmsh MSH file (as 'str') or a filename (as 'str' or 'pathlib.Path') but got {!r}".format(fname))

  # split sections
  sections = {}
  for line in lines:
    line = line.strip()
    assert line[0]=='$'
    sname = line[1:]
    slines = []
    for sline in lines:
      sline = sline.strip()
      if sline == '$End'+sname:
        break
      slines.append(sline)
    sections[sname] = slines

  # discard section MeshFormat
  sections.pop('MeshFormat', None)

  # parse section PhysicalNames
  PhysicalNames = sections.pop('PhysicalNames', [0])
  assert int(PhysicalNames[0]) == len(PhysicalNames)-1
  tagmapbydim = {}, {}, {}, {} # tagid->tagname dictionary
  for line in PhysicalNames[1:]:
    nd, tagid, tagname = line.split(' ', 2)
    nd = int(nd)
    tagmapbydim[nd][tagid] = tagname.strip('"')

  # determine the dimension of the mesh
  ndims = 2 if not tagmapbydim[3] else 3
  if ndims == 3 and tagmapbydim[1]:
    raise NotImplementedError('Physical line groups are not supported in volumetric meshes')

  # parse section Nodes
  Nodes = sections.pop('Nodes')
  nnodes = len(Nodes)-1
  assert int(Nodes[0]) == nnodes
  nodes = numpy.empty((nnodes, 3))
  nodemap = {}
  for i, line in enumerate(Nodes[1:]):
    n, *c = line.split()
    nodemap[n] = i
    nodes[i] = c
  assert not numpy.isnan(nodes).any()
  if ndims == 2:
    assert numpy.all(nodes[:,2]) == 0, 'Non-zero z-coordinates found in 2D mesh.'
    nodes = nodes[:,:2]

  # parse section Elements
  Elements = sections.pop('Elements')
  assert int(Elements[0]) == len(Elements)-1
  inodesbydim = [], [], [], [] # nelems-list of 4-tuples of node numbers
  tagnamesbydim = {}, {}, {}, {} # tag->ielems dictionary
  etype2nd = {'15': 0, '1': 1, '2': 2, '4': 3, '8': 1, '9': 2}
  for line in Elements[1:]:
    n, e, t, m, *w = line.split()
    nd = etype2nd[e]
    ntags = int(t) - 1
    assert ntags >= 0
    tagname = tagmapbydim[nd][m]
    inodes = tuple(nodemap[nodeid] for nodeid in w[ntags:])
    if not inodesbydim[nd] or inodesbydim[nd][-1] != inodes: # multiple tags are repeated in consecutive lines
      inodesbydim[nd].append(inodes)
    tagnamesbydim[nd].setdefault(tagname, []).append(len(inodesbydim[nd])-1)
  inodesbydim = [numpy.array(e) if e else numpy.empty((0,nd+1), dtype=int) for nd, e in enumerate(inodesbydim)]

  # parse section Periodic
  Periodic = sections.pop('Periodic', [0])
  nperiodic = int(Periodic[0])
  vertex_identities = [] # slave, master
  n = 0
  for line in Periodic[1:]:
    words = line.split()
    if len(words) == 1:
      n = int(words[0]) # initialize for counting backwards
    elif len(words) == 2:
      vertex_identities.append([nodemap[w] for w in words])
      n -= 1
    else:
      assert len(words) == 3 # discard content
      assert n == 0 # check if batch of slave/master nodes matches announcement
      nperiodic -= 1
  assert nperiodic == 0 # check if number of periodic blocks matches announcement
  assert n == 0 # check if last batch of slave/master nodes matches announcement

  # warn about unused sections
  for section in sections:
    warnings.warn('section {!r} defined but not used'.format(section))

  # separate geometric dofs and sort vertices
  geomdofs = inodesbydim[ndims]
  if geomdofs.shape[1] > ndims+1: # higher order geometry
    inodesbydim = [n[:,:i+1] for i, n in enumerate(inodesbydim)] # remove high order info

  if vertex_identities:
    slaves, masters = numpy.array(vertex_identities).T
    keep = numpy.ones(len(nodes), dtype=bool)
    keep[slaves] = False
    assert keep[masters].all()
    renumber = keep.cumsum()-1
    renumber[slaves] = renumber[masters]
    inodesbydim = [renumber[n] for n in inodesbydim]

  if geomdofs is inodesbydim[ndims]: # geometry is linear and non-periodic, dofs follow in-place sorting of inodesbydim
    degree = 1
  else: # match sorting of inodesbydim and renumber higher order coeffcients
    shuffle = inodesbydim[ndims].argsort(axis=1)
    if geomdofs.shape[1] == ndims+1:
      degree = 1
    elif ndims == 2 and geomdofs.shape[1] == 6:
      degree = 2
      fullshuffle = numpy.concatenate([shuffle, numpy.take([4,5,3], shuffle)], axis=1).take([0,5,1,4,3,2], axis=1)
      fullgeomdofs = geomdofs[numpy.arange(len(geomdofs))[:,_], fullshuffle]
    else:
      raise NotImplementedError
    geomdofs = geomdofs[numpy.arange(len(geomdofs))[:,_], shuffle]
  for e in inodesbydim:
    e.sort(axis=1)

  # create simplex topology
  root = transform.Identifier(ndims, name)
  topo = topology.SimplexTopology(inodesbydim[ndims], [(root, transform.Simplex(c)) for c in nodes[geomdofs]])
  log.info('created topology consisting of {} elements'.format(len(topo)))

  if tagnamesbydim[ndims-1]: # separate boundary and interface elements by tag
    edges = {}
    for elem, vtx in zip(topo, inodesbydim[ndims].tolist()):
      for iedge, edge in enumerate(elem.edges):
        edges.setdefault(tuple(vtx[:iedge] + vtx[iedge+1:]), []).append(edge)
    tagsbelems = {}
    tagsielems = {}
    for name, ibelems in tagnamesbydim[ndims-1].items():
      for ibelem in ibelems:
        edge, *oppedge = edges[tuple(inodesbydim[ndims-1][ibelem])]
        if oppedge:
          tagsielems.setdefault(name, []).append(edge.withopposite(*oppedge))
        else:
          tagsbelems.setdefault(name, []).append(edge)
    if tagsbelems:
      topo = topo.withgroups(bgroups={tagname: topology.UnstructuredTopology(ndims-1, tagbelems) for tagname, tagbelems in tagsbelems.items()})
      log.info('boundary groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagsbelems.items()))
    if tagsielems:
      topo = topo.withgroups(igroups={tagname: topology.UnstructuredTopology(ndims-1, tagielems) for tagname, tagielems in tagsielems.items()})
      log.info('interface groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagsielems.items()))

  if tagnamesbydim[0]: # create points topology and separate by tag
    pelems = {inodes[0]: [] for inodes in inodesbydim[0]}
    pref = element.getsimplex(0)
    for elem, inodes in zip(topo, inodesbydim[ndims]):
      for ivertex, inode in enumerate(inodes):
        if inode in pelems:
          offset = elem.reference.vertices[ivertex]
          trans = elem.transform + (transform.Matrix(linear=numpy.zeros(shape=(ndims,0)), offset=offset),)
          pelems[inode].append(element.Element(pref, trans))
    tagspelems = {}
    for name, ipelems in tagnamesbydim[0].items():
      tagspelems[name] = [pelem for ipelem in ipelems for inode in inodesbydim[0][ipelem] for pelem in pelems[inode]]
    topo = topo.withgroups(pgroups={tagname: topology.UnstructuredTopology(0, tagpelems) for tagname, tagpelems in tagspelems.items()})
    log.info('point groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagspelems.items()))

  if tagnamesbydim[ndims]: # create volume groups
    vgroups = {}
    simplex = element.getsimplex(ndims)
    for name, ielems in tagnamesbydim[ndims].items():
      if len(ielems) == len(topo):
        vgroups[name] = ...
      elif ielems:
        refs = numpy.array([simplex.empty] * len(topo), dtype=object)
        refs[ielems] = simplex
        vgroups[name] = topology.SubsetTopology(topo, refs)
    topo = topo.withgroups(vgroups=vgroups)
    log.info('volume groups:', ', '.join('{} (#{})'.format(n, len(e)) for n, e in tagnamesbydim[ndims].items()))

  # create geometry
  if degree == 1:
    geom = function.rootcoords(ndims)
  else:
    coeffs = element.getsimplex(ndims).get_poly_coeffs('lagrange', degree=degree)
    transforms = [elem.transform for elem in topo]
    basis = function.polyfunc([coeffs] * len(fullgeomdofs), fullgeomdofs, len(nodes), transforms, issorted=False)
    geom = (basis[:,_] * nodes).sum(0)

  return topo, geom