Skip to content

Commit

Permalink
Introduce field method (#894)
Browse files Browse the repository at this point in the history
  • Loading branch information
gertjanvanzwieten committed Feb 4, 2025
2 parents 69202ff + be8bc53 commit d2538d9
Show file tree
Hide file tree
Showing 26 changed files with 406 additions and 229 deletions.
16 changes: 8 additions & 8 deletions examples/adaptivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ def main(etype: str = 'square',

if irefine:
refdom = domain.refined
refbasis = refdom.basis(btype, degree=degree)
ns.add_field('vref', refbasis)
res = refdom.integral('∇_k(vref) ∇_k(u) dV' @ ns, degree=degree*2)
res -= refdom.boundary.integral('vref ∇_k(u) n_k dS' @ ns, degree=degree*2)
indicator = res.derivative('vref').eval(**args)
supp = refbasis.get_support(indicator**2 > numpy.mean(indicator**2))
domain = domain.refined_by(refdom.transforms[supp])
ns.refbasis = refdom.basis(btype=btype, degree=degree)
res = refdom.integral('∇_k(refbasis_n) ∇_k(u) dV' @ ns, degree=degree*2)
res -= refdom.boundary.integral('refbasis_n ∇_k(u) n_k dS' @ ns, degree=degree*2)
indicator = numpy.square(res.eval(**args))
irefelems = ns.refbasis.get_support(indicator > indicator.mean())
domain = domain.refined_by(refdom.transforms[irefelems])

ns = Namespace()
ns.x = geom
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.add_field(('u', 'v'), domain.basis(btype, degree=degree))
ns.u = domain.field('u', btype=btype, degree=degree)
ns.v = domain.field('v', btype=btype, degree=degree)
ns.uexact = exact
ns.du = 'u - uexact'

Expand Down
10 changes: 6 additions & 4 deletions examples/burgers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ def main(nelems: int = 40,
ns = Namespace()
ns.x = geom
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.add_field(('u', 'u0', 'v'), domain.basis(btype, degree=degree))
ns.add_field(('t', 't0'))
ns.dudt = '(u - u0) / (t - t0)'
ns.u = domain.field('u', btype=btype, degree=degree)
ns.du = ns.u - function.replace_arguments(ns.u, 'u:u0')
ns.v = domain.field('v', btype=btype, degree=degree)
ns.t = function.field('t')
ns.dt = ns.t - function.field('t0')
ns.f = '.5 u^2'
ns.C = 1
ns.uinit = 'exp(-25 x^2)'

res = domain.integral('(v dudt - ∇(v) f) dV' @ ns, degree=degree*2)
res = domain.integral('(v du / dt - ∇(v) f) dV' @ ns, degree=degree*2)
res -= domain.interfaces.integral('[v] n ({f} - .5 C [u] n) dS' @ ns, degree=degree*2)

sqr = domain.integral('(u - uinit)^2 dV' @ ns, degree=max(degree*2, 5))
Expand Down
15 changes: 7 additions & 8 deletions examples/cahnhilliard.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,19 @@ def main(size: Length = parse('10cm'),
domain, geom = mesh.unitsquare(nelems, etype)

ns = Namespace()
ns.x = size * geom
ns.x = geom * size
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.add_field(('φ', 'φ0'), domain.basis('std', degree=degree))
ns.add_field('η', domain.basis('std', degree=degree) * stens / epsilon) # basis scaling to give η the required unit
ns.add_field('dt')
ns.dt *= timestep
ns.φ = domain.field('φ', btype='std', degree=degree)
ns. = ns.φ - function.replace_arguments(ns.φ, 'φ:φ0')
ns.η = domain.field('η', btype='std', degree=degree) * (stens / epsilon)
ns.dt = function.field('dt') * timestep
ns.ε = epsilon
ns.σ = stens
ns.σmean = (wtensp + wtensn) / 2
ns.σdiff = (wtensp - wtensn) / 2
ns.σwall = 'σmean + φ σdiff'
ns. = 'φ - φ0'
ns.ψ = '.25 (φ^2 - 1)^2'
ns.δψ = '.25 dφ^2 (1 - φ0^2 / 6 - φ φ0 / 3 - φ^2 / 2)' if stable else '0'
ns.δψ = '.25 dφ^2 (1 - φ^2 + 2 φ dφ / 3 - ^2 / 6)' if stable else '0'
ns.M = mobility
ns.J_i = '-M ∇_i(η)'
ns.v_i = 'φ J_i'
Expand All @@ -181,7 +180,7 @@ def main(size: Length = parse('10cm'),
system = System(nrg / tol, trial='φ,η')

numpy.random.seed(seed)
args = dict(φ=numpy.random.normal(0, .5, system.argshapes['φ'])) # initial condition
args = dict(φ=numpy.random.normal(0, .5, function.arguments_for(nrg)['φ'].shape)) # initial condition

with log.iter.fraction('timestep', range(round(endtime / timestep))) as steps:
for istep in steps:
Expand Down
6 changes: 4 additions & 2 deletions examples/coil.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ def main(nelems: int = 50,
X = RZ * REV
ns.x = ns.rz @ ns.rot
ns.define_for('x', gradient='∇', jacobians=('dV', 'dS'), curl='curl')
ns.add_field(('A', 'Atest'), RZ.basis('spline', degree=degree, removedofs=[[0, -1], [-1]])[:,numpy.newaxis] * ns., dtype=complex)
ns.A = RZ.field('A', btype='spline', degree=degree, removedofs=[[0, -1], [-1]], dtype=complex) * ns.
ns.Atest = function.replace_arguments(ns.A, 'A:Atest')
ns.B_i = 'curl_ij(A_j)'
ns.E_i = '-j ω A_i'
ns.Jind_i = 'σ E_i'
Expand All @@ -123,7 +124,8 @@ def main(nelems: int = 50,
# on a basis.

ns.Borig = ns.B
ns.add_field(('B', 'Btest'), RZ.basis('spline', degree=degree), ns.rot, dtype=complex)
ns.B = function.field('B', RZ.basis('spline', degree=degree), ns.rot, dtype=complex)
ns.Btest = function.replace_arguments(ns.B, 'B:Btest')
res = REV.integral(RZ.integral('Btest_i (B_i - Borig_i) dV' @ ns, degree=2*degree), degree=0)
args = System(res, trial='B', test='Btest').solve(arguments=args)

Expand Down
12 changes: 8 additions & 4 deletions examples/cylinderflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,15 @@ def main(nelems: int = 99,
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
J = ns.x.grad(geom)
detJ = numpy.linalg.det(J)
ns.add_field(('u', 'u0', 'v'), function.vectorize([
ns.u = function.field('u', function.vectorize([
domain.basis('spline', degree=(degree, degree-1), removedofs=((0,), None)),
domain.basis('spline', degree=(degree-1, degree))]) @ J.T / detJ)
ns.add_field(('p', 'q'), domain.basis('spline', degree=degree-1) / detJ)
ns.add_field(('t', 'dt'))
ns.p = domain.field('p', btype='spline', degree=degree-1) / detJ
ns.v = function.replace_arguments(ns.u, 'u:v')
ns.q = function.replace_arguments(ns.p, 'p:q')
ns.t = function.field('t')
ns.du = ns.u - function.replace_arguments(ns.u, 'u:u0')
ns.dt = function.field('dt')
ns.σ_ij = '(∇_j(u_i) + ∇_i(u_j)) / Re - p δ_ij'
ns.ω = 'ε_ij ∇_j(u_i)'
ns.N = 10 * degree / elemangle # Nitsche constant based on element size = elemangle/2
Expand All @@ -133,7 +137,7 @@ def main(nelems: int = 99,
sqr = domain.integral('(.5 Σ_i (u_i - uinf_i)^2 - ∇_k(u_k) p) dV' @ ns, degree=degree*2)
args = System(sqr, trial='u,p').solve(constrain=cons) # set initial condition to potential flow

res = domain.integral('v_i (u_i - u0_i) dV' @ ns, degree=degree*3)
res = domain.integral('v_i du_i dV' @ ns, degree=degree*3)
res += domain.integral('(v_i ∇_j(u_i) u_j + ∇_j(v_i) σ_ij + q ∇_k(u_k)) dt dV' @ ns, degree=degree*3)
res += domain.boundary['inner'].integral('(nitsche_i (u_i - uwall_i) - v_i σ_ij n_j) dt dS' @ ns, degree=degree*2)
div = numpy.sqrt(abs(function.factor(domain.integral('∇_k(u_k)^2 dV' @ ns, degree=2)))) # L2 norm of velocity divergence
Expand Down
38 changes: 21 additions & 17 deletions examples/drivencavity.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ def main(nelems: int = 32,
ns.x = geom
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
if not compatible:
ns.add_field(('u', 'v'), domain.basis('std', degree=degree), shape=(domain.ndims,))
ns.add_field(('p', 'q'), domain.basis('std', degree=degree-1)[1:])
ns.add_field('ψ', domain.basis('std', degree=2)[1:])
ns.u = domain.field('u', btype='std', degree=degree, shape=[2])
ns.p = domain.field('p', btype='std', degree=degree-1)
ns.ψ = domain.field('ψ', btype='std', degree=2)
else:
ns.add_field(('u', 'v'), function.vectorize([
domain.basis('spline', degree=(degree, degree-1)),
domain.basis('spline', degree=(degree-1, degree))]))
ns.add_field(('p', 'q'), domain.basis('spline', degree=degree-1)[1:])
ns.add_field('ψ', domain.basis('spline', degree=degree)[1:])
ns.u = function.field('u', function.vectorize([domain.basis('spline', degree=p) for p in degree - 1 + numpy.eye(2, dtype=int)]))
ns.p = domain.field('p', btype='spline', degree=degree-1)
ns.ψ = domain.field(', btype='spline', degree=degree)
ns.v = function.replace_arguments(ns.u, 'u:v')
ns.q = function.replace_arguments(ns.p, 'p:q')
ns.σ_ij = '(∇_j(u_i) + ∇_i(u_j)) / Re - p δ_ij'

# weak formulation for Stokes flow, over-integrating for improved
Expand All @@ -119,6 +119,8 @@ def main(nelems: int = 32,
# strong enforcement of non-penetrating boundary conditions
sqr = domain.boundary.integral('(u_k n_k)^2 dS' @ ns, degree=degree*2)
cons = System(sqr, trial='u').solve_constraints(droptol=1e-15)
cons['p'] = numpy.zeros(function.arguments_for(res)['p'].shape, dtype=bool)
cons['p'].flat[0] = True # point constraint

if strongbc:
# strong enforcement of tangential boundary conditions
Expand Down Expand Up @@ -156,7 +158,9 @@ def postprocess(domain, ns, **arguments):

# reconstruct velocity streamlines
sqr = domain.integral('Σ_i (u_i - ε_ij ∇_j(ψ))^2 dV' @ ns, degree=4)
arguments = System(sqr, trial='ψ').solve(arguments=arguments)
consψ = numpy.zeros(function.arguments_for(sqr)['ψ'].shape, dtype=bool)
consψ.flat[0] = True # point constraint
arguments = System(sqr, trial='ψ').solve(arguments=arguments, constrain={'ψ': consψ})

bezier = domain.sample('bezier', 9)
x, u, p, ψ = bezier.eval(['x_i', 'sqrt(u_i u_i)', 'p', 'ψ'] @ ns, **arguments)
Expand Down Expand Up @@ -197,7 +201,7 @@ def test_baseline(self):
YcDGBdLMJR3Y/X+zdkhHHrEHM6lENt25+OU8OUi8PUn+klm87lacqiN4uQrZ4tUCLh3g4AFrV6Q/uctG
gQ==''')
with self.subTest('stokes-pressure'):
self.assertAlmostEqual64(args0['p'], '''
self.assertAlmostEqual64(args0['p'][1:], '''
eNoBHgDh/+vVsNEXy6jTbiq1z7Av9C0mLJkw1NDTLEEtEC/xNAwED0s=''')
with self.subTest('navier-stokes-velocity'):
self.assertAlmostEqual64(args1['u'], '''
Expand All @@ -206,7 +210,7 @@ def test_baseline(self):
nM/5nf5xevqZxDOq5w4bCwLlOoD6XDV/n1t//s5ZvjPzTjmdDjx55+Slky/MGaDgHFB3vz4DgynQfS9O
A3WcBIkCAB7aSkk=''')
with self.subTest('navier-stokes-pressure'):
self.assertAlmostEqual64(args1['p'], '''
self.assertAlmostEqual64(args1['p'][1:], '''
eNpz01W9oHVmuU7SJYtzgherdcr0n59dfiZT11yP97yCGQDN0Azu''')

def test_mixed(self):
Expand All @@ -218,7 +222,7 @@ def test_mixed(self):
n+Y8k3RG+Mwio99nuoHqg4G48WzCmTignYUXDfXNzoedATuL4bMeA0Op9qczWqfXnTl2ioHhINAdHufv
ntx18qoZSH7FSRAJAB13Sc0=''')
with self.subTest('stokes-pressure'):
self.assertAlmostEqual64(args0['p'], '''
self.assertAlmostEqual64(args0['p'][1:], '''
eNp7pKl+nf1KznmxS62ns/W+az/TNTL4ondU1/46t6GKKQDiJg1H''')
with self.subTest('navier-stokes-velocity'):
self.assertAlmostEqual64(args1['u'], '''
Expand All @@ -227,7 +231,7 @@ def test_mixed(self):
n5xefpbrzEYjZ3MGhiogDr/YYbxbjYHhrH6lYcY55zNgZzGcNWBgUL0Uctr3zLzTt08xMOScZmCYdnbl
qQMnpcxB8konQSQACVZG3A==''')
with self.subTest('navier-stokes-pressure'):
self.assertAlmostEqual64(args1['p'], '''
self.assertAlmostEqual64(args1['p'][1:], '''
eNrbqjVZs1/ry/n48z1nSrW9L83RkTmneNZMO/TCOUNbMwDktQ3z''')

def test_compatible(self):
Expand All @@ -237,14 +241,14 @@ def test_compatible(self):
eNpjYIAAvwvdBr9O2Zk90E8+rXQ6yxzGZ4CDTfr3z0H45hc2mjSagFgn9f1P15+G6Fc0PHSSgQEAx7kX
6A==''')
with self.subTest('stokes-pressure'):
self.assertAlmostEqual64(args0['p'], '''
self.assertAlmostEqual64(args0['p'][1:], '''
eNoL1u+7NOfUR929ugvORxlU6W7V1TcUuyiif/PKCf1yUwDfRw2t''')
with self.subTest('navier-stokes-velocity'):
self.assertAlmostEqual64(args1['u'], '''
eNpjYICA1HNRRkGnZ5r26m86bX3awvyBftS5C6dOmDHAwWxDmbMzTUEsrfMrTA6YgFjKV53OOJ0FsR7o
F561OMnAAAC5tRfX''')
with self.subTest('navier-stokes-pressure'):
self.assertAlmostEqual64(args1['p'], '''
self.assertAlmostEqual64(args1['p'][1:], '''
eNoz1VYx3HT6t16w/uKz73Uv6R7RNzx35swh7XdXrQ0TzADbMQ6l''')

def test_strong(self):
Expand All @@ -255,15 +259,15 @@ def test_strong(self):
iNedYmDoPc3AsMGEgQGh77beyzPHzkqfYTpTcObp6Zmnlc7B5A5dOH4+9dyDMx807M50GvCdOnki8wRM
DhcAAEYiNtQ=''')
with self.subTest('stokes-pressure'):
self.assertAlmostEqual64(args0['p'], '''
self.assertAlmostEqual64(args0['p'][1:], '''
eNoBHgDh/3fRlNYxy7PR0NVKz1ktVi1E1HowsdGJ07Qt/9PINA6QEUk=''')
with self.subTest('navier-stokes-velocity'):
self.assertAlmostEqual64(args1['u'], '''
eNpjYMAPAs4H6M8zaDJOO91vKma628TihKx5kDlEbv2Z5fqFZ24aKZ/mNplmmGJy5eRbY5jcpdOGF+tP
/9BPPW1gXH6W2eSpkds5mBz72fvnUs/EnHt+etXpCWdZzgSd3W8Ck1M9L3zGGyi/4Pz80+ZnNp44c9L8
BEwOFwAA4RM3QA==''')
with self.subTest('navier-stokes-pressure'):
self.assertAlmostEqual64(args1['p'], '''
self.assertAlmostEqual64(args1['p'][1:], '''
eNoBHgDh//ot489SzMEsntHezWTPAC+jL+XN/8wF02UxTc1JNhf0ELc=''')


Expand Down
7 changes: 4 additions & 3 deletions examples/elasticity.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def main(nelems: int = 24,
ns.δ = function.eye(domain.ndims)
ns.x = geom
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.add_field(('u', 't'), domain.basis(btype, degree=degree), shape=(2,))
ns.u = domain.field('u', btype=btype, degree=degree, shape=[2])
ns.X_i = 'x_i + u_i'
ns.λ = 1
ns.μ = .5/poisson - 1
Expand All @@ -61,8 +61,9 @@ def main(nelems: int = 24,
if direct:
ns.t_i = 'σ_ij n_j' # <-- this is an inadmissible boundary term
else:
energy -= domain.boundary['top'].integral('u_i t_i dS' @ ns, degree=degree*2)
args = System(energy, trial='t', test='u').solve(constrain={'t': numpy.isnan(cons['u'])}, arguments=args)
ns.t = domain.field('t', btype=btype, degree=degree, shape=[2])
system = System(energy - domain.boundary['top'].integral('u_i t_i dS' @ ns, degree=degree*2), trial='t', test='u')
args = system.solve(constrain={'t': numpy.isnan(cons['u'])}, arguments=args)
F = domain.boundary['top'].integrate('t_i dS' @ ns, degree=degree*2, arguments=args)
log.user('total clamping force:', F)

Expand Down
2 changes: 1 addition & 1 deletion examples/finitestrain.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def main(nelems: int = 20,
ns.angle = angle * numpy.pi / 180
ns.λ = 2 * poisson
ns.μ = 1 - 2 * poisson
ns.add_field('u', domain.basis(btype, degree=degree), shape=(domain.ndims,))
ns.u = domain.field('u', btype=btype, degree=degree, shape=[domain.ndims])
ns.x_i = 'X_i + u_i'
ns.ε_ij = '.5 (∇_j(u_i) + ∇_i(u_j))'
ns.energy = 'λ ε_ii ε_jj + 2 μ ε_ij ε_ij'
Expand Down
6 changes: 3 additions & 3 deletions examples/laplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ def main(nelems: int = 10,

# To be able to write index based tensor contractions, we need to bundle
# all relevant functions together in a namespace. Here we add the geometry
# `x`, a test function `v`, and the solution `u`. The latter two are formed
# by contracting a basis with function arguments of the same name.
# `x`, a test function `v`, and the solution `u`.

ns = Namespace()
ns.x = geom
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.add_field(('u', 'v'), domain.basis(btype, degree=degree))
ns.u = domain.field('u', btype=btype, degree=degree)
ns.v = domain.field('v', btype=btype, degree=degree)

# We are now ready to implement the Laplace equation. In weak form, the
# solution is a scalar field `u` for which ∫_Ω ∇v·∇u - ∫_Γn v f = 0 ∀ v.
Expand Down
5 changes: 3 additions & 2 deletions examples/platewithhole.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def generate(self, radius):
if self.nrefine:
topo = topo.refine(self.nrefine)
bsplinebasis = topo.basis('spline', degree=2)
sqr = topo.integral((function.dotarg('w', bsplinebasis) - weightfunc)**2, degree=9)
sqr = topo.integral((function.field('w', bsplinebasis) - weightfunc)**2, degree=9)
controlweights = System(sqr, trial='w').solve()['w']
nurbsbasis = bsplinebasis * controlweights / weightfunc
return topo.withboundary(hole='left', sym='top,bottom', far='right'), geom, nurbsbasis, 5
Expand Down Expand Up @@ -121,7 +121,8 @@ def main(mode: Union[FCM, NURBS] = NURBS(),
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
ns.λ = 2 * poisson
ns.μ = 1 - poisson
ns.add_field(('u', 'v'), basis, shape=(2,))
ns.u = function.field('u', basis, shape=[2])
ns.v = function.field('v', basis, shape=[2])
ns.X_i = 'x_i + u_i'
ns.ε_ij = '(∇_j(u_i) + ∇_i(u_j)) / 2'
ns.σ_ij = 'λ ε_kk δ_ij + 2 μ ε_ij'
Expand Down
2 changes: 1 addition & 1 deletion examples/poisson.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def main(nelems: int = 32):
'''

topo, x = mesh.unitsquare(nelems, etype='square')
u = function.dotarg('u', topo.basis('std', degree=1))
u = topo.field('u', btype='std', degree=1)
g = u.grad(x)
J = function.J(x)

Expand Down
5 changes: 2 additions & 3 deletions examples/torsion.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def main(length: float = 2*np.pi,
zgrid = length * np.linspace(-.5, .5, round(length / elemsize)+1)
θgrid = np.linspace(-np.pi, np.pi, round(2 * np.pi / elemsize)+1)
cylinder, (z, θ) = mesh.rectilinear([zgrid, θgrid], periodic=(1,))
φ = θ - (z / length * np.pi / 180) * function.Argument('φ', shape=())
φ = θ - (z / length * np.pi / 180) * function.field('φ')
if trim:
cylinder = cylinder.trim(θ**2 + z**2 - trim**2, maxrefine=2)
extrusion, r = mesh.line([1 - thickness/2, 1 + thickness/2], space='T')
Expand All @@ -77,8 +77,7 @@ def main(length: float = 2*np.pi,
ns.X = np.stack([z, r * np.sin(θ), r * np.cos(θ)]) # reference geometry
ns. = np.stack([z * stretch, r * np.sin(φ), r * np.cos(φ)])
ns.define_for('X', gradient='∇', jacobians=('dV',))
ns.add_field('u', topo.basis('spline', degree=degree,
removedofs=((0,-1),None,None)), shape=(3,)) # deformation, clamped
ns.u = topo.field('u', btype='spline', degree=degree, removedofs=((0,-1),None,None), shape=[3]) # deformation, clamped
ns.x_i = 'Xφ_i + u_i' # deformed geometry
ns.F_ij = '∇_j(x_i)'
ns.J = np.linalg.det(ns.F)
Expand Down
9 changes: 7 additions & 2 deletions nutils/SI.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,16 @@ def __evaluate(op, *args, **kwargs):
dims, args = zip(*Quantity.__unpack(*args))
return tuple(dim.wrap(ret) for (dim, ret) in zip(dims, op(*args, **kwargs)))

@register('dotarg')
def __dotarg(op, *args, **kwargs):
@register('field')
def __field(op, *args, **kwargs):
dims, args = zip(*Quantity.__unpack(*args)) # we abuse the fact that unpack str returns dimensionless
return functools.reduce(operator.mul, dims).wrap(op(*args, **kwargs))

@register('arguments_for')
def __attribute(op, *args, **kwargs):
__dims, args = zip(*Quantity.__unpack(*args))
return op(*args, **kwargs)

del register

## DEFINE OPERATORS
Expand Down
2 changes: 1 addition & 1 deletion nutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'Numerical Utilities for Finite Element Analysis'

__version__ = version = '9a46'
__version__ = version = '9a47'
version_name = 'jook-sing'
Loading

0 comments on commit d2538d9

Please # to comment.