Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for facet orientation in FacetBasis #865

Merged
merged 28 commits into from
Jan 29, 2022

Conversation

kinnala
Copy link
Owner

@kinnala kinnala commented Jan 16, 2022

This PR now acknowledges the existence of oriented boundaries/interfaces (i.e. sets of facets).

We can load Gmsh file with an oriented interface in the middle:

In [1]: from skfem import *                                                     
In [2]: m = MeshTri.load('docs/examples/meshes/interface.msh')                  
In [3]: m.boundaries                                                            
Out[3]: {'interfacee': OrientedBoundary([ 7, 11, 50, 55, 60, 65, 70, 75])}

Here OrientedBoundary is a subclass of ndarray with the additional attribute ori which is either 0 or 1 for each facet.
We can visualize the orientation:

In [4]: m.draw(boundaries=True).show() 

Figure_1

The orientation is taken into account in FacetBasis, e.g.,

In [5]: fb = FacetBasis(m, ElementTriP1(), facets='interfacee')  
In [7]: from skfem.helpers import dot                                           
In [8]: Functional(lambda w: dot(w.y.grad, w.n)).assemble(fb, y=m.p[1])         
Out[8]: 1.0

Obviously there can be multiple oriented facet sets:

In [13]: m = MeshTri.load('docs/examples/meshes/internal.msh')                  
In [14]: m.draw(boundaries=True).show()

Figure_2

There is a facility for creating oriented facet sets around subdomains in Mesh.facets_around:

In [2]: from skfem import *                                                     
In [3]: m = MeshTri.load('docs/examples/meshes/internal.msh')                   
In [4]: M = m.with_boundaries({'left': m.facets_around(lambda x: x[0] < 0)})    
In [5]: M.draw(boundaries=True).show()

Figure_3

We can also now orient facet sets constructed with Mesh.facets_satisfying:

In [1]: from skfem import *
In [2]: m = MeshTri().refined(4)
In [3]: M = m.with_boundaries({'mid': m.facets_satisfying(lambda x: x[0] == 0.5,
   ...:  normal=[1, 0])})
In [4]: M.draw(boundaries=True).show()

Figure_1

Fixes #821 and fixes #870.

Missing work: boundary orientations are invalidated by save/load cycle and by refine.

@gdmcbain
Copy link
Contributor

gdmcbain commented Jan 16, 2022

Hmm. I think the only times that i've used an internal curve (in 2-D) were for lineal sources, like point sources distributed along a curve. For these the sign of the normal doesn't matter; it's just a linear form like unit_load along the curve, so i think in that case the orientation could be left as default.

I suppose cases where the sign of the normal is required might arise, though I can't think of any now. Maybe if the internal interface is specified as a sequence of facets (in two dimensions) then a consistent orientation could be inferred from that but obviously that's impossible in three dimensions.

@gdmcbain gdmcbain closed this Jan 16, 2022
@gdmcbain gdmcbain reopened this Jan 16, 2022
@kinnala
Copy link
Owner Author

kinnala commented Jan 16, 2022

Hmm. I think the only times that i've used an internal curve (in 2-D) were for lineal sources, like point sources distributed along a curve. For these the sign of the normal doesn't matter; it's just a linear form like unit_load along the curve, so i think in that case the orientation could be left as default.

I suppose cases where the sign of the normal is required might arise, though I can't think of any now. Maybe if the internal interface is specified as a sequence of face

By default (using InteriorFacetBasis) the orientation may change from one facet to another. Is that not an issue?

@kinnala
Copy link
Owner Author

kinnala commented Jan 16, 2022

I'm referring to your comment in #854:

See my 2012 slides from the Fourth FreeFem workshop : Domain decomposition techniques for interfacial discontinuities. This is all really easy in FreeFEM because it's possible to define (n-1)-dimensional bits of the mesh (basically "boundaries" but not necessarily actually on the boundary, really just subsets of facets) that are consistently oriented.

@gdmcbain
Copy link
Contributor

This is all really easy in FreeFEM because it's possible to define (n-1)-dimensional bits of the mesh (basically "boundaries" but not necessarily actually on the boundary, really just subsets of facets) that are consistently oriented.

