Coverage for picos/glyphs.py: 92.17%

383 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +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.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({}, {})"); """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.""" 

425 

426# Semi-closed glyphs. 

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

428"""Matrix p-Trace glyph.""" 

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

430"""Expression slicing glyph.""" 

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

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

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

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

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

436"""Expected value glyph.""" 

437 

438# Basic algebraic glyphs. 

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

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

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

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

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

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

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

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

447 

448# Trailer glyphs. 

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

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

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

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

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

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

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

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

457 

458# Concatenation glyphs. 

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

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

461 

462# Relation glyphs. 

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

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

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

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

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

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

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

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

471 

472# Bracket-less function glyphs. 

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

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

475 

476 

477def latin1(rebuildDerivedGlyphs=True): 

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

479 # Reset to ASCII first. 

480 ascii() 

481 

482 # Update glyphs with only template changes. 

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

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

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

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

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

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

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

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

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

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

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

494 

495 # Update all derived glyphs. 

496 if rebuildDerivedGlyphs: 

497 rebuild() 

498 

499 

500def unicode(rebuildDerivedGlyphs=True): 

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

502 # Reset to LATIN-1 first. 

503 latin1(rebuildDerivedGlyphs=False) 

504 

505 # Update glyphs with only template changes. 

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

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

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

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

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

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

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

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

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

515 glyphs.infty.template = "∞" 

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

517 glyphs.lambda_.template = "λ" 

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

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

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

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

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

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

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

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

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

527 

528 # Update all derived glyphs. 

529 if rebuildDerivedGlyphs: 

530 rebuild() 

531 

532 

533# Set and use the default charset. 

534if settings.DEFAULT_CHARSET == "unicode": 

535 default = unicode 

536elif settings.DEFAULT_CHARSET == "latin1": 

537 default = latin1 

538elif settings.DEFAULT_CHARSET == "ascii": 

539 default = ascii 

540else: 

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

542 settings.DEFAULT_CHARSET)) 

543default() 

544 

545 

546# Helper functions that mimic or create additional glyphs. 

547# -------------------------------------------------------- 

548 

549def scalar(value): 

550 """Format a scalar value. 

551 

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

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

554 

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

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

557 

558 **Example** 

559 

560 >>> from picos.glyphs import scalar 

561 >>> str(1.0) 

562 '1.0' 

563 >>> scalar(1.0) 

564 '1' 

565 """ 

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

567 value = -value 

568 negated = True 

569 else: 

570 negated = False 

571 

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

573 

574 if negated: 

575 return glyphs.neg(string) 

576 else: 

577 return string 

578 

579 

580def shape(theShape): 

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

582 

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

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

585 """ 

586 newShape = ( 

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

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

589 return glyphs.size(*newShape) 

590 

591 

592def make_function(*names): 

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

594 

595 **Example** 

596 

597 >>> from picos.glyphs import make_function 

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

599 'log∘sum∘exp(x)' 

600 """ 

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

602 

603 

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

605# ----------------------------------------------------------- 

606 

607def from_glyph(string, theGlyph): 

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

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

610 

611 

612CAN_FACTOR_OUT_NEGATION = ( 

613 glyphs.matrix, 

614 glyphs.sum, 

615 glyphs.trace, 

616 glyphs.vec, 

617 glyphs.diag, 

618 glyphs.maindiag, 

619 glyphs.real 

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

621 

622 

623def is_negated(value): 

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

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

626 return is_negated(value.operands[0]) 

627 elif from_glyph(value, glyphs.neg): 

628 return True 

629 elif type(value) is str: 

630 try: 

631 return float(value) < 0 

632 except ValueError: 

633 return False 

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

635 return value < 0 

636 else: 

637 return False 

638 

639 

640def unnegate(value): 

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

642 

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

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

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

646 negating the underlaying (numeric or string) value. 

647 

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

649 """ 

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

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

652 elif from_glyph(value, glyphs.neg): 

653 return value.operands[0] 

654 elif type(value) is str: 

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

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

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

658 return -value 

659 else: 

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

661 "supported manner.") 

662 

663 

