1 """Syntactic categories module for the music_halfspan formalism.
2
3 Syntactic classes for the halfspan formalism. Atomic categories in this
4 formalism carry information about the start and end keys of the span
5 and some cadence features. The formalism also uses modalities.
6
7 It's similar to music_keyspan, but simpler.
8
9 The old music_keyspan formalism used to have loads of unification
10 stuff. At the moment, this formalism avoids the need for unification
11 altogether. It may be that we need some (milder) form of unification,
12 at which point we can use the same framework that keyspan used. For
13 now, this is much neater for not needing this stuff.
14
15 """
16 """
17 ============================== License ========================================
18 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
19
20 This file is part of The Jazz Parser.
21
22 The Jazz Parser is free software: you can redistribute it and/or modify
23 it under the terms of the GNU General Public License as published by
24 the Free Software Foundation, either version 3 of the License, or
25 (at your option) any later version.
26
27 The Jazz Parser is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public License
33 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
34
35 ============================ End license ======================================
36
37 """
38 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
39
40 import logging
41 import copy, re
42 from jazzparser.formalisms.base.syntax import SlashBase, SignBase, \
43 ComplexCategoryBase, VariableSubstitutor, VariableSubstitution, \
44 UnificationResultBase, AtomicCategoryBase, DummyCategoryBase
45 from jazzparser.formalisms.base.modalities import ModalSlash, \
46 ModalComplexCategory, ModalAtomicCategory
47 from jazzparser.utils.chords import ChordError, chord_numeral_to_int, int_to_pitch_class
48 from jazzparser.utils.latex import filter_latex
49 from jazzparser.utils.tonalspace import root_to_et_coord
50 from .semantics import make_absolute_lf_from_relative
51
52
53 logger = logging.getLogger("main_logger")
54
55 -class Slash(SlashBase, ModalSlash):
56 - def __init__(self, dir, modality=None, **kwargs):
62
64 return Slash(self.forward,
65 modality=self.modality)
66
67 -class Sign(SignBase):
68 """
69 A CCG category and its associated semantics: a CCG sign.
70
71 Keeps a note of which rules have been applied and which other
72 signs they were applied to, so that the parser can avoid re-applying
73 the same rule to the same inputs again.
74
75 This overrides the base sign implementation with a few
76 formalism-specific things.
77
78 """
82
87
98
101
104
107
110
112 """
113 In the halfspan formalism, complex categories are made up of an
114 argument half category, a slash and a result half category. Neither
115 the argument nor the result may be whole categories, atomic or
116 complex.
117
118 This kind of complex category is hugely simpler than previous
119 incarnations, since there are no unification variables involved
120 anywhere.
121
122 """
123 ATOMIC = False
124
128
133
135 return type(other) == type(self) and \
136 self.slash == other.slash and \
137 self.argument == other.argument and \
138 self.result == other.result
139
141 """
142 One half of an atomic category, or the argument or result of a
143 complex category.
144 Stores a root value and a chord function marker (which may be
145 a set of functions in the case of an argument half category).
146
147 """
148 - def __init__(self, root_symbol=None, function='T', root_number=None):
149 """
150 Either root_symbol or root_number must be given.
151
152 Will raise a ChordError if the root symbol can't be interpreted
153 as a root number.
154
155 @type root_symbol: string
156 @param root_symbol: symbol to interpret as the root of this
157 category. This will be converted to a root number.
158 @type function: string or list of strings
159 @param function: either a single function marker (usually
160 'T', 'D' and 'S') or a list of such function markers,
161 as in the case of a complex category's argument.
162 @type root_number: int
163 @param root_number: alternative to setting the root by a
164 symbol. This will be used in preferance to root_symbol if
165 given.
166
167 """
168 if root_number is not None:
169
170 self.root = root_number
171 elif root_symbol is not None:
172 try:
173
174 self.root = chord_numeral_to_int(root_symbol, strict=True)
175 except ChordError, err:
176 raise ChordError, "could not treat '%s' as a root "\
177 "symbol: %s" % (root_symbol, err)
178 else:
179 raise ValueError, "either root_symbol or root_number must "\
180 "be given when creating a half category"
181
182 if type(function) == str:
183 self.functions = set([function])
184 else:
185 if len(function) == 0:
186 raise ValueError, "cannot create a category with an "\
187 "empty set of possible functions"
188 self.functions = set(function)
189
192
193 @property
195 """
196 Readable representation of the category's function or
197 alternative functions.
198
199 """
200 if len(self.functions) > 1:
201 return "|".join(self.functions)
202 else:
203 return self.function
204
205 @property
207 """
208 If the category has only one function (not a set of possible
209 functions), returns this. Otherwise returns None.
210
211 """
212 if len(self.functions) > 1:
213 return None
214 else:
215 return list(self.functions)[0]
216
217 @property
221
222 @property
224 """True if the category has multiple possible functions"""
225 return len(self.functions) > 1
226
228 return type(self) == type(other) and \
229 self.root == other.root and \
230 self.functions == other.functions
231
233 return not (self == other)
234
237
239 """
240 Changes the root value to a new root that is the original
241 root relative to the given root.
242 """
243 self.root = (root + self.root) % 12
244
248
251
253 """
254 Returns True if this half category, as the argument part of
255 a complex category, would accept the other half category as
256 the relevant part of its argument (in function application).
257
258 """
259 if other.ambiguous_function:
260
261 return False
262 return other.root == self.root and other.function in self.functions
263
265 """
266 An atomic category is of the form A-B, where A and B are
267 half categories.
268
269 """
270 ATOMIC = True
271
272 - def __init__(self, from_half, to_half):
277
278 @staticmethod
279 - def span(from_root, from_function,
280 to_root, to_function):
281 """
282 Construct an atomic category without having to construct
283 the root parts yourself every time.
284
285 C{from_root} and C{from_function} will get passed on to the
286 L{HalfCategory} constructor. Likewise C{to_root} and
287 C{to_function}.
288
289 """
290 return AtomicCategory(
291 HalfCategory(from_root, from_function),
292 HalfCategory(to_root, to_function))
293
297
299 return hash(self.from_half) + hash(self.to_half)
300
302 if self.from_half == self.to_half:
303 return "%s" % self.from_half
304 else:
305 return "%s-%s" % (self.from_half, self.to_half)
306
308 return "\\kcat{%s}{%s}" % (self.from_half.to_latex(),
309 self.to_half.to_latex())
310
312 return type(self) == type(other) and \
313 self.from_half == other.from_half and \
314 self.to_half == other.to_half
315
322
325 """
326 Used when adding equal signs to the same edge in the chart.
327 Currently does nothing.
328
329 """
330 pass
331
332
333
334
335 """
336 We don't use unification in this formalism (currently), but I'm
337 keeping to the old unification framework so we can just slot into
338 my nice general CCG base classes.
339
340 """
342 """
343 Dummy unification results which allows us to use the unification
344 formalism without actually unifying any variables.
345
346 """
348 """No mappings to distinguish variables, since we don't have any."""
349 pass
350
351 -def unify(category1, category2, grammar=None):
352 """
353 Dummy unification procedure.
354
355 Unification succeeds if and only if the two categories are equal
356 (using their own definition of equality). The unification
357 constraints do nothing to the categories when applied.
358
359 """
360 if category1 != category2:
361 return None
362 category1 = category1.copy()
363 category2 = category2.copy()
364
365 constraints = VariableSubstitution()
366 return UnificationResult(
367 category1,
368 constraints,
369 [category1, category2]
370 )
371
376 """
377 Given a CCGCategory and an absolute chord root in integer form,
378 alters the category to that given by considering chord roots in the
379 input category to be relative to the root of base_chord.
380 E.g. a V category when considered relative to a IV
381 chord would render a I chord.
382
383 """
384 if type(relative_cat) == AtomicCategory:
385
386 relative_cat.from_half.set_relative_to(base_root)
387 relative_cat.to_half.set_relative_to(base_root)
388 elif type(relative_cat) == ComplexCategory:
389
390
391 relative_cat.argument.set_relative_to(base_root)
392 relative_cat.result.set_relative_to(base_root)
393 else:
394 raise TypeError, "Tried to alter a category object of the wrong type"
395 return
396
398 """
399 When abstracting categories to something general that just
400 represents the structure of the category, we have to do something
401 special with half categories, since the standard abstraction
402 routine expects the children of a complex category to be
403 atomic or complex categories themselves.
404
405 @see: L{jazzparser.data.trees.build_tree_for_sequence}
406 @see: L{jazzparser.data.trees.generalize_category}
407
408 """
409 from jazzparser.data.trees import AtomicCategory as GeneralAtomic
410 if isinstance(category, HalfCategory):
411
412
413
414 return GeneralAtomic()
415 else:
416
417 return None
418
421 """
422 Builds a L{Category} instance from a string representation of the syntactic
423 category. This is mainly for testing and debugging and shouldn't be used in
424 the wild (in the parser, for example). This is not how we construct
425 categories out of the lexicon: they're specified in XML, which is a
426 safer way to build them, though more laborious to write.
427
428 The strings may be constructed as follows.
429
430 B{Full atomic category}: A-B. A and B are half categories (see below).
431
432 B{Slash category}. Forward slash: A/B; optionally with a slash modality,
433 A/E{lb}mE{rb}B. Backward slash: A\\B or A\\E{lb}mE{rb}B. A and B are half
434 categories.
435
436 B{Half category}: part of the above types. X^Y.
437 X must be a roman numeral chord root.
438 Y must be a function character (T, D or S), or multiple function characters.
439 E.g. I^T or VI^TD
440
441 """
442 def _find_matching(s, opener="{", closer="}"):
443 opened = 0
444 for i,char in enumerate(s):
445 if char == closer:
446 if opened == 0:
447 return i
448 else:
449 opened -= 1
450 elif char == opener:
451 opened += 1
452
453 raise SyntaxStringBuildError, "%s was not matched by a %s in %s" % \
454 (opener, closer, s)
455
456 fun_re = re.compile(r'^(?P<funs>[TDS]+)(?P<rest>.*)$')
457
458
459 def _build_half(text):
460 text = text.strip()
461 root,caret,functions = text.partition("^")
462
463
464 if len(caret) == 0:
465 raise SyntaxStringBuildError, "'%s' is not a valid half-category - "\
466 "it has no ^ in it. Found in '%s'" % (text, string)
467
468 match = fun_re.match(functions)
469 if match is None:
470 raise SyntaxStringBuildError, "no function characters found at the "\
471 "start of '%s' in '%s'" % (text,string)
472 matchgd = match.groupdict()
473 functions = list(matchgd['funs'])
474 leftover = matchgd['rest'].strip()
475
476 cat = HalfCategory(root_symbol=root, function=functions)
477 return cat,leftover
478
479
480 first_half,rest = _build_half(string)
481
482 if rest.startswith("-"):
483
484
485 second_half,rest = _build_half(rest[1:])
486
487 cat = AtomicCategory(first_half, second_half)
488 elif rest.startswith("\\") or rest.startswith("/"):
489
490
491 forward = (rest[0] == "/")
492 rest = rest[1:].strip()
493
494 if rest[0] == "{":
495 end = _find_matching(rest[1:]) + 1
496 modality = rest[1:end]
497 rest = rest[end+1:]
498 else:
499 modality = None
500 slash = Slash(forward, modality=modality)
501
502
503 second_half,rest = _build_half(rest)
504
505 cat = ComplexCategory(first_half, slash, second_half)
506 elif len(rest) == 0:
507
508 second_half = first_half.copy()
509 cat = AtomicCategory(first_half, second_half)
510 else:
511 raise SyntaxStringBuildError, "couldn't recognise a category in '%s'" \
512 % string
513
514 if len(rest):
515 raise SyntaxStringBuildError, "unexpected text '%s' in '%s'" % \
516 (rest, string)
517 return cat
518
535
538
541