Right. For n = 2, FreeFEM includes commands for defining two-dimensional geometry and meshing it with triangles. First one defines curves parametrically, like (x (t), y (t) for atb and then these curves can either be closed or chained together to bound a planar region. These curves can be tagged with an int and they retain the orientation implied by the parametric construction: increasing t.
Otherwise, for n = 2 or 3, one can construct a MSH in Gmsh that has (n−1)-dimensional entities, mark them as Physical Curve or Physical Surface. This is understood by FreeFEM, including the orientation. Even for n = 3, Gmsh will consistently orient hypersurfaces.

@gdmcbain
Copy link
Contributor

By default (using InteriorFacetBasis) the orientation may change from one facet to another. Is that not an issue?

For a lineal source? (Or a superficial source in three dimensions?) No, I don't think it's an issue, because the normal isn't referred to in the linear form, so I don't think it matters if the sign of the normal is arbitrary.

@kinnala
Copy link
Owner Author

kinnala commented Jan 17, 2022

This is all really easy in FreeFEM because it's possible to define (n-1)-dimensional bits of the mesh (basically "boundaries" but not necessarily actually on the boundary, really just subsets of facets) that are consistently oriented.

Right. For n = 2, FreeFEM includes commands for defining two-dimensional geometry and meshing it with triangles. First one defines curves parametrically, like (x (t), y (t) for atb and then these curves can either be closed or chained together to bound a planar region. These curves can be tagged with an int and they retain the orientation implied by the parametric construction: increasing t. Otherwise, for n = 2 or 3, one can construct a MSH in Gmsh that has (_n_−1)-dimensional entities, mark them as Physical Curve or Physical Surface. This is understood by FreeFEM, including the orientation. Even for n = 3, Gmsh will consistently orient hypersurfaces.

Alright, so there is some property in msh files that tells the orientation and we need to understand.

@gdmcbain
Copy link
Contributor

gdmcbain commented Jan 17, 2022

Well, yes, probably, for the case of an internal surface in three dimensions, but that's not terribly common so i wouldn't think it urgent. I don't think i've ever actually used that in 3-D myself. Two dimensions yes, but that's easier because it's possible to specify the facets in sequence.

I think the details of how the orientation is specified by the user can be left open for now? On the understanding that users might want to specify it in different ways.

A really common way is that the defining facets are specified not as facets of cells but by their points, with the order of the points significant. I tripped over this way back in #72. It had not occurred to me that meshes might not imply the orientation of cells. Anyway, this is how Gmsh does it on MSH: facets are (n−1)-dimensional cells supported on their own points with the sequence implying orientation.

@gdmcbain
Copy link
Contributor

Yeah, that might be a reasonable thing to support: optionally inferring the orientation of the "boundaries" if reading from meshio. I think MSH is not alone in linking the sequence of points with orientation. Typically it's a right-hand rule for trianglar facets. For line-facets in two dimensions, it's the same: the line is assumed to be described with its normal pointing right; as for an outward normal when a boundary is described counterclockwise.

@kinnala
Copy link
Owner Author

kinnala commented Jan 17, 2022

Our description of facets is more implicit though:

  • After loading, is the order of facets in mesh.boundaries the same as in the mesh file or is the sorting different?
  • We don't support an explicit description of facets (yet?) at it must always refer to the order in Mesh.facets

Why I wrote "yet"?

I've been thinking about the integration of bilinear forms over 2D surfaces embedded in 3D. There you would need an explicit description of the surface. I've done some work towards supporting this but I'd like to do it properly before releasing.

What do I mean by "properly"?

There are examples where you have one unknown on, say, a tetrahedral mesh and another unknown on it's boundary. An interesting example is elasticity equations (displacement) on a tetrahedral mesh and Reynolds equation (pressure) on the boundary. Now while you can sort of hack this together already using CellBasis for elasticity and FacetBasis for Reynolds equation, I'd like to be able to define another surface mesh and then use CellBasis on that surface mesh and then somehow persist the connection between these two CellBasis(MeshTet, ElementTriP1) and CellBasis(SurfaceMeshTri, ElementTriP1 objects.

However, this seems to be somewhat nontrivial because of how Dofs is implemented (there must be same number of DOFs for each element). If you think of the surface mesh being part of the boundary of a tetrahedral mesh, some tetrahedra only share an edge with the surface and some tetrahedra also share a facet. So the number of DOFs (related to the surface mesh) on the boundary of the tetrahedral mesh is not constant per element. I think nonvectorized codes just do business as usual. This is somewhat similar issue we have with mixed meshes.

I have several ideas and are just trying to think about the best course of action.

However, this is unrelated to what we are doing here so I'll stop the rant now.

@gdmcbain
Copy link
Contributor

Thank you; super interesting.

Is this a bit like #514 ? There I wanted what I called there a FacetMesh, to be extracted from a MeshQuad (but conceptually any skfem.Mesh of dim > 1). This was solved in #530 and has been working routinely ever since. That case was significantly simpler in that the FacetMesh was collinear. Yes, the curvature of the underlying space is a big difference.

There are also applications involving surfaces in space not attached to a volume mesh, e.g. minimal or constant-mean-curvature surfaces which cannot be expressed as z = z (x, y) as in ex10); I have solved such problems using Surface Evolver. These are deforming curved surface meshes. I suppose curves in the plane and space are also possible, e.g. two-dimensional or axially symmetric reductions of the former.

@kinnala
Copy link
Owner Author

kinnala commented Jan 18, 2022

Is this a bit like #514 ? There I wanted what I called there a FacetMesh, to be extracted from a MeshQuad (but conceptually any skfem.Mesh of dim > 1). This was solved in #530 and has been working routinely ever since. That case was significantly simpler in that the FacetMesh was collinear. Yes, the curvature of the underlying space is a big difference.

Yes, I think that would be one of the use cases.

Later this year I'm traveling abroad for a research visit where the plan is to study "shell models" using differential geometry. So solving equations on embedded surfaces will definitely be a short term priority for me.

@gatling-nrl
Copy link
Contributor

If I'm understanding what @gdmcbain is saying, sometimes he wants flux across a subset of facets that don't necessarily form the boundary of a subdomain. I think maybe SubdomainFacetBasis was the wrong name from the start. I know its a bit verbose, but

class AbstractBasis: ...
class CellBasis(AbstractBasis): ...
class FacetBasis(AbstractBasis): ...
class InteriorFacetBasis(FacetBasis): ...
class BoundaryFacetBasis(FacetBasis): ...
class InteriorBoundaryFacetBasis(BoundaryFacetBasis): ...

And I guess when I write it this way, I can understand how #865 BoundaryFacetBasis could be about generalizing the existing BoundaryFacetBasis so that it handles interior, exterior, closed and open boundaries. I think that makes sense, and no need for InteriorBoundaryFacetBasis

I wish we could get rid of init_subdomain() though. It doesn't seem very pythonic. Maybe leveraging how get_dofs() works, we could have something like

class BoundaryFacetBasis(FacetBasis):
    def __init__(self, ..., elements, facets, outward_normal, outward_trace):
    ...

If elements is supplied, BoundaryFacetBasis acts like #851, acting on the boundary of the region(s) defined by elements. The facets keyword would allow describing arbitrary, not necessarily closed, sets of facets.

@gdmcbain
Copy link
Contributor

Part of what I'm thinking is that in software terms none of the subclasses of BoundaryFacetBasis

class InteriorFacetBasis(BoundaryFacetBasis):

class MortarFacetBasis(BoundaryFacetBasis):

class SubdomainFacetBasis(BoundaryFacetBasis):

have any methods other than __init__, except for the last's with_element (which looks like something of a workaround that would ideally be made unnecessary?). That is, after they've been instantiated, aren't these all just facet bases with particular attributes? If so, shouldn't that be implemented by a single class with one or more sufficiently flexible constructors? Aren't they all just defined by the parent CellBasis and the subset of facets of that of which they're consistuted, along with the orientation in terms of which side the trace is from and which sign to give the normal. (Possibly making do with a single "side" for that, along with a convention that the normal is outward, i.e. away from the side on which the trace is approached.)

If my understanding there is right, then that refactoring can be considered and accepted or rejected without too much controversy. The important thing then, either way, is how to build the subsets of facets along with their orientation, either way, whether that's in the __init__ methods of the subclasses, in constructor classmethods, or even just in instancemethods, module functions, or user-level snippets.

There's a lot of lines of code in this PR, but if

@classmethod
def init_subdomain(cls, mesh, elem, elements=None, side=0, **kwargs):

is the core of it and the rest is just along for the ride, then yeah I think this is what I'm talking about and I think it's what's wanted, along with possible siblings to be added to the library or in user-level code as required.

By 'unpythonic' do you mean the name? I'm not terribly fond of init_subdomain either but don't have a better suggestion to hand. Or do you mean that, as in your snippet above, that the flexibility could be provided by optional keyword arguments in FacetBasis.__init__ rather than by a special classmethod. Yeah, O. K., I don't have a strong opinion on that. Yeah, maybe you're right, maybe a single more flexible class constructor can cover a lot of the most common facet bases?

Yeah, rereading your comment, I think maybe we're converging?

I think the name BoundaryFacetBasis will become bad, as will its docstring. Facets in a basis might or might not be on a boundary, it's kind of irrelevant, isn't it? Although of course very often one will want exactly those facets that are on the boundary of the domain or of a subdomain, sure, but that's a job for a constructor and irrelevant after instantiation.

@gatling-nrl
Copy link
Contributor

I think we're converging too. Let me just add this analogue to clarify what I'm saying. If we made a geometry module, they might all be polygons. And you could argue that we should just have one Polygon class with a really complex constructor, since in the end all the work (perimeter, area, whatever) will really be done in one class.

But I argue that by subclassing Polygon into Square and Triangle, we get to make more, simpler constructors (the unix mentality). We also get a convenient place to do extra parameter checks, raise informative exceptions, place extra documentation specific to those shapes. This is purely a semantic/syntactic computer sciency argument, and it can be taken to far. 100's of classes for every named Polygon would become unwieldy, hard to remember the right name, and most of that code would never get run in userland, ever. But for AbstractBasis, even with the feature creep and the extensions we're proposing now, its still less than 10 classes. Quite manageable, and they're all really useful.

Keeping with this analogy, by unpythonic, I meant that I dislike

class Polygon:
    @classmethod
    def init_square(...):...
    def init_triangle(...):...
    def __init__(self,...):...

Nothing stops from writing the class this way, but it doesn't feel like the spirit of python or object oriented to me. It's kind of like having the naked ndarray's running around. Later in my code, I have a bunch of Polygon's, instead of two Squares and a Triangle. There's a similar problem in skfem of telling the difference between the output of basis.zeros() and basis.interpolate(). And as a result, skfem is unable to tell me, "hey, exception, you gave me a projection but i wanted values at quadrature points". Duck typing is great right up until the error messages become disconnected from how the programmer is thinking.

A last point about it, is that subclassing for Square and Triangle is going to be a familiar construction for many python programmers. classmethods for init_square, etc, are not going to be familiar, and consequently they will steepen the learning curve for new adoptors. I think there's a lot of value in staying as close to familiar idioms as possible.

To answer your questions:

which looks like something of a workaround that would ideally be made unnecessary?

Whether it becomes unnecessary or not, the ability to tweak the methods of AbstractBasis before calling super() is one of the top reasons why I prefer subclassing over fancy constructors. It usually makes simpler, understandable code with less if statements.

aren't these all just facet bases with particular attributes?

And particular exceptions and particular documentation, and occasionally specialty functionality / attributes. Example: Square might implement "diagonal_length()" which might not make sense for other things. I don't have an example at hand right now for skfem, but I feel sure one will turn up. (is_boundary_closed comes to mind, but meh.)

shouldn't that be implemented by a single class with one or more sufficiently flexible constructors?

Same thing, as a general rule, I'm opposed to large functions with lots of if statements. Sometimes this rule is hard to keep (get_dofs, for example) but in this case particularly, it is one of the main use cases for subclassing.

By 'unpythonic' do you mean the name?

No, but by now you know what i meant :)

flexibility could be provided by optional keyword arguments

Where possible, I prefer simpler functions with less if statements. Optional keyword arguments gets very iffy.

Facets in a basis might or might not be on a boundary, it's kind of irrelevant, isn't it?

Computationally, I agree. Conceptually, no, its very relevant because as a user you're trying to do something, and when it doesn't work, you want messages like "Exception: can't create cell basis from a facet boundary that isn't closed"... not messages like "Exception: None type has no method is_closed"

@gatling-nrl
Copy link
Contributor

@gdmcbain thoughts? are we converging?

class AbstractBasis:
    def __init__(self, mesh, elem, etc): 
        ...

class CellBasis(AbstractBasis):
    def __init__(self, mesh, elem, etc, elements=None):
        """Define a basis over a set of cells.
        
        elements
            an arbitrary set of elements in the mesh

        For `elements`, None is interpreted as "the set of all elements"
        """
        ...

class FacetBasis(AbstractBasis):
    def __init__(self, mesh, elem, etc, facets=None):
        """Define a basis over a set of facets. Use subset of mesh if facets is not None.

        facets
            an arbitrary set of facets in the mesh.

        For `facets`, None is interpreted as "the set of all facets"
        """
        ...

class InteriorFacetBasis(FacetBasis):
    def __init__(self, mesh, elem, etc, elements=None, facets=None, combine="intersect"):
        """Define a basis over a set of facets. Excludes facets that bound a single cell of the set specified by `elements`.
        
        elements
            specifies the set of facets interior to these elements.
        facets
            an arbitrary set of facets in the mesh.
        combine
            specify how to combine the sets specified by `elements` and `facets`
                "union" - union of the two sets
                "intersect" - intersection of the two sets

        For `facets` and `elements`, None is interpreted as "the set of all facets"
        """
        ...

class BoundaryFacetBasis(FacetBasis):
    def __init__(self, mesh, elem, etc, elements=None, facets=None, combine="intersect"):
        """Define a basis over a set of facets. Excludes facets that bound two cells of the set specified by `elements`.
        
        elements
             specifies the set of facets that bound these elements.
        facets
            an arbitrary set of facets in the mesh.
        combine
            specify how to combine the sets specified by `elements` and `facets`
                "union" - union of the two sets
                "intersect " - intersection of the two sets

        For `facets` and `elements`, None is interpreted as "the set of all facets"
        """
        ...

@gdmcbain
Copy link
Contributor

No, the idea that a class's having multiple constructor functions is unpythonic is new to me; e.g. for numoy.ndarray there are a lot of array creation routines. Here a MeshTri might be:

  • translated from_meshio,
  • split from MeshQuad.to_meshtri,
  • .refined from a coarser MeshTri
  • constructed from the raw points and cell-connectivity with __init__
  • specially constructed with:
    • init_square
    • init_circle

Afterwards one just has an ndarray or a MeshTri and they all behave the same. Is it the business of a subclass to remember how an object was created?

@gdmcbain
Copy link
Contributor

But I argue that by subclassing Polygon into Square and Triangle, we get to make more, simpler constructors (the unix mentality).

No. In Unix, every command has the same type of output, line-separated text, and every command returns the same type value, an int (0 for no problem). As Alan Perlis put it in the ninth of his epigrams on programming:

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.

I suspect the success of the ecosystem growing up around NumPy is in large part due to the use of ndarray as that one data structure operated on by hundreds or thousands of functions in NumPy, SciPy, matplotlib, and beyond, including scikit-fem in particular, which distinguishes itself from other implementations of FEM in Python by making such direct use of ndarray. There are standard array subclasses but

Subclassing a numpy.ndarray is possible but if your goal is to create an array with modified behavior, as do dask arrays for distributed computation and cupy arrays for GPU-based computation, subclassing is discouraged. Instead, using numpy’s dispatch mechanism is recommended.

There was an ugly wobble with numpy.matrix but that was early on and now

It is no longer recommended to use this class, even for linear algebra. Instead use regular arrays. The class may be removed in the future.

For the polygonal example, I do quite a bit of work with polygons (e.g. integrated circuit layout) and usually it is just a single class for n-gons for n > 2 rather than a denumerably infinite sequence of subclasses Triangle, Quadrilateral, Pentagon, …, Chiliagon, …; I'm not sure how that would work. Some useful ones which I will take the opportunity to gratefully share:

But yes, resoundingly, to ‘more, simpler constructors’; this is indeed the Unix philosophy, and Perlis's, and, I think, is exactly what we want. Whether they're classmethods like this PR or module-functions, I don't think matters much; whatever makes for simpler implementation in each case. The classmethod here looks fine.

@gatling-nrl
Copy link
Contributor

These are all really good points, and I threw "unpythonic" around too flippantly. And in particular, your invocation of numpys maxtrix is a powerful one. I too am grateful that matrix type died.

I think I overreached in this PR, because your counterexamples are compelling. But I'm still quite interested in helping make skfem easier to use if I can.

Are you proposing collapsing InteriorFacetBasis and BoundaryFacetBasis into a single FacetBasis class together with a set of helper constructor methods? What about CellBasis and FacetBasis collapsed into AbstractBasis. For the sake of understanding, if you were back on day one, which Basis classes would you make, and which constructor methods?

@gdmcbain
Copy link
Contributor

Are you proposing collapsing InteriorFacetBasis and BoundaryFacetBasis into a single FacetBasis class together with a set of helper constructor methods?

Yes, I think so. I need to study MortarFacetBasis though. I notice that it doesn't have any special methods either. I haven't used it but it might be the really important one. I realized over in #150 that there's a lot of commonality between Dirichlet boundaries and interfaces: the Dirichlet boundary can be thought of as an interface between the resolved meshed part of the domain and some ideal reservoir or isopotential source. Similarly a Robin condition is like that but with a thin resistance in the way. That is, techniques for imposing Dirichlet conditions at boundaries (condense, enforce, penalize, Lagrange multiplier, Nitsche) seem usually to be very like special cases of techniques for imposing interfacial conditions (continuity or otherwise of potential and flux). @kinnala knows a lot more about this stuff.

What about CellBasis and FacetBasis collapsed into AbstractBasis. For the sake of understanding, if you were back on day one, which Basis classes would you make, and which constructor methods?

Well the obvious difference initially is that facets are (n − 1)-dimensional and so have a normal and a side (for the traces). But lately @kinnala mentioned the very exciting extension to formulating problems on what are now facet-bases. There was a little bit of this in #514 (just L2-projections on rectilinear facet-bases) but if it will mean that general meshes have nontrivial metric tensors then I suppose maybe there will be less difference between an n-dimensional cell-basis and an (n − 1)-dimensional facet-basis? That is, both might equally well be curved with respect to some still higher-dimensional space that they're embedded in.

I guess the main thing about a facet-basis though is that is related to some cell-basis of one-higher dimension. The facets which are the cells of the facet-basis are facets of cells of the cell-basis. And they're linked by trace operators: if there's a function defined on the cell-basis then it has values (traces) implied on the facet-basis. Also it has the normal component of the gradient, which is another trace operator. There are probably higher ones too, but these are the two most met in applications governed by second-order partial differential operators. Thus we do have FacetBasis.project which implements the first trace operator and this method only makes sense for a FacetBasis, not a CellBasis. I think I'm taking 'has a method which doesn't make any sense for its superclass' as an indicator that something might be a subclass, along with the classical 'is a'. But yes conceptually it should be possible to treat a facet-basis as a cell-basis in its own right, with dimension n − 1 and generally curved with respect to the n-dimensional space that it is embedded in by virtue of its facet-is-a-facet-of-a-cell relation.

@gatling-nrl
Copy link
Contributor

techniques for imposing interfacial conditions (continuity or otherwise of potential and flux)

maybe irrelevant to this PR, but I'm extremely interested in this topic. might be a good general discussion to start. i'm trying to embed skfem in an impedance tomography problem, and assumptions about the electrode interface is pretty significant to the found solutions.

That is, both might equally well be curved with respect to some still higher-dimensional space that they're embedded in.

e.g. how the latitudes are curved on the surface of a sphere, which is itself curved in 3d?

I can't seem to stop thinking about (what I think of as) pathological meshes... e.g. mobius strip... and how these concepts of orientation apply. It's very non-trivial for me to try to visualize what "consistent normal" might look like this n-dimensional context.

I think I'm taking 'has a method which doesn't make any sense for its superclass' as an indicator that something might be a subclass, along with the classical 'is a'.

I still want to pull one thing from my "unpythonic" rant... I'm specifically thinking about "should basis.zeros() return an ndarray or a subclass"...

Is it the business of a subclass to remember how an object was created?

I said "no" before, but i want to amend that to "sometimes". the basis part of basis.zeros() is useful to keep around... in fact I have to keep it around to later interpolate. Keeping the creation attributes around so they can be reused and possibly being able to report meaningful, contextual exceptions is also worth considering.

But having read all this the past couple of days, I'm in your camp now that CellBasis and FacetBasis seem like enough. Whether AbstractBasis is enough has gotten abstract enough that I can't really visualize it :)

