Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
project = 'IDTAP API'
copyright = '2025, Jon Myers'
author = 'Jon Myers'
release = '0.1.7'
version = '0.1.7'
release = '0.1.14'
version = '0.1.14'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
2 changes: 1 addition & 1 deletion idtap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Python API package exposing IDTAP data classes and client."""

__version__ = "0.1.13"
__version__ = "0.1.14"

from .client import SwaraClient
from .auth import login_google
Expand Down
30 changes: 30 additions & 0 deletions idtap/classes/pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,18 @@ def _octave_diacritic(self) -> str:
3: '\u20DB'
}
return mapping.get(self.oct, '')

def _octave_latex_diacritic(self) -> str:
"""Convert octave to LaTeX math notation for proper diacritic positioning."""
mapping = {
-3: r'\underset{\bullet\bullet\bullet}', # Triple dot below
-2: r'\underset{\bullet\bullet}', # Double dot below
-1: r'\underset{\bullet}', # Single dot below
1: r'\dot', # Single dot above
2: r'\ddot', # Double dot above
3: r'\dddot' # Triple dot above
}
return mapping.get(self.oct, '')

@property
def octaved_scale_degree(self) -> str:
Expand Down Expand Up @@ -329,6 +341,24 @@ def cents_string(self) -> str:
sign = '+' if cents >= 0 else '-'
return f"{sign}{round(abs(cents))}\u00A2"

@property
def latex_sargam_letter(self) -> str:
"""LaTeX-compatible base sargam letter."""
return self.sargam_letter

@property
def latex_octaved_sargam_letter(self) -> str:
"""LaTeX math mode sargam letter with properly positioned diacritics."""
base_letter = self.sargam_letter
latex_diacritic = self._octave_latex_diacritic()

if not latex_diacritic:
return base_letter # No octave marking
elif latex_diacritic.startswith(r'\underset'):
return f'${latex_diacritic}{{\\mathrm{{{base_letter}}}}}$'
else:
return f'${latex_diacritic}{{\\mathrm{{{base_letter}}}}}$'

@property
def a440_cents_deviation(self) -> str:
c0 = 16.3516
Expand Down
188 changes: 188 additions & 0 deletions idtap/tests/pitch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,191 @@ def test_constructor_rejects_undefined_ratios():
with pytest.raises(SyntaxError):
Pitch({'ratios': ratios2})


def test_latex_sargam_letter_basic():
"""Test that latex_sargam_letter returns the same as sargam_letter."""
# Test all sargam letters in both raised and lowered forms
sargam_tests = [
({'swara': 'sa'}, 'S'),
({'swara': 're', 'raised': False}, 'r'),
({'swara': 're', 'raised': True}, 'R'),
({'swara': 'ga', 'raised': False}, 'g'),
({'swara': 'ga', 'raised': True}, 'G'),
({'swara': 'ma', 'raised': False}, 'm'),
({'swara': 'ma', 'raised': True}, 'M'),
({'swara': 'pa'}, 'P'),
({'swara': 'dha', 'raised': False}, 'd'),
({'swara': 'dha', 'raised': True}, 'D'),
({'swara': 'ni', 'raised': False}, 'n'),
({'swara': 'ni', 'raised': True}, 'N'),
]

for options, expected in sargam_tests:
p = Pitch(options)
assert p.latex_sargam_letter == expected
assert p.latex_sargam_letter == p.sargam_letter


def test_latex_octaved_sargam_letter_no_octave():
"""Test LaTeX octaved sargam letter with no octave marking (oct=0)."""
p = Pitch({'swara': 'sa', 'oct': 0})
assert p.latex_octaved_sargam_letter == 'S'

p = Pitch({'swara': 're', 'raised': False, 'oct': 0})
assert p.latex_octaved_sargam_letter == 'r'


def test_latex_octaved_sargam_letter_positive_octaves():
"""Test LaTeX octaved sargam letter with positive octaves (dots above)."""
# Test oct=1 (single dot above)
p = Pitch({'swara': 'sa', 'oct': 1})
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{S}}$'

p = Pitch({'swara': 're', 'raised': False, 'oct': 1})
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{r}}$'

p = Pitch({'swara': 'ga', 'raised': True, 'oct': 1})
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{G}}$'

# Test oct=2 (double dot above)
p = Pitch({'swara': 'ma', 'raised': False, 'oct': 2})
assert p.latex_octaved_sargam_letter == r'$\ddot{\mathrm{m}}$'

p = Pitch({'swara': 'pa', 'oct': 2})
assert p.latex_octaved_sargam_letter == r'$\ddot{\mathrm{P}}$'

# Test oct=3 (triple dot above)
p = Pitch({'swara': 'dha', 'raised': True, 'oct': 3})
assert p.latex_octaved_sargam_letter == r'$\dddot{\mathrm{D}}$'

p = Pitch({'swara': 'ni', 'raised': False, 'oct': 3})
assert p.latex_octaved_sargam_letter == r'$\dddot{\mathrm{n}}$'


def test_latex_octaved_sargam_letter_negative_octaves():
"""Test LaTeX octaved sargam letter with negative octaves (dots below)."""
# Test oct=-1 (single dot below)
p = Pitch({'swara': 'sa', 'oct': -1})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{S}}$'

p = Pitch({'swara': 're', 'raised': True, 'oct': -1})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{R}}$'

# Test oct=-2 (double dot below)
p = Pitch({'swara': 'ga', 'raised': False, 'oct': -2})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet}{\mathrm{g}}$'

p = Pitch({'swara': 'ma', 'raised': True, 'oct': -2})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet}{\mathrm{M}}$'

# Test oct=-3 (triple dot below)
p = Pitch({'swara': 'pa', 'oct': -3})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet\bullet}{\mathrm{P}}$'

p = Pitch({'swara': 'dha', 'raised': False, 'oct': -3})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet\bullet}{\mathrm{d}}$'


def test_latex_octaved_sargam_letter_all_sargam_all_octaves():
"""Test all sargam letters across all octave levels."""
sargam_letters = ['sa', 're', 'ga', 'ma', 'pa', 'dha', 'ni']
octave_expected = {
-3: r'\underset{\bullet\bullet\bullet}',
-2: r'\underset{\bullet\bullet}',
-1: r'\underset{\bullet}',
0: '',
1: r'\dot',
2: r'\ddot',
3: r'\dddot'
}

for swara in sargam_letters:
for raised in [True, False]:
# Skip invalid combinations (sa and pa are always raised)
if swara in ['sa', 'pa'] and not raised:
continue

p = Pitch({'swara': swara, 'raised': raised})
base_letter = p.sargam_letter

for oct in range(-3, 4):
p_oct = Pitch({'swara': swara, 'raised': raised, 'oct': oct})
expected_latex = octave_expected[oct]

if oct == 0:
expected_result = base_letter
elif expected_latex.startswith(r'\underset'):
expected_result = f'${expected_latex}{{\\mathrm{{{base_letter}}}}}$'
else:
expected_result = f'${expected_latex}{{\\mathrm{{{base_letter}}}}}$'

assert p_oct.latex_octaved_sargam_letter == expected_result


def test_latex_properties_preserve_backward_compatibility():
"""Test that existing properties are not affected by LaTeX additions."""
test_cases = [
{'swara': 'sa', 'oct': 0},
{'swara': 're', 'raised': False, 'oct': 1},
{'swara': 'ga', 'raised': True, 'oct': -1},
{'swara': 'ma', 'raised': False, 'oct': 2},
{'swara': 'pa', 'oct': -2},
{'swara': 'dha', 'raised': True, 'oct': 3},
{'swara': 'ni', 'raised': False, 'oct': -3},
]

for options in test_cases:
p = Pitch(options)

# All existing properties should work exactly as before
assert hasattr(p, 'sargam_letter')
assert hasattr(p, 'octaved_sargam_letter')
assert hasattr(p, 'frequency')
assert hasattr(p, 'numbered_pitch')
assert hasattr(p, 'chroma')

# New LaTeX properties should be available
assert hasattr(p, 'latex_sargam_letter')
assert hasattr(p, 'latex_octaved_sargam_letter')

# latex_sargam_letter should match sargam_letter
assert p.latex_sargam_letter == p.sargam_letter


def test_latex_octave_diacritic_helper():
"""Test the _octave_latex_diacritic helper method."""
# Test all octave levels
octave_mapping = {
-3: r'\underset{\bullet\bullet\bullet}',
-2: r'\underset{\bullet\bullet}',
-1: r'\underset{\bullet}',
0: '',
1: r'\dot',
2: r'\ddot',
3: r'\dddot'
}

for oct, expected in octave_mapping.items():
p = Pitch({'swara': 'sa', 'oct': oct})
assert p._octave_latex_diacritic() == expected


def test_latex_properties_edge_cases():
"""Test LaTeX properties with edge cases and various combinations."""
# Test with log_offset (should not affect LaTeX output)
p = Pitch({'swara': 'ga', 'raised': False, 'oct': 1, 'log_offset': 0.1})
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{g}}$'

# Test with different fundamentals (should not affect LaTeX output)
p = Pitch({'swara': 'ma', 'raised': True, 'oct': -1, 'fundamental': 440.0})
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{M}}$'

# Test serialization includes existing functionality
p = Pitch({'swara': 'dha', 'raised': False, 'oct': 2})
json_data = p.to_json()
p_restored = Pitch.from_json(json_data)

# LaTeX properties should work after deserialization
assert p_restored.latex_sargam_letter == 'd'
assert p_restored.latex_octaved_sargam_letter == r'$\ddot{\mathrm{d}}$'

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "idtap"
version = "0.1.13"
version = "0.1.14"
description = "Python client library for IDTAP - Interactive Digital Transcription and Analysis Platform for Hindustani music"
readme = "README.md"
license = {text = "MIT"}
Expand Down