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
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', ))
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')
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)
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
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)
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
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
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}")
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
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
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
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
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
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)
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
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())
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)
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)
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
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
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
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
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)
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
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
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
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)
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
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
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)
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
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
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
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
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