@gdmcbain
Copy link
Contributor

techniques for imposing interfacial conditions (continuity or otherwise of potential and flux)

maybe irrelevant to this PR, but I'm extremely interested in this topic. might be a good general discussion to start.

Great. I launched #867 ‘interfacial coupling and discontinuities’.

@gdmcbain
Copy link
Contributor

That is, both might equally well be curved with respect to some still higher-dimensional space that they're embedded in.

e.g. how the latitudes are curved on the surface of a sphere, which is itself curved in 3d?

Yes, exactly. If the curve or surface can be described parametrically in coordinates in the embedding space, e.g. x, y, and z as functions of latitude and longitude for a sphere, or functions just of longitude for a parallel, then there are formulæ for calculating the curvature, normalls, binormals, torsion, &c., from these. These may be found in treatises on differential geometry, particularly Riemannian geometry. The key concept is the metric tensor which in simple terms gives the net infinitesimal distance implied by an infinitesimal step in each of the local coordinates. There are also formulæ for the case in which the surface is given by a level-set of a function in space; here the normal is parallel to the gradient of that function and the mean curvature is given by the divergence of the gradient. It's also possible to start with a metric tensor and not think of the working space as being embedded in a higher-dimensional space; this might arise in relativity (and so in some formulations of electromagnetism?) but not so much in my sublunary applications so far.

