Package jazzparser :: Package formalisms :: Package music_halfspan :: Module syntax
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.formalisms.music_halfspan.syntax

  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  # Get the logger from the logging system 
 53  logger = logging.getLogger("main_logger") 
54 55 -class Slash(SlashBase, ModalSlash):
56 - def __init__(self, dir, modality=None, **kwargs):
57 from . import Formalism 58 if modality is None: 59 modality = '' 60 SlashBase.__init__(self, Formalism, dir, **kwargs) 61 ModalSlash.__init__(self, modality)
62
63 - def copy(self):
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 """
79 - def __init__(self, *args, **kwargs):
80 from . import Formalism 81 super(Sign, self).__init__(Formalism, *args, **kwargs)
82
83 - def copy(self):
84 return Sign(self.category.copy(),\ 85 self.semantics.copy(),\ 86 copy.copy(self.derivation_trace))
87
88 - def apply_lexical_features(self, features):
89 if 'root' in features: 90 # Assume the category and LF are supposed to be relative to the 91 # chord root and make them absolute 92 make_absolute_category_from_relative(self.category, features['root']) 93 make_absolute_lf_from_relative(self.semantics, root_to_et_coord(features['root'])) 94 if 'duration' in features: 95 self.set_duration(features['duration']) 96 if 'time' in features: 97 self.semantics.set_all_times(features['time'])
98
99 - def __str__(self):
100 return "%s : %s" % (self.category, self.semantics)
101
102 - def format_result(self):
103 return "%s : %s" % (self.category, self.semantics.format_result())
104
105 - def set_time(self, time):
107
108 - def set_duration(self, duration):
109 self.semantics.lf.duration = duration
110
111 -class ComplexCategory(ComplexCategoryBase, ModalComplexCategory):
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
125 - def __init__(self, *args, **kwargs):
126 from . import Formalism 127 ComplexCategoryBase.__init__(self, Formalism, *args, **kwargs)
128
129 - def copy(self):
130 return ComplexCategory(result=self.result.copy(), 131 slash=self.slash.copy(), 132 argument=self.argument.copy())
133
134 - def __eq__(self, other):
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
140 -class HalfCategory(object):
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 # This is already a numeric root 170 self.root = root_number 171 elif root_symbol is not None: 172 try: 173 # Try treating this as a chord root 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
190 - def __str__(self):
191 return "%s^%s" % (self.symbol, self.function_symbol)
192 193 @property
194 - def function_symbol(self):
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
206 - def function(self):
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
218 - def symbol(self):
219 """Readable symbol of the category's root.""" 220 return int_to_pitch_class(self.root)
221 222 @property
223 - def ambiguous_function(self):
224 """True if the category has multiple possible functions""" 225 return len(self.functions) > 1
226
227 - def __eq__(self, other):
228 return type(self) == type(other) and \ 229 self.root == other.root and \ 230 self.functions == other.functions
231
232 - def __ne__(self, other):
233 return not (self == other)
234
235 - def __hash__(self):
236 return self.root
237
238 - def set_relative_to(self, root):
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
245 - def copy(self):
246 return HalfCategory(root_number=self.root, 247 function=[copy.copy(f) for f in self.functions])
248
249 - def to_latex(self):
250 return "%s^{%s}" % (self.symbol, self.function_symbol)
251
252 - def matches(self, other):
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 # The other category must have only one possible function 261 return False 262 return other.root == self.root and other.function in self.functions
263
264 -class AtomicCategory(AtomicCategoryBase, ModalAtomicCategory):
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):
273 from . import Formalism 274 super(AtomicCategory, self).__init__(Formalism) 275 self.from_half = from_half 276 self.to_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
294 - def copy(self):
295 return AtomicCategory(self.from_half.copy(), 296 self.to_half.copy())
297
298 - def __hash__(self):
299 return hash(self.from_half) + hash(self.to_half)
300
301 - def __str__(self):
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
307 - def to_latex(self):
308 return "\\kcat{%s}{%s}" % (self.from_half.to_latex(), 309 self.to_half.to_latex())
310
311 - def __eq__(self, other):
312 return type(self) == type(other) and \ 313 self.from_half == other.from_half and \ 314 self.to_half == other.to_half
315
316 -class DummyCategory(DummyCategoryBase):
317 ATOMIC = None 318
319 - def __init__(self):
320 from . import Formalism 321 super(DummyCategory, self).__init__(Formalism)
322
323 ################ Chart operations 324 -def merge_equal_signs(existing_sign, new_sign):
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 ######## Unification 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 """
341 -class UnificationResult(UnificationResultBase):
342 """ 343 Dummy unification results which allows us to use the unification 344 formalism without actually unifying any variables. 345 346 """
347 - def apply_all_mappings(self, obj):
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 # Create an empty constraint set using the base class for constraints 365 constraints = VariableSubstitution() 366 return UnificationResult( 367 category1, 368 constraints, 369 [category1, category2] 370 )
371
372 ####################### 373 ## Utilities ## 374 ####################### 375 -def make_absolute_category_from_relative(relative_cat, base_root):
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 # Atomic category: adjust chord roots on either side 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 # Complex category: adjust roots of argument and result 390 # No need for recursion in this formalism 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
397 -def pre_generalize_category(category):
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 # Just treat it as an atomic category 412 # This renders the structure fine: a complex category becomes 413 # Atom/Atom or Atom\Atom, instead of Half/Half or Half\Half. 414 return GeneralAtomic() 415 else: 416 # Pass processing on to the normal generalization routine 417 return None
418
419 420 -def syntax_from_string(string):
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 # Matching brace not found 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 # Function to build a half category: used by atomic and complex categories 459 def _build_half(text): 460 text = text.strip() 461 root,caret,functions = text.partition("^") 462 463 # Check that the caret was in there 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 # Check that valid function characters were used 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 # Any category should start with a half category 480 first_half,rest = _build_half(string) 481 # Work out whether it's atomic or complex from what follows 482 if rest.startswith("-"): 483 # Atomic category 484 # Get the second half 485 second_half,rest = _build_half(rest[1:]) 486 487 cat = AtomicCategory(first_half, second_half) 488 elif rest.startswith("\\") or rest.startswith("/"): 489 # Complex category 490 # Get the slash direction 491 forward = (rest[0] == "/") 492 rest = rest[1:].strip() 493 # Look for a modality (optional) 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 # Interpret the remainder as a half-category 503 second_half,rest = _build_half(rest) 504 505 cat = ComplexCategory(first_half, slash, second_half) 506 elif len(rest) == 0: 507 # Implicit atomic category from half-category 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
519 -def sign_from_string(string):
520 """ 521 Simple combination of L{syntax_from_string} and 522 L{jazzparser.formalisms.music_halfspan.semantics.semantics_from_string} 523 to build a full sign from a string. 524 525 """ 526 from .semantics import semantics_from_string 527 528 synstr,colon,semstr = string.partition(":") 529 if not colon: 530 raise SignStringBuildError, "a sign must be of the form "\ 531 "'<syntax> : <semantics>'" 532 category = syntax_from_string(synstr.strip()) 533 semantics = semantics_from_string(semstr.strip()) 534 return Sign(category, semantics)
535
536 -class SyntaxStringBuildError(Exception):
537 pass
538
539 -class SignStringBuildError(Exception):
540 pass
541