Annotations¶
An Annotation stores reusable content. An annotation occurrence attaches that content to a Dataset,
Task, Record, Source, or Signal.
from timenet.types import Annotation
condition = Annotation(
key="condition",
value="healthy",
description="Condition assigned by the source dataset.",
)
occurrence = record.annotate(condition)
annotate() returns the attached occurrence. A Task must refer to this occurrence, not the unattached
content object.
Content and occurrence fields¶
Reusable content includes these fields:
key,value,unit, anddescriptionmetadata, which stores JSON-compatible content details
Each occurrence adds its own source, confidence, and occurrence_metadata. This design lets
several objects share one long description without sharing occurrence details. The writer stores
equal content once. Neither the content nor an occurrence has an ID in Python.
Reuse content¶
Annotation is final, so it can't be subclassed. To reuse content, keep a constant. For content
that varies, write a builder function that returns an Annotation:
from typing import Literal
from timenet.types import Annotation
# A constant to reuse
HEALTHY_ANNOTATION = Annotation(key="condition", value="healthy")
def quality_annotation(level: Literal["low", "medium", "high"]) -> Annotation:
return Annotation(key="quality", value=level)
record.annotate(HEALTHY_ANNOTATION)
record.annotate(quality_annotation("high"))
Every occurrence of a constant shares its content. A builder call creates a new Annotation, and
calls with equal arguments give equal content. The writer stores equal content once either way, and
the reader rebuilds a plain Annotation that equals the one written. Large text read back from a
release is compared by where it is stored, without a read. So annotations read from one content
row are stored as one row again, but stored text and the same text in memory become two rows.
Static context¶
An Annotation without a span describes its complete owner:
record.annotate(Annotation(key="age", value=54, unit="years"))
record.annotate(Annotation(key="condition", value="healthy"))
Use metadata for untyped implementation details. Use an Annotation when consumers need a named,
typed fact.
Points and intervals¶
A TimePoint marks one offset. A TimeInterval covers a half-open range, [start, end).
from timenet.types import Annotation, TimeInterval, TimePoint
vibration.annotate(
Annotation(
key="impact",
span=TimePoint.seconds(
4.2,
),
)
)
vibration.annotate(
Annotation(
key="fault",
value="bearing fault",
span=TimeInterval.seconds(
5.0,
8.0,
),
)
)
The owner determines scope. A Signal annotation covers that Signal. A
Source annotation covers its subtree. A Record annotation covers
the Record. An annotation span cannot set time_series, because the owner sets the scope.
Use source.select(signals=(ecg, spo2)).annotate(annotation) for a subset. Each selected Signal gets
a separate occurrence with shared content. Other Signals remain unchanged.
Time spans use whole microseconds on the Record timeline. Step spans are reserved for Tasks on ordinal Signals.
Valid values¶
An annotation value can be a string, integer, float, Boolean, or list of strings. For example, a
list of strings can hold the names of activities in a recording. To define the allowed labels for a
classification target, use a LabelSet. See Tasks.
An Annotation must have a value, a span, or both. A marker can omit its value when its span carries the meaning.
Use annotations in Tasks¶
A Task can use attached occurrences as input context or as its expected output:
task = AnswerTask(
inputs=(record,),
prompt="What condition does the source report?",
target_annotations=(occurrence,),
)
Use input_annotations for context. Use target_annotations for the answer. A Task cannot combine
inline targets with target_annotations.
The occurrences must lie on the dataset itself or on a Record that the Task references, as
Records that a Task references describes. add_task(),
add_tasks(), and the check of streamed Tasks reject an occurrence on any other Record.