@gdmcbain
Copy link
Contributor

I can't seem to stop thinking about (what I think of as) pathological meshes... e.g. mobius strip... and how these concepts of orientation apply.

Right. A Möbius strip is nonorientable as a surface. It could be triangulated easily enough but the resulting mesh would not be orientable either. I think most meshers would baulk; e.g. Gmsh. By far the most dealings that I have had with inconsistently oriented manifolds apart from #72 and #821 have been in exceptions raised by Gmsh because I have tried to define a volume by its bounding surface which I have stitched together from multiple patches with inadvertently inconsistent orientations but, distinguishing between oriented and orientable, I have never had to deal with nonorientable manifolds and am not sure how they might arise or even what kind of boundary value problems might reasonably be posed on them.

It'd be easy enough to parameterize the Möbius strip as embedded in ℝ³ and then try to calculate the normal using the standard formulæ. I haven't tried that. I assume it's two-valued? There's probably a square-root in there somewhere?

It's very non-trivial for me to try to visualize what "consistent normal" might look like this n-dimensional context.

Yeah, it's best to fall back on the standard formulæ from differential geometry which visualization fails or deceives, then build up the intuitiion again from there. The introductions to this field that I found particularly useful and intuitive were:

but there might well be more modern ones that are good too. Very likely there are even better ones relating to electromagnetism; see https://en.wikipedia.org/wiki/Differential_form#Applications_in_physics.

