From b052251f8f366c809597227707ff54ff0b8f0618 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 21 Dec 2024 09:18:18 -0500 Subject: [PATCH 1/2] Revise `MapAt[]` part 1 (#1238) Miscellaneous lint for expanding and correcting `MapAt[]` * add eval_MapAt (move out of builtin class), and start to address error messages more correctly * walk_parts -> eval_Part which is what it is. And move to `mathics.eval.list.eol` since that is where it belongs * DRY use of "psl" message * Drop "_" at the beginning of routines which weren't really internal. --- .../builtin/functional/apply_fns_to_lists.py | 51 +-- mathics/builtin/list/eol.py | 23 +- mathics/builtin/messages.py | 1 + mathics/builtin/numbers/algebra.py | 4 +- mathics/builtin/sparse.py | 4 +- mathics/builtin/string/operations.py | 3 +- mathics/core/assignment.py | 4 +- mathics/core/subexpression.py | 2 +- mathics/core/systemsymbols.py | 4 + mathics/eval/functional/__init__.py | 0 mathics/eval/functional/apply_fns_to_lists.py | 57 +++ mathics/eval/list/__init__.py | 0 mathics/eval/list/eol.py | 332 +++++++++++++++++ mathics/eval/parts.py | 333 +----------------- mathics/packages/Combinatorica-repo | 2 +- 15 files changed, 425 insertions(+), 395 deletions(-) create mode 100644 mathics/eval/functional/__init__.py create mode 100644 mathics/eval/functional/apply_fns_to_lists.py create mode 100644 mathics/eval/list/__init__.py create mode 100644 mathics/eval/list/eol.py diff --git a/mathics/builtin/functional/apply_fns_to_lists.py b/mathics/builtin/functional/apply_fns_to_lists.py index ff47f3adc..d0ce78c02 100644 --- a/mathics/builtin/functional/apply_fns_to_lists.py +++ b/mathics/builtin/functional/apply_fns_to_lists.py @@ -11,20 +11,16 @@ from typing import Iterable -from mathics.builtin.list.constructing import List from mathics.core.atoms import Integer, Integer3 from mathics.core.builtin import Builtin, InfixOperator from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation -from mathics.core.exceptions import ( - InvalidLevelspecError, - MessageException, - PartRangeError, -) +from mathics.core.exceptions import InvalidLevelspecError, MessageException from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Atom, Symbol, SymbolNull, SymbolTrue -from mathics.core.systemsymbols import SymbolMapThread, SymbolRule +from mathics.core.symbols import Atom, SymbolNull, SymbolTrue +from mathics.core.systemsymbols import SymbolMapThread +from mathics.eval.functional.apply_fns_to_lists import eval_MapAt from mathics.eval.parts import python_levelspec, walk_levels # This tells documentation how to sort this module @@ -97,7 +93,7 @@ def callback(level): return Expression(f, *level.elements) heads = self.get_option(options, "Heads", evaluation) is SymbolTrue - result, depth = walk_levels(expr, start, stop, heads=heads, callback=callback) + result, _ = walk_levels(expr, start, stop, heads=heads, callback=callback) return result @@ -152,7 +148,7 @@ def callback(level): return Expression(f, level) heads = self.get_option(options, "Heads", evaluation) is SymbolTrue - result, depth = walk_levels(expr, start, stop, heads=heads, callback=callback) + result, _ = walk_levels(expr, start, stop, heads=heads, callback=callback) return result @@ -202,40 +198,7 @@ class MapAt(Builtin): def eval(self, f, expr, args, evaluation: Evaluation): "MapAt[f_, expr_, args_]" - - m = len(expr.elements) - - def map_at_one(i): - if 1 <= i <= m: - j = i - 1 - elif -m <= i <= -1: - j = m + i - else: - raise PartRangeError - replace_element = new_elements[j] - if hasattr(replace_element, "head") and replace_element.head is Symbol( - "System`Rule" - ): - new_elements[j] = Expression( - SymbolRule, - replace_element.elements[0], - Expression(f, replace_element.elements[1]), - ) - else: - new_elements[j] = Expression(f, replace_element) - return new_elements - - a = args.to_python() - if isinstance(a, int): - new_elements = list(expr.elements) - new_elements = map_at_one(a) - return List(*new_elements) - elif isinstance(a, list): - new_elements = list(expr.elements) - for item in a: - if len(item) == 1 and isinstance(item[0], int): - new_elements = map_at_one(item[0]) - return List(*new_elements) + return eval_MapAt(f, expr, args, evaluation) class MapIndexed(Builtin): diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 33ef1b843..bd8bf10f3 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -35,7 +35,9 @@ from mathics.core.symbols import Atom, Symbol, SymbolNull, SymbolTrue from mathics.core.systemsymbols import ( SymbolAppend, + SymbolAppendTo, SymbolByteArray, + SymbolDrop, SymbolFailed, SymbolInfinity, SymbolKey, @@ -44,26 +46,26 @@ SymbolSelect, SymbolSequence, SymbolSet, + SymbolTake, +) +from mathics.eval.list.eol import ( + drop_span_selector, + eval_Part, + parts, + take_span_selector, ) from mathics.eval.lists import delete_one, delete_rec, list_boxes from mathics.eval.parts import ( - _drop_span_selector, - _take_span_selector, deletecases_with_levelspec, - parts, python_levelspec, set_part, walk_levels, - walk_parts, ) from mathics.eval.patterns import Matcher -SymbolAppendTo = Symbol("System`AppendTo") SymbolDeleteCases = Symbol("System`DeleteCases") -SymbolDrop = Symbol("System`Drop") SymbolPrepend = Symbol("System`Prepend") SymbolPrependTo = Symbol("System`PrependTo") -SymbolTake = Symbol("System`Take") class Append(Builtin): @@ -340,7 +342,6 @@ class Delete(Builtin): # Delete *can* take more than 2 arguments. "argr": "Delete called with 1 argument; 2 arguments are expected.", "argt": "Delete called with `1` arguments; 2 arguments are expected.", - "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", "pkspec": "The expression `1` cannot be used as a part specification. Use `2` instead.", } summary_text = "delete elements from a list at given positions" @@ -586,7 +587,7 @@ def eval(self, items, seqs, evaluation: Evaluation): try: return parts( - items, [_drop_span_selector(seq) for seq in seq_tuple], evaluation + items, [drop_span_selector(seq) for seq in seq_tuple], evaluation ) except MessageException as e: e.message(evaluation) @@ -1183,7 +1184,7 @@ def eval(self, list, i, evaluation): return # Otherwise... - result = walk_parts([list], indices, evaluation) + result = eval_Part([list], indices, evaluation) if result: return result @@ -1688,7 +1689,7 @@ def eval(self, items, seqs, evaluation): return try: - return parts(items, [_take_span_selector(seq) for seq in seqs], evaluation) + return parts(items, [take_span_selector(seq) for seq in seqs], evaluation) except MessageException as e: e.message(evaluation) diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index 667a8a804..a5e13850f 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -224,6 +224,7 @@ class General(Builtin): "pspec": ( "Part specification `1` is neither an integer nor " "a list of integer." ), + "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", "rvalue": "`1` is not a variable with a value, so its value cannot be changed.", "seqs": "Sequence specification expected, but got `1`.", "setp": "Part assignment to `1` could not be made", diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 3dfe8f8e9..b3f2a35d2 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -68,9 +68,9 @@ SymbolTable, SymbolTanh, ) +from mathics.eval.list.eol import eval_Part from mathics.eval.numbers.algebra.simplify import eval_Simplify from mathics.eval.numbers.numbers import cancel, sympy_factor -from mathics.eval.parts import walk_parts from mathics.eval.patterns import match @@ -868,7 +868,7 @@ def eval_list(self, polys, varlist, evaluation: Evaluation, options: dict): if dim1 == 1 and order == 0: arrays[0] = coeff else: - walk_parts([curr_array], arrayidx, evaluation, coeff) + eval_Part([curr_array], arrayidx, evaluation, coeff) arrays[order] = curr_array return ListExpression(*arrays) diff --git a/mathics/builtin/sparse.py b/mathics/builtin/sparse.py index 547b02db4..d18cd925d 100644 --- a/mathics/builtin/sparse.py +++ b/mathics/builtin/sparse.py @@ -17,7 +17,7 @@ SymbolSparseArray, SymbolTable, ) -from mathics.eval.parts import walk_parts +from mathics.eval.list.eol import eval_Part class SparseArray(Builtin): @@ -135,7 +135,7 @@ def eval_normal(self, dims, default, data, evaluation: Evaluation): for item in data.elements: pos, val = item.elements if pos.has_form("List", None): - walk_parts([table], pos.elements, evaluation, val) + eval_Part([table], pos.elements, evaluation, val) return table def find_dimensions(self, rules, evaluation: Evaluation): diff --git a/mathics/builtin/string/operations.py b/mathics/builtin/string/operations.py index ac294c3fd..89d477132 100644 --- a/mathics/builtin/string/operations.py +++ b/mathics/builtin/string/operations.py @@ -37,8 +37,8 @@ SymbolStringRiffle, SymbolStringSplit, ) +from mathics.eval.list.eol import convert_seq, python_seq from mathics.eval.makeboxes import format_element -from mathics.eval.parts import convert_seq, python_seq from mathics.eval.strings import eval_StringFind @@ -200,7 +200,6 @@ class StringInsert(Builtin): messages = { "string": "String expected at position `1` in `2`.", "ins": "Cannot insert at position `1` in `2`.", - "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", } summary_text = "insert a string in a given position" diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index d1bb3a2e6..99bf97a10 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -38,7 +38,7 @@ SymbolPattern, SymbolRuleDelayed, ) -from mathics.eval.parts import walk_parts +from mathics.eval.list.eol import eval_Part class AssignmentException(Exception): @@ -689,7 +689,7 @@ def eval_assign_part(self, lhs, rhs, evaluation, tags, upset): evaluation.message(self.get_name(), "noval", symbol) return False indices = lhs.elements[1:] - return walk_parts([rule.replace], indices, evaluation, rhs) + return eval_Part([rule.replace], indices, evaluation, rhs) def eval_assign_random_state(self, lhs, rhs, evaluation, tags, upset): diff --git a/mathics/core/subexpression.py b/mathics/core/subexpression.py index 43a4c9fff..a0b7c9a7e 100644 --- a/mathics/core/subexpression.py +++ b/mathics/core/subexpression.py @@ -286,7 +286,7 @@ def to_expression(self): def replace(self, new): """ - Assigns `new` to the subexpression, according to the logic of `mathics.core.walk_parts` + Assigns `new` to the subexpression, according to the logic of `mathics.eval.list.eol.eval_Part` """ if (new.has_form("List", None) or new.get_head_name() == "System`List") and len( new.elements diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index a8a8faade..2f6d36f8a 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -31,6 +31,7 @@ SymbolAlternatives = Symbol("System`Alternatives") SymbolAnd = Symbol("System`And") SymbolAppend = Symbol("System`Append") +SymbolAppendTo = Symbol("System`AppendTo") SymbolApply = Symbol("System`Apply") SymbolArcCos = Symbol("System`ArcCos") SymbolArcSin = Symbol("System`ArcSin") @@ -80,6 +81,7 @@ SymbolDispatch = Symbol("System`Dispatch") SymbolDot = Symbol("System`Dot") SymbolDownValues = Symbol("System`DownValues") +SymbolDrop = Symbol("System`Drop") SymbolE = Symbol("System`E") SymbolEdgeForm = Symbol("System`EdgeForm") SymbolEndOfFile = Symbol("System`EndOfFile") @@ -146,6 +148,7 @@ SymbolMachinePrecision = Symbol("System`MachinePrecision") SymbolMakeBoxes = Symbol("System`MakeBoxes") SymbolMap = Symbol("System`Map") +SymbolMapAt = Symbol("System`MapAt") SymbolMapThread = Symbol("System`MapThread") SymbolMatchQ = Symbol("System`MatchQ") SymbolMatrixQ = Symbol("System`MatrixQ") @@ -266,6 +269,7 @@ SymbolSubsuperscriptBox = Symbol("System`SubsuperscriptBox") SymbolSuperscriptBox = Symbol("System`SuperscriptBox") SymbolTable = Symbol("System`Table") +SymbolTake = Symbol("System`Take") SymbolTan = Symbol("System`Tan") SymbolTanh = Symbol("System`Tanh") SymbolTeXForm = Symbol("System`TeXForm") diff --git a/mathics/eval/functional/__init__.py b/mathics/eval/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mathics/eval/functional/apply_fns_to_lists.py b/mathics/eval/functional/apply_fns_to_lists.py new file mode 100644 index 000000000..1b50c18d2 --- /dev/null +++ b/mathics/eval/functional/apply_fns_to_lists.py @@ -0,0 +1,57 @@ +""" +Evaluation routines for mathics.builtin.functional.appy_fns_to_lists +""" +from mathics.core.atoms import Integer +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import PartRangeError +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolMapAt, SymbolRule + + +def eval_MapAt(f, expr, args, evaluation: Evaluation): + m = len(expr.elements) + new_elements = list(expr.elements) + + def map_at_replace_one(i: int): + if 1 <= i <= m: + j = i - 1 + elif -m <= i <= -1: + j = m + i + else: + evaluation.message("MapAt", "partw", ListExpression(Integer(i)), expr) + raise PartRangeError + replace_element = new_elements[j] + if hasattr(replace_element, "head") and replace_element.head is Symbol( + "System`Rule" + ): + new_elements[j] = Expression( + SymbolRule, + replace_element.elements[0], + Expression(f, replace_element.elements[1]), + ) + else: + new_elements[j] = Expression(f, replace_element) + + try: + if isinstance(args, Integer): + map_at_replace_one(args.value) + return ListExpression(*new_elements) + elif isinstance(args, Expression): + for item in args.elements: + # Get value for arg in expr.elemnts + # Replace value + if ( + isinstance(item, Expression) + and len(item.elements) == 1 + and isinstance(item.elements[0], Integer) + ): + map_at_replace_one(item.elements[0].value) + return ListExpression(*new_elements) + else: + evaluation.message( + "MapAt", "psl", args, Expression(SymbolMapAt, f, expr, args) + ) + except PartRangeError: + return diff --git a/mathics/eval/list/__init__.py b/mathics/eval/list/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mathics/eval/list/eol.py b/mathics/eval/list/eol.py new file mode 100644 index 000000000..8387bdb47 --- /dev/null +++ b/mathics/eval/list/eol.py @@ -0,0 +1,332 @@ +from mathics.core.atoms import Integer +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import MessageException +from mathics.core.expression import Expression +from mathics.core.subexpression import SubExpression +from mathics.core.symbols import Atom +from mathics.core.systemsymbols import SymbolInfinity + + +def convert_seq(seq): + """ + converts a sequence specification into a (start, stop, step) tuple. + returns None on failure + """ + start, stop, step = 1, None, 1 + name = seq.get_name() + value = seq.get_int_value() + if name == "System`All": + pass + elif name == "System`None": + stop = 0 + elif value is not None: + if value > 0: + stop = value + else: + start = value + elif seq.has_form("List", 1, 2, 3): + if len(seq.elements) == 1: + start = stop = seq.elements[0].get_int_value() + if stop is None: + return None + else: + start = seq.elements[0].get_int_value() + stop = seq.elements[1].get_int_value() + if start is None or stop is None: + return None + if len(seq.elements) == 3: + step = seq.elements[2].get_int_value() + if step is None: + return None + else: + return None + return (start, stop, step) + + +def drop_take_selector(name, seq, sliced): + seq_tuple = convert_seq(seq) + if seq_tuple is None: + raise MessageException(name, "seqs", seq) + + def select(inner): + start, stop, step = seq_tuple + if isinstance(inner, Atom): + py_slice = None + else: + py_slice = python_seq(start, stop, step, len(inner.elements)) + if py_slice is None: + if stop is None: + stop = SymbolInfinity + raise MessageException(name, name.lower(), start, stop, inner) + return sliced(inner.elements, py_slice) + + return select + + +def eval_Part(list_of_list, indices, evaluation: Evaluation, assign_rhs=None): + """ + eval_part takes the first element of `list_of_list`, and builds + a subexpression composed of the expressions at the index positions + listed in `indices`. + + `assign_rhs`, when not empty, indicates where to the store parts of the composed list. + + list_of_list: a list of `Expression`s with a unique element. + + indices: a list of part specification `Expression`s, including + `Integer` indices, `Span` `Expression`s, `List` of `Integer`s + and + + assign_rhs: None or an `Expression` object. + """ + walk_list = list_of_list[0] + indices = [index.evaluate(evaluation) for index in indices] + if assign_rhs is not None: + try: + result = SubExpression(walk_list, indices) + result.replace(assign_rhs.copy()) + result = result.to_expression() + except MessageException as e: + e.message(evaluation) + return False + if isinstance(result, Expression): + result.clear_cache() + return result + else: + try: + result = parts(walk_list, part_selectors(indices), evaluation) + except MessageException as e: + e.message(evaluation) + return False + return result + + +def list_parts(exprs, selectors, evaluation): + """ + _list_parts returns a generator of Expressions using selectors to pick out parts of `exprs`. + If `selectors` is empty then a generator of items is returned. + + If a selector in `selectors` is a tuple it consists of a function to determine whether or + not to select an expression and a optional function to unwrap the resulting selected expressions. + + `evaluation` is used in expression restructuring an unwrapped expression when the there a + unwrapping function in the selector. + """ + if not selectors: + for expr in exprs: + yield expr + else: + selector = selectors[0] + if isinstance(selector, tuple): + select, unwrap = selector + else: + select = selector + unwrap = None + + for expr in exprs: + selected = list(select(expr)) + + picked = list(list_parts(selected, selectors[1:], evaluation)) + + if unwrap is None: + expr = expr.restructure(expr.head, picked, evaluation) + yield expr + else: + yield unwrap(picked) + + +def parts_all_selector(): + """ + Selector for `System`All` as a part specification. + """ + start = 1 + stop = None + step = 1 + + def select(inner): + if isinstance(inner, Atom): + raise MessageException("Part", "partd") + py_slice = python_seq(start, stop, step, len(inner.elements)) + if py_slice is None: + raise MessageException("Part", "take", start, stop, inner) + return inner.elements[py_slice] + + return select + + +def part_selectors(indices): + """ + part_selectors returns a suitable `selector` function according to + the kind of specifications in `indices`. + """ + for index in indices: + if index.has_form("Span", None): + yield parts_span_selector(index) + elif index.get_name() == "System`All": + yield parts_all_selector() + # FIXME: test/package/test_combinatorica.py in the benchmarking+futher-improvements + # fails with the below test. Isolate and fix. + # elif isinstance(index, ListExpression): + elif index.has_form("List", None): + yield parts_sequence_selector(index.elements) + elif isinstance(index, Integer): + yield parts_sequence_selector(index), lambda x: x[0] + else: + raise MessageException("Part", "pspec", index) + + +def parts(expr, selectors, evaluation) -> list: + """ + Select from the `Expression` expr those elements indicated by + the `selectors`. + """ + return list(list_parts([expr], list(selectors), evaluation))[0] + + +def parts_sequence_selector(pspec): + """ + Selector for `System`Sequence` part specification + """ + if not isinstance(pspec, (tuple, list)): + indices = [pspec] + else: + indices = pspec + + for index in indices: + if not isinstance(index, Integer): + raise MessageException("Part", "pspec", pspec) + + def select(inner): + if isinstance(inner, Atom): + raise MessageException("Part", "partd") + + elements = inner.elements + n = len(elements) + + for index in indices: + int_index = index.value + + if int_index == 0: + yield inner.head + elif 1 <= int_index <= n: + yield elements[int_index - 1] + elif -n <= int_index <= -1: + yield elements[int_index] + else: + raise MessageException("Part", "partw", index, inner) + + return select + + +def parts_span_selector(pspec): + """ + Selector for `System`Span` part specification + """ + if len(pspec.elements) > 3: + raise MessageException("Part", "span", pspec) + start = 1 + stop = None + step = 1 + if len(pspec.elements) > 0: + start = pspec.elements[0].get_int_value() + if len(pspec.elements) > 1: + stop = pspec.elements[1].get_int_value() + if stop is None: + if pspec.elements[1].get_name() == "System`All": + stop = None + else: + raise MessageException("Part", "span", pspec) + if len(pspec.elements) > 2: + step = pspec.elements[2].get_int_value() + + if start == 0 or stop == 0: + # index 0 is undefined + raise MessageException("Part", "span", 0) + + if start is None or step is None: + raise MessageException("Part", "span", pspec) + + def select(inner): + if isinstance(inner, Atom): + raise MessageException("Part", "partd") + py_slice = python_seq(start, stop, step, len(inner.elements)) + if py_slice is None: + raise MessageException("Part", "take", start, stop, inner) + return inner.elements[py_slice] + + return select + + +def python_seq(start: int, stop: int, step: int, length: int): + """ + Converts Mathics3 sequence tuple to python slice object. + + Based on David Mashburn's generic slice: + https://gist.github.com/davidmashburn/9764309 + """ + if step == 0: + return None + + # special empty case + if stop is None and length is not None: + empty_stop = length + else: + empty_stop = stop + if start is not None and empty_stop + 1 == start and step > 0: + return slice(0, 0, 1) + + if start == 0 or stop == 0: + return None + + # wrap negative values to positive and convert from 1-based to 0-based + if start < 0: + start += length + else: + start -= 1 + + if stop is None: + if step < 0: + stop = 0 + else: + stop = length - 1 + elif stop < 0: + stop += length + else: + assert stop > 0 + stop -= 1 + + # check bounds + if ( + not 0 <= start < length + or not 0 <= stop < length + or step > 0 + and start - stop > 1 + or step < 0 + and stop - start > 1 + ): # nopep8 + return None + + # include the stop value + if step > 0: + stop += 1 + else: + stop -= 1 + if stop == -1: + stop = None + if start == 0: + start = None + + return slice(start, stop, step) + + +def take_span_selector(seq): + return drop_take_selector("Take", seq, lambda x, s: x[s]) + + +def drop_span_selector(seq): + def sliced(x, s): + y = list(x[:]) + del y[s] + return y + + return drop_take_selector("Drop", seq, sliced) diff --git a/mathics/eval/parts.py b/mathics/eval/parts.py index 631005c2b..a70deda0f 100644 --- a/mathics/eval/parts.py +++ b/mathics/eval/parts.py @@ -11,16 +11,14 @@ from mathics.core.element import BaseElement, BoxElementMixin from mathics.core.exceptions import ( InvalidLevelspecError, - MessageException, PartDepthError, PartRangeError, ) from mathics.core.expression import Expression from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.list import ListExpression -from mathics.core.subexpression import SubExpression from mathics.core.symbols import Atom, Symbol, SymbolList -from mathics.core.systemsymbols import SymbolInfinity, SymbolNothing +from mathics.core.systemsymbols import SymbolNothing from mathics.eval.patterns import Matcher @@ -126,201 +124,7 @@ def set_subpart(sub_expression, sub_indices: List[int]) -> BaseElement: return set_subpart(expression, indices) -def _parts_all_selector(): - """ - Selector for `System`All` as a part specification. - """ - start = 1 - stop = None - step = 1 - - def select(inner): - if isinstance(inner, Atom): - raise MessageException("Part", "partd") - py_slice = python_seq(start, stop, step, len(inner.elements)) - if py_slice is None: - raise MessageException("Part", "take", start, stop, inner) - return inner.elements[py_slice] - - return select - - -def _parts_span_selector(pspec): - """ - Selector for `System`Span` part specification - """ - if len(pspec.elements) > 3: - raise MessageException("Part", "span", pspec) - start = 1 - stop = None - step = 1 - if len(pspec.elements) > 0: - start = pspec.elements[0].get_int_value() - if len(pspec.elements) > 1: - stop = pspec.elements[1].get_int_value() - if stop is None: - if pspec.elements[1].get_name() == "System`All": - stop = None - else: - raise MessageException("Part", "span", pspec) - if len(pspec.elements) > 2: - step = pspec.elements[2].get_int_value() - - if start == 0 or stop == 0: - # index 0 is undefined - raise MessageException("Part", "span", 0) - - if start is None or step is None: - raise MessageException("Part", "span", pspec) - - def select(inner): - if isinstance(inner, Atom): - raise MessageException("Part", "partd") - py_slice = python_seq(start, stop, step, len(inner.elements)) - if py_slice is None: - raise MessageException("Part", "take", start, stop, inner) - return inner.elements[py_slice] - - return select - - -def _parts_sequence_selector(pspec): - """ - Selector for `System`Sequence` part specification - """ - if not isinstance(pspec, (tuple, list)): - indices = [pspec] - else: - indices = pspec - - for index in indices: - if not isinstance(index, Integer): - raise MessageException("Part", "pspec", pspec) - - def select(inner): - if isinstance(inner, Atom): - raise MessageException("Part", "partd") - - elements = inner.elements - n = len(elements) - - for index in indices: - int_index = index.value - - if int_index == 0: - yield inner.head - elif 1 <= int_index <= n: - yield elements[int_index - 1] - elif -n <= int_index <= -1: - yield elements[int_index] - else: - raise MessageException("Part", "partw", index, inner) - - return select - - -def _part_selectors(indices): - """ - _part_selectors returns a suitable `selector` function according to - the kind of specifications in `indices`. - """ - for index in indices: - if index.has_form("Span", None): - yield _parts_span_selector(index) - elif index.get_name() == "System`All": - yield _parts_all_selector() - # FIXME: test/package/test_combinatorica.py in the benchmarking+futher-improvements - # fails with the below test. Isolate and fix. - # elif isinstance(index, ListExpression): - elif index.has_form("List", None): - yield _parts_sequence_selector(index.elements) - elif isinstance(index, Integer): - yield _parts_sequence_selector(index), lambda x: x[0] - else: - raise MessageException("Part", "pspec", index) - - -def _list_parts(exprs, selectors, evaluation): - """ - _list_parts returns a generator of Expressions using selectors to pick out parts of `exprs`. - If `selectors` is empty then a generator of items is returned. - - If a selector in `selectors` is a tuple it consists of a function to determine whether or - not to select an expression and a optional function to unwrap the resulting selected expressions. - - `evaluation` is used in expression restructuring an unwrapped expression when the there a - unwrapping function in the selector. - """ - if not selectors: - for expr in exprs: - yield expr - else: - selector = selectors[0] - if isinstance(selector, tuple): - select, unwrap = selector - else: - select = selector - unwrap = None - - for expr in exprs: - selected = list(select(expr)) - - picked = list(_list_parts(selected, selectors[1:], evaluation)) - - if unwrap is None: - expr = expr.restructure(expr.head, picked, evaluation) - yield expr - else: - yield unwrap(picked) - - -def parts(expr, selectors, evaluation) -> list: - """ - Select from the `Expression` expr those elements indicated by - the `selectors`. - """ - return list(_list_parts([expr], list(selectors), evaluation))[0] - - -def walk_parts(list_of_list, indices, evaluation, assign_rhs=None): - """ - walk_parts takes the first element of `list_of_list`, and builds - a subexpression composed of the expressions at the index positions - listed in `indices`. - - `assign_rhs`, when not empty, indicates where to the store parts of the composed list. - - list_of_list: a list of `Expression`s with a unique element. - - indices: a list of part specification `Expression`s, including - `Integer` indices, `Span` `Expression`s, `List` of `Integer`s - and - - assign_rhs: None or an `Expression` object. - """ - walk_list = list_of_list[0] - indices = [index.evaluate(evaluation) for index in indices] - if assign_rhs is not None: - try: - result = SubExpression(walk_list, indices) - result.replace(assign_rhs.copy()) - result = result.to_expression() - except MessageException as e: - e.message(evaluation) - return False - if isinstance(result, Expression): - result.clear_cache() - return result - else: - try: - result = parts(walk_list, _part_selectors(indices), evaluation) - except MessageException as e: - e.message(evaluation) - return False - return result - - -def is_in_level(current, depth, start=1, stop=None): +def is_in_level(current, depth, start=1, stop=None) -> bool: if stop is None: stop = current if start < 0: @@ -348,7 +152,7 @@ def walk_levels( else: depth = 0 if heads: - head, head_depth = walk_levels( + head, _ = walk_levels( expr.head, start, stop, @@ -413,137 +217,6 @@ def value_to_level(expr) -> Optional[int]: return 1, value_to_level(levelspec) -def python_seq(start, stop, step, length): - """ - Converts mathematica sequence tuple to python slice object. - - Based on David Mashburn's generic slice: - https://gist.github.com/davidmashburn/9764309 - """ - if step == 0: - return None - - # special empty case - if stop is None and length is not None: - empty_stop = length - else: - empty_stop = stop - if start is not None and empty_stop + 1 == start and step > 0: - return slice(0, 0, 1) - - if start == 0 or stop == 0: - return None - - # wrap negative values to positive and convert from 1-based to 0-based - if start < 0: - start += length - else: - start -= 1 - - if stop is None: - if step < 0: - stop = 0 - else: - stop = length - 1 - elif stop < 0: - stop += length - else: - assert stop > 0 - stop -= 1 - - # check bounds - if ( - not 0 <= start < length - or not 0 <= stop < length - or step > 0 - and start - stop > 1 - or step < 0 - and stop - start > 1 - ): # nopep8 - return None - - # include the stop value - if step > 0: - stop += 1 - else: - stop -= 1 - if stop == -1: - stop = None - if start == 0: - start = None - - return slice(start, stop, step) - - -def convert_seq(seq): - """ - converts a sequence specification into a (start, stop, step) tuple. - returns None on failure - """ - start, stop, step = 1, None, 1 - name = seq.get_name() - value = seq.get_int_value() - if name == "System`All": - pass - elif name == "System`None": - stop = 0 - elif value is not None: - if value > 0: - stop = value - else: - start = value - elif seq.has_form("List", 1, 2, 3): - if len(seq.elements) == 1: - start = stop = seq.elements[0].get_int_value() - if stop is None: - return None - else: - start = seq.elements[0].get_int_value() - stop = seq.elements[1].get_int_value() - if start is None or stop is None: - return None - if len(seq.elements) == 3: - step = seq.elements[2].get_int_value() - if step is None: - return None - else: - return None - return (start, stop, step) - - -def _drop_take_selector(name, seq, sliced): - seq_tuple = convert_seq(seq) - if seq_tuple is None: - raise MessageException(name, "seqs", seq) - - def select(inner): - start, stop, step = seq_tuple - if isinstance(inner, Atom): - py_slice = None - else: - py_slice = python_seq(start, stop, step, len(inner.elements)) - if py_slice is None: - if stop is None: - stop = SymbolInfinity - raise MessageException(name, name.lower(), start, stop, inner) - return sliced(inner.elements, py_slice) - - return select - - -def _take_span_selector(seq): - return _drop_take_selector("Take", seq, lambda x, s: x[s]) - - -def _drop_span_selector(seq): - def sliced(x, s): - y = list(x[:]) - del y[s] - return y - - return _drop_take_selector("Drop", seq, sliced) - - def deletecases_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): """ This function walks the expression `expr` and deleting occurrences of `pattern` diff --git a/mathics/packages/Combinatorica-repo b/mathics/packages/Combinatorica-repo index 680999ee5..de634d143 160000 --- a/mathics/packages/Combinatorica-repo +++ b/mathics/packages/Combinatorica-repo @@ -1 +1 @@ -Subproject commit 680999ee557b434531ec20e466ae5035a277ba03 +Subproject commit de634d143512af6b2494fa691baa26ad36528bcf From 6b19e87e25fc3bb417ec5e0fadfe6d8f8e542329 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 21 Dec 2024 17:58:50 -0500 Subject: [PATCH 2/2] Start adding Alignment to InsetBox in 2D graphs (#1234) Add the notion of text alignment of InsetBox which is typically used in drawing axes. The text for an x (horizontal) axis is under the ticks while the text for a y (vertical) axis is to the left. Here we've done this for asymptote and SVG. More that could be done, but this is a small start. --- mathics/builtin/box/graphics.py | 13 +++++++++++++ mathics/format/asy.py | 20 ++++++++++++-------- mathics/format/svg.py | 12 +++++++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index af9487d74..a91b55ee3 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -802,6 +802,16 @@ def add_element(element): ), ] ): + # Where should the placement of tick mark labels go? + if index == 0: + # x labels go under tick marks + alignment = "bottom" + elif index == 1: + # y labels go to the left of tick marks + alignment = "left" + else: + alignment = None + if axes[index]: add_element( LineBox( @@ -858,6 +868,7 @@ def add_element(element): ), opos=p_self0(1), opacity=1.0, + alignment=alignment, ) ) for x in ticks_small: @@ -992,6 +1003,7 @@ def init( pos=None, opos=(0, 0), opacity=None, + alignment=None, ): super(InsetBox, self).init(graphics, item, style) @@ -1008,6 +1020,7 @@ def init( opacity = Opacity(1.0) self.opacity = opacity + self.alignment = alignment if item is not None: if len(item.elements) not in (1, 2, 3): diff --git a/mathics/format/asy.py b/mathics/format/asy.py index 2dd97edd3..c055619c9 100644 --- a/mathics/format/asy.py +++ b/mathics/format/asy.py @@ -409,6 +409,16 @@ def graphics_elements(self, **options) -> str: def inset_box(self, **options) -> str: """Asymptote formatting for boxing an Inset in a graphic.""" x, y = self.pos.pos() + + alignment = "SW" + if hasattr(self, "alignment"): + if self.alignment == "bottom": + # This is typically done for labels under the x axis. + alignment = "S" + elif self.alignment == "left": + # This is typically done for labels to the left of the y axis. + alignment = "W" + opacity_value = self.opacity.opacity if self.opacity else None content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) # FIXME: don't hard code text_style_opts, but allow these to be adjustable. @@ -416,14 +426,8 @@ def inset_box(self, **options) -> str: pen = asy_create_pens( edge_color=self.color, edge_opacity=opacity_value, fontsize=font_size ) - asy = """// InsetBox -label("$%s$", (%s,%s), %s, %s);\n""" % ( - content, - x, - y, - "align=SW", - pen, - ) + asy = f"""// InsetBox +label("${content}$", ({x},{y}), align={alignment}, {pen});\n""" return asy diff --git a/mathics/format/svg.py b/mathics/format/svg.py index 62d61ecee..6bb0392a5 100644 --- a/mathics/format/svg.py +++ b/mathics/format/svg.py @@ -391,8 +391,18 @@ def inset_box(self, **options) -> str: opacity=self.opacity.opacity, ) text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"' + + alignment = " dominant-baseline:hanging;" + if hasattr(self, "alignment"): + if self.alignment == "bottom": + # This is typically done for labels under the x axis. + alignment = " dominant-baseline:hanging; text-anchor:middle;" + elif self.alignment == "left": + # This is typically done for labels to the left of the y axis. + alignment = " dominant-baseline:middle; text-anchor:end;" + # FIXME: don't hard code text_style_opts, but allow these to be adjustable. - text_style_opts = "text-anchor:end; dominant-baseline:hanging;" + text_style_opts = alignment content = self.content.boxes_to_text(evaluation=self.graphics.evaluation) font_size = f'''font-size="{options.get("point_size", "10px")}"''' svg = f'{content}'