class_docstr: The scansion symbols used can be configured by passing a suitable constants class to
  the constructor.
docstr: Scan a line of Latin hexameter and produce a scansion pattern, and other data.
  
  :param original_line: the original line of Latin verse
  :param optional_transform: whether or not to perform i to j transform for syllabification
  :param dactyl_smoothing: whether or not to perform dactyl smoothing
  :return: a Verse object
  
  >>> scanner = HexameterScanner()
  
  >>> print(HexameterScanner().scan(
  ... "ēxiguām sedēm pariturae tērra negavit").scansion) # doctest: +NORMALIZE_WHITESPACE
  - -  -   - -   U U -  -  -  U  U - U
  >>> print(scanner.scan("impulerit. Tantaene animis caelestibus irae?"))
  Verse(original='impulerit. Tantaene animis caelestibus irae?', scansion='-  U U -    -   -   U U -    - -  U U  -  - ', meter='hexameter', valid=True, syllable_count=15, accented='īmpulerīt. Tāntaene animīs caelēstibus īrae?', scansion_notes=['Valid by positional stresses.'], syllables = ['īm', 'pu', 'le', 'rīt', 'Tān', 'taen', 'a', 'ni', 'mīs', 'cae', 'lēs', 'ti', 'bus', 'i', 'rae'])
  >>> print(scanner.scan(
  ... "Arma virumque cano, Troiae qui prīmus ab ōrīs").scansion) # doctest: +NORMALIZE_WHITESPACE
  -  U  U -   U  U -    -  -   -   - U  U  - -
  >>> # some hexameters need the optional transformations:
  >>> optional_transform_scanner = HexameterScanner(optional_transform=True)
  >>> print(optional_transform_scanner.scan(
  ... "Ītaliam, fāto profugus, Lāvīniaque vēnit").scansion) # doctest: +NORMALIZE_WHITESPACE
  - -  -    - -   U U -    - -  U  U  - U
  >>> print(HexameterScanner().scan(
  ... "lītora, multum ille et terrīs iactātus et alto").scansion) # doctest: +NORMALIZE_WHITESPACE
  - U U   -     -    -   -  -   -  - U  U  -  U
  >>> print(HexameterScanner().scan(
  ... "vī superum saevae memorem Iūnōnis ob īram;").scansion) # doctest: +NORMALIZE_WHITESPACE
  -  U U -    -  -  U U -   - - U  U  - U
  >>> # handle multiple elisions
  >>> print(scanner.scan("monstrum horrendum, informe, ingens, cui lumen ademptum").scansion) # doctest: +NORMALIZE_WHITESPACE
  -        -  -      -  -     -  -      -  - U  U -   U
  >>> # if we have 17 syllables, create a chain of all dactyls
  >>> print(scanner.scan("quadrupedante putrem sonitu quatit ungula campum"
  ... ).scansion) # doctest: +NORMALIZE_WHITESPACE
  -  U U -  U  U  -   U U -   U U  -  U U  -  U
  >>> # if we have 13 syllables exactly, we'll create a spondaic hexameter
  >>> print(HexameterScanner().scan(
  ... "illi inter sese multa vi bracchia tollunt").scansion)  # doctest: +NORMALIZE_WHITESPACE
  -    -  -   - -  -  -  -   -   UU  -  -
  >>> print(HexameterScanner().scan(
  ... "dat latus; insequitur cumulo praeruptus aquae mons").scansion) # doctest: +NORMALIZE_WHITESPACE
  -   U U   -  U  U -   U U -    - -  U  U   -  -
  >>> print(optional_transform_scanner.scan(
  ... "Non quivis videt inmodulata poëmata iudex").scansion) # doctest: +NORMALIZE_WHITESPACE
  -    - -   U U  -  U U - U  U- U U  - -
  >>> print(HexameterScanner().scan(
  ... "certabant urbem Romam Remoramne vocarent").scansion) # doctest: +NORMALIZE_WHITESPACE
  -  - -   -  -   - -   U U -  U  U - -
  >>> # advanced smoothing is available via keyword flags: dactyl_smoothing
  >>> # print(HexameterScanner().scan(
  #... "his verbis: 'o gnata, tibi sunt ante ferendae",
  #... dactyl_smoothing=True).scansion) # doctest: +NORMALIZE_WHITESPACE
  #-   -  -    -   - U   U -  -   -  U  U -   -