@gdmcbain
Copy link
Contributor

I think I'm taking 'has a method which doesn't make any sense for its superclass' as an indicator that something might be a subclass, along with the classical 'is a'.

I still want to pull one thing from my "unpythonic" rant... I'm specifically thinking about "should basis.zeros() return an ndarray or a subclass"...

Is it the business of a subclass to remember how an object was created?

I said "no" before, but i want to amend that to "sometimes". the basis part of basis.zeros() is useful to keep around... in fact I have to keep it around to later interpolate. Keeping the creation attributes around so they can be reused and possibly being able to report meaningful, contextual exceptions is also worth considering.

Hmm, I see. Yeah, I'm used to just remembering which basis it came from…, even though I do very often have a lot of these floating around in the one application. I generally collect the bases in a dict, like

basis = {
v: Basis(mesh, e, intorder=4)
for v, e in [("u", ElementLineMini()), ("p", ElementLineP1())]
}

So I think of AbstractBasis.zeros as most importantly returning an example of a member of the function space spanned by the basis, as represented in the basis. If this representation is to be as an ndarray then that's what .zeros should return. Hmm, yeah, the user does need to know which basis the array of coefficients are defined with respect to. Hmm. Yeah, I'm used to remembering the basis. Having the solution just as a ndarray is definitely easy and simple. I'd hesitate to give this up and have to go through all the indirection required in other FEM codes but I can definitely see the trade-off. One does have to keep the basis aroiund. I'm inclined to stick with the status quo here; I think @kinnala made the right design choice (Perlis-style) and it's a defining distinction of scikit-fem.

