/opt/conda/envs/dxtr/lib/python3.12/site-packages/pyvista/plotting/utilities/xvfb.py:48: PyVistaDeprecationWarning: This function is deprecated and will be removed in future version of PyVista. Use vtk-osmesa instead. warnings.warn(
Operators¶
In this section we briefly introduce the ***"fundamental"* operators** implemented within the Dxtr library:
- The exterior derivative.
- The Hodge star.
- The musical isomorphisms.
- The wedge product.
We call "operators" functions that take as inputs Cochain instances and return new ones. These functions are gathered in the module operators and organized in thematic sub-modules. Let's briefly introduce each one of these sub-modules.
The exterior derivative¶
The exterior derivative is implemented by the exterior_derivative() function, located in the dxtr.operators.differential sub-module. This operator is the fundamental building block upon which all differential operators of the library depend.
Note: For conviniency, we also defined an inituite and shorter alias to invoke the exterior derivative:
d(), both functions can directly be called from thedxtr.operatorsmodule:
from dxtr import Cochain
from dxtr.cochains import random_cochain
from dxtr.operators import exterior_derivative, d
c = random_cochain('icosa') # Let's consider a random cochain defined on a icosahedral simplicial complex as example.
ext_deri_c = exterior_derivative(c)
print('Congratulations !')
print(f' * You have just computed the {ext_deri_c}')
if isinstance(ext_deri_c, Cochain) & (ext_deri_c.complex == c.complex):
print(f' * The exterior derivative of a {c.dim}D cochain is a {ext_deri_c.dim}D one, defined on the same complex.')
dc = d(c)
if (dc.values == ext_deri_c.values).all():
print(' * The alias `d()` yields the same values as the function `exterior_derivative()`.')
Congratulations ! * You have just computed the Exterior derivative of Random primal 1-Cochain, defined on 2D Simplicial Complex. * The exterior derivative of a 1D cochain is a 2D one, defined on the same complex. * The alias `d()` yields the same values as the function `exterior_derivative()`.
/builds/j2uR5rszh/0/oali/dxtr/src/dxtr/operators/differential.py:101: SyntaxWarning: invalid escape sequence '\d'
$$\delta(\cdot) = \star d \star(\cdot)$$
/builds/j2uR5rszh/0/oali/dxtr/src/dxtr/operators/differential.py:143: SyntaxWarning: invalid escape sequence '\D'
$$\Delta_{B} = \delta d(\cdot)$$
/builds/j2uR5rszh/0/oali/dxtr/src/dxtr/operators/curvature.py:140: SyntaxWarning: invalid escape sequence '\o'
$$\boldsymbol{P}(\boldsymbol{v}) = \boldsymbol{v}\otimes\boldsymbol{v}$$
/builds/j2uR5rszh/0/oali/dxtr/src/dxtr/operators/curvature.py:245: SyntaxWarning: invalid escape sequence '\k'
$$\kappa_{ij} = \tau_i\cdot\nabla_j(n).$$
/builds/j2uR5rszh/0/oali/dxtr/src/dxtr/operators/curvature.py:281: SyntaxWarning: invalid escape sequence '\k'
$$\kappa_n = \boldsymbol{\kappa} \colon \hat{\boldsymbol{v}}\otimes\hat{\boldsymbol{v}}$$
As initially proposed by M. Desbrun, A. Hirani and co-workers — see refs mentioned here — the discrete exterior derivative implementation relies on the chain/cochain complexes connecting the consecutive modules of a simplicial complex. Technically, this means that given a k-cochain defined on a n-simplicial complex, its exterior derivative corresponds to a (k+1)-cochain whose values have been computed thanks to the incidence matrix between k and k+1 simplices:
Note: In Dxtr the incidence matrices can be obtained through the
boundaryandcoboundaryproperties of a given module. Theboundaryof a k-module yields the downward incidence, i.e. k -> k-1; while thecoboundaryyields the upward incidence, k -> k+1.
c = random_cochain('icosa')
incicence_matrix = c.complex[c.dim].coboundary
if (d(c).values == incicence_matrix @ c.values).all():
print(f'The exterior derivative of our {c.name} is properly computed.')
The exterior derivative of our Random primal 1-Cochain is properly computed.
An fundamental property of the exterior derivative is its 2-nilpotency, this property is by construction verified within our implementation:
for k in range(3):
c = random_cochain('icosa', dim=k)
assert (d(d(c)).values == 0).all()
Another property to bear in mind (at play in the previous cell): The exterior derivative of a cochain defined on the top-simplices of a complex will always be null:
c = random_cochain('icosa', dim=2)
dc = d(c)
if c.dim == c.complex.dim:
assert (dc.values == 0).all()
print(dc.dim)
3
The exterior derivative can be computed for primal and dual cochains alike:
c = random_cochain('icosa', dual=True)
dc = d(c)
if dc.isdual:
print(f'The exterior derivative of a dual {c.dim}-cochain is a dual {dc.dim}-cochain.')
The exterior derivative of a dual 1-cochain is a dual 2-cochain.
Being a purely topological operator, the exterior derivative can be applied on cochains defined on abstract simplicial complexes:
from dxtr import SimplicialComplex
indices = [[0,1,2,3,4], [1,2,3,4,5], [4,5,6], [6,7]]
asc = SimplicialComplex(indices)
if asc.isabstract:
# Let's define a primal 1D cochain on this abstract complex...
k = 1
Nk = asc[k].size
c = Cochain(asc, k, [1 for i in range(Nk)])
# ...and compute its exterior derivative
dc = d(c)
if dc.complex.isabstract:
print('We computed the exterior derivative of a cochain defined on an abstrac complex.')
dxtr.abstractsimplicialcomplex | INFO: Building a 4-Abstract Simplicial Complex with 2 4-simplices
dxtr.simplicialcomplex | WARNING: No vertices are defined.
We computed the exterior derivative of a cochain defined on an abstrac complex.
Let's also mention that the exterior derivative can be applied to vector-valued cochains.
BUT: This feature is still experimental (in Dxtr V 1.0.0) and should be used with caution, especially in the case of curved complexes.
The Hodge star¶
As mentioned in the introduction section, the discrete hodge_star() operator maps primal k-cochains with dual (n-k)-cochains. This operator, tightly related to the geometry of the supporting simplicial complex, combines with the exterior_derivative() in order to implement all others differential operators.
When applied to a primal k-cochain of unit values, the hodge_star() should return a dual (n-k)-cochain whose values correspond to the ratios of the volumes of the dual (n-k)-cells by the volumes of the k-simplices:
from dxtr.cochains import unit_cochain
from dxtr.operators import hodge_star
for k in range(3):
cchn = unit_cochain(dim=k, manifold=True)
mfld = cchn.complex
cchn_2 = hodge_star(cchn)
kvols, kcovols = mfld[k].volumes, mfld[k].covolumes
s = -1 if k==2 else 1 # See note below.
has_proper_values = (cchn_2.values == s * kcovols/kvols).all()
has_proper_dim = cchn_2.dim == mfld.dim - cchn.dim
if cchn_2.isdual & has_proper_values & has_proper_dim:
print(f'The Hodge star on {k}-cochains is properly computed')
else:
print(cchn_2.values)
print( kcovols/kvols)
The Hodge star on 0-cochains is properly computed
The Hodge star on 1-cochains is properly computed
The Hodge star on 2-cochains is properly computed
Note: In the previous cell, we see that the extected values for dual 0-cochains, computed throught the Hodge star applied to primal 2-cochains, are negative. This is due to a sign correction depending on the dimensionality of the simplicial complex to account for the intrinsic orientation of its top-simplices. :point_right: see A. Hirani's PhD manuscript, remark 4.1.2. on page 41.
When applied twice, the hodge_star() should yield back the identity map, with a sign correction depending on the topological dimensions of the considered cochain (k below) and simplicial complex (n below):
import numpy as np
for k in range(3):
cchn = random_cochain(dim=k, manifold=True)
cchn_bis = hodge_star(hodge_star(cchn))
n = cchn.complex.dim
s = (-1)**(k*(n-k))
try:
np.testing.assert_array_almost_equal(cchn.values, s*cchn_bis.values)
print(f'it works for {k}-cochains.')
except:
print(f'it does not work {k}-cochains.')
it works for 0-cochains.
it works for 1-cochains. it works for 2-cochains.
Musical isomorphisms¶
Musical isomorphisms provide maps from vector fields to 1-cochains (flat()) and from 1-cochains to vector fields (sharp()).
Let's first define a simple vector field, tangent to the icosahedral simplicial complex:
from dxtr.cochains import normal_vector_field
normals = normal_vector_field('icosa')
nrl, mfld = normals.values, normals.complex
ez = np.repeat([[0,0,1]], nrl.shape[0], axis=0)
vct = Cochain(mfld, dim=0, dual=True,
values=np.cross(ez, nrl, axis=-1),
name='e_theta')
if vct.isvectorvalued & vct.isdual & (vct.dim==0):
print(f'{vct.name} is a discrete vector field on a {mfld.name}.')
e_theta is a discrete vector field on a 2D Simplicial Manifold.
Let's now compute its dedicated discrete 1-form:
from dxtr.operators import flat
flat_vct = flat(vct, name='Our first flatten vector field')
if isinstance(flat_vct, Cochain) & (not flat_vct.isvectorvalued):
dual = 'dual' if flat_vct.isdual else 'primal'
k = flat_vct.dim
print(f'{flat_vct.name} is a {dual}, {k}D, scalar-valued cochain.')
dxtr.musical | INFO: Flattening vector field as a 1-cochain.
Our first flatten vector field is a primal, 1D, scalar-valued cochain.
In the cell above, we see that by default when a vector field is flatten, the returned Cochain is a primal one.
We can, however, require a dual Cochain to be output:
dual_flat_vct = flat(vct, dual=True, name='Our second flatten vector field')
if isinstance(dual_flat_vct, Cochain) & (not dual_flat_vct.isvectorvalued):
dual = 'dual' if dual_flat_vct.isdual else 'primal'
k = dual_flat_vct.dim
print(f'This time, {dual_flat_vct.name} is a {dual}, {k}D, scalar-valued cochain.')
dxtr.musical | INFO: Flattening vector field as a 1-cochain.
This time, Our second flatten vector field is a dual, 1D, scalar-valued cochain.
Thanks to the visualize() function from the dxtr.visu module, we can display these various objects:
import pyvista as pv
from dxtr.visu import visualize
fig = pv.Plotter(shape=(1,3), border=False)
fig.subplot(0,0)
visualize(vct, fig=fig, display=False,
layout_parameters={'title':'Initial vector field'})
fig.subplot(0,1)
visualize(flat_vct, fig=fig, scaling_factor=10, display=False,
layout_parameters={'title':'Flatten as Primal 1-cochain'})
fig.subplot(0,2)
visualize(dual_flat_vct, fig=fig, scaling_factor=10, display=False,
layout_parameters={'title':'Flatten as Dual 1-cochain'})
fig.link_views()
fig.show()
Note: As one can see in the previous cell, when visualizing cochains and vector fields, we can benefit from the pyvista API and its intuitive customization.
From these flatten vector fields, it could be interesting to see if we can get back the initial vector field. This can be done, using the sharp() operator:
from dxtr.operators import sharp
sharp_flat_vct = sharp(flat_vct, name='Our first sharpened cochain')
if isinstance(sharp_flat_vct, Cochain) & sharp_flat_vct.isdual & sharp_flat_vct.isvectorvalued & (sharp_flat_vct.dim == 0):
print(f'{sharp_flat_vct.name} is indeed a proper discrete vector field.')
if sharp(dual_flat_vct) is None:
print(f'WARNING: Sharpening a dual cochain is not possible yet.')
dxtr.musical | INFO: Sharpening 1-cochain into discrete vector field.
Our first sharpened cochain is indeed a proper discrete vector field. dxtr.musical | WARNING: Sharp only works on primal cochains.
WARNING: Sharpening a dual cochain is not possible yet.
Remark: As shown above, the current version of the library (V 1.0.0), we cannot sharpen dual 1-
Cochaininstances. This limitation could be overcome by dualizing the flatten dual cochain with thehodge_star()operator, but the results would not correspond to a properly computed vector field, we therefore do not encourage users to perform this "nasty hack".
Making use of the visualize() function, we can visually compare the initial vector field and the one obtained through the composition of the musical isomorphisms:
fig = pv.Plotter(shape=(1,3), border=False)
fig.subplot(0,0)
visualize(vct, fig=fig, display=False,
layout_parameters={'title':'Vector field'})
fig.subplot(0,1)
visualize(flat_vct, fig=fig, scaling_factor=10, display=False,
layout_parameters={'title':'Flat(vector field)'})
fig.subplot(0,2)
visualize(sharp_flat_vct, fig=fig, display=False,
layout_parameters={'title':'Sharp(flat(vector field))'})
fig.link_views()
fig.show()
At first glance, the vector fields in the right hand-side panel seems rather close to the initial one on the left hand-side panel. But we know that by construction, the discrete musical isomorphisms are not proper inverse from one another. We can estimate the corresponding relative error:
import numpy.linalg as lng
err = [lng.norm(v2 - v1)/ lng.norm(v1)
for v1, v2 in zip(vct.values, sharp_flat_vct.values)]
print(f'The relative error between the amplitudes of the vectors and their sharp(flat()) counterparts is {np.mean(err):.2%} +/- {np.std(err):.2%}.')
The relative error between the amplitudes of the vectors and their sharp(flat()) counterparts is 12.73% +/- 0.00%.
The wedge product¶
Starting from two cochains of degrees $k_1$ and $k_2$, defined on a simplicial complex of dimension $n$, the wedge product enables the creation of a cochain of degree $k_1+k_2$, assuming $k_1+k_2 \leq n$.
The main property of the wedge product to show case at this point is its anti-symmetry:
from dxtr.operators import wedge
from dxtr.cochains import Cochain
from dxtr.complexes import icosahedron
mfld = icosahedron(manifold=True)
tol = 1e-15
N1 = mfld[1].size
N2 = mfld[2].size
cchn_1 = Cochain(mfld, 1, values= np.random.random(N1))
cchn_11 = wedge(cchn_1, cchn_1)
if (np.abs(cchn_11.values) < tol).all():
print(f'The wedge of a {cchn_1.name} with it self yields a {cchn_11.name} of max value: {cchn_11.values.max():.2e}\n')
cchn_2 = Cochain(mfld, 1, values= 3*np.ones(N1))
cchn_12 = wedge(cchn_1, cchn_2)
cchn_21 = wedge(cchn_2, cchn_1)
if (np.abs(cchn_12.values + cchn_21.values) < tol).all():
print('The wedge between two 1-cochains is indeed antisymmetric.')
The wedge of a Primal 1-cochain with it self yields a Primal 2-cochain of max value: 4.63e-18 The wedge between two 1-cochains is indeed antisymmetric.
If one of the considered cochain in the wedge is a 0-cochain, the wedge works as a scalar multiplication; i.e. each component of the other considered k-cochain, associated with a k-simplex, is scaled by the average value of the 0-cochain taken over the 0-faces of the k-simplex:
N0 = mfld[0].size
cchn_0 = Cochain(mfld, 0, np.random.random(N0))
cchn_2 = Cochain(mfld, 2, np.random.random(N2))
cchn_02 = wedge(cchn_0, cchn_2)
error = []
for sidx, val in cchn_02.items():
cfids = mfld.faces(2,0)[sidx]
xpct_val = cchn_0.values[cfids].mean() * cchn_2.values[sidx]
error.append(np.abs(val - xpct_val))
if (np.asarray(error) < tol).all():
print('Wedge with a 0-cochain works as expected.')
Wedge with a 0-cochain works as expected.