Coverage for picos/glyphs.py: 92.09%

392 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-12 07:53 +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# ------------------------------------------------------------------------------ 

18 

19"""String templates used to print (algebraic) expressions. 

20 

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. 

30 

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: 

34 

35 >>> import picos 

36 >>> X = picos.SymmetricVariable("X", (2,2)) 

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 

45 

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. 

49 

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)' 

59 

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: 

63 

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 

70 

71You can reset all glyphs to their initial state as follows: 

72 

73 >>> picos.glyphs.default() 

74""" 

75 

76import functools 

77import sys 

78 

79from . import settings 

80from .apidoc import api_end, api_start 

81 

82# Allow functions to modify this module directly. 

83glyphs = sys.modules[__name__] 

84 

85_API_START = api_start(globals()) 

86# ------------------------------- 

87 

88 

89# Definitions of glyph classes and the rich strings they create. 

90# -------------------------------------------------------------- 

91 

92class GlStr(str): 

93 """A string created from a :class:`glyph <Gl>`. 

94 

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 """ 

98 

99 def __new__(cls, string, glyph, operands): 

100 """Create a regular Python string.""" 

101 return str.__new__(cls, string) 

102 

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.""" 

107 

108 self.operands = operands 

109 """The operands used to create the string.""" 

110 

111 def __copy__(self): 

112 return self.__class__(str(self), self.glyph, self.operands) 

113 

114 def reglyphed(self, replace={}): 

115 """Returns a rebuilt version of the string using current glyphs. 

116 

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)) 

123 

124 

125class Gl: 

126 """The basic "glyph", an (algebraic) string formatting template. 

127 

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 """ 

133 

134 def __init__(self, glyph): 

135 """Construct a glyph. 

136 

137 :param str glyph: The glyph's format string template. 

138 """ 

139 self.template = glyph 

140 self.initial = glyph 

141 

142 def reset(self): 

143 """Reset the glyph to its initial formatting template.""" 

144 self.template = self.initial 

145 

146 def update(self, new): 

147 """Change the glyph's formatting template.""" 

148 self.template = new.template 

149 

150 def rebuild(self): 

151 """If the template was created using other glyphs, rebuild it. 

152 

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 

161 

162 def __call__(self, *args): 

163 """Format the arguments as a :class:`GlStr`.""" 

164 return GlStr(self.template.format(*args), self, args) 

165 

166 

167class OpStr(GlStr): 

168 """A string created from a math operator glyph.""" 

169 

170 pass 

171 

172 

173class Op(Gl): 

174 """The basic math operator glyph.""" 

175 

176 def __init__(self, glyph, order, assoc=False, closed=False): 

177 """Construct a math operator glyph. 

178 

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() 

196 

197 def reset(self): 

198 """Reset the glyph to its initial behavior.""" 

199 self.template, self.order, self.assoc, self.closed = self.initial 

200 

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 

207 

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) 

212 

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 

232 

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 = "{}" 

241 

242 if parenthesis: 

243 placeholders.append(glyphs.parenth(placeholder)) 

244 else: 

245 placeholders.append(placeholder) 

246 

247 return OpStr(self.template.format(*placeholders).format(*operands), 

248 self, operands) 

249 

250 

251class Am(Op): 

252 """A math atom glyph.""" 

253 

254 def __init__(self, glyph): 

255 """Construct an :class:`Am` glyph. 

256 

257 :param str glyph: The glyph's format string template. 

258 """ 

259 Op.__init__(self, glyph, 0) 

260 

261 

262class Br(Op): 

263 """A math operator glyph with enclosing brackets.""" 

264 

265 def __init__(self, glyph): 

266 """Construct a :class:`Br` glyph. 

267 

268 :param str glyph: The glyph's format string template. 

269 """ 

270 Op.__init__(self, glyph, 0, closed=True) 

271 

272 

273class Fn(Op): 

274 """A math operator glyph in function form.""" 

275 

276 def __init__(self, glyph): 

277 """Construct a :class:`Fn` glyph. 

278 

279 :param str glyph: The glyph's format string template. 

280 """ 

281 Op.__init__(self, glyph, 0, closed=True) 

282 

283 

284class Tr(Op): 

285 """A math glyph in superscript/trailer form.""" 

