Coverage for picos/glyphs.py: 92.17%
383 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2018 Maximilian Stahlberg
3#
4# This file is part of PICOS.
5#
6# PICOS is free software: you can redistribute it and/or modify it under the
7# terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17# ------------------------------------------------------------------------------
19"""String templates used to print (algebraic) expressions.
21PICOS internally uses this module to produce string representations for the
22algebraic expressions that you create.
23The function-like objects that are used to build such strings are called
24"glyphs" and are instanciated by this module following the
25`singleton pattern <https://en.wikipedia.org/wiki/Singleton_pattern>`_.
26As a result, you can modify the glyph objects listed below to influence how
27PICOS will format future strings, for example to disable use of unicode symbols
28that your console does not suppport or to adapt PICOS' output to the rest of
29your application.
31Here's an example of first swapping the entire character set to display
32expressions using only `Latin-1 <https://en.wikipedia.org/wiki/ISO/IEC_8859-1>`_
33characters, and then modifying a single glyph to our liking:
35 >>> import picos
36 >>> X = picos.Problem().add_variable("X", (2,2), "symmetric")
37 >>> print(X >> 0)
38 X ≽ 0
39 >>> picos.glyphs.latin1()
40 >>> print(X >> 0)
41 X » 0
42 >>> picos.glyphs.psdge.template = "{} - {} is psd"
43 >>> print(X >> 0)
44 X - 0 is psd
46Note that glyphs understand some algebraic rules such as operator precedence
47and associativity. This is possible because strings produced by glyphs remember
48how they were created.
50 >>> one_plus_two = picos.glyphs.add(1, 2)
51 >>> one_plus_two
52 '1 + 2'
53 >>> one_plus_two.glyph.template, one_plus_two.operands
54 ('{} + {}', (1, 2))
55 >>> picos.glyphs.add(one_plus_two, 3)
56 '1 + 2 + 3'
57 >>> picos.glyphs.sub(0, one_plus_two)
58 '0 - (1 + 2)'
60The positive semidefinite glyph above does not yet know how to properly handle
61arguments with respect to the ``-`` symbol involved, but we can modify it
62further:
64 >>> print(X + X >> X + X)
65 X + X - X + X is psd
66 >>> # Use the same operator binding strength as regular substraction.
67 >>> picos.glyphs.psdge.order = picos.glyphs.sub.order
68 >>> print(X + X >> X + X)
69 X + X - (X + X) is psd
71You can reset all glyphs to their initial state as follows:
73 >>> picos.glyphs.default()
74"""
76import functools
77import sys
79from . import settings
80from .apidoc import api_end, api_start
82# Allow functions to modify this module directly.
83glyphs = sys.modules[__name__]
85_API_START = api_start(globals())
86# -------------------------------
89# Definitions of glyph classes and the rich strings they create.
90# --------------------------------------------------------------
92class GlStr(str):
93 """A string created from a :class:`glyph <Gl>`.
95 It has an additional :attr:`glyph` field pointing to the glyph that created
96 it, and a :attr:`operands` field containing the values used to create it.
97 """
99 def __new__(cls, string, glyph, operands):
100 """Create a regular Python string."""
101 return str.__new__(cls, string)
103 def __init__(self, string, glyph, operands):
104 """Augment the Python string with metadata on its origin."""
105 self.glyph = glyph
106 """The glyph used to create the string."""
108 self.operands = operands
109 """The operands used to create the string."""
111 def __copy__(self):
112 return self.__class__(str(self), self.glyph, self.operands)
114 def reglyphed(self, replace={}):
115 """Returns a rebuilt version of the string using current glyphs.
117 :param dict replace:
118 Replace leaf-node (non :class:`GlStr`) strings with new strings.
119 This can be used, for instance, to change the names of varaibles.
120 """
121 return self.glyph(*(op.reglyphed(replace) if isinstance(op, GlStr) else
122 (replace[op] if op in replace else op) for op in self.operands))
125class Gl:
126 """The basic "glyph", an (algebraic) string formatting template.
128 Sublcasses are supposed to extend formatting routines, going beyond of what
129 Python string formatting is capabale of. In particular, glyphs can be used
130 to craft unambiguous algebraic expressions with the minimum amount of
131 parenthesis.
132 """
134 def __init__(self, glyph):
135 """Construct a glyph.
137 :param str glyph: The glyph's format string template.
138 """
139 self.template = glyph
140 self.initial = glyph
142 def reset(self):
143 """Reset the glyph to its initial formatting template."""
144 self.template = self.initial
146 def update(self, new):
147 """Change the glyph's formatting template."""
148 self.template = new.template
150 def rebuild(self):
151 """If the template was created using other glyphs, rebuild it.
153 :returns: True if the template has changed.
154 """
155 if isinstance(self.template, GlStr):
156 oldTemplate = self.template
157 self.template = self.template.reglyphed()
158 return self.template != oldTemplate
159 else:
160 return False
162 def __call__(self, *args):
163 """Format the arguments as a :class:`GlStr`."""
164 return GlStr(self.template.format(*args), self, args)
167class OpStr(GlStr):
168 """A string created from a math operator glyph."""
170 pass
173class Op(Gl):
174 """The basic math operator glyph."""
176 def __init__(self, glyph, order, assoc=False, closed=False):
177 """Construct a math operator glyph.
179 :param str glyph: The glyph's format string template.
180 :param int order: The operator's position in the binding strength
181 hierarchy. Operators with lower numbersbind more strongly.
182 :param bool assoc: If this is :obj:`True`, then the operator is
183 associative, so that parenthesis are always omitted around operands
184 with an equal outer operator. Otherwise, (1) parenthesis are used
185 around the right hand side operand of a binary operation of same
186 binding strength and (2) around all operands of non-binary
187 operations of same binding strength.
188 :param closed: If :obj:`True`, the operator already encloses the
189 operands in some sort of brackets, so that no additional parenthesis
190 are needed. For glyphs where only some operands are enclosed, this
191 can be specified per operand in the form of a list.
192 :type closed: bool or list(bool)
193 """
194 self.initial = (glyph, order, assoc, closed)
195 self.reset()
197 def reset(self):
198 """Reset the glyph to its initial behavior."""
199 self.template, self.order, self.assoc, self.closed = self.initial
201 def update(self, new):
202 """Change the glyph's behavior."""
203 self.template = new.template
204 self.order = new.order
205 self.assoc = new.assoc
206 self.closed = new.closed
208 def __call__(self, *operands):
209 """Format the arguments as an :class:`OpStr`."""
210 if self.closed is True:
211 return OpStr(self.template.format(*operands), self, operands)
213 placeholders = []
214 for i, operand in enumerate(operands):
215 if isinstance(self.closed, list) and i < len(self.closed) \
216 and self.closed[i]:
217 parenthesis = False
218 elif not isinstance(operand, OpStr):
219 parenthesis = False
220 elif operand.glyph.order < self.order:
221 parenthesis = False
222 elif operand.glyph.order == self.order:
223 if len(operands) == 2 and i == 0:
224 # By default, bind from left to right.
225 parenthesis = False
226 elif self.assoc in (None, False):
227 parenthesis = True
228 else:
229 parenthesis = operand.glyph is not self
230 else:
231 parenthesis = True
233 if type(operand) is float:
234 # If no format specifier was given, then integral floats would
235 # be formatted with a trailing '.0', which we don't want. Note
236 # that for complex numbers the default behavior is already as we
237 # want it, while 'g' would omit the parenthesis that we need.
238 placeholder = "{:g}"
239 else:
240 placeholder = "{}"
242 if parenthesis:
243 placeholders.append(glyphs.parenth(placeholder))
244 else:
245 placeholders.append(placeholder)
247 return OpStr(self.template.format(*placeholders).format(*operands),
248 self, operands)
251class Am(Op):
252 """A math atom glyph."""
254 def __init__(self, glyph):
255 """Construct an :class:`Am` glyph.
257 :param str glyph: The glyph's format string template.
258 """
259 Op.__init__(self, glyph, 0)
262class Br(Op):
263 """A math operator glyph with enclosing brackets."""
265 def __init__(self, glyph):
266 """Construct a :class:`Br` glyph.
268 :param str glyph: The glyph's format string template.
269 """
270 Op.__init__(self, glyph, 0, closed=True)
273class Fn(Op):
274 """A math operator glyph in function form."""
276 def __init__(self, glyph):
277 """Construct a :class:`Fn` glyph.
279 :param str glyph: The glyph's format string template.
280 """
281 Op.__init__(self, glyph, 0, closed=True)
284class Tr(Op):
285 """A math glyph in superscript/trailer form."""
287 def __init__(self, glyph):
288 """Construct a :class:`Tr` glyph.
290 :param str glyph: The glyph's format string template.
291 """
292 Op.__init__(self, glyph, 1)
295class Rl(Op):
296 """A math relation glyph."""
298 def __init__(self, glyph):
299 """Construct a :class:`Rl` glyph.
301 :param str glyph: The glyph's format string template.
302 """
303 Op.__init__(self, glyph, 5, assoc=True)
306# Functions that show, reset or rebuild the glyph objects.
307# --------------------------------------------------------
309def show(*args):
310 """Show output from all glyphs.
312 :param list(str) args: Strings to use as glyph operands.
313 """
314 args = list(args) + ["{}"]*4
316 print("{:8} | {:3} | {:5} | {}\n{}+{}+{}+{}".format(
317 "Glyph", "Pri", "Asso", "Value", "-"*9, "-"*5, "-"*7, "-"*10))
319 for name in sorted(list(glyphs.__dict__.keys())):
320 glyph = getattr(glyphs, name)
321 if isinstance(glyph, Gl):
322 order = glyph.order if hasattr(glyph, "order") else ""
323 assoc = str(glyph.assoc) if hasattr(glyph, "order") else ""
324 print("{:8} | {:3} | {:5} | {}".format(
325 name, order, assoc, glyph(*args)))
328def rebuild():
329 """Update glyphs that are based upon other glyphs."""
330 for i in range(100):
331 if not any(glyph.rebuild() for glyph in glyphs.__dict__.values()
332 if isinstance(glyph, Gl)):
333 return
335 raise Exception("Maximum recursion depth for glyph rebuilding reached. "
336 "There is likely a cyclic dependence between them.")
339def ascii():
340 """Let PICOS create future strings using only ASCII characters."""
341 for glyph in glyphs.__dict__.values():
342 if isinstance(glyph, Gl):
343 glyph.reset()
346# Initial glyph definitions and functions that update them.
347# ---------------------------------------------------------
349# Non-operator glyphs.
350repr1 = Gl("<{}>"); """Representation glyph."""
351repr2 = Gl(glyphs.repr1("{}: {}")); """Long representation glyph."""
352parenth = Gl("({})"); """Parenthesis glyph."""
353sep = Gl("{} : {}"); """Seperator glyph."""
354compsep = Gl("{}:{}"); """Compact seperator glyph."""
355comma = Gl("{}, {}"); """Seperated by comma glyph."""
356size = Gl("{}x{}"); """Matrix size/shape glyph."""
357compose = Gl("{}.{}"); """Function composition glyph."""
358set = Gl("{{{}}}"); """Set glyph."""
359closure = Fn("cl{}"); """Set closure glyph."""
360interval = Gl("[{}]"); """Interval glyph."""
361fromto = Gl("{}..{}"); """Range glyph."""
362intrange = Gl(glyphs.interval(glyphs.fromto("{}", "{}")))
363"""Integer range glyph."""
364shortint = Gl(glyphs.interval(glyphs.fromto("{},", ",{}")))
365"""Shortened interval glyph."""
366forall = Gl("{} f.a. {}"); """Universal quantification glyph."""
367leadsto = Gl("{} -> {}"); """Successorship glyph."""
368and_ = Gl("{} and {}"); """Logical and glyph."""
369or_ = Gl("{} or {}"); """Logical or glyph."""
371# Atomic glyphs.
372infty = Am("inf"); """Infinity glyph."""
373idmatrix = Am("I"); """Identity matrix glyph."""
374lambda_ = Am("lambda"); """Lambda symbol glyph."""
376# Bracketed glyphs.
377matrix = Br("[{}]"); """Matrix glyph."""
378dotp = Br("<{}, {}>"); """Scalar product glyph."""
379abs = Br("|{}|"); """Absolute value glyph."""
380norm = Br("||{}||"); """Norm glyph."""
382# Special norms.
383pnorm = Op(Gl("{}_{}")(glyphs.norm("{}"), "{}"), 1, closed=[True, False])
384"""p-Norm glyph."""
385pqnorm = Op(Gl("{}_{},{}")(glyphs.norm("{}"), "{}", "{}"), 1,
386 closed=[True, False, False]); """pq-Norm glyph."""
387spnorm = Op(Gl("{}_{}")(glyphs.norm("{}"), "2"), 1, closed=True)
388"""Spectral Norm glyph."""
389ncnorm = Op(Gl("{}_{}")(glyphs.norm("{}"), "*"), 1, closed=True)
390"""Nuclear Norm glyph."""
392# Function glyphs.
393sum = Fn("sum({})"); """Summation glyph."""
394prod = Fn("prod({})"); """Product glyph."""
395max = Fn("max({})"); """Maximum glyph."""
396min = Fn("min({})"); """Minimum glyph."""
397exp = Fn("exp({})"); """Exponentiation glyph."""
398log = Fn("log({})"); """Logarithm glyph."""
399vec = Fn("vec({})"); """Vectorization glyph."""
400trilvec = Fn("trilvec({})"); """Lower triangular vectorization glyph."""
401triuvec = Fn("triuvec({})"); """Upper triangular vectorization glyph."""
402svec = Fn("svec({})"); """Symmetric vectorization glyph."""
403desvec = Fn("desvec({})"); """Symmetric de-vectorization glyph."""
404trace = Fn("tr({})"); """Matrix trace glyph."""
405diag = Fn("diag({})"); """Diagonal matrix glyph."""
406maindiag = Fn("maindiag({})"); """Main diagonal glyph."""
407det = Fn("det({})"); """Determinant glyph."""
408real = Fn("Re({})"); """Real part glyph."""
409imag = Fn("Im({})"); """Imaginary part glyph."""
410conj = Fn("conj({})"); """Complex conugate glyph."""
411quadpart = Fn("quad({})"); """Quadratic part glyph."""
412affpart = Fn("aff({})"); """Affine part glyph."""
413linpart = Fn("lin({})"); """Linear part glyph."""
414blinpart = Fn("bilin({})"); """Bilinear part glyph."""
415ncstpart = Fn("noncst({})"); """Nonconstant part glyph."""
416cstpart = Fn("cst({})"); """Constant part glyph."""
417frozen = Fn("[{}]"); """Frozen mutables glyph."""
418reshaped = Fn("reshaped({}, {})"); """Column-major (Fortran) reshaped glyph."""
419reshaprm = Fn("reshaped({}, {}, C)"); """Row-major (C-order) reshaped glyph."""
420bcasted = Fn("bcasted({}, {})"); """Broadcasted glyph."""
421sqrt = Fn("sqrt({})"); """Square root glyph."""
422shuffled = Fn("shuffled({})"); """Matrix reshuffling glyph."""
423probdist = Fn("pd({})"); """Probability distribution glyph."""
424expected = Fn("E[{}]"); """Epected value glyph."""
426# Semi-closed glyphs.
427ptrace = Op("trace_{}({})", 0, closed=[False, True])
428"""Matrix p-Trace glyph."""
429slice = Op("{}[{}]", 0, closed=[False, True])
430"""Expression slicing glyph."""
431ptransp_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True])
432"""Matrix partial transposition glyph.""" # TODO: Replace ptransp.
433ptrace_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True])
434"""Matrix partial trace glyph.""" # TODO: Replace ptrace_.
435exparg = Op("E_{}({})", 0, closed=[False, True])
436"""Expected value glyph."""
438# Basic algebraic glyphs.
439add = Op("{} + {}", 3, assoc=True); """Addition glyph."""
440sub = Op("{} - {}", 3, assoc=False); """Substraction glyph."""
441hadamard = Op("{}(o){}", 2, assoc=True); """Hadamard product glyph."""
442kron = Op("{}(x){}", 2, assoc=True); """Kronecker product glyph."""
443mul = Op("{}*{}", 2, assoc=True); """Multiplication glyph."""
444div = Op("{}/{}", 2, assoc=False); """Division glyph."""
445neg = Op("-{}", 2.5); """Negation glyph."""
446plsmns = Op("[+/-]{}", 2.5); """Plus/Minus glyph."""
448# Trailer glyphs.
449power = Tr("{}^{}"); """Power glyph."""
450cubed = Tr(glyphs.power("{}", "3")); """Cubed value glyph."""
451squared = Tr(glyphs.power("{}", "2")); """Squared value glyph."""
452inverse = Tr(glyphs.power("{}", glyphs.neg(1))); """Matrix inverse glyph."""
453transp = Tr("{}.T"); """Matrix transposition glyph."""
454ptransp = Tr("{}.Tx"); """Matrix partial transposition glyph."""
455htransp = Tr("{}.H"); """Matrix hermitian transposition glyph."""
456index = Tr("{}_{}"); """Index glyph."""
458# Concatenation glyphs.
459horicat = Op("{}, {}", 4, assoc=True); """Horizontal concatenation glyph."""
460vertcat = Op("{}; {}", 4.5, assoc=True); """Vertical concatenation glyph."""
462# Relation glyphs.
463element = Rl("{} in {}"); """Set element glyph."""
464eq = Rl("{} = {}"); """Equality glyph."""
465ge = Rl("{} >= {}"); """Greater or equal glyph."""
466gt = Rl("{} > {}"); """Greater than glyph."""
467le = Rl("{} <= {}"); """Lesser or equal glyph."""
468lt = Rl("{} < {}"); """Lesser than glyph."""
469psdge = Rl("{} >> {}"); """Lesser or equal w.r.t. the p.s.d. cone glyph."""
470psdle = Rl("{} << {}"); """Greater or equal w.r.t. the p.s.d. cone glyph."""
472# Bracket-less function glyphs.
473maxarg = Op("max_{} {}", 3.5, closed=False)
474minarg = Op("min_{} {}", 3.5, closed=False)
477def latin1(rebuildDerivedGlyphs=True):
478 """Let PICOS create future strings using only ISO 8859-1 characters."""
479 # Reset to ASCII first.
480 ascii()
482 # Update glyphs with only template changes.
483 glyphs.compose.template = "{}°{}"
484 glyphs.cubed.template = "{}³"
485 glyphs.hadamard.template = "{}(·){}"
486 glyphs.kron.template = "{}(×){}"
487 glyphs.leadsto.template = "{} » {}"
488 glyphs.mul.template = "{}·{}"
489 glyphs.squared.template = "{}²"
490 glyphs.plsmns.template = "±{}"
491 glyphs.psdge.template = "{} » {}"
492 glyphs.psdle.template = "{} « {}"
493 glyphs.size.template = "{}×{}"
495 # Update all derived glyphs.
496 if rebuildDerivedGlyphs:
497 rebuild()
500def unicode(rebuildDerivedGlyphs=True):
501 """Let PICOS create future strings using only unicode characters."""
502 # Reset to LATIN-1 first.
503 latin1(rebuildDerivedGlyphs=False)
505 # Update glyphs with only template changes.
506 glyphs.and_.template = "{} ∧ {}"
507 glyphs.compose.template = "{}∘{}"
508 glyphs.dotp.template = "⟨{}, {}⟩"
509 glyphs.element.template = "{} ∈ {}"
510 glyphs.forall.template = "{} ∀ {}"
511 glyphs.fromto.template = "{}…{}"
512 glyphs.ge.template = "{} ≥ {}"
513 glyphs.hadamard.template = "{}⊙{}"
514 glyphs.htransp.template = "{}ᴴ"
515 glyphs.infty.template = "∞"
516 glyphs.kron.template = "{}⊗{}"
517 glyphs.lambda_.template = "λ"
518 glyphs.le.template = "{} ≤ {}"
519 glyphs.leadsto.template = "{} → {}"
520 glyphs.norm.template = "‖{}‖"
521 glyphs.or_.template = "{} ∨ {}"
522 glyphs.prod.template = "∏({})"
523 glyphs.psdge.template = "{} ≽ {}"
524 glyphs.psdle.template = "{} ≼ {}"
525 glyphs.sum.template = "∑({})"
526 glyphs.transp.template = "{}ᵀ"
528 # Update all derived glyphs.
529 if rebuildDerivedGlyphs:
530 rebuild()
533# Set and use the default charset.
534if settings.DEFAULT_CHARSET == "unicode":
535 default = unicode
536elif settings.DEFAULT_CHARSET == "latin1":
537 default = latin1
538elif settings.DEFAULT_CHARSET == "ascii":
539 default = ascii
540else:
541 raise ValueError("PICOS doesn't have a charset named '{}'.".format(
542 settings.DEFAULT_CHARSET))
543default()
546# Helper functions that mimic or create additional glyphs.
547# --------------------------------------------------------
549def scalar(value):
550 """Format a scalar value.
552 This function mimics an operator glyph, but it returns a normal string (as
553 opposed to an :class:`OpStr`) for nonnegative numbers.
555 This is not realized as an atomic operator glyph to not increase the
556 recursion depth of :func:`is_negated` and :func:`unnegate` unnecessarily.
558 **Example**
560 >>> from picos.glyphs import scalar
561 >>> str(1.0)
562 '1.0'
563 >>> scalar(1.0)
564 '1'
565 """
566 if not isinstance(value, complex) and value < 0:
567 value = -value
568 negated = True
569 else:
570 negated = False
572 string = ("{:g}" if type(value) is float else "{}").format(value)
574 if negated:
575 return glyphs.neg(string)
576 else:
577 return string
580def shape(theShape):
581 """Describe a matrix shape that can contain wildcards.
583 A wrapper around :obj:`size` that takes just one argument (the shape) that
584 may contain wildcards which are printed as ``'?'``.
585 """
586 newShape = (
587 "?" if theShape[0] is None else theShape[0],
588 "?" if theShape[1] is None else theShape[1])
589 return glyphs.size(*newShape)
592def make_function(*names):
593 """Create an ad-hoc composite function glyphs.
595 **Example**
597 >>> from picos.glyphs import make_function
598 >>> make_function("log", "sum", "exp")("x")
599 'log∘sum∘exp(x)'
600 """
601 return Fn("{}({{}})".format(functools.reduce(glyphs.compose, names)))
604# Helper functions that make context-sensitive use of glyphs.
605# -----------------------------------------------------------
607def from_glyph(string, theGlyph):
608 """Whether the given string was created by the given glyph."""
609 return isinstance(string, GlStr) and string.glyph is theGlyph
612CAN_FACTOR_OUT_NEGATION = (
613 glyphs.matrix,
614 glyphs.sum,
615 glyphs.trace,
616 glyphs.vec,
617 glyphs.diag,
618 glyphs.maindiag,
619 glyphs.real
620) #: Operator glyphs for which negation may be factored out.
623def is_negated(value):
624 """Check if a value can be unnegated by :func:`unnegate`."""
625 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION:
626 return is_negated(value.operands[0])
627 elif from_glyph(value, glyphs.neg):
628 return True
629 elif type(value) is str:
630 try:
631 return float(value) < 0
632 except ValueError:
633 return False
634 elif type(value) in (int, float):
635 return value < 0
636 else:
637 return False
640def unnegate(value):
641 """Unnegate a value, usually a glyph-created string, in a sensible way.
643 Unnegates a :class:`operator glyph created string <OpStr>` or other value in
644 a sensible way, more precisely by recursing through a sequence of glyphs
645 used to create the value and for which we can factor out negation, and
646 negating the underlaying (numeric or string) value.
648 :raises ValueError: When :meth:`is_negated` returns :obj:`False`.
649 """
650 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION:
651 return value.glyph(unnegate(value.operands[0]))
652 elif from_glyph(value, glyphs.neg):
653 return value.operands[0]
654 elif type(value) is str:
655 # We raise any conversion error, because is_negated returns False.
656 return "{:g}".format(-float(value))
657 elif type(value) in (int, float):
658 return -value
659 else:
660 raise ValueError("The value to recursively unnegate is not negated in a"
661 "supported manner.")
664def clever_neg(value):
665 """Describe the negation of a value in a clever way.
667 A wrapper around :attr:`neg` that resorts to unnegating an already negated
668 value.
670 **Example**
672 >>> from picos.glyphs import neg, clever_neg, matrix
673 >>> neg("x")
674 '-x'
675 >>> neg(neg("x"))
676 '-(-x)'
677 >>> clever_neg(neg("x"))
678 'x'
679 >>> neg(matrix(-1))
680 '-[-1]'
681 >>> clever_neg(matrix(-1))
682 '[1]'
683 """
684 if is_negated(value):
685 return unnegate(value)
686 else:
687 return glyphs.neg(value)
690def clever_add(left, right):
691 """Describe the addition of two values in a clever way.
693 A wrapper around :attr:`add` that resorts to :attr:`sub` if the second
694 operand was created by :attr:`neg` or is a negative number (string). In both
695 cases the second operand is adjusted accordingly.
697 **Example**
699 >>> from picos.glyphs import neg, add, clever_add, matrix
700 >>> add("x", neg("y"))
701 'x + -y'
702 >>> clever_add("x", neg("y"))
703 'x - y'
704 >>> add("X", matrix(neg("y")))
705 'X + [-y]'
706 >>> clever_add("X", matrix(neg("y")))
707 'X - [y]'
708 >>> clever_add("X", matrix(-1.5))
709 'X - [1.5]'
710 """
711 if left in (0, "0"):
712 return right
714 if right in (0, "0"):
715 return left
717 if is_negated(right):
718 return glyphs.sub(left, unnegate(right))
719 else:
720 return glyphs.add(left, right)
723def clever_sub(left, right):
724 """Describe the substraction of a value from another in a clever way.
726 A wrapper around :attr:`sub` that resorts to :attr:`add` if the second
727 operand was created by :attr:`neg` or is a negative number(string). In both
728 cases the second operand is adjusted accordingly.
730 **Example**
732 >>> from picos.glyphs import neg, sub, clever_sub, matrix
733 >>> sub("x", neg("y"))
734 'x - -y'
735 >>> clever_sub("x", neg("y"))
736 'x + y'
737 >>> sub("X", matrix(neg("y")))
738 'X - [-y]'
739 >>> clever_sub("X", matrix(neg("y")))
740 'X + [y]'
741 >>> clever_sub("X", matrix(-1.5))
742 'X + [1.5]'
743 """
744 if left in (0, "0"):
745 return clever_neg(right)
747 if right in (0, "0"):
748 return left
750 if is_negated(right):
751 return glyphs.add(left, unnegate(right))
752 else:
753 return glyphs.sub(left, right)
756def clever_mul(left, right):
757 """Describe the multiplocation of two values in a clever way.
759 A wrapper around :attr:`mul` that factors out identity factors.
760 """
761 # Apply a factor of zero on the left side.
762 if left in (0, "0"):
763 return left
765 # Apply a factor of zero on the right side.
766 if right in (0, "0"):
767 return right
769 # Factor out a scalar one on the left side.
770 if left in (1, "1"):
771 return right
773 # Factor out a scalar one on the right side.
774 if right in (1, "1"):
775 return left
777 # Detect quadratics.
778 if left == right:
779 return glyphs.squared(left)
781 # Factor out negation.
782 ln, rn = is_negated(left), is_negated(right)
783 if ln and rn:
784 return glyphs.clever_mul(unnegate(left), unnegate(right))
785 elif ln:
786 return glyphs.neg(glyphs.clever_mul(unnegate(left), right))
787 elif rn:
788 return glyphs.neg(glyphs.clever_mul(left, unnegate(right)))
790 return glyphs.mul(left, right)
793def clever_div(left, right):
794 """Describe the division of one value by another in a clever way.
796 A wrapper around :attr:`div` that factors out identity factors.
797 """
798 # Apply a factor of zero on the left side.
799 if left in (0, "0"):
800 return left
802 # Factor out a scalar one on the right side.
803 if right in (1, "1"):
804 return left
806 # Factor out negation.
807 ln, rn = is_negated(left), is_negated(right)
808 if ln and rn:
809 return glyphs.clever_div(unnegate(left), unnegate(right))
810 elif ln:
811 return glyphs.neg(glyphs.clever_div(unnegate(left), right))
812 elif rn:
813 return glyphs.neg(glyphs.clever_div(left, unnegate(right)))
815 return glyphs.div(left, right)
818def clever_dotp(left, right, complexRHS, scalar=False):
819 """Describe an inner product in a clever way.
821 :param bool complexRHS: Whether the right hand side is complex.
822 """
823 riCo = glyphs.conj(right) if complexRHS else right
825 if scalar:
826 return glyphs.clever_mul(left, right)
828 if from_glyph(left, glyphs.idmatrix):
829 return glyphs.trace(riCo)
830 elif from_glyph(riCo, glyphs.idmatrix):
831 return glyphs.trace(left)
832 elif from_glyph(left, glyphs.matrix) and left.operands[0] in (1, "1"):
833 return glyphs.sum(riCo)
834 elif from_glyph(riCo, glyphs.matrix) and riCo.operands[0] in (1, "1"):
835 return glyphs.sum(left)
836 elif left == right:
837 return glyphs.squared(glyphs.norm(left))
838 else:
839 return glyphs.dotp(left, right)
842def matrix_cat(left, right, horizontal=True):
843 """Describe matrix concatenation in a clever way.
845 A wrapper around :attr:`matrix`, :attr:`horicat` and :attr:`vertcat`.
847 **Example**
849 >>> from picos.glyphs import matrix_cat
850 >>> Z = matrix_cat("X", "Y")
851 >>> Z
852 '[X, Y]'
853 >>> matrix_cat(Z, Z)
854 '[X, Y, X, Y]'
855 >>> matrix_cat(Z, Z, horizontal = False)
856 '[X, Y; X, Y]'
857 """
858 if isinstance(left, OpStr) and left.glyph is glyphs.matrix:
859 left = left.operands[0]
861 if isinstance(right, OpStr) and right.glyph is glyphs.matrix:
862 right = right.operands[0]
864 catGlyph = glyphs.horicat if horizontal else glyphs.vertcat
866 return glyphs.matrix(catGlyph(left, right))
869def row_vectorize(*entries):
870 """Describe a row vector with the given symbolic entries."""
871 return functools.reduce(matrix_cat, entries)
874def col_vectorize(*entries):
875 """Describe a column vector with the given symbolic entries."""
876 return functools.reduce(lambda l, r: matrix_cat(l, r, False), entries)
879def free_var_name(string):
880 """Return a variable name not present in the given string."""
881 names = "xyzpqrstuvwabcdefghijklmno"
882 for name in names:
883 if name not in string:
884 return name
885 return "?"
888# --------------------------------------
889__all__ = api_end(_API_START, globals())