12
$\begingroup$

I was looking at this question here and I tried the suggested idea by Anton Antonov to use DelaunayMesh

It will look like this:

points = {{0, 0, 1}, {5, 0, 0}, {1, 3, 0}, {0, 0, 2}, {4, 3, 0}, 
               {5, 0, 2}, {1, 3, 2}, {4, 3, 2}};
r=DelaunayMesh[points]

enter image description here

I tried to take something from r and I realised that r is an Atomic expression and I can not take anything from it similar to what we do with Graphics

Looking at the FullForm of r we can see: enter image description here

How to take something from this (other than copy paste) similar to what we used to do with Graphics

Note that these methods do not work:

Cases[r, Tetrahedron[x_] :> x, -1]
Cases[r, MeshRegion[x_, __] :> x]

Same thing with other functions like BoundaryMeshRegion, MeshRegion, DiscretizeRegion,Graph and so on

Thank you

$\endgroup$
  • $\begingroup$ If it's atomic, then we're not meant to access elements using Part. What exactly are you looking to extract? Is there no existing function to extract the particular object you are trying to get? MeshCells[r, 3] will return the tetrahedra. $\endgroup$ – Szabolcs Oct 5 '15 at 15:35
  • $\begingroup$ For this particular example I want to extract the order of the points in the Tetrahedron and in general it will be helpful to be able to extract some info with the order generated in such functions. I know that Part does not work and I thought there could be another way to get what you want from such functions. $\endgroup$ – Algohi Oct 5 '15 at 15:44
  • $\begingroup$ Can you explain what you mean by "order of the points"? $\endgroup$ – Szabolcs Oct 5 '15 at 15:48
  • $\begingroup$ If you look at the Tetrahedron it contains numbers indicate the order of the points in the list. In another word Tetrahedron[{8, 1, 6, 4}] is equivalent to Tetrahedron[points[[#]] & /@ {8, 1, 6, 4}] $\endgroup$ – Algohi Oct 5 '15 at 15:56
  • $\begingroup$ Ah, you mean the indices. But MeshCells[r, 3] returns exactly that. $\endgroup$ – Szabolcs Oct 5 '15 at 15:57
19
$\begingroup$

I'm going to take this as a general question, referring to all atomic objects, not just DelaunayMesh.

By design, atomic objects like DelaunayMesh, SparseArray, Graph, etc. or even Association and Rational are not meant to be accessed directly as a Mathematica expression. There are various reasons why an object was made atomic, typically related to performance (think of the change from v8 to v9 when Image became atomic).

These objects usually have some sort of interface to allow extracting information from them. This is what we should use, as this is the only supported (i.e. guaranteed to be robust and compatible) way. For your example, you can extract the desired information as MeshCells[r, 3]. For a sparse array, we can extract the components of the objects with sa["NonzeroPositions"], sa["NonzeroValues"], etc. For a Graph object, we can use VertexList and EdgeList.

Usually the standard interface works well. But unfortunately, occasionally it happens that a use case was not anticipated by Wolfram. This happened recently to me when I had a need to extract an edge list of the graph in terms of indices, with good performance. I know the information is there, and I know that it can be extracted quickly, as e.g. AdjacencyMatrix seems to do it, but there's no documented way for me to get access to the raw information. These really made me want to poke around the internal structure of Graph ... but doing such things would be a very bad idea if we need any sort of robustness, especially inside a production package.

However, to do it at all, we need to get access it the expression's "full form". You noticed that virtually all atomic expressions have a full form, even though it is mostly inaccessible. Why is this so, if they are atomic? I believe that the answer is that often there is a need to serialize Mathematica expressions, either to write them into an .m file, save them in a notebook (when possible), or to send them through a MathLink connection. This is done by first representing them as a compound expression, which might not map directly to the internal structure of the atomic object, but should represent it fully.

How well this "full form" integrates into the rest of the language varies from case to case. E.g. SparseArray and Rational can be accessed using pattern matching:

sa = SparseArray[{5, 7} -> 1];

Replace[sa, HoldPattern@SparseArray[guts___] :> {guts}]
(* {Automatic, {5, 7}, 0, {1, {{0, 0, 0, 0, 0, 1}, {{7}}}, {1}}} *)

Graph cannot:

g = RandomGraph[{5,10}];
MatchQ[g, HoldPattern@Graph[___]]

We know though that it doe have a full form ...

In[]:= InputForm[g]    
Out[]//InputForm=
Graph[{1, 2, 3, 4, 5}, {Null, SparseArray[Automatic, {5, 5}, 0, 
   {1, {{0, 4, 8, 12, 16, 20}, {{2}, {3}, {4}, {5}, {1}, {3}, {4}, {5}, {1}, {2}, {4}, 
     {5}, {1}, {2}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}]

I think that the only way to get to it is to first convert the atomic object to another representation. We could convert it to a string and back, e.g.

ToExpression[ToString[g, InputForm], InputForm, Hold]

Hold[Graph[{1, 2, 3, 4, 5}, {Null, 
   SparseArray[Automatic, {5, 5}, 
    0, {1, {{0, 4, 8, 12, 16, 
       20}, {{2}, {3}, {4}, {5}, {1}, {3}, {4}, {5}, {1}, {2}, {4}, \
{5}, {1}, {2}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}]]

What's inside the Hold is not an atom, it's just a compound expression with head Graph that will immediately evaluate to an atomic graph once we remove the Hold.

We could also use Compress:

Uncompress[Compress[g], Hold]

Or possibly export to WDX and import back (haven't tested).

If we wanted better performance, we might send the expression through a MathLink connection and wrap it in Hold in C code ...

These are good techniques for doing some spelunking on atoms. But doing this should really really be avoided in favour of using the standard, type-specific way of extracting information. Remember that this full form used for serialization is not meant to be used directly, it's only for serialization. It may change between versions, and it may not work the way you though it did. Graph for example can have several different internal representations.

$\endgroup$
14
$\begingroup$

I think this is the simplest fast way to convert an atomic expression to an equivalent compound form, to be able to inspect and manipulate its "apparent" full form:

g = RandomGraph[{5,8}]; (* this is our atomic expression *)

ml = LinkCreate[LinkMode -> Loopback];
LinkWrite[ml, With[{e = g}, Hold[e]]]
LinkRead[ml]
LinkClose[ml]

(* Hold[Graph[{1, 2, 3, 4, 5}, {Null, 
   SparseArray[Automatic, {5, 5}, 
    0, {1, {{0, 4, 6, 9, 12, 
       16}, {{2}, {3}, {4}, {5}, {1}, {5}, {1}, {4}, {5}, {1}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}]] *)
$\endgroup$
  • 3
    $\begingroup$ Thanks. Didn't know there is a Loopback! $\endgroup$ – Silvia Mar 8 '16 at 7:24
8
$\begingroup$

Motivation

Recently, I wanted to extract parts of an atomic expression, and my first thought was to use a ToExpression/ToString roundtrip where I inactivate the atomic head. I then decided that it would be worthwhile to have a function to convert an atomic object into an inactive version where the head is wrapped in Inactive. Then, I thought such a function would be worthwhile posting here, and a search led me to this question, where I learned of @Szabolcs' clever idea of using LinkWrite/LinkRead. I really like this approach better than using ToExpression/ToString because I expect it to be quicker and more robust. For example, using ToString[expr, InputForm] is not robust because it is possible to write Format statements that will break the ToExpression/ToString round trip. So, one would have to use ToString[FullForm[expr]], but this is much slower.

Design

My first thought was to overload Inactivate so that it works with atomic objects. I decided against it for 2 reasons. One, people may not feel comfortable changing System` functions. And two, the design of Inactivate accepts patterns in the second argument, and I really wanted to restrict the second argument to a symbol, or to a list of symbols.

This meant that I needed to come up with a name. One possibility is AtomInactivate, but that seems too cumbersome. So, I decided on Nucleus (I also considered Ionize). The idea is that the nucleus is the most important part of an atom. If you think this is too cute, you may change the name to something else.

Nucleus

Here is the definition of Nucleus:

Nucleus[input_, head_:Automatic] := With[
    {
    atoms = Replace[head,
        {
        Automatic :> If[AtomQ[input], {Head[input]}, Message[Nucleus::atom]; $Failed],
        h_Symbol :> {h},
        h:{__Symbol} :> h,
        _ :> (Message[Nucleus::syms,head,2];$Failed)
        }
    ]
    },
    (
    If[!MemberQ[Links[], $AtomLink] || LinkReadyQ[$AtomLink],
        Quiet @ LinkClose[$AtomLink];
        $AtomLink = LinkCreate[LinkMode -> Loopback]
    ];
    LinkWrite[$AtomLink, input];
    inactiveBlock[atoms, LinkRead[$AtomLink]]
    ) /; atoms =!= $Failed
]

SetAttributes[inactiveBlock, HoldAll]
inactiveBlock[h_List, body_] := Block @@ Join[
    Apply[Set, Hold @ Evaluate @ Thread[{h,Inactive/@h}], {2}],
    Hold[body]
]

Nucleus::syms = "Argument `1` at position `2` is expected to be a symbol or a list of symbols";
Nucleus::atom = "Unable to determine atomic symbol";

The basic idea is to figure out what heads need to be inactivated, call LinkWrite on the input, followed by calling LinkRead after blocking the heads to inactive versions of themselves. If only a single argument is given, and the input is atomic, then the head of the input will be inactivated. Here is an example:

g = RandomGraph[{5,10}];
Nucleus[g]
First @ %

Inactive[Graph][{1, 2, 3, 4, 5}, {Null, SparseArray[ Automatic, {5, 5}, 0, { 1, {{0, 4, 8, 12, 16, 20}, {{2}, {3}, {4}, {5}, {1}, {3}, {4}, {5}, {1}, { 2}, {4}, {5}, {1}, {2}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}]

{1, 2, 3, 4, 5}

Notice that Nucleus was able to automatically determine that the head to be inactivated is Graph. And another example with the same graph:

Nucleus[g, SparseArray]

Graph[{1, 2, 3, 4, 5}, {Null, Inactive[SparseArray][Automatic, {5, 5}, 0, {1, {{0, 4, 8, 12, 16, 20}, {{2}, {3}, {4}, {5}, {1}, {3}, {4}, {5}, {1}, {2}, {4}, {5}, {1}, {2}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}]

This time Graph is not inactivated, but it is not atomic because an "invalid" SparseArray argument is used (since the SparseArray head is wrapped inside of Inactive).

Note that the object need not be atomic, as Nucleus will inactivate the requisite atomic parts. For example:

Nucleus[{Graph[{1->2,2->3}], Graph[{1->3,2->3}]}, Graph]

{Inactive[Graph][{1, 2, 3}, {{{1, 2}, {2, 3}}, Null}], Inactive[Graph][{1, 3, 2}, {{{1, 2}, {3, 2}}, Null}]}

Once part extraction or transformations of the inactive object are performed, one simply uses Activate to recreate the atomic object. For example, suppose you wanted to rename the vertices, and then have them displayed:

Append[Nucleus[g] /. {1,2,3,4,5}->{a,b,c,d,e}, VertexLabels->"Name"]
Activate[%]

Inactive[Graph][{a, b, c, d, e}, {Null, SparseArray[ Automatic, {5, 5}, 0, { 1, {{0, 4, 8, 12, 16, 20}, {{2}, {3}, {4}, {5}, {1}, {3}, {4}, {5}, {1}, { 2}, {4}, {5}, {1}, {2}, {3}, {5}, {1}, {2}, {3}, {4}}}, Pattern}]}, VertexLabels -> "Name"]

enter image description here

Of course, for this example, one can simply use VertexReplace instead:

Graph[VertexReplace[g, Thread[{1,2,3,4,5}->{a,b,c,d,e}]], VertexLabels->"Name"]

Nucleus is particularly useful when needed accessor functions are not available.

$\endgroup$
  • 1
    $\begingroup$ Wow, I am impressed. Thank you Carl, that's so useful! $\endgroup$ – Henrik Schumacher Jan 28 '18 at 16:36
  • $\begingroup$ LinkRead has a second argument which will be applied to the received expression before further evaluation (just like the third argument of ToExpression). It might allow for a slightly simpler implementation. $\endgroup$ – Szabolcs Feb 3 '18 at 7:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.