286 

287 def __init__(self, glyph): 

288 """Construct a :class:`Tr` glyph. 

289 

290 :param str glyph: The glyph's format string template. 

291 """ 

292 Op.__init__(self, glyph, 1) 

293 

294 

295class Rl(Op): 

296 """A math relation glyph.""" 

297 

298 def __init__(self, glyph): 

299 """Construct a :class:`Rl` glyph. 

300 

301 :param str glyph: The glyph's format string template. 

302 """ 

303 Op.__init__(self, glyph, 5, assoc=True) 

304 

305 

306# Functions that show, reset or rebuild the glyph objects. 

307# -------------------------------------------------------- 

308 

309def show(*args): 

310 """Show output from all glyphs. 

311 

312 :param list(str) args: Strings to use as glyph operands. 

313 """ 

314 args = list(args) + ["{}"]*4 

315 

316 print("{:8} | {:3} | {:5} | {}\n{}+{}+{}+{}".format( 

317 "Glyph", "Pri", "Asso", "Value", "-"*9, "-"*5, "-"*7, "-"*10)) 

318 

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))) 

326 

327 

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 

334 

335 raise Exception("Maximum recursion depth for glyph rebuilding reached. " 

336 "There is likely a cyclic dependence between them.") 

337 

338 

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() 

344 

345 

346# Initial glyph definitions and functions that update them. 

347# --------------------------------------------------------- 

348 

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.""" 

370 

371# Atomic glyphs. 

372infty = Am("inf"); """Infinity glyph.""" 

373idmatrix = Am("I"); """Identity matrix glyph.""" 

374lambda_ = Am("lambda"); """Lambda symbol glyph.""" 

375 

376# Bracketed glyphs. 

377matrix = Br("[{}]"); """Matrix glyph.""" 

378dotp = Br("<{}, {}>"); """Scalar product glyph.""" 

379abs = Br("|{}|"); """Absolute value glyph.""" 

380norm = Br("||{}||"); """Norm glyph.""" 

381 

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.""" 

