Let \(G\) be a permutation group. In other words, \(G\) is a finte group, together with a specified embedding into some symmetric group \(\mathfrak{S}_n\).
For example, consider the following two embeddings of \(C_2 \times C_2 \leq \mathfrak{S}_6\):
\[G_1 = \langle (1,2)(3,4), (1,2)(5,6)\rangle \]
\[G_2 = \langle (1,2)(3,4), (1,3)(2,4) \rangle \]
These are two distinct permutation groups which are not conjugate inside of \(\mathfrak{S}_6\).
The cycle index polynomial of \(G \leq \mathfrak{S}_n\) is defined to be \[ \frac{1}{|G|} \sum_{g \in G} p_{\operatorname{type}(g)},\] where \(p_{\operatorname{type}(g)}\) is the power-sum symmetric function (of degree \(n\)) indexed by the cycle type of \(g\) regarded as an element of \(\mathfrak{S}_n\).
It is reasonable to ask if the cycle index polynomial is an invariant of a permutation group \(G\) up to conjugacy (i.e. up to relabeling the set that \(G\) acts on). It is not. The two examples of \(G_1\) and \(G_2\) above provide a counter example. Since \(G_1\) acts on \({1,2,3,4,5,6}\) and \(G_2\) acts on \({1,2,3,4} \subseteq {1,2,3,4,5,6}\), they are not conjugate subgroups of \(\mathfrak{S}_6\). However, both have cycle index polynomial \[\frac{1}{4} p_{1^6} + \frac{3}{4} p_{2^21^2}.\]
The examples of \(G_1\) and \(G_2\) above give the smallest example of a collision between two permutation groups. If you ever need to find more, you can find the collisions using the following SageMath code (and suitably modifying N
). The dict
called multiples
has keys given by cycle index polynomials with multiple corresponding groups. the corresponding groups are the values.
sage: N = 6
....: SN = SymmetricGroup(N)
....: CIs = {G: G.cycle_index() for G in SN.conjugacy_classes_subgroups()}
....: Gs = dict()
....: for G, f in CIs.items():
....: l = Gs.get(f,list())
....: l.append(G)
....: Gs[f] = l
....:
....: multiples = {f:Glist for f, Glist in Gs.items() if len(Glist) > 1}
For example, with N=7
, there are 3
distict cycle index polynomials with collisions.
If you are just interested in the code, scroll right to the bottom. Before I list out all the code, I try to explain what is happening and some of the Sage quirks we encounter.
Let’s make sure we are on the same page about the group action and ring we want to work with. First, consider the action of \(\mathfrak{S}_n\) permuting \(n\) letters on a polynomial ring with \(n\) generators. The action is on the subscripts. So if a permutation \(\sigma\) takes \(i \mapsto \sigma(i)\), then the action of \(\sigma\) on the generators is \(\sigma \cdot x_i = x_{\sigma(i)}\). Let \(I_n\) denote the ideal generated by all polynomials that pointwise fixed under this action.
For example, taking \(n=3\), we would see that \(e_2 = e_2(x_1, x_2, x_3) = x_1 x_2 + x_1 x_3 + x_2 x_3\) is fixed under this action. On the other hand, \(x_1 x_2\) is not fixed under this action because if \(\sigma(1)=1,\;\sigma(2)=3\) then \(\sigma \cdot x_1x_2 = x_1x_3\).
If an ideal \(I\) in a ring \(R\) is stable under the action of a group \(G\) which acts on \(R\), then the quotient ring \(R/I\) has a well-defined action of \(G\). Note that we don’t need the elements of \(I\) to be pointwise fixed. Rather we need it to be fixed as a set. We want this sort of stability because we don’t want the action of a group element on a representative of \(0\) to be nonzero. That would be scary.
For example, the polynomial \(f = x_1^2 x_2 + x_1^2 x_3 + x_1 x_2 x_3\) is in \(I_3\), but is not fixed by every element of \(\mathfrak{S}_3\). View \(f\) as \(x_1 e_2(x_1,x_2,x_3)\), so that we can see it is in the ideal \(I_3\). Then any \(\sigma\) acting on it will act like \(\sigma x_1 e_2(x_1,x_2,x_3) = x_{\sigma(1)} e_2(x_1,x_2,x_3)\). So the action results in a genuinely different polynomial, but one that is still in the ideal. This is what we mean by a “stable” ideal.
Now we can define the coinvariant ring \(R_n\) as the quotient of the ambient polynomial ring by the ideal \(I_n\) \[R_n = \mathbb{C}[x_1, x_2, \ldots, x_n]/I_n.\]
In Sage, here is how we could construct the ring when n=3
:
sage: n = 3
sage: S = PolynomialRing(QQ,'x',n)
sage: x = S.gens()
sage: L = SymmetricFunctions(QQ)
sage: e = L.e()
sage: I = S.ideal([e[i].expand(n) for i in range(1,n+1)])
sage: R = S.quo(I, names=['z0','z1','z2'])
sage: z = R.gens()
So R
is the coinvariant ring!
We give it the variables z
so that we can quickly tell which ring Sage thinks a given polynomial is in.
If the variables are x
’s, it is in S
, and if the variables are z
’s, then the element is in R
.
The importance of working in S
vs. in R
will make more sense later.
We are defining x=S.gens()
and z=R.gens()
now so that we can use it later when we want to incorporate the group action.
The elementary symmetric polynomials in n
-many variables are known to be a nice generating set for the ideal \(I_n\).
The elementary symmetric functions are given by e[i]
, and the .expand(n)
method turns them into a polynomial in n
-many variables.
One of the most useful pieces of the puzzle when studying group representations carried by an algebra is to have a vector space basis for the algebra.
Even though it is not strictly necessary, it helps us to get our hands dirty with examples.
To me, Sage is an extremely helpful tool to get our hands dirty.
A vector space basis in terms of monomials can be found in Sage using the .normal_basis()
method (as opposed to a Gröbner basis method).
For some reason, even though we want the vector space basis for R
, we call the method on the defining ideal, rather than the quotient ring itself.
sage: B = I.normal_basis(); B
[x1*x2^2, x2^2, x1*x2, x2, x1, 1]
It is a famous fact that the coinvariant ring is isomorphic as a representation to the regular representation. In this case that means that \(R_3\) will be 6 dimensional over \(\mathbb{C}\). And indeed we see that Sage gave us a vector space basis has 6 elements. In fact, we see that Sage gives us back the Artin basis of substaircase monomials!
Now we understand R
as a vector space, but we still don’t understand it as a representation.
Now how can we figure out that this representation is the regular representation?
A first approach would be to compute the character values.
Let’s do that using Sage!
We want to work with the symmetric group, so lets go ahead and define that.
sage: Sn = SymmetricGroup(n)
This defines the symmetric group as a PermutationGroup_generic
object.
Most of the rest of the way we interact with Sn
only relies on it being a PermutationGroup_generic
.
Thus, we could do a lot of the same things with any permutation group appropriately defined. (More on this in a follow-up post!)
Now, recall that the character of a representation is constant on conjugacy classes.
Thus, we can make our computation significantly more efficient if we work with a single representative for each conjugacy class rather than iterating over the whole group.
sage: CC = Sn.conjugacy_classes_representatives()
Now we want to act by permuting the variables.
This is partiularly easy for PermutationGroup
s.
The syntax is to treat a group element s
like a function.
The input should be a list that is the same length as the domain of the permutation group.
(We can find this by using the .domain()
.)
sage: Sn.domain()
{1, 2, 3}
The output is then the list containing the same elements, but with the order permuted appropriately.
For the coinvariant ring, the list of generators, which I have named x
, has the right size.
sage: [s(x) for s in CC]
[(x0, x1, x2), (x1, x0, x2), (x1, x2, x0)]
Now we want to figure out how each group element acts on the .normal_basis()
.
The action is really a substitution action.
Substituion in Sage can be done by again calling the polynomial ring element like a function.
For example
sage: f = x[0] + x[1]
sage: f(1,2,0)
3
sage: f(x2,x0,x1)
x0 + x2
We want to do this for each of the .normal_basis()
monomials.
sage: for s in CC:
....: print([m(s(x)) for m in B])
[x1*x2^2, x2^2, x1*x2, x2, x1, 1]
[x0*x2^2, x2^2, x0*x2, x2, x0, 1]
[x0^2*x2, x0^2, x0*x2, x0, x2, 1]
So we have successfully acted upon the monomial basis by by an element of each conjugacy class.
I mentioned before that I named the variables in R
as z
’s because I want to be able to quickly tell where Sage things monomials are living.
Here, we see that the resulting monomials are not expressed in terms of our Artin basis, rather they are monomials in S
.
In order to turn these into elements of the coinvariant ring, we again call the quotient ring R
like a function.
The argument is a polynomial (in S
) that we want to convert to a quotient ring element (in R
).
sage: for s in CC:
....: print([R(m(s(x))) for m in B])
[z1*z2^2, z2^2, z1*z2, z2, z1, 1]
[-z1*z2^2, z2^2, -z1*z2 - z2^2, z2, -z1 - z2, 1]
[z1*z2^2, z1*z2, -z1*z2 - z2^2, -z1 - z2, z2, 1]
Now the variables are z
’s so we know they are written in terms of our Artin basis for R
. Thats better!
Now that we know how to act on the monomials forming a linear basis for \(R_n\), we want to better understand the action. One way to do this is to use character theory.
The way to go from a polynomial to a character value is to extract coefficients.
More explictly, if \(m\) is a monomial in a basis for \(R\) (for example, one of the monomials returned by .normal_basis()
), then we look at the coefficient of \(m\) inside of \( w \cdot m\).
This would give us the entries on the diagonal of the matrix if we were to explicitly write out the way \(w\) acts on \(R_n\) as a matrix.
Thus, we want to sum the diagonal entries to get the trace of the matrix.
In the following code we act on all of the basis monomials by the permutation \(231\) and get the coefficient of the same monomial.
We act on the z
variables because we want to express the resulting polynomial in normal form.
The .lift()
method is included because m
is a monomial in S
, so in order to extract coefficients, we need m(s(z))
to be thought of as an element of S
via the natural inclusion of a standard monomial in R
into a monomial in S
.
sage: w = Sn([2,3,1])
sage: [m(w(z)).lift().coefficient(m) for m in B]
We can’t get the coefficient of R(m)
in m(s(z))
beause Sage QuotientRing_generic
s don’t know how to get coefficients. The following line throws an AttributeError
:
sage: [m(w(z)).coefficient(R(m)) for m in B]
Now the character value is the sum of these diagonally-indexed coefficients. So the following line of code computes the character value of the permutation \(231\):
sage: sum([m(w(z)).lift().coefficient(m) for m in B])
0
One easy way to compute the irreducible decomposition from the character using Sage is to compute the Frobenius character of the representation. For the coinvariant ring, we know that we are looking to come up with the regular representation, so we should find the symmetric function s[1,1,1] + 2*s[2,1] + s[3]
.
The way that we’ll come up with these symmetric functions is by applying the definition of the Frobenius characteristic map.
The map is defined on a representation \(V\), as follows:
\[\operatorname{ch}\ V = \frac{1}{n!}\sum_{w \in \mathfrak{S}_n} \chi(w)p_{\operatorname{type}(w)} = \sum{\lambda \vdash n} \frac{\chi(\lambda)}{z_\lambda}p_\lambda,\]
where \(z_\lambda\) is the size of the centralizer of an element of cycle type \(\lambda\).
Sage knows how to compute \(z_\lambda\) using the function zee
in the symmetric function algebras package.
sage: from sage.combinat.sf.sfa import zee
Now we want to apply the Frobenius characteristic map. To collect this as a symmetric function, we give
sage: p = L.p()
sage: F = 0
....: for w in CC:
....: mu = w.cycle_type()
....: zed = zee(mu)
....: char = sum([f(w(z)).lift().coefficient(f) for f in I.normal_basis()])
....: F += QQ(char/zed) * p[mu]
sage: F
p[1,1,1]
This is what we expect, because in the regular representation, the only nonzero character value is the character value of the identity. Now to decompose the representation we should express it in the Schur basis:
sage: s = L.s()
sage: s(F)
s[1, 1, 1] + 2*s[2, 1] + s[3]
So we see that each representation occurs with multiplicity equal to its dimension. This is exactly the description of decomposition for the regular representation that we want!
But we can do even better. The coinvariant ring is a graded ring, and so we could ask for the graded irreducible decomposition. This has graded Frobenius characteristic given by a polynomial whose coefficients are symmetric funtctions.
sage: P.<q> = PolynomialRing(s)
sage: grF = 0
sage: for w in CC:
....: mu = w.cycle_type()
....: zed = zee(mu)
....: char = sum([P(f(w(z)).lift().coefficient(f))/zed*p[mu]*q^f.degree() for f in I.normal_basis()])
....: grF += char
sage: grF
s[1, 1, 1]*q^3 + s[2, 1]*q^2 + s[2, 1]*q + s[3]
This is the right grading!
So in this post we have seen how to utilize Sage’s functionality to act on quotients of polynomial rings, and how to compute their graded Frobenius characteristic, which tells us the irreducible decomposition of each homogeneous component.
Stay tuned! In this post, we acted on the variables by directly permuting their indices \(1, \ldots, n\) by an action of \(\mathfrak{S}_n\). Sometimes, however, the action is a bit more subtle. For example, if the variables are indexed by subsets of \([n]\), the action is a little trickier. In a follow up post, I’ll be discussing to act on more interesting sets of variables using the example of the Varchenko-Gel’fand ring of the braid matroid.
Here is all of the code from this post in a single block, so that you can see the full process from start to finish.
sage: from sage.combinat.sf.sfa import zee
....:
....: # define the coinvariant ring
....: n = 3
....: S = PolynomialRing(QQ,'x',n)
....: x = S.gens()
....: L = SymmetricFunctions(QQ)
....: e = L.e()
....: I = S.ideal([e[i].expand(n) for i in range(1,n+1)])
....: R = S.quo(I, names=['z0','z1','z2'])
....: z = R.gens()
....:
....: # get a vector space basis for the coinvariant ring
....: B = I.normal_basis(); B
....:
....: # get conjugacy class representatives
....: Sn = SymmetricGroup(n)
....: CC = Sn.conjugacy_classes_representatives()
....:
....: # compute the Frobenius characteristic F
....: p = L.p()
....: F = 0
....:
....: for w in CC:
....: mu = w.cycle_type()
....: zed = zee(mu)
....: char = sum([f(w(z)).lift().coefficient(f) for f in I.normal_basis()])
....: F += QQ(char/zed) * p[mu]
....: F
....:
....: s = L.s()
....: s(F) # express it in the Schur basis
....:
....: # compute the *graded* Frobenius characteristic
....: P.<q> = PolynomialRing(s)
....: grF = 0
....: for w in CC:
....: mu = w.cycle_type()
....: zed = zee(mu)
....: char = sum([P(f(w(z)).lift().coefficient(f))/zed*p[mu]*q^f.degree() for f in I.normal_basis()])
....: grF += char
....: grF
[x1*x2^2, x2^2, x1*x2, x2, x1, 1]
p[1, 1, 1]
s[1, 1, 1] + 2*s[2, 1] + s[3]
s[1, 1, 1]*q^3 + s[2, 1]*q^2 + s[2, 1]*q + s[3]
The first output is a vector space basis for \(R_3\) (the Artin basis).
The second output is the Frobenius character of \(R_3\)in the p
basis.
The third output is the Frobenius character of \(R_3\)in the s
basis, so that we can quickly read off the irreducible decomposition.
The fourth output is the graded Frobenius character of \(R_3\)in the s
basis, telling us the irreducible decomposition of each graded component of the ring.
Before I use Mathematica to model a skydiver jumping out of a plane and opening their parachute at a specified time \(t_0\), I want to set up the underlying differential equations. One way to think of this is to break it up into two initial value problems. Let \(h(t)\) denote the height of the skydiver above ground as a function of time. If we assume that air-resistance is proportional to velocity, we get the initial value problem \[ \begin{cases} h^{\prime \prime} (t) = -g - r_1 h’(t) \\ h(0) = h_0 \\ h’(0)=0\end{cases}.\] Once we have an explicit solution for \(h(t)\), we have a description of how the skydiver falls before deploying their parachute. We can then compute \(h_1 = h(t_0)\).
I’ll assume that the parachute fully deploys instantaneously (which is a bad assumption, as pointed out by Meade and Struthers), and so we have a new initial value problem \[ \begin{cases} h’‘(t) = -g - r_2 h’(t) \ h(t_0) = h_1 \end{cases}.\]
We can see that the second initial value problem is really the same as the first one except for the parameter \(r_i\) modeling the air-resistance.
This makes it easier to use Mathematica to compute the solution, once we know how to use WhenEvent
inside of NDSolve
.
The main command we will use to solve the ODE is NDSolve
.
As the name suggests, this solves a differential equation numerically.
he basic syntax is something like
g = 32;
r = 0.15;
h0 = 10000;
eqn = {h''[t] == -g - r*h'[t]}
cond = {h[0] == h0}
NDSolve[{eqn,cond},h,{t,0,10}]
This will return a Mathematica InterpolatingFunction
that satisfies the differential equation. One thing I could have done differently is to incorporate the equation and the initial condition into a single list, but when things get more complicated later this helps keep me organized.
The last list {t,0,10}
tells me that the solution should be valid on the range \(0 \leq t \leq 10\) but we don’t have any guarantees outside of that time range.
One question we might like to answer is “how long do we have until the skydiver hits the ground?” But we can’t really know that until we have the soluition. So my choice of upper bound for the time is really arbitrary.
One way to fix that is to use WhenEvent
to tell Mathematica to stop solving the equation as soon as the height is 0
. Heres how we do that:
events = {WhenEvent[h[t] <= 0, "StopIntegration"]};
NDSolve[{eqn, cond, events}, h, {t, 0, Infinity}]
Now we will keep trying to solve the equation until the skydiver’s height is 0, and we don’t have to know the upper bound.
Here, setting the upper bound of integration to Infinity
is a convenent placeholder for “not knowing it”. We can’t numerically integrate to infinity, we don’t have all day!.
But it also is probably useful to someone who is using this model to actually know the time we stop integrating. Sure we can just stop, but when does our skydiver hit the ground?! To do this, we can add a variable declaration within the WhenEvent
, like this:
events = {WhenEvent[h[t] <= 0, tmax = t; "StopIntegration"]};
NDSolve[{eqn, cond, events}, h, {t, 0, Infinity}]
Notice that the declaration of tmax
is followed by a semicolon, not a comma.
Then we can just print the value of tmax
to figure out when the skydiver hits the ground. In this case, we see it is \(53.5395\) seconds.
As mathematicians, maybe we could stop there, but our skydiver wouldn’t be too happy with us! We never deployed their parachute!!
Now that we want to deploy the parachute, things get more complicated. The main trouble is that we somehow want to change the value of r
.
Since there are only two possible values of r
in our situation (\(r_1\) and \(r_2\) from above), we want to tell Mathematica that they are DiscreteVariables
. We do this by changing the events
list as well as slightly modifying the equation and the call to NDSolve
.
g = 32;
rFree = 0.15;
rChute = 1.5;
h0 = 10000;
eqn = {h''[t] == -g - r[t]*h'[t]};
conds = {h[0] == h0, h'[0] == 0};
events = {r[0] == rFree,
WhenEvent[t == 20, r[t] -> rChute],
WhenEvent[h[t] <= 0, end = t; "StopIntegration"]};
soln = NDSolve[{eqn, conds, events}, h, {t, 0, Infinity}, DiscreteVariables -> {r}];
Here is what the solution looks like, plotted out:
We might also want to check that they decelerate, so we can plot the velocity too:
The two plots above were made using the following code:
Plot[First[h /. soln][t], {t, 0, tmax},
PlotLabel -> "Height of skydiver over time",
AxesLabel -> {"t", "h(t)"}]
and
Plot[First[h /. soln]'[t], {t, 0, tmax}, PlotRange -> Full,
PlotLabel -> "Velocity of skydiver over time",
AxesLabel -> {"t", "h'(t)"}]
Let’s unpack the solution a bit. Before, r
was just a constant, but now we want to make it a function of time since it will change at some point. Thus, we change our equation from eqn = {h''[t] == -g - r*h'[t]}
to eqn = {h''[t] == -g - r[t]*h'[t]};
. We also add the option DiscreteVariables -> {r}
in the call to NDSolve
.
The way I have organized it, we don’t have to change anything with conds
because those are just the initial conditions that force the rest of problem. Here is a nice time to point out that Mathematica will take care of figuring out the initial conditions for the “second” initial value problem we wanted to solve, namely the one after the parachute is deployed.
Now since r
is a function of time we need to specify more information about it. We do this by giving the value r[0]==rFree
of r
when the skydiver is in freefall.
We also specify the event of parachute deployment by adding WhenEvent[t == 20, r[t] -> rChute]
. This means that at 20 seconds, the parachute will be deployed.
Printing the value of tmax
, we see that the ride now takes 346
seconds.
I think our skydiver will feel much better about this. We can get the speed at which they land by plugging in the value of tmax
to the solution
First[h /. soln]'[tmax]
-21.3333
and we find that they hit the ground at a speed of a bit faster than 21 ft per second. Based on Human survivability of extreme impacts in free-fall (Snyder 1963) I think it is likely that our skydiver would be just fine.
The US Parachute Association has a guideline that the parachutes should be deployed above somewhere between 2000 and 4500 feet above ground level depending on the skydivers experience. If we have a very experienced skydiver, lets have them jump not at around 7000 feet, but closer to 2000. We can do this by changing our WhenEvent
. We’ll also want to know time at which the parachute is deployed, so we can throw a new variable tchute
in to record the time at which the parachute is actually deployed. This code is
g = 32;
rFree = 0.15;
rChute = 1.5;
h0 = 10000;
eqn = {h''[t] == -g - r[t]*h'[t]};
conds = {h[0] == h0, h'[0] == 0};
events = {r[0] == rFree,
WhenEvent[h[t] == 2000, tchute = t; r[t] -> rChute],
WhenEvent[h[t] <= 0, tmax = t; "StopIntegration"]};
soln = NDSolve[{eqn, conds, events}, h, {t, 0, Infinity},
DiscreteVariables -> {r}];
Plot[First[h /. soln][t], {t, 0, tmax},
PlotLabel -> "Height of experienced skydiver over time",
AxesLabel -> {"t", "h(t)"}]
Plot[First[h /. soln]'[t], {t, 0, tmax}, PlotRange -> Full,
PlotLabel -> "Velocity of experienced skydiver over time",
AxesLabel -> {"t", "h'(t)"}]
which gives us the plots
and we can see that the total jump takes around 132 seconds, with parachute deployment occuring at 44 seconds.
]]>First, make sure that you have graphviz
and dot2tex
installed. (Thanks to this ask.sagemath post for pointing this out.)
You can do this by calling
sage --pip install graphviz
sage --pip install dot2tex
in your terminal.
First, create your poset in Sage. For example
sage: P = posets.SetPartitions(4)
Of course, you could also make your own custom poset, but that is well-documented, so I won’t rehash that.
Now, calling view(P)
will create a PDF of the Hasse diagram. For me, it automatically opens in my system viewer. I hope it does the same for you. From there I could “save-as” to put the PDF where I want it. I know I could probably choose a custom path yada yada yada… but my file-saving GUI is nice so I use it. If it looks funky and curvy, double check that you installed graphviz
and dot2tex
. Sometimes, a PDF is enough, and you can include it into your \(\LaTeX\) document using \includegraphics
.
Sometimes, you might want to customize the image to call attention to a specific element (for me, I’d do this when localizing at a specific flat in a matroid). To get the Tikz code
that create the beautiful PDF, just run view(P, debug=True)
.
Edit: 3/20/24 It turns out to be easier to import the _latex_file_
function after running from sage.misc.latex import _latex_file_
. Still set debug=True
.
This will give you a long output. But the top part is what matters. Heres what I get
sage: view(P, debug=True)
\documentclass{article}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{amsfonts}
\usepackage{graphicx}
\usepackage{mathrsfs}
\pagestyle{empty}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\oddsidemargin 0.0in
\evensidemargin 0.0in
\textwidth 6.45in
\topmargin 0.0in
\headheight 0.0in
\headsep 0.0in
\textheight 9.0in
\usepackage{tikz}
\usepackage{tkz-graph}
\usepackage{tkz-berge}
\usetikzlibrary{arrows,shapes}
\newcommand{\ZZ}{\Bold{Z}}
\newcommand{\NN}{\Bold{N}}
\newcommand{\RR}{\Bold{R}}
\newcommand{\CC}{\Bold{C}}
\newcommand{\QQ}{\Bold{Q}}
\newcommand{\QQbar}{\overline{\QQ}}
\newcommand{\GF}[1]{\Bold{F}_{#1}}
\newcommand{\Zp}[1]{\Bold{Z}_{#1}}
\newcommand{\Qp}[1]{\Bold{Q}_{#1}}
\newcommand{\Zmod}[1]{\ZZ/#1\ZZ}
\newcommand{\CDF}{\Bold{C}}
\newcommand{\CIF}{\Bold{C}}
\newcommand{\CLF}{\Bold{C}}
\newcommand{\RDF}{\Bold{R}}
\newcommand{\RIF}{\Bold{I} \Bold{R}}
\newcommand{\RLF}{\Bold{R}}
\newcommand{\SL}{\mathrm{SL}}
\newcommand{\PSL}{\mathrm{PSL}}
\newcommand{\Bold}[1]{\mathbf{#1}}
\usepackage[tightpage,active]{preview}
\PreviewEnvironment{page}
\begin{document}\begin{page}$\begin{tikzpicture}[>=latex,line join=bevel,]
%%
\node (node_0) at (285.0bp,8.5bp) [draw,draw=none] {$\{\{1\}, \{2\}, \{3\}, \{4\}\}$};
\node (node_1) at (236.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2\}, \{3, 4\}\}$};
\node (node_2) at (138.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2, 3\}, \{4\}\}$};
\node (node_3) at (334.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2, 4\}, \{3\}\}$};
\node (node_5) at (40.0bp,61.5bp) [draw,draw=none] {$\{\{1, 2\}, \{3\}, \{4\}\}$};
\node (node_7) at (530.0bp,61.5bp) [draw,draw=none] {$\{\{1, 3\}, \{2\}, \{4\}\}$};
\node (node_10) at (432.0bp,61.5bp) [draw,draw=none] {$\{\{1, 4\}, \{2\}, \{3\}\}$};
\node (node_4) at (221.0bp,114.5bp) [draw,draw=none] {$\{\{1\}, \{2, 3, 4\}\}$};
\node (node_6) at (45.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2\}, \{3, 4\}\}$};
\node (node_12) at (485.0bp,114.5bp) [draw,draw=none] {$\{\{1, 3, 4\}, \{2\}\}$};
\node (node_8) at (133.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2, 3\}, \{4\}\}$};
\node (node_13) at (397.0bp,114.5bp) [draw,draw=none] {$\{\{1, 4\}, \{2, 3\}\}$};
\node (node_9) at (573.0bp,114.5bp) [draw,draw=none] {$\{\{1, 3\}, \{2, 4\}\}$};
\node (node_11) at (309.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2, 4\}, \{3\}\}$};
\node (node_14) at (309.0bp,167.5bp) [draw,draw=none] {$\{\{1, 2, 3, 4\}\}$};
\draw [black,->] (node_0) ..controls (271.1bp,23.968bp) and (259.79bp,35.744bp) .. (node_1);
\draw [black,->] (node_0) ..controls (238.91bp,25.49bp) and (198.0bp,39.683bp) .. (node_2);
\draw [black,->] (node_0) ..controls (298.9bp,23.968bp) and (310.21bp,35.744bp) .. (node_3);
\draw [black,->] (node_0) ..controls (206.08bp,25.929bp) and (133.85bp,40.965bp) .. (node_5);
\draw [black,->] (node_0) ..controls (363.92bp,25.929bp) and (436.15bp,40.965bp) .. (node_7);
\draw [black,->] (node_0) ..controls (331.09bp,25.49bp) and (372.0bp,39.683bp) .. (node_10);
\draw [black,->] (node_1) ..controls (231.92bp,76.365bp) and (228.84bp,86.851bp) .. (node_4);
\draw [black,->] (node_1) ..controls (175.27bp,78.716bp) and (120.14bp,93.438bp) .. (node_6);
\draw [black,->] (node_1) ..controls (316.71bp,79.031bp) and (391.31bp,94.31bp) .. (node_12);
\draw [black,->] (node_2) ..controls (162.88bp,77.79bp) and (183.95bp,90.737bp) .. (node_4);
\draw [black,->] (node_2) ..controls (136.66bp,76.215bp) and (135.66bp,86.386bp) .. (node_8);
\draw [black,->] (node_2) ..controls (222.57bp,79.153bp) and (301.64bp,94.722bp) .. (node_13);
\draw [black,->] (node_3) ..controls (299.45bp,78.091bp) and (269.25bp,91.721bp) .. (node_4);
\draw [black,->] (node_3) ..controls (411.02bp,78.935bp) and (481.56bp,93.987bp) .. (node_9);
\draw [black,->] (node_3) ..controls (327.13bp,76.516bp) and (321.83bp,87.318bp) .. (node_11);
\draw [black,->] (node_4) ..controls (247.51bp,130.87bp) and (270.15bp,143.98bp) .. (node_14);
\draw [black,->] (node_5) ..controls (41.345bp,76.215bp) and (42.342bp,86.386bp) .. (node_6);
\draw [black,->] (node_5) ..controls (68.088bp,77.903bp) and (92.161bp,91.105bp) .. (node_8);
\draw [black,->] (node_5) ..controls (128.45bp,79.269bp) and (212.04bp,95.117bp) .. (node_11);
\draw [black,->] (node_6) ..controls (128.44bp,131.62bp) and (216.64bp,148.66bp) .. (node_14);
\draw [black,->] (node_7) ..controls (487.07bp,69.16bp) and (483.98bp,69.595bp) .. (481.0bp,70.0bp) .. controls (349.6bp,87.831bp) and (313.48bp,86.638bp) .. (node_8);
\draw [black,->] (node_7) ..controls (542.13bp,76.892bp) and (551.93bp,88.504bp) .. (node_9);
\draw [black,->] (node_7) ..controls (517.3bp,76.892bp) and (507.06bp,88.504bp) .. (node_12);
\draw [black,->] (node_8) ..controls (188.7bp,131.64bp) and (238.89bp,146.19bp) .. (node_14);
\draw [black,->] (node_9) ..controls (489.56bp,131.62bp) and (401.36bp,148.66bp) .. (node_14);
\draw [black,->] (node_10) ..controls (393.89bp,78.301bp) and (360.71bp,92.061bp) .. (node_11);
\draw [black,->] (node_10) ..controls (447.11bp,77.043bp) and (459.52bp,88.985bp) .. (node_12);
\draw [black,->] (node_10) ..controls (422.28bp,76.666bp) and (414.64bp,87.79bp) .. (node_13);
\draw [black,->] (node_11) ..controls (309.0bp,129.21bp) and (309.0bp,139.39bp) .. (node_14);
\draw [black,->] (node_12) ..controls (429.3bp,131.64bp) and (379.11bp,146.19bp) .. (node_14);
\draw [black,->] (node_13) ..controls (370.49bp,130.87bp) and (347.85bp,143.98bp) .. (node_14);
%
\end{tikzpicture}$\end{page}
\end{document}
['pdflatex', '\\nonstopmode', '\\input{sage.tex}']
This is pdfTeX, Version 3.141592653-2.6-1.40.25 (TeX Live 2023) (preloaded format=pdflatex)
restricted \write18 enabled.
entering extended mode
LaTeX2e <2022-11-01> patch level 1
...
(see the transcript file for additional information)</usr/local/texlive/2023/te
xmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2023/tex
mf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2023/texmf
-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb>
Output written on sage.pdf (1 page, 29017 bytes).
Transcript written on sage.log.
viewer: "open"
The ...
eight lines from the bottom represents over a hundred lines of output. Don’t worry about those to get your Tikz output. All you need is
\begin{tikzpicture}[>=latex,line join=bevel,]
%%
\node (node_0) at (285.0bp,8.5bp) [draw,draw=none] {$\{\{1\}, \{2\}, \{3\}, \{4\}\}$};
\node (node_1) at (236.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2\}, \{3, 4\}\}$};
\node (node_2) at (138.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2, 3\}, \{4\}\}$};
\node (node_3) at (334.0bp,61.5bp) [draw,draw=none] {$\{\{1\}, \{2, 4\}, \{3\}\}$};
\node (node_5) at (40.0bp,61.5bp) [draw,draw=none] {$\{\{1, 2\}, \{3\}, \{4\}\}$};
\node (node_7) at (530.0bp,61.5bp) [draw,draw=none] {$\{\{1, 3\}, \{2\}, \{4\}\}$};
\node (node_10) at (432.0bp,61.5bp) [draw,draw=none] {$\{\{1, 4\}, \{2\}, \{3\}\}$};
\node (node_4) at (221.0bp,114.5bp) [draw,draw=none] {$\{\{1\}, \{2, 3, 4\}\}$};
\node (node_6) at (45.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2\}, \{3, 4\}\}$};
\node (node_12) at (485.0bp,114.5bp) [draw,draw=none] {$\{\{1, 3, 4\}, \{2\}\}$};
\node (node_8) at (133.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2, 3\}, \{4\}\}$};
\node (node_13) at (397.0bp,114.5bp) [draw,draw=none] {$\{\{1, 4\}, \{2, 3\}\}$};
\node (node_9) at (573.0bp,114.5bp) [draw,draw=none] {$\{\{1, 3\}, \{2, 4\}\}$};
\node (node_11) at (309.0bp,114.5bp) [draw,draw=none] {$\{\{1, 2, 4\}, \{3\}\}$};
\node (node_14) at (309.0bp,167.5bp) [draw,draw=none] {$\{\{1, 2, 3, 4\}\}$};
\draw [black,->] (node_0) ..controls (271.1bp,23.968bp) and (259.79bp,35.744bp) .. (node_1);
\draw [black,->] (node_0) ..controls (238.91bp,25.49bp) and (198.0bp,39.683bp) .. (node_2);
\draw [black,->] (node_0) ..controls (298.9bp,23.968bp) and (310.21bp,35.744bp) .. (node_3);
\draw [black,->] (node_0) ..controls (206.08bp,25.929bp) and (133.85bp,40.965bp) .. (node_5);
\draw [black,->] (node_0) ..controls (363.92bp,25.929bp) and (436.15bp,40.965bp) .. (node_7);
\draw [black,->] (node_0) ..controls (331.09bp,25.49bp) and (372.0bp,39.683bp) .. (node_10);
\draw [black,->] (node_1) ..controls (231.92bp,76.365bp) and (228.84bp,86.851bp) .. (node_4);
\draw [black,->] (node_1) ..controls (175.27bp,78.716bp) and (120.14bp,93.438bp) .. (node_6);
\draw [black,->] (node_1) ..controls (316.71bp,79.031bp) and (391.31bp,94.31bp) .. (node_12);
\draw [black,->] (node_2) ..controls (162.88bp,77.79bp) and (183.95bp,90.737bp) .. (node_4);
\draw [black,->] (node_2) ..controls (136.66bp,76.215bp) and (135.66bp,86.386bp) .. (node_8);
\draw [black,->] (node_2) ..controls (222.57bp,79.153bp) and (301.64bp,94.722bp) .. (node_13);
\draw [black,->] (node_3) ..controls (299.45bp,78.091bp) and (269.25bp,91.721bp) .. (node_4);
\draw [black,->] (node_3) ..controls (411.02bp,78.935bp) and (481.56bp,93.987bp) .. (node_9);
\draw [black,->] (node_3) ..controls (327.13bp,76.516bp) and (321.83bp,87.318bp) .. (node_11);
\draw [black,->] (node_4) ..controls (247.51bp,130.87bp) and (270.15bp,143.98bp) .. (node_14);
\draw [black,->] (node_5) ..controls (41.345bp,76.215bp) and (42.342bp,86.386bp) .. (node_6);
\draw [black,->] (node_5) ..controls (68.088bp,77.903bp) and (92.161bp,91.105bp) .. (node_8);
\draw [black,->] (node_5) ..controls (128.45bp,79.269bp) and (212.04bp,95.117bp) .. (node_11);
\draw [black,->] (node_6) ..controls (128.44bp,131.62bp) and (216.64bp,148.66bp) .. (node_14);
\draw [black,->] (node_7) ..controls (487.07bp,69.16bp) and (483.98bp,69.595bp) .. (481.0bp,70.0bp) .. controls (349.6bp,87.831bp) and (313.48bp,86.638bp) .. (node_8);
\draw [black,->] (node_7) ..controls (542.13bp,76.892bp) and (551.93bp,88.504bp) .. (node_9);
\draw [black,->] (node_7) ..controls (517.3bp,76.892bp) and (507.06bp,88.504bp) .. (node_12);
\draw [black,->] (node_8) ..controls (188.7bp,131.64bp) and (238.89bp,146.19bp) .. (node_14);
\draw [black,->] (node_9) ..controls (489.56bp,131.62bp) and (401.36bp,148.66bp) .. (node_14);
\draw [black,->] (node_10) ..controls (393.89bp,78.301bp) and (360.71bp,92.061bp) .. (node_11);
\draw [black,->] (node_10) ..controls (447.11bp,77.043bp) and (459.52bp,88.985bp) .. (node_12);
\draw [black,->] (node_10) ..controls (422.28bp,76.666bp) and (414.64bp,87.79bp) .. (node_13);
\draw [black,->] (node_11) ..controls (309.0bp,129.21bp) and (309.0bp,139.39bp) .. (node_14);
\draw [black,->] (node_12) ..controls (429.3bp,131.64bp) and (379.11bp,146.19bp) .. (node_14);
\draw [black,->] (node_13) ..controls (370.49bp,130.87bp) and (347.85bp,143.98bp) .. (node_14);
%
\end{tikzpicture}
Thats it. Thats the post.
]]>The zeroth “definition” of a variety is as a curve we want to study geometrically. To make this more precise, we can state the first definition of a variety. This is the one that I have internalized from reading Cox, Little, and O’Shea’s book Ideals, Varieties, and Algorithms, is as “the set of solutions of a polynomial equation.” Lets think about how we can think of the parabola \(y = x^2 - 1\) as an algebraic object in order to study it geometrically.
I’ll regard this as a curve in the real plane. Since we are working in the real numbers, our polynomials should have real coefficients, and since we are working in the plane, we should have an \(x\) coordinate and a \(y\) coordinate. Thus our polynomials are in the polynomial ring \(S = \mathbb{R}[x,y]\). It is a common technique in solving linear differential equations to first solve a homogeneous version of the equation. A rough reason for this is that it is easier to reason about zero than other numbers. We’ll take the same perspective here. Instead of thinking about the equation \(y = x^2 - 1 \) we can think about the polynomial \(f = y - x^2 + 1\) with the understanding that \(y - x^2 + 1 = 0\). So then the variety corresponding to \(f\) contains the points \((0,-1)\), \((\pm 1,0)\), and \((\pm 2,3)\) among others. Lets introduce notation for a variety corresponding to \(f\): \[ V((f)) = \{(x,y) : y - x^2 + 1\}. \] The “data type” of \(V((f))\) is a set of points in the plane.
It is a fact that there is a correspondence between ideals and varieties. This basically comes down to the fact that if \(f = 0\) then \(gf = 0\) for any polynomial \(g\). So if \(g\) is any polynomial in the ideal \(I = (f)\subseteq S\) generated by \(f\), and \((x,y)\) is any point in \(V((f))\), then \((x,y)\) is a solution to \(g = 0\).
We can go backwards too, but it is a little more subtle. If \(g \in S\) is a polynomial that factors as \(g = pq\), then either \(p\) or \(q\) can be zero to solve \(g\). For example, consider \(g = (x+1)^2\). Then \(V((g)) = {(-1,y) : y \in \mathbb{R}}\) but this is the same as \(V((x+1))\). The two different ideals define the same variety. In order to “reduce” the ideal, we take the radical of the ideal. This is sort of like taking \(n\)th roots in an ideal so we denote the radical of an ideal \(\sqrt{J}\).
Now we can introduce the second, to me more sophisticated but also more opaque, definition. Denote by \( \operatorname{Spec}(s) \) the set of all prime ideals of \(S\). One nice fact about prime ideals \(p\) is that \(\sqrt{p} = p\). Additionally, every maximal ideal is prime. The more sophisticated definition is that the variety of \(I\) is \[ V(I) = \{ p \in \operatorname{Spec}(s) : I \supseteq p \}.\] The “data type” of \(V(I)\) is a collection of prime ideals.
In algebraic geometry, one defines a “point” to be a maximal ideal. This makes sense when working over the complex numbers, because maximal ideals of \(\mathbb{C}[z_1, z_2, \ldots, z_n]\) all look like \((z_1 - a_1, z_2 - a_2, \ldots, z_n - a_n)\). Lets consider the zero set of this maximal ideal inside of \(\mathbb{C}^n\). (We want to think about \(\mathbb{C}^n\) because we have coefficients in \(\mathbb{C}\) and \(n\) many indeterminates.) We need every polynomial in the ideal to vanish on the whole zero set. Since \(z_1 - a_1\) must vanish, any point in the variety must have \(z_1\)-coordinate equal to \(a_1\). Since \(z_2 - a_2\) must vanish, any point in the variety must have \(z_2\)-coordinate equal to \(a_2\). Since \(z_3 - a_3\) must vanish, any point in the variety must have \(z_3\)-coordinate equal to \(a_3\). Since \(z_4 - a_4\) must vanish… you get the point. We have \(n\) linear equations and \(n\) unknowns, so if a solution exists it is a single point. The solution does exist - it is \((a_1, a_2, \ldots, a_n) \in \mathbb{C}^n\).
In \(\mathbb{R}[x,y]\), not every maximal ideal defines a point in euclidean space (for example \((x^2+1, y)\) doesn’t). On the other hand, any point in euclidean space does define a maximal ideal - \((x_0, y_0)\) defines the maximal ideal \((x - x_0, y- y_0)\). So defining a “point” to be a maximal ideal generalizes what we intuitively consider to be a point. The point (get it) of all of this is that it makes sense to call maximal ideals points. Then points in a variety are the maximal ideals which contain a specific ideal.
In the first definition we considered points to be in a variety if they solved a polynomial equation. If we pick the second definition, we considered points to be maximal ideals containing a specific ideal. It would be pretty disheartening if these two definitions of varieties corresponded to completely different notions. Fortunately it works out. Lets see how with our example of the parabola works out.
We want the points \((0,-1)\) and \((1,0)\) and \((2,3)\) to be on the parabola \(f = y - x^2 + 1\). Certainly they satisfy the equation. So if we take \(I\) to be the ideal generated by \(f\) we would hope that the points are in the variety no matter which way define it. That means that we would hope that the maximal ideals \[m_1 = (x,y+1),\; m_2 = (x-1, y),\; m_3 = (x-2, y-3)\] contain the ideal \(I\). It is enough to show that \(m_1, m_2, m_3\) contain the polynomial \(f\) because \(f\) generates \(I\). Let’s write \(f\) in a bunch of different ways. We have \[f = x(-x) + (y+1),\] \[f = (x-1)\cdot (-x-1) + y,\] \[f = (x-2)\cdot (-x-2) + (y-3).\] The first way shows that \((x, y+1) \subseteq (y-x^2+1)\). The second way shows that \((x-1, y) \subseteq (y-x^2+1)\). The third way shows that \((x-2, y-3) \subseteq (y-x^2+1)\).
So whether we regard these points as given in the Cartesian plane by \((x,y)\) coordinates, or as a maximal ideal of \(\mathbb{R}[x,y]\), they are in the variety defined (using definition 1) by the polynomial \(f = y - x^2 +1\) or (using definition 2) by the ideal \( (f) \subseteq \mathbb{R}[x,y]\).
]]>The columns of a fixed matrix are a particular choice of a set of vectors. Lets consider the matrix \[ M_1 = \begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & 1 \end{bmatrix}. \] Let \(a,b,c\) denote the columns respectively. Then we can consider \[ M_2 = \begin{bmatrix} 1 & -1 & 0 \\ -1 & 0 & -1 \\ 0 & 1 & 1 \end{bmatrix}, \] with columns \(a^\prime, b^\prime,c^\prime\). We see that both \[ a + b = c\] and \[ a^\prime + b^\prime = c^\prime \] The punchline is that the two of these matrices define the same matroid, isomorphic to \(U_{2,3}\), the uniform matroid of rank \(2\) on \(3\) elements. The matroids have groundsets \(E = \{a,b,c\}\) and \(E^\prime = \{a^\prime, b^\prime, c^\prime\}\).
A realization of a matroid \(M\) on a groundset \(E\) over \(\mathbb{C}\) is a subspace \(L \subseteq \mathbb{C}^E \) which is the span of the rows of the matrix whose columns are the groundset of the matroid. So for example, the two different realizations of \(U_{2,3}\) described by \(M_1\) and \(M_2\) are \[ L = \operatorname{span}_{\mathbb{C}}{\langle 1, 0, 1 \rangle, \langle 0, 1, 1 \rangle } \] and \[ L^\prime = \operatorname{span}_{\mathbb{C}}{\langle 1, -1, 0 \rangle,\langle -1, 0, -1 \rangle,\langle 0, 1, 1 \rangle }.\] It is clear that \(L\) is two-dimensional, but it is also true that \(L^\prime\) is two dimensional, since \(\langle 1, -1, 0 \rangle + \langle -1, 0, -1 \rangle, = -\langle 0, 1, 1 \rangle \). So we can take \(\langle 1, -1, 0 \rangle, \langle -1, 0, -1 \rangle\) to be a basis for \(L^\prime\).
The core of matroid theory is the notion of independence, so we should say how the independent sets are found by this construction. The independent sets are the sets $I \subseteq E$ where \[ L \hookrightarrow \mathbb{C}^E \twoheadrightarrow \mathbb{C}^I \] composes to a surjection. The projection \( \mathbb{C}^E \twoheadrightarrow \mathbb{C}^I \) is the one which picks the coordinates indexed by \(I\).
We can see from the vectors that any proper subsets of \(E\) and \(E^\prime\) should be independent. Lets check this for \(a,b\) and \(a^\prime, c^\prime\). Just to be sure, we’ll show \(a,b,c\) isn’t independent too. We’ll view elements of \(L\) as vectors \(\langle s, t, s+t\rangle\). and elements of \(L^\prime\) as vectors \(\langle s-t, -s, -t \rangle\).
Then the matrix representing the projection from \(L\) to \(\mathbb{C}^{{a,b}}\) is \[ \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{bmatrix} \] so the image of \(L\) is \[ \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{bmatrix} \begin{bmatrix}s\\ t\\ s+t\end{bmatrix} = \begin{bmatrix} s \\ s + t \end{bmatrix} \] which is a two dimension subspace of the two dimensional \(\mathbb{C}^{{a,b}}\). A two-dimensional subspace of a two-dimensional space is just the whole space, so the projection from \(L\) to \(\mathbb{C}^{{a,b}}\) is a surjection.
Similarly, the matrix representing the projection from \(L^\prime\) to \(\mathbb{C}^{{a^\prime, c^\prime}}\) is \[ \begin{bmatrix} 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} \] so the image of (L) is \[ \begin{bmatrix} 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix}s-t\\ -s\\ -t\end{bmatrix} = \begin{bmatrix} s-t \\ - t \end{bmatrix} \] which is a surjection onto \(\mathbb{C}^{{a^\prime,c^\prime}}\).
There is also no way that \(L\) or \(L^\prime\), two-dimensional spaces, could surject onto the three dimensional \(\mathbb{C}^{{a,b,c}}\) and \(\mathbb{C}^{{a^\prime,b^\prime,c^\prime}}\), so \(E\) and \(E^\prime\) are not independent.
I hope that these examples show exactly how a realizable matroid corresponds to a vector subspace inside of the vector space \(\mathbb{C}^E\) with basis given by the whole groundset.
]]>A sequence finite sequence \(a_1, a_2, \ldots, a_n\) is said to be “unimodal” if \[ a_1 \leq a_2 \leq \cdots a_m \geq a_{m+1} \geq \cdots \geq a_n.\]
A finite sequence is said to be “log(arithmically) concave” if \[ a_k^2 \geq a_{k-1}a_{k+1} \] for all \(k\).
Suppose \(\{a_i\}_{i=1}^n\) is a sequence of positive numbers with no internal zeroes which is log concave. Now suppose for the sake of contradiction that \(\{a_i\}\) is not unimodal. Then there exists an index \(k\) such that \(a_{k-1} \geq a_k \leq a_{k+1}\). In particular this means that both \(a_k \leq a_{k+1}\) and \(a_k \leq a_{k-1}\). Then since \(a_k > 0\) (note we are using both positivity and the fact there are no internal zeros to establish this) we have \[ a_k^2 \leq a_{k+1}a_k \leq a_{k+1}a_{k-1}. \] This contradicts the assumption that \(\{a_i\}\) was log concave. QED.
I hope that you would agree it is not too bad of a proof, but in case you (like me) have always been too embarassed to ask to see it, here it is.
Edit (19JAN23): Thanks to Anastasia Nathanson for catching a typo! Its now fixed.
]]>One of the things Sage is useful for is working with a mathematical structure and also elements of it. For example, we could work with a group \(G\) and an element \(x \in G\). As an example in Sage, lets consider the cyclic group of order seven:
sage: G = CyclicPermutationGroup(7)
sage: x = G.an_element(); x
(1,2,3,4,5,6,7)
sage: x(6)
7
This element x
is the element which cycles everything up by one, as we see by calling it on the
integer \(6\).
The fact that \(x\) is an element of \(G\) is an inextricable part of the information of \(x\).
Sage models this fundamental relationship by including G
as a “parent” and x
as an “element”.
From the documentation, a parent is
a Python instance modelling a set of mathematical elements together with its additional (algebraic) structure.
and an element is
a Python instance modelling a mathematical element of a set.
The power of this framework comes from the fact that operations on elements +, -, *, /
, etc.
will create a new element of the parent (when they make mathematical sense). The “standard idiom”
in Sage is to create a parent before creating elements, and then use that parent to create the
element. This can get slightly annoying to the user sometimes, and so later we’ll see a way
to use that idiom in code that is hidden to user but still happens in Sage.
This summer, part of my project is to implement combinatorial diagrams in Sage. A combinatorial diagram is just a collection of cells \((i,j)\) indexed by positive (or nonnegative) integers. The perhaps most familiar example to people will be the “Ferrer’s diagram”, which represents a partition of an integer by including left justified cells in rows corresponding to each part in the partition, with the number of cells in each row corresponding to the size of the part itself. A less familiar class of diagrams is those with the “northwest property,” that: if \((i_1, j_1),\,(i_2, j_2)\) are cells in a diagram \(D\), then the cell \((\min(i_1, i_2), \min(j_1, j_2))\) is also in \(D\).
In order to model diagrams in Sage, I want to have two objects, the first a class to model the diagrams themselves, and then also a class to model the class of all diagrams. The former will be the element and the latter will be the parent.
This post will build up the code one step at a time, so WARNING the blocks of code other than the final example may or may not run or make mathematical sense.
Since we are trying to have a parent/element relationship between an instance of a Diagram
and
the parent of all Diagrams
, we’ll start by importing the Parent
class, and defining Diagrams
as
as subclass.
from sage.structure.parent import Parent
class Diagrams(Parent):
pass
If you think about it, there should be only one class of all diagrams. Mathematically this makes sense,
but in Python we could (in theory) create many instances of this object. Fortunately, Sage has a way
to take care of this for us, by making Diagrams
a UniqueRepresentation
.
from sage.structure.parent import Parent
from sage.structure.unique_representation import UniqueRepresentation
class Diagrams(UniqueRepresentation, Parent):
pass
Next, we want to initialize Diagrams
. Every mathematical object in Sage should live in some
category (literally mathematically a category, with objects and morphisms). There is not too
much structure to the set of all diagrams, so it is enough to put it in the category of Sets
.
(This is the default for parent so this step is not actually necesarry, but if you wanted
to do something with more structure, you might want to put in the category of Rings
or
something.)
from sage.categories.sets_cat import Sets
from sage.structure.parent import Parent
from sage.structure.unique_representation import UniqueRepresentation
class Diagrams(UniqueRepresentation, Parent):
def __init__(self):
Parent.__init__(self, category=Sets())
Now that we have established a parent, we should establish the elements. In order to tell
Sage that elements of Diagrams
should be objects of type
Diagram
, we just assign
the attribute Element
:
from sage.categories.sets_cat import Sets
from sage.structure.parent import Parent
from sage.structure.unique_representation import UniqueRepresentation
class Diagram():
pass
class Diagrams(UniqueRepresentation, Parent):
def __init__(self):
Parent.__init__(self, category=Sets())
Element = Diagram
It is worth noting at this point that in the source code you’ll see two different
idioms for doing this. Sometimes, you’ll see the element
defined as a class inside of the parent (as in the Lie algebras code),
and sometimes it will be outside, as I have done below.
My interpretation of this is that it has to do with complexity and if you will be constructing elements
directly. For example, since I want to create a Diagram
directly and sometimes
hide creation of the Diagrams
from the user, so I’ll include it as its own class.
Don’t worry about it too much. They are doing the same thing - defining the
Element
attribute of the Parent
subclass.
Now, if we have a Parent
, we want Sage to know how to construct elements of that parent.
We do that by by defining the _element_constructor_
method.
from sage.categories.sets_cat import Sets
from sage.structure.parent import Parent
from sage.structure.unique_representation import UniqueRepresentation
class Diagram():
pass
class Diagrams(UniqueRepresentation, Parent):
def __init__(self):
Parent.__init__(self, category=Sets())
def _element_constructor_(self):
return self.element_class(self, cells)
Element = Diagram
The _element_constructor_
method is utilized when we call
instances of a parent in order to create elements from the given data.
The basic syntax is Parent()(data)
.
Calling Parent()
creates an instance of the parent, and then we call that object with
the argument data
.
Under the hood, here is what is happening:
Parent()(data)
is calling the object Parent()
with the argument(s) data
.Parent()
, we run into Parent._element_constructor_
.self.element_class(self, data)
where self
is the parent, in this case Diagrams
. (In case the two appearances of self
here worry you, we’ll come back to that later.)self.element_class
does the same thing as Parent.Element.__init__
In the sample code of this example, I haven’t created the Parent.Element.__init__
method
yet, so lets do that. I want a single diagram to be an immutable collection of cells (i,j)
,
and so I’ll make it a ClonableArray
, which is a subclass of sage.structure.element.Element
.
I don’t have great insight into why to chose this one over a different subclass, it was the one
suggested to me by my GSoC mentors. But a ClonableArray
is an immutable array of elements,
subject to some invariant, which is enforced by the (required) ClonableArray.check
method.
Lets ignore the check for the purpose of this example, but you could imagine the “check”
being a test that all cells are pairs, and the list of all of them has no repeated elements.
from sage.categories.sets_cat import Sets
from sage.structure.list_clone import ClonableArray
from sage.structure.parent import Parent
from sage.structure.unique_representation import UniqueRepresentation
class Diagram(ClonableArray):
def __init__(self, parent, cells):
self._cells = {c: True for c in cells}
ClonableArray.__init__(self, parent, cells)
def check(self):
pass
class Diagrams(UniqueRepresentation, Parent):
def __init__(self):
Parent.__init__(self, category=Sets())
def _element_constructor(self):
return self.element_class(self, cells)
Element = Diagram
Now, if you are a Sage user, you might be familiar with using the Partition
class. In the framework we’ve
described so far, Partition
is an element class whose parent is Partitions
. If you have tried to create
a partition, you usually just want to type something like mu = Partition([5,3,1])
. In particular, it is
cumbersome to create an instance of the parent class every time we create an instance of an element, especially
if we don’t use the parent class anywhere in our code. But as it stands, our code above would require
being run like this:
sage: Dgms = Diagrams()
sage: Diagram(Dgms, [(1,1), (3,5)])
which is more cumbersome than
sage: Partition([3,2,1])
especially because we know any instance of Diagram
should be in the parent of Diagrams
- why
are we manually passing it?
In order to fix this problem, we will set the metaclass of Diagram
to be
InheritComparisonClasscallMetaclass
. We get a few different things out of this, but the primary
one I want to point out is that it allows us to define a __classcall_private__
method (for
technical reasons I don’t understand, it must be a @staticmethod
) which is called
whenever the class is called (and importantly before the usual __call__
). In this case, calling
Diagram(cells)
will return Diagrams()(cells)
. Recall that Diagrams()(cells)
constructs the element
known to be an element of the parent Diagrams
. Thus, we no longer have to manually create the parent
class and instead we can just create the Diagram
, but we still have the power of the parent/element
framework to utilize later if we need it.
from sage.categories.sets_cat import Sets
from sage.structure.unique_representation import UniqueRepresentation
from sage.structure.list_clone import ClonableArray
from sage.structure.parent import Parent
from sage.misc.inherit_comparison import InheritComparisonClasscallMetaclass
class Diagram(ClonableArray, metaclass=InheritComparisonClasscallMetaclass):
@staticmethod
def __classcall_private__(cls, cells):
return Diagrams()(cells)
def __init__(self, parent, cells):
self._cells = {c: True for c in cells}
ClonableArray.__init__(self, parent, cells)
def check(self):
pass
class Diagrams(UniqueRepresentation, Parent):
def __init__(self):
Parent.__init__(self, category=Sets())
def _element_constructor_(self, cells):
return self.element_class(self, cells)
Element = Diagram
The code above now would run like this:
sage: D = Diagram([(2,1), (3,1)])
sage: D.parent() is Diagrams
True
In the process of calling Diagram
, we create Diagrams
under the hood, and know that our
Diagram
should belong to it. Woo!
self.element_class(self, data)
?Above, I had mentioned that it was mysterious why self.element_class
utilized self
twice. One reason
this is actualy awesome is because it allows us to subclass things very easily. For example, suppose I wanted
to create a class for subdiagrams. This should definitely be a subclass of Diagram
and its parent should be
a subclass of Diagrams
, so we could do something like this:
class SubDiagram(Diagram):
@static_method
def __classcall_private__(cls, cells, D):
return SubDiagrams()(cells, D)
def __init__(self, parent, cells, D):
self._super_diagram = D
Diagram.__init__(self, parent, cells)
class SubDiagrams(Diagrams):
Element = SubDiagram
If the SubDiagram.__init__
method didn’t require a parent
argument, we’d have to duplicate all
of the code we wrote for the Diagram
, so that the parent of SubDiagram
was not Diagrams
.
Then, we’d have to rewrite the _element_constructor_
method because self
would be a diagram, not
a subdiagram. In short, the self.element_class(self, data)
and requiring the parent as an argument
allows us to be a lot more flexible with how we create classes and avoid unnecessary duplication of code.
I hope that this example helps show you how to create parents and elements and how to subclass them. If this helps you in your Sage development, or if anything is unclear, please let me know! Thanks for reading!
]]>Suppose we wanted to compute the Stirling number \(c(100,10)\). There are two ways to compute this in Sage. The first is
sage: stirling_number1(100,10)
1125272380578944825172147216455781795628518533063359467753257922095126858974036298524724548786755944321693935796805099192994799803568595147738316800000000000
The second is a little more involved, but is a great example of how to extract coefficients from a generating function. We’ll compute the product above, and then take a derivative. Then, we’ll extract the coefficient by evaluating that derivative at \(x=0\).
sage: R.<x> = PolynomialRing(QQ)
sage: G = R.prod((x+i) for i in range(100))
sage: G.derivative(x, 10)(0)/factorial(10) # the 10th derivative
1125272380578944825172147216455781795628518533063359467753257922095126858974036298524724548786755944321693935796805099192994799803568595147738316800000000000.
Both of these seemed fast to my human eye, so I wanted to look into how long they took in comparison to eachother. On my MacBook Air, running SageMath 9.5.beta9, here were the results:
sage: timeit('G.derivative(x, 10)(0)/factorial(10)')
625 loops, best of 3: 56.4 μs per loop
sage: timeit('stirling_number1(100,10)')
625 loops, best of 3: 171 μs per loop
I find these results rather surprising! Computing the derivative seems like it would be faster than the built-in function. Why would we have a built in function, if it was slower? Well, it seems the answer to that would be that it doesn’t take that long to compute 10 derivatives, but if we wanted a larger value of \(k\) then it might be slower. For example, when \(k=90\), it takes less than half the time.
sage: timeit('G.derivative(x, 90)(0)/factorial(10)')
625 loops, best of 3: 371 μs per loop
sage: timeit('stirling_number1(100,90)')
625 loops, best of 3: 120 μs per loop
Another reason we might want to use the built-in version over the derivative, even for small \(k\), is that creating the polynomial ring and the generating function introduces some overhead that adds unnecessary time:
sage: timeit('R.<x> = PolynomialRing(QQ); G = R.prod((x+i) for i in range(100)); G.derivative(x, 10)(0)/factorial(10)')
625 loops, best of 3: 363 μs per loop
The reason this is called the hat-check problem is because it can be phrased as the question: “if \(n\)-many people check their hats, how many ways are there to give the \(n\)-many hats back to them, such that no person recieves back their own hat?”
The answer is that it is the nearest integer to \(n!/e\). Let \(f_n\) denote the number of permutations of \(\{1,2,\ldots, n\}\) with no fixed points. The exact solution is \[ f_n = n! \sum_{i=0}^n \frac{(-1)^i}{i!}.\] Today I am less concerned with how to find \(f_n\) exactly (it is given in Stanley’s EC1), and instead I want to show the statement that it is the nearest integer to \(n!/e\).
The idea is that we can compute the power series representation \(e^x = \sum_{i=1}^\infty \frac{x^i}{i!}\). So then \(1/e = \sum_{i=1}^\infty \frac{(-1)^i}{i!}\). Thus, from the product given above \(f_n/n!\) is the Taylor approximation of \(1/e\) to order \(n\).
We want to show that \[ \left | f_n - n!/e \right | < \frac{1}{2} \] because then \(f_n\) (which we can see is an integer from the formula) is the closest integer to \(n!/e.\)
Taylor’s theorem applied to the interval \([-2,0]\) containing \(1\) tells us that since the approximation to \(1/e\) given by \(f_n/n!\) was centered at \(0\) the quantity \[ R_n = \frac{1}{e} - \sum_{i=0}^n \frac{(-1)^i}{i!}\] satisfies \[ R_n < \frac{1}{(n+1)!} \] so as long as \(n\) is at least one, \(n! R_n < 1/2\) and so \(f_n\) is the nearest integer to \(n!/e\).
]]>