Skip to content

Commit a80f026

Browse files
jon-myersclaude
andauthored
Add LaTeX sargam letter properties for better visualization rendering (#20)
* Add LaTeX sargam letter properties for better visualization rendering This addresses issue #19 by adding LaTeX-compatible sargam notation properties: - latex_sargam_letter: LaTeX-compatible base sargam letter - latex_octaved_sargam_letter: LaTeX math mode with properly positioned diacritics - _octave_latex_diacritic(): Helper method for LaTeX octave notation Benefits: - Perfect rendering in matplotlib and LaTeX-compatible libraries - Consistent cross-platform appearance regardless of font - Backward compatibility - all existing properties unchanged - Clean mathematical notation instead of problematic Unicode diacritics Closes #19 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Bump version to 0.1.14 for LaTeX sargam properties release - Updated __init__.py: 0.1.13 → 0.1.14 - Updated pyproject.toml: 0.1.13 → 0.1.14 - Updated docs/conf.py: 0.1.7 → 0.1.14 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 57b9078 commit a80f026

File tree

5 files changed

+222
-4
lines changed

5 files changed

+222
-4
lines changed

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
project = 'IDTAP API'
1414
copyright = '2025, Jon Myers'
1515
author = 'Jon Myers'
16-
release = '0.1.7'
17-
version = '0.1.7'
16+
release = '0.1.14'
17+
version = '0.1.14'
1818

1919
# -- General configuration ---------------------------------------------------
2020
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

idtap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Python API package exposing IDTAP data classes and client."""
22

3-
__version__ = "0.1.13"
3+
__version__ = "0.1.14"
44

55
from .client import SwaraClient
66
from .auth import login_google

idtap/classes/pitch.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ def _octave_diacritic(self) -> str:
290290
3: '\u20DB'
291291
}
292292
return mapping.get(self.oct, '')
293+
294+
def _octave_latex_diacritic(self) -> str:
295+
"""Convert octave to LaTeX math notation for proper diacritic positioning."""
296+
mapping = {
297+
-3: r'\underset{\bullet\bullet\bullet}', # Triple dot below
298+
-2: r'\underset{\bullet\bullet}', # Double dot below
299+
-1: r'\underset{\bullet}', # Single dot below
300+
1: r'\dot', # Single dot above
301+
2: r'\ddot', # Double dot above
302+
3: r'\dddot' # Triple dot above
303+
}
304+
return mapping.get(self.oct, '')
293305

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

344+
@property
345+
def latex_sargam_letter(self) -> str:
346+
"""LaTeX-compatible base sargam letter."""
347+
return self.sargam_letter
348+
349+
@property
350+
def latex_octaved_sargam_letter(self) -> str:
351+
"""LaTeX math mode sargam letter with properly positioned diacritics."""
352+
base_letter = self.sargam_letter
353+
latex_diacritic = self._octave_latex_diacritic()
354+
355+
if not latex_diacritic:
356+
return base_letter # No octave marking
357+
elif latex_diacritic.startswith(r'\underset'):
358+
return f'${latex_diacritic}{{\\mathrm{{{base_letter}}}}}$'
359+
else:
360+
return f'${latex_diacritic}{{\\mathrm{{{base_letter}}}}}$'
361+
332362
@property
333363
def a440_cents_deviation(self) -> str:
334364
c0 = 16.3516

idtap/tests/pitch_test.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,191 @@ def test_constructor_rejects_undefined_ratios():
552552
with pytest.raises(SyntaxError):
553553
Pitch({'ratios': ratios2})
554554

555+
556+
def test_latex_sargam_letter_basic():
557+
"""Test that latex_sargam_letter returns the same as sargam_letter."""
558+
# Test all sargam letters in both raised and lowered forms
559+
sargam_tests = [
560+
({'swara': 'sa'}, 'S'),
561+
({'swara': 're', 'raised': False}, 'r'),
562+
({'swara': 're', 'raised': True}, 'R'),
563+
({'swara': 'ga', 'raised': False}, 'g'),
564+
({'swara': 'ga', 'raised': True}, 'G'),
565+
({'swara': 'ma', 'raised': False}, 'm'),
566+
({'swara': 'ma', 'raised': True}, 'M'),
567+
({'swara': 'pa'}, 'P'),
568+
({'swara': 'dha', 'raised': False}, 'd'),
569+
({'swara': 'dha', 'raised': True}, 'D'),
570+
({'swara': 'ni', 'raised': False}, 'n'),
571+
({'swara': 'ni', 'raised': True}, 'N'),
572+
]
573+
574+
for options, expected in sargam_tests:
575+
p = Pitch(options)
576+
assert p.latex_sargam_letter == expected
577+
assert p.latex_sargam_letter == p.sargam_letter
578+
579+
580+
def test_latex_octaved_sargam_letter_no_octave():
581+
"""Test LaTeX octaved sargam letter with no octave marking (oct=0)."""
582+
p = Pitch({'swara': 'sa', 'oct': 0})
583+
assert p.latex_octaved_sargam_letter == 'S'
584+
585+
p = Pitch({'swara': 're', 'raised': False, 'oct': 0})
586+
assert p.latex_octaved_sargam_letter == 'r'
587+
588+
589+
def test_latex_octaved_sargam_letter_positive_octaves():
590+
"""Test LaTeX octaved sargam letter with positive octaves (dots above)."""
591+
# Test oct=1 (single dot above)
592+
p = Pitch({'swara': 'sa', 'oct': 1})
593+
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{S}}$'
594+
595+
p = Pitch({'swara': 're', 'raised': False, 'oct': 1})
596+
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{r}}$'
597+
598+
p = Pitch({'swara': 'ga', 'raised': True, 'oct': 1})
599+
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{G}}$'
600+
601+
# Test oct=2 (double dot above)
602+
p = Pitch({'swara': 'ma', 'raised': False, 'oct': 2})
603+
assert p.latex_octaved_sargam_letter == r'$\ddot{\mathrm{m}}$'
604+
605+
p = Pitch({'swara': 'pa', 'oct': 2})
606+
assert p.latex_octaved_sargam_letter == r'$\ddot{\mathrm{P}}$'
607+
608+
# Test oct=3 (triple dot above)
609+
p = Pitch({'swara': 'dha', 'raised': True, 'oct': 3})
610+
assert p.latex_octaved_sargam_letter == r'$\dddot{\mathrm{D}}$'
611+
612+
p = Pitch({'swara': 'ni', 'raised': False, 'oct': 3})
613+
assert p.latex_octaved_sargam_letter == r'$\dddot{\mathrm{n}}$'
614+
615+
616+
def test_latex_octaved_sargam_letter_negative_octaves():
617+
"""Test LaTeX octaved sargam letter with negative octaves (dots below)."""
618+
# Test oct=-1 (single dot below)
619+
p = Pitch({'swara': 'sa', 'oct': -1})
620+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{S}}$'
621+
622+
p = Pitch({'swara': 're', 'raised': True, 'oct': -1})
623+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{R}}$'
624+
625+
# Test oct=-2 (double dot below)
626+
p = Pitch({'swara': 'ga', 'raised': False, 'oct': -2})
627+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet}{\mathrm{g}}$'
628+
629+
p = Pitch({'swara': 'ma', 'raised': True, 'oct': -2})
630+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet}{\mathrm{M}}$'
631+
632+
# Test oct=-3 (triple dot below)
633+
p = Pitch({'swara': 'pa', 'oct': -3})
634+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet\bullet}{\mathrm{P}}$'
635+
636+
p = Pitch({'swara': 'dha', 'raised': False, 'oct': -3})
637+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet\bullet\bullet}{\mathrm{d}}$'
638+
639+
640+
def test_latex_octaved_sargam_letter_all_sargam_all_octaves():
641+
"""Test all sargam letters across all octave levels."""
642+
sargam_letters = ['sa', 're', 'ga', 'ma', 'pa', 'dha', 'ni']
643+
octave_expected = {
644+
-3: r'\underset{\bullet\bullet\bullet}',
645+
-2: r'\underset{\bullet\bullet}',
646+
-1: r'\underset{\bullet}',
647+
0: '',
648+
1: r'\dot',
649+
2: r'\ddot',
650+
3: r'\dddot'
651+
}
652+
653+
for swara in sargam_letters:
654+
for raised in [True, False]:
655+
# Skip invalid combinations (sa and pa are always raised)
656+
if swara in ['sa', 'pa'] and not raised:
657+
continue
658+
659+
p = Pitch({'swara': swara, 'raised': raised})
660+
base_letter = p.sargam_letter
661+
662+
for oct in range(-3, 4):
663+
p_oct = Pitch({'swara': swara, 'raised': raised, 'oct': oct})
664+
expected_latex = octave_expected[oct]
665+
666+
if oct == 0:
667+
expected_result = base_letter
668+
elif expected_latex.startswith(r'\underset'):
669+
expected_result = f'${expected_latex}{{\\mathrm{{{base_letter}}}}}$'
670+
else:
671+
expected_result = f'${expected_latex}{{\\mathrm{{{base_letter}}}}}$'
672+
673+
assert p_oct.latex_octaved_sargam_letter == expected_result
674+
675+
676+
def test_latex_properties_preserve_backward_compatibility():
677+
"""Test that existing properties are not affected by LaTeX additions."""
678+
test_cases = [
679+
{'swara': 'sa', 'oct': 0},
680+
{'swara': 're', 'raised': False, 'oct': 1},
681+
{'swara': 'ga', 'raised': True, 'oct': -1},
682+
{'swara': 'ma', 'raised': False, 'oct': 2},
683+
{'swara': 'pa', 'oct': -2},
684+
{'swara': 'dha', 'raised': True, 'oct': 3},
685+
{'swara': 'ni', 'raised': False, 'oct': -3},
686+
]
687+
688+
for options in test_cases:
689+
p = Pitch(options)
690+
691+
# All existing properties should work exactly as before
692+
assert hasattr(p, 'sargam_letter')
693+
assert hasattr(p, 'octaved_sargam_letter')
694+
assert hasattr(p, 'frequency')
695+
assert hasattr(p, 'numbered_pitch')
696+
assert hasattr(p, 'chroma')
697+
698+
# New LaTeX properties should be available
699+
assert hasattr(p, 'latex_sargam_letter')
700+
assert hasattr(p, 'latex_octaved_sargam_letter')
701+
702+
# latex_sargam_letter should match sargam_letter
703+
assert p.latex_sargam_letter == p.sargam_letter
704+
705+
706+
def test_latex_octave_diacritic_helper():
707+
"""Test the _octave_latex_diacritic helper method."""
708+
# Test all octave levels
709+
octave_mapping = {
710+
-3: r'\underset{\bullet\bullet\bullet}',
711+
-2: r'\underset{\bullet\bullet}',
712+
-1: r'\underset{\bullet}',
713+
0: '',
714+
1: r'\dot',
715+
2: r'\ddot',
716+
3: r'\dddot'
717+
}
718+
719+
for oct, expected in octave_mapping.items():
720+
p = Pitch({'swara': 'sa', 'oct': oct})
721+
assert p._octave_latex_diacritic() == expected
722+
723+
724+
def test_latex_properties_edge_cases():
725+
"""Test LaTeX properties with edge cases and various combinations."""
726+
# Test with log_offset (should not affect LaTeX output)
727+
p = Pitch({'swara': 'ga', 'raised': False, 'oct': 1, 'log_offset': 0.1})
728+
assert p.latex_octaved_sargam_letter == r'$\dot{\mathrm{g}}$'
729+
730+
# Test with different fundamentals (should not affect LaTeX output)
731+
p = Pitch({'swara': 'ma', 'raised': True, 'oct': -1, 'fundamental': 440.0})
732+
assert p.latex_octaved_sargam_letter == r'$\underset{\bullet}{\mathrm{M}}$'
733+
734+
# Test serialization includes existing functionality
735+
p = Pitch({'swara': 'dha', 'raised': False, 'oct': 2})
736+
json_data = p.to_json()
737+
p_restored = Pitch.from_json(json_data)
738+
739+
# LaTeX properties should work after deserialization
740+
assert p_restored.latex_sargam_letter == 'd'
741+
assert p_restored.latex_octaved_sargam_letter == r'$\ddot{\mathrm{d}}$'
742+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "idtap"
7-
version = "0.1.13"
7+
version = "0.1.14"
88
description = "Python client library for IDTAP - Interactive Digital Transcription and Analysis Platform for Hindustani music"
99
readme = "README.md"
1010
license = {text = "MIT"}

0 commit comments

Comments
 (0)