@gdmcbain
Copy link
Contributor

But having read all this the past couple of days, I'm in your camp now that CellBasis and FacetBasis seem like enough.

I'd like to understand MortarFacetBasis first. It looks important but there's quite a lot going on in ex04. Maybe it can be the one class for facet-bases; it would need to be checked that it doesn't have features (attributes, methods) that don't make sense for other facet-bases.

@gdmcbain
Copy link
Contributor

Yeah nah, a global sign attribute seems like a cumbersome effort to avoid minus-signs in formulæ which is impossible: move a term from one side of an equation to the other and it changes sign, e.g. Please show a plausible case in which this would result in simpler code. Conditionals inside forms öook reslly bad.

@kinnala
Copy link
Owner Author

kinnala commented Jan 28, 2022

But then you can't freely choose the trace.

That is done using FacetBasis(..., side=0/1) which does not affect the normal.

@gatling-nrl
Copy link
Contributor

skfem should be close to what would be wriiten on paper or the blackboard. Mathematics written in those media doesn't have the constraint of having to be implementable and so often elides much by using common unwritten contextual assumptions

numpy is this way too, I think. I write A @ x and A.T @ A a lot, and its pretty close to blackboard notation. But I can also write A.shape and A.dtype, details I don't usually think about on paper. Maybe you would point out there's no A.transposed and that basis.flipped would be something like that. I guess I'm on the fence about whether or not the flipped nature of the traces is akin to the dtype. It seems like a valuable tidbit of data to have on each object for which it applies, similar to dtype.