664def clever_neg(value): 

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

666 

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

668 value. 

669 

670 **Example** 

671 

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

673 >>> neg("x") 

674 '-x' 

675 >>> neg(neg("x")) 

676 '-(-x)' 

677 >>> clever_neg(neg("x")) 

678 'x' 

679 >>> neg(matrix(-1)) 

680 '-[-1]' 

681 >>> clever_neg(matrix(-1)) 

682 '[1]' 

683 """ 

684 if is_negated(value): 

685 return unnegate(value) 

686 else: 

687 return glyphs.neg(value) 

688 

689 

690def clever_add(left, right): 

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

692 

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

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

695 cases the second operand is adjusted accordingly. 

696 

697 **Example** 

698 

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

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

701 'x + -y' 

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

703 'x - y' 

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

705 'X + [-y]' 

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

707 'X - [y]' 

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

709 'X - [1.5]' 

710 """ 

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

712 return right 

713 

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

715 return left 

716 

717 if is_negated(right): 

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

719 else: 

720 return glyphs.add(left, right) 

721 

722 

723def clever_sub(left, right): 

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

725 

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

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

728 cases the second operand is adjusted accordingly. 

729 

730 **Example** 

731 

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

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

734 'x - -y' 

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

736 'x + y' 

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

738 'X - [-y]' 

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

740 'X + [y]' 

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

742 'X + [1.5]' 

743 """ 

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

745 return clever_neg(right) 

746 

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

748 return left 

749 

750 if is_negated(right): 

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

752 else: 

753 return glyphs.sub(left, right) 

754 

755 

756def clever_mul(left, right): 

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

758 

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

760 """ 

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

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

763 return left 

764 

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

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

767 return right 

768 

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

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

771 return right 

772 

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

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

775 return left 

776 

777 # Detect quadratics. 

778 if left == right: 

779 return glyphs.squared(left) 

780 

781 # Factor out negation. 

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

783 if ln and rn: 

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

785 elif ln: 

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

787 elif rn: 

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

789 

790 return glyphs.mul(left, right) 

791 

792 

793def clever_div(left, right): 

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

795 

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

797 """ 

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

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

800 return left 

801 

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

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

804 return left 

805 

806 # Factor out negation. 

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

808 if ln and rn: 

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

810 elif ln: 

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

812 elif rn: 

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

814 

815 return glyphs.div(left, right) 

816 

817 

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

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

820 

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

822 """ 

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

824 

825 if scalar: 

826 return glyphs.clever_mul(left, right) 

827 

828 if from_glyph(left, glyphs.idmatrix): 

829 return glyphs.trace(riCo) 

830 elif from_glyph(riCo, glyphs.idmatrix): 

831 return glyphs.trace(left) 

832 elif from_glyph(left, glyphs.matrix) and left.operands[0] in (1, "1"): 

833 return glyphs.sum(riCo) 

834 elif from_glyph(riCo, glyphs.matrix) and riCo.operands[0] in (1, "1"): 

835 return glyphs.sum(left) 

836 elif left == right: 

837 return glyphs.squared(glyphs.norm(left)) 

838 else: 

839 return glyphs.dotp(left, right) 

840 

841 

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

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

844 

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

846 

847 **Example** 

848 

849 >>> from picos.glyphs import matrix_cat 

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

851 >>> Z 

852 '[X, Y]' 

853 >>> matrix_cat(Z, Z) 

854 '[X, Y, X, Y]' 

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

856 '[X, Y; X, Y]' 

857 """ 

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

859 left = left.operands[0] 

860 

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

862 right = right.operands[0] 

863 

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

865 

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

867 

868 

869def row_vectorize(*entries): 

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

871 return functools.reduce(matrix_cat, entries) 

872 

873 

874def col_vectorize(*entries): 

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

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

877 

878 

879def free_var_name(string): 

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

881 names = "xyzpqrstuvwabcdefghijklmno" 

882 for name in names: 

883 if name not in string: 

884 return name 

885 return "?" 

886 

887 

888# -------------------------------------- 

889__all__ = api_end(_API_START, globals())