From 4b52f085f8eb3fc9b81f4f37e4e5ef7f197d0248 Mon Sep 17 00:00:00 2001 From: "aram.dermenjian" Date: Tue, 29 Sep 2020 15:11:43 -0400 Subject: [PATCH 01/10] Add Nu Dyck Words and Nu Tamari Lattice --- src/doc/en/reference/combinat/module_list.rst | 2 + src/doc/en/reference/references/index.rst | 7 + src/sage/combinat/all.py | 1 + src/sage/combinat/nu_dyck_word.py | 1403 +++++++++++++++++ src/sage/combinat/nu_tamari_lattice.py | 105 ++ src/sage/combinat/words/paths.py | 38 + 6 files changed, 1556 insertions(+) create mode 100644 src/sage/combinat/nu_dyck_word.py create mode 100644 src/sage/combinat/nu_tamari_lattice.py diff --git a/src/doc/en/reference/combinat/module_list.rst b/src/doc/en/reference/combinat/module_list.rst index d1da854a364..7a97a40e30d 100644 --- a/src/doc/en/reference/combinat/module_list.rst +++ b/src/doc/en/reference/combinat/module_list.rst @@ -164,6 +164,8 @@ Comprehensive Module List sage/combinat/ncsym/ncsym sage/combinat/necklace sage/combinat/non_decreasing_parking_function + sage/combinat/nu_dyck_word + sage/combinat/nu_tamari_lattice sage/combinat/ordered_tree sage/combinat/output sage/combinat/parallelogram_polyomino diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 46e46557a02..04547fd8456 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -998,6 +998,9 @@ REFERENCES: lacunas of the Thue-Morse word*, Proc. GASCOM 2008 (June 16-20 2008, Bibbiena, Arezzo-Italia), 53--67. +.. [BMFPR] \M. Bousquet-Melou, E. Fusy, L.-F. Preville Ratelle. + *The number of intervals in the m-Tamari lattices*. :arxiv:`1106.1498` + .. [BMS2006] Bugeaud, Mignotte, and Siksek. "Classical and modular approaches to exponential Diophantine equations: I. Fibonacci and Lucas perfect powers." Annals @@ -4775,6 +4778,10 @@ REFERENCES: *PICARO - a block cipher allowing efficient higher-order side-channel resistance*; in ACNS, (2012), pp. 311-328. +.. [PRV2017] \L.-F. Préville-Ratelle and X. Viennot, + *The enumeration of generalized Tamari intervals*. + Trans. Amer. Math. Soc. 369 (2017), pp 5219--5239 + .. [Prototype_pattern] Prototype pattern, :wikipedia:`Prototype_pattern` diff --git a/src/sage/combinat/all.py b/src/sage/combinat/all.py index 953c6b29761..00153b736e5 100644 --- a/src/sage/combinat/all.py +++ b/src/sage/combinat/all.py @@ -216,6 +216,7 @@ from .subset import Subsets from .necklace import Necklaces lazy_import('sage.combinat.dyck_word', ('DyckWords', 'DyckWord')) +lazy_import('sage.combinat.nu_dyck_word', ('NuDyckWords', 'NuDyckWord')) from .sloane_functions import sloane lazy_import('sage.combinat.superpartition', ('SuperPartition', 'SuperPartitions')) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py new file mode 100644 index 00000000000..07b39d3e965 --- /dev/null +++ b/src/sage/combinat/nu_dyck_word.py @@ -0,0 +1,1403 @@ +# -*- coding: utf-8 -*- +r""" +`\nu`-Dyck Words + +A class of the `\nu`-Dyck word, see [PRV2017]_ for details. + +AUTHORS: + +- Aram Dermenjian (2020-09-26) + +This file is based off the class ``DyckWords`` written by Mke Hansen, Dan +Drake, Florent Hivert, Christian Stump, Mike Zabrocki, Jean--Baptiste Priez +and Travis Scrimshaw + +""" + +# **************************************************************************** +# Copyright (C) 2020 Aram Dermenjian , +# +# Distributed under the terms of the GNU General Public License (GPL) +# +# This code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# The full text of the GPL is available at: +# +# https://www.gnu.org/licenses/ +# **************************************************************************** + +from sage.structure.element import Element +from sage.rings.integer import Integer +from sage.combinat.combinat import CombinatorialElement +from sage.structure.global_options import GlobalOptions +from sage.structure.parent import Parent +from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets +from sage.combinat.permutation import Permutations +from sage.misc.latex import latex + +from sage.combinat.words.paths import WordPaths_north_east +from sage.combinat.words.paths import FiniteWordPath_north_east + +ndw_open_symbol = 1 +ndw_close_symbol = 0 + + +def update_ndw_symbols(os, cs): + r""" + A way to alter the open and close symbols from sage + + INPUT: + + - ``os`` -- The open symbol + - ``cs`` -- The close symbol + """ + global ndw_open_symbol + global ndw_close_symbol + ndw_open_symbol = os + ndw_close_symbol = cs + + +def replace_dyck_char(x): + r""" + A map sending an opening character (``'1'``, ``'N'``, and ``'('``) to + ``ndw_open_symbol`` and a closing character (``'0'``, ``'E'``, and ``')'``) to + ``ndw_close_symbol``, and raising an error on any input other than one of the + opening or closing characters. + + This is the inverse map of :func:`replace_dyck_symbol`. + + INPUT: + + - ``x`` -- str - A ``'1'``, ``'0'``, ``'N'``, ``'E'``, ``'('`` or ``')'`` + + OUTPUT: + + - If ``x`` is an opening character, replace ``x`` with the + constant ``ndw_open_symbol``. + + - If ``x`` is a closing character, replace ``x`` with the + constant ``ndw_close_symbol``. + + - Raise a ``ValueError`` if ``x`` is neither an opening nor a + closing character. + + .. SEEALSO:: :func:`replace_dyck_symbol` + + EXAMPLES:: + + sage: from sage.combinat.nu_dyck_word import replace_dyck_char + sage: replace_dyck_char('(') + 1 + sage: replace_dyck_char(')') + 0 + sage: replace_dyck_char(1) + Traceback (most recent call last): + ... + ValueError + """ + if x == '(' or x == 'N' or x == str(ndw_open_symbol): + return ndw_open_symbol + elif x == ')' or x == 'E' or x == str(ndw_close_symbol): + return ndw_close_symbol + else: + raise ValueError + + +def replace_dyck_symbol(x, open_char='N', close_char='E'): + r""" + A map sending ``ndw_open_symbol`` to ``open_char`` and ``ndw_close_symbol`` to + ``close_char``, and raising an error on any input other than + ``ndw_open_symbol`` and ``ndw_close_symbol``. The values of the constants + ``ndw_open_symbol`` and ``ndw_close_symbol`` are subject to change. + + This is the inverse map of :func:`replace_dyck_char`. + + INPUT: + + - ``x`` -- either ``ndw_open_symbol`` or ``ndw_close_symbol``. + + - ``open_char`` -- str (optional) default ``'N'`` + + - ``close_char`` -- str (optional) default ``'E'`` + + OUTPUT: + + - If ``x`` is ``ndw_open_symbol``, replace ``x`` with ``open_char``. + + - If ``x`` is ``ndw_close_symbol``, replace ``x`` with ``close_char``. + + - If ``x`` is neither ``ndw_open_symbol`` nor ``ndw_close_symbol``, a + ``ValueError`` is raised. + + .. SEEALSO:: :func:`replace_dyck_char` + + EXAMPLES:: + + sage: from sage.combinat.nu_dyck_word import replace_dyck_symbol + sage: replace_dyck_symbol(1) + 'N' + sage: replace_dyck_symbol(0) + 'E' + sage: replace_dyck_symbol(3) + Traceback (most recent call last): + ... + ValueError + """ + if x == ndw_open_symbol: + return open_char + elif x == ndw_close_symbol: + return close_char + else: + raise ValueError + + +class NuDyckWord(CombinatorialElement): + r""" + A `\nu`-Dyck word. + + Given a lattice path `\nu` in the `\ZZ^2` grid starting at the origin + `(0,0)` consisting of North `N = (0,1)` and East `E = (1,0)` steps, a + `\nu`-Dyck path is a lattice path in the `\ZZ^2` grid starting at the + origin `(0,0)` and ending at the same coordinate as `\nu` such that it is + weakly above `\nu`. A `\nu`-Dyck word is the representation of a + `\nu`-Dyck path where a North step is represented by a 1 and an East step + is represented by a 0. + + INPUT: + + - k1 -- A path for the `\nu`-Dyck word + + - k2 -- A path for `\nu` + + EXAMPLES:: + + sage: dw = NuDyckWord([1,0,1,0],[1,0,0,1]); dw + [1, 0, 1, 0] + sage: print(dw) + NENE + sage: dw.height() + 2 + + :: + + sage: dw = NuDyckWord('1010',[1,0,0,1]); dw + [1, 0, 1, 0] + + :: + + sage: dw = NuDyckWord('NENE',[1,0,0,1]); dw + [1, 0, 1, 0] + + :: + + sage: NuDyckWord([1,0,1,0],[1,0,0,1]).pretty_print() + __ + _|x + | . . + + :: + + sage: from sage.combinat.nu_dyck_word import update_ndw_symbols + sage: update_ndw_symbols(0,1) + sage: dw = NuDyckWord('0101001','0110010'); dw + [0, 1, 0, 1, 0, 0, 1] + sage: dw.pp() + __ + |x + _| . + _|x . + | . . . + sage: update_ndw_symbols(1,0) + + """ + @staticmethod + def __classcall_private__(cls, dw=None, nu=None, **kwargs): + """ + Return an element with the appropriate parent. + + EXAMPLES:: + + sage: NuDyckWord('110100','101010') + [1, 1, 0, 1, 0, 0] + sage: NuDyckWord('010','010') + [0, 1, 0] + """ + + # if dw is none, then we might have a normal Dyck word + if dw is None: + from sage.combinat.dyck_word import DyckWord + return DyckWord(dw, kwargs) + + if isinstance(dw, NuDyckWord): + return dw + + if nu is None: + raise ValueError("nu required") + + dw = to_word_path(dw) + nu = to_word_path(nu) + + if path_weakly_above_other(dw, nu): + return NuDyckWords(nu)(dw) + + raise ValueError("invalid nu-Dyck word") + + def __init__(self, parent, dw, latex_options={}): + Element.__init__(self, parent) + self._path = to_word_path(dw) + + if parent is None: + raise ValueError("Need Parent object") + + self._nu = parent._nu + + self._latex_options = dict(latex_options) + + def _list(self): + """ + Return list of self + """ + return list(self._path) + + def __eq__(self, other): + """ + Return if two paths are equal + """ + if type(other) != type(self): + return False + return self._path == other._path and self._nu == other._nu + + def __neq__(self, other): + """ + Return if two paths are not equal + """ + return not self.__eq__(other) + + def __le__(self, other): + """ + Returns if one path is included in another + """ + if self._nu == other._nu: + return path_weakly_above_other(other._path, self._path) + return False + + def __lt__(self, other): + """ + Returns if one path is strictly included in another + """ + return self.__le__(other) and not self.__eq__(other) + + def __ge__(self, other): + """ + Returns if one path is included in another + """ + if self._nu == other._nu: + return path_weakly_above_other(self._path, other._path) + return False + + def __gt__(self, other): + """ + Returns if one path is strictly included in another + """ + return self.__ge__(other) and not self.__eq__(other) + + def _cache_key(self): + """ + Return a cache key + """ + return str(self._path) + "-" + str(self._nu) + + def __hash__(self): + return hash(''.join([str(i) for i in self._list()])) + + def set_latex_options(self, D): + r""" + Set the latex options for use in the ``_latex_`` function. + + The default values are set in the ``__init__`` function. + + - ``color`` -- (default: black) the line color. + + - ``line width`` -- (default: `2 \times` ``tikz_scale``) value representing the + line width. + + - ``nu_options`` -- (default: ``'rounded corners=1, color=red, line + width=1'``) str to indicate what the tikz options should be for path + of `\nu`. + + - ``points_color`` -- (default: ``'black'``) str to indicate color + points should be drawn with. + + - ``show_grid`` -- (default: ``True``) boolean value to indicate if + grid should be shown. + + - ``show_nu`` -- (default: ``True``) boolean value to indicate if `\nu` + should be shown. + + - ``show_points`` -- (default: ``False``) boolean value to indicate + if points should be shown on path. + + - ``tikz_scale`` -- (default: 1) scale for use with the tikz package. + + INPUT: + + - ``D`` -- a dictionary with a list of latex parameters to change + + EXAMPLES:: + + sage: NDW = NuDyckWord('010','010') + sage: NDW.set_latex_options({"tikz_scale":2}) + sage: NDW.set_latex_options({"color":"blue", "show_points":True}) + + .. TODO:: + + This should probably be merged into NuDyckWord.options. + """ + for opt in D: + self._latex_options[opt] = D[opt] + + def latex_options(self): + r""" + Return the latex options for use in the ``_latex_`` function as a + dictionary. + + The default values are set using the options. + + - ``color`` -- (default: black) the line color. + + - ``line width`` -- (default: 2*``tikz_scale``) value representing the + line width. + + - ``nu_options`` -- (default: ``'rounded corners=1, color=red, line + width=1'``) str to indicate what the tikz options should be for path + of `\nu`. + + - ``points_color`` -- (default: ``'black'``) str to indicate color + points should be drawn with. + + - ``show_grid`` -- (default: ``True``) boolean value to indicate if + grid should be shown. + + - ``show_nu`` -- (default: ``True``) boolean value to indicate if `\nu` + should be shown. + + - ``show_points`` -- (default: ``False``) boolean value to indicate + if points should be shown on path. + + - ``tikz_scale`` -- (default: 1) scale for use with the tikz package. + + EXAMPLES:: + + sage: NDW = NuDyckWord('010','010') + sage: NDW.latex_options() + {'color': black, + 'line width': 2, + 'nu_options': rounded corners=1, color=red, line width=1, + 'points_color': black, + 'show_grid': True, + 'show_nu': True, + 'show_points': False, + 'tikz_scale': 1} + + .. TODO:: + + This should probably be merged into NuDyckWord.options. + """ + d = self._latex_options.copy() + opts = self.parent().options + if "tikz_scale" not in d: + d["tikz_scale"] = opts.latex_tikz_scale + if "line width" not in d: + d["line width"] = opts.latex_line_width_scalar * d["tikz_scale"] + if "color" not in d: + d["color"] = opts.latex_color + if "show_points" not in d: + d["show_points"] = opts.latex_show_points + if "points_color" not in d: + d["points_color"] = opts.latex_points_color + if "show_grid" not in d: + d["show_grid"] = opts.latex_show_grid + if "show_nu" not in d: + d["show_nu"] = opts.latex_show_nu + if "nu_options" not in d: + d["nu_options"] = opts.latex_nu_options + return d + + def _repr_(self): + r""" + Return a string representation of ``self`` depending on + :meth:`NuDyckWords.options`. + + TESTS:: + + sage: NuDyckWord('01010','00011') + [0, 1, 0, 1, 0] + sage: NuDyckWord('10010','00011') + [1, 0, 0, 1, 0] + sage: NuDyckWords.options.display="lattice" + sage: NuDyckWord('10010','00011') + __ + ___|x + |x x x + + sage: NuDyckWords.options._reset() + """ + return self.parent().options._dispatch(self, '_repr_', 'display') + + def _repr_list(self): + r""" + Return a string representation of ``self`` as a list. + + TESTS:: + + sage: NuDyckWord([1,1,0],[1,0,1]) + [1, 1, 0] + sage: NuDyckWord('NNEE','NENE') + [1, 1, 0, 0] + """ + return str(list(self._path)) + + def _repr_lattice(self, type=None, labelling=None): + r""" + See :meth:`pretty_print()`. + + TESTS:: + + sage: print(NuDyckWord('00011001000100','00011001000100')._repr_lattice(type='N-E', labelling=[1,2,3,4])) + ____ + _____| . . 4 + ___| . . . . . 3 + | . . . . . . . 2 + ______| . . . . . . . 1 + + + sage: print(NuDyckWord('100101','010011')._repr_lattice()) + _| + ___|x + |x . . + + sage: print(NuDyckWord('110010','001011')._repr_lattice()) + __ + ___|x + |x x x + |x x . + + + """ + if type is None: + type = self.parent().options.diagram_style + if type == "grid": + type = "N-E" + + if type == "N-E": + path_length = self._path.length() + height = self._path.height() + width = self._path.width() + if path_length == 0: + return ".\n" + + # Handle right-hand side labels + if labelling is None: + labels = [" "] * height + else: + if len(labelling) != height: + raise ValueError( + "The given labelling has the wrong length. {num} needed".format(num=height)) + labels = [str(label) for label in labelling] + max_length = max(len(label) for label in labels) + labels = [lbl.rjust(max_length + 1) for lbl in labels] + + rev_path = list(self._path.reversal()) + rev_nu_path = list(self._nu.reversal()) + ts = "" + + # Grab first line + cur_pos = rev_path.index(ndw_open_symbol) + cur_nu_pos = rev_nu_path.index(ndw_open_symbol) + if cur_pos > 0: + ts += " " * (width - cur_pos) + ts += " _" + "__" * (cur_pos-1) + ts += "_\n" + + # Middle Lines + for i in range(height-1): + old_pos = cur_pos + old_nu_pos = cur_nu_pos + cur_pos = rev_path.index(ndw_open_symbol, cur_pos + 1) + cur_nu_pos = rev_nu_path.index(ndw_open_symbol, cur_nu_pos + 1) + + ts += " " * (width - cur_pos + i + 1) + if cur_pos != old_pos + 1: + ts += " _" + "__" * (cur_pos - old_pos - 2) + ts += "|" + if old_pos >= 0: + ts += "x " * (old_pos - old_nu_pos) + ts += " ." * (old_nu_pos - i) + ts += labels[height-i-1] + ts += "\n" + + # Final line + ts += "__" * (path_length - cur_pos - 1) + ts += "|" + ts += "x " * (cur_pos - cur_nu_pos) + ts += " ." * (cur_nu_pos - i - 1) + ts += labels[0] + ts += "\n" + return ts + else: + raise ValueError("The given type (=%s) is not valid." % type) + + def _ascii_art_(self): + r""" + Return an ASCII art representation of ``self``. + + TESTS:: + + sage: ascii_art(NuDyckWord('00011001000100','00011001000100')) + ____ + _____| . . + ___| . . . . . + | . . . . . . . + ______| . . . . . . . + + """ + from sage.typeset.ascii_art import AsciiArt + rep = self.parent().options.ascii_art + if rep == "pretty_output": + ret = self._repr_lattice() + return AsciiArt(ret.splitlines(), baseline=0) + + def __str__(self): + r""" + Return a string consisting of N and E steps corresponding to + the `\nu`-Dyck word. + + EXAMPLES:: + + sage: str(NuDyckWord('100101','010011')) + 'NEENEN' + sage: str(NuDyckWord('101010','100110')) + 'NENENE' + """ + return "".join(map(replace_dyck_symbol, list(self._path))) + + def pretty_print(self, type=None, labelling=None): + r""" + Display a NuDyckWord as a lattice path in the `\ZZ^2` grid. + + If the ``type`` is "N-E", then a cell below the diagonal is + indicated by a period, whereas a cell below the path but above + the diagonal is indicated by an x. If a list of labels is + included, they are displayed along the vertical edges of the + Dyck path. + + INPUT: + + - ``type`` -- (default: ``None``) can either be: + + - ``None`` to use the option default + - "N-E" to show ``self`` as a path of north and east steps, or + + - ``labelling`` -- (if type is "N-E") a list of labels assigned to + the up steps in ``self``. + + - ``underpath`` -- (if type is "N-E", default: ``True``) If ``True``, + an ``x`` to show the boxes between `\nu` and the `\nu`-Dyck Path. + + EXAMPLES:: + + sage: for ND in NuDyckWords('101010'): ND.pretty_print() + ______ + |x x . + |x . . + | . . . + ____ + _|x . + |x . . + | . . . + __ + ___| . + |x . . + | . . . + ____ + |x . + _| . . + | . . . + __ + _| . + _| . . + | . . . + + :: + + sage: ND = NuDyckWord([1,1,1,0,1,0,0,1,1,0,0,0],[1,0,1,0,1,0,1,0,1,0,1,0]) + sage: ND.pretty_print() + ______ + |x x . + ___|x . . + _|x x . . . + |x x . . . . + |x . . . . . + | . . . . . . + + :: + + sage: NuDyckWord([1,1,0,0,1,0],[1,0,1,0,1,0]).pretty_print( + ....: labelling=[1,3,2]) + __ + ___| . 2 + |x . . 3 + | . . . 1 + + :: + + sage: NuDyckWord('1101110011010010001101111000110000', + ....: '1010101010101010101010101010101010').pretty_print( + ....: labelling=list(range(1,18))) + ________ + |x x x . 17 + _____|x x . . 16 + |x x x x . . . 15 + |x x x . . . . 14 + |x x . . . . . 13 + _|x . . . . . . 12 + |x . . . . . . . 11 + _____| . . . . . . . . 10 + ___|x x . . . . . . . . . 9 + _|x x x . . . . . . . . . . 8 + |x x x . . . . . . . . . . . 7 + ___|x x . . . . . . . . . . . . 6 + |x x x . . . . . . . . . . . . . 5 + |x x . . . . . . . . . . . . . . 4 + _|x . . . . . . . . . . . . . . . 3 + |x . . . . . . . . . . . . . . . . 2 + | . . . . . . . . . . . . . . . . . 1 + + + :: + + sage: NuDyckWord().pretty_print() + . + """ + print(self._repr_lattice(type, labelling)) + + pp = pretty_print + + def _latex_(self): + r""" + A latex representation of ``self`` using the tikzpicture package. + + EXAMPLES:: + sage: NDW = NuDyckWord('010','010') + sage: NDW.set_latex_options({"show_points":True}) + sage: latex(NDW) + \vcenter{\hbox{$\begin{tikzpicture}[scale=1] + \draw[dotted] (0, 0) grid (2, 1); + \draw[line width=2,color=black,fill=black](0, 0) circle (0.21); + \draw[line width=2,color=black,fill=black](1, 0) circle (0.21); + \draw[line width=2,color=black,fill=black](1, 1) circle (0.21); + \draw[line width=2,color=black,fill=black](2, 1) circle (0.21); + \draw[rounded corners=1, color=red, line width=1] (0, 0) -- (1, 0) -- (1, 1) -- (2, 1); + \draw[rounded corners=1, color=black, line width=2] (0, 0) -- (1, 0) -- (1, 1) -- (2, 1); + \end{tikzpicture}$}} + sage: NuDyckWord('01','01')._latex_() + '\\vcenter{\\hbox{$\\begin{tikzpicture}[scale=1]\n \\draw[dotted] (0, 0) grid (1, 1);\n \\draw[rounded corners=1, color=red, line width=1] (0, 0) -- (1, 0) -- (1, 1);\n \\draw[rounded corners=1, color=black, line width=2] (0, 0) -- (1, 0) -- (1, 1);\n\\end{tikzpicture}$}}' + sage: NuDyckWord('101100','101010')._latex_() + '\\vcenter{\\hbox{$\\begin{tikzpicture}[scale=1]\n \\draw[dotted] (0, 0) grid (3, 3);\n \\draw[rounded corners=1, color=red, line width=1] (0, 0) -- (0, 1) -- (1, 1) -- (1, 2) -- (2, 2) -- (2, 3) -- (3, 3);\n \\draw[rounded corners=1, color=black, line width=2] (0, 0) -- (0, 1) -- (1, 1) -- (1, 2) -- (1, 3) -- (2, 3) -- (3, 3);\n\\end{tikzpicture}$}}' + """ + latex.add_package_to_preamble_if_available("tikz") + latex_options = self.latex_options() + + # Start setting up tikz + res = "\\vcenter{\\hbox{$\\begin{tikzpicture}" + res += "[scale=" + str(latex_options['tikz_scale']) + "]" + res += "\n" + + # Setup background grid + if latex_options['show_grid']: + grid = [((0, 0), (self.width(), self.height()))] + for v1, v2 in grid: + res += " \\draw[dotted] %s grid %s;" % (str(v1), str(v2)) + res += "\n" + + # Add points if wanted + if latex_options['show_points']: + pt_color = latex_options['points_color'] + radius = 0.15 + .03 * latex_options['line width'] + for v in self.points(): + res += " \\draw[line width=2," + res += "color=%s,fill=%s]" % (pt_color, pt_color) + res += "%s circle (%s);" % (str(v), str(radius)) + res += "\n" + + # Add nu if wanted + if latex_options['show_nu']: + res += " \\draw[%s]" % (str(latex_options['nu_options'])) + for k, p in enumerate(self._nu.points()): + if k == 0: + res += " %s" % (str(p)) + else: + res += " -- %s" % (str(p)) + res += ";\n" + + # setup Path + res += " \\draw[rounded corners=1, color=%s, line width=%s]" % ( + latex_options['color'], + str(latex_options['line width']) + ) + for k, p in enumerate(self._path.points()): + if k == 0: + res += " %s" % (str(p)) + else: + res += " -- %s" % (str(p)) + res += ";\n" + res += "\\end{tikzpicture}$}}" + return res + + def plot(self, **kwds): + r""" + Plot a `\nu`-Dyck word as a continuous path. + + EXAMPLES:: + + sage: NDW = NuDyckWord('010','010') + sage: NDW.plot() + Graphics object consisting of 1 graphics primitive + """ + from sage.plot.plot import list_plot + return list_plot(list(self.points()), plotjoined=True, **kwds) + + def path(self): + r""" + Return the underlying path object + + EXAMPLES:: + + sage: NDW = NuDyckWord('10011001000','00100101001') + sage: NDW.path() + Path: 10011001000 + """ + return self._path + + def height(self): + r""" + Return the height of ``self``. + + The height is the number of ``north`` steps. + + EXAMPLES:: + + sage: NuDyckWord('1101110011010010001101111000110000', + ....: '1010101010101010101010101010101010').height() + 17 + """ + return self._path.height() + + def width(self): + r""" + Return the width of ``self``. + + The width is the number of ``east`` steps. + + EXAMPLES:: + + sage: NuDyckWord('110111001101001000110111100011000', + ....: '101010101010101010101010101010101').width() + 16 + """ + return self._path.width() + + def length(self): + r""" + Return the length of ``self``. + + The length is the total number of steps. + + EXAMPLES:: + + sage: NDW = NuDyckWord('10011001000','00100101001') + sage: NDW.length() + 11 + """ + return self._path.length() + + def points(self): + r""" + Returns an iter with the points on the `\nu`-Dyck path. + + EXAMPLES:: + + sage: list(NuDyckWord('110111001101001000110111100011000', + ....: '101010101010101010101010101010101')._path.points()) + [(0, 0), + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 5), + (3, 5), + (3, 6), + (3, 7), + (4, 7), + (4, 8), + (5, 8), + (6, 8), + (6, 9), + (7, 9), + (8, 9), + (9, 9), + (9, 10), + (9, 11), + (10, 11), + (10, 12), + (10, 13), + (10, 14), + (10, 15), + (11, 15), + (12, 15), + (13, 15), + (13, 16), + (13, 17), + (14, 17), + (15, 17), + (16, 17)] + """ + return self._path.points() + + def heights(self): + r""" + Return the heights of each point on ``self``. + + We view the Dyck word as a Dyck path from `(0,0)` to + `(x,y)` in the first quadrant by letting ``1``'s represent + steps in the direction `(0,1)` and ``0``'s represent steps in + the direction `(1,0)`. + + The heights is the sequence of the `y`-coordinates of all + `x+y` lattice points along the path. + + EXAMPLES:: + + sage: NuDyckWord('010','010').heights() + [0, 0, 1, 1] + sage: NuDyckWord('110100','101010').heights() + [0, 1, 2, 2, 3, 3, 3] + sage: NuDyckWord('110111001101001000110111100011000', + ....: '101010101010101010101010101010101').heights() + [0, + 1, + 2, + 2, + 3, + 4, + 5, + 5, + 5, + 6, + 7, + 7, + 8, + 8, + 8, + 9, + 9, + 9, + 9, + 10, + 11, + 11, + 12, + 13, + 14, + 15, + 15, + 15, + 15, + 16, + 17, + 17, + 17, + 17] + """ + return self._path.height_vector() + + def widths(self): + r""" + Return the widths of each point on ``self``. + + We view the Dyck word as a Dyck path from `(0,0)` to + `(x,y)` in the first quadrant by letting ``1``'s represent + steps in the direction `(0,1)` and ``0``'s represent steps in + the direction `(1,0)`. + + The widths is the sequence of the `x`-coordinates of all + `x+y` lattice points along the path. + + EXAMPLES:: + + sage: NuDyckWord('010','010').widths() + [0, 1, 1, 2] + sage: NuDyckWord('110100','101010').widths() + [0, 0, 0, 1, 1, 2, 3] + sage: NuDyckWord('110111001101001000110111100011000', + ....: '101010101010101010101010101010101').widths() + [0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 3, + 3, + 3, + 4, + 4, + 5, + 6, + 6, + 7, + 8, + 9, + 9, + 9, + 10, + 10, + 10, + 10, + 10, + 11, + 12, + 13, + 13, + 13, + 14, + 15, + 16] + """ + return self._path.width_vector() + + def horizontal_distance(self): + r""" + Return a list of how far each point is from `\nu`. + + EXAMPLES:: + + sage: NDW = NuDyckWord('10010100','00000111') + sage: NDW.horizontal_distance() + [5, 5, 4, 3, 3, 2, 2, 1, 0] + sage: NDW = NuDyckWord('10010100','00001011') + sage: NDW.horizontal_distance() + [4, 5, 4, 3, 3, 2, 2, 1, 0] + sage: NDW = NuDyckWord('10011001000','00100101001') + sage: NDW.horizontal_distance() + [2, 4, 3, 2, 3, 5, 4, 3, 3, 2, 1, 0] + + + """ + # Grab furthest east point at each height of nu + nu_points = list(self._nu.points()) + nu_easts = [max([i for i, j in nu_points if j == k]) + for k in range(self._nu.height()+1)] + + points = list(self._path.points()) + return [nu_easts[j] - i for i, j in points] + + def can_mutate(self, i): + """ + Return True/False based off if mutatable at height `i`. + + Can only mutate if an east step is followed by a north step at height + `i`. + + OUTPUT: + + - Whether we can mutate at height of `i`. + """ + if i > self.height() or i <= 0: + raise ValueError('Can\'t mutate above or below path') + + # Find the ith north step + level = 0 + ndw = self._list() + for j, k in enumerate(ndw): + if k == ndw_open_symbol: + level += 1 + if level == i: + break + if j > 0 and ndw[j-1] == ndw_close_symbol: + return j + return False + + def mutate(self, i): + r""" + return a new `\nu`-Dyck Word if possible. + + We mutate ``dEfg`` to ``dfEg`` where: + + - d is everything up until EN + + - f is everything between N and where horiz is same + + - g is everything after + """ + mutation_index = self.can_mutate(i) + if not mutation_index: + return None + + horiz = self.horizontal_distance() + # Find horiz in between East and North Step + horiz_num = horiz[mutation_index] + other_index = len(horiz) + for i in range(mutation_index + 1, len(horiz)): + if horiz[i] == horiz_num: + other_index = i + break + ndw = self._list() + d = ndw[0:mutation_index-1] + e = ndw[mutation_index:other_index] + f = ndw[other_index:] + return NuDyckWord(d + e + [ndw_close_symbol] + f, self._nu) + + +class NuDyckWords(Parent): + r""" + `\nu`-Dyck words. + + Given a lattice path `\nu` in the `\ZZ^2` grid starting at the origin + `(0,0)` consisting of North `N = (0,1)` and East `E = (1,0)` steps, a + `\nu`-Dyck path is a lattice path in the`\ZZ^2` grid starting at the + origin `(0,0)` and ending at the same coordinate as `\nu` such that it is + weakly above `\nu`. A `\nu`-Dyck word is the representation of a + `\nu`-Dyck path where a North step is represented by a 1 and an East step + is represented by a 0. + + INPUT: + + - ``nu`` -- the base lattice path. + + EXAMPLES:: + + sage: NDW = NuDyckWords('1010'); NDW + [1, 0, 1, 0] Dyck words + sage: [1,0,1,0] in NDW + True + sage: [1,1,0,0] in NDW + True + sage: [1,0,0,1] in NDW + False + sage: [0,1,0,1] in NDW + False + sage: NDW.cardinality() + 2 + """ + + Element = NuDyckWord + + def __init__(self, nu=tuple()): + """ + Intialize ``self``. + + EXAMPLES:: + + sage: TestSuite(NuDyckWords(nu=[1,0,1])).run() + """ + Parent.__init__(self, category=FiniteEnumeratedSets()) + + self._nu = to_word_path(nu) + if self._nu is None: + raise ValueError("Invalud nu supplied") + + # add options to class + class options(GlobalOptions): + r""" + Set and display the options for `\nu`-Dyck words. If no parameters + are set, then the function returns a copy of the options dictionary. + + The ``options`` to `\nu`-Dyck words can be accessed as the method + :meth:`NuDyckWords.options` of :class:`NuDyckWords` and + related parent classes. + + @OPTIONS + + EXAMPLES:: + + sage: ND = NuDyckWords('101') + sage: ND + [1, 0, 1] Dyck words + sage: ND.options + Current options for NuDyckWords + - ascii_art: pretty_output + - diagram_style: grid + - display: list + - latex_color: black + - latex_line_width_scalar: 2 + - latex_nu_options: rounded corners=1, color=red, line width=1 + - latex_points_color: black + - latex_show_grid: True + - latex_show_nu: True + - latex_show_points: False + - latex_tikz_scale: 1 + """ + NAME = 'NuDyckWords' + module = 'sage.combinat.nu_dyck_path' + display = dict(default="list", + description='Specifies how nu Dyck words should be printed', + values=dict(list='displayed as a list', + lattice='displayed on the lattice defined by ``diagram_style``'), + case_sensitive=False) + ascii_art = dict(default="pretty_output", + description='Specifies how the ascii art of nu Dyck words should be printed', + values=dict(pretty_output="Using pretty printing"), + alias=dict(pretty_print="pretty_output",), + case_sensitive=False) + diagram_style = dict(default="grid", + values=dict( + grid='printing as paths on a grid using N and E steps',), + alias={'N-E': 'grid'}, + case_sensitive=False) + latex_tikz_scale = dict(default=1, + description='The default value for the tikz scale when latexed', + checker=lambda x: True) # More trouble than it's worth to check + latex_line_width_scalar = dict(default=2, + description='The default value for the line width as a ' + 'multiple of the tikz scale when latexed', + checker=lambda x: True) # More trouble than it's worth to check + latex_color = dict(default="black", + description='The default value for the color when latexed', + checker=lambda x: isinstance(x, str)) + latex_show_points = dict(default=False, + description='The default value for showing points', + checker=lambda x: isinstance(x, bool)) + latex_points_color = dict(default='black', + description='The default value for path color.', + checker=lambda x: isinstance(x, str)) + latex_show_grid = dict(default=True, + description='The default value for showing grid', + checker=lambda x: isinstance(x, bool)) + latex_show_nu = dict(default=True, + description='The default value for showing nu', + checker=lambda x: isinstance(x, bool)) + latex_nu_options = dict(default='rounded corners=1, color=red, line width=1', + description='The default value for options for nu path', + checker=lambda x: isinstance(x, str)) + + def _element_constructor_(self, word): + """ + Construct an element of ``self``. + + EXAMPLES:: + + sage: NDW = NuDyckWords('101') + sage: elt = NDW('110'); elt + [1, 1, 0] + sage: elt.parent() is NDW + True + + """ + if isinstance(word, NuDyckWord) and word.parent() is self: + return word + return self.element_class(self, to_word_path(word)) + + def __contains__(self, x): + r""" + TESTS:: + + sage: NDW = NuDyckWords([1,0,1,1]) + sage: [1,1,0,1] in NDW + True + sage: [1,0,1,1] in NDW + True + sage: [0] in NDW + False + sage: [1, 0] in NDW + False + """ + return path_weakly_above_other(to_word_path(x), self._nu) + + def __eq__(self, other): + """ + Return equality. + """ + if type(other) != type(self): + return False + return self._nu == other._nu + + def __neq__(self, other): + """ + Return inequality. + """ + return not self.__eq__(other) + + def _repr_(self): + r""" + TESTS:: + + sage: NuDyckWords([1,0,1,1]) + [1, 0, 1, 1] Dyck words + """ + return "{nu} Dyck words".format(nu=list(self._nu)) + + def _cache_key(self): + """ + Return a cache key + """ + return str(self._nu) + + def _an_element_(self): + r""" + TESTS:: + + sage: NuDyckWords('101').an_element() + [1, 0, 1] + + """ + return self.element_class(self, self._nu) + + def __iter__(self): + """ + Iterate over ``self``. + + EXAMPLES:: + + sage: it = NuDyckWords('101010').__iter__() + sage: [i for i in it] + [[1, 1, 1, 0, 0, 0], + [1, 1, 0, 1, 0, 0], + [1, 1, 0, 0, 1, 0], + [1, 0, 1, 1, 0, 0], + [1, 0, 1, 0, 1, 0]] + + """ + P = Permutations(list(self._nu)) + for p in P: + if path_weakly_above_other(to_word_path(p), self._nu): + yield self.element_class(self, list(p)) + + def cardinality(self): + r""" + Return the number of `\nu`-Dyck words. + + EXAMPLES:: + + sage: NDW = NuDyckWords('101010'); NDW.cardinality() + 5 + sage: NDW = NuDyckWords('1010010'); NDW.cardinality() + 7 + sage: NDW = NuDyckWords('100100100'); NDW.cardinality() + 12 + """ + i = 0 + for i, j in enumerate(self): + pass + return Integer(i + 1) + + +def to_word_path(word): + r""" + Helper function which converts input into a word path over an appropriate + alphabet. + + INPUT: + + - ``word`` -- word to convert to wordpath + + OUTPUT: + + - A ``FiniteWordPath_north_east`` object. + + EXAMPLES:: + + sage: from sage.combinat.nu_dyck_word import to_word_path + sage: wp = to_word_path('NEENENEN'); wp + Path: 10010101 + sage: from sage.combinat.words.paths import FiniteWordPath_north_east + sage: isinstance(wp,FiniteWordPath_north_east) + True + sage: to_word_path('1001') + Path: 1001 + sage: to_word_path([0,1,0,0,1,0]) + Path: 010010 + """ + + # If we already have the object, don't worry + if isinstance(word, FiniteWordPath_north_east): + return word + + # If we have a Nu Dyck Path, return the path it contains + if isinstance(word, NuDyckWord): + return word.path() + + # if we have a string, convert to list + if isinstance(word, str): + word = map(replace_dyck_char, word) + + # By default "north" is first symbol, "east" is second symbol + P = WordPaths_north_east([ndw_open_symbol, ndw_close_symbol]) + + return P(word) + + +def path_weakly_above_other(path, other): + r""" + Tests if ``path`` is weakly above ``other``. + + A path `P` is wealy above another path `Q` if `P` and `Q` are the same + length and if any prefix of length `n` of `Q` contains more North steps + than the prefix of length `n` of `P`. + + INPUT: + + - ``path`` -- The path to verify is weakly above the other path. + + - ``other`` -- The other path to verify is weakly below the path. + + OUTPUT: + + - bool + + EXAMPLES:: + + sage: from sage.combinat.nu_dyck_word import path_weakly_above_other + sage: path_weakly_above_other('1001','0110') + False + sage: path_weakly_above_other('1001','0101') + True + sage: path_weakly_above_other('1111','0101') + False + sage: path_weakly_above_other('111100','0101') + False + """ + # Ensure we have word paths: + path = to_word_path(path) + other = to_word_path(other) + + # Must be same length and must have same height + if path.length() != other.length() or path.height() != other.height(): + return False + + # path is above other if height is always >= height of other + p_height = path.height_vector() + o_height = other.height_vector() + for i, j in enumerate(p_height): + if(o_height[i] > j): + return False + return True + + +def is_nu_path(path, nu): + r""" + Test if ``path`` is a `\nu`-Dyck word. + + A ``path`` is a `\nu`-Dyck word if ``path`` is weakly above the path of + `\nu`. + + .. SEEALSO:: :meth:`path_weakly_above_other` + """ + return path_weakly_above_other(path, nu) diff --git a/src/sage/combinat/nu_tamari_lattice.py b/src/sage/combinat/nu_tamari_lattice.py new file mode 100644 index 00000000000..5762ba8aae2 --- /dev/null +++ b/src/sage/combinat/nu_tamari_lattice.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +r""" +`\nu`-Tamari lattice + +A class of the `\nu`-Tamari lattice, see [PRV2017]_ for details. + +These lattices depend on one parameter `\nu` where `\nu` is a path of North +and East steps. + +The elements are :func:`\nu-Dyck paths` +which are weakly above `\nu`. + +To use the provided functionality, you should import `\nu`-Tamari lattices by +typing:: + + sage: from sage.combinat.nu_tamari_lattice import NuTamariLattice + +Then, :: + + sage: NuTamariLattice([1,1,1,0,0,1,1,0]) + Finite lattice containing 6 elements + sage: NuTamariLattice([0,0,0,1,1,0,0,1]) + Finite lattice containing 40 elements + +The classical **Tamari lattices** and the **Generalized Tamari lattices** are +special cases of this construction and are also available with this poset:: + + sage: NuTamariLattice([1,0,1,0,1,0]) + Finite lattice containing 5 elements + + sage: NuTamariLattice([1,0,0,1,0,0,1,0,0]) + Finite lattice containing 12 elements + +.. SEEALSO:: + + For more detailed information see :meth:`NuTamariLattice`. For more + information on the standard Tamari lattice see + :meth:`sage.combinat.tamari_lattices.TamariLattice`, + :meth:`sage.combinat.tamar_lattices.GeneralizedTamariLattice` +""" +# **************************************************************************** +# Copyright (C) 2020-2020 Aram Dermenjian +# +# Distributed under the terms of the GNU General Public License (GPL) +# +# This code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# The full text of the GPL is available at: +# +# https://www.gnu.org/licenses/ +# **************************************************************************** +from sage.combinat.nu_dyck_word import NuDyckWords +from sage.combinat.posets.lattices import LatticePoset + + +def NuTamariLattice(nu): + r""" + Return the `\nu`-Tamari lattice. + + INPUT: + + - `\nu` -- a list of of 0s and 1s or a string of 0s and 1s. + + OUTPUT: + + - a finite lattice + + The elements of the lattice are + :func:`\nu-Dyck paths` weakly above + `\nu`. + + The usual :wikipedia:`Tamari lattice` is the special case + where `\nu = (NE)^h` where `h` is the height. + + Other special cases give the `m`-Tamari lattices studied in [BMFPR]_. + + EXAMPLES:: + + sage: from sage.combinat.nu_tamari_lattices import NuTamariLattice + sage: NuTamariLattice([1,0,1,0,0,1,0]) + Finite lattice containing 7 elements + sage: NuTamariLattice([1,0,1,0,1,0]) + Finite lattice containing 5 elements + sage: NuTamariLattice([1,0,1,0,1,0,1,0]) + Finite lattice containing 14 elements + sage: NuTamariLattice([1,0,1,0,1,0,0,0,1]) + Finite lattice containing 24 elements + + """ + + NDW = NuDyckWords(nu) + + covers = [] + elements = [] + height = NDW[0].height() + for ndw in NDW: + elements.append(ndw) + for i in range(1, height+1): + new_ndw = ndw.mutate(i) + if new_ndw is not None: + covers.append([ndw, new_ndw]) + return LatticePoset([elements, covers]) diff --git a/src/sage/combinat/words/paths.py b/src/sage/combinat/words/paths.py index 2a847391252..b0f0d8c1427 100644 --- a/src/sage/combinat/words/paths.py +++ b/src/sage/combinat/words/paths.py @@ -1781,6 +1781,25 @@ def height(self): """ return self.ymax() - self.ymin() + def height_vector(self): + r""" + Returns the height at each point. + """ + h_vec = [] + y_min = None + y_max = None + for (_, y) in self.points(): + if y_min is None: + y_min = y + y_max = y + else: + if y > y_max: + y_max = y + if y < y_min: + y_min = y + h_vec.append(y_max - y_min) + return h_vec + def width(self): r""" Returns the width of self. @@ -1822,6 +1841,25 @@ def width(self): """ return self.xmax() - self.xmin() + def width_vector(self): + r""" + Returns the width at each point. + """ + w_vec = [] + x_min = None + x_max = None + for (x, _) in self.points(): + if x_min is None: + x_min = x + x_max = x + else: + if x > x_max: + x_max = x + if x < x_min: + x_min = x + w_vec.append(x_max - x_min) + return w_vec + def xmin(self): r""" Returns the minimum of the x-coordinates of the path. From 1cdf03896235ad84c8b524d5d3293d20a06c763e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 7 May 2022 20:43:07 +0200 Subject: [PATCH 02/10] reviewer commit --- src/sage/combinat/nu_dyck_word.py | 177 +++++++------------------ src/sage/combinat/nu_tamari_lattice.py | 6 +- 2 files changed, 52 insertions(+), 131 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index 07b39d3e965..633ddf7bd44 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -8,12 +8,12 @@ - Aram Dermenjian (2020-09-26) -This file is based off the class ``DyckWords`` written by Mke Hansen, Dan +This file is based off the class +:func:`DyckWords` written by Mike Hansen, Dan Drake, Florent Hivert, Christian Stump, Mike Zabrocki, Jean--Baptiste Priez and Travis Scrimshaw """ - # **************************************************************************** # Copyright (C) 2020 Aram Dermenjian , # @@ -100,10 +100,9 @@ def replace_dyck_char(x): """ if x == '(' or x == 'N' or x == str(ndw_open_symbol): return ndw_open_symbol - elif x == ')' or x == 'E' or x == str(ndw_close_symbol): + if x == ')' or x == 'E' or x == str(ndw_close_symbol): return ndw_close_symbol - else: - raise ValueError + raise ValueError def replace_dyck_symbol(x, open_char='N', close_char='E'): @@ -148,10 +147,9 @@ def replace_dyck_symbol(x, open_char='N', close_char='E'): """ if x == ndw_open_symbol: return open_char - elif x == ndw_close_symbol: + if x == ndw_close_symbol: return close_char - else: - raise ValueError + raise ValueError class NuDyckWord(CombinatorialElement): @@ -225,7 +223,6 @@ def __classcall_private__(cls, dw=None, nu=None, **kwargs): sage: NuDyckWord('010','010') [0, 1, 0] """ - # if dw is none, then we might have a normal Dyck word if dw is None: from sage.combinat.dyck_word import DyckWord @@ -250,7 +247,7 @@ def __init__(self, parent, dw, latex_options={}): self._path = to_word_path(dw) if parent is None: - raise ValueError("Need Parent object") + raise ValueError("need parent object") self._nu = parent._nu @@ -460,7 +457,7 @@ def _repr_list(self): """ return str(list(self._path)) - def _repr_lattice(self, type=None, labelling=None): + def _repr_lattice(self, typ=None, labelling=None): r""" See :meth:`pretty_print()`. @@ -487,12 +484,12 @@ def _repr_lattice(self, type=None, labelling=None): """ - if type is None: - type = self.parent().options.diagram_style - if type == "grid": - type = "N-E" + if typ is None: + typ = self.parent().options.diagram_style + if typ == "grid": + typ = "N-E" - if type == "N-E": + if typ == "N-E": path_length = self._path.length() height = self._path.height() width = self._path.width() @@ -505,7 +502,7 @@ def _repr_lattice(self, type=None, labelling=None): else: if len(labelling) != height: raise ValueError( - "The given labelling has the wrong length. {num} needed".format(num=height)) + "the given labelling has the wrong length: {num} needed".format(num=height)) labels = [str(label) for label in labelling] max_length = max(len(label) for label in labels) labels = [lbl.rjust(max_length + 1) for lbl in labels] @@ -519,11 +516,11 @@ def _repr_lattice(self, type=None, labelling=None): cur_nu_pos = rev_nu_path.index(ndw_open_symbol) if cur_pos > 0: ts += " " * (width - cur_pos) - ts += " _" + "__" * (cur_pos-1) + ts += " _" + "__" * (cur_pos - 1) ts += "_\n" # Middle Lines - for i in range(height-1): + for i in range(height - 1): old_pos = cur_pos old_nu_pos = cur_nu_pos cur_pos = rev_path.index(ndw_open_symbol, cur_pos + 1) @@ -536,7 +533,7 @@ def _repr_lattice(self, type=None, labelling=None): if old_pos >= 0: ts += "x " * (old_pos - old_nu_pos) ts += " ." * (old_nu_pos - i) - ts += labels[height-i-1] + ts += labels[height - i - 1] ts += "\n" # Final line @@ -547,8 +544,7 @@ def _repr_lattice(self, type=None, labelling=None): ts += labels[0] ts += "\n" return ts - else: - raise ValueError("The given type (=%s) is not valid." % type) + raise ValueError(f"the given type (={typ}) is not valid") def _ascii_art_(self): r""" @@ -691,6 +687,7 @@ def _latex_(self): A latex representation of ``self`` using the tikzpicture package. EXAMPLES:: + sage: NDW = NuDyckWord('010','010') sage: NDW.set_latex_options({"show_points":True}) sage: latex(NDW) @@ -772,7 +769,7 @@ def plot(self, **kwds): def path(self): r""" - Return the underlying path object + Return the underlying path object. EXAMPLES:: @@ -826,7 +823,7 @@ def length(self): def points(self): r""" - Returns an iter with the points on the `\nu`-Dyck path. + Return an iterator for the points on the `\nu`-Dyck path. EXAMPLES:: @@ -887,42 +884,6 @@ def heights(self): [0, 0, 1, 1] sage: NuDyckWord('110100','101010').heights() [0, 1, 2, 2, 3, 3, 3] - sage: NuDyckWord('110111001101001000110111100011000', - ....: '101010101010101010101010101010101').heights() - [0, - 1, - 2, - 2, - 3, - 4, - 5, - 5, - 5, - 6, - 7, - 7, - 8, - 8, - 8, - 9, - 9, - 9, - 9, - 10, - 11, - 11, - 12, - 13, - 14, - 15, - 15, - 15, - 15, - 16, - 17, - 17, - 17, - 17] """ return self._path.height_vector() @@ -944,42 +905,6 @@ def widths(self): [0, 1, 1, 2] sage: NuDyckWord('110100','101010').widths() [0, 0, 0, 1, 1, 2, 3] - sage: NuDyckWord('110111001101001000110111100011000', - ....: '101010101010101010101010101010101').widths() - [0, - 0, - 0, - 1, - 1, - 1, - 1, - 2, - 3, - 3, - 3, - 4, - 4, - 5, - 6, - 6, - 7, - 8, - 9, - 9, - 9, - 10, - 10, - 10, - 10, - 10, - 11, - 12, - 13, - 13, - 13, - 14, - 15, - 16] """ return self._path.width_vector() @@ -998,20 +923,18 @@ def horizontal_distance(self): sage: NDW = NuDyckWord('10011001000','00100101001') sage: NDW.horizontal_distance() [2, 4, 3, 2, 3, 5, 4, 3, 3, 2, 1, 0] - - """ # Grab furthest east point at each height of nu - nu_points = list(self._nu.points()) - nu_easts = [max([i for i, j in nu_points if j == k]) - for k in range(self._nu.height()+1)] + nu_points = self._nu.points() + nu_easts = [max(i for i, j in nu_points if j == k) + for k in range(self._nu.height() + 1)] points = list(self._path.points()) return [nu_easts[j] - i for i, j in points] - def can_mutate(self, i): + def can_mutate(self, i) -> bool: """ - Return True/False based off if mutatable at height `i`. + Return True/False based off if mutable at height `i`. Can only mutate if an east step is followed by a north step at height `i`. @@ -1021,7 +944,7 @@ def can_mutate(self, i): - Whether we can mutate at height of `i`. """ if i > self.height() or i <= 0: - raise ValueError('Can\'t mutate above or below path') + raise ValueError('cannot mutate above or below path') # Find the ith north step level = 0 @@ -1031,21 +954,26 @@ def can_mutate(self, i): level += 1 if level == i: break - if j > 0 and ndw[j-1] == ndw_close_symbol: + if j > 0 and ndw[j - 1] == ndw_close_symbol: return j return False def mutate(self, i): r""" - return a new `\nu`-Dyck Word if possible. + Return a new `\nu`-Dyck Word if possible. - We mutate ``dEfg`` to ``dfEg`` where: + If at height `i` we have an east step E meeting a north step N then we + calculate all horizontal distances from this point until we find + the first point that has the same horizontal distance to `\nu`. We let - - d is everything up until EN + - d is everything up until EN (not including EN) - - f is everything between N and where horiz is same + - f be everything between N and the point with the same horizontal + distance (including N) - - g is everything after + - g is everything after f + + .. SEEALSO:: :meth:`can_mutate` """ mutation_index = self.can_mutate(i) if not mutation_index: @@ -1060,7 +988,7 @@ def mutate(self, i): other_index = i break ndw = self._list() - d = ndw[0:mutation_index-1] + d = ndw[0:mutation_index - 1] e = ndw[mutation_index:other_index] f = ndw[other_index:] return NuDyckWord(d + e + [ndw_close_symbol] + f, self._nu) @@ -1100,7 +1028,7 @@ class NuDyckWords(Parent): Element = NuDyckWord - def __init__(self, nu=tuple()): + def __init__(self, nu=()): """ Intialize ``self``. @@ -1112,7 +1040,7 @@ def __init__(self, nu=tuple()): self._nu = to_word_path(nu) if self._nu is None: - raise ValueError("Invalud nu supplied") + raise ValueError("invalid nu supplied") # add options to class class options(GlobalOptions): @@ -1199,13 +1127,12 @@ def _element_constructor_(self, word): [1, 1, 0] sage: elt.parent() is NDW True - """ if isinstance(word, NuDyckWord) and word.parent() is self: return word return self.element_class(self, to_word_path(word)) - def __contains__(self, x): + def __contains__(self, x) -> bool: r""" TESTS:: @@ -1225,7 +1152,7 @@ def __eq__(self, other): """ Return equality. """ - if type(other) != type(self): + if not isinstance(other, NuDyckWords): return False return self._nu == other._nu @@ -1256,7 +1183,6 @@ def _an_element_(self): sage: NuDyckWords('101').an_element() [1, 0, 1] - """ return self.element_class(self, self._nu) @@ -1273,10 +1199,8 @@ def __iter__(self): [1, 1, 0, 0, 1, 0], [1, 0, 1, 1, 0, 0], [1, 0, 1, 0, 1, 0]] - """ - P = Permutations(list(self._nu)) - for p in P: + for p in Permutations(list(self._nu)): if path_weakly_above_other(to_word_path(p), self._nu): yield self.element_class(self, list(p)) @@ -1293,9 +1217,7 @@ def cardinality(self): sage: NDW = NuDyckWords('100100100'); NDW.cardinality() 12 """ - i = 0 - for i, j in enumerate(self): - pass + i = len(list(self)) return Integer(i + 1) @@ -1325,7 +1247,6 @@ def to_word_path(word): sage: to_word_path([0,1,0,0,1,0]) Path: 010010 """ - # If we already have the object, don't worry if isinstance(word, FiniteWordPath_north_east): return word @@ -1344,9 +1265,9 @@ def to_word_path(word): return P(word) -def path_weakly_above_other(path, other): +def path_weakly_above_other(path, other) -> bool: r""" - Tests if ``path`` is weakly above ``other``. + Test if ``path`` is weakly above ``other``. A path `P` is wealy above another path `Q` if `P` and `Q` are the same length and if any prefix of length `n` of `Q` contains more North steps @@ -1360,7 +1281,7 @@ def path_weakly_above_other(path, other): OUTPUT: - - bool + bool EXAMPLES:: @@ -1391,7 +1312,7 @@ def path_weakly_above_other(path, other): return True -def is_nu_path(path, nu): +def is_nu_path(path, nu) -> bool: r""" Test if ``path`` is a `\nu`-Dyck word. diff --git a/src/sage/combinat/nu_tamari_lattice.py b/src/sage/combinat/nu_tamari_lattice.py index 5762ba8aae2..f6cb83bbd5b 100644 --- a/src/sage/combinat/nu_tamari_lattice.py +++ b/src/sage/combinat/nu_tamari_lattice.py @@ -36,7 +36,7 @@ For more detailed information see :meth:`NuTamariLattice`. For more information on the standard Tamari lattice see :meth:`sage.combinat.tamari_lattices.TamariLattice`, - :meth:`sage.combinat.tamar_lattices.GeneralizedTamariLattice` + :meth:`sage.combinat.tamari_lattices.GeneralizedTamariLattice` """ # **************************************************************************** # Copyright (C) 2020-2020 Aram Dermenjian @@ -79,7 +79,7 @@ def NuTamariLattice(nu): EXAMPLES:: - sage: from sage.combinat.nu_tamari_lattices import NuTamariLattice + sage: from sage.combinat.nu_tamari_lattice import NuTamariLattice sage: NuTamariLattice([1,0,1,0,0,1,0]) Finite lattice containing 7 elements sage: NuTamariLattice([1,0,1,0,1,0]) @@ -98,7 +98,7 @@ def NuTamariLattice(nu): height = NDW[0].height() for ndw in NDW: elements.append(ndw) - for i in range(1, height+1): + for i in range(1, height + 1): new_ndw = ndw.mutate(i) if new_ndw is not None: covers.append([ndw, new_ndw]) From 45ac1d12adf984889700b52357bc1288e131d07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 7 May 2022 21:22:13 +0200 Subject: [PATCH 03/10] adding doctests in words.paths --- src/sage/combinat/words/paths.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/sage/combinat/words/paths.py b/src/sage/combinat/words/paths.py index b0f0d8c1427..3a6ac3ef29b 100644 --- a/src/sage/combinat/words/paths.py +++ b/src/sage/combinat/words/paths.py @@ -1783,12 +1783,19 @@ def height(self): def height_vector(self): r""" - Returns the height at each point. + Return the height at each point. + + EXAMPLES:: + + sage: Paths = WordPaths('ab', steps=[(1,0),(0,1)]) + sage: p = Paths('abbba') + sage: p.height_vector() + [0, 0, 1, 2, 3, 3] """ h_vec = [] y_min = None y_max = None - for (_, y) in self.points(): + for _, y in self.points(): if y_min is None: y_min = y y_max = y @@ -1843,12 +1850,19 @@ def width(self): def width_vector(self): r""" - Returns the width at each point. + Return the width at each point. + + EXAMPLES:: + + sage: Paths = WordPaths('ab', steps=[(1,0),(0,1)]) + sage: p = Paths('abbba') + sage: p.width_vector() + [0, 1, 1, 1, 1, 2] """ w_vec = [] x_min = None x_max = None - for (x, _) in self.points(): + for x, _ in self.points(): if x_min is None: x_min = x x_max = x From 71c60d6fb5adcfbfe29db98837bedb458ae0c776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 7 May 2022 21:38:33 +0200 Subject: [PATCH 04/10] fix back the doctests --- src/sage/combinat/nu_dyck_word.py | 53 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index 633ddf7bd44..dad59c801b1 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -255,27 +255,27 @@ def __init__(self, parent, dw, latex_options={}): def _list(self): """ - Return list of self + Return list of ``self``. """ return list(self._path) def __eq__(self, other): """ - Return if two paths are equal + Return if two paths are equal. """ - if type(other) != type(self): + if not isinstance(other, NuDyckWord): return False return self._path == other._path and self._nu == other._nu def __neq__(self, other): """ - Return if two paths are not equal + Return if two paths are not equal. """ return not self.__eq__(other) def __le__(self, other): """ - Returns if one path is included in another + Return if one path is included in another. """ if self._nu == other._nu: return path_weakly_above_other(other._path, self._path) @@ -457,13 +457,13 @@ def _repr_list(self): """ return str(list(self._path)) - def _repr_lattice(self, typ=None, labelling=None): + def _repr_lattice(self, style=None, labelling=None): r""" See :meth:`pretty_print()`. TESTS:: - sage: print(NuDyckWord('00011001000100','00011001000100')._repr_lattice(type='N-E', labelling=[1,2,3,4])) + sage: print(NuDyckWord('00011001000100','00011001000100')._repr_lattice(style='N-E', labelling=[1,2,3,4])) ____ _____| . . 4 ___| . . . . . 3 @@ -481,15 +481,13 @@ def _repr_lattice(self, typ=None, labelling=None): ___|x |x x x |x x . - - """ - if typ is None: - typ = self.parent().options.diagram_style - if typ == "grid": - typ = "N-E" + if style is None: + style = self.parent().options.diagram_style + if style == "grid": + style = "N-E" - if typ == "N-E": + if style == "N-E": path_length = self._path.length() height = self._path.height() width = self._path.width() @@ -501,8 +499,7 @@ def _repr_lattice(self, typ=None, labelling=None): labels = [" "] * height else: if len(labelling) != height: - raise ValueError( - "the given labelling has the wrong length: {num} needed".format(num=height)) + raise ValueError(f"the given labelling has the wrong length: {height} needed") labels = [str(label) for label in labelling] max_length = max(len(label) for label in labels) labels = [lbl.rjust(max_length + 1) for lbl in labels] @@ -544,7 +541,7 @@ def _repr_lattice(self, typ=None, labelling=None): ts += labels[0] ts += "\n" return ts - raise ValueError(f"the given type (={typ}) is not valid") + raise ValueError(f"the given style (={style}) is not valid") def _ascii_art_(self): r""" @@ -580,11 +577,11 @@ def __str__(self): """ return "".join(map(replace_dyck_symbol, list(self._path))) - def pretty_print(self, type=None, labelling=None): + def pretty_print(self, style=None, labelling=None): r""" Display a NuDyckWord as a lattice path in the `\ZZ^2` grid. - If the ``type`` is "N-E", then a cell below the diagonal is + If the ``style`` is "N-E", then a cell below the diagonal is indicated by a period, whereas a cell below the path but above the diagonal is indicated by an x. If a list of labels is included, they are displayed along the vertical edges of the @@ -592,15 +589,15 @@ def pretty_print(self, type=None, labelling=None): INPUT: - - ``type`` -- (default: ``None``) can either be: + - ``style`` -- (default: ``None``) can either be: - ``None`` to use the option default - "N-E" to show ``self`` as a path of north and east steps, or - - ``labelling`` -- (if type is "N-E") a list of labels assigned to + - ``labelling`` -- (if style is "N-E") a list of labels assigned to the up steps in ``self``. - - ``underpath`` -- (if type is "N-E", default: ``True``) If ``True``, + - ``underpath`` -- (if style is "N-E", default: ``True``) If ``True``, an ``x`` to show the boxes between `\nu` and the `\nu`-Dyck Path. EXAMPLES:: @@ -678,7 +675,7 @@ def pretty_print(self, type=None, labelling=None): sage: NuDyckWord().pretty_print() . """ - print(self._repr_lattice(type, labelling)) + print(self._repr_lattice(style, labelling)) pp = pretty_print @@ -925,7 +922,7 @@ def horizontal_distance(self): [2, 4, 3, 2, 3, 5, 4, 3, 3, 2, 1, 0] """ # Grab furthest east point at each height of nu - nu_points = self._nu.points() + nu_points = list(self._nu.points()) nu_easts = [max(i for i, j in nu_points if j == k) for k in range(self._nu.height() + 1)] @@ -1217,8 +1214,7 @@ def cardinality(self): sage: NDW = NuDyckWords('100100100'); NDW.cardinality() 12 """ - i = len(list(self)) - return Integer(i + 1) + return Integer(len([1 for _ in self.__iter__()])) def to_word_path(word): @@ -1306,10 +1302,7 @@ def path_weakly_above_other(path, other) -> bool: # path is above other if height is always >= height of other p_height = path.height_vector() o_height = other.height_vector() - for i, j in enumerate(p_height): - if(o_height[i] > j): - return False - return True + return all(p_h >= o_h for p_h, o_h in zip(p_height, o_height)) def is_nu_path(path, nu) -> bool: From c7fa2b09d7966b13a20c84443e1d4ce3e069fbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sun, 8 May 2022 09:46:06 +0200 Subject: [PATCH 05/10] add some doctests and type annotations --- src/sage/combinat/nu_dyck_word.py | 86 ++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index dad59c801b1..71b459c4b10 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -28,7 +28,7 @@ # # https://www.gnu.org/licenses/ # **************************************************************************** - +from __future__ import annotations from sage.structure.element import Element from sage.rings.integer import Integer from sage.combinat.combinat import CombinatorialElement @@ -105,7 +105,7 @@ def replace_dyck_char(x): raise ValueError -def replace_dyck_symbol(x, open_char='N', close_char='E'): +def replace_dyck_symbol(x, open_char='N', close_char='E') -> str: r""" A map sending ``ndw_open_symbol`` to ``open_char`` and ``ndw_close_symbol`` to ``close_char``, and raising an error on any input other than @@ -179,25 +179,17 @@ class NuDyckWord(CombinatorialElement): sage: dw.height() 2 - :: - sage: dw = NuDyckWord('1010',[1,0,0,1]); dw [1, 0, 1, 0] - :: - sage: dw = NuDyckWord('NENE',[1,0,0,1]); dw [1, 0, 1, 0] - :: - sage: NuDyckWord([1,0,1,0],[1,0,0,1]).pretty_print() __ _|x | . . - :: - sage: from sage.combinat.nu_dyck_word import update_ndw_symbols sage: update_ndw_symbols(0,1) sage: dw = NuDyckWord('0101001','0110010'); dw @@ -209,7 +201,6 @@ class NuDyckWord(CombinatorialElement): _|x . | . . . sage: update_ndw_symbols(1,0) - """ @staticmethod def __classcall_private__(cls, dw=None, nu=None, **kwargs): @@ -242,7 +233,7 @@ def __classcall_private__(cls, dw=None, nu=None, **kwargs): raise ValueError("invalid nu-Dyck word") - def __init__(self, parent, dw, latex_options={}): + def __init__(self, parent, dw, latex_options=None): Element.__init__(self, parent) self._path = to_word_path(dw) @@ -251,17 +242,36 @@ def __init__(self, parent, dw, latex_options={}): self._nu = parent._nu + if latex_options is None: + latex_options = {} self._latex_options = dict(latex_options) def _list(self): """ Return list of ``self``. + + EXAMPLES:: + + sage: w = NuDyckWord('110100','101010') + sage: w._list() + [1, 1, 0, 1, 0, 0] """ return list(self._path) def __eq__(self, other): """ Return if two paths are equal. + + EXAMPLES:: + + sage: u = NuDyckWord('010','010') + sage: w = NuDyckWord('110100','101010') + sage: w == w + True + sage: u == w + False + sage: u == 4 + False """ if not isinstance(other, NuDyckWord): return False @@ -270,6 +280,17 @@ def __eq__(self, other): def __neq__(self, other): """ Return if two paths are not equal. + + EXAMPLES:: + + sage: u = NuDyckWord('010','010') + sage: w = NuDyckWord('110100','101010') + sage: w != w + False + sage: u != w + True + sage: u != 4 + True """ return not self.__eq__(other) @@ -283,13 +304,13 @@ def __le__(self, other): def __lt__(self, other): """ - Returns if one path is strictly included in another + Return if one path is strictly included in another """ return self.__le__(other) and not self.__eq__(other) def __ge__(self, other): """ - Returns if one path is included in another + Return if one path is included in another """ if self._nu == other._nu: return path_weakly_above_other(self._path, other._path) @@ -297,18 +318,33 @@ def __ge__(self, other): def __gt__(self, other): """ - Returns if one path is strictly included in another + Return if one path is strictly included in another """ return self.__ge__(other) and not self.__eq__(other) - def _cache_key(self): + def _cache_key(self) -> tuple: """ - Return a cache key + Return a cache key for ``self``. + + EXAMPLES:: + + sage: u = NuDyckWord('010','010') + sage: u._cache_key() + (0, 1, 0, 0, 1, 0) + """ + return tuple(self._path) + tuple(self._nu) + + def __hash__(self) -> int: """ - return str(self._path) + "-" + str(self._nu) + Return a hash for ``self``. - def __hash__(self): - return hash(''.join([str(i) for i in self._list()])) + EXAMPLES:: + + sage: u = NuDyckWord('010','010') + sage: hash(u) # random + -4577085166836515071 + """ + return hash(''.join(str(i) for i in self._list())) def set_latex_options(self, D): r""" @@ -356,7 +392,7 @@ def set_latex_options(self, D): for opt in D: self._latex_options[opt] = D[opt] - def latex_options(self): + def latex_options(self) -> dict: r""" Return the latex options for use in the ``_latex_`` function as a dictionary. @@ -423,7 +459,7 @@ def latex_options(self): d["nu_options"] = opts.latex_nu_options return d - def _repr_(self): + def _repr_(self) -> str: r""" Return a string representation of ``self`` depending on :meth:`NuDyckWords.options`. @@ -444,7 +480,7 @@ def _repr_(self): """ return self.parent().options._dispatch(self, '_repr_', 'display') - def _repr_list(self): + def _repr_list(self) -> str: r""" Return a string representation of ``self`` as a list. @@ -563,7 +599,7 @@ def _ascii_art_(self): ret = self._repr_lattice() return AsciiArt(ret.splitlines(), baseline=0) - def __str__(self): + def __str__(self) -> str: r""" Return a string consisting of N and E steps corresponding to the `\nu`-Dyck word. @@ -575,7 +611,7 @@ def __str__(self): sage: str(NuDyckWord('101010','100110')) 'NENENE' """ - return "".join(map(replace_dyck_symbol, list(self._path))) + return "".join(replace_dyck_symbol(let) for let in self._path) def pretty_print(self, style=None, labelling=None): r""" From 3d170c96c918007ed6216ae7810e3341e1b4402f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sun, 15 May 2022 08:46:34 +0200 Subject: [PATCH 06/10] adding more doctests --- src/sage/combinat/nu_dyck_word.py | 98 +++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index 71b459c4b10..e8fdb669416 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -47,12 +47,20 @@ def update_ndw_symbols(os, cs): r""" - A way to alter the open and close symbols from sage + A way to alter the open and close symbols from sage. INPUT: - - ``os`` -- The open symbol - - ``cs`` -- The close symbol + - ``os`` -- the open symbol + - ``cs`` -- the close symbol + + EXAMPLES:: + + sage: from sage.combinat.nu_dyck_word import update_ndw_symbols + sage: update_ndw_symbols(0,1) + sage: dw = NuDyckWord('0101001','0110010'); dw + [0, 1, 0, 1, 0, 0, 1] + sage: update_ndw_symbols(1,0) """ global ndw_open_symbol global ndw_close_symbol @@ -234,6 +242,9 @@ def __classcall_private__(cls, dw=None, nu=None, **kwargs): raise ValueError("invalid nu-Dyck word") def __init__(self, parent, dw, latex_options=None): + """ + Initialize a nu-Dyck word. + """ Element.__init__(self, parent) self._path = to_word_path(dw) @@ -486,7 +497,7 @@ def _repr_list(self) -> str: TESTS:: - sage: NuDyckWord([1,1,0],[1,0,1]) + sage: NuDyckWord([1,1,0],[1,0,1]) # indirect doctest [1, 1, 0] sage: NuDyckWord('NNEE','NENE') [1, 1, 0, 0] @@ -965,7 +976,7 @@ def horizontal_distance(self): points = list(self._path.points()) return [nu_easts[j] - i for i, j in points] - def can_mutate(self, i) -> bool: + def can_mutate(self, i) -> bool | int: """ Return True/False based off if mutable at height `i`. @@ -974,7 +985,23 @@ def can_mutate(self, i) -> bool: OUTPUT: - - Whether we can mutate at height of `i`. + Whether we can mutate at height of `i`. + + EXAMPLES:: + + sage: NDW = NuDyckWord('10010100','00000111') + sage: NDW.can_mutate(1) + False + sage: NDW.can_mutate(3) + 5 + + TESTS:: + + sage: NDW = NuDyckWord('10010100','00000111') + sage: NDW.can_mutate(33) + Traceback (most recent call last): + ... + ValueError: cannot mutate above or below path """ if i > self.height() or i <= 0: raise ValueError('cannot mutate above or below path') @@ -991,7 +1018,7 @@ def can_mutate(self, i) -> bool: return j return False - def mutate(self, i): + def mutate(self, i) -> None | NuDyckWord: r""" Return a new `\nu`-Dyck Word if possible. @@ -1007,6 +1034,13 @@ def mutate(self, i): - g is everything after f .. SEEALSO:: :meth:`can_mutate` + + EXAMPLES:: + + sage: NDW = NuDyckWord('10010100','00000111') + sage: NDW.mutate(1) + sage: NDW.mutate(3) + [1, 0, 0, 1, 1, 0, 0, 0] """ mutation_index = self.can_mutate(i) if not mutation_index: @@ -1184,6 +1218,16 @@ def __contains__(self, x) -> bool: def __eq__(self, other): """ Return equality. + + TESTS:: + + sage: A = NuDyckWords([1,0,1,1]) + sage: B = NuDyckWords([1,0,1,1]) + sage: C = NuDyckWords([1,0,1,1,1]) + sage: A == B + True + sage: A == C + False """ if not isinstance(other, NuDyckWords): return False @@ -1192,21 +1236,36 @@ def __eq__(self, other): def __neq__(self, other): """ Return inequality. + + TESTS:: + + sage: A = NuDyckWords([1,0,1,1]) + sage: B = NuDyckWords([1,0,1,1]) + sage: C = NuDyckWords([1,0,1,1,1]) + sage: A != B + False + sage: A != C + True """ return not self.__eq__(other) - def _repr_(self): + def _repr_(self) -> str: r""" TESTS:: sage: NuDyckWords([1,0,1,1]) [1, 0, 1, 1] Dyck words """ - return "{nu} Dyck words".format(nu=list(self._nu)) + return f"{list(self._nu)} Dyck words" - def _cache_key(self): + def _cache_key(self) -> str: """ Return a cache key + + TESTS:: + + sage: NuDyckWords([1,0,1,1])._cache_key() + '1011' """ return str(self._nu) @@ -1223,6 +1282,8 @@ def __iter__(self): """ Iterate over ``self``. + .. TODO:: make a better iterator. + EXAMPLES:: sage: it = NuDyckWords('101010').__iter__() @@ -1255,8 +1316,9 @@ def cardinality(self): def to_word_path(word): r""" - Helper function which converts input into a word path over an appropriate - alphabet. + Convert input into a word path over an appropriate alphabet. + + Helper function. INPUT: @@ -1339,15 +1401,3 @@ def path_weakly_above_other(path, other) -> bool: p_height = path.height_vector() o_height = other.height_vector() return all(p_h >= o_h for p_h, o_h in zip(p_height, o_height)) - - -def is_nu_path(path, nu) -> bool: - r""" - Test if ``path`` is a `\nu`-Dyck word. - - A ``path`` is a `\nu`-Dyck word if ``path`` is weakly above the path of - `\nu`. - - .. SEEALSO:: :meth:`path_weakly_above_other` - """ - return path_weakly_above_other(path, nu) From b6c2839696f4bd78d7ce647e1b8c799f91fa1aff Mon Sep 17 00:00:00 2001 From: Aram Dermenjian Date: Tue, 17 May 2022 09:38:02 +0100 Subject: [PATCH 07/10] Add more doctests and make iterator more efficient --- src/sage/combinat/nu_dyck_word.py | 201 ++++++++++++++++++++++++------ 1 file changed, 162 insertions(+), 39 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index e8fdb669416..baf5f9b830e 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -35,8 +35,8 @@ from sage.structure.global_options import GlobalOptions from sage.structure.parent import Parent from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets -from sage.combinat.permutation import Permutations from sage.misc.latex import latex +from copy import copy from sage.combinat.words.paths import WordPaths_north_east from sage.combinat.words.paths import FiniteWordPath_north_east @@ -60,6 +60,11 @@ def update_ndw_symbols(os, cs): sage: update_ndw_symbols(0,1) sage: dw = NuDyckWord('0101001','0110010'); dw [0, 1, 0, 1, 0, 0, 1] + + sage: dw = NuDyckWord('1010110','1001101'); dw + Traceback (most recent call last): + ... + ValueError: invalid nu-Dyck word sage: update_ndw_symbols(1,0) """ global ndw_open_symbol @@ -71,9 +76,9 @@ def update_ndw_symbols(os, cs): def replace_dyck_char(x): r""" A map sending an opening character (``'1'``, ``'N'``, and ``'('``) to - ``ndw_open_symbol`` and a closing character (``'0'``, ``'E'``, and ``')'``) to - ``ndw_close_symbol``, and raising an error on any input other than one of the - opening or closing characters. + ``ndw_open_symbol``, a closing character (``'0'``, ``'E'``, and ``')'``) to + ``ndw_close_symbol``, and raising an error on any input other than one of + the opening or closing characters. This is the inverse map of :func:`replace_dyck_symbol`. @@ -115,8 +120,8 @@ def replace_dyck_char(x): def replace_dyck_symbol(x, open_char='N', close_char='E') -> str: r""" - A map sending ``ndw_open_symbol`` to ``open_char`` and ``ndw_close_symbol`` to - ``close_char``, and raising an error on any input other than + A map sending ``ndw_open_symbol`` to ``open_char`` and ``ndw_close_symbol`` + to ``close_char``, and raising an error on any input other than ``ndw_open_symbol`` and ``ndw_close_symbol``. The values of the constants ``ndw_open_symbol`` and ``ndw_close_symbol`` are subject to change. @@ -244,6 +249,13 @@ def __classcall_private__(cls, dw=None, nu=None, **kwargs): def __init__(self, parent, dw, latex_options=None): """ Initialize a nu-Dyck word. + + EXAMPLES:: + + sage: NuDyckWord('010', '010') + [0, 1, 0] + sage: NuDyckWord('110100','101010') + [1, 1, 0, 1, 0, 0] """ Element.__init__(self, parent) self._path = to_word_path(dw) @@ -308,6 +320,23 @@ def __neq__(self, other): def __le__(self, other): """ Return if one path is included in another. + + EXAMPLES:: + + sage: ND1 = NuDyckWord('101', '011') + sage: ND2 = NuDyckWord('110', '011') + sage: ND3 = NuDyckWord('011', '011') + sage: ND1 <= ND1 + True + sage: ND1 <= ND2 + True + sage: ND2 <= ND1 + False + sage: ND3 <= ND2 + True + sage: ND3 <= ND1 + True + """ if self._nu == other._nu: return path_weakly_above_other(other._path, self._path) @@ -316,12 +345,44 @@ def __le__(self, other): def __lt__(self, other): """ Return if one path is strictly included in another + + EXAMPLES:: + + sage: ND1 = NuDyckWord('101', '011') + sage: ND2 = NuDyckWord('110', '011') + sage: ND3 = NuDyckWord('011', '011') + sage: ND1 < ND1 + False + sage: ND1 < ND2 + True + sage: ND2 < ND1 + False + sage: ND3 < ND2 + True + sage: ND3 < ND1 + True """ return self.__le__(other) and not self.__eq__(other) def __ge__(self, other): """ Return if one path is included in another + + EXAMPLES:: + + sage: ND1 = NuDyckWord('101', '011') + sage: ND2 = NuDyckWord('110', '011') + sage: ND3 = NuDyckWord('011', '011') + sage: ND1 >= ND1 + True + sage: ND1 >= ND2 + False + sage: ND2 >= ND1 + True + sage: ND1 >= ND3 + True + sage: ND2 >= ND3 + True """ if self._nu == other._nu: return path_weakly_above_other(self._path, other._path) @@ -330,6 +391,22 @@ def __ge__(self, other): def __gt__(self, other): """ Return if one path is strictly included in another + + EXAMPLES:: + + sage: ND1 = NuDyckWord('101', '011') + sage: ND2 = NuDyckWord('110', '011') + sage: ND3 = NuDyckWord('011', '011') + sage: ND1 > ND1 + False + sage: ND1 > ND2 + False + sage: ND2 > ND1 + True + sage: ND1 > ND3 + True + sage: ND2 > ND3 + True """ return self.__ge__(other) and not self.__eq__(other) @@ -365,8 +442,8 @@ def set_latex_options(self, D): - ``color`` -- (default: black) the line color. - - ``line width`` -- (default: `2 \times` ``tikz_scale``) value representing the - line width. + - ``line width`` -- (default: `2 \times` ``tikz_scale``) value + representing the line width. - ``nu_options`` -- (default: ``'rounded corners=1, color=red, line width=1'``) str to indicate what the tikz options should be for path @@ -510,7 +587,8 @@ def _repr_lattice(self, style=None, labelling=None): TESTS:: - sage: print(NuDyckWord('00011001000100','00011001000100')._repr_lattice(style='N-E', labelling=[1,2,3,4])) + sage: n = NuDyckWord('00011001000100','00011001000100') + sage: print(n._repr_lattice(style='N-E', labelling=[1,2,3,4])) ____ _____| . . 4 ___| . . . . . 3 @@ -650,30 +728,31 @@ def pretty_print(self, style=None, labelling=None): EXAMPLES:: sage: for ND in NuDyckWords('101010'): ND.pretty_print() - ______ - |x x . - |x . . + __ + _| . + _| . . | . . . ____ - _|x . - |x . . + |x . + _| . . | . . . __ ___| . |x . . | . . . ____ - |x . - _| . . + _|x . + |x . . | . . . - __ - _| . - _| . . + ______ + |x x . + |x . . | . . . :: - sage: ND = NuDyckWord([1,1,1,0,1,0,0,1,1,0,0,0],[1,0,1,0,1,0,1,0,1,0,1,0]) + sage: nu = [1,0,1,0,1,0,1,0,1,0,1,0] + sage: ND = NuDyckWord([1,1,1,0,1,0,0,1,1,0,0,0],nu) sage: ND.pretty_print() ______ |x x . @@ -1201,6 +1280,8 @@ def _element_constructor_(self, word): def __contains__(self, x) -> bool: r""" + Checks for containment. + TESTS:: sage: NDW = NuDyckWords([1,0,1,1]) @@ -1271,6 +1352,8 @@ def _cache_key(self) -> str: def _an_element_(self): r""" + Returns an element. + TESTS:: sage: NuDyckWords('101').an_element() @@ -1278,25 +1361,65 @@ def _an_element_(self): """ return self.element_class(self, self._nu) - def __iter__(self): + def __iter__(self, N=[], D=[], i=None, X=None): """ Iterate over ``self``. - .. TODO:: make a better iterator. + The iterator procedes by letting `N` be the locations of all the north + steps in `\nu`. Then we proceed by decreasing the locations as much as + we can from right to left. EXAMPLES:: sage: it = NuDyckWords('101010').__iter__() sage: [i for i in it] - [[1, 1, 1, 0, 0, 0], - [1, 1, 0, 1, 0, 0], - [1, 1, 0, 0, 1, 0], + [[1, 0, 1, 0, 1, 0], [1, 0, 1, 1, 0, 0], - [1, 0, 1, 0, 1, 0]] + [1, 1, 0, 0, 1, 0], + [1, 1, 0, 1, 0, 0], + [1, 1, 1, 0, 0, 0]] """ - for p in Permutations(list(self._nu)): - if path_weakly_above_other(to_word_path(p), self._nu): - yield self.element_class(self, list(p)) + # Location of all the ones + N = [k for k, v in enumerate(list(self._nu)) if v == 1] + + # Our first return element + D = copy(N) + + # Lambda function for adding a one in the appropriate spot + def X(i, N): return 1 if i in N else 0 + + # i is the final entry + i = len(N) - 1 + + # j is our iterator + j = i + + # return nu first + nu = [X(i, D) for i in range(self._nu.length())] + yield self.element_class(self, nu) + + # While we haven't reached the start of our list + while j >= 0: + # If the final number is more than 1 away from second to last + if D[i] > D[i-1] + 1: + D[i] -= 1 + DW = [X(i, D) for i in range(self._nu.length())] + yield self.element_class(self, DW) + else: + # reset j just in case + j = i + # find the last index where two numbers are more than 1 away. + while j > 0 and D[j] == D[j - 1] + 1: + j -= 1 + if j > 0 or (j == 0 and D[j] > 0): + D[j] -= 1 + # reset all the entries above j + for k in range(j + 1, len(N)): + D[k] = N[k] + DW = [X(i, D) for i in range(self._nu.length())] + yield self.element_class(self, DW) + else: + j = -1 def cardinality(self): r""" @@ -1330,16 +1453,16 @@ def to_word_path(word): EXAMPLES:: - sage: from sage.combinat.nu_dyck_word import to_word_path - sage: wp = to_word_path('NEENENEN'); wp - Path: 10010101 - sage: from sage.combinat.words.paths import FiniteWordPath_north_east - sage: isinstance(wp,FiniteWordPath_north_east) - True - sage: to_word_path('1001') - Path: 1001 - sage: to_word_path([0,1,0,0,1,0]) - Path: 010010 + sage: from sage.combinat.nu_dyck_word import to_word_path + sage: wp = to_word_path('NEENENEN'); wp + Path: 10010101 + sage: from sage.combinat.words.paths import FiniteWordPath_north_east + sage: isinstance(wp,FiniteWordPath_north_east) + True + sage: to_word_path('1001') + Path: 1001 + sage: to_word_path([0,1,0,0,1,0]) + Path: 010010 """ # If we already have the object, don't worry if isinstance(word, FiniteWordPath_north_east): From ec793fc9533278b310eeecb8fee4b5946b594be4 Mon Sep 17 00:00:00 2001 From: Aram Dermenjian Date: Tue, 17 May 2022 11:36:58 +0100 Subject: [PATCH 08/10] Fix doc to use imperative --- src/sage/combinat/nu_dyck_word.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index baf5f9b830e..38521334568 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -1280,7 +1280,7 @@ def _element_constructor_(self, word): def __contains__(self, x) -> bool: r""" - Checks for containment. + Check for containment. TESTS:: @@ -1352,7 +1352,7 @@ def _cache_key(self) -> str: def _an_element_(self): r""" - Returns an element. + Return an element. TESTS:: From e3925245f576144eff5a546f36592ee1c83a6687 Mon Sep 17 00:00:00 2001 From: Aram Dermenjian Date: Tue, 17 May 2022 14:41:39 +0100 Subject: [PATCH 09/10] Make iterator a recursively enumerated set --- src/sage/combinat/nu_dyck_word.py | 91 +++++++++---------------------- 1 file changed, 26 insertions(+), 65 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index 38521334568..503b1e00fe5 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -32,11 +32,11 @@ from sage.structure.element import Element from sage.rings.integer import Integer from sage.combinat.combinat import CombinatorialElement +from sage.sets.recursively_enumerated_set import RecursivelyEnumeratedSet from sage.structure.global_options import GlobalOptions from sage.structure.parent import Parent from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets from sage.misc.latex import latex -from copy import copy from sage.combinat.words.paths import WordPaths_north_east from sage.combinat.words.paths import FiniteWordPath_north_east @@ -259,6 +259,7 @@ def __init__(self, parent, dw, latex_options=None): """ Element.__init__(self, parent) self._path = to_word_path(dw) + self._list = list(self._path) if parent is None: raise ValueError("need parent object") @@ -269,18 +270,6 @@ def __init__(self, parent, dw, latex_options=None): latex_options = {} self._latex_options = dict(latex_options) - def _list(self): - """ - Return list of ``self``. - - EXAMPLES:: - - sage: w = NuDyckWord('110100','101010') - sage: w._list() - [1, 1, 0, 1, 0, 0] - """ - return list(self._path) - def __eq__(self, other): """ Return if two paths are equal. @@ -432,7 +421,7 @@ def __hash__(self) -> int: sage: hash(u) # random -4577085166836515071 """ - return hash(''.join(str(i) for i in self._list())) + return hash(''.join(str(i) for i in self._list)) def set_latex_options(self, D): r""" @@ -731,14 +720,14 @@ def pretty_print(self, style=None, labelling=None): __ _| . _| . . - | . . . - ____ - |x . - _| . . | . . . __ ___| . |x . . + | . . . + ____ + |x . + _| . . | . . . ____ _|x . @@ -1087,7 +1076,7 @@ def can_mutate(self, i) -> bool | int: # Find the ith north step level = 0 - ndw = self._list() + ndw = self._list for j, k in enumerate(ndw): if k == ndw_open_symbol: level += 1 @@ -1133,7 +1122,7 @@ def mutate(self, i) -> None | NuDyckWord: if horiz[i] == horiz_num: other_index = i break - ndw = self._list() + ndw = self._list d = ndw[0:mutation_index - 1] e = ndw[mutation_index:other_index] f = ndw[other_index:] @@ -1365,61 +1354,33 @@ def __iter__(self, N=[], D=[], i=None, X=None): """ Iterate over ``self``. - The iterator procedes by letting `N` be the locations of all the north - steps in `\nu`. Then we proceed by decreasing the locations as much as - we can from right to left. + The iterator interchanges a 0,1 pair whenever the 0 comes before a 1 EXAMPLES:: sage: it = NuDyckWords('101010').__iter__() sage: [i for i in it] [[1, 0, 1, 0, 1, 0], - [1, 0, 1, 1, 0, 0], [1, 1, 0, 0, 1, 0], + [1, 0, 1, 1, 0, 0], [1, 1, 0, 1, 0, 0], [1, 1, 1, 0, 0, 0]] """ - # Location of all the ones - N = [k for k, v in enumerate(list(self._nu)) if v == 1] - - # Our first return element - D = copy(N) - - # Lambda function for adding a one in the appropriate spot - def X(i, N): return 1 if i in N else 0 - - # i is the final entry - i = len(N) - 1 - - # j is our iterator - j = i - - # return nu first - nu = [X(i, D) for i in range(self._nu.length())] - yield self.element_class(self, nu) - - # While we haven't reached the start of our list - while j >= 0: - # If the final number is more than 1 away from second to last - if D[i] > D[i-1] + 1: - D[i] -= 1 - DW = [X(i, D) for i in range(self._nu.length())] - yield self.element_class(self, DW) - else: - # reset j just in case - j = i - # find the last index where two numbers are more than 1 away. - while j > 0 and D[j] == D[j - 1] + 1: - j -= 1 - if j > 0 or (j == 0 and D[j] > 0): - D[j] -= 1 - # reset all the entries above j - for k in range(j + 1, len(N)): - D[k] = N[k] - DW = [X(i, D) for i in range(self._nu.length())] - yield self.element_class(self, DW) - else: - j = -1 + # Define successor function for recursion + def transpose_close_open(N): + return [self.element_class(self, + N._list[0:k-1] + + list(reversed(N._list[k-1:k+1])) + + N._list[k+1:]) + for k, v in enumerate(N._list) + if k > 0 + and v == ndw_open_symbol + and N._list[k-1] == ndw_close_symbol + ] + + RES = RecursivelyEnumeratedSet([self.element_class(self, self._nu)], + transpose_close_open) + return RES.breadth_first_search_iterator() def cardinality(self): r""" From 8de8827ed0fd5a864c9e0b99da7016be8cf4e279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Mon, 6 Jun 2022 08:41:16 +0200 Subject: [PATCH 10/10] a few more tweaks in nu-Tamari --- src/sage/combinat/nu_dyck_word.py | 15 ++++++--------- src/sage/combinat/nu_tamari_lattice.py | 5 +---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/sage/combinat/nu_dyck_word.py b/src/sage/combinat/nu_dyck_word.py index 503b1e00fe5..b319f1326fe 100644 --- a/src/sage/combinat/nu_dyck_word.py +++ b/src/sage/combinat/nu_dyck_word.py @@ -1368,15 +1368,12 @@ def __iter__(self, N=[], D=[], i=None, X=None): """ # Define successor function for recursion def transpose_close_open(N): - return [self.element_class(self, - N._list[0:k-1] - + list(reversed(N._list[k-1:k+1])) - + N._list[k+1:]) - for k, v in enumerate(N._list) - if k > 0 - and v == ndw_open_symbol - and N._list[k-1] == ndw_close_symbol - ] + for k, v in enumerate(N._list): + if k > 0 and v == ndw_open_symbol: + w = N._list[k - 1] + if w == ndw_close_symbol: + new = N._list[:k - 1] + [v, w] + N._list[k + 1:] + yield self.element_class(self, new) RES = RecursivelyEnumeratedSet([self.element_class(self, self._nu)], transpose_close_open) diff --git a/src/sage/combinat/nu_tamari_lattice.py b/src/sage/combinat/nu_tamari_lattice.py index f6cb83bbd5b..e90fc4d6791 100644 --- a/src/sage/combinat/nu_tamari_lattice.py +++ b/src/sage/combinat/nu_tamari_lattice.py @@ -66,7 +66,7 @@ def NuTamariLattice(nu): OUTPUT: - - a finite lattice + a finite lattice The elements of the lattice are :func:`\nu-Dyck paths` weakly above @@ -88,11 +88,8 @@ def NuTamariLattice(nu): Finite lattice containing 14 elements sage: NuTamariLattice([1,0,1,0,1,0,0,0,1]) Finite lattice containing 24 elements - """ - NDW = NuDyckWords(nu) - covers = [] elements = [] height = NDW[0].height()