391 

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.""" 

425qe = Fn("S({})"); """Quantum entropy glyph.""" 

426qre = Fn("S({}||{})"); """Quantum relative entropy glyph.""" 

427renyi = Fn("S_({})({}||{})"); """Renyi entropy glyph.""" 

428 

429# Semi-closed glyphs. 

430ptrace = Op("trace_{}({})", 0, closed=[False, True]) 

431"""Matrix p-Trace glyph.""" 

432slice = Op("{}[{}]", 0, closed=[False, True]) 

433"""Expression slicing glyph.""" 

434ptransp_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True]) 

435"""Matrix partial transposition glyph.""" # TODO: Replace ptransp. 

436ptrace_ = Op("{}.{{{{{}}}}}", 1, closed=[False, True]) 

437"""Matrix partial trace glyph.""" # TODO: Replace ptrace_. 

438exparg = Op("E_{}({})", 0, closed=[False, True]) 

439"""Expected value glyph.""" 

440 

441# Basic algebraic glyphs. 

442add = Op("{} + {}", 3, assoc=True); """Addition glyph.""" 

443sub = Op("{} - {}", 3, assoc=False); """Substraction glyph.""" 

444hadamard = Op("{}(o){}", 2, assoc=True); """Hadamard product glyph.""" 

445kron = Op("{}(x){}", 2, assoc=True); """Kronecker product glyph.""" 

446mul = Op("{}*{}", 2, assoc=True); """Multiplication glyph.""" 

447div = Op("{}/{}", 2, assoc=False); """Division glyph.""" 

448neg = Op("-{}", 2.5); """Negation glyph.""" 

449plsmns = Op("[+/-]{}", 2.5); """Plus/Minus glyph.""" 

450 

451# Matrix geometric mean glyphs. 

452geomean = Op("{} # {}", 3, assoc=False) 

453"""Matrix geometric mean glyph.""" 

454wgeomean = Op("{} #_{} {}", 3, assoc=False) 

455"""Weighted matrix geometric mean glyph.""" 

456 

457# Trailer glyphs. 

458power = Tr("{}^{}"); """Power glyph.""" 

459cubed = Tr(glyphs.power("{}", "3")); """Cubed value glyph.""" 

460squared = Tr(glyphs.power("{}", "2")); """Squared value glyph.""" 

461inverse = Tr(glyphs.power("{}", glyphs.neg(1))); """Matrix inverse glyph.""" 

462transp = Tr("{}.T"); """Matrix transposition glyph.""" 

463ptransp = Tr("{}.Tx"); """Matrix partial transposition glyph.""" 

464htransp = Tr("{}.H"); """Matrix hermitian transposition glyph.""" 

465index = Tr("{}_{}"); """Index glyph.""" 

466 

467# Concatenation glyphs. 

468horicat = Op("{}, {}", 4, assoc=True); """Horizontal concatenation glyph.""" 

469vertcat = Op("{}; {}", 4.5, assoc=True); """Vertical concatenation glyph.""" 

470 

471# Relation glyphs. 

472element = Rl("{} in {}"); """Set element glyph.""" 

473eq = Rl("{} = {}"); """Equality glyph.""" 

474ge = Rl("{} >= {}"); """Greater or equal glyph.""" 

475gt = Rl("{} > {}"); """Greater than glyph.""" 

476le = Rl("{} <= {}"); """Lesser or equal glyph.""" 

477lt = Rl("{} < {}"); """Lesser than glyph.""" 

478psdge = Rl("{} >> {}"); """Lesser or equal w.r.t. the p.s.d. cone glyph.""" 

479psdle = Rl("{} << {}"); """Greater or equal w.r.t. the p.s.d. cone glyph.""" 

480 

481# Bracket-less function glyphs. 

482maxarg = Op("max_{} {}", 3.5, closed=False) 

483minarg = Op("min_{} {}", 3.5, closed=False) 

484 

485 

486def latin1(rebuildDerivedGlyphs=True): 

487 """Let PICOS create future strings using only ISO 8859-1 characters.""" 

488 # Reset to ASCII first. 

489 ascii() 

490 

491 # Update glyphs with only template changes. 

492 glyphs.compose.template = "{}°{}" 

493 glyphs.cubed.template = "{}³" 

494 glyphs.hadamard.template = "{}(·){}" 

495 glyphs.kron.template = "{}(×){}" 

496 glyphs.leadsto.template = "{} » {}" 

497 glyphs.mul.template = "{}·{}" 

498 glyphs.squared.template = "{}²" 

499 glyphs.plsmns.template = "±{}" 

500 glyphs.psdge.template = "{} » {}" 

501 glyphs.psdle.template = "{} « {}" 

502 glyphs.size.template = "{}×{}" 

503 

504 # Update all derived glyphs. 

505 if rebuildDerivedGlyphs: 

506 rebuild() 

507 

508 

509def unicode(rebuildDerivedGlyphs=True): 

510 """Let PICOS create future strings using only unicode characters.""" 

511 # Reset to LATIN-1 first. 

512 latin1(rebuildDerivedGlyphs=False) 

513 

514 # Update glyphs with only template changes. 

515 glyphs.and_.template = "{} ∧ {}" 

516 glyphs.compose.template = "{}∘{}" 

517 glyphs.dotp.template = "⟨{}, {}⟩" 

518 glyphs.element.template = "{} ∈ {}" 

519 glyphs.forall.template = "{} ∀ {}" 

520 glyphs.fromto.template = "{}…{}" 

521 glyphs.ge.template = "{} ≥ {}" 

522 glyphs.hadamard.template = "{}⊙{}" 

523 glyphs.htransp.template = "{}ᴴ" 

524 glyphs.infty.template = "∞" 

525 glyphs.kron.template = "{}⊗{}" 

526 glyphs.lambda_.template = "λ" 

527 glyphs.le.template = "{} ≤ {}" 

528 glyphs.leadsto.template = "{} → {}" 

529 glyphs.norm.template = "‖{}‖" 

530 glyphs.or_.template = "{} ∨ {}" 

531 glyphs.prod.template = "∏({})" 

532 glyphs.psdge.template = "{} ≽ {}" 

533 glyphs.psdle.template = "{} ≼ {}" 

534 glyphs.sum.template = "∑({})" 

535 glyphs.transp.template = "{}ᵀ" 

536 glyphs.qre.template = "S({}‖{})" 

537 

538 # Update all derived glyphs. 

539 if rebuildDerivedGlyphs: 

540 rebuild() 

541 

542 

543# Set and use the default charset. 

544if settings.DEFAULT_CHARSET == "unicode": 

545 default = unicode 

546elif settings.DEFAULT_CHARSET == "latin1": 

547 default = latin1 

548elif settings.DEFAULT_CHARSET == "ascii": 

549 default = ascii 

550else: 

551 raise ValueError("PICOS doesn't have a charset named '{}'.".format( 

552 settings.DEFAULT_CHARSET)) 

553default() 

554 

555 

556# Helper functions that mimic or create additional glyphs. 

557# -------------------------------------------------------- 

558 

559def scalar(value): 

560 """Format a scalar value. 

