Partially ordered sets for Julia based on Graphs. See HasseDiagrams for functions to draw posets.
A partially ordered set, or poset for short, is a pair
-
irreflexive (for all
$v \in V$ , it is never the case that$v \prec v$ ), -
antisymmetric (for all
$v,w \in V$ , we never have both$v \prec w$ and$w \prec v$ ), and -
transitive (for all
$u,v,w \in V$ , if$u \prec v$ and$v \prec w$ then$u \prec w$ ).
Posets are naturally represented as transitively closed, directed, acyclic graphs. This is how this module implements posets using the DiGraph
type in Graphs
.
The design philosophy for this module is modeled exactly on Graphs
. In particular, the vertex set of a poset is necessarily of the form {1,2,...,n}
.
Create a new poset with no elements using Poset()
or a poset with a specified number
of elements with Poset(n)
.
Given a poset p
, use Poset(p)
to create an independent copy of p
.
Given a directed graph d
, use Poset(d)
to create a new poset from the transitive
closure of d
. An error is thrown if d
has cycles. (Self loops in d
are ignored.)
Given a square matrix A
, create a poset in which i < j
exactly when the i,j
-entry
of A
is nonzero. Diagonal entries are ignored. If this matrix would create a cycle, an
error is thrown.
For consistency with Graph
, we call the elements of a Poset
vertices and the functions add_vertex!
and add_vertices!
work exactly as in the Graphs
module.
julia> using Posets
julia> p = Poset()
{0, 0} Int64 poset
julia> add_vertex!(p)
true
julia> add_vertices!(p,5)
5
julia> p
{6, 0} Int64 poset
Use nv(p)
to return the number of elements (vertices) in p
.
To add a relation to a poset, use add_relation!
. This returns true
when successful.
julia> p = Poset(4)
{4, 0} Int64 poset
julia> add_relation!(p,1,2)
true
julia> add_relation!(p,2,3)
true
julia> add_relation!(p,3,1)
false
Let's look at this carefully to understand why the third call to add_relation!
does not succeed:
- The first call to
add_relation!
causes the relation1 < 2
to hold inp
. - The second call to
add_relation!
causes the relation2 < 3
to be added top
. Given that1 < 2
and2 < 3
, by transitivity we automatically have1 < 3
inp
. - Therefore, we cannot add
3 < 1
as a relation to this poset as that would violate antisymmetry.
The add_relation!
function may also be called as add_relation!(p, (a,b))
or
add_relation!(p, a => b)
. Both are equivalent to add_relations(p, a, b)
.
The addition of a relation to a poset can be somewhat slow.
Each addition involves error checking and calculations to ensure the integrity
of the underlying data structure. See the Implementation section at the
end of this document. Adding a list of relations one at a time can be inefficient,
but it is safe. We also provide the function add_relations!
(plural) that is more
efficient, but can cause serious problems.
To underscore the risk, this function
is not exported, but needs to be invoked as Posets.add_relations!(p, rlist)
where rlist
is a list of either tuples (a,b)
or pairs a => b
.
Here is a good application of this function (although using chain(10)
is safer):
julia> p = Poset(10)
{10, 0} Int64 poset
julia> rlist = ((i,i+1) for i=1:9)
Base.Generator{UnitRange{Int64}, var"#13#14"}(var"#13#14"(), 1:9)
julia> Posets.add_relations!(p, rlist)
julia> p == chain(10)
true
Here is what happens with misuse:
julia> p = Poset(5)
{5, 0} Int64 poset
julia> rlist = [ 1=>2, 2=>3, 3=>1 ]
3-element Vector{Pair{Int64, Int64}}:
1 => 2
2 => 3
3 => 1
julia> Posets.add_relations!(p, rlist)
ERROR: This poset has been become corrupted!
The function rem_vertex!
behaves exactly as in Graphs
. It removes the given vertex from the poset. For example:
julia> p = Poset(5)
{5, 0} Int64 poset
julia> add_relation!(p,1,5)
true
julia> rem_vertex!(p,2)
true
julia> has_relation(p,1,2)
true
When element 2
is removed from p
, element 5
takes its place. Because of this renumbering,
we have some unexpected behavior:
julia> p = subset_lattice(4)
{16, 65} Int64 poset
julia> q = Poset(p) # make a copy of p
{16, 65} Int64 poset
julia> rem_vertex!(q, 9)
true
julia> q
{15, 57} Int64 poset
julia> q ⊆ p
false
julia> maximals(p) |> collect
1-element Vector{Int64}:
16
julia> maximals(q) |> collect
1-element Vector{Int64}:
9
One might expect that deleting a vertex from a poset results in a poset that is a subset of the original. However,
when vertex 9
was removed from (a copy of) p
, the vertex 16
is relabeled 9
. Hence vertex 9
in p
is
not maximal, but it is maximal in q
.
For a more extensive explanation, see poset-deletion.pdf in the
delete-doc
folder.
Removing relations from a poset is accomplished with rem_relation!(p,a,b)
. Assuming a<b
in p
,
this deletes the relation a<b
from p
, but also deletes all relations a<x
and x<b
for
vertices x
that lie between a
and b
.
julia> p = chain(5)
{5, 10} Int64 poset
julia> rem_relation!(p, 2, 4)
true
julia> collect(relations(p))
8-element Vector{Relation{Int64}}:
Relation 1 < 2
Relation 1 < 3
Relation 1 < 4
Relation 1 < 5
Relation 2 < 4
Relation 2 < 5
Relation 3 < 5
Relation 4 < 5
Note that relations 2<3
and 3<4
have been removed.
For a more extensive explanation, see poset-deletion.pdf in the
delete-doc
folder.
Posets are a collection of objects and a poset_builder
function
takes a list of objects and a function the determines if they obey a
The function is invoked as poset_builder(list, comp_func)
where
-
list
is a list (vector) of objects and -
comp_func
is a function that takes a pair of objects and returnstrue
if they satisfy a$\prec$ relation.
For example, suppose we want to construct a poset consisting of the integers 1
to 100
which
julia> nums = collect(1:100);
julia> divs(a,b) = mod(b,a)==0 # return true if a divides b
divs (generic function with 1 method)
julia> p = poset_builder(nums, divs)
{100, 382} Int64 poset
The elements that cover 1
are the primes less than 100
:
julia> [ x for x in 1:100 if covered_by(p,1,x) ]
25-element Vector{Int64}:
2
3
5
7
11
13
17
19
⋮
67
71
73
79
83
89
97
Use nv(p)
to return the number of vertices in the poset p
. As in Graphs
, the
elements of the poset are integers from 1
to n
.
Use in(a, p)
[or a ∈ p
] to determine if a
is an element of p
.
This is equivalent to 1 <= a <= nv(p)
.
There are three ways to check if elements are related in a poset.
First, to see if 1 < 3
in p
we use the has_relation
function:
julia> has_relation(p,1,3)
true
Second, the syntax p(a,b)
is equivalent to has_relation(p,a,b)
:
julia> p(1,3)
true
julia> p(3,1)
false
There is a third way to determine the relation between elements a
and b
in a poset p
. Instead of has_relation(p,a,b)
or p(a,b)
we may use this instead: p[a] < p[b]
.
julia> has_relation(p,1,3)
true
julia> p[1] < p[3]
true
julia> p[3] < p[1]
false
The other comparison operators (<=
, >
, >=
, ==
, !=
) works as expected.
julia> p[3] > p[1]
true
Neither has_relation(p,a,b)
nor p(a,b)
generate errors; they return false
even if a
or b
are not elements of p
.
julia> p(-2,9)
false
However, the expression p[a] < p[b]
throws an error in either of these situations:
- Using the syntax
p[a]
ifa
is not an element ofp
. - Trying to compare elements of different posets (even if the two posets are equal).
The functions are_comparable(p,a,b)
and are_incomparable(p,a,b)
behave as follows:
are_comparable(p,a,b)
returnstrue
exactly whena
andb
are both in the poset, and one of the following is true:a<b
,a==b
, ora>b
.are_incompable(p,a,b)
returnstrue
exactly whena
andb
are both in the poset, but none of the follower are true:a<b
,a==b
, ora>b
.
Alternatively, use p[a] ⟂ p[b]
to test if a
and b
are comparable, and use p[a] ∥ p[b]
to test if a
and b
are incomparable.
Given a list of elements vlist
of a poset p
:
is_chain(p, vlist)
returnstrue
if the elements ofvlist
form a chain inp
.is_antichain(p, vlist)
returnstrue
if the elements ofvlist
form an antichain inp
.
Both return false
if an element of vlist
is not in p
.
Use nr
to return the number of relations in the poset (this is analogous to ne
in Graphs
):
julia> nr(p)
3
The function relations
returns an iterator for all the relations in a poset.
julia> p = chain(4)
{4, 6} Int64 poset
julia> collect(relations(p))
6-element Vector{Relation{Int64}}:
Relation 1 < 2
Relation 1 < 3
Relation 1 < 4
Relation 2 < 3
Relation 2 < 4
Relation 3 < 4
The functions src
and dst
return the lesser and greater elements of a relation, respectively:
julia> r = first(relations(p))
Relation 1 < 2
julia> src(r), dst(r)
(1, 2)
issubset(p,q)
(orp ⊆ q
) returnstrue
exactly whennv(p) ≤ nv(q)
and wheneverv < w
inp
we also havev < w
inq
.
above(p,a)
returns an iterator for all elementsk
ofp
such thata<k
.below(p,a)
returns an iterator for all elementsk
ofp
such thatk<a
.between(p,a,b)
returns an iterator for all elementsk
ofp
such thata<k<b
.
julia> p = chain(10)
{10, 45} Int64 poset
julia> collect(above(p,6))
4-element Vector{Int64}:
7
8
9
10
julia> collect(below(p,6))
5-element Vector{Int64}:
1
2
3
4
5
julia> collect(between(p,3,7))
3-element Vector{Int64}:
4
5
6
In a poset, we say a
is covered by b
provided a < b
and there is no element c
such
that a < c < b
.
Use covered_by(p,a,b)
to determine if a
is covered by b
. Alternatively, use
p[a] << p[b]
or p[b] >> p[a]
.
julia> p = chain(8)
{8, 28} Int64 poset
julia> p[4] << p[5]
true
julia> p[4] << p[6]
false
The functions just_above
and just_below
can be used to find elements that cover, or are covered by, a given vertex.
julia> p = chain(9)
{9, 36} Int64 poset
julia> above(p,5) |> collect
4-element Vector{Int64}:
6
7
8
9
julia> just_above(p,5) |> collect
1-element Vector{Int64}:
6
julia> below(p,5) |> collect
4-element Vector{Int64}:
1
2
3
4
julia> just_below(p,5) |> collect
1-element Vector{Int64}:
4
maximals(p)
returns an iterator for the maximal elements ofp
.minimals(p)
returns an iterator for the minimal elements ofp
.maximum(p)
returns the maximum element ofp
or0
if no such element exists.minimum(p)
returns the minimum element ofp
or0
if no such element exists.max_chain(p)
returns a vector containing the elements of a largest chain inp
.max_antichain(p)
returns a vector containing the elements of a largest antichain inp
.height(p)
returns the size of a largest chain inp
.width(p)
returns the size of a largest antichain inp
.chain_cover(p)
returns a minimum-size collection of chains ofp
such that every element ofp
is in one of the chains. The number of chains is the width ofp
.antichain_cover(p)
returns a minimum-size collection of antichains ofp
such that every element ofp
is in one of the antichains. The number of antichains is the height ofp
.
Note: The function
max_chain
returns a largest chain in the poset. It is possible that there are two or more possible answers because there are two or more such chains of maximum size. There is no guarantee as to which largest chain will be returned. Likewise formax_antichain
. Similarly,chain_cover
returns a minimum-size partition of the elements into chains. If there are multiple minimum-size chain covers, there is no guarantee which will be returned bychain_cover
. Likewise forantichain_cover
.
For posets p
and q
, use iso(p,q)
to compute an isomorphism from p
to q
,
or throw an error if the posets are not isomorphic.
Let f = iso(p,q)
. Then f
is a Dict
mapping vertices of p
to vertices of q
.
For example, if p
has a unique minimal element x
, then f[x]
is the unique minimal
element of q
.
To check if posets are isomorphic, use iso_check
(which calls iso
inside a try/catch
block).
A realizer for a poset p
is a set of linear extensions whose intersection is p
.
The function realizer(p, d)
returns a list of d
linear extensions (total orders)
that form a realizer of p
, or throws an error if no realizer of that size exists.
julia> p = standard_example(3)
{6, 6} Int64 poset
julia> r = realizer(p, 3)
3-element Vector{Poset{Int64}}:
{6, 15} Int64 poset
{6, 15} Int64 poset
{6, 15} Int64 poset
julia> r[1] ∩ r[2] ∩ r[3] == p
true
julia> realizer(p, 2)
ERROR: This poset has dimension greater than 2; no realizer found.
The dimension of a poset is the size of a smallest realizer. Use dimension(p)
to calculate its dimension.
julia> p = standard_example(4)
{8, 12} Int64 poset
julia> dimension(p)
4
Note: Computation of the dimension of a poset is NP-hard. The
dimension
function may be slow, even for moderate-size posets.
The function ranking(p)
returns a ranking (as a dictionary) of the poset
starting from the minimal elements.
That is, the minimal elements are assigned rank 0. Elements that are above only elements of
rank 0 are assigned rank 1. Elements that are only above elements of rank 0 and 1 are assigned
rank 2. And so forth.
Note that not all posets are ranked, but this function always returns a result.
We also provide dual_ranking
which combines the rankings of p
and its dual, p'
.
This removes the bias that minimal elements are treated differently than maximal elements.
For example, suppose the poset is a the disjoint union of a 3-element and a 2-element chain.
The function ranking
places both minimal elements at rank 0
, then their covers at
rank 1
, and finally the maximal element in the 3-chain at rank 2
.
By contrast, dual_ranking
places the elements in the 3-chain at ranks 0
, 2
, and 4
,
and the elements of the 2-chain at ranks 1
and 3
.
julia> p = chain(3) + chain(2)
{5, 4} Int64 poset
julia> ranking(p)
Dict{Int64, Int64} with 5 entries:
5 => 1
4 => 0
2 => 1
3 => 2
1 => 0
julia> dual_ranking(p)
Dict{Int64, Int64} with 5 entries:
5 => 3
4 => 1
2 => 2
3 => 4
1 => 0
The following functions create standard partially ordered sets. See the Gallery for pictures of some of these posets.
-
antichain(n)
creates the poset withn
elements and no relations. Same asPoset(n)
. -
chain(n)
creates the poset withn
elements in which1 < 2 < 3 < ... < n
. -
chain(vlist)
creates a chain from the integer vectorvlist
(which must be a permutation of1:n
). For example,chain([2,1,3])
creates a chain in which2 < 1 < 3
. -
chevron()
creates a poset with6
elements that has dimension equal to3
. It is different fromstandard_example(3)
. -
crown(n,k)
creates the crown poset with2n
elements with two levels:n
elements as minimals andn
as maximals. Each minimal is comparable ton-k
maximals. See the help message for more information. -
random_linear_order(n)
: Create a linear order in which the numbers1
throughn
appear in random order. -
random_poset(n,d=2)
: Create a randomd
-dimensional poset by intersectingd
random linear orders, each withn
elements. -
standard_example(n)
creates a poset with2n
elements. Elements1
throughn
form an antichain as do elementsn+1
through2n
. The only relations are of the formj < k
where1 ≤ j ≤ n
andk = n+i
where1 ≤ i ≤ n
andi ≠ j
. This is a smallest-size poset of dimensionn
. Equivalent tocrown(n,1)
. -
subset_lattice(d)
: Create the poset corresponding to the2^d
subsets of{1,2,...,d}
ordered by inclusion. Fora
between1
and2^d
, elementa
corresponds to a subset of{1,2,...,d}
as follows: Writea-1
in binary and view the bits as the characteristic vector indicating the members of the set. For example, ifa
equals12
, thena-1
is1011
in binary. Reading off the digits from the right, this gives the set{1,2,4}
.- Use
subset_decode(a)
to convert an elementa
of this poset into a set of positive integers,A
. - Use
subset_encode(A)
to convert a set of positive integers to its name in this poset.
- Use
-
weak_order(vals)
: Create a weak orderp
from a list of real numbers. Inp
elementi
is less than elementj
providedvals[i] < vals[j]
.
Let p
be a poset. The following two functions create graphs from p
with the same
vertex set as p
:
comparability_graph(p)
creates an undirected graph in which there is an edge fromv
tow
exactly whenv < w
orw < v
inp
.cover_digraph(p)
creates a directed graph in which there is an edge fromv
tow
exactly whenv
is covered byw
.
julia> p = chain(9)
{9, 36} Int64 poset
julia> g = comparability_graph(p)
{9, 36} undirected simple Int64 graph
julia> g == complete_graph(9)
true
julia> d = cover_digraph(p)
{9, 8} directed simple Int64 graph
julia> d == path_digraph(9)
true
Given a graph g
, calling incidence_poset(p)
creates a poset whose
elements correspond to the vertices and edges of g
. In this poset the only relations
are of the form v < e
where v
is a vertex that is an end point of the edge e
.
Deprecation notice: This function was previously named
vertex_edge_incidence_poset
and that name is now deprecated. It will be removed in future releases.
zeta_matrix(p)
returns the zeta matrix of the poset. This is a0,1
-matrix whosei,j
-entry is1
exactly whenp[i] ≤ p[j]
.strict_zeta_matrix(p)
returns a0,1
-matrix whosei,j
entry is1
exactly whenp[i] < p[j]
.mobius_matrix(p)
returns the inverse ofzeta(p)
.
In all cases, the output is a dense, integer matrix.
The dual of poset p
is created using reverse(p)
. This returns a new poset with the
same elements as p
in which all relations are reversed (i.e., v < w
in p
if and
only if w < v
in reverse(p)
). The dual (reverse) of p
can also be created with p'
.
Given two posets p
and q
, the result of p+q
is a new poset formed from the
disjoint union of p
and q
. Note that p+q
and q+p
are isomorphic, but
may be unequal because of the vertex numbering convention.
Alternatively hcat(p,q)
.
Given two posets p
and q
, the result of p/q
is a new poset from a copy of p
and a copy of q
with all elements of p
above all elements of q
.
Alternatively, vcat(p,q)
or q\p
.
Given posets
Cartesian product is implemented in Posets
as p * q
. The result is a new poset
that is isomorphic to the Cartesian product of p
and q
.
Given a poset p
and a list of vertices vlist
, use induced_subposet(p)
to return a
pair (q,vmap)
. The poset q
is the induced subposet and the vector vmap
maps
the new vertices to the old ones
(the vertex i
in the subposet corresponds to the vertex vmap[i]
in p
).
This is exactly analogous to Graphs.induced_subgraph
.
Given two posets p
and q
, intersect(p,q)
is a new poset in which v < w
if and only
if v < w
in both p
and q
. The number of elements is the smaller of nv(p)
and nv(q)
.
This may also be invoked as p ∩ q
.
For example, the intersection of a chain with its reversal has no relations:
julia> p = chain(5)
{5, 10} Int64 poset
julia> p ∩ reverse(p)
{5, 0} Int64 poset
Use linear_extension(p)
to create a linear extension of p
.
This is a total order q
with the same elements as p
and with p ⊆ q
.
The function random_linear_extension(p)
creates a pseudo-random linear extension of p
as follows: Pick a minimal element of p
at random. Make that the first element
of the result. Now ignore that element and pick a minimal element at random from
among those that remain. Continue in this way until all elements have been
incorporated.
Let
Similarly, let
There are two ways to compute the join [or meet] of elements in a poset.
- The join and meet of elements
x
andy
in posetp
can be computed asp[x] ∨ p[y]
andp[x] ∧ p[y]
. - Or use the functions
lattice_join(p,x,y)
orlattice_meet(p,x,y)
Important notes:
- The meet [or join] of two elements need not exist.
- In the operation form,
p[x] ∧ p[y]
[orp[x] ∨ p[y]
], if there is no meet [or join], an error is thrown. - In the function form,
lattice_meet(p,x,y)
[orlattice_join(x,y)
], if there is no meet [or join] then0
is returned.
- In the operation form,
- Cannot compute the meet [or join] of elements in different posets.
- The expression
p[x]
throws an error ifx
is not an element ofp
. - The symbol
∨
is typed\vee<TAB>
and∧
is typed\wedge<TAB>
. - The result of
p[x] ∨ p[y]
is an object of typePosetElement
(likewise for∧
). - The result of
lattice_join
[orlattice_meet
] is always an integer. The return value of0
is used to show that the join [or meet] does not exist.
The join and meet operations for posets are analogous to union and intersection for sets as illustrated here:
julia> using ShowSet
julia> p = subset_lattice(4)
{16, 65} Int64 poset
julia> A = Set([1,2,3])
{1,2,3}
julia> B = Set([2,3,4])
{2,3,4}
julia> a = subset_encode(A)
8
julia> b = subset_encode(B)
15
julia> p[a] ∨ p[b]
Element 16 in a {16, 65} Int64 poset
julia> subset_decode(integer(ans))
{1,2,3,4}
julia> p[a] ∧ p[b]
Element 7 in a {16, 65} Int64 poset
julia> subset_decode(integer(ans))
{2,3}
Further, the function join_table(p)
[or meet_table
] creates a matrix
whose i,j
-entry is the join [or meet] of elements i
and j
in poset p
,
or 0
if the join [or meet] doesn't exist.
A Poset
is a structure that contains a single data element: a DiGraph
.
Users should not be accessing this directly, but it may be useful to understand
how posets are implemented. The directed graph is acyclic (including loopless)
and transitively closed. This means if
First, the graph may be larger than needed. If we only kept cover edges
(the transitive reduction of the digraph) we might have many fewer edges.
For example, a linear order with
Second, the computational cost of adding (or deleting) a relation is nontrivial.
The add_relation!
function first checks if the added relation would violate
transitivity; this is speedy because we can add the relation transitiveclosure!
and that takes some
work. Adding several relations to the poset, one at a time, can be slow.
This can be greatly accelerated by using Posets.add_relations!
but (as discussed above)
this function can cause severe problems if not used carefully.
The extras
folder includes additional code that may be useful in
working with Posets
. See the README
in the extras
directory.
Of note is extras/converter.jl
that defines the function poset_converter
that can
be used to transform a Poset
(defined in this module) to a SimplePoset
(defined in the SimplePosets module).