Skip to content

Commit 6fcd7d1

Browse files
authored
Merge pull request #79 from CAVEconnectome/typehints+spatial
Typehints+spatial
2 parents ca7e375 + 22b8a07 commit 6fcd7d1

File tree

12 files changed

+2385
-74
lines changed

12 files changed

+2385
-74
lines changed

CLAUDE.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# NGLUI Development Guide
2+
3+
This guide documents the development workflow, testing strategies, and coding standards for the NGLUI package.
4+
5+
## Development Environment
6+
7+
### Package Management: UV + Poe
8+
9+
This project uses **UV** for dependency management and **Poe** for task running instead of the system Python environment:
10+
11+
```bash
12+
# Install dependencies (automatically creates virtual environment)
13+
uv sync
14+
15+
# Run tasks via Poe (preferred over direct python commands)
16+
poe test # Run tests with coverage
17+
poe doc-preview # Preview documentation
18+
poe bump patch # Bump version (patch/minor/major)
19+
poe drybump patch # Dry run version bump
20+
```
21+
22+
### Key Commands
23+
24+
From `pyproject.toml`:
25+
26+
- **Testing**: `poe test``uv run pytest --cov=nglui tests`
27+
- **Documentation**: `poe doc-preview``uv run mkdocs serve`
28+
- **Version Management**: `poe bump patch/minor/major`
29+
- **Linting**: `uv run ruff check src/ tests/`
30+
31+
## Python Version Requirements
32+
33+
- **Minimum**: Python 3.10+ (leverages match statements and improved type annotations)
34+
- **Tested**: Python 3.10, 3.11, 3.12
35+
- **Compatibility**: Use `typing-extensions` for Python < 3.11 features
36+
37+
## Type Hinting Standards
38+
39+
### Required Practices
40+
41+
- **All functions/methods** must have complete type annotations
42+
- **All class attributes** must have type hints (use `attrs` with type annotations)
43+
- **Import patterns**:
44+
```python
45+
from typing import Optional, Union, Literal, Any, Dict, List, Tuple
46+
from typing_extensions import Self # For Python < 3.11
47+
```
48+
49+
### Common Patterns
50+
51+
```python
52+
from typing import Optional, Union, Literal
53+
import attrs
54+
import numpy as np
55+
import pandas as pd
56+
57+
@attrs.define
58+
class ExampleClass:
59+
# Required attributes with types
60+
name: str
61+
values: List[float] = attrs.field(factory=list)
62+
63+
# Optional attributes
64+
description: Optional[str] = None
65+
66+
# Use converters for type safety
67+
point: List[float] = attrs.field(converter=strip_numpy_types)
68+
69+
# Use validators for constraints
70+
resolution: Optional[np.ndarray] = attrs.field(
71+
default=None,
72+
converter=attrs.converters.optional(np.array)
73+
)
74+
75+
def process_data(
76+
data: pd.DataFrame,
77+
column: Union[str, List[str]],
78+
optional_param: Optional[int] = None
79+
) -> Tuple[pd.DataFrame, Dict[str, Any]]:
80+
"""Process dataframe with proper type annotations."""
81+
pass
82+
```
83+
84+
## Testing Strategy
85+
86+
### Dual-Level Testing Approach
87+
88+
Always implement **both** high-level integration tests and low-level unit tests:
89+
90+
#### High-Level Integration Tests
91+
Focus on real-world workflows and end-to-end functionality:
92+
93+
```python
94+
def test_complete_annotation_workflow(self):
95+
"""Test full annotation workflow with real data."""
96+
# Create realistic DataFrame
97+
df = pd.DataFrame({
98+
'x': [100, 200, 300],
99+
'y': [150, 250, 350],
100+
'z': [10, 20, 30],
101+
'segment_id': [12345, 67890, 11111]
102+
})
103+
104+
# Test complete workflow
105+
vs = ViewerState()
106+
vs.add_points(
107+
data=df,
108+
point_column=['x', 'y', 'z'],
109+
segment_column='segment_id'
110+
)
111+
112+
# Verify end-to-end behavior
113+
assert len(vs.layers) == 1
114+
assert vs.layers[0].layer_type == 'annotation'
115+
```
116+
117+
#### Low-Level Unit Tests
118+
Focus on individual methods, edge cases, and error conditions:
119+
120+
```python
121+
def test_scale_points_edge_cases(self):
122+
"""Test scaling with edge cases."""
123+
point = PointAnnotation(point=[100, 200, 300])
124+
125+
# Test zero scaling
126+
point._scale_points([0, 1, 2])
127+
assert point.point == [0.0, 200.0, 600.0]
128+
129+
# Test negative scaling
130+
point = PointAnnotation(point=[100, 200, 300])
131+
point._scale_points([-1, -1, -1])
132+
assert point.point == [-100.0, -200.0, -300.0]
133+
```
134+
135+
### Testing Guidelines
136+
137+
- **Coverage Target**: Aim for >90% line coverage, >85% branch coverage
138+
- **Test Organization**: Mirror source structure in `tests/` directory
139+
- **Mocking Strategy**: Mock external dependencies (neuroglancer), test actual behavior for internal logic
140+
- **Parametrization**: Use `@pytest.mark.parametrize` for testing multiple inputs
141+
- **Fixtures**: Create reusable test data in `conftest.py`
142+
143+
### Running Tests
144+
145+
```bash
146+
# Full test suite with coverage
147+
poe test
148+
149+
# Specific test file
150+
uv run pytest tests/test_ngl_annotations.py -v
151+
152+
# Coverage report
153+
uv run pytest --cov=nglui --cov-report=html tests/
154+
```
155+
156+
## Code Architecture Patterns
157+
158+
### Core Components
159+
160+
- **StateBuilder**: Main entry point for creating neuroglancer states
161+
- **Annotations**: Point, Line, Ellipsoid, BoundingBox with neuroglancer conversion
162+
- **Components**: ImageLayer, AnnotationLayer, SegmentationLayer
163+
- **DataMap**: Dynamic data binding with priority system
164+
- **Utils**: Type conversion, color parsing, coordinate handling
165+
166+
### Key Design Patterns
167+
168+
1. **Attrs Classes**: Use `@attrs.define` for all data classes
169+
2. **Converter Functions**: `strip_numpy_types`, coordinate transformers
170+
3. **Method Chaining**: Fluent API for building complex states
171+
4. **DataMap Priority**: Higher numbers override lower numbers for dynamic updates
172+
5. **Column Handling**: Support both string columns and list column specifications
173+
174+
### Point Column Feature
175+
176+
Support flexible coordinate specification:
177+
178+
```python
179+
# Explicit column list
180+
point_column=['x', 'y', 'z']
181+
182+
# Prefix expansion
183+
point_column='position' # → ['position_x', 'position_y', 'position_z']
184+
```
185+
186+
## Common Development Workflows
187+
188+
### Adding New Annotation Types
189+
190+
1. Create class inheriting from `AnnotationBase`
191+
2. Add type hints for all attributes
192+
3. Implement `_scale_points()` method
193+
4. Implement `to_neuroglancer()` method
194+
5. Add comprehensive tests (unit + integration)
195+
6. Update documentation
196+
197+
### Adding New Layer Types
198+
199+
1. Create class inheriting from `LayerBase`
200+
2. Implement required abstract methods
201+
3. Add DataMap integration
202+
4. Create builder methods in StateBuilder
203+
5. Add comprehensive tests
204+
6. Update documentation
205+
206+
### Bug Fixes
207+
208+
1. Write failing test first (TDD approach)
209+
2. Implement minimal fix
210+
3. Ensure all tests pass
211+
4. Check type annotations are correct
212+
5. Run full test suite: `poe test`
213+
214+
## Documentation Standards
215+
216+
- **Docstrings**: Use Google/NumPy style docstrings
217+
- **Examples**: Include usage examples in docstrings
218+
- **Type Information**: Document parameter and return types in docstrings
219+
- **API Documentation**: Use mkdocs with mkdocstrings for auto-generation
220+
221+
```python
222+
def add_points(
223+
self,
224+
data: pd.DataFrame,
225+
point_column: Union[str, List[str]],
226+
segment_column: Optional[str] = None
227+
) -> Self:
228+
"""Add point annotations to the viewer state.
229+
230+
Args:
231+
data: DataFrame containing point data
232+
point_column: Column name(s) for coordinates. Can be:
233+
- Single string for prefix (e.g., 'pos' → ['pos_x', 'pos_y', 'pos_z'])
234+
- List of column names (e.g., ['x', 'y', 'z'])
235+
segment_column: Optional column containing segment IDs
236+
237+
Returns:
238+
Self for method chaining
239+
240+
Examples:
241+
>>> vs = ViewerState()
242+
>>> vs.add_points(df, point_column=['x', 'y', 'z'])
243+
>>> vs.add_points(df, point_column='position') # Uses position_x, position_y, position_z
244+
"""
245+
```
246+
247+
## Pre-Commit Workflow
248+
249+
Before committing changes:
250+
251+
```bash
252+
# Run full test suite
253+
poe test
254+
255+
# Run linting
256+
uv run ruff check src/ tests/
257+
258+
# Check type annotations
259+
uv run mypy src/ --ignore-missing-imports # if mypy is configured
260+
261+
# Ensure documentation builds
262+
poe doc-preview
263+
```
264+
265+
## Release Process
266+
267+
```bash
268+
# Check what will be bumped
269+
poe drybump patch # or minor/major
270+
271+
# Bump version and create tag
272+
poe bump patch # or minor/major
273+
```
274+
275+
This automatically:
276+
- Updates version in `pyproject.toml` and `src/nglui/__init__.py`
277+
- Creates git commit and tag
278+
- Runs pre/post commit hooks including `uv sync`