It is possible in numpy to change the dtype of an array you already have. Might I construct new facet basises this way? Similar to basis.with_element?

b1 = FacetBasis(...)
b2 = b1.with_flipped(normals=False, traces=True)

Yeah nah, a global sign attribute

I hadn't thought of it this way, but I actually like the metaphor a lot. I'm not sure I'm giving good use cases, but it does seem like the facet basises are "signed", and if so, I think you should be able to get at the sign? Maybe I'm not proposing the right implementation.

@gatling-nrl
Copy link
Contributor

But then you can't freely choose the trace.

That is done using FacetBasis(..., side=0/1) which does not affect the normal.

Oh... maybe I'm misunderstanding the new syntax then.

@gatling-nrl
Copy link
Contributor

Conditionals inside forms look really bad.

Is this because the design goal of a forms/functional is one line of code that looks very similar to what's written on blackboard?

@gatling-nrl
Copy link
Contributor

gatling-nrl commented Jan 28, 2022

But then you can't freely choose the trace.
That is done using FacetBasis(..., side=0/1) which does not affect the normal.
Oh... maybe I'm misunderstanding the new syntax then.

I haven't pulled your latest commits, but I did move from #851 to #865. Currently I'm using

f_p2 = skfem.FacetBasis(mesh, skfem.ElementTriP2(), facets=mesh.facets_around('inner'), flip_traces=True)

This is giving me outside traces and outside pointing normals.
Are the four cases in the current commit now written like this below?

a = FacetBasis(m, e, facets=m.facets_around('subdomain'), flip=False, side=0) # default
b = FacetBasis(m, e, facets=m.facets_around('subdomain'), flip=False, side=1)
c = FacetBasis(m, e, facets=m.facets_around('subdomain'), flip=True, side=0)  
d = FacetBasis(m, e, facets=m.facets_around('subdomain'), flip=True, side=1)

Am I just doing things all wrong to be worrying so much about which trace and which way the normal? It is currently very important to keep track of in my code. In most cases I care about, the traces aren't equal. Often (but not always) I have a DC boundary condition on one side, and I never want that trace.

A lot of my bugs have come down to which way the normals are and which trace I'm using.

edit: Almost everything I'm doing with skfem involves working with net fluxes into and out of specific regions in the domain.

@kinnala
Copy link
Owner Author

kinnala commented Jan 28, 2022

a = FacetBasis(m, e, facets=m.facets_around('subdomain', flip=False), side=0) # default
b = FacetBasis(m, e, facets=m.facets_around('subdomain', flip=False), side=1)
c = FacetBasis(m, e, facets=m.facets_around('subdomain', flip=True), side=0)  
d = FacetBasis(m, e, facets=m.facets_around('subdomain', flip=True), side=1)

Fixed.

@kinnala
Copy link
Owner Author

kinnala commented Jan 28, 2022

edit: Almost everything I'm doing with skfem involves working with net fluxes into and out of specific regions in the domain.

Is it possible to use Gauss divergence theorem and turn it into a domain integral?

@kinnala
Copy link
Owner Author

kinnala commented Jan 28, 2022

This PR is hopefully ready soon. I think I'll need to leave the save/load later because I'm running out of time.

@gdmcbain
Copy link
Contributor

Am I just doing things all wrong to be worrying so much about which trace and which way the normal?

No and yes. The trace is really important because it can have completely different values on the two sides (e.g. ex26); it does need to be worried about and selected carefully. Yes for the normal though; as long as it's consistent, it's just a matter of global sign and this cannot be fixed with an attribute.

It might be reasonable to define the unary negation of an oriented facet array as the same array with all the orientations flipped. This is consistent with the mathematical idea of a contour integral in the plane with a counterclockwise or clockwise contour; similarly for surface integrals in space.

@gdmcbain
Copy link
Contributor

I think I'll need to leave the save/load later because I'm running out of time.

Yeah, I wonder whether we were a bit too clever in #681 ...or do we just need a little bit more of such cleverness here. I have not successfully run the failing encode decide tests in the mental Python interpreter or gotten to a terminal so I'm not yet clear on the issue. I hope to get to this next week but I would think it reasonable to proceed with the rest of the PR as serialization is fairly specialized.

@gdmcbain
Copy link
Contributor

Is it possible to use Gauss divergence theorem and turn it into a domain integral?

