Hide keyboard shortcuts

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

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

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({}, {})"); """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.""" 

424 

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

436 

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

446 

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

456 

457# Concatenation glyphs. 

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

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

460 

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

470 

471# Bracket-less function glyphs. 

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

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

474 

475 

476def latin1(rebuildDerivedGlyphs=True): 

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

478 # Reset to ASCII first. 

479 ascii() 

480 

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

493 

494 # Update all derived glyphs. 

495 if rebuildDerivedGlyphs: 

496 rebuild() 

497 

498 

499def unicode(rebuildDerivedGlyphs=True): 

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

501 # Reset to LATIN-1 first. 

502 latin1(rebuildDerivedGlyphs=False) 

503 

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

526 

527 # Update all derived glyphs. 

528 if rebuildDerivedGlyphs: 

529 rebuild() 

530 

531 

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

543 

544 

545# Helper functions that mimic or create additional glyphs. 

546# -------------------------------------------------------- 

547 

548def scalar(value): 

549 """Format a scalar value. 

550 

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

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

553 

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. 

556 

557 **Example** 

558 

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 

570 

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

572 

573 if negated: 

574 return glyphs.neg(string) 

575 else: 

576 return string 

577 

578 

579def shape(theShape): 

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

581 

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) 

589 

590 

591def make_function(*names): 

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

593 

594 **Example** 

595 

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

601 

602 

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

604# ----------------------------------------------------------- 

605 

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 

609 

610 

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. 

620 

621 

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 

637 

638 

639def unnegate(value): 

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

641 

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. 

646 

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

661 

662 

663def clever_neg(value): 

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

665 

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

667 value. 

668 

669 **Example** 

670 

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) 

687 

688 

689def clever_add(left, right): 

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

691 

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. 

695 

696 **Example** 

697 

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 

712 

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

714 return left 

715 

716 if is_negated(right): 

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

718 else: 

719 return glyphs.add(left, right) 

720 

721 

722def clever_sub(left, right): 

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

724 

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. 

728 

729 **Example** 

730 

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) 

745 

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

747 return left 

748 

749 if is_negated(right): 

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

751 else: 

752 return glyphs.sub(left, right) 

753 

754 

755def clever_mul(left, right): 

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

757 

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 

763 

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

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

766 return right 

767 

768 # Detect quadratics. 

769 if left == right: 

770 return glyphs.squared(left) 

771 

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

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

774 return right 

775 

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

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

778 return left 

779 

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

788 

789 return glyphs.mul(left, right) 

790 

791 

792def clever_div(left, right): 

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

794 

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 

800 

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

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

803 return left 

804 

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

813 

814 return glyphs.div(left, right) 

815 

816 

817def clever_dotp(left, right, complexRHS, scalar=False): 

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

819 

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

821 """ 

822 riCo = glyphs.conj(right) if complexRHS else right 

823 

824 if scalar: 

825 return glyphs.clever_mul(left, right) 

826 

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) 

839 

840 

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

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

843 

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

845 

846 **Example** 

847 

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] 

859 

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

861 right = right.operands[0] 

862 

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

864 

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

866 

867 

868def row_vectorize(*entries): 

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

870 return functools.reduce(matrix_cat, entries) 

871 

872 

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) 

876 

877 

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

885 

886 

887# -------------------------------------- 

888__all__ = api_end(_API_START, globals())