Coverage for picos/glyphs.py : 91.37%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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({}, {})"); """Reshaped glyph."""
419bcasted = Fn("bcasted({}, {})"); """Broadcasted glyph."""
420sqrt = Fn("sqrt({})"); """Square root glyph."""
421shuffled = Fn("shuffled({})"); """Matrix reshuffling glyph."""
422probdist = Fn("pd({})"); """Probability distribution glyph."""
423expected = Fn("E[{}]"); """Epected value glyph."""
425# Semi-closed glyphs.
426ptrace = Op("trace_{}({})", 0, closed=[False, True])
427"""Matrix p-Trace glyph."""
428slice = Op("{}[{}]", 0, closed=[False, True])
429"""Expression slicing glyph."""
430ptransp_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True])
431"""Matrix partial transposition glyph.""" # TODO: Replace ptransp.
432ptrace_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True])
433"""Matrix partial trace glyph.""" # TODO: Replace ptrace_.
434exparg = Op("E_{}({})", 0, closed=[False, True])
435"""Expected value glyph."""
437# Basic algebraic glyphs.
438add = Op("{} + {}", 3, assoc=True); """Addition glyph."""
439sub = Op("{} - {}", 3, assoc=False); """Substraction glyph."""
440hadamard = Op("{}(o){}", 2, assoc=True); """Hadamard product glyph."""
441kron = Op("{}(x){}", 2, assoc=True); """Kronecker product glyph."""
442mul = Op("{}*{}", 2, assoc=True); """Multiplication glyph."""
443div = Op("{}/{}", 2, assoc=False); """Division glyph."""
444neg = Op("-{}", 2.5); """Negation glyph."""
445plsmns = Op("[+/-]{}", 2.5); """Plus/Minus glyph."""
447# Trailer glyphs.
448power = Tr("{}^{}"); """Power glyph."""
449cubed = Tr(glyphs.power("{}", "3")); """Cubed value glyph."""
450squared = Tr(glyphs.power("{}", "2")); """Squared value glyph."""
451inverse = Tr(glyphs.power("{}", glyphs.neg(1))); """Matrix inverse glyph."""
452transp = Tr("{}.T"); """Matrix transposition glyph."""
453ptransp = Tr("{}.Tx"); """Matrix partial transposition glyph."""
454htransp = Tr("{}.H"); """Matrix hermitian transposition glyph."""
455index = Tr("{}_{}"); """Index glyph."""
457# Concatenation glyphs.
458horicat = Op("{}, {}", 4, assoc=True); """Horizontal concatenation glyph."""
459vertcat = Op("{}; {}", 4.5, assoc=True); """Vertical concatenation glyph."""
461# Relation glyphs.
462element = Rl("{} in {}"); """Set element glyph."""
463eq = Rl("{} = {}"); """Equality glyph."""
464ge = Rl("{} >= {}"); """Greater or equal glyph."""
465gt = Rl("{} > {}"); """Greater than glyph."""
466le = Rl("{} <= {}"); """Lesser or equal glyph."""
467lt = Rl("{} < {}"); """Lesser than glyph."""
468psdge = Rl("{} >> {}"); """Lesser or equal w.r.t. the p.s.d. cone glyph."""
469psdle = Rl("{} << {}"); """Greater or equal w.r.t. the p.s.d. cone glyph."""
471# Bracket-less function glyphs.
472maxarg = Op("max_{} {}", 3.5, closed=False)
473minarg = Op("min_{} {}", 3.5, closed=False)
476def latin1(rebuildDerivedGlyphs=True):
477 """Let PICOS create future strings using only ISO 8859-1 characters."""
478 # Reset to ASCII first.
479 ascii()
481 # Update glyphs with only template changes.
482 glyphs.compose.template = "{}°{}"
483 glyphs.cubed.template = "{}³"
484 glyphs.hadamard.template = "{}(·){}"
485 glyphs.kron.template = "{}(×){}"
486 glyphs.leadsto.template = "{} » {}"
487 glyphs.mul.template = "{}·{}"
488 glyphs.squared.template = "{}²"
489 glyphs.plsmns.template = "±{}"
490 glyphs.psdge.template = "{} » {}"
491 glyphs.psdle.template = "{} « {}"
492 glyphs.size.template = "{}×{}"
494 # Update all derived glyphs.
495 if rebuildDerivedGlyphs:
496 rebuild()
499def unicode(rebuildDerivedGlyphs=True):
500 """Let PICOS create future strings using only unicode characters."""
501 # Reset to LATIN-1 first.
502 latin1(rebuildDerivedGlyphs=False)
504 # Update glyphs with only template changes.
505 glyphs.and_.template = "{} ∧ {}"
506 glyphs.compose.template = "{}∘{}"
507 glyphs.dotp.template = "⟨{}, {}⟩"
508 glyphs.element.template = "{} ∈ {}"
509 glyphs.forall.template = "{} ∀ {}"
510 glyphs.fromto.template = "{}…{}"
511 glyphs.ge.template = "{} ≥ {}"
512 glyphs.hadamard.template = "{}⊙{}"
513 glyphs.htransp.template = "{}ᴴ"
514 glyphs.infty.template = "∞"
515 glyphs.kron.template = "{}⊗{}"
516 glyphs.lambda_.template = "λ"
517 glyphs.le.template = "{} ≤ {}"
518 glyphs.leadsto.template = "{} → {}"
519 glyphs.norm.template = "‖{}‖"
520 glyphs.or_.template = "{} ∨ {}"
521 glyphs.prod.template = "∏({})"
522 glyphs.psdge.template = "{} ≽ {}"
523 glyphs.psdle.template = "{} ≼ {}"
524 glyphs.sum.template = "∑({})"
525 glyphs.transp.template = "{}ᵀ"
527 # Update all derived glyphs.
528 if rebuildDerivedGlyphs:
529 rebuild()
532# Set and use the default charset.
533if settings.DEFAULT_CHARSET == "unicode":
534 default = unicode
535elif settings.DEFAULT_CHARSET == "latin1":
536 default = latin1
537elif settings.DEFAULT_CHARSET == "ascii":
538 default = ascii
539else:
540 raise ValueError("PICOS doesn't have a charset named '{}'.".format(
541 settings.DEFAULT_CHARSET))
542default()
545# Helper functions that mimic or create additional glyphs.
546# --------------------------------------------------------
548def scalar(value):
549 """Format a scalar value.
551 This function mimics an operator glyph, but it returns a normal string (as
552 opposed to an :class:`OpStr`) for nonnegative numbers.
554 This is not realized as an atomic operator glyph to not increase the
555 recursion depth of :func:`is_negated` and :func:`unnegate` unnecessarily.
557 **Example**
559 >>> from picos.glyphs import scalar
560 >>> str(1.0)
561 '1.0'
562 >>> scalar(1.0)
563 '1'
564 """
565 if not isinstance(value, complex) and value < 0:
566 value = -value
567 negated = True
568 else:
569 negated = False
571 string = ("{:g}" if type(value) is float else "{}").format(value)
573 if negated:
574 return glyphs.neg(string)
575 else:
576 return string
579def shape(theShape):
580 """Describe a matrix shape that can contain wildcards.
582 A wrapper around :obj:`size` that takes just one argument (the shape) that
583 may contain wildcards which are printed as ``'?'``.
584 """
585 newShape = (
586 "?" if theShape[0] is None else theShape[0],
587 "?" if theShape[1] is None else theShape[1])
588 return glyphs.size(*newShape)
591def make_function(*names):
592 """Create an ad-hoc composite function glyphs.
594 **Example**
596 >>> from picos.glyphs import make_function
597 >>> make_function("log", "sum", "exp")("x")
598 'log∘sum∘exp(x)'
599 """
600 return Fn("{}({{}})".format(functools.reduce(glyphs.compose, names)))
603# Helper functions that make context-sensitive use of glyphs.
604# -----------------------------------------------------------
606def from_glyph(string, theGlyph):
607 """Whether the given string was created by the given glyph."""
608 return isinstance(string, GlStr) and string.glyph is theGlyph
611CAN_FACTOR_OUT_NEGATION = (
612 glyphs.matrix,
613 glyphs.sum,
614 glyphs.trace,
615 glyphs.vec,
616 glyphs.diag,
617 glyphs.maindiag,
618 glyphs.real
619) #: Operator glyphs for which negation may be factored out.
622def is_negated(value):
623 """Check if a value can be unnegated by :func:`unnegate`."""
624 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION:
625 return is_negated(value.operands[0])
626 elif from_glyph(value, glyphs.neg):
627 return True
628 elif type(value) is str:
629 try:
630 return float(value) < 0
631 except ValueError:
632 return False
633 elif type(value) in (int, float):
634 return value < 0
635 else:
636 return False
639def unnegate(value):
640 """Unnegate a value, usually a glyph-created string, in a sensible way.
642 Unnegates a :class:`operator glyph created string <OpStr>` or other value in
643 a sensible way, more precisely by recursing through a sequence of glyphs
644 used to create the value and for which we can factor out negation, and
645 negating the underlaying (numeric or string) value.
647 :raises ValueError: When :meth:`is_negated` returns :obj:`False`.
648 """
649 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION:
650 return value.glyph(unnegate(value.operands[0]))
651 elif from_glyph(value, glyphs.neg):
652 return value.operands[0]
653 elif type(value) is str:
654 # We raise any conversion error, because is_negated returns False.
655 return "{:g}".format(-float(value))
656 elif type(value) in (int, float):
657 return -value
658 else:
659 raise ValueError("The value to recursively unnegate is not negated in a"
660 "supported manner.")
663def clever_neg(value):
664 """Describe the negation of a value in a clever way.
666 A wrapper around :attr:`neg` that resorts to unnegating an already negated
667 value.
669 **Example**
671 >>> from picos.glyphs import neg, clever_neg, matrix
672 >>> neg("x")
673 '-x'
674 >>> neg(neg("x"))
675 '-(-x)'
676 >>> clever_neg(neg("x"))
677 'x'
678 >>> neg(matrix(-1))
679 '-[-1]'
680 >>> clever_neg(matrix(-1))
681 '[1]'
682 """
683 if is_negated(value):
684 return unnegate(value)
685 else:
686 return glyphs.neg(value)
689def clever_add(left, right):
690 """Describe the addition of two values in a clever way.
692 A wrapper around :attr:`add` that resorts to :attr:`sub` if the second
693 operand was created by :attr:`neg` or is a negative number (string). In both
694 cases the second operand is adjusted accordingly.
696 **Example**
698 >>> from picos.glyphs import neg, add, clever_add, matrix
699 >>> add("x", neg("y"))
700 'x + -y'
701 >>> clever_add("x", neg("y"))
702 'x - y'
703 >>> add("X", matrix(neg("y")))
704 'X + [-y]'
705 >>> clever_add("X", matrix(neg("y")))
706 'X - [y]'
707 >>> clever_add("X", matrix(-1.5))
708 'X - [1.5]'
709 """
710 if left in (0, "0"):
711 return right
713 if right in (0, "0"):
714 return left
716 if is_negated(right):
717 return glyphs.sub(left, unnegate(right))
718 else:
719 return glyphs.add(left, right)
722def clever_sub(left, right):
723 """Describe the substraction of a value from another in a clever way.
725 A wrapper around :attr:`sub` that resorts to :attr:`add` if the second
726 operand was created by :attr:`neg` or is a negative number(string). In both
727 cases the second operand is adjusted accordingly.
729 **Example**
731 >>> from picos.glyphs import neg, sub, clever_sub, matrix
732 >>> sub("x", neg("y"))
733 'x - -y'
734 >>> clever_sub("x", neg("y"))
735 'x + y'
736 >>> sub("X", matrix(neg("y")))
737 'X - [-y]'
738 >>> clever_sub("X", matrix(neg("y")))
739 'X + [y]'
740 >>> clever_sub("X", matrix(-1.5))
741 'X + [1.5]'
742 """
743 if left in (0, "0"):
744 return clever_neg(right)
746 if right in (0, "0"):
747 return left
749 if is_negated(right):
750 return glyphs.add(left, unnegate(right))
751 else:
752 return glyphs.sub(left, right)
755def clever_mul(left, right):
756 """Describe the multiplocation of two values in a clever way.
758 A wrapper around :attr:`mul` that factors out identity factors.
759 """
760 # Apply a factor of zero on the left side.
761 if left in (0, "0"):
762 return left
764 # Apply a factor of zero on the right side.
765 if right in (0, "0"):
766 return right
768 # Detect quadratics.
769 if left == right:
770 return glyphs.squared(left)
772 # Factor out a scalar one on the left side.
773 if left in (1, "1"):
774 return right
776 # Factor out a scalar one on the right side.
777 if right in (1, "1"):
778 return left
780 # Factor out negation.
781 ln, rn = is_negated(left), is_negated(right)
782 if ln and rn:
783 return glyphs.clever_mul(unnegate(left), unnegate(right))
784 elif ln:
785 return glyphs.neg(glyphs.clever_mul(unnegate(left), right))
786 elif rn:
787 return glyphs.neg(glyphs.clever_mul(left, unnegate(right)))
789 return glyphs.mul(left, right)
792def clever_div(left, right):
793 """Describe the division of one value by another in a clever way.
795 A wrapper around :attr:`div` that factors out identity factors.
796 """
797 # Apply a factor of zero on the left side.
798 if left in (0, "0"):
799 return left
801 # Factor out a scalar one on the right side.
802 if right in (1, "1"):
803 return left
805 # Factor out negation.
806 ln, rn = is_negated(left), is_negated(right)
807 if ln and rn:
808 return glyphs.clever_div(unnegate(left), unnegate(right))
809 elif ln:
810 return glyphs.neg(glyphs.clever_div(unnegate(left), right))
811 elif rn:
812 return glyphs.neg(glyphs.clever_div(left, unnegate(right)))
814 return glyphs.div(left, right)
817def clever_dotp(left, right, complexRHS, scalar=False):
818 """Describe an inner product in a clever way.
820 :param bool complexRHS: Whether the right hand side is complex.
821 """
822 riCo = glyphs.conj(right) if complexRHS else right
824 if scalar:
825 return glyphs.clever_mul(left, right)
827 if from_glyph(left, glyphs.idmatrix):
828 return glyphs.trace(riCo)
829 elif from_glyph(riCo, glyphs.idmatrix):
830 return glyphs.trace(left)
831 elif from_glyph(left, glyphs.matrix) and left.operands[0] in (1, "1"):
832 return glyphs.sum(riCo)
833 elif from_glyph(riCo, glyphs.matrix) and riCo.operands[0] in (1, "1"):
834 return glyphs.sum(left)
835 elif left == right:
836 return glyphs.squared(glyphs.norm(left))
837 else:
838 return glyphs.dotp(left, right)
841def matrix_cat(left, right, horizontal=True):
842 """Describe matrix concatenation in a clever way.
844 A wrapper around :attr:`matrix`, :attr:`horicat` and :attr:`vertcat`.
846 **Example**
848 >>> from picos.glyphs import matrix_cat
849 >>> Z = matrix_cat("X", "Y")
850 >>> Z
851 '[X, Y]'
852 >>> matrix_cat(Z, Z)
853 '[X, Y, X, Y]'
854 >>> matrix_cat(Z, Z, horizontal = False)
855 '[X, Y; X, Y]'
856 """
857 if isinstance(left, OpStr) and left.glyph is glyphs.matrix:
858 left = left.operands[0]
860 if isinstance(right, OpStr) and right.glyph is glyphs.matrix:
861 right = right.operands[0]
863 catGlyph = glyphs.horicat if horizontal else glyphs.vertcat
865 return glyphs.matrix(catGlyph(left, right))
868def row_vectorize(*entries):
869 """Describe a row vector with the given symbolic entries."""
870 return functools.reduce(matrix_cat, entries)
873def col_vectorize(*entries):
874 """Describe a column vector with the given symbolic entries."""
875 return functools.reduce(lambda l, r: matrix_cat(l, r, False), entries)
878def free_var_name(string):
879 """Return a variable name not present in the given string."""
880 names = "xyzpqrstuvwabcdefghijklmno"
881 for name in names:
882 if name not in string:
883 return name
884 return "?"
887# --------------------------------------
888__all__ = api_end(_API_START, globals())