Yes, this is relatively straightforward.
First, it seems that Sage has this built-in (see the dual flag, although I didn't test it).
I'll describe the "mathematical" way to proceed, as I think it is conceptually more useful.
For a lattice with basis $B$, it is well-known (see theorem 2) that the dual has basis:
$$D = B (B^t B)^{-1}$$
It follows that the following code snippet:
S = sage.crypto.gen_lattice()
D = S * (S.T * S).inverse()
will generate the desired basis. Note that the basis is not an integer matrix in general --- for a (random) matrix $S$ generated in an execution of sage.crypto.gen_lattice()
, I get that the dual basis is:
[ 1/11 0 0 0 -4/11 -3/11 2/11 0]
[ 0 1/11 0 0 -5/11 1/11 -3/11 0]
[ 0 0 1/11 0 0 5/11 -4/11 -5/11]
[ 0 0 0 1/11 -2/11 4/11 4/11 -5/11]
[ 0 0 0 0 1 0 0 0]
[ 0 0 0 0 0 1 0 0]
[ 0 0 0 0 0 0 1 0]
[ 0 0 0 0 0 0 0 1]
The (primal) basis was chosen to be $q$-ary for $q = 11$.
You might notice that by scaling things up by $q = 11$, we get an integer matrix.
This holds in general, and can be seen by noting that a $q$-ary lattice $L$ satisfies:
\begin{align*}
q\mathbb{Z}^m&\subseteq L\subseteq \mathbb{Z}^m\\
\iff (q\mathbb{Z}^m)^*&\supseteq L^* \supseteq \mathbb{Z}^m\\
\iff \frac{1}{q}\mathbb{Z}^m&\supseteq L^*\supseteq \mathbb{Z}^m\\
\iff \mathbb{Z}^m &\supseteq qL^*\supseteq q\mathbb{Z}^m
\end{align*}
This is to say that if $q\mathbb{Z}^m\subseteq L\subseteq \mathbb{Z}^m$, then while the dual lattice $L^*$ may not be contained between $q\mathbb{Z}^m$ and $\mathbb{Z}^m$, the scaled dual lattice always is.
One often sees this in papers as the statement:
$$\Lambda_q(A)^* = \frac{1}{q}\Lambda_q^\perp(A)$$
In fact, in the notation of $\Lambda_q(A)$ and $\Lambda_q^\perp(A)$, you are simply asking, given a basis for $\Lambda_q(A)$, to construct a basis for $\Lambda_q^\perp(A)$.