Almost certainly, and this will be more robust and accurate as demonstrated in our ANZIAM Journal paper and ex. 13.

@kinnala kinnala merged commit af69c7f into master Jan 29, 2022
@kinnala kinnala deleted the add-fbasis-init-subdomain branch January 29, 2022 14:08
@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 2, 2022

This PR is hopefully ready soon. I think I'll need to leave the save/load later because I'm running out of time.

Discussion: #873

@gatling-nrl
Copy link
Contributor

Oops! I almost missed these comments. Does github preserve these discussions on deleted branches indefinitely? There's a lot in this one I want to try to migrate over to more formal documentation when I have time.

It might be reasonable to define the unary negation of an oriented facet array as the same array with all the orientations flipped. This is consistent with the mathematical idea of a contour integral in the plane with a counterclockwise or clockwise contour; similarly for surface integrals in space.

I think this is along the lines of what I was trying to describe. And that idea of being able to -1* something does lead pretty naturally to the idea of that thing being signed doesn't it? But I can't stand the idea of reading if oriented_array < 0 so I don't know what practical significance it has.

In any case, @kinnala set me straight on the syntax, and so now all my facet basises are just born correct and I don't foresee a reason to want to be flipping the normals around or ever testing which way they go.

Is it possible to use Gauss divergence theorem and turn it into a domain integral?

Almost certainly, and this will be more robust and accurate as demonstrated in our ANZIAM Journal paper

I'll look into these, thanks for the reference.

@gatling-nrl
Copy link
Contributor

Is it possible to use Gauss divergence theorem and turn it into a domain integral?

Almost certainly, and this will be more robust and accurate as demonstrated in our ANZIAM Journal paper and ex. 13.

Is this the relevant portion? This is basically what I am doing, except my conductivity is not uniform. I thought you meant to change from an integral over facets to an integral over cells.

@Functional
def port_flux(w):
from skfem.helpers import dot, grad
return dot(w.n, grad(w['u']))
current = {}
for port, boundary in mesh.boundaries.items():
fbasis = FacetBasis(mesh, elements, facets=boundary)
current[port] = asm(port_flux, fbasis, u=u)

@kinnala
Copy link
Owner Author

kinnala commented Feb 3, 2022

If you apply Gauss divergence on that integral around a closed loop you can replace it by u.T @ A @ u where A is the matrix (sigma grad(u), grad(v)) integrated over the subdomain. But I think this requires u is not constant inside the subdomain?

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

No, that's a boundary integral; the bit in ex13 that's being recommended is

conductance = {'skfem': u @ A @ u,

It doesn't immediately look like a 'domain integral', more like a quadratic form, but this is the third of the ''Three ways to compute multiport inertance'; the boundary flux integral is the first and the connection is made explicit by the second.

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

But I think this requires u is not constant inside the subdomain?

Oh yeah you'd want to evaluate the (sub)domain integral over the complementary subdomain in which the potential actually varies between the source and sink values.

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

This is basically what I am doing, except my conductivity is not uniform.

The nonuniformity shouldn't matter, even anisotropy, so long as the conductivity is everywhere a positive definite symmetric tensor bounded above a positive constant. It might be nice to generalize the derivation in the ANZIAM J. to this case; I think that should be straightforward.

Insofar as I understand the application it's going to conceptually involve calculating the conductance matrix for a bunch of electrodes ('ports') embedded in a conducting solid and there'll be one such matrix for each geometry, set of electrode positions, and conductivity fields.

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

What the conductance matrix is is that if one charges the ith electrode to unit potential and earth the rest, the ijth coefficient is minus (can't avoid those minus signs!) the current out of the jth electrode.

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

Further generalization: if one uses AC instead of DC the conductivity becomes complex and the symmetric conductance matrix becomes a Hermitian admittance matrix. It should still be positive though if the medium is passive.

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

I think this is along the lines of what I was trying to describe. And that idea of being able to -1* something does lead pretty naturally to the idea of that thing being signed doesn't it?

No. A numoy.ndarray has a unary negation but doesn't have a sign; each of its entries has a sign (if it's real—if it's complex it still has a unary negation but entries can't be compared to zero with >).

@gdmcbain
Copy link
Contributor

gdmcbain commented Feb 3, 2022

I don't think it's really worth defining the unary negation of an OrientedBoundary (which I'm taking to mean ori = 1 - ori). Just remember where the minus signs are in the Green's identities.

@gatling-nrl
Copy link
Contributor

No. A numoy.ndarray has a unary negation but doesn't have a sign; each of its entries has a sign (if it's real—if it's complex it still has a unary negation but entries can't be compared to zero with >).

Fair. After I wrote that comment it did occur to me that even just a single complex number has unary negation but there's no good concept of the "sign" of a complex number. So clearly not all things that can be negated are signed.

I don't think it's really worth defining the unary negation of an OrientedBoundary (which I'm taking to mean ori = 1 - ori). Just remember where the minus signs are in the Green's identities.

I agree completely. After the dust settled I think FacetBasis and OrientedBoundary came out making a lot of sense. All that sign/negation stuff was born out of some confusion mid-PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants