Skip to content

Commit 21c514b

Browse files
shanestclaude
andcommitted
Prepare ULTK for release: fix packaging, source bugs, and tests
## Packaging - Bump version from invalid `0.0.1c` to PEP 440-compliant `0.1.0` - Add `tqdm` to runtime dependencies (imported by `effcomm/optimization.py` and `language/sampling.py` but was missing from `pyproject.toml`) - Move dev-only tools (`mypy`, `pytest`, `scipy-stubs`) out of `dependencies` and into a `[dependency-groups] dev` section so they are not installed by end-users of the library - Delete `setup.py` legacy shim, which conflicted with the `uv_build` backend ## Source-code bug fixes - `language/sampling.py` `all_meanings()`: fix `Meaning` construction to use the tuple-based API (boolean values indexed parallel to `universe.referents`) instead of passing a raw referent subset-tuple, which broke after the `FrozenDict`→tuple refactor - `language/language.py` `Expression` default: replace `Meaning(FrozenDict(), ...)` with `Meaning(tuple(), ...)` and remove the now-unused `FrozenDict` import - `language/grammar/grammar.py`: - `complement()`: replace the non-existent `.referents` attribute with `tuple(not val for val in self.meaning.mapping)` - `draw_referent()`: replace `.referents` attribute access with correct `zip(universe.referents, mapping)` iteration ## Test fixes - `test_language.py`: replace all `FrozenDict({ref: bool})` `Meaning` constructors with `tuple(bool for ref in ...)`, fix `isAnimal` helper to iterate via `zip(universe.referents, mapping)` instead of dict `.items()`, rename duplicate `test_exp_subset` method to `test_exp_can_express_positive` so both positive and negative can_express tests actually run, remove unused `FrozenDict` import - `test_grammar.py`: fix `goal_meaning` to use the tuple-based `Meaning` API ## CI workflows - Replace `actions/setup-python` + `pip` with `astral-sh/setup-uv@v5` + `uv` in all three workflows (`test.yml`, `docs.yml`, `pypi-publish.yml`) - `test.yml`: `uv sync --group dev` + `uv run pytest src/tests/` - `docs.yml`: `uv sync` + `uv run pdoc ...`; upgrade to Python 3.13 - `pypi-publish.yml`: replace `python -m build` with `uv build` ## Documentation - `README.md`: update install instructions to recommend `uv sync`, note Python 3.13 requirement, update testing section to use `uv run pytest` - `CLAUDE.md`: update commands to use `uv sync --group dev` and `uv run pytest src/tests/` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 907b087 commit 21c514b

13 files changed

Lines changed: 132 additions & 65 deletions

File tree

.github/workflows/docs.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,10 @@ jobs:
1919
runs-on: ubuntu-latest
2020
steps:
2121
- uses: actions/checkout@v4
22-
- uses: actions/setup-python@v5
23-
with:
24-
python-version: '3.12'
22+
- uses: astral-sh/setup-uv@v5
2523

26-
# ADJUST THIS: install all dependencies (including pdoc)
27-
- run: pip install -e .
28-
- run: pip install pdoc
29-
# ADJUST THIS: build your documentation into docs/.
30-
# We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here.
31-
- run: pdoc src/ultk -d google --math -o ./docs
24+
- run: uv sync
25+
- run: uv run pdoc src/ultk -d google --math -o ./docs
3226

3327
- uses: actions/upload-pages-artifact@v3
3428
with:

.github/workflows/pypi-publish.yml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,10 @@ jobs:
1313
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
1414
steps:
1515
# retrieve your distributions here
16-
- uses: actions/checkout@v3
17-
- uses: actions/setup-python@v4
18-
with:
19-
python-version: '3.11'
20-
- name: Install package
21-
run: pip install --upgrade build
22-
pip install -e .
16+
- uses: actions/checkout@v4
17+
- uses: astral-sh/setup-uv@v5
2318
- name: Build dist
24-
run: python -m build
19+
run: uv build
2520
- name: Publish package distributions to PyPI
2621
uses: pypa/gh-action-pypi-publish@release/v1
2722
with:

