Temporal Normalization¶
Run the synthetic SDOH and timeline example for public normalization calls and a state-only projection that omits date values.
Clinical notes mix absolute dates with expressions such as 3 weeks ago, since last March, POD 2, and q6h x5 days. The temporal normalizer converts caller-supplied spans into deterministic TIMEX3-style DATE, TIME, DURATION, and SET records with ISO 8601 values.
Normalize temporal spans¶
from openmed.clinical import normalize_temporal
text = "Symptoms began 3 weeks ago; continue q6h x5 days."
spans = [
{"start": 15, "end": 26},
{"start": 37, "end": 48},
]
records = normalize_temporal(
text,
spans,
reference_time="2026-06-15T10:30:00Z",
)
records[0].to_dict()
# {
# "text": "3 weeks ago",
# "span": [15, 26],
# "start": 15,
# "end": 26,
# "type": "DATE",
# "value": "2026-05-25",
# "anchor": "2026-06-15T10:30:00+00:00",
# "granularity_flags": ["day"],
# }
records[1].value
# "R20/PT6H"
Spans can also be two-item (start, end) sequences. Results preserve input order and use inclusive start/exclusive end offsets, so text[result.start:result.end] == result.text always holds.
Values and anchors¶
| Type | Examples | Value form |
|---|---|---|
DATE | 2026-06-01, 3 weeks ago, last March, POD 2 | ISO date, month, year, or datetime |
TIME | 14:30, 2:30 PM | ISO datetime when anchored; ISO time when unanchored |
DURATION | for 5 days, 2 weeks | ISO duration such as P5D or P2W |
SET | daily, q6h, q6h x5 days | ISO repeating interval such as R/P1D or R20/PT6H |
For relative expressions, anchor records the normalized reference time used for date arithmetic. reference_time accepts an ISO string, date, datetime, or None. Passing None never substitutes the current time.
Shared ISO input profile¶
Clinical and Journey ISO parsing uses openmed.core.iso_temporal rather than the interpreter's changing fromisoformat() grammar. The shared profile accepts ASCII calendar dates YYYY-MM-DD and optional HH[:MM[:SS[.ffffff]]] times, with a T, t or space separator. A fractional second has one to six digits. Offsets are Z/z or +HH:MM/-HH:MM; offset minutes are 00–59 and the absolute offset is less than 24 hours.
Basic-format dates/times, week dates, ordinal dates, compact +HHMM offsets, offset seconds, comma fractions, more than six fractional digits, arbitrary Unicode separators and impossible calendar/clock values are rejected. Existing normalization and contract errors remain the caller-visible outcomes and do not echo the rejected value. The shared parsers raise only ValueError("invalid_iso_temporal"), without retaining a raw parsing exception.
parse_iso_date() requires a complete calendar date; parse_iso_datetime() maps a date-only input to naive midnight, preserving existing date coercion; parse_iso_time() parses a clock value. Each caller still owns requirements for a time, timezone or reduced precision. Journey timestamps, for example, continue to require a timezone. Temporal-expression rules, year/month precision and caller-supplied date/datetime objects keep their existing semantics.
The same synthetic conformance vectors run in the existing Python 3.10–3.13 compatibility lanes. An AST check prevents direct clinical fromisoformat access. Python's datetime documentation records the grammar expansion in 3.11; this profile is defined independently of that expansion.
since X stores the normalized start in value, the supplied document time as anchor, and adds the since flag. This keeps the value layer separate from interval and event ordering, which belong to the timeline resolver.
Granularity and ambiguity¶
granularity_flags identifies the supported precision (second, minute, hour, day, week, month, year, or part_of_day) and adds qualifiers when needed:
unanchored: a relative date has no supplied reference time, or a time has no document date.ambiguous: the source permits multiple interpretations, such as03/04/2026; the normalizer returnsvalue=Noneinstead of choosing a locale convention.approximate: the source explicitly saysabout,around,roughly, orapproximately.bounded: a recurring set includes a duration, such asq6h x5 days.since: the value is the beginning of a since-interval whose end is the recorded anchor.
Month-only expressions without last, next, or an explicit year remain ambiguous. Numeric dates with two-digit years, impossible dates, zero-length recurrence periods, inexact bounded schedules, and vague quantities are likewise flagged rather than coerced into precise timestamps.
Privacy and determinism¶
Normalization is pure, offline, and rules-based. The module performs no network access, reads no environment state, emits no logs, and never defaults to the wall clock. Temporal spans can contain sensitive dates, so callers should apply the same PHI handling policy to returned values as to the source document.
Anchor EVENT spans to document time¶
Use anchor_events to attach already-extracted EVENT spans to the nearest resolved DATE or TIME in the same sentence. Relative TIMEX values are resolved against the required document creation time (DCT). Events without a usable nearby TIMEX receive an explicit dct_fallback anchor.
from openmed.clinical import anchor_events
text = "Fever started 3 days ago."
result = anchor_events(
text,
event_spans=[(6, 13)],
document_creation_time="2026-06-15",
timex_spans=[(14, 24)],
)
result.anchors[0].anchor_value
# "2026-06-12"
result.anchors[0].dct_position
# "before"
When timex_spans is omitted, local deterministic detection supplies TIMEX offsets. Passing an empty iterable disables detection. Supplied NormalizedTimex records retain their resolved values rather than being normalized again. The anchoring result does not retain raw EVENT or TIMEX text: source evidence is represented by inclusive/exclusive offsets and SHA-256 hashes only.