docstr_tok: ["Scan","a","line","of","Latin","hexameter","and","produce","a","scansion","pattern",",","and","other","data",".",":","param","original_line",":","the","original","line","of","Latin","verse",":","param","optional_transform",":","whether","or","not","to","perform","i","to","j","transform","for","syllabification",":","param","dactyl_smoothing",":","whether","or","not","to","perform","dactyl","smoothing",":","return",":","a","Verse","object",">",">",">","scanner","=","HexameterScanner","(",")",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","\u0113xigu\u0101m","sed\u0113m","pariturae","t\u0113rra","negavit","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","-","-","U","U","-","-","-","U","U","-","U",">",">",">","print","(","scanner.scan","(","``","impulerit",".","Tantaene","animis","caelestibus","irae","?","''",")",")","Verse","(","original='impulerit",".","Tantaene","animis","caelestibus","irae","?","'",",","scansion='-","U","U","-","-","-","U","U","-","-","-","U","U","-","-","'",",","meter='hexameter","'",",","valid=True",",","syllable_count=15",",","accented='\u012bmpuler\u012bt",".","T\u0101ntaene","anim\u012bs","cael\u0113stibus","\u012brae","?","'",",","scansion_notes=","[","'Valid","by","positional","stresses",".","'","]",",","syllables","=","[","'\u012bm","'",",","'pu","'",",","'le","'",",","'r\u012bt","'",",","'T\u0101n","'",",","'taen","'",",","'","a","'",",","'ni","'",",","'m\u012bs","'",",","'cae","'",",","'l\u0113s","'",",","'ti","'",",","'bus","'",",","'","i","'",",","'rae","'","]",")",">",">",">","print","(","scanner.scan","(","...","``","Arma","virumque","cano",",","Troiae","qui","pr\u012bmus","ab","\u014dr\u012bs","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","U","U","-","U","U","-","-","-","-","-","U","U","-","-",">",">",">","#","some","hexameters","need","the","optional","transformations",":",">",">",">","optional_transform_scanner","=","HexameterScanner","(","optional_transform=True",")",">",">",">","print","(","optional_transform_scanner.scan","(","...","``","\u012ataliam",",","f\u0101to","profugus",",","L\u0101v\u012bniaque","v\u0113nit","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","-","-","U","U","-","-","-","U","U","-","U",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","l\u012btora",",","multum","ille","et","terr\u012bs","iact\u0101tus","et","alto","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","U","U","-","-","-","-","-","-","-","U","U","-","U",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","v\u012b","superum","saevae","memorem","I\u016bn\u014dnis","ob","\u012bram",";","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","U","U","-","-","-","U","U","-","-","-","U","U","-","U",">",">",">","#","handle","multiple","elisions",">",">",">","print","(","scanner.scan","(","``","monstrum","horrendum",",","informe",",","ingens",",","cui","lumen","ademptum","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","-","-","-","-","-","-","U","U","-","U",">",">",">","#","if","we","have","17","syllables",",","create","a","chain","of","all","dactyls",">",">",">","print","(","scanner.scan","(","``","quadrupedante","putrem","sonitu","quatit","ungula","campum","''","...",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","U","U","-","U","U","-","U","U","-","U","U","-","U","U","-","U",">",">",">","#","if","we","have","13","syllables","exactly",",","we","'ll","create","a","spondaic","hexameter",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","illi","inter","sese","multa","vi","bracchia","tollunt","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","-","-","-","-","-","-","UU","-","-",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","dat","latus",";","insequitur","cumulo","praeruptus","aquae","mons","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","U","U","-","U","U","-","U","U","-","-","-","U","U","-","-",">",">",">","print","(","optional_transform_scanner.scan","(","...","``","Non","quivis","videt","inmodulata","po\u00ebmata","iudex","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","U","U","-","U","U","-","U","U-","U","U","-","-",">",">",">","print","(","HexameterScanner","(",")",".scan","(","...","``","certabant","urbem","Romam","Remoramne","vocarent","''",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","-","-","-","-","-","-","-","U","U","-","U","U","-","-",">",">",">","#","advanced","smoothing","is","available","via","keyword","flags",":","dactyl_smoothing",">",">",">","#","print","(","HexameterScanner","(",")",".scan","(","#","...","``","his","verbis",":","'","o","gnata",",","tibi","sunt","ante","ferendae","''",",","#","...","dactyl_smoothing=True",")",".scansion",")","#","doctest",":","+NORMALIZE_WHITESPACE","#","-","-","-","-","-","U","U","-","-","-","U","U","-","-"]
code: def scan(
      self,
      original_line: str,
      optional_transform: bool = False,
      dactyl_smoothing: bool = False,
  ) -> Verse:
      """
      Scan a line of Latin hexameter and produce a scansion pattern, and other data.
  
      :param original_line: the original line of Latin verse
      :param optional_transform: whether or not to perform i to j transform for syllabification
      :param dactyl_smoothing: whether or not to perform dactyl smoothing
      :return: a Verse object
  
      >>> scanner = HexameterScanner()
  
      >>> print(HexameterScanner().scan(
      ... "ēxiguām sedēm pariturae tērra negavit").scansion) # doctest: +NORMALIZE_WHITESPACE
      - -  -   - -   U U -  -  -  U  U - U
      >>> print(scanner.scan("impulerit. Tantaene animis caelestibus irae?"))
      Verse(original='impulerit. Tantaene animis caelestibus irae?', scansion='-  U U -    -   -   U U -    - -  U U  -  - ', meter='hexameter', valid=True, syllable_count=15, accented='īmpulerīt. Tāntaene animīs caelēstibus īrae?', scansion_notes=['Valid by positional stresses.'], syllables = ['īm', 'pu', 'le', 'rīt', 'Tān', 'taen', 'a', 'ni', 'mīs', 'cae', 'lēs', 'ti', 'bus', 'i', 'rae'])
      >>> print(scanner.scan(
      ... "Arma virumque cano, Troiae qui prīmus ab ōrīs").scansion) # doctest: +NORMALIZE_WHITESPACE
      -  U  U -   U  U -    -  -   -   - U  U  - -
      >>> # some hexameters need the optional transformations:
      >>> optional_transform_scanner = HexameterScanner(optional_transform=True)
      >>> print(optional_transform_scanner.scan(
      ... "Ītaliam, fāto profugus, Lāvīniaque vēnit").scansion) # doctest: +NORMALIZE_WHITESPACE
      - -  -    - -   U U -    - -  U  U  - U
      >>> print(HexameterScanner().scan(
      ... "lītora, multum ille et terrīs iactātus et alto").scansion) # doctest: +NORMALIZE_WHITESPACE
      - U U   -     -    -   -  -   -  - U  U  -  U
      >>> print(HexameterScanner().scan(
      ... "vī superum saevae memorem Iūnōnis ob īram;").scansion) # doctest: +NORMALIZE_WHITESPACE
      -  U U -    -  -  U U -   - - U  U  - U
      >>> # handle multiple elisions
      >>> print(scanner.scan("monstrum horrendum, informe, ingens, cui lumen ademptum").scansion) # doctest: +NORMALIZE_WHITESPACE
      -        -  -      -  -     -  -      -  - U  U -   U
      >>> # if we have 17 syllables, create a chain of all dactyls
      >>> print(scanner.scan("quadrupedante putrem sonitu quatit ungula campum"
      ... ).scansion) # doctest: +NORMALIZE_WHITESPACE
      -  U U -  U  U  -   U U -   U U  -  U U  -  U
      >>> # if we have 13 syllables exactly, we'll create a spondaic hexameter
      >>> print(HexameterScanner().scan(
      ... "illi inter sese multa vi bracchia tollunt").scansion)  # doctest: +NORMALIZE_WHITESPACE
      -    -  -   - -  -  -  -   -   UU  -  -
      >>> print(HexameterScanner().scan(
      ... "dat latus; insequitur cumulo praeruptus aquae mons").scansion) # doctest: +NORMALIZE_WHITESPACE
      -   U U   -  U  U -   U U -    - -  U  U   -  -
      >>> print(optional_transform_scanner.scan(
      ... "Non quivis videt inmodulata poëmata iudex").scansion) # doctest: +NORMALIZE_WHITESPACE
      -    - -   U U  -  U U - U  U- U U  - -
      >>> print(HexameterScanner().scan(
      ... "certabant urbem Romam Remoramne vocarent").scansion) # doctest: +NORMALIZE_WHITESPACE
      -  - -   -  -   - -   U U -  U  U - -
      >>> # advanced smoothing is available via keyword flags: dactyl_smoothing
      >>> # print(HexameterScanner().scan(
      #... "his verbis: 'o gnata, tibi sunt ante ferendae",
      #... dactyl_smoothing=True).scansion) # doctest: +NORMALIZE_WHITESPACE
      #-   -  -    -   - U   U -  -   -  U  U -   -
      """
      verse = Verse(original_line, meter="hexameter")
      # replace punctuation with spaces
      line = original_line.translate(self.punctuation_substitutions)
      # conservative i to j
      line = self.transform_i_to_j(line)
      working_line = self.elide_all(line)
      working_line = self.accent_by_position(working_line)
      syllables = self.syllabifier.syllabify(working_line)
      if optional_transform:
          working_line = self.transform_i_to_j_optional(line)
          working_line = self.elide_all(working_line)
          working_line = self.accent_by_position(working_line)
          syllables = self.syllabifier.syllabify(working_line)
          verse.scansion_notes += [
              self.constants.NOTE_MAP["optional i to j"]
          ]
      verse.working_line = working_line
      verse.syllable_count = self.syllabifier.get_syllable_count(
          syllables
      )
      verse.syllables = syllables
      if verse.syllable_count < 12:
          verse.valid = False
          verse.scansion_notes += [self.constants.NOTE_MAP["< 12"]]
          return verse
      stresses = self.flag_dipthongs(syllables)
      syllables_wspaces = (
          string_utils.to_syllables_with_trailing_spaces(
              working_line, syllables
          )
      )
      offset_map = self.calc_offset(syllables_wspaces)
      for idx, syl in enumerate(syllables):
          for accented in self.constants.ACCENTED_VOWELS:
              if accented in syl:
                  stresses.append(idx)
      # first syllable is always long in hexameter
      stresses.append(0)
      # second to last syllable is always long
      stresses.append(verse.syllable_count - 2)
  
      verse.scansion = self.produce_scansion(
          stresses, syllables_wspaces, offset_map
      )
      if len(
          string_utils.stress_positions(
              self.constants.STRESSED, verse.scansion
          )
      ) != len(set(stresses)):
          verse.valid = False
          verse.scansion_notes += [
              self.constants.NOTE_MAP["invalid syllables"]
          ]
          return verse
  
      if self.metrical_validator.is_valid_hexameter(verse.scansion):
          verse.scansion_notes += [
              self.constants.NOTE_MAP["positionally"]
          ]
          return self.assign_candidate(verse, verse.scansion)
  
      # identify some obvious and probably choices based on number of syllables
      if verse.syllable_count == 17:  # produce all dactyls
          candidate = self.produce_scansion(
              self.metrical_validator.hexameter_known_stresses(),
              syllables_wspaces,
              offset_map,
          )
          verse.scansion_notes += [self.constants.NOTE_MAP["17"]]
          if self.metrical_validator.is_valid_hexameter(candidate):
              return self.assign_candidate(verse, candidate)
      if verse.syllable_count == 12:  # create all spondee hexameter
          candidate = self.produce_scansion(
              list(range(12)), syllables_wspaces, offset_map
          )
          if self.metrical_validator.is_valid_hexameter(verse.scansion):
              verse.scansion_notes += [self.constants.NOTE_MAP["12"]]
              return self.assign_candidate(verse, candidate)
      if (
          verse.syllable_count == 13
      ):  # create spondee hexameter with a dactyl at 5th foot
          known_unaccents = [9, 10]
          last_syllable_accented = False
          for vowel in self.constants.ACCENTED_VOWELS:
              if vowel in verse.syllables[12]:
                  last_syllable_accented = True
          if not last_syllable_accented:
              known_unaccents.append(12)
          if set(known_unaccents) - set(stresses) != len(
              known_unaccents
          ):
              verse.scansion = self.produce_scansion(
                  [x for x in range(13) if x not in known_unaccents],
                  syllables_wspaces,
                  offset_map,
              )
              verse.scansion_notes += [
                  self.constants.NOTE_MAP["5th dactyl"]
              ]
              if self.metrical_validator.is_valid_hexameter(
                  verse.scansion
              ):
                  return self.assign_candidate(verse, verse.scansion)
      if verse.syllable_count > 17:
          verse.valid = False
          verse.scansion_notes += [self.constants.NOTE_MAP["> 17"]]
          return verse
  
      smoothed = self.correct_inverted_amphibrachs(verse.scansion)
      if verse.scansion != smoothed:
          verse.scansion_notes += [self.constants.NOTE_MAP["inverted"]]
          verse.scansion = smoothed
          stresses += string_utils.differences(verse.scansion, smoothed)
  
      if self.metrical_validator.is_valid_hexameter(verse.scansion):
          return self.assign_candidate(verse, verse.scansion)
  
      smoothed = self.correct_first_two_dactyls(verse.scansion)
      if verse.scansion != smoothed:
          verse.scansion_notes += [
              self.constants.NOTE_MAP["invalid start"]
          ]
          verse.scansion = smoothed
          stresses += string_utils.differences(verse.scansion, smoothed)
  
      if self.metrical_validator.is_valid_hexameter(verse.scansion):
          return self.assign_candidate(verse, verse.scansion)
  
      smoothed = self.correct_invalid_fifth_foot(verse.scansion)
      if verse.scansion != smoothed:
          verse.scansion_notes += [
              self.constants.NOTE_MAP["invalid 5th"]
          ]
          verse.scansion = smoothed
          stresses += string_utils.differences(verse.scansion, smoothed)
  
      if self.metrical_validator.is_valid_hexameter(verse.scansion):
          return self.assign_candidate(verse, verse.scansion)
  
      feet = self.metrical_validator.hexameter_feet(
          verse.scansion.replace(" ", "")
      )
      if feet:
          #  Normal good citizens are unwelcome in the house of hexameter
          invalid_feet_in_hexameter = [
              self.constants.IAMB,
              self.constants.TROCHEE,
          ]
          current_foot = 0
          ending = (
              feet.pop()
          )  # don't process the ending, a possible trochee, add it back after
          scanned_line = ""
          for foot in feet:
              if foot.replace(" ", "") in invalid_feet_in_hexameter:
                  scanned_line = self.invalid_foot_to_spondee(
                      feet, foot, current_foot
                  )
                  scanned_line = scanned_line + ending
              current_foot += 1
          smoothed = self.produce_scansion(
              stresses
              + string_utils.stress_positions(
                  self.constants.STRESSED, scanned_line
              ),
              syllables_wspaces,
              offset_map,
          )
  
          if self.metrical_validator.is_valid_hexameter(smoothed):
              verse.scansion_notes += [
                  self.constants.NOTE_MAP["invalid foot"]
              ]
              return self.assign_candidate(verse, smoothed)
  
      # need to do this again, since the scansion has changed
      smoothed = self.correct_inverted_amphibrachs(verse.scansion)
      if verse.scansion != smoothed:
          verse.scansion_notes += [self.constants.NOTE_MAP["inverted"]]
          verse.scansion = smoothed
          stresses += string_utils.differences(verse.scansion, smoothed)
  
      if self.metrical_validator.is_valid_hexameter(verse.scansion):
          return self.assign_candidate(verse, verse.scansion)
  
      candidates = self.metrical_validator.closest_hexameter_patterns(
          verse.scansion
      )
      if candidates is not None:
          if (
              len(candidates) == 1
              and len(verse.scansion.replace(" ", ""))
              == len(candidates[0])
              and len(
                  string_utils.differences(
                      verse.scansion, candidates[0]
                  )
              )
              == 1
          ):
              tmp_scansion = self.produce_scansion(
                  string_utils.differences(
                      verse.scansion, candidates[0]
                  ),
                  syllables_wspaces,
                  offset_map,
              )
              if self.metrical_validator.is_valid_hexameter(
                  tmp_scansion
              ):
                  verse.scansion_notes += [
                      self.constants.NOTE_MAP["closest match"]
                  ]
                  return self.assign_candidate(verse, tmp_scansion)
  
      # need to do this again, since the scansion has changed
      smoothed = self.correct_inverted_amphibrachs(smoothed)
      if self.metrical_validator.is_valid_hexameter(smoothed):
          verse.scansion_notes += [self.constants.NOTE_MAP["inverted"]]
          return self.assign_candidate(verse, smoothed)
  
      if dactyl_smoothing:
          smoothed = self.correct_dactyl_chain(smoothed)
          if verse.scansion != smoothed:
              verse.scansion_notes += [
                  self.constants.NOTE_MAP["dactyl smoothing"]
              ]
              verse.scansion = smoothed
          if self.metrical_validator.is_valid_hexameter(verse.scansion):
              return self.assign_candidate(verse, verse.scansion)
  
      # if the line doesn't scan "as is", if may scan if the optional i to j transformations
      # are made, so here we set them and try again.
      if self.optional_transform and not verse.valid:
          return self.scan(
              original_line,
              optional_transform=True,
              dactyl_smoothing=True,
          )
      return verse
  
file_path: cltk/src/cltk/prosody/lat/hexameter_scanner.py
imports: import re
  import cltk.prosody.lat.string_utils
  from cltk.prosody.lat.metrical_validator import MetricalValidator
  from cltk.prosody.lat.scansion_constants import ScansionConstants
  from cltk.prosody.lat.scansion_formatter import ScansionFormatter
  from cltk.prosody.lat.syllabifier import Syllabifier
  from cltk.prosody.lat.verse import Verse
  from cltk.prosody.lat.verse_scanner import VerseScanner
label_desc: Use image node_id 2 for calling the HexameterScanner obj's underlying member method code with example usage: obj.scan(original_line, optional_transform, dactyl_smoothing) and returns: verse, verse, verse, self, verse, self, self, self, self, self, self, self, self, self, self, self, self