docs/changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
This project attempts to follow [Semantic Versioning](https://semver.org) and uses [Keep-a-Changelog formatting](https://keepachangelog.com/en/1.0.0/). But I make mistakes sometimes.
44

5+
## [4.5.1] - 2025-08-29
6+
7+
### Added
8+
9+
- **StateBuilder**: Added support for passing an x,y,z column names explicitly as a list and improved docstrings around this parameter.
10+
11+
### Fixed
12+
13+
- **StateBuilder**: Slight improvements to docstrings.
14+
515
## [4.5.0] - 2025-08-15
616

717
### Added

docs/usage/statebuilder.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ seg_layer = (
325325
)
326326
```
327327

328+
As in images, any specification of sources can be either a string URL or a list of URLs.
329+
328330
Would select all segment ids in `my_dataframe['pt_root_id']` to the segmentation layer, toggle their visibility by the boolean values in `my_dataframe['is_visible']`, and set their colors to the values in `my_dataframe['color_value']`.
329331
Colors can be hex values or web-readable color names, such as `'red'`, `'blue'`, or `'green'`.
330332

@@ -341,6 +343,27 @@ Segment properties can be treated as an additional source and can be added direc
341343
However, if you want to generate segment properties dynamically from a dataframe, you can use the `add_segment_properties` method, which will generate the segment properties file, upload it to a CAVE state server, and attach the resulting URL to the segmentation layer.
342344
Note that `add_segment_properties` requires a CAVEclient object and also has a `dry_run` option to avoid many duplicative uploads while developing your code.
343345

346+
Segment properties can be added inline to the ViewerState and do not require a specified segmentation layer if only one segmentation layer is present.
347+
There is also a `random_columns` parameter which can be used to add a random number column to make it easier to subsample sets of cells without using a smaller table.
348+
For example, to download a CAVE table and then add it to a viewerstate with one random column, you could do:
349+
350+
```pycon
351+
from caveclient import CAVEclient
352+
client = CAVEclient('minnie65_public')
353+
ct_df = client.materialize.tables.allen_column_mtypes_v2.get_all(split_positions=True)
354+
vs = (
355+
ViewerState(client=client)
356+
.add_layers_from_client(segmentation='seg')
357+
.add_segment_properties(
358+
data=ct_df,
359+
id_column='pt_supervoxel_id',
360+
label_column='target_id',
361+
tag_value_columns=['cell_type'],
362+
random_columns=1,
363+
)
364+
)
365+
```
366+
344367
See the [Segment Properties documentation](segmentprops.md) for more information on how to generate segment properties in Neuroglancer and what different options mean.
345368

346369
#### Skeleton Shader
@@ -478,7 +501,7 @@ This might be useful if you want to preserve things that cannot be easily set in
478501
NGLui integrates with the CAVEclient to make it easy to work with Neuroglancer states that are hosted on CAVE.
479502
You can use an initialized CAVEclient object to configure the resolution, image, and segmentation layers of a ViewerState:
480503

481-
``` py
504+
``` pycon
482505
from caveclient import CAVEclient
483506
client = CAVEclient('minnie65_public')
484507
viewer_state = (
@@ -507,7 +530,7 @@ This is handled now through a special `DataMap` class that can be used to replac
507530
For example, instead of making a Image and Segmentation layers with pre-specified sources, you can add the source as a 'DataMap` object.
508531
Each DataMap has a `key` attribute that is used to map the data you will provide later to the correct role in state creation.
509532

510-
``` py
533+
``` pycon
511534
from nglui.statebuilder import ViewerState, SegmentationLayer, DataMap
512535

513536
viewer_state = (
@@ -522,7 +545,7 @@ If you tried to run `viewer_state.to_link()` now, you would get an `UnmappedData
522545
To actually map data, you can use the `map` method of the ViewerState object, which takes a dictionary of key-value pairs where the keys are the DataMap keys and the values are the actual data to be used.
523546
For example, to replicate the previous example with the CAVEclient info, you could do:
524547

525-
``` py
548+
``` pycon
526549
viewer_state.map(
527550
{
528551
'img_source': 'precomputed://https://bossdb-open-data.s3.amazonaws.com/iarpa_microns/minnie/minnie65/em',
@@ -539,7 +562,7 @@ Applying this across a list of data sources can easily generate a large collecti
539562
The other principle use of DataMaps is to support annotation creation by replacing the `data` argument in the `add_points`, `add_lines`, `add_boxes`, and `add_ellipses` methods.
540563
For example, you can create a DataMap for the annotation data and then use it to add points to the annotation layer with the following pattern:
541564

542-
``` py
565+
``` pycon
543566
from nglui.statebuilder import ViewerState, AnnotationLayer, DataMap
544567
viewer_state = (
545568
ViewerState()

0 commit comments

Comments
 (0)