561 

562 This function mimics an operator glyph, but it returns a normal string (as 

563 opposed to an :class:`OpStr`) for nonnegative numbers. 

564 

565 This is not realized as an atomic operator glyph to not increase the 

566 recursion depth of :func:`is_negated` and :func:`unnegate` unnecessarily. 

567 

568 **Example** 

569 

570 >>> from picos.glyphs import scalar 

571 >>> str(1.0) 

572 '1.0' 

573 >>> scalar(1.0) 

574 '1' 

575 """ 

576 if not isinstance(value, complex) and value < 0: 

577 value = -value 

578 negated = True 

579 else: 

580 negated = False 

581 

582 string = ("{:g}" if type(value) is float else "{}").format(value) 

583 

584 if negated: 

585 return glyphs.neg(string) 

586 else: 

587 return string 

588 

589 

590def shape(theShape): 

591 """Describe a matrix shape that can contain wildcards. 

592 

593 A wrapper around :obj:`size` that takes just one argument (the shape) that 

594 may contain wildcards which are printed as ``'?'``. 

595 """ 

596 newShape = ( 

597 "?" if theShape[0] is None else theShape[0], 

598 "?" if theShape[1] is None else theShape[1]) 

599 return glyphs.size(*newShape) 

600 

601 

602def make_function(*names): 

603 """Create an ad-hoc composite function glyphs. 

604 

605 **Example** 

606 

607 >>> from picos.glyphs import make_function 

608 >>> make_function("log", "sum", "exp")("x") 

609 'log∘sum∘exp(x)' 

610 """ 

611 return Fn("{}({{}})".format(functools.reduce(glyphs.compose, names))) 

612 

613 

614# Helper functions that make context-sensitive use of glyphs. 

615# ----------------------------------------------------------- 

616 

617def from_glyph(string, theGlyph): 

618 """Whether the given string was created by the given glyph.""" 

619 return isinstance(string, GlStr) and string.glyph is theGlyph 

620 

621 

622CAN_FACTOR_OUT_NEGATION = ( 

623 glyphs.matrix, 

624 glyphs.sum, 

625 glyphs.trace, 

626 glyphs.vec, 

627 glyphs.diag, 

628 glyphs.maindiag, 

629 glyphs.real 

630) #: Operator glyphs for which negation may be factored out. 

631 

632 

633def is_negated(value): 

634 """Check if a value can be unnegated by :func:`unnegate`.""" 

635 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION: 

636 return is_negated(value.operands[0]) 

637 elif from_glyph(value, glyphs.neg): 

638 return True 

639 elif type(value) is str: 

640 try: 

641 return float(value) < 0 

642 except ValueError: 

643 return False 

644 elif type(value) in (int, float): 

645 return value < 0 

646 else: 

647 return False 

648 

649 

650def unnegate(value): 

651 """Unnegate a value, usually a glyph-created string, in a sensible way. 

652 

653 Unnegates a :class:`operator glyph created string <OpStr>` or other value in 

654 a sensible way, more precisely by recursing through a sequence of glyphs 

655 used to create the value and for which we can factor out negation, and 

656 negating the underlaying (numeric or string) value. 

657 

658 :raises ValueError: When :meth:`is_negated` returns :obj:`False`. 