.github/workflows/test.yml

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ jobs:
66
build:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v3
10-
- name: Set up Python
11-
uses: actions/setup-python@v4
12-
with:
13-
python-version: '3.11'
14-
- name: Install package
15-
run: pip install -e .
9+
- uses: actions/checkout@v4
10+
- uses: astral-sh/setup-uv@v5
11+
- name: Install dependencies
12+
run: uv sync --group dev
1613
- name: Test with pytest
17-
run: |
18-
pip install pytest
19-
pytest
14+
run: uv run pytest src/tests/

CLAUDE.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
ULTK (Unnatural Language ToolKit) is a Python library for computational semantic typology research — specifically for "efficient communication" analyses that explain natural language structure in terms of competing pressures: minimizing cognitive complexity vs. maximizing communicative accuracy.
8+
9+
## Commands
10+
11+
```bash
12+
# Install all dependencies (including dev group for tests)
13+
uv sync --group dev
14+
15+
# Run all tests
16+
uv run pytest src/tests/
17+
18+
# Run a single test file
19+
uv run pytest src/tests/test_language.py
20+
21+
# Run a single test by name
22+
uv run pytest src/tests/test_language.py::TestLanguage::test_name
23+
24+
# Format code (Black is enforced via CI on PRs)
25+
black src/
26+
```
27+
28+
Tests are discovered automatically by pytest from `src/tests/`. The CI workflow runs `uv run pytest src/tests/` from the repo root.
29+
30+
## Architecture
31+
32+
### Two Main Modules
33+
34+
**`ultk.language`** — Core data structures for semantic representations:
35+
- `semantics.py`: `Referent` (immutable semantic object), `Universe` (collection of Referents with a prior distribution), `Meaning` (mapping from Universe to arbitrary type T — e.g., booleans for truth values)
36+
- `language.py`: `Expression` (form + meaning pair), `Language` (frozenset of Expressions sharing a Universe). Helper `aggregate_expression_complexity()` bridges language and effcomm.
37+
- `sampling.py`: Generators for all meanings, expressions, and languages from a universe — used to enumerate the full hypothesis space.
38+
- `grammar/`: A probabilistic context-free grammar (PCFG) framework for building expressions as programs in a Language of Thought. `grammar.py` defines `Rule` and `Grammar`/`GrammaticalExpression`; `likelihood.py` provides scoring functions; `inference.py` handles MDL/Bayesian inference.
39+
40+
**`ultk.effcomm`** — Efficient communication analysis tools:
41+
- `agent.py`: RSA (Rational Speech Act) agents — `LiteralSpeaker`, `LiteralListener`, `PragmaticSpeaker`, `PragmaticListener` — represented as weight matrices.
42+
- `informativity.py`: `informativity()` and `communicative_success()` — compute how well a language supports communication (vectorized as `diag(prior) @ S @ R ⊙ U`).
43+
- `tradeoff.py`: Pareto front computation (`pareto_optimal_languages`, `non_dominated_2d`, `dominates`) for simplicity/informativeness trade-off analysis.
44+
- `optimization.py`: `EvolutionaryOptimizer` — iterative algorithm to approximate the Pareto frontier via mutations (`AddExpression`, `RemoveExpression`).
45+
- `sampling.py`: `get_hypothetical_variants()` — generates null-hypothesis languages by permuting speaker weight matrices.
46+
- `analysis.py`: Aggregation utilities for building results DataFrames.
47+
48+
**`ultk.util`**:
49+
- `frozendict.py`: `FrozenDict` — an immutable dict used extensively as keys in frozen dataclasses.
50+
- `io.py`: I/O helpers.
51+
52+
### Key Design Patterns
53+
54+
- Core objects (`Universe`, `Meaning`, `Expression`) are **frozen/immutable** (`@dataclass(frozen=True)` or manual `_frozen` flag), enabling hashing and use as dict keys.
55+
- `Meaning` stores its mapping as a `tuple[T, ...]` indexed parallel to `Universe.referents`, with `_ref_to_idx` for O(1) lookup. Access via `meaning[referent]`.
56+
- `Language` stores expressions as a `frozenset` — order-independent, hashable.
57+
- Grammar rules are defined via Python type annotations; `Rule.from_callable()` introspects function signatures to build rules automatically.
58+
59+
### Examples
60+
61+
`src/examples/` contains complete worked analyses:
62+
- `indefinites/` — efficient communication analysis of indefinite pronouns
63+
- `modals/` — semantic universals for modals
64+
- `learn_quant/` — quantifier learning

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ Read the [documentation](https://clmbr.shane.st/ultk).
99

1010
## Installing ULTK
1111

12-
First, set up a virtual environment (e.g. via [miniconda](https://docs.conda.io/en/latest/miniconda.html), `conda create -n ultk python=3.11`, and `conda activate ultk`).
12+
ULTK requires Python 3.13+. We recommend using [uv](https://docs.astral.sh/uv/) to manage dependencies.
1313

1414
1. Download or clone this repository and navigate to the root folder.
1515

16-
2. Install ULTK (We recommend doing this inside a virtual environment)
16+
2. Install ULTK and all dependencies:
1717

18-
`pip install -e .`
18+
```
19+
uv sync
20+
```
21+
22+
Alternatively, if you prefer pip inside an activated virtual environment:
23+
24+
```
25+
pip install -e .
26+
```
1927
2028
## Getting started
2129
@@ -33,7 +41,13 @@ The source code is available on github [here](https://github.com/CLMBRs/ultk).
3341
3442
## Testing
3543
36-
Unit tests are written in [pytest](https://docs.pytest.org/en/7.3.x/) and executed via running `pytest` in the `src/tests` folder.
44+
Unit tests are written in [pytest](https://docs.pytest.org/en/7.3.x/) and executed via:
45+
46+
```
47+
uv run pytest src/tests/
48+
```
49+
50+
Or, if inside an activated virtual environment: `pytest src/tests/`.
3751
3852
## References
3953

pyproject.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ultk"
3-
version = "0.0.1c"
3+
version = "0.1.0"
44
authors = [
55
{ name="Chris Haberland", email="haberc@uw.edu"},
66
{ name="Nathaniel Imel", email="nimel@uci.edu"},
@@ -16,18 +16,19 @@ classifiers = [
1616
]
1717
license = {file = "LICENSE.txt"}
1818
dependencies = [
19-
"mypy",
2019
"numpy",
2120
"nltk",
2221
"pyyaml",
2322
"pandas",
2423
"plotnine",
2524
"pathos",
26-
"pytest",
2725
"scipy>=1.7.3",
28-
"scipy-stubs[scipy]>=1.16.3.3",
26+
"tqdm",
2927
]
3028

29+
[dependency-groups]
30+
dev = ["mypy", "pytest", "scipy-stubs[scipy]>=1.16.3.3"]
31+
3132
[project.urls]
3233
"Homepage" = "https://clmbr.shane.st/ultk"
3334
"Bug Tracker" = "https://github.com/CLMBRs/ultk/issues"

setup.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/tests/test_grammar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_meaning(self):
2525
parsed_expression = TestGrammar.grammar.parse(TestGrammar.geq2_expr_str)
2626
expr_meaning = parsed_expression.evaluate(TestGrammar.universe)
2727
goal_meaning = Meaning(
28-
{referent: referent.num > 2 for referent in TestGrammar.referents},
28+
tuple(referent.num > 2 for referent in TestGrammar.referents),
2929
TestGrammar.universe,
3030
)
3131
assert expr_meaning == goal_meaning

src/tests/test_language.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from ultk.language.language import Expression, Language
66
from ultk.language.semantics import Referent, Universe, Meaning
7-
from ultk.util.frozendict import FrozenDict
87

98

109
class TestLanguage:
@@ -27,35 +26,35 @@ class TestLanguage:
2726
dog = Expression(
2827
form="dog",
2928
meaning=Meaning(
30-
mapping=FrozenDict({ref: ref.name == "dog" for ref in uni_refs}),
29+
mapping=tuple(ref.name == "dog" for ref in uni_refs),
3130
universe=uni,
3231
),
3332
)
3433
cat = Expression(
3534
form="cat",
3635
meaning=Meaning(
37-
mapping=FrozenDict({ref: ref.name == "cat" for ref in uni_refs}),
36+
mapping=tuple(ref.name == "cat" for ref in uni_refs),
3837
universe=uni,
3938
),
4039
)
4140
tree = Expression(
4241
form="tree",
4342
meaning=Meaning(
44-
mapping=FrozenDict({ref: ref.name == "tree" for ref in uni_refs}),
43+
mapping=tuple(ref.name == "tree" for ref in uni_refs),
4544
universe=uni,
4645
),
4746
)
4847
shroom = Expression(
4948
form="shroom",
5049
meaning=Meaning(
51-
mapping=FrozenDict({ref: ref.name == "shroom" for ref in uni_refs}),
50+
mapping=tuple(ref.name == "shroom" for ref in uni_refs),
5251
universe=uni,
5352
),
5453
)
5554
bird = Expression(
5655
form="bird",
5756
meaning=Meaning(
58-
mapping=FrozenDict({ref: ref.name == "bird" for ref in uni_refs}),
57+
mapping=tuple(ref.name == "bird" for ref in uni_refs),
5958
universe=uni,
6059
),
6160
)
@@ -65,7 +64,7 @@ class TestLanguage:
6564
lang_subset_expr = Language(expressions=tuple([dog, cat, tree]))
6665
lang_of_different_order = Language(expressions=tuple([dog, cat, shroom, tree]))
6766

68-
def test_exp_subset(self):
67+
def test_exp_can_express_positive(self):
6968
assert TestLanguage.dog.can_express(Referent("dog", {"phylum": "animal"}))
7069

7170
def test_exp_subset(self):
@@ -83,11 +82,9 @@ def test_language_universe_check(self):
8382
Expression(
8483
form="dog",
8584
meaning=Meaning(
86-
mapping=FrozenDict(
87-
{
88-
ref: ref.name == "dog"
89-
for ref in TestLanguage.uni.referents
90-
}
85+
mapping=tuple(
86+
ref.name == "dog"
87+
for ref in TestLanguage.uni.referents
9188
),
9289
universe=TestLanguage.uni2,
9390
),
@@ -98,8 +95,8 @@ def test_language_universe_check(self):
9895
def test_language_degree(self):
9996
def isAnimal(exp: Expression) -> bool:
10097
print("checking phylum of " + str(exp))
101-
for k, v in exp.meaning.mapping.items():
102-
if v and k.phylum != "animal":
98+
for ref, v in zip(exp.meaning.universe.referents, exp.meaning.mapping):
99+
if v and ref.phylum != "animal":
103100
return False
104101
return True
105102

src/ultk/language/grammar/grammar.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,16 @@ def complement(self) -> Meaning:
168168
the expression evaluates to False."""
169169

170170
return Meaning(
171-
tuple(set(self.meaning.universe.referents) - set(self.meaning.referents)),
171+
tuple(not val for val in self.meaning.mapping),
172172
self.meaning.universe,
173173
)
174174

175175
def draw_referent(self, complement=False):
176176
"""Get a random referent from the meaning's referents."""
177+
universe_refs = self.meaning.universe.referents
177178
if complement:
178-
return random.choice(list(self.complement().referents))
179-
return random.choice(list(self.meaning.referents))
179+
return random.choice([r for r, v in zip(universe_refs, self.meaning.mapping) if not v])
180+
return random.choice([r for r, v in zip(universe_refs, self.meaning.mapping) if v])
180181

181182
def to_dict(self) -> dict:
182183
the_dict = super().to_dict()

0 commit comments

Comments
 (0)