659 """ 

660 if isinstance(value, OpStr) and value.glyph in CAN_FACTOR_OUT_NEGATION: 

661 return value.glyph(unnegate(value.operands[0])) 

662 elif from_glyph(value, glyphs.neg): 

663 return value.operands[0] 

664 elif type(value) is str: 

665 # We raise any conversion error, because is_negated returns False. 

666 return "{:g}".format(-float(value)) 

667 elif type(value) in (int, float): 

668 return -value 

669 else: 

670 raise ValueError("The value to recursively unnegate is not negated in a" 

671 "supported manner.") 

672 

673 

674def clever_neg(value): 

675 """Describe the negation of a value in a clever way. 

676 

677 A wrapper around :attr:`neg` that resorts to unnegating an already negated 

678 value. 

679 

680 **Example** 

681 

682 >>> from picos.glyphs import neg, clever_neg, matrix 

683 >>> neg("x") 

684 '-x' 

685 >>> neg(neg("x")) 

686 '-(-x)' 

687 >>> clever_neg(neg("x")) 

688 'x' 

689 >>> neg(matrix(-1)) 

690 '-[-1]' 

691 >>> clever_neg(matrix(-1)) 

692 '[1]' 

693 """ 

694 if is_negated(value): 

695 return unnegate(value) 

696 else: 

697 return glyphs.neg(value) 

698 

699 

700def clever_add(left, right): 

701 """Describe the addition of two values in a clever way. 

702 

703 A wrapper around :attr:`add` that resorts to :attr:`sub` if the second 

704 operand was created by :attr:`neg` or is a negative number (string). In both 

705 cases the second operand is adjusted accordingly. 

706 

707 **Example** 

708 

709 >>> from picos.glyphs import neg, add, clever_add, matrix 

710 >>> add("x", neg("y")) 

711 'x + -y' 

712 >>> clever_add("x", neg("y")) 

713 'x - y' 

714 >>> add("X", matrix(neg("y"))) 

715 'X + [-y]' 

716 >>> clever_add("X", matrix(neg("y"))) 

717 'X - [y]' 

718 >>> clever_add("X", matrix(-1.5)) 

719 'X - [1.5]' 

720 """ 

721 if left in (0, "0"): 

722 return right 

723 

724 if right in (0, "0"): 

725 return left 

726 

727 if is_negated(right): 

728 return glyphs.sub(left, unnegate(right)) 

729 else: 

730 return glyphs.add(left, right) 

731 

732 

733def clever_sub(left, right): 

734 """Describe the substraction of a value from another in a clever way. 

735 

736 A wrapper around :attr:`sub` that resorts to :attr:`add` if the second 

737 operand was created by :attr:`neg` or is a negative number(string). In both 

738 cases the second operand is adjusted accordingly. 

739 

740 **Example** 

741 

742 >>> from picos.glyphs import neg, sub, clever_sub, matrix 

743 >>> sub("x", neg("y")) 

744 'x - -y' 

745 >>> clever_sub("x", neg("y")) 

746 'x + y' 

747 >>> sub("X", matrix(neg("y"))) 

748 'X - [-y]' 

749 >>> clever_sub("X", matrix(neg("y"))) 

750 'X + [y]' 

751 >>> clever_sub("X", matrix(-1.5)) 

752 'X + [1.5]' 

753 """ 

754 if left in (0, "0"): 

755 return clever_neg(right) 

756 

757 if right in (0, "0"): 

758 return left 

759 

760 if is_negated(right): 

761 return glyphs.add(left, unnegate(right)) 

762 else: 

763 return glyphs.sub(left, right) 

764 

765 

766def clever_mul(left, right): 

767 """Describe the multiplocation of two values in a clever way. 

768 

769 A wrapper around :attr:`mul` that factors out identity factors. 

770 """ 

771 # Apply a factor of zero on the left side. 

772 if left in (0, "0"): 

773 return left 

774 

775 # Apply a factor of zero on the right side. 

776 if right in (0, "0"): 

777 return right 

778 

779 # Factor out a scalar one on the left side. 

780 if left in (1, "1"): 

781 return right 

782 

783 # Factor out a scalar one on the right side. 

784 if right in (1, "1"): 

785 return left 

786 

787 # Detect quadratics. 

788 if left == right: 

789 return glyphs.squared(left) 

790 

791 # Factor out negation. 

792 ln, rn = is_negated(left), is_negated(right) 

793 if ln and rn: 

794 return glyphs.clever_mul(unnegate(left), unnegate(right)) 

795 elif ln: 

796 return glyphs.neg(glyphs.clever_mul(unnegate(left), right)) 

797 elif rn: 

798 return glyphs.neg(glyphs.clever_mul(left, unnegate(right))) 

799 

800 return glyphs.mul(left, right) 

801 

802 

803def clever_div(left, right): 

804 """Describe the division of one value by another in a clever way. 

805 

806 A wrapper around :attr:`div` that factors out identity factors. 

807 """ 

808 # Apply a factor of zero on the left side. 

809 if left in (0, "0"): 

810 return left 

811 

812 # Factor out a scalar one on the right side. 

813 if right in (1, "1"): 

814 return left 

815 

816 # Factor out negation. 

817 ln, rn = is_negated(left), is_negated(right) 

818 if ln and rn: 

819 return glyphs.clever_div(unnegate(left), unnegate(right)) 

820 elif ln: 

821 return glyphs.neg(glyphs.clever_div(unnegate(left), right)) 

822 elif rn: 

823 return glyphs.neg(glyphs.clever_div(left, unnegate(right))) 

824 

825 return glyphs.div(left, right) 

826 

827 

828def clever_dotp(left, right, complexLHS, scalar=False): 

829 """Describe an inner product in a clever way. 

830 

831 :param bool complexRHS: Whether the right hand side is complex. 

832 """ 

833 lconj = glyphs.conj(left) if complexLHS else left 

834 

835 # Detect scalar multiplication. 

836 if scalar: 

837 if left == right: 

838 if complexLHS: 

839 return glyphs.squared(glyphs.abs(left)) 

840 else: 

841 return glyphs.squared(left) 

842 else: 

843 return glyphs.clever_mul(lconj, right) 

844 

845 # Detect identities. 

846 if from_glyph(lconj, glyphs.idmatrix): 

847 return glyphs.trace(right) 

848 if from_glyph(right, glyphs.idmatrix): 

849 return glyphs.trace(lconj) 

850 

851 # Detect sums. 

852 if from_glyph(lconj, glyphs.matrix) and lconj.operands[0] in (1, "1"): 

853 return glyphs.sum(right) 

854 if from_glyph(right, glyphs.matrix) and right.operands[0] in (1, "1"): 

855 return glyphs.sum(lconj) 

856 

857 # Fall back to the inner product glyph. 

858 return glyphs.dotp(left, right) 

859 

860 

861def matrix_cat(left, right, horizontal=True): 

862 """Describe matrix concatenation in a clever way. 

863 

864 A wrapper around :attr:`matrix`, :attr:`horicat` and :attr:`vertcat`. 

865 

866 **Example** 

867 

868 >>> from picos.glyphs import matrix_cat 

869 >>> Z = matrix_cat("X", "Y") 

870 >>> Z 

871 '[X, Y]' 

872 >>> matrix_cat(Z, Z) 

873 '[X, Y, X, Y]' 

874 >>> matrix_cat(Z, Z, horizontal = False) 

875 '[X, Y; X, Y]' 

876 """ 

877 if isinstance(left, OpStr) and left.glyph is glyphs.matrix: 

878 left = left.operands[0] 

879 

880 if isinstance(right, OpStr) and right.glyph is glyphs.matrix: 

881 right = right.operands[0] 

882 

883 catGlyph = glyphs.horicat if horizontal else glyphs.vertcat 

884 

885 return glyphs.matrix(catGlyph(left, right)) 

886 

887 

888def row_vectorize(*entries): 

889 """Describe a row vector with the given symbolic entries.""" 

890 return functools.reduce(matrix_cat, entries) 

891 

892 

893def col_vectorize(*entries): 

894 """Describe a column vector with the given symbolic entries.""" 

895 return functools.reduce(lambda l, r: matrix_cat(l, r, False), entries) 

896 

897 

898def free_var_name(string): 

899 """Return a variable name not present in the given string.""" 

900 names = "xyzpqrstuvwabcdefghijklmno" 

901 for name in names: 

902 if name not in string: 

903 return name 

904 return "?" 

905 

906 

907# -------------------------------------- 

908__all__ = api_end(_API_START, globals())